fix(host,client): review round 24 — close-edge facts synced; wire-hop pointer; platform-flavored double join; loading reset
This commit is contained in:
@@ -48,10 +48,10 @@ export interface IWorkspaces {
|
|||||||
* Create one child directory through the Host's `browse` capability.
|
* Create one child directory through the Host's `browse` capability.
|
||||||
* @param path - absolute existing parent directory.
|
* @param path - absolute existing parent directory.
|
||||||
* @param name - single non-blank path segment.
|
* @param name - single non-blank path segment.
|
||||||
* @returns the created directory's absolute path, in the shape
|
* @returns the created directory's absolute path, in the shape the wire
|
||||||
* `DirectoryPickerBrowseCapability.createDirectory` contracts: verbatim
|
* `HostApi.createDirectory` contracts: verbatim equal to the child's
|
||||||
* equal to the child's `entries[].path` in the parent's next listing
|
* `entries[].path` in the parent's next listing (the browser anchors a
|
||||||
* (the browser anchors a create landing's selection on that equality).
|
* create landing's selection on that equality).
|
||||||
*/
|
*/
|
||||||
createDirectory(path: string, name: string): Promise<string>
|
createDirectory(path: string, name: string): Promise<string>
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -149,8 +149,11 @@ export class TestWorkspaces implements IWorkspaces {
|
|||||||
this.calls.push({ method: 'createDirectory', args: [path, name] })
|
this.calls.push({ method: 'createDirectory', args: [path, name] })
|
||||||
const stub = this.stubs.get('createDirectory')
|
const stub = this.stubs.get('createDirectory')
|
||||||
if (stub !== undefined) return await (stub(path, name) as Promise<string>)
|
if (stub !== undefined) return await (stub(path, name) as Promise<string>)
|
||||||
// Canonical join: a bare-root parent must not double the separator.
|
// Join in the parent's own separator flavor (a canonical parent ends
|
||||||
return path.endsWith('/') ? `${path}${name}` : `${path}/${name}`
|
// with one only when it is a bare root), so the contract's verbatim
|
||||||
|
// equality holds for POSIX and Windows fixture trees alike.
|
||||||
|
const sep = path.includes('\\') ? '\\' : '/'
|
||||||
|
return path.endsWith(sep) ? `${path}${name}` : `${path}${sep}${name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -329,9 +329,12 @@ describe('workspaces', () => {
|
|||||||
await expect(runtime.workspaces.listDirectory()).resolves.toMatchObject({ path: '/home/test', entries: [] })
|
await expect(runtime.workspaces.listDirectory()).resolves.toMatchObject({ path: '/home/test', entries: [] })
|
||||||
await expect(runtime.workspaces.listDirectory('/home/test')).resolves.toMatchObject({ path: '/home/test' })
|
await expect(runtime.workspaces.listDirectory('/home/test')).resolves.toMatchObject({ path: '/home/test' })
|
||||||
await expect(runtime.workspaces.createDirectory('/home/test', 'fresh')).resolves.toBe('/home/test/fresh')
|
await expect(runtime.workspaces.createDirectory('/home/test', 'fresh')).resolves.toBe('/home/test/fresh')
|
||||||
// Canonical join: a bare-root parent yields /top, not //top (the
|
// Canonical join in the parent's own separator flavor: bare roots do
|
||||||
|
// not double the separator, Windows parents keep backslashes (the
|
||||||
// IWorkspaces contract's verbatim entries[].path equality).
|
// IWorkspaces contract's verbatim entries[].path equality).
|
||||||
await expect(runtime.workspaces.createDirectory('/', 'top')).resolves.toBe('/top')
|
await expect(runtime.workspaces.createDirectory('/', 'top')).resolves.toBe('/top')
|
||||||
|
await expect(runtime.workspaces.createDirectory('C:\\', 'top')).resolves.toBe('C:\\top')
|
||||||
|
await expect(runtime.workspaces.createDirectory('C:\\Users', 'Alice')).resolves.toBe('C:\\Users\\Alice')
|
||||||
// The recorded signal seat mirrors the production face (undefined here;
|
// The recorded signal seat mirrors the production face (undefined here;
|
||||||
// cancellation tests pass and observe a real one).
|
// cancellation tests pass and observe a real one).
|
||||||
expect(runtime.workspaces.calls).toEqual([
|
expect(runtime.workspaces.calls).toEqual([
|
||||||
@@ -339,6 +342,8 @@ describe('workspaces', () => {
|
|||||||
{ method: 'listDirectory', args: ['/home/test', undefined] },
|
{ method: 'listDirectory', args: ['/home/test', undefined] },
|
||||||
{ method: 'createDirectory', args: ['/home/test', 'fresh'] },
|
{ method: 'createDirectory', args: ['/home/test', 'fresh'] },
|
||||||
{ method: 'createDirectory', args: ['/', 'top'] },
|
{ method: 'createDirectory', args: ['/', 'top'] },
|
||||||
|
{ method: 'createDirectory', args: ['C:\\', 'top'] },
|
||||||
|
{ method: 'createDirectory', args: ['C:\\Users', 'Alice'] },
|
||||||
])
|
])
|
||||||
// Stubs replace the defaults like every sibling method.
|
// Stubs replace the defaults like every sibling method.
|
||||||
const listing = { path: '/x', home: '/x', crumbs: [], entries: [] }
|
const listing = { path: '/x', home: '/x', crumbs: [], entries: [] }
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import css from './DirectoryBrowser.module.css'
|
|||||||
|
|
||||||
/** Owner-supplied browser props: browse calls, pick semantics, and copy. */
|
/** Owner-supplied browser props: browse calls, pick semantics, and copy. */
|
||||||
export interface DirectoryBrowserProps {
|
export interface DirectoryBrowserProps {
|
||||||
/** Dialog visibility (owner-local; closed unmounts nothing but resets on reopen). */
|
/** Dialog visibility (owner-local; closing resets the per-open state, so a reopen starts clean on its first frame). */
|
||||||
open: boolean
|
open: boolean
|
||||||
/** List one directory level (absent path = the Host home directory); the signal aborts a superseded scan on the wire. */
|
/** List one directory level (absent path = the Host home directory); the signal aborts a superseded scan on the wire. */
|
||||||
listDirectory: (path?: string, signal?: AbortSignal) => Promise<DirectoryListing>
|
listDirectory: (path?: string, signal?: AbortSignal) => Promise<DirectoryListing>
|
||||||
@@ -258,7 +258,7 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
|
|||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
// Path-edit state: null = breadcrumb mode; a string = the draft being typed.
|
// Path-edit state: null = breadcrumb mode; a string = the draft being typed.
|
||||||
const [pathDraft, setPathDraft] = useState<string | null>(null)
|
const [pathDraft, setPathDraft] = useState<string | null>(null)
|
||||||
// Show-hidden toggle state (pure client-side filter, reset on each open).
|
// Show-hidden toggle state (pure client-side filter, reset on close).
|
||||||
const [showHidden, setShowHidden] = useState(false)
|
const [showHidden, setShowHidden] = useState(false)
|
||||||
// Create-folder state: null = closed; a string = the nested dialog's draft.
|
// Create-folder state: null = closed; a string = the nested dialog's draft.
|
||||||
const [folderDraft, setFolderDraft] = useState<string | null>(null)
|
const [folderDraft, setFolderDraft] = useState<string | null>(null)
|
||||||
@@ -334,9 +334,10 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
|
|||||||
* show-hidden toggle's click to decide whether to reclaim the native
|
* show-hidden toggle's click to decide whether to reclaim the native
|
||||||
* focus outcome. A probe only: it never gates its caller — a torn-down
|
* focus outcome. A probe only: it never gates its caller — a torn-down
|
||||||
* ref in a landing's close race merely skips the parking, and
|
* ref in a landing's close race merely skips the parking, and
|
||||||
* committing the landing into a closing dialog is safe (the component
|
* committing the landing into a closing dialog is safe: the close edge's
|
||||||
* already renders null, and the open effect resets parent/selected/child
|
* supersede() fences every later settlement, and the same close effect
|
||||||
* on the next open).
|
* zeroes parent/selected/child for the one frame that can slip between
|
||||||
|
* the close render and its effect.
|
||||||
* @returns true when `document.activeElement` is inside the miller row.
|
* @returns true when `document.activeElement` is inside the miller row.
|
||||||
*/
|
*/
|
||||||
const focusInMillerRows = useCallback((): boolean => {
|
const focusInMillerRows = useCallback((): boolean => {
|
||||||
@@ -509,7 +510,9 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
|
|||||||
// dialog. The per-open state resets live on the CLOSE edge: resetting on
|
// dialog. The per-open state resets live on the CLOSE edge: resetting on
|
||||||
// open would let the reopen's first commit paint one frame of the stale
|
// open would let the reopen's first commit paint one frame of the stale
|
||||||
// view (revealed hidden rows, a pressed toggle) before this passive
|
// view (revealed hidden rows, a pressed toggle) before this passive
|
||||||
// effect runs.
|
// effect runs. No automated gate observes that ordering (act() hides the
|
||||||
|
// frame in tests) — this comment is the guard; read it before moving
|
||||||
|
// these back.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
openGeneration.current += 1
|
openGeneration.current += 1
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -522,6 +525,7 @@ export function DirectoryBrowser({ open, listDirectory, createDirectory, onOpen,
|
|||||||
setChild(null)
|
setChild(null)
|
||||||
setCreatingFolder(false)
|
setCreatingFolder(false)
|
||||||
setShowHidden(false)
|
setShowHidden(false)
|
||||||
|
setLoading(false)
|
||||||
setError(null)
|
setError(null)
|
||||||
setPathDraft(null)
|
setPathDraft(null)
|
||||||
setFolderDraft(null)
|
setFolderDraft(null)
|
||||||
|
|||||||
@@ -15,11 +15,7 @@ import { DirectoryBrowser } from './DirectoryBrowser.tsx'
|
|||||||
export interface BrowseFlowInjected {
|
export interface BrowseFlowInjected {
|
||||||
/** List one directory level (absent path = the Host home directory); the signal aborts a superseded scan. */
|
/** List one directory level (absent path = the Host home directory); the signal aborts a superseded scan. */
|
||||||
listDirectory: (path?: string, signal?: AbortSignal) => Promise<DirectoryListing>
|
listDirectory: (path?: string, signal?: AbortSignal) => Promise<DirectoryListing>
|
||||||
/**
|
/** Create one child directory under an existing parent; returns the created path in the shape `IWorkspaces.createDirectory` contracts. */
|
||||||
* Create one child directory under an existing parent; returns the
|
|
||||||
* created path in the shape `IWorkspaces.createDirectory` contracts
|
|
||||||
* (verbatim equal to the child's next `entries[].path`).
|
|
||||||
*/
|
|
||||||
createDirectory: (path: string, name: string) => Promise<string>
|
createDirectory: (path: string, name: string) => Promise<string>
|
||||||
/** Localized dialog copy (this package's namespace). */
|
/** Localized dialog copy (this package's namespace). */
|
||||||
t: Translate
|
t: Translate
|
||||||
|
|||||||
Reference in New Issue
Block a user