mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
fix(console): should not display unsaved alert on item deleted (#1507)
This commit is contained in:
parent
f387652bfd
commit
459af3823c
18 changed files with 231 additions and 306 deletions
|
@ -4,6 +4,6 @@
|
|||
font: var(--font-body-medium);
|
||||
|
||||
> :not(:first-child) {
|
||||
margin: _.unit(6) 0;
|
||||
margin: _.unit(6) 0 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ export type ConfirmModalProps = {
|
|||
confirmButtonText?: AdminConsoleKey;
|
||||
cancelButtonText?: AdminConsoleKey;
|
||||
isOpen: boolean;
|
||||
isConfirmButtonDisabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
@ -29,6 +31,8 @@ const ConfirmModal = ({
|
|||
confirmButtonText = 'general.confirm',
|
||||
cancelButtonText = 'general.cancel',
|
||||
isOpen,
|
||||
isConfirmButtonDisabled = false,
|
||||
isLoading = false,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: ConfirmModalProps) => {
|
||||
|
@ -43,7 +47,13 @@ const ConfirmModal = ({
|
|||
footer={
|
||||
<>
|
||||
<Button type="outline" title={cancelButtonText} onClick={onCancel} />
|
||||
<Button type={confirmButtonType} title={confirmButtonText} onClick={onConfirm} />
|
||||
<Button
|
||||
type={confirmButtonType}
|
||||
title={confirmButtonText}
|
||||
disabled={isConfirmButtonDisabled}
|
||||
isLoading={isLoading}
|
||||
onClick={onConfirm}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
className={classNames(styles.content, className)}
|
||||
|
|
55
packages/console/src/components/DeleteConfirmModal/index.tsx
Normal file
55
packages/console/src/components/DeleteConfirmModal/index.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import React, { ReactNode, useState } from 'react';
|
||||
|
||||
import ConfirmModal from '@/components/ConfirmModal';
|
||||
import TextInput from '@/components/TextInput';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
isLoading?: boolean;
|
||||
children: ReactNode;
|
||||
expectedInput?: string;
|
||||
inputPlaceholder?: string;
|
||||
className?: string;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
const DeleteConfirmModal = ({
|
||||
isOpen,
|
||||
isLoading = false,
|
||||
expectedInput,
|
||||
inputPlaceholder,
|
||||
children,
|
||||
className,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: Props) => {
|
||||
const [input, setInput] = useState('');
|
||||
const isConfirmBlocked = Boolean(expectedInput) && input !== expectedInput;
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
isOpen={isOpen}
|
||||
isLoading={isLoading}
|
||||
isConfirmButtonDisabled={isConfirmBlocked}
|
||||
confirmButtonText="general.delete"
|
||||
className={className}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirm}
|
||||
>
|
||||
{children}
|
||||
{expectedInput && (
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={input}
|
||||
placeholder={inputPlaceholder}
|
||||
onChange={(event) => {
|
||||
setInput(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteConfirmModal;
|
|
@ -1,5 +1,5 @@
|
|||
import type { Blocker, Transition } from 'history';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useContext, useLayoutEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { UNSAFE_NavigationContext, Navigator } from 'react-router-dom';
|
||||
|
||||
|
@ -22,7 +22,7 @@ const UnsavedChangesAlertModal = ({ hasUnsavedChanges }: Props) => {
|
|||
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!hasUnsavedChanges) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.content {
|
||||
> :not(:first-child) {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-medium);
|
||||
}
|
||||
|
||||
.hightlight {
|
||||
color: var(--color-primary-50);
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
name: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const DeleteForm = ({ id, name, onClose }: Props) => {
|
||||
const api = useApi();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [inputName, setInputName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const inputMismatched = inputName !== name;
|
||||
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await api.delete(`/api/resources/${id}`);
|
||||
onClose();
|
||||
navigate(`/api-resources`);
|
||||
toast.success(t('api_resource_details.api_resource_deleted', { name }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalLayout
|
||||
title="general.reminder"
|
||||
footer={
|
||||
<>
|
||||
<Button type="outline" title="general.cancel" onClick={onClose} />
|
||||
<Button
|
||||
disabled={inputMismatched}
|
||||
isLoading={loading}
|
||||
type="danger"
|
||||
title="general.delete"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
className={styles.content}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.description}>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="api_resource_details.delete_description"
|
||||
values={{ name }}
|
||||
components={{ span: <span className={styles.hightlight} /> }}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={inputName}
|
||||
placeholder={t('api_resource_details.enter_your_api_resource_name')}
|
||||
onChange={(event) => {
|
||||
setInputName(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</ModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteForm;
|
|
@ -4,6 +4,20 @@
|
|||
margin: _.unit(1) 0 0 _.unit(1);
|
||||
}
|
||||
|
||||
.deleteConfirm {
|
||||
> :not(:first-child) {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-medium);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--color-primary-50);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
|
@ -4,9 +4,8 @@ import classNames from 'classnames';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import ApiResourceDark from '@/assets/images/api-resource-dark.svg';
|
||||
|
@ -15,6 +14,7 @@ import ActionMenu, { ActionMenuItem } from '@/components/ActionMenu';
|
|||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||
import DeleteConfirmModal from '@/components/DeleteConfirmModal';
|
||||
import DetailsSkeleton from '@/components/DetailsSkeleton';
|
||||
import FormField from '@/components/FormField';
|
||||
import LinkButton from '@/components/LinkButton';
|
||||
|
@ -27,9 +27,7 @@ import Back from '@/icons/Back';
|
|||
import Delete from '@/icons/Delete';
|
||||
import More from '@/icons/More';
|
||||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import DeleteForm from './components/DeleteForm';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type FormData = {
|
||||
|
@ -41,7 +39,7 @@ const ApiResourceDetails = () => {
|
|||
const location = useLocation();
|
||||
const { id } = useParams();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { data, error, mutate } = useSWR<Resource, RequestError>(id && `/api/resources/${id}`);
|
||||
const isLoading = !data && !error;
|
||||
const theme = useTheme();
|
||||
|
@ -49,6 +47,9 @@ const ApiResourceDetails = () => {
|
|||
|
||||
const isLogtoManagementApiResource = data?.id === managementResource.id;
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
register,
|
||||
|
@ -81,6 +82,25 @@ const ApiResourceDetails = () => {
|
|||
toast.success(t('general.saved'));
|
||||
});
|
||||
|
||||
const onDelete = async () => {
|
||||
if (!data || isDeleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
await api.delete(`/api/resources/${data.id}`);
|
||||
setIsDeleted(true);
|
||||
setIsDeleting(false);
|
||||
setIsDeleteFormOpen(false);
|
||||
toast.success(t('api_resource_details.api_resource_deleted', { name: data.name }));
|
||||
navigate(`/api-resources`);
|
||||
} catch {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={detailsStyles.container}>
|
||||
<LinkButton
|
||||
|
@ -117,19 +137,23 @@ const ApiResourceDetails = () => {
|
|||
{t('general.delete')}
|
||||
</ActionMenuItem>
|
||||
</ActionMenu>
|
||||
<Modal
|
||||
<DeleteConfirmModal
|
||||
isOpen={isDeleteFormOpen}
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
isLoading={isDeleting}
|
||||
expectedInput={data.name}
|
||||
className={styles.deleteConfirm}
|
||||
inputPlaceholder={t('api_resource_details.enter_your_api_resource_name')}
|
||||
onCancel={() => {
|
||||
setIsDeleteFormOpen(false);
|
||||
}}
|
||||
onConfirm={onDelete}
|
||||
>
|
||||
<DeleteForm
|
||||
id={data.id}
|
||||
name={data.name}
|
||||
onClose={() => {
|
||||
setIsDeleteFormOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<div className={styles.description}>
|
||||
<Trans components={{ span: <span className={styles.highlight} /> }}>
|
||||
{t('api_resource_details.delete_description', { name: data.name })}
|
||||
</Trans>
|
||||
</div>
|
||||
</DeleteConfirmModal>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
@ -176,7 +200,7 @@ const ApiResourceDetails = () => {
|
|||
</Card>
|
||||
</>
|
||||
)}
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,9 +11,10 @@ import * as styles from '../index.module.scss';
|
|||
type Props = {
|
||||
oidcConfig: SnakeCaseOidcConfig;
|
||||
defaultData: Application;
|
||||
isDeleted: boolean;
|
||||
};
|
||||
|
||||
const AdvancedSettings = ({ oidcConfig, defaultData }: Props) => {
|
||||
const AdvancedSettings = ({ oidcConfig, defaultData, isDeleted }: Props) => {
|
||||
const {
|
||||
reset,
|
||||
formState: { isDirty },
|
||||
|
@ -36,7 +37,7 @@ const AdvancedSettings = ({ oidcConfig, defaultData }: Props) => {
|
|||
variant="border"
|
||||
/>
|
||||
</FormField>
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.content {
|
||||
> :not(:first-child) {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-medium);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--color-primary-50);
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import TextInput from '@/components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
name: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const DeleteForm = ({ id, name, onClose }: Props) => {
|
||||
const api = useApi();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [inputName, setInputName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const inputMismatched = inputName !== name;
|
||||
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await api.delete(`/api/applications/${id}`);
|
||||
onClose();
|
||||
navigate(`/applications`);
|
||||
toast.success(t('application_details.application_deleted', { name }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalLayout
|
||||
title="general.reminder"
|
||||
footer={
|
||||
<>
|
||||
<Button type="outline" title="general.cancel" onClick={onClose} />
|
||||
<Button
|
||||
disabled={inputMismatched}
|
||||
isLoading={loading}
|
||||
type="danger"
|
||||
title="general.delete"
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
className={styles.content}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles.description}>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="application_details.delete_description"
|
||||
values={{ name }}
|
||||
components={{ span: <span className={styles.highlight} /> }}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={inputName}
|
||||
placeholder={t('application_details.enter_your_application_name')}
|
||||
onChange={(event) => {
|
||||
setInputName(event.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</ModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteForm;
|
|
@ -18,9 +18,10 @@ type Props = {
|
|||
applicationType: ApplicationType;
|
||||
oidcConfig: SnakeCaseOidcConfig;
|
||||
defaultData: Application;
|
||||
isDeleted: boolean;
|
||||
};
|
||||
|
||||
const Settings = ({ applicationType, oidcConfig, defaultData }: Props) => {
|
||||
const Settings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
control,
|
||||
|
@ -156,7 +157,7 @@ const Settings = ({ applicationType, oidcConfig, defaultData }: Props) => {
|
|||
)}
|
||||
/>
|
||||
</FormField>
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,6 +4,20 @@
|
|||
margin: _.unit(1) 0 0 _.unit(1);
|
||||
}
|
||||
|
||||
.deleteConfirm {
|
||||
> :not(:first-child) {
|
||||
margin-top: _.unit(6);
|
||||
}
|
||||
|
||||
.description {
|
||||
font: var(--font-body-medium);
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--color-primary-50);
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
> :first-child {
|
||||
margin-top: 0;
|
||||
|
|
|
@ -3,9 +3,8 @@ import classNames from 'classnames';
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import ActionMenu, { ActionMenuItem } from '@/components/ActionMenu';
|
||||
|
@ -13,6 +12,7 @@ import ApplicationIcon from '@/components/ApplicationIcon';
|
|||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||
import DeleteConfirmModal from '@/components/DeleteConfirmModal';
|
||||
import DetailsSkeleton from '@/components/DetailsSkeleton';
|
||||
import Drawer from '@/components/Drawer';
|
||||
import LinkButton from '@/components/LinkButton';
|
||||
|
@ -22,12 +22,10 @@ import Back from '@/icons/Back';
|
|||
import Delete from '@/icons/Delete';
|
||||
import More from '@/icons/More';
|
||||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { applicationTypeI18nKey } from '@/types/applications';
|
||||
|
||||
import Guide from '../Applications/components/Guide';
|
||||
import AdvancedSettings from './components/AdvancedSettings';
|
||||
import DeleteForm from './components/DeleteForm';
|
||||
import Settings from './components/Settings';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -51,7 +49,10 @@ const ApplicationDetails = () => {
|
|||
const isLoading = (!data && !error) || (!oidcConfig && !fetchOidcConfigError);
|
||||
const [isReadmeOpen, setIsReadmeOpen] = useState(false);
|
||||
const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const api = useApi();
|
||||
const navigate = useNavigate();
|
||||
const formMethods = useForm<Application>();
|
||||
|
||||
const {
|
||||
|
@ -97,6 +98,23 @@ const ApplicationDetails = () => {
|
|||
toast.success(t('general.saved'));
|
||||
});
|
||||
|
||||
const onDelete = async () => {
|
||||
if (!data || isDeleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.delete(`/api/applications/${data.id}`);
|
||||
setIsDeleted(true);
|
||||
setIsDeleting(false);
|
||||
setIsDeleteFormOpen(false);
|
||||
toast.success(t('application_details.application_deleted', { name: data.name }));
|
||||
navigate(`/applications`);
|
||||
} catch {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onCloseDrawer = () => {
|
||||
setIsReadmeOpen(false);
|
||||
};
|
||||
|
@ -150,19 +168,23 @@ const ApplicationDetails = () => {
|
|||
{t('general.delete')}
|
||||
</ActionMenuItem>
|
||||
</ActionMenu>
|
||||
<Modal
|
||||
<DeleteConfirmModal
|
||||
isOpen={isDeleteFormOpen}
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
isLoading={isDeleting}
|
||||
expectedInput={data.name}
|
||||
inputPlaceholder={t('application_details.enter_your_application_name')}
|
||||
className={styles.deleteConfirm}
|
||||
onCancel={() => {
|
||||
setIsDeleteFormOpen(false);
|
||||
}}
|
||||
onConfirm={onDelete}
|
||||
>
|
||||
<DeleteForm
|
||||
id={data.id}
|
||||
name={data.name}
|
||||
onClose={() => {
|
||||
setIsDeleteFormOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
<div className={styles.description}>
|
||||
<Trans components={{ span: <span className={styles.highlight} /> }}>
|
||||
{t('application_details.delete_description', { name: data.name })}
|
||||
</Trans>
|
||||
</div>
|
||||
</DeleteConfirmModal>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={classNames(styles.body, detailsStyles.body)}>
|
||||
|
@ -178,13 +200,18 @@ const ApplicationDetails = () => {
|
|||
<form className={classNames(styles.form, detailsStyles.body)} onSubmit={onSubmit}>
|
||||
<div className={styles.fields}>
|
||||
{isAdvancedSettings && (
|
||||
<AdvancedSettings oidcConfig={oidcConfig} defaultData={data} />
|
||||
<AdvancedSettings
|
||||
oidcConfig={oidcConfig}
|
||||
defaultData={data}
|
||||
isDeleted={isDeleted}
|
||||
/>
|
||||
)}
|
||||
{!isAdvancedSettings && (
|
||||
<Settings
|
||||
applicationType={data.type}
|
||||
oidcConfig={oidcConfig}
|
||||
defaultData={data}
|
||||
isDeleted={isDeleted}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import ModalLayout from '@/components/ModalLayout';
|
||||
import useApi from '@/hooks/use-api';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const DeleteForm = ({ id, onClose }: Props) => {
|
||||
const api = useApi();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await api.delete(`/api/users/${id}`);
|
||||
onClose();
|
||||
navigate('/users');
|
||||
toast.success(t('user_details.deleted', { name }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalLayout
|
||||
title="general.reminder"
|
||||
footer={
|
||||
<>
|
||||
<Button type="outline" title="general.cancel" onClick={onClose} />
|
||||
<Button disabled={loading} type="danger" title="general.delete" onClick={handleDelete} />
|
||||
</>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div>{t('user_details.delete_description')}</div>
|
||||
</ModalLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteForm;
|
|
@ -5,8 +5,8 @@ import React, { useMemo, useState } from 'react';
|
|||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import ConfirmModal from '@/components/ConfirmModal';
|
||||
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||
import DeleteConfirmModal from '@/components/DeleteConfirmModal';
|
||||
import TableError from '@/components/Table/TableError';
|
||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||
import useApi from '@/hooks/use-api';
|
||||
|
@ -142,9 +142,8 @@ const UserConnectors = ({ userId, connectors, onDelete }: Props) => {
|
|||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
<ConfirmModal
|
||||
<DeleteConfirmModal
|
||||
isOpen={deletingConnector !== undefined}
|
||||
confirmButtonText="general.delete"
|
||||
onCancel={() => {
|
||||
setDeletingConnector(undefined);
|
||||
}}
|
||||
|
@ -162,7 +161,7 @@ const UserConnectors = ({ userId, connectors, onDelete }: Props) => {
|
|||
components={{ name: <UnnamedTrans resource={deletingConnector.name} /> }}
|
||||
/>
|
||||
)}
|
||||
</ConfirmModal>
|
||||
</DeleteConfirmModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -32,9 +32,10 @@ type Props = {
|
|||
userData: User;
|
||||
userFormData: FormData;
|
||||
onUserUpdated: (user?: User) => void;
|
||||
isDeleted: boolean;
|
||||
};
|
||||
|
||||
const UserSettings = ({ userData, userFormData, onUserUpdated }: Props) => {
|
||||
const UserSettings = ({ userData, userFormData, isDeleted, onUserUpdated }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const {
|
||||
|
@ -145,7 +146,7 @@ const UserSettings = ({ userData, userFormData, onUserUpdated }: Props) => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDirty} />
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import { User } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import ActionMenu, { ActionMenuItem } from '@/components/ActionMenu';
|
||||
import Card from '@/components/Card';
|
||||
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||
import DeleteConfirmModal from '@/components/DeleteConfirmModal';
|
||||
import DetailsSkeleton from '@/components/DetailsSkeleton';
|
||||
import LinkButton from '@/components/LinkButton';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import { generateAvatarPlaceHolderById } from '@/consts/avatars';
|
||||
import { RequestError } from '@/hooks/use-api';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import Back from '@/icons/Back';
|
||||
import Delete from '@/icons/Delete';
|
||||
import More from '@/icons/More';
|
||||
|
@ -22,7 +24,6 @@ import * as detailsStyles from '@/scss/details.module.scss';
|
|||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import CreateSuccess from './components/CreateSuccess';
|
||||
import DeleteForm from './components/DeleteForm';
|
||||
import ResetPasswordForm from './components/ResetPasswordForm';
|
||||
import UserLogs from './components/UserLogs';
|
||||
import UserSettings from './components/UserSettings';
|
||||
|
@ -37,11 +38,15 @@ const UserDetails = () => {
|
|||
const password = passwordEncoded && atob(passwordEncoded);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const [isDeleteFormOpen, setIsDeleteFormOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isDeleted, setIsDeleted] = useState(false);
|
||||
const [isResetPasswordFormOpen, setIsResetPasswordFormOpen] = useState(false);
|
||||
const [resetResult, setResetResult] = useState<string>();
|
||||
|
||||
const { data, error, mutate } = useSWR<User, RequestError>(userId && `/api/users/${userId}`);
|
||||
const isLoading = !data && !error;
|
||||
const api = useApi();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const userFormData = useMemo(() => {
|
||||
if (!data) {
|
||||
|
@ -54,6 +59,25 @@ const UserDetails = () => {
|
|||
};
|
||||
}, [data]);
|
||||
|
||||
const onDelete = async () => {
|
||||
if (!data || isDeleting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
|
||||
try {
|
||||
await api.delete(`/api/users/${data.id}`);
|
||||
setIsDeleted(true);
|
||||
setIsDeleting(false);
|
||||
setIsDeleteFormOpen(false);
|
||||
toast.success(t('user_details.deleted', { name: data.name }));
|
||||
navigate('/users');
|
||||
} catch {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={detailsStyles.container}>
|
||||
<LinkButton
|
||||
|
@ -124,18 +148,16 @@ const UserDetails = () => {
|
|||
}}
|
||||
/>
|
||||
</ReactModal>
|
||||
<ReactModal
|
||||
<DeleteConfirmModal
|
||||
isOpen={isDeleteFormOpen}
|
||||
className={modalStyles.content}
|
||||
overlayClassName={modalStyles.overlay}
|
||||
isLoading={isDeleting}
|
||||
onCancel={() => {
|
||||
setIsDeleteFormOpen(false);
|
||||
}}
|
||||
onConfirm={onDelete}
|
||||
>
|
||||
<DeleteForm
|
||||
id={data.id}
|
||||
onClose={() => {
|
||||
setIsDeleteFormOpen(false);
|
||||
}}
|
||||
/>
|
||||
</ReactModal>
|
||||
<div>{t('user_details.delete_description')}</div>
|
||||
</DeleteConfirmModal>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={classNames(styles.body, detailsStyles.body)}>
|
||||
|
@ -148,6 +170,7 @@ const UserDetails = () => {
|
|||
<UserSettings
|
||||
userData={data}
|
||||
userFormData={userFormData}
|
||||
isDeleted={isDeleted}
|
||||
onUserUpdated={(user) => {
|
||||
void mutate(user);
|
||||
}}
|
||||
|
|
Loading…
Reference in a new issue