@ibgib/space-gib 0.0.33 → 0.0.35
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 +24 -51
- package/dist/client/chunk-FIAGRVBY.mjs +10933 -0
- package/dist/client/chunk-XLQ3UCQY.mjs +53 -0
- package/dist/client/css/activity-bar.css +435 -0
- package/dist/client/css/base.css +167 -0
- package/dist/client/css/components.css +218 -0
- package/dist/client/css/cosmic.css +557 -0
- package/dist/client/css/dev-tools.css +163 -0
- package/dist/client/css/landing.css +184 -0
- package/dist/client/css/layout.css +1017 -0
- package/dist/client/css/onboarding.css +181 -0
- package/dist/client/css/repos.css +740 -0
- package/dist/client/css/variables.css +64 -0
- package/dist/client/index.html +208 -31
- 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 +11 -1056
- package/dist/respec-gib.node.mjs +5 -0
- package/dist/server/server.mjs +6880 -3110
- package/package.json +6 -6
- package/src/client/AUTO-GENERATED-version.mts +1 -1
- package/src/client/cosmos/seed-cosmos-workspace.mts +83 -0
- package/src/client/css/activity-bar.css +435 -0
- package/src/client/css/base.css +167 -0
- package/src/client/css/components.css +218 -0
- package/src/client/css/cosmic.css +557 -0
- package/src/client/css/dev-tools.css +163 -0
- package/src/client/css/landing.css +184 -0
- package/src/client/css/layout.css +1017 -0
- package/src/client/css/onboarding.css +181 -0
- package/src/client/css/repos.css +740 -0
- package/src/client/css/variables.css +64 -0
- package/src/client/dev-tools/vcs-workspace.mts +7 -7
- package/src/client/index.html +208 -31
- package/src/client/privacy.html +22 -22
- package/src/client/style.css +11 -1056
- 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/IMPLEMENTATION.md +48 -0
- package/src/client/ui/component/chat-view/chat-view.css +381 -0
- package/src/client/ui/component/chat-view/chat-view.html +52 -0
- package/src/client/ui/component/chat-view/chat-view.mts +264 -0
- package/src/client/ui/component/chat-view/index.mts +9 -0
- package/src/client/ui/component/clone/IMPLEMENTATION.md +68 -0
- package/src/client/ui/component/clone/clone-constants.mts +16 -0
- package/src/client/ui/component/clone/clone-types.mts +60 -0
- package/src/client/ui/component/clone/clone.css +383 -0
- package/src/client/ui/component/clone/clone.html +89 -0
- package/src/client/ui/component/clone/clone.mts +648 -0
- package/src/client/ui/component/clone/index.mts +11 -0
- package/src/client/ui/component/code-editor/IMPLEMENTATION.md +89 -0
- package/src/client/ui/component/code-editor/code-editor.css +357 -0
- package/src/client/ui/component/code-editor/code-editor.html +67 -0
- package/src/client/ui/component/code-editor/code-editor.mts +643 -0
- package/src/client/ui/component/code-editor/index.mts +9 -0
- package/src/client/ui/component/cosmos-navigator/IMPLEMENTATION.md +31 -0
- package/src/client/ui/component/cosmos-navigator/cosmos-navigator.css +864 -0
- package/src/client/ui/component/cosmos-navigator/cosmos-navigator.html +92 -0
- package/src/client/ui/component/cosmos-navigator/cosmos-navigator.mts +1317 -0
- package/src/client/ui/component/cosmos-navigator/index.mts +18 -0
- package/src/client/ui/component/file-explorer/IMPLEMENTATION.md +42 -0
- package/src/client/ui/component/file-explorer/file-explorer.css +720 -0
- package/src/client/ui/component/file-explorer/file-explorer.html +55 -0
- package/src/client/ui/component/file-explorer/file-explorer.mts +1412 -0
- package/src/client/ui/component/file-explorer/index.mts +9 -0
- package/src/client/ui/component/nested-chat/IMPLEMENTATION.md +18 -0
- package/src/client/ui/component/nested-chat/chat/IMPLEMENTATION.md +14 -0
- package/src/client/ui/component/nested-chat/chat/chat-constants.mts +14 -0
- package/src/client/ui/component/nested-chat/chat/chat-helpers.mts +66 -0
- package/src/client/ui/component/nested-chat/chat/chat.css +654 -0
- package/src/client/ui/component/nested-chat/chat/chat.html +90 -0
- package/src/client/ui/component/nested-chat/chat/chat.mts +561 -0
- package/src/client/ui/component/nested-chat/chat/index.mts +7 -0
- package/src/client/ui/component/nested-chat/index.mts +14 -0
- package/src/client/ui/component/nested-chat/nested-chat-constants.mts +12 -0
- package/src/client/ui/component/nested-chat/nested-chat-types.mts +53 -0
- package/src/client/ui/component/nested-chat/nested-chat.css +142 -0
- package/src/client/ui/component/nested-chat/nested-chat.html +11 -0
- package/src/client/ui/component/nested-chat/nested-chat.mts +395 -0
- package/src/client/ui/component/notes-explorer/index.mts +9 -0
- package/src/client/ui/component/notes-explorer/notes-explorer.css +313 -0
- package/src/client/ui/component/notes-explorer/notes-explorer.html +136 -0
- package/src/client/ui/component/notes-explorer/notes-explorer.mts +240 -0
- package/src/client/ui/component/notes-inspector/index.mts +9 -0
- package/src/client/ui/component/notes-inspector/notes-inspector.css +244 -0
- package/src/client/ui/component/notes-inspector/notes-inspector.html +97 -0
- package/src/client/ui/component/notes-inspector/notes-inspector.mts +152 -0
- package/src/client/ui/component/pitch/IMPLEMENTATION.md +48 -0
- package/src/client/ui/component/pitch/index.mts +9 -0
- package/src/client/ui/component/pitch/pitch.css +295 -0
- package/src/client/ui/component/pitch/pitch.html +97 -0
- package/src/client/ui/component/pitch/pitch.mts +135 -0
- package/src/client/ui/component/timeline/IMPLEMENTATION.md +33 -0
- package/src/client/ui/component/timeline/index.mts +9 -0
- package/src/client/ui/component/timeline/timeline.css +279 -0
- package/src/client/ui/component/timeline/timeline.html +23 -0
- package/src/client/ui/component/timeline/timeline.mts +261 -0
- package/src/client/ui/component/timeline-item-details/IMPLEMENTATION.md +28 -0
- package/src/client/ui/component/timeline-item-details/index.mts +9 -0
- package/src/client/ui/component/timeline-item-details/timeline-item-details.css +286 -0
- package/src/client/ui/component/timeline-item-details/timeline-item-details.html +64 -0
- package/src/client/ui/component/timeline-item-details/timeline-item-details.mts +194 -0
- package/src/client/ui/router/index.mts +3 -0
- package/src/client/ui/router/space-gib-router-helpers.mts +72 -0
- package/src/client/ui/router/space-gib-router-helpers.respec.mts +529 -0
- package/src/client/ui/router/space-gib-router-service.mts +63 -0
- package/src/client/ui/router/space-gib-router-types.mts +49 -0
- package/src/client/ui/shell/cosmic-big-bang.mts +853 -0
- package/src/client/ui/shell/space-gib-shell-constants.mts +7 -0
- package/src/client/ui/shell/space-gib-shell-service.mts +2297 -32
- package/src/server/server.mts +20 -1
- package/tsconfig.test.json +5 -1
- package/dist/client/chunk-CRJV762I.mjs +0 -3102
- package/dist/client/chunk-LXRCF5OS.mjs +0 -25
- package/dist/client/chunk-UBDQUZZV.mjs +0 -1
|
@@ -0,0 +1,1412 @@
|
|
|
1
|
+
import thisCss from "./file-explorer.css";
|
|
2
|
+
import thisHtml from "./file-explorer.html";
|
|
3
|
+
import vcsStylesCss from "@ibgib/web-gib/dist/ui/component/vcs-styles.css";
|
|
4
|
+
|
|
5
|
+
import { ROOT_ADDR } from "@ibgib/ts-gib/dist/V1/constants.mjs";
|
|
6
|
+
import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs";
|
|
7
|
+
import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
|
|
8
|
+
import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
9
|
+
import { promptForText, promptForConfirm } from "@ibgib/web-gib/dist/helpers.web.mjs";
|
|
10
|
+
import {
|
|
11
|
+
IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase,
|
|
12
|
+
} from "@ibgib/web-gib/dist/ui/component/ibgib-dynamic-component-bases.mjs";
|
|
13
|
+
import {
|
|
14
|
+
ElementsBase, IbGibDynamicComponentInstance,
|
|
15
|
+
} from "@ibgib/web-gib/dist/ui/component/component-types.mjs";
|
|
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";
|
|
35
|
+
|
|
36
|
+
export const FILE_EXPLORER_COMPONENT_NAME = 'space-gib-file-explorer';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Metadata factory for space-gib-file-explorer Web Component.
|
|
40
|
+
*/
|
|
41
|
+
export class FileExplorerComponentMeta extends IbGibDynamicComponentMetaBase {
|
|
42
|
+
protected lc: string = `[${FileExplorerComponentMeta.name}]`;
|
|
43
|
+
|
|
44
|
+
routeRegExp?: RegExp = new RegExp(`^${FILE_EXPLORER_COMPONENT_NAME}$`);
|
|
45
|
+
componentName = FILE_EXPLORER_COMPONENT_NAME;
|
|
46
|
+
|
|
47
|
+
constructor() {
|
|
48
|
+
super(getComponentCtorArg());
|
|
49
|
+
if (!customElements.get(this.componentName)) {
|
|
50
|
+
customElements.define(this.componentName, FileExplorerComponentInstance);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async createInstance({
|
|
55
|
+
path,
|
|
56
|
+
ibGibAddr
|
|
57
|
+
}: {
|
|
58
|
+
path: string;
|
|
59
|
+
ibGibAddr: IbGibAddr;
|
|
60
|
+
}): Promise<IbGibDynamicComponentInstance> {
|
|
61
|
+
const component = document.createElement(this.componentName) as FileExplorerComponentInstance;
|
|
62
|
+
await component.initialize({
|
|
63
|
+
ibGibAddr: ibGibAddr || ROOT_ADDR,
|
|
64
|
+
meta: this,
|
|
65
|
+
html: thisHtml,
|
|
66
|
+
css: [vcsStylesCss, thisCss],
|
|
67
|
+
});
|
|
68
|
+
return component;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
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
|
+
|
|
83
|
+
/**
|
|
84
|
+
* References to DOM elements within the component's Shadow Root.
|
|
85
|
+
*/
|
|
86
|
+
export interface FileExplorerElements extends ElementsBase {
|
|
87
|
+
rootEl: HTMLDivElement;
|
|
88
|
+
modeToolbarEl?: HTMLDivElement;
|
|
89
|
+
btnModeSingleEl?: HTMLButtonElement;
|
|
90
|
+
btnModeCompositeEl?: HTMLButtonElement;
|
|
91
|
+
btnRefreshExplorerEl?: HTMLButtonElement;
|
|
92
|
+
singleBranchViewEl?: HTMLDivElement;
|
|
93
|
+
compositeTreeContainerEl?: HTMLDivElement;
|
|
94
|
+
branchHeaderEl: HTMLDivElement;
|
|
95
|
+
btnBranchToggleEl: HTMLButtonElement;
|
|
96
|
+
btnBranchSelectEl: HTMLButtonElement;
|
|
97
|
+
branchTitleEl: HTMLSpanElement;
|
|
98
|
+
branchDirtyBadgeEl?: HTMLSpanElement;
|
|
99
|
+
branchDropdownMenuEl: HTMLDivElement;
|
|
100
|
+
fileTreeContainerEl: HTMLDivElement;
|
|
101
|
+
btnBranchMenuEl?: HTMLButtonElement;
|
|
102
|
+
commandMenuPopupEl?: HTMLDivElement;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Concrete implementation of the Branch File Explorer component.
|
|
107
|
+
*/
|
|
108
|
+
export class FileExplorerComponentInstance
|
|
109
|
+
extends IbGibDynamicComponentInstanceBase<IbGib_V1, FileExplorerElements>
|
|
110
|
+
implements IbGibDynamicComponentInstance<IbGib_V1, FileExplorerElements> {
|
|
111
|
+
|
|
112
|
+
protected lc: string = `[${FileExplorerComponentInstance.name}]`;
|
|
113
|
+
|
|
114
|
+
private galaxyName: string = '';
|
|
115
|
+
private branchName: string = 'main';
|
|
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
|
+
}
|
|
151
|
+
|
|
152
|
+
get el(): FileExplorerElements {
|
|
153
|
+
if (!this.elements) {
|
|
154
|
+
throw new Error(`elements is not defined in ${this.lc}`);
|
|
155
|
+
}
|
|
156
|
+
return this.elements;
|
|
157
|
+
}
|
|
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
|
+
|
|
199
|
+
override async created(): Promise<void> {
|
|
200
|
+
const lc = `${this.lc}[${this.created.name}]`;
|
|
201
|
+
try {
|
|
202
|
+
const shadow = this.shadowRoot!;
|
|
203
|
+
this.elements = {
|
|
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,
|
|
211
|
+
branchHeaderEl: shadow.getElementById('branch-header') as HTMLDivElement,
|
|
212
|
+
btnBranchToggleEl: shadow.getElementById('btn-branch-toggle') as HTMLButtonElement,
|
|
213
|
+
btnBranchSelectEl: shadow.getElementById('btn-branch-select') as HTMLButtonElement,
|
|
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,
|
|
217
|
+
fileTreeContainerEl: shadow.getElementById('file-tree-container') as HTMLDivElement,
|
|
218
|
+
btnBranchMenuEl: shadow.getElementById('btn-branch-menu') as HTMLButtonElement,
|
|
219
|
+
commandMenuPopupEl: shadow.getElementById('command-menu-popup') as HTMLDivElement,
|
|
220
|
+
contentEl: shadow.getElementById('file-explorer-root') as HTMLDivElement,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const isExpanded = this.viewMode === 'single' ? this.isSingleExpanded : this.isCompositeExpanded;
|
|
224
|
+
this.toggleAttribute('collapsed', !isExpanded);
|
|
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);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private bindHandlers(): void {
|
|
242
|
+
const shadow = this.shadowRoot!;
|
|
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
|
+
|
|
259
|
+
// Branch header toggle
|
|
260
|
+
this.el.branchHeaderEl.addEventListener('click', (e) => {
|
|
261
|
+
const target = e.target as HTMLElement;
|
|
262
|
+
if (target.closest('.action-btn')) return; // ignore action buttons
|
|
263
|
+
if (target.closest('#branch-selector-container')) return; // ignore branch selector
|
|
264
|
+
this.toggleBranchAccordion();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Branch dropdown toggle
|
|
268
|
+
this.el.btnBranchSelectEl.addEventListener('click', (e) => {
|
|
269
|
+
e.stopPropagation();
|
|
270
|
+
this.toggleBranchDropdown();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Single Branch Command Menu (⋮)
|
|
274
|
+
this.el.btnBranchMenuEl?.addEventListener('click', (e) => {
|
|
275
|
+
e.stopPropagation();
|
|
276
|
+
this.showCommandMenu(this.el.btnBranchMenuEl!, this.getBranchCommandMenuItems(this.branchName));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Single Item Branch actions
|
|
280
|
+
shadow.querySelectorAll('.file-branch-btn').forEach(btn => {
|
|
281
|
+
btn.addEventListener('click', async (e) => {
|
|
282
|
+
e.stopPropagation();
|
|
283
|
+
const treeItem = btn.closest('.tree-item');
|
|
284
|
+
const path = treeItem?.getAttribute('data-path') || treeItem?.getAttribute('data-folder') || '';
|
|
285
|
+
const isDir = treeItem?.classList.contains('folder') || false;
|
|
286
|
+
const filename = path.split('/').pop() || path;
|
|
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 = '';
|
|
290
|
+
|
|
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;
|
|
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
|
+
}));
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Single Item Clone actions
|
|
326
|
+
shadow.querySelectorAll('.file-clone-btn').forEach(btn => {
|
|
327
|
+
btn.addEventListener('click', (e) => {
|
|
328
|
+
e.stopPropagation();
|
|
329
|
+
const treeItem = btn.closest('.tree-item');
|
|
330
|
+
const path = treeItem?.getAttribute('data-path') || treeItem?.getAttribute('data-folder') || '';
|
|
331
|
+
const isDir = treeItem?.classList.contains('folder') || false;
|
|
332
|
+
this.dispatchEvent(new CustomEvent('space-gib-clone-requested', {
|
|
333
|
+
detail: {
|
|
334
|
+
singleItem: {
|
|
335
|
+
path,
|
|
336
|
+
isDir,
|
|
337
|
+
galaxyName: 'Code Galaxy',
|
|
338
|
+
branchName: this.branchName,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
bubbles: true,
|
|
342
|
+
composed: true,
|
|
343
|
+
}));
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Folder toggles
|
|
348
|
+
shadow.querySelectorAll('.tree-item.folder').forEach(folderEl => {
|
|
349
|
+
folderEl.addEventListener('click', () => {
|
|
350
|
+
const folderName = folderEl.getAttribute('data-folder');
|
|
351
|
+
if (!folderName) { return; }
|
|
352
|
+
|
|
353
|
+
const isExpanded = folderEl.classList.contains('expanded');
|
|
354
|
+
const chevron = folderEl.querySelector('.tree-chevron');
|
|
355
|
+
|
|
356
|
+
if (isExpanded) {
|
|
357
|
+
folderEl.classList.remove('expanded');
|
|
358
|
+
if (chevron) { chevron.textContent = '▼'; }
|
|
359
|
+
shadow.querySelectorAll(`.child-of-${folderName}`).forEach(child => child.classList.add('hidden'));
|
|
360
|
+
} else {
|
|
361
|
+
folderEl.classList.add('expanded');
|
|
362
|
+
if (chevron) { chevron.textContent = '▼'; }
|
|
363
|
+
shadow.querySelectorAll(`.child-of-${folderName}`).forEach(child => child.classList.remove('hidden'));
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// File selections
|
|
369
|
+
shadow.querySelectorAll('.tree-item.file').forEach(fileEl => {
|
|
370
|
+
fileEl.addEventListener('click', () => {
|
|
371
|
+
const path = fileEl.getAttribute('data-path') || '';
|
|
372
|
+
const name = fileEl.getAttribute('data-name') || '';
|
|
373
|
+
this.selectFile(path);
|
|
374
|
+
|
|
375
|
+
this.dispatchEvent(new CustomEvent('space-gib-file-selected', {
|
|
376
|
+
detail: {
|
|
377
|
+
branch: this.branchName,
|
|
378
|
+
path,
|
|
379
|
+
name,
|
|
380
|
+
},
|
|
381
|
+
bubbles: true,
|
|
382
|
+
composed: true,
|
|
383
|
+
}));
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
}
|
|
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
|
+
|
|
539
|
+
public toggleBranchAccordion(): void {
|
|
540
|
+
this.isSingleExpanded = !this.isSingleExpanded;
|
|
541
|
+
this.toggleAttribute('collapsed', !this.isSingleExpanded);
|
|
542
|
+
if (this.isSingleExpanded) {
|
|
543
|
+
this.el.branchHeaderEl.classList.remove('collapsed');
|
|
544
|
+
this.el.branchHeaderEl.classList.add('expanded');
|
|
545
|
+
this.el.fileTreeContainerEl.classList.remove('collapsed');
|
|
546
|
+
} else {
|
|
547
|
+
this.el.branchHeaderEl.classList.add('collapsed');
|
|
548
|
+
this.el.branchHeaderEl.classList.remove('expanded');
|
|
549
|
+
this.el.fileTreeContainerEl.classList.add('collapsed');
|
|
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
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
public collapseAllFolders(): void {
|
|
578
|
+
const shadow = this.shadowRoot!;
|
|
579
|
+
shadow.querySelectorAll('.tree-item.folder').forEach(folderEl => {
|
|
580
|
+
const folderName = folderEl.getAttribute('data-folder');
|
|
581
|
+
folderEl.classList.remove('expanded');
|
|
582
|
+
const chevron = folderEl.querySelector('.tree-chevron');
|
|
583
|
+
if (chevron) { chevron.textContent = '▼'; }
|
|
584
|
+
if (folderName) {
|
|
585
|
+
shadow.querySelectorAll(`.child-of-${folderName}`).forEach(child => child.classList.add('hidden'));
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
public selectFile(path: string): void {
|
|
591
|
+
this.selectedFilePath = path;
|
|
592
|
+
const shadow = this.shadowRoot!;
|
|
593
|
+
shadow.querySelectorAll('.tree-item.file').forEach(el => {
|
|
594
|
+
if (el.getAttribute('data-path') === path) {
|
|
595
|
+
el.classList.add('selected');
|
|
596
|
+
} else {
|
|
597
|
+
el.classList.remove('selected');
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
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;
|
|
623
|
+
this.branchName = branchName;
|
|
624
|
+
if (this.el?.branchTitleEl) {
|
|
625
|
+
this.el.branchTitleEl.textContent = branchName;
|
|
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);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
public getBranch(): string {
|
|
818
|
+
return this.branchName;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
public getBranchName(): string {
|
|
822
|
+
return this.branchName;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
public getGalaxyName(): string {
|
|
826
|
+
return this.galaxyName;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
public getSelectedFilePath(): string {
|
|
830
|
+
return this.selectedFilePath;
|
|
831
|
+
}
|
|
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
|
+
|
|
1402
|
+
override async disconnected(): Promise<void> {
|
|
1403
|
+
const lc = `${this.lc}[${this.disconnected.name}]`;
|
|
1404
|
+
try {
|
|
1405
|
+
window.removeEventListener('click', this.onWindowClick);
|
|
1406
|
+
window.removeEventListener('keydown', this.onWindowKeyDown);
|
|
1407
|
+
window.removeEventListener('space-gib-code-changed', this.onCodeChanged);
|
|
1408
|
+
} catch (error) {
|
|
1409
|
+
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|