fix(ui-workspace): keep the per-resource access gates on the whenAccess client
Master's rows and menus carry no answer of their own, so the served-page seams thread the Slot-standard access Hooks through the workspace browser and the session actions: New Session, workspace rename/delete/reorder, and a Session's rename, pin, archive, and reorder appear only while their own resource allows them. The rows spec keeps master's pointer-armed rename helper and supplies the allow-all answers as props.
This commit is contained in:
1 parent
0d521bdd0d
commit
55deb7a6c6
3 files changed
+194
-51
No files matched your search
@@ -18,8 +18,8 @@
|
||||
en: 650a0b951c92626e
|
||||
zh: 8b059c0678656a79
|
||||
/deepseek-ai-dsh-client-ui-workspace/use-this-package/reordering-and-view-options:
|
||||
en: febc46e6c0bdc58d
|
||||
zh: 1e5196e31d6e8b06
|
||||
en: 8673afc1d72ad077
|
||||
zh: 9fbb83a4029c5d10
|
||||
/deepseek-ai-dsh-client-ui-workspace/use-this-package/workspace-hierarchy:
|
||||
en: a94394a5a9f70ed8
|
||||
zh: 62ce596a95395ad8
|
||||
@@ -27,14 +27,14 @@
|
||||
en: 29facc4eae028dae
|
||||
zh: 754ebac4cc294e65
|
||||
/deepseek-ai-dsh-client-ui-workspace/use-this-package/managing-sessions:
|
||||
en: 9e36b54394c1d8b6
|
||||
zh: 34d464a7eddfc58d
|
||||
en: 353ed9b66fab6486
|
||||
zh: e38a4a37627a9426
|
||||
/deepseek-ai-dsh-client-ui-workspace/use-this-package/pending-interactions:
|
||||
en: 9fb88260380bf093
|
||||
zh: 3bace2b96ecd4a82
|
||||
/deepseek-ai-dsh-client-ui-workspace/use-this-package/active-schedule-markers:
|
||||
en: 24888375e0d70b50
|
||||
zh: 692b880a1ab05d94
|
||||
en: ac15c05cb045f748
|
||||
zh: 6330172c7231b2b8
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation:
|
||||
en: 96a37e74555a36b7
|
||||
zh: d03941523509a5cf
|
||||
@@ -42,8 +42,8 @@
|
||||
en: 868904dbfb5713f8
|
||||
zh: 225906bfef7398bf
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/session-row-actions:
|
||||
en: c98df20bff0173f6
|
||||
zh: 98a26e175ab3debc
|
||||
en: 5bbba6e462faf3dc
|
||||
zh: 4734263208ee07c9
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/session-row-actions/packaged-client-plugin:
|
||||
en: 36934671b80b11ae
|
||||
zh: 8e0aea6162e35e77
|
||||
@@ -51,8 +51,8 @@
|
||||
en: ad4a333892f2fe92
|
||||
zh: 3a670fd401f4d7a7
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/view-state:
|
||||
en: faedf74d08113970
|
||||
zh: c5f383727a65a1b1
|
||||
en: b62b5a344b962c45
|
||||
zh: 6c773bece95c3621
|
||||
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/hover-cards:
|
||||
en: 8bcb7bd90d7c5773
|
||||
zh: 17663be926a099d2
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import type { ShortcutCatalogEntry } from '@deepseek-ai/dsh-client-shortcuts/client'
|
||||
import type { StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import { abbreviateHomePath } from '@deepseek-ai/dsh-util-workspace-path'
|
||||
import type { WorkspaceId } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
import type { MenuOpenState, WorkspaceBrowserProps } from '../contract/slots.ts'
|
||||
import type { GroupNode, SearchResultNode, SessionNode } from '../tree.ts'
|
||||
import css from './Rows.module.css'
|
||||
@@ -203,21 +204,8 @@ function rowHalf(e: { clientY: number; currentTarget: HTMLElement }): 'before' |
|
||||
return e.clientY < rect.top + rect.height / 2 ? 'before' : 'after'
|
||||
}
|
||||
|
||||
/**
|
||||
* Project (workspace) header row: folder + title;
|
||||
* hover reveals the chevron and create button, and dwelling on a real
|
||||
* Workspace shows its hover card (the ungrouped bucket has none).
|
||||
* `containsCurrent` arrives on the node (derivation fact, no renderer scan).
|
||||
* @param props.group - derived group node.
|
||||
* @param props.containsCurrentDescendant - highlight an ancestor even when its subtree is collapsed.
|
||||
* @param props.onToggle - expand/collapse the group.
|
||||
* @param props.onCreate - start a frontend Session inside this Workspace.
|
||||
* @param props.drag - optional workspace-row drag wiring.
|
||||
* @param props.home - host account home for POSIX hover-path abbreviation.
|
||||
* @param props.t - the browser root's locale seat.
|
||||
* @returns the row element.
|
||||
*/
|
||||
export function ProjectRowItem({ group, containsCurrentDescendant = false, onToggle, onCreate, actions, drag, home, newShortcut, t }: {
|
||||
/** Owner props of one Workspace header row; the access Hooks are the browser root's own standard seats. */
|
||||
type ProjectRowItemProps = {
|
||||
group: GroupNode
|
||||
newShortcut?: ShortcutCatalogEntry | undefined
|
||||
containsCurrentDescendant?: boolean
|
||||
@@ -230,7 +218,62 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog
|
||||
/** Host account home; POSIX home-rooted hover paths display as `~`. */
|
||||
home?: string | undefined
|
||||
t: RowTranslate
|
||||
}) {
|
||||
} & Pick<WorkspaceBrowserProps, 'useRemoteAccess' | 'useWorkspaceAccess'>
|
||||
|
||||
/** Which of one header row's actions the caller's access snapshot allows. */
|
||||
interface ProjectRowAccess {
|
||||
readonly create: boolean
|
||||
readonly rename: boolean
|
||||
readonly delete: boolean
|
||||
readonly reorder: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Project (workspace) header row: folder + title;
|
||||
* hover reveals the chevron and create button, and dwelling on a real
|
||||
* Workspace shows its hover card (the ungrouped bucket has none).
|
||||
* `containsCurrent` arrives on the node (derivation fact, no renderer scan).
|
||||
* The create button, each Workspace menu row, and the drag handle are absent
|
||||
* while the access snapshot excludes their endpoint: a real Workspace asks
|
||||
* its own resource, and the ungrouped bucket asks the global resource.
|
||||
* @param props.group - derived group node.
|
||||
* @param props.containsCurrentDescendant - highlight an ancestor even when its subtree is collapsed.
|
||||
* @param props.onToggle - expand/collapse the group.
|
||||
* @param props.onCreate - start a frontend Session inside this Workspace.
|
||||
* @param props.drag - optional workspace-row drag wiring.
|
||||
* @param props.home - host account home for POSIX hover-path abbreviation.
|
||||
* @param props.t - the browser root's locale seat.
|
||||
* @returns the row element.
|
||||
*/
|
||||
export function ProjectRowItem(props: ProjectRowItemProps) {
|
||||
const workspaceId = props.group.workspaceId
|
||||
return workspaceId === undefined
|
||||
? <UngroupedProjectRow {...props} />
|
||||
: <WorkspaceProjectRow {...props} workspaceId={workspaceId} />
|
||||
}
|
||||
|
||||
/** The ungrouped bucket's header: no Workspace resource, so starting a Session there asks the global resource. */
|
||||
function UngroupedProjectRow(props: ProjectRowItemProps) {
|
||||
const create = props.useRemoteAccess('session/create')
|
||||
return <ProjectRow {...props} access={{ create, rename: false, delete: false, reorder: false }} />
|
||||
}
|
||||
|
||||
/** A real Workspace's header: every action asks that Workspace's resource. */
|
||||
function WorkspaceProjectRow({ workspaceId, ...props }: ProjectRowItemProps & { workspaceId: WorkspaceId }) {
|
||||
const { useWorkspaceAccess } = props
|
||||
const access: ProjectRowAccess = {
|
||||
create: useWorkspaceAccess(workspaceId, 'session/create'),
|
||||
rename: useWorkspaceAccess(workspaceId, 'workspace/rename'),
|
||||
delete: useWorkspaceAccess(workspaceId, 'workspace/delete'),
|
||||
reorder: useWorkspaceAccess(workspaceId, 'workspace/insertBefore'),
|
||||
}
|
||||
return <ProjectRow {...props} access={access} />
|
||||
}
|
||||
|
||||
function ProjectRow({
|
||||
group, containsCurrentDescendant = false, onToggle, onCreate, actions, drag: offeredDrag, home, newShortcut, t, access,
|
||||
}: ProjectRowItemProps & { access: ProjectRowAccess }) {
|
||||
const drag = access.reorder ? offeredDrag : undefined
|
||||
const row = group
|
||||
// The ungrouped bucket has no workspace title: its label is dictionary copy.
|
||||
const label = row.workspaceId === undefined ? t('group.ungrouped') : row.label
|
||||
@@ -238,8 +281,8 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const dragPreview = useRowDragPreview(css.dragPreview as string)
|
||||
const workspaceMenuItems = [
|
||||
{ id: 'rename', label: t('rename'), icon: <IconEditOutlineRegular /> },
|
||||
{ id: 'delete', label: t('delete.workspace'), icon: <IconTrashOutlineRegular />, danger: true },
|
||||
...access.rename ? [{ id: 'rename', label: t('rename'), icon: <IconEditOutlineRegular /> }] : [],
|
||||
...access.delete ? [{ id: 'delete', label: t('delete.workspace'), icon: <IconTrashOutlineRegular />, danger: true }] : [],
|
||||
]
|
||||
const ownRow = (
|
||||
<div
|
||||
@@ -269,7 +312,7 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog
|
||||
<span className={css.title}>{label}</span>
|
||||
</span>
|
||||
<span className={css.rowActions}>
|
||||
{actions !== undefined && (
|
||||
{actions !== undefined && workspaceMenuItems.length > 0 && (
|
||||
<Menu
|
||||
open={menuOpen}
|
||||
onClose={() => { setMenuOpen(false) }}
|
||||
@@ -297,17 +340,19 @@ export function ProjectRowItem({ group, containsCurrentDescendant = false, onTog
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Tooltip label={t('actions.newSession')} shortcutKeys={newShortcut?.keys} side="bottom" align="end" delayMs={500} portal>
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-keyshortcuts={newShortcut?.aria}
|
||||
aria-label={t('actions.newSession.aria', { name: label })}
|
||||
onClick={(e) => { e.stopPropagation(); onCreate() }}
|
||||
>
|
||||
<IconNewChatOutlineRegular />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{access.create && (
|
||||
<Tooltip label={t('actions.newSession')} shortcutKeys={newShortcut?.keys} side="bottom" align="end" delayMs={500} portal>
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-keyshortcuts={newShortcut?.aria}
|
||||
aria-label={t('actions.newSession.aria', { name: label })}
|
||||
onClick={(e) => { e.stopPropagation(); onCreate() }}
|
||||
>
|
||||
<IconNewChatOutlineRegular />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
@@ -547,13 +592,16 @@ export function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }:
|
||||
* (`sidebar.workspaces.session.menu.item` / `sidebar.workspaces.session.row.action`),
|
||||
* its leading and trailing decorations, and its hover-card section.
|
||||
* @param props.onReveal - scroll this row into view after search navigation, then acknowledge it.
|
||||
* @param props.drag - optional row-drag target wiring; blank rows cannot start a drag.
|
||||
* @param props.drag - optional row-drag target wiring; blank rows, and rows whose access excludes
|
||||
* `workspace/insertSessionBefore`, cannot start a drag.
|
||||
* @param props.home - host account home for POSIX hover-path abbreviation.
|
||||
* @param props.useSessionAccess - the browser root's Session access Hook; the title double-click
|
||||
* renames only while it allows `session/rename`.
|
||||
* @param props.t - the browser root's locale seat.
|
||||
* @returns the session row.
|
||||
*/
|
||||
export function SessionNodeItem({
|
||||
node, currentId, now, onOpen, onRenameRequest, renderSlot, onReveal, drag, home, t,
|
||||
node, currentId, now, onOpen, onRenameRequest, renderSlot, onReveal, drag, home, useSessionAccess, t,
|
||||
}: {
|
||||
node: SessionNode
|
||||
currentId: string | undefined
|
||||
@@ -568,7 +616,7 @@ export function SessionNodeItem({
|
||||
/** Host account home; POSIX home-rooted hover paths display as `~`. */
|
||||
home?: string | undefined
|
||||
t: RowTranslate
|
||||
} & PropsRenderSlots<
|
||||
} & Pick<WorkspaceBrowserProps, 'useSessionAccess'> & PropsRenderSlots<
|
||||
| 'sidebar.workspaces.session.menu.item'
|
||||
| 'sidebar.workspaces.session.row.action'
|
||||
| 'sidebar.session.row.trailing'
|
||||
@@ -584,7 +632,11 @@ export function SessionNodeItem({
|
||||
// Archived rows hold their in-place grayed slot, so manual reorder cannot
|
||||
// move them. Pinned rows drag within the pinned block: the browser gates
|
||||
// their drop targets to fellow pinned rows.
|
||||
const draggable = drag !== undefined && !row.blank && !row.archived
|
||||
// Renaming from the title and starting a drag ask the row's own access snapshot; a row that
|
||||
// cannot start a drag still accepts drops from rows that can.
|
||||
const renamable = useSessionAccess(node.id, 'session/rename')
|
||||
const reorderable = useSessionAccess(node.id, 'workspace/insertSessionBefore')
|
||||
const draggable = drag !== undefined && reorderable && !row.blank && !row.archived
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const dragPreview = useRowDragPreview(css.dragPreview as string)
|
||||
// The menu's open state, bound into the row entries' `useMenuOpenState` hook.
|
||||
@@ -670,7 +722,7 @@ export function SessionNodeItem({
|
||||
// One shared function and options: repeated presses add one listener.
|
||||
window.addEventListener('blur', disarmRename, { once: true })
|
||||
}}
|
||||
onDoubleClick={row.blank
|
||||
onDoubleClick={row.blank || !renamable
|
||||
? undefined
|
||||
: (e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
@@ -4,7 +4,7 @@ import { act, cleanup, createEvent, fireEvent, render, screen } from '@testing-l
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { WorkspaceId } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { allowAllAccess, makeTranslate } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts'
|
||||
import { HoverCardGroup, MenuItemButton, Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
@@ -12,7 +12,7 @@ import type { MenuOpenState, SessionRowOwnerProps } from '../src/client/contract
|
||||
import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts'
|
||||
import type { RowDragProps } from '../src/client/rows/Rows.tsx'
|
||||
import {
|
||||
ProjectRowItem, SearchResultItem, SessionNodeItem as SessionNodeItemComponent,
|
||||
ProjectRowItem as ProjectRowItemComponent, SearchResultItem, SessionNodeItem as SessionNodeItemComponent,
|
||||
} from '../src/client/rows/Rows.tsx'
|
||||
import type { GroupNode, SearchResultNode, SessionNode } from '../src/client/tree.ts'
|
||||
import { en, zh } from '../src/client/locales.ts'
|
||||
@@ -34,6 +34,7 @@ type RowSlotName =
|
||||
| 'sidebar.session.row.hover'
|
||||
type RowRenderSlot = PropsRenderSlots<RowSlotName>['renderSlot']
|
||||
type SessionNodeItemProps = ComponentProps<typeof SessionNodeItemComponent>
|
||||
type ProjectRowItemProps = ComponentProps<typeof ProjectRowItemComponent>
|
||||
|
||||
const renderNoRowEntries: RowRenderSlot = () => null
|
||||
|
||||
@@ -48,10 +49,24 @@ function realDoubleClick(el: Element): void {
|
||||
fireEvent.doubleClick(el)
|
||||
}
|
||||
|
||||
function SessionNodeItem({ renderSlot = renderNoRowEntries, onRenameRequest = () => {}, ...props }: Omit<
|
||||
SessionNodeItemProps, 'renderSlot' | 'onRenameRequest'
|
||||
> & Partial<Pick<SessionNodeItemProps, 'renderSlot' | 'onRenameRequest'>>) {
|
||||
return <SessionNodeItemComponent {...props} renderSlot={renderSlot} onRenameRequest={onRenameRequest} />
|
||||
function SessionNodeItem({
|
||||
renderSlot = renderNoRowEntries, onRenameRequest = () => {}, useSessionAccess = allowAllAccess.useSessionAccess, ...props
|
||||
}: Omit<
|
||||
SessionNodeItemProps, 'renderSlot' | 'onRenameRequest' | 'useSessionAccess'
|
||||
> & Partial<Pick<SessionNodeItemProps, 'renderSlot' | 'onRenameRequest' | 'useSessionAccess'>>) {
|
||||
return (
|
||||
<SessionNodeItemComponent
|
||||
{...props} renderSlot={renderSlot} onRenameRequest={onRenameRequest} useSessionAccess={useSessionAccess}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Direct header specs hold every access answer unless the case refuses one.
|
||||
function ProjectRowItem({
|
||||
useRemoteAccess = allowAllAccess.useRemoteAccess, useWorkspaceAccess = allowAllAccess.useWorkspaceAccess, ...props
|
||||
}: Omit<ProjectRowItemProps, 'useRemoteAccess' | 'useWorkspaceAccess'>
|
||||
& Partial<Pick<ProjectRowItemProps, 'useRemoteAccess' | 'useWorkspaceAccess'>>) {
|
||||
return <ProjectRowItemComponent {...props} useRemoteAccess={useRemoteAccess} useWorkspaceAccess={useWorkspaceAccess} />
|
||||
}
|
||||
|
||||
/** Half detection reads the row rect; jsdom rects are all-zero by default. */
|
||||
@@ -617,6 +632,82 @@ describe('workspace browser rows', () => {
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
})
|
||||
|
||||
it('workspace row offers only the actions its Workspace access snapshot allows', () => {
|
||||
const group: GroupNode = {
|
||||
key: 'project', workspaceId: wid('project'), cwd: '/projects/project', createdAt: 0, label: 'Project',
|
||||
sessionCount: 0, expanded: false, containsCurrent: false, sessions: [],
|
||||
}
|
||||
const allowed = new Set<string>(['workspace/rename'])
|
||||
const useWorkspaceAccess = vi.fn((_workspaceId: WorkspaceId, endpoint: string) => allowed.has(endpoint))
|
||||
const drag = { start: vi.fn(), end: vi.fn() }
|
||||
const props = {
|
||||
group, onToggle: vi.fn(), onCreate: vi.fn(), actions: { rename: vi.fn(), delete: vi.fn() }, drag, t,
|
||||
}
|
||||
const view = render(<ProjectRowItem {...props} useWorkspaceAccess={useWorkspaceAccess} />)
|
||||
expect(useWorkspaceAccess).toHaveBeenCalledWith(wid('project'), 'session/create')
|
||||
// No New Session button, no drag handle, and a menu holding only the allowed rename row.
|
||||
expect(screen.queryByRole('button', { name: '在“Project”中新建会话' })).toBeNull()
|
||||
expect(screen.getByRole('treeitem').getAttribute('draggable')).toBe('false')
|
||||
fireEvent.click(screen.getByRole('button', { name: '工作区“Project”的操作' }))
|
||||
expect(screen.getByRole('menuitem', { name: '重命名' })).toBeTruthy()
|
||||
expect(screen.queryByRole('menuitem', { name: '删除工作区' })).toBeNull()
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
|
||||
// Without rename either, the header carries no menu at all.
|
||||
allowed.clear()
|
||||
view.rerender(<ProjectRowItem {...props} useWorkspaceAccess={useWorkspaceAccess} />)
|
||||
expect(screen.queryByRole('button', { name: '工作区“Project”的操作' })).toBeNull()
|
||||
|
||||
// Every answer allowed restores the button, the full menu, and the drag handle.
|
||||
for (const endpoint of ['session/create', 'workspace/rename', 'workspace/delete', 'workspace/insertBefore']) {
|
||||
allowed.add(endpoint)
|
||||
}
|
||||
view.rerender(<ProjectRowItem {...props} useWorkspaceAccess={useWorkspaceAccess} />)
|
||||
expect(screen.getByRole('treeitem').getAttribute('draggable')).toBe('true')
|
||||
fireEvent.click(screen.getByRole('button', { name: '在“Project”中新建会话' }))
|
||||
expect(props.onCreate).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('ungrouped bucket asks the global resource whether it may start a Session', () => {
|
||||
const group: GroupNode = {
|
||||
key: 'ungrouped', workspaceId: undefined, cwd: undefined, createdAt: undefined, label: '',
|
||||
sessionCount: 0, expanded: false, containsCurrent: false, sessions: [],
|
||||
}
|
||||
const useRemoteAccess = vi.fn((action: string) => action !== 'session/create')
|
||||
const useWorkspaceAccess = vi.fn(() => true)
|
||||
render(<ProjectRowItem group={group} onToggle={vi.fn()} onCreate={vi.fn()} t={t}
|
||||
useRemoteAccess={useRemoteAccess} useWorkspaceAccess={useWorkspaceAccess} />)
|
||||
expect(useRemoteAccess).toHaveBeenCalledWith('session/create')
|
||||
expect(useWorkspaceAccess).not.toHaveBeenCalled()
|
||||
expect(screen.queryByRole('button', { name: /新建会话/ })).toBeNull()
|
||||
})
|
||||
|
||||
it('session row drags and renames from its title only while its access snapshot allows each', () => {
|
||||
const node: SessionNode = {
|
||||
id: sid('s1'), title: 'One', blank: false, running: false,
|
||||
runningSubagentCount: 0, completed: false, updatedAt: 0, pinned: false, archived: false,
|
||||
}
|
||||
const drag: RowDragProps = {
|
||||
active: false, marker: null, start: vi.fn(), end: vi.fn(), hover: vi.fn(), drop: vi.fn(),
|
||||
}
|
||||
const onRenameRequest = vi.fn()
|
||||
const useSessionAccess = vi.fn((_sessionId: SessionId, _endpoint: string) => false)
|
||||
const view = render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()} drag={drag}
|
||||
onRenameRequest={onRenameRequest} useSessionAccess={useSessionAccess} t={t} />)
|
||||
expect(useSessionAccess).toHaveBeenCalledWith(sid('s1'), 'session/rename')
|
||||
expect(useSessionAccess).toHaveBeenCalledWith(sid('s1'), 'workspace/insertSessionBefore')
|
||||
expect(screen.getByRole('treeitem').getAttribute('draggable')).toBe('false')
|
||||
fireEvent.doubleClick(screen.getByText('One'))
|
||||
expect(onRenameRequest).not.toHaveBeenCalled()
|
||||
|
||||
view.rerender(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()} drag={drag}
|
||||
onRenameRequest={onRenameRequest} useSessionAccess={() => true} t={t} />)
|
||||
expect(screen.getByRole('treeitem').getAttribute('draggable')).toBe('true')
|
||||
// A browser rename arms on the first press and fires on the double-click; the helper states both.
|
||||
realDoubleClick(screen.getByText('One'))
|
||||
expect(onRenameRequest).toHaveBeenCalledWith(sid('s1'), 'One')
|
||||
})
|
||||
|
||||
it('workspace hover card shows its details and copies the full directory path', async () => {
|
||||
vi.useFakeTimers()
|
||||
const writeText = vi.fn(async () => {})
|
||||
|
||||
Reference in new issue
Block a user