flexdesk 0.17.17 → 0.18.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/CHANGELOG.md +70 -0
- package/LICENSE +0 -0
- package/README.md +1 -0
- package/THIRD_PARTY_LICENSES.md +0 -0
- package/canvas.js +0 -0
- package/charts.js +0 -0
- package/core.js +0 -0
- package/css/flexdesk.css +7499 -5851
- package/css/reset.css +1 -1
- package/css/tokens.css +23 -1
- package/dist/canvas.js +0 -0
- package/dist/canvas.js.map +0 -0
- package/dist/charts.js +10 -6
- package/dist/charts.js.map +1 -1
- package/dist/chunk-3PHCPZHT.js +0 -0
- package/dist/chunk-3PHCPZHT.js.map +0 -0
- package/dist/chunk-5W4EEKSK.js +778 -0
- package/dist/chunk-5W4EEKSK.js.map +7 -0
- package/dist/chunk-DNJ3N4T4.js +82 -0
- package/dist/chunk-DNJ3N4T4.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +0 -0
- package/dist/chunk-FOOS3T5L.js.map +0 -0
- package/dist/chunk-GOQLR5WR.js +0 -0
- package/dist/chunk-GOQLR5WR.js.map +0 -0
- package/dist/{chunk-JKZLNKG6.js → chunk-HKPB6OFA.js} +90 -25
- package/dist/{chunk-JKZLNKG6.js.map → chunk-HKPB6OFA.js.map} +2 -2
- package/dist/chunk-HNU6MM2B.js +0 -0
- package/dist/chunk-HNU6MM2B.js.map +0 -0
- package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
- package/dist/chunk-JYWURG5T.js +0 -0
- package/dist/chunk-JYWURG5T.js.map +0 -0
- package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
- package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
- package/dist/chunk-N7HC3ADV.js +0 -0
- package/dist/chunk-N7HC3ADV.js.map +0 -0
- package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
- package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
- package/dist/chunk-PAI2ZUWV.js +260 -0
- package/dist/chunk-PAI2ZUWV.js.map +7 -0
- package/dist/chunk-Q7BALEPX.js +527 -0
- package/dist/chunk-Q7BALEPX.js.map +7 -0
- package/dist/chunk-QXAOTGCC.js +40 -0
- package/dist/chunk-QXAOTGCC.js.map +7 -0
- package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
- package/dist/chunk-RKVYIXDC.js.map +7 -0
- package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
- package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
- package/dist/chunk-UKP22RUG.js +219 -0
- package/dist/chunk-UKP22RUG.js.map +7 -0
- package/dist/chunk-VLOAI3HS.js +43 -0
- package/dist/chunk-VLOAI3HS.js.map +7 -0
- package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
- package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
- package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
- package/dist/chunk-ZL3VKINP.js.map +7 -0
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/flexdesk.css +7499 -5851
- package/dist/flow.js +29 -97
- package/dist/flow.js.map +3 -3
- package/dist/git.js +1356 -0
- package/dist/git.js.map +7 -0
- package/dist/host.js +0 -0
- package/dist/host.js.map +0 -0
- package/dist/reset.css +1 -1
- package/dist/tile_registry-6WZPTUZV.js +0 -0
- package/dist/tile_registry-6WZPTUZV.js.map +0 -0
- package/dist/tiles.js +20 -15
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +23 -1
- package/dist/widgets.js +3969 -1189
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +121 -33
- package/dist/wm.js.map +2 -2
- package/docs/README.md +9 -4
- package/docs/best-practices.md +2 -2
- package/docs/components/chat-shell.md +198 -0
- package/docs/components/command-palette.md +0 -0
- package/docs/components/conversation.md +10 -0
- package/docs/components/data-table-grid.md +0 -0
- package/docs/components/data-table.md +0 -0
- package/docs/components/editor.md +0 -0
- package/docs/components/filter-builder.md +246 -0
- package/docs/components/flow-canvas-editor.md +0 -0
- package/docs/components/flow-editors.md +0 -0
- package/docs/components/flow-lane-editor.md +0 -0
- package/docs/components/flow-outline-editor.md +0 -0
- package/docs/components/forms-and-dialogs.md +22 -0
- package/docs/components/git.md +355 -0
- package/docs/components/input-fields.md +167 -2
- package/docs/components/keymap.md +8 -0
- package/docs/components/markdown-and-outline.md +0 -0
- package/docs/components/menus.md +120 -3
- package/docs/components/panels-and-desktops.md +16 -5
- package/docs/components/record-list-and-fact-table.md +0 -0
- package/docs/components/settings-view.md +0 -0
- package/docs/components/shell.md +104 -1
- package/docs/components/wizard.md +0 -0
- package/docs/components/workspace-picker.md +149 -0
- package/docs/concepts/building-and-packaging.md +2 -2
- package/docs/concepts/domain-neutrality.md +0 -0
- package/docs/concepts/host-port.md +0 -0
- package/docs/concepts/packages-and-entry-points.md +14 -13
- package/docs/concepts/peer-dependencies.md +0 -0
- package/docs/concepts/state-guard.md +3 -3
- package/docs/quickstart.md +1 -1
- package/editor.js +0 -0
- package/flow.js +0 -0
- package/git.js +13 -0
- package/host.js +0 -0
- package/package.json +6 -1
- package/src/canvas/canvas_adapter.js +0 -0
- package/src/canvas/edge_router.js +0 -0
- package/src/canvas/graph/connector_router.js +0 -0
- package/src/canvas/graph/history_service.js +0 -0
- package/src/canvas/graph/node_platform.js +0 -0
- package/src/canvas/index.js +0 -0
- package/src/canvas/layered_layout.js +0 -0
- package/src/charts/data_series_plot_window.js +0 -0
- package/src/charts/downsample.js +0 -0
- package/src/charts/plot_config.js +0 -0
- package/src/charts/plotly_wrapper.js +0 -0
- package/src/core/event_bus.js +0 -0
- package/src/core/logging.js +0 -0
- package/src/core/settings.js +0 -0
- package/src/core/state_guard.js +0 -0
- package/src/core/state_machine.js +0 -0
- package/src/editor/editor_pane.js +0 -0
- package/src/editor/monaco_editor_factory.js +0 -0
- package/src/editor/monaco_loader.js +0 -0
- package/src/editor/notebook_search.js +0 -0
- package/src/editor/notebook_undo_manager.js +0 -0
- package/src/editor/split_pane_container.js +0 -0
- package/src/flow/canvas_editor/connections.js +0 -0
- package/src/flow/canvas_editor/editor.js +0 -0
- package/src/flow/canvas_editor/index.js +0 -0
- package/src/flow/canvas_editor/layout.js +0 -0
- package/src/flow/canvas_editor/strings.js +0 -0
- package/src/flow/kit/always_before.js +0 -0
- package/src/flow/kit/catalogue.js +0 -0
- package/src/flow/kit/chip_input.js +0 -0
- package/src/flow/kit/dom.js +28 -119
- package/src/flow/kit/findings.js +0 -0
- package/src/flow/kit/history.js +0 -0
- package/src/flow/kit/index.js +0 -0
- package/src/flow/kit/keys.js +0 -0
- package/src/flow/kit/logic_graph.js +0 -0
- package/src/flow/kit/references.js +0 -0
- package/src/flow/kit/settings_panel.js +0 -0
- package/src/flow/kit/settings_schema.js +0 -0
- package/src/flow/kit/step_picker.js +1 -1
- package/src/flow/kit/strings.js +0 -0
- package/src/flow/kit/value_picker.js +1 -1
- package/src/flow/kit/widgets.js +0 -0
- package/src/flow/lanes/editor.js +0 -0
- package/src/flow/lanes/index.js +0 -0
- package/src/flow/lanes/layout.js +0 -0
- package/src/flow/lanes/pipeline.js +0 -0
- package/src/flow/lanes/preview.js +0 -0
- package/src/flow/lanes/strings.js +0 -0
- package/src/flow/outline/editor.js +0 -0
- package/src/flow/outline/index.js +0 -0
- package/src/flow/outline/mapping.js +0 -0
- package/src/flow/outline/menu.js +1 -1
- package/src/flow/outline/operations.js +0 -0
- package/src/flow/outline/recognise.js +0 -0
- package/src/flow/outline/strings.js +0 -0
- package/src/git/diff_viewer.js +212 -0
- package/src/git/diff_window.js +51 -0
- package/src/git/git_graph.js +401 -0
- package/src/git/git_ops.js +61 -0
- package/src/git/git_panel.js +755 -0
- package/src/git/index.js +9 -0
- package/src/git/line_diff.js +202 -0
- package/src/help/help_modal.js +0 -0
- package/src/help/help_registry.js +0 -0
- package/src/host/external_links.js +0 -0
- package/src/host/file_picker.js +0 -0
- package/src/host/host.js +0 -0
- package/src/host/photino_host.js +0 -0
- package/src/host/photino_host.md +0 -0
- package/src/host/pywebview_host.js +0 -0
- package/src/tiles/chart_tiles.js +0 -0
- package/src/tiles/config_schema.js +0 -0
- package/src/tiles/figure_tile.js +0 -0
- package/src/tiles/layout_persistence.js +0 -0
- package/src/tiles/list_tiles.js +0 -0
- package/src/tiles/scope_picker.js +0 -0
- package/src/tiles/tile_base.js +5 -4
- package/src/tiles/tile_config_form.js +0 -0
- package/src/tiles/tile_registry.js +0 -0
- package/src/tiling/bottom_panel_tabs.js +0 -0
- package/src/tiling/command_palette.js +0 -0
- package/src/tiling/content_registry.js +0 -0
- package/src/tiling/desktops.js +10 -8
- package/src/tiling/entity_sources.js +0 -0
- package/src/tiling/keymap.js +6 -5
- package/src/tiling/kind_taxonomy.js +15 -2
- package/src/tiling/landing_table.js +0 -0
- package/src/tiling/loading_overlay.js +0 -0
- package/src/tiling/nav_panel.js +0 -0
- package/src/tiling/page_factory.js +0 -0
- package/src/tiling/panel_availability.js +0 -0
- package/src/tiling/panel_keys.js +0 -0
- package/src/tiling/shell.js +124 -17
- package/src/tiling/tab_strip.js +0 -0
- package/src/tiling/tile_renderer.js +2 -3
- package/src/tiling/tile_tab_menu.js +0 -0
- package/src/tiling/zoom.js +0 -0
- package/src/ui/base/component_base.js +0 -0
- package/src/ui/base/controller_base.js +0 -0
- package/src/ui/base/page_base.js +0 -0
- package/src/ui/components/about_dialog.js +0 -0
- package/src/ui/components/action_dropdown.js +4 -78
- package/src/ui/components/alert_log.js +0 -0
- package/src/ui/components/alerts_pane.js +0 -0
- package/src/ui/components/anchored_layer.js +255 -0
- package/src/ui/components/attribute_list_editor.js +0 -0
- package/src/ui/components/autocomplete_field.js +0 -0
- package/src/ui/components/cell_editor_host.js +0 -0
- package/src/ui/components/cell_editors.js +0 -0
- package/src/ui/components/cell_grid.js +0 -0
- package/src/ui/components/cell_paste.js +0 -0
- package/src/ui/components/cell_selection.js +0 -0
- package/src/ui/components/chat_shell.js +920 -0
- package/src/ui/components/chat_state_machine.js +223 -0
- package/src/ui/components/choice_controls.js +45 -2
- package/src/ui/components/color_field.js +3 -2
- package/src/ui/components/color_list_field.js +3 -2
- package/src/ui/components/column_fit.js +0 -0
- package/src/ui/components/column_groups.js +0 -0
- package/src/ui/components/column_order.js +0 -0
- package/src/ui/components/combobox.js +422 -0
- package/src/ui/components/conversation_view.js +17 -2
- package/src/ui/components/crosstab.js +0 -0
- package/src/ui/components/data_table.js +2 -2
- package/src/ui/components/detail_header.js +0 -0
- package/src/ui/components/document_outline.js +0 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/fact_table.js +0 -0
- package/src/ui/components/fill_handle.js +0 -0
- package/src/ui/components/filter_builder.js +766 -0
- package/src/ui/components/filter_model.js +614 -0
- package/src/ui/components/gallery_picker.js +0 -0
- package/src/ui/components/header_actions.js +0 -0
- package/src/ui/components/inline_renamer.js +0 -0
- package/src/ui/components/job_progress.js +0 -0
- package/src/ui/components/managed_window.js +24 -47
- package/src/ui/components/menu_bar.js +101 -24
- package/src/ui/components/modal.js +79 -109
- package/src/ui/components/option_list.js +160 -0
- package/src/ui/components/popover.js +94 -0
- package/src/ui/components/progress.js +0 -0
- package/src/ui/components/recent_items.js +169 -0
- package/src/ui/components/record_list.js +0 -0
- package/src/ui/components/row_activation.js +0 -0
- package/src/ui/components/section_stack.js +0 -0
- package/src/ui/components/select.js +322 -0
- package/src/ui/components/series.js +0 -0
- package/src/ui/components/settings_view.js +0 -0
- package/src/ui/components/slide_out_panel.js +0 -0
- package/src/ui/components/slider_field.js +0 -0
- package/src/ui/components/sortable_list.js +0 -0
- package/src/ui/components/staged_edits.js +0 -0
- package/src/ui/components/staged_history.js +0 -0
- package/src/ui/components/table_filter_chips.js +0 -0
- package/src/ui/components/table_state_store.js +0 -0
- package/src/ui/components/table_structure.js +0 -0
- package/src/ui/components/table_values.js +0 -0
- package/src/ui/components/tabs.js +0 -0
- package/src/ui/components/tag_input.js +77 -8
- package/src/ui/components/toast.js +0 -0
- package/src/ui/components/toggle_field.js +0 -0
- package/src/ui/components/tsv.js +0 -0
- package/src/ui/components/window_taskbar.js +0 -0
- package/src/ui/components/workspace_picker.js +1155 -0
- package/src/ui/controllers/window_chrome_controller.js +4 -1
- package/src/ui/utils/auto_scrollbars.js +0 -0
- package/src/ui/utils/fire_event.js +17 -0
- package/src/ui/utils/focusable.js +25 -0
- package/src/ui/utils/markdown.js +0 -0
- package/src/ui/utils/modal_host.js +27 -0
- package/src/ui/utils/native_context_menu.js +0 -0
- package/src/ui/utils/overlay_scrollbar.js +0 -0
- package/src/ui/utils/raf_resize_observer.js +0 -0
- package/src/ui/utils/scroll_lock.js +0 -0
- package/src/ui/utils/strip_scroll.js +0 -0
- package/src/ui/utils/tooltip_service.js +0 -0
- package/tiles.js +0 -0
- package/widgets.js +12 -2
- package/wm.js +0 -0
- package/css/base.css +0 -10527
- package/css/overrides.css +0 -2596
- package/dist/chunk-7JI7HHSY.js +0 -107
- package/dist/chunk-7JI7HHSY.js.map +0 -7
- package/dist/chunk-MIW7D6WC.js +0 -526
- package/dist/chunk-MIW7D6WC.js.map +0 -7
- package/dist/chunk-PGZ4EHSE.js.map +0 -7
- package/dist/chunk-XJNADRGR.js.map +0 -7
- /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git_graph.js — a commit history drawn as a vertical SVG swimlane graph, one
|
|
3
|
+
* row a commit, loaded a page at a time from the adapter's `log`.
|
|
4
|
+
*
|
|
5
|
+
* const graph = createGitGraph(el, { adapter, onOpenDiff, onChanged });
|
|
6
|
+
* graph.refresh();
|
|
7
|
+
* graph.destroy();
|
|
8
|
+
*
|
|
9
|
+
* Give `el` a height: the graph scrolls inside it and asks for the next page
|
|
10
|
+
* when the last row comes into view. `refresh()` re-reads what is loaded and
|
|
11
|
+
* redraws only if something differs, so it is safe to call on every status
|
|
12
|
+
* change without losing the scroll position.
|
|
13
|
+
*
|
|
14
|
+
* A row's context menu offers what the adapter can do (docs/components/git.md);
|
|
15
|
+
* each call goes through the adapter and a failure is an error toast with the
|
|
16
|
+
* backend's words. Nothing is stored on the DOM.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { showContextMenu } from '../ui/components/context_menu.js';
|
|
20
|
+
import { reportBridgeError, toastInfo } from '../ui/components/toast.js';
|
|
21
|
+
import { askFor, callGit, openDiffHandler } from './git_ops.js';
|
|
22
|
+
|
|
23
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
24
|
+
/** The tokens `--twm-git-lane-0` … `-7`; a lane past the last takes them round again. */
|
|
25
|
+
const LANE_COLOURS = 8;
|
|
26
|
+
const LANE_WIDTH = 11;
|
|
27
|
+
const ROW_HEIGHT = 22; // .twm-git-graph__row's height in flexdesk.css
|
|
28
|
+
const NODE_RADIUS = 4;
|
|
29
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Assign each commit to a lane. Commits come newest first, children before
|
|
33
|
+
* their parents (`git log --topo-order`). The answer is the same for a list
|
|
34
|
+
* and for any longer list that starts with it, so pages only ever add rows.
|
|
35
|
+
*
|
|
36
|
+
* Each row says what to draw in its own 22 pixels:
|
|
37
|
+
* lane the commit's own lane
|
|
38
|
+
* hasChild a line comes down into the commit (a branch tip has none)
|
|
39
|
+
* outputLanes `{ lane, parentHash }`: a line leaving the commit for each parent
|
|
40
|
+
* passLanes lanes that run straight through this row to a later commit
|
|
41
|
+
*
|
|
42
|
+
* @param {Array<{hash: string, parentHashes: string[]}>} commits
|
|
43
|
+
* @returns {{ rows: Array<Object>, laneCount: number }}
|
|
44
|
+
*/
|
|
45
|
+
export function computeGraphLanes(commits) {
|
|
46
|
+
const active = []; // active[i]: the hash lane i is waiting for; null when free
|
|
47
|
+
const allocate = () => {
|
|
48
|
+
const free = active.indexOf(null);
|
|
49
|
+
if (free !== -1) return free;
|
|
50
|
+
active.push(null);
|
|
51
|
+
return active.length - 1;
|
|
52
|
+
};
|
|
53
|
+
const rows = commits.map((commit) => {
|
|
54
|
+
// A lane waits for one hash and no hash waits in two lanes: a parent
|
|
55
|
+
// already waited for is joined at its lane, not given another.
|
|
56
|
+
const waiting = active.indexOf(commit.hash);
|
|
57
|
+
const hasChild = waiting !== -1;
|
|
58
|
+
const lane = hasChild ? waiting : allocate();
|
|
59
|
+
active[lane] = null;
|
|
60
|
+
const passLanes = [];
|
|
61
|
+
active.forEach((hash, l) => { if (hash !== null) passLanes.push(l); });
|
|
62
|
+
|
|
63
|
+
const outputLanes = (commit.parentHashes ?? []).map((parentHash, i) => {
|
|
64
|
+
let parentLane = active.indexOf(parentHash);
|
|
65
|
+
if (parentLane === -1) parentLane = i === 0 ? lane : allocate();
|
|
66
|
+
active[parentLane] = parentHash;
|
|
67
|
+
return { lane: parentLane, parentHash };
|
|
68
|
+
});
|
|
69
|
+
return { lane, hasChild, outputLanes, passLanes };
|
|
70
|
+
});
|
|
71
|
+
return { rows, laneCount: Math.max(active.length, 1) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** What each full ref name in a decoration list is, by how it starts. */
|
|
75
|
+
const REF_KINDS = [
|
|
76
|
+
['HEAD -> refs/heads/', 'head'],
|
|
77
|
+
['tag: refs/tags/', 'tag'],
|
|
78
|
+
['refs/heads/', 'branch'],
|
|
79
|
+
['refs/remotes/', 'remote'],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read `git log --decorate=full`'s `%D` list into pills. Full names are what
|
|
84
|
+
* tell a remote's `refs/remotes/origin/main` from a local branch that happens
|
|
85
|
+
* to be called `origin/main`. Other refs, such as `refs/stash`, are left out.
|
|
86
|
+
* @param {string} refs - `HEAD -> refs/heads/main, tag: refs/tags/v1, refs/remotes/origin/main`
|
|
87
|
+
* @returns {Array<{name: string, type: 'head'|'tag'|'branch'|'remote'}>}
|
|
88
|
+
*/
|
|
89
|
+
export function parseGitRefs(refs) {
|
|
90
|
+
if (!refs) return [];
|
|
91
|
+
return String(refs).split(',').map((s) => s.trim()).flatMap((part) => {
|
|
92
|
+
if (part === 'HEAD') return [{ name: 'HEAD', type: 'head' }];
|
|
93
|
+
const kind = REF_KINDS.find(([prefix]) => part.startsWith(prefix));
|
|
94
|
+
return kind ? [{ name: part.slice(kind[0].length), type: kind[1] }] : [];
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const laneX = (lane) => lane * LANE_WIDTH + LANE_WIDTH / 2;
|
|
99
|
+
|
|
100
|
+
function edge(d, lane) {
|
|
101
|
+
const path = document.createElementNS(SVG_NS, 'path');
|
|
102
|
+
path.setAttribute('class', `twm-git-graph__edge twm-git-lane--${lane % LANE_COLOURS}`);
|
|
103
|
+
path.setAttribute('d', d);
|
|
104
|
+
return path;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sizeGutter(svg, laneCount) {
|
|
108
|
+
const width = Math.max(laneCount * LANE_WIDTH, LANE_WIDTH * 2);
|
|
109
|
+
svg.setAttribute('width', String(width));
|
|
110
|
+
svg.setAttribute('viewBox', `0 0 ${width} ${ROW_HEIGHT}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createGutter(row, laneCount) {
|
|
114
|
+
const svg = document.createElementNS(SVG_NS, 'svg');
|
|
115
|
+
svg.setAttribute('class', 'twm-git-graph__svg');
|
|
116
|
+
svg.setAttribute('height', String(ROW_HEIGHT));
|
|
117
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
118
|
+
sizeGutter(svg, laneCount);
|
|
119
|
+
|
|
120
|
+
const h = ROW_HEIGHT;
|
|
121
|
+
const cx = laneX(row.lane);
|
|
122
|
+
const cy = h / 2;
|
|
123
|
+
for (const lane of row.passLanes) {
|
|
124
|
+
const x = laneX(lane);
|
|
125
|
+
svg.appendChild(edge(`M${x},0 L${x},${h}`, lane));
|
|
126
|
+
}
|
|
127
|
+
if (row.hasChild) svg.appendChild(edge(`M${cx},0 L${cx},${cy}`, row.lane));
|
|
128
|
+
for (const { lane } of row.outputLanes) {
|
|
129
|
+
const x = laneX(lane);
|
|
130
|
+
svg.appendChild(edge(x === cx
|
|
131
|
+
? `M${cx},${cy} L${cx},${h}`
|
|
132
|
+
: `M${cx},${cy} C${cx},${h * 0.8} ${x},${h * 0.6} ${x},${h}`, lane));
|
|
133
|
+
}
|
|
134
|
+
const node = document.createElementNS(SVG_NS, 'circle');
|
|
135
|
+
node.setAttribute('class', `twm-git-graph__node twm-git-lane--${row.lane % LANE_COLOURS}`);
|
|
136
|
+
node.setAttribute('cx', String(cx));
|
|
137
|
+
node.setAttribute('cy', String(cy));
|
|
138
|
+
node.setAttribute('r', String(NODE_RADIUS));
|
|
139
|
+
svg.appendChild(node);
|
|
140
|
+
return svg;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function commitTitle(commit) {
|
|
144
|
+
const date = new Date(commit.timestamp * 1000).toLocaleDateString();
|
|
145
|
+
return `${commit.shortHash} — ${commit.author}\n${date}\n\n${commit.subject}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {HTMLElement} host
|
|
150
|
+
* @param {Object} [options]
|
|
151
|
+
* @param {Object|null} [options.adapter] - A git adapter with `log`; the rest is used when present.
|
|
152
|
+
* @param {(file: {path: string, status: string, ref: string}) => void} [options.onOpenDiff] - Opens a file of a
|
|
153
|
+
* commit. Without it, a diff window when the adapter has `diffFile`.
|
|
154
|
+
* @param {() => void} [options.onChanged] - Told after a menu operation changed the repository.
|
|
155
|
+
* @param {number} [options.pageSize=50]
|
|
156
|
+
* @param {boolean} [options.showRemoteRefs=false] - Draw remote branches' pills as well; a narrow panel leaves them out.
|
|
157
|
+
* @returns {{ el: HTMLElement, refresh(): Promise<void>, setAdapter(adapter: Object|null): void, destroy(): void }}
|
|
158
|
+
*/
|
|
159
|
+
export function createGitGraph(host, options = {}) {
|
|
160
|
+
const { onChanged = null, pageSize = DEFAULT_PAGE_SIZE, showRemoteRefs = false } = options;
|
|
161
|
+
let adapter = options.adapter ?? null;
|
|
162
|
+
const openDiff = openDiffHandler(() => adapter, options.onOpenDiff);
|
|
163
|
+
|
|
164
|
+
const root = document.createElement('div');
|
|
165
|
+
root.className = 'twm-git-graph';
|
|
166
|
+
host.appendChild(root);
|
|
167
|
+
|
|
168
|
+
let commits = [];
|
|
169
|
+
let drawnJson = '';
|
|
170
|
+
let allLoaded = false;
|
|
171
|
+
let generation = 0; // a newer read makes an older answer stale
|
|
172
|
+
let observer = null;
|
|
173
|
+
let sentinel = null;
|
|
174
|
+
let destroyed = false;
|
|
175
|
+
|
|
176
|
+
const say = (text) => {
|
|
177
|
+
const m = document.createElement('div');
|
|
178
|
+
m.className = 'twm-git-graph__message';
|
|
179
|
+
m.textContent = text;
|
|
180
|
+
root.replaceChildren(m);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const stopObserving = () => {
|
|
184
|
+
observer?.disconnect();
|
|
185
|
+
observer = null;
|
|
186
|
+
sentinel?.remove();
|
|
187
|
+
sentinel = null;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
/** `git` is the adapter the menu was opened on; a file opens only while that repository still shows. */
|
|
191
|
+
const openFiles = async (git, commit, x, y) => {
|
|
192
|
+
const result = await callGit('Git changes', () => git.commitFiles({ hash: commit.hash }));
|
|
193
|
+
if (!result || git !== adapter) return;
|
|
194
|
+
const files = result.files ?? [];
|
|
195
|
+
if (files.length === 0) {
|
|
196
|
+
toastInfo('No files changed', commit.shortHash);
|
|
197
|
+
} else if (files.length === 1) {
|
|
198
|
+
openDiff({ ...files[0], ref: commit.hash });
|
|
199
|
+
} else {
|
|
200
|
+
const items = files.map((f, i) => ({ label: f.path, icon: 'difference', action: String(i) }));
|
|
201
|
+
showContextMenu(x, y, items, (action) => {
|
|
202
|
+
if (git === adapter) openDiff({ ...files[Number(action)], ref: commit.hash });
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const menuItems = (git) => {
|
|
208
|
+
const groups = [
|
|
209
|
+
[
|
|
210
|
+
git.checkout && { label: 'Checkout (detached)', icon: 'logout', action: 'checkout' },
|
|
211
|
+
git.createBranch && { label: 'Create Branch…', icon: 'commit', action: 'create-branch' },
|
|
212
|
+
{ label: 'Copy Commit Hash', icon: 'content_copy', action: 'copy-hash' },
|
|
213
|
+
],
|
|
214
|
+
[
|
|
215
|
+
git.cherryPick && { label: 'Cherry-pick', icon: 'content_copy', action: 'cherry-pick' },
|
|
216
|
+
git.createTag && { label: 'Create Tag…', icon: 'sell', action: 'create-tag' },
|
|
217
|
+
git.commitFiles && { label: 'View Changes', icon: 'difference', action: 'view-changes' },
|
|
218
|
+
],
|
|
219
|
+
].map((group) => group.filter(Boolean)).filter((group) => group.length > 0);
|
|
220
|
+
return groups.flatMap((group, i) => (i === 0 ? group : [{ separator: true }, ...group]));
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const onMenuAction = async (git, action, commit, x, y) => {
|
|
224
|
+
const { hash } = commit;
|
|
225
|
+
/** Run an operation; whatever came of it, the graph and its owner read the repository again. */
|
|
226
|
+
const change = async (opName, fn) => {
|
|
227
|
+
await callGit(opName, fn);
|
|
228
|
+
await refresh();
|
|
229
|
+
onChanged?.();
|
|
230
|
+
};
|
|
231
|
+
switch (action) {
|
|
232
|
+
case 'checkout':
|
|
233
|
+
await change('Git checkout', () => git.checkout({ ref: hash }));
|
|
234
|
+
break;
|
|
235
|
+
case 'create-branch': {
|
|
236
|
+
const values = await askFor('Create Branch', 'Create', [['name', 'Branch name']]);
|
|
237
|
+
if (values) await change('Git create branch', () => git.createBranch({ name: values.name, from: hash }));
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'copy-hash':
|
|
241
|
+
try {
|
|
242
|
+
await navigator.clipboard.writeText(hash);
|
|
243
|
+
toastInfo('Commit hash copied', commit.shortHash);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
reportBridgeError('Copy commit hash failed', e);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
case 'cherry-pick':
|
|
249
|
+
await change('Git cherry-pick', () => git.cherryPick({ hash }));
|
|
250
|
+
break;
|
|
251
|
+
case 'create-tag': {
|
|
252
|
+
const values = await askFor('Create Tag', 'Create', [['name', 'Tag name']]);
|
|
253
|
+
if (values) await change('Git create tag', () => git.createTag({ name: values.name, hash }));
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case 'view-changes':
|
|
257
|
+
await openFiles(git, commit, x, y);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const createRow = (commit, laneRow, laneCount) => {
|
|
263
|
+
const row = document.createElement('div');
|
|
264
|
+
row.className = 'twm-git-graph__row';
|
|
265
|
+
row.title = commitTitle(commit);
|
|
266
|
+
row.tabIndex = 0;
|
|
267
|
+
row.appendChild(createGutter(laneRow, laneCount));
|
|
268
|
+
|
|
269
|
+
const info = document.createElement('div');
|
|
270
|
+
info.className = 'twm-git-graph__info';
|
|
271
|
+
for (const pill of parseGitRefs(commit.refs)) {
|
|
272
|
+
if (pill.type === 'remote' && !showRemoteRefs) continue;
|
|
273
|
+
const span = document.createElement('span');
|
|
274
|
+
span.className = `twm-git-ref twm-git-ref--${pill.type}`;
|
|
275
|
+
span.textContent = pill.name;
|
|
276
|
+
span.title = pill.name;
|
|
277
|
+
info.appendChild(span);
|
|
278
|
+
}
|
|
279
|
+
const subject = document.createElement('span');
|
|
280
|
+
subject.className = 'twm-git-graph__subject';
|
|
281
|
+
subject.textContent = commit.subject;
|
|
282
|
+
info.appendChild(subject);
|
|
283
|
+
row.appendChild(info);
|
|
284
|
+
|
|
285
|
+
const openMenu = (x, y) => {
|
|
286
|
+
const git = adapter;
|
|
287
|
+
showContextMenu(x, y, menuItems(git), (action) => onMenuAction(git, action, commit, x, y));
|
|
288
|
+
};
|
|
289
|
+
row.addEventListener('contextmenu', (e) => {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
openMenu(e.clientX, e.clientY);
|
|
292
|
+
});
|
|
293
|
+
row.addEventListener('keydown', (e) => {
|
|
294
|
+
if (e.key !== 'ContextMenu' && !(e.key === 'F10' && e.shiftKey)) return;
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
const r = row.getBoundingClientRect();
|
|
297
|
+
openMenu(r.left, r.bottom);
|
|
298
|
+
});
|
|
299
|
+
return row;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const observeSentinel = () => {
|
|
303
|
+
stopObserving();
|
|
304
|
+
if (allLoaded || commits.length === 0) return;
|
|
305
|
+
sentinel = document.createElement('div');
|
|
306
|
+
sentinel.className = 'twm-git-graph__sentinel';
|
|
307
|
+
root.appendChild(sentinel);
|
|
308
|
+
observer = new IntersectionObserver((entries) => {
|
|
309
|
+
if (entries[0]?.isIntersecting) loadMore();
|
|
310
|
+
}, { root });
|
|
311
|
+
observer.observe(sentinel);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/** Draw the rows from `from` on, and widen the gutters above them when the lanes outgrew them. */
|
|
315
|
+
const drawRows = (from) => {
|
|
316
|
+
stopObserving();
|
|
317
|
+
const { rows, laneCount } = computeGraphLanes(commits);
|
|
318
|
+
for (const svg of root.querySelectorAll('.twm-git-graph__svg')) sizeGutter(svg, laneCount);
|
|
319
|
+
const fragment = document.createDocumentFragment();
|
|
320
|
+
for (let i = from; i < commits.length; i++) fragment.appendChild(createRow(commits[i], rows[i], laneCount));
|
|
321
|
+
root.appendChild(fragment);
|
|
322
|
+
observeSentinel();
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const clear = () => {
|
|
326
|
+
stopObserving();
|
|
327
|
+
commits = [];
|
|
328
|
+
drawnJson = '';
|
|
329
|
+
allLoaded = false;
|
|
330
|
+
root.replaceChildren();
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/** Read from the top as many commits as are loaded, replacing what is drawn only if they differ. */
|
|
334
|
+
const refresh = async () => {
|
|
335
|
+
const gen = ++generation;
|
|
336
|
+
if (!adapter?.log) {
|
|
337
|
+
clear();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const count = Math.max(commits.length, pageSize);
|
|
341
|
+
let result;
|
|
342
|
+
try {
|
|
343
|
+
result = await adapter.log({ count, skip: 0 });
|
|
344
|
+
} catch (e) {
|
|
345
|
+
result = { ok: false, error: e?.message || String(e) };
|
|
346
|
+
}
|
|
347
|
+
if (destroyed || gen !== generation) return;
|
|
348
|
+
if (!result?.ok) {
|
|
349
|
+
clear();
|
|
350
|
+
say(result?.error || 'The history could not be read.');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const next = result.commits ?? [];
|
|
354
|
+
const json = JSON.stringify(next);
|
|
355
|
+
if (json === drawnJson) {
|
|
356
|
+
if (!observer) observeSentinel(); // a read that overtook loadMore must not end the paging
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const top = root.scrollTop;
|
|
360
|
+
clear();
|
|
361
|
+
drawnJson = json;
|
|
362
|
+
commits = next;
|
|
363
|
+
allLoaded = next.length < count;
|
|
364
|
+
if (commits.length === 0) {
|
|
365
|
+
say('No commits yet');
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
drawRows(0);
|
|
369
|
+
root.scrollTop = top;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const loadMore = async () => {
|
|
373
|
+
const gen = ++generation;
|
|
374
|
+
stopObserving();
|
|
375
|
+
const skip = commits.length;
|
|
376
|
+
const result = await callGit('Git log', () => adapter.log({ count: pageSize, skip }));
|
|
377
|
+
if (destroyed || gen !== generation || !result) return;
|
|
378
|
+
const page = result.commits ?? [];
|
|
379
|
+
allLoaded = page.length < pageSize;
|
|
380
|
+
commits = commits.concat(page);
|
|
381
|
+
drawnJson = JSON.stringify(commits);
|
|
382
|
+
drawRows(skip);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
refresh();
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
el: root,
|
|
389
|
+
refresh,
|
|
390
|
+
setAdapter(next) {
|
|
391
|
+
adapter = next ?? null;
|
|
392
|
+
clear();
|
|
393
|
+
refresh();
|
|
394
|
+
},
|
|
395
|
+
destroy() {
|
|
396
|
+
destroyed = true;
|
|
397
|
+
stopObserving();
|
|
398
|
+
root.remove();
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git_ops.js — what the panel and the graph share about calling an adapter.
|
|
3
|
+
* Internal to @flexdesk/git: not exported from its barrel.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { openForm } from '../ui/components/modal.js';
|
|
7
|
+
import { reportBridgeError } from '../ui/components/toast.js';
|
|
8
|
+
import { openGitDiffWindow } from './diff_window.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Call an adapter method. A failure, an `{ ok: false, error }` answer or a
|
|
12
|
+
* throw alike, is an error toast carrying the backend's words; a stop on
|
|
13
|
+
* conflicts is said to be that.
|
|
14
|
+
* @param {string} opName - As the toast names it: 'Git push'.
|
|
15
|
+
* @param {() => Promise<Object>} fn
|
|
16
|
+
* @returns {Promise<Object|null>} The answer, or null when it failed.
|
|
17
|
+
*/
|
|
18
|
+
export async function callGit(opName, fn) {
|
|
19
|
+
let result;
|
|
20
|
+
try {
|
|
21
|
+
result = await fn();
|
|
22
|
+
} catch (e) {
|
|
23
|
+
reportBridgeError(`${opName} failed`, e instanceof Error ? e : new Error(String(e)));
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const title = result?.conflicts ? `${opName} stopped on conflicts` : `${opName} failed`;
|
|
27
|
+
return reportBridgeError(title, result) ? (result ?? { ok: true }) : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ask for text values in a FlexDesk form.
|
|
32
|
+
* @param {string} title
|
|
33
|
+
* @param {string} submitLabel
|
|
34
|
+
* @param {Array<[string, string]>} fields - `[name, label]` pairs, all required.
|
|
35
|
+
* @returns {Promise<Object|null>} The trimmed values, or null when cancelled or left blank.
|
|
36
|
+
*/
|
|
37
|
+
export async function askFor(title, submitLabel, fields) {
|
|
38
|
+
const values = await openForm({
|
|
39
|
+
title,
|
|
40
|
+
submitLabel,
|
|
41
|
+
fields: fields.map(([name, label]) => ({ name, label, type: 'text', required: true })),
|
|
42
|
+
});
|
|
43
|
+
if (!values) return null;
|
|
44
|
+
const trimmed = Object.fromEntries(fields.map(([name]) => [name, String(values[name] ?? '').trim()]));
|
|
45
|
+
return Object.values(trimmed).every(Boolean) ? trimmed : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* What opening a file's diff does: the consumer's callback, else a diff window
|
|
50
|
+
* when the adapter has `diffFile`.
|
|
51
|
+
* @param {() => Object|null} getAdapter
|
|
52
|
+
* @param {Function|null|undefined} onOpenDiff
|
|
53
|
+
* @returns {(file: {path: string, status: string, renamedFrom?: string, ref?: string}) => void}
|
|
54
|
+
*/
|
|
55
|
+
export function openDiffHandler(getAdapter, onOpenDiff) {
|
|
56
|
+
if (onOpenDiff) return onOpenDiff;
|
|
57
|
+
return ({ path, status, renamedFrom, ref }) => {
|
|
58
|
+
const adapter = getAdapter();
|
|
59
|
+
if (adapter?.diffFile) openGitDiffWindow({ adapter, path, status, renamedFrom, ref });
|
|
60
|
+
};
|
|
61
|
+
}
|