Browse Source

delete client using radix dialog

pull/1572/head
Bernd Storath 3 months ago
parent
commit
f58f308d1f
  1. 3
      .vscode/settings.json
  2. 126
      src/app/components/Clients/DeleteDialog.vue
  3. 4
      src/app/components/form/ActionField.vue
  4. 32
      src/app/pages/clients/[id].vue
  5. 4
      src/app/pages/index.vue
  6. 13
      src/app/stores/modal.ts
  7. 6
      src/app/utils/api.ts

3
.vscode/settings.json

@ -21,5 +21,6 @@
],
"i18n-ally.sortKeys": false,
"i18n-ally.keepFulfilled": false,
"i18n-ally.keystyle": "nested"
"i18n-ally.keystyle": "nested",
"editor.gotoLocation.multipleDefinitions": "goto"
}

126
src/app/components/Clients/DeleteDialog.vue

@ -1,99 +1,41 @@
<template>
<div
v-if="modalStore.clientDelete"
class="fixed inset-0 z-10 overflow-y-auto"
>
<div
class="flex min-h-screen items-center justify-center px-4 pb-20 pt-4 text-center sm:block sm:p-0"
>
<!--
Background overlay, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0"
To: "opacity-100"
Leaving: "ease-in duration-200"
From: "opacity-100"
To: "opacity-0"
-->
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50"
/>
</div>
<!-- This element is to trick the browser into centering the modal contents. -->
<span
class="hidden sm:inline-block sm:h-screen sm:align-middle"
aria-hidden="true"
>&#8203;</span
>
<!--
Modal panel, show/hide based on modal state.
Entering: "ease-out duration-300"
From: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95"
To: "opacity-100 tranneutral-y-0 sm:scale-100"
Leaving: "ease-in duration-200"
From: "opacity-100 tranneutral-y-0 sm:scale-100"
To: "opacity-0 tranneutral-y-4 sm:tranneutral-y-0 sm:scale-95"
-->
<div
class="inline-block w-full transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:max-w-lg sm:align-middle dark:bg-neutral-700"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
<DialogRoot :modal="true">
<DialogTrigger :class="triggerClass"><slot /></DialogTrigger>
<DialogPortal>
<DialogOverlay
class="data-[state=open]:animate-overlayShow fixed inset-0 z-30 bg-gray-500 opacity-75 dark:bg-black dark:opacity-50"
/>
<DialogContent
class="data-[state=open]:animate-contentShow fixed left-1/2 top-1/2 z-[100] max-h-[85vh] w-[90vw] max-w-md -translate-x-1/2 -translate-y-1/2 rounded-md p-6 shadow-2xl focus:outline-none dark:bg-neutral-700"
>
<div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4 dark:bg-neutral-700">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"
>
<IconsWarning class="h-6 w-6 text-red-600" />
</div>
<div class="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<h3
id="modal-headline"
class="text-lg font-medium leading-6 text-gray-900 dark:text-neutral-200"
>
{{ $t('deleteClient') }}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-neutral-300">
{{ $t('deleteDialog1') }}
<strong>{{ modalStore.clientDelete.name }}</strong
>? {{ $t('deleteDialog2') }}
</p>
</div>
</div>
</div>
</div>
<div
class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 dark:bg-neutral-600"
<DialogTitle
class="m-0 text-lg font-semibold text-gray-900 dark:text-neutral-200"
>
{{ $t('deleteClient') }}
</DialogTitle>
<DialogDescription
class="mb-5 mt-2 text-sm leading-normal text-gray-500 dark:text-neutral-300"
>
<button
type="button"
class="inline-flex w-full justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm dark:bg-red-600 dark:text-white dark:hover:bg-red-700"
@click="
modalStore.deleteClient(modalStore.clientDelete);
modalStore.clientDelete = null;
"
>
{{ $t('deleteClient') }}
</button>
<button
type="button"
class="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none sm:ml-3 sm:mt-0 sm:w-auto sm:text-sm dark:border-neutral-500 dark:bg-neutral-500 dark:text-neutral-50 dark:hover:border-neutral-600 dark:hover:bg-neutral-600"
@click="modalStore.clientDelete = null"
>
{{ $t('cancel') }}
</button>
{{ $t('deleteDialog1') }}
<strong>{{ 'test' }}</strong
>? {{ $t('deleteDialog2') }}
</DialogDescription>
<div class="mt-6 flex justify-end gap-2">
<DialogClose as-child>
<BaseButton>{{ $t('cancel') }}</BaseButton>
</DialogClose>
<DialogClose as-child>
<BaseButton @click="$emit('delete')">{{
$t('deleteClient')
}}</BaseButton>
</DialogClose>
</div>
</div>
</div>
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>
<script setup lang="ts">
const modalStore = useModalStore();
<script lang="ts" setup>
defineEmits(['delete']);
defineProps<{ triggerClass?: string }>();
</script>

