From 67be1bd801992bbf5bef429b909e1e8d1d5e4bb4 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:43:13 -0700 Subject: [PATCH] feat: add search to workspace kanban board Adds a search field to filter the kanban board by workspace name. Range selections now index rendered cards only, preventing silent selection of hidden items when filtering. Selection badges count only the visible cards that drag/context-menu actions will move. Lane totals distinguish between empty-by-definition and filtered-away cards. Drop operations commit against the full lane while displaying filtered indices. Whitespace-only queries don't show match counts, since they don't narrow the board. --- .../WorkspaceKanbanDrawer.search.test.tsx | 62 ++++++++-- .../sidebar/WorkspaceKanbanDrawer.tsx | 16 ++- .../WorkspaceKanbanDrawerHeader.test.tsx | 22 +++- .../sidebar/WorkspaceKanbanDrawerHeader.tsx | 3 + .../WorkspaceKanbanSearchField.test.tsx | 20 ++- .../sidebar/WorkspaceKanbanSearchField.tsx | 23 ++-- .../WorkspaceKanbanStatusLane.test.tsx | 21 ++++ .../sidebar/WorkspaceKanbanStatusLane.tsx | 19 ++- .../src/components/sidebar/WorktreeList.tsx | 8 +- .../use-workspace-kanban-selection.test.tsx | 114 ++++++++++++++++++ .../sidebar/use-workspace-kanban-selection.ts | 21 +++- .../workspace-kanban-sidebar-drop.test.ts | 28 ++++- .../sidebar/workspace-kanban-sidebar-drop.ts | 38 +++--- src/renderer/src/i18n/locales/es.json | 12 +- src/renderer/src/i18n/locales/ja.json | 12 +- src/renderer/src/i18n/locales/ko.json | 12 +- src/renderer/src/i18n/locales/zh.json | 12 +- 17 files changed, 361 insertions(+), 82 deletions(-) create mode 100644 src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx index f398b9c19ef..fb037ce8dc9 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.search.test.tsx @@ -9,7 +9,9 @@ import WorkspaceKanbanDrawer from './WorkspaceKanbanDrawer' import type { WorkspaceKanbanLaneView } from './workspace-kanban-search' type HeaderCapture = { + selectedCount: number query: string + isFiltering: boolean matchCount: number totalCount: number onQueryChange: (query: string) => void @@ -42,7 +44,8 @@ const { headerState, gridState, pointerDragState, - selectionState + selectionState, + selectionScopeState } = vi.hoisted(() => ({ syncWorkspaceBoardTaskStatusesMock: vi.fn(() => Promise.resolve({ updated: 1, skipped: 0, failed: 0, messages: [] }) @@ -50,7 +53,8 @@ const { headerState: { current: null as HeaderCapture | null }, gridState: { current: null as GridCapture | null }, pointerDragState: { current: null as PointerDragCapture | null }, - selectionState: { current: [] as Worktree[] } + selectionState: { current: [] as Worktree[] }, + selectionScopeState: { current: [] as readonly Worktree[] } })) ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true @@ -88,15 +92,22 @@ vi.mock('./use-visible-workspace-kanban-worktree-ids', () => ({ })) vi.mock('./use-workspace-kanban-selection', () => ({ - useWorkspaceKanbanSelection: () => ({ - selectedWorktreeIds: new Set(selectionState.current.map((worktree) => worktree.id)), - selectedWorktrees: selectionState.current, - selectionAnchorId: null, - updateSelectionForGesture: vi.fn(), - updateSelectionForArea: vi.fn(), - clearSelection: vi.fn(), - selectForContextMenu: vi.fn(() => selectionState.current) - }) + useWorkspaceKanbanSelection: ( + _open: boolean, + boardWorktrees: readonly Worktree[], + renderedWorktrees?: readonly Worktree[] + ) => { + selectionScopeState.current = renderedWorktrees ?? boardWorktrees + return { + selectedWorktreeIds: new Set(selectionState.current.map((worktree) => worktree.id)), + selectedWorktrees: selectionState.current, + selectionAnchorId: null, + updateSelectionForGesture: vi.fn(), + updateSelectionForArea: vi.fn(), + clearSelection: vi.fn(), + selectForContextMenu: vi.fn(() => selectionState.current) + } + } })) vi.mock('./use-workspace-kanban-area-selection', () => ({ @@ -217,6 +228,7 @@ beforeEach(() => { gridState.current = null pointerDragState.current = null selectionState.current = [] + selectionScopeState.current = [] syncWorkspaceBoardTaskStatusesMock.mockClear() updateWorktreesMeta = vi.fn(() => Promise.resolve()) useAppStore.setState({ @@ -334,6 +346,34 @@ describe('WorkspaceKanbanDrawer search', () => { expect(gridState.current?.selectedWorktrees).toEqual([gamma]) }) + it('counts only the rendered cards in the header selection badge', () => { + selectionState.current = [alpha, gamma] + renderDrawer() + expect(headerState.current?.selectedCount).toBe(2) + + typeQuery('gamma') + + expect(headerState.current?.selectedCount).toBe(1) + }) + + it('scopes selection gestures to the rendered cards', () => { + renderDrawer() + expect(selectionScopeState.current).toHaveLength(5) + + typeQuery('gamma') + + expect(selectionScopeState.current).toEqual([gamma]) + }) + + it('reports a non-filtering query so the header withholds match counts', () => { + renderDrawer() + + typeQuery(' ') + + expect(headerState.current).toMatchObject({ isFiltering: false, matchCount: 5, totalCount: 5 }) + expect(laneIds('todo')).toHaveLength(4) + }) + it('ranks a drop into a filtered lane against the full lane, not the rendered one', () => { renderDrawer() typeQuery('gamma') diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx index ae896297df5..8bee18ab590 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawer.tsx @@ -204,6 +204,15 @@ export default function WorkspaceKanbanDrawer({ () => buildWorkspaceKanbanLaneViews({ worktreesByStatus, matchingWorktreeIds }), [matchingWorktreeIds, worktreesByStatus] ) + // Why: range and area gestures must index the cards the user can actually see, + // or a shift-click across a filtered gap silently selects hidden workspaces. + const renderedBoardWorktrees = useMemo( + () => + matchingWorktreeIds + ? boardWorktrees.filter((worktree) => matchingWorktreeIds.has(worktree.id)) + : boardWorktrees, + [boardWorktrees, matchingWorktreeIds] + ) const { selectedWorktreeIds, selectedWorktrees, @@ -212,7 +221,7 @@ export default function WorkspaceKanbanDrawer({ updateSelectionForArea, clearSelection, selectForContextMenu - } = useWorkspaceKanbanSelection(open, boardWorktrees) + } = useWorkspaceKanbanSelection(open, boardWorktrees, renderedBoardWorktrees) const { handleAreaSelectionPointerDown } = useWorkspaceKanbanAreaSelection({ open, boardRef, @@ -800,8 +809,11 @@ export default function WorkspaceKanbanDrawer({ }} > { }) it('renders the search field as a sibling of the sheet title, not inside it', () => { - const header = renderHeader(vi.fn(), { query: 'orca', matchCount: 2, totalCount: 15 }) + const header = renderHeader(vi.fn(), { + query: 'orca', + isFiltering: true, + matchCount: 2, + totalCount: 15 + }) const title = findByType(header, SheetTitle) expect(title).not.toBeNull() expect(findByType(title, WorkspaceKanbanSearchField)).toBeNull() const field = findByType(header, WorkspaceKanbanSearchField) - expect(field?.props).toMatchObject({ query: 'orca', matchCount: 2, totalCount: 15 }) + expect(field?.props).toMatchObject({ + query: 'orca', + isFiltering: true, + matchCount: 2, + totalCount: 15 + }) }) it('keeps the filter, settings, and close cluster reachable alongside the field', () => { - const header = renderHeader(vi.fn(), { query: 'orca', matchCount: 2, totalCount: 15 }) + const header = renderHeader(vi.fn(), { + query: 'orca', + isFiltering: true, + matchCount: 2, + totalCount: 15 + }) expect(findByType(header, SidebarFilter)).not.toBeNull() expect(findByType(header, WorkspaceKanbanSettingsMenu)).not.toBeNull() diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.tsx index 6d1b513f388..732adb8750a 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanDrawerHeader.tsx @@ -11,6 +11,7 @@ import { translate } from '@/i18n/i18n' type WorkspaceKanbanDrawerHeaderProps = { selectedCount: number query: string + isFiltering: boolean matchCount: number totalCount: number onQueryChange: (query: string) => void @@ -31,6 +32,7 @@ type WorkspaceKanbanDrawerHeaderProps = { export default function WorkspaceKanbanDrawerHeader({ selectedCount, query, + isFiltering, matchCount, totalCount, onQueryChange, @@ -72,6 +74,7 @@ export default function WorkspaceKanbanDrawerHeader({ { root.render( { expect(clearButton()?.getAttribute('aria-label')).toBe('Clear search') }) + it('withholds counts for text that never narrows the board', () => { + renderField({ query: ' ', isFiltering: false, matchCount: 12, totalCount: 12 }) + + expect(container.querySelector('span[aria-hidden="true"]')).toBeNull() + expect(clearButton()).not.toBeNull() + + act(() => { + vi.advanceTimersByTime(400) + }) + expect(liveRegion().textContent).toBe('') + }) + it('announces match counts only after the query settles', () => { renderField({ query: 'orca', matchCount: 3, totalCount: 12 }) expect(liveRegion().textContent).toBe('') diff --git a/src/renderer/src/components/sidebar/WorkspaceKanbanSearchField.tsx b/src/renderer/src/components/sidebar/WorkspaceKanbanSearchField.tsx index 4d5e36ce2cf..a418d3fa288 100644 --- a/src/renderer/src/components/sidebar/WorkspaceKanbanSearchField.tsx +++ b/src/renderer/src/components/sidebar/WorkspaceKanbanSearchField.tsx @@ -9,6 +9,8 @@ const ANNOUNCE_DEBOUNCE_MS = 400 type WorkspaceKanbanSearchFieldProps = { query: string + /** False for text that never narrows the board (whitespace-only, over-bound). */ + isFiltering: boolean matchCount: number totalCount: number onQueryChange: (query: string) => void @@ -30,18 +32,19 @@ function formatAnnouncement(matchCount: number, totalCount: number): string { export default function WorkspaceKanbanSearchField({ query, + isFiltering, matchCount, totalCount, onQueryChange, onClear }: WorkspaceKanbanSearchFieldProps): React.JSX.Element { - const hasQuery = query !== '' + const hasText = query !== '' const [announcement, setAnnouncement] = useState('') // Why: the filter itself is undebounced, but a polite live region that changes // on every keystroke produces continuous speech and makes the field unusable. useEffect(() => { - if (!hasQuery) { + if (!isFiltering) { setAnnouncement('') return } @@ -50,7 +53,7 @@ export default function WorkspaceKanbanSearchField({ ANNOUNCE_DEBOUNCE_MS ) return () => window.clearTimeout(timer) - }, [hasQuery, matchCount, totalCount]) + }, [isFiltering, matchCount, totalCount]) return (
@@ -67,13 +70,13 @@ export default function WorkspaceKanbanSearchField({ )} className={cn( 'h-7 border-worktree-sidebar-border bg-background pl-7 text-xs', - hasQuery && 'pr-16' + hasText && (isFiltering ? 'pr-16' : 'pr-8') )} onChange={(event) => onQueryChange(event.target.value)} onKeyDown={(event) => { // Why: the board swallows Radix close requests, so an empty-field // Escape has nothing local to do and is left to bubble. - if (event.key !== 'Escape' || !hasQuery) { + if (event.key !== 'Escape' || !hasText) { return } event.preventDefault() @@ -81,11 +84,13 @@ export default function WorkspaceKanbanSearchField({ onClear() }} /> - {hasQuery ? ( + {hasText ? (
- + {isFiltering ? ( + + ) : null}
- {hasQuery ? `${items.length} / ${totalCount ?? items.length}` : items.length} + {isFiltered ? `${items.length} / ${laneTotalCount}` : items.length}
@@ -192,7 +201,7 @@ export default function WorkspaceKanbanStatusLane({ ) : (
- {hasQuery + {isFiltered ? translate( 'auto.components.sidebar.WorkspaceKanbanStatusLane.2df01a03ff', 'No matches' diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index 2abe607a1c2..3d315cb616f 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -170,6 +170,7 @@ import { getWorkspaceKanbanSidebarDropTarget, hasWorkspaceKanbanSidebarDropBoard, isWorkspaceKanbanSidebarDropPointInBoard, + resolveWorkspaceKanbanSidebarFullLaneDropIndex, updateWorkspaceKanbanSidebarDropTargetVisual } from './workspace-kanban-sidebar-drop' import { @@ -3220,7 +3221,12 @@ const VirtualizedWorktreeViewport = React.memo(function VirtualizedWorktreeViewp onDropWorktreesOnWorkspaceBoard({ worktreeIds: drag.reorderDraggedIds, status: boardDropTarget.status, - dropIndex: boardDropTarget.dropIndex, + // Why: the target counts rendered cards, but the groups are the full + // lane. Board search can make those two differ. + dropIndex: resolveWorkspaceKanbanSidebarFullLaneDropIndex( + boardDropTarget.status, + boardDropTarget.dropIndex + ), groups: getWorkspaceKanbanSidebarDropGroups() }) } else { diff --git a/src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx b/src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx new file mode 100644 index 00000000000..c885df7888f --- /dev/null +++ b/src/renderer/src/components/sidebar/use-workspace-kanban-selection.test.tsx @@ -0,0 +1,114 @@ +// @vitest-environment happy-dom + +import React, { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import type { Worktree } from '../../../../shared/types' +import { useWorkspaceKanbanSelection } from './use-workspace-kanban-selection' + +;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + +type Selection = ReturnType + +function worktree(id: string): Worktree { + return { id, repoId: 'repo-a', displayName: id } as Worktree +} + +const alpha = worktree('alpha') +const beta = worktree('beta') +const gamma = worktree('gamma') +const delta = worktree('delta') +const fullBoard = [alpha, beta, gamma, delta] + +let container: HTMLDivElement +let root: Root +let selection: Selection + +function Probe({ + board, + rendered +}: { + board: readonly Worktree[] + rendered: readonly Worktree[] +}): null { + selection = useWorkspaceKanbanSelection(true, board, rendered) + return null +} + +function renderSelection( + rendered: readonly Worktree[] = fullBoard, + board: readonly Worktree[] = fullBoard +): void { + act(() => { + root.render() + }) +} + +function click(worktreeId: string, shiftKey = false): void { + act(() => { + selection.updateSelectionForGesture( + { metaKey: false, ctrlKey: false, shiftKey } as React.MouseEvent, + worktreeId + ) + }) +} + +function selectedIds(): string[] { + return [...selection.selectedWorktreeIds].sort() +} + +beforeEach(() => { + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) +}) + +afterEach(() => { + act(() => { + root.unmount() + }) + container.remove() +}) + +describe('useWorkspaceKanbanSelection', () => { + it('ranges across the whole board when nothing is filtered', () => { + renderSelection() + + click(alpha.id) + click(gamma.id, true) + + expect(selectedIds()).toEqual(['alpha', 'beta', 'gamma']) + }) + + it('never ranges through cards a search has hidden', () => { + renderSelection([alpha, gamma]) + + click(alpha.id) + click(gamma.id, true) + + expect(selectedIds()).toEqual(['alpha', 'gamma']) + }) + + it('keeps a hidden card selected so clearing the search restores the selection', () => { + renderSelection() + click(alpha.id) + click(beta.id, true) + expect(selectedIds()).toEqual(['alpha', 'beta']) + + renderSelection([alpha]) + expect(selectedIds()).toEqual(['alpha', 'beta']) + + renderSelection() + expect(selectedIds()).toEqual(['alpha', 'beta']) + }) + + it('still prunes ids that leave the board entirely', () => { + renderSelection() + click(alpha.id) + click(gamma.id, true) + + renderSelection([alpha, beta], [alpha, beta]) + + expect(selectedIds()).toEqual(['alpha', 'beta']) + }) +}) diff --git a/src/renderer/src/components/sidebar/use-workspace-kanban-selection.ts b/src/renderer/src/components/sidebar/use-workspace-kanban-selection.ts index a73fad19796..4db4f132a4c 100644 --- a/src/renderer/src/components/sidebar/use-workspace-kanban-selection.ts +++ b/src/renderer/src/components/sidebar/use-workspace-kanban-selection.ts @@ -8,11 +8,22 @@ import { updateWorktreeSelection } from './worktree-multi-selection' -export function useWorkspaceKanbanSelection(open: boolean, boardWorktrees: readonly Worktree[]) { +// Why: board search hides cards without dropping them from the board, so range +// and area gestures index the rendered subset while pruning still spans the +// whole board — otherwise a query would silently discard hidden selections. +export function useWorkspaceKanbanSelection( + open: boolean, + boardWorktrees: readonly Worktree[], + renderedWorktrees: readonly Worktree[] = boardWorktrees +) { const boardWorktreeIds = useMemo( () => boardWorktrees.map((worktree) => worktree.id), [boardWorktrees] ) + const renderedWorktreeIds = useMemo( + () => renderedWorktrees.map((worktree) => worktree.id), + [renderedWorktrees] + ) const [selectedWorktreeIds, setSelectedWorktreeIds] = useState>(new Set()) const [selectionAnchorId, setSelectionAnchorId] = useState(null) const selectedWorktrees = useMemo( @@ -43,7 +54,7 @@ export function useWorkspaceKanbanSelection(open: boolean, boardWorktrees: reado (event: React.MouseEvent, worktreeId: string): boolean => { const intent = getWorktreeSelectionIntent(event, navigator.userAgent.includes('Mac')) const result = updateWorktreeSelection({ - visibleIds: boardWorktreeIds, + visibleIds: renderedWorktreeIds, previousSelectedIds: selectedWorktreeIds, previousAnchorId: selectionAnchorId, targetId: worktreeId, @@ -53,7 +64,7 @@ export function useWorkspaceKanbanSelection(open: boolean, boardWorktrees: reado setSelectionAnchorId(result.anchorId) return intent !== 'replace' }, - [boardWorktreeIds, selectedWorktreeIds, selectionAnchorId] + [renderedWorktreeIds, selectedWorktreeIds, selectionAnchorId] ) const selectForContextMenu = useCallback( @@ -76,7 +87,7 @@ export function useWorkspaceKanbanSelection(open: boolean, boardWorktrees: reado baseAnchorId: string | null = selectionAnchorId ): void => { const result = updateWorktreeAreaSelection({ - visibleIds: boardWorktreeIds, + visibleIds: renderedWorktreeIds, previousSelectedIds: baseSelectedIds, previousAnchorId: baseAnchorId, areaIds, @@ -89,7 +100,7 @@ export function useWorkspaceKanbanSelection(open: boolean, boardWorktrees: reado previous === result.anchorId ? previous : result.anchorId ) }, - [boardWorktreeIds, selectedWorktreeIds, selectionAnchorId] + [renderedWorktreeIds, selectedWorktreeIds, selectionAnchorId] ) const clearSelection = useCallback(() => { diff --git a/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts b/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts index 4bf51612f09..67fdd0f67e3 100644 --- a/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts +++ b/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.test.ts @@ -6,6 +6,7 @@ import { getWorkspaceKanbanSidebarDropGroups, getWorkspaceKanbanSidebarDropTarget, isWorkspaceKanbanSidebarDropPointInBoard, + resolveWorkspaceKanbanSidebarFullLaneDropIndex, updateWorkspaceKanbanSidebarDropTargetVisual } from './workspace-kanban-sidebar-drop' @@ -257,16 +258,33 @@ describe('workspace kanban sidebar drop DOM bridge', () => { ]) }) - it('translates the rendered drop index onto the full lane', () => { + it('keeps the tracked drop target in the rendered index space of the indicator', () => { + const { lane } = appendBoard() + lane.dataset.workspaceLaneFullIds = ['doing-x', 'doing-a', 'doing-y', 'doing-b'].join('\n') + setElementFromPoint(lane) + + expect(getWorkspaceKanbanSidebarDropTarget(24, 60)).toMatchObject({ + status: 'doing', + dropIndex: 1 + }) + }) + + it('translates a rendered drop index onto the full lane at the commit boundary', () => { const { lane } = appendBoard() lane.dataset.workspaceLaneFullIds = ['doing-x', 'doing-a', 'doing-y', 'doing-b'].join('\n') setElementFromPoint(lane) // Rendered index 1 means "before doing-b", which is index 3 in the full lane. - expect(getWorkspaceKanbanSidebarDropTarget(24, 60)).toMatchObject({ - status: 'doing', - dropIndex: 3 - }) + expect(resolveWorkspaceKanbanSidebarFullLaneDropIndex('doing', 1)).toBe(3) + // Why: a tracked target can be committed after the pointer left the lane, + // so the translation must not depend on the current pointer position. + expect(resolveWorkspaceKanbanSidebarFullLaneDropIndex('doing', 2)).toBe(4) + }) + + it('passes the drop index through for a lane it cannot find', () => { + appendBoard() + + expect(resolveWorkspaceKanbanSidebarFullLaneDropIndex('todo', 2)).toBe(2) }) it('marks and clears the external board hover target', () => { diff --git a/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.ts b/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.ts index f2a3361fca1..f5daca23b7f 100644 --- a/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.ts +++ b/src/renderer/src/components/sidebar/workspace-kanban-sidebar-drop.ts @@ -111,24 +111,30 @@ export function getWorkspaceKanbanSidebarDropTarget( if (!board) { return { status: null, isPinDrop: false, dropIndex: 0 } } - const target = getCardDropTarget(board, x, y) - if (!target.status) { - return target - } - const lane = getStatusDropTargetElement(board, target.status) + return getCardDropTarget(board, x, y) +} + +/** + * Translates a tracked drop index — which counts *rendered* cards, matching the + * drop indicator — onto the full lane that `getWorkspaceKanbanSidebarDropGroups` + * reports. Call this once, at the commit boundary: a tracked target can be + * committed after the pointer has left the lane, so translating earlier would + * miss that path. + */ +export function resolveWorkspaceKanbanSidebarFullLaneDropIndex( + status: WorkspaceStatus, + renderedDropIndex: number +): number { + const board = getWorkspaceKanbanBoardElement() + const lane = board ? getStatusDropTargetElement(board, status) : null if (!lane) { - return target - } - // Why: getCardDropTarget counts rendered cards, but the groups above are the - // full lane. Committing the two together would interpolate the wrong rank. - return { - ...target, - dropIndex: resolveFullLaneDropIndex({ - fullLaneIds: getLaneFullWorktreeIds(lane), - renderedIds: getRenderedLaneCardIds(lane), - filteredDropIndex: target.dropIndex - }) + return renderedDropIndex } + return resolveFullLaneDropIndex({ + fullLaneIds: getLaneFullWorktreeIds(lane), + renderedIds: getRenderedLaneCardIds(lane), + filteredDropIndex: renderedDropIndex + }) } export function updateWorkspaceKanbanSidebarDropTargetVisual(args: { diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 648122ce0c0..1ef31c9d5a8 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -4366,7 +4366,7 @@ "WorkspaceKanbanStatusLane": { "8ad104642b": "Vacío", "3611d1ae7f": "Cambiar tamaño de columnas del tablero de espacios de trabajo", - "2df01a03ff": "No matches" + "2df01a03ff": "No hay coincidencias" }, "WorkspaceStatusAppearancePopover": { "514be2f569": "Establecer color de {{value0}} en {{value1}}", @@ -5004,11 +5004,11 @@ "parkTerminal": "Park terminal" }, "WorkspaceKanbanSearchField": { - "bdb753c78d": "No workspaces match", - "4d96c209d6": "{{value0}} of {{value1}} workspaces match", - "c0cd6bdf6c": "Search workspaces", - "49c266baaa": "Search", - "3b7ea51793": "Clear search" + "bdb753c78d": "Ningún espacio de trabajo coincide", + "4d96c209d6": "{{value0}} de {{value1}} espacios de trabajo coinciden", + "c0cd6bdf6c": "Buscar espacios de trabajo", + "49c266baaa": "Buscar", + "3b7ea51793": "Borrar búsqueda" } }, "shared": { diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 197c9d97362..9ba43069893 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -4347,7 +4347,7 @@ "WorkspaceKanbanStatusLane": { "8ad104642b": "空の", "3611d1ae7f": "ワークスペースボードの列のサイズを変更する", - "2df01a03ff": "No matches" + "2df01a03ff": "一致なし" }, "WorkspaceStatusAppearancePopover": { "514be2f569": "{{value0}} の色を {{value1}} に設定します", @@ -5004,11 +5004,11 @@ "parkTerminal": "Park terminal" }, "WorkspaceKanbanSearchField": { - "bdb753c78d": "No workspaces match", - "4d96c209d6": "{{value0}} of {{value1}} workspaces match", - "c0cd6bdf6c": "Search workspaces", - "49c266baaa": "Search", - "3b7ea51793": "Clear search" + "bdb753c78d": "一致するワークスペースはありません", + "4d96c209d6": "{{value1}} 件中 {{value0}} 件のワークスペースが一致します", + "c0cd6bdf6c": "ワークスペースの検索", + "49c266baaa": "検索", + "3b7ea51793": "検索をクリア" } }, "shared": { diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index c8fffed7bd1..1bfe9d0e3fa 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -4347,7 +4347,7 @@ "WorkspaceKanbanStatusLane": { "8ad104642b": "비어 있음", "3611d1ae7f": "워크스페이스 보드 열 크기 조정", - "2df01a03ff": "No matches" + "2df01a03ff": "일치하는 항목 없음" }, "WorkspaceStatusAppearancePopover": { "514be2f569": "{{value0}} 색상을 {{value1}}로 설정", @@ -5004,11 +5004,11 @@ "parkTerminal": "Park terminal" }, "WorkspaceKanbanSearchField": { - "bdb753c78d": "No workspaces match", - "4d96c209d6": "{{value0}} of {{value1}} workspaces match", - "c0cd6bdf6c": "Search workspaces", - "49c266baaa": "Search", - "3b7ea51793": "Clear search" + "bdb753c78d": "일치하는 워크스페이스가 없습니다", + "4d96c209d6": "워크스페이스 {{value1}}개 중 {{value0}}개 일치", + "c0cd6bdf6c": "워크스페이스 검색", + "49c266baaa": "검색", + "3b7ea51793": "검색 지우기" } }, "shared": { diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 0a5883e8bef..4ddcd3749cc 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -4347,7 +4347,7 @@ "WorkspaceKanbanStatusLane": { "8ad104642b": "空的", "3611d1ae7f": "调整工作区板列的大小", - "2df01a03ff": "No matches" + "2df01a03ff": "没有匹配项" }, "WorkspaceStatusAppearancePopover": { "514be2f569": "将 {{value0}} 颜色设置为 {{value1}}", @@ -5004,11 +5004,11 @@ "parkTerminal": "Park terminal" }, "WorkspaceKanbanSearchField": { - "bdb753c78d": "No workspaces match", - "4d96c209d6": "{{value0}} of {{value1}} workspaces match", - "c0cd6bdf6c": "Search workspaces", - "49c266baaa": "Search", - "3b7ea51793": "Clear search" + "bdb753c78d": "没有匹配的工作区", + "4d96c209d6": "{{value1}} 个工作区中有 {{value0}} 个匹配", + "c0cd6bdf6c": "搜索工作区", + "49c266baaa": "搜索", + "3b7ea51793": "清除搜索" } }, "shared": {