mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-04-14 02:26:35 +00:00
feat: add various improvements to the table component (#961)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
This commit is contained in:
@@ -1,33 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import { Badge, type BadgeVariant } from '$lib/components/ui/badge';
|
||||
import { Button, buttonVariants } from '$lib/components/ui/button';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type {
|
||||
AdvancedTableColumn,
|
||||
CreateAdvancedTableActions
|
||||
} from '$lib/types/advanced-table.type';
|
||||
import type { SignupTokenDto } from '$lib/types/signup-token.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { Copy, Ellipsis, Trash2 } from '@lucide/svelte';
|
||||
import { Copy, Trash2 } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
signupTokens = $bindable(),
|
||||
signupTokensRequestOptions,
|
||||
onTokenDeleted
|
||||
open = $bindable()
|
||||
}: {
|
||||
open: boolean;
|
||||
signupTokens: Paginated<SignupTokenDto>;
|
||||
signupTokensRequestOptions: SearchPaginationSortRequest;
|
||||
onTokenDeleted?: () => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
const userService = new UserService();
|
||||
let tableRef: AdvancedTable<SignupTokenDto>;
|
||||
|
||||
function formatDate(dateStr: string | undefined) {
|
||||
if (!dateStr) return m.never();
|
||||
@@ -44,12 +40,8 @@
|
||||
action: async () => {
|
||||
try {
|
||||
await userService.deleteSignupToken(token.id);
|
||||
await tableRef.refresh();
|
||||
toast.success(m.signup_token_deleted_successfully());
|
||||
|
||||
// Refresh the tokens
|
||||
if (onTokenDeleted) {
|
||||
await onTokenDeleted();
|
||||
}
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -98,8 +90,69 @@
|
||||
axiosErrorToast(err);
|
||||
});
|
||||
}
|
||||
|
||||
const columns: AdvancedTableColumn<SignupTokenDto>[] = [
|
||||
{ label: m.token(), column: 'token', cell: TokenCell },
|
||||
{ label: m.status(), key: 'status', cell: StatusCell },
|
||||
{
|
||||
label: m.usage(),
|
||||
column: 'usageCount',
|
||||
sortable: true,
|
||||
cell: UsageCell
|
||||
},
|
||||
{
|
||||
label: m.expires(),
|
||||
column: 'expiresAt',
|
||||
sortable: true,
|
||||
value: (item) => formatDate(item.expiresAt)
|
||||
},
|
||||
{ label: 'Usage Limit', column: 'usageLimit' },
|
||||
{
|
||||
label: m.created(),
|
||||
column: 'createdAt',
|
||||
sortable: true,
|
||||
hidden: true,
|
||||
value: (item) => formatDate(item.createdAt)
|
||||
}
|
||||
];
|
||||
|
||||
const actions: CreateAdvancedTableActions<SignupTokenDto> = (_) => [
|
||||
{
|
||||
label: m.copy(),
|
||||
icon: Copy,
|
||||
onClick: (token) => copySignupLink(token)
|
||||
},
|
||||
{
|
||||
label: m.delete(),
|
||||
icon: Trash2,
|
||||
variant: 'danger',
|
||||
onClick: (token) => deleteToken(token)
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
{#snippet TokenCell({ item }: { item: SignupTokenDto })}
|
||||
<span class="font-mono text-xs">
|
||||
{item.token.substring(0, 3)}...{item.token.substring(Math.max(item.token.length - 4, 0))}
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet StatusCell({ item }: { item: SignupTokenDto })}
|
||||
{@const status = getTokenStatus(item)}
|
||||
{@const statusBadge = getStatusBadge(status)}
|
||||
<Badge class="rounded-full" variant={statusBadge.variant}>
|
||||
{statusBadge.text}
|
||||
</Badge>
|
||||
{/snippet}
|
||||
|
||||
{#snippet UsageCell({ item }: { item: SignupTokenDto })}
|
||||
<div class="flex items-center gap-1">
|
||||
{item.usageCount}
|
||||
{m.of()}
|
||||
{item.usageLimit}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Dialog.Root {open} {onOpenChange}>
|
||||
<Dialog.Content class="sm-min-w[500px] max-h-[90vh] min-w-[90vw] overflow-auto lg:min-w-[1000px]">
|
||||
<Dialog.Header>
|
||||
@@ -111,70 +164,13 @@
|
||||
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<AdvancedTable
|
||||
items={signupTokens}
|
||||
requestOptions={signupTokensRequestOptions}
|
||||
id="signup-token-list"
|
||||
withoutSearch={true}
|
||||
onRefresh={async (options) => {
|
||||
const result = await userService.listSignupTokens(options);
|
||||
signupTokens = result;
|
||||
return result;
|
||||
}}
|
||||
columns={[
|
||||
{ label: m.token() },
|
||||
{ label: m.status() },
|
||||
{ label: m.usage(), sortColumn: 'usageCount' },
|
||||
{ label: m.expires(), sortColumn: 'expiresAt' },
|
||||
{ label: m.created(), sortColumn: 'createdAt' },
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell class="font-mono text-xs">
|
||||
{item.token.substring(0, 2)}...{item.token.substring(item.token.length - 4)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
{@const status = getTokenStatus(item)}
|
||||
{@const statusBadge = getStatusBadge(status)}
|
||||
<Badge class="rounded-full" variant={statusBadge.variant}>
|
||||
{statusBadge.text}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-1">
|
||||
{`${item.usageCount} ${m.of()} ${item.usageLimit}`}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-sm">
|
||||
<div class="flex items-center gap-1">
|
||||
{formatDate(item.expiresAt)}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-sm">
|
||||
{formatDate(item.createdAt)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
|
||||
<Ellipsis class="size-4" />
|
||||
<span class="sr-only">{m.toggle_menu()}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={() => copySignupLink(item)}>
|
||||
<Copy class="mr-2 size-4" />
|
||||
{m.copy()}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
class="text-red-500 focus:!text-red-700"
|
||||
onclick={() => deleteToken(item)}
|
||||
>
|
||||
<Trash2 class="mr-2 size-4" />
|
||||
{m.delete()}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
fetchCallback={userService.listSignupTokens}
|
||||
bind:this={tableRef}
|
||||
{columns}
|
||||
{actions}
|
||||
/>
|
||||
</div>
|
||||
<Dialog.Footer class="mt-3">
|
||||
<Button onclick={() => (open = false)}>
|
||||
|
||||
@@ -13,11 +13,9 @@
|
||||
import { mode } from 'mode-watcher';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
onTokenCreated
|
||||
open = $bindable()
|
||||
}: {
|
||||
open: boolean;
|
||||
onTokenCreated?: () => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
const userService = new UserService();
|
||||
@@ -37,12 +35,11 @@
|
||||
|
||||
async function createSignupToken() {
|
||||
try {
|
||||
signupToken = await userService.createSignupToken(availableExpirations[selectedExpiration], usageLimit);
|
||||
signupToken = await userService.createSignupToken(
|
||||
availableExpirations[selectedExpiration],
|
||||
usageLimit
|
||||
);
|
||||
signupLink = `${page.url.origin}/st/${signupToken}`;
|
||||
|
||||
if (onTokenCreated) {
|
||||
await onTokenCreated();
|
||||
}
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user