Add image uploads to components, fix sidebar count for disabled
Deploy to LXC / deploy (push) Successful in 19s
Deploy to LXC / deploy (push) Successful in 19s
- Image upload/delete on component detail page (same pattern as devices) - Images section with grid, hover-to-delete, 50MB client-side limit - Sidebar component count now excludes disabled components by joining componentInstances with components and filtering disabled=false Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import type { LayoutServerLoad } from './$types';
|
import type { LayoutServerLoad } from './$types';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/db/index.js';
|
import { db } from '$lib/server/db/index.js';
|
||||||
import { devices, componentInstances } from '$lib/server/db/schema.js';
|
import { devices, components, componentInstances } from '$lib/server/db/schema.js';
|
||||||
import { count, eq, or, and } from 'drizzle-orm';
|
import { count, eq, or, and } from 'drizzle-orm';
|
||||||
|
|
||||||
export const load: LayoutServerLoad = async ({ locals }) => {
|
export const load: LayoutServerLoad = async ({ locals }) => {
|
||||||
@@ -10,7 +10,11 @@ export const load: LayoutServerLoad = async ({ locals }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [deviceCount] = await db.select({ value: count() }).from(devices).where(eq(devices.disabled, false));
|
const [deviceCount] = await db.select({ value: count() }).from(devices).where(eq(devices.disabled, false));
|
||||||
const [componentCount] = await db.select({ value: count() }).from(componentInstances);
|
const [componentCount] = await db
|
||||||
|
.select({ value: count() })
|
||||||
|
.from(componentInstances)
|
||||||
|
.innerJoin(components, eq(componentInstances.componentId, components.id))
|
||||||
|
.where(eq(components.disabled, false));
|
||||||
const [repairCount] = await db
|
const [repairCount] = await db
|
||||||
.select({ value: count() })
|
.select({ value: count() })
|
||||||
.from(devices)
|
.from(devices)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { db } from '$lib/server/db/index.js';
|
|||||||
import { components, componentInstances, devices, locations, installationLog, componentImages, componentDocuments } from '$lib/server/db/schema.js';
|
import { components, componentInstances, devices, locations, installationLog, componentImages, componentDocuments } from '$lib/server/db/schema.js';
|
||||||
import { eq, desc } from 'drizzle-orm';
|
import { eq, desc } from 'drizzle-orm';
|
||||||
import { error, fail, redirect } from '@sveltejs/kit';
|
import { error, fail, redirect } from '@sveltejs/kit';
|
||||||
import { saveDocument, deleteFile } from '$lib/server/uploads.js';
|
import { saveImage, saveDocument, deleteFile } from '$lib/server/uploads.js';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
const [component] = await db
|
const [component] = await db
|
||||||
@@ -164,6 +164,42 @@ export const actions: Actions = {
|
|||||||
return { documentDeleted: true };
|
return { documentDeleted: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uploadImage: async ({ request, params }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const file = formData.get('image') as File;
|
||||||
|
if (!file || file.size === 0) return fail(400, { error: 'No file selected' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { filePath, thumbnailPath } = await saveImage(file, 'components');
|
||||||
|
const caption = formData.get('caption') as string;
|
||||||
|
|
||||||
|
await db.insert(componentImages).values({
|
||||||
|
componentId: params.id,
|
||||||
|
filePath,
|
||||||
|
thumbnailPath,
|
||||||
|
caption: caption || null
|
||||||
|
});
|
||||||
|
|
||||||
|
return { imageUploaded: true };
|
||||||
|
} catch (err) {
|
||||||
|
return fail(400, { error: `Upload failed: ${err instanceof Error ? err.message : 'Unknown error'}` });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteImage: async ({ request }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const imageId = formData.get('imageId') as string;
|
||||||
|
|
||||||
|
const [img] = await db.select().from(componentImages).where(eq(componentImages.id, imageId));
|
||||||
|
if (img) {
|
||||||
|
await deleteFile(img.filePath);
|
||||||
|
if (img.thumbnailPath) await deleteFile(img.thumbnailPath);
|
||||||
|
await db.delete(componentImages).where(eq(componentImages.id, imageId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { imageDeleted: true };
|
||||||
|
},
|
||||||
|
|
||||||
disable: async ({ params }) => {
|
disable: async ({ params }) => {
|
||||||
await db
|
await db
|
||||||
.update(components)
|
.update(components)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
let editingInstanceId = $state<string | null>(null);
|
let editingInstanceId = $state<string | null>(null);
|
||||||
let showAddInstances = $state(false);
|
let showAddInstances = $state(false);
|
||||||
|
let showUploadForm = $state(false);
|
||||||
let showDocForm = $state(false);
|
let showDocForm = $state(false);
|
||||||
let showDeleteConfirm = $state(false);
|
let showDeleteConfirm = $state(false);
|
||||||
|
|
||||||
@@ -85,6 +86,55 @@
|
|||||||
|
|
||||||
<div class="grid gap-6 lg:grid-cols-3">
|
<div class="grid gap-6 lg:grid-cols-3">
|
||||||
<div class="space-y-6 lg:col-span-2">
|
<div class="space-y-6 lg:col-span-2">
|
||||||
|
<!-- Images -->
|
||||||
|
<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-center justify-between">
|
||||||
|
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Images</h2>
|
||||||
|
<button type="button" onclick={() => (showUploadForm = !showUploadForm)}
|
||||||
|
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
|
||||||
|
{showUploadForm ? 'Cancel' : 'Upload'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showUploadForm}
|
||||||
|
<form method="POST" action="?/uploadImage" enctype="multipart/form-data" use:enhance class="mb-4 flex flex-wrap items-end gap-3">
|
||||||
|
<input type="file" name="image" accept="image/*" required
|
||||||
|
onchange={(e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
|
if (file && file.size > 50 * 1024 * 1024) {
|
||||||
|
alert('File too large. Maximum size is 50MB.');
|
||||||
|
(e.target as HTMLInputElement).value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="text-sm text-gray-600 dark:text-gray-400" />
|
||||||
|
<input type="text" name="caption" placeholder="Caption (optional)"
|
||||||
|
class="rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
|
||||||
|
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Upload</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if data.images.length === 0}
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">No images yet.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="grid gap-2 sm:grid-cols-3">
|
||||||
|
{#each data.images as img}
|
||||||
|
<div class="group relative overflow-hidden rounded-md">
|
||||||
|
<img src={img.filePath} alt={img.caption ?? c.title} class="h-32 w-full object-cover" />
|
||||||
|
<form method="POST" action="?/deleteImage" use:enhance
|
||||||
|
class="absolute top-1 right-1 hidden group-hover:block">
|
||||||
|
<input type="hidden" name="imageId" value={img.id} />
|
||||||
|
<button type="submit" class="rounded bg-red-600 p-1 text-xs text-white hover:bg-red-700" title="Delete">
|
||||||
|
<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="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Instances -->
|
<!-- Instances -->
|
||||||
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
|
<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-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user