mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 02:40:45 +08:00
Fix kbRoot browse ignoring Typert Result from directoryPicker.pick.
Unwrap { ok, value } so the chosen folder path lands in the field and auto-saves.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f64f1279e7
commit
fcdd11ac59
4 changed files with 165 additions and 57 deletions
|
|
@ -2021,19 +2021,30 @@ class NetxopsCardController {
|
||||||
this.kbUiError = null;
|
this.kbUiError = null;
|
||||||
this.store.set(this.projection());
|
this.store.set(this.projection());
|
||||||
const applyPath = (path) => {
|
const applyPath = (path) => {
|
||||||
if (typeof path !== "string" || path.trim() === "") {
|
this.form.actions().edit("kbRoot", path);
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.form.actions().edit("kbRoot", path.trim());
|
|
||||||
this.refreshKbPreview();
|
this.refreshKbPreview();
|
||||||
|
this.form.save().then(() => {
|
||||||
|
this.refreshKbStatus();
|
||||||
|
this.refreshKbPreview();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const fail = (error) => {
|
const fail = (error) => {
|
||||||
this.kbUiError = error instanceof Error ? error.message : String(error);
|
this.kbUiError = error instanceof Error ? error.message : String(error);
|
||||||
this.store.set(this.projection());
|
this.store.set(this.projection());
|
||||||
};
|
};
|
||||||
|
const acceptPickResult = (result) => {
|
||||||
|
const path = unwrapDirectoryPickResult(result);
|
||||||
|
if (path === null)
|
||||||
|
return;
|
||||||
|
if (path === undefined) {
|
||||||
|
fail(`unexpected directoryPicker result: ${safeJson(result)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyPath(path);
|
||||||
|
};
|
||||||
const picker = this.kbDirectoryPicker;
|
const picker = this.kbDirectoryPicker;
|
||||||
if (picker !== undefined) {
|
if (picker !== undefined) {
|
||||||
picker.pick().then(applyPath).catch(fail).finally(() => {
|
picker.pick().then(acceptPickResult).catch(fail).finally(() => {
|
||||||
this.kbBrowseInFlight = false;
|
this.kbBrowseInFlight = false;
|
||||||
this.store.set(this.projection());
|
this.store.set(this.projection());
|
||||||
});
|
});
|
||||||
|
|
@ -2046,24 +2057,7 @@ class NetxopsCardController {
|
||||||
this.store.set(this.projection());
|
this.store.set(this.projection());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
call("/api", "directoryPicker/pick", { args: {} }).then((result) => {
|
call("/api", "directoryPicker/pick", { args: {} }).then(acceptPickResult).catch(fail).finally(() => {
|
||||||
if (typeof result === "string" || result === null) {
|
|
||||||
applyPath(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result !== null && typeof result === "object") {
|
|
||||||
const row = result;
|
|
||||||
if (row.ok === false) {
|
|
||||||
fail(row.error?.message || "directoryPicker/pick failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ("value" in row) {
|
|
||||||
applyPath(row.value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
applyPath(result);
|
|
||||||
}).catch(fail).finally(() => {
|
|
||||||
this.kbBrowseInFlight = false;
|
this.kbBrowseInFlight = false;
|
||||||
this.store.set(this.projection());
|
this.store.set(this.projection());
|
||||||
});
|
});
|
||||||
|
|
@ -2215,6 +2209,37 @@ function refOf(snapshot) {
|
||||||
const declared = snapshot.value?.tokenCredentialRef;
|
const declared = snapshot.value?.tokenCredentialRef;
|
||||||
return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF;
|
return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF;
|
||||||
}
|
}
|
||||||
|
function unwrapDirectoryPickResult(result) {
|
||||||
|
if (result === null)
|
||||||
|
return null;
|
||||||
|
if (typeof result === "string") {
|
||||||
|
const trimmed = result.trim();
|
||||||
|
return trimmed === "" ? null : trimmed;
|
||||||
|
}
|
||||||
|
if (result === null || typeof result !== "object" || Array.isArray(result))
|
||||||
|
return;
|
||||||
|
const row = result;
|
||||||
|
if (row.ok === false) {
|
||||||
|
throw new Error(row.error?.message || "directoryPicker/pick failed");
|
||||||
|
}
|
||||||
|
if ("value" in row) {
|
||||||
|
if (row.value === null)
|
||||||
|
return null;
|
||||||
|
if (typeof row.value === "string") {
|
||||||
|
const trimmed = row.value.trim();
|
||||||
|
return trimmed === "" ? null : trimmed;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function safeJson(value) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
} catch {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// src/client/locales.ts
|
// src/client/locales.ts
|
||||||
var en = {
|
var en = {
|
||||||
|
|
@ -2237,11 +2262,11 @@ var en = {
|
||||||
groupInPreset: "In Netx Ops preset",
|
groupInPreset: "In Netx Ops preset",
|
||||||
groupPublic: "Publish to other presets",
|
groupPublic: "Publish to other presets",
|
||||||
kbRoot: "Package root",
|
kbRoot: "Package root",
|
||||||
kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Empty = pure netx.",
|
kbRootHint: "Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves.",
|
||||||
kbBrowse: "Browse…",
|
kbBrowse: "Browse…",
|
||||||
kbBrowseUnavailable: "Directory picker unavailable — paste an absolute folder path, then Save.",
|
kbBrowseUnavailable: "Directory picker unavailable — paste an absolute folder path, then Save.",
|
||||||
kbBrowseFailed: "Browse failed: {detail}",
|
kbBrowseFailed: "Browse failed: {detail}",
|
||||||
kbSaveHint: "Click Save after choosing a path to apply the knowledge base.",
|
kbSaveHint: "Browse writes and saves automatically. Manual paste still needs Save.",
|
||||||
kbStatusConfigured: "Knowledge base: {operator} ({country}) v{version}",
|
kbStatusConfigured: "Knowledge base: {operator} ({country}) v{version}",
|
||||||
kbStatusUnconfigured: "Knowledge base: not configured (pure netx)",
|
kbStatusUnconfigured: "Knowledge base: not configured (pure netx)",
|
||||||
kbStatusError: "Knowledge base: error — {detail}",
|
kbStatusError: "Knowledge base: error — {detail}",
|
||||||
|
|
@ -2309,11 +2334,11 @@ var zh = {
|
||||||
groupInPreset: "在 Netx Ops 预设中启用",
|
groupInPreset: "在 Netx Ops 预设中启用",
|
||||||
groupPublic: "对其他预设公开",
|
groupPublic: "对其他预设公开",
|
||||||
kbRoot: "知识包根目录",
|
kbRoot: "知识包根目录",
|
||||||
kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。留空=纯 netx。",
|
kbRootHint: "含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存。",
|
||||||
kbBrowse: "浏览…",
|
kbBrowse: "浏览…",
|
||||||
kbBrowseUnavailable: "目录选择器不可用 — 请粘贴绝对目录路径后点保存。",
|
kbBrowseUnavailable: "目录选择器不可用 — 请粘贴绝对目录路径后点保存。",
|
||||||
kbBrowseFailed: "浏览失败:{detail}",
|
kbBrowseFailed: "浏览失败:{detail}",
|
||||||
kbSaveHint: "选好路径后请点「保存」,知识库才会生效。",
|
kbSaveHint: "点「浏览」选目录后会自动写入并保存;若是手粘路径仍需点保存。",
|
||||||
kbStatusConfigured: "知识库: {operator}({country}) v{version}",
|
kbStatusConfigured: "知识库: {operator}({country}) v{version}",
|
||||||
kbStatusUnconfigured: "知识库: 未配置(纯 netx)",
|
kbStatusUnconfigured: "知识库: 未配置(纯 netx)",
|
||||||
kbStatusError: "知识库: 错误 — {detail}",
|
kbStatusError: "知识库: 错误 — {detail}",
|
||||||
|
|
|
||||||
|
|
@ -412,13 +412,14 @@ export class NetxopsCardController {
|
||||||
this.kbUiError = null
|
this.kbUiError = null
|
||||||
this.store.set(this.projection())
|
this.store.set(this.projection())
|
||||||
|
|
||||||
const applyPath = (path: unknown): void => {
|
const applyPath = (path: string): void => {
|
||||||
if (typeof path !== 'string' || path.trim() === '') {
|
this.form.actions().edit('kbRoot', path)
|
||||||
// Cancel / empty — no error
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.form.actions().edit('kbRoot', path.trim())
|
|
||||||
void this.refreshKbPreview()
|
void this.refreshKbPreview()
|
||||||
|
// Selecting a folder should land immediately — don't require a separate Save.
|
||||||
|
void this.form.save().then(() => {
|
||||||
|
void this.refreshKbStatus()
|
||||||
|
void this.refreshKbPreview()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fail = (error: unknown): void => {
|
const fail = (error: unknown): void => {
|
||||||
|
|
@ -426,10 +427,24 @@ export class NetxopsCardController {
|
||||||
this.store.set(this.projection())
|
this.store.set(this.projection())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `remote.directoryPicker.pick()` returns a Typert Result `{ ok, value }`,
|
||||||
|
* not a bare string (see dsh ui-workspace Navigation.pickDirectory).
|
||||||
|
*/
|
||||||
|
const acceptPickResult = (result: unknown): void => {
|
||||||
|
const path = unwrapDirectoryPickResult(result)
|
||||||
|
if (path === null) return // user cancelled
|
||||||
|
if (path === undefined) {
|
||||||
|
fail(`unexpected directoryPicker result: ${safeJson(result)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applyPath(path)
|
||||||
|
}
|
||||||
|
|
||||||
const picker = this.kbDirectoryPicker
|
const picker = this.kbDirectoryPicker
|
||||||
if (picker !== undefined) {
|
if (picker !== undefined) {
|
||||||
void picker.pick()
|
void picker.pick()
|
||||||
.then(applyPath)
|
.then(acceptPickResult)
|
||||||
.catch(fail)
|
.catch(fail)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.kbBrowseInFlight = false
|
this.kbBrowseInFlight = false
|
||||||
|
|
@ -447,25 +462,7 @@ export class NetxopsCardController {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void call('/api', 'directoryPicker/pick', { args: {} })
|
void call('/api', 'directoryPicker/pick', { args: {} })
|
||||||
.then((result) => {
|
.then(acceptPickResult)
|
||||||
// Remotes often return the path directly, or { ok, value }.
|
|
||||||
if (typeof result === 'string' || result === null) {
|
|
||||||
applyPath(result)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (result !== null && typeof result === 'object') {
|
|
||||||
const row = result as { ok?: boolean; value?: unknown; error?: { message?: string } }
|
|
||||||
if (row.ok === false) {
|
|
||||||
fail(row.error?.message || 'directoryPicker/pick failed')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ('value' in row) {
|
|
||||||
applyPath(row.value)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
applyPath(result)
|
|
||||||
})
|
|
||||||
.catch(fail)
|
.catch(fail)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.kbBrowseInFlight = false
|
this.kbBrowseInFlight = false
|
||||||
|
|
@ -625,3 +622,41 @@ function refOf(snapshot: SettingsScopeSnapshot<NetxopsSettings>): string {
|
||||||
const declared = snapshot.value?.tokenCredentialRef
|
const declared = snapshot.value?.tokenCredentialRef
|
||||||
return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF
|
return declared !== undefined && declared.length > 0 ? declared : DEFAULT_TOKEN_REF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize directoryPicker pick outcomes.
|
||||||
|
* @returns trimmed path; `null` = cancelled; `undefined` = unparseable.
|
||||||
|
*/
|
||||||
|
function unwrapDirectoryPickResult(result: unknown): string | null | undefined {
|
||||||
|
if (result === null) return null
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
const trimmed = result.trim()
|
||||||
|
return trimmed === '' ? null : trimmed
|
||||||
|
}
|
||||||
|
if (result === null || typeof result !== 'object' || Array.isArray(result)) return undefined
|
||||||
|
const row = result as {
|
||||||
|
ok?: boolean
|
||||||
|
value?: unknown
|
||||||
|
error?: { message?: string }
|
||||||
|
}
|
||||||
|
if (row.ok === false) {
|
||||||
|
throw new Error(row.error?.message || 'directoryPicker/pick failed')
|
||||||
|
}
|
||||||
|
if ('value' in row) {
|
||||||
|
if (row.value === null) return null
|
||||||
|
if (typeof row.value === 'string') {
|
||||||
|
const trimmed = row.value.trim()
|
||||||
|
return trimmed === '' ? null : trimmed
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeJson(value: unknown): string {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
} catch {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,11 +92,11 @@ export const en: Record<NetxopsLocaleKey, string> = {
|
||||||
groupInPreset: 'In Netx Ops preset',
|
groupInPreset: 'In Netx Ops preset',
|
||||||
groupPublic: 'Publish to other presets',
|
groupPublic: 'Publish to other presets',
|
||||||
kbRoot: 'Package root',
|
kbRoot: 'Package root',
|
||||||
kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Empty = pure netx.',
|
kbRootHint: 'Folder with MANIFEST.json (operator-subset v1.0). Browse auto-saves.',
|
||||||
kbBrowse: 'Browse…',
|
kbBrowse: 'Browse…',
|
||||||
kbBrowseUnavailable: 'Directory picker unavailable — paste an absolute folder path, then Save.',
|
kbBrowseUnavailable: 'Directory picker unavailable — paste an absolute folder path, then Save.',
|
||||||
kbBrowseFailed: 'Browse failed: {detail}',
|
kbBrowseFailed: 'Browse failed: {detail}',
|
||||||
kbSaveHint: 'Click Save after choosing a path to apply the knowledge base.',
|
kbSaveHint: 'Browse writes and saves automatically. Manual paste still needs Save.',
|
||||||
kbStatusConfigured: 'Knowledge base: {operator} ({country}) v{version}',
|
kbStatusConfigured: 'Knowledge base: {operator} ({country}) v{version}',
|
||||||
kbStatusUnconfigured: 'Knowledge base: not configured (pure netx)',
|
kbStatusUnconfigured: 'Knowledge base: not configured (pure netx)',
|
||||||
kbStatusError: 'Knowledge base: error — {detail}',
|
kbStatusError: 'Knowledge base: error — {detail}',
|
||||||
|
|
@ -165,11 +165,11 @@ export const zh: Record<NetxopsLocaleKey, string> = {
|
||||||
groupInPreset: '在 Netx Ops 预设中启用',
|
groupInPreset: '在 Netx Ops 预设中启用',
|
||||||
groupPublic: '对其他预设公开',
|
groupPublic: '对其他预设公开',
|
||||||
kbRoot: '知识包根目录',
|
kbRoot: '知识包根目录',
|
||||||
kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。留空=纯 netx。',
|
kbRootHint: '含 MANIFEST.json 的运营商子集包(v1.0)。浏览选目录后会自动保存。',
|
||||||
kbBrowse: '浏览…',
|
kbBrowse: '浏览…',
|
||||||
kbBrowseUnavailable: '目录选择器不可用 — 请粘贴绝对目录路径后点保存。',
|
kbBrowseUnavailable: '目录选择器不可用 — 请粘贴绝对目录路径后点保存。',
|
||||||
kbBrowseFailed: '浏览失败:{detail}',
|
kbBrowseFailed: '浏览失败:{detail}',
|
||||||
kbSaveHint: '选好路径后请点「保存」,知识库才会生效。',
|
kbSaveHint: '点「浏览」选目录后会自动写入并保存;若是手粘路径仍需点保存。',
|
||||||
kbStatusConfigured: '知识库: {operator}({country}) v{version}',
|
kbStatusConfigured: '知识库: {operator}({country}) v{version}',
|
||||||
kbStatusUnconfigured: '知识库: 未配置(纯 netx)',
|
kbStatusUnconfigured: '知识库: 未配置(纯 netx)',
|
||||||
kbStatusError: '知识库: 错误 — {detail}',
|
kbStatusError: '知识库: 错误 — {detail}',
|
||||||
|
|
|
||||||
48
test/kb-pick-result.test.mjs
Normal file
48
test/kb-pick-result.test.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/**
|
||||||
|
* Directory-picker Result unwrapping (mirrors controller helper).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
function unwrapDirectoryPickResult(result) {
|
||||||
|
if (result === null) return null
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
const trimmed = result.trim()
|
||||||
|
return trimmed === '' ? null : trimmed
|
||||||
|
}
|
||||||
|
if (typeof result !== 'object' || Array.isArray(result)) return undefined
|
||||||
|
const row = result
|
||||||
|
if (row.ok === false) {
|
||||||
|
throw new Error(row.error?.message || 'directoryPicker/pick failed')
|
||||||
|
}
|
||||||
|
if ('value' in row) {
|
||||||
|
if (row.value === null) return null
|
||||||
|
if (typeof row.value === 'string') {
|
||||||
|
const trimmed = row.value.trim()
|
||||||
|
return trimmed === '' ? null : trimmed
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
test('unwrap bare string path', () => {
|
||||||
|
assert.equal(unwrapDirectoryPickResult('D:\\kb'), 'D:\\kb')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unwrap typert Result value', () => {
|
||||||
|
assert.equal(unwrapDirectoryPickResult({ ok: true, value: 'D:\\kb\\ioh' }), 'D:\\kb\\ioh')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unwrap cancel null', () => {
|
||||||
|
assert.equal(unwrapDirectoryPickResult(null), null)
|
||||||
|
assert.equal(unwrapDirectoryPickResult({ ok: true, value: null }), null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reject Result error', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => unwrapDirectoryPickResult({ ok: false, error: { message: 'no native' } }),
|
||||||
|
/no native/,
|
||||||
|
)
|
||||||
|
})
|
||||||
Loading…
Add table
Add a link
Reference in a new issue