@ibgib/space-gib 0.0.34 → 0.0.36
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/dist/client/bootstrap.mjs +1 -1
- package/dist/client/{chunk-25PXC4HP.mjs → chunk-5DTAUY3E.mjs} +1586 -464
- package/dist/client/chunk-J7X3XTAI.mjs +53 -0
- package/dist/client/css/activity-bar.css +21 -3
- package/dist/client/css/layout.css +126 -5
- package/dist/client/css/tutorial.css +327 -0
- package/dist/client/index.html +6 -0
- package/dist/client/index.mjs +22 -45
- package/dist/client/privacy.html +22 -22
- package/dist/client/script.mjs +1 -1
- package/dist/client/style.css +1 -0
- package/dist/server/server.mjs +3 -3
- package/package.json +6 -6
- package/src/client/AUTO-GENERATED-version.mts +1 -1
- package/src/client/cosmos/seed-cosmos-workspace.mts +20 -6
- package/src/client/css/activity-bar.css +21 -3
- package/src/client/css/layout.css +126 -5
- package/src/client/css/tutorial.css +327 -0
- package/src/client/dev-tools/vcs-workspace.mts +4 -4
- package/src/client/index.html +6 -0
- package/src/client/privacy.html +22 -22
- package/src/client/style.css +1 -0
- package/src/client/ui/component/changes/changes.css +413 -0
- package/src/client/ui/component/changes/changes.html +37 -0
- package/src/client/ui/component/changes/changes.mts +766 -0
- package/src/client/ui/component/changes/index.mts +9 -0
- package/src/client/ui/component/chat-view/chat-view.html +6 -58
- package/src/client/ui/component/chat-view/chat-view.mts +13 -26
- package/src/client/ui/component/clone/clone.mts +67 -39
- package/src/client/ui/component/code-editor/code-editor.css +91 -9
- package/src/client/ui/component/code-editor/code-editor.html +58 -45
- package/src/client/ui/component/code-editor/code-editor.mts +265 -49
- package/src/client/ui/component/cosmos-navigator/cosmos-navigator.css +1 -1
- package/src/client/ui/component/cosmos-navigator/cosmos-navigator.html +5 -10
- package/src/client/ui/component/cosmos-navigator/cosmos-navigator.mts +328 -64
- package/src/client/ui/component/file-explorer/file-explorer.css +475 -11
- package/src/client/ui/component/file-explorer/file-explorer.html +45 -65
- package/src/client/ui/component/file-explorer/file-explorer.mts +1164 -98
- package/src/client/ui/component/nested-chat/chat/chat.mts +5 -90
- package/src/client/ui/component/nested-chat/nested-chat.mts +10 -8
- package/src/client/ui/component/timeline/timeline.css +16 -1
- package/src/client/ui/component/timeline/timeline.mts +37 -93
- package/src/client/ui/component/tutorial/index.mts +9 -0
- package/src/client/ui/component/tutorial/scripts/pitch-tour-script.mts +265 -0
- package/src/client/ui/component/tutorial/tutorial-controller.mts +622 -0
- package/src/client/ui/component/tutorial/tutorial-types.mts +45 -0
- package/src/client/ui/component/tutorial/tutorial.css +260 -0
- package/src/client/ui/router/space-gib-router-helpers.mts +33 -233
- package/src/client/ui/router/space-gib-router-helpers.respec.mts +298 -122
- package/src/client/ui/router/space-gib-router-service.mts +28 -151
- package/src/client/ui/router/space-gib-router-types.mts +32 -76
- package/src/client/ui/shell/cosmic-big-bang.mts +12 -3
- package/src/client/ui/shell/space-gib-shell-service.mts +715 -143
- package/dist/client/chunk-RXWLL2AI.mjs +0 -30
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import thisCss from "./file-explorer.css";
|
|
2
2
|
import thisHtml from "./file-explorer.html";
|
|
3
|
+
import vcsStylesCss from "@ibgib/web-gib/dist/ui/component/vcs-styles.css";
|
|
3
4
|
|
|
4
5
|
import { ROOT_ADDR } from "@ibgib/ts-gib/dist/V1/constants.mjs";
|
|
5
6
|
import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs";
|
|
6
7
|
import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
|
|
7
8
|
import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
8
|
-
import { promptForText } from "@ibgib/web-gib/dist/helpers.web.mjs";
|
|
9
|
+
import { promptForText, promptForConfirm } from "@ibgib/web-gib/dist/helpers.web.mjs";
|
|
9
10
|
import {
|
|
10
11
|
IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase,
|
|
11
12
|
} from "@ibgib/web-gib/dist/ui/component/ibgib-dynamic-component-bases.mjs";
|
|
@@ -13,6 +14,24 @@ import {
|
|
|
13
14
|
ElementsBase, IbGibDynamicComponentInstance,
|
|
14
15
|
} from "@ibgib/web-gib/dist/ui/component/component-types.mjs";
|
|
15
16
|
import { getComponentCtorArg } from "@ibgib/web-gib/dist/app-bootstrap/init-orchestration.mjs";
|
|
17
|
+
import { getPlaygroundMetaspaceAndSpace } from "../../../cosmos/seed-cosmos-workspace.mjs";
|
|
18
|
+
import {
|
|
19
|
+
getBranchAndRootItemInfosAndIbGibsAndSpaceFromGalaxyIbGib,
|
|
20
|
+
createFileInGalaxyBranch,
|
|
21
|
+
getFolderChildren,
|
|
22
|
+
getFileItemFromPath,
|
|
23
|
+
} from "@ibgib/core-gib/dist/cosmology/branching/item/item-helpers.mjs";
|
|
24
|
+
import { GalaxyIbGib_V1 } from "@ibgib/core-gib/dist/cosmology/galaxy/galaxy-types.mjs";
|
|
25
|
+
import { B2tFSGalaxyIbGib_V1 } from "@ibgib/core-gib/dist/cosmology/branching/repo/b2tfs-repo-types.mjs";
|
|
26
|
+
import { getIbGibFromSpace, getTjpAddr } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
27
|
+
import { getSpaceGibRouterSvc } from "../../router/index.mjs";
|
|
28
|
+
import {
|
|
29
|
+
getAllBranchInfosFromGalaxy, getCompositeBranchTreeFromGalaxy,
|
|
30
|
+
validateBranchName,
|
|
31
|
+
} from "@ibgib/core-gib/dist/cosmology/branching/branch/branch-helpers.mjs";
|
|
32
|
+
import { CompositeBranchNode } from "@ibgib/core-gib/dist/cosmology/branching/branch/branch-types.mjs";
|
|
33
|
+
import { getDeterministicColorInfo } from "@ibgib/web-gib/dist/helpers.mjs";
|
|
34
|
+
import { GibColorInfo } from "@ibgib/web-gib/dist/types.mjs";
|
|
16
35
|
|
|
17
36
|
export const FILE_EXPLORER_COMPONENT_NAME = 'space-gib-file-explorer';
|
|
18
37
|
|
|
@@ -44,26 +63,43 @@ export class FileExplorerComponentMeta extends IbGibDynamicComponentMetaBase {
|
|
|
44
63
|
ibGibAddr: ibGibAddr || ROOT_ADDR,
|
|
45
64
|
meta: this,
|
|
46
65
|
html: thisHtml,
|
|
47
|
-
css: [thisCss],
|
|
66
|
+
css: [vcsStylesCss, thisCss],
|
|
48
67
|
});
|
|
49
68
|
return component;
|
|
50
69
|
}
|
|
51
70
|
}
|
|
52
71
|
|
|
72
|
+
/**
|
|
73
|
+
* A single command item in the file explorer popup command menu.
|
|
74
|
+
*/
|
|
75
|
+
export interface CommandMenuItem {
|
|
76
|
+
id: string;
|
|
77
|
+
icon: string;
|
|
78
|
+
label: string;
|
|
79
|
+
action: () => void | Promise<void>;
|
|
80
|
+
danger?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
53
83
|
/**
|
|
54
84
|
* References to DOM elements within the component's Shadow Root.
|
|
55
85
|
*/
|
|
56
86
|
export interface FileExplorerElements extends ElementsBase {
|
|
57
87
|
rootEl: HTMLDivElement;
|
|
88
|
+
modeToolbarEl?: HTMLDivElement;
|
|
89
|
+
btnModeSingleEl?: HTMLButtonElement;
|
|
90
|
+
btnModeCompositeEl?: HTMLButtonElement;
|
|
91
|
+
btnRefreshExplorerEl?: HTMLButtonElement;
|
|
92
|
+
singleBranchViewEl?: HTMLDivElement;
|
|
93
|
+
compositeTreeContainerEl?: HTMLDivElement;
|
|
58
94
|
branchHeaderEl: HTMLDivElement;
|
|
59
95
|
btnBranchToggleEl: HTMLButtonElement;
|
|
96
|
+
btnBranchSelectEl: HTMLButtonElement;
|
|
60
97
|
branchTitleEl: HTMLSpanElement;
|
|
98
|
+
branchDirtyBadgeEl?: HTMLSpanElement;
|
|
99
|
+
branchDropdownMenuEl: HTMLDivElement;
|
|
61
100
|
fileTreeContainerEl: HTMLDivElement;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
btnCollapseAllEl: HTMLButtonElement;
|
|
65
|
-
btnBranchBranchEl?: HTMLButtonElement;
|
|
66
|
-
btnCloneBranchEl?: HTMLButtonElement;
|
|
101
|
+
btnBranchMenuEl?: HTMLButtonElement;
|
|
102
|
+
commandMenuPopupEl?: HTMLDivElement;
|
|
67
103
|
}
|
|
68
104
|
|
|
69
105
|
/**
|
|
@@ -75,9 +111,43 @@ export class FileExplorerComponentInstance
|
|
|
75
111
|
|
|
76
112
|
protected lc: string = `[${FileExplorerComponentInstance.name}]`;
|
|
77
113
|
|
|
114
|
+
private galaxyName: string = '';
|
|
78
115
|
private branchName: string = 'main';
|
|
79
|
-
private
|
|
80
|
-
private
|
|
116
|
+
private viewMode: 'single' | 'composite' = 'single';
|
|
117
|
+
private isSingleExpanded: boolean = true;
|
|
118
|
+
private isCompositeExpanded: boolean = true;
|
|
119
|
+
private isDropdownOpen: boolean = false;
|
|
120
|
+
private selectedFilePath: string = '';
|
|
121
|
+
private branchFileStatusMap: Map<string, Map<string, 'untracked' | 'modified'>> = new Map();
|
|
122
|
+
private branchColorCache: Map<string, GibColorInfo> = new Map();
|
|
123
|
+
private loadTreeSeq: number = 0;
|
|
124
|
+
private activeCommandMenuAnchor: HTMLElement | null = null;
|
|
125
|
+
|
|
126
|
+
private getBranchKey(galaxyName: string = this.galaxyName, branchName: string = this.branchName): string {
|
|
127
|
+
return `${galaxyName || 'code-1'}~${branchName || 'main'}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public getFileStatusMap(galaxyName: string = this.galaxyName, branchName: string = this.branchName): Map<string, 'untracked' | 'modified'> {
|
|
131
|
+
const key = this.getBranchKey(galaxyName, branchName);
|
|
132
|
+
let map = this.branchFileStatusMap.get(key);
|
|
133
|
+
if (!map) {
|
|
134
|
+
map = new Map();
|
|
135
|
+
this.branchFileStatusMap.set(key, map);
|
|
136
|
+
}
|
|
137
|
+
return map;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async resolveGalaxyIbGib(galaxyName: string, space: any): Promise<B2tFSGalaxyIbGib_V1 | undefined> {
|
|
141
|
+
const router = getSpaceGibRouterSvc();
|
|
142
|
+
for (const manifest of router.cosmosManifestCache?.values() || []) {
|
|
143
|
+
const entry = manifest.data?.galaxies?.[galaxyName];
|
|
144
|
+
if (entry?.galaxyAddr) {
|
|
145
|
+
const gib = await getIbGibFromSpace<B2tFSGalaxyIbGib_V1>({ addr: entry.galaxyAddr, space });
|
|
146
|
+
if (gib) return gib;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
81
151
|
|
|
82
152
|
get el(): FileExplorerElements {
|
|
83
153
|
if (!this.elements) {
|
|
@@ -86,25 +156,83 @@ export class FileExplorerComponentInstance
|
|
|
86
156
|
return this.elements;
|
|
87
157
|
}
|
|
88
158
|
|
|
159
|
+
private onWindowClick = (e: MouseEvent) => {
|
|
160
|
+
const path = e.composedPath();
|
|
161
|
+
if (this.isDropdownOpen) {
|
|
162
|
+
const insideSelector = path.some(
|
|
163
|
+
(target: any) => target.id === 'branch-selector-container' || (target.classList && target.classList.contains('branch-dropdown-menu'))
|
|
164
|
+
);
|
|
165
|
+
if (!insideSelector) {
|
|
166
|
+
this.closeBranchDropdown();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (this.el?.commandMenuPopupEl && this.el.commandMenuPopupEl.style.display !== 'none') {
|
|
171
|
+
const insideMenu = path.some(
|
|
172
|
+
(target: any) => target.id === 'command-menu-popup' || (target.classList && target.classList.contains('branch-menu-btn'))
|
|
173
|
+
);
|
|
174
|
+
if (!insideMenu) {
|
|
175
|
+
this.closeCommandMenu();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
private onWindowKeyDown = (e: KeyboardEvent) => {
|
|
181
|
+
if (e.key === 'Escape') {
|
|
182
|
+
if (this.isDropdownOpen) {
|
|
183
|
+
this.closeBranchDropdown();
|
|
184
|
+
}
|
|
185
|
+
this.closeCommandMenu();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
private onCodeChanged = (e: any) => {
|
|
190
|
+
const { filepath, branch } = e.detail || {};
|
|
191
|
+
if (filepath) {
|
|
192
|
+
const statusMap = this.getFileStatusMap(this.galaxyName, branch || this.branchName);
|
|
193
|
+
if (!statusMap.has(filepath)) {
|
|
194
|
+
this.setFileStatus(filepath, 'modified', branch || this.branchName);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
89
199
|
override async created(): Promise<void> {
|
|
90
200
|
const lc = `${this.lc}[${this.created.name}]`;
|
|
91
201
|
try {
|
|
92
202
|
const shadow = this.shadowRoot!;
|
|
93
203
|
this.elements = {
|
|
94
204
|
rootEl: shadow.getElementById('file-explorer-root') as HTMLDivElement,
|
|
205
|
+
modeToolbarEl: shadow.getElementById('explorer-mode-toolbar') as HTMLDivElement,
|
|
206
|
+
btnModeSingleEl: shadow.getElementById('btn-mode-single') as HTMLButtonElement,
|
|
207
|
+
btnModeCompositeEl: shadow.getElementById('btn-mode-composite') as HTMLButtonElement,
|
|
208
|
+
btnRefreshExplorerEl: shadow.getElementById('btn-refresh-explorer') as HTMLButtonElement,
|
|
209
|
+
singleBranchViewEl: shadow.getElementById('single-branch-view') as HTMLDivElement,
|
|
210
|
+
compositeTreeContainerEl: shadow.getElementById('composite-tree-container') as HTMLDivElement,
|
|
95
211
|
branchHeaderEl: shadow.getElementById('branch-header') as HTMLDivElement,
|
|
96
212
|
btnBranchToggleEl: shadow.getElementById('btn-branch-toggle') as HTMLButtonElement,
|
|
213
|
+
btnBranchSelectEl: shadow.getElementById('btn-branch-select') as HTMLButtonElement,
|
|
97
214
|
branchTitleEl: shadow.getElementById('branch-title') as HTMLSpanElement,
|
|
215
|
+
branchDirtyBadgeEl: shadow.getElementById('branch-dirty-badge') as HTMLSpanElement,
|
|
216
|
+
branchDropdownMenuEl: shadow.getElementById('branch-dropdown-menu') as HTMLDivElement,
|
|
98
217
|
fileTreeContainerEl: shadow.getElementById('file-tree-container') as HTMLDivElement,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
btnCollapseAllEl: shadow.getElementById('btn-collapse-all') as HTMLButtonElement,
|
|
102
|
-
btnBranchBranchEl: shadow.getElementById('btn-branch-branch') as HTMLButtonElement,
|
|
103
|
-
btnCloneBranchEl: shadow.getElementById('btn-clone-branch') as HTMLButtonElement,
|
|
218
|
+
btnBranchMenuEl: shadow.getElementById('btn-branch-menu') as HTMLButtonElement,
|
|
219
|
+
commandMenuPopupEl: shadow.getElementById('command-menu-popup') as HTMLDivElement,
|
|
104
220
|
contentEl: shadow.getElementById('file-explorer-root') as HTMLDivElement,
|
|
105
221
|
};
|
|
106
222
|
|
|
223
|
+
const isExpanded = this.viewMode === 'single' ? this.isSingleExpanded : this.isCompositeExpanded;
|
|
224
|
+
this.toggleAttribute('collapsed', !isExpanded);
|
|
107
225
|
this.bindHandlers();
|
|
226
|
+
if (this.galaxyName) {
|
|
227
|
+
this.updateBranchColorStyling();
|
|
228
|
+
}
|
|
229
|
+
if (this.el?.fileTreeContainerEl) {
|
|
230
|
+
this.el.fileTreeContainerEl.innerHTML = `<div class="empty-tree-msg">No galaxy selected</div>`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
window.addEventListener('click', this.onWindowClick);
|
|
234
|
+
window.addEventListener('keydown', this.onWindowKeyDown);
|
|
235
|
+
window.addEventListener('space-gib-code-changed', this.onCodeChanged);
|
|
108
236
|
} catch (error) {
|
|
109
237
|
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
110
238
|
}
|
|
@@ -113,73 +241,39 @@ export class FileExplorerComponentInstance
|
|
|
113
241
|
private bindHandlers(): void {
|
|
114
242
|
const shadow = this.shadowRoot!;
|
|
115
243
|
|
|
244
|
+
// View Mode Switcher
|
|
245
|
+
this.el.btnModeSingleEl?.addEventListener('click', () => {
|
|
246
|
+
this.setViewMode('single');
|
|
247
|
+
});
|
|
248
|
+
this.el.btnModeCompositeEl?.addEventListener('click', () => {
|
|
249
|
+
this.setViewMode('composite');
|
|
250
|
+
});
|
|
251
|
+
this.el.btnRefreshExplorerEl?.addEventListener('click', () => {
|
|
252
|
+
if (this.viewMode === 'single') {
|
|
253
|
+
this.loadBranchTree(this.galaxyName, this.branchName);
|
|
254
|
+
} else {
|
|
255
|
+
this.loadCompositeBranchTree();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
116
259
|
// Branch header toggle
|
|
117
260
|
this.el.branchHeaderEl.addEventListener('click', (e) => {
|
|
118
261
|
const target = e.target as HTMLElement;
|
|
119
262
|
if (target.closest('.action-btn')) return; // ignore action buttons
|
|
263
|
+
if (target.closest('#branch-selector-container')) return; // ignore branch selector
|
|
120
264
|
this.toggleBranchAccordion();
|
|
121
265
|
});
|
|
122
266
|
|
|
123
|
-
//
|
|
124
|
-
this.el.
|
|
267
|
+
// Branch dropdown toggle
|
|
268
|
+
this.el.btnBranchSelectEl.addEventListener('click', (e) => {
|
|
125
269
|
e.stopPropagation();
|
|
126
|
-
this.
|
|
127
|
-
detail: { branch: this.branchName },
|
|
128
|
-
bubbles: true,
|
|
129
|
-
composed: true,
|
|
130
|
-
}));
|
|
270
|
+
this.toggleBranchDropdown();
|
|
131
271
|
});
|
|
132
272
|
|
|
133
|
-
|
|
273
|
+
// Single Branch Command Menu (⋮)
|
|
274
|
+
this.el.btnBranchMenuEl?.addEventListener('click', (e) => {
|
|
134
275
|
e.stopPropagation();
|
|
135
|
-
this.
|
|
136
|
-
detail: { branch: this.branchName },
|
|
137
|
-
bubbles: true,
|
|
138
|
-
composed: true,
|
|
139
|
-
}));
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
this.el.btnCollapseAllEl.addEventListener('click', (e) => {
|
|
143
|
-
e.stopPropagation();
|
|
144
|
-
this.collapseAllFolders();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Worktree Branch action
|
|
148
|
-
this.el.btnBranchBranchEl?.addEventListener('click', async (e) => {
|
|
149
|
-
e.stopPropagation();
|
|
150
|
-
const newBranchName = await promptForText({
|
|
151
|
-
title: 'Branch from Worktree',
|
|
152
|
-
msg: `Create new branch diverging from [${this.branchName}]:`,
|
|
153
|
-
defaultValue: `feature/branch-from-${this.branchName}`,
|
|
154
|
-
cancelable: true,
|
|
155
|
-
});
|
|
156
|
-
if (newBranchName) {
|
|
157
|
-
console.log(`${this.lc} New branch created from worktree [${this.branchName}]: ${newBranchName}`);
|
|
158
|
-
this.setBranch(newBranchName);
|
|
159
|
-
this.dispatchEvent(new CustomEvent('space-gib-branch-requested', {
|
|
160
|
-
detail: {
|
|
161
|
-
galaxyName: 'Code Galaxy',
|
|
162
|
-
sourceBranch: this.branchName,
|
|
163
|
-
newBranchName,
|
|
164
|
-
scope: 'branch',
|
|
165
|
-
},
|
|
166
|
-
bubbles: true,
|
|
167
|
-
composed: true,
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Branch Clone action
|
|
173
|
-
this.el.btnCloneBranchEl?.addEventListener('click', (e) => {
|
|
174
|
-
e.stopPropagation();
|
|
175
|
-
this.dispatchEvent(new CustomEvent('space-gib-clone-requested', {
|
|
176
|
-
detail: {
|
|
177
|
-
galaxyName: 'Code Galaxy',
|
|
178
|
-
branchName: this.branchName,
|
|
179
|
-
},
|
|
180
|
-
bubbles: true,
|
|
181
|
-
composed: true,
|
|
182
|
-
}));
|
|
276
|
+
this.showCommandMenu(this.el.btnBranchMenuEl!, this.getBranchCommandMenuItems(this.branchName));
|
|
183
277
|
});
|
|
184
278
|
|
|
185
279
|
// Single Item Branch actions
|
|
@@ -190,31 +284,41 @@ export class FileExplorerComponentInstance
|
|
|
190
284
|
const path = treeItem?.getAttribute('data-path') || treeItem?.getAttribute('data-folder') || '';
|
|
191
285
|
const isDir = treeItem?.classList.contains('folder') || false;
|
|
192
286
|
const filename = path.split('/').pop() || path;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
title: `Branch from ${isDir ? 'Directory' : 'File'} Item`,
|
|
197
|
-
msg: `Create new branch isolating and diverging from item [${path}]:`,
|
|
198
|
-
defaultValue: defaultBranch,
|
|
199
|
-
cancelable: true,
|
|
200
|
-
});
|
|
287
|
+
let candidateName = `feature-branch-${filename.replace(/[^a-zA-Z0-9_.-]/g, '-')}`;
|
|
288
|
+
let promptMsg = `Create new branch isolating and diverging from item [${path}]:`;
|
|
289
|
+
let cleanBranchName = '';
|
|
201
290
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
291
|
+
while (true) {
|
|
292
|
+
const newBranchName = await promptForText({
|
|
293
|
+
title: `Branch from ${isDir ? 'Directory' : 'File'} Item`,
|
|
294
|
+
msg: promptMsg,
|
|
295
|
+
defaultValue: candidateName,
|
|
296
|
+
cancelable: true,
|
|
297
|
+
});
|
|
298
|
+
if (!newBranchName || !newBranchName.trim()) return;
|
|
299
|
+
cleanBranchName = newBranchName.trim();
|
|
300
|
+
const validation = validateBranchName(cleanBranchName);
|
|
301
|
+
if (!validation.isValid) {
|
|
302
|
+
candidateName = cleanBranchName;
|
|
303
|
+
promptMsg = `Invalid branch name: ${validation.error}\n\nPlease enter a valid branch name:`;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
217
307
|
}
|
|
308
|
+
|
|
309
|
+
console.log(`${this.lc} New branch requested from item [${path}]: ${cleanBranchName}`);
|
|
310
|
+
this.dispatchEvent(new CustomEvent('space-gib-branch-requested', {
|
|
311
|
+
detail: {
|
|
312
|
+
galaxyName: this.galaxyName,
|
|
313
|
+
sourceBranch: this.branchName,
|
|
314
|
+
itemPath: path,
|
|
315
|
+
isDir,
|
|
316
|
+
newBranchName: cleanBranchName,
|
|
317
|
+
scope: 'item',
|
|
318
|
+
},
|
|
319
|
+
bubbles: true,
|
|
320
|
+
composed: true,
|
|
321
|
+
}));
|
|
218
322
|
});
|
|
219
323
|
});
|
|
220
324
|
|
|
@@ -281,9 +385,161 @@ export class FileExplorerComponentInstance
|
|
|
281
385
|
});
|
|
282
386
|
}
|
|
283
387
|
|
|
388
|
+
public toggleBranchDropdown(): void {
|
|
389
|
+
if (this.isDropdownOpen) {
|
|
390
|
+
this.closeBranchDropdown();
|
|
391
|
+
} else {
|
|
392
|
+
this.openBranchDropdown();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
public async openBranchDropdown(): Promise<void> {
|
|
397
|
+
if (!this.el?.branchDropdownMenuEl) return;
|
|
398
|
+
this.isDropdownOpen = true;
|
|
399
|
+
this.el.branchDropdownMenuEl.style.display = 'flex';
|
|
400
|
+
await this.populateBranchDropdown();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
public closeBranchDropdown(): void {
|
|
404
|
+
if (!this.el?.branchDropdownMenuEl) return;
|
|
405
|
+
this.isDropdownOpen = false;
|
|
406
|
+
this.el.branchDropdownMenuEl.style.display = 'none';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
public async populateBranchDropdown(): Promise<void> {
|
|
410
|
+
const menu = this.el?.branchDropdownMenuEl;
|
|
411
|
+
if (!menu) return;
|
|
412
|
+
menu.innerHTML = '<div class="branch-dropdown-item" style="color: #64748b; cursor: default;">Loading branches...</div>';
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const { metaspace, space } = await getPlaygroundMetaspaceAndSpace();
|
|
416
|
+
const galaxyIbGib = await this.resolveGalaxyIbGib(this.galaxyName, space);
|
|
417
|
+
if (!galaxyIbGib) {
|
|
418
|
+
menu.innerHTML = '<div class="branch-dropdown-item" style="color: #ef4444; cursor: default;">No galaxy found</div>';
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const branchInfos = await getAllBranchInfosFromGalaxy({ galaxyIbGib, metaspace }).catch(() => []);
|
|
423
|
+
if (branchInfos.length === 0) {
|
|
424
|
+
menu.innerHTML = '<div class="branch-dropdown-item" style="color: #64748b; cursor: default;">No branches found</div>';
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Sort: 'main' first, then alphabetical
|
|
429
|
+
branchInfos.sort((a, b) => {
|
|
430
|
+
if (a.branchName === 'main') return -1;
|
|
431
|
+
if (b.branchName === 'main') return 1;
|
|
432
|
+
return a.branchName.localeCompare(b.branchName);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
menu.innerHTML = '';
|
|
436
|
+
for (const info of branchInfos) {
|
|
437
|
+
const itemEl = document.createElement('div');
|
|
438
|
+
const isActive = info.branchName === this.branchName;
|
|
439
|
+
itemEl.className = `branch-dropdown-item ${isActive ? 'active' : ''}`;
|
|
440
|
+
itemEl.setAttribute('data-branch', info.branchName);
|
|
441
|
+
|
|
442
|
+
// Dot color derived from branch TJP
|
|
443
|
+
const colorInfo = await this.getBranchColor(info.branchName);
|
|
444
|
+
const dotEl = document.createElement('span');
|
|
445
|
+
dotEl.className = 'branch-color-dot';
|
|
446
|
+
dotEl.style.backgroundColor = colorInfo.punctiliarColor;
|
|
447
|
+
|
|
448
|
+
const nameEl = document.createElement('span');
|
|
449
|
+
nameEl.className = 'branch-item-name';
|
|
450
|
+
nameEl.textContent = info.branchName;
|
|
451
|
+
|
|
452
|
+
itemEl.appendChild(dotEl);
|
|
453
|
+
itemEl.appendChild(nameEl);
|
|
454
|
+
|
|
455
|
+
if (isActive) {
|
|
456
|
+
const checkEl = document.createElement('span');
|
|
457
|
+
checkEl.className = 'branch-dropdown-check';
|
|
458
|
+
checkEl.textContent = '✓';
|
|
459
|
+
itemEl.appendChild(checkEl);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
itemEl.addEventListener('click', (e) => {
|
|
463
|
+
e.stopPropagation();
|
|
464
|
+
this.closeBranchDropdown();
|
|
465
|
+
if (info.branchName !== this.branchName) {
|
|
466
|
+
this.dispatchEvent(new CustomEvent('space-gib-branch-switched', {
|
|
467
|
+
detail: {
|
|
468
|
+
galaxy: this.galaxyName,
|
|
469
|
+
branch: info.branchName,
|
|
470
|
+
},
|
|
471
|
+
bubbles: true,
|
|
472
|
+
composed: true,
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
menu.appendChild(itemEl);
|
|
478
|
+
}
|
|
479
|
+
} catch (err) {
|
|
480
|
+
menu.innerHTML = `<div class="branch-dropdown-item" style="color: #ef4444; cursor: default;">Error loading branches</div>`;
|
|
481
|
+
console.error(`${this.lc} Error populating branch dropdown: ${extractErrorMsg(err)}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
public clearBranchColorCache(): void {
|
|
486
|
+
this.branchColorCache.clear();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
public async getBranchColor(branchName: string, galaxyName: string = this.galaxyName): Promise<GibColorInfo> {
|
|
490
|
+
const cacheKey = `${galaxyName || 'unknown'}~${branchName}`;
|
|
491
|
+
let colorInfo = this.branchColorCache.get(cacheKey);
|
|
492
|
+
if (colorInfo) return colorInfo;
|
|
493
|
+
|
|
494
|
+
let branchAddrOrTjp: string | undefined;
|
|
495
|
+
try {
|
|
496
|
+
if (galaxyName) {
|
|
497
|
+
const { metaspace, space } = await getPlaygroundMetaspaceAndSpace();
|
|
498
|
+
const galaxyIbGib = await this.resolveGalaxyIbGib(galaxyName, space);
|
|
499
|
+
if (galaxyIbGib) {
|
|
500
|
+
const branchAndRoot = await getBranchAndRootItemInfosAndIbGibsAndSpaceFromGalaxyIbGib({
|
|
501
|
+
galaxyIbGib,
|
|
502
|
+
branchName,
|
|
503
|
+
metaspace,
|
|
504
|
+
}).catch(() => undefined);
|
|
505
|
+
if (branchAndRoot?.branchIbGib) {
|
|
506
|
+
branchAddrOrTjp = getTjpAddr({ ibGib: branchAndRoot.branchIbGib }) || branchAndRoot.branchInfo?.branchAddr;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
} catch (err) {
|
|
511
|
+
// ignore and fallback
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
colorInfo = getDeterministicColorInfo({
|
|
515
|
+
ibGibAddr: branchAddrOrTjp,
|
|
516
|
+
gib: branchAddrOrTjp ? undefined : branchName,
|
|
517
|
+
translucentAlpha: 18,
|
|
518
|
+
});
|
|
519
|
+
if (galaxyName && branchAddrOrTjp) {
|
|
520
|
+
this.branchColorCache.set(cacheKey, colorInfo);
|
|
521
|
+
}
|
|
522
|
+
return colorInfo;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
public async updateBranchColorStyling(): Promise<void> {
|
|
526
|
+
try {
|
|
527
|
+
if (!this.galaxyName) return;
|
|
528
|
+
const colorInfo = await this.getBranchColor(this.branchName, this.galaxyName);
|
|
529
|
+
if (colorInfo) {
|
|
530
|
+
this.style.setProperty('--branch-accent-color', colorInfo.punctiliarColor);
|
|
531
|
+
this.style.setProperty('--branch-accent-translucent', colorInfo.punctiliarColorTranslucent);
|
|
532
|
+
this.style.setProperty('--branch-accent-contrast', colorInfo.punctiliarColorContrast);
|
|
533
|
+
}
|
|
534
|
+
} catch (err) {
|
|
535
|
+
console.warn(`${this.lc} Error updating branch color styling: ${extractErrorMsg(err)}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
284
539
|
public toggleBranchAccordion(): void {
|
|
285
|
-
this.
|
|
286
|
-
|
|
540
|
+
this.isSingleExpanded = !this.isSingleExpanded;
|
|
541
|
+
this.toggleAttribute('collapsed', !this.isSingleExpanded);
|
|
542
|
+
if (this.isSingleExpanded) {
|
|
287
543
|
this.el.branchHeaderEl.classList.remove('collapsed');
|
|
288
544
|
this.el.branchHeaderEl.classList.add('expanded');
|
|
289
545
|
this.el.fileTreeContainerEl.classList.remove('collapsed');
|
|
@@ -292,6 +548,30 @@ export class FileExplorerComponentInstance
|
|
|
292
548
|
this.el.branchHeaderEl.classList.remove('expanded');
|
|
293
549
|
this.el.fileTreeContainerEl.classList.add('collapsed');
|
|
294
550
|
}
|
|
551
|
+
this.dispatchEvent(new CustomEvent('space-gib-accordion-toggled', {
|
|
552
|
+
detail: { isExpanded: this.isSingleExpanded },
|
|
553
|
+
bubbles: true,
|
|
554
|
+
composed: true,
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
public toggleCompositeRootAccordion(headerEl: HTMLElement, bodyEl: HTMLElement): void {
|
|
559
|
+
this.isCompositeExpanded = !this.isCompositeExpanded;
|
|
560
|
+
this.toggleAttribute('collapsed', !this.isCompositeExpanded);
|
|
561
|
+
if (this.isCompositeExpanded) {
|
|
562
|
+
headerEl.classList.remove('collapsed');
|
|
563
|
+
headerEl.classList.add('expanded');
|
|
564
|
+
bodyEl.style.display = 'flex';
|
|
565
|
+
} else {
|
|
566
|
+
headerEl.classList.add('collapsed');
|
|
567
|
+
headerEl.classList.remove('expanded');
|
|
568
|
+
bodyEl.style.display = 'none';
|
|
569
|
+
}
|
|
570
|
+
this.dispatchEvent(new CustomEvent('space-gib-accordion-toggled', {
|
|
571
|
+
detail: { isExpanded: this.isCompositeExpanded },
|
|
572
|
+
bubbles: true,
|
|
573
|
+
composed: true,
|
|
574
|
+
}));
|
|
295
575
|
}
|
|
296
576
|
|
|
297
577
|
public collapseAllFolders(): void {
|
|
@@ -300,7 +580,6 @@ export class FileExplorerComponentInstance
|
|
|
300
580
|
const folderName = folderEl.getAttribute('data-folder');
|
|
301
581
|
folderEl.classList.remove('expanded');
|
|
302
582
|
const chevron = folderEl.querySelector('.tree-chevron');
|
|
303
|
-
// if (chevron) chevron.textContent = '▶';
|
|
304
583
|
if (chevron) { chevron.textContent = '▼'; }
|
|
305
584
|
if (folderName) {
|
|
306
585
|
shadow.querySelectorAll(`.child-of-${folderName}`).forEach(child => child.classList.add('hidden'));
|
|
@@ -320,25 +599,812 @@ export class FileExplorerComponentInstance
|
|
|
320
599
|
});
|
|
321
600
|
}
|
|
322
601
|
|
|
323
|
-
public
|
|
602
|
+
public setGalaxyAndBranch(galaxyName: string, branchName: string = 'main', forceReload: boolean = false): void {
|
|
603
|
+
const changed = this.galaxyName !== galaxyName || this.branchName !== branchName;
|
|
604
|
+
this.galaxyName = galaxyName;
|
|
605
|
+
this.branchName = branchName;
|
|
606
|
+
if (this.el?.branchTitleEl) {
|
|
607
|
+
this.el.branchTitleEl.textContent = branchName;
|
|
608
|
+
}
|
|
609
|
+
this.updateBranchColorStyling();
|
|
610
|
+
this.updateDirtyBadges();
|
|
611
|
+
if (changed || forceReload) {
|
|
612
|
+
if (this.viewMode === 'composite') {
|
|
613
|
+
this.loadCompositeBranchTree();
|
|
614
|
+
} else {
|
|
615
|
+
this.loadBranchTree(galaxyName, branchName);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
public async loadBranchTree(galaxyName: string = this.galaxyName, branchName: string = this.branchName): Promise<void> {
|
|
621
|
+
const seq = ++this.loadTreeSeq;
|
|
622
|
+
this.galaxyName = galaxyName;
|
|
324
623
|
this.branchName = branchName;
|
|
325
624
|
if (this.el?.branchTitleEl) {
|
|
326
625
|
this.el.branchTitleEl.textContent = branchName;
|
|
327
626
|
}
|
|
627
|
+
|
|
628
|
+
const container = this.el?.fileTreeContainerEl;
|
|
629
|
+
if (!container) return;
|
|
630
|
+
|
|
631
|
+
if (!galaxyName) {
|
|
632
|
+
container.innerHTML = '<div class="empty-tree-msg">No galaxy selected</div>';
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const { metaspace, space } = await getPlaygroundMetaspaceAndSpace();
|
|
638
|
+
if (seq !== this.loadTreeSeq) return;
|
|
639
|
+
|
|
640
|
+
const galaxyIbGib = await this.resolveGalaxyIbGib(galaxyName, space);
|
|
641
|
+
if (seq !== this.loadTreeSeq) return;
|
|
642
|
+
|
|
643
|
+
if (!galaxyIbGib) {
|
|
644
|
+
container.innerHTML = `<div class="empty-tree-msg">Galaxy [${galaxyName}] not found in cosmos manifest</div>`;
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const branchAndRoot = await getBranchAndRootItemInfosAndIbGibsAndSpaceFromGalaxyIbGib({
|
|
649
|
+
galaxyIbGib,
|
|
650
|
+
branchName,
|
|
651
|
+
metaspace,
|
|
652
|
+
});
|
|
653
|
+
if (seq !== this.loadTreeSeq) return;
|
|
654
|
+
|
|
655
|
+
const branchSpace = branchAndRoot.branchSpace;
|
|
656
|
+
const rootItem = branchAndRoot.rootItemIbGib_latest || branchAndRoot.rootItemIbGib;
|
|
657
|
+
|
|
658
|
+
const children = await getFolderChildren({ branchSpace, folderItem: rootItem });
|
|
659
|
+
if (seq !== this.loadTreeSeq) return;
|
|
660
|
+
|
|
661
|
+
const committedNames = new Set(children.map(c => c.data.name));
|
|
662
|
+
|
|
663
|
+
// Merge untracked working-tree files not yet in CAS for this specific branch
|
|
664
|
+
const statusMap = this.getFileStatusMap(galaxyName, branchName);
|
|
665
|
+
const untrackedNames: string[] = [];
|
|
666
|
+
for (const [filePath, status] of statusMap) {
|
|
667
|
+
if (status === 'untracked' && !committedNames.has(filePath)) {
|
|
668
|
+
untrackedNames.push(filePath);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (children.length === 0 && untrackedNames.length === 0) {
|
|
673
|
+
const emptyMsg = document.createElement('div');
|
|
674
|
+
emptyMsg.className = 'empty-tree-msg';
|
|
675
|
+
emptyMsg.innerHTML = `No files in worktree yet.<br/>Click <span class="highlight">📄+</span> to create a file.`;
|
|
676
|
+
if (seq !== this.loadTreeSeq) return;
|
|
677
|
+
container.replaceChildren(emptyMsg);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const createRow = (name: string, isDir: boolean): HTMLElement => {
|
|
682
|
+
const itemEl = document.createElement('div');
|
|
683
|
+
itemEl.className = `tree-item ${isDir ? 'folder' : 'file'} ${name === this.selectedFilePath ? 'selected' : ''}`;
|
|
684
|
+
itemEl.setAttribute(isDir ? 'data-folder' : 'data-path', name);
|
|
685
|
+
itemEl.setAttribute('data-name', name);
|
|
686
|
+
itemEl.tabIndex = 0;
|
|
687
|
+
|
|
688
|
+
const icon = isDir ? '📁' : (name.endsWith('.md') ? '📝' : (name.endsWith('.json') ? '⚙️' : '📄'));
|
|
689
|
+
const status = !isDir ? statusMap.get(name) : undefined;
|
|
690
|
+
const statusClass = status ? (status === 'untracked' ? 'vcs-untracked' : 'vcs-modified') : '';
|
|
691
|
+
const badgeHtml = status ? `<span class="vcs-status-badge ${status}" title="${status === 'untracked' ? 'Untracked' : 'Modified'}">${status === 'untracked' ? 'U' : 'M'}</span>` : '';
|
|
692
|
+
|
|
693
|
+
itemEl.innerHTML = `
|
|
694
|
+
<span class="tree-indent tree-indent-2"></span>
|
|
695
|
+
<span class="tree-icon">${icon}</span>
|
|
696
|
+
<span class="tree-label ${statusClass}">${name}</span>
|
|
697
|
+
${badgeHtml}
|
|
698
|
+
`;
|
|
699
|
+
|
|
700
|
+
if (!isDir) {
|
|
701
|
+
itemEl.addEventListener('click', () => {
|
|
702
|
+
this.selectFile(name);
|
|
703
|
+
this.dispatchEvent(new CustomEvent('space-gib-file-selected', {
|
|
704
|
+
detail: {
|
|
705
|
+
galaxy: this.galaxyName,
|
|
706
|
+
branch: this.branchName,
|
|
707
|
+
path: name,
|
|
708
|
+
name: name,
|
|
709
|
+
},
|
|
710
|
+
bubbles: true,
|
|
711
|
+
composed: true,
|
|
712
|
+
}));
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return itemEl;
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
interface ExplorerItem {
|
|
720
|
+
name: string;
|
|
721
|
+
isDir: boolean;
|
|
722
|
+
}
|
|
723
|
+
const allItems: ExplorerItem[] = [
|
|
724
|
+
...children.map(c => ({ name: c.data.name, isDir: c.data.fsType === 'folder' })),
|
|
725
|
+
...untrackedNames.map(name => ({ name, isDir: false })),
|
|
726
|
+
];
|
|
727
|
+
|
|
728
|
+
if (this.selectedFilePath && !allItems.some(i => i.name === this.selectedFilePath)) {
|
|
729
|
+
this.selectedFilePath = '';
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Sort: folders first, then alphabetically by name (case-insensitive, natural numeric sort)
|
|
733
|
+
allItems.sort((a, b) => {
|
|
734
|
+
if (a.isDir !== b.isDir) {
|
|
735
|
+
return a.isDir ? -1 : 1;
|
|
736
|
+
}
|
|
737
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: 'base', numeric: true });
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const fragment = document.createDocumentFragment();
|
|
741
|
+
for (const item of allItems) {
|
|
742
|
+
fragment.appendChild(createRow(item.name, item.isDir));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (seq !== this.loadTreeSeq) return;
|
|
746
|
+
container.replaceChildren(fragment);
|
|
747
|
+
this.updateDirtyBadges();
|
|
748
|
+
} catch (err) {
|
|
749
|
+
console.error(`${this.lc} Failed to load branch tree: ${extractErrorMsg(err)}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
public isUntracked(filepath: string, branchName: string = this.branchName, galaxyName: string = this.galaxyName): boolean {
|
|
754
|
+
return this.getFileStatusMap(galaxyName, branchName).get(filepath) === 'untracked';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
public removeFile(filepath: string, branchName: string = this.branchName, galaxyName: string = this.galaxyName): void {
|
|
758
|
+
this.getFileStatusMap(galaxyName, branchName).delete(filepath);
|
|
759
|
+
if (this.selectedFilePath === filepath && branchName === this.branchName) {
|
|
760
|
+
this.selectedFilePath = '';
|
|
761
|
+
}
|
|
762
|
+
if (branchName === this.branchName && galaxyName === this.galaxyName) {
|
|
763
|
+
this.loadBranchTree(this.galaxyName, this.branchName);
|
|
764
|
+
}
|
|
765
|
+
this.updateDirtyBadges();
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
public setFileStatus(filepath: string, status?: 'untracked' | 'modified', branchName: string = this.branchName, galaxyName: string = this.galaxyName): void {
|
|
769
|
+
const statusMap = this.getFileStatusMap(galaxyName, branchName);
|
|
770
|
+
if (status) {
|
|
771
|
+
statusMap.set(filepath, status);
|
|
772
|
+
} else {
|
|
773
|
+
statusMap.delete(filepath);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
this.updateDirtyBadges();
|
|
777
|
+
|
|
778
|
+
// Only update DOM badges if this is the currently viewed branch
|
|
779
|
+
if (branchName !== this.branchName || galaxyName !== this.galaxyName) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const shadow = this.shadowRoot;
|
|
784
|
+
if (!shadow) return;
|
|
785
|
+
const fileEl = shadow.querySelector(`.tree-item.file[data-path="${filepath}"]`);
|
|
786
|
+
if (!fileEl) return;
|
|
787
|
+
const labelEl = fileEl.querySelector('.tree-label');
|
|
788
|
+
let badgeEl = fileEl.querySelector('.vcs-status-badge');
|
|
789
|
+
if (!status) {
|
|
790
|
+
labelEl?.classList.remove('vcs-untracked', 'vcs-modified');
|
|
791
|
+
badgeEl?.remove();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
labelEl?.classList.remove('vcs-untracked', 'vcs-modified');
|
|
795
|
+
labelEl?.classList.add(status === 'untracked' ? 'vcs-untracked' : 'vcs-modified');
|
|
796
|
+
if (!badgeEl) {
|
|
797
|
+
badgeEl = document.createElement('span');
|
|
798
|
+
fileEl.appendChild(badgeEl);
|
|
799
|
+
}
|
|
800
|
+
badgeEl.className = `vcs-status-badge ${status}`;
|
|
801
|
+
badgeEl.textContent = status === 'untracked' ? 'U' : 'M';
|
|
802
|
+
badgeEl.setAttribute('title', status === 'untracked' ? 'Untracked' : 'Modified');
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
public clearAllStatusBadges(branchName: string = this.branchName, galaxyName: string = this.galaxyName): void {
|
|
806
|
+
this.getFileStatusMap(galaxyName, branchName).clear();
|
|
807
|
+
this.updateDirtyBadges();
|
|
808
|
+
if (branchName === this.branchName && galaxyName === this.galaxyName) {
|
|
809
|
+
this.loadBranchTree(this.galaxyName, this.branchName);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
public setBranch(branchName: string): void {
|
|
814
|
+
this.setGalaxyAndBranch(this.galaxyName, branchName);
|
|
328
815
|
}
|
|
329
816
|
|
|
330
817
|
public getBranch(): string {
|
|
331
818
|
return this.branchName;
|
|
332
819
|
}
|
|
333
820
|
|
|
821
|
+
public getBranchName(): string {
|
|
822
|
+
return this.branchName;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
public getGalaxyName(): string {
|
|
826
|
+
return this.galaxyName;
|
|
827
|
+
}
|
|
828
|
+
|
|
334
829
|
public getSelectedFilePath(): string {
|
|
335
830
|
return this.selectedFilePath;
|
|
336
831
|
}
|
|
337
832
|
|
|
833
|
+
public setViewMode(mode: 'single' | 'composite'): void {
|
|
834
|
+
this.viewMode = mode;
|
|
835
|
+
this.closeCommandMenu();
|
|
836
|
+
if (this.el.btnModeSingleEl) {
|
|
837
|
+
this.el.btnModeSingleEl.classList.toggle('active', mode === 'single');
|
|
838
|
+
}
|
|
839
|
+
if (this.el.btnModeCompositeEl) {
|
|
840
|
+
this.el.btnModeCompositeEl.classList.toggle('active', mode === 'composite');
|
|
841
|
+
}
|
|
842
|
+
if (this.el.singleBranchViewEl) {
|
|
843
|
+
this.el.singleBranchViewEl.style.display = mode === 'single' ? 'flex' : 'none';
|
|
844
|
+
this.el.singleBranchViewEl.style.flexDirection = 'column';
|
|
845
|
+
}
|
|
846
|
+
if (this.el.compositeTreeContainerEl) {
|
|
847
|
+
this.el.compositeTreeContainerEl.style.display = mode === 'composite' ? 'flex' : 'none';
|
|
848
|
+
this.el.compositeTreeContainerEl.style.flexDirection = 'column';
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const isExpanded = mode === 'single' ? this.isSingleExpanded : this.isCompositeExpanded;
|
|
852
|
+
this.toggleAttribute('collapsed', !isExpanded);
|
|
853
|
+
|
|
854
|
+
this.dispatchEvent(new CustomEvent('space-gib-view-mode-changed', {
|
|
855
|
+
detail: { mode },
|
|
856
|
+
bubbles: true,
|
|
857
|
+
composed: true,
|
|
858
|
+
}));
|
|
859
|
+
|
|
860
|
+
this.dispatchEvent(new CustomEvent('space-gib-accordion-toggled', {
|
|
861
|
+
detail: { isExpanded },
|
|
862
|
+
bubbles: true,
|
|
863
|
+
composed: true,
|
|
864
|
+
}));
|
|
865
|
+
|
|
866
|
+
if (mode === 'composite') {
|
|
867
|
+
this.loadCompositeBranchTree();
|
|
868
|
+
} else {
|
|
869
|
+
this.loadBranchTree(this.galaxyName, this.branchName);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
public getViewMode(): 'single' | 'composite' {
|
|
874
|
+
return this.viewMode;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
public getBranchCommandMenuItems(branchName: string): CommandMenuItem[] {
|
|
878
|
+
const items: CommandMenuItem[] = [
|
|
879
|
+
{
|
|
880
|
+
id: 'new-sub-branch',
|
|
881
|
+
icon: '🌿+',
|
|
882
|
+
label: `New Sub-Branch...`,
|
|
883
|
+
action: () => this.handleNewSubBranch(branchName),
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
id: 'new-file',
|
|
887
|
+
icon: '📄+',
|
|
888
|
+
label: `New File...`,
|
|
889
|
+
action: () => this.handleNewFile(branchName),
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
id: 'new-folder',
|
|
893
|
+
icon: '📁+',
|
|
894
|
+
label: `New Folder...`,
|
|
895
|
+
action: () => this.handleNewFolder(branchName),
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
id: 'clone-branch',
|
|
899
|
+
icon: '⤓',
|
|
900
|
+
label: `Clone / Export Worktree...`,
|
|
901
|
+
action: () => this.handleCloneBranch(branchName),
|
|
902
|
+
},
|
|
903
|
+
];
|
|
904
|
+
|
|
905
|
+
if (this.viewMode === 'single' || branchName === this.branchName) {
|
|
906
|
+
items.push({
|
|
907
|
+
id: 'collapse-all',
|
|
908
|
+
icon: '⇱',
|
|
909
|
+
label: 'Collapse Folders',
|
|
910
|
+
action: () => this.collapseAllFolders(),
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return items;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
public showCommandMenu(anchorEl: HTMLElement, items: CommandMenuItem[]): void {
|
|
918
|
+
const menuEl = this.el?.commandMenuPopupEl;
|
|
919
|
+
if (!menuEl) return;
|
|
920
|
+
|
|
921
|
+
if (this.activeCommandMenuAnchor === anchorEl && menuEl.style.display !== 'none') {
|
|
922
|
+
this.closeCommandMenu();
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
this.closeCommandMenu();
|
|
927
|
+
this.activeCommandMenuAnchor = anchorEl;
|
|
928
|
+
anchorEl.classList.add('active');
|
|
929
|
+
menuEl.innerHTML = '';
|
|
930
|
+
|
|
931
|
+
for (const item of items) {
|
|
932
|
+
const row = document.createElement('div');
|
|
933
|
+
row.className = `command-menu-item ${item.danger ? 'danger' : ''}`;
|
|
934
|
+
row.innerHTML = `
|
|
935
|
+
<span class="cmd-icon">${item.icon}</span>
|
|
936
|
+
<span class="cmd-label">${item.label}</span>
|
|
937
|
+
`;
|
|
938
|
+
row.addEventListener('click', async (e) => {
|
|
939
|
+
e.stopPropagation();
|
|
940
|
+
this.closeCommandMenu();
|
|
941
|
+
await item.action();
|
|
942
|
+
});
|
|
943
|
+
menuEl.appendChild(row);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Calculate absolute position inside file-explorer-root
|
|
947
|
+
const rootRect = this.el.rootEl.getBoundingClientRect();
|
|
948
|
+
const anchorRect = anchorEl.getBoundingClientRect();
|
|
949
|
+
|
|
950
|
+
const top = anchorRect.bottom - rootRect.top + 4;
|
|
951
|
+
const right = rootRect.right - anchorRect.right;
|
|
952
|
+
|
|
953
|
+
menuEl.style.top = `${Math.max(0, top)}px`;
|
|
954
|
+
menuEl.style.right = `${Math.max(4, right)}px`;
|
|
955
|
+
menuEl.style.left = 'auto';
|
|
956
|
+
menuEl.style.display = 'flex';
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
public closeCommandMenu(): void {
|
|
960
|
+
if (!this.el?.commandMenuPopupEl) return;
|
|
961
|
+
this.el.commandMenuPopupEl.style.display = 'none';
|
|
962
|
+
if (this.activeCommandMenuAnchor) {
|
|
963
|
+
this.activeCommandMenuAnchor.classList.remove('active');
|
|
964
|
+
this.activeCommandMenuAnchor = null;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
public async handleNewSubBranch(sourceBranchName: string): Promise<void> {
|
|
969
|
+
let candidateName = `feature-from-${sourceBranchName}`;
|
|
970
|
+
let promptMsg = `Create new branch diverging from [${sourceBranchName}]:`;
|
|
971
|
+
let cleanBranchName = '';
|
|
972
|
+
|
|
973
|
+
while (true) {
|
|
974
|
+
const newBranchName = await promptForText({
|
|
975
|
+
title: 'Branch from Worktree',
|
|
976
|
+
msg: promptMsg,
|
|
977
|
+
defaultValue: candidateName,
|
|
978
|
+
cancelable: true,
|
|
979
|
+
});
|
|
980
|
+
if (!newBranchName || !newBranchName.trim()) return;
|
|
981
|
+
cleanBranchName = newBranchName.trim();
|
|
982
|
+
const validation = validateBranchName(cleanBranchName);
|
|
983
|
+
if (!validation.isValid) {
|
|
984
|
+
candidateName = cleanBranchName;
|
|
985
|
+
promptMsg = `Invalid branch name: ${validation.error}\n\nPlease enter a valid branch name:`;
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
console.log(`${this.lc} New branch requested from worktree [${sourceBranchName}]: ${cleanBranchName}`);
|
|
992
|
+
this.dispatchEvent(new CustomEvent('space-gib-branch-requested', {
|
|
993
|
+
detail: {
|
|
994
|
+
galaxyName: this.galaxyName,
|
|
995
|
+
sourceBranch: sourceBranchName,
|
|
996
|
+
newBranchName: cleanBranchName,
|
|
997
|
+
scope: 'branch',
|
|
998
|
+
},
|
|
999
|
+
bubbles: true,
|
|
1000
|
+
composed: true,
|
|
1001
|
+
}));
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
public async handleNewFile(targetBranchName: string): Promise<void> {
|
|
1005
|
+
const filename = await promptForText({
|
|
1006
|
+
title: 'New File',
|
|
1007
|
+
msg: `Create new file on branch [${targetBranchName}]:`,
|
|
1008
|
+
defaultValue: '',
|
|
1009
|
+
cancelable: true,
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
if (!filename || !filename.trim()) return;
|
|
1013
|
+
const cleanFilename = filename.trim();
|
|
1014
|
+
|
|
1015
|
+
try {
|
|
1016
|
+
const statusMap = this.getFileStatusMap(this.galaxyName, targetBranchName);
|
|
1017
|
+
if (statusMap.has(cleanFilename)) {
|
|
1018
|
+
alert(`A file named "${cleanFilename}" already exists in your working tree on branch [${targetBranchName}].`);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const { metaspace, space } = await getPlaygroundMetaspaceAndSpace();
|
|
1023
|
+
const galaxyIbGib = await this.resolveGalaxyIbGib(this.galaxyName, space);
|
|
1024
|
+
if (galaxyIbGib) {
|
|
1025
|
+
const branchAndRoot = await getBranchAndRootItemInfosAndIbGibsAndSpaceFromGalaxyIbGib({
|
|
1026
|
+
galaxyIbGib,
|
|
1027
|
+
branchName: targetBranchName,
|
|
1028
|
+
metaspace,
|
|
1029
|
+
});
|
|
1030
|
+
const existing = await getFileItemFromPath({
|
|
1031
|
+
branchSpace: branchAndRoot.branchSpace,
|
|
1032
|
+
rootItem: branchAndRoot.rootItemIbGib_latest || branchAndRoot.rootItemIbGib,
|
|
1033
|
+
filePath: cleanFilename,
|
|
1034
|
+
});
|
|
1035
|
+
if (existing) {
|
|
1036
|
+
alert(`A file or folder named "${cleanFilename}" already exists on branch [${targetBranchName}].`);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Add to working tree as untracked for this branch
|
|
1042
|
+
statusMap.set(cleanFilename, 'untracked');
|
|
1043
|
+
|
|
1044
|
+
if (this.viewMode === 'composite') {
|
|
1045
|
+
await this.loadCompositeBranchTree();
|
|
1046
|
+
} else {
|
|
1047
|
+
if (targetBranchName !== this.branchName) {
|
|
1048
|
+
this.setGalaxyAndBranch(this.galaxyName, targetBranchName);
|
|
1049
|
+
} else {
|
|
1050
|
+
await this.loadBranchTree(this.galaxyName, targetBranchName);
|
|
1051
|
+
}
|
|
1052
|
+
this.selectFile(cleanFilename);
|
|
1053
|
+
}
|
|
1054
|
+
this.updateDirtyBadges();
|
|
1055
|
+
|
|
1056
|
+
this.dispatchEvent(new CustomEvent('space-gib-file-created', {
|
|
1057
|
+
detail: {
|
|
1058
|
+
galaxy: this.galaxyName,
|
|
1059
|
+
branch: targetBranchName,
|
|
1060
|
+
path: cleanFilename,
|
|
1061
|
+
content: '',
|
|
1062
|
+
},
|
|
1063
|
+
bubbles: true,
|
|
1064
|
+
composed: true,
|
|
1065
|
+
}));
|
|
1066
|
+
|
|
1067
|
+
this.dispatchEvent(new CustomEvent('space-gib-file-selected', {
|
|
1068
|
+
detail: {
|
|
1069
|
+
galaxy: this.galaxyName,
|
|
1070
|
+
branch: targetBranchName,
|
|
1071
|
+
path: cleanFilename,
|
|
1072
|
+
name: cleanFilename,
|
|
1073
|
+
},
|
|
1074
|
+
bubbles: true,
|
|
1075
|
+
composed: true,
|
|
1076
|
+
}));
|
|
1077
|
+
} catch (err) {
|
|
1078
|
+
console.error(`${this.lc} Failed to create file: ${extractErrorMsg(err)}`);
|
|
1079
|
+
alert(`Failed to create file: ${extractErrorMsg(err)}`);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
public handleNewFolder(targetBranchName: string): void {
|
|
1084
|
+
this.dispatchEvent(new CustomEvent('space-gib-new-folder-requested', {
|
|
1085
|
+
detail: { branch: targetBranchName, galaxy: this.galaxyName },
|
|
1086
|
+
bubbles: true,
|
|
1087
|
+
composed: true,
|
|
1088
|
+
}));
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
public handleCloneBranch(targetBranchName: string): void {
|
|
1092
|
+
this.dispatchEvent(new CustomEvent('space-gib-clone-requested', {
|
|
1093
|
+
detail: {
|
|
1094
|
+
galaxyName: this.galaxyName,
|
|
1095
|
+
branchName: targetBranchName,
|
|
1096
|
+
},
|
|
1097
|
+
bubbles: true,
|
|
1098
|
+
composed: true,
|
|
1099
|
+
}));
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
public getDirtyCount(galaxyName: string = this.galaxyName, branchName: string = this.branchName): number {
|
|
1103
|
+
const map = this.getFileStatusMap(galaxyName, branchName);
|
|
1104
|
+
let count = 0;
|
|
1105
|
+
for (const [_, status] of map) {
|
|
1106
|
+
if (status === 'modified' || status === 'untracked') {
|
|
1107
|
+
count++;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return count;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
public updateDirtyBadges(): void {
|
|
1114
|
+
const shadow = this.shadowRoot;
|
|
1115
|
+
if (!shadow) return;
|
|
1116
|
+
|
|
1117
|
+
// 1. Single branch header dirty badge
|
|
1118
|
+
const singleCount = this.getDirtyCount(this.galaxyName, this.branchName);
|
|
1119
|
+
if (this.el.branchDirtyBadgeEl) {
|
|
1120
|
+
if (singleCount > 0) {
|
|
1121
|
+
this.el.branchDirtyBadgeEl.style.display = 'inline-flex';
|
|
1122
|
+
this.el.branchDirtyBadgeEl.textContent = `● ${singleCount}`;
|
|
1123
|
+
} else {
|
|
1124
|
+
this.el.branchDirtyBadgeEl.style.display = 'none';
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// 2. Composite tree branch nodes dirty badges
|
|
1129
|
+
const badges = shadow.querySelectorAll('.composite-tree-container .branch-dirty-badge');
|
|
1130
|
+
badges.forEach((badge) => {
|
|
1131
|
+
const bName = badge.getAttribute('data-branch');
|
|
1132
|
+
if (!bName) return;
|
|
1133
|
+
const count = this.getDirtyCount(this.galaxyName, bName);
|
|
1134
|
+
const htmlBadge = badge as HTMLElement;
|
|
1135
|
+
if (count > 0) {
|
|
1136
|
+
htmlBadge.style.display = 'inline-flex';
|
|
1137
|
+
htmlBadge.textContent = `● ${count}`;
|
|
1138
|
+
} else {
|
|
1139
|
+
htmlBadge.style.display = 'none';
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
public async loadCompositeBranchTree(): Promise<void> {
|
|
1145
|
+
const container = this.el?.compositeTreeContainerEl;
|
|
1146
|
+
if (!container) return;
|
|
1147
|
+
|
|
1148
|
+
if (!this.galaxyName) {
|
|
1149
|
+
container.innerHTML = '<div class="empty-tree-msg">No galaxy selected</div>';
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
try {
|
|
1154
|
+
const { metaspace, space } = await getPlaygroundMetaspaceAndSpace();
|
|
1155
|
+
const galaxyIbGib = await this.resolveGalaxyIbGib(this.galaxyName, space);
|
|
1156
|
+
if (!galaxyIbGib) {
|
|
1157
|
+
container.innerHTML = `<div class="empty-tree-msg">Galaxy [${this.galaxyName}] not found</div>`;
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
const rootNode = await getCompositeBranchTreeFromGalaxy({
|
|
1162
|
+
galaxyIbGib,
|
|
1163
|
+
metaspace,
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
if (!rootNode) {
|
|
1167
|
+
container.innerHTML = '<div class="empty-tree-msg">No branches in composite tree</div>';
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
container.innerHTML = '';
|
|
1172
|
+
await this.renderCompositeBranchNode(rootNode, container, metaspace, space, galaxyIbGib);
|
|
1173
|
+
this.updateDirtyBadges();
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
container.innerHTML = `<div class="empty-tree-msg" style="color: #ef4444;">Error loading composite branch tree: ${extractErrorMsg(err)}</div>`;
|
|
1176
|
+
console.error(`${this.lc} Error loading composite branch tree: ${extractErrorMsg(err)}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
private async renderCompositeBranchNode(
|
|
1181
|
+
node: CompositeBranchNode,
|
|
1182
|
+
containerEl: HTMLElement,
|
|
1183
|
+
metaspace: any,
|
|
1184
|
+
space: any,
|
|
1185
|
+
galaxyIbGib: B2tFSGalaxyIbGib_V1
|
|
1186
|
+
): Promise<void> {
|
|
1187
|
+
const branchName = node.branchInfo.branchName;
|
|
1188
|
+
const colorInfo = await this.getBranchColor(branchName, this.galaxyName);
|
|
1189
|
+
|
|
1190
|
+
const nodeEl = document.createElement('div');
|
|
1191
|
+
nodeEl.className = 'composite-branch-node';
|
|
1192
|
+
nodeEl.setAttribute('data-branch', branchName);
|
|
1193
|
+
|
|
1194
|
+
// Header
|
|
1195
|
+
const headerEl = document.createElement('div');
|
|
1196
|
+
headerEl.className = `composite-branch-header ${branchName === this.branchName ? 'active' : ''}`;
|
|
1197
|
+
const branchColor = colorInfo?.punctiliarColor || '#818cf8';
|
|
1198
|
+
const branchTranslucent = colorInfo?.punctiliarColorTranslucent || 'rgba(129, 140, 248, 0.15)';
|
|
1199
|
+
headerEl.style.setProperty('--node-accent-color', branchColor);
|
|
1200
|
+
headerEl.style.setProperty('--node-accent-translucent', branchTranslucent);
|
|
1201
|
+
headerEl.style.setProperty('--branch-accent-color', branchColor);
|
|
1202
|
+
headerEl.style.setProperty('--branch-accent-translucent', branchTranslucent);
|
|
1203
|
+
headerEl.style.paddingLeft = `${node.depth * 14 + 6}px`;
|
|
1204
|
+
|
|
1205
|
+
const toggleBtn = document.createElement('button');
|
|
1206
|
+
toggleBtn.className = 'branch-toggle-btn';
|
|
1207
|
+
toggleBtn.type = 'button';
|
|
1208
|
+
toggleBtn.setAttribute('aria-label', `Toggle Branch ${branchName}`);
|
|
1209
|
+
const chevronEl = document.createElement('span');
|
|
1210
|
+
chevronEl.className = 'chevron-icon';
|
|
1211
|
+
chevronEl.textContent = '▼';
|
|
1212
|
+
toggleBtn.appendChild(chevronEl);
|
|
1213
|
+
|
|
1214
|
+
const iconEl = document.createElement('span');
|
|
1215
|
+
iconEl.className = 'branch-icon composite-branch-icon';
|
|
1216
|
+
iconEl.textContent = '🌿';
|
|
1217
|
+
iconEl.style.color = branchColor;
|
|
1218
|
+
|
|
1219
|
+
const titleEl = document.createElement('span');
|
|
1220
|
+
titleEl.className = 'branch-title composite-branch-title';
|
|
1221
|
+
titleEl.textContent = branchName;
|
|
1222
|
+
titleEl.style.color = branchColor;
|
|
1223
|
+
|
|
1224
|
+
headerEl.appendChild(toggleBtn);
|
|
1225
|
+
headerEl.appendChild(iconEl);
|
|
1226
|
+
headerEl.appendChild(titleEl);
|
|
1227
|
+
|
|
1228
|
+
if (node.scopedItemPath) {
|
|
1229
|
+
const scopedEl = document.createElement('span');
|
|
1230
|
+
scopedEl.className = 'composite-scoped-badge';
|
|
1231
|
+
scopedEl.textContent = `↳ ${node.scopedItemPath}`;
|
|
1232
|
+
scopedEl.title = `Scoped to item: ${node.scopedItemPath}`;
|
|
1233
|
+
headerEl.appendChild(scopedEl);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const dirtyBadge = document.createElement('span');
|
|
1237
|
+
dirtyBadge.className = 'branch-dirty-badge';
|
|
1238
|
+
dirtyBadge.setAttribute('data-branch', branchName);
|
|
1239
|
+
dirtyBadge.style.display = 'none';
|
|
1240
|
+
dirtyBadge.style.marginLeft = 'auto';
|
|
1241
|
+
dirtyBadge.style.marginRight = '0.25rem';
|
|
1242
|
+
dirtyBadge.style.setProperty('--branch-accent-color', colorInfo?.punctiliarColor || '#fbbf24');
|
|
1243
|
+
dirtyBadge.style.setProperty('--branch-accent-translucent', colorInfo?.punctiliarColorTranslucent || 'rgba(234, 179, 8, 0.18)');
|
|
1244
|
+
headerEl.appendChild(dirtyBadge);
|
|
1245
|
+
|
|
1246
|
+
// DRY Branch actions menu (⋮)
|
|
1247
|
+
const actionsEl = document.createElement('div');
|
|
1248
|
+
actionsEl.className = 'branch-actions';
|
|
1249
|
+
|
|
1250
|
+
const menuBtn = document.createElement('button');
|
|
1251
|
+
menuBtn.className = 'action-btn branch-menu-btn';
|
|
1252
|
+
menuBtn.type = 'button';
|
|
1253
|
+
menuBtn.title = `Branch actions for [${branchName}] (⋮)`;
|
|
1254
|
+
menuBtn.textContent = '⋮';
|
|
1255
|
+
menuBtn.addEventListener('click', (e) => {
|
|
1256
|
+
e.stopPropagation();
|
|
1257
|
+
this.showCommandMenu(menuBtn, this.getBranchCommandMenuItems(branchName));
|
|
1258
|
+
});
|
|
1259
|
+
actionsEl.appendChild(menuBtn);
|
|
1260
|
+
headerEl.appendChild(actionsEl);
|
|
1261
|
+
|
|
1262
|
+
// Body (Files + Children)
|
|
1263
|
+
const bodyEl = document.createElement('div');
|
|
1264
|
+
bodyEl.className = 'composite-branch-body';
|
|
1265
|
+
|
|
1266
|
+
if (node.depth === 0) {
|
|
1267
|
+
if (this.isCompositeExpanded) {
|
|
1268
|
+
headerEl.classList.remove('collapsed');
|
|
1269
|
+
headerEl.classList.add('expanded');
|
|
1270
|
+
bodyEl.style.display = 'flex';
|
|
1271
|
+
} else {
|
|
1272
|
+
headerEl.classList.add('collapsed');
|
|
1273
|
+
headerEl.classList.remove('expanded');
|
|
1274
|
+
bodyEl.style.display = 'none';
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
headerEl.addEventListener('click', (e) => {
|
|
1278
|
+
const target = e.target as HTMLElement;
|
|
1279
|
+
if (target.closest('.action-btn') || target.closest('.branch-menu-btn')) return;
|
|
1280
|
+
this.toggleCompositeRootAccordion(headerEl, bodyEl);
|
|
1281
|
+
});
|
|
1282
|
+
} else {
|
|
1283
|
+
headerEl.addEventListener('click', (e) => {
|
|
1284
|
+
const target = e.target as HTMLElement;
|
|
1285
|
+
if (target.closest('.action-btn') || target.closest('.branch-menu-btn')) return;
|
|
1286
|
+
const isCollapsed = headerEl.classList.toggle('collapsed');
|
|
1287
|
+
headerEl.classList.toggle('expanded', !isCollapsed);
|
|
1288
|
+
bodyEl.style.display = isCollapsed ? 'none' : 'flex';
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Files container for this branch
|
|
1293
|
+
const filesEl = document.createElement('div');
|
|
1294
|
+
filesEl.className = 'composite-branch-files';
|
|
1295
|
+
|
|
1296
|
+
// Load files for this branch
|
|
1297
|
+
try {
|
|
1298
|
+
const branchAndRoot = await getBranchAndRootItemInfosAndIbGibsAndSpaceFromGalaxyIbGib({
|
|
1299
|
+
galaxyIbGib,
|
|
1300
|
+
branchName,
|
|
1301
|
+
metaspace,
|
|
1302
|
+
});
|
|
1303
|
+
const rootItem = branchAndRoot.rootItemIbGib_latest || branchAndRoot.rootItemIbGib;
|
|
1304
|
+
const children = await getFolderChildren({ branchSpace: branchAndRoot.branchSpace, folderItem: rootItem });
|
|
1305
|
+
const committedNames = new Set(children.map(c => c.data.name));
|
|
1306
|
+
|
|
1307
|
+
const statusMap = this.getFileStatusMap(this.galaxyName, branchName);
|
|
1308
|
+
const untrackedNames: string[] = [];
|
|
1309
|
+
for (const [filePath, status] of statusMap) {
|
|
1310
|
+
if (status === 'untracked' && !committedNames.has(filePath)) {
|
|
1311
|
+
untrackedNames.push(filePath);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (children.length === 0 && untrackedNames.length === 0) {
|
|
1316
|
+
const emptyFileEl = document.createElement('div');
|
|
1317
|
+
emptyFileEl.className = 'file-item-empty';
|
|
1318
|
+
emptyFileEl.textContent = '(no files)';
|
|
1319
|
+
emptyFileEl.style.cssText = 'color: #64748b; font-size: 0.72rem; padding: 2px 6px; font-style: italic;';
|
|
1320
|
+
filesEl.appendChild(emptyFileEl);
|
|
1321
|
+
} else {
|
|
1322
|
+
for (const child of children) {
|
|
1323
|
+
const fileRow = this.createCompositeFileRow(child.data.name, branchName, false);
|
|
1324
|
+
filesEl.appendChild(fileRow);
|
|
1325
|
+
}
|
|
1326
|
+
for (const untrackedPath of untrackedNames) {
|
|
1327
|
+
const fileRow = this.createCompositeFileRow(untrackedPath, branchName, true);
|
|
1328
|
+
filesEl.appendChild(fileRow);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
console.warn(`${this.lc} Could not load files for branch [${branchName}]: ${extractErrorMsg(err)}`);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
bodyEl.appendChild(filesEl);
|
|
1336
|
+
|
|
1337
|
+
// Nested sub-branches
|
|
1338
|
+
if (node.children && node.children.length > 0) {
|
|
1339
|
+
const subBranchesEl = document.createElement('div');
|
|
1340
|
+
subBranchesEl.className = 'composite-branch-children';
|
|
1341
|
+
for (const childNode of node.children) {
|
|
1342
|
+
await this.renderCompositeBranchNode(childNode, subBranchesEl, metaspace, space, galaxyIbGib);
|
|
1343
|
+
}
|
|
1344
|
+
bodyEl.appendChild(subBranchesEl);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
nodeEl.appendChild(headerEl);
|
|
1348
|
+
nodeEl.appendChild(bodyEl);
|
|
1349
|
+
containerEl.appendChild(nodeEl);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
private createCompositeFileRow(filePath: string, branchName: string, isUntracked: boolean): HTMLElement {
|
|
1353
|
+
const fileEl = document.createElement('div');
|
|
1354
|
+
fileEl.className = `tree-item file ${filePath === this.selectedFilePath && branchName === this.branchName ? 'selected' : ''}`;
|
|
1355
|
+
fileEl.setAttribute('data-path', filePath);
|
|
1356
|
+
fileEl.setAttribute('data-branch', branchName);
|
|
1357
|
+
|
|
1358
|
+
const statusMap = this.getFileStatusMap(this.galaxyName, branchName);
|
|
1359
|
+
const status = isUntracked ? 'untracked' : statusMap.get(filePath);
|
|
1360
|
+
|
|
1361
|
+
const iconEl = document.createElement('span');
|
|
1362
|
+
iconEl.className = 'tree-icon';
|
|
1363
|
+
iconEl.textContent = filePath.endsWith('.md') ? '📝' : (filePath.endsWith('.json') ? '⚙️' : '📄');
|
|
1364
|
+
|
|
1365
|
+
const labelEl = document.createElement('span');
|
|
1366
|
+
labelEl.className = `tree-label ${status === 'untracked' ? 'vcs-untracked' : (status === 'modified' ? 'vcs-modified' : '')}`;
|
|
1367
|
+
labelEl.textContent = filePath;
|
|
1368
|
+
labelEl.title = `${filePath} [${branchName}]`;
|
|
1369
|
+
|
|
1370
|
+
fileEl.appendChild(iconEl);
|
|
1371
|
+
fileEl.appendChild(labelEl);
|
|
1372
|
+
|
|
1373
|
+
if (status) {
|
|
1374
|
+
const badgeEl = document.createElement('span');
|
|
1375
|
+
badgeEl.className = `vcs-status-badge ${status}`;
|
|
1376
|
+
badgeEl.textContent = status === 'untracked' ? 'U' : 'M';
|
|
1377
|
+
badgeEl.title = status === 'untracked' ? 'Untracked' : 'Modified';
|
|
1378
|
+
fileEl.appendChild(badgeEl);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
fileEl.addEventListener('click', () => {
|
|
1382
|
+
this.selectedFilePath = filePath;
|
|
1383
|
+
const shadow = this.shadowRoot;
|
|
1384
|
+
shadow?.querySelectorAll('.tree-item.file').forEach(el => el.classList.remove('selected'));
|
|
1385
|
+
fileEl.classList.add('selected');
|
|
1386
|
+
|
|
1387
|
+
this.dispatchEvent(new CustomEvent('space-gib-file-selected', {
|
|
1388
|
+
detail: {
|
|
1389
|
+
galaxy: this.galaxyName,
|
|
1390
|
+
branch: branchName,
|
|
1391
|
+
path: filePath,
|
|
1392
|
+
name: filePath,
|
|
1393
|
+
},
|
|
1394
|
+
bubbles: true,
|
|
1395
|
+
composed: true,
|
|
1396
|
+
}));
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
return fileEl;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
338
1402
|
override async disconnected(): Promise<void> {
|
|
339
1403
|
const lc = `${this.lc}[${this.disconnected.name}]`;
|
|
340
1404
|
try {
|
|
341
|
-
|
|
1405
|
+
window.removeEventListener('click', this.onWindowClick);
|
|
1406
|
+
window.removeEventListener('keydown', this.onWindowKeyDown);
|
|
1407
|
+
window.removeEventListener('space-gib-code-changed', this.onCodeChanged);
|
|
342
1408
|
} catch (error) {
|
|
343
1409
|
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
344
1410
|
}
|