diff --git a/blocksuite/affine/data-view/src/__tests__/hotkeys.unit.spec.ts b/blocksuite/affine/data-view/src/__tests__/hotkeys.unit.spec.ts index 515dce0f4..17f6e2ad6 100644 --- a/blocksuite/affine/data-view/src/__tests__/hotkeys.unit.spec.ts +++ b/blocksuite/affine/data-view/src/__tests__/hotkeys.unit.spec.ts @@ -1,5 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; +import { multiSelectPropertyType } from '../property-presets/multi-select/define.js'; +import { selectPropertyType } from '../property-presets/select/define.js'; import { TableHotkeysController } from '../view-presets/table/pc/controller/hotkeys.js'; import { TableHotkeysController as VirtualHotkeysController } from '../view-presets/table/pc-virtual/controller/hotkeys.js'; import { @@ -7,6 +9,11 @@ import { TableViewRowSelection, } from '../view-presets/table/selection'; +const TAG_COLUMN_TYPES = [ + selectPropertyType.type, + multiSelectPropertyType.type, +] as const; + function createLogic() { const view = { rowsDelete: vi.fn(), @@ -66,7 +73,10 @@ describe('TableHotkeysController', () => { const cell = { rowId: 'r1', dataset: { rowId: 'r1', columnId: 'c1' }, - column: { valueSetFromString: vi.fn() }, + column: { + valueSetFromString: vi.fn(), + type$: { value: 'text' }, + }, }; selectionController.getCellContainer.mockReturnValue(cell); selectionController.selection = TableViewAreaSelection.create({ @@ -85,6 +95,41 @@ describe('TableHotkeysController', () => { expect(selectionController.selection.isEditing).toBe(true); expect(evt.preventDefault).toHaveBeenCalled(); }); + + it.each(TAG_COLUMN_TYPES)( + 'stages draft for %s column instead of valueSetFromString', + columnType => { + const { logic, selectionController } = createLogic(); + const ctrl = new TableHotkeysController(logic as any); + ctrl.hostConnected(); + const setTagDraft = vi.fn(); + const cell = { + rowId: 'r1', + dataset: { rowId: 'r1', columnId: 'c1' }, + column: { + valueSetFromString: vi.fn(), + type$: { value: columnType }, + }, + setTagDraft, + }; + selectionController.getCellContainer.mockReturnValue(cell); + selectionController.selection = TableViewAreaSelection.create({ + focus: { rowIndex: 0, columnIndex: 0 }, + isEditing: false, + }); + const evt = { + key: 'C', + metaKey: false, + ctrlKey: false, + altKey: false, + preventDefault: vi.fn(), + }; + logic.keyDown({ get: () => ({ raw: evt }) }); + expect(cell.column.valueSetFromString).not.toHaveBeenCalled(); + expect(setTagDraft).toHaveBeenCalledWith('C'); + expect(selectionController.selection.isEditing).toBe(true); + } + ); }); describe('Virtual TableHotkeysController', () => { @@ -95,7 +140,12 @@ describe('Virtual TableHotkeysController', () => { const cell = { rowId: 'r1', dataset: { rowId: 'r1', columnId: 'c1' }, - column$: { value: { valueSetFromString: vi.fn() } }, + column$: { + value: { + valueSetFromString: vi.fn(), + type$: { value: 'text' }, + }, + }, }; selectionController.getCellContainer.mockReturnValue(cell); selectionController.selection = TableViewAreaSelection.create({ @@ -117,4 +167,41 @@ describe('Virtual TableHotkeysController', () => { expect(selectionController.selection.isEditing).toBe(true); expect(evt.preventDefault).toHaveBeenCalled(); }); + + it.each(TAG_COLUMN_TYPES)( + 'stages draft for %s column instead of valueSetFromString', + columnType => { + const { logic, selectionController } = createLogic(); + const ctrl = new VirtualHotkeysController(logic as any); + ctrl.hostConnected(); + const setTagDraft = vi.fn(); + const cell = { + rowId: 'r1', + dataset: { rowId: 'r1', columnId: 'c1' }, + column$: { + value: { + valueSetFromString: vi.fn(), + type$: { value: columnType }, + }, + }, + setTagDraft, + }; + selectionController.getCellContainer.mockReturnValue(cell); + selectionController.selection = TableViewAreaSelection.create({ + focus: { rowIndex: 1, columnIndex: 0 }, + isEditing: false, + }); + const evt = { + key: 'C', + metaKey: false, + ctrlKey: false, + altKey: false, + preventDefault: vi.fn(), + }; + logic.keyDown({ get: () => ({ raw: evt }) }); + expect(cell.column$.value.valueSetFromString).not.toHaveBeenCalled(); + expect(setTagDraft).toHaveBeenCalledWith('C'); + expect(selectionController.selection.isEditing).toBe(true); + } + ); }); diff --git a/blocksuite/affine/data-view/src/core/component/tags/multi-tag-select.ts b/blocksuite/affine/data-view/src/core/component/tags/multi-tag-select.ts index 06fd71859..9947b9626 100644 --- a/blocksuite/affine/data-view/src/core/component/tags/multi-tag-select.ts +++ b/blocksuite/affine/data-view/src/core/component/tags/multi-tag-select.ts @@ -69,8 +69,20 @@ export type TagManagerOptions = { options: ReadonlySignal; onOptionsChange: (options: SelectTag[]) => void; onComplete?: () => void; + initialDraftText?: string; }; +// parent elements that can consume tag draft +const TABLE_CELL_HOST_SELECTOR = + 'dv-table-view-cell-container, affine-database-virtual-cell-container'; + +export function consumeTagDraftFromTableCellHost( + fromElement: Element +): string | undefined { + const host = fromElement.closest(TABLE_CELL_HOST_SELECTOR) as any; + return host?.consumeTagDraft?.(); +} + class TagManager { changeTag = (option: Partial) => { this.ops.onOptionsChange( @@ -427,6 +439,15 @@ export class MultiTagSelect extends SignalWatcher( ); } + override connectedCallback() { + super.connectedCallback(); + const draft = this.initialDraftText; + if (draft != null && draft !== '') { + this.tagManager.text$.value = draft; + this.initialDraftText = undefined; + } + } + protected override firstUpdated() { const disposables = this.disposables; this.classList.add(tagSelectContainerStyle); @@ -471,6 +492,9 @@ export class MultiTagSelect extends SignalWatcher( @property({ attribute: false }) accessor value!: ReadonlySignal; + + @property({ attribute: false }) + accessor initialDraftText: string | undefined; } declare global { @@ -481,6 +505,9 @@ declare global { const popMobileTagSelect = (target: PopupTarget, ops: TagSelectOptions) => { const tagManager = new TagManager(ops); + if (ops.initialDraftText) { + tagManager.text$.value = ops.initialDraftText; + } const onInput = (e: InputEvent) => { tagManager.text$.value = (e.target as HTMLInputElement).value; }; @@ -604,6 +631,7 @@ export const popTagSelect = (target: PopupTarget, ops: TagSelectOptions) => { component.onChange = ops.onChange; component.options = ops.options; component.onOptionsChange = ops.onOptionsChange; + component.initialDraftText = ops.initialDraftText; component.onComplete = () => { ops.onComplete?.(); remove(); diff --git a/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts index c424cf65d..111e41be4 100644 --- a/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/multi-select/cell-renderer.ts @@ -2,7 +2,10 @@ import { popupTargetFromElement } from '@blocksuite/affine-components/context-me import { computed } from '@preact/signals-core'; import { html } from 'lit/static-html.js'; -import { popTagSelect } from '../../core/component/tags/multi-tag-select.js'; +import { + consumeTagDraftFromTableCellHost, + popTagSelect, +} from '../../core/component/tags/multi-tag-select.js'; import type { SelectTag } from '../../core/index.js'; import { BaseCellRenderer } from '../../core/property/index.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; @@ -19,6 +22,7 @@ export class MultiSelectCell extends BaseCellRenderer< > { closePopup?: () => void; private readonly popTagSelect = () => { + const initialDraftText = consumeTagDraftFromTableCellHost(this); this.closePopup = popTagSelect(popupTargetFromElement(this), { name: this.cell.property.name$.value, options: this.options$, @@ -29,6 +33,7 @@ export class MultiSelectCell extends BaseCellRenderer< }, onComplete: this._editComplete, minWidth: 400, + initialDraftText, }); }; diff --git a/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts b/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts index 617c57525..143fbb9f4 100644 --- a/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts +++ b/blocksuite/affine/data-view/src/property-presets/select/cell-renderer.ts @@ -2,7 +2,10 @@ import { popupTargetFromElement } from '@blocksuite/affine-components/context-me import { computed } from '@preact/signals-core'; import { html } from 'lit/static-html.js'; -import { popTagSelect } from '../../core/component/tags/multi-tag-select.js'; +import { + consumeTagDraftFromTableCellHost, + popTagSelect, +} from '../../core/component/tags/multi-tag-select.js'; import type { SelectTag } from '../../core/index.js'; import { BaseCellRenderer } from '../../core/property/index.js'; import { createFromBaseCellRenderer } from '../../core/property/renderer.js'; @@ -20,6 +23,7 @@ export class SelectCell extends BaseCellRenderer< > { closePopup?: () => void; private readonly popTagSelect = () => { + const initialDraftText = consumeTagDraftFromTableCellHost(this); this.closePopup = popTagSelect(popupTargetFromElement(this), { name: this.cell.property.name$.value, mode: 'single', @@ -31,6 +35,7 @@ export class SelectCell extends BaseCellRenderer< }, onComplete: this._editComplete, minWidth: 400, + initialDraftText, }); }; diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/cell.ts b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/cell.ts index b95dd17e0..a000d2e67 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/cell.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc-virtual/row/cell.ts @@ -181,6 +181,19 @@ export class DatabaseCellContainer extends SignalWatcher( }, }); } + + private _tagDraft: string | undefined; + + setTagDraft(value: string) { + this._tagDraft = value; + } + + consumeTagDraft(): string | undefined { + const value = this._tagDraft; + this._tagDraft = undefined; + return value; + } + isEditing$ = signal(false); rowIndex$ = computed(() => { diff --git a/blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts b/blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts index 32a0825e6..c426fb209 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/pc/cell.ts @@ -46,6 +46,18 @@ export class TableViewCellContainer extends SignalWatcher( @property({ attribute: false }) accessor rowId!: string; + private _tagDraft: string | undefined; + + setTagDraft(value: string) { + this._tagDraft = value; + } + + consumeTagDraft(): string | undefined { + const value = this._tagDraft; + this._tagDraft = undefined; + return value; + } + cell$ = computed(() => { return this.column.cellGetOrCreate(this.rowId); }); diff --git a/blocksuite/affine/data-view/src/view-presets/table/utils.ts b/blocksuite/affine/data-view/src/view-presets/table/utils.ts index 2d35e27d9..e04881f9c 100644 --- a/blocksuite/affine/data-view/src/view-presets/table/utils.ts +++ b/blocksuite/affine/data-view/src/view-presets/table/utils.ts @@ -1,13 +1,26 @@ +import type { ReadonlySignal } from '@preact/signals-core'; + +import { multiSelectPropertyType } from '../../property-presets/multi-select/define.js'; +import { selectPropertyType } from '../../property-presets/select/define.js'; import type { TableViewSelectionWithType } from './selection'; import { TableViewRowSelection } from './selection'; export interface TableCell { rowId: string; + setTagDraft?(value: string): void; } -export type ColumnAccessor = ( - cell: T -) => { valueSetFromString(rowId: string, value: string): void } | undefined; +const TAG_COLUMN_TYPES = new Set([ + selectPropertyType.type, + multiSelectPropertyType.type, +]); + +export type ColumnAccessor = (cell: T) => + | { + valueSetFromString(rowId: string, value: string): void; + type$: ReadonlySignal; + } + | undefined; export interface StartEditOptions { event: KeyboardEvent; @@ -48,7 +61,13 @@ export function handleCharStartEdit( ); if (cell) { const column = getColumn(cell); - column?.valueSetFromString(cell.rowId, event.key); + if (column) { + if (TAG_COLUMN_TYPES.has(column.type$.value) && cell.setTagDraft) { + cell.setTagDraft(event.key); + } else { + column.valueSetFromString(cell.rowId, event.key); + } + } updateSelection({ ...selection, isEditing: true }); event.preventDefault(); return true;