@kubohiroya/turbowarp-plus-editor-git 0.17.0 → 0.18.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.ja.md +4 -2
- package/README.md +3 -1
- package/dist/branch-button.d.ts +32 -0
- package/dist/index.d.ts +10 -5
- package/dist/index.js +566 -148
- package/dist/messages.d.ts +8 -2
- package/dist/{project-asset-paths-nL5sV35A.js → project-asset-paths-BzwVf9HE.js} +1 -1
- package/dist/{project-comparison-DjMlwyp8.js → project-comparison-BXOQcZ73.js} +1 -1
- package/dist/project-source-diff.js +2 -2
- package/dist/{repository-xIv0iZkj.js → repository-ByKRBftJ.js} +25 -8
- package/dist/repository.d.ts +7 -0
- package/dist/repository.js +1 -1
- package/dist/style.css +28 -0
- package/package.json +1 -1
- package/src/branch-button.tsx +216 -0
- package/src/index.tsx +183 -65
- package/src/messages.ts +6 -4
- package/src/repository.ts +22 -8
- package/src/style.css +28 -0
package/dist/messages.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ export declare const messages: {
|
|
|
3
3
|
pullRequests: string;
|
|
4
4
|
localChanges: string;
|
|
5
5
|
title: string;
|
|
6
|
-
menu: string;
|
|
7
6
|
close: string;
|
|
8
7
|
choose: string;
|
|
9
8
|
refresh: string;
|
|
@@ -110,12 +109,15 @@ export declare const messages: {
|
|
|
110
109
|
historyCopyFailed: string;
|
|
111
110
|
historyRefs: string;
|
|
112
111
|
cancel: string;
|
|
112
|
+
branchMenu: string;
|
|
113
|
+
openPanel: string;
|
|
114
|
+
newBranchItem: string;
|
|
115
|
+
newBranchTitle: string;
|
|
113
116
|
};
|
|
114
117
|
ja: {
|
|
115
118
|
pullRequests: string;
|
|
116
119
|
localChanges: string;
|
|
117
120
|
title: string;
|
|
118
|
-
menu: string;
|
|
119
121
|
close: string;
|
|
120
122
|
choose: string;
|
|
121
123
|
refresh: string;
|
|
@@ -222,6 +224,10 @@ export declare const messages: {
|
|
|
222
224
|
historyCopyFailed: string;
|
|
223
225
|
historyRefs: string;
|
|
224
226
|
cancel: string;
|
|
227
|
+
branchMenu: string;
|
|
228
|
+
openPanel: string;
|
|
229
|
+
newBranchItem: string;
|
|
230
|
+
newBranchTitle: string;
|
|
225
231
|
};
|
|
226
232
|
};
|
|
227
233
|
export type Labels = typeof messages.en;
|
|
@@ -1410,4 +1410,4 @@ function assetsDirectoryParts(settings) {
|
|
|
1410
1410
|
const parts = value.split("/");
|
|
1411
1411
|
return parts.every((part) => part && part !== "." && part !== ".." && part.toLowerCase() !== ".git" && !/[\\\0-\x1f\x7f]/.test(part)) ? parts : null;
|
|
1412
1412
|
}
|
|
1413
|
-
export { compareProjectBlocks as a, isRecord as c, targetKey as d,
|
|
1413
|
+
export { compareProjectBlocks as a, isRecord as c, targetKey as d, _objectWithoutProperties as f, isAssetName as i, parseProjectSource as l, assetsDirectoryParts as n, createProjectSourceDiff as o, createTwoFilesPatch as p, folderSettingsLimit as r, equal as s, assetLimit as t, readProjectSourceDiff as u };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
|
|
2
|
-
import { a as compareProjectBlocks, c as isRecord, d as targetKey, i as isAssetName, l as parseProjectSource, n as assetsDirectoryParts, o as createProjectSourceDiff, r as folderSettingsLimit, s as equal, t as assetLimit, u as readProjectSourceDiff } from "./project-asset-paths-
|
|
2
|
+
import { a as compareProjectBlocks, c as isRecord, d as targetKey, i as isAssetName, l as parseProjectSource, n as assetsDirectoryParts, o as createProjectSourceDiff, r as folderSettingsLimit, s as equal, t as assetLimit, u as readProjectSourceDiff } from "./project-asset-paths-BzwVf9HE.js";
|
|
3
3
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { render, unmountComponentAtNode } from "react-dom";
|
|
5
5
|
// src/close-button.tsx
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as compareProjectBlocks, o as createProjectSourceDiff, u as readProjectSourceDiff } from "./project-asset-paths-
|
|
2
|
-
import { _ as usePreviewSync, a as openProjectSourceTabs, c as gitHubRepositoryFromOrigin, d as assetFileName, f as organizeProjectChanges, g as createPreviewSync, h as BlockPreview, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, m as compareProjectForReview, n as openProjectComparison, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, t as ProjectComparisonDialog, u as ProjectSourceReview, v as currentCostume } from "./project-comparison-
|
|
1
|
+
import { a as compareProjectBlocks, o as createProjectSourceDiff, u as readProjectSourceDiff } from "./project-asset-paths-BzwVf9HE.js";
|
|
2
|
+
import { _ as usePreviewSync, a as openProjectSourceTabs, c as gitHubRepositoryFromOrigin, d as assetFileName, f as organizeProjectChanges, g as createPreviewSync, h as BlockPreview, i as createScratchBlocksDiffLocator, l as ProjectSourceComparison, m as compareProjectForReview, n as openProjectComparison, p as projectPropertyLabel, r as createScratchBlocksDiffRenderer, s as createGitHubPullRequests, t as ProjectComparisonDialog, u as ProjectSourceReview, v as currentCostume } from "./project-comparison-BXOQcZ73.js";
|
|
3
3
|
import { n as init_objectSpread2, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
|
|
4
4
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
// src/project-source-diff.tsx
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as __toESM, n as init_objectSpread2, r as __commonJSMin, t as _objectSpread2 } from "./objectSpread2-Dkd68nJH.js";
|
|
2
|
-
import {
|
|
2
|
+
import { i as isAssetName, n as assetsDirectoryParts, o as createProjectSourceDiff, p as createTwoFilesPatch, r as folderSettingsLimit, t as assetLimit } from "./project-asset-paths-BzwVf9HE.js";
|
|
3
3
|
import { a as _OverloadYield, i as _wrapAsyncGenerator, o as _asyncIterator, r as createGitSmartHttp, t as GitSmartHttpError } from "./smart-http-BmHEkE7K.js";
|
|
4
4
|
// node_modules/inherits/inherits_browser.js
|
|
5
5
|
var require_inherits_browser = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -18014,12 +18014,7 @@ async function openRepository(root) {
|
|
|
18014
18014
|
const assertSnapshot = async (snapshot) => {
|
|
18015
18015
|
if (!sameMetadata(snapshot.metadata, await metadata())) throw new GitPanelError("changed");
|
|
18016
18016
|
};
|
|
18017
|
-
const
|
|
18018
|
-
scope = pathParts(scope).join("/");
|
|
18019
|
-
fs.clearReads();
|
|
18020
|
-
cache = {};
|
|
18021
|
-
const fingerprint = await probe(scope);
|
|
18022
|
-
const before = await metadata();
|
|
18017
|
+
const readRefs = async () => {
|
|
18023
18018
|
let head;
|
|
18024
18019
|
try {
|
|
18025
18020
|
head = await resolveRef(_objectSpread2(_objectSpread2({}, options), {}, { ref: "HEAD" }));
|
|
@@ -18030,6 +18025,20 @@ async function openRepository(root) {
|
|
|
18030
18025
|
const branch = await currentBranch(options);
|
|
18031
18026
|
const branches = await listBranches(options);
|
|
18032
18027
|
const remoteBranches = (await listBranches(_objectSpread2(_objectSpread2({}, options), {}, { remote: "origin" }))).filter((name) => name !== "HEAD" && !branches.includes(name)).sort();
|
|
18028
|
+
return {
|
|
18029
|
+
branch: branch || void 0,
|
|
18030
|
+
head,
|
|
18031
|
+
branches,
|
|
18032
|
+
remoteBranches
|
|
18033
|
+
};
|
|
18034
|
+
};
|
|
18035
|
+
const read = async (scope = "") => {
|
|
18036
|
+
scope = pathParts(scope).join("/");
|
|
18037
|
+
fs.clearReads();
|
|
18038
|
+
cache = {};
|
|
18039
|
+
const fingerprint = await probe(scope);
|
|
18040
|
+
const before = await metadata();
|
|
18041
|
+
const { branch, head, branches, remoteBranches } = await readRefs();
|
|
18033
18042
|
const commits = head ? (await log(_objectSpread2(_objectSpread2({}, options), {}, {
|
|
18034
18043
|
cache,
|
|
18035
18044
|
ref: head,
|
|
@@ -18082,7 +18091,7 @@ async function openRepository(root) {
|
|
|
18082
18091
|
if (!sameMetadata(before, await metadata())) throw new GitPanelError("changed");
|
|
18083
18092
|
await fs.checkReads();
|
|
18084
18093
|
return {
|
|
18085
|
-
branch
|
|
18094
|
+
branch,
|
|
18086
18095
|
head,
|
|
18087
18096
|
branches,
|
|
18088
18097
|
remoteBranches,
|
|
@@ -18694,6 +18703,14 @@ async function openRepository(root) {
|
|
|
18694
18703
|
}
|
|
18695
18704
|
}),
|
|
18696
18705
|
read: (scope) => serial(() => read(scope)),
|
|
18706
|
+
/**
|
|
18707
|
+
* The current branch, HEAD and the branch names, read from refs only. Cheap enough to follow branch changes made
|
|
18708
|
+
* by other tools while nothing else is read; `read` returns the same fields.
|
|
18709
|
+
*/
|
|
18710
|
+
refs: () => serial(() => {
|
|
18711
|
+
fs.clearReads();
|
|
18712
|
+
return readRefs();
|
|
18713
|
+
}),
|
|
18697
18714
|
readDiff: (snapshot, change) => serial(() => readDiff(snapshot, change)),
|
|
18698
18715
|
historyChanges: (snapshot, oid) => serial(() => historyChanges(snapshot, oid)),
|
|
18699
18716
|
/**
|
package/dist/repository.d.ts
CHANGED
|
@@ -82,6 +82,8 @@ export interface HistoryPage {
|
|
|
82
82
|
/** Commits read per history page unless asked otherwise. */
|
|
83
83
|
export declare const historyPageSize = 50;
|
|
84
84
|
type Metadata = Map<string, Uint8Array | null>;
|
|
85
|
+
/** The branch fields of a snapshot. */
|
|
86
|
+
export type BranchRefs = Pick<Snapshot, 'branch' | 'head' | 'branches' | 'remoteBranches'>;
|
|
85
87
|
export interface Snapshot {
|
|
86
88
|
branch: string | undefined;
|
|
87
89
|
head: string | null;
|
|
@@ -156,6 +158,11 @@ export declare function openRepository(root: FileSystemDirectoryHandle): Promise
|
|
|
156
158
|
*/
|
|
157
159
|
hasFolder: (name: string, path: string, remote?: boolean | "commit") => Promise<boolean>;
|
|
158
160
|
read: (scope?: string) => Promise<Snapshot>;
|
|
161
|
+
/**
|
|
162
|
+
* The current branch, HEAD and the branch names, read from refs only. Cheap enough to follow branch changes made
|
|
163
|
+
* by other tools while nothing else is read; `read` returns the same fields.
|
|
164
|
+
*/
|
|
165
|
+
refs: () => Promise<BranchRefs>;
|
|
159
166
|
readDiff: (snapshot: Snapshot, change: FileChange) => Promise<FileDiff>;
|
|
160
167
|
historyChanges: (snapshot: Snapshot, oid: string) => Promise<FileChange[]>;
|
|
161
168
|
/**
|
package/dist/repository.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { o as GitPanelError, s as createReadOnlyFs, t as historyPageSize, n as openRepository, a as pullRequestRef, r as upstreamWalkLimit } from "./repository-
|
|
1
|
+
export { o as GitPanelError, s as createReadOnlyFs, t as historyPageSize, n as openRepository, a as pullRequestRef, r as upstreamWalkLimit } from "./repository-ByKRBftJ.js";
|
package/dist/style.css
CHANGED
|
@@ -214,4 +214,32 @@
|
|
|
214
214
|
.twgit-graph-status { margin: 4px 0; font-size: 11px; min-height: 1em; }
|
|
215
215
|
.twgit-panel .twgit-graph-popup [role="menuitem"]:focus-visible, .twgit-graph-popup [role="menuitem"]:focus-visible { outline-offset: -2px; }
|
|
216
216
|
.twgit-graph-form-error { margin: 0; color: #a11919; overflow-wrap: anywhere; }
|
|
217
|
+
/* The branch button sits in the host's menu bar and takes its text color; its menu uses the host's menu colors. */
|
|
218
|
+
.twgit-branch-entry { position: relative; flex: 0 1 auto; align-self: center; display: flex; min-width: 0; font: inherit; }
|
|
219
|
+
.twgit-branch-button { display: flex; align-items: center; gap: 4px; min-width: 0; max-width: 16rem; min-height: 2rem; padding: 0 8px;
|
|
220
|
+
color: inherit; background: transparent; border: 0; border-radius: 4px; font: inherit; white-space: nowrap; cursor: pointer; }
|
|
221
|
+
.twgit-branch-button:hover, .twgit-branch-button[aria-expanded="true"] { background: color-mix(in srgb, currentColor 15%, transparent); }
|
|
222
|
+
.twgit-branch-button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
|
|
223
|
+
.twgit-branch-icon { flex: none; }
|
|
224
|
+
.twgit-branch-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
225
|
+
.twgit-branch-unborn { flex: none; font-size: 11px; opacity: .85; }
|
|
226
|
+
.twgit-branch-caret { flex: none; font-size: 10px; }
|
|
227
|
+
.twgit-branch-popup { position: absolute; z-index: 510; top: calc(100% + 4px); right: 0; display: grid; gap: 2px; min-width: 220px;
|
|
228
|
+
max-width: min(360px, 90vw); max-height: 70vh; overflow: auto; box-sizing: border-box; padding: 4px;
|
|
229
|
+
color: var(--text-primary, CanvasText); background: var(--ui-white, Canvas); border: 1px solid var(--ui-black-transparent, GrayText);
|
|
230
|
+
border-radius: 6px; box-shadow: 0 8px 24px #0004; font: 14px/1.5 system-ui, sans-serif; white-space: normal; }
|
|
231
|
+
.twgit-branch-popup [role^="menuitem"] { display: flex; align-items: center; gap: 6px; width: 100%; padding: 6px 10px; color: inherit;
|
|
232
|
+
background: transparent; border: 0; border-radius: 4px; font: inherit; text-align: start; overflow-wrap: anywhere; cursor: pointer; }
|
|
233
|
+
.twgit-branch-popup [role^="menuitem"]:hover, .twgit-branch-popup [role^="menuitem"]:focus-visible {
|
|
234
|
+
background: color-mix(in srgb, currentColor 12%, transparent); }
|
|
235
|
+
.twgit-branch-popup [role^="menuitem"]:focus-visible { outline: 2px solid var(--looks-secondary, Highlight); outline-offset: -2px; }
|
|
236
|
+
.twgit-branch-popup [aria-disabled="true"]:not([aria-checked="true"]) { opacity: .55; cursor: default; }
|
|
237
|
+
.twgit-branch-popup [aria-checked="true"] { font-weight: 600; }
|
|
238
|
+
.twgit-branch-popup [data-separator-before="true"] { margin-top: 3px; box-shadow: 0 -3px 0 -2px color-mix(in srgb, currentColor 25%, transparent); }
|
|
239
|
+
.twgit-branch-check { flex: none; width: 1em; text-align: center; }
|
|
240
|
+
.twgit-branch-popup form { display: grid; gap: 6px; padding: 6px; font-size: 12px; }
|
|
241
|
+
.twgit-branch-popup form label { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
|
|
242
|
+
.twgit-branch-popup form input:not([type]) { flex: 1; min-width: 10em; font: inherit; }
|
|
243
|
+
.twgit-branch-popup form button { color: inherit; background: transparent; border: 1px solid currentColor; border-radius: 4px; padding: 3px 8px; font: inherit; cursor: pointer; }
|
|
244
|
+
.twgit-branch-popup form button:disabled { opacity: .5; cursor: default; }
|
|
217
245
|
/*$vite$:1*/
|
package/package.json
CHANGED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, {useEffect, useLayoutEffect, useRef, useState} from 'react';
|
|
2
|
+
import type {HistoryActionResult} from './history-graph-view.js';
|
|
3
|
+
import {labelsFor} from './messages.js';
|
|
4
|
+
import type {BranchRefs} from './repository.js';
|
|
5
|
+
|
|
6
|
+
/** What the branch button shows: the branches of the chosen repository as the Git panel last read them. */
|
|
7
|
+
export interface BranchButtonState extends BranchRefs {
|
|
8
|
+
/** Whether a repository has been chosen. */
|
|
9
|
+
repository: boolean;
|
|
10
|
+
/** Whether the host lets the panel switch and create branches. */
|
|
11
|
+
writable: boolean;
|
|
12
|
+
/** Whether the panel is running an operation. */
|
|
13
|
+
busy: boolean;
|
|
14
|
+
}
|
|
15
|
+
/** What the branch button asks the Git panel to do. Branch operations resolve as the panel's own buttons do. */
|
|
16
|
+
export interface BranchButtonActions {
|
|
17
|
+
open(): void;
|
|
18
|
+
choose(): void;
|
|
19
|
+
switchBranch(name: string): Promise<HistoryActionResult>;
|
|
20
|
+
/** Creates a branch at HEAD. */
|
|
21
|
+
createBranch(name: string, checkout: boolean): Promise<HistoryActionResult>;
|
|
22
|
+
/** Creates a local branch from `origin/<name>` and switches to it. */
|
|
23
|
+
createFromRemote(name: string): Promise<HistoryActionResult>;
|
|
24
|
+
}
|
|
25
|
+
export interface BranchButtonProps extends BranchButtonActions {
|
|
26
|
+
state: BranchButtonState;
|
|
27
|
+
locale?: string;
|
|
28
|
+
}
|
|
29
|
+
interface Item {id: string; label: string; checked?: boolean; disabled?: boolean; separatorBefore?: boolean; run(): void}
|
|
30
|
+
|
|
31
|
+
const menuItems = '[role="menuitem"], [role="menuitemradio"]';
|
|
32
|
+
|
|
33
|
+
/** Git's branch glyph, drawn in the text color. */
|
|
34
|
+
function BranchIcon() {
|
|
35
|
+
return <svg className="twgit-branch-icon" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true" focusable="false">
|
|
36
|
+
<g fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
37
|
+
<circle cx="4.5" cy="3.5" r="1.75"/><circle cx="4.5" cy="12.5" r="1.75"/><circle cx="11.5" cy="5.5" r="1.75"/>
|
|
38
|
+
<path d="M4.5 5.25v5.5M11.5 7.25c0 2.5-2.5 2.75-6 3.75"/>
|
|
39
|
+
</g>
|
|
40
|
+
</svg>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The current branch as a menu button for the host's menu bar, with the menu of branches. Before a repository is
|
|
45
|
+
* chosen it reads "Git" and offers to choose one. A detached HEAD shows its short commit ID. The menu moves with the
|
|
46
|
+
* arrow keys, Home and End; Escape closes it and returns to the button. Without `writable` it lists the branches only.
|
|
47
|
+
*/
|
|
48
|
+
export function BranchButton({state, locale = 'en', ...actions}: BranchButtonProps) {
|
|
49
|
+
const labels = labelsFor(locale);
|
|
50
|
+
const [mode, setMode] = useState<'closed' | 'menu' | 'create'>('closed');
|
|
51
|
+
const [initial, setInitial] = useState<'checked' | 'first' | 'last'>('checked');
|
|
52
|
+
const [name, setName] = useState('');
|
|
53
|
+
const [checkout, setCheckout] = useState(true);
|
|
54
|
+
const [creating, setCreating] = useState(false);
|
|
55
|
+
const [createError, setCreateError] = useState('');
|
|
56
|
+
const root = useRef<HTMLDivElement>(null);
|
|
57
|
+
const trigger = useRef<HTMLButtonElement>(null);
|
|
58
|
+
const popup = useRef<HTMLDivElement>(null);
|
|
59
|
+
const mounted = useRef(true);
|
|
60
|
+
useEffect(() => () => {mounted.current = false;}, []);
|
|
61
|
+
useLayoutEffect(() => {
|
|
62
|
+
const element = popup.current;
|
|
63
|
+
if (!element) return;
|
|
64
|
+
if (mode === 'create') {
|
|
65
|
+
element.querySelector('input')?.focus();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const items = [...element.querySelectorAll<HTMLElement>(menuItems)];
|
|
69
|
+
const checked = items.find(item => item.getAttribute('aria-checked') === 'true');
|
|
70
|
+
(initial === 'last' ? items[items.length - 1] : initial === 'checked' && checked || items[0])?.focus();
|
|
71
|
+
}, [mode]);
|
|
72
|
+
// A click elsewhere closes the menu, also where nothing takes the focus.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (mode === 'closed') return;
|
|
75
|
+
const node = root.current!;
|
|
76
|
+
const dismiss = (event: Event) => {
|
|
77
|
+
if (!node.contains(event.target as Node) && !creating) setMode('closed');
|
|
78
|
+
};
|
|
79
|
+
node.ownerDocument.addEventListener('pointerdown', dismiss, true);
|
|
80
|
+
return () => node.ownerDocument.removeEventListener('pointerdown', dismiss, true);
|
|
81
|
+
}, [mode, creating]);
|
|
82
|
+
// After a refused create, the name is selected for correcting.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!createError || creating) return;
|
|
85
|
+
const input = popup.current?.querySelector('input:not([type])') as HTMLInputElement | null;
|
|
86
|
+
input?.focus();
|
|
87
|
+
input?.select();
|
|
88
|
+
}, [createError, creating]);
|
|
89
|
+
|
|
90
|
+
const {branch, head} = state;
|
|
91
|
+
const unborn = state.repository && !head;
|
|
92
|
+
const text = !state.repository ? 'Git' : branch ?? (head ? head.slice(0, 7) : labels.unborn);
|
|
93
|
+
const accessibleName = !state.repository ? labels.title :
|
|
94
|
+
`${branch ? labels.branch : labels.detached}: ${text}${unborn && branch ? ` (${labels.unborn})` : ''}`;
|
|
95
|
+
const close = (focus = true) => {
|
|
96
|
+
setMode('closed');
|
|
97
|
+
if (focus) trigger.current?.focus({preventScroll: true});
|
|
98
|
+
};
|
|
99
|
+
const openMenu = (from: 'checked' | 'first' | 'last') => {
|
|
100
|
+
setInitial(from);
|
|
101
|
+
setMode('menu');
|
|
102
|
+
};
|
|
103
|
+
/** Runs a branch operation and shows its result, or why it did not run, in the panel. */
|
|
104
|
+
const change = async (operation: () => Promise<HistoryActionResult>) => {
|
|
105
|
+
const result = await operation();
|
|
106
|
+
if (result.done || result.error) actions.open();
|
|
107
|
+
};
|
|
108
|
+
const items: Item[] = [];
|
|
109
|
+
if (!state.repository) {
|
|
110
|
+
items.push({id: 'choose', label: labels.choose, run: () => {close(); actions.choose();}});
|
|
111
|
+
} else {
|
|
112
|
+
const switching = state.writable && !state.busy;
|
|
113
|
+
for (const local of state.branches) {
|
|
114
|
+
items.push({id: 'branch:' + local, label: local, checked: local === branch, disabled: !state.writable || state.busy && local !== branch,
|
|
115
|
+
run: () => {
|
|
116
|
+
close();
|
|
117
|
+
if (local !== branch && switching) void change(() => actions.switchBranch(local));
|
|
118
|
+
}});
|
|
119
|
+
}
|
|
120
|
+
if (state.writable) {
|
|
121
|
+
state.remoteBranches.forEach((remote, index) => items.push({id: 'remote:' + remote,
|
|
122
|
+
label: labels.historyCreateFromRemote.replace('{name}', remote), disabled: state.busy, separatorBefore: index === 0,
|
|
123
|
+
run: () => {
|
|
124
|
+
close();
|
|
125
|
+
if (!state.busy) void change(() => actions.createFromRemote(remote));
|
|
126
|
+
}}));
|
|
127
|
+
if (head) items.push({id: 'create', label: labels.newBranchItem, disabled: state.busy, separatorBefore: true, run: () => {
|
|
128
|
+
if (state.busy) return;
|
|
129
|
+
setName('');
|
|
130
|
+
setCheckout(true);
|
|
131
|
+
setCreateError('');
|
|
132
|
+
setMode('create');
|
|
133
|
+
}});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// "Open Local Git" goes with "New Branch…" and stands apart from the branches.
|
|
137
|
+
items.push({id: 'open', label: labels.openPanel, separatorBefore: items.length > 0 && items[items.length - 1].id !== 'create',
|
|
138
|
+
run: () => {close(); actions.open();}});
|
|
139
|
+
|
|
140
|
+
const onMenuKey = (event: React.KeyboardEvent<HTMLElement>) => {
|
|
141
|
+
if (event.key === 'Escape') {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
if (!creating) close();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (mode !== 'menu') return;
|
|
148
|
+
if (event.key === 'Tab') {close(false); return;}
|
|
149
|
+
const elements = [...event.currentTarget.querySelectorAll<HTMLElement>(menuItems)];
|
|
150
|
+
const index = elements.indexOf(event.target as HTMLElement);
|
|
151
|
+
const target = {ArrowDown: index + 1, ArrowUp: index - 1, Home: 0, End: elements.length - 1}[event.key];
|
|
152
|
+
if (target === undefined || !elements.length) return;
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
elements[(target + elements.length) % elements.length].focus();
|
|
155
|
+
};
|
|
156
|
+
const submit = async (event: React.FormEvent) => {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
const branchName = name.trim();
|
|
159
|
+
if (!branchName || creating) return;
|
|
160
|
+
setCreating(true);
|
|
161
|
+
setCreateError('');
|
|
162
|
+
let result: HistoryActionResult;
|
|
163
|
+
try {
|
|
164
|
+
result = await actions.createBranch(branchName, checkout);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
result = {done: false, error: error instanceof Error && error.message ? error.message : labels.failed};
|
|
167
|
+
}
|
|
168
|
+
if (!mounted.current) return;
|
|
169
|
+
setCreating(false);
|
|
170
|
+
if (result.done) {
|
|
171
|
+
close();
|
|
172
|
+
actions.open();
|
|
173
|
+
} else setCreateError(result.error ?? '');
|
|
174
|
+
};
|
|
175
|
+
return <div className="twgit-branch-entry" ref={root} onBlur={event => {
|
|
176
|
+
const next = event.relatedTarget as Node | null;
|
|
177
|
+
if (next && !event.currentTarget.contains(next) && !creating) setMode('closed');
|
|
178
|
+
}}>
|
|
179
|
+
<button type="button" className="twgit-branch-button" ref={trigger} aria-label={accessibleName} title={accessibleName}
|
|
180
|
+
aria-haspopup="menu" aria-expanded={mode === 'menu'} data-git-branch={state.repository ? branch ?? head ?? '' : undefined}
|
|
181
|
+
onClick={() => mode === 'closed' ? openMenu('checked') : close(false)}
|
|
182
|
+
onKeyDown={event => {
|
|
183
|
+
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
|
|
184
|
+
event.preventDefault();
|
|
185
|
+
openMenu(event.key === 'ArrowUp' ? 'last' : 'checked');
|
|
186
|
+
}}>
|
|
187
|
+
<BranchIcon/>
|
|
188
|
+
<span className="twgit-branch-name">{text}</span>
|
|
189
|
+
{unborn && branch && <small className="twgit-branch-unborn">{labels.unborn}</small>}
|
|
190
|
+
<span className="twgit-branch-caret" aria-hidden="true">▾</span>
|
|
191
|
+
</button>
|
|
192
|
+
{mode === 'menu' && <div ref={popup} className="twgit-branch-popup" role="menu" aria-label={labels.branchMenu} onKeyDown={onMenuKey}>
|
|
193
|
+
{items.map(item => <button key={item.id} type="button" tabIndex={-1} data-action={item.id}
|
|
194
|
+
role={item.checked === undefined ? 'menuitem' : 'menuitemradio'} aria-checked={item.checked}
|
|
195
|
+
aria-disabled={item.disabled || undefined} data-separator-before={item.separatorBefore || undefined}
|
|
196
|
+
onClick={() => {if (!item.disabled || item.checked) item.run();}}>
|
|
197
|
+
{item.checked !== undefined && <span className="twgit-branch-check" aria-hidden="true">{item.checked ? '✓' : ''}</span>}
|
|
198
|
+
{item.label}
|
|
199
|
+
</button>)}
|
|
200
|
+
</div>}
|
|
201
|
+
{mode === 'create' && <div ref={popup} className="twgit-branch-popup" role="dialog" aria-label={labels.newBranchTitle}
|
|
202
|
+
onKeyDown={onMenuKey}>
|
|
203
|
+
<form onSubmit={event => void submit(event)}>
|
|
204
|
+
<label>{labels.newBranch} <input value={name} required disabled={creating} aria-invalid={createError ? true : undefined}
|
|
205
|
+
onChange={event => setName(event.currentTarget.value)}/></label>
|
|
206
|
+
<label><input type="checkbox" checked={checkout} disabled={creating}
|
|
207
|
+
onChange={event => setCheckout(event.currentTarget.checked)}/> {labels.switchAfterCreate}</label>
|
|
208
|
+
{createError && <p className="twgit-graph-form-error" role="alert">{createError}</p>}
|
|
209
|
+
<span className="twgit-graph-form-actions">
|
|
210
|
+
<button type="submit" disabled={creating || state.busy || !name.trim()}>{labels.createBranch}</button>
|
|
211
|
+
<button type="button" disabled={creating} onClick={() => close()}>{labels.cancel}</button>
|
|
212
|
+
</span>
|
|
213
|
+
</form>
|
|
214
|
+
</div>}
|
|
215
|
+
</div>;
|
|
216
|
+
}
|