fix(editor): can not undo and redo of color of edgeless blocks (#12414)
Close [BS-3507](https://linear.app/affine-design/issue/BS-3507/edgeless-text-颜色无法-undoredo) Close [BS-3426](https://linear.app/affine-design/issue/BS-3426/frame-修改背景色后不能撤销) This PR fixes the issue where the color change of edgeless blocks could not be undone/redone, including notes, edgeless-text, and frames. It also addresses the problem of a tiny shape being unexpectedly retained on the canvas. The key changes are: - Removal of `transact` from the `pop` method of edgeless elements. - Refactoring of `onPickColor` for all edgeless elements and blocks to better control the lifecycle of custom color property changes. - Addition of the missing custom background color feature for notes. - Addition of undo/redo color tests for notes, frames, and edgeless-text. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added undo and redo support for color changes in frames, notes, and text blocks, allowing users to revert or reapply background and text color modifications. - **Bug Fixes** - Improved reliability of color picker interactions, ensuring consistent state management and transactional updates during color changes. - **Tests** - Introduced new end-to-end tests to verify undo/redo functionality for color changes in frames, notes, and text blocks. - **Refactor** - Streamlined color picker event handling for better maintainability and consistency across toolbars and style panels. - Updated style panel structure and event handling for improved interaction and state management. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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<PickColorEvent>) => {
|
||||
e.stopPropagation();
|
||||
const pickColorWrapper =
|
||||
(field: string, pickCallback: (palette: Palette) => void) =>
|
||||
(e: CustomEvent<PickColorEvent>) => {
|
||||
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<PickColorEvent>) => {
|
||||
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<LineDetailType>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user