@elytracms/studio 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/convex/assets.ts +107 -0
- package/convex/auth.config.ts +15 -0
- package/convex/auth.ts +50 -0
- package/convex/cliTokens.ts +63 -0
- package/convex/cms.ts +174 -0
- package/convex/content.ts +316 -0
- package/convex/delivery.ts +266 -0
- package/convex/functions.ts +35 -0
- package/convex/graphs.ts +51 -0
- package/convex/guard.ts +67 -0
- package/convex/http.ts +30 -0
- package/convex/members.ts +83 -0
- package/convex/publishing.ts +102 -0
- package/convex/references.ts +168 -0
- package/convex/schema.ts +173 -0
- package/convex/sync.ts +322 -0
- package/convex/tsconfig.json +12 -0
- package/convex/users.ts +23 -0
- package/convex/webhooks.ts +85 -0
- package/dist/components/assets/asset-library.d.ts +14 -0
- package/dist/components/assets/asset-library.js +191 -0
- package/dist/components/assets/asset-library.js.map +1 -0
- package/dist/components/assets/asset-metadata.d.ts +34 -0
- package/dist/components/assets/asset-metadata.js +40 -0
- package/dist/components/assets/asset-metadata.js.map +1 -0
- package/dist/components/assets/asset-search.d.ts +7 -0
- package/dist/components/assets/asset-search.js +12 -0
- package/dist/components/assets/asset-search.js.map +1 -0
- package/dist/components/assets/asset-selection.d.ts +17 -0
- package/dist/components/assets/asset-selection.js +33 -0
- package/dist/components/assets/asset-selection.js.map +1 -0
- package/dist/components/assets/assets-store.d.ts +51 -0
- package/dist/components/assets/assets-store.js +153 -0
- package/dist/components/assets/assets-store.js.map +1 -0
- package/dist/components/auth-context.d.ts +38 -0
- package/dist/components/auth-context.js +88 -0
- package/dist/components/auth-context.js.map +1 -0
- package/dist/components/backend-gate.d.ts +19 -0
- package/dist/components/backend-gate.js +63 -0
- package/dist/components/backend-gate.js.map +1 -0
- package/dist/components/breadcrumb-context.d.ts +39 -0
- package/dist/components/breadcrumb-context.js +38 -0
- package/dist/components/breadcrumb-context.js.map +1 -0
- package/dist/components/builder/blocks-model.d.ts +80 -0
- package/dist/components/builder/blocks-model.js +181 -0
- package/dist/components/builder/blocks-model.js.map +1 -0
- package/dist/components/builder/blocks-panel.d.ts +44 -0
- package/dist/components/builder/blocks-panel.js +147 -0
- package/dist/components/builder/blocks-panel.js.map +1 -0
- package/dist/components/builder/builder-store.d.ts +69 -0
- package/dist/components/builder/builder-store.js +294 -0
- package/dist/components/builder/builder-store.js.map +1 -0
- package/dist/components/builder/canvas-stylesheet.d.ts +4 -0
- package/dist/components/builder/canvas-stylesheet.js +24 -0
- package/dist/components/builder/canvas-stylesheet.js.map +1 -0
- package/dist/components/builder/canvas.d.ts +8 -0
- package/dist/components/builder/canvas.js +723 -0
- package/dist/components/builder/canvas.js.map +1 -0
- package/dist/components/builder/clipboard-actions.d.ts +93 -0
- package/dist/components/builder/clipboard-actions.js +246 -0
- package/dist/components/builder/clipboard-actions.js.map +1 -0
- package/dist/components/builder/composition-field-editor.d.ts +11 -0
- package/dist/components/builder/composition-field-editor.js +150 -0
- package/dist/components/builder/composition-field-editor.js.map +1 -0
- package/dist/components/builder/composition-scope.d.ts +16 -0
- package/dist/components/builder/composition-scope.js +7 -0
- package/dist/components/builder/composition-scope.js.map +1 -0
- package/dist/components/builder/drag-session.d.ts +41 -0
- package/dist/components/builder/drag-session.js +51 -0
- package/dist/components/builder/drag-session.js.map +1 -0
- package/dist/components/builder/fixture-thumbnail.d.ts +18 -0
- package/dist/components/builder/fixture-thumbnail.js +97 -0
- package/dist/components/builder/fixture-thumbnail.js.map +1 -0
- package/dist/components/builder/inline-edit.d.ts +142 -0
- package/dist/components/builder/inline-edit.js +136 -0
- package/dist/components/builder/inline-edit.js.map +1 -0
- package/dist/components/builder/insert-context.d.ts +14 -0
- package/dist/components/builder/insert-context.js +65 -0
- package/dist/components/builder/insert-context.js.map +1 -0
- package/dist/components/builder/insert-menu.d.ts +9 -0
- package/dist/components/builder/insert-menu.js +9 -0
- package/dist/components/builder/insert-menu.js.map +1 -0
- package/dist/components/builder/layout-inspector.d.ts +14 -0
- package/dist/components/builder/layout-inspector.js +84 -0
- package/dist/components/builder/layout-inspector.js.map +1 -0
- package/dist/components/builder/node-factory.d.ts +17 -0
- package/dist/components/builder/node-factory.js +90 -0
- package/dist/components/builder/node-factory.js.map +1 -0
- package/dist/components/builder/page-tree.d.ts +3 -0
- package/dist/components/builder/page-tree.js +207 -0
- package/dist/components/builder/page-tree.js.map +1 -0
- package/dist/components/builder/property-inspector.d.ts +1 -0
- package/dist/components/builder/property-inspector.js +233 -0
- package/dist/components/builder/property-inspector.js.map +1 -0
- package/dist/components/builder/rich-text-toolbar.d.ts +4 -0
- package/dist/components/builder/rich-text-toolbar.js +104 -0
- package/dist/components/builder/rich-text-toolbar.js.map +1 -0
- package/dist/components/builder/spacing-handles.d.ts +22 -0
- package/dist/components/builder/spacing-handles.js +213 -0
- package/dist/components/builder/spacing-handles.js.map +1 -0
- package/dist/components/cms/asset-picker.d.ts +12 -0
- package/dist/components/cms/asset-picker.js +60 -0
- package/dist/components/cms/asset-picker.js.map +1 -0
- package/dist/components/cms/bulk-actions.d.ts +60 -0
- package/dist/components/cms/bulk-actions.js +95 -0
- package/dist/components/cms/bulk-actions.js.map +1 -0
- package/dist/components/cms/bulk-bar.d.ts +23 -0
- package/dist/components/cms/bulk-bar.js +26 -0
- package/dist/components/cms/bulk-bar.js.map +1 -0
- package/dist/components/cms/cell-edit.d.ts +91 -0
- package/dist/components/cms/cell-edit.js +144 -0
- package/dist/components/cms/cell-edit.js.map +1 -0
- package/dist/components/cms/cell.d.ts +29 -0
- package/dist/components/cms/cell.js +127 -0
- package/dist/components/cms/cell.js.map +1 -0
- package/dist/components/cms/cms-index.d.ts +8 -0
- package/dist/components/cms/cms-index.js +23 -0
- package/dist/components/cms/cms-index.js.map +1 -0
- package/dist/components/cms/cms-store.d.ts +188 -0
- package/dist/components/cms/cms-store.js +702 -0
- package/dist/components/cms/cms-store.js.map +1 -0
- package/dist/components/cms/collection-table.d.ts +6 -0
- package/dist/components/cms/collection-table.js +250 -0
- package/dist/components/cms/collection-table.js.map +1 -0
- package/dist/components/cms/collections-sidebar.d.ts +9 -0
- package/dist/components/cms/collections-sidebar.js +25 -0
- package/dist/components/cms/collections-sidebar.js.map +1 -0
- package/dist/components/cms/detail-editor.d.ts +7 -0
- package/dist/components/cms/detail-editor.js +386 -0
- package/dist/components/cms/detail-editor.js.map +1 -0
- package/dist/components/cms/doc-state.d.ts +13 -0
- package/dist/components/cms/doc-state.js +18 -0
- package/dist/components/cms/doc-state.js.map +1 -0
- package/dist/components/cms/document-editor-context.d.ts +48 -0
- package/dist/components/cms/document-editor-context.js +23 -0
- package/dist/components/cms/document-editor-context.js.map +1 -0
- package/dist/components/cms/editable-cell.d.ts +22 -0
- package/dist/components/cms/editable-cell.js +176 -0
- package/dist/components/cms/editable-cell.js.map +1 -0
- package/dist/components/cms/field-control.d.ts +8 -0
- package/dist/components/cms/field-control.js +149 -0
- package/dist/components/cms/field-control.js.map +1 -0
- package/dist/components/cms/filter-builder.d.ts +12 -0
- package/dist/components/cms/filter-builder.js +84 -0
- package/dist/components/cms/filter-builder.js.map +1 -0
- package/dist/components/cms/import-export.d.ts +6 -0
- package/dist/components/cms/import-export.js +33 -0
- package/dist/components/cms/import-export.js.map +1 -0
- package/dist/components/cms/new-page-dialog.d.ts +22 -0
- package/dist/components/cms/new-page-dialog.js +42 -0
- package/dist/components/cms/new-page-dialog.js.map +1 -0
- package/dist/components/cms/reference-confirm.d.ts +25 -0
- package/dist/components/cms/reference-confirm.js +124 -0
- package/dist/components/cms/reference-confirm.js.map +1 -0
- package/dist/components/cms/relation-picker.d.ts +16 -0
- package/dist/components/cms/relation-picker.js +47 -0
- package/dist/components/cms/relation-picker.js.map +1 -0
- package/dist/components/cms/use-cell-edit.d.ts +33 -0
- package/dist/components/cms/use-cell-edit.js +125 -0
- package/dist/components/cms/use-cell-edit.js.map +1 -0
- package/dist/components/cms/versions-model.d.ts +15 -0
- package/dist/components/cms/versions-model.js +23 -0
- package/dist/components/cms/versions-model.js.map +1 -0
- package/dist/components/cms/versions-pane.d.ts +14 -0
- package/dist/components/cms/versions-pane.js +86 -0
- package/dist/components/cms/versions-pane.js.map +1 -0
- package/dist/components/cms/view-bar.d.ts +11 -0
- package/dist/components/cms/view-bar.js +80 -0
- package/dist/components/cms/view-bar.js.map +1 -0
- package/dist/components/collab/collab-context.d.ts +34 -0
- package/dist/components/collab/collab-context.js +52 -0
- package/dist/components/collab/collab-context.js.map +1 -0
- package/dist/components/collab/collab-seed.d.ts +27 -0
- package/dist/components/collab/collab-seed.js +42 -0
- package/dist/components/collab/collab-seed.js.map +1 -0
- package/dist/components/components/component-detail.d.ts +9 -0
- package/dist/components/components/component-detail.js +30 -0
- package/dist/components/components/component-detail.js.map +1 -0
- package/dist/components/components/component-status.d.ts +10 -0
- package/dist/components/components/component-status.js +30 -0
- package/dist/components/components/component-status.js.map +1 -0
- package/dist/components/components/components-index.d.ts +8 -0
- package/dist/components/components/components-index.js +42 -0
- package/dist/components/components/components-index.js.map +1 -0
- package/dist/components/components/field-inspector.d.ts +9 -0
- package/dist/components/components/field-inspector.js +38 -0
- package/dist/components/components/field-inspector.js.map +1 -0
- package/dist/components/components/fixture-runner.d.ts +10 -0
- package/dist/components/components/fixture-runner.js +24 -0
- package/dist/components/components/fixture-runner.js.map +1 -0
- package/dist/components/components/registry-browser.d.ts +4 -0
- package/dist/components/components/registry-browser.js +64 -0
- package/dist/components/components/registry-browser.js.map +1 -0
- package/dist/components/current-user-menu.d.ts +13 -0
- package/dist/components/current-user-menu.js +31 -0
- package/dist/components/current-user-menu.js.map +1 -0
- package/dist/components/dev-login-badge.d.ts +9 -0
- package/dist/components/dev-login-badge.js +18 -0
- package/dist/components/dev-login-badge.js.map +1 -0
- package/dist/components/extensions/extensions-view.d.ts +7 -0
- package/dist/components/extensions/extensions-view.js +126 -0
- package/dist/components/extensions/extensions-view.js.map +1 -0
- package/dist/components/guards.d.ts +27 -0
- package/dist/components/guards.js +46 -0
- package/dist/components/guards.js.map +1 -0
- package/dist/components/inspector/inspector-sidebar.d.ts +26 -0
- package/dist/components/inspector/inspector-sidebar.js +17 -0
- package/dist/components/inspector/inspector-sidebar.js.map +1 -0
- package/dist/components/operation-error.d.ts +17 -0
- package/dist/components/operation-error.js +54 -0
- package/dist/components/operation-error.js.map +1 -0
- package/dist/components/playground-badge.d.ts +10 -0
- package/dist/components/playground-badge.js +19 -0
- package/dist/components/playground-badge.js.map +1 -0
- package/dist/components/project-list.d.ts +15 -0
- package/dist/components/project-list.js +52 -0
- package/dist/components/project-list.js.map +1 -0
- package/dist/components/project-session.d.ts +46 -0
- package/dist/components/project-session.js +107 -0
- package/dist/components/project-session.js.map +1 -0
- package/dist/components/project-switcher.d.ts +11 -0
- package/dist/components/project-switcher.js +44 -0
- package/dist/components/project-switcher.js.map +1 -0
- package/dist/components/references/builder-deep-link.d.ts +12 -0
- package/dist/components/references/builder-deep-link.js +53 -0
- package/dist/components/references/builder-deep-link.js.map +1 -0
- package/dist/components/references/references-panel.d.ts +17 -0
- package/dist/components/references/references-panel.js +66 -0
- package/dist/components/references/references-panel.js.map +1 -0
- package/dist/components/routes/redirect-chain.d.ts +9 -0
- package/dist/components/routes/redirect-chain.js +16 -0
- package/dist/components/routes/redirect-chain.js.map +1 -0
- package/dist/components/routes/redirect-manager.d.ts +13 -0
- package/dist/components/routes/redirect-manager.js +24 -0
- package/dist/components/routes/redirect-manager.js.map +1 -0
- package/dist/components/routes/route-list.d.ts +35 -0
- package/dist/components/routes/route-list.js +69 -0
- package/dist/components/routes/route-list.js.map +1 -0
- package/dist/components/routes/routes-shell.d.ts +14 -0
- package/dist/components/routes/routes-shell.js +68 -0
- package/dist/components/routes/routes-shell.js.map +1 -0
- package/dist/components/section-nav.d.ts +17 -0
- package/dist/components/section-nav.js +33 -0
- package/dist/components/section-nav.js.map +1 -0
- package/dist/components/section-state.d.ts +24 -0
- package/dist/components/section-state.js +78 -0
- package/dist/components/section-state.js.map +1 -0
- package/dist/components/section-surface.d.ts +31 -0
- package/dist/components/section-surface.js +46 -0
- package/dist/components/section-surface.js.map +1 -0
- package/dist/components/sections.d.ts +38 -0
- package/dist/components/sections.js +72 -0
- package/dist/components/sections.js.map +1 -0
- package/dist/components/settings-form.d.ts +10 -0
- package/dist/components/settings-form.js +17 -0
- package/dist/components/settings-form.js.map +1 -0
- package/dist/components/studio-context.d.ts +22 -0
- package/dist/components/studio-context.js +48 -0
- package/dist/components/studio-context.js.map +1 -0
- package/dist/components/studio-project-screen.d.ts +19 -0
- package/dist/components/studio-project-screen.js +195 -0
- package/dist/components/studio-project-screen.js.map +1 -0
- package/dist/components/studio-shell.d.ts +18 -0
- package/dist/components/studio-shell.js +64 -0
- package/dist/components/studio-shell.js.map +1 -0
- package/dist/components/theme-context.d.ts +17 -0
- package/dist/components/theme-context.js +26 -0
- package/dist/components/theme-context.js.map +1 -0
- package/dist/components/workspace-gate.d.ts +12 -0
- package/dist/components/workspace-gate.js +27 -0
- package/dist/components/workspace-gate.js.map +1 -0
- package/dist/components/workspace-projects.d.ts +13 -0
- package/dist/components/workspace-projects.js +18 -0
- package/dist/components/workspace-projects.js.map +1 -0
- package/dist/lib/assets/delete-asset.d.ts +34 -0
- package/dist/lib/assets/delete-asset.js +34 -0
- package/dist/lib/assets/delete-asset.js.map +1 -0
- package/dist/lib/assets/image-dimensions.d.ts +18 -0
- package/dist/lib/assets/image-dimensions.js +26 -0
- package/dist/lib/assets/image-dimensions.js.map +1 -0
- package/dist/lib/assets/index.d.ts +9 -0
- package/dist/lib/assets/index.js +10 -0
- package/dist/lib/assets/index.js.map +1 -0
- package/dist/lib/assets/upload-state.d.ts +53 -0
- package/dist/lib/assets/upload-state.js +59 -0
- package/dist/lib/assets/upload-state.js.map +1 -0
- package/dist/lib/assets/upload.d.ts +56 -0
- package/dist/lib/assets/upload.js +125 -0
- package/dist/lib/assets/upload.js.map +1 -0
- package/dist/lib/auth/auth-adapter.d.ts +57 -0
- package/dist/lib/auth/auth-adapter.js +13 -0
- package/dist/lib/auth/auth-adapter.js.map +1 -0
- package/dist/lib/auth/convex-auth-adapter.d.ts +39 -0
- package/dist/lib/auth/convex-auth-adapter.js +171 -0
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -0
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +25 -0
- package/dist/lib/auth/dev-auto-login-adapter.js +40 -0
- package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -0
- package/dist/lib/auth/index.d.ts +28 -0
- package/dist/lib/auth/index.js +61 -0
- package/dist/lib/auth/index.js.map +1 -0
- package/dist/lib/auth/local-auth-adapter.d.ts +57 -0
- package/dist/lib/auth/local-auth-adapter.js +78 -0
- package/dist/lib/auth/local-auth-adapter.js.map +1 -0
- package/dist/lib/auth/route-guard.d.ts +33 -0
- package/dist/lib/auth/route-guard.js +51 -0
- package/dist/lib/auth/route-guard.js.map +1 -0
- package/dist/lib/auth-errors.d.ts +42 -0
- package/dist/lib/auth-errors.js +74 -0
- package/dist/lib/auth-errors.js.map +1 -0
- package/dist/lib/bindings/section-bindings.d.ts +24 -0
- package/dist/lib/bindings/section-bindings.js +9 -0
- package/dist/lib/bindings/section-bindings.js.map +1 -0
- package/dist/lib/clipboard/index.d.ts +7 -0
- package/dist/lib/clipboard/index.js +8 -0
- package/dist/lib/clipboard/index.js.map +1 -0
- package/dist/lib/clipboard/os-clipboard.d.ts +46 -0
- package/dist/lib/clipboard/os-clipboard.js +105 -0
- package/dist/lib/clipboard/os-clipboard.js.map +1 -0
- package/dist/lib/clipboard/paste-issues.d.ts +34 -0
- package/dist/lib/clipboard/paste-issues.js +67 -0
- package/dist/lib/clipboard/paste-issues.js.map +1 -0
- package/dist/lib/clipboard/paste-target.d.ts +20 -0
- package/dist/lib/clipboard/paste-target.js +28 -0
- package/dist/lib/clipboard/paste-target.js.map +1 -0
- package/dist/lib/cms/published-documents.d.ts +30 -0
- package/dist/lib/cms/published-documents.js +75 -0
- package/dist/lib/cms/published-documents.js.map +1 -0
- package/dist/lib/cms-views/index.d.ts +9 -0
- package/dist/lib/cms-views/index.js +10 -0
- package/dist/lib/cms-views/index.js.map +1 -0
- package/dist/lib/cms-views/selection.d.ts +29 -0
- package/dist/lib/cms-views/selection.js +46 -0
- package/dist/lib/cms-views/selection.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +192 -0
- package/dist/lib/cms-views/view-model.js +353 -0
- package/dist/lib/cms-views/view-model.js.map +1 -0
- package/dist/lib/cms-views/views-store.d.ts +70 -0
- package/dist/lib/cms-views/views-store.js +129 -0
- package/dist/lib/cms-views/views-store.js.map +1 -0
- package/dist/lib/data/collection-content.d.ts +105 -0
- package/dist/lib/data/collection-content.js +196 -0
- package/dist/lib/data/collection-content.js.map +1 -0
- package/dist/lib/data/data-store.d.ts +97 -0
- package/dist/lib/data/data-store.js +205 -0
- package/dist/lib/data/data-store.js.map +1 -0
- package/dist/lib/data/index.d.ts +4 -0
- package/dist/lib/data/index.js +8 -0
- package/dist/lib/data/index.js.map +1 -0
- package/dist/lib/data/providers.d.ts +143 -0
- package/dist/lib/data/providers.js +255 -0
- package/dist/lib/data/providers.js.map +1 -0
- package/dist/lib/data/sample-capture.d.ts +30 -0
- package/dist/lib/data/sample-capture.js +70 -0
- package/dist/lib/data/sample-capture.js.map +1 -0
- package/dist/lib/data/sources-store.d.ts +42 -0
- package/dist/lib/data/sources-store.js +120 -0
- package/dist/lib/data/sources-store.js.map +1 -0
- package/dist/lib/dnd/drop-rules.d.ts +219 -0
- package/dist/lib/dnd/drop-rules.js +257 -0
- package/dist/lib/dnd/drop-rules.js.map +1 -0
- package/dist/lib/dnd/gap-drag.d.ts +66 -0
- package/dist/lib/dnd/gap-drag.js +95 -0
- package/dist/lib/dnd/gap-drag.js.map +1 -0
- package/dist/lib/locale/binding-fallback.d.ts +28 -0
- package/dist/lib/locale/binding-fallback.js +66 -0
- package/dist/lib/locale/binding-fallback.js.map +1 -0
- package/dist/lib/locale/fallback.d.ts +43 -0
- package/dist/lib/locale/fallback.js +52 -0
- package/dist/lib/locale/fallback.js.map +1 -0
- package/dist/lib/locale/index.d.ts +6 -0
- package/dist/lib/locale/index.js +7 -0
- package/dist/lib/locale/index.js.map +1 -0
- package/dist/lib/locale/locale-store.d.ts +40 -0
- package/dist/lib/locale/locale-store.js +61 -0
- package/dist/lib/locale/locale-store.js.map +1 -0
- package/dist/lib/locale/locale-switcher.d.ts +10 -0
- package/dist/lib/locale/locale-switcher.js +24 -0
- package/dist/lib/locale/locale-switcher.js.map +1 -0
- package/dist/lib/locale/route-variant.d.ts +14 -0
- package/dist/lib/locale/route-variant.js +11 -0
- package/dist/lib/locale/route-variant.js.map +1 -0
- package/dist/lib/locale/use-studio-locale.d.ts +10 -0
- package/dist/lib/locale/use-studio-locale.js +8 -0
- package/dist/lib/locale/use-studio-locale.js.map +1 -0
- package/dist/lib/operations/asset-repository.d.ts +15 -0
- package/dist/lib/operations/asset-repository.js +56 -0
- package/dist/lib/operations/asset-repository.js.map +1 -0
- package/dist/lib/operations/cms-persistence.d.ts +35 -0
- package/dist/lib/operations/cms-persistence.js +75 -0
- package/dist/lib/operations/cms-persistence.js.map +1 -0
- package/dist/lib/operations/graph-commands.d.ts +140 -0
- package/dist/lib/operations/graph-commands.js +86 -0
- package/dist/lib/operations/graph-commands.js.map +1 -0
- package/dist/lib/operations/graph-persistence.d.ts +27 -0
- package/dist/lib/operations/graph-persistence.js +35 -0
- package/dist/lib/operations/graph-persistence.js.map +1 -0
- package/dist/lib/operations/index.d.ts +13 -0
- package/dist/lib/operations/index.js +14 -0
- package/dist/lib/operations/index.js.map +1 -0
- package/dist/lib/operations/operations-package.d.ts +6 -0
- package/dist/lib/operations/operations-package.js +7 -0
- package/dist/lib/operations/operations-package.js.map +1 -0
- package/dist/lib/operations/persist-queue.d.ts +16 -0
- package/dist/lib/operations/persist-queue.js +18 -0
- package/dist/lib/operations/persist-queue.js.map +1 -0
- package/dist/lib/operations/session-hydration.d.ts +84 -0
- package/dist/lib/operations/session-hydration.js +112 -0
- package/dist/lib/operations/session-hydration.js.map +1 -0
- package/dist/lib/operations/studio-operations.d.ts +59 -0
- package/dist/lib/operations/studio-operations.js +92 -0
- package/dist/lib/operations/studio-operations.js.map +1 -0
- package/dist/lib/persistence/backend-mode.d.ts +69 -0
- package/dist/lib/persistence/backend-mode.js +120 -0
- package/dist/lib/persistence/backend-mode.js.map +1 -0
- package/dist/lib/persistence/convex-adapter.d.ts +34 -0
- package/dist/lib/persistence/convex-adapter.js +681 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -0
- package/dist/lib/persistence/convex-blob-storage.d.ts +57 -0
- package/dist/lib/persistence/convex-blob-storage.js +97 -0
- package/dist/lib/persistence/convex-blob-storage.js.map +1 -0
- package/dist/lib/persistence/convex-client.d.ts +19 -0
- package/dist/lib/persistence/convex-client.js +40 -0
- package/dist/lib/persistence/convex-client.js.map +1 -0
- package/dist/lib/persistence/index.d.ts +30 -0
- package/dist/lib/persistence/index.js +60 -0
- package/dist/lib/persistence/index.js.map +1 -0
- package/dist/lib/persona/labels.d.ts +23 -0
- package/dist/lib/persona/labels.js +122 -0
- package/dist/lib/persona/labels.js.map +1 -0
- package/dist/lib/rail.d.ts +11 -0
- package/dist/lib/rail.js +26 -0
- package/dist/lib/rail.js.map +1 -0
- package/dist/lib/references/impact.d.ts +64 -0
- package/dist/lib/references/impact.js +62 -0
- package/dist/lib/references/impact.js.map +1 -0
- package/dist/lib/references/index.d.ts +7 -0
- package/dist/lib/references/index.js +8 -0
- package/dist/lib/references/index.js.map +1 -0
- package/dist/lib/references/reference-model.d.ts +125 -0
- package/dist/lib/references/reference-model.js +242 -0
- package/dist/lib/references/reference-model.js.map +1 -0
- package/dist/lib/references/references-store.d.ts +65 -0
- package/dist/lib/references/references-store.js +221 -0
- package/dist/lib/references/references-store.js.map +1 -0
- package/dist/lib/registry/index.d.ts +65 -0
- package/dist/lib/registry/index.js +114 -0
- package/dist/lib/registry/index.js.map +1 -0
- package/dist/lib/registry/preview.d.ts +15 -0
- package/dist/lib/registry/preview.js +110 -0
- package/dist/lib/registry/preview.js.map +1 -0
- package/dist/lib/registry/project-components.d.ts +16 -0
- package/dist/lib/registry/project-components.js +152 -0
- package/dist/lib/registry/project-components.js.map +1 -0
- package/dist/lib/routing/index.d.ts +10 -0
- package/dist/lib/routing/index.js +11 -0
- package/dist/lib/routing/index.js.map +1 -0
- package/dist/lib/routing/routing-analysis.d.ts +26 -0
- package/dist/lib/routing/routing-analysis.js +37 -0
- package/dist/lib/routing/routing-analysis.js.map +1 -0
- package/dist/lib/routing/routing-store.d.ts +50 -0
- package/dist/lib/routing/routing-store.js +72 -0
- package/dist/lib/routing/routing-store.js.map +1 -0
- package/dist/lib/starters/kit-session.d.ts +5 -0
- package/dist/lib/starters/kit-session.js +17 -0
- package/dist/lib/starters/kit-session.js.map +1 -0
- package/dist/lib/theme.d.ts +21 -0
- package/dist/lib/theme.js +32 -0
- package/dist/lib/theme.js.map +1 -0
- package/dist/lib/utils.d.ts +11 -0
- package/dist/lib/utils.js +28 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/webhooks/config.d.ts +15 -0
- package/dist/lib/webhooks/config.js +64 -0
- package/dist/lib/webhooks/config.js.map +1 -0
- package/dist/lib/webhooks/dispatcher.d.ts +67 -0
- package/dist/lib/webhooks/dispatcher.js +61 -0
- package/dist/lib/webhooks/dispatcher.js.map +1 -0
- package/dist/lib/webhooks/index.d.ts +11 -0
- package/dist/lib/webhooks/index.js +10 -0
- package/dist/lib/webhooks/index.js.map +1 -0
- package/dist/lib/webhooks/signing.d.ts +15 -0
- package/dist/lib/webhooks/signing.js +24 -0
- package/dist/lib/webhooks/signing.js.map +1 -0
- package/dist/lib/webhooks/tags.d.ts +25 -0
- package/dist/lib/webhooks/tags.js +173 -0
- package/dist/lib/webhooks/tags.js.map +1 -0
- package/dist/lib/working-graph.d.ts +82 -0
- package/dist/lib/working-graph.js +113 -0
- package/dist/lib/working-graph.js.map +1 -0
- package/dist/lib/workspace/index.d.ts +109 -0
- package/dist/lib/workspace/index.js +206 -0
- package/dist/lib/workspace/index.js.map +1 -0
- package/dist/lib/workspace/switcher-source.d.ts +39 -0
- package/dist/lib/workspace/switcher-source.js +23 -0
- package/dist/lib/workspace/switcher-source.js.map +1 -0
- package/dist/lib/workspace/workspace-source.d.ts +47 -0
- package/dist/lib/workspace/workspace-source.js +67 -0
- package/dist/lib/workspace/workspace-source.js.map +1 -0
- package/dist/router/index.d.ts +32 -0
- package/dist/router/index.js +32 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/match.d.ts +75 -0
- package/dist/router/match.js +87 -0
- package/dist/router/match.js.map +1 -0
- package/dist/router/search.d.ts +22 -0
- package/dist/router/search.js +16 -0
- package/dist/router/search.js.map +1 -0
- package/dist/ui/avatar.d.ts +17 -0
- package/dist/ui/avatar.js +38 -0
- package/dist/ui/avatar.js.map +1 -0
- package/dist/ui/badge.d.ts +9 -0
- package/dist/ui/badge.js +23 -0
- package/dist/ui/badge.js.map +1 -0
- package/dist/ui/button.d.ts +11 -0
- package/dist/ui/button.js +30 -0
- package/dist/ui/button.js.map +1 -0
- package/dist/ui/card.d.ts +11 -0
- package/dist/ui/card.js +23 -0
- package/dist/ui/card.js.map +1 -0
- package/dist/ui/dialog.d.ts +12 -0
- package/dist/ui/dialog.js +27 -0
- package/dist/ui/dialog.js.map +1 -0
- package/dist/ui/dropdown-menu.d.ts +15 -0
- package/dist/ui/dropdown-menu.js +24 -0
- package/dist/ui/dropdown-menu.js.map +1 -0
- package/dist/ui/field.d.ts +15 -0
- package/dist/ui/field.js +14 -0
- package/dist/ui/field.js.map +1 -0
- package/dist/ui/index.d.ts +14 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/input.d.ts +3 -0
- package/dist/ui/input.js +7 -0
- package/dist/ui/input.js.map +1 -0
- package/dist/ui/inspector.d.ts +18 -0
- package/dist/ui/inspector.js +14 -0
- package/dist/ui/inspector.js.map +1 -0
- package/dist/ui/layout.d.ts +45 -0
- package/dist/ui/layout.js +48 -0
- package/dist/ui/layout.js.map +1 -0
- package/dist/ui/panel.d.ts +12 -0
- package/dist/ui/panel.js +20 -0
- package/dist/ui/panel.js.map +1 -0
- package/dist/ui/separator.d.ts +6 -0
- package/dist/ui/separator.js +7 -0
- package/dist/ui/separator.js.map +1 -0
- package/dist/ui/toolbar.d.ts +9 -0
- package/dist/ui/toolbar.js +16 -0
- package/dist/ui/toolbar.js.map +1 -0
- package/dist/ui/typography.d.ts +50 -0
- package/dist/ui/typography.js +43 -0
- package/dist/ui/typography.js.map +1 -0
- package/package.json +149 -0
- package/src/styles/studio.css +101 -0
- package/src/styles/tokens.css +63 -0
package/convex/sync.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { makeFunctionReference } from 'convex/server'
|
|
2
|
+
import type { GenericActionCtx, GenericDataModel } from 'convex/server'
|
|
3
|
+
import { v } from 'convex/values'
|
|
4
|
+
import { verifyCliToken } from '@elytracms/operations'
|
|
5
|
+
import type { VerifiedCliToken } from '@elytracms/operations'
|
|
6
|
+
import type { CliTokenRepository, PersistenceAdapter } from '@elytracms/persistence'
|
|
7
|
+
import { httpAction, internalQuery } from './functions'
|
|
8
|
+
import type { QueryCtx } from './functions'
|
|
9
|
+
import type { ProjectRole } from './guard'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* EC-157 — the builder-sync HTTP endpoint. Serves the contract fixed in
|
|
13
|
+
* `@elytracms/export-sync`'s `registrySyncEndpoints` from this deployment's
|
|
14
|
+
* `.convex.site` origin, so a host repo's `builder check` / `typegen` run against
|
|
15
|
+
* the real builder instead of a `fetchImpl` double:
|
|
16
|
+
*
|
|
17
|
+
* - `GET /api/projects/:projectId/registry-sync/snapshot?mode=&environment=` — the
|
|
18
|
+
* typegen/check snapshot: locale config and the graph revision for the requested
|
|
19
|
+
* mode. Collection schemas and routes/redirects are config-owned (EC-183/EC-207,
|
|
20
|
+
* AD-11) — the CLI reads them from `elytra.config.ts`, not this snapshot.
|
|
21
|
+
*
|
|
22
|
+
* ## Package-shipped, deployment-agnostic (EC-223/EC-252)
|
|
23
|
+
*
|
|
24
|
+
* This module ships inside `@elytracms/studio`; the consumer's `convex/sync.ts` is a
|
|
25
|
+
* one-line `export *` shim. It therefore NEVER imports the per-deployment
|
|
26
|
+
* `./_generated/api` — it addresses sibling functions by STRING ref via
|
|
27
|
+
* `makeFunctionReference` (resolved against the consumer's deployment), exactly like
|
|
28
|
+
* `publishing.ts`. That decoupling is also what makes it typecheck under a cross-repo
|
|
29
|
+
* `link:` loop: there is no `ApiFromModules` inference to unify across two `convex`
|
|
30
|
+
* instances (the bug EC-252 fixes — a copied `sync.ts` collapsed `api.cliTokens.byHash`
|
|
31
|
+
* to `{}`).
|
|
32
|
+
*
|
|
33
|
+
* ## Auth (EC-150 CLI tokens)
|
|
34
|
+
*
|
|
35
|
+
* The endpoint requires `Authorization: Bearer <ect_…>`. Tokens are verified by
|
|
36
|
+
* `verifyCliToken` (`@elytracms/operations`): the presented plaintext is SHA-256-hashed
|
|
37
|
+
* and looked up via `cliTokens.byHash`; unknown and revoked tokens are rejected with
|
|
38
|
+
* a 401. Single-tenant (EC-182): a deployment hosts one project, so a token carries
|
|
39
|
+
* no per-project scope — it just needs at least the `viewer` capability the snapshot
|
|
40
|
+
* read requires.
|
|
41
|
+
*
|
|
42
|
+
* ## Error contract
|
|
43
|
+
*
|
|
44
|
+
* Failures are structured JSON bodies — `{ error: { code, message, issues? } }` —
|
|
45
|
+
* matching `RegistrySyncErrorBody` in `@elytracms/export-sync`; even unexpected
|
|
46
|
+
* exceptions return a structured body, never an opaque crash page. The CLI surfaces
|
|
47
|
+
* them as `transport` issues.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
type ActionCtx = GenericActionCtx<GenericDataModel>
|
|
51
|
+
|
|
52
|
+
// Sibling functions are addressed by STRING ref (resolved against the consumer's
|
|
53
|
+
// deployment), never `./_generated/api` — see the module note. The explicit arg +
|
|
54
|
+
// return generics keep the call sites typed without `ApiFromModules` inference.
|
|
55
|
+
type CliTokenRow = {
|
|
56
|
+
tokenId: string
|
|
57
|
+
label: string
|
|
58
|
+
tokenHash: string
|
|
59
|
+
createdAt: string
|
|
60
|
+
revokedAt?: string
|
|
61
|
+
}
|
|
62
|
+
const cliTokensByHash = makeFunctionReference<
|
|
63
|
+
'query',
|
|
64
|
+
{ tokenHash: string },
|
|
65
|
+
CliTokenRow | null
|
|
66
|
+
>('cliTokens:byHash')
|
|
67
|
+
const syncExportSnapshot = makeFunctionReference<
|
|
68
|
+
'query',
|
|
69
|
+
{ projectId: string; mode: SnapshotMode; environment: string },
|
|
70
|
+
BuilderSnapshot
|
|
71
|
+
>('sync:exportSnapshot')
|
|
72
|
+
|
|
73
|
+
// --- Structured responses ----------------------------------------------------
|
|
74
|
+
|
|
75
|
+
interface SyncErrorIssue {
|
|
76
|
+
code: string
|
|
77
|
+
message: string
|
|
78
|
+
componentId?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function jsonResponse(value: unknown, status = 200): Response {
|
|
82
|
+
return new Response(JSON.stringify(value), {
|
|
83
|
+
status,
|
|
84
|
+
headers: { 'content-type': 'application/json' },
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function errorResponse(
|
|
89
|
+
status: number,
|
|
90
|
+
code: string,
|
|
91
|
+
message: string,
|
|
92
|
+
issues?: SyncErrorIssue[],
|
|
93
|
+
): Response {
|
|
94
|
+
return jsonResponse(
|
|
95
|
+
{ error: { code, message, ...(issues && issues.length > 0 ? { issues } : {}) } },
|
|
96
|
+
status,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- Routing -----------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
const PROJECTS_PREFIX = '/api/projects/'
|
|
103
|
+
|
|
104
|
+
type SyncEndpoint = 'snapshot'
|
|
105
|
+
|
|
106
|
+
interface SyncRoute {
|
|
107
|
+
projectId: string
|
|
108
|
+
endpoint: SyncEndpoint
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Parse `/api/projects/:projectId/registry-sync/<rest>` (Convex routes by prefix only). */
|
|
112
|
+
function parseSyncRoute(method: string, pathname: string): SyncRoute | undefined {
|
|
113
|
+
if (!pathname.startsWith(PROJECTS_PREFIX)) return undefined
|
|
114
|
+
const segments = pathname
|
|
115
|
+
.slice(PROJECTS_PREFIX.length)
|
|
116
|
+
.split('/')
|
|
117
|
+
.map((segment) => decodeURIComponent(segment))
|
|
118
|
+
const [projectId, scope, ...rest] = segments
|
|
119
|
+
if (!projectId || scope !== 'registry-sync') return undefined
|
|
120
|
+
const tail = rest.join('/')
|
|
121
|
+
if (tail === 'snapshot' && method === 'GET') return { projectId, endpoint: 'snapshot' }
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Token auth (EC-150) -------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
const ROLE_RANK: Record<ProjectRole, number> = { viewer: 0, editor: 1, admin: 2 }
|
|
128
|
+
|
|
129
|
+
/** What each endpoint requires; the snapshot read is `viewer`-level (EC-150). */
|
|
130
|
+
const REQUIRED_ROLE: Record<SyncEndpoint, ProjectRole> = {
|
|
131
|
+
snapshot: 'viewer',
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* A {@link CliTokenRepository} over this deployment's `cliTokens` table, narrowed to
|
|
136
|
+
* the verification path. `verifyCliToken` only ever calls `findByHash`; the write
|
|
137
|
+
* methods live behind the admin-only operations layer and are not reachable here.
|
|
138
|
+
*/
|
|
139
|
+
function cliTokenRepository(ctx: ActionCtx): CliTokenRepository {
|
|
140
|
+
const unsupported = (method: string) =>
|
|
141
|
+
Promise.reject(new Error(`cli-token ${method} is not available on the sync endpoint`))
|
|
142
|
+
return {
|
|
143
|
+
createToken: () => unsupported('createToken'),
|
|
144
|
+
listTokens: () => unsupported('listTokens'),
|
|
145
|
+
revokeToken: () => unsupported('revokeToken'),
|
|
146
|
+
async findByHash(tokenHash) {
|
|
147
|
+
const doc = await ctx.runQuery(cliTokensByHash, { tokenHash })
|
|
148
|
+
if (!doc) return undefined
|
|
149
|
+
return {
|
|
150
|
+
id: doc.tokenId,
|
|
151
|
+
label: doc.label,
|
|
152
|
+
tokenHash: doc.tokenHash,
|
|
153
|
+
createdAt: doc.createdAt,
|
|
154
|
+
...(doc.revokedAt ? { revokedAt: doc.revokedAt } : {}),
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type AuthResult = { ok: true; verified: VerifiedCliToken } | { ok: false; response: Response }
|
|
161
|
+
|
|
162
|
+
/** Resolve and verify the Bearer CLI token, then check it grants the required role. */
|
|
163
|
+
async function authorize(
|
|
164
|
+
ctx: ActionCtx,
|
|
165
|
+
request: Request,
|
|
166
|
+
route: SyncRoute,
|
|
167
|
+
): Promise<AuthResult> {
|
|
168
|
+
const header = request.headers.get('authorization')
|
|
169
|
+
if (!header || !header.toLowerCase().startsWith('bearer ')) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
response: errorResponse(
|
|
173
|
+
401,
|
|
174
|
+
'unauthorized',
|
|
175
|
+
'Missing CLI token — send "Authorization: Bearer <token>" (issue one with tokens.issue).',
|
|
176
|
+
),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const token = header.slice('bearer '.length).trim()
|
|
180
|
+
// verifyCliToken only reads `adapter.cliTokens`; the rest of the adapter surface
|
|
181
|
+
// is deliberately absent here (the assertion documents exactly that contract).
|
|
182
|
+
const adapter = { cliTokens: cliTokenRepository(ctx) } as PersistenceAdapter
|
|
183
|
+
const verified = await verifyCliToken(adapter, token)
|
|
184
|
+
if (verified === null) {
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
response: errorResponse(401, 'unauthorized', 'Unknown, malformed, or revoked CLI token.'),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (ROLE_RANK[verified.role] < ROLE_RANK[REQUIRED_ROLE[route.endpoint]]) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
response: errorResponse(
|
|
194
|
+
403,
|
|
195
|
+
'forbidden',
|
|
196
|
+
`This endpoint requires the "${REQUIRED_ROLE[route.endpoint]}" role (token grants "${verified.role}").`,
|
|
197
|
+
),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, verified }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- The HTTP action ------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
export const registrySync = httpAction(async (ctx, request) => {
|
|
206
|
+
const pathname = new URL(request.url).pathname
|
|
207
|
+
try {
|
|
208
|
+
const route = parseSyncRoute(request.method, pathname)
|
|
209
|
+
if (!route) {
|
|
210
|
+
return errorResponse(
|
|
211
|
+
404,
|
|
212
|
+
'not-found',
|
|
213
|
+
`No registry-sync endpoint matches ${request.method} ${pathname}.`,
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
const auth = await authorize(ctx, request, route)
|
|
217
|
+
if (!auth.ok) return auth.response
|
|
218
|
+
const { projectId } = route
|
|
219
|
+
|
|
220
|
+
// snapshot
|
|
221
|
+
const params = new URL(request.url).searchParams
|
|
222
|
+
const mode = params.get('mode') ?? 'published'
|
|
223
|
+
if (mode !== 'published' && mode !== 'draft') {
|
|
224
|
+
return errorResponse(
|
|
225
|
+
400,
|
|
226
|
+
'invalid-request',
|
|
227
|
+
`Invalid mode "${mode}" (expected published|draft).`,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
const environment = params.get('environment') ?? 'production'
|
|
231
|
+
const snapshot = await ctx.runQuery(syncExportSnapshot, {
|
|
232
|
+
projectId,
|
|
233
|
+
mode,
|
|
234
|
+
environment,
|
|
235
|
+
})
|
|
236
|
+
return jsonResponse(snapshot)
|
|
237
|
+
} catch (cause) {
|
|
238
|
+
// Even unexpected failures keep the structured contract — never an opaque crash.
|
|
239
|
+
return errorResponse(
|
|
240
|
+
500,
|
|
241
|
+
'internal',
|
|
242
|
+
`Registry sync failed: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
// --- Typegen/check snapshot -----------------------------------------------------
|
|
248
|
+
|
|
249
|
+
type SnapshotMode = 'published' | 'draft'
|
|
250
|
+
|
|
251
|
+
interface GraphRevisionPayload {
|
|
252
|
+
revision: number
|
|
253
|
+
serializedGraph: string
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
interface BuilderSnapshot {
|
|
257
|
+
projectId: string
|
|
258
|
+
mode: SnapshotMode
|
|
259
|
+
environment: string
|
|
260
|
+
project: null
|
|
261
|
+
localeConfig: { default: string; locales: string[] }
|
|
262
|
+
collections: never[]
|
|
263
|
+
graph: GraphRevisionPayload | null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The publishing-state-pinned revision (single-environment; mirrors `delivery.ts`). */
|
|
267
|
+
async function loadPinnedGraph(ctx: QueryCtx): Promise<GraphRevisionPayload | null> {
|
|
268
|
+
const state = await ctx.db.query('publishingState').first()
|
|
269
|
+
const pinned = state?.status === 'published' ? state.publishedRevision : undefined
|
|
270
|
+
if (pinned === undefined) return null
|
|
271
|
+
const row = await ctx.db
|
|
272
|
+
.query('graphRevisions')
|
|
273
|
+
.withIndex('by_revision', (q) => q.eq('revision', pinned))
|
|
274
|
+
.first()
|
|
275
|
+
return row ? { revision: row.revision, serializedGraph: row.serializedGraph } : null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function loadLatestGraph(ctx: QueryCtx): Promise<GraphRevisionPayload | null> {
|
|
279
|
+
const row = await ctx.db
|
|
280
|
+
.query('graphRevisions')
|
|
281
|
+
.withIndex('by_revision')
|
|
282
|
+
.order('desc')
|
|
283
|
+
.first()
|
|
284
|
+
return row ? { revision: row.revision, serializedGraph: row.serializedGraph } : null
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Everything `builder typegen`/`builder check` need from live builder state, in the
|
|
289
|
+
* `BuilderSnapshot` wire shape of `@elytracms/export-sync`'s `createHttpBuilderSource`.
|
|
290
|
+
* Unlike `delivery:snapshot` this is token-gated and serves exactly one graph per
|
|
291
|
+
* request: published = the pinned revision for `environment`, draft = latest.
|
|
292
|
+
* Component manifests are code-owned, so the snapshot no longer carries them.
|
|
293
|
+
*/
|
|
294
|
+
export const exportSnapshot = internalQuery({
|
|
295
|
+
args: {
|
|
296
|
+
projectId: v.string(),
|
|
297
|
+
mode: v.union(v.literal('published'), v.literal('draft')),
|
|
298
|
+
environment: v.string(),
|
|
299
|
+
},
|
|
300
|
+
handler: async (ctx, { projectId, mode, environment }): Promise<BuilderSnapshot> => {
|
|
301
|
+
// `environment` is a vestigial wire-compat field on the CLI snapshot contract
|
|
302
|
+
// (EC-179 §2): a deployment IS one environment, so the pinned read is single-row.
|
|
303
|
+
const graph = mode === 'published' ? await loadPinnedGraph(ctx) : await loadLatestGraph(ctx)
|
|
304
|
+
|
|
305
|
+
// AD-11 (EC-182/EC-183/EC-207): project identity, locale config, CMS
|
|
306
|
+
// collection schemas, AND routes/redirects are all config-owned —
|
|
307
|
+
// `builder typegen`/`check` run in the consuming repo and read them from
|
|
308
|
+
// `elytra.config.ts`, not the wire. The DB carries only data (documents,
|
|
309
|
+
// assets, graph). `project`/`localeConfig`/`collections` stay in the wire
|
|
310
|
+
// shape as config-sourced placeholders until the CLI reads config directly
|
|
311
|
+
// (C1) — the snapshot no longer carries a schema or a route table to return.
|
|
312
|
+
return {
|
|
313
|
+
projectId,
|
|
314
|
+
mode,
|
|
315
|
+
environment,
|
|
316
|
+
project: null,
|
|
317
|
+
localeConfig: { default: 'en', locales: ['en'] },
|
|
318
|
+
collections: [],
|
|
319
|
+
graph,
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
})
|
package/convex/users.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getAuthUserId } from '@convex-dev/auth/server'
|
|
2
|
+
import { query } from './functions'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Current-user lookup (EC-150). The Convex `AuthAdapter` implementation calls
|
|
6
|
+
* this after sign-in (and on session restore) to materialize the studio's
|
|
7
|
+
* `AuthSession`. Returns `null` when the caller is unauthenticated — never an
|
|
8
|
+
* error, so the adapter can treat it as "signed out".
|
|
9
|
+
*/
|
|
10
|
+
export const me = query({
|
|
11
|
+
args: {},
|
|
12
|
+
handler: async (ctx) => {
|
|
13
|
+
const userId = await getAuthUserId(ctx)
|
|
14
|
+
if (userId === null) return null
|
|
15
|
+
const user = await ctx.db.get(userId)
|
|
16
|
+
if (user === null) return null
|
|
17
|
+
return {
|
|
18
|
+
id: String(userId),
|
|
19
|
+
email: user.email ?? '',
|
|
20
|
+
...(user.name ? { name: user.name } : {}),
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { internalActionGeneric } from 'convex/server'
|
|
2
|
+
import { v } from 'convex/values'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EC-146 instant publish — Convex-side webhook delivery variant.
|
|
6
|
+
*
|
|
7
|
+
* The v1 dispatcher (`src/lib/webhooks`) runs in the browser next to the
|
|
8
|
+
* operations client and POSTs to the host directly (CORS + bundle-visible
|
|
9
|
+
* secret, documented there). For Convex-backed deployments this action keeps
|
|
10
|
+
* the secret server-side instead: `publishing:publish` / `publishing:unpublish`
|
|
11
|
+
* schedule it via `ctx.scheduler.runAfter(0, internal.webhooks.deliver, …)`
|
|
12
|
+
* (EC-180) with the already-computed tag set, and the action signs + POSTs from
|
|
13
|
+
* the Convex runtime. No CORS applies and the secret lives in Convex env config.
|
|
14
|
+
*
|
|
15
|
+
* It is an **internal** action: only server-side callers (the scheduler) may
|
|
16
|
+
* invoke it — a secret-signing HTTP emitter must never be client-callable.
|
|
17
|
+
*
|
|
18
|
+
* Environment (set via `npx convex env set …`):
|
|
19
|
+
* - `CANVAS_REVALIDATE_ENDPOINT` — the host's revalidate route URL
|
|
20
|
+
* - `CANVAS_REVALIDATE_SECRET` — shared HMAC secret (= host `REVALIDATE_SECRET`)
|
|
21
|
+
* - `CANVAS_PROJECT_SCOPE` — the host's `cache.scope` (= `ELYTRA_PROJECT_ID`),
|
|
22
|
+
* read by the publishing mutations to scope the sweep tag (`p:<scope>:project`)
|
|
23
|
+
*
|
|
24
|
+
* Signature scheme mirrors `src/lib/webhooks/signing.ts` and is verified by
|
|
25
|
+
* `@elytracms/next`'s `evaluateRevalidateRequest`: HMAC-SHA256 over the exact
|
|
26
|
+
* raw JSON body, hex-encoded, sent as `x-elytra-signature: sha256=<hex>`.
|
|
27
|
+
* Uses Web Crypto (`crypto.subtle`), which the Convex runtime provides.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const SIGNATURE_HEADER = 'x-elytra-signature'
|
|
31
|
+
|
|
32
|
+
function toHex(buffer: ArrayBuffer): string {
|
|
33
|
+
return [...new Uint8Array(buffer)].map((byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function signBody(rawBody: string, secret: string): Promise<string> {
|
|
37
|
+
const encoder = new TextEncoder()
|
|
38
|
+
const key = await crypto.subtle.importKey(
|
|
39
|
+
'raw',
|
|
40
|
+
encoder.encode(secret),
|
|
41
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
42
|
+
false,
|
|
43
|
+
['sign'],
|
|
44
|
+
)
|
|
45
|
+
return `sha256=${toHex(await crypto.subtle.sign('HMAC', key, encoder.encode(rawBody)))}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const deliver = internalActionGeneric({
|
|
49
|
+
args: {
|
|
50
|
+
projectId: v.string(),
|
|
51
|
+
/** Operation name carried as the webhook `event` (e.g. `publishing.publish`). */
|
|
52
|
+
event: v.string(),
|
|
53
|
+
/** Fully-formatted, scoped Next tag strings (see src/lib/webhooks/tags.ts). */
|
|
54
|
+
tags: v.array(v.string()),
|
|
55
|
+
},
|
|
56
|
+
handler: async (_ctx, args) => {
|
|
57
|
+
// The Convex runtime exposes deployment env vars on `process.env`; this
|
|
58
|
+
// tsconfig has no Node types, so reach it via `globalThis`.
|
|
59
|
+
const env =
|
|
60
|
+
(globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
|
|
61
|
+
const endpoint = env['CANVAS_REVALIDATE_ENDPOINT']
|
|
62
|
+
const secret = env['CANVAS_REVALIDATE_SECRET']
|
|
63
|
+
if (!endpoint || !secret) {
|
|
64
|
+
// Unconfigured deployments are a visible no-op, never an error — local
|
|
65
|
+
// dev must not depend on a reachable host site.
|
|
66
|
+
return { delivered: false as const, reason: 'unconfigured' as const }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const rawBody = JSON.stringify({
|
|
70
|
+
projectId: args.projectId,
|
|
71
|
+
event: args.event,
|
|
72
|
+
tags: args.tags,
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
})
|
|
75
|
+
const response = await fetch(endpoint, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'content-type': 'application/json',
|
|
79
|
+
[SIGNATURE_HEADER]: await signBody(rawBody, secret),
|
|
80
|
+
},
|
|
81
|
+
body: rawBody,
|
|
82
|
+
})
|
|
83
|
+
return { delivered: response.ok, status: response.status }
|
|
84
|
+
},
|
|
85
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset library surface (EC-151, EC-152): file picker + drag-and-drop uploads
|
|
3
|
+
* with visible progress/failure/retry, a searchable grid of stored assets
|
|
4
|
+
* (filename/alt/title), metadata editing (alt, title — persisted through the
|
|
5
|
+
* operations asset facade), and reference-respecting deletion (usage count +
|
|
6
|
+
* block-or-confirm). All storage access goes through the persistence adapter
|
|
7
|
+
* via the assets store. Each card answers "where is this asset used?" via the
|
|
8
|
+
* shared references panel (EC-172); `focusAssetId` (the `?asset=` deep-link
|
|
9
|
+
* param) scrolls that card into view and opens its references dialog.
|
|
10
|
+
*/
|
|
11
|
+
export declare function AssetLibrary({ projectId, focusAssetId, }: {
|
|
12
|
+
projectId: string;
|
|
13
|
+
focusAssetId?: string;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { AlertTriangle, ImageIcon, Link2, Pencil, RotateCcw, Search, Trash2, Upload, X, } from 'lucide-react';
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { buildAssetReferenceImpact } from '@elytracms/studio/lib/references';
|
|
5
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
6
|
+
import { ReferenceImpactRows } from '../cms/reference-confirm';
|
|
7
|
+
import { ReferencesPanel } from '../references/references-panel';
|
|
8
|
+
import { Badge, Button, Code, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Field, Grid, Heading, Input, Panel, PanelBody, PanelHeader, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
|
|
9
|
+
import { filterAssets } from './asset-search';
|
|
10
|
+
import { useAssets } from './assets-store';
|
|
11
|
+
/**
|
|
12
|
+
* Asset library surface (EC-151, EC-152): file picker + drag-and-drop uploads
|
|
13
|
+
* with visible progress/failure/retry, a searchable grid of stored assets
|
|
14
|
+
* (filename/alt/title), metadata editing (alt, title — persisted through the
|
|
15
|
+
* operations asset facade), and reference-respecting deletion (usage count +
|
|
16
|
+
* block-or-confirm). All storage access goes through the persistence adapter
|
|
17
|
+
* via the assets store. Each card answers "where is this asset used?" via the
|
|
18
|
+
* shared references panel (EC-172); `focusAssetId` (the `?asset=` deep-link
|
|
19
|
+
* param) scrolls that card into view and opens its references dialog.
|
|
20
|
+
*/
|
|
21
|
+
export function AssetLibrary({ projectId, focusAssetId, }) {
|
|
22
|
+
const store = useAssets();
|
|
23
|
+
const fileInputRef = useRef(null);
|
|
24
|
+
const [dragging, setDragging] = useState(false);
|
|
25
|
+
const [query, setQuery] = useState('');
|
|
26
|
+
const onPick = (event) => {
|
|
27
|
+
if (event.target.files)
|
|
28
|
+
store.upload([...event.target.files]);
|
|
29
|
+
event.target.value = '';
|
|
30
|
+
};
|
|
31
|
+
const onDrop = (event) => {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
setDragging(false);
|
|
34
|
+
if (event.dataTransfer.files.length > 0)
|
|
35
|
+
store.upload([...event.dataTransfer.files]);
|
|
36
|
+
};
|
|
37
|
+
if (store.status === 'error') {
|
|
38
|
+
return (_jsxs(Stack, { gap: "2", className: "p-6", "data-testid": "assets-error", children: [_jsxs(Row, { gap: "2", children: [_jsx(AlertTriangle, { className: "size-4 text-destructive" }), _jsx(Text, { weight: "semibold", children: "Assets failed to load" })] }), _jsx(Text, { size: "sm", tone: "muted", children: store.errorMessage ?? 'The persistence backend is unreachable.' }), _jsx(Row, { children: _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void store.refresh(), children: [_jsx(RotateCcw, {}), " Retry"] }) })] }));
|
|
39
|
+
}
|
|
40
|
+
return (_jsxs(Stack, { gap: "4", className: cn('flex-1 p-4 sm:p-6', dragging && 'outline-2 outline-dashed outline-primary/60 -outline-offset-4'), onDragOver: (event) => {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
setDragging(true);
|
|
43
|
+
}, onDragLeave: () => setDragging(false), onDrop: onDrop, "data-testid": "asset-library", children: [_jsxs(Row, { gap: "2", children: [_jsx(Heading, { level: 2, size: "h2", children: "Assets" }), _jsx(Badge, { variant: "outline", children: store.assets.length }), _jsx(Spacer, {}), _jsx(AssetSearchInput, { value: query, onChange: setQuery }), _jsx("input", { ref: fileInputRef, type: "file", multiple: true, className: "hidden", onChange: onPick, "data-testid": "asset-file-input" }), _jsxs(Button, { size: "sm", onClick: () => fileInputRef.current?.click(), children: [_jsx(Upload, {}), " Upload"] })] }), _jsx(DropHint, { visible: dragging || store.assets.length === 0 }), store.uploads.length > 0 ? (_jsxs(Panel, { "data-testid": "upload-queue", children: [_jsx(PanelHeader, { children: _jsx(Text, { size: "xs", weight: "semibold", tone: "muted", children: "Uploads" }) }), _jsx(PanelBody, { className: "divide-y divide-border p-0", children: store.uploads.map((entry) => (_jsx(UploadRow, { entry: entry, onRetry: () => store.retryUpload(entry.id), onDismiss: () => store.dismissUpload(entry.id) }, entry.id))) })] })) : null, store.status === 'loading' ? (_jsx(Text, { size: "sm", tone: "muted", children: "Loading assets\u2026" })) : (_jsx(AssetGrid, { assets: filterAssets(store.assets, query), filtered: query.trim().length > 0, projectId: projectId, focusAssetId: focusAssetId }))] }));
|
|
44
|
+
}
|
|
45
|
+
/** Named search primitive over filename / alt / title (EC-152). */
|
|
46
|
+
function AssetSearchInput({ value, onChange, }) {
|
|
47
|
+
return (_jsxs("span", { className: "relative", children: [_jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { value: value, onChange: (e) => onChange(e.target.value), placeholder: "Search assets\u2026", "aria-label": "Search assets", className: "w-56 pl-8", "data-testid": "asset-search" })] }));
|
|
48
|
+
}
|
|
49
|
+
/** Persistent drop affordance; doubles as the empty state. */
|
|
50
|
+
function DropHint({ visible }) {
|
|
51
|
+
return (_jsxs(Stack, { gap: "1", align: "center", className: cn('rounded-lg border border-dashed border-border p-6 text-center', !visible && 'hidden sm:flex'), children: [_jsx(ImageIcon, { className: "size-5 text-muted-foreground" }), _jsx(Text, { size: "sm", tone: "muted", children: "Drop files anywhere to upload, or use the Upload button." })] }));
|
|
52
|
+
}
|
|
53
|
+
const PHASE_LABEL = {
|
|
54
|
+
idle: 'queued',
|
|
55
|
+
preparing: 'preparing',
|
|
56
|
+
uploading: 'uploading',
|
|
57
|
+
confirming: 'saving metadata',
|
|
58
|
+
done: 'done',
|
|
59
|
+
failed: 'failed',
|
|
60
|
+
};
|
|
61
|
+
/** One in-flight upload: progress while transferring, explicit failure + retry. */
|
|
62
|
+
function UploadRow({ entry, onRetry, onDismiss, }) {
|
|
63
|
+
const { state } = entry;
|
|
64
|
+
const failed = state.phase === 'failed';
|
|
65
|
+
return (_jsxs(Stack, { gap: "1", className: "px-3 py-2", "data-testid": "upload-row", children: [_jsxs(Row, { gap: "2", children: [_jsx(Text, { size: "sm", weight: "medium", className: "min-w-0 flex-1 truncate", children: entry.filename }), _jsxs(Badge, { variant: failed ? 'error' : state.phase === 'done' ? 'success' : 'info', children: [PHASE_LABEL[state.phase], state.attempt > 1 ? ` · attempt ${state.attempt}` : ''] }), failed ? (_jsxs(_Fragment, { children: [state.failure?.retryable !== false ? (_jsxs(Button, { variant: "outline", size: "sm", onClick: onRetry, "data-testid": "upload-retry", children: [_jsx(RotateCcw, {}), " Retry"] })) : null, _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onDismiss, "aria-label": "Dismiss upload", children: _jsx(X, {}) })] })) : null] }), failed ? (_jsxs(Text, { size: "xs", tone: "muted", "data-testid": "upload-error", children: [state.failure?.stage, ": ", state.failure?.message] })) : (_jsx(ProgressBar, { value: state.progress }))] }));
|
|
66
|
+
}
|
|
67
|
+
/** Named progress primitive for byte transfer. */
|
|
68
|
+
function ProgressBar({ value }) {
|
|
69
|
+
return (_jsx("span", { className: "block h-1.5 w-full overflow-hidden rounded-full bg-muted", children: _jsx("span", { className: "block h-full rounded-full bg-primary transition-[width]", style: { width: `${Math.round(value * 100)}%` } }) }));
|
|
70
|
+
}
|
|
71
|
+
function AssetGrid({ assets, filtered, projectId, focusAssetId, }) {
|
|
72
|
+
if (assets.length === 0) {
|
|
73
|
+
// The unfiltered empty state is the DropHint above; the *filtered* empty
|
|
74
|
+
// state must say so explicitly (deterministic, reproducible).
|
|
75
|
+
return filtered ? (_jsx(Text, { size: "sm", tone: "muted", "data-testid": "asset-search-empty", children: "No assets match your search." })) : null;
|
|
76
|
+
}
|
|
77
|
+
return (_jsx(Grid, { minItemWidth: "220px", gap: "3", "data-testid": "asset-grid", children: assets.map((asset) => (_jsx(AssetCard, { asset: asset, projectId: projectId, focused: asset.id === focusAssetId }, asset.id))) }));
|
|
78
|
+
}
|
|
79
|
+
function formatBytes(byteSize) {
|
|
80
|
+
if (byteSize < 1024)
|
|
81
|
+
return `${byteSize} B`;
|
|
82
|
+
if (byteSize < 1024 * 1024)
|
|
83
|
+
return `${(byteSize / 1024).toFixed(1)} KB`;
|
|
84
|
+
return `${(byteSize / (1024 * 1024)).toFixed(1)} MB`;
|
|
85
|
+
}
|
|
86
|
+
function AssetCard({ asset, projectId, focused, }) {
|
|
87
|
+
const cardRef = useRef(null);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (focused)
|
|
90
|
+
cardRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
91
|
+
}, [focused]);
|
|
92
|
+
return (_jsx("div", { ref: cardRef, children: _jsxs(Panel, { className: cn('overflow-hidden', focused && 'ring-2 ring-primary/70'), "data-testid": "asset-card", children: [_jsx(AssetThumbnail, { asset: asset }), _jsxs(Stack, { gap: "1", className: "p-3", children: [_jsx(Text, { size: "sm", weight: "medium", className: "truncate", title: asset.filename, children: asset.title ?? asset.filename }), asset.title ? (_jsx(Text, { size: "xs", tone: "muted", className: "truncate", children: asset.filename })) : null, _jsx(Text, { size: "xs", tone: asset.alt ? 'muted' : 'subtle', className: "truncate", "data-testid": "asset-alt", children: asset.alt ? `Alt: ${asset.alt}` : 'No alt text' }), _jsxs(Row, { gap: "1", wrap: true, children: [_jsx(Code, { tone: "muted", children: asset.contentType }), asset.width && asset.height ? (_jsxs(Code, { tone: "muted", children: [asset.width, "\u00D7", asset.height] })) : null, _jsx(Code, { tone: "muted", children: formatBytes(asset.byteSize) })] }), _jsxs(Row, { gap: "1", children: [_jsx(Code, { tone: "muted", className: "min-w-0 flex-1 truncate", children: asset.id }), _jsx(AssetReferencesButton, { asset: asset, projectId: projectId, autoOpen: focused }), _jsx(EditAssetButton, { asset: asset }), _jsx(DeleteAssetButton, { asset: asset })] })] })] }) }));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* "Where is this asset used?" (EC-172): the shared references panel, inbound
|
|
96
|
+
* over the EC-155 index, hosted in a dialog per asset card.
|
|
97
|
+
*/
|
|
98
|
+
function AssetReferencesButton({ asset, projectId, autoOpen, }) {
|
|
99
|
+
const [open, setOpen] = useState(false);
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (autoOpen)
|
|
102
|
+
setOpen(true);
|
|
103
|
+
}, [autoOpen]);
|
|
104
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": `References of ${asset.filename}`, onClick: () => setOpen(true), "data-testid": "asset-references", children: _jsx(Link2, {}) }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Used by \u2014 ", asset.title ?? asset.filename] }), _jsx(DialogDescription, { children: "Everything referencing this asset, with deep links to the exact field or node." })] }), open ? (_jsx(ReferencesPanel, { projectId: projectId, target: { type: 'asset', id: asset.id } })) : null] }) })] }));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Metadata editing (EC-152): alt + title, persisted through the operations
|
|
108
|
+
* asset facade (`assets.upsert`). Failures render in the dialog — never a
|
|
109
|
+
* silent loss.
|
|
110
|
+
*/
|
|
111
|
+
function EditAssetButton({ asset }) {
|
|
112
|
+
const store = useAssets();
|
|
113
|
+
const [open, setOpen] = useState(false);
|
|
114
|
+
const [alt, setAlt] = useState('');
|
|
115
|
+
const [title, setTitle] = useState('');
|
|
116
|
+
const [error, setError] = useState(null);
|
|
117
|
+
const [busy, setBusy] = useState(false);
|
|
118
|
+
const openDialog = () => {
|
|
119
|
+
setAlt(asset.alt ?? '');
|
|
120
|
+
setTitle(asset.title ?? '');
|
|
121
|
+
setError(null);
|
|
122
|
+
setOpen(true);
|
|
123
|
+
};
|
|
124
|
+
const save = async () => {
|
|
125
|
+
setBusy(true);
|
|
126
|
+
setError(null);
|
|
127
|
+
const result = await store.updateMetadata(asset.id, { alt, title });
|
|
128
|
+
setBusy(false);
|
|
129
|
+
if (result.ok)
|
|
130
|
+
setOpen(false);
|
|
131
|
+
else
|
|
132
|
+
setError(result.message);
|
|
133
|
+
};
|
|
134
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": `Edit ${asset.filename}`, onClick: openDialog, "data-testid": "asset-edit", children: _jsx(Pencil, {}) }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Edit ", asset.filename] }), _jsx(DialogDescription, { children: "Alt text flows through the delivery shape into rendered markup; the title is the library display name." })] }), _jsx(Field, { label: "Alt text", htmlFor: `asset-alt-${asset.id}`, children: _jsx(Input, { id: `asset-alt-${asset.id}`, value: alt, onChange: (e) => setAlt(e.target.value), placeholder: "Describe the image\u2026", "data-testid": "asset-edit-alt" }) }), _jsx(Field, { label: "Title", htmlFor: `asset-title-${asset.id}`, children: _jsx(Input, { id: `asset-title-${asset.id}`, value: title, onChange: (e) => setTitle(e.target.value), placeholder: asset.filename, "data-testid": "asset-edit-title" }) }), error ? (_jsx(Text, { size: "sm", className: "text-destructive", "data-testid": "asset-edit-error", children: error })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpen(false), disabled: busy, children: "Cancel" }), _jsx(Button, { size: "sm", onClick: () => void save(), disabled: busy, "data-testid": "asset-edit-save", children: "Save" })] })] }) })] }));
|
|
135
|
+
}
|
|
136
|
+
/** Image preview from the stored serve URL; icon fallback for other types. */
|
|
137
|
+
function AssetThumbnail({ asset }) {
|
|
138
|
+
const isImage = asset.contentType.startsWith('image/');
|
|
139
|
+
// In-memory/opaque locators (memory://, convex://) are not fetchable by <img>.
|
|
140
|
+
const resolvable = asset.url?.startsWith('http') ?? false;
|
|
141
|
+
if (isImage && asset.url && resolvable) {
|
|
142
|
+
return (_jsx("img", { src: asset.url, alt: asset.alt ?? asset.filename, className: "aspect-video w-full bg-muted object-cover" }));
|
|
143
|
+
}
|
|
144
|
+
return (_jsx("span", { className: "grid aspect-video w-full place-items-center bg-muted text-muted-foreground", children: _jsx(ImageIcon, { className: "size-6" }) }));
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Reference-respecting delete (EC-151, EC-202): the dialog enumerates the exact
|
|
148
|
+
* inbound references — index-accurate (EC-155), with deep links to the field or
|
|
149
|
+
* node — using the shared EC-173 impact rows. Deleting a referenced asset is
|
|
150
|
+
* weak-by-default (allowed, with an explicit "delete anyway"): every reference
|
|
151
|
+
* dangles into a visible validation issue rather than silently breaking.
|
|
152
|
+
*/
|
|
153
|
+
function DeleteAssetButton({ asset }) {
|
|
154
|
+
const store = useAssets();
|
|
155
|
+
const [open, setOpen] = useState(false);
|
|
156
|
+
const [usages, setUsages] = useState(null);
|
|
157
|
+
const [error, setError] = useState(null);
|
|
158
|
+
const [busy, setBusy] = useState(false);
|
|
159
|
+
const openDialog = async () => {
|
|
160
|
+
setOpen(true);
|
|
161
|
+
setError(null);
|
|
162
|
+
setUsages(null);
|
|
163
|
+
try {
|
|
164
|
+
setUsages(await store.usagesOf(asset.id));
|
|
165
|
+
}
|
|
166
|
+
catch (cause) {
|
|
167
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const confirm = async () => {
|
|
171
|
+
setBusy(true);
|
|
172
|
+
setError(null);
|
|
173
|
+
const inUse = (usages?.length ?? 0) > 0;
|
|
174
|
+
const result = await store.remove(asset.id, { confirmInUse: inUse });
|
|
175
|
+
setBusy(false);
|
|
176
|
+
if (result.outcome === 'deleted' || result.outcome === 'not-found') {
|
|
177
|
+
setOpen(false);
|
|
178
|
+
}
|
|
179
|
+
else if (result.outcome === 'blocked') {
|
|
180
|
+
// A reference appeared between the pre-flight read and confirm (rare race).
|
|
181
|
+
setError(`Asset is now referenced in ${result.usageCount} place(s) — reopen to review.`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
setError(result.message);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const impact = usages ? buildAssetReferenceImpact(usages) : null;
|
|
188
|
+
const usageCount = impact?.broken.length ?? 0;
|
|
189
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": `Delete ${asset.filename}`, onClick: () => void openDialog(), "data-testid": "asset-delete", children: _jsx(Trash2, {}) }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-h-[80vh] max-w-md overflow-y-auto", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Delete ", asset.filename, "?"] }), _jsx(DialogDescription, { children: "Removes the stored bytes and the metadata record. This cannot be undone." })] }), usages === null && error === null ? (_jsx(Text, { size: "sm", tone: "muted", children: "Checking references\u2026" })) : usageCount > 0 ? (_jsxs(Stack, { gap: "2", "data-testid": "asset-usage-warning", children: [_jsxs(Row, { gap: "2", children: [_jsx(AlertTriangle, { className: "size-4 text-warning" }), _jsxs(Text, { size: "sm", weight: "semibold", children: ["In use in ", usageCount, " place", usageCount === 1 ? '' : 's'] })] }), _jsx(ReferenceImpactRows, { projectId: store.projectId, broken: impact.broken, onNavigate: () => setOpen(false) }), _jsx(Text, { size: "xs", tone: "muted", children: "Deleting will leave these references broken \u2014 they will surface as validation issues." })] })) : (_jsx(Text, { size: "sm", tone: "muted", children: "No references found. Safe to delete." })), error ? (_jsx(Text, { size: "sm", className: "text-destructive", "data-testid": "asset-delete-error", children: error })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => setOpen(false), disabled: busy, children: "Cancel" }), _jsx(Button, { variant: "destructive", size: "sm", onClick: () => void confirm(), disabled: busy || usages === null, "data-testid": "asset-delete-confirm", children: usageCount > 0 ? 'Delete anyway' : 'Delete' })] })] }) })] }));
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=asset-library.js.map
|