Add checklist editing: rename, edit items, reorder with arrows
Deploy to LXC / deploy (push) Successful in 20s
Deploy to LXC / deploy (push) Successful in 20s
Templates page (/checklists): - Edit template name/description inline (pencil icon) - Edit item text and unit inline (pencil icon on hover) - Move items up/down with arrow buttons - Reorder swaps sort order values in the database Device checklists: - Rename checklist title inline (pencil icon) - Edit item text and unit inline (pencil icon on hover) - Move items up/down with arrow buttons - All item types (checkbox and input) support editing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,61 @@ export const actions: Actions = {
|
||||
return { itemAdded: true };
|
||||
},
|
||||
|
||||
renameTemplate: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const templateId = formData.get('templateId') as string;
|
||||
const title = (formData.get('title') as string)?.trim();
|
||||
const description = (formData.get('description') as string)?.trim();
|
||||
if (!title) return fail(400, { error: 'Title is required' });
|
||||
|
||||
await db
|
||||
.update(checklistTemplates)
|
||||
.set({ title, description: description || null, updatedAt: new Date() })
|
||||
.where(eq(checklistTemplates.id, templateId));
|
||||
return { renamed: true };
|
||||
},
|
||||
|
||||
editItem: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const itemId = formData.get('itemId') as string;
|
||||
const text = (formData.get('text') as string)?.trim();
|
||||
const unit = (formData.get('unit') as string)?.trim();
|
||||
if (!text) return fail(400, { error: 'Item text is required' });
|
||||
|
||||
await db
|
||||
.update(templateItems)
|
||||
.set({ text, unit: unit || null })
|
||||
.where(eq(templateItems.id, itemId));
|
||||
return { itemEdited: true };
|
||||
},
|
||||
|
||||
moveItem: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const itemId = formData.get('itemId') as string;
|
||||
const direction = formData.get('direction') as string; // 'up' or 'down'
|
||||
const templateId = formData.get('templateId') as string;
|
||||
|
||||
const items = await db
|
||||
.select({ id: templateItems.id, sortOrder: templateItems.sortOrder })
|
||||
.from(templateItems)
|
||||
.where(eq(templateItems.templateId, templateId))
|
||||
.orderBy(templateItems.sortOrder);
|
||||
|
||||
const idx = items.findIndex((i) => i.id === itemId);
|
||||
if (idx < 0) return fail(400, { error: 'Item not found' });
|
||||
|
||||
const swapIdx = direction === 'up' ? idx - 1 : idx + 1;
|
||||
if (swapIdx < 0 || swapIdx >= items.length) return { moved: true };
|
||||
|
||||
// Swap sort orders
|
||||
const a = items[idx];
|
||||
const b = items[swapIdx];
|
||||
await db.update(templateItems).set({ sortOrder: b.sortOrder }).where(eq(templateItems.id, a.id));
|
||||
await db.update(templateItems).set({ sortOrder: a.sortOrder }).where(eq(templateItems.id, b.id));
|
||||
|
||||
return { moved: true };
|
||||
},
|
||||
|
||||
deleteItem: async ({ request }) => {
|
||||
const formData = await request.formData();
|
||||
const itemId = formData.get('itemId') as string;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
let { data, form } = $props();
|
||||
let showNewTemplate = $state(false);
|
||||
let editingTemplateId = $state<string | null>(null);
|
||||
let editingItemId = $state<string | null>(null);
|
||||
function resetForm(form: HTMLFormElement) {
|
||||
const input = form.querySelector('input[name="text"]') as HTMLInputElement;
|
||||
const unit = form.querySelector('input[name="unit"]') as HTMLInputElement;
|
||||
@@ -59,48 +61,132 @@
|
||||
<div class="space-y-4">
|
||||
{#each data.templates as template}
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="mb-3 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 class="font-medium text-gray-900 dark:text-white">{template.title}</h2>
|
||||
{#if template.description}
|
||||
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">{template.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
||||
{template.items.length} item{template.items.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<form method="POST" action="?/deleteTemplate" use:enhance>
|
||||
<input type="hidden" name="templateId" value={template.id} />
|
||||
<button type="submit" class="text-gray-400 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-400" title="Delete template">
|
||||
<!-- Template header -->
|
||||
{#if editingTemplateId === template.id}
|
||||
<form method="POST" action="?/renameTemplate" use:enhance={() => {
|
||||
return async ({ update, result }) => {
|
||||
await update();
|
||||
if (result.type === 'success') editingTemplateId = null;
|
||||
};
|
||||
}} class="mb-3 flex flex-wrap items-end gap-2">
|
||||
<input type="hidden" name="templateId" value={template.id} />
|
||||
<div class="flex-1">
|
||||
<label for="title-{template.id}" class="mb-1 block text-xs text-gray-500 dark:text-gray-400">Name</label>
|
||||
<input type="text" id="title-{template.id}" name="title" value={template.title} required
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label for="desc-{template.id}" class="mb-1 block text-xs text-gray-500 dark:text-gray-400">Description</label>
|
||||
<input type="text" id="desc-{template.id}" name="description" value={template.description ?? ''}
|
||||
class="w-full rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
</div>
|
||||
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Save</button>
|
||||
<button type="button" onclick={() => (editingTemplateId = null)} class="rounded-md px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">Cancel</button>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="mb-3 flex items-start justify-between">
|
||||
<div>
|
||||
<h2 class="font-medium text-gray-900 dark:text-white">{template.title}</h2>
|
||||
{#if template.description}
|
||||
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">{template.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
||||
{template.items.length} item{template.items.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
<button type="button" onclick={() => (editingTemplateId = template.id)} class="text-gray-400 hover:text-blue-600 dark:text-gray-500 dark:hover:text-blue-400" title="Edit template">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Items -->
|
||||
<div class="space-y-1">
|
||||
{#each template.items as item}
|
||||
<div class="group flex items-center gap-2 rounded-md py-1">
|
||||
<span class="text-sm text-gray-400 dark:text-gray-500">{item.sortOrder + 1}.</span>
|
||||
<span class="flex-1 text-sm text-gray-700 dark:text-gray-300">{item.text}</span>
|
||||
{#if item.itemType === 'input'}
|
||||
<span class="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/40 dark:text-purple-300">
|
||||
input{#if item.unit}: {item.unit}{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<form method="POST" action="?/deleteItem" use:enhance>
|
||||
<input type="hidden" name="itemId" value={item.id} />
|
||||
<button type="submit" class="hidden text-gray-400 hover:text-red-500 group-hover:block dark:text-gray-500 dark:hover:text-red-400" title="Remove">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
<form method="POST" action="?/deleteTemplate" use:enhance>
|
||||
<input type="hidden" name="templateId" value={template.id} />
|
||||
<button type="submit" class="text-gray-400 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-400" title="Delete template">
|
||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Items -->
|
||||
<div class="space-y-1">
|
||||
{#each template.items as item, idx}
|
||||
{#if editingItemId === item.id}
|
||||
<!-- Inline edit item -->
|
||||
<form method="POST" action="?/editItem" use:enhance={() => {
|
||||
return async ({ update, result }) => {
|
||||
await update();
|
||||
if (result.type === 'success') editingItemId = null;
|
||||
};
|
||||
}} class="flex items-center gap-2 rounded-md bg-gray-50 p-2 dark:bg-gray-700/30">
|
||||
<input type="hidden" name="itemId" value={item.id} />
|
||||
<input type="text" name="text" value={item.text} required
|
||||
class="flex-1 rounded-md border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
{#if item.itemType === 'input'}
|
||||
<input type="text" name="unit" value={item.unit ?? ''} placeholder="Unit"
|
||||
class="w-16 rounded-md border border-gray-300 px-2 py-1 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||
{/if}
|
||||
<button type="submit" class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">Save</button>
|
||||
<button type="button" onclick={() => (editingItemId = null)} class="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400">Cancel</button>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="group flex items-center gap-2 rounded-md py-1">
|
||||
<!-- Move buttons -->
|
||||
<div class="flex flex-col">
|
||||
{#if idx > 0}
|
||||
<form method="POST" action="?/moveItem" use:enhance>
|
||||
<input type="hidden" name="itemId" value={item.id} />
|
||||
<input type="hidden" name="templateId" value={template.id} />
|
||||
<input type="hidden" name="direction" value="up" />
|
||||
<button type="submit" class="text-gray-300 hover:text-gray-600 dark:text-gray-600 dark:hover:text-gray-400" title="Move up">
|
||||
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="h-3 w-3"></div>
|
||||
{/if}
|
||||
{#if idx < template.items.length - 1}
|
||||
<form method="POST" action="?/moveItem" use:enhance>
|
||||
<input type="hidden" name="itemId" value={item.id} />
|
||||
<input type="hidden" name="templateId" value={template.id} />
|
||||
<input type="hidden" name="direction" value="down" />
|
||||
<button type="submit" class="text-gray-300 hover:text-gray-600 dark:text-gray-600 dark:hover:text-gray-400" title="Move down">
|
||||
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{:else}
|
||||
<div class="h-3 w-3"></div>
|
||||
{/if}
|
||||
</div>
|
||||
<span class="flex-1 text-sm text-gray-700 dark:text-gray-300">{item.text}</span>
|
||||
{#if item.itemType === 'input'}
|
||||
<span class="rounded bg-purple-100 px-1.5 py-0.5 text-xs text-purple-700 dark:bg-purple-900/40 dark:text-purple-300">
|
||||
input{#if item.unit}: {item.unit}{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<button type="button" onclick={() => (editingItemId = item.id)} class="hidden text-gray-400 hover:text-blue-600 group-hover:block dark:text-gray-500 dark:hover:text-blue-400" title="Edit">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
</button>
|
||||
<form method="POST" action="?/deleteItem" use:enhance>
|
||||
<input type="hidden" name="itemId" value={item.id} />
|
||||
<button type="submit" class="hidden text-gray-400 hover:text-red-500 group-hover:block dark:text-gray-500 dark:hover:text-red-400" title="Remove">
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user