feat: add app edit page with pre-populated form
Add /apps/[id]/edit route that loads existing app data into the form, allowing users to update app properties. Adds edit pencil button to AppCard (visible on hover) and i18n keys for both EN and RU.
This commit is contained in:
@@ -68,11 +68,12 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<div
|
||||||
href={app.url}
|
role="link"
|
||||||
target="_blank"
|
tabindex="0"
|
||||||
rel="noopener noreferrer"
|
onclick={() => window.open(app.url, '_blank', 'noopener,noreferrer')}
|
||||||
class="card-hover group flex flex-col rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/50"
|
onkeydown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') window.open(app.url, '_blank', 'noopener,noreferrer'); }}
|
||||||
|
class="card-hover group flex cursor-pointer flex-col rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/50"
|
||||||
title={app.description ?? app.name}
|
title={app.description ?? app.name}
|
||||||
>
|
>
|
||||||
<div class="mb-3 flex items-start justify-between">
|
<div class="mb-3 flex items-start justify-between">
|
||||||
@@ -93,7 +94,29 @@
|
|||||||
<span class="text-xs font-bold">{app.name.charAt(0).toUpperCase()}</span>
|
<span class="text-xs font-bold">{app.name.charAt(0).toUpperCase()}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<AppHealthBadge status={currentStatus} />
|
<div class="flex items-center gap-1.5">
|
||||||
|
<a
|
||||||
|
href="/apps/{app.id}/edit"
|
||||||
|
onclick={(e: MouseEvent) => e.stopPropagation()}
|
||||||
|
class="rounded-md p-1 text-muted-foreground opacity-0 transition-all hover:bg-accent hover:text-foreground group-hover:opacity-100"
|
||||||
|
title={$t('app.edit')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-3.5 w-3.5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
||||||
|
<path d="m15 5 4 4" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<AppHealthBadge status={currentStatus} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="truncate text-sm font-semibold text-card-foreground transition-colors group-hover:text-primary">
|
<h3 class="truncate text-sm font-semibold text-card-foreground transition-colors group-hover:text-primary">
|
||||||
@@ -123,4 +146,4 @@
|
|||||||
{app.category}
|
{app.category}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</div>
|
||||||
|
|||||||
@@ -14,12 +14,13 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
form: SuperValidated<AppSchema>;
|
form: SuperValidated<AppSchema>;
|
||||||
action?: string;
|
action?: string;
|
||||||
|
mode?: 'create' | 'edit';
|
||||||
}
|
}
|
||||||
|
|
||||||
let { form: formData, action = '?/create' }: Props = $props();
|
let { form: formData, action = '?/create', mode = 'create' }: Props = $props();
|
||||||
|
|
||||||
const { form, errors, enhance, submitting } = superForm(formData, {
|
const { form, errors, enhance, submitting } = superForm(formData, {
|
||||||
resetForm: true
|
resetForm: mode === 'create'
|
||||||
});
|
});
|
||||||
|
|
||||||
let showAdvanced = $state(false);
|
let showAdvanced = $state(false);
|
||||||
@@ -383,7 +384,7 @@
|
|||||||
{#if $submitting}
|
{#if $submitting}
|
||||||
{$t('app.saving')}
|
{$t('app.saving')}
|
||||||
{:else}
|
{:else}
|
||||||
{$t('app.save')}
|
{mode === 'edit' ? $t('app.update') : $t('app.save')}
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -333,6 +333,10 @@
|
|||||||
"settings.bookmarklet_drag_hint": "Drag this to your bookmarks bar",
|
"settings.bookmarklet_drag_hint": "Drag this to your bookmarks bar",
|
||||||
"settings.bookmarklet_show_code": "Show bookmarklet code",
|
"settings.bookmarklet_show_code": "Show bookmarklet code",
|
||||||
|
|
||||||
|
"app.edit": "Edit",
|
||||||
|
"app.edit_title": "Edit App",
|
||||||
|
"app.update": "Update App",
|
||||||
|
|
||||||
"app.quick_add_title": "Quick Add App",
|
"app.quick_add_title": "Quick Add App",
|
||||||
"app.quick_add_description": "Review the details below and save to add this app to your launcher.",
|
"app.quick_add_description": "Review the details below and save to add this app to your launcher.",
|
||||||
"app.quick_add_success": "App added successfully!",
|
"app.quick_add_success": "App added successfully!",
|
||||||
|
|||||||
@@ -302,6 +302,10 @@
|
|||||||
"settings.bookmarklet_drag": "Добавить в Launcher",
|
"settings.bookmarklet_drag": "Добавить в Launcher",
|
||||||
"settings.bookmarklet_drag_hint": "Перетащите на панель закладок",
|
"settings.bookmarklet_drag_hint": "Перетащите на панель закладок",
|
||||||
"settings.bookmarklet_show_code": "Показать код букмарклета",
|
"settings.bookmarklet_show_code": "Показать код букмарклета",
|
||||||
|
"app.edit": "Редактировать",
|
||||||
|
"app.edit_title": "Редактирование приложения",
|
||||||
|
"app.update": "Обновить приложение",
|
||||||
|
|
||||||
"app.quick_add_title": "Быстрое добавление приложения",
|
"app.quick_add_title": "Быстрое добавление приложения",
|
||||||
"app.quick_add_description": "Проверьте данные ниже и сохраните, чтобы добавить приложение в лаунчер.",
|
"app.quick_add_description": "Проверьте данные ниже и сохраните, чтобы добавить приложение в лаунчер.",
|
||||||
"app.quick_add_success": "Приложение успешно добавлено!",
|
"app.quick_add_success": "Приложение успешно добавлено!",
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { Actions, PageServerLoad } from './$types.js';
|
||||||
|
import { superValidate, setError } from 'sveltekit-superforms';
|
||||||
|
import { zod } from '$lib/utils/zod-adapter.js';
|
||||||
|
import { error, fail, redirect } from '@sveltejs/kit';
|
||||||
|
import { requireAuth } from '$lib/server/middleware/authenticate.js';
|
||||||
|
import * as appService from '$lib/server/services/appService.js';
|
||||||
|
import { createAppSchema } from '$lib/utils/validators.js';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async (event) => {
|
||||||
|
requireAuth(event);
|
||||||
|
|
||||||
|
let app;
|
||||||
|
try {
|
||||||
|
app = await appService.findById(event.params.id);
|
||||||
|
} catch {
|
||||||
|
throw error(404, 'App not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = await superValidate(zod(createAppSchema));
|
||||||
|
|
||||||
|
// Pre-fill form with existing app data
|
||||||
|
form.data.name = app.name;
|
||||||
|
form.data.url = app.url;
|
||||||
|
form.data.icon = app.icon ?? undefined;
|
||||||
|
form.data.iconType = app.iconType as typeof form.data.iconType;
|
||||||
|
form.data.description = app.description ?? undefined;
|
||||||
|
form.data.category = app.category ?? undefined;
|
||||||
|
form.data.tags = app.tags ?? undefined;
|
||||||
|
form.data.healthcheckEnabled = app.healthcheckEnabled;
|
||||||
|
form.data.healthcheckInterval = app.healthcheckInterval;
|
||||||
|
form.data.healthcheckMethod = app.healthcheckMethod as typeof form.data.healthcheckMethod;
|
||||||
|
form.data.healthcheckExpectedStatus = app.healthcheckExpectedStatus;
|
||||||
|
form.data.healthcheckTimeout = app.healthcheckTimeout;
|
||||||
|
form.data.integrationType = app.integrationType ?? undefined;
|
||||||
|
form.data.integrationConfig = (app.integrationConfig as string) ?? undefined;
|
||||||
|
form.data.integrationEnabled = app.integrationEnabled;
|
||||||
|
|
||||||
|
return { form, app: { id: app.id, name: app.name } };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
update: async (event) => {
|
||||||
|
requireAuth(event);
|
||||||
|
|
||||||
|
const form = await superValidate(event.request, zod(createAppSchema));
|
||||||
|
|
||||||
|
if (!form.valid) {
|
||||||
|
return fail(400, { form });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await appService.update(event.params.id, form.data);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to update app';
|
||||||
|
return setError(form, '', message);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw redirect(303, '/apps');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types.js';
|
||||||
|
import AppForm from '$lib/components/app/AppForm.svelte';
|
||||||
|
|
||||||
|
let { data }: { data: PageData } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t('app.edit_title')} — {data.app.name}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="mx-auto max-w-3xl">
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-foreground">{$t('app.edit_title')}</h1>
|
||||||
|
<p class="mt-1 text-sm text-muted-foreground">{data.app.name}</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="/apps"
|
||||||
|
class="rounded-lg border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
>
|
||||||
|
{$t('common.cancel')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||||
|
<AppForm form={data.form} action="?/update" mode="edit" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user