4
src/app/components/form/ActionField.vue

@ -7,8 +7,10 @@
</template>
<script lang="ts" setup>
import type { InputTypeHTMLAttribute } from 'vue';
defineProps<{
label: string;
type?: string;
type?: InputTypeHTMLAttribute;
}>();
</script>

32
src/app/pages/clients/[id].vue

@ -60,7 +60,12 @@
<FormHeading>Actions</FormHeading>
<FormActionField type="submit" label="Save" />
<FormActionField label="Revert" @click="revert" />
<FormActionField type="submit" formmethod="delete" label="Delete" />
<ClientsDeleteDialog
trigger-class="col-span-2"
@delete="deleteClient"
>
<FormActionField label="Delete" class="w-full" />
</ClientsDeleteDialog>
</FormGroup>
</FormElement>
</PanelBody>
@ -111,4 +116,29 @@ async function revert() {
await refresh();
data.value = toRef(_data.value).value;
}
async function deleteClient() {
try {
const res = await $fetch(`/api/client/${id}`, {
method: 'delete',
});
toast.showToast({
type: 'success',
title: 'Success',
message: 'Deleted',
});
if (!res.success) {
throw new Error('Failed to delete');
}
router.push('/');
} catch (e) {
if (e instanceof Error) {
toast.showToast({
type: 'error',
title: 'Error',
message: e.message,
});
}
}
}
</script>

4
src/app/pages/index.vue

@ -27,7 +27,6 @@
<ClientsQRCodeDialog />
<ClientsCreateDialog />
<ClientsDeleteDialog />
</main>
</template>
@ -37,6 +36,9 @@ authStore.update();
const globalStore = useGlobalStore();
const clientsStore = useClientsStore();
// TODO?: use hover card to show more detailed info without leaving the page
// or do something like a accordion
const intervalId = ref<NodeJS.Timeout | null>(null);
clientsStore.refresh();

13
src/app/stores/modal.ts

@ -2,7 +2,6 @@ import { defineStore } from 'pinia';
export const useModalStore = defineStore('Modal', () => {
const clientsStore = useClientsStore();
const clientDelete = ref<null | WGClient>(null);
const clientCreate = ref<null | boolean>(null);
const clientCreateName = ref<string>('');
const clientExpireDate = ref<string>('');
@ -19,23 +18,11 @@ export const useModalStore = defineStore('Modal', () => {
.finally(() => clientsStore.refresh().catch(console.error));
}
function deleteClient(client: WGClient | null) {
if (client === null) {
return;
}
api
.deleteClient({ clientId: client.id })
.catch((err) => alert(err.message || err.toString()))
.finally(() => clientsStore.refresh().catch(console.error));
}
return {
clientDelete,
clientCreate,
clientCreateName,
clientExpireDate,
qrcode,
createClient,
deleteClient,
};
});

6
src/app/utils/api.ts

@ -45,12 +45,6 @@ class API {
});
}
async deleteClient({ clientId }: { clientId: string }) {
return $fetch(`/api/client/${clientId}`, {
method: 'delete',
});
}
async showOneTimeLink({ clientId }: { clientId: string }) {
return $fetch(`/api/client/${clientId}/generateOneTimeLink`, {
method: 'post',

Loading…
Cancel
Save