feat(ui-workspace): reorder ungrouped sessions
This commit is contained in:
@@ -130,7 +130,8 @@ function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }: {
|
||||
|
||||
/** In-flight root-row drag: source identity plus the current insert marker. */
|
||||
interface DragState {
|
||||
workspaceId: WorkspaceId
|
||||
/** Workspace id, or {@link UNGROUPED_KEY} for the browser-local loose-session account. */
|
||||
workspaceKey: string
|
||||
sessionId: SessionNode['id']
|
||||
/** Row the marker sits on and which half (insert above/below it). */
|
||||
over: { id: SessionNode['id']; half: 'before' | 'after' } | null
|
||||
@@ -227,13 +228,22 @@ function SessionTree({
|
||||
() => Object.entries(workspaceExpansion).filter(([, expanded]) => expanded).map(([key]) => key),
|
||||
[workspaceExpansion],
|
||||
)
|
||||
const ungroupedSessionIds = useMemo(() => {
|
||||
const accounted = new Set(workspaces.flatMap(workspace => workspace.sessionIds))
|
||||
return list.ids.filter(id => list.byId[id] !== undefined && !accounted.has(id))
|
||||
}, [list, workspaces])
|
||||
useEffect(() => {
|
||||
if (list.phase !== 'ready') return
|
||||
const switchedToUpdated = previousOrderBy.current !== 'updated' && orderBy === 'updated'
|
||||
previousOrderBy.current = orderBy
|
||||
for (const workspace of workspaces) {
|
||||
const key = workspace.workspaceId as string
|
||||
const sessionIds = workspace.sessionIds.filter(id => list.byId[id] !== undefined)
|
||||
const accounts = [
|
||||
...workspaces.map(workspace => ({
|
||||
key: workspace.workspaceId as string,
|
||||
sessionIds: workspace.sessionIds.filter(id => list.byId[id] !== undefined),
|
||||
})),
|
||||
{ key: UNGROUPED_KEY, sessionIds: ungroupedSessionIds },
|
||||
]
|
||||
for (const { key, sessionIds } of accounts) {
|
||||
const previousOrder = recentSessionOrder[key]
|
||||
const previousUpdatedAt = recentSessionUpdatedAt[key] ?? {}
|
||||
let nextOrder = reconciledSessionOrder(sessionIds, previousOrder)
|
||||
@@ -266,7 +276,7 @@ function SessionTree({
|
||||
syncRecentSessions(key, nextOrder.map(id => id as string), nextUpdatedAt)
|
||||
}
|
||||
}
|
||||
}, [list, orderBy, recentSessionOrder, recentSessionUpdatedAt, syncRecentSessions, workspaces])
|
||||
}, [list, orderBy, recentSessionOrder, recentSessionUpdatedAt, syncRecentSessions, ungroupedSessionIds, workspaces])
|
||||
const orderedWorkspaces = useMemo(() => {
|
||||
return workspaces.map((workspace) => {
|
||||
const stored = recentSessionOrder[workspace.workspaceId as string]
|
||||
@@ -274,16 +284,25 @@ function SessionTree({
|
||||
return { ...workspace, sessionIds }
|
||||
})
|
||||
}, [recentSessionOrder, workspaces])
|
||||
const orderedUngroupedSessionIds = useMemo(
|
||||
() => reconciledSessionOrder(ungroupedSessionIds, recentSessionOrder[UNGROUPED_KEY]),
|
||||
[recentSessionOrder, ungroupedSessionIds],
|
||||
)
|
||||
const groups = useMemo(
|
||||
() => deriveGroups(list, orderedWorkspaces, archivedSessionIds, { expandedProjects }),
|
||||
[list, orderedWorkspaces, archivedSessionIds, expandedProjects],
|
||||
() => deriveGroups(list, orderedWorkspaces, archivedSessionIds, {
|
||||
expandedProjects,
|
||||
...(recentSessionOrder[UNGROUPED_KEY] === undefined
|
||||
? {}
|
||||
: { ungroupedOrder: recentSessionOrder[UNGROUPED_KEY] }),
|
||||
}),
|
||||
[list, orderedWorkspaces, archivedSessionIds, expandedProjects, recentSessionOrder],
|
||||
)
|
||||
const now = Date.now()
|
||||
const commitSessionDrag = (activeDrag: DragState, over: NonNullable<DragState['over']>): void => {
|
||||
if (sessionDropCommitted.current) return
|
||||
sessionDropCommitted.current = true
|
||||
setDrag(null)
|
||||
const group = groups.find(candidate => candidate.workspaceId === activeDrag.workspaceId)
|
||||
const group = groups.find(candidate => candidate.key === activeDrag.workspaceKey)
|
||||
if (group === undefined) return
|
||||
const targetIndex = group.sessions.findIndex(session => session.id === over.id)
|
||||
if (targetIndex === -1) return
|
||||
@@ -294,14 +313,16 @@ function SessionTree({
|
||||
? group.sessions.length
|
||||
: group.sessions.findIndex(session => session.id === anchor)
|
||||
if (sourceIndex !== -1 && (anchorIndex === sourceIndex || anchorIndex === sourceIndex + 1)) return
|
||||
const account = orderedWorkspaces.find(workspace => workspace.workspaceId === activeDrag.workspaceId)
|
||||
if (account === undefined) return
|
||||
const nextOrder = account.sessionIds.filter(id => id !== activeDrag.sessionId)
|
||||
const accountSessionIds = activeDrag.workspaceKey === UNGROUPED_KEY
|
||||
? orderedUngroupedSessionIds
|
||||
: orderedWorkspaces.find(workspace => workspace.workspaceId === activeDrag.workspaceKey)?.sessionIds
|
||||
if (accountSessionIds === undefined) return
|
||||
const nextOrder = accountSessionIds.filter(id => id !== activeDrag.sessionId)
|
||||
const insertAt = anchor === undefined ? nextOrder.length : nextOrder.indexOf(anchor)
|
||||
nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId)
|
||||
setRecentSessionOrder(activeDrag.workspaceId, nextOrder.map(id => id as string))
|
||||
if (orderBy === 'updated') return
|
||||
insertSessionBefore(activeDrag.workspaceId, activeDrag.sessionId, anchor).catch((reason: unknown) => {
|
||||
setRecentSessionOrder(activeDrag.workspaceKey, nextOrder.map(id => id as string))
|
||||
if (orderBy === 'updated' || activeDrag.workspaceKey === UNGROUPED_KEY) return
|
||||
insertSessionBefore(activeDrag.workspaceKey as WorkspaceId, activeDrag.sessionId, anchor).catch((reason: unknown) => {
|
||||
console.warn('session reorder rejected:', reason)
|
||||
})
|
||||
}
|
||||
@@ -427,15 +448,13 @@ function SessionTree({
|
||||
? group.sessions
|
||||
: group.sessions.slice(0, COLLAPSED_SESSION_LIMIT)
|
||||
).map((node) => {
|
||||
// Draggable: real-workspace session rows. The drag
|
||||
// never leaves its group — rows of other groups show no markers
|
||||
// and reject drops (visual movement confined to this section).
|
||||
const draggable = group.workspaceId !== undefined
|
||||
const sameGroupDrag = drag !== null && drag.workspaceId === group.workspaceId
|
||||
const dragProps = !draggable || group.workspaceId === undefined ? undefined : {
|
||||
// Session drag never leaves its group. Ungrouped writes only the
|
||||
// browser-local account; real Workspaces may also write Host order.
|
||||
const sameGroupDrag = drag !== null && drag.workspaceKey === group.key
|
||||
const dragProps = {
|
||||
start: () => {
|
||||
sessionDropCommitted.current = false
|
||||
setDrag({ workspaceId: group.workspaceId as WorkspaceId, sessionId: node.id, over: null })
|
||||
setDrag({ workspaceKey: group.key, sessionId: node.id, over: null })
|
||||
},
|
||||
active: sameGroupDrag,
|
||||
marker: sameGroupDrag && drag.over?.id === node.id ? drag.over.half : null,
|
||||
|
||||
@@ -78,6 +78,8 @@ export interface SearchResultSet {
|
||||
/** Viewing state consumed by the derivation. */
|
||||
export interface TreeView {
|
||||
expandedProjects: readonly string[]
|
||||
/** Browser-local order for Sessions without a backing Workspace account. */
|
||||
ungroupedOrder?: readonly string[]
|
||||
}
|
||||
|
||||
interface Group {
|
||||
@@ -139,21 +141,41 @@ function buildGroup(
|
||||
order: 'account' | 'recency',
|
||||
): Group {
|
||||
const sessions = [...members]
|
||||
// Workspace order is the caller-selected sessionIds; only Ungrouped lacks
|
||||
// an account order and therefore falls back to recency.
|
||||
// Real Workspace order comes from sessionIds. Ungrouped falls back to
|
||||
// recency until the browser supplies its persisted local order.
|
||||
if (order === 'recency') sessions.sort(byRecency)
|
||||
return { key, workspaceId, cwd, createdAt, label, sessions }
|
||||
}
|
||||
|
||||
/** Apply a stored Ungrouped order and append newly loose Sessions by recency. */
|
||||
function orderedUngrouped(members: readonly SessionSummary[], stored: readonly string[]): SessionSummary[] {
|
||||
const byId = new Map(members.map(session => [session.id as string, session]))
|
||||
const included = new Set<string>()
|
||||
const ordered: SessionSummary[] = []
|
||||
for (const key of stored) {
|
||||
const session = byId.get(key)
|
||||
if (session === undefined || included.has(key)) continue
|
||||
ordered.push(session)
|
||||
included.add(key)
|
||||
}
|
||||
for (const session of [...members].sort(byRecency)) {
|
||||
if (included.has(session.id)) continue
|
||||
ordered.push(session)
|
||||
}
|
||||
return ordered
|
||||
}
|
||||
|
||||
/**
|
||||
* Group Sessions by Host Workspace: one group per entity in stable Host
|
||||
* order, with members resolved from sessionIds in their stored order. Sessions
|
||||
* outside every Workspace trail in the recency-ordered Ungrouped bucket.
|
||||
* outside every Workspace trail in the browser-local Ungrouped order, which
|
||||
* falls back to recency before that order is initialized.
|
||||
*/
|
||||
function groupByWorkspace(
|
||||
list: SessionListState,
|
||||
workspaces: readonly WorkspaceView[],
|
||||
archived: ReadonlySet<SessionId>,
|
||||
ungroupedOrder: readonly string[] | undefined,
|
||||
): Group[] {
|
||||
const groups: Group[] = []
|
||||
const accounted = new Set<SessionId>()
|
||||
@@ -176,7 +198,15 @@ function groupByWorkspace(
|
||||
.filter((s): s is SessionSummary =>
|
||||
s !== undefined && !accounted.has(s.id) && sessionVisible(s, list.current, archived))
|
||||
if (stray.length > 0) {
|
||||
groups.push(buildGroup(UNGROUPED_KEY, undefined, undefined, undefined, UNGROUPED_LABEL, stray, 'recency'))
|
||||
groups.push(buildGroup(
|
||||
UNGROUPED_KEY,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
UNGROUPED_LABEL,
|
||||
ungroupedOrder === undefined ? stray : orderedUngrouped(stray, ungroupedOrder),
|
||||
ungroupedOrder === undefined ? 'recency' : 'account',
|
||||
))
|
||||
}
|
||||
return groups
|
||||
}
|
||||
@@ -225,7 +255,7 @@ export function deriveGroups(
|
||||
: (workspaces.find(w => w.sessionIds.includes(list.current as SessionId))?.workspaceId as string | undefined)
|
||||
?? UNGROUPED_KEY
|
||||
const groups: GroupNode[] = []
|
||||
for (const g of groupByWorkspace(list, workspaces, archived)) {
|
||||
for (const g of groupByWorkspace(list, workspaces, archived, view.ungroupedOrder)) {
|
||||
const expanded = expandedProjects.has(g.key)
|
||||
groups.push({
|
||||
key: g.key,
|
||||
|
||||
Reference in New Issue
Block a user