@elytracms/studio 0.0.97 → 0.0.99
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/convex/assets.ts +16 -2
- package/convex/ingest-attempt.test.ts +161 -0
- package/convex/ingest.test.ts +269 -16
- package/convex/ingest.ts +238 -47
- package/convex/path-segment-refusal.test.ts +201 -0
- package/convex/prototype-key.test.ts +92 -0
- package/convex/records-column-transform.convex.test.ts +147 -0
- package/convex/records-zod-refinements.convex.test.ts +94 -0
- package/convex/schema.ts +1 -1
- package/dist/addon.d.ts +5 -3
- package/dist/addon.js +4 -3
- package/dist/addon.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +10 -3
- package/dist/components/app-views/default-modules.js +5 -1
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +15 -8
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/builder/reuse-context.d.ts +29 -1
- package/dist/components/builder/reuse-context.js +58 -12
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +5 -6
- package/dist/components/cms/cms-store.js +12 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/field-control.d.ts +12 -1
- package/dist/components/cms/field-control.js +31 -6
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/import-batch.d.ts +23 -0
- package/dist/components/cms/import-batch.js +43 -0
- package/dist/components/cms/import-batch.js.map +1 -0
- package/dist/components/cms/rich-text/editor.js +13 -10
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/value.d.ts +9 -0
- package/dist/components/cms/rich-text/value.js +13 -0
- package/dist/components/cms/rich-text/value.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +39 -3
- package/dist/components/cms/use-document-draft.js +46 -16
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +7 -0
- package/dist/components/cms/use-publish-controller.js +17 -3
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/versions-pane.js +8 -5
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/delivery-health-banner.d.ts +34 -16
- package/dist/components/delivery-health-banner.js +59 -27
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/issue-severity.d.ts +3 -2
- package/dist/components/issue-severity.js.map +1 -1
- package/dist/components/nav-issues-registry.d.ts +39 -24
- package/dist/components/nav-issues-registry.js +50 -21
- package/dist/components/nav-issues-registry.js.map +1 -1
- package/dist/components/nav-issues.d.ts +23 -50
- package/dist/components/nav-issues.js +31 -59
- package/dist/components/nav-issues.js.map +1 -1
- package/dist/components/notifications/notice-center.d.ts +61 -0
- package/dist/components/notifications/notice-center.js +119 -0
- package/dist/components/notifications/notice-center.js.map +1 -0
- package/dist/components/notifications/notice-model.d.ts +137 -0
- package/dist/components/notifications/notice-model.js +107 -0
- package/dist/components/notifications/notice-model.js.map +1 -0
- package/dist/components/notifications/outcome-notice.d.ts +16 -0
- package/dist/components/notifications/outcome-notice.js +34 -0
- package/dist/components/notifications/outcome-notice.js.map +1 -0
- package/dist/components/notifications/shell-findings.d.ts +36 -0
- package/dist/components/notifications/shell-findings.js +140 -0
- package/dist/components/notifications/shell-findings.js.map +1 -0
- package/dist/components/rail-view-source.js +5 -0
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/records/record-lens.js.map +1 -1
- package/dist/components/records/record-rail.d.ts +10 -2
- package/dist/components/records/record-rail.js +74 -4
- package/dist/components/records/record-rail.js.map +1 -1
- package/dist/components/section-nav.d.ts +15 -3
- package/dist/components/section-nav.js +39 -147
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +8 -3
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/use-narrow-viewport.d.ts +11 -0
- package/dist/components/use-narrow-viewport.js +25 -0
- package/dist/components/use-narrow-viewport.js.map +1 -0
- package/dist/components/users/role-policy-drift-issues.d.ts +14 -7
- package/dist/components/users/role-policy-drift-issues.js +17 -8
- package/dist/components/users/role-policy-drift-issues.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +4 -2
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +24 -3
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +22 -2
- package/dist/lib/i18n/messages.en.js +27 -4
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/notices/toast-store.d.ts +61 -0
- package/dist/lib/notices/toast-store.js +116 -0
- package/dist/lib/notices/toast-store.js.map +1 -0
- package/dist/lib/operations/cms-persistence.d.ts +32 -7
- package/dist/lib/operations/cms-persistence.js +511 -71
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +61 -30
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persona/labels.js +1 -0
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/ui/toast.d.ts +24 -16
- package/dist/ui/toast.js +23 -32
- package/dist/ui/toast.js.map +1 -1
- package/package.json +4 -3
|
@@ -11,6 +11,7 @@ import { useDocumentEditor } from './document-editor-context.js';
|
|
|
11
11
|
import { useDocumentVersionsQuery } from './use-document-versions.js';
|
|
12
12
|
import { useRestoreVersion } from './use-restore-version.js';
|
|
13
13
|
import { formatRelativeVersionTimestamp, formatVersionTimestamp } from './versions-model.js';
|
|
14
|
+
import { publishFailureToast } from './use-publish-controller.js';
|
|
14
15
|
/**
|
|
15
16
|
* EC-224 — the document-level **Versions** inspector pane. Re-homes the EC-180
|
|
16
17
|
* VersionsPanel at the document level (the Sanity model): the continuous draft
|
|
@@ -82,11 +83,13 @@ export function VersionsPane() {
|
|
|
82
83
|
await cache.invalidate(versionKey);
|
|
83
84
|
}
|
|
84
85
|
catch (error) {
|
|
85
|
-
// EC-585: a refused publish rejects
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
// EC-585: a refused publish rejects — caught here so it never escapes the click
|
|
87
|
+
// handler. EC-690: a REFUSAL is already the session's `persistError` (an error notice of
|
|
88
|
+
// the issues tray that stays until dismissed), so it is not toasted a second time; a
|
|
89
|
+
// failure nobody reported still is (the shared decision, `publishFailureToast`).
|
|
90
|
+
const message = publishFailureToast(error);
|
|
91
|
+
if (message !== null)
|
|
92
|
+
toast.error(message);
|
|
90
93
|
}
|
|
91
94
|
finally {
|
|
92
95
|
setBusy(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"versions-pane.js","sourceRoot":"","sources":["../../../src/components/cms/versions-pane.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { KeyboardEvent } from 'react'\nimport { Eye, GitCompareArrows, MoreHorizontal, RotateCcw } from 'lucide-react'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n StatusDot,\n Text,\n toast,\n} from '@elytracms/studio/ui'\nimport { documentsSemanticEqual } from '@elytracms/core/content'\nimport { useOperationCache } from '../../lib/query'\nimport { translate, useUiLocale } from '../../lib/i18n'\nimport { cn } from '../../lib/utils'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { useDocumentEditor } from './document-editor-context'\nimport { useDocumentVersionsQuery } from './use-document-versions'\nimport { useRestoreVersion } from './use-restore-version'\nimport { formatRelativeVersionTimestamp, formatVersionTimestamp } from './versions-model'\n\n/**\n * EC-224 — the document-level **Versions** inspector pane. Re-homes the EC-180\n * VersionsPanel at the document level (the Sanity model): the continuous draft\n * on top with **Publish**, then the recorded version history (the live pin\n * badged), each older version **restorable** into the draft.\n *\n * Publish routes through the same pointer op as the header control\n * (`cms.publishDocument`); restore loads a recorded snapshot as the new working\n * draft (the operation snapshots the current draft first, so a rollback is\n * itself undoable) and the open editor adopts it via `replaceDraft`. Unpublish\n * deliberately stays in the header — it routes through the EC-173\n * reference-aware confirmation, which this pane does not duplicate.\n *\n * EC-298: each row also opens the compare view (⤢) and, once comparing, doubles\n * as the navigator — clicking a row sets it as the base of the diff.\n */\nexport function VersionsPane() {\n const source = useCollectionView()\n const {\n docRef,\n draft,\n dirty,\n hasErrors,\n viewReadOnly,\n save,\n replaceDraft,\n compare,\n compareActive,\n startCompare,\n setCompareBase,\n } = useDocumentEditor()\n const [busy, setBusy] = useState(false)\n const published = source.publishing?.isPublished(docRef) ?? false\n const cache = useOperationCache()\n // EC-446 — the active studio locale + a render-time clock, so each row can show\n // a locale-aware RELATIVE timestamp (\"vor 3 Tagen\") with the absolute value on\n // hover. `now` is captured per render (good enough — the list re-renders on any\n // version mutation; a version's age doesn't need per-second reactivity).\n const locale = useUiLocale()\n const now = Date.now()\n\n // The recorded version history, read through the one shared query (EC-308/AD-17,\n // deduped with the compare view). The flush-before-read ordering lives in the\n // loader, so the listed history is authoritative regardless of which surface\n // triggered the change; it refreshes on any version-mutating command\n // (publish/unpublish/snapshot/restore/pin/delete — EC-322) from anywhere, but\n // NOT on a plain draft save (which appends no version). Editor draft/dirty/busy\n // stays local — it is not server state (principle 12).\n const { versionsQuery, versionKey } = useDocumentVersionsQuery(docRef)\n const view = versionsQuery.data ?? null\n // EC-325 — the shared restore sequence (flush → restore → replaceDraft →\n // invalidate). Its `restoreBusy` is OR-ed with the local publish `busy` below so\n // the pane keeps its original single-in-flight gate: while EITHER a publish or a\n // restore is round-tripping, all of the pane's actions disable together.\n const { restore: restoreVersion, busy: restoreBusy } = useRestoreVersion({\n source,\n docRef,\n versionKey,\n dirty,\n save,\n replaceDraft,\n })\n const anyBusy = busy || restoreBusy\n\n const liveSnapshot =\n view?.publishedVersion !== undefined\n ? view.versions.find((v) => v.version === view.publishedVersion)?.snapshot\n : undefined\n // SEMANTIC equality (EC-298) — the same engine the compare view uses, so the two\n // never contradict: a doc that only differs by EC-277 envelope drift reads as\n // \"up to date\", not \"unpublished changes\".\n const liveMatchesDraft =\n liveSnapshot !== undefined && documentsSemanticEqual(liveSnapshot, draft)\n const canPublish = !liveMatchesDraft\n\n const handlePublish = useCallback(async () => {\n setBusy(true)\n try {\n // Publish snapshots the SAVED draft — capture any unsaved edits first so\n // the published version is exactly what the editor shows. Await the\n // publish (it flushes + reconciles the pin, EC-225) before re-reading the\n // version list so the new version shows.\n if (dirty) save()\n await source.publishing?.publish(docRef)\n await cache.invalidate(versionKey)\n } catch (error) {\n // EC-585: a refused publish rejects (it is also on the persist banner) — say so\n // here too rather than let the rejection escape the click handler unhandled.\n toast.error(\n error instanceof Error\n ? translate('cms.editor.publishFailedReason', { error: error.message })\n : translate('cms.editor.publishFailed'),\n )\n } finally {\n setBusy(false)\n }\n }, [source, docRef, dirty, save, cache, versionKey])\n\n // Restore a recorded version into the working draft — the whole-document\n // restore (no scope) via the shared EC-325 helper, which flushes any unsaved\n // buffer FIRST so the operation's pre-restore snapshot records it (else the\n // rollback wouldn't be undoable — restore snapshots the persisted draft).\n const handleRestore = useCallback(\n (version: number) => restoreVersion(version),\n [restoreVersion],\n )\n\n return (\n <Stack gap=\"3\" className=\"p-3\">\n {canPublish ? (\n <Row gap=\"2\" align=\"center\" className=\"rounded-md border border-primary/40 bg-primary/5 px-2.5 py-2\">\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text size=\"sm\" weight=\"medium\">\n {translate('cms.editor.draftState')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {published\n ? translate('cms.version.unpublishedChanges')\n : translate('cms.version.notPublishedYet')}\n </Text>\n </Stack>\n <Spacer />\n <Button\n size=\"sm\"\n onClick={handlePublish}\n disabled={anyBusy || hasErrors || viewReadOnly}\n title={\n viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : hasErrors\n ? translate('cms.version.fixValidationToPublish')\n : undefined\n }\n >\n <Eye /> {translate('cms.editor.publish')}\n </Button>\n </Row>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.liveUpToDate')}\n </Text>\n )}\n\n {compareActive && compare ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.comparingHint', {\n base: compare.base,\n target:\n compare.target === 'draft'\n ? translate('cms.version.currentDraft')\n : `v${compare.target}`,\n })}\n </Text>\n ) : null}\n\n <Stack gap=\"1\">\n {view === null ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('common.loading')}\n </Text>\n ) : view.versions.length === 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.noVersionsYet')}\n </Text>\n ) : (\n view.versions.map((entry) => {\n const isLive = entry.version === view.publishedVersion\n const isBase = compareActive && compare?.base === entry.version\n const isTarget = compareActive && compare?.target === entry.version\n const selectable = compareActive && !isTarget\n return (\n <Row\n key={entry.version}\n gap=\"2\"\n align=\"center\"\n {...(selectable\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: () => setCompareBase(entry.version),\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n setCompareBase(entry.version)\n }\n },\n }\n : {})}\n className={cn(\n 'rounded-md border px-2.5 py-1.5',\n isBase ? 'border-primary bg-primary/5' : 'border-border/60',\n selectable && !isBase && 'cursor-pointer hover:bg-muted/60',\n )}\n >\n {/* EC-446 — the live/published version leads with a green STATUS\n DOT (hover/focus reveals \"Published\"); historical versions\n reserve the same footprint so the version codes stay aligned. */}\n {isLive ? (\n <StatusDot tone=\"success\" label={translate('cms.editor.publishedState')} />\n ) : (\n <span aria-hidden className=\"size-2 shrink-0\" />\n )}\n <Code tone={isLive ? 'primary' : 'muted'}>v{entry.version}</Code>\n {isBase ? (\n <Badge variant=\"outline\">{translate('cms.version.baseBadge')}</Badge>\n ) : isTarget ? (\n <Badge variant=\"outline\">{translate('cms.version.comparedBadge')}</Badge>\n ) : null}\n {entry.origin === 'assistant' ? (\n <Badge variant=\"outline\" title={translate('cms.version.aiAuthoredTitle')}>\n {translate('cms.version.aiAuthored')}\n </Badge>\n ) : null}\n {/* EC-446 — readable RELATIVE date; the full absolute timestamp\n on hover (`title`), replacing the old truncated stub. */}\n <Text\n size=\"xs\"\n tone=\"subtle\"\n className=\"truncate\"\n title={formatVersionTimestamp(entry.createdAt)}\n >\n {formatRelativeVersionTimestamp(entry.createdAt, now, locale)}\n </Text>\n <Spacer />\n {/* EC-446 — actions collapse into a per-row `⋯` overflow menu\n (mirrors the collection-table row menu). Suppressed in compare\n mode exactly as the buttons were, so the row stays a base picker. */}\n {compareActive ? null : (\n <VersionRowActions\n version={entry.version}\n isLive={isLive}\n restoreDisabled={anyBusy || viewReadOnly}\n restoreDisabledTitle={\n viewReadOnly ? translate('cms.version.switchToDraftToRestore') : undefined\n }\n onCompare={() => startCompare(entry.version)}\n onRestore={() => void handleRestore(entry.version)}\n />\n )}\n </Row>\n )\n })\n )}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * EC-446 — one version row's `⋯` overflow menu, mirroring the collection-table\n * row-actions menu (`RowActionsMenu`, built on the same `DropdownMenu`\n * primitive). Compare (⤢) is always offered; Restore (↺) is offered on every\n * version EXCEPT the live/published one (which is already the draft's baseline —\n * matching the pre-redesign behavior). The live version therefore shows a\n * Compare-only menu, never a bare/empty trigger.\n *\n * Restore keeps the pane's original gates: `restoreDisabled` (the single\n * in-flight gate `anyBusy` OR `viewReadOnly`) disables the item, and the\n * read-only hint rides its `title`.\n */\nfunction VersionRowActions({\n version,\n isLive,\n restoreDisabled,\n restoreDisabledTitle,\n onCompare,\n onRestore,\n}: {\n version: number\n isLive: boolean\n restoreDisabled: boolean\n restoreDisabledTitle: string | undefined\n onCompare: () => void\n onRestore: () => void\n}) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.version.rowActionsLabel', { version })}\n data-testid={`version-actions-${version}`}\n >\n <MoreHorizontal />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem\n onClick={onCompare}\n data-testid={`version-action-compare-${version}`}\n >\n <GitCompareArrows /> {translate('cms.version.compare')}\n </DropdownMenuItem>\n {isLive ? null : (\n <DropdownMenuItem\n onClick={onRestore}\n disabled={restoreDisabled}\n {...(restoreDisabledTitle ? { title: restoreDisabledTitle } : {})}\n data-testid={`version-action-restore-${version}`}\n >\n <RotateCcw /> {translate('cms.version.restore')}\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"versions-pane.js","sourceRoot":"","sources":["../../../src/components/cms/versions-pane.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { KeyboardEvent } from 'react'\nimport { Eye, GitCompareArrows, MoreHorizontal, RotateCcw } from 'lucide-react'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n StatusDot,\n Text,\n toast,\n} from '@elytracms/studio/ui'\nimport { documentsSemanticEqual } from '@elytracms/core/content'\nimport { useOperationCache } from '../../lib/query'\nimport { translate, useUiLocale } from '../../lib/i18n'\nimport { cn } from '../../lib/utils'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { useDocumentEditor } from './document-editor-context'\nimport { useDocumentVersionsQuery } from './use-document-versions'\nimport { useRestoreVersion } from './use-restore-version'\nimport { formatRelativeVersionTimestamp, formatVersionTimestamp } from './versions-model'\nimport { publishFailureToast } from './use-publish-controller'\n\n/**\n * EC-224 — the document-level **Versions** inspector pane. Re-homes the EC-180\n * VersionsPanel at the document level (the Sanity model): the continuous draft\n * on top with **Publish**, then the recorded version history (the live pin\n * badged), each older version **restorable** into the draft.\n *\n * Publish routes through the same pointer op as the header control\n * (`cms.publishDocument`); restore loads a recorded snapshot as the new working\n * draft (the operation snapshots the current draft first, so a rollback is\n * itself undoable) and the open editor adopts it via `replaceDraft`. Unpublish\n * deliberately stays in the header — it routes through the EC-173\n * reference-aware confirmation, which this pane does not duplicate.\n *\n * EC-298: each row also opens the compare view (⤢) and, once comparing, doubles\n * as the navigator — clicking a row sets it as the base of the diff.\n */\nexport function VersionsPane() {\n const source = useCollectionView()\n const {\n docRef,\n draft,\n dirty,\n hasErrors,\n viewReadOnly,\n save,\n replaceDraft,\n compare,\n compareActive,\n startCompare,\n setCompareBase,\n } = useDocumentEditor()\n const [busy, setBusy] = useState(false)\n const published = source.publishing?.isPublished(docRef) ?? false\n const cache = useOperationCache()\n // EC-446 — the active studio locale + a render-time clock, so each row can show\n // a locale-aware RELATIVE timestamp (\"vor 3 Tagen\") with the absolute value on\n // hover. `now` is captured per render (good enough — the list re-renders on any\n // version mutation; a version's age doesn't need per-second reactivity).\n const locale = useUiLocale()\n const now = Date.now()\n\n // The recorded version history, read through the one shared query (EC-308/AD-17,\n // deduped with the compare view). The flush-before-read ordering lives in the\n // loader, so the listed history is authoritative regardless of which surface\n // triggered the change; it refreshes on any version-mutating command\n // (publish/unpublish/snapshot/restore/pin/delete — EC-322) from anywhere, but\n // NOT on a plain draft save (which appends no version). Editor draft/dirty/busy\n // stays local — it is not server state (principle 12).\n const { versionsQuery, versionKey } = useDocumentVersionsQuery(docRef)\n const view = versionsQuery.data ?? null\n // EC-325 — the shared restore sequence (flush → restore → replaceDraft →\n // invalidate). Its `restoreBusy` is OR-ed with the local publish `busy` below so\n // the pane keeps its original single-in-flight gate: while EITHER a publish or a\n // restore is round-tripping, all of the pane's actions disable together.\n const { restore: restoreVersion, busy: restoreBusy } = useRestoreVersion({\n source,\n docRef,\n versionKey,\n dirty,\n save,\n replaceDraft,\n })\n const anyBusy = busy || restoreBusy\n\n const liveSnapshot =\n view?.publishedVersion !== undefined\n ? view.versions.find((v) => v.version === view.publishedVersion)?.snapshot\n : undefined\n // SEMANTIC equality (EC-298) — the same engine the compare view uses, so the two\n // never contradict: a doc that only differs by EC-277 envelope drift reads as\n // \"up to date\", not \"unpublished changes\".\n const liveMatchesDraft =\n liveSnapshot !== undefined && documentsSemanticEqual(liveSnapshot, draft)\n const canPublish = !liveMatchesDraft\n\n const handlePublish = useCallback(async () => {\n setBusy(true)\n try {\n // Publish snapshots the SAVED draft — capture any unsaved edits first so\n // the published version is exactly what the editor shows. Await the\n // publish (it flushes + reconciles the pin, EC-225) before re-reading the\n // version list so the new version shows.\n if (dirty) save()\n await source.publishing?.publish(docRef)\n await cache.invalidate(versionKey)\n } catch (error) {\n // EC-585: a refused publish rejects — caught here so it never escapes the click\n // handler. EC-690: a REFUSAL is already the session's `persistError` (an error notice of\n // the issues tray that stays until dismissed), so it is not toasted a second time; a\n // failure nobody reported still is (the shared decision, `publishFailureToast`).\n const message = publishFailureToast(error)\n if (message !== null) toast.error(message)\n } finally {\n setBusy(false)\n }\n }, [source, docRef, dirty, save, cache, versionKey])\n\n // Restore a recorded version into the working draft — the whole-document\n // restore (no scope) via the shared EC-325 helper, which flushes any unsaved\n // buffer FIRST so the operation's pre-restore snapshot records it (else the\n // rollback wouldn't be undoable — restore snapshots the persisted draft).\n const handleRestore = useCallback(\n (version: number) => restoreVersion(version),\n [restoreVersion],\n )\n\n return (\n <Stack gap=\"3\" className=\"p-3\">\n {canPublish ? (\n <Row gap=\"2\" align=\"center\" className=\"rounded-md border border-primary/40 bg-primary/5 px-2.5 py-2\">\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text size=\"sm\" weight=\"medium\">\n {translate('cms.editor.draftState')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {published\n ? translate('cms.version.unpublishedChanges')\n : translate('cms.version.notPublishedYet')}\n </Text>\n </Stack>\n <Spacer />\n <Button\n size=\"sm\"\n onClick={handlePublish}\n disabled={anyBusy || hasErrors || viewReadOnly}\n title={\n viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : hasErrors\n ? translate('cms.version.fixValidationToPublish')\n : undefined\n }\n >\n <Eye /> {translate('cms.editor.publish')}\n </Button>\n </Row>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.liveUpToDate')}\n </Text>\n )}\n\n {compareActive && compare ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.comparingHint', {\n base: compare.base,\n target:\n compare.target === 'draft'\n ? translate('cms.version.currentDraft')\n : `v${compare.target}`,\n })}\n </Text>\n ) : null}\n\n <Stack gap=\"1\">\n {view === null ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('common.loading')}\n </Text>\n ) : view.versions.length === 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.noVersionsYet')}\n </Text>\n ) : (\n view.versions.map((entry) => {\n const isLive = entry.version === view.publishedVersion\n const isBase = compareActive && compare?.base === entry.version\n const isTarget = compareActive && compare?.target === entry.version\n const selectable = compareActive && !isTarget\n return (\n <Row\n key={entry.version}\n gap=\"2\"\n align=\"center\"\n {...(selectable\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: () => setCompareBase(entry.version),\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n setCompareBase(entry.version)\n }\n },\n }\n : {})}\n className={cn(\n 'rounded-md border px-2.5 py-1.5',\n isBase ? 'border-primary bg-primary/5' : 'border-border/60',\n selectable && !isBase && 'cursor-pointer hover:bg-muted/60',\n )}\n >\n {/* EC-446 — the live/published version leads with a green STATUS\n DOT (hover/focus reveals \"Published\"); historical versions\n reserve the same footprint so the version codes stay aligned. */}\n {isLive ? (\n <StatusDot tone=\"success\" label={translate('cms.editor.publishedState')} />\n ) : (\n <span aria-hidden className=\"size-2 shrink-0\" />\n )}\n <Code tone={isLive ? 'primary' : 'muted'}>v{entry.version}</Code>\n {isBase ? (\n <Badge variant=\"outline\">{translate('cms.version.baseBadge')}</Badge>\n ) : isTarget ? (\n <Badge variant=\"outline\">{translate('cms.version.comparedBadge')}</Badge>\n ) : null}\n {entry.origin === 'assistant' ? (\n <Badge variant=\"outline\" title={translate('cms.version.aiAuthoredTitle')}>\n {translate('cms.version.aiAuthored')}\n </Badge>\n ) : null}\n {/* EC-446 — readable RELATIVE date; the full absolute timestamp\n on hover (`title`), replacing the old truncated stub. */}\n <Text\n size=\"xs\"\n tone=\"subtle\"\n className=\"truncate\"\n title={formatVersionTimestamp(entry.createdAt)}\n >\n {formatRelativeVersionTimestamp(entry.createdAt, now, locale)}\n </Text>\n <Spacer />\n {/* EC-446 — actions collapse into a per-row `⋯` overflow menu\n (mirrors the collection-table row menu). Suppressed in compare\n mode exactly as the buttons were, so the row stays a base picker. */}\n {compareActive ? null : (\n <VersionRowActions\n version={entry.version}\n isLive={isLive}\n restoreDisabled={anyBusy || viewReadOnly}\n restoreDisabledTitle={\n viewReadOnly ? translate('cms.version.switchToDraftToRestore') : undefined\n }\n onCompare={() => startCompare(entry.version)}\n onRestore={() => void handleRestore(entry.version)}\n />\n )}\n </Row>\n )\n })\n )}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * EC-446 — one version row's `⋯` overflow menu, mirroring the collection-table\n * row-actions menu (`RowActionsMenu`, built on the same `DropdownMenu`\n * primitive). Compare (⤢) is always offered; Restore (↺) is offered on every\n * version EXCEPT the live/published one (which is already the draft's baseline —\n * matching the pre-redesign behavior). The live version therefore shows a\n * Compare-only menu, never a bare/empty trigger.\n *\n * Restore keeps the pane's original gates: `restoreDisabled` (the single\n * in-flight gate `anyBusy` OR `viewReadOnly`) disables the item, and the\n * read-only hint rides its `title`.\n */\nfunction VersionRowActions({\n version,\n isLive,\n restoreDisabled,\n restoreDisabledTitle,\n onCompare,\n onRestore,\n}: {\n version: number\n isLive: boolean\n restoreDisabled: boolean\n restoreDisabledTitle: string | undefined\n onCompare: () => void\n onRestore: () => void\n}) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.version.rowActionsLabel', { version })}\n data-testid={`version-actions-${version}`}\n >\n <MoreHorizontal />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem\n onClick={onCompare}\n data-testid={`version-action-compare-${version}`}\n >\n <GitCompareArrows /> {translate('cms.version.compare')}\n </DropdownMenuItem>\n {isLive ? null : (\n <DropdownMenuItem\n onClick={onRestore}\n disabled={restoreDisabled}\n {...(restoreDisabledTitle ? { title: restoreDisabledTitle } : {})}\n data-testid={`version-action-restore-${version}`}\n >\n <RotateCcw /> {translate('cms.version.restore')}\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"]}
|
|
@@ -1,18 +1,36 @@
|
|
|
1
|
+
import type { DeliveryConfigStatus } from '@elytracms/core/persistence';
|
|
2
|
+
import type { FindingAction } from './notifications/notice-model.js';
|
|
3
|
+
import type { NavIssueItem } from './nav-issues.js';
|
|
4
|
+
/** A delivery-health finding: an engine finding, so it may carry a Retry. */
|
|
5
|
+
export type DeliveryHealthFinding = NavIssueItem & {
|
|
6
|
+
action?: FindingAction;
|
|
7
|
+
};
|
|
1
8
|
/**
|
|
2
|
-
* EC-
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* not apply — no host reads the in-browser adapter, so there is no emitter — and the
|
|
11
|
-
* banner never renders (the query is disabled, so it never has data). A failed
|
|
12
|
-
* read degrades silently — it must never block the studio — by treating the
|
|
13
|
-
* query's error branch the same as "nothing missing", matching the former raw
|
|
14
|
-
* client call's swallowed `.catch()`.
|
|
9
|
+
* The pure decision (EC-690): what instant-publish health reports, from the query's state.
|
|
10
|
+
*
|
|
11
|
+
* - opted out (`instantPublish: false` in `elytra.config.ts`) or not connected → nothing;
|
|
12
|
+
* - data with missing vars → ONE warning naming them, located at the deployment;
|
|
13
|
+
* - NO data and an error → one warning "could not be checked", with a Retry. A failed read
|
|
14
|
+
* used to be swallowed as "nothing missing" (a silent failure, AGENTS §4); a failed
|
|
15
|
+
* RE-read while earlier data is held reports from that data instead (critic point 7: the
|
|
16
|
+
* query does not retry on its own, so one transient error must not stick for the session).
|
|
15
17
|
*/
|
|
16
|
-
export declare function
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
export declare function deliveryHealthFindings(input: {
|
|
19
|
+
checked: boolean;
|
|
20
|
+
data: DeliveryConfigStatus | undefined;
|
|
21
|
+
error: unknown;
|
|
22
|
+
retry: () => void;
|
|
23
|
+
}): DeliveryHealthFinding[];
|
|
24
|
+
/**
|
|
25
|
+
* EC-268 → EC-690: the boot-up health check for instant publish (EC-146), as findings of the
|
|
26
|
+
* one issues tray (it used to be a banner above the rail with a per-session dismiss; the
|
|
27
|
+
* owner replaced the dismiss with the declared `instantPublish: false`, ruling (c)).
|
|
28
|
+
*
|
|
29
|
+
* When the studio is connected to a Convex deployment it reads the delivery config through
|
|
30
|
+
* the operations seam (EC-329 — `operations.delivery.config`, cached by `useOperationQuery`
|
|
31
|
+
* like every studio read, AD-17). Without this the pipeline fails SILENTLY — a publish
|
|
32
|
+
* reports success but `scheduleRevalidate` returns before scheduling (no
|
|
33
|
+
* `CANVAS_REVALIDATE_ENDPOINT`/`_SECRET` pair) and the live site never updates. In
|
|
34
|
+
* playground mode (no backend) instant publish does not apply, so the query is disabled.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useDeliveryHealthFindings(projectId: string): DeliveryHealthFinding[];
|
|
@@ -1,29 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useState } from 'react';
|
|
3
|
-
import { Rocket, X } from 'lucide-react';
|
|
4
|
-
import { Button, Row, Spacer, Text } from '@elytracms/studio/ui';
|
|
1
|
+
import { useMemo } from 'react';
|
|
5
2
|
import { translate } from '../lib/i18n/index.js';
|
|
6
3
|
import { resolveBackendConfig } from '../lib/persistence/backend-mode.js';
|
|
7
4
|
import { missingInstantPublishVars } from '../lib/delivery/instant-publish.js';
|
|
8
5
|
import { useOperationQuery } from '../lib/query/index.js';
|
|
6
|
+
import { PROJECT_CONFIG_FILE, configProjectFor } from '../lib/workspace/index.js';
|
|
7
|
+
/** The Convex settings the finding names (`INSTANT_PUBLISH_VARS`), for a never-linked location. */
|
|
8
|
+
const deploymentAt = (settings) => ({ kind: 'deployment', settings });
|
|
9
9
|
/**
|
|
10
|
-
* EC-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* not apply — no host reads the in-browser adapter, so there is no emitter — and the
|
|
19
|
-
* banner never renders (the query is disabled, so it never has data). A failed
|
|
20
|
-
* read degrades silently — it must never block the studio — by treating the
|
|
21
|
-
* query's error branch the same as "nothing missing", matching the former raw
|
|
22
|
-
* client call's swallowed `.catch()`.
|
|
10
|
+
* The pure decision (EC-690): what instant-publish health reports, from the query's state.
|
|
11
|
+
*
|
|
12
|
+
* - opted out (`instantPublish: false` in `elytra.config.ts`) or not connected → nothing;
|
|
13
|
+
* - data with missing vars → ONE warning naming them, located at the deployment;
|
|
14
|
+
* - NO data and an error → one warning "could not be checked", with a Retry. A failed read
|
|
15
|
+
* used to be swallowed as "nothing missing" (a silent failure, AGENTS §4); a failed
|
|
16
|
+
* RE-read while earlier data is held reports from that data instead (critic point 7: the
|
|
17
|
+
* query does not retry on its own, so one transient error must not stick for the session).
|
|
23
18
|
*/
|
|
24
|
-
export function
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
export function deliveryHealthFindings(input) {
|
|
20
|
+
if (!input.checked)
|
|
21
|
+
return [];
|
|
22
|
+
if (input.data) {
|
|
23
|
+
const missing = missingInstantPublishVars(input.data);
|
|
24
|
+
if (missing.length === 0)
|
|
25
|
+
return [];
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
key: 'instant-publish-missing',
|
|
29
|
+
severity: 'warning',
|
|
30
|
+
title: translate('shell.deliveryHealth.title'),
|
|
31
|
+
message: translate('shell.deliveryHealth.missing', { vars: missing.join(', '), file: PROJECT_CONFIG_FILE }),
|
|
32
|
+
location: deploymentAt(missing),
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
if (input.error === null || input.error === undefined)
|
|
37
|
+
return [];
|
|
38
|
+
const reason = input.error instanceof Error ? input.error.message : String(input.error);
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
key: 'instant-publish-unchecked',
|
|
42
|
+
severity: 'warning',
|
|
43
|
+
title: translate('shell.deliveryHealth.title'),
|
|
44
|
+
message: translate('shell.deliveryHealth.unchecked', { error: reason }),
|
|
45
|
+
action: { label: translate('common.retry'), run: input.retry },
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* EC-268 → EC-690: the boot-up health check for instant publish (EC-146), as findings of the
|
|
51
|
+
* one issues tray (it used to be a banner above the rail with a per-session dismiss; the
|
|
52
|
+
* owner replaced the dismiss with the declared `instantPublish: false`, ruling (c)).
|
|
53
|
+
*
|
|
54
|
+
* When the studio is connected to a Convex deployment it reads the delivery config through
|
|
55
|
+
* the operations seam (EC-329 — `operations.delivery.config`, cached by `useOperationQuery`
|
|
56
|
+
* like every studio read, AD-17). Without this the pipeline fails SILENTLY — a publish
|
|
57
|
+
* reports success but `scheduleRevalidate` returns before scheduling (no
|
|
58
|
+
* `CANVAS_REVALIDATE_ENDPOINT`/`_SECRET` pair) and the live site never updates. In
|
|
59
|
+
* playground mode (no backend) instant publish does not apply, so the query is disabled.
|
|
60
|
+
*/
|
|
61
|
+
export function useDeliveryHealthFindings(projectId) {
|
|
62
|
+
const checked = resolveBackendConfig().kind === 'convex' && configProjectFor(projectId)?.instantPublish !== false;
|
|
27
63
|
const query = useOperationQuery({
|
|
28
64
|
queryKey: ['delivery', 'config', projectId],
|
|
29
65
|
loader: async (operations) => {
|
|
@@ -32,13 +68,9 @@ export function DeliveryHealthBanner({ projectId }) {
|
|
|
32
68
|
throw new Error(result.error.message);
|
|
33
69
|
return result.value;
|
|
34
70
|
},
|
|
35
|
-
enabled,
|
|
71
|
+
enabled: checked,
|
|
36
72
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const missing = query.data ? missingInstantPublishVars(query.data) : [];
|
|
40
|
-
if (dismissed || missing.length === 0)
|
|
41
|
-
return null;
|
|
42
|
-
return (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-warning/30 bg-warning/10 px-4 py-1.5 text-warning sm:px-6", "data-testid": "delivery-health-banner", children: [_jsx(Rocket, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsxs(Text, { size: "xs", className: "text-warning", children: ["Instant publish isn\u2019t fully configured \u2014 ", missing.join(', '), " not set on this Convex deployment. Publishes succeed, but the live site won\u2019t update until you set them (see CONNECT.md) and re-push functions."] }), _jsx(Spacer, {}), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => setDismissed(true), "aria-label": translate('shell.deliveryHealth.dismiss'), title: translate('shell.deliveryHealth.dismiss'), children: _jsx(X, { "aria-hidden": true }) })] }));
|
|
73
|
+
const { data, error, refetch } = query;
|
|
74
|
+
return useMemo(() => deliveryHealthFindings({ checked, data, error, retry: () => void refetch() }), [checked, data, error, refetch]);
|
|
43
75
|
}
|
|
44
76
|
//# sourceMappingURL=delivery-health-banner.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delivery-health-banner.js","sourceRoot":"","sources":["../../src/components/delivery-health-banner.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"delivery-health-banner.js","sourceRoot":"","sources":["../../src/components/delivery-health-banner.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport type { DeliveryConfigStatus } from '@elytracms/core/persistence'\nimport { translate } from '../lib/i18n'\nimport { resolveBackendConfig } from '../lib/persistence/backend-mode'\nimport { missingInstantPublishVars } from '../lib/delivery/instant-publish'\nimport { useOperationQuery } from '../lib/query'\nimport { PROJECT_CONFIG_FILE, configProjectFor } from '../lib/workspace'\nimport type { FindingAction, NoticeLocation } from './notifications/notice-model'\nimport type { NavIssueItem } from './nav-issues'\n\n/** A delivery-health finding: an engine finding, so it may carry a Retry. */\nexport type DeliveryHealthFinding = NavIssueItem & { action?: FindingAction }\n\n/** The Convex settings the finding names (`INSTANT_PUBLISH_VARS`), for a never-linked location. */\nconst deploymentAt = (settings: readonly string[]): NoticeLocation => ({ kind: 'deployment', settings })\n\n/**\n * The pure decision (EC-690): what instant-publish health reports, from the query's state.\n *\n * - opted out (`instantPublish: false` in `elytra.config.ts`) or not connected → nothing;\n * - data with missing vars → ONE warning naming them, located at the deployment;\n * - NO data and an error → one warning \"could not be checked\", with a Retry. A failed read\n * used to be swallowed as \"nothing missing\" (a silent failure, AGENTS §4); a failed\n * RE-read while earlier data is held reports from that data instead (critic point 7: the\n * query does not retry on its own, so one transient error must not stick for the session).\n */\nexport function deliveryHealthFindings(input: {\n checked: boolean\n data: DeliveryConfigStatus | undefined\n error: unknown\n retry: () => void\n}): DeliveryHealthFinding[] {\n if (!input.checked) return []\n if (input.data) {\n const missing = missingInstantPublishVars(input.data)\n if (missing.length === 0) return []\n return [\n {\n key: 'instant-publish-missing',\n severity: 'warning',\n title: translate('shell.deliveryHealth.title'),\n message: translate('shell.deliveryHealth.missing', { vars: missing.join(', '), file: PROJECT_CONFIG_FILE }),\n location: deploymentAt(missing),\n },\n ]\n }\n if (input.error === null || input.error === undefined) return []\n const reason = input.error instanceof Error ? input.error.message : String(input.error)\n return [\n {\n key: 'instant-publish-unchecked',\n severity: 'warning',\n title: translate('shell.deliveryHealth.title'),\n message: translate('shell.deliveryHealth.unchecked', { error: reason }),\n action: { label: translate('common.retry'), run: input.retry },\n },\n ]\n}\n\n/**\n * EC-268 → EC-690: the boot-up health check for instant publish (EC-146), as findings of the\n * one issues tray (it used to be a banner above the rail with a per-session dismiss; the\n * owner replaced the dismiss with the declared `instantPublish: false`, ruling (c)).\n *\n * When the studio is connected to a Convex deployment it reads the delivery config through\n * the operations seam (EC-329 — `operations.delivery.config`, cached by `useOperationQuery`\n * like every studio read, AD-17). Without this the pipeline fails SILENTLY — a publish\n * reports success but `scheduleRevalidate` returns before scheduling (no\n * `CANVAS_REVALIDATE_ENDPOINT`/`_SECRET` pair) and the live site never updates. In\n * playground mode (no backend) instant publish does not apply, so the query is disabled.\n */\nexport function useDeliveryHealthFindings(projectId: string): DeliveryHealthFinding[] {\n const checked =\n resolveBackendConfig().kind === 'convex' && configProjectFor(projectId)?.instantPublish !== false\n\n const query = useOperationQuery<DeliveryConfigStatus>({\n queryKey: ['delivery', 'config', projectId],\n loader: async (operations) => {\n const result = await operations.delivery.config({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n },\n enabled: checked,\n })\n const { data, error, refetch } = query\n return useMemo(\n () => deliveryHealthFindings({ checked, data, error, retry: () => void refetch() }),\n [checked, data, error, refetch],\n )\n}\n"]}
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* EC-507 — the studio's ONE issue-severity treatment.
|
|
3
3
|
*
|
|
4
4
|
* Three surfaces render a finding by severity: the document Issues pane
|
|
5
|
-
* (`components/cms/issues-pane.tsx`, EC-283), the
|
|
6
|
-
* (`components/
|
|
5
|
+
* (`components/cms/issues-pane.tsx`, EC-283), the shell's issues tray
|
|
6
|
+
* (`components/notifications/notice-center.tsx`, EC-690 — it was the section-rail foot,
|
|
7
|
+
* EC-493) and the AI proposal card
|
|
7
8
|
* (`components/ai/proposal-issues.tsx`, EC-496). They agreed on the treatment but
|
|
8
9
|
* not on where it lived: EC-493 extracted `navIssueSeverityStyle`/`navIssueSeverityIcon`
|
|
9
10
|
* beside the rail, EC-496 imported those `nav`-prefixed cores into a surface with
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issue-severity.js","sourceRoot":"","sources":["../../src/components/issue-severity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"issue-severity.js","sourceRoot":"","sources":["../../src/components/issue-severity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAkCzD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAuB;IAMxD,OAAO,QAAQ,KAAK,OAAO;QACzB,CAAC,CAAC,EAAE,IAAI,EAAE,wCAAwC,EAAE,IAAI,EAAE,kBAAkB,EAAE;QAC9E,CAAC,CAAC,EAAE,IAAI,EAAE,gCAAgC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAA;AACtE,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,iBAAiB,CAAC,QAAuB;IACvD,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,CAAA;AAC3D,CAAC;AAED;;;;;GAKG;AACH,SAAS,OAAO,CAAC,KAAkC;IACjD,OAAO,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAA;AACnC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA8C;IAE9C,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAoB;IAEpB,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { CircleAlert, TriangleAlert } from 'lucide-react'\n\n/**\n * EC-507 — the studio's ONE issue-severity treatment.\n *\n * Three surfaces render a finding by severity: the document Issues pane\n * (`components/cms/issues-pane.tsx`, EC-283), the shell's issues tray\n * (`components/notifications/notice-center.tsx`, EC-690 — it was the section-rail foot,\n * EC-493) and the AI proposal card\n * (`components/ai/proposal-issues.tsx`, EC-496). They agreed on the treatment but\n * not on where it lived: EC-493 extracted `navIssueSeverityStyle`/`navIssueSeverityIcon`\n * beside the rail, EC-496 imported those `nav`-prefixed cores into a surface with\n * nothing to do with navigation, and the original stayed inlined in the pane — three\n * expressions of one rule, and no obvious home for the fourth surface to import from.\n * This module is that home. The `nav*` names are retired.\n *\n * Deliberately NOT here: the label/copy vocabulary. `cmsIssueLabel`, the message\n * catalog and `issueBodyDescriptor` are the EC-192 persona boundary — a separate\n * concern that happens to be rendered by the same cards. Presentation and copy stay\n * apart.\n *\n * Also not here: the `data-*` attributes each surface emits\n * (`data-nav-issue-severity`, `data-proposal-issue-severity`). Those are per-surface\n * test/inspection hooks, not part of the shared rule.\n */\n\n/**\n * The two values every contributing producer publishes — `CmsValidationIssue.severity`,\n * `CustomizationIssue.severity` and the project-graph `ValidationIssue.severity` are all\n * `'error' | 'warning'`. A surface carries the field across rather than re-deriving a\n * local notion of \"bad\".\n */\nexport type IssueSeverity = 'error' | 'warning'\n\n/**\n * The per-severity palette, as a pure mapping onto the shared token palette —\n * unit-testable without a DOM (the `statusDotToneClass` pattern).\n *\n * The values are the document Issues pane's, which had them first: destructive\n * border/fill/text for an error, warning border/fill/text for a warning. A surface\n * applies `card` to the finding's container and `text` to at least its glyph; whether\n * the tint also carries to the body copy is the surface's call (the pane keeps its\n * body `muted`, the rail and the proposal card tint the whole row), because that is\n * layout, not severity.\n */\nexport function issueSeverityStyle(severity: IssueSeverity): {\n /** Border + fill for the finding's card. */\n card: string\n /** Icon + message tone. */\n text: string\n} {\n return severity === 'error'\n ? { card: 'border-destructive/30 bg-destructive/5', text: 'text-destructive' }\n : { card: 'border-warning/30 bg-warning/5', text: 'text-warning' }\n}\n\n/** The glyph for a severity — `CircleAlert` for an error, `TriangleAlert` for a warning. */\nexport function issueSeverityIcon(severity: IssueSeverity) {\n return severity === 'error' ? CircleAlert : TriangleAlert\n}\n\n/**\n * **The rank rule: `error` outranks `warning`.** Stated once here because two\n * surfaces express it differently — the collapsed rail shows one aggregate glyph\n * ({@link highestIssueSeverity}), the proposal card shows every finding and sorts\n * them ({@link orderBySeverity}) — and a rule expressed twice drifts.\n */\nfunction isError(issue: { severity: IssueSeverity }): boolean {\n return issue.severity === 'error'\n}\n\n/**\n * The severity ONE aggregate glyph should report for a mixed list: **the highest\n * present**.\n *\n * An icons-only rail may over-state the mildest finding, but it must never\n * under-report a hard error down to an advisory — the reader has no second signal\n * there, and expanding immediately corrects any over-statement.\n *\n * An empty list has no severity to report; callers do not render the widget at all\n * in that case, and `'warning'` is the conservative answer for one that does.\n */\nexport function highestIssueSeverity(\n issues: readonly { severity: IssueSeverity }[],\n): IssueSeverity {\n return issues.some(isError) ? 'error' : 'warning'\n}\n\n/**\n * The same rank as ordering: **errors first**, warnings after, stable within each\n * severity.\n *\n * For a surface that shows every finding rather than one glyph. Where the card's\n * primary action commits the change and the reader scans top-down, the finding that\n * should give pause must not sit below one that should merely inform.\n */\nexport function orderBySeverity<T extends { severity: IssueSeverity }>(\n issues: readonly T[],\n): T[] {\n return [...issues.filter(isError), ...issues.filter((issue) => !isError(issue))]\n}\n"]}
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { IssueSeverity } from './issue-severity.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Finding, NoticeLocation } from './notifications/notice-model.js';
|
|
4
4
|
/**
|
|
5
|
-
* EC-616 — the
|
|
5
|
+
* EC-616 — the studio's CONTRIBUTION seam for findings (`useNavIssues`, ruling § 6b item 1);
|
|
6
|
+
* since EC-690 the registry of the ONE notification-and-issue surface: everything it holds
|
|
7
|
+
* is listed in the shell header's issues tray (the badge), beside the engine's own findings.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* show; on unmount they go. A rail item that must ALWAYS report (not only while its view
|
|
13
|
-
* is open) declares `useIssues` on its view (`nav.view({ useIssues })` /
|
|
14
|
-
* `nav.rail(kind, { useIssues })`) — the rail mounts that hook for it.
|
|
9
|
+
* Any component under the studio shell publishes findings with {@link useNavIssues}. While
|
|
10
|
+
* the publishing component is mounted its findings are listed; on unmount they go. A rail
|
|
11
|
+
* item that must ALWAYS report (not only while its view is open) declares `useIssues` on its
|
|
12
|
+
* view (`nav.view({ useIssues })` / `nav.rail(kind, { useIssues })`) — the shell mounts that
|
|
13
|
+
* hook for it.
|
|
15
14
|
*
|
|
16
15
|
* A finding is plain data: a stable `key` (unique within its source), the localized
|
|
17
|
-
* `message`,
|
|
16
|
+
* `message`, a `severity` (`'error' | 'warning'`, default `'warning'`), and since EC-690 an
|
|
17
|
+
* optional short `title` and a `location` (where the tray's "Go to" leads, or the file it names).
|
|
18
|
+
*
|
|
19
|
+
* EC-690 (critic point 1): every publisher has its OWN identity, assigned by the registry
|
|
20
|
+
* (`useId`), so two mounted publishers that name the same source both stay listed — one can
|
|
21
|
+
* never overwrite or retract the other's findings. The engine's sources (`config`, `routes`,
|
|
22
|
+
* …, and the `engine:` prefix) are reserved: a contributor using one is still listed, and the
|
|
23
|
+
* misuse is reported as a finding of its own.
|
|
18
24
|
*/
|
|
19
|
-
/** One finding a view or addon
|
|
25
|
+
/** One finding a view or addon publishes. */
|
|
20
26
|
export interface NavIssue {
|
|
21
27
|
/** Stable within its source (re-publishing the same key replaces, never duplicates). */
|
|
22
28
|
key: string;
|
|
@@ -24,30 +30,39 @@ export interface NavIssue {
|
|
|
24
30
|
message: string;
|
|
25
31
|
/** Default `'warning'`. */
|
|
26
32
|
severity?: IssueSeverity;
|
|
33
|
+
/** EC-690: a short localized title the tray leads the row with (e.g. "Invalid redirect"). */
|
|
34
|
+
title?: string;
|
|
35
|
+
/** EC-690: where the finding is — the tray's "Go to" (or the file it names). */
|
|
36
|
+
location?: NoticeLocation;
|
|
27
37
|
}
|
|
28
38
|
type Listener = () => void;
|
|
29
|
-
/** The shell-scoped store of contributed findings, keyed by
|
|
39
|
+
/** The shell-scoped store of contributed findings, keyed by PUBLISHER. */
|
|
30
40
|
export declare class NavIssuesRegistry {
|
|
31
|
-
private readonly
|
|
41
|
+
private readonly byPublisher;
|
|
32
42
|
private readonly listeners;
|
|
33
43
|
private snapshot;
|
|
34
44
|
subscribe: (listener: Listener) => (() => void);
|
|
35
|
-
getSnapshot: () => readonly
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
getSnapshot: () => readonly Finding[];
|
|
46
|
+
/**
|
|
47
|
+
* Publish (or replace) ONE publisher's findings under `source`; an empty list removes the
|
|
48
|
+
* publisher. `publisher` is the registry-assigned identity ({@link useNavIssues} passes its
|
|
49
|
+
* `useId`), so another publisher naming the same source is untouched.
|
|
50
|
+
*/
|
|
51
|
+
publish(publisher: string, source: string, issues: readonly NavIssue[]): void;
|
|
52
|
+
retract(publisher: string): void;
|
|
39
53
|
private recompute;
|
|
40
54
|
}
|
|
41
|
-
/** Mounted once by the studio project screen, around the shell (rail + content). */
|
|
55
|
+
/** Mounted once by the studio project screen, around the shell (header, rail + content). */
|
|
42
56
|
export declare function NavIssuesProvider({ children }: {
|
|
43
57
|
children: ReactNode;
|
|
44
58
|
}): import("react").JSX.Element;
|
|
45
59
|
/**
|
|
46
|
-
* Publish findings to the
|
|
47
|
-
* publisher (`'forms'`, `'analytics:beacon'`)
|
|
48
|
-
*
|
|
60
|
+
* Publish findings to the shell's issues tray while this component is mounted. `source`
|
|
61
|
+
* names the publisher's kind (`'forms'`, `'analytics:beacon'`) — it groups and orders, it is
|
|
62
|
+
* not an address: each mounted call is its own publisher. Pass the same `issues` array
|
|
63
|
+
* identity between renders (memoize it) or the tray re-renders on every render.
|
|
49
64
|
*/
|
|
50
65
|
export declare function useNavIssues(source: string, issues: readonly NavIssue[]): void;
|
|
51
|
-
/** The contributed findings, for the
|
|
52
|
-
export declare function useContributedNavIssues(): readonly
|
|
66
|
+
/** The contributed findings, for the issues tray. */
|
|
67
|
+
export declare function useContributedNavIssues(): readonly Finding[];
|
|
53
68
|
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
3
|
-
|
|
2
|
+
import { createContext, useContext, useEffect, useId, useMemo, useSyncExternalStore } from 'react';
|
|
3
|
+
import { translate } from '../lib/i18n/index.js';
|
|
4
|
+
import { isReservedFindingSource } from './notifications/notice-model.js';
|
|
5
|
+
/** The shell-scoped store of contributed findings, keyed by PUBLISHER. */
|
|
4
6
|
export class NavIssuesRegistry {
|
|
5
|
-
|
|
7
|
+
byPublisher = new Map();
|
|
6
8
|
listeners = new Set();
|
|
7
9
|
snapshot = [];
|
|
8
10
|
subscribe = (listener) => {
|
|
@@ -12,23 +14,48 @@ export class NavIssuesRegistry {
|
|
|
12
14
|
};
|
|
13
15
|
};
|
|
14
16
|
getSnapshot = () => this.snapshot;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Publish (or replace) ONE publisher's findings under `source`; an empty list removes the
|
|
19
|
+
* publisher. `publisher` is the registry-assigned identity ({@link useNavIssues} passes its
|
|
20
|
+
* `useId`), so another publisher naming the same source is untouched.
|
|
21
|
+
*/
|
|
22
|
+
publish(publisher, source, issues) {
|
|
23
|
+
if (issues.length === 0) {
|
|
24
|
+
if (!this.byPublisher.delete(publisher))
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this.byPublisher.set(publisher, { source, issues });
|
|
29
|
+
}
|
|
21
30
|
this.recompute();
|
|
22
31
|
}
|
|
23
|
-
retract(
|
|
24
|
-
if (this.
|
|
32
|
+
retract(publisher) {
|
|
33
|
+
if (this.byPublisher.delete(publisher))
|
|
25
34
|
this.recompute();
|
|
26
35
|
}
|
|
27
36
|
recompute() {
|
|
28
37
|
const items = [];
|
|
29
|
-
for (const [source, issues] of this.
|
|
38
|
+
for (const [publisher, { source, issues }] of this.byPublisher) {
|
|
30
39
|
for (const issue of issues) {
|
|
31
|
-
items.push({
|
|
40
|
+
items.push({
|
|
41
|
+
id: `${publisher}:${issue.key}`,
|
|
42
|
+
source,
|
|
43
|
+
key: issue.key,
|
|
44
|
+
message: issue.message,
|
|
45
|
+
severity: issue.severity ?? 'warning',
|
|
46
|
+
...(issue.title !== undefined ? { title: issue.title } : {}),
|
|
47
|
+
...(issue.location !== undefined ? { location: issue.location } : {}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (isReservedFindingSource(source)) {
|
|
51
|
+
items.push({
|
|
52
|
+
id: `${publisher}:reserved-source`,
|
|
53
|
+
source: 'engine:registry',
|
|
54
|
+
key: `reserved-source:${source}`,
|
|
55
|
+
message: translate('shell.notices.reservedSource', { source }),
|
|
56
|
+
severity: 'error',
|
|
57
|
+
location: { kind: 'config', declaredIn: 'studio' },
|
|
58
|
+
});
|
|
32
59
|
}
|
|
33
60
|
}
|
|
34
61
|
this.snapshot = items;
|
|
@@ -37,7 +64,7 @@ export class NavIssuesRegistry {
|
|
|
37
64
|
}
|
|
38
65
|
}
|
|
39
66
|
const NavIssuesContext = createContext(null);
|
|
40
|
-
/** Mounted once by the studio project screen, around the shell (rail + content). */
|
|
67
|
+
/** Mounted once by the studio project screen, around the shell (header, rail + content). */
|
|
41
68
|
export function NavIssuesProvider({ children }) {
|
|
42
69
|
const registry = useMemo(() => new NavIssuesRegistry(), []);
|
|
43
70
|
return _jsx(NavIssuesContext.Provider, { value: registry, children: children });
|
|
@@ -50,18 +77,20 @@ function useRegistry(caller) {
|
|
|
50
77
|
return registry;
|
|
51
78
|
}
|
|
52
79
|
/**
|
|
53
|
-
* Publish findings to the
|
|
54
|
-
* publisher (`'forms'`, `'analytics:beacon'`)
|
|
55
|
-
*
|
|
80
|
+
* Publish findings to the shell's issues tray while this component is mounted. `source`
|
|
81
|
+
* names the publisher's kind (`'forms'`, `'analytics:beacon'`) — it groups and orders, it is
|
|
82
|
+
* not an address: each mounted call is its own publisher. Pass the same `issues` array
|
|
83
|
+
* identity between renders (memoize it) or the tray re-renders on every render.
|
|
56
84
|
*/
|
|
57
85
|
export function useNavIssues(source, issues) {
|
|
58
86
|
const registry = useRegistry('useNavIssues');
|
|
87
|
+
const publisher = useId();
|
|
59
88
|
useEffect(() => {
|
|
60
|
-
registry.publish(source, issues);
|
|
61
|
-
}, [registry, source, issues]);
|
|
62
|
-
useEffect(() => () => registry.retract(
|
|
89
|
+
registry.publish(publisher, source, issues);
|
|
90
|
+
}, [registry, publisher, source, issues]);
|
|
91
|
+
useEffect(() => () => registry.retract(publisher), [registry, publisher]);
|
|
63
92
|
}
|
|
64
|
-
/** The contributed findings, for the
|
|
93
|
+
/** The contributed findings, for the issues tray. */
|
|
65
94
|
export function useContributedNavIssues() {
|
|
66
95
|
const registry = useRegistry('useContributedNavIssues');
|
|
67
96
|
return useSyncExternalStore(registry.subscribe, registry.getSnapshot, registry.getSnapshot);
|