Skip to content

Commit

Permalink
feat: add extra confirmation when deleting domain or importing a back…
Browse files Browse the repository at this point in the history
…up (#1391)

* Add word confirmation when deleting domain/import a backup

* New promptModal & fix modal boolean validation & fix flash errors

* ruff format

* added promptmodal for the supression of a domain and corrected useless action.ts delete hidden input on forms

* tolowercase yes to have m.yes = Yes instead of yes

* corrected tests

* changed tests and test-ids

* corrected tests

* correct tests

---------

Co-authored-by: Mohamed-Hacene <[email protected]>
  • Loading branch information
melinoix and Mohamed-Hacene authored Feb 3, 2025
1 parent d5afb49 commit 385366c
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 53 deletions.
3 changes: 3 additions & 0 deletions frontend/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,9 @@
"startDate": "Start date",
"startDateHelpText": "Start date (useful for timeline)",
"backupLoadingError": "An error occurred while loading the backup.",
"backupVersionError": "Can't load the backup, the version of the backup is invalid",
"confirmYes": "Confirm by typing 'yes' below:",
"confirmYesPlaceHolder": "Type 'yes' to confirm",
"backupGreaterVersionError": "Can't load the backup, the version of the backup is higher than the current version of your application.",
"backupLowerVersionError": "An error occurred, the backup version may be too old, if so it must be updated before retrying.",
"entityAssessmentEvidenceHelpText": "An external questionnaire",
Expand Down
3 changes: 3 additions & 0 deletions frontend/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,9 @@
"errorOccuredDuringImport": "Une erreur s'est produite lors de l'import",
"successfullyImportedFolder": "Domaine importé avec succès",
"missingLibrariesInImport": "Certaines bibliothèques sont manquantes, voir la liste ci-dessus",
"backupVersionError": "Impossible de charger la sauvegarde, la version de la sauvegarde est invalide",
"confirmYes": "Confirmez en écrivant 'oui' puis validez:",
"confirmYesPlaceHolder": "Écrivez 'oui' puis validez",
"loadMissingLibraries": "Charger les librairies manquantes",
"loadMissingLibrariesHelpText": "Charger les librairies manquantes pour compléter l'import si elles sont disponibles.",
"yes": "Oui",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
<!-- prettier-ignore -->
<footer class="modal-footer {parent.regionFooter}">
<button type="button" class="btn {parent.buttonNeutral}" data-testid="delete-cancel-button" on:click={parent.onClose}>{m.cancel()}</button>
<input type="hidden" name="delete" />
<input type="hidden" name="urlmodel" value={URLModel} />
<input type="hidden" name="id" value={id} />
<button class="btn variant-filled-error" data-testid="delete-confirm-button" type="submit" on:click={parent.onClose}>{m.submit()}</button>
Expand Down
103 changes: 103 additions & 0 deletions frontend/src/lib/components/Modals/PromptConfirmModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<script lang="ts">
import type { urlModel } from '$lib/utils/types';
// Props
/** Exposes parent props to this component. */
export let parent: any;
// Stores
import { getModalStore } from '@skeletonlabs/skeleton';
import type { ModalStore } from '@skeletonlabs/skeleton';
import * as m from '$paraglide/messages';
const modalStore: ModalStore = getModalStore();
export let _form = {};
export let URLModel: urlModel | '' = '';
export let id: string = '';
export let formAction: string = '';
export let bodyComponent: ComponentType | undefined;
export let bodyProps: Record<string, unknown> = {};
import { superForm } from 'sveltekit-superforms';
const { form } = _form
? superForm(_form, {
dataType: 'json',
id: `confirm-modal-form-${crypto.randomUUID()}`
})
: null;
// Base Classes
const cBase = 'card p-4 w-modal shadow-xl space-y-4';
const cHeader = 'text-2xl font-bold';
const cForm = 'p-4 space-y-4 rounded-container-token';
import SuperDebug from 'sveltekit-superforms';
import type { ComponentType } from 'svelte';
import { enhance } from '$app/forms';
export let debug = false;
let userInput = '';
</script>

{#if $modalStore[0]}
<div class="modal-example-form {cBase}">
<header class={cHeader}>{$modalStore[0].title ?? '(title missing)'}</header>
<article>{$modalStore[0].body ?? '(body missing)'}</article>

<p class="text-red-500 font-bold">{m.confirmYes()}</p>
<input
type="text"
data-testid="delete-prompt-confirm-textfield"
bind:value={userInput}
placeholder={m.confirmYesPlaceHolder()}
class="w-full mt-2 p-2 border border-gray-300 rounded"
/>

{#if bodyComponent}
<div class="max-h-96 overflow-y-scroll scroll card">
<svelte:component this={bodyComponent} {...bodyProps} />
</div>
{/if}
{#if _form && Object.keys(_form).length > 0}
<form method="POST" action={formAction} use:enhance class="modal-form {cForm}">
<footer class="modal-footer {parent.regionFooter}">
<button type="button" class="btn {parent.buttonNeutral}" on:click={parent.onClose}
>{m.cancel()}</button
>
<input type="hidden" name="urlmodel" value={URLModel} />
<input type="hidden" name="id" value={id} />
<button
class="btn variant-filled-error"
type="submit"
data-testid="delete-prompt-confirm-button"
on:click={parent.onConfirm}
disabled={!userInput || userInput.trim().toLowerCase() !== m.yes().toLowerCase()}
>
{m.submit()}
</button>
</footer>
</form>

{#if debug === true}
<SuperDebug data={$form} />
{/if}
{:else}
<footer class="modal-footer {parent.regionFooter}">
<button type="button" class="btn {parent.buttonNeutral}" on:click={parent.onClose}
>{m.cancel()}</button
>
<button
class="btn variant-filled-error"
type="button"
on:click={parent.onConfirm}
disabled={!userInput || userInput.trim().toLowerCase() !== m.yes().toLowerCase()}
>
{m.submit()}
</button>
</footer>
{/if}
</div>
{/if}
63 changes: 54 additions & 9 deletions frontend/src/lib/components/TableRowActions/TableRowActions.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { page } from '$app/stores';
import DeleteConfirmModal from '$lib/components/Modals/DeleteConfirmModal.svelte';
import PromptConfirmModal from '$lib/components/Modals/PromptConfirmModal.svelte';
import type { ModelMapEntry } from '$lib/utils/crud';
import type { urlModel } from '$lib/utils/types';
import type { ModalComponent, ModalSettings, ModalStore } from '@skeletonlabs/skeleton';
Expand Down Expand Up @@ -60,6 +61,38 @@
modalStore.trigger(modal);
}
function promptModalConfirmDelete(
id: string,
row: { [key: string]: string | number | boolean | null }
): void {
const modalComponent: ModalComponent = {
ref: PromptConfirmModal,
props: {
_form: deleteForm,
id: id,
debug: false,
URLModel: URLModel,
formAction: '?/delete'
}
};
const name =
URLModel === 'users' && row.first_name
? `${row.first_name} ${row.last_name} (${row.email})`
: (row.name ?? Object.values(row)[0]);
const body =
URLModel === 'users'
? m.deleteUserMessage({ name: name })
: m.deleteModalMessage({ name: name });
const modal: ModalSettings = {
type: 'component',
component: modalComponent,
// Data
title: m.deleteModalTitle(),
body: body
};
modalStore.trigger(modal);
}
const user = $page.data.user;
$: canDeleteObject = Object.hasOwn(user.permissions, `delete_${model?.name}`) && !preventDelete;
$: canEditObject = Object.hasOwn(user.permissions, `change_${model?.name}`);
Expand Down Expand Up @@ -98,15 +131,27 @@
>
{/if}
{#if displayDelete}
<button
on:click={(_) => {
modalConfirmDelete(row.meta[identifierField], row);
stopPropagation(_);
}}
on:keydown={() => modalConfirmDelete(row.meta.id, row)}
class="cursor-pointer hover:text-primary-500"
data-testid="tablerow-delete-button"><i class="fa-solid fa-trash" /></button
>
{#if URLModel === 'folders'}
<button
on:click={(_) => {
promptModalConfirmDelete(row.meta[identifierField], row);
stopPropagation(_);
}}
on:keydown={() => promptModalConfirmDelete(row.meta.id, row)}
class="cursor-pointer hover:text-primary-500"
data-testid="tablerow-delete-button"><i class="fa-solid fa-trash" /></button
>
{:else}
<button
on:click={(_) => {
modalConfirmDelete(row.meta[identifierField], row);
stopPropagation(_);
}}
on:keydown={() => modalConfirmDelete(row.meta.id, row)}
class="cursor-pointer hover:text-primary-500"
data-testid="tablerow-delete-button"><i class="fa-solid fa-trash" /></button
>
{/if}
{/if}
{/if}
<slot name="tail" />
Expand Down
47 changes: 22 additions & 25 deletions frontend/src/lib/utils/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,33 +216,30 @@ export async function defaultDeleteFormAction({
return fail(400, { form: deleteForm });
}

if (formData.has('delete')) {
const requestInitOptions: RequestInit = {
method: 'DELETE'
};
const res = await event.fetch(endpoint, requestInitOptions);
if (!res.ok) {
const response = await res.json();
if (response.error) {
setFlash({ type: 'error', message: safeTranslate(response.error) }, event);
return fail(403, { form: deleteForm });
}
if (response.non_field_errors) {
setError(deleteForm, 'non_field_errors', response.non_field_errors);
}
return fail(400, { form: deleteForm });
const requestInitOptions: RequestInit = {
method: 'DELETE'
};
const res = await event.fetch(endpoint, requestInitOptions);
if (!res.ok) {
const response = await res.json();
if (response.error) {
setFlash({ type: 'error', message: safeTranslate(response.error) }, event);
return fail(403, { form: deleteForm });
}
const model: string = urlParamModelVerboseName(urlModel!);
setFlash(
{
type: 'success',
message: m.successfullyDeletedObject({
object: safeTranslate(model).toLowerCase()
})
},
event
);
if (response.non_field_errors) {
setError(deleteForm, 'non_field_errors', response.non_field_errors);
}
return fail(400, { form: deleteForm });
}
setFlash(
{
type: 'success',
message: m.successfullyDeletedObject({
object: safeTranslate(model.localName).toLowerCase()
})
},
event
);

return { deleteForm };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const actions: Actions = {
return nestedWriteFormAction({ event, action: 'create', redirectToWrittenObject });
},
delete: async (event) => {
console.log('delete');
return nestedDeleteFormAction({ event });
},
duplicate: async (event) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const actions: Actions = {
default: async (event) => {
const { request, fetch } = event;
const formData = Object.fromEntries(await request.formData());
if (!(formData.file as File).name || (formData.file as File).name === 'undefined') {
if (!(formData.file as File)?.name || (formData.file as File)?.name === 'undefined') {
return fail(400, {
error: true,
message: 'You must provide a file to upload'
Expand All @@ -32,13 +32,21 @@ export const actions: Actions = {
body: file
});
const data = await response.json();

if (response.status >= 400 && !data.error) {
setFlash({ type: 'error', message: m.backupLoadingError() }, event);
} else if (data.error === 'GreaterBackupVersion') {
setFlash({ type: 'error', message: m.backupGreaterVersionError() }, event);
} else if (data.error === 'LowerBackupVersion') {
setFlash({ type: 'error', message: m.backupLowerVersionError() }, event);
if (response.status >= 400) {
switch (data.error) {
case 'errorBackupInvalidVersion':
setFlash({ type: 'error', message: m.backupVersionError() }, event);
break;
case 'GreaterBackupVersion':
setFlash({ type: 'error', message: m.backupGreaterVersionError() }, event);
break;
case 'LowerBackupVersion':
setFlash({ type: 'error', message: m.backupLowerVersionError() }, event);
break;
default:
setFlash({ type: 'error', message: m.backupLoadingError() }, event);
break;
}
}

return {
Expand Down
26 changes: 20 additions & 6 deletions frontend/src/routes/(app)/(internal)/backup-restore/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,35 @@
import { enhance } from '$app/forms';
import { page } from '$app/stores';
import * as m from '$paraglide/messages';
import type { PageData } from './$types';
import { getModalStore, type ModalSettings } from '@skeletonlabs/skeleton';
export let data: PageData;
const modalStore = getModalStore();
import type { ModalSettings, ModalComponent, ModalStore } from '@skeletonlabs/skeleton';
import { getModalStore } from '@skeletonlabs/skeleton';
import PromptConfirmModal from '$lib/components/Modals/PromptConfirmModal.svelte';
const modalStore: ModalStore = getModalStore();
let form: HTMLFormElement;
let file: HTMLInputElement;
function modalConfirmUpload(): void {
// Function to handle modal confirmation for any action
function modalConfirm(): void {
const modalComponent: ModalComponent = {
ref: PromptConfirmModal
};
const modal: ModalSettings = {
type: 'confirm',
type: 'component',
component: modalComponent,
title: m.importBackup(),
body: m.confirmImportBackup(),
response: () => form.requestSubmit()
response: (r: boolean) => {
if (r) form.requestSubmit();
}
};
if (file) modalStore.trigger(modal);
}
Expand Down Expand Up @@ -56,7 +70,7 @@
<button
class="btn variant-filled mt-2 lg:mt-0 {uploadButtonStyles}"
type="button"
on:click={modalConfirmUpload}>{m.upload()}</button
on:click={modalConfirm}>{m.upload()}</button
>
</form>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LoginPage } from '../../utils/login-page.js';
import { PageContent } from '../../utils/page-content.js';
import { TestContent, test, expect } from '../../utils/test-utils.js';
import * as m from '$paraglide/messages';

let vars = TestContent.generateTestVars();
let testObjectsData: { [k: string]: any } = TestContent.itemBuilder(vars);
Expand Down Expand Up @@ -209,6 +210,8 @@ test.afterAll('cleanup', async ({ browser }) => {
await loginPage.login();
await foldersPage.goto();
await foldersPage.deleteItemButton(vars.folderName).click();
await foldersPage.deleteModalConfirmButton.click();
await expect(foldersPage.deletePromptConfirmTextField()).toBeVisible();
await foldersPage.deletePromptConfirmTextField().fill(m.yes());
await foldersPage.deletePromptConfirmButton().click();
await expect(foldersPage.getRow(vars.folderName)).not.toBeVisible();
});
Loading

0 comments on commit 385366c

Please sign in to comment.