fix(client,doc): address review — flow-open busy gating, seam on the architecture map, browse gap documented
- While a picking flow is open (native chooser pending, browse dialog up) or its pick is being adopted, every other menu action disables: a late outcome must not race a concurrent selection or creation (ds-review-bot warning). - ctx.directoryPicker joins the architecture Capability Services map (both languages); neighboring rows condensed to keep the doc inside its ceiling. - directory-picker-browse documents that its client half lands in the next stacked PR: a -browse composition today hides the picking affordance (the documented empty-hole default) rather than misbehaving (ds-review-bot critical; the dialog itself ships in #821).
This commit is contained in:
@@ -81,6 +81,11 @@ export function WorkspaceCreateFlow({
|
||||
const [pickingFolder, setPickingFolder] = useState(false)
|
||||
const [folderConflict, setFolderConflict] = useState(false)
|
||||
const composingRef = useRef(false)
|
||||
// One picking interaction at a time: while the flow is open (native chooser
|
||||
// pending, browse dialog up) or its pick is being adopted, every other
|
||||
// menu action stays disabled — a late outcome must not race a concurrent
|
||||
// selection or creation.
|
||||
const flowBusy = flowOpen || pickingFolder
|
||||
const normalizedWorkspaceName = workspaceName.trim()
|
||||
const duplicateWorkspaceName = !creating && normalizedWorkspaceName !== ''
|
||||
&& workspaces.some(workspace => workspace.title === normalizedWorkspaceName)
|
||||
@@ -91,9 +96,9 @@ export function WorkspaceCreateFlow({
|
||||
// activation, and the menu re-renders on every toggle.
|
||||
const createEntries: MenuEntry[] = [
|
||||
...(hasDirectoryFlow()
|
||||
? [{ id: OPEN_LOCAL_FOLDER, label: 'Open local folder…', icon: <IconFolderClose16 size={16} />, disabled: pickingFolder }]
|
||||
? [{ id: OPEN_LOCAL_FOLDER, label: 'Open local folder…', icon: <IconFolderClose16 size={16} />, disabled: flowBusy }]
|
||||
: []),
|
||||
{ id: CREATE_NEW, label: 'Create a new workspace', icon: <IconPlusOutline16 size={16} />, disabled: pickingFolder },
|
||||
{ id: CREATE_NEW, label: 'Create a new workspace', icon: <IconPlusOutline16 size={16} />, disabled: flowBusy },
|
||||
]
|
||||
// With workspaces listed, the create actions pin below the scroll region
|
||||
// (divider + always visible); otherwise they ARE the menu.
|
||||
@@ -103,7 +108,7 @@ export function WorkspaceCreateFlow({
|
||||
id: workspace.workspaceId,
|
||||
label: workspace.title,
|
||||
icon: <IconFolderClose16 size={16} />,
|
||||
disabled: pickingFolder,
|
||||
disabled: flowBusy,
|
||||
}))
|
||||
: createEntries
|
||||
|
||||
|
||||
@@ -149,12 +149,16 @@ describe('WorkspacePicker', () => {
|
||||
expect(b.onPick).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('disables the create actions and reports busy to the flow while adopting', async () => {
|
||||
it('disables every menu action from flow open through adoption, and reports busy to the flow', async () => {
|
||||
let resolve!: (workspace: WorkspaceView) => void
|
||||
const pending = new Promise<WorkspaceView>((settle) => { resolve = settle })
|
||||
const created = workspace('adopted')
|
||||
const b = mount([], vi.fn(() => pending))
|
||||
const b = mount([workspace('alpha', 'Alpha')], vi.fn(() => pending))
|
||||
chooseItem('Open local folder…')
|
||||
// The flow is open but nothing is picked yet: a chooser pending on the
|
||||
// host display must already block concurrent workspace actions.
|
||||
expect(screen.getByRole<HTMLButtonElement>('menuitem', { name: 'Alpha' }).disabled).toBe(true)
|
||||
expect(screen.getByRole<HTMLButtonElement>('menuitem', { name: 'Create a new workspace' }).disabled).toBe(true)
|
||||
act(() => { b.probe.owner!.onPicked('/tmp/project') })
|
||||
expect(b.probe.owner!.busy).toBe(true)
|
||||
expect(screen.getByRole<HTMLButtonElement>('menuitem', { name: 'Open local folder…' }).disabled).toBe(true)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/host/directory-picker-browse/README.md
|
||||
README.md: 688a60894cb0ab066a6d501e4df310f8d31bfb5f
|
||||
README.zh.md: c19bccc2ff9268cb7a6c931da4671bebc9f76b0c
|
||||
README.md: 632dfec3dac57cac9ea7a02225959fe6e3acf6a0
|
||||
README.zh.md: 81a1eb53eac0d3b5a1ef8f2f98c4359ef6a4c5fd
|
||||
|
||||
@@ -16,6 +16,7 @@ None; this package neither assembles nor sends a provider request.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **No client half yet** — the in-app browsing dialog that consumes these primitives lands in the next PR of this stack; until then a `-browse` composition hides the picking affordance entirely (ui-workspace's documented empty-hole default) and the listing/creation RPCs go unconsumed.
|
||||
- **Windows hidden attribute is not read** — Node dirents do not expose `FILE_ATTRIBUTE_HIDDEN`, so `hidden` means dot-prefixed on every platform until a native probe is worth its cost.
|
||||
- **No drive-root enumeration** — on Windows the ancestry stops at the drive root; crossing drives waits for the browser UI's path-entry affordance rather than an enumeration primitive here.
|
||||
- **Whole-filesystem scope** — no per-deployment browse-root restriction; `workspace.create` accepts arbitrary paths today, so a root here would be UX scoping, not a boundary — deferred until a deployment needs it.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- **尚无 client half**——消费这些原语的应用内浏览对话框在本栈的下一个 PR 落地;在那之前 `-browse` 组合会完全隐藏选目录入口(ui-workspace 文档化的空洞默认行为),列举/创建 RPC 无消费者。
|
||||
- **不读取 Windows 隐藏属性**——Node 的 dirent 不暴露 `FILE_ATTRIBUTE_HIDDEN`,因此在所有平台上 `hidden` 都意味着点前缀,直到原生探测值回其成本为止。
|
||||
- **不枚举盘符根**——Windows 上祖先链止于盘符根;跨盘依赖浏览器 UI 的路径输入入口,而不是这里的枚举原语。
|
||||
- **全盘可浏览**——没有按部署限定的浏览根;`workspace.create` 今天就接受任意路径,这里的根只会是 UX 范围而非边界——等到有部署需要时再做。
|
||||
|
||||
Reference in New Issue
Block a user