@elytracms/forms 0.0.103 → 0.0.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/studio/inbox.d.ts
CHANGED
|
@@ -32,7 +32,11 @@ export interface FormsInboxConfig<V> {
|
|
|
32
32
|
export declare const INBOX_CAPABILITIES: CollectionViewCapabilities;
|
|
33
33
|
/** The issues-tray finding: how many submissions' mail did not (fully) go out. */
|
|
34
34
|
declare function failedDeliveryIssues(count: number): NavIssue[];
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* The inbox's deep link: `[]` the list, `[submissionId]` one submission. The segment arrives
|
|
37
|
+
* decoded by the router (the `parsePath` contract), so it IS the id (EC-768: decoding it again
|
|
38
|
+
* threw on `50%` and read `a%41` as `aA`).
|
|
39
|
+
*/
|
|
36
40
|
declare function parseInboxPath(segments: string[]): Record<string, string | null> | null;
|
|
37
41
|
/** The forms inbox as a rail entry (see the module header). */
|
|
38
42
|
export declare function formsInbox<V>(config: FormsInboxConfig<V>): import("@elytracms/studio/addon").SidebarViewNode;
|
package/dist/studio/inbox.js
CHANGED
|
@@ -192,12 +192,16 @@ function FormsInbox({ ctx, config, id }) {
|
|
|
192
192
|
function InboxMessage({ text }) {
|
|
193
193
|
return (_jsx(Text, { tone: "muted", className: "p-6", "data-testid": "forms-inbox-message", children: text }));
|
|
194
194
|
}
|
|
195
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* The inbox's deep link: `[]` the list, `[submissionId]` one submission. The segment arrives
|
|
197
|
+
* decoded by the router (the `parsePath` contract), so it IS the id (EC-768: decoding it again
|
|
198
|
+
* threw on `50%` and read `a%41` as `aA`).
|
|
199
|
+
*/
|
|
196
200
|
function parseInboxPath(segments) {
|
|
197
201
|
if (segments.length === 0)
|
|
198
202
|
return {};
|
|
199
203
|
if (segments.length === 1)
|
|
200
|
-
return { submissionId:
|
|
204
|
+
return { submissionId: segments[0] };
|
|
201
205
|
return null;
|
|
202
206
|
}
|
|
203
207
|
/** The forms inbox as a rail entry (see the module header). */
|
package/dist/studio/inbox.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../src/studio/inbox.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport { Inbox } from 'lucide-react'\nimport {\n PortableDetailEditor,\n CollectionViewSurface,\n nav,\n normalizeOkResult,\n recordListKey,\n useRecordList,\n useRailViewSource,\n} from '@elytracms/studio/addon'\nimport type {\n AppViewContext,\n CollectionViewCapabilities,\n NavIssue,\n NavPermission,\n NoticeLocation,\n OkResult,\n} from '@elytracms/studio/addon'\nimport { createCollectionRegistry, parseLocaleConfig, validateDocument } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CmsValidationIssue, DocumentRef, Locale, LocalizedString } from '@elytracms/core/cms-core'\nimport { Text } from '@elytracms/studio/ui'\nimport { DELIVERY_STATES, submissionRecordKind } from '../shared/kind'\nimport type { SubmissionRecord } from '../shared/kind'\nimport { deliveryNeedsAttention } from '../shared/delivery'\nimport { labelPayload } from '../shared/mail'\nimport type { FormDocument, FormSource } from '../shared/source'\nimport {\n formOf,\n inboxIssueCount,\n inboxSaveWrite,\n submissionLens,\n submissionSender,\n submissionToLensDocument,\n} from './lens'\nimport type { InboxForms } from './lens'\nimport { SubmissionAppendix } from './appendix'\n\n/**\n * EC-613 — the forms INBOX: the submission kind on the studio's portable list/edit surfaces\n * (AD-19, EC-616's scaffold for a source of one's own), one entry in the project's rail:\n *\n * ```tsx\n * // studio/studio.config.tsx\n * import { formsInbox } from '@elytracms/forms/studio'\n * import { contactForms } from '../cms/forms'\n * export default defineStudio({ sidebar: [ …, formsInbox({ source: contactForms }) ] })\n * ```\n *\n * The list shows every submission, newest first; one opens in the portable edit shell, where\n * the editor sets the status, the assignee and notes (the inbox lifecycle) and reads — below\n * the fields — what was sent, the files and the mail delivery per recipient, with a retry for\n * a failed mail. A failed or partly failed delivery is an issue on the row AND at the rail\n * foot, from any surface. Reads and writes go through the operations client\n * (`operations.records(submissionRecordKind)`) and the one query cache (AD-17).\n */\n\nexport interface FormsInboxConfig<V> {\n /** How the inbox reads your form documents (labels, titles) — the same declaration the site's delivery route reads. */\n source: FormSource<V>\n /** The route segment (default `submissions`). */\n id?: string\n /** The rail label (default `Submissions`). */\n label?: string\n /** OPTIONAL, default-open nav permission (AD-18). */\n visible?: NavPermission\n}\n\n/** The inbox's capabilities: no \"New\" (submissions come from the site), delete and bulk delete. */\nexport const INBOX_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false,\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\nconst LENS_LOCALE: Locale = 'en'\nconst LENS_LOCALE_CONFIG = parseLocaleConfig({ default: LENS_LOCALE, locales: [LENS_LOCALE] })\n\nconst DELIVERY_STATE_LABELS: Record<(typeof DELIVERY_STATES)[number], LocalizedString> = {\n pending: { en: 'Sending', de: 'Wird gesendet' },\n sent: { en: 'Sent', de: 'Gesendet' },\n partial: { en: 'Partly failed', de: 'Teilweise fehlgeschlagen' },\n failed: { en: 'Failed', de: 'Fehlgeschlagen' },\n skipped: { en: 'Not sent', de: 'Nicht gesendet' },\n}\n\n/**\n * EC-690 — where the finding is: the inbox, by the record kind it lists (the view declares\n * `recordKind`), so the studio's issues tray offers \"Go to\" the inbox and shows the finding to\n * whoever can open the inbox — not to every role.\n */\nconst INBOX_LOCATION: NoticeLocation = { kind: 'record', recordKind: submissionRecordKind.kind }\n\n/** The issues-tray finding: how many submissions' mail did not (fully) go out. */\nfunction failedDeliveryIssues(count: number): NavIssue[] {\n return count === 0\n ? []\n : [\n {\n key: 'forms-delivery-failed',\n message: `${count} form submission${count === 1 ? '' : 's'}: the mail was not delivered — open the inbox to retry.`,\n severity: 'error',\n location: INBOX_LOCATION,\n },\n ]\n}\n\n/** The always-on findings hook for the issues tray (it reads the inbox's own cached list). */\nfunction useInboxIssues({ projectId }: { projectId: string }): readonly NavIssue[] {\n const list = useRecordList(submissionRecordKind, projectId)\n const count = inboxIssueCount(list.data ?? [])\n return useMemo(() => failedDeliveryIssues(count), [count])\n}\n\n/** The form documents of the source's collection, through the operations client. */\nfunction useInboxForms<V>(ctx: AppViewContext, source: FormSource<V>): InboxForms<V> | undefined {\n const forms = ctx.query.useOperationQuery<FormDocument<V>[]>({\n queryKey: ['forms-inbox', 'forms', ctx.projectId, source.collection],\n loader: async (operations) => {\n const result = await operations.documents.list({ projectId: ctx.projectId, collection: source.collection })\n if (!result.ok) throw new Error(result.error.message)\n return result.value.map((record) => ({ id: record.document.id, values: record.document.values as V }))\n },\n })\n return useMemo(() => (forms.data ? { source, documents: forms.data } : undefined), [forms.data, source])\n}\n\n/** A failed delivery as a warning on the row's own Issues pane (never blocks a save). */\nfunction deliveryIssues(record: SubmissionRecord | undefined): CmsValidationIssue[] {\n if (!record || !deliveryNeedsAttention(record.deliveryState)) return []\n const failed = (record.deliveryLog ?? []).filter((entry) => !entry.ok)\n const last = failed.at(-1)\n return [\n {\n code: 'custom-validation',\n severity: 'warning',\n message: `The mail was not delivered${last?.to ? ` to ${last.to}` : ''}${last?.error ? `: ${last.error}` : ''}.`,\n path: ['deliveryState'],\n collectionId: submissionRecordKind.kind,\n documentId: record.id,\n },\n ]\n}\n\nfunction FormsInbox<V>({ ctx, config, id }: { ctx: AppViewContext; config: FormsInboxConfig<V>; id: string }) {\n const { projectId } = ctx\n const recordId = ctx.deepLink['submissionId'] ?? null\n const list = useRecordList(submissionRecordKind, projectId)\n const cache = ctx.query.useOperationCache()\n const forms = useInboxForms(ctx, config.source)\n const [retrying, setRetrying] = useState(false)\n const [retryError, setRetryError] = useState<string | null>(null)\n\n // Newest first — the inbox order.\n const records = useMemo(\n () => [...(list.data ?? [])].sort((a, b) => (a.submittedAt < b.submittedAt ? 1 : a.submittedAt > b.submittedAt ? -1 : 0)),\n [list.data],\n )\n const byId = useMemo(() => new Map(records.map((record) => [record.id, record])), [records])\n\n const schema = useMemo(() => submissionLens({ detail: recordId !== null }), [recordId])\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(() => records.map((record) => submissionToLensDocument(record, forms)), [records, forms])\n const get = useCallback(\n (ref: DocumentRef) => {\n const record = byId.get(ref.id)\n return record ? submissionToLensDocument(record, forms) : undefined\n },\n [byId, forms],\n )\n const titleOf = useCallback(\n (ref: DocumentRef) => {\n const record = byId.get(ref.id)\n return record ? submissionSender(forms, record) || record.form : ref.id\n },\n [byId, forms],\n )\n const validate = useCallback(\n (doc: CmsDocument) => [\n ...validateDocument(registry, doc, { localeConfig: LENS_LOCALE_CONFIG }),\n ...deliveryIssues(byId.get(doc.id)),\n ],\n [registry, byId],\n )\n\n /** Write through the operations client and place the answer in the one cached list. */\n const write = useCallback(\n async (input: { id: string } & Record<string, unknown>): Promise<OkResult> => {\n const operations = await ctx.operations()\n const result = await operations.records(submissionRecordKind).save({ projectId, ...input } as never)\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value as SubmissionRecord\n cache.patch<SubmissionRecord[]>(recordListKey(submissionRecordKind, projectId), (prev) => [\n ...(prev ?? []).filter((record) => record.id !== saved.id),\n saved,\n ])\n return { ok: true }\n },\n [ctx, projectId, cache],\n )\n\n const save = useCallback(\n async (doc: CmsDocument, base?: CmsDocument): Promise<OkResult> => {\n const stored = byId.get(doc.id)\n if (!stored) return { ok: false, message: `Submission “${doc.id}” is not loaded.` }\n // Against the record as the editor OPENED it (`base`, EC-616) — never the cache, which may\n // already hold another editor's change to a column this editor did not touch.\n const change = inboxSaveWrite(doc, base, stored, forms)\n // Nothing the editor owns changed: nothing to write (the displayed columns are never sent).\n if (Object.keys(change).length === 1) return { ok: true }\n return write(change)\n },\n [byId, write, forms],\n )\n const remove = useCallback(\n async (ref: DocumentRef): Promise<OkResult> => {\n const operations = await ctx.operations()\n const result = await operations.records(submissionRecordKind).remove({ projectId, id: ref.id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<SubmissionRecord[]>(recordListKey(submissionRecordKind, projectId), (prev) =>\n (prev ?? []).filter((record) => record.id !== ref.id),\n )\n return { ok: true }\n },\n [ctx, projectId, cache],\n )\n const create = useCallback((): DocumentRef => {\n throw new Error('Submissions come from the site — the inbox creates none.')\n }, [])\n\n const source = useRailViewSource({\n projectId,\n schema,\n capabilities: INBOX_CAPABILITIES,\n localeConfig: LENS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n detailPath: (submissionId: string) => `${id}/${submissionId}`,\n listPath: id,\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n\n const retry = useCallback(async () => {\n if (!recordId) return\n setRetrying(true)\n setRetryError(null)\n // Moving the row INTO `pending` is the retry: the deployment schedules the delivery.\n const result = await write({ id: recordId, deliveryState: 'pending' })\n if (!result.ok) setRetryError(result.message)\n setRetrying(false)\n }, [recordId, write])\n\n if (list.status === 'pending') return <InboxMessage text=\"Loading submissions…\" />\n if (list.status === 'error') return <InboxMessage text={`Submissions could not be loaded: ${list.error?.message ?? 'unknown error'}`} />\n if (recordId === null) return <CollectionViewSurface source={source} projectId={projectId} />\n\n const record = byId.get(recordId)\n const form = record ? formOf(forms, record) : undefined\n const fields = record ? labelPayload(record.payload, form ? (config.source.fields?.(form.values) ?? []) : []) : []\n return (\n <PortableDetailEditor\n source={source}\n projectId={projectId}\n documentId={recordId}\n formAppendix={\n record ? (\n <>\n <SubmissionAppendix\n record={record}\n fields={fields}\n stateLabel={DELIVERY_STATE_LABELS[record.deliveryState]}\n onRetry={() => void retry()}\n retrying={retrying}\n />\n {retryError ? <InboxMessage text={retryError} /> : null}\n </>\n ) : null\n }\n />\n )\n}\n\nfunction InboxMessage({ text }: { text: string }) {\n return (\n <Text tone=\"muted\" className=\"p-6\" data-testid=\"forms-inbox-message\">\n {text}\n </Text>\n )\n}\n\n/** The inbox's deep link: `[]` the list, `[submissionId]` one submission. */\nfunction parseInboxPath(segments: string[]): Record<string, string | null> | null {\n if (segments.length === 0) return {}\n if (segments.length === 1) return { submissionId: decodeURIComponent(segments[0]!) }\n return null\n}\n\n/** The forms inbox as a rail entry (see the module header). */\nexport function formsInbox<V>(config: FormsInboxConfig<V>) {\n const id = config.id ?? 'submissions'\n return nav.view({\n id,\n label: config.label ?? 'Submissions',\n description: 'Form submissions from the site, with their mail delivery.',\n icon: Inbox,\n parsePath: parseInboxPath,\n useIssues: useInboxIssues,\n // EC-690: the record kind this view lists — a submission-located finding opens here.\n recordKind: submissionRecordKind.kind,\n ...(config.visible ? { visible: config.visible } : {}),\n render: (ctx) => <FormsInbox ctx={ctx} config={config} id={id} />,\n })\n}\n\nexport { failedDeliveryIssues, parseInboxPath }\n"]}
|
|
1
|
+
{"version":3,"file":"inbox.js","sourceRoot":"","sources":["../../src/studio/inbox.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport { Inbox } from 'lucide-react'\nimport {\n PortableDetailEditor,\n CollectionViewSurface,\n nav,\n normalizeOkResult,\n recordListKey,\n useRecordList,\n useRailViewSource,\n} from '@elytracms/studio/addon'\nimport type {\n AppViewContext,\n CollectionViewCapabilities,\n NavIssue,\n NavPermission,\n NoticeLocation,\n OkResult,\n} from '@elytracms/studio/addon'\nimport { createCollectionRegistry, parseLocaleConfig, validateDocument } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CmsValidationIssue, DocumentRef, Locale, LocalizedString } from '@elytracms/core/cms-core'\nimport { Text } from '@elytracms/studio/ui'\nimport { DELIVERY_STATES, submissionRecordKind } from '../shared/kind'\nimport type { SubmissionRecord } from '../shared/kind'\nimport { deliveryNeedsAttention } from '../shared/delivery'\nimport { labelPayload } from '../shared/mail'\nimport type { FormDocument, FormSource } from '../shared/source'\nimport {\n formOf,\n inboxIssueCount,\n inboxSaveWrite,\n submissionLens,\n submissionSender,\n submissionToLensDocument,\n} from './lens'\nimport type { InboxForms } from './lens'\nimport { SubmissionAppendix } from './appendix'\n\n/**\n * EC-613 — the forms INBOX: the submission kind on the studio's portable list/edit surfaces\n * (AD-19, EC-616's scaffold for a source of one's own), one entry in the project's rail:\n *\n * ```tsx\n * // studio/studio.config.tsx\n * import { formsInbox } from '@elytracms/forms/studio'\n * import { contactForms } from '../cms/forms'\n * export default defineStudio({ sidebar: [ …, formsInbox({ source: contactForms }) ] })\n * ```\n *\n * The list shows every submission, newest first; one opens in the portable edit shell, where\n * the editor sets the status, the assignee and notes (the inbox lifecycle) and reads — below\n * the fields — what was sent, the files and the mail delivery per recipient, with a retry for\n * a failed mail. A failed or partly failed delivery is an issue on the row AND at the rail\n * foot, from any surface. Reads and writes go through the operations client\n * (`operations.records(submissionRecordKind)`) and the one query cache (AD-17).\n */\n\nexport interface FormsInboxConfig<V> {\n /** How the inbox reads your form documents (labels, titles) — the same declaration the site's delivery route reads. */\n source: FormSource<V>\n /** The route segment (default `submissions`). */\n id?: string\n /** The rail label (default `Submissions`). */\n label?: string\n /** OPTIONAL, default-open nav permission (AD-18). */\n visible?: NavPermission\n}\n\n/** The inbox's capabilities: no \"New\" (submissions come from the site), delete and bulk delete. */\nexport const INBOX_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n duplicate: false,\n remove: true,\n bulk: true,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false,\n}\n\nconst LENS_LOCALE: Locale = 'en'\nconst LENS_LOCALE_CONFIG = parseLocaleConfig({ default: LENS_LOCALE, locales: [LENS_LOCALE] })\n\nconst DELIVERY_STATE_LABELS: Record<(typeof DELIVERY_STATES)[number], LocalizedString> = {\n pending: { en: 'Sending', de: 'Wird gesendet' },\n sent: { en: 'Sent', de: 'Gesendet' },\n partial: { en: 'Partly failed', de: 'Teilweise fehlgeschlagen' },\n failed: { en: 'Failed', de: 'Fehlgeschlagen' },\n skipped: { en: 'Not sent', de: 'Nicht gesendet' },\n}\n\n/**\n * EC-690 — where the finding is: the inbox, by the record kind it lists (the view declares\n * `recordKind`), so the studio's issues tray offers \"Go to\" the inbox and shows the finding to\n * whoever can open the inbox — not to every role.\n */\nconst INBOX_LOCATION: NoticeLocation = { kind: 'record', recordKind: submissionRecordKind.kind }\n\n/** The issues-tray finding: how many submissions' mail did not (fully) go out. */\nfunction failedDeliveryIssues(count: number): NavIssue[] {\n return count === 0\n ? []\n : [\n {\n key: 'forms-delivery-failed',\n message: `${count} form submission${count === 1 ? '' : 's'}: the mail was not delivered — open the inbox to retry.`,\n severity: 'error',\n location: INBOX_LOCATION,\n },\n ]\n}\n\n/** The always-on findings hook for the issues tray (it reads the inbox's own cached list). */\nfunction useInboxIssues({ projectId }: { projectId: string }): readonly NavIssue[] {\n const list = useRecordList(submissionRecordKind, projectId)\n const count = inboxIssueCount(list.data ?? [])\n return useMemo(() => failedDeliveryIssues(count), [count])\n}\n\n/** The form documents of the source's collection, through the operations client. */\nfunction useInboxForms<V>(ctx: AppViewContext, source: FormSource<V>): InboxForms<V> | undefined {\n const forms = ctx.query.useOperationQuery<FormDocument<V>[]>({\n queryKey: ['forms-inbox', 'forms', ctx.projectId, source.collection],\n loader: async (operations) => {\n const result = await operations.documents.list({ projectId: ctx.projectId, collection: source.collection })\n if (!result.ok) throw new Error(result.error.message)\n return result.value.map((record) => ({ id: record.document.id, values: record.document.values as V }))\n },\n })\n return useMemo(() => (forms.data ? { source, documents: forms.data } : undefined), [forms.data, source])\n}\n\n/** A failed delivery as a warning on the row's own Issues pane (never blocks a save). */\nfunction deliveryIssues(record: SubmissionRecord | undefined): CmsValidationIssue[] {\n if (!record || !deliveryNeedsAttention(record.deliveryState)) return []\n const failed = (record.deliveryLog ?? []).filter((entry) => !entry.ok)\n const last = failed.at(-1)\n return [\n {\n code: 'custom-validation',\n severity: 'warning',\n message: `The mail was not delivered${last?.to ? ` to ${last.to}` : ''}${last?.error ? `: ${last.error}` : ''}.`,\n path: ['deliveryState'],\n collectionId: submissionRecordKind.kind,\n documentId: record.id,\n },\n ]\n}\n\nfunction FormsInbox<V>({ ctx, config, id }: { ctx: AppViewContext; config: FormsInboxConfig<V>; id: string }) {\n const { projectId } = ctx\n const recordId = ctx.deepLink['submissionId'] ?? null\n const list = useRecordList(submissionRecordKind, projectId)\n const cache = ctx.query.useOperationCache()\n const forms = useInboxForms(ctx, config.source)\n const [retrying, setRetrying] = useState(false)\n const [retryError, setRetryError] = useState<string | null>(null)\n\n // Newest first — the inbox order.\n const records = useMemo(\n () => [...(list.data ?? [])].sort((a, b) => (a.submittedAt < b.submittedAt ? 1 : a.submittedAt > b.submittedAt ? -1 : 0)),\n [list.data],\n )\n const byId = useMemo(() => new Map(records.map((record) => [record.id, record])), [records])\n\n const schema = useMemo(() => submissionLens({ detail: recordId !== null }), [recordId])\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(() => records.map((record) => submissionToLensDocument(record, forms)), [records, forms])\n const get = useCallback(\n (ref: DocumentRef) => {\n const record = byId.get(ref.id)\n return record ? submissionToLensDocument(record, forms) : undefined\n },\n [byId, forms],\n )\n const titleOf = useCallback(\n (ref: DocumentRef) => {\n const record = byId.get(ref.id)\n return record ? submissionSender(forms, record) || record.form : ref.id\n },\n [byId, forms],\n )\n const validate = useCallback(\n (doc: CmsDocument) => [\n ...validateDocument(registry, doc, { localeConfig: LENS_LOCALE_CONFIG }),\n ...deliveryIssues(byId.get(doc.id)),\n ],\n [registry, byId],\n )\n\n /** Write through the operations client and place the answer in the one cached list. */\n const write = useCallback(\n async (input: { id: string } & Record<string, unknown>): Promise<OkResult> => {\n const operations = await ctx.operations()\n const result = await operations.records(submissionRecordKind).save({ projectId, ...input } as never)\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value as SubmissionRecord\n cache.patch<SubmissionRecord[]>(recordListKey(submissionRecordKind, projectId), (prev) => [\n ...(prev ?? []).filter((record) => record.id !== saved.id),\n saved,\n ])\n return { ok: true }\n },\n [ctx, projectId, cache],\n )\n\n const save = useCallback(\n async (doc: CmsDocument, base?: CmsDocument): Promise<OkResult> => {\n const stored = byId.get(doc.id)\n if (!stored) return { ok: false, message: `Submission “${doc.id}” is not loaded.` }\n // Against the record as the editor OPENED it (`base`, EC-616) — never the cache, which may\n // already hold another editor's change to a column this editor did not touch.\n const change = inboxSaveWrite(doc, base, stored, forms)\n // Nothing the editor owns changed: nothing to write (the displayed columns are never sent).\n if (Object.keys(change).length === 1) return { ok: true }\n return write(change)\n },\n [byId, write, forms],\n )\n const remove = useCallback(\n async (ref: DocumentRef): Promise<OkResult> => {\n const operations = await ctx.operations()\n const result = await operations.records(submissionRecordKind).remove({ projectId, id: ref.id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<SubmissionRecord[]>(recordListKey(submissionRecordKind, projectId), (prev) =>\n (prev ?? []).filter((record) => record.id !== ref.id),\n )\n return { ok: true }\n },\n [ctx, projectId, cache],\n )\n const create = useCallback((): DocumentRef => {\n throw new Error('Submissions come from the site — the inbox creates none.')\n }, [])\n\n const source = useRailViewSource({\n projectId,\n schema,\n capabilities: INBOX_CAPABILITIES,\n localeConfig: LENS_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n detailPath: (submissionId: string) => `${id}/${submissionId}`,\n listPath: id,\n validate,\n create,\n save,\n remove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: normalizeOkResult,\n })\n\n const retry = useCallback(async () => {\n if (!recordId) return\n setRetrying(true)\n setRetryError(null)\n // Moving the row INTO `pending` is the retry: the deployment schedules the delivery.\n const result = await write({ id: recordId, deliveryState: 'pending' })\n if (!result.ok) setRetryError(result.message)\n setRetrying(false)\n }, [recordId, write])\n\n if (list.status === 'pending') return <InboxMessage text=\"Loading submissions…\" />\n if (list.status === 'error') return <InboxMessage text={`Submissions could not be loaded: ${list.error?.message ?? 'unknown error'}`} />\n if (recordId === null) return <CollectionViewSurface source={source} projectId={projectId} />\n\n const record = byId.get(recordId)\n const form = record ? formOf(forms, record) : undefined\n const fields = record ? labelPayload(record.payload, form ? (config.source.fields?.(form.values) ?? []) : []) : []\n return (\n <PortableDetailEditor\n source={source}\n projectId={projectId}\n documentId={recordId}\n formAppendix={\n record ? (\n <>\n <SubmissionAppendix\n record={record}\n fields={fields}\n stateLabel={DELIVERY_STATE_LABELS[record.deliveryState]}\n onRetry={() => void retry()}\n retrying={retrying}\n />\n {retryError ? <InboxMessage text={retryError} /> : null}\n </>\n ) : null\n }\n />\n )\n}\n\nfunction InboxMessage({ text }: { text: string }) {\n return (\n <Text tone=\"muted\" className=\"p-6\" data-testid=\"forms-inbox-message\">\n {text}\n </Text>\n )\n}\n\n/**\n * The inbox's deep link: `[]` the list, `[submissionId]` one submission. The segment arrives\n * decoded by the router (the `parsePath` contract), so it IS the id (EC-768: decoding it again\n * threw on `50%` and read `a%41` as `aA`).\n */\nfunction parseInboxPath(segments: string[]): Record<string, string | null> | null {\n if (segments.length === 0) return {}\n if (segments.length === 1) return { submissionId: segments[0]! }\n return null\n}\n\n/** The forms inbox as a rail entry (see the module header). */\nexport function formsInbox<V>(config: FormsInboxConfig<V>) {\n const id = config.id ?? 'submissions'\n return nav.view({\n id,\n label: config.label ?? 'Submissions',\n description: 'Form submissions from the site, with their mail delivery.',\n icon: Inbox,\n parsePath: parseInboxPath,\n useIssues: useInboxIssues,\n // EC-690: the record kind this view lists — a submission-located finding opens here.\n recordKind: submissionRecordKind.kind,\n ...(config.visible ? { visible: config.visible } : {}),\n render: (ctx) => <FormsInbox ctx={ctx} config={config} id={id} />,\n })\n}\n\nexport { failedDeliveryIssues, parseInboxPath }\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elytracms/forms",
|
|
3
3
|
"author": "finaldream <hi@finaldream.de>",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.105",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.18.0"
|
|
7
7
|
},
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"lucide-react": "^1.23.0",
|
|
30
30
|
"zod": "^4.4.3",
|
|
31
|
-
"@elytracms/core": "^0.0.
|
|
31
|
+
"@elytracms/core": "^0.0.105"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"convex": "^1.42.1",
|
|
35
35
|
"react": "^19.0.0",
|
|
36
|
-
"@elytracms/next": "^0.0.
|
|
37
|
-
"@elytracms/studio": "^0.0.
|
|
36
|
+
"@elytracms/next": "^0.0.105",
|
|
37
|
+
"@elytracms/studio": "^0.0.105"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@elytracms/next": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"react": "^19.2.7",
|
|
60
60
|
"react-dom": "^19.2.7",
|
|
61
61
|
"typescript": "^6.0.3",
|
|
62
|
-
"@elytracms/next": "^0.0.
|
|
63
|
-
"@elytracms/studio": "^0.0.
|
|
62
|
+
"@elytracms/next": "^0.0.105",
|
|
63
|
+
"@elytracms/studio": "^0.0.105"
|
|
64
64
|
},
|
|
65
65
|
"files": [
|
|
66
66
|
"dist",
|
|
@@ -195,21 +195,22 @@ describe('pnpm dev:ingest — every scenario against the real engine (convex-tes
|
|
|
195
195
|
expect(await t.run(async (ctx) => (await ctx.db.system.query('_storage').collect()).length)).toBe(0)
|
|
196
196
|
})
|
|
197
197
|
|
|
198
|
-
it('the EC-750 scenario:
|
|
198
|
+
it('the EC-750 scenario: 12/12 — a sibling forged by forms:save with B’s files is removed, B keeps its bytes and binding (the close’s expected lines)', async () => {
|
|
199
199
|
const core = await runner()
|
|
200
200
|
const t = await backend()
|
|
201
201
|
const lines: string[] = []
|
|
202
202
|
const scenario = core.INGEST_SCENARIOS.find((entry) => entry.name === 'ec750')!
|
|
203
203
|
const result = await core.runIngestScenario(scenario, port(core, t), { say: (line) => lines.push(line), wire, runId: 'r', now: () => T0 })
|
|
204
|
-
expect(result).toEqual({ ok: true, passed:
|
|
204
|
+
expect(result).toEqual({ ok: true, passed: 12, failed: [] })
|
|
205
205
|
expect(lines).toEqual(
|
|
206
206
|
expect.arrayContaining([
|
|
207
207
|
' ✓ B’s binding records B as its owner: true',
|
|
208
|
+
' ✓ …and B’s kind: true', // EC-762: the owner is the record's kind AND id
|
|
208
209
|
' ✓ A, created by forms:save, lists B’s file: true',
|
|
209
210
|
' ✓ removing A keeps B’s bytes: true',
|
|
210
211
|
' ✓ …and B’s binding: true',
|
|
211
212
|
' ✓ removing B deletes B’s file: false',
|
|
212
|
-
'✓ ec750:
|
|
213
|
+
'✓ ec750: 12/12 expectation(s) held',
|
|
213
214
|
]),
|
|
214
215
|
)
|
|
215
216
|
expect(await leftovers(t)).toEqual({ submissions: 0, bindings: 0, assets: 0 })
|
package/src/convex/index.ts
CHANGED
|
@@ -66,8 +66,8 @@ import type { FormDeliveryRequest } from '../shared/delivery'
|
|
|
66
66
|
* predictable file another intent stored can never be deleted through a rollback). The
|
|
67
67
|
* one deletion, `remove`, goes through the engine's `releaseIngestUploads`, which deletes
|
|
68
68
|
* only bytes a claim bound (EC-671) to THAT row (EC-750 — every binding records the row
|
|
69
|
-
* that claimed it): an id a visitor or an editor merely NAMES is
|
|
70
|
-
* another row's claimed file;
|
|
69
|
+
* that claimed it, by its kind and id, EC-762): an id a visitor or an editor merely NAMES is
|
|
70
|
+
* never deleted, even another row's claimed file;
|
|
71
71
|
* - **`deliver`** — the scheduled action per row: it asks the site's delivery route to send
|
|
72
72
|
* (`attemptFormDelivery`) and `recordDelivery` logs the answer per recipient and schedules
|
|
73
73
|
* the next attempt (`applyDeliveryOutcome`);
|
|
@@ -139,10 +139,11 @@ export const save = mutation({
|
|
|
139
139
|
|
|
140
140
|
/**
|
|
141
141
|
* Delete a submission AND the files it claimed (no claimed bytes outlive their row). EC-671 /
|
|
142
|
-
* EC-750: through `releaseIngestUploads`, asked for THIS row
|
|
143
|
-
* this row's claim bound, read by owner, never the row's
|
|
144
|
-
* put any id there, another row's file included, or clear
|
|
145
|
-
* file an editor has since made an asset of (the asset owns
|
|
142
|
+
* EC-750: through `releaseIngestUploads`, asked for THIS row — the submission kind and its id
|
|
143
|
+
* (EC-762) — it deletes only the files this row's claim bound, read by owner, never the row's
|
|
144
|
+
* `files` column (an editor's write can put any id there, another row's file included, or clear
|
|
145
|
+
* it) — and, EC-685, keeps a claimed file an editor has since made an asset of (the asset owns
|
|
146
|
+
* it now).
|
|
146
147
|
*/
|
|
147
148
|
export const remove = mutation({
|
|
148
149
|
args: { [kind.idKey]: v.string() },
|
|
@@ -150,7 +151,7 @@ export const remove = mutation({
|
|
|
150
151
|
await requireCapability(ctx, kind.access.write)
|
|
151
152
|
const doc = await findRecordDoc(ctx, kind, args[kind.idKey] as string)
|
|
152
153
|
if (!doc) return
|
|
153
|
-
await releaseIngestUploads(ctx, doc.recordId)
|
|
154
|
+
await releaseIngestUploads(ctx, kind, doc.recordId)
|
|
154
155
|
await ctx.db.delete(doc._id)
|
|
155
156
|
},
|
|
156
157
|
})
|
|
@@ -179,14 +180,14 @@ export const ingest = internalMutation({
|
|
|
179
180
|
if (!read.ok) return { ok: false, status: read.status, message: read.message, issues: read.issues }
|
|
180
181
|
|
|
181
182
|
// EC-578: the one id factory (UUID v4) — never a clock or a counter. EC-750: minted BEFORE
|
|
182
|
-
// the claim, which binds every file to this row
|
|
183
|
-
// retried request re-mints).
|
|
183
|
+
// the claim, which binds every file to this row — its kind and this id, EC-762 (a refused
|
|
184
|
+
// claim leaves the id unused; a retried request re-mints).
|
|
184
185
|
mintSubmissionUuid ??= createUuidIdFactory()
|
|
185
186
|
const id = `${kind.idPrefix}_${mintSubmissionUuid()}`
|
|
186
187
|
// EC-695: every upload claimed in ONE engine call, after every check above that can refuse —
|
|
187
188
|
// all bound, or none (a refused claim refuses the submission, and nothing was bound, so
|
|
188
189
|
// nothing is released: this handler never deletes bytes). Nothing after it refuses.
|
|
189
|
-
const claim = await claimIngestUploads(ctx, body.uploads ?? [], body.target, receivedAt, id)
|
|
190
|
+
const claim = await claimIngestUploads(ctx, body.uploads ?? [], body.target, receivedAt, kind, id)
|
|
190
191
|
if (!claim.ok) return claim
|
|
191
192
|
const files: SubmissionFile[] = []
|
|
192
193
|
const namesUsed = new Map<string, number>()
|
|
@@ -137,7 +137,10 @@ const bindings = (t: Backend) =>
|
|
|
137
137
|
t.run(async (ctx) =>
|
|
138
138
|
(await ctx.db.query('records').collect())
|
|
139
139
|
.filter((record) => record.kind === 'ingestBinding')
|
|
140
|
-
.map((record) =>
|
|
140
|
+
.map((record) => {
|
|
141
|
+
const data = record.data as { ownerKind?: string; owner?: string }
|
|
142
|
+
return { storageId: record.recordId, ownerKind: data.ownerKind, owner: data.owner }
|
|
143
|
+
}),
|
|
141
144
|
)
|
|
142
145
|
const deleteBlob = makeFunctionReference<'mutation', { storageKey: string }, unknown>('assets:deleteBlob')
|
|
143
146
|
|
|
@@ -155,10 +158,11 @@ async function promote(t: Backend, assetId: string, storageKey: string) {
|
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
describe('EC-750 probe — removing a submission deletes only the files THAT submission claimed', () => {
|
|
158
|
-
it('a claim binds each file to the submission that claimed it (the owner
|
|
161
|
+
it('a claim binds each file to the submission that claimed it (the owner — its kind and id — not the form)', async () => {
|
|
159
162
|
const t = await backend()
|
|
160
163
|
const b = await genuineSubmission(t)
|
|
161
|
-
|
|
164
|
+
// EC-762: the owner is the record's kind AND id (a record is both).
|
|
165
|
+
expect(await bindings(t)).toEqual([{ storageId: b.file, ownerKind: 'submission', owner: b.id }]) // base: owner undefined (the form key only)
|
|
162
166
|
})
|
|
163
167
|
|
|
164
168
|
it('Codex’s trace: A is created by forms:save carrying B’s files; removing A keeps B’s bytes and B’s binding', async () => {
|
|
@@ -171,7 +175,7 @@ describe('EC-750 probe — removing a submission deletes only the files THAT sub
|
|
|
171
175
|
expect(await row(t, 'submission_forged')).toBeUndefined()
|
|
172
176
|
// Measured on the base (0b7bb049), counterexample form: both lines below read false / [].
|
|
173
177
|
expect(await survives(t, b.file)).toBe(true) // base: false — A's removal deleted B's bytes
|
|
174
|
-
expect(await bindings(t)).toEqual([{ storageId: b.file, owner: b.id }]) // base: [] — B's binding stripped
|
|
178
|
+
expect(await bindings(t)).toEqual([{ storageId: b.file, ownerKind: 'submission', owner: b.id }]) // base: [] — B's binding stripped
|
|
175
179
|
// B still holds its file: its row is intact, and its own removal deletes it (below).
|
|
176
180
|
expect((await row(t, b.id))!.files!.map((file) => file.storageId)).toEqual([b.file])
|
|
177
181
|
})
|
|
@@ -221,7 +225,34 @@ describe('EC-750 probe — removing a submission deletes only the files THAT sub
|
|
|
221
225
|
await forgeSibling(t, 'submission_legacy', [{ field: 'cv', storageId: legacyFile, contentType: 'application/pdf', size: 300 }])
|
|
222
226
|
await editor(t).remove(PROJECT_ID, 'submission_legacy')
|
|
223
227
|
expect(await survives(t, legacyFile)).toBe(true) // base: false — any bound id the row named was deleted
|
|
224
|
-
expect(await bindings(t)).toEqual([{ storageId: legacyFile, owner: undefined }])
|
|
228
|
+
expect(await bindings(t)).toEqual([{ storageId: legacyFile, ownerKind: undefined, owner: undefined }])
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('EC-762: a KIND-LESS binding (its owner’s id alone, as EC-750 wrote it) is never deleted by its submission’s removal — residue, never loss', async () => {
|
|
232
|
+
const t = await backend()
|
|
233
|
+
const kindlessFile = await storeFile(t, 300, 'application/pdf')
|
|
234
|
+
const id = 'submission_kindless'
|
|
235
|
+
// The binding a claim wrote between EC-750 and EC-762: the owner's id in `key1`, no kind, no `key2`.
|
|
236
|
+
await t.run(async (ctx) => {
|
|
237
|
+
await ctx.db.insert('records', {
|
|
238
|
+
kind: 'ingestBinding',
|
|
239
|
+
recordId: kindlessFile,
|
|
240
|
+
data: { uploadId: 'upload_kindless', target: 'contact', owner: id, boundAt: T0 },
|
|
241
|
+
key1: id,
|
|
242
|
+
at: T0,
|
|
243
|
+
createdAt: T0,
|
|
244
|
+
updatedAt: T0,
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
await forgeSibling(t, id, [{ field: 'cv', storageId: kindlessFile, contentType: 'application/pdf', size: 300 }])
|
|
248
|
+
await editor(t).remove(PROJECT_ID, id)
|
|
249
|
+
expect(await row(t, id)).toBeUndefined()
|
|
250
|
+
// No owner-only fallback (it would re-open F51): the bytes stay, and the binding keeps holding them.
|
|
251
|
+
expect(await survives(t, kindlessFile)).toBe(true)
|
|
252
|
+
expect(await bindings(t)).toEqual([{ storageId: kindlessFile, ownerKind: undefined, owner: id }])
|
|
253
|
+
await promote(t, 'kindless-asset', kindlessFile)
|
|
254
|
+
expect(await asAdmin(t).mutation(deleteBlob, { storageKey: kindlessFile })).toEqual({ deleted: false, heldBy: 'ingest' })
|
|
255
|
+
expect(await survives(t, kindlessFile)).toBe(true)
|
|
225
256
|
})
|
|
226
257
|
|
|
227
258
|
it('assets:deleteBlob still keeps bytes a binding holds — after the forged A’s removal, B’s binding still protects f', async () => {
|