@elytracms/studio 0.0.93 → 0.0.94
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/analytics-cost.test.ts +817 -0
- package/convex/auth.ts +5 -2
- package/convex/authority.ts +35 -11
- package/convex/contract-parity.test.ts +7 -0
- package/convex/delivery.ts +12 -9
- package/convex/guard.ts +9 -7
- package/convex/http-routes.test.ts +18 -8
- package/convex/http.ts +23 -17
- package/convex/ingest.test.ts +326 -0
- package/convex/ingest.ts +281 -0
- package/convex/invites.ts +13 -5
- package/convex/members.ts +10 -4
- package/convex/record-functions.ts +325 -0
- package/convex/records.test.ts +156 -0
- package/convex/redirect-migration.test.ts +205 -0
- package/convex/redirect-revalidate.test.ts +5 -1
- package/convex/redirects.ts +120 -79
- package/convex/role-policy.test.ts +315 -0
- package/convex/roles.ts +83 -0
- package/convex/schema.ts +30 -0
- package/convex/triggers.ts +21 -1
- package/dist/addon.d.ts +71 -0
- package/dist/addon.js +67 -0
- package/dist/addon.js.map +1 -0
- package/dist/components/account/account-settings.js +2 -2
- package/dist/components/account/account-settings.js.map +1 -1
- package/dist/components/account/credential-panel.js +2 -2
- package/dist/components/account/credential-panel.js.map +1 -1
- package/dist/components/account/self-account-schema.d.ts +6 -15
- package/dist/components/account/self-account-schema.js +52 -34
- package/dist/components/account/self-account-schema.js.map +1 -1
- package/dist/components/account/self-account-view-source.d.ts +1 -1
- package/dist/components/account/self-account-view-source.js +6 -4
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/ai/ai-chat-panel-lazy.js +1 -1
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
- package/dist/components/ai/ai-chat-panel.js +6 -6
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/ai-cms-panel.js +2 -2
- package/dist/components/ai/ai-cms-panel.js.map +1 -1
- package/dist/components/ai/ai-panel-toggle.js +4 -4
- package/dist/components/ai/ai-panel-toggle.js.map +1 -1
- package/dist/components/ai/chat-ui.js +2 -2
- package/dist/components/ai/chat-ui.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +2 -2
- package/dist/components/ai/proposal-card.js +2 -2
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-issues.js +2 -2
- package/dist/components/ai/proposal-issues.js.map +1 -1
- package/dist/components/ai/translate-document-action.js +2 -2
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/ai/translate-document-dialog.js +6 -6
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/ai/translate-skipped.d.ts +1 -1
- package/dist/components/ai/translate-skipped.js +2 -2
- package/dist/components/ai/translate-skipped.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +50 -2
- package/dist/components/app-views/default-modules.js +65 -20
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/app-views/module.d.ts +53 -3
- package/dist/components/app-views/module.js +14 -7
- package/dist/components/app-views/module.js.map +1 -1
- package/dist/components/app-views/sidebar.js +1 -1
- package/dist/components/app-views/sidebar.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +11 -2
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/assets/asset-detail.js +2 -2
- package/dist/components/assets/asset-detail.js.map +1 -1
- package/dist/components/assets/asset-grid.js +2 -2
- package/dist/components/assets/asset-grid.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +3 -3
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-collection-view-source.d.ts +2 -2
- package/dist/components/assets/assets-collection-view-source.js.map +1 -1
- package/dist/components/assets/assets-module.js +1 -1
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +2 -2
- package/dist/components/assets/assets-store.js +6 -6
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/assets/assets-view-preference.js +1 -1
- package/dist/components/assets/assets-view-preference.js.map +1 -1
- package/dist/components/assets/replace-file.js +1 -1
- package/dist/components/assets/replace-file.js.map +1 -1
- package/dist/components/auth-context.d.ts +1 -1
- package/dist/components/auth-context.js +6 -6
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +1 -1
- package/dist/components/builder/blocks-model.js +3 -3
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +1 -1
- package/dist/components/builder/blocks-panel.js +4 -4
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/builder-store.js +2 -2
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-breakpoints.js +1 -1
- package/dist/components/builder/canvas-breakpoints.js.map +1 -1
- package/dist/components/builder/canvas-dnd.d.ts +2 -2
- package/dist/components/builder/canvas-dnd.js +1 -1
- package/dist/components/builder/canvas-dnd.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.d.ts +1 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/canvas-overlay-geometry.d.ts +1 -1
- package/dist/components/builder/canvas-overlay-geometry.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +1 -1
- package/dist/components/builder/canvas-render-context.js +4 -4
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-theme-notice.js +2 -2
- package/dist/components/builder/canvas-theme-notice.js.map +1 -1
- package/dist/components/builder/canvas.js +3 -3
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/clipboard-actions.d.ts +2 -2
- package/dist/components/builder/clipboard-actions.js +4 -4
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
- package/dist/components/builder/composition-field-editor.d.ts +1 -1
- package/dist/components/builder/composition-field-editor.js +3 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/drag-session.d.ts +1 -1
- package/dist/components/builder/drag-session.js.map +1 -1
- package/dist/components/builder/fixture-thumbnail.js +1 -1
- package/dist/components/builder/fixture-thumbnail.js.map +1 -1
- package/dist/components/builder/insert-context.d.ts +1 -1
- package/dist/components/builder/insert-context.js +3 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/layout-inspector.js +1 -1
- package/dist/components/builder/layout-inspector.js.map +1 -1
- package/dist/components/builder/node-title.js +1 -1
- package/dist/components/builder/node-title.js.map +1 -1
- package/dist/components/builder/page-tree.d.ts +1 -1
- package/dist/components/builder/page-tree.js +5 -5
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/placement-refusal.js +3 -3
- package/dist/components/builder/placement-refusal.js.map +1 -1
- package/dist/components/builder/property-inspector.js +4 -4
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +4 -4
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.js +1 -1
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/builder/rich-text-toolbar.js +1 -1
- package/dist/components/builder/rich-text-toolbar.js.map +1 -1
- package/dist/components/builder/run-inspector.js +1 -1
- package/dist/components/builder/run-inspector.js.map +1 -1
- package/dist/components/builder/selection-overlay.js +3 -3
- package/dist/components/builder/selection-overlay.js.map +1 -1
- package/dist/components/builder/spacing-handles.d.ts +2 -2
- package/dist/components/builder/spacing-handles.js +3 -3
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.js +2 -2
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker.js +2 -2
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/bulk-actions.d.ts +1 -1
- package/dist/components/cms/bulk-actions.js +2 -2
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/bulk-bar.d.ts +2 -2
- package/dist/components/cms/bulk-bar.js +2 -2
- package/dist/components/cms/bulk-bar.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +1 -1
- package/dist/components/cms/cell-edit.js +1 -1
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cell.d.ts +1 -1
- package/dist/components/cms/cell.js +3 -3
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-index.js +2 -2
- package/dist/components/cms/cms-index.js.map +1 -1
- package/dist/components/cms/cms-labels.js +2 -2
- package/dist/components/cms/cms-labels.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +2 -2
- package/dist/components/cms/cms-store.js +10 -10
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -7
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.d.ts +5 -2
- package/dist/components/cms/detail-editor.js +20 -14
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.js +1 -1
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/doc-state.js +1 -1
- package/dist/components/cms/doc-state.js.map +1 -1
- package/dist/components/cms/document-diff-view.js +5 -5
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/document-editor-context.js +1 -1
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/document-staging.js +1 -1
- package/dist/components/cms/document-staging.js.map +1 -1
- package/dist/components/cms/document-url-path.js +2 -2
- package/dist/components/cms/document-url-path.js.map +1 -1
- package/dist/components/cms/duplicate-document.d.ts +1 -1
- package/dist/components/cms/duplicate-document.js.map +1 -1
- package/dist/components/cms/editable-cell.js +3 -3
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-collaboration.d.ts +1 -1
- package/dist/components/cms/field-collaboration.js +1 -1
- package/dist/components/cms/field-collaboration.js.map +1 -1
- package/dist/components/cms/field-connection.d.ts +1 -1
- package/dist/components/cms/field-connection.js +2 -2
- package/dist/components/cms/field-connection.js.map +1 -1
- package/dist/components/cms/field-control-lib.js +1 -1
- package/dist/components/cms/field-control-lib.js.map +1 -1
- package/dist/components/cms/field-control.js +4 -4
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/field-renderer.d.ts +1 -1
- package/dist/components/cms/field-renderer.js +2 -2
- package/dist/components/cms/field-renderer.js.map +1 -1
- package/dist/components/cms/filter-builder.d.ts +1 -1
- package/dist/components/cms/filter-builder.js +3 -3
- package/dist/components/cms/filter-builder.js.map +1 -1
- package/dist/components/cms/gate-validation.js +1 -1
- package/dist/components/cms/gate-validation.js.map +1 -1
- package/dist/components/cms/import-export.js +2 -2
- package/dist/components/cms/import-export.js.map +1 -1
- package/dist/components/cms/issue-copy.js +1 -1
- package/dist/components/cms/issue-copy.js.map +1 -1
- package/dist/components/cms/issues-pane.js +3 -3
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/object-array-field.js +2 -2
- package/dist/components/cms/object-array-field.js.map +1 -1
- package/dist/components/cms/prop-fields.js +2 -2
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/reference-confirm.d.ts +1 -1
- package/dist/components/cms/reference-confirm.js +2 -2
- package/dist/components/cms/reference-confirm.js.map +1 -1
- package/dist/components/cms/relation-picker.js +2 -2
- package/dist/components/cms/relation-picker.js.map +1 -1
- package/dist/components/cms/rich-text/block-style.js +1 -1
- package/dist/components/cms/rich-text/block-style.js.map +1 -1
- package/dist/components/cms/rich-text/component-embed.js +4 -4
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.js +3 -3
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-picker.js +3 -3
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.js +3 -3
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.js +1 -1
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/font-size-control.d.ts +1 -1
- package/dist/components/cms/rich-text/font-size-control.js +2 -2
- package/dist/components/cms/rich-text/font-size-control.js.map +1 -1
- package/dist/components/cms/rich-text/lazy.js +1 -1
- package/dist/components/cms/rich-text/lazy.js.map +1 -1
- package/dist/components/cms/rich-text/link-control.js +2 -2
- package/dist/components/cms/rich-text/link-control.js.map +1 -1
- package/dist/components/cms/rich-text/slash-command.js +3 -3
- package/dist/components/cms/rich-text/slash-command.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.js +1 -1
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/rich-text/unknown-node.js +2 -2
- package/dist/components/cms/rich-text/unknown-node.js.map +1 -1
- package/dist/components/cms/rich-text-preview.js +2 -2
- package/dist/components/cms/rich-text-preview.js.map +1 -1
- package/dist/components/cms/row-actions.d.ts +1 -1
- package/dist/components/cms/row-actions.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.d.ts +1 -1
- package/dist/components/cms/scalar-value-editor.js +2 -2
- package/dist/components/cms/scalar-value-editor.js.map +1 -1
- package/dist/components/cms/use-discard-controller.d.ts +1 -1
- package/dist/components/cms/use-discard-controller.js +1 -1
- package/dist/components/cms/use-discard-controller.js.map +1 -1
- package/dist/components/cms/use-document-draft.d.ts +7 -1
- package/dist/components/cms/use-document-draft.js +16 -2
- package/dist/components/cms/use-document-draft.js.map +1 -1
- package/dist/components/cms/use-document-versions.js +1 -1
- package/dist/components/cms/use-document-versions.js.map +1 -1
- package/dist/components/cms/use-field-focus.d.ts +1 -1
- package/dist/components/cms/use-field-focus.js +2 -2
- package/dist/components/cms/use-field-focus.js.map +1 -1
- package/dist/components/cms/use-live-url.d.ts +1 -1
- package/dist/components/cms/use-live-url.js +3 -3
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.d.ts +1 -1
- package/dist/components/cms/use-publish-controller.js +1 -1
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/cms/use-published-perspective.d.ts +1 -1
- package/dist/components/cms/use-published-perspective.js.map +1 -1
- package/dist/components/cms/use-restore-version.d.ts +1 -1
- package/dist/components/cms/use-restore-version.js +1 -1
- package/dist/components/cms/use-restore-version.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.js +1 -1
- package/dist/components/cms/version-compare-lazy.js.map +1 -1
- package/dist/components/cms/version-compare.js +1 -1
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/cms/versions-pane.js +3 -3
- package/dist/components/cms/versions-pane.js.map +1 -1
- package/dist/components/cms/view-bar.js +3 -3
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.d.ts +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +2 -2
- package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
- package/dist/components/collection-view/collection-view-context.d.ts +1 -1
- package/dist/components/collection-view/collection-view-context.js +1 -1
- package/dist/components/collection-view/collection-view-context.js.map +1 -1
- package/dist/components/collection-view/collection-view-surface.d.ts +13 -0
- package/dist/components/collection-view/collection-view-surface.js +18 -0
- package/dist/components/collection-view/collection-view-surface.js.map +1 -0
- package/dist/components/components/component-detail.js +10 -10
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +2 -2
- package/dist/components/components/component-preview.js +5 -5
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/component-status.d.ts +1 -1
- package/dist/components/components/component-status.js +1 -1
- package/dist/components/components/component-status.js.map +1 -1
- package/dist/components/components/components-index.js +2 -2
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/components/field-inspector.js +1 -1
- package/dist/components/components/field-inspector.js.map +1 -1
- package/dist/components/components/registry-browser.js +3 -3
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.d.ts +1 -1
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +3 -3
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/current-user-menu.js +1 -1
- package/dist/components/current-user-menu.js.map +1 -1
- package/dist/components/delivery-health-banner.js +2 -2
- package/dist/components/delivery-health-banner.js.map +1 -1
- package/dist/components/deployment-setup-gate.js +3 -3
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/dev-login-badge.js +2 -2
- package/dist/components/dev-login-badge.js.map +1 -1
- package/dist/components/guards.js +1 -1
- package/dist/components/guards.js.map +1 -1
- package/dist/components/inspector/document-panes.d.ts +54 -0
- package/dist/components/inspector/document-panes.js +41 -0
- package/dist/components/inspector/document-panes.js.map +1 -0
- package/dist/components/inspector/inspector-sidebar.js +1 -1
- package/dist/components/inspector/inspector-sidebar.js.map +1 -1
- package/dist/components/nav-issues-registry.d.ts +53 -0
- package/dist/components/nav-issues-registry.js +69 -0
- package/dist/components/nav-issues-registry.js.map +1 -0
- package/dist/components/nav-issues.js +4 -4
- package/dist/components/nav-issues.js.map +1 -1
- package/dist/components/operation-error.d.ts +1 -1
- package/dist/components/operation-error.js +4 -4
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/playground-badge.js +2 -2
- package/dist/components/playground-badge.js.map +1 -1
- package/dist/components/project-list.js +3 -3
- package/dist/components/project-list.js.map +1 -1
- package/dist/components/project-session.d.ts +2 -2
- package/dist/components/project-session.js +5 -5
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/project-switcher.js +2 -2
- package/dist/components/project-switcher.js.map +1 -1
- package/dist/components/rail-store-status.js +1 -1
- package/dist/components/rail-store-status.js.map +1 -1
- package/dist/components/rail-view-source.d.ts +6 -5
- package/dist/components/rail-view-source.js +4 -3
- package/dist/components/rail-view-source.js.map +1 -1
- package/dist/components/records/record-lens.d.ts +51 -0
- package/dist/components/records/record-lens.js +156 -0
- package/dist/components/records/record-lens.js.map +1 -0
- package/dist/components/records/record-rail.d.ts +86 -0
- package/dist/components/records/record-rail.js +241 -0
- package/dist/components/records/record-rail.js.map +1 -0
- package/dist/components/redirects/redirects-schema.js +1 -1
- package/dist/components/redirects/redirects-schema.js.map +1 -1
- package/dist/components/references/references-panel.js +2 -2
- package/dist/components/references/references-panel.js.map +1 -1
- package/dist/components/section-nav.js +27 -11
- package/dist/components/section-nav.js.map +1 -1
- package/dist/components/section-state.js +2 -2
- package/dist/components/section-state.js.map +1 -1
- package/dist/components/section-surface.js +2 -2
- package/dist/components/section-surface.js.map +1 -1
- package/dist/components/session-expired-overlay.js +3 -3
- package/dist/components/session-expired-overlay.js.map +1 -1
- package/dist/components/settings-form.js +1 -1
- package/dist/components/settings-form.js.map +1 -1
- package/dist/components/sign-in-needed-badge.js +1 -1
- package/dist/components/sign-in-needed-badge.js.map +1 -1
- package/dist/components/sign-in-view.d.ts +2 -2
- package/dist/components/sign-in-view.js +5 -5
- package/dist/components/sign-in-view.js.map +1 -1
- package/dist/components/studio-context.js +3 -3
- package/dist/components/studio-context.js.map +1 -1
- package/dist/components/studio-project-screen.js +30 -3
- package/dist/components/studio-project-screen.js.map +1 -1
- package/dist/components/studio-shell.js +6 -5
- package/dist/components/studio-shell.js.map +1 -1
- package/dist/components/theme-context.d.ts +1 -1
- package/dist/components/theme-context.js +2 -2
- package/dist/components/theme-context.js.map +1 -1
- package/dist/components/use-hydration-phase.d.ts +1 -1
- package/dist/components/use-hydration-phase.js +1 -1
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/components/users/invite-user-bar.d.ts +27 -0
- package/dist/components/users/invite-user-bar.js +25 -9
- package/dist/components/users/invite-user-bar.js.map +1 -1
- package/dist/components/users/role-policy-drift-issues.d.ts +38 -0
- package/dist/components/users/role-policy-drift-issues.js +71 -0
- package/dist/components/users/role-policy-drift-issues.js.map +1 -0
- package/dist/components/users/users-collection-view-source.d.ts +1 -1
- package/dist/components/users/users-collection-view-source.js +4 -2
- package/dist/components/users/users-collection-view-source.js.map +1 -1
- package/dist/components/users/users-module.js +1 -1
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/components/users/users-schema.d.ts +10 -1
- package/dist/components/users/users-schema.js +45 -25
- package/dist/components/users/users-schema.js.map +1 -1
- package/dist/components/users/users-store.js +3 -3
- package/dist/components/users/users-store.js.map +1 -1
- package/dist/components/workspace-projects.d.ts +1 -1
- package/dist/components/workspace-projects.js +1 -1
- package/dist/components/workspace-projects.js.map +1 -1
- package/dist/lib/ai/index.d.ts +1 -1
- package/dist/lib/ai/index.js +1 -1
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/node-handler.d.ts +1 -1
- package/dist/lib/ai/node-handler.js.map +1 -1
- package/dist/lib/ai/translate-document.js +1 -1
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.js +1 -1
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +2 -2
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/cms/published-documents.js +1 -1
- package/dist/lib/cms/published-documents.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +12 -12
- package/dist/lib/cms-views/view-model.js +2 -2
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +7 -2
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/data/content-session-refresh.d.ts +1 -1
- package/dist/lib/data/content-session-refresh.js.map +1 -1
- package/dist/lib/data/playground-unload-guard.d.ts +1 -1
- package/dist/lib/data/playground-unload-guard.js +1 -1
- package/dist/lib/data/playground-unload-guard.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/i18n.js +1 -1
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +4 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +4 -0
- package/dist/lib/i18n/messages.en.js +8 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/project-runtime-config.d.ts +9 -0
- package/dist/lib/operations/project-runtime-config.js +13 -0
- package/dist/lib/operations/project-runtime-config.js.map +1 -1
- package/dist/lib/operations/studio-operations.d.ts +9 -1
- package/dist/lib/operations/studio-operations.js +6 -1
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +16 -3
- package/dist/lib/persistence/convex-adapter.js +78 -47
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.js +14 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.js +1 -1
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/query/operations-query.d.ts +1 -1
- package/dist/lib/query/operations-query.js +1 -1
- package/dist/lib/query/operations-query.js.map +1 -1
- package/dist/lib/references/reference-model.js +1 -1
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/references/references-store.js +1 -1
- package/dist/lib/references/references-store.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +6 -0
- package/dist/lib/workspace/index.js +8 -0
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/mount.d.ts +28 -0
- package/dist/mount.js +27 -0
- package/dist/mount.js.map +1 -0
- package/dist/router/root.js +10 -10
- package/dist/router/root.js.map +1 -1
- package/dist/ui/badge.d.ts +1 -1
- package/dist/ui/button.d.ts +2 -2
- package/package.json +15 -11
- package/dist/components/redirects/redirects-collection-view-source.d.ts +0 -53
- package/dist/components/redirects/redirects-collection-view-source.js +0 -146
- package/dist/components/redirects/redirects-collection-view-source.js.map +0 -1
- package/dist/components/redirects/redirects-module.d.ts +0 -21
- package/dist/components/redirects/redirects-module.js +0 -40
- package/dist/components/redirects/redirects-module.js.map +0 -1
- package/dist/components/redirects/redirects-store.d.ts +0 -68
- package/dist/components/redirects/redirects-store.js +0 -132
- package/dist/components/redirects/redirects-store.js.map +0 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { convexTest } from 'convex-test'
|
|
3
|
+
import type { ConvexHttpClient } from 'convex/browser'
|
|
4
|
+
import { makeFunctionReference } from 'convex/server'
|
|
5
|
+
import type { FunctionReference } from 'convex/server'
|
|
6
|
+
import type { GenericId } from 'convex/values'
|
|
7
|
+
import {
|
|
8
|
+
ROLE_POLICY_ENV,
|
|
9
|
+
defaultRolePolicy,
|
|
10
|
+
rolePolicyFingerprint,
|
|
11
|
+
serializeRolePolicy,
|
|
12
|
+
} from '@elytracms/core/persistence'
|
|
13
|
+
import type { RolePolicy } from '@elytracms/core/persistence'
|
|
14
|
+
import { resolveProject } from '@elytracms/core/studio-core'
|
|
15
|
+
import { ConvexPersistenceAdapter } from '../src/lib/persistence/convex-adapter'
|
|
16
|
+
import { gateSessionCreation } from './auth'
|
|
17
|
+
import { requireCapability } from './guard'
|
|
18
|
+
import { deployedRolePolicy } from './roles'
|
|
19
|
+
import schema from './schema'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* EC-605 — the Convex trust boundary reads the project's role policy from the copy the
|
|
23
|
+
* CLI provisions into `ELYTRA_ROLE_POLICY`, and only that copy.
|
|
24
|
+
*
|
|
25
|
+
* The REAL `members` / `invites` / `roles` handlers and the REAL guard + integrity floor
|
|
26
|
+
* run on convex-test's in-memory backend; the env is stubbed per test (set, unset,
|
|
27
|
+
* malformed). Two member rows hold DIFFERENT custom roles, so a decision that silently
|
|
28
|
+
* fell back to the default policy (where both roles are unknown) is visible.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const modules = {
|
|
32
|
+
...import.meta.glob(['./**/*.ts', '!./**/*.test.ts', '!./_generated/**']),
|
|
33
|
+
'./_generated/api.ts': async () => ({}),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ISSUER = 'https://convex.test'
|
|
37
|
+
|
|
38
|
+
/** The fourth-role proof: a `reviewer` that reads, between viewer and editor. */
|
|
39
|
+
const FOUR_ROLE: RolePolicy = {
|
|
40
|
+
roles: [
|
|
41
|
+
{ id: 'viewer', label: 'Viewer', capabilities: ['content.read'] },
|
|
42
|
+
{ id: 'reviewer', label: 'Reviewer', capabilities: ['content.read'] },
|
|
43
|
+
{ id: 'editor', label: 'Editor', capabilities: ['content.read', 'content.write'] },
|
|
44
|
+
{ id: 'admin', label: 'Admin', capabilities: ['content.read', 'content.write', 'project.administer'] },
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** An administering role BELOW the strongest — what the floor's rank invariant guards. */
|
|
49
|
+
const RANKED: RolePolicy = {
|
|
50
|
+
roles: [
|
|
51
|
+
{ id: 'author', capabilities: ['content.read', 'content.write'] },
|
|
52
|
+
{ id: 'manager', capabilities: ['content.read', 'content.write', 'project.administer'] },
|
|
53
|
+
{ id: 'owner', capabilities: ['content.read', 'content.write', 'project.administer'] },
|
|
54
|
+
],
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const membersSet = makeFunctionReference<'mutation'>('members:set')
|
|
58
|
+
const membersList = makeFunctionReference<'query'>('members:list')
|
|
59
|
+
const invitesCreate = makeFunctionReference<'mutation'>('invites:create')
|
|
60
|
+
const fingerprintQuery = makeFunctionReference<'query'>('roles:fingerprint')
|
|
61
|
+
|
|
62
|
+
const NOW = '2026-10-26T00:00:00.000Z'
|
|
63
|
+
|
|
64
|
+
async function backendWith(rows: Array<{ userId: string; role: string }>) {
|
|
65
|
+
const t = convexTest(schema, modules)
|
|
66
|
+
await t.run(async (ctx) => {
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
await ctx.db.insert('members', { ...row, createdAt: NOW, updatedAt: NOW })
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
return t
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const as = (t: ReturnType<typeof convexTest>, userId: string) =>
|
|
75
|
+
t.withIdentity({ subject: userId, issuer: ISSUER })
|
|
76
|
+
|
|
77
|
+
/** Run the guard itself as `userId` — the one primitive every engine handler gates on. */
|
|
78
|
+
async function canDo(
|
|
79
|
+
t: ReturnType<typeof convexTest>,
|
|
80
|
+
userId: string,
|
|
81
|
+
capability: 'content.read' | 'content.write' | 'project.administer',
|
|
82
|
+
): Promise<boolean> {
|
|
83
|
+
try {
|
|
84
|
+
await as(t, userId).run((ctx) => requireCapability(ctx, capability))
|
|
85
|
+
return true
|
|
86
|
+
} catch {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
vi.unstubAllEnvs()
|
|
93
|
+
})
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
vi.unstubAllEnvs()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('the deployed role policy (EC-605) — absent → the default, byte-identical', () => {
|
|
99
|
+
it('reads the default policy when ELYTRA_ROLE_POLICY is unset or blank', () => {
|
|
100
|
+
expect(deployedRolePolicy({})).toBe(defaultRolePolicy)
|
|
101
|
+
expect(deployedRolePolicy({ [ROLE_POLICY_ENV]: ' ' })).toBe(defaultRolePolicy)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('keeps the historical tiers with no env: viewer reads, editor writes, admin administers', async () => {
|
|
105
|
+
const t = await backendWith([
|
|
106
|
+
{ userId: 'u_viewer', role: 'viewer' },
|
|
107
|
+
{ userId: 'u_editor', role: 'editor' },
|
|
108
|
+
{ userId: 'u_admin', role: 'admin' },
|
|
109
|
+
])
|
|
110
|
+
expect(await canDo(t, 'u_viewer', 'content.read')).toBe(true)
|
|
111
|
+
expect(await canDo(t, 'u_viewer', 'content.write')).toBe(false)
|
|
112
|
+
expect(await canDo(t, 'u_editor', 'content.write')).toBe(true)
|
|
113
|
+
expect(await canDo(t, 'u_editor', 'project.administer')).toBe(false)
|
|
114
|
+
expect(await canDo(t, 'u_admin', 'project.administer')).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('refuses to store a role the default policy does not declare (members:set, invites:create)', async () => {
|
|
118
|
+
const t = await backendWith([{ userId: 'u_admin', role: 'admin' }])
|
|
119
|
+
await expect(
|
|
120
|
+
as(t, 'u_admin').mutation(membersSet, { userId: 'u_x', role: 'reviewer', createdAt: NOW, updatedAt: NOW }),
|
|
121
|
+
).rejects.toThrow(/invalid-role: role "reviewer" is not declared/)
|
|
122
|
+
await expect(
|
|
123
|
+
as(t, 'u_admin').mutation(invitesCreate, {
|
|
124
|
+
email: 'x@example.com',
|
|
125
|
+
role: 'reviewer',
|
|
126
|
+
createdAt: NOW,
|
|
127
|
+
updatedAt: NOW,
|
|
128
|
+
}),
|
|
129
|
+
).rejects.toThrow(/invalid-role/)
|
|
130
|
+
// Control: a declared role is stored.
|
|
131
|
+
const stored = await as(t, 'u_admin').mutation(membersSet, {
|
|
132
|
+
userId: 'u_x',
|
|
133
|
+
role: 'editor',
|
|
134
|
+
createdAt: NOW,
|
|
135
|
+
updatedAt: NOW,
|
|
136
|
+
})
|
|
137
|
+
expect(stored).toMatchObject({ userId: 'u_x', role: 'editor' })
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('answers the default fingerprint, sourced from the default', async () => {
|
|
141
|
+
const t = convexTest(schema, modules)
|
|
142
|
+
expect(await t.query(fingerprintQuery, {})).toEqual({
|
|
143
|
+
status: 'ok',
|
|
144
|
+
fingerprint: rolePolicyFingerprint(defaultRolePolicy),
|
|
145
|
+
source: 'default',
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
describe('a provisioned four-role policy — honoured by the guard with two custom-role members', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
vi.stubEnv(ROLE_POLICY_ENV, serializeRolePolicy(FOUR_ROLE))
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('stores the custom role (members:set) and the guard grants by it', async () => {
|
|
156
|
+
const t = await backendWith([{ userId: 'u_admin', role: 'admin' }])
|
|
157
|
+
for (const [userId, role] of [
|
|
158
|
+
['u_reviewer', 'reviewer'],
|
|
159
|
+
['u_editor', 'editor'],
|
|
160
|
+
] as const) {
|
|
161
|
+
const stored = await as(t, 'u_admin').mutation(membersSet, { userId, role, createdAt: NOW, updatedAt: NOW })
|
|
162
|
+
expect(stored).toMatchObject({ userId, role })
|
|
163
|
+
}
|
|
164
|
+
// The reviewer reads (a real handler) but cannot write; the editor writes.
|
|
165
|
+
expect(await as(t, 'u_reviewer').query(membersList, {})).toHaveLength(3)
|
|
166
|
+
expect(await canDo(t, 'u_reviewer', 'content.read')).toBe(true)
|
|
167
|
+
expect(await canDo(t, 'u_reviewer', 'content.write')).toBe(false)
|
|
168
|
+
expect(await canDo(t, 'u_editor', 'content.write')).toBe(true)
|
|
169
|
+
expect(await canDo(t, 'u_editor', 'project.administer')).toBe(false)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('control: without the env the same reviewer row grants NOTHING (unknown role fails closed)', async () => {
|
|
173
|
+
vi.unstubAllEnvs()
|
|
174
|
+
const t = await backendWith([{ userId: 'u_reviewer', role: 'reviewer' }])
|
|
175
|
+
expect(await canDo(t, 'u_reviewer', 'content.read')).toBe(false)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('answers the provisioned policy’s fingerprint, sourced from the env', async () => {
|
|
179
|
+
const t = convexTest(schema, modules)
|
|
180
|
+
expect(await t.query(fingerprintQuery, {})).toEqual({
|
|
181
|
+
status: 'ok',
|
|
182
|
+
fingerprint: rolePolicyFingerprint(FOUR_ROLE),
|
|
183
|
+
source: 'env',
|
|
184
|
+
})
|
|
185
|
+
expect(rolePolicyFingerprint(FOUR_ROLE)).not.toBe(rolePolicyFingerprint(defaultRolePolicy))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('stores a custom role through the production adapter unchanged (no silent viewer floor)', async () => {
|
|
189
|
+
const t = await backendWith([{ userId: 'u_admin', role: 'admin' }])
|
|
190
|
+
const asAdmin = as(t, 'u_admin')
|
|
191
|
+
const client = {
|
|
192
|
+
query: (ref: FunctionReference<'query'>, args?: Record<string, unknown>) => asAdmin.query(ref, args ?? {}),
|
|
193
|
+
mutation: (ref: FunctionReference<'mutation'>, args?: Record<string, unknown>) =>
|
|
194
|
+
asAdmin.mutation(ref, args ?? {}),
|
|
195
|
+
} as unknown as ConvexHttpClient
|
|
196
|
+
// users:get joins the auth `users` row; give the member an identity row first.
|
|
197
|
+
const userId = await t.run(async (ctx) => {
|
|
198
|
+
const id = await ctx.db.insert('users', { email: 'rev@example.com' })
|
|
199
|
+
return String(id)
|
|
200
|
+
})
|
|
201
|
+
const adapter = new ConvexPersistenceAdapter(client, 'role-policy')
|
|
202
|
+
await adapter.users.saveUser('role-policy', { id: userId, roles: ['reviewer'] })
|
|
203
|
+
const row = await t.run((ctx) =>
|
|
204
|
+
ctx.db
|
|
205
|
+
.query('members')
|
|
206
|
+
.withIndex('by_user', (q) => q.eq('userId', userId))
|
|
207
|
+
.first(),
|
|
208
|
+
)
|
|
209
|
+
expect(row?.role).toBe('reviewer')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('the integrity floor under a ranked custom policy (an administering role below the strongest)', () => {
|
|
214
|
+
beforeEach(() => {
|
|
215
|
+
vi.stubEnv(ROLE_POLICY_ENV, serializeRolePolicy(RANKED))
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('a manager assigns roles up to its own rank, never above (members:set and invites:create)', async () => {
|
|
219
|
+
const t = await backendWith([
|
|
220
|
+
{ userId: 'u_owner', role: 'owner' },
|
|
221
|
+
{ userId: 'u_manager', role: 'manager' },
|
|
222
|
+
])
|
|
223
|
+
const manager = as(t, 'u_manager')
|
|
224
|
+
await expect(
|
|
225
|
+
manager.mutation(membersSet, { userId: 'u_new', role: 'author', createdAt: NOW, updatedAt: NOW }),
|
|
226
|
+
).resolves.toMatchObject({ role: 'author' })
|
|
227
|
+
await expect(
|
|
228
|
+
manager.mutation(membersSet, { userId: 'u_new', role: 'manager', createdAt: NOW, updatedAt: NOW }),
|
|
229
|
+
).resolves.toMatchObject({ role: 'manager' })
|
|
230
|
+
await expect(
|
|
231
|
+
manager.mutation(membersSet, { userId: 'u_new', role: 'owner', createdAt: NOW, updatedAt: NOW }),
|
|
232
|
+
).rejects.toThrow(/cannot assign the “owner” role/)
|
|
233
|
+
await expect(
|
|
234
|
+
manager.mutation(invitesCreate, { email: 'boss@example.com', role: 'owner', createdAt: NOW, updatedAt: NOW }),
|
|
235
|
+
).rejects.toThrow(/cannot assign the “owner” role/)
|
|
236
|
+
// Control: the owner (strongest) may invite an owner.
|
|
237
|
+
await expect(
|
|
238
|
+
as(t, 'u_owner').mutation(invitesCreate, { email: 'boss@example.com', role: 'owner', createdAt: NOW, updatedAt: NOW }),
|
|
239
|
+
).resolves.toMatchObject({ role: 'owner' })
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('counts every administering role for "the last administrator" — demoting one of two is allowed, the last is not', async () => {
|
|
243
|
+
const t = await backendWith([
|
|
244
|
+
{ userId: 'u_owner', role: 'owner' },
|
|
245
|
+
{ userId: 'u_manager', role: 'manager' },
|
|
246
|
+
])
|
|
247
|
+
// Two administrators: the owner demotes the manager to author.
|
|
248
|
+
await expect(
|
|
249
|
+
as(t, 'u_owner').mutation(membersSet, { userId: 'u_manager', role: 'author', createdAt: NOW, updatedAt: NOW }),
|
|
250
|
+
).resolves.toMatchObject({ role: 'author' })
|
|
251
|
+
// Now the owner is the last administrator: demoting themselves is refused.
|
|
252
|
+
await expect(
|
|
253
|
+
as(t, 'u_owner').mutation(membersSet, { userId: 'u_owner', role: 'manager', createdAt: NOW, updatedAt: NOW }),
|
|
254
|
+
).resolves.toMatchObject({ role: 'manager' })
|
|
255
|
+
await expect(
|
|
256
|
+
as(t, 'u_owner').mutation(membersSet, { userId: 'u_owner', role: 'author', createdAt: NOW, updatedAt: NOW }),
|
|
257
|
+
).rejects.toThrow(/last admin/)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('bootstraps the first user into the strongest role (owner), not a literal "admin"', async () => {
|
|
261
|
+
const t = convexTest(schema, modules)
|
|
262
|
+
const userId = await t.run((ctx) => ctx.db.insert('users', { email: 'first@example.com' }))
|
|
263
|
+
await t.run((ctx) => gateSessionCreation(ctx as never, { userId: userId as GenericId<'users'> }))
|
|
264
|
+
const row = await t.run((ctx) => ctx.db.query('members').first())
|
|
265
|
+
expect(row?.role).toBe('owner')
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
describe('a malformed or invalid deployed copy throws — never a silent default', () => {
|
|
270
|
+
it('every guarded function fails, naming ELYTRA_ROLE_POLICY', async () => {
|
|
271
|
+
vi.stubEnv(ROLE_POLICY_ENV, '{not json')
|
|
272
|
+
const t = await backendWith([{ userId: 'u_admin', role: 'admin' }])
|
|
273
|
+
await expect(as(t, 'u_admin').query(membersList, {})).rejects.toThrow(/ELYTRA_ROLE_POLICY is not valid JSON/)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('an invalid policy (no administering role) is refused with the located rule', () => {
|
|
277
|
+
const invalid = JSON.stringify({ v: 1, roles: [{ id: 'viewer', capabilities: ['content.read'] }] })
|
|
278
|
+
expect(() => deployedRolePolicy({ [ROLE_POLICY_ENV]: invalid })).toThrow(/No role grants “project.administer”/)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('the fingerprint query answers `invalid` with the reason instead of throwing', async () => {
|
|
282
|
+
vi.stubEnv(ROLE_POLICY_ENV, '{"v":2,"roles":[]}')
|
|
283
|
+
const t = convexTest(schema, modules)
|
|
284
|
+
const answer = (await t.query(fingerprintQuery, {})) as { status: string; detail: string }
|
|
285
|
+
expect(answer.status).toBe('invalid')
|
|
286
|
+
expect(answer.detail).toMatch(/ELYTRA_ROLE_POLICY is not a serialised role policy/)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('a changed env is read on the next call (the parse is cached per string, not per process)', () => {
|
|
290
|
+
const four = serializeRolePolicy(FOUR_ROLE)
|
|
291
|
+
expect(deployedRolePolicy({ [ROLE_POLICY_ENV]: four }).roles.map((r) => r.id)).toContain('reviewer')
|
|
292
|
+
expect(deployedRolePolicy({ [ROLE_POLICY_ENV]: serializeRolePolicy(RANKED) }).roles.map((r) => r.id)).toEqual([
|
|
293
|
+
'author',
|
|
294
|
+
'manager',
|
|
295
|
+
'owner',
|
|
296
|
+
])
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
describe('one declaration, two readers — the config entry reaches the guard through the provisioned copy', () => {
|
|
301
|
+
it('resolveProject → serializeRolePolicy → ELYTRA_ROLE_POLICY → the guard grants the declared reviewer', async () => {
|
|
302
|
+
const resolved = resolveProject({
|
|
303
|
+
name: 'Acme',
|
|
304
|
+
slug: 'acme',
|
|
305
|
+
locales: { default: 'en', locales: ['en'] },
|
|
306
|
+
roles: FOUR_ROLE.roles.map((role) => ({ ...role, capabilities: [...role.capabilities] })),
|
|
307
|
+
})
|
|
308
|
+
expect(resolved.ok).toBe(true)
|
|
309
|
+
if (!resolved.ok) return
|
|
310
|
+
vi.stubEnv(ROLE_POLICY_ENV, serializeRolePolicy(resolved.project.roles!))
|
|
311
|
+
const t = await backendWith([{ userId: 'u_reviewer', role: 'reviewer' }])
|
|
312
|
+
expect(await canDo(t, 'u_reviewer', 'content.read')).toBe(true)
|
|
313
|
+
expect(await canDo(t, 'u_reviewer', 'content.write')).toBe(false)
|
|
314
|
+
})
|
|
315
|
+
})
|
package/convex/roles.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ROLE_POLICY_ENV,
|
|
3
|
+
defaultRolePolicy,
|
|
4
|
+
parseRolePolicy,
|
|
5
|
+
rolePolicyFingerprint,
|
|
6
|
+
} from '@elytracms/core/persistence'
|
|
7
|
+
import type { RolePolicy } from '@elytracms/core/persistence'
|
|
8
|
+
import { query } from './functions'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The role policy this deployment ENFORCES (EC-605) — the Convex trust boundary's
|
|
12
|
+
* reader of the project's `roles` declaration.
|
|
13
|
+
*
|
|
14
|
+
* Convex functions cannot import `elytra.config.ts`, and the nine engine modules that
|
|
15
|
+
* gate on a capability import `./guard` inside the package, so the declaration reaches
|
|
16
|
+
* the deployment as a serialised copy in its env: `ELYTRA_ROLE_POLICY`, provisioned by
|
|
17
|
+
* the `elytra` CLI from the resolved project (`elytra init`, and `elytra build` before
|
|
18
|
+
* every Convex deploy). `guard.ts`, `authority.ts`, `members.ts`, `invites.ts` and the
|
|
19
|
+
* first-user bootstrap (`auth.ts`) all read it through {@link deployedRolePolicy}.
|
|
20
|
+
*
|
|
21
|
+
* - **Absent (or blank)** → the shipped default policy, byte-identical to before EC-605.
|
|
22
|
+
* - **Present but malformed or invalid** → THROWS, naming every reason
|
|
23
|
+
* (`parseRolePolicy`): every guarded function then fails loudly rather than enforcing
|
|
24
|
+
* a policy nobody declared. Never a silent default.
|
|
25
|
+
*
|
|
26
|
+
* The env is read per CALL (a provisioned change takes effect without a redeploy) and
|
|
27
|
+
* the parse is cached per serialised string, so the steady state costs one string
|
|
28
|
+
* comparison.
|
|
29
|
+
*
|
|
30
|
+
* The public {@link fingerprint} query is how a drift between this copy and the config
|
|
31
|
+
* becomes visible: `elytra doctor` and the studio compare it with the config's own
|
|
32
|
+
* fingerprint. Consumers reach it through a one-line re-export (`convex/roles.ts`); the
|
|
33
|
+
* readers above work without it.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
let cached: { readonly serialized: string; readonly policy: RolePolicy } | undefined
|
|
37
|
+
|
|
38
|
+
/** The deployment env, reached via `globalThis` so this module needs no Node types at runtime. */
|
|
39
|
+
function deploymentEnv(): Record<string, string | undefined> {
|
|
40
|
+
return (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The deployed role policy: the parsed `ELYTRA_ROLE_POLICY`, or the default when it is
|
|
45
|
+
* unset. Throws on a malformed or invalid value (see the module comment). `env` is
|
|
46
|
+
* injectable for tests.
|
|
47
|
+
*/
|
|
48
|
+
export function deployedRolePolicy(env: Record<string, string | undefined> = deploymentEnv()): RolePolicy {
|
|
49
|
+
const serialized = env[ROLE_POLICY_ENV]
|
|
50
|
+
if (serialized === undefined || serialized.trim() === '') return defaultRolePolicy
|
|
51
|
+
if (cached?.serialized === serialized) return cached.policy
|
|
52
|
+
const policy = parseRolePolicy(serialized)
|
|
53
|
+
cached = { serialized, policy }
|
|
54
|
+
return policy
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What {@link fingerprint} answers — the deployed policy's identity, never its content. */
|
|
58
|
+
export type DeployedRolePolicyFingerprint =
|
|
59
|
+
| { readonly status: 'ok'; readonly fingerprint: string; readonly source: 'env' | 'default' }
|
|
60
|
+
| { readonly status: 'invalid'; readonly detail: string }
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* PUBLIC, unauthenticated: the fingerprint of the role policy this deployment enforces
|
|
64
|
+
* (`rolePolicyFingerprint` — 16 hex chars over ids, rank and capabilities; labels and
|
|
65
|
+
* the policy itself never leave the deployment) and whether it comes from
|
|
66
|
+
* `ELYTRA_ROLE_POLICY` or the default. `elytra doctor` reads it without a session (the
|
|
67
|
+
* doctor's env rule — names only, values never printed — stands: this is not a value),
|
|
68
|
+
* and the studio reads it at boot. A malformed or invalid deployed copy answers
|
|
69
|
+
* `invalid` with the located reasons instead of throwing, so the drift check can say
|
|
70
|
+
* exactly what is wrong.
|
|
71
|
+
*/
|
|
72
|
+
export const fingerprint = query({
|
|
73
|
+
args: {},
|
|
74
|
+
handler: async (): Promise<DeployedRolePolicyFingerprint> => {
|
|
75
|
+
const env = deploymentEnv()
|
|
76
|
+
const source = env[ROLE_POLICY_ENV]?.trim() ? 'env' : 'default'
|
|
77
|
+
try {
|
|
78
|
+
return { status: 'ok', fingerprint: rolePolicyFingerprint(deployedRolePolicy(env)), source }
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return { status: 'invalid', detail: error instanceof Error ? error.message : String(error) }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
})
|
package/convex/schema.ts
CHANGED
|
@@ -299,6 +299,36 @@ export default defineSchema({
|
|
|
299
299
|
updatedAt: v.string(),
|
|
300
300
|
}).index('by_redirect', ['redirectId']),
|
|
301
301
|
|
|
302
|
+
/**
|
|
303
|
+
* The generic RECORD STORE (EC-616) — one row per record of a DECLARED record kind
|
|
304
|
+
* (`defineRecordKind` in `@elytracms/core/persistence`): the engine's own kinds and
|
|
305
|
+
* every addon's. `data` holds the kind's columns + its open `meta` bag, validated by
|
|
306
|
+
* the kind's schema in the write path (`defineRecordFunctions`, `record-functions.ts`)
|
|
307
|
+
* — the data SHAPE, never project structure (EC-585 1e).
|
|
308
|
+
*
|
|
309
|
+
* The index slots are STATIC (a consumer re-exports this schema, so an addon can never
|
|
310
|
+
* add an index); a kind fills them from its own declared columns: `key1`/`key2` from
|
|
311
|
+
* its `keys`, `at` from its `time` column (else `createdAt`). The indexes are the
|
|
312
|
+
* known access paths: one record by id; a kind by time (recent-first lists, a TTL
|
|
313
|
+
* sweep); by either key + time (submissions by form or by status, newest first); by
|
|
314
|
+
* both keys + time (an aggregate by day + document).
|
|
315
|
+
*/
|
|
316
|
+
records: defineTable({
|
|
317
|
+
kind: v.string(),
|
|
318
|
+
recordId: v.string(),
|
|
319
|
+
data: v.any(),
|
|
320
|
+
key1: v.optional(v.string()),
|
|
321
|
+
key2: v.optional(v.string()),
|
|
322
|
+
at: v.string(),
|
|
323
|
+
createdAt: v.string(),
|
|
324
|
+
updatedAt: v.string(),
|
|
325
|
+
})
|
|
326
|
+
.index('by_kind_record', ['kind', 'recordId'])
|
|
327
|
+
.index('by_kind_at', ['kind', 'at'])
|
|
328
|
+
.index('by_kind_key1', ['kind', 'key1', 'at'])
|
|
329
|
+
.index('by_kind_key2', ['kind', 'key2', 'at'])
|
|
330
|
+
.index('by_kind_keys', ['kind', 'key1', 'key2', 'at']),
|
|
331
|
+
|
|
302
332
|
/**
|
|
303
333
|
* Reference index entries (EC-155, vision AD-6) — derived data, recomputed
|
|
304
334
|
* replace-all-for-source on every write. Indexed for efficient lookup in both
|
package/convex/triggers.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { unfoldDocument } from '@elytracms/core/cms-core'
|
|
2
2
|
import type { CmsDocument } from '@elytracms/core/cms-core'
|
|
3
|
+
import { redirectRecordKind } from '@elytracms/core/persistence'
|
|
3
4
|
import { Triggers } from 'convex-helpers/server/triggers'
|
|
4
5
|
import type { DataModel } from './functions'
|
|
5
6
|
import { maintainDocumentReferences, removeDocumentReferences } from './referenceMaintenance'
|
|
@@ -33,7 +34,8 @@ import { scheduleRevalidate } from './revalidate'
|
|
|
33
34
|
*
|
|
34
35
|
* - `cmsDocuments` — only when the live pin (`publishedVersion`) moves
|
|
35
36
|
* (`publish` / `unpublish`), or a pinned row is deleted (`delete`);
|
|
36
|
-
* - `
|
|
37
|
+
* - `records` of the redirect kind (EC-616; EC-510 before it on the `redirects` table) —
|
|
38
|
+
* every write (`redirects`; the row IS the live state); no other record kind;
|
|
37
39
|
* - `assets` (EC-579) — every edit and delete (`asset`); a create fires nothing.
|
|
38
40
|
*
|
|
39
41
|
* The snapshot reads one more table, `documentVersions`, but only the version a
|
|
@@ -129,6 +131,24 @@ triggers.register('redirects', async (ctx) => {
|
|
|
129
131
|
await scheduleRevalidate(ctx, { kind: 'redirects' })
|
|
130
132
|
})
|
|
131
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The generic record store (EC-616). Of all record kinds, ONLY the engine's redirect
|
|
136
|
+
* kind is projected onto the live wire (`delivery.ts` reads it through
|
|
137
|
+
* `loadRedirectEntries`), so only a write to a `redirect` row revalidates — with exactly
|
|
138
|
+
* the event the legacy `redirects` table fired above (EC-510: every write, the receiver
|
|
139
|
+
* answers with the sweep). Every other kind — an addon's submissions, analytics rows —
|
|
140
|
+
* is never on the wire and fires nothing; a kind that starts reaching the snapshot
|
|
141
|
+
* belongs in this check.
|
|
142
|
+
*
|
|
143
|
+
* Since the move, a redirect write is a `records` write; the legacy table's trigger
|
|
144
|
+
* above still fires for the transitional deletes (a save lifting a legacy-only redirect
|
|
145
|
+
* deletes its legacy row: two sweeps for that one save, until the migration ran).
|
|
146
|
+
*/
|
|
147
|
+
triggers.register('records', async (ctx, change) => {
|
|
148
|
+
const kind = (change.newDoc ?? change.oldDoc)?.kind
|
|
149
|
+
if (kind === redirectRecordKind.kind) await scheduleRevalidate(ctx, { kind: 'redirects' })
|
|
150
|
+
})
|
|
151
|
+
|
|
132
152
|
/**
|
|
133
153
|
* Asset records (EC-579). The delivery snapshot projects every asset row onto
|
|
134
154
|
* the live wire field by field (url, dimensions, alt, focal point, lqip, the
|
package/dist/addon.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@elytracms/studio/addon` — THE public addon API (EC-616, ruling EC-565 § 6b item 2).
|
|
3
|
+
*
|
|
4
|
+
* An addon (`@elytracms/forms`, `@elytracms/analytics`, or your own) is an optional
|
|
5
|
+
* package whose studio half contributes navigation items to a project's
|
|
6
|
+
* `defineStudio({ sidebar })` — a rail bound to a record kind, or a view with its own
|
|
7
|
+
* component — and whose backend half is a record kind in the generic record store
|
|
8
|
+
* (`defineRecordKind` / `adapter.records(kind)` in `@elytracms/core/persistence`, its
|
|
9
|
+
* Convex functions from `defineRecordFunctions` in `@elytracms/studio/convex/
|
|
10
|
+
* record-functions`). Everything an addon's studio code touches is exported HERE; the
|
|
11
|
+
* studio's internals (`lib/*`, `components/*`) are not part of the published package.
|
|
12
|
+
* `@elytracms/studio/ui` stays the primitive set (Button, Panel, Text, …).
|
|
13
|
+
*
|
|
14
|
+
* ## The chrome contract
|
|
15
|
+
*
|
|
16
|
+
* The SHELL draws the top bar, the breadcrumb (its section crumb is the view's `label`;
|
|
17
|
+
* deeper crumbs are contributed with {@link useSetBreadcrumb}), the rail with its issues
|
|
18
|
+
* foot (findings contributed with {@link useNavIssues}, or always-on through a view's
|
|
19
|
+
* `useIssues`), the banners and the theme. A VIEW owns the whole scrollable content area
|
|
20
|
+
* under it — including its own title header: the shell adds no frame, title or padding.
|
|
21
|
+
* A view reaches the studio through its {@link AppViewContext}: the project, its deep
|
|
22
|
+
* link and search, the operations client (every read and write goes through the one
|
|
23
|
+
* authorized chokepoint), the query hooks over the one cache, the content locale, and
|
|
24
|
+
* the signed-in user with their role.
|
|
25
|
+
*
|
|
26
|
+
* ## The two navigation items
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* import { nav } from '@elytracms/studio/addon'
|
|
30
|
+
* import { submissionRecordKind } from './kinds'
|
|
31
|
+
*
|
|
32
|
+
* // a rail bound to a record kind — the portable list/edit surfaces, no component to write
|
|
33
|
+
* nav.rail(submissionRecordKind, { label: 'Submissions' })
|
|
34
|
+
* // a custom component — the whole content area is yours
|
|
35
|
+
* nav.view({ id: 'forms-stats', label: 'Form stats', render: (ctx) => <Stats ctx={ctx} /> })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export { nav } from './components/app-views/default-modules.js';
|
|
39
|
+
export type { CustomViewConfig, RailViewConfig, BuiltinViewConfig, } from './components/app-views/default-modules.js';
|
|
40
|
+
export type { AppViewContext, AppViewDeepLink, AppViewModule, AppViewQueryHooks, NavIssuesHook, NavPermission, NavPermissionContext, SidebarNode, SidebarViewNode, } from './components/app-views/module.js';
|
|
41
|
+
/** A record rail's pieces, for an addon composing its own view around one. */
|
|
42
|
+
export { RECORD_RAIL_CAPABILITIES, RecordRailModule, RecordRailProvider, useRecordCollectionViewSource, useRecordList, useRecordRail, } from './components/records/record-rail.js';
|
|
43
|
+
export type { RecordRailModuleProps, RecordRailPaths, RecordRailStore } from './components/records/record-rail.js';
|
|
44
|
+
export { lensDocumentToRecordWrite, recordLensDef, recordToLensDocument } from './components/records/record-lens.js';
|
|
45
|
+
/** The portable list/edit surfaces over any source, and the scaffold that assembles one. */
|
|
46
|
+
export { CollectionViewSurface } from './components/collection-view/collection-view-surface.js';
|
|
47
|
+
export { assembleRailViewSource, metaBagProjection, normalizeOkResult, useRailViewSource, } from './components/rail-view-source.js';
|
|
48
|
+
export type { OkResult, RailAdapter } from './components/rail-view-source.js';
|
|
49
|
+
export type { CollectionViewCapabilities, CollectionViewSource, FieldAccess, } from './lib/collection-view/collection-view-source.js';
|
|
50
|
+
export { useOperationCache, useOperationQuery } from './lib/query/index.js';
|
|
51
|
+
export type { OperationCache, UseOperationQueryOptions } from './lib/query/index.js';
|
|
52
|
+
export { useStudioLocale } from './lib/locale/index.js';
|
|
53
|
+
export type { StudioLocale } from './lib/locale/index.js';
|
|
54
|
+
export { useSetBreadcrumb } from './components/breadcrumb-context.js';
|
|
55
|
+
export type { BreadcrumbItem } from './components/breadcrumb-context.js';
|
|
56
|
+
export { useNavIssues } from './components/nav-issues-registry.js';
|
|
57
|
+
export type { NavIssue } from './components/nav-issues-registry.js';
|
|
58
|
+
/** A pane in the document editor's inspector — `defineStudio({ documentPanes: [documentPane({ … })] })`. */
|
|
59
|
+
export { documentPane } from './components/inspector/document-panes.js';
|
|
60
|
+
export type { DocumentPane, DocumentPaneContext } from './components/inspector/document-panes.js';
|
|
61
|
+
/**
|
|
62
|
+
* The portable edit shell alone (AD-19), with its form-column appendix seam (EC-396): an addon
|
|
63
|
+
* that assembles its own source (`useRailViewSource`) renders one record's detail with
|
|
64
|
+
* read-only sections below the editable fields — the forms inbox shows a submission's answers,
|
|
65
|
+
* files and delivery log there.
|
|
66
|
+
*/
|
|
67
|
+
export { PortableDetailEditor } from './components/cms/detail-editor.js';
|
|
68
|
+
/** The one query key of a kind's records in a project — patch it after a write (`useOperationCache`). */
|
|
69
|
+
export { recordListKey } from './components/records/record-rail.js';
|
|
70
|
+
/** Resolve a `LocalizedString` (`'Text'` or `{ en, de, … }`) in the studio's UI language. */
|
|
71
|
+
export { useLabel } from './lib/i18n/index.js';
|
package/dist/addon.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@elytracms/studio/addon` — THE public addon API (EC-616, ruling EC-565 § 6b item 2).
|
|
3
|
+
*
|
|
4
|
+
* An addon (`@elytracms/forms`, `@elytracms/analytics`, or your own) is an optional
|
|
5
|
+
* package whose studio half contributes navigation items to a project's
|
|
6
|
+
* `defineStudio({ sidebar })` — a rail bound to a record kind, or a view with its own
|
|
7
|
+
* component — and whose backend half is a record kind in the generic record store
|
|
8
|
+
* (`defineRecordKind` / `adapter.records(kind)` in `@elytracms/core/persistence`, its
|
|
9
|
+
* Convex functions from `defineRecordFunctions` in `@elytracms/studio/convex/
|
|
10
|
+
* record-functions`). Everything an addon's studio code touches is exported HERE; the
|
|
11
|
+
* studio's internals (`lib/*`, `components/*`) are not part of the published package.
|
|
12
|
+
* `@elytracms/studio/ui` stays the primitive set (Button, Panel, Text, …).
|
|
13
|
+
*
|
|
14
|
+
* ## The chrome contract
|
|
15
|
+
*
|
|
16
|
+
* The SHELL draws the top bar, the breadcrumb (its section crumb is the view's `label`;
|
|
17
|
+
* deeper crumbs are contributed with {@link useSetBreadcrumb}), the rail with its issues
|
|
18
|
+
* foot (findings contributed with {@link useNavIssues}, or always-on through a view's
|
|
19
|
+
* `useIssues`), the banners and the theme. A VIEW owns the whole scrollable content area
|
|
20
|
+
* under it — including its own title header: the shell adds no frame, title or padding.
|
|
21
|
+
* A view reaches the studio through its {@link AppViewContext}: the project, its deep
|
|
22
|
+
* link and search, the operations client (every read and write goes through the one
|
|
23
|
+
* authorized chokepoint), the query hooks over the one cache, the content locale, and
|
|
24
|
+
* the signed-in user with their role.
|
|
25
|
+
*
|
|
26
|
+
* ## The two navigation items
|
|
27
|
+
*
|
|
28
|
+
* ```tsx
|
|
29
|
+
* import { nav } from '@elytracms/studio/addon'
|
|
30
|
+
* import { submissionRecordKind } from './kinds'
|
|
31
|
+
*
|
|
32
|
+
* // a rail bound to a record kind — the portable list/edit surfaces, no component to write
|
|
33
|
+
* nav.rail(submissionRecordKind, { label: 'Submissions' })
|
|
34
|
+
* // a custom component — the whole content area is yours
|
|
35
|
+
* nav.view({ id: 'forms-stats', label: 'Form stats', render: (ctx) => <Stats ctx={ctx} /> })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
// --- Navigation items and the view contract --------------------------------------------
|
|
39
|
+
export { nav } from './components/app-views/default-modules.js';
|
|
40
|
+
// --- Rails: a record kind, or your own source -------------------------------------------
|
|
41
|
+
/** A record rail's pieces, for an addon composing its own view around one. */
|
|
42
|
+
export { RECORD_RAIL_CAPABILITIES, RecordRailModule, RecordRailProvider, useRecordCollectionViewSource, useRecordList, useRecordRail, } from './components/records/record-rail.js';
|
|
43
|
+
export { lensDocumentToRecordWrite, recordLensDef, recordToLensDocument } from './components/records/record-lens.js';
|
|
44
|
+
/** The portable list/edit surfaces over any source, and the scaffold that assembles one. */
|
|
45
|
+
export { CollectionViewSurface } from './components/collection-view/collection-view-surface.js';
|
|
46
|
+
export { assembleRailViewSource, metaBagProjection, normalizeOkResult, useRailViewSource, } from './components/rail-view-source.js';
|
|
47
|
+
// --- Hooks: the cache, the locale, the breadcrumb, the rail foot -------------------------
|
|
48
|
+
export { useOperationCache, useOperationQuery } from './lib/query/index.js';
|
|
49
|
+
export { useStudioLocale } from './lib/locale/index.js';
|
|
50
|
+
export { useSetBreadcrumb } from './components/breadcrumb-context.js';
|
|
51
|
+
export { useNavIssues } from './components/nav-issues-registry.js';
|
|
52
|
+
// --- The document editor: contributed inspector panes (EC-618) ---------------------------
|
|
53
|
+
/** A pane in the document editor's inspector — `defineStudio({ documentPanes: [documentPane({ … })] })`. */
|
|
54
|
+
export { documentPane } from './components/inspector/document-panes.js';
|
|
55
|
+
// --- EC-613 (the forms addon): an own source's edit shell, its list key, the UI language -----
|
|
56
|
+
/**
|
|
57
|
+
* The portable edit shell alone (AD-19), with its form-column appendix seam (EC-396): an addon
|
|
58
|
+
* that assembles its own source (`useRailViewSource`) renders one record's detail with
|
|
59
|
+
* read-only sections below the editable fields — the forms inbox shows a submission's answers,
|
|
60
|
+
* files and delivery log there.
|
|
61
|
+
*/
|
|
62
|
+
export { PortableDetailEditor } from './components/cms/detail-editor.js';
|
|
63
|
+
/** The one query key of a kind's records in a project — patch it after a write (`useOperationCache`). */
|
|
64
|
+
export { recordListKey } from './components/records/record-rail.js';
|
|
65
|
+
/** Resolve a `LocalizedString` (`'Text'` or `{ en, de, … }`) in the studio's UI language. */
|
|
66
|
+
export { useLabel } from './lib/i18n/index.js';
|
|
67
|
+
//# sourceMappingURL=addon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addon.js","sourceRoot":"","sources":["../src/addon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,0FAA0F;AAE1F,OAAO,EAAE,GAAG,EAAE,MAAM,wCAAwC,CAAA;AAkB5D,2FAA2F;AAE3F,8EAA8E;AAC9E,OAAO,EACL,wBAAwB,EACxB,gBAAgB,EAChB,kBAAkB,EAClB,6BAA6B,EAC7B,aAAa,EACb,aAAa,GACd,MAAM,kCAAkC,CAAA;AAEzC,OAAO,EAAE,yBAAyB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAEjH,4FAA4F;AAC5F,OAAO,EAAE,qBAAqB,EAAE,MAAM,sDAAsD,CAAA;AAC5F,OAAO,EACL,sBAAsB,EACtB,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,+BAA+B,CAAA;AAQtC,4FAA4F;AAE5F,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAElE,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAG/D,4FAA4F;AAE5F,4GAA4G;AAC5G,OAAO,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAA;AAGpE,gGAAgG;AAEhG;;;;;GAKG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAA;AACrE,yGAAyG;AACzG,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAA;AAChE,6FAA6F;AAC7F,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA","sourcesContent":["/**\n * `@elytracms/studio/addon` — THE public addon API (EC-616, ruling EC-565 § 6b item 2).\n *\n * An addon (`@elytracms/forms`, `@elytracms/analytics`, or your own) is an optional\n * package whose studio half contributes navigation items to a project's\n * `defineStudio({ sidebar })` — a rail bound to a record kind, or a view with its own\n * component — and whose backend half is a record kind in the generic record store\n * (`defineRecordKind` / `adapter.records(kind)` in `@elytracms/core/persistence`, its\n * Convex functions from `defineRecordFunctions` in `@elytracms/studio/convex/\n * record-functions`). Everything an addon's studio code touches is exported HERE; the\n * studio's internals (`lib/*`, `components/*`) are not part of the published package.\n * `@elytracms/studio/ui` stays the primitive set (Button, Panel, Text, …).\n *\n * ## The chrome contract\n *\n * The SHELL draws the top bar, the breadcrumb (its section crumb is the view's `label`;\n * deeper crumbs are contributed with {@link useSetBreadcrumb}), the rail with its issues\n * foot (findings contributed with {@link useNavIssues}, or always-on through a view's\n * `useIssues`), the banners and the theme. A VIEW owns the whole scrollable content area\n * under it — including its own title header: the shell adds no frame, title or padding.\n * A view reaches the studio through its {@link AppViewContext}: the project, its deep\n * link and search, the operations client (every read and write goes through the one\n * authorized chokepoint), the query hooks over the one cache, the content locale, and\n * the signed-in user with their role.\n *\n * ## The two navigation items\n *\n * ```tsx\n * import { nav } from '@elytracms/studio/addon'\n * import { submissionRecordKind } from './kinds'\n *\n * // a rail bound to a record kind — the portable list/edit surfaces, no component to write\n * nav.rail(submissionRecordKind, { label: 'Submissions' })\n * // a custom component — the whole content area is yours\n * nav.view({ id: 'forms-stats', label: 'Form stats', render: (ctx) => <Stats ctx={ctx} /> })\n * ```\n */\n\n// --- Navigation items and the view contract --------------------------------------------\n\nexport { nav } from './components/app-views/default-modules'\nexport type {\n CustomViewConfig,\n RailViewConfig,\n BuiltinViewConfig,\n} from './components/app-views/default-modules'\nexport type {\n AppViewContext,\n AppViewDeepLink,\n AppViewModule,\n AppViewQueryHooks,\n NavIssuesHook,\n NavPermission,\n NavPermissionContext,\n SidebarNode,\n SidebarViewNode,\n} from './components/app-views/module'\n\n// --- Rails: a record kind, or your own source -------------------------------------------\n\n/** A record rail's pieces, for an addon composing its own view around one. */\nexport {\n RECORD_RAIL_CAPABILITIES,\n RecordRailModule,\n RecordRailProvider,\n useRecordCollectionViewSource,\n useRecordList,\n useRecordRail,\n} from './components/records/record-rail'\nexport type { RecordRailModuleProps, RecordRailPaths, RecordRailStore } from './components/records/record-rail'\nexport { lensDocumentToRecordWrite, recordLensDef, recordToLensDocument } from './components/records/record-lens'\n\n/** The portable list/edit surfaces over any source, and the scaffold that assembles one. */\nexport { CollectionViewSurface } from './components/collection-view/collection-view-surface'\nexport {\n assembleRailViewSource,\n metaBagProjection,\n normalizeOkResult,\n useRailViewSource,\n} from './components/rail-view-source'\nexport type { OkResult, RailAdapter } from './components/rail-view-source'\nexport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n FieldAccess,\n} from './lib/collection-view/collection-view-source'\n\n// --- Hooks: the cache, the locale, the breadcrumb, the rail foot -------------------------\n\nexport { useOperationCache, useOperationQuery } from './lib/query'\nexport type { OperationCache, UseOperationQueryOptions } from './lib/query'\nexport { useStudioLocale } from './lib/locale'\nexport type { StudioLocale } from './lib/locale'\nexport { useSetBreadcrumb } from './components/breadcrumb-context'\nexport type { BreadcrumbItem } from './components/breadcrumb-context'\nexport { useNavIssues } from './components/nav-issues-registry'\nexport type { NavIssue } from './components/nav-issues-registry'\n\n// --- The document editor: contributed inspector panes (EC-618) ---------------------------\n\n/** A pane in the document editor's inspector — `defineStudio({ documentPanes: [documentPane({ … })] })`. */\nexport { documentPane } from './components/inspector/document-panes'\nexport type { DocumentPane, DocumentPaneContext } from './components/inspector/document-panes'\n\n// --- EC-613 (the forms addon): an own source's edit shell, its list key, the UI language -----\n\n/**\n * The portable edit shell alone (AD-19), with its form-column appendix seam (EC-396): an addon\n * that assembles its own source (`useRailViewSource`) renders one record's detail with\n * read-only sections below the editable fields — the forms inbox shows a submission's answers,\n * files and delivery log there.\n */\nexport { PortableDetailEditor } from './components/cms/detail-editor'\n/** The one query key of a kind's records in a project — patch it after a write (`useOperationCache`). */\nexport { recordListKey } from './components/records/record-rail'\n/** Resolve a `LocalizedString` (`'Text'` or `{ en, de, … }`) in the studio's UI language. */\nexport { useLabel } from './lib/i18n'\n"]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { UserCog } from 'lucide-react';
|
|
3
3
|
import { Heading, Stack, Text } from '@elytracms/studio/ui';
|
|
4
|
-
import { listStoreState, useOperationQuery } from '
|
|
5
|
-
import { translate } from '
|
|
4
|
+
import { listStoreState, useOperationQuery } from '../../lib/query/index.js';
|
|
5
|
+
import { translate } from '../../lib/i18n/index.js';
|
|
6
6
|
import { useAuth } from '../auth-context.js';
|
|
7
7
|
import { PortableDetailEditor } from '../cms/detail-editor.js';
|
|
8
8
|
import { CredentialPanel } from './credential-panel.js';
|