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 7fa752cac..11ed6fc50 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 6777c5dc5..0678344dd 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 b52ffa2ef..0e349cde1 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 98156c700..faa70c0bf 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 7cd4c512a..660fce99f 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 8caaaa622..e4cde95c6 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 bda7d61b8..a88d8950f 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 a8c715f2b..b4c8c6fac 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 1d292ca76..47569eba6 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 514a7c20c..151d837e6 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 8cc28bea2..6b016bcd6 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 f3c1371f7..951969199 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 a5bb364a0..3b0df94cd 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 7d5158c16..457c1d7c8 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 39626e2fc..29411f9f2 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 15dd637ad..a467119e3 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 d17a87420..869a4bb02 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 919183305..702e7dfc3 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 bf6b2cf31..f54293867 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 62d2493d4..460736b0f 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 6a02c2f2d..844f1b5c8 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 000000000..ca1c29bbb --- /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(); + }); +});