Commit 1: Create individual form components per route, remove generic AdminForm dependency
- Add per-route form components: UserRoleForm, UserForm, ProclamationForm (with category cascade), CategoryForm, FreqForm, LevelForm - Update all admin +page.svelte files to use AdminTable + AdminModal + their specific form component instead of AdminPage - Update profiles page to inline fields instead of using AdminForm - Add custom Proclamation server action with category fallback logic (categoryId falls back to parentCategoryId when no sub-category) - Remove cascade/were/fieldLabels logic from generic crud.ts - Remove outdated cascade/where tests from crud.spec.ts - Remove fieldLabels from AdminModelConfig type
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import TipTap from '$lib/components/tiptap/TipTap.svelte';
|
||||
|
||||
let {
|
||||
record = {} as Record<string, unknown>,
|
||||
fieldErrors = {} as Record<string, string>,
|
||||
formError = '',
|
||||
relationOptionsData = {} as Record<string, { label: string; value: string | number }[]>,
|
||||
categories = [] as { id: number; name: string; parentId: number | null }[],
|
||||
action = '?/upsert',
|
||||
submitLabel = 'Save'
|
||||
}: {
|
||||
record?: Record<string, unknown>;
|
||||
fieldErrors?: Record<string, string>;
|
||||
formError?: string;
|
||||
relationOptionsData?: Record<string, { label: string; value: string | number }[]>;
|
||||
categories?: { id: number; name: string; parentId: number | null }[];
|
||||
action?: string;
|
||||
submitLabel?: string;
|
||||
} = $props();
|
||||
|
||||
let dirty = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
dirty = false;
|
||||
});
|
||||
|
||||
function markDirty() {
|
||||
if (record.id) dirty = true;
|
||||
}
|
||||
|
||||
let name = $state(String(record.name ?? ''));
|
||||
let solfeggioFrequencyId = $state(String(record.solfeggioFrequencyId ?? ''));
|
||||
let subscriptionLevelId = $state(String(record.subscriptionLevelId ?? ''));
|
||||
|
||||
let parentCategoryId = $state(
|
||||
String(
|
||||
record.parentCategoryId ??
|
||||
(record.category as Record<string, unknown> | undefined)?.parentId ??
|
||||
''
|
||||
)
|
||||
);
|
||||
let categoryId = $state(String(record.categoryId ?? ''));
|
||||
|
||||
let parentOptions = $derived(categories.filter((c) => c.parentId === null));
|
||||
|
||||
let childOptions = $derived(
|
||||
parentCategoryId ? categories.filter((c) => c.parentId === Number(parentCategoryId)) : []
|
||||
);
|
||||
|
||||
function handleParentChange(e: Event) {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
parentCategoryId = target.value;
|
||||
categoryId = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<form {action} method="POST" use:enhance oninput={markDirty}>
|
||||
{#if record?.id}
|
||||
<input type="hidden" name="id" value={String(record.id)} />
|
||||
{/if}
|
||||
|
||||
{#if formError}
|
||||
<div class="mb-4 border border-red-300 bg-red-50 px-4 py-2 text-sm text-red-700">
|
||||
{formError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<label
|
||||
for="field-name"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="field-name"
|
||||
name="name"
|
||||
bind:value={name}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.name
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-solfeggioFrequencyId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Frequency
|
||||
</label>
|
||||
<select
|
||||
id="field-solfeggioFrequencyId"
|
||||
name="solfeggioFrequencyId"
|
||||
bind:value={solfeggioFrequencyId}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.solfeggioFrequencyId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Frequency --</option>
|
||||
{#each relationOptionsData.solfeggioFrequencyId ?? [] as opt (String(opt.value))}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.solfeggioFrequencyId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.solfeggioFrequencyId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-parentCategoryId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Category
|
||||
</label>
|
||||
<select
|
||||
id="field-parentCategoryId"
|
||||
name="parentCategoryId"
|
||||
value={parentCategoryId}
|
||||
onchange={handleParentChange}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.parentCategoryId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Category --</option>
|
||||
{#each parentOptions as cat (String(cat.id))}
|
||||
<option value={cat.id}>{cat.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.parentCategoryId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.parentCategoryId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-categoryId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Sub-Category
|
||||
</label>
|
||||
<select
|
||||
id="field-categoryId"
|
||||
name="categoryId"
|
||||
bind:value={categoryId}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.categoryId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Sub-Category --</option>
|
||||
{#each childOptions as cat (String(cat.id))}
|
||||
<option value={cat.id}>{cat.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.categoryId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.categoryId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-subscriptionLevelId"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Subscription Level
|
||||
</label>
|
||||
<select
|
||||
id="field-subscriptionLevelId"
|
||||
name="subscriptionLevelId"
|
||||
bind:value={subscriptionLevelId}
|
||||
class="w-full border px-3 py-1.5 text-sm {fieldErrors.subscriptionLevelId
|
||||
? 'border-red-400'
|
||||
: 'border-gray-300 dark:border-gray-600'} dark:bg-gray-800 dark:text-gray-100"
|
||||
>
|
||||
<option value="">-- Select Level --</option>
|
||||
{#each relationOptionsData.subscriptionLevelId ?? [] as opt (String(opt.value))}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if fieldErrors.subscriptionLevelId}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.subscriptionLevelId}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
for="field-content"
|
||||
class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
Content
|
||||
</label>
|
||||
<div id="field-content">
|
||||
<TipTap
|
||||
initialContent={typeof record.content === 'string' ? record.content : ''}
|
||||
name="content"
|
||||
/>
|
||||
</div>
|
||||
{#if fieldErrors.content}
|
||||
<p class="mt-1 text-xs text-red-600">{fieldErrors.content}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!!record?.id && !dirty}
|
||||
class="border border-gray-300 bg-gray-100 px-4 py-1.5 text-sm font-medium hover:bg-gray-200 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
Reference in New Issue
Block a user