feat(web): workspace header hover card with cwd and creation time
This commit is contained in:
@@ -192,6 +192,13 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hoverPath {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #CFD3D6;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.hoverTime {
|
.hoverTime {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
* Workspace browser tree row components (figma Cell set 14:3080): pure presentational —
|
* Workspace browser tree row components (figma Cell set 14:3080): pure presentational —
|
||||||
* all data and callbacks arrive via props. Hover swaps (folder->chevron,
|
* all data and callbacks arrive via props. Hover swaps (folder->chevron,
|
||||||
* time->ellipsis, action buttons) are CSS-only. Row ... menus are visual-only
|
* time->ellipsis, action buttons) are CSS-only. Row ... menus are visual-only
|
||||||
* except workspace Rename/Delete and session Rename; the session hover card is
|
* except workspace Rename/Delete and session Rename; the session and workspace
|
||||||
* suppressed while a menu is open.
|
* hover cards are suppressed while a menu is open.
|
||||||
*/
|
*/
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
@@ -30,10 +30,26 @@ const WORKSPACE_MENU_ITEMS = [
|
|||||||
{ id: 'delete', label: 'Delete workspace', icon: <IconTrashOutline16 />, danger: true },
|
{ id: 'delete', label: 'Delete workspace', icon: <IconTrashOutline16 />, danger: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
/** Hover-card body: workspace title, full directory path, absolute creation time. */
|
||||||
|
function WorkspaceHoverContent({ label, cwd, createdAt }: {
|
||||||
|
label: string
|
||||||
|
cwd: string | undefined
|
||||||
|
createdAt: number
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={css.hoverContent}>
|
||||||
|
<div className={css.hoverTitle}>{label}</div>
|
||||||
|
<div className={css.hoverPath}>{cwd}</div>
|
||||||
|
<div className={css.hoverTime}>{`Created ${new Date(createdAt).toLocaleString()}`}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project (workspace) header row: 54px, folder + title + session count;
|
* Project (workspace) header row: 54px, folder + title + session count;
|
||||||
* hover reveals the chevron and create button. `containsCurrent` arrives on
|
* hover reveals the chevron and create button, and dwelling on a real
|
||||||
* the node (derivation fact, no renderer scan).
|
* 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.group - derived group node.
|
||||||
* @param props.onToggle - expand/collapse the group.
|
* @param props.onToggle - expand/collapse the group.
|
||||||
* @param props.onCreate - start a frontend Session inside this Workspace.
|
* @param props.onCreate - start a frontend Session inside this Workspace.
|
||||||
@@ -50,7 +66,7 @@ export function ProjectRowItem({ group, onToggle, onCreate, actions }: {
|
|||||||
const active = group.expanded && group.containsCurrent
|
const active = group.expanded && group.containsCurrent
|
||||||
const count = `${row.sessionCount} ${row.sessionCount === 1 ? 'session' : 'sessions'}`
|
const count = `${row.sessionCount} ${row.sessionCount === 1 ? 'session' : 'sessions'}`
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
return (
|
const ownRow = (
|
||||||
<div
|
<div
|
||||||
className={clsx(css.projectRow, menuOpen && css.menuOpen)}
|
className={clsx(css.projectRow, menuOpen && css.menuOpen)}
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
@@ -107,6 +123,15 @@ export function ProjectRowItem({ group, onToggle, onCreate, actions }: {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
// The ungrouped bucket has no backing Workspace: no card to show.
|
||||||
|
if (row.createdAt === undefined) return ownRow
|
||||||
|
return (
|
||||||
|
<HoverCard
|
||||||
|
anchor={ownRow}
|
||||||
|
content={<WorkspaceHoverContent label={row.label} cwd={row.cwd} createdAt={row.createdAt} />}
|
||||||
|
disabled={menuOpen}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export interface GroupNode {
|
|||||||
/** Backing Workspace id; absent only for the ungrouped bucket. */
|
/** Backing Workspace id; absent only for the ungrouped bucket. */
|
||||||
workspaceId: WorkspaceId | undefined
|
workspaceId: WorkspaceId | undefined
|
||||||
cwd: string | undefined
|
cwd: string | undefined
|
||||||
|
/** Workspace creation time (epoch ms); absent only for the ungrouped bucket. */
|
||||||
|
createdAt: number | undefined
|
||||||
label: string
|
label: string
|
||||||
/** Total visible sessions in the group. */
|
/** Total visible sessions in the group. */
|
||||||
sessionCount: number
|
sessionCount: number
|
||||||
@@ -52,6 +54,7 @@ interface Group {
|
|||||||
key: string
|
key: string
|
||||||
workspaceId: WorkspaceId | undefined
|
workspaceId: WorkspaceId | undefined
|
||||||
cwd: string | undefined
|
cwd: string | undefined
|
||||||
|
createdAt: number | undefined
|
||||||
label: string
|
label: string
|
||||||
summaries: Map<SessionId, SessionSummary>
|
summaries: Map<SessionId, SessionSummary>
|
||||||
roots: SessionId[]
|
roots: SessionId[]
|
||||||
@@ -91,6 +94,7 @@ function buildGroup(
|
|||||||
key: string,
|
key: string,
|
||||||
workspaceId: WorkspaceId | undefined,
|
workspaceId: WorkspaceId | undefined,
|
||||||
cwd: string | undefined,
|
cwd: string | undefined,
|
||||||
|
createdAt: number | undefined,
|
||||||
label: string,
|
label: string,
|
||||||
members: readonly SessionSummary[],
|
members: readonly SessionSummary[],
|
||||||
order: 'account' | 'recency',
|
order: 'account' | 'recency',
|
||||||
@@ -142,7 +146,7 @@ function buildGroup(
|
|||||||
for (const m of members) {
|
for (const m of members) {
|
||||||
if (!reachable.has(m.id)) rootIds.push(m.id)
|
if (!reachable.has(m.id)) rootIds.push(m.id)
|
||||||
}
|
}
|
||||||
return { key, workspaceId, cwd, label, summaries, roots: rootIds, children }
|
return { key, workspaceId, cwd, createdAt, label, summaries, roots: rootIds, children }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,7 +167,8 @@ function groupByWorkspace(list: SessionListState, workspaces: readonly Workspace
|
|||||||
members.push(summary)
|
members.push(summary)
|
||||||
}
|
}
|
||||||
groups.push(buildGroup(
|
groups.push(buildGroup(
|
||||||
workspace.workspaceId, workspace.workspaceId, workspace.path, workspace.title, members, 'account',
|
workspace.workspaceId, workspace.workspaceId, workspace.path,
|
||||||
|
Date.parse(workspace.createdAt), workspace.title, members, 'account',
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
const stray = list.ids
|
const stray = list.ids
|
||||||
@@ -171,7 +176,7 @@ function groupByWorkspace(list: SessionListState, workspaces: readonly Workspace
|
|||||||
.filter((s): s is SessionSummary =>
|
.filter((s): s is SessionSummary =>
|
||||||
s !== undefined && !accounted.has(s.id) && sessionVisible(s, list.current))
|
s !== undefined && !accounted.has(s.id) && sessionVisible(s, list.current))
|
||||||
if (stray.length > 0) {
|
if (stray.length > 0) {
|
||||||
groups.push(buildGroup(UNGROUPED_KEY, undefined, undefined, UNGROUPED_LABEL, stray, 'recency'))
|
groups.push(buildGroup(UNGROUPED_KEY, undefined, undefined, undefined, UNGROUPED_LABEL, stray, 'recency'))
|
||||||
}
|
}
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
@@ -267,6 +272,7 @@ export function deriveGroups(
|
|||||||
key: g.key,
|
key: g.key,
|
||||||
workspaceId: g.workspaceId,
|
workspaceId: g.workspaceId,
|
||||||
cwd: g.cwd,
|
cwd: g.cwd,
|
||||||
|
createdAt: g.createdAt,
|
||||||
label: g.label,
|
label: g.label,
|
||||||
sessionCount: g.summaries.size,
|
sessionCount: g.summaries.size,
|
||||||
expanded,
|
expanded,
|
||||||
@@ -280,6 +286,7 @@ export function deriveGroups(
|
|||||||
key: g.key,
|
key: g.key,
|
||||||
workspaceId: g.workspaceId,
|
workspaceId: g.workspaceId,
|
||||||
cwd: g.cwd,
|
cwd: g.cwd,
|
||||||
|
createdAt: g.createdAt,
|
||||||
label: g.label,
|
label: g.label,
|
||||||
sessionCount: g.summaries.size,
|
sessionCount: g.summaries.size,
|
||||||
expanded: visible.size > 0,
|
expanded: visible.size > 0,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ describe('workspace browser rows', () => {
|
|||||||
const onToggle = vi.fn()
|
const onToggle = vi.fn()
|
||||||
const onCreate = vi.fn()
|
const onCreate = vi.fn()
|
||||||
const group: GroupNode = {
|
const group: GroupNode = {
|
||||||
key: 'project', workspaceId: wid('project'), cwd: '/projects/project', label: 'Project',
|
key: 'project', workspaceId: wid('project'), cwd: '/projects/project', createdAt: 0, label: 'Project',
|
||||||
sessionCount: 1, expanded: true, containsCurrent: true, sessions: [],
|
sessionCount: 1, expanded: true, containsCurrent: true, sessions: [],
|
||||||
}
|
}
|
||||||
render(<ProjectRowItem group={group} onToggle={onToggle} onCreate={onCreate} />)
|
render(<ProjectRowItem group={group} onToggle={onToggle} onCreate={onCreate} />)
|
||||||
@@ -103,7 +103,7 @@ describe('workspace browser rows', () => {
|
|||||||
const onDelete = vi.fn()
|
const onDelete = vi.fn()
|
||||||
const onToggle = vi.fn()
|
const onToggle = vi.fn()
|
||||||
const group: GroupNode = {
|
const group: GroupNode = {
|
||||||
key: 'project', workspaceId: wid('project'), cwd: '/projects/project', label: 'Project',
|
key: 'project', workspaceId: wid('project'), cwd: '/projects/project', createdAt: 0, label: 'Project',
|
||||||
sessionCount: 0, expanded: false, containsCurrent: false, sessions: [],
|
sessionCount: 0, expanded: false, containsCurrent: false, sessions: [],
|
||||||
}
|
}
|
||||||
render(<ProjectRowItem
|
render(<ProjectRowItem
|
||||||
@@ -130,7 +130,7 @@ describe('workspace browser rows', () => {
|
|||||||
|
|
||||||
it('ungrouped bucket renders no workspace menu', () => {
|
it('ungrouped bucket renders no workspace menu', () => {
|
||||||
const group: GroupNode = {
|
const group: GroupNode = {
|
||||||
key: '', workspaceId: undefined, cwd: undefined, label: 'Ungrouped',
|
key: '', workspaceId: undefined, cwd: undefined, createdAt: undefined, label: 'Ungrouped',
|
||||||
sessionCount: 0, expanded: false, containsCurrent: false, sessions: [],
|
sessionCount: 0, expanded: false, containsCurrent: false, sessions: [],
|
||||||
}
|
}
|
||||||
render(<ProjectRowItem group={group} onToggle={vi.fn()} onCreate={vi.fn()} />)
|
render(<ProjectRowItem group={group} onToggle={vi.fn()} onCreate={vi.fn()} />)
|
||||||
|
|||||||
Reference in New Issue
Block a user