--- /* --------------------------------------------------------------------------- * ResourceEditPanel — right-slide panel for create + edit. * * Rendered alongside ResourceListView when the URL carries ?edit= or * ?new=1. POSTs back to the same URL; the route handler in step 7 reads * _action (save | delete | ) and dispatches. * * Visible-when predicates are evaluated server-side against current values * (existing item or form defaults). Live-toggle while editing is Phase 2. * ------------------------------------------------------------------------- */ import FieldRenderer from './FieldRenderer.astro'; import PulseSubForm from '../embeds/PulseSubForm.astro'; import type { Field, FieldContext, Resource } from '../resource-types'; interface Props { resource: Resource; /** The item being edited, or null when creating. */ item: Record | null; /** Pre-validated form values from a failed prior submission (re-fill). */ formValues?: Record; errors?: Record; actingUserId: number; } const { resource, item, formValues, errors = {}, actingUserId } = Astro.props; const isCreate = item === null; // Review mode = the panel is showing an existing item AND either the resource // has no form, OR it has a summary that should be preferred over the form // (e.g. invitations are write-once: form is for create, summary is for edit). const isReviewMode = !isCreate && (resource.form === null || resource.summary !== undefined); const singular = resource.singularLabel.toLowerCase(); const title = isReviewMode ? `Review ${singular}` : isCreate ? `New ${singular}` : `Edit ${singular}`; // In review mode, the resource MUST have a summary function — that's what // fills the panel body when the form is suppressed. if (isReviewMode && !resource.summary) { throw new Error( `ResourceEditPanel: ${resource.key} is in review mode but has no summary — define resource.summary.`, ); } // And the create path needs a form. if (isCreate && !resource.form) { throw new Error( `ResourceEditPanel: cannot render create panel for ${resource.key} — form is null.`, ); } // Initial form values: prior failed submission > existing item > defaults const seedValues: Record = { ...(item ?? {}), ...(formValues ?? {}) }; const ctx: FieldContext = { formValues: seedValues, item, actingUserId, }; function resolveDefault(field: Field): unknown { if (field.defaultValue === undefined) return undefined; if (typeof field.defaultValue === 'function') { return (field.defaultValue as (c: FieldContext) => unknown)(ctx); } return field.defaultValue; } function valueFor(field: Field): unknown { if (field.key in seedValues) return seedValues[field.key]; return resolveDefault(field); } const visibleFields = resource.form ? resource.form.fields.filter((f) => !f.visibleWhen || f.visibleWhen(ctx)) : []; const embeds = resource.form?.embeds ?? []; // Review-mode summary const summaryEntries = isReviewMode && item ? resource.summary!(item) : []; // One-shot invite link surfaced after create/action — read from URL const inviteUrl = Astro.url.searchParams.get('invite_url'); // Build the close URL — drop edit/new but keep filter/q/page const closeUrl = (() => { const next = new URLSearchParams(Astro.url.searchParams); next.delete('edit'); next.delete('new'); const s = next.toString(); return s ? `${Astro.url.pathname}?${s}` : Astro.url.pathname; })(); // Actions visible for this item — only when editing an existing item const actions = isCreate ? [] : (resource.actions ?? []).filter( (a) => !a.visibleWhen || a.visibleWhen(item!), ); // Form action URL: keep the panel-state params so a re-render after a // validation failure stays on the same item. const formAction = Astro.url.pathname + Astro.url.search; ---