From 8ca17864f1b1a935cc04c20d118f6ece67cfe9ee Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Thu, 12 Jun 2025 04:02:37 -0400 Subject: [PATCH] fix(editor): show added or deleted rows immediately in grouped table and Kanban views (#12731) https://github.com/user-attachments/assets/214fbe4f-b667-44b7-85a3-77ef4cfa8cca This PR fixes a bug where adding or deleting rows in a grouped table view did not visually update the UI until the user manually refreshed the page or navigated away and back. The issue gave the impression that the action had not completed. Same issue for Kanban cards. The result now is: Users now see new rows or deleted rows reflected in real-time without needing to reload or navigate away. This applies to both grouped table views and Kanban cards. ## Summary by CodeRabbit - **Bug Fixes** - Ensured the UI updates immediately after adding, deleting, or moving cards and rows in Kanban and Table views on both mobile and desktop. - Fixed issues where UI changes were not reflected after certain actions, such as ungrouping, deleting, or inserting items. - Improved row locking behavior during add and delete operations to prevent UI inconsistencies. - **Tests** - Added comprehensive tests for row operations and menu interactions to verify UI updates and correct method calls in data views. --------- Co-authored-by: zzj3720 --- .../data-view/src/core/group-by/trait.ts | 10 +- .../src/core/view-manager/single-view.ts | 2 + .../src/view-presets/kanban/mobile/group.ts | 4 + .../kanban/mobile/kanban-view-ui-logic.ts | 4 +- .../src/view-presets/kanban/mobile/menu.ts | 3 + .../kanban/pc/controller/selection.ts | 1 + .../src/view-presets/kanban/pc/group.ts | 4 + .../kanban/pc/kanban-view-ui-logic.ts | 1 + .../src/view-presets/table/mobile/group.ts | 3 + .../src/view-presets/table/mobile/menu.ts | 1 + .../table/pc-virtual/controller/clipboard.ts | 1 + .../table/pc-virtual/controller/hotkeys.ts | 1 + .../table/pc-virtual/controller/selection.ts | 1 + .../pc-virtual/group/bottom/group-footer.ts | 1 + .../pc-virtual/group/top/group-header.ts | 2 + .../view-presets/table/pc-virtual/row/menu.ts | 1 + .../table/pc/controller/clipboard.ts | 1 + .../table/pc/controller/hotkeys.ts | 1 + .../table/pc/controller/selection.ts | 1 + .../src/view-presets/table/pc/group.ts | 3 + .../src/view-presets/table/pc/menu.ts | 1 + .../src/__tests__/data-view-actions.spec.ts | 129 ++++++++++++++++++ 22 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/core/src/__tests__/data-view-actions.spec.ts diff --git a/blocksuite/affine/data-view/src/core/group-by/trait.ts b/blocksuite/affine/data-view/src/core/group-by/trait.ts index 7fa752caca..11ed6fc507 100644 --- a/blocksuite/affine/data-view/src/core/group-by/trait.ts +++ b/blocksuite/affine/data-view/src/core/group-by/trait.ts @@ -112,7 +112,10 @@ export class GroupTrait { return; } const { staticMap, groupInfo } = staticInfo; - const groupMap: Record = { ...staticMap }; + const groupMap: Record = {}; + Object.entries(staticMap).forEach(([key, group]) => { + groupMap[key] = new Group(key, group.value, groupInfo, this); + }); this.view.rows$.value.forEach(row => { const value = this.view.cellGetOrCreate(row.rowId, groupInfo.property.id) .jsonValue$.value; @@ -182,6 +185,7 @@ export class GroupTrait { ) {} addToGroup(rowId: string, key: string) { + this.view.lockRows(false); const groupMap = this.groupDataMap$.value; const groupInfo = this.groupInfo$.value; if (!groupMap || !groupInfo) { @@ -254,6 +258,7 @@ export class GroupTrait { toGroupKey: string, position: InsertToPosition ) { + this.view.lockRows(false); const groupMap = this.groupDataMap$.value; if (!groupMap) { return; @@ -290,6 +295,7 @@ export class GroupTrait { } moveGroupTo(groupKey: string, position: InsertToPosition) { + this.view.lockRows(false); const groups = this.groupsDataList$.value; if (!groups) { return; @@ -305,6 +311,7 @@ export class GroupTrait { } removeFromGroup(rowId: string, key: string) { + this.view.lockRows(false); const groupMap = this.groupDataMap$.value; if (!groupMap) { return; @@ -323,6 +330,7 @@ export class GroupTrait { } updateValue(rows: string[], value: unknown) { + this.view.lockRows(false); const propertyId = this.property$.value?.id; if (!propertyId) { return; diff --git a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts index 6777c5dc5a..0678344dd3 100644 --- a/blocksuite/affine/data-view/src/core/view-manager/single-view.ts +++ b/blocksuite/affine/data-view/src/core/view-manager/single-view.ts @@ -128,6 +128,7 @@ export abstract class SingleViewBase< ); rowsDelete(rows: string[]): void { + this.lockRows(false); this.dataSource.rowDelete(rows); } @@ -258,6 +259,7 @@ export abstract class SingleViewBase< abstract propertyGetOrCreate(propertyId: string): Property; rowAdd(insertPosition: InsertToPosition | number): string { + this.lockRows(false); return this.dataSource.rowAdd(insertPosition); } diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/group.ts b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/group.ts index b52ffa2ef3..0e349cde13 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/group.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/group.ts @@ -61,10 +61,12 @@ export class MobileKanbanGroup extends SignalWatcher( private readonly clickAddCard = () => { this.view.addCard('end', this.group.key); + this.requestUpdate(); }; private readonly clickAddCardInStart = () => { this.view.addCard('start', this.group.key); + this.requestUpdate(); }; private readonly clickGroupOptions = (e: MouseEvent) => { @@ -79,12 +81,14 @@ export class MobileKanbanGroup extends SignalWatcher( this.group.rows.forEach(row => { this.group.manager.removeFromGroup(row.rowId, this.group.key); }); + this.requestUpdate(); }, }), menu.action({ name: 'Delete Cards', select: () => { this.view.rowsDelete(this.group.rows.map(row => row.rowId)); + this.requestUpdate(); }, }), ], diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/kanban-view-ui-logic.ts b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/kanban-view-ui-logic.ts index 98156c700a..faa70c0bfd 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/kanban-view-ui-logic.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/kanban-view-ui-logic.ts @@ -66,7 +66,9 @@ export class MobileKanbanViewUILogic extends DataViewUILogicBase< addRow = (position: InsertToPosition) => { if (this.readonly) return; - return this.view.rowAdd(position); + const id = this.view.rowAdd(position); + this.ui$.value?.requestUpdate(); + return id; }; focusFirstCell = () => {}; diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/menu.ts b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/menu.ts index 7cd4c512ae..660fce99ff 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/mobile/menu.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/mobile/menu.ts @@ -83,6 +83,7 @@ export const popCardMenu = ( { before: true, id: cardId }, groupKey ); + kanbanViewLogic.ui$.value?.requestUpdate(); }, }), menu.action({ @@ -97,6 +98,7 @@ export const popCardMenu = ( { before: false, id: cardId }, groupKey ); + kanbanViewLogic.ui$.value?.requestUpdate(); }, }), ], @@ -111,6 +113,7 @@ export const popCardMenu = ( prefix: DeleteIcon(), select: () => { kanbanViewLogic.view.rowsDelete([cardId]); + kanbanViewLogic.ui$.value?.requestUpdate(); }, }), ], diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts index 8caaaa6227..e4cde95c61 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/controller/selection.ts @@ -128,6 +128,7 @@ export class KanbanSelectionController implements ReactiveController { if (selection.selectionType === 'card') { this.view.rowsDelete(selection.cards.map(v => v.cardId)); this.selection = undefined; + this.logic.ui$.value?.requestUpdate(); } } diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/group.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/group.ts index bda7d61b83..a88d8950fa 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/group.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/group.ts @@ -110,6 +110,7 @@ export class KanbanGroup extends SignalWatcher( isEditing: true, }; }); + this.requestUpdate(); }; private readonly clickAddCardInStart = () => { @@ -127,6 +128,7 @@ export class KanbanGroup extends SignalWatcher( isEditing: true, }; }); + this.requestUpdate(); }; private readonly clickGroupOptions = (e: MouseEvent) => { @@ -139,12 +141,14 @@ export class KanbanGroup extends SignalWatcher( this.group.rows.forEach(row => { this.group.manager.removeFromGroup(row.rowId, this.group.key); }); + this.requestUpdate(); }, }), menu.action({ name: 'Delete Cards', select: () => { this.view.rowsDelete(this.group.rows.map(row => row.rowId)); + this.requestUpdate(); }, }), ]); diff --git a/blocksuite/affine/data-view/src/view-presets/kanban/pc/kanban-view-ui-logic.ts b/blocksuite/affine/data-view/src/view-presets/kanban/pc/kanban-view-ui-logic.ts index a8c715f2bc..b4c8c6fac8 100644 --- a/blocksuite/affine/data-view/src/view-presets/kanban/pc/kanban-view-ui-logic.ts +++ b/blocksuite/affine/data-view/src/view-presets/kanban/pc/kanban-view-ui-logic.ts @@ -73,6 +73,7 @@ export class KanbanViewUILogic extends DataViewUILogicBase< rowId, }); } + this.ui$.value?.requestUpdate(); return rowId; }; diff --git a/blocksuite/affine/data-view/src/view-presets/table/mobile/group.ts b/blocksuite/affine/data-view/src/view-presets/table/mobile/group.ts index 1d292ca760..47569eba6c 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/mobile/group.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/mobile/group.ts @@ -51,10 +51,12 @@ export class MobileTableGroup extends SignalWatcher( private readonly clickAddRow = () => { this.view.rowAdd('end', this.group?.key); + this.requestUpdate(); }; private readonly clickAddRowInStart = () => { this.view.rowAdd('start', this.group?.key); + this.requestUpdate(); }; private readonly clickGroupOptions = (e: MouseEvent) => { @@ -77,6 +79,7 @@ export class MobileTableGroup extends SignalWatcher( name: 'Delete Cards', select: () => { this.view.rowsDelete(group.rows.map(row => row.rowId)); + this.requestUpdate(); }, }), ]); diff --git a/blocksuite/affine/data-view/src/view-presets/table/mobile/menu.ts b/blocksuite/affine/data-view/src/view-presets/table/mobile/menu.ts index 514a7c20cb..151d837e69 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/mobile/menu.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/mobile/menu.ts @@ -38,6 +38,7 @@ export const popMobileRowMenu = ( prefix: DeleteIcon(), select: () => { view.rowsDelete([rowId]); + tableViewLogic.ui$.value?.requestUpdate(); }, }), ], diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/clipboard.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/clipboard.ts index 8cc28bea29..6b016bcd62 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/clipboard.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/clipboard.ts @@ -44,6 +44,7 @@ export class TableClipboardController implements ReactiveController { } if (deleteRows.length) { this.logic.view.rowsDelete(deleteRows); + this.logic.ui$.value?.requestUpdate(); } } this.clipboard diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.ts index f3c1371f73..951969199b 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/hotkeys.ts @@ -30,6 +30,7 @@ export class TableHotkeysController implements ReactiveController { const rows = TableViewRowSelection.rowsIds(selection); this.selectionController.selection = undefined; this.logic.view.rowsDelete(rows); + this.logic.ui$.value?.requestUpdate(); return; } const { diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/selection.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/selection.ts index a5bb364a0d..3b0df94cda 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/selection.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/controller/selection.ts @@ -376,6 +376,7 @@ export class TableSelectionController implements ReactiveController { deleteRow(rowId: string) { this.view.rowsDelete([rowId]); this.focusToCell('up'); + this.logic.ui$.value?.requestUpdate(); } focusFirstCell() { diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/group-footer.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/group-footer.ts index 7d5158c161..457c1d7c83 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/group-footer.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/bottom/group-footer.ts @@ -45,6 +45,7 @@ export class TableGroupFooter extends WithDisposable(ShadowlessElement) { private readonly clickAddRow = () => { const group = this.group$.value; const rowId = this.tableViewManager.rowAdd('end', group?.key); + this.requestUpdate(); requestAnimationFrame(() => { const rowIndex = this.selectionController.getRow(group?.key, rowId) diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts index 39626e2fc0..29411f9f23 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/group/top/group-header.ts @@ -58,6 +58,7 @@ export class TableGroupHeader extends SignalWatcher( return; } this.tableViewManager.rowAdd('start', group.key); + this.requestUpdate(); const selectionController = this.selectionController; selectionController.selection = undefined; requestAnimationFrame(() => { @@ -95,6 +96,7 @@ export class TableGroupHeader extends SignalWatcher( name: 'Delete Cards', select: () => { this.tableViewManager.rowsDelete(group.rows.map(row => row.rowId)); + this.requestUpdate(); }, }), ]); diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/menu.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/menu.ts index 15dd637ad2..a467119e3d 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/menu.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/menu.ts @@ -71,6 +71,7 @@ export const popRowMenu = ( prefix: DeleteIcon(), select: () => { selectionController.view.rowsDelete(rows); + selectionController.logic.ui$.value?.requestUpdate(); }, }), ], diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/clipboard.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/clipboard.ts index d17a874203..869a4bb02c 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/clipboard.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/clipboard.ts @@ -43,6 +43,7 @@ export class TableClipboardController implements ReactiveController { } if (deleteRows.length) { this.logic.view.rowsDelete(deleteRows); + this.logic.ui$.value?.requestUpdate(); } } this.clipboard diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/hotkeys.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/hotkeys.ts index 9191833059..702e7dfc3c 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/hotkeys.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/hotkeys.ts @@ -28,6 +28,7 @@ export class TableHotkeysController implements ReactiveController { const rows = TableViewRowSelection.rowsIds(selection); this.selectionController.selection = undefined; this.logic.view.rowsDelete(rows); + this.logic.ui$.value?.requestUpdate(); return; } const { diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/selection.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/selection.ts index bf6b2cf317..f542938673 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/controller/selection.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/controller/selection.ts @@ -351,6 +351,7 @@ export class TableSelectionController implements ReactiveController { deleteRow(rowId: string) { this.view.rowsDelete([rowId]); this.focusToCell('up'); + this.logic.ui$.value?.requestUpdate(); } focusFirstCell() { diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/group.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/group.ts index 62d2493d4f..460736b0f4 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/group.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/group.ts @@ -83,6 +83,7 @@ export class TableGroup extends SignalWatcher( }, isEditing: true, }); + this.requestUpdate(); }); }; @@ -102,6 +103,7 @@ export class TableGroup extends SignalWatcher( }, isEditing: true, }); + this.requestUpdate(); }); }; @@ -125,6 +127,7 @@ export class TableGroup extends SignalWatcher( name: 'Delete Cards', select: () => { this.view.rowsDelete(group.rows.map(row => row.rowId)); + this.requestUpdate(); }, }), ]); diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/menu.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/menu.ts index 6a02c2f2d1..844f1b5c85 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/menu.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/menu.ts @@ -71,6 +71,7 @@ export const popRowMenu = ( prefix: DeleteIcon(), select: () => { selectionController.view.rowsDelete(rows); + selectionController.logic.ui$.value?.requestUpdate(); }, }), ], diff --git a/packages/frontend/core/src/__tests__/data-view-actions.spec.ts b/packages/frontend/core/src/__tests__/data-view-actions.spec.ts new file mode 100644 index 0000000000..ca1c29bbb5 --- /dev/null +++ b/packages/frontend/core/src/__tests__/data-view-actions.spec.ts @@ -0,0 +1,129 @@ +/* eslint-disable rxjs/finnish */ +import { computed, signal } from '@preact/signals-core'; +import { describe, expect, test, vi } from 'vitest'; + +// mock context-menu utilities +const popFilterableSimpleMenu = vi.fn(); +vi.mock('@blocksuite/affine-components/context-menu', () => ({ + menu: { + action: (opts: any) => opts, + group: (opts: any) => opts, + subMenu: (opts: any) => opts, + }, + // avoid early access during module mocking + popFilterableSimpleMenu: (...args: any[]) => popFilterableSimpleMenu(...args), + popupTargetFromElement: (el: any) => el, +})); + +import { SingleViewBase } from '../../../../../blocksuite/affine/data-view/src/core/view-manager/single-view.js'; +import { MobileKanbanViewUILogic } from '../../../../../blocksuite/affine/data-view/src/view-presets/kanban/mobile/kanban-view-ui-logic.js'; +import { popCardMenu } from '../../../../../blocksuite/affine/data-view/src/view-presets/kanban/mobile/menu.js'; +import { popMobileRowMenu } from '../../../../../blocksuite/affine/data-view/src/view-presets/table/mobile/menu.js'; + +class TestView extends SingleViewBase { + detailProperties$ = computed(() => []); + mainProperties$ = computed(() => ({})); + properties$ = computed(() => []); + propertiesRaw$ = computed(() => []); + readonly$ = computed(() => false); + get type() { + return 'test'; + } + isShow() { + return true; + } + propertyGetOrCreate() { + return {} as any; + } +} + +describe('data view helpers', () => { + test('rowAdd and rowsDelete unlock the view', () => { + const ds = { + rowAdd: vi.fn().mockReturnValue('id'), + rowDelete: vi.fn(), + } as any; + const manager = { dataSource: ds } as any; + const view = new TestView(manager, 'v1'); + view.lockRows(true); + const id = view.rowAdd('end'); + expect(id).toBe('id'); + expect(ds.rowAdd).toHaveBeenCalledWith('end'); + expect(view.isLocked).toBe(false); + + view.lockRows(true); + view.rowsDelete(['a']); + expect(ds.rowDelete).toHaveBeenCalledWith(['a']); + expect(view.isLocked).toBe(false); + }); + + test('MobileKanbanViewUILogic.addRow triggers update', () => { + const root = { + setSelection: vi.fn(), + selection$: signal(undefined), + config: {}, + } as any; + const view = { + readonly$: signal(false), + rowAdd: vi.fn().mockReturnValue('r1'), + groupTrait: {}, + id: 'v', + manager: {}, + } as any; + const logic = new MobileKanbanViewUILogic(root, view); + const update = vi.fn(); + logic.ui$.value = { requestUpdate: update } as any; + + if (!logic.ui$.value) { + throw new Error('UI state must be defined before calling addRow'); + } + + const id = logic.addRow('end'); + expect(id).toBe('r1'); + expect(view.rowAdd).toHaveBeenCalledWith('end'); + expect(update).toHaveBeenCalled(); + }); + + test('popCardMenu actions request update', () => { + const update = vi.fn(); + const kanbanViewLogic = { + view: { + addCard: vi.fn(), + rowsDelete: vi.fn(), + traitGet: () => ({ groupsDataList$: signal([]), moveCardTo: vi.fn() }), + }, + ui$: signal({ requestUpdate: update }), + root: { openDetailPanel: vi.fn() }, + } as any; + popCardMenu({} as any, 'g', 'c', kanbanViewLogic); + const groups = popFilterableSimpleMenu.mock.calls[0][1] as any; + groups[2].items[0].select(); + expect(kanbanViewLogic.view.addCard).toHaveBeenCalledWith( + { before: true, id: 'c' }, + 'g' + ); + expect(update).toHaveBeenCalledTimes(1); + groups[2].items[1].select(); + expect(kanbanViewLogic.view.addCard).toHaveBeenCalledWith( + { before: false, id: 'c' }, + 'g' + ); + groups[3].items[0].select(); + expect(kanbanViewLogic.view.rowsDelete).toHaveBeenCalledWith(['c']); + expect(update).toHaveBeenCalledTimes(3); + }); + + test('popMobileRowMenu delete action requests update', () => { + const update = vi.fn(); + const tableViewLogic = { + ui$: signal({ requestUpdate: update }), + root: { openDetailPanel: vi.fn() }, + } as any; + const view = { rowsDelete: vi.fn() } as any; + popMobileRowMenu({} as any, 'r1', tableViewLogic, view); + const groups = popFilterableSimpleMenu.mock.calls.pop()![1] as any; + groups[1].items[0].select(); + expect(view.rowsDelete).toHaveBeenCalledWith(['r1']); + expect(update).toHaveBeenCalled(); + }); +});