@kubohiroya/turbowarp-plus-editor-git 0.12.1 → 0.13.1
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.ja.md +14 -1
- package/README.md +14 -1
- package/dist/connection-settings.d.ts +2 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -3
- package/dist/{scratch-block-diff-renderer-e_drVlHE.js → project-comparison-D1rHzlCw.js} +79 -1
- package/dist/project-comparison.d.ts +31 -0
- package/dist/project-source-diff.d.ts +2 -0
- package/dist/project-source-diff.js +2 -2
- package/dist/smart-http.d.ts +3 -11
- package/dist/style.css +4 -0
- package/package.json +8 -1
- package/src/connection-settings.tsx +3 -4
- package/src/index.tsx +2 -0
- package/src/project-comparison.tsx +88 -0
- package/src/project-source-diff.tsx +2 -0
- package/src/smart-http.ts +5 -7
- package/src/style.css +4 -0
package/README.ja.md
CHANGED
|
@@ -18,7 +18,7 @@ Node.js 26 で `npm ci` と `npm test` を実行します。Vite でライブラ
|
|
|
18
18
|
|
|
19
19
|
`test/smart-http-fixture.js` は、`git-http-backend` を IPv4 のループバックで動かす検証用のフィクスチャです。Node.js の標準モジュールだけを使い、利用側のブラウザーテストが同じ版を使えるよう配布物に含めます。
|
|
20
20
|
|
|
21
|
-
`@kubohiroya/turbowarp-plus-editor-git/smart-http` の `createGitSmartHttp` に、Git 設定から選んだ GitHub の HTTPS リモート、信頼済みの接続先(`mode` と `proxyOrigin`)、現在のプロキシセッションか `null` を返す関数を渡します。auth パッケージの `GitAuthProvider` を使えます。Git
|
|
21
|
+
`@kubohiroya/turbowarp-plus-editor-git/smart-http` の `createGitSmartHttp` に、Git 設定から選んだ GitHub の HTTPS リモート、信頼済みの接続先(`mode` と `proxyOrigin`)、現在のプロキシセッションか `null` を返す関数を渡します。auth パッケージの `GitAuthProvider` を使えます。Git パッケージはこれらの型を auth パッケージから取り込みます。auth は省略可能な peer で、型を使う TypeScript の利用者だけがインストールすれば足ります。読み取り専用パネルとアダプターは auth パッケージが無くても動きます。
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
const transport = createGitSmartHttp({
|
|
@@ -114,6 +114,19 @@ const diffFile = new Blob([patch], {type: 'text/x-diff'});
|
|
|
114
114
|
|
|
115
115
|
どちらかの版を作品として読めないときは理由を示し、`fallback` を表示します。`fallback` がなければエラーだけを示します。
|
|
116
116
|
|
|
117
|
+
### 比べてから選ぶダイアログ
|
|
118
|
+
|
|
119
|
+
`openProjectComparison(document, options)` は、見出しの付いた複数の比較を 1 つのモーダルに並べ、利用者に操作を選んでもらいます。各 `sections` は `ProjectSourceComparison` と同じ `before`・`after` を取ります。選ばれた `actions` の `id` で解決し、閉じるか Escape では `null` で解決します。最初の操作にフォーカスを置くので、失われるものが少ない操作を先に並べます。作品フォルダは、未保存の編集があるときに「あなたの未保存の編集」と「フォルダでの変更」を並べるのに使います。
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const choice = await openProjectComparison(document, {
|
|
123
|
+
title: '外部の変更と未保存の編集',
|
|
124
|
+
sections: [{title: 'あなたの未保存の編集', before: base, after: edited}, {title: 'フォルダでの変更', before: base, after: disk}],
|
|
125
|
+
actions: [{id: 'keep', label: '編集を残す'}, {id: 'discard', label: '編集を破棄してフォルダから読み込む'}],
|
|
126
|
+
renderBlock, locale: 'ja'
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
117
130
|
## 公開
|
|
118
131
|
|
|
119
132
|
npm への公開は `.github/workflows/release.yml` が行う。`package.json` の版と同じ `v<版>` のタグを
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The panel structure and local Git approach were informed by [duff](https://githu
|
|
|
18
18
|
|
|
19
19
|
`test/smart-http-fixture.js` runs `git-http-backend` on the IPv4 loopback for tests. It uses only Node.js built-in modules and ships in the package so browser tests of a host use the fixture of the same version.
|
|
20
20
|
|
|
21
|
-
Import `createGitSmartHttp` from `@kubohiroya/turbowarp-plus-editor-git/smart-http`. Pass the selected GitHub HTTPS remote from Git config, a trusted connection (`mode` and `proxyOrigin`), and a function that returns the current proxy session or `null`. A `GitAuthProvider` from the auth package satisfies this contract. The Git package
|
|
21
|
+
Import `createGitSmartHttp` from `@kubohiroya/turbowarp-plus-editor-git/smart-http`. Pass the selected GitHub HTTPS remote from Git config, a trusted connection (`mode` and `proxyOrigin`), and a function that returns the current proxy session or `null`. A `GitAuthProvider` from the auth package satisfies this contract. The Git package takes these types from the auth package, which is an optional peer: only TypeScript users need it installed, and the read-only panel and the adapter run without it.
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
const transport = createGitSmartHttp({
|
|
@@ -37,6 +37,19 @@ Each receive-pack POST carries a new UUID v4 in `Git-Push-Id`. A browser replay
|
|
|
37
37
|
|
|
38
38
|
When the Editor host supplies connection settings, the Local Git panel offers fetch, fast-forward pull and push for the current branch and its existing GitHub HTTPS `origin`. Authorize push when connecting to enable the explicit push action. Fetch updates Git metadata. Pull requires a stopped project, a clean worktree, and regular non-executable files. Pull stages the changes, rechecks the local state, and then applies them. Reopen the project from the panel after a successful pull.
|
|
39
39
|
|
|
40
|
+
## Comparing before choosing
|
|
41
|
+
|
|
42
|
+
`openProjectComparison(document, options)` lays several titled comparisons of `project.source.json` texts in one modal and asks the user to choose an action. Each of `sections` takes the same `before` and `after` as `ProjectSourceComparison`. The promise resolves with the chosen action's `id`, or with `null` when the dialog is closed or Escape is pressed. The first action receives focus, so list the one that loses the least first. The project folder uses it to show "Your unsaved edits" next to "Changes in the folder" before replacing unsaved edits.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const choice = await openProjectComparison(document, {
|
|
46
|
+
title: 'External changes and your unsaved edits',
|
|
47
|
+
sections: [{title: 'Your unsaved edits', before: base, after: edited}, {title: 'Changes in the folder', before: base, after: disk}],
|
|
48
|
+
actions: [{id: 'keep', label: 'Keep My Edits'}, {id: 'discard', label: 'Discard Edits and Load from Folder'}],
|
|
49
|
+
renderBlock
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
40
53
|
## Release
|
|
41
54
|
|
|
42
55
|
`.github/workflows/release.yml` publishes to npm. Push a `v<version>` tag that names the
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { GitAuthState, GitConnectionMode as Mode } from '@kubohiroya/turbowarp-plus-editor-auth';
|
|
2
3
|
import type { GetRemoteSession, RemoteOperation } from './remote-operations.js';
|
|
3
|
-
type Mode = 'desktop' | 'cloudflare';
|
|
4
|
-
type Status = 'disconnected' | 'connecting' | 'connected' | 'reauth-required';
|
|
5
4
|
export interface GitConnectionSettings {
|
|
6
5
|
modes: readonly Mode[];
|
|
7
6
|
getView(): {
|
|
8
7
|
mode: Mode | null;
|
|
9
|
-
state:
|
|
10
|
-
status: Status;
|
|
11
|
-
account?: {
|
|
12
|
-
login: string;
|
|
13
|
-
};
|
|
14
|
-
expiresAt?: number;
|
|
15
|
-
};
|
|
8
|
+
state: GitAuthState;
|
|
16
9
|
proxyOrigin?: string;
|
|
17
10
|
write?: boolean;
|
|
18
11
|
authorization?: {
|
|
@@ -32,4 +25,3 @@ export declare function ConnectionSettings({ settings, locale, readOrigin, runOp
|
|
|
32
25
|
readOrigin?: () => Promise<string | undefined>;
|
|
33
26
|
runOperation?: (operation: RemoteOperation) => Promise<void>;
|
|
34
27
|
}): React.JSX.Element;
|
|
35
|
-
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { createEditorBlockLocator } from './editor-block-locator.js';
|
|
|
6
6
|
export { createScratchBlocksDiffRenderer } from './scratch-block-diff-renderer.js';
|
|
7
7
|
export type { ScratchBlockDiffRendererOptions, ScratchBlocksPort } from './scratch-block-diff-renderer.js';
|
|
8
8
|
export { openRepository, createReadOnlyFs, GitPanelError } from './repository.js';
|
|
9
|
+
export { openProjectComparison } from './project-comparison.js';
|
|
10
|
+
export type { ProjectComparisonAction, ProjectComparisonOptions, ProjectComparisonSection } from './project-comparison.js';
|
|
9
11
|
export type { Repository, Snapshot, FileChange, FileDiff } from './repository.js';
|
|
10
12
|
export interface GitHost {
|
|
11
13
|
connectionSettings?: GitConnectionSettings;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as parseProjectSource, a as PullRequestError, c as ProjectSourceComparison, d as organizeProjectChanges, f as projectPropertyLabel, i as createScratchBlocksDiffLocator, l as ProjectSourceReview, n as openProjectComparison, o as createGitHubPullRequests, r as createScratchBlocksDiffRenderer, s as gitHubRepositoryFromOrigin, v as useAssetImage, y as useCostumeImage } from "./project-comparison-D1rHzlCw.js";
|
|
2
2
|
import { n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2$1 } from "./objectSpread2-Dkd68nJH.js";
|
|
3
3
|
import { i as createReadOnlyFs, r as GitPanelError, t as openRepository } from "./repository-CtvWYPQ8.js";
|
|
4
4
|
import * as m from "react";
|
|
@@ -3277,7 +3277,7 @@ function ConnectionSettings({ settings, locale, readOrigin, runOperation }) {
|
|
|
3277
3277
|
}, labels[choice])))), /* @__PURE__ */ React.createElement("p", null, labels.changed), /* @__PURE__ */ React.createElement("p", {
|
|
3278
3278
|
"data-git-connection-state": "",
|
|
3279
3279
|
role: "status"
|
|
3280
|
-
}, labels[state.status]), proxyOrigin && /* @__PURE__ */ React.createElement("p", null, labels.proxy, " · ", /* @__PURE__ */ React.createElement("code", null, proxyOrigin)), state.
|
|
3280
|
+
}, labels[state.status]), proxyOrigin && /* @__PURE__ */ React.createElement("p", null, labels.proxy, " · ", /* @__PURE__ */ React.createElement("code", null, proxyOrigin)), state.status === "connected" && /* @__PURE__ */ React.createElement("p", null, labels.account, " · ", state.account.login), !readOrigin && /* @__PURE__ */ React.createElement("p", null, labels.noRemote), mode && /* @__PURE__ */ React.createElement(React.Fragment, null, runOperation && /* @__PURE__ */ React.createElement("label", null, /* @__PURE__ */ React.createElement("input", {
|
|
3281
3281
|
type: "checkbox",
|
|
3282
3282
|
checked: allowPush,
|
|
3283
3283
|
disabled: busy || state.status === "connecting" || state.status === "connected",
|
|
@@ -4065,4 +4065,4 @@ function renderGitMenu(createElement, { vm, locale = "en", MenuItem, MenuSection
|
|
|
4065
4065
|
}
|
|
4066
4066
|
}, labelsFor(locale).menu));
|
|
4067
4067
|
}
|
|
4068
|
-
export { GitPanelError, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, mountGitPanel, openRepository, renderGitMenu };
|
|
4068
|
+
export { GitPanelError, createEditorBlockLocator, createReadOnlyFs, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, mountGitPanel, openProjectComparison, openRepository, renderGitMenu };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
|
|
2
2
|
import { a as createTwoFilesPatch, i as isAssetName, n as assetsDirectoryParts, o as hasOnlyUnixLineEndings, r as folderSettingsLimit, s as hasOnlyWinLineEndings, t as assetLimit } from "./project-asset-paths-KV715791.js";
|
|
3
3
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { render, unmountComponentAtNode } from "react-dom";
|
|
4
5
|
// node_modules/diff/libesm/patch/line-endings.js
|
|
5
6
|
function unixToWin(patch) {
|
|
6
7
|
if (Array.isArray(patch)) return patch.map((p) => unixToWin(p));
|
|
@@ -2414,4 +2415,81 @@ function createScratchBlocksDiffRenderer(options) {
|
|
|
2414
2415
|
};
|
|
2415
2416
|
};
|
|
2416
2417
|
}
|
|
2417
|
-
|
|
2418
|
+
// src/project-comparison.tsx
|
|
2419
|
+
init_objectSpread2();
|
|
2420
|
+
var focusable = "button:not(:disabled), input:not(:disabled), select:not(:disabled), a[href], summary, [tabindex=\"0\"]";
|
|
2421
|
+
/** A modal that lays project comparisons side by side in sections and asks the user to choose an action. */
|
|
2422
|
+
function ProjectComparisonDialog({ title, description, notes = [], sections, actions, closeLabel = "Close", renderBlock, locale = "en", onChoose }) {
|
|
2423
|
+
const dialog = useRef(null);
|
|
2424
|
+
const first = useRef(null);
|
|
2425
|
+
useEffect(() => {
|
|
2426
|
+
var _dialog$current, _ref, _first$current;
|
|
2427
|
+
const previous = (_dialog$current = dialog.current) === null || _dialog$current === void 0 ? void 0 : _dialog$current.ownerDocument.activeElement;
|
|
2428
|
+
(_ref = (_first$current = first.current) !== null && _first$current !== void 0 ? _first$current : dialog.current) === null || _ref === void 0 || _ref.focus();
|
|
2429
|
+
return () => {
|
|
2430
|
+
if (previous === null || previous === void 0 ? void 0 : previous.isConnected) previous.focus();
|
|
2431
|
+
};
|
|
2432
|
+
}, []);
|
|
2433
|
+
const keyDown = (event) => {
|
|
2434
|
+
if (event.key === "Escape") {
|
|
2435
|
+
event.stopPropagation();
|
|
2436
|
+
onChoose(null);
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
if (event.key !== "Tab") return;
|
|
2440
|
+
const items = [...event.currentTarget.querySelectorAll(focusable)].filter((item) => item.getClientRects().length > 0);
|
|
2441
|
+
const start = items[0], end = items[items.length - 1];
|
|
2442
|
+
const active = event.currentTarget.ownerDocument.activeElement;
|
|
2443
|
+
if (event.shiftKey && (active === start || active === event.currentTarget)) {
|
|
2444
|
+
event.preventDefault();
|
|
2445
|
+
end === null || end === void 0 || end.focus();
|
|
2446
|
+
} else if (!event.shiftKey && active === end) {
|
|
2447
|
+
event.preventDefault();
|
|
2448
|
+
start === null || start === void 0 || start.focus();
|
|
2449
|
+
}
|
|
2450
|
+
};
|
|
2451
|
+
return /* @__PURE__ */ React.createElement("div", { className: "twgit-backdrop" }, /* @__PURE__ */ React.createElement("div", {
|
|
2452
|
+
className: "twgit-panel twgit-comparison",
|
|
2453
|
+
ref: dialog,
|
|
2454
|
+
tabIndex: -1,
|
|
2455
|
+
role: "dialog",
|
|
2456
|
+
"aria-modal": "true",
|
|
2457
|
+
"aria-label": title,
|
|
2458
|
+
onKeyDown: keyDown
|
|
2459
|
+
}, /* @__PURE__ */ React.createElement("header", null, /* @__PURE__ */ React.createElement("h2", null, title), /* @__PURE__ */ React.createElement("button", {
|
|
2460
|
+
type: "button",
|
|
2461
|
+
onClick: () => onChoose(null)
|
|
2462
|
+
}, closeLabel)), /* @__PURE__ */ React.createElement("div", { className: "twgit-comparison-body" }, description && /* @__PURE__ */ React.createElement("p", null, description), notes.map((note) => /* @__PURE__ */ React.createElement("p", {
|
|
2463
|
+
key: note,
|
|
2464
|
+
role: "note"
|
|
2465
|
+
}, note)), sections.map((section) => /* @__PURE__ */ React.createElement("section", {
|
|
2466
|
+
key: section.title,
|
|
2467
|
+
"aria-label": section.title,
|
|
2468
|
+
"data-twgit-comparison": ""
|
|
2469
|
+
}, /* @__PURE__ */ React.createElement("h3", null, section.title), /* @__PURE__ */ React.createElement(ProjectSourceComparison, {
|
|
2470
|
+
before: section.before,
|
|
2471
|
+
after: section.after,
|
|
2472
|
+
renderBlock,
|
|
2473
|
+
locale
|
|
2474
|
+
})))), /* @__PURE__ */ React.createElement("footer", { className: "twgit-comparison-actions" }, actions.map((action, index) => /* @__PURE__ */ React.createElement("button", {
|
|
2475
|
+
key: action.id,
|
|
2476
|
+
type: "button",
|
|
2477
|
+
ref: index === 0 ? first : void 0,
|
|
2478
|
+
"data-twgit-action": action.id,
|
|
2479
|
+
onClick: () => onChoose(action.id)
|
|
2480
|
+
}, action.label)))));
|
|
2481
|
+
}
|
|
2482
|
+
/** Shows {@link ProjectComparisonDialog} and resolves with the chosen action's id, or `null` when it is closed. */
|
|
2483
|
+
function openProjectComparison(document, options) {
|
|
2484
|
+
const root = document.createElement("div");
|
|
2485
|
+
document.body.append(root);
|
|
2486
|
+
return new Promise((resolve) => {
|
|
2487
|
+
const choose = (action) => {
|
|
2488
|
+
unmountComponentAtNode(root);
|
|
2489
|
+
root.remove();
|
|
2490
|
+
resolve(action);
|
|
2491
|
+
};
|
|
2492
|
+
render(/* @__PURE__ */ React.createElement(ProjectComparisonDialog, _objectSpread2(_objectSpread2({}, options), {}, { onChoose: choose })), root);
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
export { readProjectSourceDiff as C, parseProjectSource as S, currentCostume as _, PullRequestError as a, compareProjectBlocks as b, ProjectSourceComparison as c, organizeProjectChanges as d, projectPropertyLabel as f, usePreviewSync as g, createPreviewSync as h, createScratchBlocksDiffLocator as i, ProjectSourceReview as l, BlockPreview as m, openProjectComparison as n, createGitHubPullRequests as o, compareProjectForReview as p, createScratchBlocksDiffRenderer as r, gitHubRepositoryFromOrigin as s, ProjectComparisonDialog as t, assetFileName as u, useAssetImage as v, createProjectSourceDiff as x, useCostumeImage as y };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
|
|
3
|
+
/** One comparison of two project.source.json texts, shown under its own heading. */
|
|
4
|
+
export interface ProjectComparisonSection {
|
|
5
|
+
title: string;
|
|
6
|
+
before: string | null;
|
|
7
|
+
after: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface ProjectComparisonAction {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ProjectComparisonOptions {
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
/** Notes shown after the description, such as edits the comparison cannot show. */
|
|
17
|
+
notes?: readonly string[];
|
|
18
|
+
sections: readonly ProjectComparisonSection[];
|
|
19
|
+
/** The first action is the one the dialog focuses. Closing the dialog or pressing Escape chooses none. */
|
|
20
|
+
actions: readonly ProjectComparisonAction[];
|
|
21
|
+
closeLabel?: string;
|
|
22
|
+
renderBlock: ProjectBlockRenderer;
|
|
23
|
+
locale?: string;
|
|
24
|
+
}
|
|
25
|
+
export interface ProjectComparisonDialogProps extends ProjectComparisonOptions {
|
|
26
|
+
onChoose(action: string | null): void;
|
|
27
|
+
}
|
|
28
|
+
/** A modal that lays project comparisons side by side in sections and asks the user to choose an action. */
|
|
29
|
+
export declare function ProjectComparisonDialog({ title, description, notes, sections, actions, closeLabel, renderBlock, locale, onChoose }: ProjectComparisonDialogProps): React.JSX.Element;
|
|
30
|
+
/** Shows {@link ProjectComparisonDialog} and resolves with the chosen action's id, or `null` when it is closed. */
|
|
31
|
+
export declare function openProjectComparison(document: Document, options: ProjectComparisonOptions): Promise<string | null>;
|
|
@@ -4,6 +4,8 @@ import type { ProjectBlockRenderer } from './scratch-block-diff-renderer.js';
|
|
|
4
4
|
import './project-source-diff.css';
|
|
5
5
|
export { createGitHubPullRequests, gitHubRepositoryFromOrigin } from './github-pull-requests.js';
|
|
6
6
|
export { ProjectSourceComparison, ProjectSourceReview } from './project-source-review.js';
|
|
7
|
+
export { ProjectComparisonDialog, openProjectComparison } from './project-comparison.js';
|
|
8
|
+
export type { ProjectComparisonAction, ProjectComparisonDialogProps, ProjectComparisonOptions, ProjectComparisonSection } from './project-comparison.js';
|
|
7
9
|
export type { ProjectSourceComparisonProps, ProjectSourceReviewFile, ProjectSourceReviewProps } from './project-source-review.js';
|
|
8
10
|
export { createScratchBlocksDiffLocator } from './block-diff-location.js';
|
|
9
11
|
export type { ProjectBlockLocation, ProjectBlockLocator, ScratchBlockLocationWorkspace } from './block-diff-location.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as readProjectSourceDiff, _ as currentCostume, b as compareProjectBlocks, c as ProjectSourceComparison, d as organizeProjectChanges, f as projectPropertyLabel, g as usePreviewSync, h as createPreviewSync, i as createScratchBlocksDiffLocator, l as ProjectSourceReview, m as BlockPreview, n as openProjectComparison, o as createGitHubPullRequests, p as compareProjectForReview, r as createScratchBlocksDiffRenderer, s as gitHubRepositoryFromOrigin, t as ProjectComparisonDialog, u as assetFileName, x as createProjectSourceDiff } from "./project-comparison-D1rHzlCw.js";
|
|
2
2
|
import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
|
|
3
3
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
// src/project-source-diff.tsx
|
|
@@ -169,4 +169,4 @@ function ProjectSourceBlockDiff({ patch, beforeSource, renderBlock, locateBlock,
|
|
|
169
169
|
})
|
|
170
170
|
}, labels.more));
|
|
171
171
|
}
|
|
172
|
-
export { ProjectSourceBlockDiff, ProjectSourceComparison, ProjectSourceReview, assetFileName, compareProjectForReview, createGitHubPullRequests, createPreviewSync, createProjectSourceDiff, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, currentCostume, gitHubRepositoryFromOrigin, organizeProjectChanges, projectPropertyLabel };
|
|
172
|
+
export { ProjectComparisonDialog, ProjectSourceBlockDiff, ProjectSourceComparison, ProjectSourceReview, assetFileName, compareProjectForReview, createGitHubPullRequests, createPreviewSync, createProjectSourceDiff, createScratchBlocksDiffLocator, createScratchBlocksDiffRenderer, currentCostume, gitHubRepositoryFromOrigin, openProjectComparison, organizeProjectChanges, projectPropertyLabel };
|
package/dist/smart-http.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
+
import type { GitProxyConnection, GitProxySession } from '@kubohiroya/turbowarp-plus-editor-auth';
|
|
1
2
|
export declare const MAX_GIT_REQUEST_BYTES: number;
|
|
2
|
-
type Connection = Readonly<{
|
|
3
|
-
mode: 'desktop' | 'cloudflare';
|
|
4
|
-
proxyOrigin: string;
|
|
5
|
-
}>;
|
|
6
|
-
type Session = Readonly<{
|
|
7
|
-
connection: Connection;
|
|
8
|
-
bearer: string;
|
|
9
|
-
expiresAt: number;
|
|
10
|
-
}>;
|
|
11
3
|
export interface GitSmartHttpOptions {
|
|
12
4
|
readonly remote: string;
|
|
13
|
-
readonly connection:
|
|
14
|
-
readonly getSession: () => Promise<
|
|
5
|
+
readonly connection: GitProxyConnection;
|
|
6
|
+
readonly getSession: () => Promise<GitProxySession | null>;
|
|
15
7
|
readonly maxRequestBytes?: number;
|
|
16
8
|
readonly signal?: AbortSignal;
|
|
17
9
|
readonly fetch?: typeof fetch;
|
package/dist/style.css
CHANGED
|
@@ -179,4 +179,8 @@
|
|
|
179
179
|
.twgit-git-details > .twgit-connection-body { left: auto; right: 0; }
|
|
180
180
|
/* In a layout tab the panel fills the tab and leaves the Editor beside it. */
|
|
181
181
|
.twgit-panel.twgit-panel-tab { width: 100%; height: 100%; border: 0; border-radius: 0; box-shadow: none; }
|
|
182
|
+
.twgit-comparison-body { flex: 1; overflow: auto; padding: 12px 16px; }
|
|
183
|
+
.twgit-comparison-body > p { margin: 0 0 8px; }
|
|
184
|
+
.twgit-comparison-body > section { margin-top: 16px; }
|
|
185
|
+
.twgit-comparison-actions { display: flex; gap: 8px; justify-content: flex-end; padding: 10px 16px; border-top: 1px solid GrayText; }
|
|
182
186
|
/*$vite$:1*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubohiroya/turbowarp-plus-editor-git",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Read-only local Git panel for TurboWarp Plus Editor",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MPL-2.0",
|
|
@@ -49,10 +49,17 @@
|
|
|
49
49
|
"prepack": "npm run build"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
+
"@kubohiroya/turbowarp-plus-editor-auth": ">=0.3.0 <0.4.0",
|
|
52
53
|
"react": ">=16.14 <19",
|
|
53
54
|
"react-dom": ">=16.14 <19"
|
|
54
55
|
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"@kubohiroya/turbowarp-plus-editor-auth": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
55
61
|
"devDependencies": {
|
|
62
|
+
"@kubohiroya/turbowarp-plus-editor-auth": "0.3.0",
|
|
56
63
|
"@types/node": "24.19.2",
|
|
57
64
|
"@types/react": "16.14.70",
|
|
58
65
|
"@types/react-dom": "16.9.25",
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import React, {useEffect, useRef, useState} from 'react';
|
|
2
|
+
import type {GitAuthState, GitConnectionMode as Mode} from '@kubohiroya/turbowarp-plus-editor-auth';
|
|
2
3
|
import type {GetRemoteSession, RemoteOperation} from './remote-operations.js';
|
|
3
4
|
|
|
4
|
-
type Mode = 'desktop' | 'cloudflare';
|
|
5
|
-
type Status = 'disconnected' | 'connecting' | 'connected' | 'reauth-required';
|
|
6
5
|
export interface GitConnectionSettings {
|
|
7
6
|
modes: readonly Mode[];
|
|
8
7
|
getView(): {
|
|
9
8
|
mode: Mode | null;
|
|
10
|
-
state:
|
|
9
|
+
state: GitAuthState;
|
|
11
10
|
proxyOrigin?: string;
|
|
12
11
|
write?: boolean;
|
|
13
12
|
authorization?: {url: string; userCode?: string};
|
|
@@ -87,7 +86,7 @@ export function ConnectionSettings({settings, locale, readOrigin, runOperation}:
|
|
|
87
86
|
<p>{labels.changed}</p>
|
|
88
87
|
<p data-git-connection-state="" role="status">{labels[state.status]}</p>
|
|
89
88
|
{proxyOrigin && <p>{labels.proxy} · <code>{proxyOrigin}</code></p>}
|
|
90
|
-
{state.
|
|
89
|
+
{state.status === 'connected' && <p>{labels.account} · {state.account.login}</p>}
|
|
91
90
|
{!readOrigin && <p>{labels.noRemote}</p>}
|
|
92
91
|
{mode && <>
|
|
93
92
|
{runOperation && <label><input type="checkbox" checked={allowPush}
|
package/src/index.tsx
CHANGED
|
@@ -22,6 +22,8 @@ export {createScratchBlocksDiffRenderer} from './scratch-block-diff-renderer.js'
|
|
|
22
22
|
export type {ScratchBlockDiffRendererOptions, ScratchBlocksPort} from './scratch-block-diff-renderer.js';
|
|
23
23
|
|
|
24
24
|
export {openRepository, createReadOnlyFs, GitPanelError} from './repository.js';
|
|
25
|
+
export {openProjectComparison} from './project-comparison.js';
|
|
26
|
+
export type {ProjectComparisonAction, ProjectComparisonOptions, ProjectComparisonSection} from './project-comparison.js';
|
|
25
27
|
export type {Repository, Snapshot, FileChange, FileDiff} from './repository.js';
|
|
26
28
|
|
|
27
29
|
export interface GitHost {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, {useEffect, useRef} from 'react';
|
|
2
|
+
import {render, unmountComponentAtNode} from 'react-dom';
|
|
3
|
+
import {ProjectSourceComparison} from './project-source-review.js';
|
|
4
|
+
import type {ProjectBlockRenderer} from './scratch-block-diff-renderer.js';
|
|
5
|
+
|
|
6
|
+
/** One comparison of two project.source.json texts, shown under its own heading. */
|
|
7
|
+
export interface ProjectComparisonSection {
|
|
8
|
+
title: string;
|
|
9
|
+
before: string | null;
|
|
10
|
+
after: string | null;
|
|
11
|
+
}
|
|
12
|
+
export interface ProjectComparisonAction {
|
|
13
|
+
id: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ProjectComparisonOptions {
|
|
17
|
+
title: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Notes shown after the description, such as edits the comparison cannot show. */
|
|
20
|
+
notes?: readonly string[];
|
|
21
|
+
sections: readonly ProjectComparisonSection[];
|
|
22
|
+
/** The first action is the one the dialog focuses. Closing the dialog or pressing Escape chooses none. */
|
|
23
|
+
actions: readonly ProjectComparisonAction[];
|
|
24
|
+
closeLabel?: string;
|
|
25
|
+
renderBlock: ProjectBlockRenderer;
|
|
26
|
+
locale?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ProjectComparisonDialogProps extends ProjectComparisonOptions {
|
|
29
|
+
onChoose(action: string | null): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const focusable = 'button:not(:disabled), input:not(:disabled), select:not(:disabled), a[href], summary, [tabindex="0"]';
|
|
33
|
+
|
|
34
|
+
/** A modal that lays project comparisons side by side in sections and asks the user to choose an action. */
|
|
35
|
+
export function ProjectComparisonDialog({title, description, notes = [], sections, actions, closeLabel = 'Close',
|
|
36
|
+
renderBlock, locale = 'en', onChoose}: ProjectComparisonDialogProps) {
|
|
37
|
+
const dialog = useRef<HTMLDivElement>(null);
|
|
38
|
+
const first = useRef<HTMLButtonElement>(null);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const previous = dialog.current?.ownerDocument.activeElement as HTMLElement | null;
|
|
41
|
+
(first.current ?? dialog.current)?.focus();
|
|
42
|
+
return () => {if (previous?.isConnected) previous.focus();};
|
|
43
|
+
}, []);
|
|
44
|
+
const keyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
45
|
+
if (event.key === 'Escape') {event.stopPropagation(); onChoose(null); return;}
|
|
46
|
+
if (event.key !== 'Tab') return;
|
|
47
|
+
const items = [...event.currentTarget.querySelectorAll<HTMLElement>(focusable)].filter(item => item.getClientRects().length > 0);
|
|
48
|
+
const start = items[0], end = items[items.length - 1];
|
|
49
|
+
const active = event.currentTarget.ownerDocument.activeElement;
|
|
50
|
+
if (event.shiftKey && (active === start || active === event.currentTarget)) {event.preventDefault(); end?.focus();}
|
|
51
|
+
else if (!event.shiftKey && active === end) {event.preventDefault(); start?.focus();}
|
|
52
|
+
};
|
|
53
|
+
return <div className="twgit-backdrop">
|
|
54
|
+
<div className="twgit-panel twgit-comparison" ref={dialog} tabIndex={-1} role="dialog" aria-modal="true"
|
|
55
|
+
aria-label={title} onKeyDown={keyDown}>
|
|
56
|
+
<header>
|
|
57
|
+
<h2>{title}</h2>
|
|
58
|
+
<button type="button" onClick={() => onChoose(null)}>{closeLabel}</button>
|
|
59
|
+
</header>
|
|
60
|
+
<div className="twgit-comparison-body">
|
|
61
|
+
{description && <p>{description}</p>}
|
|
62
|
+
{notes.map(note => <p key={note} role="note">{note}</p>)}
|
|
63
|
+
{sections.map(section => <section key={section.title} aria-label={section.title} data-twgit-comparison="">
|
|
64
|
+
<h3>{section.title}</h3>
|
|
65
|
+
<ProjectSourceComparison before={section.before} after={section.after} renderBlock={renderBlock} locale={locale}/>
|
|
66
|
+
</section>)}
|
|
67
|
+
</div>
|
|
68
|
+
<footer className="twgit-comparison-actions">
|
|
69
|
+
{actions.map((action, index) => <button key={action.id} type="button" ref={index === 0 ? first : undefined}
|
|
70
|
+
data-twgit-action={action.id} onClick={() => onChoose(action.id)}>{action.label}</button>)}
|
|
71
|
+
</footer>
|
|
72
|
+
</div>
|
|
73
|
+
</div>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Shows {@link ProjectComparisonDialog} and resolves with the chosen action's id, or `null` when it is closed. */
|
|
77
|
+
export function openProjectComparison(document: Document, options: ProjectComparisonOptions): Promise<string | null> {
|
|
78
|
+
const root = document.createElement('div');
|
|
79
|
+
document.body.append(root);
|
|
80
|
+
return new Promise(resolve => {
|
|
81
|
+
const choose = (action: string | null) => {
|
|
82
|
+
unmountComponentAtNode(root);
|
|
83
|
+
root.remove();
|
|
84
|
+
resolve(action);
|
|
85
|
+
};
|
|
86
|
+
render(<ProjectComparisonDialog {...options} onChoose={choose}/>, root);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -9,6 +9,8 @@ import './project-source-diff.css';
|
|
|
9
9
|
|
|
10
10
|
export {createGitHubPullRequests, gitHubRepositoryFromOrigin} from './github-pull-requests.js';
|
|
11
11
|
export {ProjectSourceComparison, ProjectSourceReview} from './project-source-review.js';
|
|
12
|
+
export {ProjectComparisonDialog, openProjectComparison} from './project-comparison.js';
|
|
13
|
+
export type {ProjectComparisonAction, ProjectComparisonDialogProps, ProjectComparisonOptions, ProjectComparisonSection} from './project-comparison.js';
|
|
12
14
|
export type {ProjectSourceComparisonProps, ProjectSourceReviewFile, ProjectSourceReviewProps} from './project-source-review.js';
|
|
13
15
|
|
|
14
16
|
export {createScratchBlocksDiffLocator} from './block-diff-location.js';
|
package/src/smart-http.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import type {GitProxyConnection, GitProxySession} from '@kubohiroya/turbowarp-plus-editor-auth';
|
|
1
2
|
import type {HttpClient} from 'isomorphic-git';
|
|
2
3
|
|
|
3
4
|
export const MAX_GIT_REQUEST_BYTES = 32 * 1024 * 1024;
|
|
4
5
|
|
|
5
|
-
type Connection = Readonly<{mode: 'desktop' | 'cloudflare'; proxyOrigin: string}>;
|
|
6
|
-
type Session = Readonly<{connection: Connection; bearer: string; expiresAt: number}>;
|
|
7
|
-
|
|
8
6
|
export interface GitSmartHttpOptions {
|
|
9
7
|
readonly remote: string;
|
|
10
|
-
readonly connection:
|
|
11
|
-
readonly getSession: () => Promise<
|
|
8
|
+
readonly connection: GitProxyConnection;
|
|
9
|
+
readonly getSession: () => Promise<GitProxySession | null>;
|
|
12
10
|
readonly maxRequestBytes?: number;
|
|
13
11
|
readonly signal?: AbortSignal;
|
|
14
12
|
readonly fetch?: typeof fetch;
|
|
@@ -54,7 +52,7 @@ function remoteUrl(remote: string) {
|
|
|
54
52
|
return `https://github.com/${match[1]}/${match[2]}.git`;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
function checkConnection({mode, proxyOrigin}:
|
|
55
|
+
function checkConnection({mode, proxyOrigin}: GitProxyConnection) {
|
|
58
56
|
const url = URL.parse(proxyOrigin);
|
|
59
57
|
if (!url || url.origin !== proxyOrigin ||
|
|
60
58
|
(mode === 'desktop' ? !/^http:\/\/127\.0\.0\.1:[1-9][0-9]{0,4}$/u.test(proxyOrigin) :
|
|
@@ -142,7 +140,7 @@ export function createGitSmartHttp(options: GitSmartHttpOptions) {
|
|
|
142
140
|
checkAbort(abort);
|
|
143
141
|
const payload = body ? await boundedBody(body, maxRequestBytes, abort) : undefined;
|
|
144
142
|
checkAbort(abort);
|
|
145
|
-
let session:
|
|
143
|
+
let session: GitProxySession | null;
|
|
146
144
|
try {
|
|
147
145
|
session = await getSession();
|
|
148
146
|
} catch {
|
package/src/style.css
CHANGED
|
@@ -57,3 +57,7 @@
|
|
|
57
57
|
.twgit-git-details > .twgit-connection-body { left: auto; right: 0; }
|
|
58
58
|
/* In a layout tab the panel fills the tab and leaves the Editor beside it. */
|
|
59
59
|
.twgit-panel.twgit-panel-tab { width: 100%; height: 100%; border: 0; border-radius: 0; box-shadow: none; }
|
|
60
|
+
.twgit-comparison-body { flex: 1; overflow: auto; padding: 12px 16px; }
|
|
61
|
+
.twgit-comparison-body > p { margin: 0 0 8px; }
|
|
62
|
+
.twgit-comparison-body > section { margin-top: 16px; }
|
|
63
|
+
.twgit-comparison-actions { display: flex; gap: 8px; justify-content: flex-end; padding: 10px 16px; border-top: 1px solid GrayText; }
|