diff --git a/blocksuite/affine/blocks/frame/src/frame-toolbar.ts b/blocksuite/affine/blocks/frame/src/frame-toolbar.ts index 084e1a4dd4..d42e3e11b7 100644 --- a/blocksuite/affine/blocks/frame/src/frame-toolbar.ts +++ b/blocksuite/affine/blocks/frame/src/frame-toolbar.ts @@ -160,19 +160,30 @@ const builtinSurfaceToolbarConfig = { background => resolveColor(background, theme) ) ?? DefaultTheme.transparent; const onPick = (e: PickColorEvent) => { - if (e.type === 'pick') { - const color = e.detail.value; - for (const model of models) { - const props = packColor(field, color); - ctx.std - .get(EdgelessCRUDIdentifier) - .updateElement(model.id, props); - } - return; - } - - for (const model of models) { - model[e.type === 'start' ? 'stash' : 'pop'](field); + switch (e.type) { + case 'pick': + { + const color = e.detail.value; + const props = packColor(field, color); + const crud = ctx.std.get(EdgelessCRUDIdentifier); + models.forEach(model => { + crud.updateElement(model.id, props); + }); + } + break; + case 'start': + ctx.store.captureSync(); + models.forEach(model => { + model.stash(field); + }); + break; + case 'end': + ctx.store.transact(() => { + models.forEach(model => { + model.pop(field); + }); + }); + break; } }; diff --git a/blocksuite/affine/blocks/note/src/components/edgeless-note-style-panel.ts b/blocksuite/affine/blocks/note/src/components/edgeless-note-style-panel.ts index 2e4b23c3d3..6de59d66b5 100644 --- a/blocksuite/affine/blocks/note/src/components/edgeless-note-style-panel.ts +++ b/blocksuite/affine/blocks/note/src/components/edgeless-note-style-panel.ts @@ -40,12 +40,12 @@ export class EdgelessNoteStylePanel extends SignalWatcher( @property({ attribute: false }) accessor std!: BlockStdScope; - @query('.edgeless-note-style-panel') - private accessor _panel!: HTMLDivElement; - @state() accessor tabType: 'style' | 'customColor' = 'style'; + @query('div.edgeless-note-style-panel-container') + accessor container!: HTMLDivElement; + static override styles = css` .edgeless-note-style-panel { display: flex; @@ -187,7 +187,32 @@ export class EdgelessNoteStylePanel extends SignalWatcher( }; private readonly _pickColor = (e: PickColorEvent) => { - console.log(e); + switch (e.type) { + case 'pick': + { + const color = e.detail.value; + const crud = this.std.get(EdgelessCRUDIdentifier); + this.notes.forEach(note => { + crud.updateElement(note.id, { + background: color, + } satisfies Partial); + }); + } + break; + case 'start': + this._beforeChange(); + this.notes.forEach(note => { + note.stash('background'); + }); + break; + case 'end': + this.std.store.transact(() => { + this.notes.forEach(note => { + note.pop('background'); + }); + }); + break; + } }; private readonly _selectShadow = (e: CustomEvent) => { @@ -265,7 +290,7 @@ export class EdgelessNoteStylePanel extends SignalWatcher( }; private _renderStylePanel() { - return html`
+ return html`
Fill color
{ - e.stopPropagation(); - }); + if (this.container) { + this.disposables.addFromEvent(this.container, 'click', e => { + e.stopPropagation(); + }); + } } override render() { @@ -383,11 +410,18 @@ export class EdgelessNoteStylePanel extends SignalWatcher( ${PaletteIcon()} `} + @toggle=${(e: CustomEvent) => { + if (!e.detail) { + this.tabType = 'style'; + } + }} > - ${choose(this.tabType, [ - ['style', () => this._renderStylePanel()], - ['customColor', () => this._renderCustomColorPanel()], - ])} +
+ ${choose(this.tabType, [ + ['style', () => this._renderStylePanel()], + ['customColor', () => this._renderCustomColorPanel()], + ])} +
`; } diff --git a/blocksuite/affine/gfx/brush/src/toolbar/configs/brush.ts b/blocksuite/affine/gfx/brush/src/toolbar/configs/brush.ts index c1ef1c6340..b803cc18e1 100644 --- a/blocksuite/affine/gfx/brush/src/toolbar/configs/brush.ts +++ b/blocksuite/affine/gfx/brush/src/toolbar/configs/brush.ts @@ -68,19 +68,30 @@ export const brushToolbarConfig = { resolveColor(color, theme) ) ?? resolveColor(DefaultTheme.black, theme); const onPick = (e: PickColorEvent) => { - if (e.type === 'pick') { - const color = e.detail.value; - for (const model of models) { - const props = packColor(field, color); - ctx.std - .get(EdgelessCRUDIdentifier) - .updateElement(model.id, props); - } - return; - } - - for (const model of models) { - model[e.type === 'start' ? 'stash' : 'pop'](field); + switch (e.type) { + case 'pick': + { + const color = e.detail.value; + const props = packColor(field, color); + const crud = ctx.std.get(EdgelessCRUDIdentifier); + models.forEach(model => { + crud.updateElement(model.id, props); + }); + } + break; + case 'start': + ctx.store.captureSync(); + models.forEach(model => { + model.stash(field); + }); + break; + case 'end': + ctx.store.transact(() => { + models.forEach(model => { + model.pop(field); + }); + }); + break; } }; diff --git a/blocksuite/affine/gfx/connector/src/toolbar/config.ts b/blocksuite/affine/gfx/connector/src/toolbar/config.ts index 2a43a86751..0a1c1fa6e6 100644 --- a/blocksuite/affine/gfx/connector/src/toolbar/config.ts +++ b/blocksuite/affine/gfx/connector/src/toolbar/config.ts @@ -148,19 +148,30 @@ export const connectorToolbarConfig = { ) ?? resolveColor(DefaultTheme.connectorColor, theme); const onPickColor = (e: PickColorEvent) => { - if (e.type === 'pick') { - const color = e.detail.value; - for (const model of models) { - const props = packColor(field, color); - ctx.std - .get(EdgelessCRUDIdentifier) - .updateElement(model.id, props); - } - return; - } - - for (const model of models) { - model[e.type === 'start' ? 'stash' : 'pop'](field); + switch (e.type) { + case 'pick': + { + const color = e.detail.value; + const props = packColor(field, color); + const crud = ctx.std.get(EdgelessCRUDIdentifier); + models.forEach(model => { + crud.updateElement(model.id, props); + }); + } + break; + case 'start': + ctx.store.captureSync(); + models.forEach(model => { + model.stash(field); + }); + break; + case 'end': + ctx.store.transact(() => { + models.forEach(model => { + model.pop(field); + }); + }); + break; } }; diff --git a/blocksuite/affine/gfx/shape/src/toolbar/config.ts b/blocksuite/affine/gfx/shape/src/toolbar/config.ts index 9e660ee014..5a6a146c6a 100644 --- a/blocksuite/affine/gfx/shape/src/toolbar/config.ts +++ b/blocksuite/affine/gfx/shape/src/toolbar/config.ts @@ -15,6 +15,7 @@ import { isTransparent, LineWidth, MindmapElementModel, + type Palette, resolveColor, ShapeElementModel, type ShapeName, @@ -167,56 +168,53 @@ export const shapeToolbarConfig = { const strokeStyle = getMostCommonValue(mapped, 'strokeStyle') ?? StrokeStyle.Solid; - const onPickFillColor = (e: CustomEvent) => { - e.stopPropagation(); + const pickColorWrapper = + (field: string, pickCallback: (palette: Palette) => void) => + (e: CustomEvent) => { + e.stopPropagation(); - const d = e.detail; - - const field = 'fillColor'; - - if (d.type === 'pick') { - const value = d.detail.value; - const filled = isTransparent(value); - for (const model of models) { - const props = packColor(field, value); - // If `filled` can be set separately, this logic can be removed - if (field && !model.filled) { - const color = getTextColor(value, filled); - Object.assign(props, { filled, color }); - } - ctx.std - .get(EdgelessCRUDIdentifier) - .updateElement(model.id, props); + switch (e.detail.type) { + case 'pick': + pickCallback(e.detail.detail); + break; + case 'start': + ctx.store.captureSync(); + models.forEach(model => { + model.stash(field); + }); + break; + case 'end': + ctx.store.transact(() => { + models.forEach(model => { + model.pop(field); + }); + }); } - return; - } + }; - for (const model of models) { - model[d.type === 'start' ? 'stash' : 'pop'](field); - } - }; - const onPickStrokeColor = (e: CustomEvent) => { - e.stopPropagation(); - - const d = e.detail; - - const field = 'strokeColor'; - - if (d.type === 'pick') { - const value = d.detail.value; - for (const model of models) { - const props = packColor(field, value); - ctx.std - .get(EdgelessCRUDIdentifier) - .updateElement(model.id, props); + const onPickFillColor = pickColorWrapper('fillColor', palette => { + const value = palette.value; + const filled = isTransparent(value); + const props = packColor('fillColor', value); + const crud = ctx.std.get(EdgelessCRUDIdentifier); + models.forEach(model => { + if (filled && !model.filled) { + const color = getTextColor(value, filled); + Object.assign(props, { filled, color }); } - return; - } + crud.updateElement(model.id, props); + }); + }); + + const onPickStrokeColor = pickColorWrapper('strokeColor', palette => { + const value = palette.value; + const props = packColor('strokeColor', value); + const crud = ctx.std.get(EdgelessCRUDIdentifier); + models.forEach(model => { + crud.updateElement(model.id, props); + }); + }); - for (const model of models) { - model[d.type === 'start' ? 'stash' : 'pop'](field); - } - }; const onPickStrokeStyle = (e: CustomEvent) => { e.stopPropagation(); diff --git a/blocksuite/affine/gfx/text/src/toolbar/actions.ts b/blocksuite/affine/gfx/text/src/toolbar/actions.ts index 9a9d50218b..acbad863ce 100644 --- a/blocksuite/affine/gfx/text/src/toolbar/actions.ts +++ b/blocksuite/affine/gfx/text/src/toolbar/actions.ts @@ -210,17 +210,29 @@ export function createTextActions< ) ?? resolveColor(defaultColor, theme); const onPick = (e: PickColorEvent) => { - if (e.type === 'pick') { - const color = e.detail.value; - for (const model of models) { - const props = packColor(field, color); - update(ctx, model, props); - } - return; - } - - for (const model of models) { - stash(model, e.type === 'start' ? 'stash' : 'pop', field); + switch (e.type) { + case 'pick': + { + const color = e.detail.value; + const props = packColor(field, color); + models.forEach(model => { + update(ctx, model, props); + }); + } + break; + case 'start': + ctx.store.captureSync(); + models.forEach(model => { + stash(model, 'stash', field); + }); + break; + case 'end': + ctx.store.transact(() => { + models.forEach(model => { + stash(model, 'pop', field); + }); + }); + break; } }; diff --git a/blocksuite/framework/std/src/gfx/model/surface/element-model.ts b/blocksuite/framework/std/src/gfx/model/surface/element-model.ts index 22fbfb8e11..5ba9ffaa24 100644 --- a/blocksuite/framework/std/src/gfx/model/surface/element-model.ts +++ b/blocksuite/framework/std/src/gfx/model/surface/element-model.ts @@ -278,9 +278,7 @@ export abstract class GfxPrimitiveElementModel< if (getFieldPropsSet(this).has(prop as string)) { if (!isEqual(value, this.yMap.get(prop as string))) { - this.surface.store.transact(() => { - this.yMap.set(prop as string, value); - }); + this.yMap.set(prop as string, value); } } else { console.warn('pop a prop that is not field or local:', prop); diff --git a/tests/affine-desktop/e2e/workspace.spec.ts b/tests/affine-desktop/e2e/workspace.spec.ts index dc313147bb..e167c59955 100644 --- a/tests/affine-desktop/e2e/workspace.spec.ts +++ b/tests/affine-desktop/e2e/workspace.spec.ts @@ -2,7 +2,10 @@ import path from 'node:path'; import type { apis } from '@affine/electron-api'; import { test } from '@affine-test/kit/electron'; -import { getBlockSuiteEditorTitle } from '@affine-test/kit/utils/page-logic'; +import { + getBlockSuiteEditorTitle, + waitForEditorLoad, +} from '@affine-test/kit/utils/page-logic'; import { clickNewPageButton, clickSideBarCurrentWorkspaceBanner, @@ -99,8 +102,8 @@ test('export then add', async ({ page, appInfo, workspace }) => { await page.waitForTimeout(1000); // find button which has the title "test1" - await page.getByText('test1').click(); - + await page.getByTestId('page-list-item').getByText('test1').click(); + await waitForEditorLoad(page); const title = page.locator('[data-block-is-title] >> text="test1"'); await expect(title).toBeVisible(); }); diff --git a/tests/blocksuite/e2e/edgeless/edgeless-text.spec.ts b/tests/blocksuite/e2e/edgeless/edgeless-text.spec.ts index fb646f54f0..5794161102 100644 --- a/tests/blocksuite/e2e/edgeless/edgeless-text.spec.ts +++ b/tests/blocksuite/e2e/edgeless/edgeless-text.spec.ts @@ -6,7 +6,9 @@ import { autoFit, captureHistory, cutByKeyboard, + dblclickView, dragBetweenIndices, + edgelessCommonSetup, enterPlaygroundRoom, getEdgelessSelectedRect, getPageSnapshot, @@ -19,6 +21,7 @@ import { pressBackspace, pressEnter, pressEscape, + redoByKeyboard, selectAllByKeyboard, setEdgelessTool, switchEditorMode, @@ -598,3 +601,62 @@ test('press backspace at the start of first line when edgeless text exist', asyn `${testInfo.title}_finial.json` ); }); + +test('undo/redo should work when changing text color', async ({ page }) => { + await edgelessCommonSetup(page); + await dblclickView(page, [100, 100]); + await type(page, 'abc'); + await pressEscape(page, 3); + await waitNextFrame(page); + + const edgelessText = page.locator('affine-edgeless-text'); + await edgelessText.click(); + + const getTextColor = async () => { + return edgelessText.locator('span[data-v-text="true"]').evaluate(el => { + return getComputedStyle(el).color; + }); + }; + const colorPanel = page.locator('edgeless-color-picker-button'); + + let prevTextColor = await getTextColor(); + + // preset color + { + await colorPanel.click(); + await colorPanel.getByLabel('LightRed').click(); + expect(await getTextColor()).not.toBe(prevTextColor); + + await undoByKeyboard(page); + await waitNextFrame(page); + expect(await getTextColor()).toBe(prevTextColor); + + await redoByKeyboard(page); + await waitNextFrame(page); + expect(await getTextColor()).not.toBe(prevTextColor); + } + + prevTextColor = await getTextColor(); + + // custom color + { + await colorPanel.click(); + await colorPanel.locator('edgeless-color-custom-button').click(); + await page.locator('.color-palette').click({ + position: { + x: 100, + y: 100, + }, + }); + await pressEscape(page); + + expect(await getTextColor()).not.toBe(prevTextColor); + await undoByKeyboard(page); + await waitNextFrame(page); + expect(await getTextColor()).toBe(prevTextColor); + + await redoByKeyboard(page); + await waitNextFrame(page); + expect(await getTextColor()).not.toBe(prevTextColor); + } +}); diff --git a/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts b/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts index dba64ded24..44c318a5df 100644 --- a/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts +++ b/tests/blocksuite/e2e/edgeless/frame/frame.spec.ts @@ -24,9 +24,11 @@ import { import { pressBackspace, pressEscape, + redoByKeyboard, SHORT_KEY, undoByKeyboard, } from '../../utils/actions/keyboard.js'; +import { waitNextFrame } from '../../utils/actions/misc.js'; import { assertCanvasElementsCount, assertContainerChildCount, @@ -420,3 +422,59 @@ test('undo should work when create a frame by dragging', async ({ page }) => { await undoByKeyboard(page); await expect(page.locator('affine-frame')).toHaveCount(0); }); + +test('undo/redo should work when change frame background', async ({ page }) => { + await createFrame(page, [50, 50], [450, 450]); + await pressEscape(page); + + const frameTitle = page.locator('affine-frame-title'); + await frameTitle.click(); + + const getFrameBackground = async () => { + return page.locator('affine-frame .affine-frame-container').evaluate(el => { + return getComputedStyle(el).backgroundColor; + }); + }; + const colorPanel = page.locator('edgeless-color-picker-button'); + + let prevBackground = await getFrameBackground(); + + // preset color + { + await colorPanel.click(); + await colorPanel.getByLabel('LightRed').click(); + expect(await getFrameBackground()).not.toBe(prevBackground); + + await undoByKeyboard(page); + await waitNextFrame(page); + expect(await getFrameBackground()).toBe(prevBackground); + + await redoByKeyboard(page); + await waitNextFrame(page); + expect(await getFrameBackground()).not.toBe(prevBackground); + } + + prevBackground = await getFrameBackground(); + + // custom color + { + await colorPanel.click(); + await colorPanel.locator('edgeless-color-custom-button').click(); + await page.locator('.color-palette').click({ + position: { + x: 100, + y: 100, + }, + }); + await pressEscape(page); + + expect(await getFrameBackground()).not.toBe(prevBackground); + await undoByKeyboard(page); + await waitNextFrame(page); + expect(await getFrameBackground()).toBe(prevBackground); + + await redoByKeyboard(page); + await waitNextFrame(page); + expect(await getFrameBackground()).not.toBe(prevBackground); + } +}); diff --git a/tests/blocksuite/e2e/edgeless/note/undo-redo.spec.ts b/tests/blocksuite/e2e/edgeless/note/undo-redo.spec.ts index 8513f265b3..5ddcc9e797 100644 --- a/tests/blocksuite/e2e/edgeless/note/undo-redo.spec.ts +++ b/tests/blocksuite/e2e/edgeless/note/undo-redo.spec.ts @@ -13,6 +13,7 @@ import { initEmptyEdgelessState, initSixParagraphs, pasteByKeyboard, + pressEscape, redoByClick, redoByKeyboard, selectNoteInEdgeless, @@ -137,3 +138,63 @@ test('continuous undo and redo (note block add operation) should work', async ({ count = await countBlock(page, 'affine-edgeless-note'); expect(count).toBe(4); }); + +test('undo/redo should work when change note custom background', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + const { noteId } = await initEmptyEdgelessState(page); + await switchEditorMode(page); + await selectNoteInEdgeless(page, noteId); + + const getNoteBackground = async () => { + return page.locator('edgeless-note-background > div').evaluate(el => { + return getComputedStyle(el).backgroundColor; + }); + }; + + const stylePanel = page.locator('edgeless-note-style-panel'); + + let prevBackground = await getNoteBackground(); + + // preset color + { + await stylePanel.click(); + await stylePanel.getByLabel('Red').click(); + await pressEscape(page); + + expect(await getNoteBackground()).not.toBe(prevBackground); + await undoByKeyboard(page); + await waitNextFrame(page); + expect(await getNoteBackground()).toBe(prevBackground); + + await redoByKeyboard(page); + await waitNextFrame(page); + expect(await getNoteBackground()).not.toBe(prevBackground); + } + + prevBackground = await getNoteBackground(); + + // custom color + { + await selectNoteInEdgeless(page, noteId); + await stylePanel.click(); + await stylePanel.locator('edgeless-color-custom-button').click(); + await stylePanel.locator('.color-palette').click({ + position: { + x: 100, + y: 100, + }, + }); + await pressEscape(page); + + expect(await getNoteBackground()).not.toBe(prevBackground); + await undoByKeyboard(page); + await waitNextFrame(page); + expect(await getNoteBackground()).toBe(prevBackground); + + await redoByKeyboard(page); + await waitNextFrame(page); + expect(await getNoteBackground()).not.toBe(prevBackground); + } +});