@kubohiroya/turbowarp-plus-editor-git 0.9.0 → 0.11.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/dist/editor-block-locator.d.ts +27 -0
- package/dist/file-diff-view.d.ts +3 -1
- package/dist/index.d.ts +25 -0
- package/dist/index.js +243 -101
- package/dist/messages.d.ts +34 -0
- package/dist/project-change-list.d.ts +3 -1
- package/dist/project-source-diff.js +1 -1
- package/dist/project-source-review.d.ts +6 -1
- package/dist/pull-request-panel.d.ts +4 -1
- package/dist/{repository-VBp-UNWc.js → repository-CtvWYPQ8.js} +120 -4
- package/dist/repository.d.ts +17 -0
- package/dist/repository.js +1 -1
- package/dist/{scratch-block-diff-renderer-DZR8ep-T.js → scratch-block-diff-renderer-CgAGfmAc.js} +65 -9
- package/dist/style.css +15 -2
- package/package.json +1 -1
- package/src/connection-settings.tsx +14 -8
- package/src/editor-block-locator.ts +55 -0
- package/src/file-diff-view.tsx +8 -5
- package/src/index.tsx +105 -29
- package/src/messages.ts +34 -26
- package/src/project-change-list.tsx +6 -5
- package/src/project-source-review.tsx +57 -11
- package/src/pull-request-panel.tsx +9 -4
- package/src/repository.ts +76 -3
- package/src/style.css +15 -2
package/src/file-diff-view.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import type {FileChange, FileDiff} from './repository.js';
|
|
|
5
5
|
import type {labelsFor} from './messages.js';
|
|
6
6
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
7
7
|
import type {ProjectAssetLoader} from './project-assets.js';
|
|
8
|
+
import type {ProjectBlockLocator} from './block-diff-location.js';
|
|
8
9
|
|
|
9
10
|
const en = {view: 'Show changes as', blocks: 'Blocks', text: 'Text'};
|
|
10
11
|
const ja: typeof en = {view: '変更の表示', blocks: 'ブロック', text: 'テキスト'};
|
|
@@ -15,16 +16,18 @@ export const isProjectSource = (path: string) => path.split('/').pop() === 'proj
|
|
|
15
16
|
/**
|
|
16
17
|
* Shows one changed file. Project sources are compared as blocks by default, with their text diff one switch away.
|
|
17
18
|
*/
|
|
18
|
-
export function FileDiffView({change, diff, labels, locale, renderBlock, loadAsset}: {
|
|
19
|
+
export function FileDiffView({change, diff, labels, locale, renderBlock, loadAsset, locateBlock}: {
|
|
19
20
|
change: FileChange; diff: FileDiff | null; labels: ReturnType<typeof labelsFor>; locale: string;
|
|
20
|
-
renderBlock?: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader;
|
|
21
|
+
renderBlock?: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader; locateBlock?: ProjectBlockLocator;
|
|
21
22
|
}) {
|
|
22
23
|
const [view, setView] = useState<'blocks' | 'text'>('blocks');
|
|
23
24
|
const names = locale.startsWith('ja') ? ja : en;
|
|
24
25
|
const parsed = diff?.patch ? parseDiff(diff.patch)[0] : null;
|
|
25
26
|
const text = <>
|
|
26
|
-
<
|
|
27
|
-
|
|
27
|
+
<details className="twgit-file-details"><summary>{labels.details}</summary>
|
|
28
|
+
<p>{labels.mode} · {change.oldMode?.toString(8) || '—'} → {change.newMode?.toString(8) || '—'}</p>
|
|
29
|
+
{diff && <p>{diff.beforeSize} → {diff.afterSize} {labels.bytes}</p>}
|
|
30
|
+
</details>
|
|
28
31
|
{diff?.binary && <p>{labels.binary}</p>}
|
|
29
32
|
{diff?.tooLarge && <p>{labels.large}</p>}
|
|
30
33
|
{parsed && <Diff viewType="unified" diffType={parsed.type} hunks={parsed.hunks}>
|
|
@@ -38,7 +41,7 @@ export function FileDiffView({change, diff, labels, locale, renderBlock, loadAss
|
|
|
38
41
|
{(['blocks', 'text'] as const).map(mode => <button key={mode} type="button" aria-pressed={view === mode}
|
|
39
42
|
data-git-view={mode} onClick={() => setView(mode)}>{names[mode]}</button>)}
|
|
40
43
|
</div>}
|
|
41
|
-
{sources && view === 'blocks' ? <ProjectSourceComparison renderBlock={renderBlock!} locale={locale} fallback={text} loadAsset={loadAsset}
|
|
44
|
+
{sources && view === 'blocks' ? <ProjectSourceComparison renderBlock={renderBlock!} locale={locale} fallback={text} loadAsset={loadAsset} locateBlock={locateBlock}
|
|
42
45
|
before={change.before === null ? null : sources.before} after={change.after === null ? null : sources.after}/> : text}
|
|
43
46
|
</>;
|
|
44
47
|
}
|
package/src/index.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
|
2
|
-
import {render, unmountComponentAtNode} from 'react-dom';
|
|
2
|
+
import {createPortal, render, unmountComponentAtNode} from 'react-dom';
|
|
3
3
|
import {FileDiffView} from './file-diff-view.js';
|
|
4
4
|
import {findElement, ProjectChangeListView, ProjectElementView, projectChangeLabels, useProjectChanges} from './project-change-list.js';
|
|
5
5
|
import type {ChangedFile} from './project-change-model.js';
|
|
@@ -12,6 +12,11 @@ import type {GitConnectionSettings} from './connection-settings.js';
|
|
|
12
12
|
import type {RemoteOperation} from './remote-operations.js';
|
|
13
13
|
import {PullRequestReviewPanel} from './pull-request-panel.js';
|
|
14
14
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
15
|
+
import type {ScratchBlockLocationWorkspace} from './block-diff-location.js';
|
|
16
|
+
export {createScratchBlocksDiffLocator} from './block-diff-location.js';
|
|
17
|
+
export {createEditorBlockLocator} from './editor-block-locator.js';
|
|
18
|
+
import {createEditorBlockLocator} from './editor-block-locator.js';
|
|
19
|
+
import type {LocatorVM} from './editor-block-locator.js';
|
|
15
20
|
|
|
16
21
|
export {createScratchBlocksDiffRenderer} from './scratch-block-diff-renderer.js';
|
|
17
22
|
export type {ScratchBlockDiffRendererOptions, ScratchBlocksPort} from './scratch-block-diff-renderer.js';
|
|
@@ -26,6 +31,22 @@ export interface GitHost {
|
|
|
26
31
|
isProjectDirty(): boolean;
|
|
27
32
|
isProjectRunning?(): boolean;
|
|
28
33
|
subscribe?(listener: (event: 'loaded' | 'changed') => void): () => void;
|
|
34
|
+
/**
|
|
35
|
+
* Opens the panel as a tab of the host's layout, with the same shape as the layout package's DocumentPanelRequest.
|
|
36
|
+
* Returning `false` keeps the modal dialog.
|
|
37
|
+
*/
|
|
38
|
+
openPanel?(request: GitPanelRequest): boolean;
|
|
39
|
+
/** Opens the code of one stage or sprite, so a diff can point at its block. Without it, no block is located. */
|
|
40
|
+
openTargetCode?(targetId: string): void | Promise<void>;
|
|
41
|
+
/** The Editor's code workspace as it is now. */
|
|
42
|
+
getWorkspace?(): ScratchBlockLocationWorkspace | null | Promise<ScratchBlockLocationWorkspace | null>;
|
|
43
|
+
}
|
|
44
|
+
export interface GitPanelRequest {
|
|
45
|
+
reference: {providerId: string; documentId: string};
|
|
46
|
+
filename: string;
|
|
47
|
+
title?: string;
|
|
48
|
+
transient?: boolean;
|
|
49
|
+
mount(element: HTMLElement, close: () => void): {requestClose(): void; dispose(): void};
|
|
29
50
|
}
|
|
30
51
|
interface Controller {open(locale: string): void; refresh(automatic?: boolean): Promise<void>}
|
|
31
52
|
interface State {
|
|
@@ -51,8 +72,14 @@ type ObserverWindow = Window & {FileSystemObserver?: new (callback: () => void)
|
|
|
51
72
|
}};
|
|
52
73
|
const watchInterval = 2000;
|
|
53
74
|
|
|
54
|
-
function GitPanel({document, host, controller}: {document: Document; host: GitHost; controller: Controller}) {
|
|
75
|
+
function GitPanel({vm, document, host, controller}: {vm: object; document: Document; host: GitHost; controller: Controller}) {
|
|
55
76
|
const [visible, setVisible] = useState(false);
|
|
77
|
+
// A layout tab that holds the panel. The panel component stays mounted, so the repository survives closing the tab.
|
|
78
|
+
const [slot, setSlot] = useState<{element: HTMLElement; close(): void} | null>(null);
|
|
79
|
+
const shown = visible || slot !== null;
|
|
80
|
+
// Only a tab leaves the Editor visible, so only a tab offers to point at blocks in it.
|
|
81
|
+
const locator = useMemo(() => slot && host.openTargetCode && host.getWorkspace ? createEditorBlockLocator({
|
|
82
|
+
vm: vm as LocatorVM, openTargetCode: host.openTargetCode, getWorkspace: host.getWorkspace}) : undefined, [slot !== null, host, vm]);
|
|
56
83
|
const [review, setReview] = useState(false);
|
|
57
84
|
const [locale, setLocale] = useState(document.documentElement.lang || 'en');
|
|
58
85
|
const [dirty, setDirty] = useState(host.isProjectDirty());
|
|
@@ -99,7 +126,21 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
99
126
|
}
|
|
100
127
|
}
|
|
101
128
|
};
|
|
102
|
-
controller.open = language => {
|
|
129
|
+
controller.open = language => {
|
|
130
|
+
setLocale(language);
|
|
131
|
+
const opened = host.openPanel?.({reference: {providerId: 'turbowarp-plus-git', documentId: 'local-git'},
|
|
132
|
+
filename: 'Git', title: labelsFor(language).title, transient: true,
|
|
133
|
+
mount(element, close) {
|
|
134
|
+
setVisible(false);
|
|
135
|
+
setSlot({element, close});
|
|
136
|
+
return {requestClose: close, dispose() {setSlot(previous => previous?.element === element ? null : previous);}};
|
|
137
|
+
}});
|
|
138
|
+
if (!opened) setVisible(true);
|
|
139
|
+
};
|
|
140
|
+
const closePanel = () => {
|
|
141
|
+
if (slot) slot.close();
|
|
142
|
+
else setVisible(false);
|
|
143
|
+
};
|
|
103
144
|
controller.refresh = async (automatic = false) => {
|
|
104
145
|
if (!alive.current || !current.current.repository) return;
|
|
105
146
|
if (running.current) {
|
|
@@ -128,7 +169,7 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
128
169
|
// While the panel is open, notice Git commands, agents and editors that change the repository outside the Editor.
|
|
129
170
|
useEffect(() => {
|
|
130
171
|
const repository = state.repository;
|
|
131
|
-
if (!
|
|
172
|
+
if (!shown || !repository) return;
|
|
132
173
|
const view = document.defaultView as ObserverWindow;
|
|
133
174
|
let stopped = false;
|
|
134
175
|
let checking = false;
|
|
@@ -177,13 +218,13 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
177
218
|
document.removeEventListener('visibilitychange', onFocus);
|
|
178
219
|
observer?.disconnect();
|
|
179
220
|
};
|
|
180
|
-
}, [
|
|
221
|
+
}, [shown, state.repository, locale]);
|
|
181
222
|
useEffect(() => {
|
|
182
|
-
if (!visible) return;
|
|
223
|
+
if (!visible || slot) return;
|
|
183
224
|
const previous = document.activeElement as HTMLElement | null;
|
|
184
225
|
dialog.current?.focus();
|
|
185
226
|
return () => {if (previous?.isConnected) previous.focus();};
|
|
186
|
-
}, [visible]);
|
|
227
|
+
}, [visible, slot]);
|
|
187
228
|
const chooseRoot = async () => {
|
|
188
229
|
const view = document.defaultView as PickerWindow;
|
|
189
230
|
await run(async () => {
|
|
@@ -208,6 +249,8 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
208
249
|
const result = await host.openProject(directory, () => document.defaultView!.confirm(labels.replace));
|
|
209
250
|
if (result === 'cancelled') return {};
|
|
210
251
|
} finally {opening.current = false;}
|
|
252
|
+
// Loading the project brings its code to the front; keep the panel the person is working in in front of it.
|
|
253
|
+
if (slot) controller.open(locale);
|
|
211
254
|
if (!alive.current) return {};
|
|
212
255
|
setDirty(host.isProjectDirty());
|
|
213
256
|
setState(previous => ({...previous, project: path, snapshot: null, changes: [], selected: null, diff: null, commit: null}));
|
|
@@ -256,7 +299,7 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
256
299
|
});
|
|
257
300
|
};
|
|
258
301
|
const focusTrap = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
259
|
-
if (event.key === 'Escape') {event.stopPropagation();
|
|
302
|
+
if (event.key === 'Escape') {event.stopPropagation(); closePanel(); return;}
|
|
260
303
|
if (event.key !== 'Tab') return;
|
|
261
304
|
const items = [...event.currentTarget.querySelectorAll<HTMLElement>('button:not(:disabled), input:not(:disabled), select:not(:disabled), a[href], summary, [tabindex="0"]')]
|
|
262
305
|
.filter(item => item.getClientRects().length > 0);
|
|
@@ -268,13 +311,19 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
268
311
|
// One loader per selected change, so thumbnails read each asset once.
|
|
269
312
|
const loadAsset = useMemo(() => state.repository && state.snapshot && state.selected ?
|
|
270
313
|
state.repository.assetLoader(state.snapshot, state.selected) : undefined, [state.repository, state.snapshot, state.selected]);
|
|
271
|
-
if (!
|
|
314
|
+
if (!shown) return null;
|
|
272
315
|
const groups: ChangeGroup[] = state.commit ? ['commit'] : ['staged', 'unstaged', 'untracked'];
|
|
273
316
|
const projectList = typeof projectChanges === 'object' ? projectChanges : null;
|
|
317
|
+
// The open project matches the working tree, so only the working-tree side of its project file can be located.
|
|
318
|
+
const projectFile = state.project === null ? null : [state.project, 'project.source.json'].filter(Boolean).join('/');
|
|
319
|
+
const fileLocator = (change: FileChange) => locator && change.path === projectFile &&
|
|
320
|
+
(change.group === 'unstaged' || change.group === 'untracked' ||
|
|
321
|
+
(change.group === 'staged' && !state.changes.some(item => item.path === change.path && item.group === 'unstaged'))) ?
|
|
322
|
+
locator : undefined;
|
|
323
|
+
const reviewLocator = (path: string, headSha: string) => locator && path === projectFile && state.snapshot?.head === headSha &&
|
|
324
|
+
!state.changes.length ? locator : undefined;
|
|
274
325
|
const selection = projectList && !state.selected ? findElement(projectList.list, element) : null;
|
|
275
|
-
|
|
276
|
-
<div className="twgit-panel" ref={dialog} tabIndex={-1} role="dialog" aria-modal="true" aria-label={labels.title}
|
|
277
|
-
onKeyDown={focusTrap}>
|
|
326
|
+
const content = <>
|
|
278
327
|
<header>
|
|
279
328
|
<h2>{labels.title}</h2>
|
|
280
329
|
<button onClick={chooseRoot} disabled={state.busy}>{labels.choose}</button>
|
|
@@ -282,28 +331,38 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
282
331
|
{!review && state.project !== null && <button disabled={state.busy} onClick={() => void openProject(state.project!)}>{labels.reload}</button>}
|
|
283
332
|
{state.repository && host.renderReviewBlock && <button type="button" disabled={state.busy}
|
|
284
333
|
onClick={() => setReview(!review)}>{review ? labels.localChanges : labels.pullRequests}</button>}
|
|
285
|
-
<button onClick={
|
|
334
|
+
<button onClick={closePanel}>{labels.close}</button>
|
|
286
335
|
</header>
|
|
287
336
|
<div className="twgit-context">
|
|
288
337
|
<span>{labels.repository} · {state.repository?.root.name || '—'}</span>
|
|
289
338
|
<span>{labels.project} · {state.project === null ? labels.none : state.project || labels.root}</span>
|
|
290
|
-
<strong>{state.snapshot?.branch || (state.snapshot?.head ? labels.detached : labels.unborn)}</strong>
|
|
291
|
-
<code>{state.snapshot?.head?.slice(0, 12)}</code>
|
|
292
339
|
<span data-git-dirty="">{dirty ? labels.dirty : labels.saved}</span>
|
|
340
|
+
{state.snapshot && <span data-git-upstream={state.snapshot.upstream.state} title={labels.checkedAt}>{upstreamText(state.snapshot, labels)}</span>}
|
|
341
|
+
{!review && host.connectionSettings && <ConnectionSettings settings={host.connectionSettings} locale={locale}
|
|
342
|
+
runOperation={host.connectionSettings.getSession ? remoteOperation : undefined}
|
|
343
|
+
readOrigin={state.repository && !state.busy ? async () => {
|
|
344
|
+
const ticket = epoch.current;
|
|
345
|
+
const remote = await state.repository!.readOrigin();
|
|
346
|
+
return epoch.current === ticket ? remote : undefined;
|
|
347
|
+
} : undefined}/>}
|
|
348
|
+
{state.snapshot && <details className="twgit-git-details">
|
|
349
|
+
<summary>{labels.details}</summary>
|
|
350
|
+
<div className="twgit-connection-body">
|
|
351
|
+
<p>{labels.branch} · <strong>{state.snapshot.branch || (state.snapshot.head ? labels.detached : labels.unborn)}</strong></p>
|
|
352
|
+
<p>{labels.head} · <code>{state.snapshot.head?.slice(0, 12) ?? '—'}</code></p>
|
|
353
|
+
<p>{labels.branches} · {state.snapshot.branches.join(', ') || '—'}</p>
|
|
354
|
+
<p>{labels.groups} · {labels.gitStaged} = {labels.staged} / {labels.gitUnstaged} = {labels.unstaged} / {labels.gitUntracked} = {labels.untracked}</p>
|
|
355
|
+
<p>{labels.permissions}</p>
|
|
356
|
+
</div>
|
|
357
|
+
</details>}
|
|
293
358
|
</div>
|
|
294
|
-
{!review && host.connectionSettings && <ConnectionSettings settings={host.connectionSettings} locale={locale}
|
|
295
|
-
runOperation={host.connectionSettings.getSession ? remoteOperation : undefined}
|
|
296
|
-
readOrigin={state.repository && !state.busy ? async () => {
|
|
297
|
-
const ticket = epoch.current;
|
|
298
|
-
const remote = await state.repository!.readOrigin();
|
|
299
|
-
return epoch.current === ticket ? remote : undefined;
|
|
300
|
-
} : undefined}/>}
|
|
301
359
|
{state.error && <p className="twgit-error" role="alert">{state.error}</p>}
|
|
302
360
|
{state.notice && <p className="twgit-context" role="status">{state.notice}</p>}
|
|
303
361
|
{state.busy && <p role="status">{labels.loading}</p>}
|
|
304
362
|
{!state.repository && <p>{labels.empty}</p>}
|
|
305
363
|
{review && state.repository && host.renderReviewBlock &&
|
|
306
|
-
<PullRequestReviewPanel repository={state.repository} locale={locale} renderBlock={host.renderReviewBlock}
|
|
364
|
+
<PullRequestReviewPanel repository={state.repository} locale={locale} renderBlock={host.renderReviewBlock}
|
|
365
|
+
locateFor={reviewLocator}/>}
|
|
307
366
|
{!review && state.repository && <div className="twgit-content">
|
|
308
367
|
<aside>
|
|
309
368
|
{state.browser && <details>
|
|
@@ -323,7 +382,7 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
323
382
|
<ul>{state.changes.filter(change => change.group === group).map(change => <li key={change.path}>
|
|
324
383
|
<button className="twgit-file" data-git-group={group} disabled={state.busy}
|
|
325
384
|
aria-pressed={state.selected === change} onClick={() => openFile(change)}>
|
|
326
|
-
<
|
|
385
|
+
<small className="twgit-change-kind">{!change.after ? labels.deleted : !change.before ? labels.added : labels.modified}</small> {change.path}
|
|
327
386
|
</button>
|
|
328
387
|
</li>)}</ul>
|
|
329
388
|
</section>);
|
|
@@ -332,33 +391,50 @@ function GitPanel({document, host, controller}: {document: Document; host: GitHo
|
|
|
332
391
|
fileListOpen={fileListOpen} setFileListOpen={setFileListOpen}/> : fileList;
|
|
333
392
|
})()}
|
|
334
393
|
{!state.changes.length && <p>{labels.clean}</p>}
|
|
335
|
-
<details><summary>{labels.branches}</summary><ul>{state.snapshot?.branches.map(branch => <li key={branch}>{branch}</li>)}</ul></details>
|
|
336
394
|
<h3>{labels.history}</h3>
|
|
337
395
|
<ol className="twgit-history">{state.snapshot?.commits.map(commit => <li key={commit.oid}>
|
|
338
396
|
<button disabled={state.busy} aria-pressed={state.commit === commit.oid} title={commit.oid}
|
|
339
397
|
onClick={() => void selectCommit(commit.oid)}>{commit.message.split('\n')[0]}</button>
|
|
340
|
-
<small
|
|
398
|
+
<small>{commit.author} · {new Date(commit.timestamp * 1000).toLocaleString(locale)}</small>
|
|
341
399
|
</li>)}</ol>
|
|
342
400
|
</aside>
|
|
343
401
|
<main className="twgit-diff">
|
|
344
402
|
{selection && projectList ? <ProjectElementView changes={projectList} selection={selection} locale={locale}
|
|
403
|
+
locateBlock={state.commit ? undefined : locator}
|
|
345
404
|
renderBlock={host.renderReviewBlock} onSelectFile={openFile}/> :
|
|
346
405
|
state.selected ? <FileDiffView change={state.selected} diff={state.diff} labels={labels} locale={locale}
|
|
406
|
+
locateBlock={fileLocator(state.selected)}
|
|
347
407
|
renderBlock={host.renderReviewBlock} loadAsset={loadAsset}/> : <p>{labels.select}</p>}
|
|
348
408
|
</main>
|
|
349
409
|
</div>}
|
|
350
|
-
|
|
351
|
-
|
|
410
|
+
</>;
|
|
411
|
+
// In a tab the Editor stays usable beside the panel, so there is no backdrop, focus trap or Escape to close.
|
|
412
|
+
if (slot) return createPortal(<div className="twgit-panel twgit-panel-tab" role="region" aria-label={labels.title}>{content}</div>,
|
|
413
|
+
slot.element);
|
|
414
|
+
return <div className="twgit-backdrop">
|
|
415
|
+
<div className="twgit-panel" ref={dialog} tabIndex={-1} role="dialog" aria-modal="true" aria-label={labels.title}
|
|
416
|
+
onKeyDown={focusTrap}>{content}</div>
|
|
352
417
|
</div>;
|
|
353
418
|
}
|
|
354
419
|
|
|
420
|
+
/** The GitHub relation in plain words, with the number of records where it helps. */
|
|
421
|
+
function upstreamText({upstream}: Snapshot, labels: ReturnType<typeof labelsFor>) {
|
|
422
|
+
switch (upstream.state) {
|
|
423
|
+
case 'same': return labels.upstreamSame;
|
|
424
|
+
case 'ahead': return `${labels.upstreamAhead} · ${upstream.ahead} ${labels.records}`;
|
|
425
|
+
case 'behind': return `${labels.upstreamBehind} · ${upstream.behind} ${labels.records}`;
|
|
426
|
+
case 'diverged': return `${labels.upstreamDiverged} · +${upstream.ahead} / −${upstream.behind}`;
|
|
427
|
+
default: return labels.upstreamUnknown;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
355
431
|
export function mountGitPanel({vm, document, host}: {vm: object; document: Document; host: GitHost}) {
|
|
356
432
|
if (!(document.defaultView as PickerWindow)?.showDirectoryPicker) return null;
|
|
357
433
|
const root = document.createElement('div');
|
|
358
434
|
document.body.append(root);
|
|
359
435
|
const controller: Controller = {open() {}, async refresh() {}};
|
|
360
436
|
controllers.set(vm, controller);
|
|
361
|
-
render(<GitPanel document={document} host={host} controller={controller}/>, root);
|
|
437
|
+
render(<GitPanel vm={vm} document={document} host={host} controller={controller}/>, root);
|
|
362
438
|
return {refresh: () => controller.refresh(), dispose() {
|
|
363
439
|
if (controllers.get(vm) === controller) controllers.delete(vm);
|
|
364
440
|
void host.connectionSettings?.disconnect().catch(() => {});
|
package/src/messages.ts
CHANGED
|
@@ -7,9 +7,9 @@ export const messages = {
|
|
|
7
7
|
open: 'Open This Project', reload: 'Reload Project', up: 'Parent folder', browse: 'Choose a project inside this repository',
|
|
8
8
|
replace: 'Replace the open project and discard its unsaved changes?',
|
|
9
9
|
dirty: 'The Editor has unsaved changes.', saved: 'The Editor has no unsaved changes.',
|
|
10
|
-
staged: '
|
|
11
|
-
history: '
|
|
12
|
-
branches: 'Local branches', unborn: 'No
|
|
10
|
+
staged: 'Changes in the next record', unstaged: 'Saved, not yet recorded', untracked: 'New files never recorded', commit: 'Changes in this record',
|
|
11
|
+
history: 'Records (commits)', worktree: 'Saved files', clean: 'No saved changes in the selected folder.',
|
|
12
|
+
branches: 'Local branches', unborn: 'No records yet', detached: 'Detached HEAD', select: 'Select a file to view its diff.',
|
|
13
13
|
binary: 'Binary asset', large: 'Text preview exceeds its display limit.', bytes: 'bytes',
|
|
14
14
|
mode: 'Git file mode', permissions: 'Working-tree executable permission changes are unavailable in the browser.',
|
|
15
15
|
unchanged: 'The text is unchanged.', loading: 'Reading repository…',
|
|
@@ -17,16 +17,20 @@ export const messages = {
|
|
|
17
17
|
changed: 'Files or Git state changed while reading. Refresh to read the current state.',
|
|
18
18
|
outdated: 'The repository changed outside the Editor. Refresh to read the current state.',
|
|
19
19
|
unsupported: 'This repository contains an entry the browser cannot represent.',
|
|
20
|
-
pullUnsafe: 'Stop the project and
|
|
21
|
-
diverged: '
|
|
20
|
+
pullUnsafe: 'Stop the project, and save or undo unsaved edits and unrecorded changes, before getting changes from GitHub (pull).',
|
|
21
|
+
diverged: 'This branch and GitHub\'s have both gained records, so they cannot be combined here (pull only fast-forwards). Your branch and files are unchanged.',
|
|
22
22
|
session: 'Reconnect GitHub for this repository and requested permission.',
|
|
23
|
-
pushUnknown: '
|
|
24
|
-
pushRefresh: '
|
|
25
|
-
rollbackFailed: '
|
|
26
|
-
remoteFailed: '
|
|
27
|
-
remoteDone: '
|
|
28
|
-
pulledProject: '
|
|
29
|
-
confirmPush: '
|
|
23
|
+
pushUnknown: 'It is unknown whether GitHub received the records. Check GitHub (fetch) before sending again.',
|
|
24
|
+
pushRefresh: 'GitHub received the records. Check GitHub (fetch) to update the status.',
|
|
25
|
+
rollbackFailed: 'Some local changes could not be restored. Check the files and the Git details.',
|
|
26
|
+
remoteFailed: 'Could not complete the GitHub operation. Check the connection and try again.',
|
|
27
|
+
remoteDone: 'The GitHub operation finished.',
|
|
28
|
+
pulledProject: 'Got the changes from GitHub. Use Reload Project to open the updated files.',
|
|
29
|
+
confirmPush: 'Send the records on this branch to GitHub (push)?',
|
|
30
|
+
details: 'Git details', branch: 'Branch', head: 'HEAD', groups: 'Git groups', added: 'Added', modified: 'Changed', deleted: 'Deleted',
|
|
31
|
+
gitStaged: 'Staged', gitUnstaged: 'Unstaged', gitUntracked: 'Untracked',
|
|
32
|
+
upstreamSame: 'Same as GitHub', upstreamAhead: 'Ahead of GitHub', upstreamBehind: 'Behind GitHub', upstreamDiverged: 'Diverged from GitHub',
|
|
33
|
+
upstreamUnknown: 'Not checked with GitHub yet', records: 'records', checkedAt: 'as of the last check',
|
|
30
34
|
failed: 'Could not read the repository. Check folder access and refresh.'
|
|
31
35
|
},
|
|
32
36
|
ja: {
|
|
@@ -35,11 +39,11 @@ export const messages = {
|
|
|
35
39
|
empty: '既存の Git リポジトリを選び、ファイルの状態と履歴を確認できます。',
|
|
36
40
|
repository: 'リポジトリ', project: '作品フォルダ', none: '作品は未選択', root: 'リポジトリルート',
|
|
37
41
|
open: 'この作品を開く', reload: '作品を再読込', up: '上のフォルダ', browse: 'リポジトリ内の作品を選択',
|
|
38
|
-
replace: '
|
|
39
|
-
dirty: 'Editor
|
|
40
|
-
staged: '
|
|
41
|
-
history: '
|
|
42
|
-
branches: 'ローカルブランチ', unborn: '
|
|
42
|
+
replace: '保存していない編集を破棄して、別の作品を開きますか?',
|
|
43
|
+
dirty: 'Editor に保存していない編集があります。', saved: 'Editor に保存していない編集はありません。',
|
|
44
|
+
staged: '次の記録に入る変更', unstaged: '保存したが、まだ記録していない変更', untracked: 'まだ記録したことのない新しいファイル', commit: 'この記録の変更',
|
|
45
|
+
history: '記録(コミット)の履歴', worktree: '保存したファイル', clean: '対象フォルダに、保存した変更はありません。',
|
|
46
|
+
branches: 'ローカルブランチ', unborn: 'まだ記録がありません', detached: 'ブランチ未選択の HEAD', select: 'ファイルを選ぶと差分を表示します。',
|
|
43
47
|
binary: 'バイナリアセット', large: 'テキスト差分の表示上限を超えています。', bytes: 'バイト',
|
|
44
48
|
mode: 'Git のファイルモード', permissions: '作業ツリーの実行権限の変更はブラウザーから確認できません。',
|
|
45
49
|
unchanged: 'テキストに変更はありません。', loading: 'リポジトリを読込中…',
|
|
@@ -47,16 +51,20 @@ export const messages = {
|
|
|
47
51
|
changed: '読込中にファイルまたは Git の状態が変わりました。再取得してください。',
|
|
48
52
|
outdated: 'Editor の外でリポジトリが変更されました。再取得すると最新の状態を読み込みます。',
|
|
49
53
|
unsupported: 'ブラウザーで扱えない種類の Git エントリーがあります。',
|
|
50
|
-
pullUnsafe: '
|
|
51
|
-
diverged: 'fast-forward
|
|
54
|
+
pullUnsafe: 'GitHub から取り込む前に、実行を止め、保存していない編集とまだ記録していない変更をなくしてください。',
|
|
55
|
+
diverged: 'このブランチと GitHub の両方に新しい記録があるため、ここでは取り込めません(pull は fast-forward のみ)。ブランチとファイルはそのままです。',
|
|
52
56
|
session: 'このリポジトリと操作に必要な権限で GitHub に接続してください。',
|
|
53
|
-
pushUnknown: '
|
|
54
|
-
pushRefresh: '
|
|
55
|
-
rollbackFailed: '
|
|
56
|
-
remoteFailed: '
|
|
57
|
-
remoteDone: '
|
|
58
|
-
pulledProject: '
|
|
59
|
-
confirmPush: '
|
|
57
|
+
pushUnknown: 'GitHub が記録を受け取ったか分かりません。送り直す前に「GitHub と確かめる」を使ってください。',
|
|
58
|
+
pushRefresh: 'GitHub が記録を受け取りました。「GitHub と確かめる」で状態を更新してください。',
|
|
59
|
+
rollbackFailed: '一部のローカルの変更を元に戻せませんでした。ファイルと Git の詳細を確認してください。',
|
|
60
|
+
remoteFailed: 'GitHub との操作を完了できませんでした。接続を確認して、もう一度試してください。',
|
|
61
|
+
remoteDone: 'GitHub との操作が終わりました。',
|
|
62
|
+
pulledProject: 'GitHub から取り込みました。「作品を再読込」で更新したファイルを開けます。',
|
|
63
|
+
confirmPush: 'このブランチの記録を GitHub に送りますか(push)?',
|
|
64
|
+
details: 'Git の詳細', branch: 'ブランチ', head: 'HEAD', groups: 'Git の区分', added: '追加', modified: '変更', deleted: '削除',
|
|
65
|
+
gitStaged: 'ステージ済み (staged)', gitUnstaged: '未ステージ (unstaged)', gitUntracked: '未追跡 (untracked)',
|
|
66
|
+
upstreamSame: 'GitHub と同じ', upstreamAhead: 'GitHub より進んでいる', upstreamBehind: 'GitHub より遅れている', upstreamDiverged: 'GitHub と分かれている',
|
|
67
|
+
upstreamUnknown: 'まだ GitHub と確かめていない', records: '件', checkedAt: '最後に確かめた時点',
|
|
60
68
|
failed: 'リポジトリを読み込めませんでした。フォルダへのアクセスを確認し、再取得してください。'
|
|
61
69
|
}
|
|
62
70
|
};
|
|
@@ -7,6 +7,7 @@ import type {ProjectAssetLoader, ProjectVersion} from './project-assets.js';
|
|
|
7
7
|
import {ProjectSourceComparison} from './project-source-review.js';
|
|
8
8
|
import type {ChangeGroup, FileChange, Repository, Snapshot} from './repository.js';
|
|
9
9
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
10
|
+
import type {ProjectBlockLocator} from './block-diff-location.js';
|
|
10
11
|
|
|
11
12
|
const en = {
|
|
12
13
|
title: 'Changes by project element', stage: 'Stage', code: (count: number) => `Code · ${count} ${count === 1 ? 'change' : 'changes'}`,
|
|
@@ -15,7 +16,7 @@ const en = {
|
|
|
15
16
|
files: 'Files and Git groups', before: 'Before', after: 'After', absent: 'Not in this version',
|
|
16
17
|
loading: 'Reading the project changes…', unreadable: 'The project data could not be read, so the changes are listed by file.',
|
|
17
18
|
fileList: 'List by file', property: 'Item', format: 'Format', rate: 'Sample rate', samples: 'Samples', file: 'File name',
|
|
18
|
-
noImage: 'No image', groups: {staged: '
|
|
19
|
+
noImage: 'No image', groups: {staged: 'in the next record', unstaged: 'not yet recorded', untracked: 'new file', commit: 'in this record'} as Record<ChangeGroup, string>
|
|
19
20
|
};
|
|
20
21
|
const ja: typeof en = {
|
|
21
22
|
title: '作品の要素ごとの変更', stage: 'ステージ', code: count => `コード · ${count} か所`,
|
|
@@ -24,7 +25,7 @@ const ja: typeof en = {
|
|
|
24
25
|
files: 'ファイルと Git の区分', before: '変更前', after: '変更後', absent: 'この版にはありません',
|
|
25
26
|
loading: '作品の変更を読み込み中…', unreadable: '作品データを読み込めないため、ファイル単位で一覧にします。',
|
|
26
27
|
fileList: 'ファイル単位の一覧', property: '項目', format: '形式', rate: 'サンプリング周波数', samples: 'サンプル数', file: 'ファイル名',
|
|
27
|
-
noImage: '画像なし', groups: {staged: '
|
|
28
|
+
noImage: '画像なし', groups: {staged: '次の記録に入る', unstaged: 'まだ記録していない', untracked: '新しいファイル', commit: 'この記録'}
|
|
28
29
|
};
|
|
29
30
|
type Labels = typeof en;
|
|
30
31
|
export const projectChangeLabels = (locale: string): Labels => locale.startsWith('ja') ? ja : en;
|
|
@@ -164,9 +165,9 @@ const show = (value: unknown) => {
|
|
|
164
165
|
};
|
|
165
166
|
|
|
166
167
|
/** The details of one selected row: its code, pictures or values in both versions, and the files behind it. */
|
|
167
|
-
export function ProjectElementView({changes, selection, locale, renderBlock, onSelectFile}: {
|
|
168
|
+
export function ProjectElementView({changes, selection, locale, renderBlock, onSelectFile, locateBlock}: {
|
|
168
169
|
changes: ProjectChanges; selection: {element: ProjectElementChange; target: ProjectTargetChanges | null}; locale: string;
|
|
169
|
-
renderBlock?: ProjectBlockRenderer; onSelectFile(file: ChangedFile): void;
|
|
170
|
+
renderBlock?: ProjectBlockRenderer; onSelectFile(file: ChangedFile): void; locateBlock?: ProjectBlockLocator;
|
|
170
171
|
}) {
|
|
171
172
|
const labels = projectChangeLabels(locale);
|
|
172
173
|
const {element, target} = selection;
|
|
@@ -189,7 +190,7 @@ export function ProjectElementView({changes, selection, locale, renderBlock, onS
|
|
|
189
190
|
<h3>{target && <span>{target.isStage ? labels.stage : target.name} · </span>}{elementTitle(element, target, labels, locale)}</h3>
|
|
190
191
|
{element.kind === 'code' && target && renderBlock && <ProjectSourceComparison before={changes.sources.before}
|
|
191
192
|
after={changes.sources.after} renderBlock={renderBlock} locale={locale} loadAsset={changes.loadAsset}
|
|
192
|
-
target={{isStage: target.isStage, name: target.name}}/>}
|
|
193
|
+
target={{isStage: target.isStage, name: target.name}} locateBlock={locateBlock}/>}
|
|
193
194
|
{element.kind === 'costumes' && <div className="twgit-element-pictures">
|
|
194
195
|
{(['before', 'after'] as const).map(version => <Picture key={version} md5ext={element.md5ext?.[version]} version={version}
|
|
195
196
|
loadAsset={changes.loadAsset} labels={labels} title={element[version]?.name === undefined ? labels[version] : `${labels[version]} · ${String(element[version]!.name)}`}/>)}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import React, {useMemo, useState} from 'react';
|
|
1
|
+
import React, {useEffect, useMemo, useRef, useState} from 'react';
|
|
2
2
|
import {BlockPreview, usePreviewSync} from './block-diff-preview.js';
|
|
3
3
|
import {isRecord, parseProjectSource, readProjectSourceDiff} from './project-source-patch.js';
|
|
4
4
|
import type {BlockSide} from './project-source-patch.js';
|
|
5
5
|
import {compareProjectForReview} from './project-source-review-model.js';
|
|
6
6
|
import type {ScriptReview, TargetReview} from './project-source-review-model.js';
|
|
7
7
|
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
8
|
+
import type {ProjectBlockLocator} from './block-diff-location.js';
|
|
9
|
+
import type {LocationStatus} from './block-diff-preview.js';
|
|
8
10
|
import {currentCostume, useCostumeImage} from './project-assets.js';
|
|
9
11
|
import type {ProjectAssetLoader, ProjectVersion} from './project-assets.js';
|
|
10
12
|
|
|
@@ -15,7 +17,9 @@ export type ProjectSourceReviewFile = {
|
|
|
15
17
|
summary?: string;
|
|
16
18
|
previousPath?: string;
|
|
17
19
|
unavailable?: boolean;
|
|
18
|
-
} & ({patch: string; beforeSource?: string; loadAsset?: ProjectAssetLoader
|
|
20
|
+
} & ({patch: string; beforeSource?: string; loadAsset?: ProjectAssetLoader;
|
|
21
|
+
/** Offers "Highlight in Editor" on this file's after version, which the caller has matched with the open project. */
|
|
22
|
+
locateBlock?: ProjectBlockLocator} | {status: 'added' | 'removed' | 'updated'});
|
|
19
23
|
|
|
20
24
|
export interface ProjectSourceReviewProps {
|
|
21
25
|
title: string;
|
|
@@ -37,7 +41,9 @@ const ja = {
|
|
|
37
41
|
related: '関連ファイル', properties: 'コード以外の変更', invalid: 'この作品の差分を読み込めません。',
|
|
38
42
|
flag: '緑の旗が押されたとき', broadcast: 'を受け取ったとき', key: 'キーが押されたとき',
|
|
39
43
|
click: 'このスプライトが押されたとき', script: 'コード', noChanges: '作品に内容の変更はありません。',
|
|
40
|
-
comparison: '作品の変更', unreadable: '作品データとして読み込めないため、テキストの差分を表示します。'
|
|
44
|
+
comparison: '作品の変更', unreadable: '作品データとして読み込めないため、テキストの差分を表示します。',
|
|
45
|
+
locate: '編集画面で強調', locating: '編集画面でブロックを検索中…', located: '編集画面で強調しました。',
|
|
46
|
+
missingLocation: '編集画面に該当する版またはブロックがありません。'
|
|
41
47
|
};
|
|
42
48
|
const en: typeof ja = {
|
|
43
49
|
review: 'Change review', fullDescription: 'Show full PR description', checks: 'Review checklist', files: 'Changed files', targets: 'Affected stages and sprites',
|
|
@@ -49,7 +55,9 @@ const en: typeof ja = {
|
|
|
49
55
|
related: 'Related file', properties: 'Changes outside the code', invalid: 'This project diff could not be read.',
|
|
50
56
|
flag: 'When green flag clicked', broadcast: 'received', key: 'When key pressed',
|
|
51
57
|
click: 'When this sprite clicked', script: 'Code', noChanges: 'No changes to the project contents.',
|
|
52
|
-
comparison: 'Project changes', unreadable: 'This file could not be read as project data, so its text diff is shown.'
|
|
58
|
+
comparison: 'Project changes', unreadable: 'This file could not be read as project data, so its text diff is shown.',
|
|
59
|
+
locate: 'Highlight in Editor', locating: 'Finding the block in the Editor…', located: 'Highlighted in the Editor.',
|
|
60
|
+
missingLocation: 'This version or block is unavailable in the Editor.'
|
|
53
61
|
};
|
|
54
62
|
|
|
55
63
|
const properties = new Map<string, readonly [string, string]>([
|
|
@@ -74,9 +82,39 @@ function scriptTitle(side: BlockSide, labels: typeof ja) {
|
|
|
74
82
|
return typeof block.opcode === 'string' ? block.opcode : labels.script;
|
|
75
83
|
}
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
type BlockLocation = {locate?(side: BlockSide): void; status(side: BlockSide): LocationStatus | undefined};
|
|
86
|
+
const sideKey = (side: BlockSide) => JSON.stringify([side.target.isStage, side.target.name, side.id]);
|
|
87
|
+
|
|
88
|
+
/** Highlights the after version of a script in the Editor, one request per comparison, and cancels it on unmount. */
|
|
89
|
+
function useBlockLocation(comparison: Comparison, locateBlock?: ProjectBlockLocator): BlockLocation {
|
|
90
|
+
const request = useRef<AbortController>();
|
|
91
|
+
const [location, setLocation] = useState<{comparison: Comparison; key: string; locator: ProjectBlockLocator; status: LocationStatus}>();
|
|
92
|
+
useEffect(() => () => request.current?.abort(), [comparison, locateBlock]);
|
|
93
|
+
const locate = async (side: BlockSide) => {
|
|
94
|
+
if (!locateBlock) return;
|
|
95
|
+
request.current?.abort();
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
request.current = controller;
|
|
98
|
+
const selected = {comparison, key: sideKey(side), locator: locateBlock};
|
|
99
|
+
setLocation({...selected, status: 'locating'});
|
|
100
|
+
let found = false;
|
|
101
|
+
try {
|
|
102
|
+
found = await locateBlock({target: side.target, id: side.id, version: 'after', parts: [], signal: controller.signal});
|
|
103
|
+
} catch {
|
|
104
|
+
// The host may replace the active project while resolving its workspace.
|
|
105
|
+
}
|
|
106
|
+
if (!controller.signal.aborted) setLocation({...selected, status: found ? 'located' : 'missingLocation'});
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
locate: locateBlock && (side => void locate(side)),
|
|
110
|
+
status: side => location?.comparison === comparison && location.locator === locateBlock && location.key === sideKey(side) ?
|
|
111
|
+
location.status : undefined
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ScriptComparison({script, labels, reviewed = false, setReviewed, renderBlock, loadAsset, location}: {
|
|
78
116
|
script: ScriptReview; labels: typeof ja; reviewed?: boolean; setReviewed?(value: boolean): void;
|
|
79
|
-
renderBlock: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader;
|
|
117
|
+
renderBlock: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader; location?: BlockLocation;
|
|
80
118
|
}) {
|
|
81
119
|
const [open, setOpen] = useState(false);
|
|
82
120
|
const {sync, toggle} = usePreviewSync(labels === ja ? 'ja' : 'en');
|
|
@@ -98,7 +136,9 @@ function ScriptComparison({script, labels, reviewed = false, setReviewed, render
|
|
|
98
136
|
{script[version].length ? script[version].map((side, index, sides) => <BlockPreview key={side.id} side={side}
|
|
99
137
|
title={sides.length > 1 ? `${labels[version]} ${index + 1}` : labels[version]}
|
|
100
138
|
marks={side.marks} renderBlock={renderBlock} withContext labels={labels}
|
|
101
|
-
version={version} loadAsset={loadAsset} sync={sync}
|
|
139
|
+
version={version} loadAsset={loadAsset} sync={sync}
|
|
140
|
+
onLocate={version === 'after' && location?.locate ? () => location.locate!(side) : undefined}
|
|
141
|
+
locationStatus={version === 'after' ? location?.status(side) : undefined}/>) :
|
|
102
142
|
<BlockPreview title={labels[version]} renderBlock={renderBlock} withContext labels={labels}/>}
|
|
103
143
|
</div>)}
|
|
104
144
|
</div>
|
|
@@ -135,10 +175,12 @@ function TargetThumbnails({target, labels, loadAsset}: {target: TargetReview; la
|
|
|
135
175
|
}
|
|
136
176
|
|
|
137
177
|
/** Lists the changed stages and sprites of one project file, each with its code comparisons. */
|
|
138
|
-
function ComparisonTargets({comparison, labels, renderBlock, loadAsset, review}: {
|
|
178
|
+
function ComparisonTargets({comparison, labels, renderBlock, loadAsset, locateBlock, review}: {
|
|
139
179
|
comparison: Comparison; labels: typeof ja; renderBlock: ProjectBlockRenderer; loadAsset?: ProjectAssetLoader;
|
|
180
|
+
locateBlock?: ProjectBlockLocator;
|
|
140
181
|
review?: {file: string; keys: ReadonlySet<string>; set(key: string, value: boolean): void};
|
|
141
182
|
}) {
|
|
183
|
+
const location = useBlockLocation(comparison, locateBlock);
|
|
142
184
|
return <>
|
|
143
185
|
{comparison.properties.length > 0 && <p>{labels.properties} · {propertyNames(comparison.properties, labels)}</p>}
|
|
144
186
|
{!comparison.targets.length && !comparison.properties.length && <p>{labels.noChanges}</p>}
|
|
@@ -149,7 +191,7 @@ function ComparisonTargets({comparison, labels, renderBlock, loadAsset, review}:
|
|
|
149
191
|
{target.scripts.map(script => {
|
|
150
192
|
const key = JSON.stringify([review?.file, target.isStage, target.name, script.id]);
|
|
151
193
|
return <ScriptComparison key={key} script={script} labels={labels} renderBlock={renderBlock} loadAsset={loadAsset}
|
|
152
|
-
reviewed={review?.keys.has(key)} setReviewed={review && (value => review.set(key, value))}/>;
|
|
194
|
+
location={location} reviewed={review?.keys.has(key)} setReviewed={review && (value => review.set(key, value))}/>;
|
|
153
195
|
})}
|
|
154
196
|
</section>)}
|
|
155
197
|
</>;
|
|
@@ -168,12 +210,14 @@ export interface ProjectSourceComparisonProps {
|
|
|
168
210
|
fallback?: React.ReactNode;
|
|
169
211
|
/** Reads the images of both versions for thumbnails. Without it, stages and sprites are shown by name. */
|
|
170
212
|
loadAsset?: ProjectAssetLoader;
|
|
213
|
+
/** Offers "Highlight in Editor" on the after version, which the caller has matched with the open project. */
|
|
214
|
+
locateBlock?: ProjectBlockLocator;
|
|
171
215
|
/** Shows only the code of this stage or sprite, without the overview and the changes outside the code. */
|
|
172
216
|
target?: {isStage: boolean; name: string};
|
|
173
217
|
}
|
|
174
218
|
|
|
175
219
|
/** Compares two versions of a project.source.json by stage, sprite and code, as the pull request review does. */
|
|
176
|
-
export function ProjectSourceComparison({before, after, renderBlock, locale = 'en', fallback, loadAsset, target}: ProjectSourceComparisonProps) {
|
|
220
|
+
export function ProjectSourceComparison({before, after, renderBlock, locale = 'en', fallback, loadAsset, locateBlock, target}: ProjectSourceComparisonProps) {
|
|
177
221
|
const labels = locale.startsWith('ja') ? ja : en;
|
|
178
222
|
const only = target && JSON.stringify([target.isStage, target.isStage || target.name]);
|
|
179
223
|
const comparison = useMemo(() => {
|
|
@@ -196,7 +240,8 @@ export function ProjectSourceComparison({before, after, renderBlock, locale = 'e
|
|
|
196
240
|
<div><dt>{labels.scripts}</dt><dd>{countScripts(comparison)}</dd></div>
|
|
197
241
|
</dl>
|
|
198
242
|
</header>}
|
|
199
|
-
<ComparisonTargets comparison={comparison} labels={labels} renderBlock={renderBlock} loadAsset={loadAsset}
|
|
243
|
+
<ComparisonTargets comparison={comparison} labels={labels} renderBlock={renderBlock} loadAsset={loadAsset}
|
|
244
|
+
locateBlock={locateBlock}/>
|
|
200
245
|
</section>;
|
|
201
246
|
}
|
|
202
247
|
|
|
@@ -251,6 +296,7 @@ export function ProjectSourceReview({title, description, verification, files, re
|
|
|
251
296
|
{(invalid || file.unavailable) && <p role="alert">{labels.invalid}</p>}
|
|
252
297
|
{comparison && <ComparisonTargets comparison={comparison} labels={labels} renderBlock={renderBlock}
|
|
253
298
|
loadAsset={'loadAsset' in file ? file.loadAsset : undefined}
|
|
299
|
+
locateBlock={'locateBlock' in file ? file.locateBlock : undefined}
|
|
254
300
|
review={{file: file.path, keys: reviewed, set: setReviewed}}/>}
|
|
255
301
|
</section>)}
|
|
256
302
|
</section>;
|