xtralab 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/lib/editorBreadcrumbs/index.d.ts +11 -0
- package/lib/editorBreadcrumbs/index.js +41 -0
- package/lib/editorBreadcrumbs/widget.d.ts +34 -0
- package/lib/editorBreadcrumbs/widget.js +131 -0
- package/lib/editorIndent/index.d.ts +17 -0
- package/lib/editorIndent/index.js +142 -0
- package/lib/fileBrowser/commands.d.ts +3 -0
- package/lib/fileBrowser/commands.js +40 -1
- package/lib/fileBrowser/fileBrowser.js +393 -10
- package/lib/fileBrowser/gitStatus.d.ts +13 -0
- package/lib/fileBrowser/gitStatus.js +60 -0
- package/lib/fileBrowser/widget.d.ts +48 -0
- package/lib/fileBrowser/widget.js +24 -0
- package/lib/git/commands.d.ts +12 -13
- package/lib/git/commands.js +7 -28
- package/lib/git/diffProvider.d.ts +8 -0
- package/lib/git/diffProvider.js +314 -0
- package/lib/git/diffSurface.d.ts +115 -0
- package/lib/git/diffSurface.js +342 -0
- package/lib/git/diffWidget.d.ts +8 -13
- package/lib/git/diffWidget.js +65 -311
- package/lib/git/imageDiff.d.ts +23 -0
- package/lib/git/imageDiff.js +184 -0
- package/lib/git/index.d.ts +10 -8
- package/lib/git/index.js +37 -52
- package/lib/index.d.ts +3 -2
- package/lib/index.js +9 -4
- package/lib/launcher/dashboard.js +27 -15
- package/package.json +24 -4
- package/src/editorBreadcrumbs/index.ts +70 -0
- package/src/editorBreadcrumbs/widget.ts +158 -0
- package/src/editorIndent/index.ts +177 -0
- package/src/fileBrowser/commands.ts +43 -1
- package/src/fileBrowser/fileBrowser.tsx +427 -11
- package/src/fileBrowser/gitStatus.ts +66 -0
- package/src/fileBrowser/widget.tsx +77 -0
- package/src/git/commands.ts +12 -38
- package/src/git/diffProvider.tsx +462 -0
- package/src/git/diffSurface.tsx +588 -0
- package/src/git/diffWidget.tsx +105 -465
- package/src/git/imageDiff.tsx +368 -0
- package/src/git/index.ts +38 -68
- package/src/index.ts +9 -4
- package/src/launcher/dashboard.tsx +28 -15
- package/style/editorBreadcrumbs.css +86 -0
- package/style/git.css +158 -266
- package/style/index.css +2 -0
- package/style/index.js +1 -0
- package/style/launcher.css +0 -5
- package/style/tabs.css +66 -0
- package/lib/git/icons.d.ts +0 -14
- package/lib/git/icons.js +0 -19
- package/lib/git/panel.d.ts +0 -29
- package/lib/git/panel.js +0 -42
- package/lib/git/panelComponent.d.ts +0 -28
- package/lib/git/panelComponent.js +0 -303
- package/src/git/icons.ts +0 -20
- package/src/git/panel.tsx +0 -56
- package/src/git/panelComponent.tsx +0 -561
|
@@ -1,561 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
|
|
3
|
-
import { ISignal } from '@lumino/signaling';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
add,
|
|
7
|
-
addAll,
|
|
8
|
-
checkout,
|
|
9
|
-
porcelainToFileChanges,
|
|
10
|
-
reset,
|
|
11
|
-
resetAll,
|
|
12
|
-
showTopLevel,
|
|
13
|
-
status
|
|
14
|
-
} from './api';
|
|
15
|
-
import { FileChangeStatus, IFileChange, IGitStatusResult } from './tokens';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Callbacks the panel needs to delegate to (so the React component stays
|
|
19
|
-
* free of any JupyterLab application coupling). The plugin wires these up
|
|
20
|
-
* with the actual `JupyterFrontEnd` reference.
|
|
21
|
-
*/
|
|
22
|
-
export interface IGitPanelHandlers {
|
|
23
|
-
/** Open a diff viewer for the given file in the main area. */
|
|
24
|
-
openDiff(
|
|
25
|
-
change: IFileChange,
|
|
26
|
-
options?: {
|
|
27
|
-
pin?: boolean;
|
|
28
|
-
}
|
|
29
|
-
): void;
|
|
30
|
-
/** Show a confirmation dialog. Returns true when the user accepted. */
|
|
31
|
-
confirm(title: string, body: string, accept: string): Promise<boolean>;
|
|
32
|
-
/** Show an error dialog. */
|
|
33
|
-
showError(title: string, error: Error): Promise<void>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface IPanelState {
|
|
37
|
-
/** True while the initial status request is in flight. */
|
|
38
|
-
loading: boolean;
|
|
39
|
-
/** The raw result of the last status call, or `null` if it hasn't completed yet. */
|
|
40
|
-
result: IGitStatusResult | null;
|
|
41
|
-
/** Set when the status request errored or the path is not in a repo. */
|
|
42
|
-
error: string | null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The polling interval for the panel. We refresh the status periodically so
|
|
47
|
-
* out-of-band changes (e.g. the user runs `git add` from a terminal) become
|
|
48
|
-
* visible without requiring a manual refresh.
|
|
49
|
-
*/
|
|
50
|
-
const STATUS_POLL_INTERVAL_MS = 5000;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Top-level React component rendered inside the {@link GitPanel} widget.
|
|
54
|
-
* Owns the polling + refresh wiring around the `/git/status` endpoint and
|
|
55
|
-
* the small bit of UI state that controls section collapse and the
|
|
56
|
-
* in-flight per-file action spinners.
|
|
57
|
-
*/
|
|
58
|
-
export function GitPanelComponent(props: {
|
|
59
|
-
handlers: IGitPanelHandlers;
|
|
60
|
-
refreshSignal: ISignal<unknown, void>;
|
|
61
|
-
}): React.ReactElement {
|
|
62
|
-
const { handlers, refreshSignal } = props;
|
|
63
|
-
|
|
64
|
-
const [state, setState] = React.useState<IPanelState>({
|
|
65
|
-
loading: true,
|
|
66
|
-
result: null,
|
|
67
|
-
error: null
|
|
68
|
-
});
|
|
69
|
-
// The absolute filesystem path of the resolved git top-level. Used purely
|
|
70
|
-
// for display in the panel header.
|
|
71
|
-
const [topLevel, setTopLevel] = React.useState<string | null>(null);
|
|
72
|
-
// Per-action spinner gating: keyed by `${group}:${path}`, value is the
|
|
73
|
-
// action currently in flight. Lets the row render a disabled state on
|
|
74
|
-
// exactly the file that is being mutated, without locking the whole panel.
|
|
75
|
-
const [pendingActions, setPendingActions] = React.useState<
|
|
76
|
-
Record<string, string>
|
|
77
|
-
>({});
|
|
78
|
-
const [stagedCollapsed, setStagedCollapsed] = React.useState(false);
|
|
79
|
-
const [changesCollapsed, setChangesCollapsed] = React.useState(false);
|
|
80
|
-
|
|
81
|
-
// The repo path passed to every API call. We use the JupyterLab server's
|
|
82
|
-
// root (the empty string) and let `git` resolve the enclosing repo on the
|
|
83
|
-
// server side. This works as long as the server was started inside a git
|
|
84
|
-
// repo — the common case.
|
|
85
|
-
const repoPath = '';
|
|
86
|
-
|
|
87
|
-
const refresh = React.useCallback(async (): Promise<void> => {
|
|
88
|
-
try {
|
|
89
|
-
const [topLevelResult, statusResult] = await Promise.all([
|
|
90
|
-
showTopLevel(repoPath),
|
|
91
|
-
status(repoPath)
|
|
92
|
-
]);
|
|
93
|
-
setTopLevel(topLevelResult);
|
|
94
|
-
if (statusResult.code !== 0) {
|
|
95
|
-
setState({
|
|
96
|
-
loading: false,
|
|
97
|
-
result: null,
|
|
98
|
-
error: statusResult.message ?? 'Failed to load git status'
|
|
99
|
-
});
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
setState({ loading: false, result: statusResult, error: null });
|
|
103
|
-
} catch (err) {
|
|
104
|
-
setState({
|
|
105
|
-
loading: false,
|
|
106
|
-
result: null,
|
|
107
|
-
error: err instanceof Error ? err.message : String(err)
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}, [repoPath]);
|
|
111
|
-
|
|
112
|
-
// Initial load + polling. `refresh` is stable for a given `repoPath`, so
|
|
113
|
-
// the polling interval is only re-created when the repo path changes.
|
|
114
|
-
React.useEffect(() => {
|
|
115
|
-
void refresh();
|
|
116
|
-
const interval = window.setInterval(
|
|
117
|
-
() => void refresh(),
|
|
118
|
-
STATUS_POLL_INTERVAL_MS
|
|
119
|
-
);
|
|
120
|
-
return () => {
|
|
121
|
-
window.clearInterval(interval);
|
|
122
|
-
};
|
|
123
|
-
}, [refresh]);
|
|
124
|
-
|
|
125
|
-
// External refresh triggers (toolbar button, plugin-level events).
|
|
126
|
-
React.useEffect(() => {
|
|
127
|
-
const handler = (): void => {
|
|
128
|
-
void refresh();
|
|
129
|
-
};
|
|
130
|
-
refreshSignal.connect(handler);
|
|
131
|
-
return () => {
|
|
132
|
-
refreshSignal.disconnect(handler);
|
|
133
|
-
};
|
|
134
|
-
}, [refresh, refreshSignal]);
|
|
135
|
-
|
|
136
|
-
const runAction = React.useCallback(
|
|
137
|
-
async (
|
|
138
|
-
change: IFileChange,
|
|
139
|
-
action: string,
|
|
140
|
-
run: () => Promise<void>
|
|
141
|
-
): Promise<void> => {
|
|
142
|
-
const key = `${change.group}:${change.path}`;
|
|
143
|
-
setPendingActions(prev => ({ ...prev, [key]: action }));
|
|
144
|
-
try {
|
|
145
|
-
await run();
|
|
146
|
-
await refresh();
|
|
147
|
-
} catch (err) {
|
|
148
|
-
await handlers.showError(
|
|
149
|
-
'Git operation failed',
|
|
150
|
-
err instanceof Error ? err : new Error(String(err))
|
|
151
|
-
);
|
|
152
|
-
} finally {
|
|
153
|
-
setPendingActions(prev => {
|
|
154
|
-
const next = { ...prev };
|
|
155
|
-
delete next[key];
|
|
156
|
-
return next;
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
[handlers, refresh]
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
const onStage = React.useCallback(
|
|
164
|
-
(change: IFileChange) =>
|
|
165
|
-
runAction(change, 'stage', () => add(repoPath, change.path)),
|
|
166
|
-
[repoPath, runAction]
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const onUnstage = React.useCallback(
|
|
170
|
-
(change: IFileChange) =>
|
|
171
|
-
runAction(change, 'unstage', () => reset(repoPath, change.path)),
|
|
172
|
-
[repoPath, runAction]
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
const onDiscard = React.useCallback(
|
|
176
|
-
async (change: IFileChange): Promise<void> => {
|
|
177
|
-
const verb = change.status === 'untracked' ? 'remove' : 'restore';
|
|
178
|
-
const accepted = await handlers.confirm(
|
|
179
|
-
verb === 'remove' ? 'Remove file' : 'Discard changes',
|
|
180
|
-
verb === 'remove'
|
|
181
|
-
? `Permanently remove ${change.path}? This cannot be undone.`
|
|
182
|
-
: `Discard all changes to ${change.path}? This will revert the file to its last staged version and cannot be undone.`,
|
|
183
|
-
verb === 'remove' ? 'Remove' : 'Discard'
|
|
184
|
-
);
|
|
185
|
-
if (!accepted) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
// The git extension's `checkout` endpoint without a branch reverts the
|
|
189
|
-
// working tree copy of the file to the index. For untracked files we
|
|
190
|
-
// can't checkout, so the user is told the action is unsupported and we
|
|
191
|
-
// bail out — they can delete the file via the file browser instead.
|
|
192
|
-
if (change.status === 'untracked') {
|
|
193
|
-
await handlers.showError(
|
|
194
|
-
'Cannot discard untracked file',
|
|
195
|
-
new Error(
|
|
196
|
-
'Untracked files cannot be discarded from the git panel — delete them from the file browser instead.'
|
|
197
|
-
)
|
|
198
|
-
);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
await runAction(change, 'discard', () => checkout(repoPath, change.path));
|
|
202
|
-
},
|
|
203
|
-
[handlers, repoPath, runAction]
|
|
204
|
-
);
|
|
205
|
-
|
|
206
|
-
const onStageAll = React.useCallback(async (): Promise<void> => {
|
|
207
|
-
try {
|
|
208
|
-
await addAll(repoPath);
|
|
209
|
-
await refresh();
|
|
210
|
-
} catch (err) {
|
|
211
|
-
await handlers.showError(
|
|
212
|
-
'Stage all failed',
|
|
213
|
-
err instanceof Error ? err : new Error(String(err))
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}, [handlers, refresh, repoPath]);
|
|
217
|
-
|
|
218
|
-
const onUnstageAll = React.useCallback(async (): Promise<void> => {
|
|
219
|
-
try {
|
|
220
|
-
await resetAll(repoPath);
|
|
221
|
-
await refresh();
|
|
222
|
-
} catch (err) {
|
|
223
|
-
await handlers.showError(
|
|
224
|
-
'Unstage all failed',
|
|
225
|
-
err instanceof Error ? err : new Error(String(err))
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}, [handlers, refresh, repoPath]);
|
|
229
|
-
|
|
230
|
-
// Categorize the porcelain entries into the two visible groups. Because
|
|
231
|
-
// `porcelainToFileChanges` may emit two changes per file (one staged, one
|
|
232
|
-
// unstaged), the same file can show up in both lists when it has been
|
|
233
|
-
// partially staged.
|
|
234
|
-
const { staged, unstaged } = React.useMemo(() => {
|
|
235
|
-
const stagedAcc: IFileChange[] = [];
|
|
236
|
-
const unstagedAcc: IFileChange[] = [];
|
|
237
|
-
if (state.result === null) {
|
|
238
|
-
return { staged: stagedAcc, unstaged: unstagedAcc };
|
|
239
|
-
}
|
|
240
|
-
for (const file of state.result.files) {
|
|
241
|
-
for (const change of porcelainToFileChanges(file)) {
|
|
242
|
-
if (change.group === 'staged') {
|
|
243
|
-
stagedAcc.push(change);
|
|
244
|
-
} else {
|
|
245
|
-
unstagedAcc.push(change);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
stagedAcc.sort((a, b) => a.path.localeCompare(b.path));
|
|
250
|
-
unstagedAcc.sort((a, b) => a.path.localeCompare(b.path));
|
|
251
|
-
return { staged: stagedAcc, unstaged: unstagedAcc };
|
|
252
|
-
}, [state.result]);
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<div className="jp-xtralab-GitPanel-content">
|
|
256
|
-
<PanelHeader
|
|
257
|
-
topLevel={topLevel}
|
|
258
|
-
result={state.result}
|
|
259
|
-
loading={state.loading}
|
|
260
|
-
error={state.error}
|
|
261
|
-
onRefresh={() => void refresh()}
|
|
262
|
-
/>
|
|
263
|
-
{state.error !== null ? (
|
|
264
|
-
<div className="jp-xtralab-GitPanel-error">{state.error}</div>
|
|
265
|
-
) : (
|
|
266
|
-
<>
|
|
267
|
-
<ChangeSection
|
|
268
|
-
label="Staged Changes"
|
|
269
|
-
count={staged.length}
|
|
270
|
-
collapsed={stagedCollapsed}
|
|
271
|
-
onToggleCollapsed={() => setStagedCollapsed(c => !c)}
|
|
272
|
-
actions={
|
|
273
|
-
staged.length > 0
|
|
274
|
-
? [
|
|
275
|
-
{
|
|
276
|
-
label: 'Unstage all changes',
|
|
277
|
-
symbol: '−',
|
|
278
|
-
onClick: onUnstageAll
|
|
279
|
-
}
|
|
280
|
-
]
|
|
281
|
-
: undefined
|
|
282
|
-
}
|
|
283
|
-
>
|
|
284
|
-
{staged.map(change => (
|
|
285
|
-
<ChangeRow
|
|
286
|
-
key={`${change.group}:${change.path}`}
|
|
287
|
-
change={change}
|
|
288
|
-
pendingAction={pendingActions[`${change.group}:${change.path}`]}
|
|
289
|
-
onClick={event =>
|
|
290
|
-
handlers.openDiff(change, {
|
|
291
|
-
pin: event.ctrlKey || event.metaKey
|
|
292
|
-
})
|
|
293
|
-
}
|
|
294
|
-
actions={[
|
|
295
|
-
{
|
|
296
|
-
label: 'Unstage changes',
|
|
297
|
-
symbol: '−',
|
|
298
|
-
onClick: () => void onUnstage(change)
|
|
299
|
-
}
|
|
300
|
-
]}
|
|
301
|
-
/>
|
|
302
|
-
))}
|
|
303
|
-
</ChangeSection>
|
|
304
|
-
<ChangeSection
|
|
305
|
-
label="Changes"
|
|
306
|
-
count={unstaged.length}
|
|
307
|
-
collapsed={changesCollapsed}
|
|
308
|
-
onToggleCollapsed={() => setChangesCollapsed(c => !c)}
|
|
309
|
-
actions={
|
|
310
|
-
unstaged.length > 0
|
|
311
|
-
? [
|
|
312
|
-
{
|
|
313
|
-
label: 'Stage all changes',
|
|
314
|
-
symbol: '+',
|
|
315
|
-
onClick: onStageAll
|
|
316
|
-
}
|
|
317
|
-
]
|
|
318
|
-
: undefined
|
|
319
|
-
}
|
|
320
|
-
>
|
|
321
|
-
{unstaged.map(change => (
|
|
322
|
-
<ChangeRow
|
|
323
|
-
key={`${change.group}:${change.path}`}
|
|
324
|
-
change={change}
|
|
325
|
-
pendingAction={pendingActions[`${change.group}:${change.path}`]}
|
|
326
|
-
onClick={event =>
|
|
327
|
-
handlers.openDiff(change, {
|
|
328
|
-
pin: event.ctrlKey || event.metaKey
|
|
329
|
-
})
|
|
330
|
-
}
|
|
331
|
-
actions={[
|
|
332
|
-
{
|
|
333
|
-
label: 'Discard changes',
|
|
334
|
-
symbol: '⟲',
|
|
335
|
-
onClick: () => void onDiscard(change)
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
label: 'Stage changes',
|
|
339
|
-
symbol: '+',
|
|
340
|
-
onClick: () => void onStage(change)
|
|
341
|
-
}
|
|
342
|
-
]}
|
|
343
|
-
/>
|
|
344
|
-
))}
|
|
345
|
-
</ChangeSection>
|
|
346
|
-
{staged.length === 0 && unstaged.length === 0 && !state.loading ? (
|
|
347
|
-
<div className="jp-xtralab-GitPanel-empty">
|
|
348
|
-
No changes detected.
|
|
349
|
-
</div>
|
|
350
|
-
) : null}
|
|
351
|
-
</>
|
|
352
|
-
)}
|
|
353
|
-
</div>
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
interface IRowAction {
|
|
358
|
-
label: string;
|
|
359
|
-
symbol: string;
|
|
360
|
-
onClick: () => void;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function PanelHeader(props: {
|
|
364
|
-
topLevel: string | null;
|
|
365
|
-
result: IGitStatusResult | null;
|
|
366
|
-
loading: boolean;
|
|
367
|
-
error: string | null;
|
|
368
|
-
onRefresh: () => void;
|
|
369
|
-
}): React.ReactElement {
|
|
370
|
-
const { topLevel, result, loading, error, onRefresh } = props;
|
|
371
|
-
const repoName =
|
|
372
|
-
topLevel !== null
|
|
373
|
-
? (topLevel
|
|
374
|
-
.split('/')
|
|
375
|
-
.filter(s => s.length > 0)
|
|
376
|
-
.pop() ?? topLevel)
|
|
377
|
-
: null;
|
|
378
|
-
return (
|
|
379
|
-
<div className="jp-xtralab-GitPanel-header">
|
|
380
|
-
<div className="jp-xtralab-GitPanel-headerInfo">
|
|
381
|
-
{repoName !== null ? (
|
|
382
|
-
<div className="jp-xtralab-GitPanel-repo" title={topLevel ?? ''}>
|
|
383
|
-
{repoName}
|
|
384
|
-
</div>
|
|
385
|
-
) : null}
|
|
386
|
-
{result?.branch !== undefined && result?.branch !== null ? (
|
|
387
|
-
<div className="jp-xtralab-GitPanel-branch">
|
|
388
|
-
{result.branch}
|
|
389
|
-
{result.ahead > 0 ? <span> ↑{result.ahead}</span> : null}
|
|
390
|
-
{result.behind > 0 ? <span> ↓{result.behind}</span> : null}
|
|
391
|
-
</div>
|
|
392
|
-
) : null}
|
|
393
|
-
</div>
|
|
394
|
-
<button
|
|
395
|
-
type="button"
|
|
396
|
-
className="jp-xtralab-GitPanel-iconButton"
|
|
397
|
-
title="Refresh"
|
|
398
|
-
disabled={loading && error === null}
|
|
399
|
-
onClick={onRefresh}
|
|
400
|
-
>
|
|
401
|
-
⟳
|
|
402
|
-
</button>
|
|
403
|
-
</div>
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function ChangeSection(props: {
|
|
408
|
-
label: string;
|
|
409
|
-
count: number;
|
|
410
|
-
collapsed: boolean;
|
|
411
|
-
onToggleCollapsed: () => void;
|
|
412
|
-
actions?: IRowAction[];
|
|
413
|
-
children: React.ReactNode;
|
|
414
|
-
}): React.ReactElement {
|
|
415
|
-
const { label, count, collapsed, onToggleCollapsed, actions, children } =
|
|
416
|
-
props;
|
|
417
|
-
return (
|
|
418
|
-
<div className="jp-xtralab-GitPanel-section">
|
|
419
|
-
<div
|
|
420
|
-
className="jp-xtralab-GitPanel-sectionHeader"
|
|
421
|
-
onClick={onToggleCollapsed}
|
|
422
|
-
>
|
|
423
|
-
<span className="jp-xtralab-GitPanel-sectionChevron">
|
|
424
|
-
{collapsed ? '▸' : '▾'}
|
|
425
|
-
</span>
|
|
426
|
-
<span className="jp-xtralab-GitPanel-sectionLabel">{label}</span>
|
|
427
|
-
{actions !== undefined ? (
|
|
428
|
-
<span className="jp-xtralab-GitPanel-sectionActions">
|
|
429
|
-
{actions.map(action => (
|
|
430
|
-
<button
|
|
431
|
-
key={action.label}
|
|
432
|
-
type="button"
|
|
433
|
-
className="jp-xtralab-GitPanel-iconButton"
|
|
434
|
-
title={action.label}
|
|
435
|
-
onClick={event => {
|
|
436
|
-
event.stopPropagation();
|
|
437
|
-
action.onClick();
|
|
438
|
-
}}
|
|
439
|
-
>
|
|
440
|
-
{action.symbol}
|
|
441
|
-
</button>
|
|
442
|
-
))}
|
|
443
|
-
</span>
|
|
444
|
-
) : null}
|
|
445
|
-
<span className="jp-xtralab-GitPanel-sectionCount">{count}</span>
|
|
446
|
-
</div>
|
|
447
|
-
{!collapsed ? (
|
|
448
|
-
<div className="jp-xtralab-GitPanel-sectionBody">{children}</div>
|
|
449
|
-
) : null}
|
|
450
|
-
</div>
|
|
451
|
-
);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function ChangeRow(props: {
|
|
455
|
-
change: IFileChange;
|
|
456
|
-
pendingAction: string | undefined;
|
|
457
|
-
onClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
|
458
|
-
actions: IRowAction[];
|
|
459
|
-
}): React.ReactElement {
|
|
460
|
-
const { change, pendingAction, onClick, actions } = props;
|
|
461
|
-
const filename = basename(change.path);
|
|
462
|
-
const dirname = dirnameOrEmpty(change.path);
|
|
463
|
-
return (
|
|
464
|
-
<div
|
|
465
|
-
className="jp-xtralab-GitPanel-row"
|
|
466
|
-
data-status={change.status}
|
|
467
|
-
title={
|
|
468
|
-
change.from !== undefined
|
|
469
|
-
? `${change.from} → ${change.path}`
|
|
470
|
-
: change.path
|
|
471
|
-
}
|
|
472
|
-
onClick={onClick}
|
|
473
|
-
>
|
|
474
|
-
<span className="jp-xtralab-GitPanel-rowLabel">
|
|
475
|
-
<span className="jp-xtralab-GitPanel-rowName">{filename}</span>
|
|
476
|
-
{dirname !== '' ? (
|
|
477
|
-
<span className="jp-xtralab-GitPanel-rowPath">{dirname}</span>
|
|
478
|
-
) : null}
|
|
479
|
-
</span>
|
|
480
|
-
<span
|
|
481
|
-
className="jp-xtralab-GitPanel-rowActions"
|
|
482
|
-
data-pending={pendingAction !== undefined}
|
|
483
|
-
>
|
|
484
|
-
{actions.map(action => (
|
|
485
|
-
<button
|
|
486
|
-
key={action.label}
|
|
487
|
-
type="button"
|
|
488
|
-
className="jp-xtralab-GitPanel-iconButton"
|
|
489
|
-
title={action.label}
|
|
490
|
-
disabled={pendingAction !== undefined}
|
|
491
|
-
onClick={event => {
|
|
492
|
-
event.stopPropagation();
|
|
493
|
-
action.onClick();
|
|
494
|
-
}}
|
|
495
|
-
>
|
|
496
|
-
{action.symbol}
|
|
497
|
-
</button>
|
|
498
|
-
))}
|
|
499
|
-
</span>
|
|
500
|
-
<span
|
|
501
|
-
className="jp-xtralab-GitPanel-rowBadge"
|
|
502
|
-
data-status={change.status}
|
|
503
|
-
title={statusTooltip(change.status)}
|
|
504
|
-
>
|
|
505
|
-
{statusBadge(change.status)}
|
|
506
|
-
</span>
|
|
507
|
-
</div>
|
|
508
|
-
);
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function statusBadge(status: FileChangeStatus): string {
|
|
512
|
-
switch (status) {
|
|
513
|
-
case 'modified':
|
|
514
|
-
return 'M';
|
|
515
|
-
case 'added':
|
|
516
|
-
return 'A';
|
|
517
|
-
case 'deleted':
|
|
518
|
-
return 'D';
|
|
519
|
-
case 'renamed':
|
|
520
|
-
return 'R';
|
|
521
|
-
case 'untracked':
|
|
522
|
-
return 'U';
|
|
523
|
-
case 'unmerged':
|
|
524
|
-
return '!';
|
|
525
|
-
case 'typechange':
|
|
526
|
-
return 'T';
|
|
527
|
-
default:
|
|
528
|
-
return '?';
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
function statusTooltip(status: FileChangeStatus): string {
|
|
533
|
-
switch (status) {
|
|
534
|
-
case 'modified':
|
|
535
|
-
return 'Modified';
|
|
536
|
-
case 'added':
|
|
537
|
-
return 'Added';
|
|
538
|
-
case 'deleted':
|
|
539
|
-
return 'Deleted';
|
|
540
|
-
case 'renamed':
|
|
541
|
-
return 'Renamed';
|
|
542
|
-
case 'untracked':
|
|
543
|
-
return 'Untracked';
|
|
544
|
-
case 'unmerged':
|
|
545
|
-
return 'Unmerged (conflict)';
|
|
546
|
-
case 'typechange':
|
|
547
|
-
return 'Type changed';
|
|
548
|
-
default:
|
|
549
|
-
return 'Unknown';
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function basename(path: string): string {
|
|
554
|
-
const idx = path.lastIndexOf('/');
|
|
555
|
-
return idx < 0 ? path : path.slice(idx + 1);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function dirnameOrEmpty(path: string): string {
|
|
559
|
-
const idx = path.lastIndexOf('/');
|
|
560
|
-
return idx < 0 ? '' : path.slice(0, idx);
|
|
561
|
-
}
|