Add workspace board column layout setting (#3822)
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
parent
a931fde9e3
commit
aed7703d30
|
|
@ -96,6 +96,7 @@ import {
|
|||
DEFAULT_WORKSPACE_STATUS_ID,
|
||||
clampWorkspaceBoardColumnWidth,
|
||||
clampWorkspaceBoardOpacity,
|
||||
normalizeWorkspaceBoardColumnLayout,
|
||||
normalizePersistedWorkspaceStatuses,
|
||||
normalizeWorkspaceStatuses
|
||||
} from '../shared/workspace-statuses'
|
||||
|
|
@ -2825,6 +2826,9 @@ export class Store {
|
|||
),
|
||||
workspaceStatuses: normalizeWorkspaceStatuses(this.state.ui?.workspaceStatuses),
|
||||
workspaceBoardOpacity: clampWorkspaceBoardOpacity(this.state.ui?.workspaceBoardOpacity),
|
||||
workspaceBoardColumnLayout: normalizeWorkspaceBoardColumnLayout(
|
||||
this.state.ui?.workspaceBoardColumnLayout
|
||||
),
|
||||
workspaceBoardColumnWidth: clampWorkspaceBoardColumnWidth(
|
||||
this.state.ui?.workspaceBoardColumnWidth
|
||||
),
|
||||
|
|
@ -2862,6 +2866,9 @@ export class Store {
|
|||
workspaceBoardOpacity: clampWorkspaceBoardOpacity(
|
||||
updates.workspaceBoardOpacity ?? this.state.ui?.workspaceBoardOpacity
|
||||
),
|
||||
workspaceBoardColumnLayout: normalizeWorkspaceBoardColumnLayout(
|
||||
updates.workspaceBoardColumnLayout ?? this.state.ui?.workspaceBoardColumnLayout
|
||||
),
|
||||
workspaceBoardColumnWidth: clampWorkspaceBoardColumnWidth(
|
||||
updates.workspaceBoardColumnWidth ?? this.state.ui?.workspaceBoardColumnWidth
|
||||
),
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ const UiUpdate = z
|
|||
agentActivityDisplayMode: AgentActivityDisplayMode.optional(),
|
||||
workspaceStatuses: z.array(WorkspaceStatusDefinition).optional(),
|
||||
workspaceBoardOpacity: z.number().finite().optional(),
|
||||
workspaceBoardColumnLayout: z.enum(['full', 'fit']).optional(),
|
||||
workspaceBoardColumnWidth: z.number().finite().optional(),
|
||||
_workspaceStatusesDefaultOrderMigrated: z.boolean().optional(),
|
||||
_workspaceStatusesDefaultWorkflowMigrated: z.boolean().optional(),
|
||||
|
|
|
|||
|
|
@ -31,7 +31,11 @@ import {
|
|||
type WorktreeDragGroup
|
||||
} from './worktree-manual-order'
|
||||
import type { WorkspaceStatus, WorktreeMeta } from '../../../../shared/types'
|
||||
import { makeWorkspaceStatusId } from '../../../../shared/workspace-statuses'
|
||||
import {
|
||||
WORKSPACE_BOARD_COLUMN_GAP,
|
||||
fitWorkspaceBoardColumnWidth,
|
||||
makeWorkspaceStatusId
|
||||
} from '../../../../shared/workspace-statuses'
|
||||
|
||||
type WorkspaceKanbanDrawerProps = {
|
||||
open: boolean
|
||||
|
|
@ -53,6 +57,8 @@ export default function WorkspaceKanbanDrawer({
|
|||
const updateWorktreesMeta = useAppStore((s) => s.updateWorktreesMeta)
|
||||
const workspaceStatuses = useAppStore((s) => s.workspaceStatuses)
|
||||
const setWorkspaceStatuses = useAppStore((s) => s.setWorkspaceStatuses)
|
||||
const workspaceBoardColumnLayout = useAppStore((s) => s.workspaceBoardColumnLayout)
|
||||
const setWorkspaceBoardColumnLayout = useAppStore((s) => s.setWorkspaceBoardColumnLayout)
|
||||
const workspaceBoardColumnWidth = useAppStore((s) => s.workspaceBoardColumnWidth)
|
||||
const setWorkspaceBoardColumnWidth = useAppStore((s) => s.setWorkspaceBoardColumnWidth)
|
||||
const sortBy = useAppStore((s) => s.sortBy)
|
||||
|
|
@ -64,6 +70,7 @@ export default function WorkspaceKanbanDrawer({
|
|||
const areaSelectionOverlayRef = useRef<HTMLDivElement>(null)
|
||||
const [dragOverStatus, setDragOverStatus] = useState<WorkspaceStatus | null>(null)
|
||||
const [pinDragOver, setPinDragOver] = useState(false)
|
||||
const [laneScrollerWidth, setLaneScrollerWidth] = useState(0)
|
||||
const { canCreateWorktree, createWorktreeForStatus } = useWorkspaceKanbanCreateWorktree()
|
||||
const visibleWorktreeIdSet = useVisibleWorkspaceKanbanWorktreeIds({
|
||||
allWorktrees,
|
||||
|
|
@ -112,6 +119,33 @@ export default function WorkspaceKanbanDrawer({
|
|||
})
|
||||
const { columnWidth, isResizingColumn, onColumnResizeStart, onColumnResizeKeyDown } =
|
||||
useWorkspaceKanbanColumnResize(workspaceBoardColumnWidth, setWorkspaceBoardColumnWidth)
|
||||
const laneScrollerResizeRef = useRef<ResizeObserver | null>(null)
|
||||
const attachLaneScroller = useCallback((node: HTMLDivElement | null) => {
|
||||
laneScrollerRef.current = node
|
||||
laneScrollerResizeRef.current?.disconnect()
|
||||
laneScrollerResizeRef.current = null
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
setLaneScrollerWidth(node.clientWidth)
|
||||
if (typeof ResizeObserver === 'undefined') {
|
||||
return
|
||||
}
|
||||
const observer = new ResizeObserver(() => setLaneScrollerWidth(node.clientWidth))
|
||||
observer.observe(node)
|
||||
laneScrollerResizeRef.current = observer
|
||||
}, [])
|
||||
const renderColumnWidth = useMemo(
|
||||
() =>
|
||||
workspaceBoardColumnLayout === 'fit'
|
||||
? fitWorkspaceBoardColumnWidth({
|
||||
containerWidth: laneScrollerWidth,
|
||||
columnCount: workspaceStatuses.length,
|
||||
capWidth: columnWidth
|
||||
})
|
||||
: columnWidth,
|
||||
[columnWidth, laneScrollerWidth, workspaceBoardColumnLayout, workspaceStatuses.length]
|
||||
)
|
||||
const moveWorktreeToStatus = useCallback(
|
||||
(worktreeId: string, status: WorkspaceStatus) => {
|
||||
const current = worktreeById.get(worktreeId)
|
||||
|
|
@ -464,6 +498,19 @@ export default function WorkspaceKanbanDrawer({
|
|||
const drawerLeftCss = sidebarOpen
|
||||
? `var(--workspace-sidebar-live-width, ${sidebarWidth}px)`
|
||||
: '0px'
|
||||
const fitPanelBoardWidthCss = `min(calc(100vw - ${drawerLeftCss}), 1294px)`
|
||||
// Why: full-width mode keeps user-sized columns and expands the companion
|
||||
// board over adjacent panes; fit mode preserves the older fixed panel.
|
||||
const boardContentWidth =
|
||||
workspaceStatuses.length * columnWidth +
|
||||
Math.max(0, workspaceStatuses.length - 1) * WORKSPACE_BOARD_COLUMN_GAP +
|
||||
24
|
||||
const fullBoardWidthCss = `min(calc(100vw - ${drawerLeftCss}), ${Math.max(
|
||||
boardContentWidth,
|
||||
1294
|
||||
)}px)`
|
||||
const boardWidthCss =
|
||||
workspaceBoardColumnLayout === 'fit' ? fitPanelBoardWidthCss : fullBoardWidthCss
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={handleSheetOpenChange} modal={false}>
|
||||
|
|
@ -479,7 +526,7 @@ export default function WorkspaceKanbanDrawer({
|
|||
left: drawerLeftCss,
|
||||
top: 36,
|
||||
height: 'calc(100% - 36px)',
|
||||
width: `min(calc(100vw - ${drawerLeftCss}), 1294px)`
|
||||
width: boardWidthCss
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onOpenAutoFocus={(event) => {
|
||||
|
|
@ -539,7 +586,12 @@ export default function WorkspaceKanbanDrawer({
|
|||
>
|
||||
<WorkspaceKanbanDrawerHeader
|
||||
selectedCount={selectedWorktrees.length}
|
||||
columnLayout={workspaceBoardColumnLayout}
|
||||
workspaceStatuses={workspaceStatuses}
|
||||
onColumnLayoutChange={(layout) => {
|
||||
useAppStore.getState().recordFeatureInteraction('workspace-board-actions')
|
||||
setWorkspaceBoardColumnLayout(layout)
|
||||
}}
|
||||
onRenameStatus={handleRenameStatus}
|
||||
onChangeStatusColor={handleChangeStatusColor}
|
||||
onChangeStatusIcon={handleChangeStatusIcon}
|
||||
|
|
@ -562,7 +614,7 @@ export default function WorkspaceKanbanDrawer({
|
|||
onDragLeave={handlePinDragLeave}
|
||||
/>
|
||||
<div
|
||||
ref={laneScrollerRef}
|
||||
ref={attachLaneScroller}
|
||||
className="min-h-0 flex-1 overflow-x-auto overflow-y-hidden scrollbar-sleek"
|
||||
>
|
||||
<WorkspaceKanbanLaneGrid
|
||||
|
|
@ -571,6 +623,7 @@ export default function WorkspaceKanbanDrawer({
|
|||
repoMap={repoMap}
|
||||
activeWorktreeId={activeWorktreeId}
|
||||
columnWidth={columnWidth}
|
||||
renderColumnWidth={renderColumnWidth}
|
||||
isResizingColumn={isResizingColumn}
|
||||
dragOverStatus={dragOverStatus}
|
||||
canCreateWorktree={canCreateWorktree}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,18 @@ import React from 'react'
|
|||
import { X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { SheetClose, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
||||
import type { WorkspaceStatusDefinition } from '../../../../shared/types'
|
||||
import type {
|
||||
WorkspaceBoardColumnLayout,
|
||||
WorkspaceStatusDefinition
|
||||
} from '../../../../shared/types'
|
||||
import SidebarFilter from './SidebarFilter'
|
||||
import WorkspaceKanbanSettingsMenu from './WorkspaceKanbanSettingsMenu'
|
||||
|
||||
type WorkspaceKanbanDrawerHeaderProps = {
|
||||
selectedCount: number
|
||||
columnLayout: WorkspaceBoardColumnLayout
|
||||
workspaceStatuses: readonly WorkspaceStatusDefinition[]
|
||||
onColumnLayoutChange: (layout: WorkspaceBoardColumnLayout) => void
|
||||
onRenameStatus: (statusId: string, label: string) => void
|
||||
onChangeStatusColor: (statusId: string, color: string) => void
|
||||
onChangeStatusIcon: (statusId: string, icon: string) => void
|
||||
|
|
@ -20,7 +25,9 @@ type WorkspaceKanbanDrawerHeaderProps = {
|
|||
|
||||
export default function WorkspaceKanbanDrawerHeader({
|
||||
selectedCount,
|
||||
columnLayout,
|
||||
workspaceStatuses,
|
||||
onColumnLayoutChange,
|
||||
onRenameStatus,
|
||||
onChangeStatusColor,
|
||||
onChangeStatusIcon,
|
||||
|
|
@ -53,7 +60,9 @@ export default function WorkspaceKanbanDrawerHeader({
|
|||
onMenuOpenChange={onFilterMenuOpenChange}
|
||||
/>
|
||||
<WorkspaceKanbanSettingsMenu
|
||||
columnLayout={columnLayout}
|
||||
workspaceStatuses={workspaceStatuses}
|
||||
onColumnLayoutChange={onColumnLayoutChange}
|
||||
onRenameStatus={onRenameStatus}
|
||||
onChangeStatusColor={onChangeStatusColor}
|
||||
onChangeStatusIcon={onChangeStatusIcon}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type WorkspaceKanbanLaneGridProps = {
|
|||
repoMap: Map<string, Repo>
|
||||
activeWorktreeId: string | null
|
||||
columnWidth: number
|
||||
renderColumnWidth: number
|
||||
isResizingColumn: boolean
|
||||
dragOverStatus: WorkspaceStatus | null
|
||||
canCreateWorktree: boolean
|
||||
|
|
@ -38,6 +39,7 @@ export default function WorkspaceKanbanLaneGrid({
|
|||
repoMap,
|
||||
activeWorktreeId,
|
||||
columnWidth,
|
||||
renderColumnWidth,
|
||||
isResizingColumn,
|
||||
dragOverStatus,
|
||||
canCreateWorktree,
|
||||
|
|
@ -57,7 +59,7 @@ export default function WorkspaceKanbanLaneGrid({
|
|||
<div
|
||||
className="grid h-full min-h-0 min-w-full grid-rows-[minmax(0,1fr)] gap-3"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${statuses.length}, minmax(${columnWidth}px, ${columnWidth}px))`
|
||||
gridTemplateColumns: `repeat(${statuses.length}, minmax(${renderColumnWidth}px, ${renderColumnWidth}px))`
|
||||
}}
|
||||
>
|
||||
{statuses.map((status) => (
|
||||
|
|
|
|||
|
|
@ -5,16 +5,23 @@ import {
|
|||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { WorkspaceStatusDefinition } from '../../../../shared/types'
|
||||
import type {
|
||||
WorkspaceBoardColumnLayout,
|
||||
WorkspaceStatusDefinition
|
||||
} from '../../../../shared/types'
|
||||
import { getWorkspaceStatusVisualMeta } from './workspace-status'
|
||||
import WorkspaceStatusAppearancePopover from './WorkspaceStatusAppearancePopover'
|
||||
|
||||
type WorkspaceKanbanSettingsMenuProps = {
|
||||
columnLayout: WorkspaceBoardColumnLayout
|
||||
workspaceStatuses: readonly WorkspaceStatusDefinition[]
|
||||
onColumnLayoutChange: (layout: WorkspaceBoardColumnLayout) => void
|
||||
onRenameStatus: (statusId: string, label: string) => void
|
||||
onChangeStatusColor: (statusId: string, color: string) => void
|
||||
onChangeStatusIcon: (statusId: string, icon: string) => void
|
||||
|
|
@ -24,7 +31,9 @@ type WorkspaceKanbanSettingsMenuProps = {
|
|||
}
|
||||
|
||||
export default function WorkspaceKanbanSettingsMenu({
|
||||
columnLayout,
|
||||
workspaceStatuses,
|
||||
onColumnLayoutChange,
|
||||
onRenameStatus,
|
||||
onChangeStatusColor,
|
||||
onChangeStatusIcon,
|
||||
|
|
@ -66,6 +75,36 @@ export default function WorkspaceKanbanSettingsMenu({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuLabel>Column layout</DropdownMenuLabel>
|
||||
<div className="px-2 pt-0.5 pb-2">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={columnLayout}
|
||||
onValueChange={(value) => {
|
||||
if (value === 'full' || value === 'fit') {
|
||||
onColumnLayoutChange(value)
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 w-full justify-stretch"
|
||||
aria-label="Workspace board column layout"
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="full"
|
||||
className="h-7 grow basis-0 px-1.5 text-[11px] data-[state=on]:bg-foreground/10 data-[state=on]:font-semibold data-[state=on]:text-foreground"
|
||||
>
|
||||
Full width
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="fit"
|
||||
className="h-7 grow basis-0 px-1.5 text-[11px] data-[state=on]:bg-foreground/10 data-[state=on]:font-semibold data-[state=on]:text-foreground"
|
||||
>
|
||||
Fit panel
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>Statuses</DropdownMenuLabel>
|
||||
<div className="space-y-2 px-1 pb-1">
|
||||
{workspaceStatuses.map((status, index) => {
|
||||
|
|
|
|||
|
|
@ -620,6 +620,30 @@ describe('createUISlice hydratePersistedUI', () => {
|
|||
expect(store.getState().workspaceBoardColumnWidth).toBe(520)
|
||||
})
|
||||
|
||||
it('hydrates workspace board column layout', () => {
|
||||
const store = createUIStore()
|
||||
|
||||
store.getState().hydratePersistedUI(
|
||||
makePersistedUI({
|
||||
workspaceBoardColumnLayout: 'fit'
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.getState().workspaceBoardColumnLayout).toBe('fit')
|
||||
})
|
||||
|
||||
it('defaults invalid workspace board column layout to full width', () => {
|
||||
const store = createUIStore()
|
||||
|
||||
store.getState().hydratePersistedUI(
|
||||
makePersistedUI({
|
||||
workspaceBoardColumnLayout: 'compact' as never
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.getState().workspaceBoardColumnLayout).toBe('full')
|
||||
})
|
||||
|
||||
it('hydrates a valid Kagi session link', () => {
|
||||
const store = createUIStore()
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type {
|
|||
TaskViewPresetId,
|
||||
TuiAgent,
|
||||
UpdateStatus,
|
||||
WorkspaceBoardColumnLayout,
|
||||
WorkspaceStatusDefinition,
|
||||
AgentActivityDisplayMode,
|
||||
WorktreeCardProperty
|
||||
|
|
@ -55,6 +56,7 @@ import {
|
|||
clampWorkspaceBoardColumnWidth,
|
||||
clampWorkspaceBoardOpacity,
|
||||
cloneDefaultWorkspaceStatuses,
|
||||
normalizeWorkspaceBoardColumnLayout,
|
||||
normalizeWorkspaceStatuses
|
||||
} from '../../../../shared/workspace-statuses'
|
||||
import { normalizeKagiSessionLink } from '../../../../shared/browser-url'
|
||||
|
|
@ -610,6 +612,8 @@ export type UISlice = {
|
|||
setWorkspaceStatuses: (statuses: WorkspaceStatusDefinition[]) => void
|
||||
workspaceBoardOpacity: number
|
||||
setWorkspaceBoardOpacity: (opacity: number) => void
|
||||
workspaceBoardColumnLayout: WorkspaceBoardColumnLayout
|
||||
setWorkspaceBoardColumnLayout: (layout: WorkspaceBoardColumnLayout) => void
|
||||
workspaceBoardColumnWidth: number
|
||||
setWorkspaceBoardColumnWidth: (width: number) => void
|
||||
statusBarItems: StatusBarItem[]
|
||||
|
|
@ -1291,6 +1295,13 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
|
|||
set({ workspaceBoardOpacity: clamped })
|
||||
},
|
||||
|
||||
workspaceBoardColumnLayout: 'full',
|
||||
setWorkspaceBoardColumnLayout: (layout) => {
|
||||
const normalized = normalizeWorkspaceBoardColumnLayout(layout)
|
||||
window.api.ui.set({ workspaceBoardColumnLayout: normalized }).catch(console.error)
|
||||
set({ workspaceBoardColumnLayout: normalized })
|
||||
},
|
||||
|
||||
workspaceBoardColumnWidth: WORKSPACE_BOARD_COLUMN_WIDTH_DEFAULT,
|
||||
setWorkspaceBoardColumnWidth: (width) => {
|
||||
const clamped = clampWorkspaceBoardColumnWidth(width)
|
||||
|
|
@ -1466,6 +1477,9 @@ export const createUISlice: StateCreator<AppState, [], [], UISlice> = (set, get)
|
|||
agentActivityDisplayMode: normalizeAgentActivityDisplayMode(ui.agentActivityDisplayMode),
|
||||
workspaceStatuses: normalizeWorkspaceStatuses(ui.workspaceStatuses),
|
||||
workspaceBoardOpacity: clampWorkspaceBoardOpacity(ui.workspaceBoardOpacity),
|
||||
workspaceBoardColumnLayout: normalizeWorkspaceBoardColumnLayout(
|
||||
ui.workspaceBoardColumnLayout
|
||||
),
|
||||
workspaceBoardColumnWidth: clampWorkspaceBoardColumnWidth(ui.workspaceBoardColumnWidth),
|
||||
statusBarItems,
|
||||
statusBarVisible: ui.statusBarVisible ?? true,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ import { DEFAULT_STATUS_BAR_ITEMS } from './status-bar-defaults'
|
|||
import { DEFAULT_TERMINAL_FONT_WEIGHT } from './terminal-fonts'
|
||||
import { getDefaultTerminalQuickCommands } from './terminal-quick-commands'
|
||||
import type { VoiceSettings } from './speech-types'
|
||||
import { cloneDefaultWorkspaceStatuses } from './workspace-statuses'
|
||||
import {
|
||||
WORKSPACE_BOARD_COLUMN_LAYOUT_DEFAULT,
|
||||
cloneDefaultWorkspaceStatuses
|
||||
} from './workspace-statuses'
|
||||
import { TASK_PROVIDERS } from './task-providers'
|
||||
import { DEFAULT_WORKTREE_CARD_PROPERTIES } from './worktree-card-properties'
|
||||
import { getDefaultSourceControlAiSettings } from './source-control-ai'
|
||||
|
|
@ -395,6 +398,7 @@ export function getDefaultUIState(): PersistedUIState {
|
|||
agentActivityDisplayMode: DEFAULT_AGENT_ACTIVITY_DISPLAY_MODE,
|
||||
workspaceStatuses: cloneDefaultWorkspaceStatuses(),
|
||||
workspaceBoardOpacity: 1,
|
||||
workspaceBoardColumnLayout: WORKSPACE_BOARD_COLUMN_LAYOUT_DEFAULT,
|
||||
workspaceBoardColumnWidth: 308,
|
||||
_workspaceStatusesDefaultOrderMigrated: true,
|
||||
_workspaceStatusesDefaultWorkflowMigrated: true,
|
||||
|
|
|
|||
|
|
@ -2378,6 +2378,8 @@ export type WorktreeCardProperty =
|
|||
|
||||
export type AgentActivityDisplayMode = 'compact' | 'full'
|
||||
|
||||
export type WorkspaceBoardColumnLayout = 'full' | 'fit'
|
||||
|
||||
export type StatusBarItem =
|
||||
| 'claude'
|
||||
| 'codex'
|
||||
|
|
@ -2436,6 +2438,7 @@ export type PersistedUIState = {
|
|||
agentActivityDisplayMode?: AgentActivityDisplayMode
|
||||
workspaceStatuses?: WorkspaceStatusDefinition[]
|
||||
workspaceBoardOpacity?: number
|
||||
workspaceBoardColumnLayout?: WorkspaceBoardColumnLayout
|
||||
workspaceBoardColumnWidth?: number
|
||||
/** One-shot migration flag for a short-lived build that persisted the
|
||||
* default workspace statuses in reverse workflow order. Once stamped,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import {
|
|||
WORKSPACE_BOARD_COLUMN_WIDTH_MIN,
|
||||
clampWorkspaceBoardColumnWidth,
|
||||
cloneDefaultWorkspaceStatuses,
|
||||
fitWorkspaceBoardColumnWidth,
|
||||
normalizePersistedWorkspaceStatuses,
|
||||
normalizeWorkspaceBoardColumnLayout,
|
||||
normalizeWorkspaceStatuses
|
||||
} from './workspace-statuses'
|
||||
|
||||
|
|
@ -249,4 +251,34 @@ describe('workspace status visuals', () => {
|
|||
expect(clampWorkspaceBoardColumnWidth(321.6)).toBe(322)
|
||||
expect(clampWorkspaceBoardColumnWidth(900)).toBe(WORKSPACE_BOARD_COLUMN_WIDTH_MAX)
|
||||
})
|
||||
|
||||
it('defaults workspace board column layout to full width', () => {
|
||||
expect(normalizeWorkspaceBoardColumnLayout(undefined)).toBe('full')
|
||||
expect(normalizeWorkspaceBoardColumnLayout('fit')).toBe('fit')
|
||||
expect(normalizeWorkspaceBoardColumnLayout('compact')).toBe('full')
|
||||
})
|
||||
|
||||
it('fits workspace board columns inside the visible panel without exceeding saved width', () => {
|
||||
expect(
|
||||
fitWorkspaceBoardColumnWidth({
|
||||
containerWidth: 960,
|
||||
columnCount: 4,
|
||||
capWidth: 308
|
||||
})
|
||||
).toBe(231)
|
||||
expect(
|
||||
fitWorkspaceBoardColumnWidth({
|
||||
containerWidth: 1600,
|
||||
columnCount: 4,
|
||||
capWidth: 308
|
||||
})
|
||||
).toBe(308)
|
||||
expect(
|
||||
fitWorkspaceBoardColumnWidth({
|
||||
containerWidth: 100,
|
||||
columnCount: 4,
|
||||
capWidth: 308
|
||||
})
|
||||
).toBe(WORKSPACE_BOARD_COLUMN_WIDTH_MIN)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import type { Worktree, WorkspaceStatus, WorkspaceStatusDefinition } from './types'
|
||||
import type {
|
||||
Worktree,
|
||||
WorkspaceBoardColumnLayout,
|
||||
WorkspaceStatus,
|
||||
WorkspaceStatusDefinition
|
||||
} from './types'
|
||||
import { DEFAULT_STATUS_VISUALS, DEFAULT_WORKSPACE_STATUSES } from './workspace-status-defaults'
|
||||
import {
|
||||
isKnownBadPRReorderedDefaultStatusPayload,
|
||||
|
|
@ -22,6 +27,10 @@ export const WORKSPACE_BOARD_COLUMN_WIDTH_DEFAULT = 308
|
|||
export const WORKSPACE_BOARD_COLUMN_WIDTH_MIN = 220
|
||||
export const WORKSPACE_BOARD_COLUMN_WIDTH_MAX = 520
|
||||
export const WORKSPACE_BOARD_COLUMN_WIDTH_STEP = 20
|
||||
// Why: keep this aligned with the `gap-3` lane gap in WorkspaceKanbanLaneGrid;
|
||||
// layout calculations need the actual rendered gap between status columns.
|
||||
export const WORKSPACE_BOARD_COLUMN_GAP = 12
|
||||
export const WORKSPACE_BOARD_COLUMN_LAYOUT_DEFAULT: WorkspaceBoardColumnLayout = 'full'
|
||||
|
||||
export const WORKSPACE_STATUS_COLOR_IDS = [
|
||||
'neutral',
|
||||
|
|
@ -235,6 +244,10 @@ export function clampWorkspaceBoardOpacity(value: unknown): number {
|
|||
return Math.min(1, Math.max(0.2, Math.round(value * 100) / 100))
|
||||
}
|
||||
|
||||
export function normalizeWorkspaceBoardColumnLayout(value: unknown): WorkspaceBoardColumnLayout {
|
||||
return value === 'fit' ? 'fit' : WORKSPACE_BOARD_COLUMN_LAYOUT_DEFAULT
|
||||
}
|
||||
|
||||
export function clampWorkspaceBoardColumnWidth(value: unknown): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return WORKSPACE_BOARD_COLUMN_WIDTH_DEFAULT
|
||||
|
|
@ -245,6 +258,22 @@ export function clampWorkspaceBoardColumnWidth(value: unknown): number {
|
|||
)
|
||||
}
|
||||
|
||||
export function fitWorkspaceBoardColumnWidth(args: {
|
||||
containerWidth: number
|
||||
columnCount: number
|
||||
capWidth: number
|
||||
gap?: number
|
||||
}): number {
|
||||
const cap = clampWorkspaceBoardColumnWidth(args.capWidth)
|
||||
if (args.columnCount <= 0 || !Number.isFinite(args.containerWidth) || args.containerWidth <= 0) {
|
||||
return cap
|
||||
}
|
||||
const gap = args.gap ?? WORKSPACE_BOARD_COLUMN_GAP
|
||||
const available = args.containerWidth - gap * Math.max(0, args.columnCount - 1)
|
||||
const perColumn = Math.floor(available / args.columnCount)
|
||||
return Math.min(cap, Math.max(WORKSPACE_BOARD_COLUMN_WIDTH_MIN, perColumn))
|
||||
}
|
||||
|
||||
export function isWorkspaceStatusId(
|
||||
value: string,
|
||||
statuses: readonly WorkspaceStatusDefinition[]
|
||||
|
|
|
|||
Loading…
Reference in New Issue