@jxsuite/studio 2.0.1 → 2.1.0
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/canvas.html +21 -0
- package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
- package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
- package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
- package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
- package/dist/chunks/{components-799jpvhm.js → components-2bpqp4mq.js} +5 -7
- package/dist/chunks/{components-799jpvhm.js.map → components-2bpqp4mq.js.map} +1 -1
- package/dist/chunks/content-source-34gnkbnn.js +38 -0
- package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
- package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
- package/dist/chunks/extension-sections-6mqydz35.js +301 -0
- package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
- package/dist/chunks/files-8kn321h1.js +70 -0
- package/dist/chunks/files-8kn321h1.js.map +9 -0
- package/dist/chunks/{format-host-8jxwts2p.js → format-host-mkwsmt8d.js} +3 -3
- package/dist/chunks/git-panel-tjbr191d.js +51 -0
- package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
- package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
- package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
- package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
- package/dist/chunks/layers-panel-87pb25km.js +58 -0
- package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
- package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
- package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
- package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
- package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
- package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
- package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
- package/dist/chunks/right-panel-zpdxd98t.js +37 -0
- package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
- package/dist/chunks/section-registry-zmckaxf4.js +30 -0
- package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
- package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
- package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
- package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
- package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
- package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
- package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
- package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
- package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
- package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
- package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
- package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
- package/dist/chunks/studio-41exgzhq.js +6690 -0
- package/dist/chunks/studio-41exgzhq.js.map +38 -0
- package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
- package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
- package/dist/chunks/studio-50k77cca.js +1787 -0
- package/dist/chunks/studio-50k77cca.js.map +18 -0
- package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
- package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
- package/dist/chunks/studio-6f0am8f2.js +53 -0
- package/dist/chunks/studio-6f0am8f2.js.map +10 -0
- package/dist/chunks/studio-6nmjc5qk.js +7002 -0
- package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
- package/dist/chunks/studio-7afd21nw.js +67 -0
- package/dist/chunks/studio-7afd21nw.js.map +10 -0
- package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
- package/dist/chunks/studio-7bmdvzne.js.map +10 -0
- package/dist/chunks/studio-8jpzqk5s.js +128 -0
- package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
- package/dist/chunks/studio-94m77tgk.js +1482 -0
- package/dist/chunks/studio-94m77tgk.js.map +13 -0
- package/dist/chunks/studio-9je6w5sq.js +1000 -0
- package/dist/chunks/studio-9je6w5sq.js.map +16 -0
- package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
- package/dist/chunks/{studio-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
- package/dist/chunks/{studio-gs6kqdt1.js.map → studio-adn0z1k3.js.map} +3 -3
- package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
- package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
- package/dist/chunks/studio-bw5dz6km.js +608 -0
- package/dist/chunks/studio-bw5dz6km.js.map +21 -0
- package/dist/chunks/studio-c6s5getc.js +4232 -0
- package/dist/chunks/studio-c6s5getc.js.map +39 -0
- package/dist/chunks/studio-cjm7fdjf.js +451 -0
- package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
- package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
- package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
- package/dist/chunks/studio-dg7zyt5x.js +28 -0
- package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
- package/dist/chunks/studio-dzcyvvvz.js +166 -0
- package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
- package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
- package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
- package/dist/chunks/studio-f12h6177.js +411 -0
- package/dist/chunks/studio-f12h6177.js.map +13 -0
- package/dist/chunks/studio-hbp1s8m9.js +1817 -0
- package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
- package/dist/chunks/studio-k2sxjph5.js +15295 -0
- package/dist/chunks/studio-k2sxjph5.js.map +235 -0
- package/dist/chunks/studio-myje51t3.js +385 -0
- package/dist/chunks/studio-myje51t3.js.map +12 -0
- package/dist/chunks/studio-na6rwzhw.js +353 -0
- package/dist/chunks/studio-na6rwzhw.js.map +11 -0
- package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
- package/dist/chunks/studio-p471gg5n.js.map +14 -0
- package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
- package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
- package/dist/chunks/{studio-esrewxap.js → studio-s519zc99.js} +3 -3
- package/dist/chunks/studio-sas7mt3k.js +75 -0
- package/dist/chunks/studio-sas7mt3k.js.map +10 -0
- package/dist/chunks/studio-t5wqwzdh.js +74 -0
- package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
- package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
- package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
- package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
- package/dist/chunks/studio-tr20m8g8.js.map +9 -0
- package/dist/chunks/{studio-v7jkjqng.js → studio-v96qfnww.js} +5514 -4291
- package/dist/chunks/studio-v96qfnww.js.map +54 -0
- package/dist/chunks/studio-vcetc91k.js +659 -0
- package/dist/chunks/studio-vcetc91k.js.map +10 -0
- package/dist/chunks/{studio-jm0z592w.js → studio-vmymjefp.js} +2972 -4937
- package/dist/chunks/studio-vmymjefp.js.map +98 -0
- package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
- package/dist/chunks/studio-x9hmy5kr.js +390 -0
- package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
- package/dist/chunks/studio-xa0ce0n7.js +335 -0
- package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
- package/dist/chunks/studio-xm3dbxvn.js +378 -0
- package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
- package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
- package/dist/chunks/studio-zv37yrfa.js +1054 -0
- package/dist/chunks/studio-zv37yrfa.js.map +13 -0
- package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
- package/dist/chunks/ws-client-654ts66j.js.map +11 -0
- package/dist/iframe-entry.js +668 -230
- package/dist/studio.js +21080 -32585
- package/index.html +35 -4685
- package/package.json +17 -15
- package/src/about/about-modal.ts +42 -11
- package/src/browse/library-commands.ts +240 -0
- package/src/browse/library-layouts.ts +369 -0
- package/src/browse/library-model.ts +396 -0
- package/src/browse/library-pane.ts +1049 -0
- package/src/browse/library-preview.ts +252 -0
- package/src/browse/library-source.ts +150 -0
- package/src/canvas/canvas-helpers.ts +33 -9
- package/src/canvas/canvas-live-render.ts +45 -29
- package/src/canvas/canvas-patcher.ts +151 -31
- package/src/canvas/canvas-perf.ts +46 -7
- package/src/canvas/canvas-render.ts +1092 -220
- package/src/canvas/canvas-surface.ts +240 -0
- package/src/canvas/canvas-utils.ts +1166 -124
- package/src/canvas/iframe-drop.ts +2 -1
- package/src/canvas/iframe-entry.ts +241 -4
- package/src/canvas/iframe-host.ts +1226 -143
- package/src/canvas/iframe-inline-edit.ts +31 -0
- package/src/canvas/iframe-interaction.ts +76 -16
- package/src/canvas/iframe-keys.ts +107 -45
- package/src/canvas/iframe-overlay.ts +38 -1
- package/src/canvas/iframe-protocol.ts +137 -2
- package/src/canvas/iframe-render.ts +70 -2
- package/src/canvas/iframe-slash.ts +6 -0
- package/src/canvas/path-mapping.ts +56 -6
- package/src/canvas/surface-registry.ts +292 -0
- package/src/collab/collab-commands.ts +244 -0
- package/src/collab/collab-session.ts +198 -32
- package/src/collab/collab-state.ts +28 -2
- package/src/collab/presence-chips.ts +100 -9
- package/src/commands/active-registry.ts +42 -0
- package/src/commands/app-commands.ts +176 -0
- package/src/commands/budget.ts +158 -0
- package/src/commands/command-args.ts +253 -0
- package/src/commands/context.ts +387 -0
- package/src/commands/defaults.ts +580 -0
- package/src/commands/keymap.ts +571 -0
- package/src/commands/levels.ts +345 -0
- package/src/commands/live-context.ts +193 -0
- package/src/commands/reference.ts +227 -0
- package/src/commands/registry.ts +293 -0
- package/src/component-props.ts +3 -2
- package/src/content/draft-state.ts +106 -0
- package/src/content/entry-commands.ts +282 -0
- package/src/content/entry-editor.ts +327 -0
- package/src/content/entry-fields.ts +116 -0
- package/src/content/entry-model.ts +175 -0
- package/src/editor/canvas-context-menu.ts +17 -11
- package/src/editor/canvas-slash-bridge.ts +11 -4
- package/src/editor/context-menu.ts +804 -204
- package/src/editor/convert-to-component.ts +31 -10
- package/src/editor/convert-to-repeater.ts +3 -2
- package/src/editor/inline-edit-apply.ts +5 -5
- package/src/editor/inline-edit.ts +86 -50
- package/src/editor/insert-zone-action.ts +1 -1
- package/src/editor/shortcuts.ts +1043 -329
- package/src/editor/slash-menu.ts +1 -0
- package/src/files/elements.ts +166 -0
- package/src/files/file-ops.ts +178 -31
- package/src/files/files.ts +898 -268
- package/src/files/fs-events.ts +21 -0
- package/src/files/media-meta.ts +203 -0
- package/src/files/media-paths.ts +141 -0
- package/src/files/media-upload.ts +7 -3
- package/src/files/media-usage.ts +189 -0
- package/src/github/github-auth.ts +235 -33
- package/src/github/github-publish.ts +80 -21
- package/src/grid/grid-controller.ts +152 -12
- package/src/grid/grid-layout.ts +225 -20
- package/src/grid/grid-open.ts +120 -0
- package/src/grid/grid-panel.ts +554 -39
- package/src/grid/grid-source.ts +27 -1
- package/src/grid/redirects-grid.ts +599 -0
- package/src/grid/redirects.ts +398 -0
- package/src/grid/sources/content-source.ts +108 -2
- package/src/i18n/i18n-commands.ts +322 -0
- package/src/new-project/add-repo-modal.ts +8 -10
- package/src/new-project/import-tab.ts +7 -0
- package/src/new-project/location-fields.ts +9 -1
- package/src/new-project/new-project-modal.ts +221 -245
- package/src/packages/jxsuite-update.ts +97 -48
- package/src/packages/pull-package-sync.ts +18 -9
- package/src/page-params.ts +2 -1
- package/src/panels/activity-bar.ts +152 -91
- package/src/panels/activity-panel.ts +481 -0
- package/src/panels/ai-chat/chat-markdown.ts +8 -3
- package/src/panels/ai-chat/chat-view.ts +245 -18
- package/src/panels/ai-chat/composer.ts +31 -5
- package/src/panels/ai-chat/sessions-view.ts +17 -10
- package/src/panels/ai-panel.ts +279 -53
- package/src/panels/block-action-bar.ts +1115 -224
- package/src/panels/bottom-dock.ts +337 -0
- package/src/panels/canvas-dnd-bridge.ts +37 -8
- package/src/panels/chat-panel.ts +29 -12
- package/src/panels/data-explorer.ts +254 -112
- package/src/panels/data-grid.ts +10 -4
- package/src/panels/dnd.ts +28 -51
- package/src/panels/editors.ts +434 -107
- package/src/panels/elements-panel.ts +66 -2
- package/src/panels/empty-state.ts +112 -0
- package/src/panels/events-panel.ts +596 -82
- package/src/panels/formula-workspace.ts +408 -218
- package/src/panels/frontmatter-fields.ts +51 -10
- package/src/panels/frontmatter-panel.ts +357 -79
- package/src/panels/git-panel.ts +375 -164
- package/src/panels/head-panel.ts +791 -85
- package/src/panels/i18n-panel.ts +592 -0
- package/src/panels/imports-panel.ts +95 -85
- package/src/panels/jump-bar.ts +584 -0
- package/src/panels/layers-panel.ts +1106 -328
- package/src/panels/left-panel.ts +109 -204
- package/src/panels/navigator-panels.ts +131 -0
- package/src/panels/overlays.ts +7 -2
- package/src/panels/pane-context.ts +1477 -0
- package/src/panels/pane-grid.ts +406 -0
- package/src/panels/panel-registry.ts +340 -0
- package/src/panels/panel-scheduler.ts +75 -0
- package/src/panels/problems-panel.ts +234 -0
- package/src/panels/properties-panel.ts +946 -1139
- package/src/panels/provenance.ts +258 -0
- package/src/panels/quick-search.ts +791 -122
- package/src/panels/right-panel.ts +228 -102
- package/src/panels/seo-modal.ts +395 -0
- package/src/panels/settings-pane.ts +203 -0
- package/src/panels/signals-panel.ts +354 -137
- package/src/panels/statement-editor.ts +41 -25
- package/src/panels/statusbar.ts +342 -106
- package/src/panels/style-panel.ts +974 -463
- package/src/panels/style-utils.ts +3 -3
- package/src/panels/stylebook-doc.ts +4 -2
- package/src/panels/stylebook-layers-panel.ts +3 -2
- package/src/panels/stylebook-panel.ts +51 -26
- package/src/panels/tab-strip.ts +1058 -78
- package/src/panels/target-line.ts +274 -0
- package/src/panels/toolbar.ts +631 -484
- package/src/panels/welcome-screen.ts +301 -198
- package/src/platform-errors.ts +26 -3
- package/src/platform.ts +96 -2
- package/src/platforms/cloud.ts +71 -8
- package/src/platforms/devserver.ts +92 -29
- package/src/publish/deploy-checklist.ts +383 -0
- package/src/publish/pages-service.ts +10 -1
- package/src/publish/publish-commands.ts +218 -0
- package/src/publish/publish-panel.ts +110 -16
- package/src/recent-projects.ts +3 -2
- package/src/services/a11y-report.ts +505 -0
- package/src/services/ai-models.ts +30 -1
- package/src/services/ai-project-tools.ts +29 -10
- package/src/services/ai-system-prompt.ts +53 -14
- package/src/services/ai-tools.ts +39 -3
- package/src/services/ai-writes.ts +154 -0
- package/src/services/announce.ts +97 -0
- package/src/services/automation.ts +424 -175
- package/src/services/cem-export.ts +9 -4
- package/src/services/clock.ts +61 -0
- package/src/services/document-assistant.ts +36 -8
- package/src/services/idle.ts +179 -0
- package/src/services/import-client.ts +26 -0
- package/src/services/monaco-buffer.ts +627 -0
- package/src/services/monaco-lazy.ts +36 -0
- package/src/services/notify.ts +271 -0
- package/src/services/profile.ts +155 -0
- package/src/services/references.ts +0 -0
- package/src/services/token-lint.ts +3 -2
- package/src/services/tool-executor.ts +53 -7
- package/src/services/trusted-types.ts +123 -0
- package/src/settings/contexts-section.ts +427 -0
- package/src/settings/contributed-section.ts +149 -11
- package/src/settings/css-vars-editor.ts +306 -349
- package/src/settings/defs-editor.ts +93 -34
- package/src/settings/dependencies-editor.ts +7 -7
- package/src/settings/extension-sections.ts +37 -5
- package/src/settings/general-settings.ts +204 -121
- package/src/settings/locales-section.ts +347 -0
- package/src/settings/preferences-accounts.ts +140 -0
- package/src/settings/preferences-dialog.ts +620 -0
- package/src/settings/preferences-keymap.ts +270 -0
- package/src/settings/project-sections.ts +260 -0
- package/src/settings/section-registry.ts +169 -0
- package/src/settings/settings-document.ts +385 -0
- package/src/shell.ts +1549 -0
- package/src/site-context.ts +45 -20
- package/src/state.ts +14 -275
- package/src/store.ts +88 -43
- package/src/studio.ts +790 -346
- package/src/style/live-preview.ts +36 -0
- package/src/style/project-styles.ts +349 -0
- package/src/style/token-ref.ts +123 -0
- package/src/tabs/patch-ops.ts +16 -4
- package/src/tabs/project-config.ts +416 -0
- package/src/tabs/selection.ts +323 -0
- package/src/tabs/tab.ts +182 -47
- package/src/tabs/transact.ts +209 -28
- package/src/types.ts +68 -13
- package/src/ui/ai-credentials-form.ts +39 -35
- package/src/ui/ai-managed-connect.ts +7 -1
- package/src/ui/dynamic-slot.ts +224 -81
- package/src/ui/expression-editor.ts +71 -63
- package/src/ui/field-input.ts +4 -6
- package/src/ui/field-row.ts +215 -38
- package/src/ui/form-controls.ts +194 -7
- package/src/ui/layers.ts +492 -69
- package/src/ui/media-picker.ts +115 -25
- package/src/ui/panel-resize.ts +109 -91
- package/src/ui/progress-modal.ts +131 -50
- package/src/ui/regions.ts +462 -0
- package/src/ui/schema-form.ts +229 -45
- package/src/ui/spectrum.ts +24 -8
- package/src/ui/timing.ts +49 -0
- package/src/ui/value-selector.ts +20 -0
- package/src/ui/value-source.ts +348 -0
- package/src/ui/virtual-window.ts +400 -0
- package/src/utils/edit-display.ts +3 -2
- package/src/utils/inherited-style.ts +67 -37
- package/src/view.ts +64 -78
- package/src/workspace/pane-derive.ts +1258 -0
- package/src/workspace/session.ts +317 -0
- package/src/workspace/workspace.ts +1345 -23
- package/styles/canvas.css +653 -0
- package/styles/forced-colors.css +106 -0
- package/styles/inspector.css +1454 -0
- package/styles/overlays.css +2704 -0
- package/styles/panels.css +1701 -0
- package/styles/shell.css +1423 -0
- package/styles/tokens.css +151 -0
- package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
- package/dist/chunks/extension-sections-qr3drkkw.js +0 -531
- package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
- package/dist/chunks/files-j8a1t06k.js +0 -50
- package/dist/chunks/files-j8a1t06k.js.map +0 -9
- package/dist/chunks/git-panel-xtq5z81x.js +0 -28
- package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
- package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
- package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
- package/dist/chunks/settings-modal-17ttr96g.js +0 -29
- package/dist/chunks/settings-modal-17ttr96g.js.map +0 -9
- package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
- package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
- package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
- package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
- package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
- package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
- package/dist/chunks/studio-0xgs7xrs.js +0 -429
- package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
- package/dist/chunks/studio-4d8hzcz5.js +0 -3427
- package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
- package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
- package/dist/chunks/studio-9smfzqhc.js +0 -19
- package/dist/chunks/studio-9smfzqhc.js.map +0 -10
- package/dist/chunks/studio-jm0z592w.js.map +0 -115
- package/dist/chunks/studio-m2t4mdg5.js +0 -1854
- package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
- package/dist/chunks/studio-n4p11dfy.js +0 -853
- package/dist/chunks/studio-n4p11dfy.js.map +0 -12
- package/dist/chunks/studio-qkvt37c0.js.map +0 -9
- package/dist/chunks/studio-sjh1wt1a.js +0 -3744
- package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
- package/dist/chunks/studio-v7jkjqng.js.map +0 -46
- package/dist/chunks/studio-xq0t7n5t.js +0 -140
- package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
- package/dist/chunks/studio-xze4zrs1.js.map +0 -10
- package/dist/chunks/studio-yfcstdzy.js +0 -399
- package/dist/chunks/studio-yfcstdzy.js.map +0 -12
- package/dist/chunks/studio-zp7jncs8.js +0 -469
- package/dist/chunks/studio-zp7jncs8.js.map +0 -14
- package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
- package/src/browse/browse-modal.ts +0 -69
- package/src/browse/browse.ts +0 -904
- package/src/canvas/canvas-diff.ts +0 -216
- package/src/canvas/nested-site-style.ts +0 -58
- package/src/new-project/design-fields.ts +0 -264
- package/src/new-project/templates.ts +0 -61
- package/src/panels/tab-bar.ts +0 -548
- package/src/settings/settings-modal.ts +0 -203
- /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
- /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
- /package/dist/chunks/{format-host-8jxwts2p.js.map → format-host-mkwsmt8d.js.map} +0 -0
- /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
- /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
- /package/dist/chunks/{studio-esrewxap.js.map → studio-s519zc99.js.map} +0 -0
- /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
- /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
package/src/grid/grid-source.ts
CHANGED
|
@@ -143,8 +143,19 @@ export function cellValuesEqual(a: GridCellValue, b: GridCellValue): boolean {
|
|
|
143
143
|
|
|
144
144
|
const GRID_SCHEME = "grid://";
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The virtual sources a tab id can name.
|
|
148
|
+
*
|
|
149
|
+
* `library` is the odd one and deliberately so: the Library is a source over this same contract
|
|
150
|
+
* (`browse/library-source.ts`) but its tab's editor kind is `library`, not `grid` — Table is one of
|
|
151
|
+
* its five layouts rather than its identity. It lives in this id space because "which virtual tab
|
|
152
|
+
* is this?" has exactly one answer per window and there is no second scheme worth inventing.
|
|
153
|
+
* Category and layout are VIEW state and are deliberately absent from the id: a Library that
|
|
154
|
+
* changed identity when you clicked "Pages" would open a second tab per filter.
|
|
155
|
+
*/
|
|
146
156
|
export type GridTabRef =
|
|
147
157
|
| { kind: "pages" }
|
|
158
|
+
| { kind: "library" }
|
|
148
159
|
| { kind: "collection"; name: string }
|
|
149
160
|
| { kind: "data"; connection: string; table: string };
|
|
150
161
|
|
|
@@ -154,6 +165,9 @@ export function makeGridTabId(ref: GridTabRef): string {
|
|
|
154
165
|
case "pages": {
|
|
155
166
|
return `${GRID_SCHEME}pages`;
|
|
156
167
|
}
|
|
168
|
+
case "library": {
|
|
169
|
+
return `${GRID_SCHEME}library`;
|
|
170
|
+
}
|
|
157
171
|
case "collection": {
|
|
158
172
|
return `${GRID_SCHEME}collection/${encodeURIComponent(ref.name)}`;
|
|
159
173
|
}
|
|
@@ -177,6 +191,9 @@ export function parseGridTabId(id: string): GridTabRef | null {
|
|
|
177
191
|
if (parts[0] === "pages" && parts.length === 1) {
|
|
178
192
|
return { kind: "pages" };
|
|
179
193
|
}
|
|
194
|
+
if (parts[0] === "library" && parts.length === 1) {
|
|
195
|
+
return { kind: "library" };
|
|
196
|
+
}
|
|
180
197
|
if (parts[0] === "collection" && parts.length === 2 && parts[1]) {
|
|
181
198
|
return { kind: "collection", name: decodeURIComponent(parts[1]) };
|
|
182
199
|
}
|
|
@@ -190,7 +207,13 @@ export function parseGridTabId(id: string): GridTabRef | null {
|
|
|
190
207
|
return null;
|
|
191
208
|
}
|
|
192
209
|
|
|
193
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* Short human label for a virtual tab ("posts · grid", "users @ main · grid", "Pages · grid").
|
|
212
|
+
*
|
|
213
|
+
* The Library is just "Library": the `· grid` suffix distinguishes a grid over a source from the
|
|
214
|
+
* document tab of the same name, and the Library has no document tab to be confused with — nor is a
|
|
215
|
+
* grid what it is, only what one of its layouts draws.
|
|
216
|
+
*/
|
|
194
217
|
export function gridTabLabel(id: string): string | null {
|
|
195
218
|
const ref = parseGridTabId(id);
|
|
196
219
|
if (!ref) {
|
|
@@ -200,6 +223,9 @@ export function gridTabLabel(id: string): string | null {
|
|
|
200
223
|
case "pages": {
|
|
201
224
|
return "Pages · grid";
|
|
202
225
|
}
|
|
226
|
+
case "library": {
|
|
227
|
+
return "Library";
|
|
228
|
+
}
|
|
203
229
|
case "collection": {
|
|
204
230
|
return `${ref.name} · grid`;
|
|
205
231
|
}
|
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Redirects as a {@link GridSource} — the editor §11.4 asks for, over the table the grid already
|
|
4
|
+
* is.
|
|
5
|
+
*
|
|
6
|
+
* `project.json`'s `redirects` map is tabular, so it does not need a bespoke editor: it needs the
|
|
7
|
+
* one Studio already has. Columns, inline editing, add/delete rows, undo, the dirty dot, ⌘S,
|
|
8
|
+
* find-and-replace, saved views and CSV export all arrive with the contract. What this module adds
|
|
9
|
+
* is the three things a redirect table needs and a spreadsheet cannot know — that a chain costs a
|
|
10
|
+
* round trip, that a loop is a broken page, and that a rule shadowed by a real route is dead config
|
|
11
|
+
* — and it says each one as a **Problem naming the rule** (studio.md §16) rather than a toast that
|
|
12
|
+
* disappears before the author has read the path it printed.
|
|
13
|
+
*
|
|
14
|
+
* **Every write goes through the one door.** Rows commit with {@link commitProjectConfig}, so a
|
|
15
|
+
* redirect edit is a transaction on the `project.json` document with the same undo, the same
|
|
16
|
+
* serialisation and the same refusal-with-a-Problem as every other configuration write (P6.1). This
|
|
17
|
+
* module never touches `platform.writeFile`.
|
|
18
|
+
*
|
|
19
|
+
* **Row keys are positions.** A redirect's identity in `project.json` IS its source string, and the
|
|
20
|
+
* source is the cell an author most often edits — so keying rows by it would turn every rename into
|
|
21
|
+
* a delete plus an insert and lose the row's pending edits. Positions survive a rename; an insert
|
|
22
|
+
* or a delete is structural and makes the controller reload, which re-keys everything.
|
|
23
|
+
*
|
|
24
|
+
* @docs studio/projects/settings
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { html } from "lit-html";
|
|
28
|
+
import { getPlatform } from "../platform";
|
|
29
|
+
import { projectState } from "../store";
|
|
30
|
+
import { clearProblems, notify } from "../services/notify";
|
|
31
|
+
import { showDialog } from "../ui/layers";
|
|
32
|
+
import { activateTab, openTab, workspace } from "../workspace/workspace";
|
|
33
|
+
import { PROJECT_CONFIG_PATH, commitProjectConfig } from "../tabs/project-config";
|
|
34
|
+
import { pageRoute } from "../panels/tab-strip";
|
|
35
|
+
import { createGridController, getGridController } from "./grid-controller";
|
|
36
|
+
import {
|
|
37
|
+
REDIRECT_TARGETS,
|
|
38
|
+
REWRITE,
|
|
39
|
+
configFromRules,
|
|
40
|
+
parseRedirectImport,
|
|
41
|
+
rulesFromConfig,
|
|
42
|
+
validateRedirects,
|
|
43
|
+
} from "./redirects";
|
|
44
|
+
import { optionalStringArg, stringProperty } from "../commands/command-args";
|
|
45
|
+
import type { RedirectConfig, RedirectRule, RedirectTarget } from "./redirects";
|
|
46
|
+
import type {
|
|
47
|
+
CommitResult,
|
|
48
|
+
GridCellValue,
|
|
49
|
+
GridColumn,
|
|
50
|
+
GridEditBatch,
|
|
51
|
+
GridRow,
|
|
52
|
+
GridRowsResult,
|
|
53
|
+
GridSource,
|
|
54
|
+
} from "./grid-source";
|
|
55
|
+
import type { GridController } from "./grid-controller";
|
|
56
|
+
import type { ProjectConfig } from "@jxsuite/schema/types";
|
|
57
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The redirect table's tab id.
|
|
61
|
+
*
|
|
62
|
+
* A `grid://` id, in the space `grid-source.ts` owns — but NOT yet one of its `GridTabRef` kinds,
|
|
63
|
+
* which is a one-line union member plus three switch cases in a file this workstream does not own.
|
|
64
|
+
* Until that lands `gridTabLabel()` cannot name the tab and the strip falls back to "Untitled".
|
|
65
|
+
*/
|
|
66
|
+
export const REDIRECTS_TAB_ID = "grid://redirects";
|
|
67
|
+
|
|
68
|
+
/** Who the Problems list attributes a redirect finding to. Also the key it clears them by. */
|
|
69
|
+
export const REDIRECTS_PROBLEM_SOURCE = "Redirects";
|
|
70
|
+
|
|
71
|
+
/** Placeholder document for the virtual tab — the grid never reads it; save routes to the source. */
|
|
72
|
+
const GRID_STUB_DOCUMENT = { children: [], tagName: "div" };
|
|
73
|
+
|
|
74
|
+
/** Extensions the router treats as pages. Anything else under `pages/` is an asset, not a route. */
|
|
75
|
+
const PAGE_EXTENSIONS = [".json", ".md", ".html"];
|
|
76
|
+
|
|
77
|
+
// ─── Columns and rows ─────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Three columns, all editable.
|
|
81
|
+
*
|
|
82
|
+
* `source` is not declared `pk`: a primary-key column is frozen and read-only by contract, and
|
|
83
|
+
* retargeting an old URL is the second most common edit a redirect table sees.
|
|
84
|
+
*/
|
|
85
|
+
export function redirectColumns(): GridColumn[] {
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
editable: true,
|
|
89
|
+
field: "source",
|
|
90
|
+
kind: "string",
|
|
91
|
+
required: true,
|
|
92
|
+
title: "Source",
|
|
93
|
+
widthHint: 260,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
editable: true,
|
|
97
|
+
field: "destination",
|
|
98
|
+
kind: "string",
|
|
99
|
+
required: true,
|
|
100
|
+
title: "Destination",
|
|
101
|
+
widthHint: 260,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
editable: true,
|
|
105
|
+
field: "status",
|
|
106
|
+
kind: "enum",
|
|
107
|
+
schema: { enum: REDIRECT_TARGETS.map(String) },
|
|
108
|
+
title: "Status",
|
|
109
|
+
widthHint: 110,
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** One rule as a grid row. Status is text because that is what an enum editor traffics in. */
|
|
115
|
+
export function redirectRow(rule: RedirectRule, index: number): GridRow {
|
|
116
|
+
return {
|
|
117
|
+
cells: { destination: rule.destination, source: rule.source, status: String(rule.status) },
|
|
118
|
+
key: String(index),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The live redirects map, straight off the configuration document. */
|
|
123
|
+
function currentConfig(): RedirectConfig | undefined {
|
|
124
|
+
return projectState?.projectConfig?.redirects as RedirectConfig | undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A cell's text back to a target. An unrecognised value becomes the string itself, so
|
|
129
|
+
* {@link ruleErrors} can name what the author actually typed rather than reporting a silent 0.
|
|
130
|
+
*/
|
|
131
|
+
function parseTarget(raw: string): RedirectTarget {
|
|
132
|
+
const trimmed = raw.trim();
|
|
133
|
+
if (trimmed.toLowerCase() === REWRITE) {
|
|
134
|
+
return REWRITE;
|
|
135
|
+
}
|
|
136
|
+
const value = Number(trimmed);
|
|
137
|
+
return Number.isInteger(value) ? (value as RedirectTarget) : (trimmed as RedirectTarget);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A cell bag back to a rule. An unrecognised target is refused by {@link ruleErrors}. */
|
|
141
|
+
function ruleFromCells(cells: Record<string, GridCellValue>): RedirectRule {
|
|
142
|
+
const text = (value: GridCellValue | undefined) =>
|
|
143
|
+
value === null || value === undefined ? "" : String(value);
|
|
144
|
+
return {
|
|
145
|
+
destination: text(cells.destination).trim(),
|
|
146
|
+
source: text(cells.source).trim(),
|
|
147
|
+
status: parseTarget(text(cells.status)),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Why one rule cannot be written, or null. */
|
|
152
|
+
function ruleErrors(rule: RedirectRule): string | null {
|
|
153
|
+
if (rule.source === "") {
|
|
154
|
+
return "A source path is required.";
|
|
155
|
+
}
|
|
156
|
+
if (rule.destination === "") {
|
|
157
|
+
return "A destination is required.";
|
|
158
|
+
}
|
|
159
|
+
if (!REDIRECT_TARGETS.includes(rule.status)) {
|
|
160
|
+
return `Status must be one of ${REDIRECT_TARGETS.join(", ")}.`;
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Routes, for the shadow check ─────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
export interface ProjectRoutes {
|
|
168
|
+
routes: string[];
|
|
169
|
+
/** False when a directory could not be listed — the shadow check MUST NOT run on a partial list. */
|
|
170
|
+
complete: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The site's real routes, derived from `pages/` by the same function the tab strip labels with.
|
|
175
|
+
*
|
|
176
|
+
* `complete` is the honest half: an unreadable directory means the route list is short, and a short
|
|
177
|
+
* list makes every shadowed rule look clean. The caller reports the gap instead of the finding.
|
|
178
|
+
*/
|
|
179
|
+
export async function projectRoutes(): Promise<ProjectRoutes> {
|
|
180
|
+
const platform = getPlatform();
|
|
181
|
+
const routes = new Set<string>();
|
|
182
|
+
let complete = true;
|
|
183
|
+
|
|
184
|
+
const walkDir = async (dir: string): Promise<void> => {
|
|
185
|
+
let entries;
|
|
186
|
+
try {
|
|
187
|
+
entries = await platform.listDirectory(dir);
|
|
188
|
+
} catch {
|
|
189
|
+
complete = false;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
const path = entry.path || `${dir}/${entry.name}`;
|
|
194
|
+
if (entry.type === "directory") {
|
|
195
|
+
await walkDir(path);
|
|
196
|
+
} else if (PAGE_EXTENSIONS.some((ext) => path.endsWith(ext))) {
|
|
197
|
+
const route = pageRoute(path);
|
|
198
|
+
if (route !== null) {
|
|
199
|
+
routes.add(route);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
await walkDir("pages");
|
|
205
|
+
return { complete, routes: [...routes] };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Re-file the redirect Problems: one per finding, named by its rule, keyed so a re-run replaces
|
|
210
|
+
* rather than stacks.
|
|
211
|
+
*
|
|
212
|
+
* Previous findings are cleared first, so a fixed chain stops being listed without anybody having
|
|
213
|
+
* kept its record id — and a run that finds nothing leaves an empty list rather than a stale one.
|
|
214
|
+
*
|
|
215
|
+
* @returns The number of Problems filed.
|
|
216
|
+
*/
|
|
217
|
+
export function reportRedirectProblems(
|
|
218
|
+
rules: readonly RedirectRule[],
|
|
219
|
+
routes: ProjectRoutes,
|
|
220
|
+
): number {
|
|
221
|
+
clearProblems((record) => record.source === REDIRECTS_PROBLEM_SOURCE);
|
|
222
|
+
const problems = validateRedirects(rules, routes.complete ? routes.routes : []);
|
|
223
|
+
for (const problem of problems) {
|
|
224
|
+
notify(problem.rule === "loop" ? "error" : "warn", problem.message, {
|
|
225
|
+
action: "redirects.open",
|
|
226
|
+
detail: problem.detail,
|
|
227
|
+
key: `redirects.${problem.rule}:${problem.source}`,
|
|
228
|
+
path: PROJECT_CONFIG_PATH,
|
|
229
|
+
source: REDIRECTS_PROBLEM_SOURCE,
|
|
230
|
+
tier: "problem",
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (!routes.complete) {
|
|
234
|
+
notify.warn("Redirects were checked for chains and loops only.", {
|
|
235
|
+
action: "redirects.validate",
|
|
236
|
+
detail:
|
|
237
|
+
"pages/ could not be listed, so this run cannot say whether a rule is shadowed by a real " +
|
|
238
|
+
"route. Reporting no shadowed rules from an incomplete list would be a guess.",
|
|
239
|
+
key: "redirects.routes",
|
|
240
|
+
path: "pages",
|
|
241
|
+
source: REDIRECTS_PROBLEM_SOURCE,
|
|
242
|
+
tier: "problem",
|
|
243
|
+
});
|
|
244
|
+
return problems.length + 1;
|
|
245
|
+
}
|
|
246
|
+
return problems.length;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── The source ───────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The redirect table as a grid source.
|
|
253
|
+
*
|
|
254
|
+
* Rows are read from the live configuration each time, because `project.json` is a document and an
|
|
255
|
+
* undo on its tab is a legitimate way for the table to change underneath this.
|
|
256
|
+
*/
|
|
257
|
+
export function createRedirectsSource(): GridSource {
|
|
258
|
+
const load = () => rulesFromConfig(currentConfig());
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
backingPaths: () => new Map([[PROJECT_CONFIG_PATH, "*"]]),
|
|
262
|
+
capabilities: { delete: true, insert: true, remotePaging: false, remoteSort: false },
|
|
263
|
+
columns: () => Promise.resolve(redirectColumns()),
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Write the batch, then say what happened to **every** row in it.
|
|
267
|
+
*
|
|
268
|
+
* The result is not a log: `edit-buffer.ts`'s `applyCommitResult` clears a pending change only
|
|
269
|
+
* when an outcome naming it comes back `ok`. An arm that writes the file and reports nothing
|
|
270
|
+
* leaves its rows pending forever — the dirty dot never goes out, `grid-controller` counts the
|
|
271
|
+
* write as a failure and skips the reload, and the table shows a phantom row on top of the one
|
|
272
|
+
* it just saved. All three arms report; a refusal reports through {@link refuseAll}.
|
|
273
|
+
*/
|
|
274
|
+
async commit(batch: GridEditBatch): Promise<CommitResult> {
|
|
275
|
+
const rules = load();
|
|
276
|
+
const result: CommitResult = { cells: [], deletes: [], inserts: [] };
|
|
277
|
+
|
|
278
|
+
// Cell edits land on the rule at that position; a row whose position no longer exists is a
|
|
279
|
+
// Row the author cannot see, so it is refused rather than written somewhere else.
|
|
280
|
+
for (const cell of batch.cells) {
|
|
281
|
+
const rule = rules[Number(cell.rowKey)];
|
|
282
|
+
if (!rule) {
|
|
283
|
+
result.cells.push({
|
|
284
|
+
error: "This row is no longer in project.json — refresh the grid.",
|
|
285
|
+
field: cell.field,
|
|
286
|
+
ok: false,
|
|
287
|
+
rowKey: cell.rowKey,
|
|
288
|
+
stale: true,
|
|
289
|
+
});
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const text = cell.value === null ? "" : String(cell.value);
|
|
293
|
+
if (cell.field === "status") {
|
|
294
|
+
rule.status = parseTarget(text);
|
|
295
|
+
} else if (cell.field === "source") {
|
|
296
|
+
rule.source = text.trim();
|
|
297
|
+
} else {
|
|
298
|
+
rule.destination = text.trim();
|
|
299
|
+
}
|
|
300
|
+
result.cells.push({ field: cell.field, ok: true, rowKey: cell.rowKey });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const deleted = new Set(batch.deletes.map((row) => Number(row.rowKey)));
|
|
304
|
+
const next = rules.filter((_rule, index) => !deleted.has(index));
|
|
305
|
+
for (const row of batch.deletes) {
|
|
306
|
+
result.deletes.push({ ok: true, rowKey: row.rowKey });
|
|
307
|
+
}
|
|
308
|
+
for (const insert of batch.inserts) {
|
|
309
|
+
next.push(ruleFromCells(insert.cells));
|
|
310
|
+
result.inserts.push({ ok: true, tempKey: insert.tempKey });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Validity and uniqueness are decided BEFORE anything is written: a half-written redirects
|
|
314
|
+
// Map is a site with some of its old URLs working.
|
|
315
|
+
const seen = new Set<string>();
|
|
316
|
+
const failures: string[] = [];
|
|
317
|
+
for (const rule of next) {
|
|
318
|
+
const error = ruleErrors(rule) ?? (seen.has(rule.source) ? `Duplicate source.` : null);
|
|
319
|
+
if (error) {
|
|
320
|
+
failures.push(`${rule.source || "(blank)"} — ${error}`);
|
|
321
|
+
}
|
|
322
|
+
seen.add(rule.source);
|
|
323
|
+
}
|
|
324
|
+
if (failures.length > 0) {
|
|
325
|
+
return refuseAll(batch, failures.join(" "));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// `undefined` CLEARS the key (see `commitProjectConfig`): an empty table should leave no
|
|
329
|
+
// `"redirects": {}` behind. The local type spells the undefined out, which
|
|
330
|
+
// `exactOptionalPropertyTypes` will not let `Partial<ProjectConfig>` do.
|
|
331
|
+
const patch: { redirects: RedirectConfig | undefined } = {
|
|
332
|
+
redirects: next.length > 0 ? configFromRules(next) : undefined,
|
|
333
|
+
};
|
|
334
|
+
const commit = await commitProjectConfig(patch as Partial<ProjectConfig>);
|
|
335
|
+
if (!commit.ok) {
|
|
336
|
+
// `commitProjectConfig` already filed the Problem; repeating it would list one failure
|
|
337
|
+
// Twice. The cell errors are what keep the rows pending and marked.
|
|
338
|
+
return refuseAll(batch, "project.json could not be written — see Problems.");
|
|
339
|
+
}
|
|
340
|
+
reportRedirectProblems(next, await projectRoutes());
|
|
341
|
+
return result;
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
id: REDIRECTS_TAB_ID,
|
|
345
|
+
label: "Redirects",
|
|
346
|
+
refresh: () => Promise.resolve(),
|
|
347
|
+
rows(): Promise<GridRowsResult> {
|
|
348
|
+
const rules = load();
|
|
349
|
+
return Promise.resolve({
|
|
350
|
+
rows: rules.map((rule, index) => redirectRow(rule, index)),
|
|
351
|
+
total: rules.length,
|
|
352
|
+
});
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Mark every pending change failed with one reason — a refusal is all-or-nothing here. */
|
|
358
|
+
function refuseAll(batch: GridEditBatch, error: string): CommitResult {
|
|
359
|
+
return {
|
|
360
|
+
cells: batch.cells.map((cell) => ({
|
|
361
|
+
error,
|
|
362
|
+
field: cell.field,
|
|
363
|
+
ok: false,
|
|
364
|
+
rowKey: cell.rowKey,
|
|
365
|
+
})),
|
|
366
|
+
deletes: batch.deletes.map((row) => ({ error, ok: false, rowKey: row.rowKey })),
|
|
367
|
+
inserts: batch.inserts.map((row) => ({ error, ok: false, tempKey: row.tempKey })),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── Opening ──────────────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Open (or activate) the redirect table, and hand back its controller.
|
|
375
|
+
*
|
|
376
|
+
* Idempotent, and awaits the first load, so a caller that wants to stage imported rows into the
|
|
377
|
+
* buffer has columns to stage them against.
|
|
378
|
+
*/
|
|
379
|
+
export async function openRedirectsGrid(): Promise<GridController> {
|
|
380
|
+
const existing = workspace.tabs.get(REDIRECTS_TAB_ID);
|
|
381
|
+
if (existing) {
|
|
382
|
+
activateTab(REDIRECTS_TAB_ID);
|
|
383
|
+
const controller = getGridController(existing);
|
|
384
|
+
if (controller) {
|
|
385
|
+
return controller;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const tab =
|
|
389
|
+
existing ??
|
|
390
|
+
openTab({
|
|
391
|
+
capabilities: { modes: ["grid"] },
|
|
392
|
+
document: structuredClone(GRID_STUB_DOCUMENT),
|
|
393
|
+
documentPath: null,
|
|
394
|
+
id: REDIRECTS_TAB_ID,
|
|
395
|
+
});
|
|
396
|
+
const controller = createGridController(tab, createRedirectsSource());
|
|
397
|
+
await controller.load();
|
|
398
|
+
return controller;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ─── Import ───────────────────────────────────────────────────────────────────
|
|
402
|
+
|
|
403
|
+
/** The paste box. One textarea, because both formats are things an author copies out of a file. */
|
|
404
|
+
export function promptRedirectImport(): Promise<string | null> {
|
|
405
|
+
let text = "";
|
|
406
|
+
return showDialog<string | null>(
|
|
407
|
+
(done) => html`
|
|
408
|
+
<sp-dialog-wrapper
|
|
409
|
+
open
|
|
410
|
+
dismissable
|
|
411
|
+
underlay
|
|
412
|
+
headline="Import Redirects"
|
|
413
|
+
confirm-label="Import"
|
|
414
|
+
cancel-label="Cancel"
|
|
415
|
+
@confirm=${() => done(text)}
|
|
416
|
+
@cancel=${() => done(null)}
|
|
417
|
+
@close=${() => done(null)}
|
|
418
|
+
>
|
|
419
|
+
<p>
|
|
420
|
+
Paste a Netlify/Cloudflare <code>_redirects</code> file, or CSV with
|
|
421
|
+
<code>source,destination,status</code> columns. Rows are staged for review — nothing is
|
|
422
|
+
written until you save.
|
|
423
|
+
</p>
|
|
424
|
+
<textarea
|
|
425
|
+
class="jx-grid-input jx-redirect-import"
|
|
426
|
+
rows="10"
|
|
427
|
+
placeholder="/old-page /new-page 301"
|
|
428
|
+
@input=${(e: Event) => (text = (e.target as HTMLTextAreaElement).value)}
|
|
429
|
+
></textarea>
|
|
430
|
+
</sp-dialog-wrapper>
|
|
431
|
+
`,
|
|
432
|
+
{ region: "redirects/import" },
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Stage parsed rules as pending rows on the open table.
|
|
438
|
+
*
|
|
439
|
+
* Staged, not written: an import is the one redirect edit an author has not read line by line, so
|
|
440
|
+
* it arrives in the same reviewable, undoable buffer a typed row does. A source the table already
|
|
441
|
+
* holds is skipped and named — silently overwriting a rule with a pasted one is how an import
|
|
442
|
+
* quietly retargets a URL that was already correct.
|
|
443
|
+
*
|
|
444
|
+
* @returns How many rows were staged.
|
|
445
|
+
*/
|
|
446
|
+
export function stageRedirectImport(
|
|
447
|
+
controller: GridController,
|
|
448
|
+
rules: readonly RedirectRule[],
|
|
449
|
+
errors: readonly string[],
|
|
450
|
+
): number {
|
|
451
|
+
const existing = new Set(
|
|
452
|
+
controller.effectiveRows().map((row) => String(row.source ?? "").trim()),
|
|
453
|
+
);
|
|
454
|
+
const skipped: string[] = [];
|
|
455
|
+
let staged = 0;
|
|
456
|
+
for (const rule of rules) {
|
|
457
|
+
if (existing.has(rule.source)) {
|
|
458
|
+
skipped.push(rule.source);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
existing.add(rule.source);
|
|
462
|
+
controller.addRow({
|
|
463
|
+
destination: rule.destination,
|
|
464
|
+
source: rule.source,
|
|
465
|
+
status: String(rule.status),
|
|
466
|
+
});
|
|
467
|
+
staged += 1;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const notes = [
|
|
471
|
+
...errors,
|
|
472
|
+
...(skipped.length > 0 ? [`Already in the table, skipped: ${skipped.join(", ")}.`] : []),
|
|
473
|
+
];
|
|
474
|
+
if (notes.length > 0) {
|
|
475
|
+
notify.warn(
|
|
476
|
+
`Imported ${staged} redirect${staged === 1 ? "" : "s"} · ${notes.length} line${notes.length === 1 ? "" : "s"} not imported.`,
|
|
477
|
+
{
|
|
478
|
+
action: "redirects.import",
|
|
479
|
+
detail: notes.join("\n"),
|
|
480
|
+
key: "redirects.import",
|
|
481
|
+
path: PROJECT_CONFIG_PATH,
|
|
482
|
+
source: REDIRECTS_PROBLEM_SOURCE,
|
|
483
|
+
tier: "problem",
|
|
484
|
+
},
|
|
485
|
+
);
|
|
486
|
+
} else if (staged > 0) {
|
|
487
|
+
notify.success(`Staged ${staged} redirect${staged === 1 ? "" : "s"} — review, then save.`, {
|
|
488
|
+
action: "file.save",
|
|
489
|
+
key: "redirects.import",
|
|
490
|
+
});
|
|
491
|
+
} else {
|
|
492
|
+
notify.info("Nothing to import.", { key: "redirects.import" });
|
|
493
|
+
}
|
|
494
|
+
return staged;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ─── Commands ─────────────────────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* The redirect verbs.
|
|
501
|
+
*
|
|
502
|
+
* All three are `project` level: a redirect belongs to the site, not to whichever document happens
|
|
503
|
+
* to be open. `redirects.validate` exists as a command because it is the `action` a Problem can
|
|
504
|
+
* offer, and because "check this again" is what an author wants after fixing one.
|
|
505
|
+
*
|
|
506
|
+
* @returns {AnyCommand[]}
|
|
507
|
+
*/
|
|
508
|
+
export function redirectsCommands(): AnyCommand[] {
|
|
509
|
+
return [
|
|
510
|
+
{
|
|
511
|
+
category: "Project",
|
|
512
|
+
id: "redirects.open",
|
|
513
|
+
level: "project",
|
|
514
|
+
menus: ["palette"],
|
|
515
|
+
group: "5_data",
|
|
516
|
+
requires: "an open project",
|
|
517
|
+
when: (ctx) => ctx.project.open,
|
|
518
|
+
aiTool: {
|
|
519
|
+
description:
|
|
520
|
+
"Open the site's redirect rules as an editable table, validated for chains, loops and rules shadowed by a real page.",
|
|
521
|
+
name: "open_redirects",
|
|
522
|
+
},
|
|
523
|
+
run: async () => {
|
|
524
|
+
await openRedirectsGrid();
|
|
525
|
+
reportRedirectProblems(rulesFromConfig(currentConfig()), await projectRoutes());
|
|
526
|
+
},
|
|
527
|
+
title: "Edit Redirects",
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
category: "Project",
|
|
531
|
+
id: "redirects.validate",
|
|
532
|
+
level: "project",
|
|
533
|
+
menus: ["palette"],
|
|
534
|
+
group: "5_data",
|
|
535
|
+
requires: "an open project",
|
|
536
|
+
when: (ctx) => ctx.project.open,
|
|
537
|
+
aiTool: {
|
|
538
|
+
description:
|
|
539
|
+
"Check the site's redirect rules for chains, loops and rules shadowed by a real page. Reports each finding as a Problem.",
|
|
540
|
+
name: "validate_redirects",
|
|
541
|
+
},
|
|
542
|
+
run: async () => {
|
|
543
|
+
const rules = rulesFromConfig(currentConfig());
|
|
544
|
+
const filed = reportRedirectProblems(rules, await projectRoutes());
|
|
545
|
+
if (filed === 0) {
|
|
546
|
+
notify.success(
|
|
547
|
+
`${rules.length} redirect${rules.length === 1 ? "" : "s"} checked — no chains, loops or shadowed rules.`,
|
|
548
|
+
{ key: "redirects.validate" },
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
title: "Validate Redirects",
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
args: {
|
|
556
|
+
additionalProperties: false,
|
|
557
|
+
properties: {
|
|
558
|
+
text: stringProperty(
|
|
559
|
+
"A _redirects file or CSV to import. Omit it and Studio asks for a paste.",
|
|
560
|
+
),
|
|
561
|
+
},
|
|
562
|
+
required: [],
|
|
563
|
+
type: "object",
|
|
564
|
+
},
|
|
565
|
+
category: "Project",
|
|
566
|
+
id: "redirects.import",
|
|
567
|
+
level: "project",
|
|
568
|
+
menus: ["palette"],
|
|
569
|
+
group: "5_data",
|
|
570
|
+
requires: "an open project",
|
|
571
|
+
when: (ctx) => ctx.project.open,
|
|
572
|
+
aiTool: {
|
|
573
|
+
description:
|
|
574
|
+
"Import redirect rules from _redirects or CSV text. Rows are staged in the redirect table for review, not written.",
|
|
575
|
+
name: "import_redirects",
|
|
576
|
+
},
|
|
577
|
+
run: async (_ctx, args) => {
|
|
578
|
+
const given = optionalStringArg("redirects.import", args, "text");
|
|
579
|
+
const text = given ?? (await promptRedirectImport());
|
|
580
|
+
if (text === null || text.trim() === "") {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const parsed = parseRedirectImport(text);
|
|
584
|
+
const controller = await openRedirectsGrid();
|
|
585
|
+
stageRedirectImport(controller, parsed.rules, parsed.errors);
|
|
586
|
+
},
|
|
587
|
+
title: "Import Redirects…",
|
|
588
|
+
},
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Register the redirect commands.
|
|
594
|
+
*
|
|
595
|
+
* @param {CommandRegistry} registry
|
|
596
|
+
*/
|
|
597
|
+
export function registerRedirectsCommands(registry: CommandRegistry): void {
|
|
598
|
+
registry.registerAll(redirectsCommands());
|
|
599
|
+
}
|