From 9559fa8346dd29dab3889509e8ec455bd97bb25d Mon Sep 17 00:00:00 2001 From: Quentin AUBERT Date: Fri, 20 Dec 2024 17:20:05 +0100 Subject: [PATCH] FIX #790 - display a better confirm modal on deletion of team, api, plan & user --- .../adminbackoffice/teams/TeamList.tsx | 49 +++++++-- .../adminbackoffice/users/UserList.tsx | 47 ++++++-- .../backoffice/apikeys/TeamApiKeysForApi.tsx | 14 +++ .../backoffice/apis/TeamApiPricings.tsx | 31 +++++- .../backoffice/apis/TeamApiSettings.tsx | 104 +++++++++++------- .../components/backoffice/apis/TeamApis.tsx | 2 +- .../components/backoffice/teams/TeamEdit.tsx | 50 ++++++--- .../src/contexts/modals/FormModal.tsx | 4 +- .../javascript/src/contexts/modals/types.ts | 2 + .../src/locales/en/translation.json | 19 +++- .../src/locales/fr/translation.json | 19 +++- 11 files changed, 252 insertions(+), 89 deletions(-) diff --git a/daikoku/javascript/src/components/adminbackoffice/teams/TeamList.tsx b/daikoku/javascript/src/components/adminbackoffice/teams/TeamList.tsx index 2f917a665..53665bbc1 100644 --- a/daikoku/javascript/src/components/adminbackoffice/teams/TeamList.tsx +++ b/daikoku/javascript/src/components/adminbackoffice/teams/TeamList.tsx @@ -165,16 +165,41 @@ export const TeamList = () => { }; - const deleteTeam = (teamId: string) => { - confirm({ message: translate('delete team') }) - .then((ok) => { - if (ok) { - Services.deleteTeam(teamId) - .then(() => { - queryClient.invalidateQueries({ queryKey: ['teams'] }); - }); - } - }); + const deleteTeam = (team: ITeamFullGql) => { + openFormModal({ + title: translate('Confirm'), + description:
+

{translate('Warning')}

+

{translate("delete.team.confirm.modal.description.1")}

