mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 00:33:20 +08:00
fixed:issues#69
This commit is contained in:
parent
91e2d5962a
commit
7c5279e445
6 changed files with 254 additions and 9 deletions
|
|
@ -423,6 +423,11 @@ var EN = Object.freeze({
|
|||
"\u9009\u62E9\u673A\u5668\u4EBA\u5DE5\u4F5C\u533A\u76EE\u5F55": "Select bot workspace folder",
|
||||
"\u5F53\u524D\u76EE\u5F55": "Current folder",
|
||||
"\u4E3B\u76EE\u5F55": "Home",
|
||||
"\u76F4\u63A5\u8F93\u5165\u8DEF\u5F84": "Enter a path directly",
|
||||
"\u652F\u6301 Windows \u76D8\u7B26\u3001UNC \u4E0E POSIX \u7EDD\u5BF9\u8DEF\u5F84\u3002": "Supports Windows drives, UNC paths, and POSIX absolute paths.",
|
||||
"\u8F93\u5165 Host \u4E0A\u7684\u5B8C\u6574\u7EDD\u5BF9\u8DEF\u5F84": "Enter a full absolute path on the Host",
|
||||
"\u524D\u5F80": "Go",
|
||||
"\u8BFB\u53D6\u4E2D\u2026": "Loading\u2026",
|
||||
"\u6B63\u5728\u51C6\u5907\u76EE\u5F55\u9009\u62E9\u5668\u2026": "Preparing folder picker\u2026",
|
||||
"\u6B63\u5728\u8BFB\u53D6\u76EE\u5F55\u2026": "Loading folders\u2026",
|
||||
"\u8FD9\u4E2A\u76EE\u5F55\u4E2D\u6CA1\u6709\u5B50\u6587\u4EF6\u5939\u3002": "This folder has no subfolders.",
|
||||
|
|
@ -1548,6 +1553,7 @@ function WorkspaceDirectoryPicker({
|
|||
const [listing, setListing] = React5.useState(null);
|
||||
const [loading, setLoading] = React5.useState(false);
|
||||
const [error, setError] = React5.useState(null);
|
||||
const [pathDraft, setPathDraft] = React5.useState(startPath ?? "");
|
||||
const [showHidden, setShowHidden] = React5.useState(false);
|
||||
const [retryKey, setRetryKey] = React5.useState(0);
|
||||
const requestRef = React5.useRef(0);
|
||||
|
|
@ -1556,6 +1562,8 @@ function WorkspaceDirectoryPicker({
|
|||
const bodyRef = React5.useRef(null);
|
||||
const titleId = React5.useId();
|
||||
const noticeId = React5.useId();
|
||||
const pathInputId = React5.useId();
|
||||
const errorId = React5.useId();
|
||||
const initialPathRef = React5.useRef(startPath);
|
||||
const onPickedRef = React5.useRef(onPicked);
|
||||
const onCancelRef = React5.useRef(onCancel);
|
||||
|
|
@ -1576,6 +1584,7 @@ function WorkspaceDirectoryPicker({
|
|||
if (request !== requestRef.current || controller.signal.aborted) return { aborted: true };
|
||||
if (bodyRef.current) bodyRef.current.scrollTop = 0;
|
||||
setListing(next);
|
||||
if (typeof next?.path === "string") setPathDraft(next.path);
|
||||
setError(null);
|
||||
return { value: next };
|
||||
} catch (cause) {
|
||||
|
|
@ -1591,6 +1600,7 @@ function WorkspaceDirectoryPicker({
|
|||
let active = true;
|
||||
setListing(null);
|
||||
setError(null);
|
||||
setPathDraft(initialPathRef.current ?? "");
|
||||
setShowHidden(false);
|
||||
dialogRef.current?.focus?.();
|
||||
const handleKeyDown = (event) => {
|
||||
|
|
@ -1637,6 +1647,7 @@ function WorkspaceDirectoryPicker({
|
|||
const entries = (listing?.entries ?? []).filter((entry) => showHidden || !entry.hidden);
|
||||
const crumbs = listing ? displayCrumbs(listing) : [];
|
||||
const presentedError = saveError ?? error;
|
||||
const pathReady = listing !== null && pathDraft === listing.path;
|
||||
const content = h2(
|
||||
"div",
|
||||
{
|
||||
|
|
@ -1675,7 +1686,49 @@ function WorkspaceDirectoryPicker({
|
|||
onClick: () => void loadDirectory(crumb.path)
|
||||
}, crumb.path === listing.home ? h2("span", null, "\u4E3B\u76EE\u5F55") : crumb.name || crumb.path)
|
||||
))
|
||||
) : h2("p", null, "\u6B63\u5728\u51C6\u5907\u76EE\u5F55\u9009\u62E9\u5668\u2026")
|
||||
) : h2("p", null, "\u6B63\u5728\u51C6\u5907\u76EE\u5F55\u9009\u62E9\u5668\u2026"),
|
||||
h2(
|
||||
"form",
|
||||
{
|
||||
className: "dim-directoryPathForm",
|
||||
onSubmit: (event) => {
|
||||
event.preventDefault();
|
||||
if (!busy && !loading && pathDraft.trim()) void loadDirectory(pathDraft);
|
||||
}
|
||||
},
|
||||
h2(
|
||||
"div",
|
||||
{ className: "dim-directoryPathMeta" },
|
||||
h2("label", { htmlFor: pathInputId }, "\u76F4\u63A5\u8F93\u5165\u8DEF\u5F84"),
|
||||
h2("span", null, "\u652F\u6301 Windows \u76D8\u7B26\u3001UNC \u4E0E POSIX \u7EDD\u5BF9\u8DEF\u5F84\u3002")
|
||||
),
|
||||
h2(
|
||||
"div",
|
||||
{ className: "dim-directoryPathControl" },
|
||||
h2("input", {
|
||||
id: pathInputId,
|
||||
className: "dim-directoryPathInput",
|
||||
value: pathDraft,
|
||||
placeholder: "\u8F93\u5165 Host \u4E0A\u7684\u5B8C\u6574\u7EDD\u5BF9\u8DEF\u5F84",
|
||||
"aria-label": "\u5DE5\u4F5C\u533A\u7EDD\u5BF9\u8DEF\u5F84",
|
||||
"aria-describedby": presentedError ? errorId : void 0,
|
||||
"aria-invalid": presentedError ? "true" : void 0,
|
||||
autoCapitalize: "none",
|
||||
autoCorrect: "off",
|
||||
spellCheck: false,
|
||||
maxLength: 4096,
|
||||
disabled: busy || loading,
|
||||
onChange: (event) => {
|
||||
setPathDraft(event.target.value);
|
||||
setError(null);
|
||||
}
|
||||
}),
|
||||
h2("button", {
|
||||
type: "submit",
|
||||
disabled: busy || loading || !pathDraft.trim()
|
||||
}, loading ? "\u8BFB\u53D6\u4E2D\u2026" : "\u524D\u5F80")
|
||||
)
|
||||
)
|
||||
),
|
||||
h2(
|
||||
"div",
|
||||
|
|
@ -1708,7 +1761,7 @@ function WorkspaceDirectoryPicker({
|
|||
listing?.truncated ? h2("p", { className: "dim-directoryPickerTruncated" }, "\u6B64\u76EE\u5F55\u7684\u5B50\u6587\u4EF6\u5939\u8FC7\u591A\uFF0C\u4EC5\u663E\u793A\u524D\u4E00\u90E8\u5206\u3002") : null,
|
||||
presentedError ? h2(
|
||||
"div",
|
||||
{ className: "dim-directoryPickerError", role: "alert" },
|
||||
{ id: errorId, className: "dim-directoryPickerError", role: "alert" },
|
||||
h2("span", null, presentedError),
|
||||
!listing && !busy ? h2("button", {
|
||||
type: "button",
|
||||
|
|
@ -1739,7 +1792,7 @@ function WorkspaceDirectoryPicker({
|
|||
h2("button", {
|
||||
type: "button",
|
||||
className: "dim-directoryPickerPrimary",
|
||||
disabled: busy || loading || !listing,
|
||||
disabled: busy || loading || !pathReady,
|
||||
onClick: () => listing && void onPicked(listing.path)
|
||||
}, busy ? "\u5207\u6362\u4E2D\u2026" : "\u9009\u62E9\u6B64\u76EE\u5F55")
|
||||
)
|
||||
|
|
@ -10501,8 +10554,21 @@ var CSS11 = String.raw`
|
|||
.dim-directoryCrumbs button { max-width: 210px; overflow: hidden; padding: 3px 5px; border: 0; border-radius: 6px; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; line-height: 18px; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
|
||||
.dim-directoryCrumbs button:hover:not(:disabled) { color: var(--dim-blue); background: var(--dim-blue-soft); }
|
||||
.dim-directoryCrumbs button[aria-current="page"] { color: var(--dsw-alias-label-primary, #1f2329); font-weight: 650; }
|
||||
.dim-directoryCrumbs button:focus-visible, .dim-directoryList button:focus-visible, .dim-directoryPickerActions button:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 65%, white); outline-offset: 1px; }
|
||||
.dim-directoryCrumbs button:focus-visible, .dim-directoryPathInput:focus-visible, .dim-directoryPathControl button:focus-visible, .dim-directoryList button:focus-visible, .dim-directoryPickerActions button:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 65%, white); outline-offset: 1px; }
|
||||
.dim-directoryCrumbSeparator { flex: none; font-size: 12px; }
|
||||
.dim-directoryPathForm { display: grid; gap: 7px; margin-top: 14px; }
|
||||
.dim-directoryPathMeta { min-width: 0; display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
||||
.dim-directoryPathMeta label { flex: none; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; font-weight: 650; }
|
||||
.dim-directoryPathMeta span { min-width: 0; overflow: hidden; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dim-directoryPathControl { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; gap: 8px; }
|
||||
.dim-directoryPathInput { min-width: 0; height: 38px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: 12px ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; line-height: 38px; }
|
||||
.dim-directoryPathInput::placeholder { color: var(--dsw-alias-label-tertiary, #8f959e); }
|
||||
.dim-directoryPathInput:hover:not(:disabled) { border-color: var(--dsw-alias-border-l3, #bbbfc4); }
|
||||
.dim-directoryPathInput:focus { border-color: var(--dim-blue); }
|
||||
.dim-directoryPathInput[aria-invalid="true"] { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 62%, var(--dsw-alias-border-l2, #dfe1e5)); }
|
||||
.dim-directoryPathControl button { min-height: 38px; padding: 0 14px; border: 1px solid color-mix(in srgb, var(--dim-blue) 30%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 8px; color: var(--dim-blue); background: var(--dim-blue-soft); font: inherit; font-size: 13px; font-weight: 650; cursor: pointer; }
|
||||
.dim-directoryPathControl button:hover:not(:disabled) { border-color: color-mix(in srgb, var(--dim-blue) 48%, var(--dsw-alias-border-l2, #dfe1e5)); background: color-mix(in srgb, var(--dim-blue) 13%, transparent); }
|
||||
.dim-directoryPathInput:disabled, .dim-directoryPathControl button:disabled { cursor: not-allowed; opacity: .55; }
|
||||
.dim-directoryPickerBody { min-height: 0; overflow-y: auto; padding: 14px 16px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2, #dfe1e5) transparent; }
|
||||
.dim-directoryList { display: grid; gap: 3px; margin: 0; padding: 0; list-style: none; }
|
||||
.dim-directoryList button { width: 100%; min-height: 46px; display: grid; grid-template-columns: 24px minmax(0, 1fr) 18px; align-items: center; gap: 10px; padding: 7px 11px; border: 0; border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: transparent; font: inherit; text-align: left; cursor: pointer; }
|
||||
|
|
@ -10596,6 +10662,7 @@ var CSS11 = String.raw`
|
|||
.dim-directoryPicker { height: calc(100vh - 20px); min-height: 0; border-radius: 14px; }
|
||||
.dim-directoryPickerHeader { padding: 18px 17px 14px; }
|
||||
.dim-directoryPickerHeader h3 { font-size: 18px; }
|
||||
.dim-directoryPathMeta span { display: none; }
|
||||
.dim-directoryPickerBody { padding: 10px; }
|
||||
.dim-directoryPickerFooter { grid-template-columns: minmax(0, 1fr) max-content; gap: 10px; padding: 13px 14px; }
|
||||
.dim-directoryPickerNotice { grid-column: 1 / -1; grid-row: 1; text-align: left; }
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -122,6 +122,11 @@ const EN = Object.freeze({
|
|||
'选择机器人工作区目录': 'Select bot workspace folder',
|
||||
'当前目录': 'Current folder',
|
||||
'主目录': 'Home',
|
||||
'直接输入路径': 'Enter a path directly',
|
||||
'支持 Windows 盘符、UNC 与 POSIX 绝对路径。': 'Supports Windows drives, UNC paths, and POSIX absolute paths.',
|
||||
'输入 Host 上的完整绝对路径': 'Enter a full absolute path on the Host',
|
||||
'前往': 'Go',
|
||||
'读取中…': 'Loading…',
|
||||
'正在准备目录选择器…': 'Preparing folder picker…',
|
||||
'正在读取目录…': 'Loading folders…',
|
||||
'这个目录中没有子文件夹。': 'This folder has no subfolders.',
|
||||
|
|
|
|||
|
|
@ -184,8 +184,21 @@ const CSS = String.raw`
|
|||
.dim-directoryCrumbs button { max-width: 210px; overflow: hidden; padding: 3px 5px; border: 0; border-radius: 6px; color: var(--dsw-alias-label-secondary, #646a73); background: transparent; font: inherit; font-size: 12px; line-height: 18px; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
|
||||
.dim-directoryCrumbs button:hover:not(:disabled) { color: var(--dim-blue); background: var(--dim-blue-soft); }
|
||||
.dim-directoryCrumbs button[aria-current="page"] { color: var(--dsw-alias-label-primary, #1f2329); font-weight: 650; }
|
||||
.dim-directoryCrumbs button:focus-visible, .dim-directoryList button:focus-visible, .dim-directoryPickerActions button:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 65%, white); outline-offset: 1px; }
|
||||
.dim-directoryCrumbs button:focus-visible, .dim-directoryPathInput:focus-visible, .dim-directoryPathControl button:focus-visible, .dim-directoryList button:focus-visible, .dim-directoryPickerActions button:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 65%, white); outline-offset: 1px; }
|
||||
.dim-directoryCrumbSeparator { flex: none; font-size: 12px; }
|
||||
.dim-directoryPathForm { display: grid; gap: 7px; margin-top: 14px; }
|
||||
.dim-directoryPathMeta { min-width: 0; display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
||||
.dim-directoryPathMeta label { flex: none; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; font-weight: 650; }
|
||||
.dim-directoryPathMeta span { min-width: 0; overflow: hidden; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dim-directoryPathControl { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; gap: 8px; }
|
||||
.dim-directoryPathInput { min-width: 0; height: 38px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: 12px ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace; line-height: 38px; }
|
||||
.dim-directoryPathInput::placeholder { color: var(--dsw-alias-label-tertiary, #8f959e); }
|
||||
.dim-directoryPathInput:hover:not(:disabled) { border-color: var(--dsw-alias-border-l3, #bbbfc4); }
|
||||
.dim-directoryPathInput:focus { border-color: var(--dim-blue); }
|
||||
.dim-directoryPathInput[aria-invalid="true"] { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary, #d54941) 62%, var(--dsw-alias-border-l2, #dfe1e5)); }
|
||||
.dim-directoryPathControl button { min-height: 38px; padding: 0 14px; border: 1px solid color-mix(in srgb, var(--dim-blue) 30%, var(--dsw-alias-border-l2, #dfe1e5)); border-radius: 8px; color: var(--dim-blue); background: var(--dim-blue-soft); font: inherit; font-size: 13px; font-weight: 650; cursor: pointer; }
|
||||
.dim-directoryPathControl button:hover:not(:disabled) { border-color: color-mix(in srgb, var(--dim-blue) 48%, var(--dsw-alias-border-l2, #dfe1e5)); background: color-mix(in srgb, var(--dim-blue) 13%, transparent); }
|
||||
.dim-directoryPathInput:disabled, .dim-directoryPathControl button:disabled { cursor: not-allowed; opacity: .55; }
|
||||
.dim-directoryPickerBody { min-height: 0; overflow-y: auto; padding: 14px 16px; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2, #dfe1e5) transparent; }
|
||||
.dim-directoryList { display: grid; gap: 3px; margin: 0; padding: 0; list-style: none; }
|
||||
.dim-directoryList button { width: 100%; min-height: 46px; display: grid; grid-template-columns: 24px minmax(0, 1fr) 18px; align-items: center; gap: 10px; padding: 7px 11px; border: 0; border-radius: 9px; color: var(--dsw-alias-label-primary, #1f2329); background: transparent; font: inherit; text-align: left; cursor: pointer; }
|
||||
|
|
@ -279,6 +292,7 @@ const CSS = String.raw`
|
|||
.dim-directoryPicker { height: calc(100vh - 20px); min-height: 0; border-radius: 14px; }
|
||||
.dim-directoryPickerHeader { padding: 18px 17px 14px; }
|
||||
.dim-directoryPickerHeader h3 { font-size: 18px; }
|
||||
.dim-directoryPathMeta span { display: none; }
|
||||
.dim-directoryPickerBody { padding: 10px; }
|
||||
.dim-directoryPickerFooter { grid-template-columns: minmax(0, 1fr) max-content; gap: 10px; padding: 13px 14px; }
|
||||
.dim-directoryPickerNotice { grid-column: 1 / -1; grid-row: 1; text-align: left; }
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export function WorkspaceDirectoryPicker({
|
|||
const [listing, setListing] = React.useState(null);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [pathDraft, setPathDraft] = React.useState(startPath ?? '');
|
||||
const [showHidden, setShowHidden] = React.useState(false);
|
||||
const [retryKey, setRetryKey] = React.useState(0);
|
||||
const requestRef = React.useRef(0);
|
||||
|
|
@ -56,6 +57,8 @@ export function WorkspaceDirectoryPicker({
|
|||
const bodyRef = React.useRef(null);
|
||||
const titleId = React.useId();
|
||||
const noticeId = React.useId();
|
||||
const pathInputId = React.useId();
|
||||
const errorId = React.useId();
|
||||
const initialPathRef = React.useRef(startPath);
|
||||
const onPickedRef = React.useRef(onPicked);
|
||||
const onCancelRef = React.useRef(onCancel);
|
||||
|
|
@ -78,6 +81,7 @@ export function WorkspaceDirectoryPicker({
|
|||
if (request !== requestRef.current || controller.signal.aborted) return { aborted: true };
|
||||
if (bodyRef.current) bodyRef.current.scrollTop = 0;
|
||||
setListing(next);
|
||||
if (typeof next?.path === 'string') setPathDraft(next.path);
|
||||
setError(null);
|
||||
return { value: next };
|
||||
} catch (cause) {
|
||||
|
|
@ -94,6 +98,7 @@ export function WorkspaceDirectoryPicker({
|
|||
let active = true;
|
||||
setListing(null);
|
||||
setError(null);
|
||||
setPathDraft(initialPathRef.current ?? '');
|
||||
setShowHidden(false);
|
||||
dialogRef.current?.focus?.();
|
||||
const handleKeyDown = (event) => {
|
||||
|
|
@ -145,6 +150,7 @@ export function WorkspaceDirectoryPicker({
|
|||
const entries = (listing?.entries ?? []).filter((entry) => showHidden || !entry.hidden);
|
||||
const crumbs = listing ? displayCrumbs(listing) : [];
|
||||
const presentedError = saveError ?? error;
|
||||
const pathReady = listing !== null && pathDraft === listing.path;
|
||||
|
||||
const content = h('div', {
|
||||
className: 'dim-directoryPickerBackdrop',
|
||||
|
|
@ -177,7 +183,40 @@ export function WorkspaceDirectoryPicker({
|
|||
? h('span', null, '主目录')
|
||||
: (crumb.name || crumb.path)))),
|
||||
)
|
||||
: h('p', null, '正在准备目录选择器…')),
|
||||
: h('p', null, '正在准备目录选择器…'),
|
||||
h('form', {
|
||||
className: 'dim-directoryPathForm',
|
||||
onSubmit: (event) => {
|
||||
event.preventDefault();
|
||||
if (!busy && !loading && pathDraft.trim()) void loadDirectory(pathDraft);
|
||||
},
|
||||
},
|
||||
h('div', { className: 'dim-directoryPathMeta' },
|
||||
h('label', { htmlFor: pathInputId }, '直接输入路径'),
|
||||
h('span', null, '支持 Windows 盘符、UNC 与 POSIX 绝对路径。')),
|
||||
h('div', { className: 'dim-directoryPathControl' },
|
||||
h('input', {
|
||||
id: pathInputId,
|
||||
className: 'dim-directoryPathInput',
|
||||
value: pathDraft,
|
||||
placeholder: '输入 Host 上的完整绝对路径',
|
||||
'aria-label': '工作区绝对路径',
|
||||
'aria-describedby': presentedError ? errorId : undefined,
|
||||
'aria-invalid': presentedError ? 'true' : undefined,
|
||||
autoCapitalize: 'none',
|
||||
autoCorrect: 'off',
|
||||
spellCheck: false,
|
||||
maxLength: 4_096,
|
||||
disabled: busy || loading,
|
||||
onChange: (event) => {
|
||||
setPathDraft(event.target.value);
|
||||
setError(null);
|
||||
},
|
||||
}),
|
||||
h('button', {
|
||||
type: 'submit',
|
||||
disabled: busy || loading || !pathDraft.trim(),
|
||||
}, loading ? '读取中…' : '前往')))),
|
||||
h('div', { ref: bodyRef, className: 'dim-directoryPickerBody', 'aria-busy': loading },
|
||||
loading && !listing
|
||||
? h('div', { className: 'dim-directoryPickerState' },
|
||||
|
|
@ -201,7 +240,7 @@ export function WorkspaceDirectoryPicker({
|
|||
listing?.truncated
|
||||
? h('p', { className: 'dim-directoryPickerTruncated' }, '此目录的子文件夹过多,仅显示前一部分。')
|
||||
: null,
|
||||
presentedError ? h('div', { className: 'dim-directoryPickerError', role: 'alert' },
|
||||
presentedError ? h('div', { id: errorId, className: 'dim-directoryPickerError', role: 'alert' },
|
||||
h('span', null, presentedError),
|
||||
!listing && !busy ? h('button', {
|
||||
type: 'button', onClick: () => setRetryKey((value) => value + 1),
|
||||
|
|
@ -222,7 +261,7 @@ export function WorkspaceDirectoryPicker({
|
|||
h('button', {
|
||||
type: 'button',
|
||||
className: 'dim-directoryPickerPrimary',
|
||||
disabled: busy || loading || !listing,
|
||||
disabled: busy || loading || !pathReady,
|
||||
onClick: () => listing && void onPicked(listing.path),
|
||||
}, busy ? '切换中…' : '选择此目录')))));
|
||||
|
||||
|
|
|
|||
|
|
@ -184,6 +184,121 @@ test('WorkspaceEditor browses from the current path and saves the selected direc
|
|||
assert.equal(renderer.root.findByType('code').props.title, '/workspace/current/next project');
|
||||
});
|
||||
|
||||
test('WorkspaceEditor navigates to arbitrary absolute paths across drives and UNC shares', async () => {
|
||||
const start = 'C:\\Users\\alice\\project';
|
||||
const targets = [
|
||||
'D:\\projects\\bot',
|
||||
'\\\\server\\share\\bot',
|
||||
'/mnt/bot',
|
||||
];
|
||||
const listed = [];
|
||||
const saved = [];
|
||||
const picker = {
|
||||
async listDirectory(path) {
|
||||
listed.push(path);
|
||||
return {
|
||||
path,
|
||||
home: 'C:\\Users\\alice',
|
||||
crumbs: [{ name: path, path, hidden: false }],
|
||||
entries: [],
|
||||
truncated: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
let renderer;
|
||||
await act(async () => {
|
||||
renderer = create(React.createElement(WorkspaceEditor, {
|
||||
workspace: start,
|
||||
directoryPicker: picker,
|
||||
async onSave(value) { saved.push(value); },
|
||||
}));
|
||||
});
|
||||
await act(async () => {
|
||||
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
|
||||
await flushMicrotasks();
|
||||
});
|
||||
|
||||
const pathForm = renderer.root.findByProps({ className: 'dim-directoryPathForm' });
|
||||
assert.equal(renderer.root.findByProps({ className: 'dim-directoryPathInput' }).props.value, start);
|
||||
for (const target of targets) {
|
||||
await act(async () => {
|
||||
renderer.root.findByProps({ className: 'dim-directoryPathInput' }).props.onChange({
|
||||
target: { value: target },
|
||||
});
|
||||
});
|
||||
assert.equal(
|
||||
renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.disabled,
|
||||
true,
|
||||
);
|
||||
await act(async () => {
|
||||
pathForm.props.onSubmit({ preventDefault() {} });
|
||||
await flushMicrotasks();
|
||||
});
|
||||
assert.equal(renderer.root.findByProps({ className: 'dim-directoryPathInput' }).props.value, target);
|
||||
assert.equal(
|
||||
renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.disabled,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
assert.deepEqual(listed, [start, ...targets]);
|
||||
await act(async () => {
|
||||
renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.onClick();
|
||||
await flushMicrotasks();
|
||||
});
|
||||
assert.deepEqual(saved, [targets.at(-1)]);
|
||||
});
|
||||
|
||||
test('WorkspaceEditor keeps the prior folder unselectable when a typed path cannot be read', async () => {
|
||||
const start = 'C:\\Users\\alice';
|
||||
const missing = 'Z:\\missing';
|
||||
const picker = {
|
||||
async listDirectory(path) {
|
||||
if (path === missing) {
|
||||
const error = new Error('cannot read requested folder');
|
||||
error.rpcError = { code: 'directory-unreadable', message: error.message, details: { path } };
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
path,
|
||||
home: start,
|
||||
crumbs: [{ name: 'Home', path, hidden: false }],
|
||||
entries: [],
|
||||
truncated: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
let renderer;
|
||||
await act(async () => {
|
||||
renderer = create(React.createElement(WorkspaceEditor, {
|
||||
workspace: start,
|
||||
directoryPicker: picker,
|
||||
async onSave() { throw new Error('must not save'); },
|
||||
}));
|
||||
});
|
||||
await act(async () => {
|
||||
renderer.root.findByProps({ className: 'dim-workspaceEdit' }).props.onClick();
|
||||
await flushMicrotasks();
|
||||
});
|
||||
await act(async () => {
|
||||
renderer.root.findByProps({ className: 'dim-directoryPathInput' }).props.onChange({
|
||||
target: { value: missing },
|
||||
});
|
||||
await flushMicrotasks();
|
||||
});
|
||||
await act(async () => {
|
||||
renderer.root.findByProps({ className: 'dim-directoryPathForm' }).props.onSubmit({ preventDefault() {} });
|
||||
await flushMicrotasks();
|
||||
});
|
||||
|
||||
assert.equal(textOf(renderer.root.findByProps({ role: 'alert' })), 'cannot read requested folder');
|
||||
assert.equal(renderer.root.findByProps({ className: 'dim-directoryPathInput' }).props.value, missing);
|
||||
assert.equal(
|
||||
renderer.root.findByProps({ className: 'dim-directoryPickerPrimary' }).props.disabled,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('WorkspaceEditor keeps the picker open and presents a rejected workspace error', async () => {
|
||||
const picker = {
|
||||
async listDirectory(path) {
|
||||
|
|
@ -386,6 +501,11 @@ test('WorkspaceEditor never translates Host filesystem names in the English UI',
|
|||
const directory = renderer.root.findByProps({ title: '/workspace/current/微信' });
|
||||
assert.equal(textOf(directory), '微信');
|
||||
assert.doesNotMatch(textOf(directory), /WeChat/);
|
||||
assert.equal(
|
||||
renderer.root.findByProps({ className: 'dim-directoryPathInput' }).props.placeholder,
|
||||
'Enter a full absolute path on the Host',
|
||||
);
|
||||
assert.ok(buttonNamed(renderer.root, 'Go'));
|
||||
await act(async () => { renderer.unmount(); });
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue