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,755 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* git_panel.js — a source-control panel: the branch and what can be done with
|
|
3
|
+
* it, a commit box, the changed files, and the commit graph.
|
|
4
|
+
*
|
|
5
|
+
* const panel = createGitPanel(el, { adapter, onCountChange: (n) => setBadge(n) });
|
|
6
|
+
* panel.setAdapter(otherRepository); // or null: nothing to show
|
|
7
|
+
* panel.stopPolling(); // while its tab is hidden
|
|
8
|
+
* panel.startPolling();
|
|
9
|
+
* panel.destroy();
|
|
10
|
+
*
|
|
11
|
+
* Every repository operation is a method of `adapter`, whose methods and
|
|
12
|
+
* payloads are the written contract in docs/components/git.md: `isRepo` and
|
|
13
|
+
* `status` are required, and an action whose method the adapter lacks is not
|
|
14
|
+
* offered. The panel knows nothing of where the repository is or how the
|
|
15
|
+
* adapter reaches it.
|
|
16
|
+
*
|
|
17
|
+
* The panel's own elements are built once and updated in place, so a status
|
|
18
|
+
* change never touches what somebody is typing into the commit box. A failed
|
|
19
|
+
* operation is an error toast carrying the backend's words, and the panel reads
|
|
20
|
+
* the repository again either way. Nothing is stored on the DOM.
|
|
21
|
+
*
|
|
22
|
+
* An operation belongs to the repository it started on: whatever waits on a
|
|
23
|
+
* dialog or on an earlier step holds that adapter, not the panel's current one,
|
|
24
|
+
* so `setAdapter` can never send the rest of an operation to another repository.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { ActionDropdown } from '../ui/components/action_dropdown.js';
|
|
28
|
+
import { showConfirmDialog, showSelectDialog } from '../ui/components/confirm_dialog.js';
|
|
29
|
+
import { showContextMenu } from '../ui/components/context_menu.js';
|
|
30
|
+
import { toastInfo } from '../ui/components/toast.js';
|
|
31
|
+
import { createGitGraph } from './git_graph.js';
|
|
32
|
+
import { askFor, callGit, openDiffHandler } from './git_ops.js';
|
|
33
|
+
|
|
34
|
+
const DEFAULT_POLL_MS = 3000;
|
|
35
|
+
const COMMIT_BOX_MAX_PX = 80;
|
|
36
|
+
|
|
37
|
+
function el(tag, className, text) {
|
|
38
|
+
const n = document.createElement(tag);
|
|
39
|
+
if (className) n.className = className;
|
|
40
|
+
if (text !== undefined) n.textContent = text;
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function icon(name) {
|
|
45
|
+
const span = el('span', 'material-symbols-outlined', name);
|
|
46
|
+
span.setAttribute('aria-hidden', 'true');
|
|
47
|
+
return span;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function iconButton(className, iconName, label, onClick) {
|
|
51
|
+
const b = el('button', className);
|
|
52
|
+
b.type = 'button';
|
|
53
|
+
b.title = label;
|
|
54
|
+
b.setAttribute('aria-label', label);
|
|
55
|
+
b.appendChild(icon(iconName));
|
|
56
|
+
b.addEventListener('click', onClick);
|
|
57
|
+
return b;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** ActionDropdown draws a label as markup; a branch name is text. */
|
|
61
|
+
const escapeLabel = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
62
|
+
|
|
63
|
+
/** A path's last segment, and the folder before it. */
|
|
64
|
+
function splitPath(path) {
|
|
65
|
+
const parts = String(path).replace(/\\/g, '/').split('/').filter(Boolean);
|
|
66
|
+
return { name: parts[parts.length - 1] ?? String(path), dir: parts.slice(0, -1).join('/') };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function requireMethods(adapter) {
|
|
70
|
+
const missing = ['isRepo', 'status'].filter((m) => typeof adapter[m] !== 'function');
|
|
71
|
+
if (missing.length > 0) throw new TypeError(`createGitPanel: the adapter has no ${missing.join(' or ')} method`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {HTMLElement} host
|
|
76
|
+
* @param {Object} [options]
|
|
77
|
+
* @param {Object|null} [options.adapter] - The git adapter; null shows `emptyLabel`.
|
|
78
|
+
* @param {(file: {path: string, status: string, staged?: boolean, renamedFrom?: string, ref?: string}) => void} [options.onOpenDiff]
|
|
79
|
+
* Opens a file's diff. Without it, a diff window when the adapter has `diffFile`.
|
|
80
|
+
* @param {(count: number) => void} [options.onCountChange] - Told how many files have changes, whenever that is read.
|
|
81
|
+
* @param {number} [options.pollInterval=3000] - Milliseconds between status reads; 0 reads only on `refresh()`.
|
|
82
|
+
* @param {string} [options.emptyLabel='No repository'] - Shown while there is no adapter.
|
|
83
|
+
* @param {boolean} [options.changesExpanded=true]
|
|
84
|
+
* @param {boolean} [options.graphExpanded=false]
|
|
85
|
+
* @param {boolean} [options.showRemoteRefs=false] - As for createGitGraph.
|
|
86
|
+
* @returns {{ el: HTMLElement, refresh(): Promise<void>, setAdapter(adapter: Object|null): void,
|
|
87
|
+
* startPolling(): void, stopPolling(): void, destroy(): void }}
|
|
88
|
+
*/
|
|
89
|
+
export function createGitPanel(host, options = {}) {
|
|
90
|
+
const {
|
|
91
|
+
onCountChange = null,
|
|
92
|
+
pollInterval = DEFAULT_POLL_MS,
|
|
93
|
+
emptyLabel = 'No repository',
|
|
94
|
+
showRemoteRefs = false,
|
|
95
|
+
} = options;
|
|
96
|
+
|
|
97
|
+
let adapter = options.adapter ?? null;
|
|
98
|
+
if (adapter) requireMethods(adapter);
|
|
99
|
+
const openDiff = openDiffHandler(() => adapter, options.onOpenDiff);
|
|
100
|
+
|
|
101
|
+
let isRepo = false;
|
|
102
|
+
let status = null;
|
|
103
|
+
let statusJson = '';
|
|
104
|
+
let branches = [];
|
|
105
|
+
let busy = false;
|
|
106
|
+
let pollTimer = null;
|
|
107
|
+
let generation = 0; // a newer read, or another adapter, makes an older answer stale
|
|
108
|
+
let reading = false;
|
|
109
|
+
let destroyed = false;
|
|
110
|
+
let changesExpanded = options.changesExpanded ?? true;
|
|
111
|
+
let graphExpanded = options.graphExpanded ?? false;
|
|
112
|
+
let graph = null;
|
|
113
|
+
|
|
114
|
+
// ── the elements ────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
const root = el('div', 'twm-git-panel');
|
|
117
|
+
const messageView = el('div', 'twm-git-panel__empty');
|
|
118
|
+
const mainView = el('div', 'twm-git-panel__main');
|
|
119
|
+
const busyBar = el('div', 'twm-git-panel__busy-bar');
|
|
120
|
+
root.append(messageView, mainView);
|
|
121
|
+
|
|
122
|
+
const branchButton = el('button', 'twm-git-panel__branch');
|
|
123
|
+
branchButton.type = 'button';
|
|
124
|
+
const branchLabel = el('span', 'twm-git-panel__branch-label');
|
|
125
|
+
branchButton.append(icon('commit'), branchLabel);
|
|
126
|
+
const badges = el('div', 'twm-git-panel__badges');
|
|
127
|
+
const barActions = el('div', 'twm-git-panel__bar-actions');
|
|
128
|
+
const bar = el('div', 'twm-git-panel__bar');
|
|
129
|
+
bar.append(branchButton, badges, barActions);
|
|
130
|
+
|
|
131
|
+
const fetchButton = iconButton('twm-git-panel__bar-btn', 'sync', 'Fetch',
|
|
132
|
+
() => runOp('fetch', () => adapter.fetch()));
|
|
133
|
+
const pullButton = iconButton('twm-git-panel__bar-btn', 'arrow_downward', 'Pull',
|
|
134
|
+
() => runOp('pull', () => adapter.pull()));
|
|
135
|
+
const pushButton = iconButton('twm-git-panel__bar-btn', 'arrow_upward', 'Push',
|
|
136
|
+
() => runOp('push', () => adapter.push({ setUpstream: !status?.hasUpstream })));
|
|
137
|
+
const overflowButton = iconButton('twm-git-panel__bar-btn', 'more_horiz', 'More actions', () => {});
|
|
138
|
+
barActions.append(fetchButton, pullButton, pushButton, overflowButton);
|
|
139
|
+
|
|
140
|
+
const commitArea = el('div', 'twm-git-panel__commit');
|
|
141
|
+
const commitBox = el('textarea', 'twm-git-panel__commit-box');
|
|
142
|
+
commitBox.rows = 1;
|
|
143
|
+
commitBox.placeholder = 'Commit message (Ctrl+Enter to commit)';
|
|
144
|
+
commitBox.setAttribute('aria-label', 'Commit message');
|
|
145
|
+
const commitButton = el('button', 'twm-git-panel__commit-btn', 'Commit');
|
|
146
|
+
commitButton.type = 'button';
|
|
147
|
+
const commitMenuButton = iconButton('twm-git-panel__commit-menu', 'expand_more', 'Commit options', () => {});
|
|
148
|
+
const commitActions = el('div', 'twm-git-panel__commit-actions');
|
|
149
|
+
commitActions.append(commitButton, commitMenuButton);
|
|
150
|
+
commitArea.append(commitBox, commitActions);
|
|
151
|
+
|
|
152
|
+
const section = (title, expanded, onToggle) => {
|
|
153
|
+
const arrow = el('span', 'material-symbols-outlined twm-collapsible-arrow twm-git-panel__arrow', 'expand_more');
|
|
154
|
+
arrow.setAttribute('aria-hidden', 'true');
|
|
155
|
+
const count = el('span', 'twm-git-panel__count');
|
|
156
|
+
const head = el('button', 'twm-collapsible-header twm-git-panel__section-head');
|
|
157
|
+
head.type = 'button';
|
|
158
|
+
head.append(arrow, title, count);
|
|
159
|
+
const body = el('div', 'twm-git-panel__section-body');
|
|
160
|
+
const setExpanded = (on) => {
|
|
161
|
+
arrow.classList.toggle('twm-git-panel__arrow--collapsed', !on);
|
|
162
|
+
head.setAttribute('aria-expanded', String(on));
|
|
163
|
+
body.hidden = !on;
|
|
164
|
+
};
|
|
165
|
+
setExpanded(expanded);
|
|
166
|
+
head.addEventListener('click', () => onToggle(setExpanded));
|
|
167
|
+
const wrap = el('div', 'twm-git-panel__section');
|
|
168
|
+
wrap.append(head, body);
|
|
169
|
+
return { wrap, body, count };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const changes = section('Changes', changesExpanded, (setExpanded) => {
|
|
173
|
+
changesExpanded = !changesExpanded;
|
|
174
|
+
setExpanded(changesExpanded);
|
|
175
|
+
});
|
|
176
|
+
const history = section('Graph', graphExpanded, (setExpanded) => {
|
|
177
|
+
graphExpanded = !graphExpanded;
|
|
178
|
+
setExpanded(graphExpanded);
|
|
179
|
+
if (graphExpanded) ensureGraph();
|
|
180
|
+
});
|
|
181
|
+
mainView.append(busyBar, bar, commitArea, changes.wrap, history.wrap);
|
|
182
|
+
host.appendChild(root);
|
|
183
|
+
|
|
184
|
+
// ── the views ───────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
const showMessage = (iconName, text, action = null) => {
|
|
187
|
+
const parts = [icon(iconName), el('span', null, text)];
|
|
188
|
+
if (action) parts.push(action);
|
|
189
|
+
messageView.replaceChildren(...parts);
|
|
190
|
+
messageView.hidden = false;
|
|
191
|
+
mainView.hidden = true;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const showNotRepo = () => {
|
|
195
|
+
let action = null;
|
|
196
|
+
if (adapter.init) {
|
|
197
|
+
action = el('button', 'twm-btn twm-btn--primary twm-git-panel__init', 'Initialize Repository');
|
|
198
|
+
action.type = 'button';
|
|
199
|
+
action.addEventListener('click', initRepo);
|
|
200
|
+
}
|
|
201
|
+
showMessage('source', 'Not a git repository', action);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const fileRow = (file) => {
|
|
205
|
+
const { name, dir } = splitPath(file.path);
|
|
206
|
+
const row = el('div', 'twm-git-panel__file');
|
|
207
|
+
row.tabIndex = 0;
|
|
208
|
+
row.setAttribute('role', 'button');
|
|
209
|
+
row.title = file.renamedFrom ? `${file.renamedFrom} → ${file.path}` : file.path;
|
|
210
|
+
row.append(
|
|
211
|
+
el('span', `twm-git-panel__status twm-git-panel__status--${file.status}`, file.status),
|
|
212
|
+
el('span', 'twm-git-panel__file-name', name),
|
|
213
|
+
);
|
|
214
|
+
if (dir) row.appendChild(el('span', 'twm-git-panel__file-dir', dir));
|
|
215
|
+
const open = () => openDiff({ path: file.path, status: file.status, staged: file.staged,
|
|
216
|
+
...(file.renamedFrom ? { renamedFrom: file.renamedFrom } : {}) });
|
|
217
|
+
row.addEventListener('click', open);
|
|
218
|
+
row.addEventListener('keydown', (e) => {
|
|
219
|
+
if (e.target === row && (e.key === 'Enter' || e.key === ' ')) {
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
open();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const actions = el('div', 'twm-git-panel__file-actions');
|
|
226
|
+
const action = (iconName, label, fn) => {
|
|
227
|
+
actions.appendChild(iconButton('twm-git-panel__file-btn', iconName, label, (e) => {
|
|
228
|
+
e.stopPropagation();
|
|
229
|
+
fn();
|
|
230
|
+
}));
|
|
231
|
+
};
|
|
232
|
+
if (!file.staged && adapter.stage) {
|
|
233
|
+
action('add', 'Stage', () => runOp('stage', () => adapter.stage({ paths: [file.path] })));
|
|
234
|
+
}
|
|
235
|
+
if (file.staged && adapter.unstage) {
|
|
236
|
+
// A rename is two paths to the index: the new one alone would leave the old one staged as deleted.
|
|
237
|
+
const paths = file.renamedFrom ? [file.renamedFrom, file.path] : [file.path];
|
|
238
|
+
action('remove', 'Unstage', () => runOp('unstage', () => adapter.unstage({ paths })));
|
|
239
|
+
}
|
|
240
|
+
// A conflict is resolved or the merge aborted; there is no one state to put it back to.
|
|
241
|
+
if (adapter.discard && file.status !== 'C') action('undo', 'Discard changes', () => discard(file));
|
|
242
|
+
row.appendChild(actions);
|
|
243
|
+
return row;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const drawStatus = () => {
|
|
247
|
+
branchLabel.textContent = status.branch ?? '';
|
|
248
|
+
const marks = [];
|
|
249
|
+
if (status.hasUpstream && status.ahead > 0) marks.push(el('span', 'twm-git-panel__badge', `↑${status.ahead}`));
|
|
250
|
+
if (status.hasUpstream && status.behind > 0) marks.push(el('span', 'twm-git-panel__badge', `↓${status.behind}`));
|
|
251
|
+
badges.replaceChildren(...marks);
|
|
252
|
+
|
|
253
|
+
const files = status.files ?? [];
|
|
254
|
+
changes.count.textContent = String(files.length);
|
|
255
|
+
changes.body.replaceChildren(...(files.length > 0
|
|
256
|
+
? files.map(fileRow)
|
|
257
|
+
: [el('div', 'twm-git-panel__no-changes', 'No changes')]));
|
|
258
|
+
overflowDropdown.setOptions(overflowOptions());
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const drawBusy = () => {
|
|
262
|
+
busyBar.hidden = !busy;
|
|
263
|
+
for (const b of [branchButton, fetchButton, pullButton, pushButton, overflowButton, commitButton, commitMenuButton]) {
|
|
264
|
+
b.disabled = busy;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** What is on offer is what the adapter can do. */
|
|
269
|
+
const drawCapabilities = () => {
|
|
270
|
+
if (!adapter) return;
|
|
271
|
+
fetchButton.hidden = !adapter.fetch;
|
|
272
|
+
pullButton.hidden = !adapter.pull;
|
|
273
|
+
pushButton.hidden = !adapter.push;
|
|
274
|
+
commitArea.hidden = !adapter.commit;
|
|
275
|
+
const overflow = overflowOptions();
|
|
276
|
+
overflowButton.hidden = overflow.every((o) => o.separator);
|
|
277
|
+
overflowDropdown.setOptions(overflow);
|
|
278
|
+
commitDropdown.setOptions(commitOptions());
|
|
279
|
+
branchDropdown.setOptions(branchOptions());
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// ── the menus ───────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
const commitOptions = () => [
|
|
285
|
+
{ type: 'commit', label: 'Commit', icon: 'check' },
|
|
286
|
+
{ type: 'amend', label: 'Commit (Amend)', icon: 'edit' },
|
|
287
|
+
{ separator: true },
|
|
288
|
+
...(adapter?.push ? [{ type: 'commitPush', label: 'Commit & Push', icon: 'arrow_upward' }] : []),
|
|
289
|
+
...(adapter?.pull && adapter?.push ? [{ type: 'commitSync', label: 'Commit & Sync', icon: 'sync' }] : []),
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
const overflowOptions = () => {
|
|
293
|
+
if (!adapter) return [];
|
|
294
|
+
return [
|
|
295
|
+
...(adapter.stash ? [{ type: 'stash', label: 'Stash', icon: 'archive' }] : []),
|
|
296
|
+
...(adapter.stashPop ? [{ type: 'stashPop', label: 'Pop Stash', icon: 'unarchive' }] : []),
|
|
297
|
+
{ separator: true },
|
|
298
|
+
...(adapter.merge ? [{ type: 'merge', label: 'Merge…', icon: 'merge' }] : []),
|
|
299
|
+
...(adapter.rebase ? [{ type: 'rebase', label: 'Rebase…', icon: 'rebase_edit' }] : []),
|
|
300
|
+
...(adapter.rebase && status?.rebasing ? [
|
|
301
|
+
{ type: 'rebaseContinue', label: 'Continue Rebase', icon: 'play_arrow' },
|
|
302
|
+
{ type: 'rebaseAbort', label: 'Abort Rebase', icon: 'cancel' },
|
|
303
|
+
] : []),
|
|
304
|
+
{ separator: true },
|
|
305
|
+
...(adapter.remotes ? [{ type: 'remotes', label: 'Remotes…', icon: 'cloud' }] : []),
|
|
306
|
+
...(adapter.tags ? [{ type: 'tags', label: 'Tags…', icon: 'sell' }]
|
|
307
|
+
: adapter.createTag ? [{ type: 'createTag', label: 'Create Tag…', icon: 'sell' }] : []),
|
|
308
|
+
];
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/** Local then remote branches to check out, then making and removing branches. */
|
|
312
|
+
const branchOptions = () => {
|
|
313
|
+
if (!adapter) return [];
|
|
314
|
+
const ref = (b, iconName) => ({ type: 'checkout', ref: b.name, label: escapeLabel(b.name), icon: iconName });
|
|
315
|
+
return [
|
|
316
|
+
...(adapter.checkout ? [
|
|
317
|
+
...branches.filter((b) => !b.isRemote).map((b) => ref(b, b.isCurrent ? 'check' : 'commit')),
|
|
318
|
+
{ separator: true },
|
|
319
|
+
...branches.filter((b) => b.isRemote).map((b) => ref(b, 'cloud')),
|
|
320
|
+
] : []),
|
|
321
|
+
{ separator: true },
|
|
322
|
+
...(adapter.createBranch ? [{ type: 'createBranch', label: 'Create Branch…', icon: 'add' }] : []),
|
|
323
|
+
...(adapter.deleteBranch ? [{ type: 'deleteBranch', label: 'Delete Branch…', icon: 'delete' }] : []),
|
|
324
|
+
];
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const branchDropdown = new ActionDropdown({
|
|
328
|
+
trigger: branchButton,
|
|
329
|
+
options: [],
|
|
330
|
+
onSelect: (option) => onBranchOption(option),
|
|
331
|
+
});
|
|
332
|
+
const commitDropdown = new ActionDropdown({
|
|
333
|
+
trigger: commitMenuButton,
|
|
334
|
+
options: [],
|
|
335
|
+
onSelect: (option) => commit(option.type),
|
|
336
|
+
});
|
|
337
|
+
const overflowDropdown = new ActionDropdown({
|
|
338
|
+
trigger: overflowButton,
|
|
339
|
+
options: [],
|
|
340
|
+
onSelect: (option) => onOverflowOption(option),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// ── reading the repository ──────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
const readBranches = async (gen) => {
|
|
346
|
+
if (!adapter?.branches) return;
|
|
347
|
+
let result;
|
|
348
|
+
try {
|
|
349
|
+
result = await adapter.branches();
|
|
350
|
+
} catch {
|
|
351
|
+
return; // the branch menu keeps what it has; the next read tries again
|
|
352
|
+
}
|
|
353
|
+
if (gen !== generation || destroyed || !result?.ok) return;
|
|
354
|
+
branches = result.branches ?? [];
|
|
355
|
+
branchDropdown.setOptions(branchOptions());
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const refresh = async () => {
|
|
359
|
+
const gen = ++generation;
|
|
360
|
+
if (!adapter) {
|
|
361
|
+
showMessage('folder_off', emptyLabel);
|
|
362
|
+
onCountChange?.(0);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
reading = true;
|
|
366
|
+
try {
|
|
367
|
+
// Asked until it is yes, so a repository made outside the panel is
|
|
368
|
+
// noticed; a repository that goes away fails `status` and is asked again.
|
|
369
|
+
if (!isRepo) {
|
|
370
|
+
const check = await adapter.isRepo();
|
|
371
|
+
if (gen !== generation || destroyed) return;
|
|
372
|
+
if (!check?.ok) {
|
|
373
|
+
showMessage('error', check?.error || 'The repository could not be read.');
|
|
374
|
+
onCountChange?.(0);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (!check.isRepo) {
|
|
378
|
+
showNotRepo();
|
|
379
|
+
onCountChange?.(0);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
isRepo = true;
|
|
383
|
+
}
|
|
384
|
+
const result = await adapter.status();
|
|
385
|
+
if (gen !== generation || destroyed) return;
|
|
386
|
+
if (!result?.ok) {
|
|
387
|
+
isRepo = false;
|
|
388
|
+
statusJson = '';
|
|
389
|
+
showMessage('error', result?.error || 'The repository could not be read.');
|
|
390
|
+
onCountChange?.(0);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
messageView.hidden = true;
|
|
394
|
+
mainView.hidden = false;
|
|
395
|
+
const json = JSON.stringify(result);
|
|
396
|
+
if (json === statusJson) return;
|
|
397
|
+
statusJson = json;
|
|
398
|
+
status = result;
|
|
399
|
+
drawStatus();
|
|
400
|
+
onCountChange?.((result.files ?? []).length);
|
|
401
|
+
readBranches(gen);
|
|
402
|
+
graph?.refresh();
|
|
403
|
+
} catch (e) {
|
|
404
|
+
if (gen !== generation || destroyed) return;
|
|
405
|
+
isRepo = false;
|
|
406
|
+
statusJson = '';
|
|
407
|
+
showMessage('error', e?.message || String(e));
|
|
408
|
+
onCountChange?.(0);
|
|
409
|
+
} finally {
|
|
410
|
+
if (gen === generation) reading = false;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
/** A poll that finds the last read still out waits for the next tick; it never queues. */
|
|
415
|
+
const poll = () => {
|
|
416
|
+
if (!reading && !busy) refresh();
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const startPolling = () => {
|
|
420
|
+
if (pollTimer !== null || destroyed) return;
|
|
421
|
+
refresh();
|
|
422
|
+
if (pollInterval > 0) pollTimer = setInterval(poll, pollInterval);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const stopPolling = () => {
|
|
426
|
+
if (pollTimer !== null) clearInterval(pollTimer);
|
|
427
|
+
pollTimer = null;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// ── operations ──────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Run an operation with the panel busy, one at a time; one asked for while
|
|
434
|
+
* another runs does not run, and says so. The panel reads the repository
|
|
435
|
+
* again however it went.
|
|
436
|
+
*/
|
|
437
|
+
async function runOp(opName, fn) {
|
|
438
|
+
if (busy) {
|
|
439
|
+
toastInfo('Git is busy', `The ${opName} did not run: another operation is still going.`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
busy = true;
|
|
443
|
+
drawBusy();
|
|
444
|
+
await callGit(`Git ${opName}`, fn);
|
|
445
|
+
busy = false;
|
|
446
|
+
drawBusy();
|
|
447
|
+
statusJson = '';
|
|
448
|
+
await refresh();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function initRepo() {
|
|
452
|
+
const result = await callGit('Git init', () => adapter.init());
|
|
453
|
+
if (result) await refresh();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** The answer of the first step that failed, else of the last. */
|
|
457
|
+
async function inSteps(steps) {
|
|
458
|
+
let result;
|
|
459
|
+
for (const step of steps) {
|
|
460
|
+
result = await step();
|
|
461
|
+
if (result && result.ok === false) return result;
|
|
462
|
+
}
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function commit(mode) {
|
|
467
|
+
const message = commitBox.value.trim();
|
|
468
|
+
const amend = mode === 'amend';
|
|
469
|
+
if (!message && !amend) {
|
|
470
|
+
commitBox.focus();
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const files = status?.files ?? [];
|
|
474
|
+
if (!amend && files.length === 0) {
|
|
475
|
+
toastInfo('Nothing to commit', 'There are no changes.');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const git = adapter;
|
|
479
|
+
// A branch with no upstream yet gets one on its first push.
|
|
480
|
+
const setUpstream = !status?.hasUpstream;
|
|
481
|
+
// With nothing staged, a commit takes every change.
|
|
482
|
+
const stageAll = !amend && git.stage && !files.some((f) => f.staged);
|
|
483
|
+
await runOp('commit', () => inSteps([
|
|
484
|
+
...(stageAll ? [() => git.stage({ paths: 'all' })] : []),
|
|
485
|
+
async () => {
|
|
486
|
+
const result = await git.commit({ message: message || null, amend });
|
|
487
|
+
if (git === adapter && (!result || result.ok !== false)) {
|
|
488
|
+
commitBox.value = '';
|
|
489
|
+
commitBox.style.height = '';
|
|
490
|
+
}
|
|
491
|
+
return result;
|
|
492
|
+
},
|
|
493
|
+
...(mode === 'commitSync' ? [() => git.pull()] : []),
|
|
494
|
+
...(mode === 'commitPush' || mode === 'commitSync' ? [() => git.push({ setUpstream })] : []),
|
|
495
|
+
]));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** What the dialog says, by what discarding does to this kind of change. */
|
|
499
|
+
const discardWords = ({ path, status: letter, renamedFrom }) => {
|
|
500
|
+
const { name } = splitPath(path);
|
|
501
|
+
if (letter === 'U') {
|
|
502
|
+
return { title: 'Delete File', okLabel: 'Delete', message: `Delete the untracked file "${name}"? This cannot be undone.` };
|
|
503
|
+
}
|
|
504
|
+
if (letter === 'A') {
|
|
505
|
+
return { title: 'Delete File', okLabel: 'Delete', message: `Delete the new file "${name}"? It was never committed, so this cannot be undone.` };
|
|
506
|
+
}
|
|
507
|
+
if (renamedFrom) {
|
|
508
|
+
return { title: 'Discard Changes', okLabel: 'Discard', message: `Put "${splitPath(renamedFrom).name}" back and discard all changes to "${name}"?` };
|
|
509
|
+
}
|
|
510
|
+
return { title: 'Discard Changes', okLabel: 'Discard', message: `Discard all changes to "${name}"?` };
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
async function discard(file) {
|
|
514
|
+
const git = adapter;
|
|
515
|
+
const confirmed = await showConfirmDialog({ ...discardWords(file), icon: 'warning', okVariant: 'danger' });
|
|
516
|
+
if (!confirmed) return;
|
|
517
|
+
await runOp('discard', () => git.discard({ path: file.path, status: file.status,
|
|
518
|
+
...(file.renamedFrom ? { renamedFrom: file.renamedFrom } : {}) }));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function onBranchOption(option) {
|
|
522
|
+
if (option.type === 'checkout') {
|
|
523
|
+
await runOp('checkout', () => adapter.checkout({ ref: option.ref }));
|
|
524
|
+
} else if (option.type === 'createBranch') {
|
|
525
|
+
const git = adapter;
|
|
526
|
+
const values = await askFor('Create Branch', 'Create', [['name', 'Branch name']]);
|
|
527
|
+
if (values) await runOp('create branch', () => git.createBranch({ name: values.name }));
|
|
528
|
+
} else if (option.type === 'deleteBranch') {
|
|
529
|
+
await deleteBranch();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const otherLocalBranches = () => branches.filter((b) => !b.isCurrent && !b.isRemote);
|
|
534
|
+
|
|
535
|
+
/** A branch the adapter refuses as not merged is offered once more, forced; any other refusal is only that. */
|
|
536
|
+
async function deleteBranch() {
|
|
537
|
+
const git = adapter;
|
|
538
|
+
const others = otherLocalBranches();
|
|
539
|
+
if (others.length === 0) {
|
|
540
|
+
toastInfo('Nothing to delete', 'There is no other local branch.');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const name = await showSelectDialog({
|
|
544
|
+
title: 'Delete Branch',
|
|
545
|
+
okLabel: 'Delete',
|
|
546
|
+
items: others.map((b) => ({ value: b.name, label: b.name })),
|
|
547
|
+
});
|
|
548
|
+
if (!name) return;
|
|
549
|
+
let notMerged = false;
|
|
550
|
+
await runOp('delete branch', async () => {
|
|
551
|
+
const result = await git.deleteBranch({ name, force: false });
|
|
552
|
+
notMerged = result?.notMerged === true;
|
|
553
|
+
return notMerged ? { ok: true } : result; // the dialog below says it, not a toast as well
|
|
554
|
+
});
|
|
555
|
+
if (!notMerged) return;
|
|
556
|
+
const force = await showConfirmDialog({
|
|
557
|
+
title: 'Force Delete Branch',
|
|
558
|
+
message: `"${name}" is not fully merged. Delete it anyway? Commits only on it may be lost.`,
|
|
559
|
+
icon: 'warning',
|
|
560
|
+
okLabel: 'Force Delete',
|
|
561
|
+
okVariant: 'danger',
|
|
562
|
+
});
|
|
563
|
+
if (force) await runOp('delete branch', () => git.deleteBranch({ name, force: true }));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function onOverflowOption(option) {
|
|
567
|
+
switch (option.type) {
|
|
568
|
+
case 'stash':
|
|
569
|
+
await runOp('stash', () => adapter.stash({}));
|
|
570
|
+
break;
|
|
571
|
+
case 'stashPop':
|
|
572
|
+
await runOp('stash pop', () => adapter.stashPop());
|
|
573
|
+
break;
|
|
574
|
+
case 'merge':
|
|
575
|
+
case 'rebase':
|
|
576
|
+
await mergeOrRebase(option.type);
|
|
577
|
+
break;
|
|
578
|
+
case 'rebaseContinue':
|
|
579
|
+
await runOp('rebase', () => adapter.rebase({ continue: true }));
|
|
580
|
+
break;
|
|
581
|
+
case 'rebaseAbort':
|
|
582
|
+
await runOp('rebase', () => adapter.rebase({ abort: true }));
|
|
583
|
+
break;
|
|
584
|
+
case 'remotes':
|
|
585
|
+
await showRemotesMenu();
|
|
586
|
+
break;
|
|
587
|
+
case 'tags':
|
|
588
|
+
await showTagsMenu();
|
|
589
|
+
break;
|
|
590
|
+
case 'createTag':
|
|
591
|
+
await createTag(adapter);
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Merge another local branch into the current one, or rebase onto it. */
|
|
597
|
+
async function mergeOrRebase(op) {
|
|
598
|
+
const git = adapter;
|
|
599
|
+
const others = otherLocalBranches();
|
|
600
|
+
if (others.length === 0) {
|
|
601
|
+
toastInfo(`Nothing to ${op}`, 'There is no other local branch.');
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const branch = await showSelectDialog({
|
|
605
|
+
title: op === 'merge' ? 'Merge Branch' : 'Rebase onto Branch',
|
|
606
|
+
okLabel: op === 'merge' ? 'Merge' : 'Rebase',
|
|
607
|
+
items: others.map((b) => ({ value: b.name, label: b.name })),
|
|
608
|
+
});
|
|
609
|
+
if (!branch) return;
|
|
610
|
+
await runOp(op, () => (op === 'merge' ? git.merge({ branch }) : git.rebase({ onto: branch })));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** A menu under the bar's actions: one item per thing listed, then a way to add one. */
|
|
614
|
+
const barMenu = (items, emptyTitle, onAction) => {
|
|
615
|
+
if (items.length === 0) {
|
|
616
|
+
toastInfo(emptyTitle, 'There are none in this repository.');
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
const anchor = barActions.getBoundingClientRect();
|
|
620
|
+
showContextMenu(anchor.left, anchor.bottom + 4, items, onAction);
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
async function showRemotesMenu() {
|
|
624
|
+
const git = adapter;
|
|
625
|
+
const result = await callGit('Git remotes', () => git.remotes());
|
|
626
|
+
if (!result || git !== adapter) return;
|
|
627
|
+
const remotes = result.remotes ?? [];
|
|
628
|
+
const items = git.removeRemote ? remotes.map((r, i) => ({
|
|
629
|
+
label: `Remove ${r.name}`, title: r.url, icon: 'cloud_off', action: String(i), danger: true,
|
|
630
|
+
})) : remotes.map((r) => ({ label: r.name, title: r.url, icon: 'cloud', disabled: true }));
|
|
631
|
+
if (git.addRemote) {
|
|
632
|
+
if (items.length > 0) items.push({ separator: true });
|
|
633
|
+
items.push({ label: 'Add Remote…', icon: 'add', action: 'add' });
|
|
634
|
+
}
|
|
635
|
+
barMenu(items, 'No remotes', async (action) => {
|
|
636
|
+
if (action === 'add') {
|
|
637
|
+
const values = await askFor('Add Remote', 'Add', [['name', 'Name'], ['url', 'URL']]);
|
|
638
|
+
if (values) await runOp('add remote', () => git.addRemote(values));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const { name } = remotes[Number(action)];
|
|
642
|
+
const confirmed = await showConfirmDialog({
|
|
643
|
+
title: 'Remove Remote',
|
|
644
|
+
message: `Remove remote "${name}"?`,
|
|
645
|
+
icon: 'warning',
|
|
646
|
+
okLabel: 'Remove',
|
|
647
|
+
okVariant: 'danger',
|
|
648
|
+
});
|
|
649
|
+
if (confirmed) await runOp('remove remote', () => git.removeRemote({ name }));
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function createTag(git) {
|
|
654
|
+
const values = await askFor('Create Tag', 'Create', [['name', 'Tag name']]);
|
|
655
|
+
if (values) await runOp('create tag', () => git.createTag({ name: values.name }));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function showTagsMenu() {
|
|
659
|
+
const git = adapter;
|
|
660
|
+
const result = await callGit('Git tags', () => git.tags());
|
|
661
|
+
if (!result || git !== adapter) return;
|
|
662
|
+
const tags = result.tags ?? [];
|
|
663
|
+
const items = git.deleteTag ? tags.map((t, i) => ({
|
|
664
|
+
label: `Delete ${t.name}`, title: t.hash, icon: 'delete', action: String(i), danger: true,
|
|
665
|
+
})) : tags.map((t) => ({ label: t.name, title: t.hash, icon: 'sell', disabled: true }));
|
|
666
|
+
if (git.createTag) {
|
|
667
|
+
if (items.length > 0) items.push({ separator: true });
|
|
668
|
+
items.push({ label: 'Create Tag…', icon: 'add', action: 'add' });
|
|
669
|
+
}
|
|
670
|
+
barMenu(items, 'No tags', async (action) => {
|
|
671
|
+
if (action === 'add') {
|
|
672
|
+
await createTag(git);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const { name } = tags[Number(action)];
|
|
676
|
+
const confirmed = await showConfirmDialog({
|
|
677
|
+
title: 'Delete Tag',
|
|
678
|
+
message: `Delete tag "${name}"?`,
|
|
679
|
+
icon: 'warning',
|
|
680
|
+
okLabel: 'Delete',
|
|
681
|
+
okVariant: 'danger',
|
|
682
|
+
});
|
|
683
|
+
if (confirmed) await runOp('delete tag', () => git.deleteTag({ name }));
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function ensureGraph() {
|
|
688
|
+
if (graph || !adapter?.log) return;
|
|
689
|
+
graph = createGitGraph(history.body, {
|
|
690
|
+
adapter,
|
|
691
|
+
onOpenDiff: openDiff,
|
|
692
|
+
showRemoteRefs,
|
|
693
|
+
onChanged: () => {
|
|
694
|
+
statusJson = '';
|
|
695
|
+
refresh();
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// ── wiring ──────────────────────────────────────────────────────────
|
|
701
|
+
|
|
702
|
+
commitButton.addEventListener('click', () => commit('commit'));
|
|
703
|
+
commitBox.addEventListener('keydown', (e) => {
|
|
704
|
+
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
705
|
+
e.preventDefault();
|
|
706
|
+
commit('commit');
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
commitBox.addEventListener('input', () => {
|
|
710
|
+
commitBox.style.height = 'auto';
|
|
711
|
+
commitBox.style.height = `${Math.min(commitBox.scrollHeight, COMMIT_BOX_MAX_PX)}px`;
|
|
712
|
+
});
|
|
713
|
+
// The menu opens on what is known; a branch made elsewhere arrives while it is open.
|
|
714
|
+
branchButton.addEventListener('click', () => readBranches(generation));
|
|
715
|
+
|
|
716
|
+
drawBusy();
|
|
717
|
+
drawCapabilities();
|
|
718
|
+
if (graphExpanded) ensureGraph();
|
|
719
|
+
showMessage('hourglass_empty', 'Checking repository…');
|
|
720
|
+
startPolling();
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
el: root,
|
|
724
|
+
refresh,
|
|
725
|
+
startPolling,
|
|
726
|
+
stopPolling,
|
|
727
|
+
setAdapter(next) {
|
|
728
|
+
if (next) requireMethods(next);
|
|
729
|
+
adapter = next ?? null;
|
|
730
|
+
isRepo = false;
|
|
731
|
+
status = null;
|
|
732
|
+
statusJson = '';
|
|
733
|
+
branches = [];
|
|
734
|
+
reading = false;
|
|
735
|
+
commitBox.value = '';
|
|
736
|
+
commitBox.style.height = '';
|
|
737
|
+
changes.body.replaceChildren();
|
|
738
|
+
drawCapabilities();
|
|
739
|
+
graph?.setAdapter(adapter);
|
|
740
|
+
if (graphExpanded) ensureGraph();
|
|
741
|
+
if (adapter) showMessage('hourglass_empty', 'Checking repository…');
|
|
742
|
+
refresh();
|
|
743
|
+
},
|
|
744
|
+
destroy() {
|
|
745
|
+
destroyed = true;
|
|
746
|
+
generation++;
|
|
747
|
+
stopPolling();
|
|
748
|
+
branchDropdown.destroy();
|
|
749
|
+
commitDropdown.destroy();
|
|
750
|
+
overflowDropdown.destroy();
|
|
751
|
+
graph?.destroy();
|
|
752
|
+
root.remove();
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|