+ +
, + schema: { + confirm: { + type: type.string, + label: translate({ key: 'delete.item.confirm.modal.confirm.label', replacements: [team.name] }), + constraints: [ + constraints.oneOf( + [team.name], + translate({ key: 'constraints.type.api.name', replacements: [team.name] }) + ), + ], + }, + }, + onSubmit: () => Services.deleteTeam(team._id) + .then((r) => { + if (isError(r)) { + toast.error(r.error) + } else { + queryClient.invalidateQueries({ queryKey: ['teams'] }); + toast.success(translate({ key: 'team.deleted.success', replacements: [team.name] })) + } + }), + actionLabel: translate('Confirm') + }) }; const handleChange = (e) => { @@ -196,7 +221,7 @@ export const TeamList = () => { const actions = (team: ITeamFullGql) => { const basicActions = [ { - action: () => deleteTeam(team._id), + action: () => deleteTeam(team), variant: 'error', iconClass: 'fas fa-trash delete-icon', tooltip: translate('Delete team'), @@ -251,7 +276,7 @@ export const TeamList = () => { { action: () => navigate(`/settings/teams/${team._humanReadableId}/members`), iconClass: 'fas fa-users', - tooltip: translate({key: "Member", plural: true}), + tooltip: translate({ key: "Member", plural: true }), }, ]; }; diff --git a/daikoku/javascript/src/components/adminbackoffice/users/UserList.tsx b/daikoku/javascript/src/components/adminbackoffice/users/UserList.tsx index 1a37028e3..57fa36ec1 100644 --- a/daikoku/javascript/src/components/adminbackoffice/users/UserList.tsx +++ b/daikoku/javascript/src/components/adminbackoffice/users/UserList.tsx @@ -9,12 +9,13 @@ import { GlobalContext } from '../../../contexts/globalContext'; import * as Services from '../../../services'; import { IUserSimple, isError } from '../../../types'; import { AvatarWithAction, Can, PaginatedComponent, daikoku, manage } from '../../utils'; +import { constraints, type } from '@maif/react-forms'; export const UserList = () => { const { connectedUser } = useContext(GlobalContext); useDaikokuBackOffice(); - const { alert, confirm } = useContext(ModalContext); + const { alert, confirm, openFormModal } = useContext(ModalContext); const [users, setUsers] = useState>([]); const [search, setSearch] = useState(); @@ -36,16 +37,40 @@ export const UserList = () => { }; const removeUser = (user: IUserSimple) => { - confirm({ message: translate('remove.user.confirm'), okLabel: translate('Yes') }) - .then((ok) => { - if (ok) { - Services.deleteUserById(user._id) - .then(() => { - toast.info(translate({ key: 'remove.user.success', replacements: [user.name] })); - updateUsers(); - }); - } - }); + openFormModal({ + title: translate('Confirm'), + description:
+

{translate('Warning')}

+

{translate("delete.user.confirm.modal.description.1")}

+
    +
  • {translate("delete.user.confirm.modal.description.2")}
  • +
  • {translate("delete.user.confirm.modal.description.3")}
  • +
  • {translate("delete.user.confirm.modal.description.4")}
  • +
+
, + schema: { + confirm: { + type: type.string, + label: translate({ key: 'delete.item.confirm.modal.confirm.label', replacements: [user.name] }), + constraints: [ + constraints.oneOf( + [user.name], + translate({ key: 'constraints.type.api.name', replacements: [user.name] }) + ), + ], + }, + }, + onSubmit: () => Services.deleteUserById(user._id) + .then((r) => { + if (isError(r)) { + toast.error(r.error) + } else { + toast.success(translate({ key: 'remove.user.success', replacements: [user.name] })); + updateUsers(); + } + }), + actionLabel: translate('Confirm') + }) }; const toggleAdmin = (member: IUserSimple) => { diff --git a/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx b/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx index 4f2dbeb3a..892b39340 100644 --- a/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx +++ b/daikoku/javascript/src/components/backoffice/apikeys/TeamApiKeysForApi.tsx @@ -223,6 +223,13 @@ export const TeamApiKeysForApi = () => { onSubmit: ({ choice, childId }) => openFormModal( { title: translate("apikeys.delete.confirm.modal.title"), + description:
+

{translate('Warning')}

+

{translate("delete.subscription.confirm.modal.description.1")}

+
    +
  • {translate("delete.subscription.confirm.modal.description.2")}
  • +
+
, schema: { validation: { type: type.string, @@ -246,6 +253,13 @@ export const TeamApiKeysForApi = () => { openFormModal( { title: translate("apikeys.delete.confirm.modal.title"), + description:
+

{translate('Warning')}

+

{translate("delete.subscription.confirm.modal.description.1")}

+
    +
  • {translate("delete.subscription.confirm.modal.description.2")}
  • +
+
, schema: { validation: { type: type.string, diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiPricings.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiPricings.tsx index 993fa821d..1d01a3903 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApiPricings.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiPricings.tsx @@ -471,17 +471,36 @@ const Card = ({ creation, }: CardProps) => { const { translate, Translation } = useContext(I18nContext); - const { confirm } = useContext(ModalContext); + const { openFormModal } = useContext(ModalContext); const { tenant } = useContext(GlobalContext); const pricing = renderPricing(plan, translate); const deleteWithConfirm = () => { - confirm({ message: translate('delete.plan.confirm') }).then((ok) => { - if (ok) { - deletePlan(); - } - }); + openFormModal({ + title: translate('Confirm'), + description:
+

{translate('Warning')}

+

{translate(`delete.${tenant.display === 'environment' ? 'envionment' : 'plan'}.confirm.modal.description.1`)}

+
    +
  • {translate(`delete.${tenant.display === 'environment' ? 'envionment' : 'plan'}.confirm.modal.description.2`)}
  • +
+
, + schema: { + confirm: { + type: type.string, + label: translate({ key: 'delete.item.confirm.modal.confirm.label', replacements: [plan.customName || plan.type]}), + constraints: [ + constraints.oneOf( + [plan.customName || plan.type], + translate({ key: 'constraints.type.api.name', replacements: [plan.customName || plan.type] }) + ), + ], + }, + }, + onSubmit: () => deletePlan(), + actionLabel: translate('Confirm') + }) }; const noOtoroshi = diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApiSettings.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApiSettings.tsx index 26940afa8..cc6aaa315 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApiSettings.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApiSettings.tsx @@ -17,10 +17,10 @@ type TeamApiSettingsProps = { export const TeamApiSettings = ({ api, currentTeam }: TeamApiSettingsProps) => { const { translate } = useContext(I18nContext); - const { confirm } = useContext(ModalContext); + const { confirm, openFormModal } = useContext(ModalContext); const navigate = useNavigate(); - const transferOwnership = ({team}: {team: ITeamSimple}) => { + const transferOwnership = ({ team }: { team: ITeamSimple }) => { Services.transferApiOwnership(team, api.team, api._id).then((r) => { if (r.notify) { @@ -66,47 +66,71 @@ export const TeamApiSettings = ({ api, currentTeam }: TeamApiSettingsProps) => { }; const deleteApi = () => { - return confirm({ message: translate('delete.api.confirm') }) - .then((ok) => { - if (ok) { - Services.deleteTeamApi(currentTeam._id, api._id) - .then(() => navigate(`/${currentTeam._humanReadableId}/settings/apis`)) - .then(() => toast.success(translate('deletion successful'))); - } - }); + openFormModal({ + title: translate('Confirm'), + description:
+

{translate('Warning')}

+

{translate("delete.api.confirm.modal.description.1")}

+
    +
  • {translate("delete.api.confirm.modal.description.2")}
  • +
+
, + schema: { + confirm: { + type: type.string, + label: translate({ key: 'delete.item.confirm.modal.confirm.label', replacements: [api.name] }), + constraints: [ + constraints.oneOf( + [api.name], + translate({ key: 'constraints.type.api.name', replacements: [api.name] }) + ), + ], + }, + }, + onSubmit: () => Services.deleteTeamApi(currentTeam._id, api._id) + .then((r) => { + if (isError(r)) { + toast.error(r.error) + } else { + navigate(`/${currentTeam._humanReadableId}/settings/apis`) + toast.success(translate('deletion successful')) + } + }), + actionLabel: translate('Confirm') + }) }; - return ( -
-
-

{translate('transfer.api.ownership.title')}

- {translate('transfer.api.ownership.description')} -
+ return ( +
+
+

{translate('transfer.api.ownership.title')}

+ {translate('transfer.api.ownership.description')} + +
+
+
+

{translate('delete.api.title')}

+ {translate('delete.api.description')}
-
-
-

{translate('delete.api.title')}

- {translate('delete.api.description')} -
-
- deleteApi()} - feedbackTimeout={1000} - disabled={false} - >{translate('Delete this Api')} -
+
+
- ); +
+ ); }; diff --git a/daikoku/javascript/src/components/backoffice/apis/TeamApis.tsx b/daikoku/javascript/src/components/backoffice/apis/TeamApis.tsx index a6878c881..0f671242f 100644 --- a/daikoku/javascript/src/components/backoffice/apis/TeamApis.tsx +++ b/daikoku/javascript/src/components/backoffice/apis/TeamApis.tsx @@ -89,7 +89,7 @@ export const TeamApis = () => { key={`delete-${api._humanReadableId}`} type="button" className="btn btn-sm btn-outline-danger" - title="Delete this Api" + title={translate("Delete this Api")} onClick={() => deleteApi(api)} > diff --git a/daikoku/javascript/src/components/backoffice/teams/TeamEdit.tsx b/daikoku/javascript/src/components/backoffice/teams/TeamEdit.tsx index 3ebd17855..086d0e7ae 100644 --- a/daikoku/javascript/src/components/backoffice/teams/TeamEdit.tsx +++ b/daikoku/javascript/src/components/backoffice/teams/TeamEdit.tsx @@ -133,7 +133,7 @@ export const TeamEditForm = ({ const navigate = useNavigate(); const { translate } = useContext(I18nContext); - const { confirm } = useContext(ModalContext); + const { confirm, openFormModal } = useContext(ModalContext); if (!team) { return null; @@ -144,21 +144,39 @@ export const TeamEditForm = ({ }, []); const confirmDelete = () => { - confirm({ - message: translate('delete team'), - title: 'Delete team', - }).then((ok) => { - if (ok) { - Services.deleteTeam(team._id) - .then((r) => { - if (isError(r)) { - toast.error(r.error) - } else { - navigate("/apis") - toast.success(translate({ key: 'team.deleted.success', replacements: [team.name] })) - } - }) - } + openFormModal({ + title: translate('Confirm'), + description:
+

{translate('Warning')}

+

{translate("delete.team.confirm.modal.description.1")}

+
    +
  • {translate("delete.team.confirm.modal.description.2")}
  • +
  • {translate("delete.team.confirm.modal.description.3")}
  • +
  • {translate("delete.team.confirm.modal.description.4")}
  • +
+
, + schema: { + confirm: { + type: type.string, + label: translate({ key: 'delete.item.confirm.modal.confirm.label', replacements: [team.name] }), + constraints: [ + constraints.oneOf( + [team.name], + translate({ key: 'constraints.type.api.name', replacements: [team.name] }) + ), + ], + }, + }, + onSubmit: () => Services.deleteTeam(team._id) + .then((r) => { + if (isError(r)) { + toast.error(r.error) + } else { + navigate("/apis") + toast.success(translate({ key: 'team.deleted.success', replacements: [team.name] })) + } + }), + actionLabel: translate('Confirm') }) } diff --git a/daikoku/javascript/src/contexts/modals/FormModal.tsx b/daikoku/javascript/src/contexts/modals/FormModal.tsx index b6a878c11..13c698f99 100644 --- a/daikoku/javascript/src/contexts/modals/FormModal.tsx +++ b/daikoku/javascript/src/contexts/modals/FormModal.tsx @@ -13,7 +13,8 @@ export const FormModal = ({ options, actionLabel, close, - noClose + noClose, + description }: IFormModalProps & IBaseModalProps) => { const ref = useRef(); @@ -26,6 +27,7 @@ export const FormModal = ({
+ {description && description} void; @@ -102,6 +103,7 @@ export interface IFormModalProps { options?: Option; actionLabel: string; noClose?: boolean; + description?: ReactNode } export type TestingApiKeyModalProps = { diff --git a/daikoku/javascript/src/locales/en/translation.json b/daikoku/javascript/src/locales/en/translation.json index 7401b0b0d..6f8cf0f77 100644 --- a/daikoku/javascript/src/locales/en/translation.json +++ b/daikoku/javascript/src/locales/en/translation.json @@ -1401,5 +1401,22 @@ "subscription.copy.token.help": "Copy daikoku token, used for retrieve credentials by API", "setting.panel.reset.confirm.message": "Are you sure you want to reset your organization's data?", "api.pricing.created.subscription.panel.title": "Your API Key is Ready", - "right.panel.close.aria.label": "Close the panel" + "right.panel.close.aria.label": "Close the panel", + "delete.team.confirm.modal.description.1": "Are you sure you want to delete your team? This action is irreversible and will result in the deletion of:", + "delete.team.confirm.modal.description.2": "All subscriptions and API keys associated with your team.", + "delete.team.confirm.modal.description.3": "All APIs managed by your team.", + "delete.team.confirm.modal.description.4": "All subscriptions to the APIs managed by your team.", + "delete.api.confirm.modal.description.1": "Are you sure you want to delete this API? This action is irreversible and will result in the deletion of:", + "delete.api.confirm.modal.description.2": "All API keys associated with this API.", + "delete.user.confirm.modal.description.1": "Are you sure you want to delete this user? This action is irreversible and will result in the deletion of:", + "delete.user.confirm.modal.description.2": "All subscriptions and API keys associated with this user.", + "delete.user.confirm.modal.description.3": "All APIs managed by this user.", + "delete.user.confirm.modal.description.4": "All subscriptions to the APIs managed by this user.", + "delete.subscription.confirm.modal.description.1": "Are you sure you want to delete this subscription? This action is irreversible and will result in the deletion of:", + "delete.subscription.confirm.modal.description.2": "The API key associated with this subscription.", + "delete.plan.confirm.modal.description.1": "Are you sure you want to delete this Plan? This action is irreversible and will result in the deletion of:", + "delete.plan.confirm.modal.description.2": "All API keys associated with this plan.", + "delete.environment.confirm.modal.description.1": "Are you sure you want to delete this environment? This action is irreversible and will result in the deletion of:", + "delete.environment.confirm.modal.description.2": "All API keys associated with this environment.", + "delete.item.confirm.modal.confirm.label": "Type %s to confirm deletion" } \ No newline at end of file diff --git a/daikoku/javascript/src/locales/fr/translation.json b/daikoku/javascript/src/locales/fr/translation.json index 899be6044..d8af39174 100644 --- a/daikoku/javascript/src/locales/fr/translation.json +++ b/daikoku/javascript/src/locales/fr/translation.json @@ -1402,5 +1402,22 @@ "subscription.copy.token.help": "Copier le jeton Daikoku, utilisable pour récupérer les identifiants par API", "setting.panel.reset.confirm.message": "Êtes-vous sûr de vouloir réinitialiser les données de votre organisation ?", "api.pricing.created.subscription.panel.title": "Votre clé d'API est prête", - "right.panel.close.aria.label": "Fermer le panneau latéral" + "right.panel.close.aria.label": "Fermer le panneau latéral", + "delete.team.confirm.modal.description.1": "Êtes-vous sûr de vouloir supprimer votre équipe ? Cette action est irréversible et entraînera la suppression de :", + "delete.team.confirm.modal.description.2": "Toutes les souscriptions et clés API associées à votre équipe.", + "delete.team.confirm.modal.description.3": "Toutes les API gérées par votre équipe.", + "delete.team.confirm.modal.description.4": "Toutes les souscriptions aux API gérées par votre équipe.", + "delete.api.confirm.modal.description.1": "Êtes-vous sûr de vouloir supprimer cette API ? Cette action est irréversible et entraînera la suppression de :", + "delete.api.confirm.modal.description.2": "Toutes les clés API associées à cette API.", + "delete.user.confirm.modal.description.1": "Êtes-vous sûr de vouloir supprimer cet utilisateur ? Cette action est irréversible et entraînera la suppression de :", + "delete.user.confirm.modal.description.2": "Toutes les souscriptions et clés API associées à cet utilisateur.", + "delete.user.confirm.modal.description.3": "Toutes les API gérées par cet utilisateur.", + "delete.user.confirm.modal.description.4": "Toutes les souscriptions aux API gérées par cet utilisateur.", + "delete.subscription.confirm.modal.description.1": "Êtes-vous sûr de vouloir supprimer cette souscription ? Cette action est irréversible et entraînera la suppression de :", + "delete.subscription.confirm.modal.description.2": "La clé API associée à cette souscription.", + "delete.plan.confirm.modal.description.1": "Êtes-vous sûr de vouloir supprimer ce plan ? Cette action est irréversible et entraînera la suppression de :", + "delete.plan.confirm.modal.description.2": "Toutes les clés API associées à ce plan.", + "delete.environment.confirm.modal.description.1": "Êtes-vous sûr de vouloir supprimer cet environnement ? Cette action est irréversible et entraînera la suppression de :", + "delete.environment.confirm.modal.description.2": "Toutes les clés API associées à cet environnement.", + "delete.item.confirm.modal.confirm.label": "Saisissez %s pour confirmer la suppression" } \ No newline at end of file