@jxsuite/studio 6.1.2 → 8.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/README.md +5 -5
- package/dist/chunks/2020-5wtzce5g.js +4 -0
- package/dist/chunks/adapter-5tphg7xe.js +4 -0
- package/dist/chunks/add-new-flow-psym14sn.js +4 -0
- package/dist/chunks/cf-account-picker-haqazq5r.js +4 -0
- package/dist/chunks/cf-settings-h2wrh6hq.js +4 -0
- package/dist/chunks/{color-area-0fj8a4ga.js → color-area-6rbra5dy.js} +2 -2
- package/dist/chunks/{color-field-xch8kx2g.js → color-field-gy56c94g.js} +3 -3
- package/dist/chunks/color-m7r1vk54.js +4 -0
- package/dist/chunks/{color-slider-jxd8v350.js → color-slider-sqqdyf6c.js} +2 -2
- package/dist/chunks/{combobox-p0efwjke.js → combobox-72h32ejk.js} +3 -3
- package/dist/chunks/components-r22xca09.js +4 -0
- package/dist/chunks/content-source-mmppmcyg.js +4 -0
- package/dist/chunks/convert-to-repeater-he70fc2b.js +4 -0
- package/dist/chunks/editor-ccd9jwsa.js +4 -0
- package/dist/chunks/entry-commands-6qxz9e8p.js +4 -0
- package/dist/chunks/extension-sections-0egb88dm.js +4 -0
- package/dist/chunks/file-ops-qnab36ek.js +4 -0
- package/dist/chunks/files-rcn56qvm.js +4 -0
- package/dist/chunks/format-host-yxz0kjjs.js +4 -0
- package/dist/chunks/fs-events-d1qxq4z4.js +4 -0
- package/dist/chunks/grid-open-4z1g062t.js +4 -0
- package/dist/chunks/{grid-view-d2wq9ezg.js → grid-view-qg1b5eqx.js} +25 -25
- package/dist/chunks/{icons-pnfz478q.js → icons-63bjtbs7.js} +2 -2
- package/dist/chunks/{iframe-entry-jdcktez8.js → iframe-entry-2zggzajv.js} +4 -4
- package/dist/chunks/{iframe-entry-zap785cw.js → iframe-entry-76j8vwwx.js} +4 -4
- package/dist/chunks/iframe-entry-99nr5zfb.js +5 -0
- package/dist/chunks/{iframe-entry-x0ygf2yr.js → iframe-entry-pkgmcgp0.js} +4 -4
- package/dist/chunks/iframe-host-ssv7kyj0.js +4 -0
- package/dist/chunks/{index-zpjmzqfk.js → index-32eqzv1k.js} +3 -3
- package/dist/chunks/index-f0jn7nnp.js +4 -0
- package/dist/chunks/{javascript-m54mnwp6.js → javascript-pjp734q4.js} +3 -3
- package/dist/chunks/jsonMode-njkvsa02.js +13 -0
- package/dist/chunks/jx-validate-ttasrbh7.js +4 -0
- package/dist/chunks/layers-bgyr11tr.js +4 -0
- package/dist/chunks/layers-panel-7ez66zeq.js +4 -0
- package/dist/chunks/{listbox-e74qjyq6.js → listbox-c421cf8c.js} +2 -2
- package/dist/chunks/lit-html-v9gh687x.js +4 -0
- package/dist/chunks/media-library-sq468wts.js +4 -0
- package/dist/chunks/media-picker-shfe59pt.js +4 -0
- package/dist/chunks/{menu-n6yzswmy.js → menu-bdpzbfvs.js} +2 -2
- package/dist/chunks/{monaco-binding-7gx83x3r.js → monaco-binding-vq1hgnn0.js} +3 -3
- package/dist/chunks/monaco-buffer-3afgwvbp.js +4 -0
- package/dist/chunks/monaco-setup-xrd2cbkp.js +4 -0
- package/dist/chunks/notify-kzrjr3fv.js +4 -0
- package/dist/chunks/parse-v77xtwv7.js +4 -0
- package/dist/chunks/platform-7v4rtd7m.js +4 -0
- package/dist/chunks/popover-b89sfej7.js +4 -0
- package/dist/chunks/publish-panel-2z9gw8ee.js +4 -0
- package/dist/chunks/publish-quick-qx2455cj.js +4 -0
- package/dist/chunks/publish-workspace-wrc1dgwx.js +4 -0
- package/dist/chunks/quick-search-n0t0eqxb.js +4 -0
- package/dist/chunks/references-9547rg5s.js +4 -0
- package/dist/chunks/right-panel-wsgnr9pn.js +4 -0
- package/dist/chunks/source-control-kwktr9y6.js +4 -0
- package/dist/chunks/{split-n2gfpm5m.js → split-c9zfdgxf.js} +2 -2
- package/dist/chunks/studio-03ct6yfv.js +9 -0
- package/dist/chunks/studio-07rcfje3.js +5 -0
- package/dist/chunks/studio-3e54qh39.js +7 -0
- package/dist/chunks/studio-3exxhz5g.js +5 -0
- package/dist/chunks/studio-3q40e3rj.js +5 -0
- package/dist/chunks/studio-3qeh9t0h.js +6 -0
- package/dist/chunks/studio-3qqjeep5.js +6 -0
- package/dist/chunks/studio-43sd2mam.js +4 -0
- package/dist/chunks/studio-4hp8pkyt.js +5 -0
- package/dist/chunks/studio-58xg6j2q.js +9 -0
- package/dist/chunks/studio-5g0rprdt.js +7 -0
- package/dist/chunks/{studio-2j1cb95x.js → studio-6m380wp7.js} +4 -4
- package/dist/chunks/studio-80jyh1xh.js +4 -0
- package/dist/chunks/studio-83mk4bgg.js +9 -0
- package/dist/chunks/{studio-v0jzqdjm.js → studio-881z1xrx.js} +4 -4
- package/dist/chunks/studio-8m0y2qe7.js +5 -0
- package/dist/chunks/{studio-d81xfh1g.js → studio-8tr6cfrc.js} +4 -4
- package/dist/chunks/studio-8vcvfs7r.js +6 -0
- package/dist/chunks/studio-9tmebf0s.js +5 -0
- package/dist/chunks/studio-a2bvaptz.js +5 -0
- package/dist/chunks/studio-b7y5mr30.js +5 -0
- package/dist/chunks/studio-bavb31r0.js +5 -0
- package/dist/chunks/{studio-dgz8d0c1.js → studio-bbe4c90a.js} +4 -4
- package/dist/chunks/studio-c05wdnt3.js +5 -0
- package/dist/chunks/studio-ccv2svc6.js +5 -0
- package/dist/chunks/studio-d5grtb9e.js +16 -0
- package/dist/chunks/studio-df0baea9.js +5 -0
- package/dist/chunks/studio-dgk4dqtx.js +7 -0
- package/dist/chunks/studio-e1g64q43.js +5 -0
- package/dist/chunks/studio-ed1hp6zf.js +6 -0
- package/dist/chunks/studio-ed3xx40h.js +7 -0
- package/dist/chunks/studio-f14e1zdj.js +5 -0
- package/dist/chunks/studio-fbhxxc5f.js +5 -0
- package/dist/chunks/studio-fyh0jc96.js +5 -0
- package/dist/chunks/studio-gcxk20n7.js +5 -0
- package/dist/chunks/studio-jaaz8ce9.js +5 -0
- package/dist/chunks/{studio-90pq9km1.js → studio-jcnsdaep.js} +4 -4
- package/dist/chunks/studio-jsgf2q5k.js +7 -0
- package/dist/chunks/{studio-0q988f9w.js → studio-k3xxbagr.js} +3 -3
- package/dist/chunks/{studio-5ve6bbep.js → studio-k7evxqh4.js} +4 -4
- package/dist/chunks/studio-kt3pjkwd.js +5 -0
- package/dist/chunks/studio-m8g16bgz.js +5 -0
- package/dist/chunks/studio-mn0476ag.js +5 -0
- package/dist/chunks/studio-mx1smyq0.js +5 -0
- package/dist/chunks/studio-n0c5h6mf.js +5 -0
- package/dist/chunks/{studio-485046te.js → studio-nh6d342n.js} +7 -7
- package/dist/chunks/studio-nqxqf8p2.js +4 -0
- package/dist/chunks/studio-p4nrkvqr.js +5 -0
- package/dist/chunks/{studio-1fxkaaea.js → studio-pbxa5fpc.js} +4 -4
- package/dist/chunks/studio-pgmvdv7z.js +7 -0
- package/dist/chunks/studio-ph84avkf.js +5 -0
- package/dist/chunks/{studio-tq0rtfwj.js → studio-q0mvbaq6.js} +4 -4
- package/dist/chunks/studio-q0tccd43.js +5 -0
- package/dist/chunks/studio-q7rtx6zq.js +14 -0
- package/dist/chunks/{studio-a3g98wjr.js → studio-qgh13jtz.js} +4 -4
- package/dist/chunks/studio-qmn1xc24.js +5 -0
- package/dist/chunks/studio-qwdqwkj0.js +5 -0
- package/dist/chunks/studio-r5qy30vx.js +5 -0
- package/dist/chunks/studio-rj9eamf8.js +5 -0
- package/dist/chunks/studio-rptgsk96.js +5 -0
- package/dist/chunks/studio-rthyg42z.js +928 -0
- package/dist/chunks/studio-s19tp1xr.js +5 -0
- package/dist/chunks/studio-s32578pn.js +5 -0
- package/dist/chunks/studio-svb7yyve.js +5 -0
- package/dist/chunks/studio-syreb644.js +5 -0
- package/dist/chunks/studio-tf9jryb1.js +10 -0
- package/dist/chunks/studio-tfkznv39.js +5 -0
- package/dist/chunks/studio-tft6r105.js +5 -0
- package/dist/chunks/studio-txf2kysx.js +5 -0
- package/dist/chunks/studio-v8867ddv.js +12 -0
- package/dist/chunks/studio-vxq7fcvv.js +5 -0
- package/dist/chunks/studio-w4rz9hdk.js +5 -0
- package/dist/chunks/studio-wbcjbhy1.js +9 -0
- package/dist/chunks/studio-wc649wss.js +5 -0
- package/dist/chunks/studio-wys23mxz.js +5 -0
- package/dist/chunks/studio-x1x167jh.js +5 -0
- package/dist/chunks/studio-x9hsze61.js +5 -0
- package/dist/chunks/studio-y0n7yxea.js +9 -0
- package/dist/chunks/studio-y3ahhdpx.js +5 -0
- package/dist/chunks/studio-z97c48fk.js +5 -0
- package/dist/chunks/studio-zat62m0c.js +5 -0
- package/dist/chunks/studio-zcd2e7yz.js +5 -0
- package/dist/chunks/studio-zz9qe3rw.js +64 -0
- package/dist/chunks/toast-1gv0h53h.js +4 -0
- package/dist/chunks/{toast-host-ke5z4ng7.js → toast-host-1yg5ar8d.js} +3 -3
- package/dist/chunks/{tooltip-5v8ngwj2.js → tooltip-yypz28gq.js} +3 -3
- package/dist/chunks/{tree-mdq3v5cw.js → tree-9xa4hm7n.js} +2 -2
- package/dist/chunks/tsMode-epyfz1hv.js +14 -0
- package/dist/chunks/workspace-dz7nxfhv.js +4 -0
- package/dist/chunks/workspace-home-xsd7hkjw.js +4 -0
- package/dist/chunks/{ws-client-nze0k6mv.js → ws-client-bv552sfx.js} +3 -3
- package/dist/iframe-entry.js +20 -16
- package/dist/manifest.json +77 -63
- package/dist/studio.js +103 -104
- package/dist/workers/ts.worker.js +3 -3
- package/package.json +12 -11
- package/src/browse/component-examples.ts +272 -0
- package/src/browse/library-commands.ts +64 -104
- package/src/browse/library-layouts.ts +263 -49
- package/src/browse/library-model.ts +181 -133
- package/src/browse/library-pane.ts +738 -446
- package/src/browse/library-preview.ts +149 -19
- package/src/browse/library-source.ts +308 -41
- package/src/browse/preview-root.ts +78 -0
- package/src/browse/preview-style.ts +155 -0
- package/src/canvas/asset-refs.ts +16 -1
- package/src/canvas/canvas-live-render.ts +222 -30
- package/src/canvas/canvas-origin.ts +37 -12
- package/src/canvas/canvas-page-context.ts +112 -0
- package/src/canvas/canvas-render.ts +113 -107
- package/src/canvas/canvas-utils.ts +57 -12
- package/src/canvas/iframe-drop.ts +107 -0
- package/src/canvas/iframe-entry.ts +24 -12
- package/src/canvas/iframe-host.ts +134 -62
- package/src/canvas/iframe-keys.ts +11 -0
- package/src/canvas/iframe-patch.ts +8 -7
- package/src/canvas/iframe-protocol.ts +24 -5
- package/src/canvas/iframe-render.ts +48 -1
- package/src/canvas/iframe-subtree.ts +5 -2
- package/src/canvas/npm-modules.ts +22 -0
- package/src/canvas/preview-navigate.ts +16 -0
- package/src/canvas/repeater-sources.ts +200 -0
- package/src/canvas/stage-scrollbar.ts +5 -1
- package/src/canvas/surface-registry.ts +6 -0
- package/src/commands/app-commands.ts +16 -6
- package/src/commands/budget.ts +10 -8
- package/src/commands/context.ts +35 -10
- package/src/commands/defaults.ts +69 -45
- package/src/commands/keymap.ts +9 -0
- package/src/commands/levels.ts +11 -5
- package/src/commands/live-context.ts +2 -0
- package/src/commands/registry.ts +10 -1
- package/src/content/collection-match.ts +6 -5
- package/src/content/collection-pages.ts +412 -0
- package/src/content/content-collections.ts +270 -0
- package/src/content/entry-commands.ts +51 -60
- package/src/content/entry-editor.ts +111 -18
- package/src/content/entry-fields.ts +13 -1
- package/src/content/entry-inspector.ts +225 -0
- package/src/content/entry-model.ts +17 -3
- package/src/content/entry-open.ts +80 -0
- package/src/content/entry-summaries.ts +440 -0
- package/src/content/file-record-ids.ts +80 -0
- package/src/content/file-records.ts +125 -0
- package/src/content/page-summaries.ts +129 -0
- package/src/content/record-entry.ts +201 -0
- package/src/content/writing-banner.ts +177 -0
- package/src/create/add-new-commands.ts +185 -0
- package/src/create/add-new-flow.ts +1116 -0
- package/src/create/add-new.ts +1014 -0
- package/src/create/element-name.ts +121 -0
- package/src/editor/convert-to-component.ts +67 -43
- package/src/editor/file-drop-action.ts +9 -1
- package/src/editor/inline-edit-apply.ts +44 -0
- package/src/editor/inline-edit.ts +10 -0
- package/src/editor/insert-component.ts +91 -0
- package/src/editor/insert-zone-action.ts +13 -4
- package/src/editor/shortcuts.ts +44 -22
- package/src/editor/slash-menu.ts +51 -8
- package/src/files/components.ts +74 -10
- package/src/files/file-ops.ts +15 -5
- package/src/files/files.ts +164 -150
- package/src/files/fs-events.ts +18 -7
- package/src/files/media-meta.ts +95 -1
- package/src/files/media-types.ts +55 -0
- package/src/files/media-upload.ts +209 -52
- package/src/files/new-file-name.ts +156 -0
- package/src/files/serialize-document.ts +13 -3
- package/src/format/format-host.ts +15 -0
- package/src/github/github-auth.ts +2 -2
- package/src/github/github-publish.ts +4 -3
- package/src/grid/grid-controller.ts +5 -1
- package/src/grid/grid-open.ts +0 -27
- package/src/grid/grid-panel.ts +54 -2
- package/src/grid/grid-reveal.ts +34 -0
- package/src/grid/grid-source.ts +3 -25
- package/src/grid/grid-view.ts +31 -6
- package/src/grid/sources/content-source.ts +75 -7
- package/src/grid/tabulator-tables.d.ts +6 -0
- package/src/hosting/document.ts +2 -2
- package/src/i18n/i18n-commands.ts +2 -1
- package/src/media/media-commands.ts +181 -0
- package/src/media/media-library-model.ts +232 -0
- package/src/media/media-library.ts +1283 -0
- package/src/media/media-pane.ts +47 -19
- package/src/packages/pull-package-sync.ts +1 -1
- package/src/panels/activity-panel.ts +21 -5
- package/src/panels/ai-chat/chat-markdown.ts +8 -8
- package/src/panels/block-action-bar.ts +11 -11
- package/src/panels/canvas-dnd-bridge.ts +5 -2
- package/src/panels/chat-panel.ts +6 -6
- package/src/panels/collections-panel.ts +306 -0
- package/src/panels/component-preview.ts +35 -13
- package/src/panels/data-explorer.ts +1 -1
- package/src/panels/data-grid.ts +4 -0
- package/src/panels/dnd.ts +17 -30
- package/src/panels/elements-panel.ts +9 -2
- package/src/panels/empty-state.ts +2 -2
- package/src/panels/formula-workspace.ts +2 -3
- package/src/panels/frontmatter-fields.ts +9 -9
- package/src/panels/git-diff-open.ts +9 -5
- package/src/panels/head-panel.ts +369 -103
- package/src/panels/i18n-panel.ts +11 -10
- package/src/panels/imports-panel.ts +2 -0
- package/src/panels/jump-bar.ts +27 -2
- package/src/panels/layers-panel.ts +36 -30
- package/src/panels/left-panel.ts +22 -8
- package/src/panels/navigator-panels.ts +23 -11
- package/src/panels/pane-context.ts +111 -29
- package/src/panels/pane-grid.ts +6 -6
- package/src/panels/panel-registry.ts +14 -9
- package/src/panels/properties-panel.ts +7 -3
- package/src/panels/right-panel.ts +199 -43
- package/src/panels/seo-modal.ts +41 -108
- package/src/panels/settings-menu.ts +52 -50
- package/src/panels/settings-pane.ts +268 -162
- package/src/panels/signals-panel.ts +3 -4
- package/src/panels/site-model.ts +367 -0
- package/src/panels/site-panel.ts +638 -0
- package/src/panels/style-panel.ts +37 -3
- package/src/panels/stylebook-doc.ts +142 -10
- package/src/panels/stylebook-layers-panel.ts +39 -80
- package/src/panels/stylebook-panel.ts +47 -10
- package/src/panels/styles-panel.ts +327 -0
- package/src/panels/tab-strip.ts +109 -25
- package/src/platform.ts +1 -1
- package/src/platforms/cloud.ts +28 -1
- package/src/platforms/default-platform.ts +4 -4
- package/src/platforms/devserver.ts +51 -25
- package/src/publish/deploy-checklist.ts +12 -7
- package/src/publish/pages-service.ts +38 -14
- package/src/publish/publish-changes.ts +491 -0
- package/src/publish/publish-commands.ts +83 -18
- package/src/publish/publish-ids.ts +49 -0
- package/src/publish/publish-quick.ts +148 -0
- package/src/publish/publish-workspace.ts +926 -0
- package/src/publish/source-control.ts +907 -0
- package/src/services/automation.ts +16 -20
- package/src/services/cf-settings.ts +4 -4
- package/src/services/connection.ts +2 -2
- package/src/services/directory-picker.ts +2 -2
- package/src/services/references.ts +0 -0
- package/src/services/render-critic.ts +21 -2
- package/src/services/settings/definitions.ts +25 -14
- package/src/settings/contributed-section.ts +49 -10
- package/src/settings/css-vars-editor.ts +93 -27
- package/src/settings/developer-panels.ts +43 -0
- package/src/settings/extension-sections.ts +20 -2
- package/src/settings/general-settings.ts +91 -21
- package/src/settings/preferences-dialog.ts +8 -1
- package/src/settings/project-sections.ts +27 -10
- package/src/settings/schema-field-ui.ts +3 -1
- package/src/settings/section-registry.ts +116 -11
- package/src/settings/settings-dialog.ts +192 -0
- package/src/settings/settings-document.ts +182 -162
- package/src/shell.ts +467 -431
- package/src/site-context.ts +64 -10
- package/src/state.ts +28 -9
- package/src/studio.ts +194 -41
- package/src/style/project-styles.ts +34 -0
- package/src/style/stylebook-specimens.ts +89 -0
- package/src/style/stylebook-target.ts +26 -0
- package/src/style/tag-inheritance.ts +105 -0
- package/src/style/variables-focus.ts +61 -0
- package/src/style/variables-view.ts +235 -0
- package/src/surfaces/add-new.json +1482 -0
- package/src/surfaces/add-new.ts +325 -0
- package/src/surfaces/ai-chat.ts +2 -2
- package/src/surfaces/boot-failure.json +4 -3
- package/src/surfaces/bottom-dock.json +1 -1
- package/src/surfaces/canvas-stage.json +30 -19
- package/src/surfaces/canvas-stage.ts +5 -5
- package/src/surfaces/collections-panel.json +615 -0
- package/src/surfaces/collections-panel.ts +168 -0
- package/src/surfaces/commandbar.json +241 -96
- package/src/surfaces/commandbar.ts +134 -99
- package/src/surfaces/empty-state.json +1 -1
- package/src/surfaces/entry-editor.json +136 -0
- package/src/surfaces/entry-editor.ts +21 -0
- package/src/surfaces/entry-inspector.json +371 -0
- package/src/surfaces/entry-inspector.ts +143 -0
- package/src/surfaces/inspector-dock.json +162 -14
- package/src/surfaces/inspector-dock.ts +46 -19
- package/src/surfaces/library-pane.json +670 -437
- package/src/surfaces/library-pane.ts +94 -59
- package/src/surfaces/media-browser.json +62 -6
- package/src/surfaces/media-browser.ts +15 -1
- package/src/surfaces/media-field.json +1 -1
- package/src/surfaces/media-library.json +1586 -0
- package/src/surfaces/media-library.ts +282 -0
- package/src/surfaces/media-pick-dialog.json +71 -0
- package/src/surfaces/media-pick-dialog.ts +132 -0
- package/src/surfaces/menu.ts +18 -0
- package/src/surfaces/pane-context.json +144 -77
- package/src/surfaces/pane-context.ts +24 -0
- package/src/surfaces/panel-deploy-checklist.json +12 -6
- package/src/surfaces/panel-deploy-checklist.ts +3 -1
- package/src/surfaces/panel-page.json +72 -2
- package/src/surfaces/panel-page.ts +38 -7
- package/src/surfaces/panel-signals.json +1 -1
- package/src/surfaces/panel-signals.ts +5 -5
- package/src/surfaces/panel-stylebook-layers.json +71 -204
- package/src/surfaces/panel-stylebook-layers.ts +0 -21
- package/src/surfaces/preferences.json +120 -48
- package/src/surfaces/preferences.ts +8 -0
- package/src/surfaces/publish-quick.json +428 -0
- package/src/surfaces/publish-quick.ts +165 -0
- package/src/surfaces/publish-workspace.json +2892 -0
- package/src/surfaces/publish-workspace.ts +391 -0
- package/src/surfaces/rail.json +1 -159
- package/src/surfaces/rail.ts +18 -75
- package/src/surfaces/schema-form.json +10 -2
- package/src/surfaces/schema-form.ts +2 -0
- package/src/surfaces/seo.json +1 -1
- package/src/surfaces/settings-contributed.json +5 -5
- package/src/surfaces/settings-contributed.ts +18 -2
- package/src/surfaces/settings-css-vars.json +44 -36
- package/src/surfaces/settings-css-vars.ts +77 -4
- package/src/surfaces/settings-deploy.json +2 -15
- package/src/surfaces/settings-deploy.ts +1 -1
- package/src/surfaces/settings-dialog.json +51 -0
- package/src/surfaces/settings-dialog.ts +169 -0
- package/src/surfaces/settings-overview.json +3 -3
- package/src/surfaces/settings-overview.ts +9 -2
- package/src/surfaces/settings-pane.json +269 -46
- package/src/surfaces/settings-pane.ts +85 -24
- package/src/surfaces/shell.json +2 -1
- package/src/surfaces/site-panel.json +1029 -0
- package/src/surfaces/site-panel.ts +233 -0
- package/src/surfaces/slash-menu.json +63 -21
- package/src/surfaces/slash-menu.ts +23 -0
- package/src/surfaces/statusbar.ts +39 -15
- package/src/surfaces/styles-panel.json +735 -0
- package/src/surfaces/styles-panel.ts +195 -0
- package/src/surfaces/styles-variables.json +149 -0
- package/src/surfaces/styles-variables.ts +145 -0
- package/src/surfaces/tab-strip.json +4 -4
- package/src/surfaces/welcome.ts +1 -1
- package/src/surfaces/writing-banner.json +94 -0
- package/src/surfaces/writing-banner.ts +80 -0
- package/src/tabs/bare-repeaters.ts +101 -0
- package/src/tabs/tab.ts +47 -7
- package/src/types.ts +21 -26
- package/src/ui/form-controls.ts +47 -8
- package/src/ui/layers.ts +13 -0
- package/src/ui/media-picker.ts +284 -40
- package/src/ui/regions.ts +7 -6
- package/src/ui/schema-form.ts +259 -27
- package/src/ui/virtual-window.ts +26 -1
- package/src/utils/base64.ts +3 -3
- package/src/utils/edit-display.ts +322 -67
- package/src/utils/local-day.ts +18 -0
- package/src/workspace/pane-derive.ts +4 -4
- package/src/workspace/session.ts +28 -7
- package/src/workspace/tab-workspace.ts +137 -0
- package/src/workspace/workspace-back.ts +189 -0
- package/src/workspace/workspace-home.ts +133 -0
- package/src/workspace/workspace-ids.ts +129 -0
- package/src/workspace/workspace-switch.ts +372 -0
- package/src/workspace/workspace.ts +125 -11
- package/styles/shell-frame.css +30 -7
- package/styles/shell-frame.json +15 -3
- package/dist/chunks/2020-f2gkpy8d.js +0 -4
- package/dist/chunks/adapter-4912g339.js +0 -4
- package/dist/chunks/cf-account-picker-wccaenh6.js +0 -4
- package/dist/chunks/cf-settings-br6105tr.js +0 -4
- package/dist/chunks/color-28ksk2qm.js +0 -4
- package/dist/chunks/components-db68vsn7.js +0 -4
- package/dist/chunks/content-source-1449qesq.js +0 -4
- package/dist/chunks/convert-to-repeater-5h457egj.js +0 -4
- package/dist/chunks/editor-x9jn86a7.js +0 -4
- package/dist/chunks/entry-commands-may24cpt.js +0 -4
- package/dist/chunks/extension-sections-pvr14vxr.js +0 -4
- package/dist/chunks/file-ops-x9v54pcj.js +0 -4
- package/dist/chunks/files-j3knqf0s.js +0 -4
- package/dist/chunks/format-host-tyre2trb.js +0 -4
- package/dist/chunks/fs-events-64ma9htc.js +0 -4
- package/dist/chunks/git-panel-84ps5dex.js +0 -4
- package/dist/chunks/iframe-entry-fvbk89gm.js +0 -5
- package/dist/chunks/iframe-host-ck6vejt5.js +0 -4
- package/dist/chunks/index-b8xw3vy9.js +0 -4
- package/dist/chunks/jsonMode-w15cmrcm.js +0 -13
- package/dist/chunks/jx-validate-at3zn8wa.js +0 -4
- package/dist/chunks/layers-h4xp1pss.js +0 -4
- package/dist/chunks/layers-panel-m5hw1d7g.js +0 -4
- package/dist/chunks/lit-html-8t8t3dnr.js +0 -4
- package/dist/chunks/media-picker-2cweexc5.js +0 -4
- package/dist/chunks/menu-ynw13y0s.js +0 -4
- package/dist/chunks/monaco-buffer-qzxn567r.js +0 -4
- package/dist/chunks/monaco-setup-gxf6656g.js +0 -4
- package/dist/chunks/notify-cj5zbrs2.js +0 -4
- package/dist/chunks/parse-b7ddhr8m.js +0 -4
- package/dist/chunks/platform-3dfws9js.js +0 -4
- package/dist/chunks/popover-jt2gxmzs.js +0 -4
- package/dist/chunks/publish-panel-1gvxbj21.js +0 -4
- package/dist/chunks/quick-search-0dajrwqg.js +0 -4
- package/dist/chunks/references-49qgt404.js +0 -4
- package/dist/chunks/right-panel-jm1bwe53.js +0 -4
- package/dist/chunks/section-registry-6kwc4fky.js +0 -4
- package/dist/chunks/studio-0rt52vpq.js +0 -5
- package/dist/chunks/studio-18vgbhs5.js +0 -5
- package/dist/chunks/studio-24mccdrj.js +0 -5
- package/dist/chunks/studio-2eh507zk.js +0 -9
- package/dist/chunks/studio-2hcbccfv.js +0 -5
- package/dist/chunks/studio-2jbs3rxv.js +0 -5
- package/dist/chunks/studio-4h8k4dw9.js +0 -5
- package/dist/chunks/studio-5f6n18kx.js +0 -5
- package/dist/chunks/studio-5gthpky6.js +0 -5
- package/dist/chunks/studio-6ep3js6b.js +0 -9
- package/dist/chunks/studio-6wkmf3wz.js +0 -9
- package/dist/chunks/studio-841rf3cg.js +0 -5
- package/dist/chunks/studio-8gmwgheg.js +0 -5
- package/dist/chunks/studio-8qm0ep50.js +0 -5
- package/dist/chunks/studio-924p1tm1.js +0 -6
- package/dist/chunks/studio-a3shpz9y.js +0 -5
- package/dist/chunks/studio-amsbaaar.js +0 -6
- package/dist/chunks/studio-at5spjbc.js +0 -7
- package/dist/chunks/studio-b2xv945e.js +0 -5
- package/dist/chunks/studio-bn5nkafw.js +0 -5
- package/dist/chunks/studio-bw6q5xjf.js +0 -9
- package/dist/chunks/studio-c7q0t9x1.js +0 -5
- package/dist/chunks/studio-camnhc96.js +0 -6
- package/dist/chunks/studio-cffqvnzd.js +0 -5
- package/dist/chunks/studio-efvfrsf8.js +0 -5
- package/dist/chunks/studio-er30bpcx.js +0 -5
- package/dist/chunks/studio-ezscgr64.js +0 -5
- package/dist/chunks/studio-ffrrncvz.js +0 -5
- package/dist/chunks/studio-fpphgn8y.js +0 -10
- package/dist/chunks/studio-j1kvhdjk.js +0 -4
- package/dist/chunks/studio-j430n16g.js +0 -7
- package/dist/chunks/studio-jjkg84nj.js +0 -5
- package/dist/chunks/studio-m0q32ars.js +0 -4
- package/dist/chunks/studio-m6bdst11.js +0 -5
- package/dist/chunks/studio-nv2cptd4.js +0 -64
- package/dist/chunks/studio-nwfrc98n.js +0 -5
- package/dist/chunks/studio-p1wgvp7r.js +0 -5
- package/dist/chunks/studio-pag2wncm.js +0 -5
- package/dist/chunks/studio-qvanvzfd.js +0 -5
- package/dist/chunks/studio-qwa64kt7.js +0 -5
- package/dist/chunks/studio-rk1zc4x3.js +0 -928
- package/dist/chunks/studio-se2r3ptc.js +0 -5
- package/dist/chunks/studio-sg2a0dh0.js +0 -5
- package/dist/chunks/studio-tjzjhe68.js +0 -5
- package/dist/chunks/studio-v2w7q8r1.js +0 -7
- package/dist/chunks/studio-v5vjq1h0.js +0 -4
- package/dist/chunks/studio-v8aga3vp.js +0 -14
- package/dist/chunks/studio-vfcdrfmv.js +0 -5
- package/dist/chunks/studio-vmfkgdjf.js +0 -5
- package/dist/chunks/studio-wt1816jj.js +0 -5
- package/dist/chunks/studio-wz73q5kq.js +0 -4
- package/dist/chunks/studio-y04qtxvp.js +0 -7
- package/dist/chunks/studio-y21g8zzg.js +0 -5
- package/dist/chunks/studio-y64hrt8p.js +0 -12
- package/dist/chunks/studio-ynn90f58.js +0 -16
- package/dist/chunks/studio-yxz682sx.js +0 -5
- package/dist/chunks/studio-zp7snvkq.js +0 -5
- package/dist/chunks/studio-zq4z6tm6.js +0 -5
- package/dist/chunks/toast-c9fn075p.js +0 -4
- package/dist/chunks/tsMode-sf49ee9x.js +0 -14
- package/dist/chunks/workspace-jtfahpee.js +0 -4
- package/src/panels/frontmatter-panel.ts +0 -711
- package/src/panels/git-panel.ts +0 -1108
- package/src/surfaces/doc-header.json +0 -776
- package/src/surfaces/doc-header.ts +0 -222
- package/src/surfaces/git-panel.json +0 -1880
- package/src/surfaces/git-panel.ts +0 -264
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Source control: what git is asked, in what order, and what each verb does before it runs.
|
|
4
|
+
*
|
|
5
|
+
* This is the flow the Publish workspace owns (studio.md §5.5, §22.8). It used to be the Source
|
|
6
|
+
* Control Navigator panel's own module, and the panel's buttons were the only door to most of it;
|
|
7
|
+
* the panel is gone, and its verbs live here so that Review changes, Branches, the quick publish
|
|
8
|
+
* dialog, the palette and the deploy checklist all run ONE list of them (`studio-ui-guidelines.md`
|
|
9
|
+
* §12.5). Nothing here draws: `publish/publish-workspace.ts` projects `shell.git` into its surface,
|
|
10
|
+
* and every write lands on that one hoisted record.
|
|
11
|
+
*
|
|
12
|
+
* Three rules worth knowing before editing it.
|
|
13
|
+
*
|
|
14
|
+
* **Every verb ends in a refresh.** {@link gitAction} runs the platform call under one loading and
|
|
15
|
+
* error contract and re-reads the status after it, because a commit, a discard or a checkout moves
|
|
16
|
+
* the working tree and the count on the Publish tab is read from the status.
|
|
17
|
+
*
|
|
18
|
+
* **A comparison follows the working tree.** A comparison is two texts read once, so a save or a
|
|
19
|
+
* commit does not reach it on its own; {@link refreshGitStatus} and {@link noteFileSaved} bump
|
|
20
|
+
* `shell.git.rev` (which re-issues a Diff lens's read) and re-read the one comparison
|
|
21
|
+
* {@link openFileComparison} opened (§21.5).
|
|
22
|
+
*
|
|
23
|
+
* **The platform's git semantics decide the copy, not an assumption.** On a host whose commits land
|
|
24
|
+
* on the remote and whose branches are sessions (`StudioPlatform.gitHosted`, the cloud), a checkout
|
|
25
|
+
* reloads the window, so {@link checkoutBranch} asks first.
|
|
26
|
+
*
|
|
27
|
+
* @docs studio/publish/source-control
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { errorMessage } from "@jxsuite/schema/parse";
|
|
31
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
32
|
+
import type { GitDiffState, GitFileStatus, StudioPlatform } from "../types";
|
|
33
|
+
import { getPlatform, hasPlatform } from "../platform";
|
|
34
|
+
import { now } from "../services/clock";
|
|
35
|
+
import {
|
|
36
|
+
PAGES_COLLECTION,
|
|
37
|
+
collectionKeyOfPath,
|
|
38
|
+
contentCollections,
|
|
39
|
+
} from "../content/content-collections";
|
|
40
|
+
import { MEDIA_EXTENSIONS, extensionOf } from "../files/media-types";
|
|
41
|
+
import { projectState } from "../store";
|
|
42
|
+
import { comparisonRefusal, openComparisonTab } from "../panels/git-diff-open";
|
|
43
|
+
import type { Tab } from "../tabs/tab";
|
|
44
|
+
import { shell } from "../shell";
|
|
45
|
+
import type { GitLogEntry } from "../shell";
|
|
46
|
+
import { showConfirmDialog, showPromptDialog } from "../ui/layers";
|
|
47
|
+
import { POLL_GIT } from "../ui/timing";
|
|
48
|
+
import { notify } from "../services/notify";
|
|
49
|
+
import { authenticateGithub } from "../github/github-auth";
|
|
50
|
+
import { createGithubRepository } from "../github/github-publish";
|
|
51
|
+
import { pullWithPackageSync } from "../packages/pull-package-sync";
|
|
52
|
+
import type { AnyCommand, CommandRegistry } from "../commands/registry";
|
|
53
|
+
|
|
54
|
+
// ─── Reading the repository ───────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read the status, then the branch list when the status says there is a repository.
|
|
58
|
+
*
|
|
59
|
+
* In that order, not in parallel. Outside a repository of its own the branch route refuses (`409
|
|
60
|
+
* conflict`, studio.md §22.8), and asked alongside the status it did so on every refresh: a failed
|
|
61
|
+
* request in the console each time for a project that cannot have branches. A failure of either
|
|
62
|
+
* read is the workspace's error line; a failed status still leaves the refresh to bump the
|
|
63
|
+
* revision, because nothing about the tree is known to be unchanged.
|
|
64
|
+
*/
|
|
65
|
+
async function readStatusAndBranches(): Promise<void> {
|
|
66
|
+
const plat = getPlatform();
|
|
67
|
+
const { git } = shell;
|
|
68
|
+
let status;
|
|
69
|
+
try {
|
|
70
|
+
status = await plat.gitStatus();
|
|
71
|
+
} catch (error) {
|
|
72
|
+
git.error = errorMessage(error);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
git.status = status;
|
|
76
|
+
if (!status.isRepo) {
|
|
77
|
+
git.branches = null;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
git.branches = await plat.gitBranches();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
git.error = errorMessage(error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Re-read the status and the branch list, then every comparison that depends on them. */
|
|
88
|
+
export async function refreshGitStatus(): Promise<void> {
|
|
89
|
+
if (!projectState) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const { git } = shell;
|
|
93
|
+
git.loading = true;
|
|
94
|
+
git.error = null;
|
|
95
|
+
try {
|
|
96
|
+
await readStatusAndBranches();
|
|
97
|
+
git.lastUpdated = now();
|
|
98
|
+
/* THE COMPARISON FOLLOWS THE TREE. Every git verb that can move the working tree ends here —
|
|
99
|
+
commit, discard, checkout, pull, fetch — and so does the 30s poll that notices a change made
|
|
100
|
+
outside Studio. Bumping the revision is what re-issues a Diff lens's read; the comparison
|
|
101
|
+
this module opened is re-read below, because nothing else owns it. */
|
|
102
|
+
git.rev += 1;
|
|
103
|
+
await rereadOpenComparison();
|
|
104
|
+
// The log moves with a commit, a pull or a checkout too; once it has been shown, keep it current.
|
|
105
|
+
if (git.logEntries !== null) {
|
|
106
|
+
await fetchGitLog();
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
git.error = errorMessage(error);
|
|
110
|
+
} finally {
|
|
111
|
+
git.loading = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Re-read the comparison {@link openFileComparison} has open, against the status just fetched.
|
|
117
|
+
*
|
|
118
|
+
* A file that is no longer changed loses its comparison rather than keeping a stale one: the stage
|
|
119
|
+
* then draws "Nothing to compare", which is the truth and is what the author's own commit just made
|
|
120
|
+
* true. A read that fails leaves the previous comparison up: a momentary git error is not a reason
|
|
121
|
+
* to blank a review someone is in the middle of.
|
|
122
|
+
*/
|
|
123
|
+
async function rereadOpenComparison(): Promise<void> {
|
|
124
|
+
const state = shell.git.diffState;
|
|
125
|
+
if (!state) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const change = shell.git.status?.files.find((file) => file.path === state.filePath);
|
|
129
|
+
if (!change || !isDiffableStatus(change.status)) {
|
|
130
|
+
shell.git.diffState = null;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
shell.git.diffState = await readGitDiff(state.filePath, change.status);
|
|
135
|
+
} catch {
|
|
136
|
+
// Intentionally ignored: the comparison on screen is still the last one that read cleanly.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Re-read an open comparison because THIS file was just saved.
|
|
142
|
+
*
|
|
143
|
+
* Separate from the git-status path, and deliberately: the status the comparison was opened with is
|
|
144
|
+
* the right one to re-read against, because the file has not changed its relationship to HEAD by
|
|
145
|
+
* being written, only its contents.
|
|
146
|
+
*
|
|
147
|
+
* @param {string | null} path - The project-relative path just written.
|
|
148
|
+
*/
|
|
149
|
+
export async function noteFileSaved(path: string | null): Promise<void> {
|
|
150
|
+
shell.git.rev += 1;
|
|
151
|
+
/* The count of unpublished changes is drawn on the Publish workspace tab in every workspace, and
|
|
152
|
+
the poll only runs while Publish is showing, so a save re-reads the status itself. Not
|
|
153
|
+
awaited: the comparison below does not depend on it. */
|
|
154
|
+
if (!shell.git.loading) {
|
|
155
|
+
void refreshGitStatus();
|
|
156
|
+
}
|
|
157
|
+
const state = shell.git.diffState;
|
|
158
|
+
if (!path || state?.filePath !== path) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
shell.git.diffState = await readGitDiff(path, state.fileStatus);
|
|
163
|
+
} catch {
|
|
164
|
+
// Intentionally ignored: see rereadOpenComparison.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Fetch the recent commits. A failure is the workspace's one error line, like any verb's. */
|
|
169
|
+
export async function fetchGitLog(): Promise<void> {
|
|
170
|
+
try {
|
|
171
|
+
shell.git.logEntries = await getPlatform().gitLog(30);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
shell.git.error = errorMessage(error);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Cloning ─────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Show a dialog to clone a git repository, then open what it cloned.
|
|
181
|
+
*
|
|
182
|
+
* @param {{ openRecentProject: (root: string) => Promise<void> }} ctx
|
|
183
|
+
*/
|
|
184
|
+
export async function cloneRepository(ctx: {
|
|
185
|
+
openRecentProject: (root: string) => Promise<void>;
|
|
186
|
+
}): Promise<void> {
|
|
187
|
+
const platform = getPlatform();
|
|
188
|
+
if (!platform.gitClone) {
|
|
189
|
+
notify.warn("Cloning is not supported on this platform.", { source: "Source Control" });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const url = await showPromptDialog("Clone Git Repository", {
|
|
193
|
+
confirmLabel: "Clone",
|
|
194
|
+
message: "Repository URL",
|
|
195
|
+
placeholder: "https://github.com/user/repo.git",
|
|
196
|
+
validate: (v) => (v.trim() ? "" : "Enter a repository URL."),
|
|
197
|
+
});
|
|
198
|
+
if (!url) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
notify.info("Cloning repository…", { key: "git.clone", source: "Source Control" });
|
|
203
|
+
const result = await platform.gitClone(url);
|
|
204
|
+
if (result.root) {
|
|
205
|
+
notify.success("Clone complete.", { key: "git.clone" });
|
|
206
|
+
await ctx.openRecentProject(result.root);
|
|
207
|
+
}
|
|
208
|
+
} catch (error) {
|
|
209
|
+
notify.error("Could not clone the repository.", {
|
|
210
|
+
detail: errorMessage(error),
|
|
211
|
+
key: "git.clone",
|
|
212
|
+
source: "Source Control",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Whether this platform can clone at all.
|
|
219
|
+
*
|
|
220
|
+
* `hasPlatform()` first, because the question is asked by the Start pane before a platform may be
|
|
221
|
+
* registered and `getPlatform()` THROWS when nothing has registered one. "There is no platform" is
|
|
222
|
+
* an answer to this question (no, it cannot clone) rather than a failure.
|
|
223
|
+
*/
|
|
224
|
+
export function platformSupportsClone(): boolean {
|
|
225
|
+
return hasPlatform() && Boolean(getPlatform().gitClone);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Whether commits on this host land on the remote and a branch is a session of its own (the cloud).
|
|
230
|
+
* Asked by every verb whose consequence differs: a checkout there reloads the window.
|
|
231
|
+
*/
|
|
232
|
+
export function platformHostsGit(): boolean {
|
|
233
|
+
return hasPlatform() && getPlatform().gitHosted === true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Whether this host can open a pull request (`StudioPlatform.createPullRequest`). */
|
|
237
|
+
export function platformOpensPullRequests(): boolean {
|
|
238
|
+
return hasPlatform() && isFunction(getPlatform().createPullRequest);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Comparisons ─────────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
/** How each status reads aloud. The badge is one letter and a colour, which a label cannot be. */
|
|
244
|
+
const STATUS_WORDS = new Map([
|
|
245
|
+
["A", "added"],
|
|
246
|
+
["D", "deleted"],
|
|
247
|
+
["M", "modified"],
|
|
248
|
+
["R", "renamed"],
|
|
249
|
+
["U", "untracked"],
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
/** A status letter, said aloud: "modified". An unknown letter still says the file changed. */
|
|
253
|
+
export function statusWord(status: string): string {
|
|
254
|
+
return STATUS_WORDS.get(status) ?? "changed";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** A status letter as the short tag a compact list prints (the quick publish dialog's). */
|
|
258
|
+
const STATUS_TAGS = new Map([
|
|
259
|
+
["A", "NEW"],
|
|
260
|
+
["D", "DELETED"],
|
|
261
|
+
["M", "EDIT"],
|
|
262
|
+
["R", "RENAMED"],
|
|
263
|
+
["U", "NEW"],
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
/** A status letter as a tag: "EDIT". An unknown letter still says the file changed. */
|
|
267
|
+
export function statusTag(status: string): string {
|
|
268
|
+
return STATUS_TAGS.get(status) ?? "EDIT";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The statuses a comparison can be built for. `R` is absent: see {@link isDiffableStatus}. */
|
|
272
|
+
export const DIFFABLE_STATUSES = new Set(["M", "A", "U", "D"]);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Whether this file's change can be opened as a comparison at all.
|
|
276
|
+
*
|
|
277
|
+
* `R` (renamed) is the one that cannot: `GitFileStatus` carries a single `path`, so the old name is
|
|
278
|
+
* not in hand and there is nothing to compare the new one against. Better refused by name than
|
|
279
|
+
* shown as a whole-file rewrite.
|
|
280
|
+
*/
|
|
281
|
+
export function isDiffableStatus(fileStatus: string): boolean {
|
|
282
|
+
return DIFFABLE_STATUSES.has(fileStatus);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* One file's comparison against HEAD: the committed text and the working copy.
|
|
287
|
+
*
|
|
288
|
+
* One definition site, because there are two callers: a changed file's **Before / after** in Review
|
|
289
|
+
* changes, and `workspace/pane-derive.ts`'s Diff lens, which must not reach `shell.git.diffState`
|
|
290
|
+
* (that slot holds whatever Review changes last opened, a different file, §18.4).
|
|
291
|
+
*
|
|
292
|
+
* Each side is read only where it exists: `M` reads both; `A` and `U` have no `HEAD` copy, so the
|
|
293
|
+
* original is the empty string; `D` is the mirror, with no working copy.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} path
|
|
296
|
+
* @param {string} fileStatus
|
|
297
|
+
* @returns {Promise<GitDiffState>}
|
|
298
|
+
*/
|
|
299
|
+
export async function readGitDiff(path: string, fileStatus: string): Promise<GitDiffState> {
|
|
300
|
+
const plat = getPlatform();
|
|
301
|
+
const inHead = fileStatus !== "A" && fileStatus !== "U";
|
|
302
|
+
const onDisk = fileStatus !== "D";
|
|
303
|
+
const [originalContent, currentContent] = await Promise.all([
|
|
304
|
+
inHead ? plat.gitShow({ path, ref: "HEAD" }) : Promise.resolve(""),
|
|
305
|
+
onDisk ? plat.readFile(path) : Promise.resolve(""),
|
|
306
|
+
]);
|
|
307
|
+
return { currentContent, filePath: path, fileStatus, originalContent };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The Diff lens's reader: {@link readGitDiff}, with a failure the PANE can state.
|
|
312
|
+
*
|
|
313
|
+
* @param {string} path
|
|
314
|
+
* @param {string} fileStatus
|
|
315
|
+
* @returns {Promise<GitDiffState | null>}
|
|
316
|
+
*/
|
|
317
|
+
export async function loadDiffForLens(
|
|
318
|
+
path: string,
|
|
319
|
+
fileStatus: string,
|
|
320
|
+
): Promise<GitDiffState | null> {
|
|
321
|
+
try {
|
|
322
|
+
return await readGitDiff(path, fileStatus);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
// Reported where it can be acted on (the pane says so) rather than as a toast over a document
|
|
325
|
+
// The author did not ask about.
|
|
326
|
+
console.warn("loadDiffForLens:", errorMessage(error));
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* What the bootstrap injects: the two writes a comparison makes outside `shell.git`, and the clone
|
|
333
|
+
* flow that needs `openRecentProject`.
|
|
334
|
+
*/
|
|
335
|
+
export interface SourceControlDeps {
|
|
336
|
+
setCanvasMode?: (tab: Tab | null, mode: string) => void;
|
|
337
|
+
setGitDiffState?: (state: GitDiffState | null) => void;
|
|
338
|
+
cloneRepository?: () => void;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let _deps: SourceControlDeps = {};
|
|
342
|
+
|
|
343
|
+
/** Hand the flow what the bootstrap owns. Replaces whatever was injected before. */
|
|
344
|
+
export function setSourceControlDeps(deps: SourceControlDeps): void {
|
|
345
|
+
_deps = deps;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Run the injected clone flow. A no-op before the bootstrap has injected one. */
|
|
349
|
+
export function runCloneRepository(): void {
|
|
350
|
+
_deps.cloneRepository?.();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** The changed file at this path, as the last status read saw it. */
|
|
354
|
+
export function changedFileAt(path: string): GitFileStatus | undefined {
|
|
355
|
+
return shell.git.status?.files.find((file: GitFileStatus) => file.path === path);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Open one changed file's comparison, on that file's OWN tab.
|
|
360
|
+
*
|
|
361
|
+
* Every changed row opens something, and it opens the file it names: renderability decides which
|
|
362
|
+
* VIEW opens (§21.3), never whether the row responds. Only `R` is refused, and out loud. The tab is
|
|
363
|
+
* keyed by the clicked path, so the strip and the stage agree (§14.1); a comparison tab belongs to
|
|
364
|
+
* Publish (§22.1), and it is active only once it is in `git-diff` (`openComparisonTab`), so opening
|
|
365
|
+
* one never leaves the workspace. The one tab per path also means a document already open in Design
|
|
366
|
+
* moves to Publish while it shows its comparison, and back when its editor is chosen again.
|
|
367
|
+
*/
|
|
368
|
+
export async function openFileComparison(path: string): Promise<void> {
|
|
369
|
+
const file = changedFileAt(path);
|
|
370
|
+
if (!file) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const refusal = isDiffableStatus(file.status)
|
|
374
|
+
? comparisonRefusal(file.path, file.status)
|
|
375
|
+
: `"${file.path}" has no change this view can open.`;
|
|
376
|
+
if (refusal) {
|
|
377
|
+
shell.git.error = refusal;
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
try {
|
|
381
|
+
shell.git.loading = true;
|
|
382
|
+
const diffState = await readGitDiff(file.path, file.status);
|
|
383
|
+
shell.git.diffState = diffState;
|
|
384
|
+
// Handed to the canvas before the tab opens, because it opens already in `git-diff`.
|
|
385
|
+
_deps.setGitDiffState?.(diffState);
|
|
386
|
+
const tab = await openComparisonTab(file.path);
|
|
387
|
+
if (tab) {
|
|
388
|
+
_deps.setCanvasMode?.(tab, "git-diff");
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
shell.git.error = `Failed to load diff: ${errorMessage(error)}`;
|
|
392
|
+
} finally {
|
|
393
|
+
shell.git.loading = false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ─── Verbs ───────────────────────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Run one platform git verb under the one loading/error contract, then re-read the status.
|
|
401
|
+
*
|
|
402
|
+
* @param run Calls the verb on the platform it is handed.
|
|
403
|
+
* @returns Whether the action succeeded; its failure is already on screen.
|
|
404
|
+
*/
|
|
405
|
+
export async function gitAction(run: (plat: StudioPlatform) => Promise<void>): Promise<boolean> {
|
|
406
|
+
const plat = getPlatform();
|
|
407
|
+
shell.git.loading = true;
|
|
408
|
+
shell.git.error = null;
|
|
409
|
+
try {
|
|
410
|
+
await run(plat);
|
|
411
|
+
await refreshGitStatus();
|
|
412
|
+
return true;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
shell.git.error = errorMessage(error);
|
|
415
|
+
shell.git.loading = false;
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Start tracking this project with git.
|
|
422
|
+
*
|
|
423
|
+
* A function rather than an inline click handler because it is a command ({@link
|
|
424
|
+
* sourceControlCommands}), the first step of the deploy checklist and a button in Review changes,
|
|
425
|
+
* and three copies of the same three lines is how they come to disagree about what the verb does.
|
|
426
|
+
*/
|
|
427
|
+
export async function initRepository(): Promise<void> {
|
|
428
|
+
try {
|
|
429
|
+
notify.info("Initializing repository…", { key: "git.init" });
|
|
430
|
+
await getPlatform().gitInit();
|
|
431
|
+
notify.success("Repository initialized.", { key: "git.init" });
|
|
432
|
+
} catch (error) {
|
|
433
|
+
notify.error("Could not initialize the repository.", {
|
|
434
|
+
action: "git.init",
|
|
435
|
+
detail: errorMessage(error),
|
|
436
|
+
key: "git.init",
|
|
437
|
+
source: "Source Control",
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
await refreshGitStatus();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** Push the current branch. Same loading/error contract as every other verb. */
|
|
445
|
+
export async function pushCurrentBranch(): Promise<void> {
|
|
446
|
+
await gitAction((plat) => plat.gitPush());
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Fetch from the remote. */
|
|
450
|
+
export async function fetchRemote(): Promise<void> {
|
|
451
|
+
await gitAction((plat) => plat.gitFetch());
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Pull through the package-aware orchestrator, under the same contract as {@link gitAction}. */
|
|
455
|
+
export async function pullRemote(): Promise<void> {
|
|
456
|
+
shell.git.loading = true;
|
|
457
|
+
shell.git.error = null;
|
|
458
|
+
try {
|
|
459
|
+
await pullWithPackageSync();
|
|
460
|
+
await refreshGitStatus();
|
|
461
|
+
} catch (error) {
|
|
462
|
+
shell.git.error = errorMessage(error);
|
|
463
|
+
shell.git.loading = false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Create the GitHub repository for this project, named after it. */
|
|
468
|
+
export async function createRepository(): Promise<void> {
|
|
469
|
+
await createGithubRepository({ projectName: projectState?.name || "my-project" });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Sign in to GitHub, or report why it could not.
|
|
474
|
+
*
|
|
475
|
+
* The report belongs to `github/github-auth.ts`: this only says what a SUCCESS was, which that
|
|
476
|
+
* module cannot, because it is also called mid-flow by "Create GitHub Repository".
|
|
477
|
+
*/
|
|
478
|
+
export async function signInToGithub(): Promise<void> {
|
|
479
|
+
const token = await authenticateGithub();
|
|
480
|
+
if (token) {
|
|
481
|
+
notify.success("Signed in to GitHub.", { key: "github.auth" });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Include a file in the next publish, or leave it out: the Review changes checkbox.
|
|
487
|
+
*
|
|
488
|
+
* Including is staging. A file is included exactly when the index holds it, so the checkbox reads
|
|
489
|
+
* the status and never a second record of the choice.
|
|
490
|
+
*/
|
|
491
|
+
export async function setIncluded(path: string, included: boolean): Promise<void> {
|
|
492
|
+
await gitAction((plat) => (included ? plat.gitStage([path]) : plat.gitUnstage([path])));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Include every changed file, or leave every one out. */
|
|
496
|
+
export async function setAllIncluded(included: boolean): Promise<void> {
|
|
497
|
+
const paths = (shell.git.status?.files ?? [])
|
|
498
|
+
.filter((file: GitFileStatus) => Boolean(file.staged) !== included)
|
|
499
|
+
.map((file: GitFileStatus) => file.path);
|
|
500
|
+
if (paths.length === 0) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
await gitAction((plat) => (included ? plat.gitStage(paths) : plat.gitUnstage(paths)));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* The changed paths Review changes has already shown, for the project it showed them in.
|
|
508
|
+
*
|
|
509
|
+
* Module state rather than a field of `shell.git`: it is read and written from inside an effect,
|
|
510
|
+
* and it says nothing anybody draws. A path leaves it when it leaves the status (committed or
|
|
511
|
+
* discarded), so its next change is new again.
|
|
512
|
+
*/
|
|
513
|
+
interface ShownChanges {
|
|
514
|
+
/** The project the paths belong to (`projectState.projectRoot`). */
|
|
515
|
+
root: string;
|
|
516
|
+
paths: Set<string>;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
let _shown: ShownChanges = { paths: new Set(), root: "" };
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The changes Review changes has not shown before and git does not have staged: what an untouched
|
|
523
|
+
* change set includes by default. Marks every listed path as shown, so a file the author then
|
|
524
|
+
* leaves out stays out.
|
|
525
|
+
*/
|
|
526
|
+
export function unshownUnstagedChanges(): string[] {
|
|
527
|
+
const root = projectState?.projectRoot ?? "";
|
|
528
|
+
if (_shown.root !== root) {
|
|
529
|
+
_shown = { paths: new Set(), root };
|
|
530
|
+
}
|
|
531
|
+
const files = shell.git.status?.files ?? [];
|
|
532
|
+
const listed = new Set(files.map((file) => file.path));
|
|
533
|
+
for (const path of _shown.paths) {
|
|
534
|
+
if (!listed.has(path)) {
|
|
535
|
+
_shown.paths.delete(path);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const fresh = files.filter((file) => !_shown.paths.has(file.path));
|
|
539
|
+
for (const file of fresh) {
|
|
540
|
+
_shown.paths.add(file.path);
|
|
541
|
+
}
|
|
542
|
+
return fresh.filter((file) => !file.staged).map((file) => file.path);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Include the changes Review changes is showing for the first time.
|
|
547
|
+
*
|
|
548
|
+
* Included means staged, so a change set nobody has curated opens with every change included, as
|
|
549
|
+
* the quick dialog publishes it, rather than "0 of 5 included" with the primary button refused. A
|
|
550
|
+
* read in flight is waited out (the effect calling this re-runs when it settles), so a path is
|
|
551
|
+
* never marked shown against a status that is about to change.
|
|
552
|
+
*/
|
|
553
|
+
export async function includeNewChanges(): Promise<void> {
|
|
554
|
+
if (shell.git.loading || !shell.git.status?.isRepo) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const paths = unshownUnstagedChanges();
|
|
558
|
+
if (paths.length > 0) {
|
|
559
|
+
await gitAction((plat) => plat.gitStage(paths));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Discard one file's changes, once the reader has said so. An untracked file has nothing to go back
|
|
565
|
+
* to.
|
|
566
|
+
*/
|
|
567
|
+
export async function discardFile(path: string): Promise<void> {
|
|
568
|
+
if (changedFileAt(path)?.status === "U") {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const confirmed = await showConfirmDialog("Discard Changes", `Discard changes to ${path}?`, {
|
|
572
|
+
confirmLabel: "Discard",
|
|
573
|
+
destructive: true,
|
|
574
|
+
});
|
|
575
|
+
if (!confirmed) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
await gitAction((plat) => plat.gitDiscard([path]));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Ask for a name and create (and check out) a branch from the current one. */
|
|
582
|
+
export async function createBranch(): Promise<void> {
|
|
583
|
+
const { branches, status } = shell.git;
|
|
584
|
+
const name = await showPromptDialog("New Branch", {
|
|
585
|
+
confirmLabel: "Create",
|
|
586
|
+
message: `Branching from ${branches?.current || status?.branch || "the current branch"}.`,
|
|
587
|
+
placeholder: "feature/my-change",
|
|
588
|
+
validate: (v) => (v.trim() ? "" : "Enter a branch name."),
|
|
589
|
+
});
|
|
590
|
+
if (name) {
|
|
591
|
+
await gitAction((plat) => plat.gitCreateBranch(name.trim()));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Check a branch out.
|
|
597
|
+
*
|
|
598
|
+
* On a host where a branch is a session (the cloud), a checkout is a page load into that branch's
|
|
599
|
+
* session, so the author is asked first: nothing about the word "Switch" says the window will
|
|
600
|
+
* reload.
|
|
601
|
+
*/
|
|
602
|
+
export async function checkoutBranch(name: string): Promise<void> {
|
|
603
|
+
if (name === shell.git.branches?.current) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (platformHostsGit()) {
|
|
607
|
+
const confirmed = await showConfirmDialog(
|
|
608
|
+
"Switch Branch",
|
|
609
|
+
`Each branch opens in its own session, so switching to ${name} reloads Studio on that branch.`,
|
|
610
|
+
{ confirmLabel: "Switch and reload" },
|
|
611
|
+
);
|
|
612
|
+
if (!confirmed) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
await gitAction((plat) => plat.gitCheckout(name));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// ─── Change rows, grouped ────────────────────────────────────────────────────
|
|
620
|
+
|
|
621
|
+
/** One changed file, as every list of changes draws it. Values only. */
|
|
622
|
+
export interface ChangeRow {
|
|
623
|
+
/** The project-relative path: the row's key and what every control hands back. */
|
|
624
|
+
key: string;
|
|
625
|
+
path: string;
|
|
626
|
+
/** The last segment, which is what the row reads as. */
|
|
627
|
+
name: string;
|
|
628
|
+
/** Everything before it, drawn small beside the name. */
|
|
629
|
+
dir: string;
|
|
630
|
+
hasDir: boolean;
|
|
631
|
+
/** The one-letter badge: `M`, `A`, `D`, `R`, `U`. */
|
|
632
|
+
status: string;
|
|
633
|
+
/** The status said aloud. */
|
|
634
|
+
statusWord: string;
|
|
635
|
+
/** The path and the status in words: what the row's controls are named after. */
|
|
636
|
+
label: string;
|
|
637
|
+
/** Whether the next publish includes it: the index holds it. */
|
|
638
|
+
included: boolean;
|
|
639
|
+
/** An untracked file has no committed version to go back to. */
|
|
640
|
+
cannotDiscard: boolean;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** One component's changed files, under the heading {@link groupFilesByComponent} spelled. */
|
|
644
|
+
export interface ChangeGroup {
|
|
645
|
+
key: string;
|
|
646
|
+
name: string;
|
|
647
|
+
files: ChangeRow[];
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** One changed file as a row. The path is split here: "the name and its folder" is a path rule. */
|
|
651
|
+
export function changeRow(file: GitFileStatus): ChangeRow {
|
|
652
|
+
const parts = file.path.split("/");
|
|
653
|
+
const name = parts.pop() ?? file.path;
|
|
654
|
+
const dir = parts.join("/");
|
|
655
|
+
const word = statusWord(file.status);
|
|
656
|
+
return {
|
|
657
|
+
cannotDiscard: file.status === "U",
|
|
658
|
+
dir,
|
|
659
|
+
hasDir: dir !== "",
|
|
660
|
+
included: Boolean(file.staged),
|
|
661
|
+
key: file.path,
|
|
662
|
+
label: `${file.path}, ${word}`,
|
|
663
|
+
name,
|
|
664
|
+
path: file.path,
|
|
665
|
+
status: file.status,
|
|
666
|
+
statusWord: word,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** The project folders a change is filed under by name, after the content collections. */
|
|
671
|
+
const FOLDER_GROUPS: readonly (readonly [folder: string, name: string])[] = [
|
|
672
|
+
["components", "Components"],
|
|
673
|
+
["layouts", "Layouts"],
|
|
674
|
+
["styles", "Styles"],
|
|
675
|
+
];
|
|
676
|
+
|
|
677
|
+
/** The group key of a change: a collection's key, `folder:<name>`, `media` or `other`. */
|
|
678
|
+
function changeGroupKey(path: string): string {
|
|
679
|
+
const collection = collectionKeyOfPath(path);
|
|
680
|
+
if (collection !== null) {
|
|
681
|
+
return collection;
|
|
682
|
+
}
|
|
683
|
+
const [top] = path.split("/");
|
|
684
|
+
// A page directory's private part (`pages/_partials/…`) is no route, but it is still the pages.
|
|
685
|
+
if (top === "pages") {
|
|
686
|
+
return PAGES_COLLECTION;
|
|
687
|
+
}
|
|
688
|
+
if (FOLDER_GROUPS.some(([folder]) => folder === top)) {
|
|
689
|
+
return `folder:${top}`;
|
|
690
|
+
}
|
|
691
|
+
return MEDIA_EXTENSIONS.has(extensionOf(path)) ? "media" : "other";
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Group the changed files the way the site is organised: Pages, then each content collection the
|
|
696
|
+
* project declares (by its title), then Components, Layouts, Styles, Media and Other, in that order
|
|
697
|
+
* and only when non-empty. Within a group the files keep the order git listed them.
|
|
698
|
+
*
|
|
699
|
+
* Not by folder name. The last folder of a path is no unit of a site: `content/events/en` and
|
|
700
|
+
* `content/exhibitions/en` both ended in `/en` and were filed together, and every file at the root
|
|
701
|
+
* got a heading of its own (studio.md §22.8).
|
|
702
|
+
*/
|
|
703
|
+
export function groupFilesByComponent(files: readonly GitFileStatus[]): ChangeGroup[] {
|
|
704
|
+
const order: [key: string, name: string][] = [
|
|
705
|
+
...contentCollections().map((collection): [string, string] => [
|
|
706
|
+
collection.key,
|
|
707
|
+
collection.title,
|
|
708
|
+
]),
|
|
709
|
+
...FOLDER_GROUPS.map(([folder, name]): [string, string] => [`folder:${folder}`, name]),
|
|
710
|
+
["media", "Media"],
|
|
711
|
+
["other", "Other"],
|
|
712
|
+
];
|
|
713
|
+
const grouped = new Map<string, GitFileStatus[]>();
|
|
714
|
+
for (const file of files) {
|
|
715
|
+
const key = changeGroupKey(file.path);
|
|
716
|
+
grouped.set(key, [...(grouped.get(key) ?? []), file]);
|
|
717
|
+
}
|
|
718
|
+
return order.flatMap(([key, name]) => {
|
|
719
|
+
const entries = grouped.get(key);
|
|
720
|
+
return entries ? [{ files: entries.map((entry) => changeRow(entry)), key, name }] : [];
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** One commit, as the log draws it. */
|
|
725
|
+
export interface CommitRow {
|
|
726
|
+
key: string;
|
|
727
|
+
/** The short hash, already sliced. */
|
|
728
|
+
hash: string;
|
|
729
|
+
message: string;
|
|
730
|
+
/** "ada · 5m ago": the author and the age, joined by the flow that knows the clock. */
|
|
731
|
+
meta: string;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** One log entry as a row. The clock is read here, which is why {@link relativeDate} is. */
|
|
735
|
+
export function commitRow(entry: GitLogEntry): CommitRow {
|
|
736
|
+
return {
|
|
737
|
+
hash: entry.hash.slice(0, 7),
|
|
738
|
+
key: entry.hash,
|
|
739
|
+
message: entry.message,
|
|
740
|
+
meta: `${entry.author} · ${relativeDate(entry.date)}`,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* An age, relative to {@link now}.
|
|
746
|
+
*
|
|
747
|
+
* Exported so it is testable at all: reading the wall clock inline meant "yesterday" and the
|
|
748
|
+
* locale-date fallback could only be asserted against an offset from the real present.
|
|
749
|
+
*
|
|
750
|
+
* @param {string} iso
|
|
751
|
+
*/
|
|
752
|
+
export function relativeDate(iso: string): string {
|
|
753
|
+
const d = new Date(iso);
|
|
754
|
+
const diff = now() - d.getTime();
|
|
755
|
+
const mins = Math.floor(diff / 60_000);
|
|
756
|
+
if (mins < 1) {
|
|
757
|
+
return "just now";
|
|
758
|
+
}
|
|
759
|
+
if (mins < 60) {
|
|
760
|
+
return `${mins}m ago`;
|
|
761
|
+
}
|
|
762
|
+
const hours = Math.floor(mins / 60);
|
|
763
|
+
if (hours < 24) {
|
|
764
|
+
return `${hours}h ago`;
|
|
765
|
+
}
|
|
766
|
+
const days = Math.floor(hours / 24);
|
|
767
|
+
if (days < 30) {
|
|
768
|
+
return `${days}d ago`;
|
|
769
|
+
}
|
|
770
|
+
return d.toLocaleDateString();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** The moment of the last successful refresh, as the workspace prints it. */
|
|
774
|
+
export function lastUpdatedLabel(stamp: number | null): string {
|
|
775
|
+
return stamp
|
|
776
|
+
? `Last updated ${new Date(stamp).toLocaleTimeString([], {
|
|
777
|
+
hour: "2-digit",
|
|
778
|
+
minute: "2-digit",
|
|
779
|
+
})}`
|
|
780
|
+
: "";
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ─── The poll ────────────────────────────────────────────────────────────────
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* The background refresh handle. The interval is infrastructure, not state: what it refreshes is on
|
|
787
|
+
* `shell.git`, so opening a second project inherits nothing from the first.
|
|
788
|
+
*/
|
|
789
|
+
let _pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Arm the background refresh, once. Idempotent.
|
|
793
|
+
*
|
|
794
|
+
* It refreshes only while the Publish workspace is showing and no read is in flight: the count on
|
|
795
|
+
* the Publish tab is kept current by saves ({@link noteFileSaved}) everywhere else, and a poll that
|
|
796
|
+
* ran in every workspace would spend a request every 30 seconds on a list nobody is looking at.
|
|
797
|
+
*/
|
|
798
|
+
export function armGitPoll(): void {
|
|
799
|
+
if (_pollTimer) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
_pollTimer = setInterval(() => {
|
|
803
|
+
if (shell.workspace === "publish" && !shell.git.loading) {
|
|
804
|
+
void refreshGitStatus();
|
|
805
|
+
}
|
|
806
|
+
}, POLL_GIT);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Stop the background refresh, and forget which changes Review changes has shown. Called whenever a
|
|
811
|
+
* different project is opened: everything the poll refreshes belongs to ONE repository, and so does
|
|
812
|
+
* every path that has been shown.
|
|
813
|
+
*/
|
|
814
|
+
export function cleanupSourceControl(): void {
|
|
815
|
+
if (_pollTimer) {
|
|
816
|
+
clearInterval(_pollTimer);
|
|
817
|
+
_pollTimer = null;
|
|
818
|
+
}
|
|
819
|
+
_shown = { paths: new Set(), root: "" };
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* The `Source Control:` family.
|
|
826
|
+
*
|
|
827
|
+
* Every one of them was once reachable from exactly one place, and the fourth (signing in to
|
|
828
|
+
* GitHub) from nowhere at all. Naming them gives the palette, `__jxAutomation`, the deploy
|
|
829
|
+
* checklist, the Publish workspace's Hosting and every `notify` Retry the same four ids, which is
|
|
830
|
+
* why they are kept as they are (§5.5).
|
|
831
|
+
*
|
|
832
|
+
* `git.signInToGithub` is `application`-level and the other three are `project`-level, and the
|
|
833
|
+
* split is the credential's: a GitHub token is one per machine, while a branch, a remote and a push
|
|
834
|
+
* belong to one repository.
|
|
835
|
+
*/
|
|
836
|
+
export function sourceControlCommands(): AnyCommand[] {
|
|
837
|
+
return [
|
|
838
|
+
{
|
|
839
|
+
category: "Source Control",
|
|
840
|
+
id: "git.init",
|
|
841
|
+
level: "project",
|
|
842
|
+
menus: ["commandbar/overflow", "palette"],
|
|
843
|
+
group: "7_scm",
|
|
844
|
+
requires: "an open project that git is not already tracking",
|
|
845
|
+
when: (ctx) => ctx.project.open,
|
|
846
|
+
// `when` already asked about the project; an `enablement` that re-asks is the same rule
|
|
847
|
+
// Written twice, and the two places drift.
|
|
848
|
+
enablement: (ctx) => !ctx.project.isRepo,
|
|
849
|
+
/* No `aiTool`, by studio-ui-guidelines.md §12.4's third deletion rule: outside the tree,
|
|
850
|
+
irreversible, and the model has no read to judge by. */
|
|
851
|
+
run: async () => {
|
|
852
|
+
await initRepository();
|
|
853
|
+
},
|
|
854
|
+
title: "Initialize Repository",
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
category: "Source Control",
|
|
858
|
+
id: "git.createGithubRepository",
|
|
859
|
+
level: "project",
|
|
860
|
+
menus: ["commandbar/overflow", "palette"],
|
|
861
|
+
group: "7_scm",
|
|
862
|
+
/* THE STRICTEST OF THE THREE, because it is the one that leaves something behind on a
|
|
863
|
+
server: its flow creates the repository on GitHub BEFORE it touches the local one, so on an
|
|
864
|
+
untracked project it would leave an empty repository on the author's account. */
|
|
865
|
+
requires: "a project tracked by git",
|
|
866
|
+
when: (ctx) => ctx.project.open,
|
|
867
|
+
enablement: (ctx) => ctx.project.isRepo,
|
|
868
|
+
/* No `aiTool`, by studio-ui-guidelines.md §12.4's third deletion rule: it creates a
|
|
869
|
+
repository on the person's account. */
|
|
870
|
+
run: async () => {
|
|
871
|
+
await createRepository();
|
|
872
|
+
},
|
|
873
|
+
title: "Create GitHub Repository",
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
category: "Source Control",
|
|
877
|
+
id: "git.push",
|
|
878
|
+
level: "project",
|
|
879
|
+
menus: ["commandbar/overflow", "palette"],
|
|
880
|
+
group: "7_scm",
|
|
881
|
+
requires: "a project tracked by git",
|
|
882
|
+
when: (ctx) => ctx.project.open,
|
|
883
|
+
enablement: (ctx) => ctx.project.isRepo,
|
|
884
|
+
/* No `aiTool`, by studio-ui-guidelines.md §12.4's third deletion rule. */
|
|
885
|
+
run: async () => {
|
|
886
|
+
await pushCurrentBranch();
|
|
887
|
+
},
|
|
888
|
+
title: "Push",
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
category: "Source Control",
|
|
892
|
+
id: "git.signInToGithub",
|
|
893
|
+
level: "application",
|
|
894
|
+
menus: ["palette"],
|
|
895
|
+
group: "7_scm",
|
|
896
|
+
run: async () => {
|
|
897
|
+
await signInToGithub();
|
|
898
|
+
},
|
|
899
|
+
title: "Sign In to GitHub",
|
|
900
|
+
},
|
|
901
|
+
];
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Register the `Source Control:` family. */
|
|
905
|
+
export function registerSourceControlCommands(registry: CommandRegistry): void {
|
|
906
|
+
registry.registerAll(sourceControlCommands());
|
|
907
|
+
}
|