gitmaps 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +267 -118
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/og-image/route.ts +14 -0
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/version/route.ts +26 -0
- package/app/globals.css +5706 -4938
- package/app/layout.tsx +1279 -490
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas.ts +625 -564
- package/app/lib/cards.tsx +1361 -916
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +71 -93
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -148
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -427
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/positions.ts +190 -121
- package/app/lib/recent-commits.test.ts +947 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -987
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -735
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -269
- package/app/page.tsx +15 -16
- package/app/state/machine.js +13 -0
- package/package.json +84 -75
- package/server.ts +10 -0
- package/app/[owner]/[repo]/page.tsx +0 -6
- package/app/[slug]/page.tsx +0 -6
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
bootstrapInitialRouteUi,
|
|
4
|
+
getInitialRouteParts,
|
|
5
|
+
handleInitialRouteError,
|
|
6
|
+
hideInitialRouteLanding,
|
|
7
|
+
hydrateInitialRouteRepo,
|
|
8
|
+
isGithubOwnerRepoSlug,
|
|
9
|
+
migrateLegacyHashRoute,
|
|
10
|
+
resolveInitialRepoPath,
|
|
11
|
+
showInitialRouteCloneStart,
|
|
12
|
+
} from './initial-route-hydration';
|
|
13
|
+
import { installFetchMock, setupDomTest } from './test-dom';
|
|
14
|
+
|
|
15
|
+
describe('initial route hydration helper', () => {
|
|
16
|
+
test('parses path and legacy hash route parts', () => {
|
|
17
|
+
const handle = setupDomTest({ url: 'http://localhost:3335/galaxy-canvas/team/platform/tools/gitmaps#legacy-slug' });
|
|
18
|
+
try {
|
|
19
|
+
expect(getInitialRouteParts()).toEqual({
|
|
20
|
+
rawPath: 'galaxy-canvas/team/platform/tools/gitmaps',
|
|
21
|
+
pathSlug: 'team/platform/tools/gitmaps',
|
|
22
|
+
hashSlug: 'legacy-slug',
|
|
23
|
+
urlSlug: 'team/platform/tools/gitmaps',
|
|
24
|
+
});
|
|
25
|
+
} finally {
|
|
26
|
+
handle.cleanup();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('detects cloneable GitHub owner/repo slugs', () => {
|
|
31
|
+
expect(isGithubOwnerRepoSlug('7flash/gitmaps')).toBe(true);
|
|
32
|
+
expect(isGithubOwnerRepoSlug('team/platform/tools/gitmaps')).toBe(false);
|
|
33
|
+
expect(isGithubOwnerRepoSlug('C:/Code/gitmaps')).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('uses cached mapped path for non-github slugs', async () => {
|
|
37
|
+
const handle = setupDomTest();
|
|
38
|
+
try {
|
|
39
|
+
localStorage.setItem('gitcanvas:slug:team/platform/tools/gitmaps', 'C:/Code/gitmaps');
|
|
40
|
+
await expect(resolveInitialRepoPath('team/platform/tools/gitmaps')).resolves.toBe('C:/Code/gitmaps');
|
|
41
|
+
} finally {
|
|
42
|
+
handle.cleanup();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('resolves github slug via resolve-slug api before cloning', async () => {
|
|
47
|
+
const handle = setupDomTest();
|
|
48
|
+
const fetchMock = mock(async (input: string) => {
|
|
49
|
+
expect(input).toBe('/api/repo/resolve-slug');
|
|
50
|
+
return new Response(JSON.stringify({ path: 'C:/Code/gitmaps' }), {
|
|
51
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await expect(resolveInitialRepoPath('7flash/gitmaps')).resolves.toBe('C:/Code/gitmaps');
|
|
58
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
59
|
+
expect(localStorage.getItem('gitcanvas:slug:7flash/gitmaps')).toBe('C:/Code/gitmaps');
|
|
60
|
+
} finally {
|
|
61
|
+
fetchHandle.restore();
|
|
62
|
+
handle.cleanup();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('falls back to cloning when github slug cannot be resolved locally', async () => {
|
|
67
|
+
const handle = setupDomTest();
|
|
68
|
+
const onCloneStart = mock(() => undefined);
|
|
69
|
+
const fetchMock = mock(async (input: string) => {
|
|
70
|
+
if (input === '/api/repo/resolve-slug') {
|
|
71
|
+
return new Response(JSON.stringify({}), {
|
|
72
|
+
headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (input === '/api/repo/clone') {
|
|
76
|
+
return new Response(JSON.stringify({ path: 'C:/Code/gitmaps' }), {
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Unexpected fetch: ${input}`);
|
|
81
|
+
});
|
|
82
|
+
const fetchHandle = installFetchMock(fetchMock as any);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await expect(resolveInitialRepoPath('7flash/gitmaps', { onCloneStart })).resolves.toBe('C:/Code/gitmaps');
|
|
86
|
+
expect(onCloneStart).toHaveBeenCalledWith('7flash/gitmaps');
|
|
87
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
88
|
+
expect(localStorage.getItem('gitcanvas:slug:7flash/gitmaps')).toBe('C:/Code/gitmaps');
|
|
89
|
+
} finally {
|
|
90
|
+
fetchHandle.restore();
|
|
91
|
+
handle.cleanup();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('hides the landing overlay for route hydration', () => {
|
|
96
|
+
const handle = setupDomTest({
|
|
97
|
+
html: '<div id="landingOverlay" style="display:block"></div>',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
hideInitialRouteLanding();
|
|
102
|
+
expect((document.getElementById('landingOverlay') as HTMLElement).style.display).toBe('none');
|
|
103
|
+
} finally {
|
|
104
|
+
handle.cleanup();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('migrates legacy hash routes into encoded path routes', () => {
|
|
109
|
+
const handle = setupDomTest({ url: 'http://localhost:3335/#7flash/jsx-ai' });
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
migrateLegacyHashRoute('7flash/jsx-ai');
|
|
113
|
+
expect(window.location.pathname).toBe('/7flash%2Fjsx-ai');
|
|
114
|
+
} finally {
|
|
115
|
+
handle.cleanup();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('shows clone-start loading ui for github route hydration', () => {
|
|
120
|
+
const handle = setupDomTest({
|
|
121
|
+
html: '<div id="landingOverlay" style="display:block"></div><div id="loadingProgress" style="display:none"><span class="loading-message"></span></div>',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
showInitialRouteCloneStart('7flash/gitmaps');
|
|
126
|
+
|
|
127
|
+
expect((document.getElementById('landingOverlay') as HTMLElement).style.display).toBe('none');
|
|
128
|
+
expect((document.getElementById('loadingProgress') as HTMLElement).style.display).toBe('flex');
|
|
129
|
+
expect(document.querySelector('.loading-message')?.textContent).toBe('Cloning 7flash/gitmaps from GitHub...');
|
|
130
|
+
} finally {
|
|
131
|
+
handle.cleanup();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('reports initial route errors and shows toast through injected helpers', async () => {
|
|
136
|
+
const reportError = mock(() => undefined);
|
|
137
|
+
const showToast = mock(() => undefined);
|
|
138
|
+
|
|
139
|
+
const result = await handleInitialRouteError(new Error('boom'), {
|
|
140
|
+
reportError,
|
|
141
|
+
showToast,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result).toBeNull();
|
|
145
|
+
expect(reportError).toHaveBeenCalledTimes(1);
|
|
146
|
+
expect(showToast).toHaveBeenCalledWith('Failed to load route: boom', 'error');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('bootstraps initial route ui in a small reusable sequence', async () => {
|
|
150
|
+
const handle = setupDomTest({
|
|
151
|
+
html: '<select id="repoSelect"><option value="">Select</option><option value="C:/Code/gitmaps">gitmaps</option></select>',
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const calls: string[] = [];
|
|
156
|
+
const snapshot = { context: { repoPath: '' } };
|
|
157
|
+
const ctx = {
|
|
158
|
+
actor: { send: mock((event: any) => calls.push(`send:${event.type}:${event.path}`)) },
|
|
159
|
+
snap: () => snapshot,
|
|
160
|
+
} as any;
|
|
161
|
+
|
|
162
|
+
await bootstrapInitialRouteUi(ctx, 'C:/Code/gitmaps', {
|
|
163
|
+
applySharedLayout: mock(async () => { calls.push('applySharedLayout'); }),
|
|
164
|
+
syncSelection: mock((path: string) => {
|
|
165
|
+
calls.push(`syncSelection:${path}`);
|
|
166
|
+
(document.getElementById('repoSelect') as HTMLSelectElement).value = path;
|
|
167
|
+
}),
|
|
168
|
+
loadPositions: mock(async () => { calls.push('loadPositions'); }),
|
|
169
|
+
initRouteLayers: mock(() => { calls.push('initLayers'); }),
|
|
170
|
+
renderRouteLayers: mock(() => { calls.push('renderLayersUI'); }),
|
|
171
|
+
restoreRouteViewport: mock(() => { calls.push('restoreViewport'); }),
|
|
172
|
+
updateRouteCanvasTransform: mock(() => { calls.push('updateCanvasTransform'); }),
|
|
173
|
+
updateRouteZoomUi: mock(() => { calls.push('updateZoomUI'); }),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect((document.getElementById('repoSelect') as HTMLSelectElement).value).toBe('C:/Code/gitmaps');
|
|
177
|
+
expect(ctx.snap().context.repoPath).toBe('C:/Code/gitmaps');
|
|
178
|
+
expect(calls).toEqual([
|
|
179
|
+
'syncSelection:C:/Code/gitmaps',
|
|
180
|
+
'send:LOAD_REPO:C:/Code/gitmaps',
|
|
181
|
+
'loadPositions',
|
|
182
|
+
'applySharedLayout',
|
|
183
|
+
'initLayers',
|
|
184
|
+
'renderLayersUI',
|
|
185
|
+
'restoreViewport',
|
|
186
|
+
'updateCanvasTransform',
|
|
187
|
+
'updateZoomUI',
|
|
188
|
+
]);
|
|
189
|
+
} finally {
|
|
190
|
+
handle.cleanup();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('bootstrap initial route ui stops after positions when disposed', async () => {
|
|
195
|
+
const handle = setupDomTest();
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const applySharedLayout = mock(async () => undefined);
|
|
199
|
+
const initRouteLayers = mock(() => undefined);
|
|
200
|
+
const snapshot = { context: { repoPath: '' } };
|
|
201
|
+
const ctx = {
|
|
202
|
+
actor: { send: mock(() => undefined) },
|
|
203
|
+
snap: () => snapshot,
|
|
204
|
+
} as any;
|
|
205
|
+
|
|
206
|
+
await bootstrapInitialRouteUi(ctx, 'C:/Code/gitmaps', {
|
|
207
|
+
disposed: true,
|
|
208
|
+
applySharedLayout,
|
|
209
|
+
loadPositions: mock(async () => undefined),
|
|
210
|
+
initRouteLayers,
|
|
211
|
+
renderRouteLayers: mock(() => undefined),
|
|
212
|
+
restoreRouteViewport: mock(() => undefined),
|
|
213
|
+
updateRouteCanvasTransform: mock(() => undefined),
|
|
214
|
+
updateRouteZoomUi: mock(() => undefined),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(ctx.snap().context.repoPath).toBe('C:/Code/gitmaps');
|
|
218
|
+
expect(applySharedLayout).not.toHaveBeenCalled();
|
|
219
|
+
expect(initRouteLayers).not.toHaveBeenCalled();
|
|
220
|
+
} finally {
|
|
221
|
+
handle.cleanup();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('hydrates initial route through shared repo handoff seam', async () => {
|
|
226
|
+
const handle = setupDomTest({
|
|
227
|
+
url: 'http://localhost:3335/team/platform/tools/gitmaps',
|
|
228
|
+
html: '<select id="repoSelect"><option value="">Select</option><option value="C:/Code/gitmaps">gitmaps</option></select><div id="landingOverlay"></div>',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const onRepoReady = mock(() => undefined);
|
|
233
|
+
const bootstrapRepoUi = mock(async () => undefined);
|
|
234
|
+
const showLandingPlaceholder = mock(() => undefined);
|
|
235
|
+
const updateFavoriteStar = mock(() => undefined);
|
|
236
|
+
const hideLanding = mock(() => {
|
|
237
|
+
const landing = document.getElementById('landingOverlay') as HTMLElement;
|
|
238
|
+
landing.style.display = 'none';
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const resolvedPath = await hydrateInitialRouteRepo({ onRepoReady } as any, {
|
|
242
|
+
resolveRepoPath: async () => 'C:/Code/gitmaps',
|
|
243
|
+
showLandingPlaceholder,
|
|
244
|
+
hideLanding,
|
|
245
|
+
bootstrapRepoUi,
|
|
246
|
+
updateFavoriteStar,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(resolvedPath).toBe('C:/Code/gitmaps');
|
|
250
|
+
expect((document.getElementById('repoSelect') as HTMLSelectElement).value).toBe('C:/Code/gitmaps');
|
|
251
|
+
expect(hideLanding).toHaveBeenCalledTimes(1);
|
|
252
|
+
expect(bootstrapRepoUi).toHaveBeenCalledWith('C:/Code/gitmaps');
|
|
253
|
+
expect(onRepoReady).toHaveBeenCalledWith('C:/Code/gitmaps');
|
|
254
|
+
expect(updateFavoriteStar).toHaveBeenCalledWith('C:/Code/gitmaps');
|
|
255
|
+
expect(showLandingPlaceholder).not.toHaveBeenCalled();
|
|
256
|
+
} finally {
|
|
257
|
+
handle.cleanup();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('shows landing on root route without trying hydration', async () => {
|
|
262
|
+
const handle = setupDomTest({ url: 'http://localhost:3335/' });
|
|
263
|
+
try {
|
|
264
|
+
const bootstrapRepoUi = mock(async () => undefined);
|
|
265
|
+
const showLandingPlaceholder = mock(() => undefined);
|
|
266
|
+
const updateFavoriteStar = mock(() => undefined);
|
|
267
|
+
|
|
268
|
+
const resolvedPath = await hydrateInitialRouteRepo({} as any, {
|
|
269
|
+
showLandingPlaceholder,
|
|
270
|
+
hideLanding: mock(() => undefined),
|
|
271
|
+
bootstrapRepoUi,
|
|
272
|
+
updateFavoriteStar,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(resolvedPath).toBeNull();
|
|
276
|
+
expect(showLandingPlaceholder).toHaveBeenCalledTimes(1);
|
|
277
|
+
expect(bootstrapRepoUi).not.toHaveBeenCalled();
|
|
278
|
+
expect(updateFavoriteStar).not.toHaveBeenCalled();
|
|
279
|
+
} finally {
|
|
280
|
+
handle.cleanup();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { CanvasContext } from './context';
|
|
2
|
+
import { loadSavedPositions } from './positions';
|
|
3
|
+
import { restoreViewport, updateCanvasTransform, updateZoomUI } from './canvas';
|
|
4
|
+
import { initLayers, renderLayersUI } from './layers';
|
|
5
|
+
import { handoffRepoLoad, syncRepoSelection } from './repo-handoff';
|
|
6
|
+
|
|
7
|
+
export interface InitialRouteParts {
|
|
8
|
+
rawPath: string;
|
|
9
|
+
pathSlug: string;
|
|
10
|
+
hashSlug: string;
|
|
11
|
+
urlSlug: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getInitialRouteParts(
|
|
15
|
+
pathname = window.location.pathname,
|
|
16
|
+
hash = window.location.hash,
|
|
17
|
+
): InitialRouteParts {
|
|
18
|
+
const rawPath = decodeURIComponent(pathname.replace(/^\//, ''));
|
|
19
|
+
const pathSlug = rawPath.replace(/^galaxy-canvas\/?/, '');
|
|
20
|
+
const hashSlug = decodeURIComponent(hash.replace('#', ''));
|
|
21
|
+
return {
|
|
22
|
+
rawPath,
|
|
23
|
+
pathSlug,
|
|
24
|
+
hashSlug,
|
|
25
|
+
urlSlug: pathSlug || hashSlug,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isGithubOwnerRepoSlug(slug: string): boolean {
|
|
30
|
+
return /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(slug) && !slug.includes('\\') && !slug.includes(':');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function resolveInitialRepoPath(
|
|
34
|
+
urlSlug: string,
|
|
35
|
+
options: {
|
|
36
|
+
fetchImpl?: typeof fetch;
|
|
37
|
+
onCloneStart?: (slug: string) => void;
|
|
38
|
+
} = {},
|
|
39
|
+
): Promise<string | null> {
|
|
40
|
+
if (!urlSlug) return null;
|
|
41
|
+
|
|
42
|
+
if (!isGithubOwnerRepoSlug(urlSlug)) {
|
|
43
|
+
return localStorage.getItem(`gitcanvas:slug:${urlSlug}`) || urlSlug;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cached = localStorage.getItem(`gitcanvas:slug:${urlSlug}`);
|
|
47
|
+
if (cached) return cached;
|
|
48
|
+
|
|
49
|
+
const fetchImpl = options.fetchImpl || fetch;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const resolveRes = await fetchImpl('/api/repo/resolve-slug', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ slug: urlSlug }),
|
|
56
|
+
});
|
|
57
|
+
if (resolveRes.ok) {
|
|
58
|
+
const resolveData = await resolveRes.json();
|
|
59
|
+
if (resolveData?.path) {
|
|
60
|
+
localStorage.setItem(`gitcanvas:slug:${urlSlug}`, resolveData.path);
|
|
61
|
+
return resolveData.path;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Fall through to clone path below.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
options.onCloneStart?.(urlSlug);
|
|
69
|
+
const cloneRes = await fetchImpl('/api/repo/clone', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({
|
|
73
|
+
url: `https://github.com/${urlSlug}.git`,
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (!cloneRes.ok) {
|
|
78
|
+
const err = await cloneRes.json().catch(() => ({ error: 'Clone failed' }));
|
|
79
|
+
throw new Error(err.error || 'Clone failed');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const cloneData = await cloneRes.json();
|
|
83
|
+
localStorage.setItem(`gitcanvas:slug:${urlSlug}`, cloneData.path);
|
|
84
|
+
return cloneData.path;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function hideInitialRouteLanding() {
|
|
88
|
+
const landing = document.getElementById('landingOverlay');
|
|
89
|
+
if (landing) landing.style.display = 'none';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function migrateLegacyHashRoute(hashSlug: string) {
|
|
93
|
+
window.history.replaceState(null, '', '/' + encodeURIComponent(hashSlug));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function showInitialRouteCloneStart(slug: string) {
|
|
97
|
+
hideInitialRouteLanding();
|
|
98
|
+
|
|
99
|
+
const loadingEl = document.getElementById('loadingProgress');
|
|
100
|
+
if (loadingEl) {
|
|
101
|
+
loadingEl.style.display = 'flex';
|
|
102
|
+
const msgEl = loadingEl.querySelector('.loading-message');
|
|
103
|
+
if (msgEl) {
|
|
104
|
+
msgEl.textContent = `Cloning ${slug} from GitHub...`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function handleInitialRouteError(err: any, options?: {
|
|
110
|
+
reportError?: (err: any) => void;
|
|
111
|
+
showToast?: (message: string, type: string) => void;
|
|
112
|
+
}) {
|
|
113
|
+
const reportError = options?.reportError || ((error) => {
|
|
114
|
+
console.error(`[gitmaps] Failed to hydrate initial route:`, error);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
reportError(err);
|
|
118
|
+
|
|
119
|
+
if (options?.showToast) {
|
|
120
|
+
options.showToast(`Failed to load route: ${err.message}`, 'error');
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { showToast } = await import('./utils');
|
|
125
|
+
showToast(`Failed to load route: ${err.message}`, 'error');
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function bootstrapInitialRouteUi(
|
|
130
|
+
ctx: CanvasContext,
|
|
131
|
+
resolvedPath: string,
|
|
132
|
+
options: {
|
|
133
|
+
disposed?: boolean;
|
|
134
|
+
applySharedLayout: () => Promise<void>;
|
|
135
|
+
syncSelection?: (path: string) => void;
|
|
136
|
+
loadPositions?: (ctx: CanvasContext) => Promise<void>;
|
|
137
|
+
initRouteLayers?: (ctx: CanvasContext) => void;
|
|
138
|
+
renderRouteLayers?: (ctx: CanvasContext) => void;
|
|
139
|
+
restoreRouteViewport?: (ctx: CanvasContext) => void;
|
|
140
|
+
updateRouteCanvasTransform?: (ctx: CanvasContext) => void;
|
|
141
|
+
updateRouteZoomUi?: (ctx: CanvasContext) => void;
|
|
142
|
+
},
|
|
143
|
+
) {
|
|
144
|
+
const syncSelection = options.syncSelection || syncRepoSelection;
|
|
145
|
+
const loadPositions = options.loadPositions || loadSavedPositions;
|
|
146
|
+
const initRouteLayers = options.initRouteLayers || initLayers;
|
|
147
|
+
const renderRouteLayers = options.renderRouteLayers || renderLayersUI;
|
|
148
|
+
const restoreRouteViewport = options.restoreRouteViewport || restoreViewport;
|
|
149
|
+
const updateRouteCanvasTransform = options.updateRouteCanvasTransform || updateCanvasTransform;
|
|
150
|
+
const updateRouteZoomUi = options.updateRouteZoomUi || updateZoomUI;
|
|
151
|
+
|
|
152
|
+
syncSelection(resolvedPath);
|
|
153
|
+
ctx.actor.send({ type: 'LOAD_REPO', path: resolvedPath });
|
|
154
|
+
ctx.snap().context.repoPath = resolvedPath;
|
|
155
|
+
await loadPositions(ctx);
|
|
156
|
+
if (options.disposed) return;
|
|
157
|
+
await options.applySharedLayout();
|
|
158
|
+
initRouteLayers(ctx);
|
|
159
|
+
renderRouteLayers(ctx);
|
|
160
|
+
restoreRouteViewport(ctx);
|
|
161
|
+
updateRouteCanvasTransform(ctx);
|
|
162
|
+
updateRouteZoomUi(ctx);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function hydrateInitialRouteRepo(
|
|
166
|
+
ctx: CanvasContext,
|
|
167
|
+
options: {
|
|
168
|
+
disposed?: boolean;
|
|
169
|
+
resolveRepoPath?: (slug: string) => Promise<string | null>;
|
|
170
|
+
showLandingPlaceholder: () => void;
|
|
171
|
+
hideLanding: () => void;
|
|
172
|
+
migrateLegacyHashRoute?: (hashSlug: string) => void;
|
|
173
|
+
bootstrapRepoUi: (path: string) => Promise<void>;
|
|
174
|
+
updateFavoriteStar: (path: string) => void;
|
|
175
|
+
},
|
|
176
|
+
) {
|
|
177
|
+
const { pathSlug, hashSlug, urlSlug } = getInitialRouteParts();
|
|
178
|
+
|
|
179
|
+
if (!urlSlug) {
|
|
180
|
+
options.showLandingPlaceholder();
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (hashSlug && !pathSlug) {
|
|
185
|
+
options.migrateLegacyHashRoute?.(hashSlug);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const resolveRepoPath = options.resolveRepoPath || ((slug: string) => resolveInitialRepoPath(slug));
|
|
189
|
+
const resolvedPath = await resolveRepoPath(urlSlug);
|
|
190
|
+
if (!resolvedPath) return null;
|
|
191
|
+
|
|
192
|
+
options.hideLanding();
|
|
193
|
+
syncRepoSelection(resolvedPath);
|
|
194
|
+
await options.bootstrapRepoUi(resolvedPath);
|
|
195
|
+
|
|
196
|
+
if (!options.disposed) {
|
|
197
|
+
handoffRepoLoad(ctx, resolvedPath);
|
|
198
|
+
options.updateFavoriteStar(resolvedPath);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return resolvedPath;
|
|
202
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from 'bun:test';
|
|
2
|
+
import { showLandingPlaceholder } from './landing-reset';
|
|
3
|
+
import { setupDomTest } from './test-dom';
|
|
4
|
+
|
|
5
|
+
describe('landing reset helper', () => {
|
|
6
|
+
test('clears repo ui and resets shared canvas state back to landing mode', () => {
|
|
7
|
+
const handle = setupDomTest({
|
|
8
|
+
html: `
|
|
9
|
+
<div id="landingOverlay" style="display:none"></div>
|
|
10
|
+
<select id="repoSelect"><option value="">Select</option><option value="C:/Code/gitmaps" selected>gitmaps</option></select>
|
|
11
|
+
<div id="fileCount">42</div>
|
|
12
|
+
<div id="commitCount">9</div>
|
|
13
|
+
<div id="currentCommitInfo">commit info</div>
|
|
14
|
+
<div id="timelineContainer">timeline</div>
|
|
15
|
+
<div id="changedFilesList">changed</div>
|
|
16
|
+
<div id="changedFilesPanel" style="display:flex"></div>
|
|
17
|
+
<div id="connectionsPanel" style="display:flex"></div>
|
|
18
|
+
<div id="arrangeToolbar" style="display:flex"></div>
|
|
19
|
+
<button id="toggleConnections" class="active"></button>
|
|
20
|
+
<button id="showHidden" style="display:block"></button>
|
|
21
|
+
<span id="hiddenCount">7</span>
|
|
22
|
+
<div id="commitProgressBar" style="display:flex"></div>
|
|
23
|
+
`,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const snapshot = { context: { repoPath: 'C:/Code/gitmaps' } };
|
|
28
|
+
const ctx = {
|
|
29
|
+
actor: { send: mock(() => undefined) },
|
|
30
|
+
snap: () => snapshot,
|
|
31
|
+
fileCards: new Map([['a.ts', document.createElement('div')]]),
|
|
32
|
+
deferredCards: new Map([['b.ts', { x: 0, y: 0, size: null, isChanged: false, file: {} }]]),
|
|
33
|
+
allFilesData: [{ path: 'a.ts' }],
|
|
34
|
+
commitFilesData: [{ path: 'b.ts' }],
|
|
35
|
+
changedFilePaths: new Set(['a.ts']),
|
|
36
|
+
} as any;
|
|
37
|
+
|
|
38
|
+
const clearWorkspace = mock(() => undefined);
|
|
39
|
+
const clearCanvasUi = mock(() => undefined);
|
|
40
|
+
const hideLoading = mock(() => undefined);
|
|
41
|
+
const updateCanvas = mock(() => undefined);
|
|
42
|
+
const updateZoom = mock(() => undefined);
|
|
43
|
+
const updateFavorite = mock(() => undefined);
|
|
44
|
+
const updateRepoStatus = mock(() => undefined);
|
|
45
|
+
const updateCommitStatus = mock(() => undefined);
|
|
46
|
+
const updateFileStatus = mock(() => undefined);
|
|
47
|
+
const updateSelectedStatus = mock(() => undefined);
|
|
48
|
+
|
|
49
|
+
showLandingPlaceholder(ctx, {
|
|
50
|
+
clearWorkspace,
|
|
51
|
+
clearCanvasUi,
|
|
52
|
+
hideLoading,
|
|
53
|
+
updateCanvas,
|
|
54
|
+
updateZoom,
|
|
55
|
+
updateFavorite,
|
|
56
|
+
updateRepoStatus,
|
|
57
|
+
updateCommitStatus,
|
|
58
|
+
updateFileStatus,
|
|
59
|
+
updateSelectedStatus,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(document.body.classList.contains('landing-placeholder-visible')).toBe(true);
|
|
63
|
+
expect(ctx.actor.send).toHaveBeenCalledWith({ type: 'RESET_APP_STATE' });
|
|
64
|
+
expect(clearWorkspace).toHaveBeenCalledWith(ctx);
|
|
65
|
+
expect(clearCanvasUi).toHaveBeenCalledWith(ctx);
|
|
66
|
+
expect(snapshot.context.repoPath).toBe('');
|
|
67
|
+
expect(ctx.fileCards.size).toBe(0);
|
|
68
|
+
expect(ctx.deferredCards.size).toBe(0);
|
|
69
|
+
expect(ctx.allFilesData).toEqual([]);
|
|
70
|
+
expect(ctx.commitFilesData).toEqual([]);
|
|
71
|
+
expect(Array.from(ctx.changedFilePaths)).toEqual([]);
|
|
72
|
+
expect((document.getElementById('landingOverlay') as HTMLElement).style.display).toBe('flex');
|
|
73
|
+
expect((document.getElementById('repoSelect') as HTMLSelectElement).value).toBe('');
|
|
74
|
+
expect(document.getElementById('fileCount')?.textContent).toBe('0');
|
|
75
|
+
expect(document.getElementById('commitCount')?.textContent).toBe('0');
|
|
76
|
+
expect(document.getElementById('currentCommitInfo')?.textContent).toContain('No commit selected');
|
|
77
|
+
expect(document.getElementById('timelineContainer')?.textContent).toContain('Load a repository');
|
|
78
|
+
expect(document.getElementById('changedFilesList')?.textContent).toBe('');
|
|
79
|
+
expect((document.getElementById('changedFilesPanel') as HTMLElement).style.display).toBe('none');
|
|
80
|
+
expect((document.getElementById('connectionsPanel') as HTMLElement).style.display).toBe('none');
|
|
81
|
+
expect((document.getElementById('arrangeToolbar') as HTMLElement).style.display).toBe('none');
|
|
82
|
+
expect(document.getElementById('toggleConnections')?.classList.contains('active')).toBe(false);
|
|
83
|
+
expect((document.getElementById('showHidden') as HTMLElement).style.display).toBe('none');
|
|
84
|
+
expect(document.getElementById('hiddenCount')?.textContent).toBe('0');
|
|
85
|
+
expect((document.getElementById('commitProgressBar') as HTMLElement).style.display).toBe('none');
|
|
86
|
+
expect(localStorage.getItem('gitcanvas:changedFilesPanelClosed')).toBe('true');
|
|
87
|
+
expect(hideLoading).toHaveBeenCalledWith(ctx);
|
|
88
|
+
expect(updateCanvas).toHaveBeenCalledWith(ctx);
|
|
89
|
+
expect(updateZoom).toHaveBeenCalledWith(ctx);
|
|
90
|
+
expect(updateFavorite).toHaveBeenCalledWith('');
|
|
91
|
+
expect(updateRepoStatus).toHaveBeenCalledWith('', '', '');
|
|
92
|
+
expect(updateCommitStatus).toHaveBeenCalledWith('');
|
|
93
|
+
expect(updateFileStatus).toHaveBeenCalledWith(0);
|
|
94
|
+
expect(updateSelectedStatus).toHaveBeenCalledWith(0);
|
|
95
|
+
} finally {
|
|
96
|
+
handle.cleanup();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { CanvasContext } from './context';
|
|
2
|
+
import { clearCanvas, updateCanvasTransform, updateZoomUI } from './canvas';
|
|
3
|
+
import { hideLoadingProgress } from './loading';
|
|
4
|
+
import { clearMultiRepoWorkspace } from './multi-repo';
|
|
5
|
+
import {
|
|
6
|
+
updateStatusBarCommit,
|
|
7
|
+
updateStatusBarFiles,
|
|
8
|
+
updateStatusBarRepo,
|
|
9
|
+
updateStatusBarSelected,
|
|
10
|
+
} from './status-bar';
|
|
11
|
+
import { updateFavoriteStar } from './user';
|
|
12
|
+
|
|
13
|
+
export function showLandingPlaceholder(
|
|
14
|
+
ctx: CanvasContext,
|
|
15
|
+
options: {
|
|
16
|
+
clearWorkspace?: (ctx: CanvasContext) => void;
|
|
17
|
+
clearCanvasUi?: (ctx: CanvasContext) => void;
|
|
18
|
+
hideLoading?: (ctx: CanvasContext) => void;
|
|
19
|
+
updateCanvas?: (ctx: CanvasContext) => void;
|
|
20
|
+
updateZoom?: (ctx: CanvasContext) => void;
|
|
21
|
+
updateFavorite?: (repoPath: string) => void;
|
|
22
|
+
updateRepoStatus?: (repoPath: string, canonicalSlug?: string, canonicalSource?: string) => void;
|
|
23
|
+
updateCommitStatus?: (hash: string) => void;
|
|
24
|
+
updateFileStatus?: (count: number) => void;
|
|
25
|
+
updateSelectedStatus?: (count: number) => void;
|
|
26
|
+
} = {},
|
|
27
|
+
) {
|
|
28
|
+
const clearWorkspace = options.clearWorkspace || clearMultiRepoWorkspace;
|
|
29
|
+
const clearCanvasUi = options.clearCanvasUi || clearCanvas;
|
|
30
|
+
const hideLoading = options.hideLoading || hideLoadingProgress;
|
|
31
|
+
const updateCanvas = options.updateCanvas || updateCanvasTransform;
|
|
32
|
+
const updateZoom = options.updateZoom || updateZoomUI;
|
|
33
|
+
const updateFavorite = options.updateFavorite || updateFavoriteStar;
|
|
34
|
+
const updateRepoStatus = options.updateRepoStatus || updateStatusBarRepo;
|
|
35
|
+
const updateCommitStatus = options.updateCommitStatus || updateStatusBarCommit;
|
|
36
|
+
const updateFileStatus = options.updateFileStatus || updateStatusBarFiles;
|
|
37
|
+
const updateSelectedStatus = options.updateSelectedStatus || updateStatusBarSelected;
|
|
38
|
+
|
|
39
|
+
document.body.classList.add('landing-placeholder-visible');
|
|
40
|
+
ctx.actor.send({ type: 'RESET_APP_STATE' });
|
|
41
|
+
clearWorkspace(ctx);
|
|
42
|
+
clearCanvasUi(ctx);
|
|
43
|
+
ctx.fileCards.clear();
|
|
44
|
+
ctx.deferredCards.clear();
|
|
45
|
+
ctx.allFilesData = [];
|
|
46
|
+
ctx.commitFilesData = [];
|
|
47
|
+
ctx.changedFilePaths = new Set();
|
|
48
|
+
ctx.snap().context.repoPath = '';
|
|
49
|
+
|
|
50
|
+
const landing = document.getElementById('landingOverlay');
|
|
51
|
+
if (landing) landing.style.display = 'flex';
|
|
52
|
+
|
|
53
|
+
const repoSelect = document.getElementById('repoSelect') as HTMLSelectElement | null;
|
|
54
|
+
if (repoSelect) repoSelect.value = '';
|
|
55
|
+
|
|
56
|
+
const fileCount = document.getElementById('fileCount');
|
|
57
|
+
if (fileCount) fileCount.textContent = '0';
|
|
58
|
+
|
|
59
|
+
const commitCount = document.getElementById('commitCount');
|
|
60
|
+
if (commitCount) commitCount.textContent = '0';
|
|
61
|
+
|
|
62
|
+
const commitInfo = document.getElementById('currentCommitInfo');
|
|
63
|
+
if (commitInfo) {
|
|
64
|
+
commitInfo.innerHTML = '<span class="commit-hash-label">No commit selected</span>';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const timeline = document.getElementById('timelineContainer');
|
|
68
|
+
if (timeline) {
|
|
69
|
+
timeline.innerHTML = '<div class="empty-state"><svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.3"><circle cx="12" cy="12" r="10"></circle><path d="M12 6v6l4 2"></path></svg><p>Load a repository</p></div>';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const changedFilesList = document.getElementById('changedFilesList');
|
|
73
|
+
if (changedFilesList) changedFilesList.innerHTML = '';
|
|
74
|
+
|
|
75
|
+
const changedFilesPanel = document.getElementById('changedFilesPanel');
|
|
76
|
+
if (changedFilesPanel) changedFilesPanel.style.display = 'none';
|
|
77
|
+
|
|
78
|
+
const connectionsPanel = document.getElementById('connectionsPanel');
|
|
79
|
+
if (connectionsPanel) connectionsPanel.style.display = 'none';
|
|
80
|
+
|
|
81
|
+
const arrangeToolbar = document.getElementById('arrangeToolbar');
|
|
82
|
+
if (arrangeToolbar) arrangeToolbar.style.display = 'none';
|
|
83
|
+
|
|
84
|
+
const toggleConnections = document.getElementById('toggleConnections');
|
|
85
|
+
if (toggleConnections) toggleConnections.classList.remove('active');
|
|
86
|
+
|
|
87
|
+
const showHidden = document.getElementById('showHidden');
|
|
88
|
+
if (showHidden) showHidden.style.display = 'none';
|
|
89
|
+
|
|
90
|
+
const hiddenCount = document.getElementById('hiddenCount');
|
|
91
|
+
if (hiddenCount) hiddenCount.textContent = '0';
|
|
92
|
+
|
|
93
|
+
const commitProgressBar = document.getElementById('commitProgressBar');
|
|
94
|
+
if (commitProgressBar) commitProgressBar.style.display = 'none';
|
|
95
|
+
|
|
96
|
+
localStorage.setItem('gitcanvas:changedFilesPanelClosed', 'true');
|
|
97
|
+
|
|
98
|
+
hideLoading(ctx);
|
|
99
|
+
updateCanvas(ctx);
|
|
100
|
+
updateZoom(ctx);
|
|
101
|
+
updateFavorite('');
|
|
102
|
+
updateRepoStatus('', '', '');
|
|
103
|
+
updateCommitStatus('');
|
|
104
|
+
updateFileStatus(0);
|
|
105
|
+
updateSelectedStatus(0);
|
|
106
|
+
}
|