- Add type (Mailing/Billing/Shipping) and isDefault fields to Address schema with migration. Unique default enforced server-side. - Rewrite profiles page with inline address creation form and editable user name/email within the profile modal. - Add upsertAddress and updateUser form actions. - Fix form redirect by adding use:enhance to AdminForm. - Disable Update button in AdminForm until a field is changed (prevents accidental submissions).
143 lines
3.9 KiB
Svelte
143 lines
3.9 KiB
Svelte
<script lang="ts">
|
|
import type { PrismaScalarType } from '$lib/svelte-admin/types';
|
|
import { fieldNameToLabel, resolveFieldValue } from '$lib/svelte-admin/introspect';
|
|
|
|
let {
|
|
records = [],
|
|
listFields = [],
|
|
searchFields = [],
|
|
fieldTypes = {} as Record<string, PrismaScalarType>,
|
|
singular = 'record',
|
|
onRowClick,
|
|
onNew
|
|
}: {
|
|
records: Record<string, unknown>[];
|
|
listFields: string[];
|
|
searchFields?: string[];
|
|
fieldTypes?: Record<string, PrismaScalarType>;
|
|
singular?: string;
|
|
onRowClick?: (record: Record<string, unknown>) => void;
|
|
onNew?: () => void;
|
|
} = $props();
|
|
|
|
let search = $state('');
|
|
let sortCol = $state<string | null>(null);
|
|
let sortAsc = $state(true);
|
|
|
|
let filtered = $derived(
|
|
search
|
|
? records.filter((r) =>
|
|
searchFields.some((f) => {
|
|
const v = resolveFieldValue(r, f);
|
|
return v != null && String(v).toLowerCase().includes(search.toLowerCase());
|
|
})
|
|
)
|
|
: records
|
|
);
|
|
|
|
let sorted = $derived(
|
|
sortCol
|
|
? [...filtered].sort((a, b) => {
|
|
const va = resolveFieldValue(a, sortCol!);
|
|
const vb = resolveFieldValue(b, sortCol!);
|
|
if (va == null) return 1;
|
|
if (vb == null) return -1;
|
|
const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true });
|
|
return sortAsc ? cmp : -cmp;
|
|
})
|
|
: filtered
|
|
);
|
|
|
|
function toggleSort(field: string) {
|
|
if (sortCol === field) {
|
|
sortAsc = !sortAsc;
|
|
} else {
|
|
sortCol = field;
|
|
sortAsc = true;
|
|
}
|
|
}
|
|
|
|
function formatCellValue(v: unknown, field: string): string {
|
|
if (v == null) return '';
|
|
const ft = fieldTypes[field];
|
|
if (ft === 'DateTime') {
|
|
if (v instanceof Date) return v.toLocaleString();
|
|
return new Date(v as string).toLocaleString();
|
|
}
|
|
if (ft === 'Boolean') return v ? 'Active' : 'Inactive';
|
|
if (ft === 'Float') {
|
|
const n = Number(v);
|
|
return Number.isNaN(n) ? String(v) : n.toFixed(2);
|
|
}
|
|
return String(v);
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-4">
|
|
<div class="flex items-center justify-between gap-4">
|
|
{#if searchFields.length > 0}
|
|
<input
|
|
type="text"
|
|
placeholder="Search {singular}s..."
|
|
bind:value={search}
|
|
class="max-w-xs border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
|
|
/>
|
|
{:else}
|
|
<div></div>
|
|
{/if}
|
|
{#if onNew}
|
|
<button
|
|
type="button"
|
|
onclick={onNew}
|
|
class="border border-gray-300 px-3 py-1.5 text-sm font-medium dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"
|
|
>
|
|
New {singular} +
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="overflow-x-auto border border-gray-200 dark:border-gray-700">
|
|
<table class="w-full">
|
|
<thead>
|
|
<tr class="border-b border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-800">
|
|
{#each listFields as field (field)}
|
|
<th
|
|
scope="col"
|
|
onclick={() => toggleSort(field)}
|
|
class="cursor-pointer px-4 py-2 text-left text-xs font-medium tracking-wider text-gray-500 uppercase hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
|
>
|
|
{fieldNameToLabel(field)}
|
|
{#if sortCol === field}
|
|
<span class="ml-1">{sortAsc ? '\u25B2' : '\u25BC'}</span>
|
|
{/if}
|
|
</th>
|
|
{/each}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each sorted as record, i ((record['id'] as string) ?? (record['key'] as string) ?? i)}
|
|
<tr
|
|
onclick={() => onRowClick?.(record)}
|
|
class="cursor-pointer border-b border-gray-200 hover:bg-gray-100 dark:border-gray-700 dark:text-gray-100 dark:hover:bg-gray-800"
|
|
>
|
|
{#each listFields as field (field)}
|
|
<td class="px-4 py-2 text-sm">
|
|
{formatCellValue(resolveFieldValue(record, field), field)}
|
|
</td>
|
|
{/each}
|
|
</tr>
|
|
{:else}
|
|
<tr>
|
|
<td
|
|
colspan={listFields.length}
|
|
class="px-4 py-8 text-center text-sm text-gray-500 dark:text-gray-400"
|
|
>
|
|
No {singular}s found.
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|