Compare commits

..

3 Commits

Author SHA1 Message Date
zzj3720 0ef01002b4 fix(editor): slice to snapshot supports flat data structures 2025-02-27 15:29:17 +08:00
Saul-Mirone b7598ff177 chore: optimize code 2025-02-27 12:57:09 +08:00
zzj3720 b07eb5678d fix(editor): toDraftModal supports flat data structures 2025-02-27 12:39:20 +08:00
44 changed files with 191 additions and 654 deletions
@@ -1,9 +1,5 @@
import type { Color, ColorScheme, Palette } from '@blocksuite/affine-model';
import {
DefaultTheme,
isTransparent,
resolveColor,
} from '@blocksuite/affine-model';
import { isTransparent, resolveColor } from '@blocksuite/affine-model';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { ColorEvent } from '@blocksuite/affine-shared/utils';
import { css, html, LitElement, nothing, svg, type TemplateResult } from 'lit';
@@ -257,7 +253,7 @@ export class EdgelessColorPanel extends LitElement {
accessor openColorPicker!: (e: MouseEvent) => void;
@property({ type: Array })
accessor palettes: readonly Palette[] = DefaultTheme.Palettes;
accessor palettes: readonly Palette[] = [];
@property({ attribute: false })
accessor theme!: ColorScheme;
@@ -1,4 +1,8 @@
import { type ColorScheme, type StrokeStyle } from '@blocksuite/affine-model';
import {
type ColorScheme,
DefaultTheme,
type StrokeStyle,
} from '@blocksuite/affine-model';
import type { ColorEvent } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/utils';
import { css, html, LitElement } from 'lit';
@@ -40,6 +44,7 @@ export class StrokeStylePanel extends WithDisposable(LitElement) {
aria-label="Border colors"
.value=${this.strokeColor}
.theme=${this.theme}
.palettes=${DefaultTheme.Palettes}
.hollowCircle=${this.hollowCircle}
@select=${(e: ColorEvent) => this.setStrokeColor(e)}
>
@@ -65,7 +65,7 @@ export class EdgelessBrushMenu extends EdgelessToolbarToolMixin(
class="one-way"
.value=${this._props$.value.color}
.theme=${this._theme$.value}
.palettes=${DefaultTheme.StrokeColorShortPalettes}
.palettes=${DefaultTheme.StrokeColorPalettes}
.hasTransparent=${!this.edgeless.doc
.get(FeatureFlagService)
.getFlag('enable_color_picker')}
@@ -133,7 +133,7 @@ export class EdgelessConnectorMenu extends EdgelessToolbarToolMixin(
class="one-way"
.value=${stroke}
.theme=${this._theme$.value}
.palettes=${DefaultTheme.StrokeColorShortPalettes}
.palettes=${DefaultTheme.StrokeColorPalettes}
.hasTransparent=${!this.edgeless.doc
.get(FeatureFlagService)
.getFlag('enable_color_picker')}
@@ -75,10 +75,9 @@ export class EdgelessShapeMenu extends SignalWatcher(
const filled = !isTransparent(value);
const fillColor = value;
const strokeColor = filled
? DefaultTheme.StrokeColorShortPalettes.find(
palette => palette.key === key
)?.value
: DefaultTheme.StrokeColorShortMap.Grey;
? DefaultTheme.StrokeColorPalettes.find(palette => palette.key === key)
?.value
: DefaultTheme.StrokeColorMap.Grey;
const { shapeName } = this._props$.value;
this.edgeless.std
@@ -174,7 +173,7 @@ export class EdgelessShapeMenu extends SignalWatcher(
class="one-way"
.value=${fillColor}
.theme=${this._theme$.value}
.palettes=${DefaultTheme.FillColorShortPalettes}
.palettes=${DefaultTheme.FillColorPalettes}
.hasTransparent=${!this.edgeless.doc
.get(FeatureFlagService)
.getFlag('enable_color_picker')}
@@ -33,7 +33,7 @@ export class EdgelessTextMenu extends EdgelessToolbarToolMixin(LitElement) {
class="one-way"
.value=${this.color}
.theme=${this._theme$.value}
.palettes=${DefaultTheme.StrokeColorShortPalettes}
.palettes=${DefaultTheme.StrokeColorPalettes}
@select=${(e: ColorEvent) => this.onChange({ color: e.detail })}
></edgeless-color-panel>
</div>
@@ -134,12 +134,13 @@ export class EdgelessChangeBrushButton extends WithDisposable(LitElement) {
return html`
<edgeless-color-picker-button
class="color"
.label="${'Color'}"
.label=${'Color'}
.pick=${this.pickColor}
.color=${selectedColor}
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
>
</edgeless-color-picker-button>
`;
@@ -158,6 +159,7 @@ export class EdgelessChangeBrushButton extends WithDisposable(LitElement) {
<edgeless-color-panel
.value=${selectedColor}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
@select=${this._setBrushColor}
>
</edgeless-color-panel>
@@ -373,12 +373,13 @@ export class EdgelessChangeConnectorButton extends WithDisposable(LitElement) {
return html`
<edgeless-color-picker-button
class="stroke-color"
.label="${'Stroke style'}"
.label=${'Stroke style'}
.pick=${this.pickColor}
.color=${selectedColor}
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
.hollowCircle=${true}
>
<div
@@ -13,6 +13,7 @@ import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar';
import {
type ColorScheme,
DEFAULT_NOTE_HEIGHT,
DefaultTheme,
type FrameBlockModel,
NoteBlockModel,
NoteDisplayMode,
@@ -200,12 +201,13 @@ export class EdgelessChangeFrameButton extends WithDisposable(LitElement) {
return html`
<edgeless-color-picker-button
class="background"
.label="${'Background'}"
.label=${'Background'}
.pick=${this.pickColor}
.color=${background}
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
>
</edgeless-color-picker-button>
`;
@@ -227,6 +229,7 @@ export class EdgelessChangeFrameButton extends WithDisposable(LitElement) {
<edgeless-color-panel
.value=${background}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
@select=${this._setFrameBackground}
>
</edgeless-color-panel>
@@ -338,6 +338,7 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
>
</edgeless-color-picker-button>
`;
@@ -361,6 +362,7 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
aria-label="Fill colors"
.value=${selectedFillColor}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
@select=${this._setShapeFillColor}
>
</edgeless-color-panel>
@@ -388,6 +390,7 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${DefaultTheme.Palettes}
.hollowCircle=${true}
>
<div
@@ -450,8 +453,8 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
() => html`
<editor-icon-button
aria-label="Add text"
.tooltip="${'Add text'}"
.iconSize="${'20px'}"
.tooltip=${'Add text'}
.iconSize=${'20px'}
@click=${this._addText}
>
${AddTextIcon()}
@@ -462,7 +465,7 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
'menu',
() => html`
<edgeless-change-text-menu
.elementType="${'shape'}"
.elementType=${'shape'}
.elements=${elements}
.edgeless=${this.edgeless}
></edgeless-change-text-menu>
@@ -344,10 +344,6 @@ export class EdgelessChangeTextMenu extends WithDisposable(LitElement) {
matchFontFaces.length === 1 &&
matchFontFaces[0].style === selectedFontStyle &&
matchFontFaces[0].weight === selectedFontWeight;
const palettes =
this.elementType === 'shape'
? DefaultTheme.ShapeTextColorPalettes
: DefaultTheme.Palettes;
return join(
[
@@ -393,14 +389,14 @@ export class EdgelessChangeTextMenu extends WithDisposable(LitElement) {
return html`
<edgeless-color-picker-button
class="text-color"
.label="${'Text color'}"
.label=${'Text color'}
.pick=${this.pickColor}
.isText=${true}
.color=${selectedColor}
.colors=${colors}
.colorType=${type}
.theme=${colorScheme}
.palettes=${palettes}
.palettes=${DefaultTheme.Palettes}
>
</edgeless-color-picker-button>
`;
@@ -422,7 +418,7 @@ export class EdgelessChangeTextMenu extends WithDisposable(LitElement) {
<edgeless-color-panel
.value=${selectedColor}
.theme=${colorScheme}
.palettes=${palettes}
.palettes=${DefaultTheme.Palettes}
@select=${this._setTextColor}
></edgeless-color-panel>
</editor-menu-button>
@@ -1,5 +1,5 @@
import type { ColorScheme, Palette } from '@blocksuite/affine-model';
import { DefaultTheme, resolveColor } from '@blocksuite/affine-model';
import { resolveColor } from '@blocksuite/affine-model';
import type { ColorEvent } from '@blocksuite/affine-shared/utils';
import { WithDisposable } from '@blocksuite/global/utils';
import { html, LitElement } from 'lit';
@@ -188,7 +188,7 @@ export class EdgelessColorPickerButton extends WithDisposable(LitElement) {
accessor menuButton!: EditorMenuButton;
@property({ attribute: false })
accessor palettes: Palette[] = DefaultTheme.Palettes;
accessor palettes: Palette[] = [];
@property({ attribute: false })
accessor pick!: (event: PickColorEvent) => void;
+2 -2
View File
@@ -23,7 +23,7 @@ export const LINE_WIDTHS = [
];
/**
* Use `DefaultTheme.StrokeColorShortMap` instead.
* Use `DefaultTheme.StrokeColorMap` instead.
*
* @deprecated
*/
@@ -44,7 +44,7 @@ export enum LineColor {
export const LineColorMap = createEnumMap(LineColor);
/**
* Use `DefaultTheme.StrokeColorShortPalettes` instead.
* Use `DefaultTheme.StrokeColorPalettes` instead.
*
* @deprecated
*/
@@ -77,11 +77,11 @@ export abstract class MindmapStyleGetter {
export class StyleOne extends MindmapStyleGetter {
private readonly _colorOrders = [
DefaultTheme.StrokeColorShortMap.Purple,
DefaultTheme.StrokeColorShortMap.Magenta,
DefaultTheme.StrokeColorShortMap.Orange,
DefaultTheme.StrokeColorShortMap.Yellow,
DefaultTheme.StrokeColorShortMap.Green,
DefaultTheme.StrokeColorMap.Purple,
DefaultTheme.StrokeColorMap.Magenta,
DefaultTheme.StrokeColorMap.Orange,
DefaultTheme.StrokeColorMap.Yellow,
DefaultTheme.StrokeColorMap.Green,
'#7ae2d5',
];
@@ -188,9 +188,9 @@ export const styleOne = new StyleOne();
export class StyleTwo extends MindmapStyleGetter {
private readonly _colorOrders = [
DefaultTheme.StrokeColorShortMap.Blue,
DefaultTheme.StrokeColorMap.Blue,
'#7ae2d5',
DefaultTheme.StrokeColorShortMap.Yellow,
DefaultTheme.StrokeColorMap.Yellow,
];
readonly root = {
@@ -207,7 +207,7 @@ export class StyleTwo extends MindmapStyleGetter {
color: DefaultTheme.pureBlack,
filled: true,
fillColor: DefaultTheme.StrokeColorShortMap.Yellow,
fillColor: DefaultTheme.StrokeColorMap.Yellow,
padding: [11, 22] as [number, number],
@@ -298,8 +298,8 @@ export const styleTwo = new StyleTwo();
export class StyleThree extends MindmapStyleGetter {
private readonly _strokeColor = [
DefaultTheme.StrokeColorShortMap.Yellow,
DefaultTheme.StrokeColorShortMap.Green,
DefaultTheme.StrokeColorMap.Yellow,
DefaultTheme.StrokeColorMap.Green,
'#5cc7ba',
];
@@ -317,7 +317,7 @@ export class StyleThree extends MindmapStyleGetter {
color: DefaultTheme.pureBlack,
filled: true,
fillColor: DefaultTheme.StrokeColorShortMap.Yellow,
fillColor: DefaultTheme.StrokeColorMap.Yellow,
padding: [10, 22] as [number, number],
@@ -407,12 +407,12 @@ export const styleThree = new StyleThree();
export class StyleFour extends MindmapStyleGetter {
private readonly _colors = [
DefaultTheme.StrokeColorShortMap.Purple,
DefaultTheme.StrokeColorShortMap.Magenta,
DefaultTheme.StrokeColorShortMap.Orange,
DefaultTheme.StrokeColorShortMap.Yellow,
DefaultTheme.StrokeColorShortMap.Green,
DefaultTheme.StrokeColorShortMap.Blue,
DefaultTheme.StrokeColorMap.Purple,
DefaultTheme.StrokeColorMap.Magenta,
DefaultTheme.StrokeColorMap.Orange,
DefaultTheme.StrokeColorMap.Yellow,
DefaultTheme.StrokeColorMap.Green,
DefaultTheme.StrokeColorMap.Blue,
];
readonly root = {
+12 -42
View File
@@ -72,44 +72,15 @@ const NoteBackgroundColorPalettes: Palette[] = [
...buildPalettes(NoteBackgroundColorMap),
] as const;
const StrokeColorShortMap = { ...Medium, Black, White } as const;
const StrokeColorMap = { ...Medium, Black, White } as const;
const StrokeColorShortPalettes: Palette[] = [
...buildPalettes(StrokeColorShortMap),
const StrokeColorPalettes: Palette[] = [
...buildPalettes(StrokeColorMap),
] as const;
const FillColorShortMap = { ...Medium, Black, White } as const;
const FillColorMap = { ...Medium, Black, White } as const;
const FillColorShortPalettes: Palette[] = [
...buildPalettes(FillColorShortMap),
] as const;
const ShapeTextColorShortMap = {
...Medium,
Black: pureBlack,
White: pureWhite,
} as const;
const ShapeTextColorShortPalettes: Palette[] = [
...buildPalettes({ ...ShapeTextColorShortMap }),
] as const;
const ShapeTextColorPalettes: Palette[] = [
// Light
...buildPalettes(Light, 'Light'),
{ key: 'Transparent', value: Transparent },
// Medium
...buildPalettes(Medium, 'Medium'),
{ key: 'White', value: pureWhite },
// Heavy
...buildPalettes(Heavy, 'Heavy'),
{ key: 'Black', value: pureBlack },
] as const;
const FillColorPalettes: Palette[] = [...buildPalettes(FillColorMap)] as const;
export const DefaultTheme: Theme = {
pureBlack,
@@ -118,19 +89,18 @@ export const DefaultTheme: Theme = {
white: White,
transparent: Transparent,
textColor: Medium.Blue,
shapeTextColor: pureBlack,
// Custom button should be selected by default,
// add transparent `ff` to distinguish `#000000`.
shapeTextColor: '#000000ff',
shapeStrokeColor: Medium.Yellow,
shapeFillColor: Medium.Yellow,
connectorColor: Medium.Grey,
noteBackgrounColor: NoteBackgroundColorMap.White,
Palettes,
ShapeTextColorPalettes,
StrokeColorMap,
StrokeColorPalettes,
FillColorMap,
FillColorPalettes,
NoteBackgroundColorMap,
NoteBackgroundColorPalettes,
StrokeColorShortMap,
StrokeColorShortPalettes,
FillColorShortMap,
FillColorShortPalettes,
ShapeTextColorShortMap,
ShapeTextColorShortPalettes,
} as const;
+7 -11
View File
@@ -21,20 +21,16 @@ export const ThemeSchema = z.object({
shapeFillColor: ColorSchema,
connectorColor: ColorSchema,
noteBackgrounColor: ColorSchema,
// Universal color palettes
// Universal color palette
Palettes: z.array(PaletteSchema),
ShapeTextColorPalettes: z.array(PaletteSchema),
StrokeColorMap: z.record(z.string(), ColorSchema),
// Usually used in global toolbar and editor preview
StrokeColorPalettes: z.array(PaletteSchema),
FillColorMap: z.record(z.string(), ColorSchema),
// Usually used in global toolbar and editor preview
FillColorPalettes: z.array(PaletteSchema),
NoteBackgroundColorMap: z.record(z.string(), ColorSchema),
NoteBackgroundColorPalettes: z.array(PaletteSchema),
// Usually used in global toolbar and editor preview
StrokeColorShortMap: z.record(z.string(), ColorSchema),
StrokeColorShortPalettes: z.array(PaletteSchema),
FillColorShortMap: z.record(z.string(), ColorSchema),
FillColorShortPalettes: z.array(PaletteSchema),
ShapeTextColorShortMap: z.record(z.string(), ColorSchema),
ShapeTextColorShortPalettes: z.array(PaletteSchema),
});
export type Theme = z.infer<typeof ThemeSchema>;
@@ -28,8 +28,8 @@ interface Tile {
zoom: number;
}
const zoomThreshold = 1; // With high enough zoom, fallback to DOM rendering
const debounceTime = 1000; // During this period, fallback to DOM
// With high enough zoom, fallback to DOM rendering
const zoomThreshold = 1;
const debug = false; // Toggle for debug logs
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
@@ -66,10 +66,6 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.viewportElement = element;
syncCanvasSize(this.canvas, this.std.host);
this.setState('pending');
this.disposables.add(
this.viewport.sizeUpdated.on(() => this.handleResize())
);
this.disposables.add(
this.viewport.viewportUpdated.on(() => {
this.refresh().catch(console.error);
@@ -138,7 +134,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
() => {
this.refresh().catch(console.error);
},
debounceTime,
1000, // During this period, fallback to DOM
{ leading: false, trailing: true }
);
@@ -261,13 +257,6 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.state = newState;
}
canOptimize(): boolean {
const isReady = this.state === 'ready';
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
const result = isReady && isBelowZoomThreshold;
return result;
}
private updateOptimizedBlocks() {
requestAnimationFrame(() => {
if (!this.viewportElement || !this.layoutCache) return;
@@ -292,6 +281,13 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.debugLog('Cleared optimized blocks');
}
canOptimize(): boolean {
const isReady = this.state === 'ready';
const isBelowZoomThreshold = this.viewport.zoom <= zoomThreshold;
const result = isReady && isBelowZoomThreshold;
return result;
}
private toggleOptimization(value: boolean) {
if (
this.viewportElement &&
@@ -301,11 +297,4 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
this.debugLog(`${value ? 'Enabled' : 'Disabled'} optimization`);
}
}
private handleResize() {
this.debugLog('Container resized, syncing canvas size');
syncCanvasSize(this.canvas, this.std.host);
this.invalidate();
this.debouncedRefresh();
}
}
@@ -31,7 +31,6 @@ type CreateProxyOptions = {
transform?: Transform;
onDispose: Slot;
shouldByPassSignal: () => boolean;
shouldByPassYjs: () => boolean;
byPassSignalUpdate: (fn: () => void) => void;
stashed: Set<string | number>;
initialized: () => boolean;
@@ -59,7 +58,6 @@ function createProxy(
const {
onDispose,
shouldByPassSignal,
shouldByPassYjs,
byPassSignalUpdate,
basePath,
onChange,
@@ -143,9 +141,6 @@ function createProxy(
if (isPureObject(value)) {
const syncYMap = () => {
if (shouldByPassYjs()) {
return;
}
yMap.forEach((_, key) => {
if (initialized() && keyWithoutPrefix(key).startsWith(fullPath)) {
yMap.delete(key);
@@ -190,7 +185,7 @@ function createProxy(
const yValue = native2Y(value);
const next = transform(firstKey, value, yValue);
if (!isStashed && initialized() && !shouldByPassYjs()) {
if (!isStashed && initialized()) {
yMap.doc?.transact(
() => {
yMap.set(keyWithPrefix(fullPath), yValue);
@@ -243,7 +238,7 @@ function createProxy(
});
};
if (!isStashed && initialized() && !shouldByPassYjs()) {
if (!isStashed && initialized()) {
yMap.doc?.transact(
() => {
const fullKey = keyWithPrefix(fullPath);
@@ -297,17 +292,12 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
if (this._stashed.has(firstKey)) {
return;
}
this._updateWithYjsSkip(() => {
void keys.reduce((acc, key, index, arr) => {
if (!acc[key] && index !== arr.length - 1) {
acc[key] = {};
}
if (index === arr.length - 1) {
acc[key] = y2Native(value);
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
});
void keys.reduce((acc, key, index, arr) => {
if (index === arr.length - 1) {
acc[key] = y2Native(value);
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
return;
}
if (type.action === 'delete') {
@@ -317,26 +307,12 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
if (this._stashed.has(firstKey)) {
return;
}
this._updateWithYjsSkip(() => {
void keys.reduce((acc, key, index, arr) => {
if (index === arr.length - 1) {
delete acc[key];
let i = index - 1;
let curr = acc;
while (i > 0) {
const parentPath = keys.slice(0, i);
const parentKey = keys[i];
const parent = parentPath.reduce((acc, key) => {
return acc[key] as UnRecord;
}, proxy as UnRecord);
deleteEmptyObject(curr, parentKey, parent);
curr = parent;
i--;
}
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
});
void keys.reduce((acc, key, index, arr) => {
if (index === arr.length - 1) {
delete acc[key];
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
return;
}
});
@@ -417,8 +393,6 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
return root;
};
private _byPassYjs = false;
private readonly _getProxy = (
source: UnRecord,
root: UnRecord,
@@ -428,7 +402,6 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
onDispose: this._onDispose,
shouldByPassSignal: () => this._skipNext,
byPassSignalUpdate: this._updateWithSkip,
shouldByPassYjs: () => this._byPassYjs,
basePath: path,
onChange: this._onChange,
transform: this._transform,
@@ -437,12 +410,6 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
});
};
private readonly _updateWithYjsSkip = (fn: () => void) => {
this._byPassYjs = true;
fn();
this._byPassYjs = false;
};
constructor(
protected readonly _ySource: YMap<unknown>,
private readonly _onDispose: Slot,
@@ -486,9 +453,3 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
this._stashed.add(prop);
};
}
function deleteEmptyObject(obj: UnRecord, key: string, parent: UnRecord): void {
if (Object.keys(obj).length === 0) {
delete parent[key];
}
}
@@ -1,5 +1,5 @@
import { BlockModel } from '../model/block/block-model';
import { type DraftModel, toDraftModel } from '../model/block/draft';
import type { DraftModel } from '../model/block/draft';
import {
type InternalPrimitives,
internalPrimitives,
@@ -20,7 +20,7 @@ export type FromSnapshotPayload = {
};
export type ToSnapshotPayload<Props extends object> = {
model: DraftModel<BlockModel<Props>> | BlockModel<Props>;
model: DraftModel<BlockModel<Props>>;
assets: AssetsManager;
};
@@ -42,16 +42,16 @@ export class BaseBlockTransformer<Props extends object = object> {
) as Props;
}
protected _propsToSnapshot(model: DraftModel | BlockModel) {
let draftModel: DraftModel;
if (model instanceof BlockModel) {
draftModel = toDraftModel(model);
} else {
draftModel = model;
}
protected _propsToSnapshot(model: DraftModel) {
const props =
model instanceof BlockModel
? model.schema.model.isFlatData
? model.props
: model
: model;
return Object.fromEntries(
draftModel.keys.map(key => {
const value = draftModel[key as keyof typeof draftModel];
model.keys.map(key => {
const value = props[key as keyof typeof model];
return [key, toJSON(value)];
})
);
@@ -48,12 +48,12 @@ describe('apply last props', () => {
const rectShape = service.crud.getElementById(rectId) as ShapeElementModel;
expect(rectShape.fillColor).toBe(DefaultTheme.shapeFillColor);
service.crud.updateElement(rectId, {
fillColor: DefaultTheme.FillColorShortMap.Orange,
fillColor: DefaultTheme.FillColorMap.Orange,
});
expect(
std.get(EditPropsStore).lastProps$.value[`shape:${ShapeType.Rect}`]
.fillColor
).toBe(DefaultTheme.FillColorShortMap.Orange);
).toBe(DefaultTheme.FillColorMap.Orange);
// diamond shape
const diamondId = service.crud.addElement('shape', {
@@ -63,14 +63,14 @@ describe('apply last props', () => {
const diamondShape = service.crud.getElementById(
diamondId
) as ShapeElementModel;
expect(diamondShape.fillColor).toBe(DefaultTheme.FillColorShortMap.Yellow);
expect(diamondShape.fillColor).toBe(DefaultTheme.FillColorMap.Yellow);
service.crud.updateElement(diamondId, {
fillColor: DefaultTheme.FillColorShortMap.Blue,
fillColor: DefaultTheme.FillColorMap.Blue,
});
expect(
std.get(EditPropsStore).lastProps$.value[`shape:${ShapeType.Diamond}`]
.fillColor
).toBe(DefaultTheme.FillColorShortMap.Blue);
).toBe(DefaultTheme.FillColorMap.Blue);
// rounded rect shape
const roundedRectId = service.crud.addElement('shape', {
@@ -81,15 +81,13 @@ describe('apply last props', () => {
const roundedRectShape = service.crud.getElementById(
roundedRectId
) as ShapeElementModel;
expect(roundedRectShape.fillColor).toBe(
DefaultTheme.FillColorShortMap.Yellow
);
expect(roundedRectShape.fillColor).toBe(DefaultTheme.FillColorMap.Yellow);
service.crud.updateElement(roundedRectId, {
fillColor: DefaultTheme.FillColorShortMap.Green,
fillColor: DefaultTheme.FillColorMap.Green,
});
expect(
std.get(EditPropsStore).lastProps$.value['shape:roundedRect'].fillColor
).toBe(DefaultTheme.FillColorShortMap.Green);
).toBe(DefaultTheme.FillColorMap.Green);
// apply last props
const rectId2 = service.crud.addElement('shape', {
@@ -99,7 +97,7 @@ describe('apply last props', () => {
const rectShape2 = service.crud.getElementById(
rectId2
) as ShapeElementModel;
expect(rectShape2.fillColor).toBe(DefaultTheme.FillColorShortMap.Orange);
expect(rectShape2.fillColor).toBe(DefaultTheme.FillColorMap.Orange);
const diamondId2 = service.crud.addElement('shape', {
shapeType: ShapeType.Diamond,
@@ -108,7 +106,7 @@ describe('apply last props', () => {
const diamondShape2 = service.crud.getElementById(
diamondId2
) as ShapeElementModel;
expect(diamondShape2.fillColor).toBe(DefaultTheme.FillColorShortMap.Blue);
expect(diamondShape2.fillColor).toBe(DefaultTheme.FillColorMap.Blue);
const roundedRectId2 = service.crud.addElement('shape', {
shapeType: ShapeType.Rect,
@@ -118,9 +116,7 @@ describe('apply last props', () => {
const roundedRectShape2 = service.crud.getElementById(
roundedRectId2
) as ShapeElementModel;
expect(roundedRectShape2.fillColor).toBe(
DefaultTheme.FillColorShortMap.Green
);
expect(roundedRectShape2.fillColor).toBe(DefaultTheme.FillColorMap.Green);
});
test('connector', () => {
@@ -208,14 +204,14 @@ describe('apply last props', () => {
expect(text.color).toBe(DefaultTheme.textColor);
expect(text.fontFamily).toBe(FontFamily.Inter);
service.crud.updateElement(id, {
color: DefaultTheme.StrokeColorShortMap.Green,
color: DefaultTheme.StrokeColorMap.Green,
fontFamily: FontFamily.OrelegaOne,
});
const id2 = service.crud.addBlock('affine:edgeless-text', {}, surface!.id);
assertExists(id2);
const text2 = service.crud.getElementById(id2) as EdgelessTextBlockModel;
expect(text2.color).toBe(DefaultTheme.StrokeColorShortMap.Green);
expect(text2.color).toBe(DefaultTheme.StrokeColorMap.Green);
expect(text2.fontFamily).toBe(FontFamily.OrelegaOne);
});
@@ -250,13 +246,13 @@ describe('apply last props', () => {
const note = service.crud.getElementById(id) as FrameBlockModel;
expect(note.background).toBe('transparent');
service.crud.updateElement(id, {
background: DefaultTheme.StrokeColorShortMap.Purple,
background: DefaultTheme.StrokeColorMap.Purple,
});
const id2 = service.crud.addBlock('affine:frame', {}, surface!.id);
assertExists(id2);
const frame2 = service.crud.getElementById(id2) as FrameBlockModel;
expect(frame2.background).toBe(DefaultTheme.StrokeColorShortMap.Purple);
expect(frame2.background).toBe(DefaultTheme.StrokeColorMap.Purple);
service.crud.updateElement(id2, {
background: { normal: '#def4e740' },
});
@@ -4,7 +4,7 @@ import { CLS_ID, ClsServiceManager } from 'nestjs-cls';
import Sinon from 'sinon';
import { EventBus, metrics } from '../../base';
import { createTestingModule, sleep } from '../utils';
import { createTestingModule } from '../utils';
import { Listeners } from './provider';
export const test = ava as TestFn<{
@@ -201,55 +201,3 @@ test('should continuously use the same request id', async t => {
t.true(listeners.onRequestId.lastCall.returned('test-request-id'));
});
test('should throw when emitting async event with uncaught error', async t => {
const { eventbus } = t.context;
await t.throwsAsync(
() => eventbus.emitAsync('__test__.throw', { count: 0 }),
{
message: 'Error in event handler',
}
);
});
test('should suppress thrown error when emitting async event', async t => {
const { eventbus } = t.context;
const spy = Sinon.spy();
// @ts-expect-error internal event
const off = eventbus.on('error', spy);
const promise = eventbus.emitAsync('__test__.suppressThrow', {});
await t.notThrowsAsync(promise);
t.true(spy.calledOnce);
const args = spy.firstCall.args[0];
t.is(args.event, '__test__.suppressThrow');
t.deepEqual(args.payload, {});
t.is(args.error.message, 'Error in event handler');
const returns = await promise;
t.deepEqual(returns, [undefined]);
off();
});
test('should catch thrown error when emitting sync event', async t => {
const { eventbus } = t.context;
const spy = Sinon.spy();
// @ts-expect-error internal event
const off = eventbus.on('error', spy);
t.notThrows(() => eventbus.emit('__test__.throw', { count: 0 }));
// wait a tick
await sleep(1);
t.true(spy.calledOnce);
const args = spy.firstCall.args[0];
t.is(args.event, '__test__.throw');
t.deepEqual(args.payload, { count: 0 });
t.is(args.error.message, 'Error in event handler');
off();
});
@@ -8,7 +8,6 @@ declare global {
'__test__.event': { count: number };
'__test__.event2': { count: number };
'__test__.throw': { count: number };
'__test__.suppressThrow': {};
'__test__.requestId': {};
}
}
@@ -33,11 +32,6 @@ export class Listeners {
throw new Error('Error in event handler');
}
@OnEvent('__test__.suppressThrow', { suppressError: true })
onSuppressThrow() {
throw new Error('Error in event handler');
}
@OnEvent('__test__.requestId')
onRequestId() {
const cls = ClsServiceManager.getClsService();
@@ -19,12 +19,6 @@ import { genRequestId } from '../utils';
import { type EventName, type EventOptions } from './def';
import { EventHandlerScanner } from './scanner';
interface EventHandlerErrorPayload {
event: string;
payload: any;
error: Error;
}
/**
* We use socket.io system to auto pub/sub on server to server broadcast events
*/
@@ -56,9 +50,6 @@ export class EventBus
async onModuleInit() {
this.bindEventHandlers();
this.emitter.on('error', ({ event, error }: EventHandlerErrorPayload) => {
this.logger.error(`Error happened when handling event ${event}`, error);
});
}
async onApplicationBootstrap() {
@@ -87,16 +78,7 @@ export class EventBus
*/
emit<T extends EventName>(event: T, payload: Events[T]) {
this.logger.log(`Dispatch event: ${event}`);
// NOTE(@forehalo):
// Because all event handlers are wrapped in promisified metrics and cls context, they will always run in standalone tick.
// In which way, if handler throws, an unhandled rejection will be triggered and end up with process exiting.
// So we catch it here with `emitAsync`
this.emitter.emitAsync(event, payload).catch(e => {
this.emitter.emit('error', { event, payload, error: e });
});
return true;
return this.emitter.emit(event, payload);
}
/**
@@ -133,11 +115,10 @@ export class EventBus
return await listener(payload);
} catch (e) {
if (suppressError) {
this.emitter.emit('error', {
event,
payload,
error: e,
} as EventHandlerErrorPayload);
this.logger.error(
`Error happened when handling event ${signature}`,
e
);
} else {
throw e;
}
@@ -5,7 +5,7 @@ import { DynamicModule } from '@nestjs/common';
import { Config } from '../../config';
import { QueueRedis } from '../../redis';
import { Queue, QUEUES } from './def';
import { QUEUES } from './def';
import { JobExecutor } from './executor';
import { JobQueue } from './queue';
import { JobHandlerScanner } from './scanner';
@@ -25,15 +25,7 @@ export class JobModule {
},
inject: [Config, QueueRedis],
}),
BullModule.registerQueue(
...QUEUES.map(name => {
if (name === Queue.NIGHTLY_JOB) {
// avoid nightly jobs been run multiple times
return { name, removeOnComplete: { age: 1000 * 60 * 60 } };
}
return { name };
})
),
BullModule.registerQueue(...QUEUES.map(name => ({ name }))),
],
providers: [JobQueue, JobExecutor, JobHandlerScanner],
exports: [JobQueue],
@@ -1,8 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaClient } from '@prisma/client';
import { JobQueue, OnJob } from '../../base';
import { OnJob } from '../../base';
import { PgWorkspaceDocStorageAdapter } from '../doc';
declare global {
@@ -16,11 +14,7 @@ declare global {
@Injectable()
export class DocServiceCronJob {
constructor(
private readonly workspace: PgWorkspaceDocStorageAdapter,
private readonly prisma: PrismaClient,
private readonly job: JobQueue
) {}
constructor(private readonly workspace: PgWorkspaceDocStorageAdapter) {}
@OnJob('doc.mergePendingDocUpdates')
async mergePendingDocUpdates({
@@ -29,29 +23,4 @@ export class DocServiceCronJob {
}: Jobs['doc.mergePendingDocUpdates']) {
await this.workspace.getDoc(workspaceId, docId);
}
@Cron(CronExpression.EVERY_10_SECONDS)
async schedule() {
const group = await this.prisma.update.groupBy({
by: ['workspaceId', 'id'],
_count: true,
});
for (const update of group) {
if (update._count > 100) {
await this.job.add(
'doc.mergePendingDocUpdates',
{
workspaceId: update.workspaceId,
docId: update.id,
},
{
jobId: `doc:merge-pending-updates:${update.workspaceId}:${update.id}`,
priority: update._count,
delay: 0,
}
);
}
}
}
}
@@ -92,7 +92,6 @@ export class DocModel extends BaseModel {
orderBy: {
createdAt: 'asc',
},
take: 100,
});
return rows.map(r => this.updateToDocRecord(r));
}
@@ -982,7 +982,7 @@ When referencing information from the provided documents in your response:
},
{
name: 'Search With AFFiNE AI',
model: 'sonar-reasoning-pro',
model: 'sonar',
messages: [],
},
// use for believer plan
@@ -187,7 +187,7 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
async generateText(
messages: PromptMessage[],
model: string = 'sonar',
model: string = 'llama-3.1-sonar-small-128k-online',
options: CopilotChatOptions = {}
): Promise<string> {
await this.checkParams({ messages, model, options });
@@ -221,12 +221,11 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
message: data.detail[0].msg || 'Unexpected perplexity response',
});
} else {
const citationParser = new CitationParser();
const parser = new CitationParser();
const { content } = data.choices[0].message;
const { citations } = data;
let result = content.replaceAll(/<\/?think>\n/g, '\n---\n');
result = citationParser.parse(result, citations);
result += citationParser.end();
let result = parser.parse(content, citations);
result += parser.end();
return result;
}
} catch (e: any) {
@@ -237,7 +236,7 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
async *generateTextStream(
messages: PromptMessage[],
model: string = 'sonar',
model: string = 'llama-3.1-sonar-small-128k-online',
options: CopilotChatOptions = {}
): AsyncIterable<string> {
await this.checkParams({ messages, model, options });
@@ -265,7 +264,7 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
params
);
if (response.body) {
const citationParser = new CitationParser();
const parser = new CitationParser();
const provider = this.type;
const eventStream = response.body
.pipeThrough(new TextDecoderStream())
@@ -290,13 +289,12 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
}
const { content } = data.choices[0].delta;
const { citations } = data;
let result = content.replaceAll(/<\/?think>\n?/g, '\n---\n');
result = citationParser.parse(result, citations);
const result = parser.parse(content, citations);
controller.enqueue(result);
}
},
flush(controller) {
controller.enqueue(citationParser.end());
controller.enqueue(parser.end());
controller.enqueue(null);
},
})
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaClient } from '@prisma/client';
import { EventBus, JobQueue, OnJob } from '../../base';
import { EventBus, JobQueue, OnEvent, OnJob } from '../../base';
import {
SubscriptionPlan,
SubscriptionRecurring,
@@ -126,15 +126,6 @@ export class SubscriptionCronJobs {
});
for (const subscription of subscriptions) {
await this.db.subscription.delete({
where: {
targetId_plan: {
targetId: subscription.targetId,
plan: subscription.plan,
},
},
});
this.event.emit('user.subscription.canceled', {
userId: subscription.targetId,
plan: subscription.plan as SubscriptionPlan,
@@ -142,4 +133,19 @@ export class SubscriptionCronJobs {
});
}
}
@OnEvent('user.subscription.canceled')
async handleUserSubscriptionCanceled({
userId,
plan,
}: Events['user.subscription.canceled']) {
await this.db.subscription.delete({
where: {
targetId_plan: {
targetId: userId,
plan,
},
},
});
}
}
@@ -47,7 +47,7 @@ export class AwarenessFrontend {
return;
}
applyAwarenessUpdate(awareness, update.bin, uniqueId);
applyAwarenessUpdate(awareness, update.bin, origin);
};
const handleSyncCollect = () => {
return Promise.resolve({
@@ -1,7 +1,7 @@
import type { EditorHost } from '@blocksuite/affine/block-std';
import type { MindmapElementModel } from '@blocksuite/affine/blocks';
import { createAIScrollableTextRenderer } from '../components/ai-scrollable-text-renderer';
import { createTextRenderer } from '../components/text-renderer';
import {
createMindmapExecuteRenderer,
createMindmapRenderer,
@@ -52,5 +52,5 @@ export function actionToAnswerRenderer<
return createImageRenderer(host, { height: 300 });
}
return createAIScrollableTextRenderer(host, {}, 320, true);
return createTextRenderer(host, { maxHeight: 320 });
}
@@ -33,7 +33,7 @@ import {
replaceWithMarkdown,
} from './actions/page-response';
import type { AIItemConfig } from './components/ai-item/types';
import { createAIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
import { createTextRenderer } from './components/text-renderer';
import { AIProvider } from './provider';
import { reportResponse } from './utils/action-reporter';
import { getAIPanelWidget } from './utils/ai-widgets';
@@ -293,7 +293,7 @@ export function buildAIPanelConfig(
const ctx = new AIContext();
const searchService = framework.get(AINetworkSearchService);
return {
answerRenderer: createAIScrollableTextRenderer(panel.host, {}, 320, true),
answerRenderer: createTextRenderer(panel.host, { maxHeight: 320 }),
finishStateConfig: buildFinishConfig(panel, 'chat', ctx),
generatingStateConfig: buildGeneratingConfig(),
errorStateConfig: buildErrorConfig(panel),
@@ -1,133 +0,0 @@
import {
type EditorHost,
ShadowlessElement,
} from '@blocksuite/affine/block-std';
import { scrollbarStyle } from '@blocksuite/affine/blocks';
import { throttle, WithDisposable } from '@blocksuite/affine/global/utils';
import type { PropertyValues } from 'lit';
import { css, html } from 'lit';
import { property, query } from 'lit/decorators.js';
import type {
AffineAIPanelState,
AffineAIPanelWidgetConfig,
} from '../widgets/ai-panel/type';
import type { TextRendererOptions } from './text-renderer';
export class AIScrollableTextRenderer extends WithDisposable(
ShadowlessElement
) {
static override styles = css`
.ai-scrollable-text-renderer {
overflow-y: auto;
}
${scrollbarStyle('.ai-scrollable-text-renderer')};
`;
private _lastScrollHeight = 0;
private readonly _scrollToEnd = () => {
requestAnimationFrame(() => {
if (!this._scrollableTextRenderer) {
return;
}
const scrollHeight = this._scrollableTextRenderer.scrollHeight || 0;
if (scrollHeight > this._lastScrollHeight) {
this._lastScrollHeight = scrollHeight;
// Scroll when scroll height greater than maxheight
this._scrollableTextRenderer?.scrollTo({
top: scrollHeight,
});
}
});
};
private readonly _throttledScrollToEnd = throttle(this._scrollToEnd, 300);
private readonly _onWheel = (e: WheelEvent) => {
e.stopPropagation();
if (this.state === 'generating') {
e.preventDefault();
}
};
protected override updated(_changedProperties: PropertyValues) {
if (
this.autoScroll &&
_changedProperties.has('answer') &&
(this.state === 'generating' || this.state === 'finished')
) {
this._throttledScrollToEnd();
}
}
override render() {
const { host, answer, state, textRendererOptions } = this;
return html` <style>
.ai-scrollable-text-renderer {
max-height: ${this.maxHeight}px;
}
</style>
<div class="ai-scrollable-text-renderer" @wheel=${this._onWheel}>
<text-renderer
.host=${host}
.answer=${answer}
.state=${state}
.options=${textRendererOptions}
></text-renderer>
</div>`;
}
@property({ attribute: false })
accessor answer!: string;
@property({ attribute: false })
accessor host: EditorHost | null = null;
@property({ attribute: false })
accessor state: AffineAIPanelState | undefined = undefined;
@property({ attribute: false })
accessor textRendererOptions!: TextRendererOptions;
@property({ attribute: false })
accessor maxHeight = 320;
@property({ attribute: false })
accessor autoScroll = true;
@query('.ai-scrollable-text-renderer')
accessor _scrollableTextRenderer: HTMLDivElement | null = null;
}
export const createAIScrollableTextRenderer: (
host: EditorHost,
textRendererOptions: TextRendererOptions,
maxHeight: number,
autoScroll: boolean
) => AffineAIPanelWidgetConfig['answerRenderer'] = (
host,
textRendererOptions,
maxHeight,
autoScroll
) => {
return (answer, state) => {
return html`<ai-scrollable-text-renderer
.host=${host}
.answer=${answer}
.state=${state}
.textRendererOptions=${textRendererOptions}
.maxHeight=${maxHeight}
.autoScroll=${autoScroll}
></ai-scrollable-text-renderer>`;
};
};
declare global {
interface HTMLElementTagNameMap {
'ai-scrollable-text-renderer': AIScrollableTextRenderer;
}
}
@@ -88,6 +88,7 @@ const customHeadingStyles = css`
`;
export type TextRendererOptions = {
maxHeight?: number;
customHeading?: boolean;
extensions?: ExtensionType[];
additionalMiddlewares?: TransformerMiddleware[];
@@ -283,12 +284,18 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
return nothing;
}
const { customHeading } = this.options;
const { maxHeight, customHeading } = this.options;
const classes = classMap({
'text-renderer-container': true,
'show-scrollbar': !!maxHeight,
'custom-heading': !!customHeading,
});
return html`
<style>
.text-renderer-container {
max-height: ${maxHeight ? Math.max(maxHeight, 200) + 'px' : ''};
}
</style>
<div class=${classes}>
${keyed(
this._doc,
@@ -325,6 +332,7 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
// Apply min-height to prevent shrinking
this._container.style.minHeight = `${this._maxContainerHeight}px`;
}
this._container.scrollTop = this._container.scrollHeight;
});
}
@@ -31,7 +31,6 @@ import { ChatPanelChip } from './chat-panel/components/chip';
import { ChatPanelDocChip } from './chat-panel/components/doc-chip';
import { ChatPanelFileChip } from './chat-panel/components/file-chip';
import { effects as componentAiItemEffects } from './components/ai-item';
import { AIScrollableTextRenderer } from './components/ai-scrollable-text-renderer';
import { AskAIButton } from './components/ask-ai-button';
import { AskAIIcon } from './components/ask-ai-icon';
import { AskAIPanel } from './components/ask-ai-panel';
@@ -108,10 +107,6 @@ export function registerAIEffects() {
customElements.define('affine-ai-chat', AIChatBlockComponent);
customElements.define('ai-chat-message', AIChatMessage);
customElements.define('ai-chat-messages', AIChatMessages);
customElements.define(
'ai-scrollable-text-renderer',
AIScrollableTextRenderer
);
customElements.define('image-placeholder', ImagePlaceholder);
customElements.define('chat-image', ChatImage);
customElements.define('chat-images', ChatImages);
@@ -51,14 +51,14 @@ export const ConnectorSettings = () => {
const { editorSetting } = framework.get(EditorSettingService);
const settings = useLiveData(editorSetting.settings$);
const {
palettes: StrokeColorShortPalettes,
palettes: strokeColorPalettes,
getCurrentColor: getCurrentStrokeColor,
} = usePalettes(
DefaultTheme.StrokeColorShortPalettes,
DefaultTheme.StrokeColorPalettes,
DefaultTheme.connectorColor
);
const { palettes: textColorPalettes, getCurrentColor: getCurrentTextColor } =
usePalettes(DefaultTheme.StrokeColorShortPalettes, DefaultTheme.black);
usePalettes(DefaultTheme.StrokeColorPalettes, DefaultTheme.black);
const connecterStyleItems = useMemo<RadioItem[]>(
() => [
@@ -165,7 +165,7 @@ export const ConnectorSettings = () => {
const colorItems = useMemo(() => {
const { stroke } = settings.connector;
return StrokeColorShortPalettes.map(({ key, value, resolvedValue }) => {
return strokeColorPalettes.map(({ key, value, resolvedValue }) => {
const handler = () => {
editorSetting.set('connector', { stroke: value });
};
@@ -181,7 +181,7 @@ export const ConnectorSettings = () => {
</MenuItem>
);
});
}, [editorSetting, settings, StrokeColorShortPalettes]);
}, [editorSetting, settings, strokeColorPalettes]);
const startEndPointItems = useMemo(() => {
const { frontEndpointStyle } = settings.connector;
@@ -32,7 +32,7 @@
"e6t9tKz8Sy": {
"index": "a5",
"seed": 338503204,
"color": "#000000",
"color": "#000000ff",
"fillColor": "#fcd34d",
"filled": true,
"fontFamily": "blocksuite:surface:Inter",
@@ -56,7 +56,7 @@
"F8qB_zDC5Q": {
"index": "a6",
"seed": 1896265661,
"color": "#000000",
"color": "#000000ff",
"fillColor": "#fcd34d",
"filled": true,
"fontFamily": "blocksuite:surface:Inter",
@@ -80,7 +80,7 @@
"mPR44JBpcd": {
"index": "a7",
"seed": 2073974140,
"color": "#000000",
"color": "#000000ff",
"fillColor": "#fcd34d",
"filled": true,
"fontFamily": "blocksuite:surface:Inter",
@@ -104,7 +104,7 @@
"cmtluc3FWR": {
"index": "a8",
"seed": 1457248130,
"color": "#000000",
"color": "#000000ff",
"fillColor": "#fcd34d",
"filled": true,
"fontFamily": "blocksuite:surface:Inter",
@@ -128,7 +128,7 @@
"knt_TKvACR": {
"index": "a9",
"seed": 1896265661,
"color": "#000000",
"color": "#000000ff",
"fillColor": "#fcd34d",
"filled": true,
"fontFamily": "blocksuite:surface:Inter",
@@ -22,7 +22,7 @@ export const FrameSettings = () => {
const { palettes, getCurrentColor } = usePalettes(
[
{ key: 'Transparent', value: DefaultTheme.transparent },
...DefaultTheme.FillColorShortPalettes,
...DefaultTheme.FillColorPalettes,
],
DefaultTheme.transparent
);
@@ -21,7 +21,7 @@ export const PenSettings = () => {
const { editorSetting } = framework.get(EditorSettingService);
const settings = useLiveData(editorSetting.settings$);
const { palettes, getCurrentColor } = usePalettes(
DefaultTheme.StrokeColorShortPalettes,
DefaultTheme.StrokeColorPalettes,
DefaultTheme.black
);
@@ -59,19 +59,11 @@ export const ShapeSettings = () => {
palettes: strokeColorPalettes,
getCurrentColor: getCurrentStrokeColor,
} = usePalettes(
DefaultTheme.StrokeColorShortPalettes,
DefaultTheme.StrokeColorPalettes,
DefaultTheme.shapeStrokeColor
);
const { palettes: fillColorPalettes, getCurrentColor: getCurrentFillColor } =
usePalettes(
DefaultTheme.FillColorShortPalettes,
DefaultTheme.shapeFillColor
);
const { palettes: textColorPalettes, getCurrentColor: getCurrentTextColor } =
usePalettes(
DefaultTheme.ShapeTextColorShortPalettes,
DefaultTheme.shapeTextColor
);
usePalettes(DefaultTheme.FillColorPalettes, DefaultTheme.shapeFillColor);
const [currentShape, setCurrentShape] = useState<ShapeName>(ShapeType.Rect);
@@ -325,7 +317,7 @@ export const ShapeSettings = () => {
const textColorItems = useMemo(() => {
const { color } = settings[`shape:${currentShape}`];
return textColorPalettes.map(({ key, value, resolvedValue }) => {
return strokeColorPalettes.map(({ key, value, resolvedValue }) => {
const handler = () => {
editorSetting.set(`shape:${currentShape}`, { color: value });
};
@@ -341,7 +333,7 @@ export const ShapeSettings = () => {
</MenuItem>
);
});
}, [editorSetting, settings, currentShape, textColorPalettes]);
}, [editorSetting, settings, currentShape, strokeColorPalettes]);
const getElements = useCallback(
(doc: Store) => {
@@ -387,8 +379,8 @@ export const ShapeSettings = () => {
const textColor = useMemo(() => {
const color = settings[`shape:${currentShape}`].color;
return getCurrentTextColor(color);
}, [currentShape, getCurrentTextColor, settings]);
return getCurrentStrokeColor(color);
}, [currentShape, getCurrentStrokeColor, settings]);
const height = currentDoc === 'flow' ? 456 : 180;
return (
@@ -32,7 +32,7 @@ export const TextSettings = () => {
const { editorSetting } = framework.get(EditorSettingService);
const settings = useLiveData(editorSetting.settings$);
const { palettes, getCurrentColor } = usePalettes(
DefaultTheme.StrokeColorShortPalettes,
DefaultTheme.StrokeColorPalettes,
DefaultTheme.textColor
);
@@ -1,113 +0,0 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
clickView,
dblclickView,
dragView,
locateEditorContainer,
setEdgelessTool,
} from '@affine-test/kit/utils/editor';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
switchEdgelessTheme,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test.beforeEach(async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await clickEdgelessModeButton(page);
const container = locateEditorContainer(page);
await container.click();
});
test('should add text to shape, default to pure black', async ({ page }) => {
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
await dblclickView(page, [150, 350]);
await expect(
page.locator('edgeless-shape-text-editor rich-text')
).toBeVisible();
await page.keyboard.type('text');
await page.keyboard.press('Escape');
const toolbar = page.locator(
'edgeless-element-toolbar-widget editor-toolbar'
);
const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color'
);
const textColorBtn = textColorContainer.getByLabel('Text color');
const blackBtn = textColorContainer
.locator('edgeless-color-button[active]')
.getByLabel('Black');
await expect(textColorContainer).toBeVisible();
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor).toBe('#000000');
await switchEdgelessTheme(page, 'dark');
await clickView(page, [150, 350]);
await textColorBtn.click();
await expect(blackBtn).toHaveCount(1);
const svgFillColor2 = await blackBtn.locator('svg').getAttribute('fill');
expect(svgFillColor2).toBe('#000000');
});
test('should add text to shape with pure white', async ({ page }) => {
await setEdgelessTool(page, 'shape');
await dragView(page, [100, 300], [200, 400]);
await dblclickView(page, [150, 350]);
await expect(
page.locator('edgeless-shape-text-editor rich-text')
).toBeVisible();
await page.keyboard.type('text');
await page.keyboard.press('Escape');
const toolbar = page.locator(
'edgeless-element-toolbar-widget editor-toolbar'
);
const textColorContainer = toolbar.locator(
'edgeless-color-picker-button.text-color'
);
const textColorBtn = textColorContainer.getByLabel('Text color');
let currentColor = await textColorBtn
.locator('svg rect')
.getAttribute('fill');
expect(currentColor).toBe('#000000');
await textColorBtn.click();
const blackBtn = textColorContainer
.locator('edgeless-color-button[active]')
.getByLabel('Black');
await expect(blackBtn).toHaveCount(1);
const whiteBtn = textColorContainer
.locator('edgeless-color-button')
.getByLabel('White');
await whiteBtn.click();
currentColor = await textColorBtn.locator('svg rect').getAttribute('fill');
expect(currentColor).toBe('#ffffff');
await switchEdgelessTheme(page, 'dark');
await clickView(page, [150, 350]);
currentColor = await textColorBtn.locator('svg rect').getAttribute('fill');
expect(currentColor).toBe('#ffffff');
});
-12
View File
@@ -1,8 +1,6 @@
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
import { openEditorInfoPanel } from './setting';
export function getAllPage(page: Page) {
const newPageButton = page.getByTestId('new-page-button-trigger');
const newPageDropdown = newPageButton.locator('svg');
@@ -238,13 +236,3 @@ export const addDatabaseRow = async (page: Page, databaseTitle: string) => {
});
await db.locator('.data-view-table-group-add-row-button').click();
};
export const switchEdgelessTheme = async (
page: Page,
type: 'system' | 'light' | 'dark'
) => {
await openEditorInfoPanel(page);
const panel = page.getByTestId('info-modal');
await panel.locator(`button[value="${type}"]`).click();
await page.keyboard.press('Escape');
};
-4
View File
@@ -29,10 +29,6 @@ export async function openAboutPanel(page: Page) {
await page.getByTestId('about-panel-trigger').click();
}
export async function openEditorInfoPanel(page: Page) {
await page.getByTestId('header-info-button').click();
}
export async function openExperimentalFeaturesPanel(page: Page) {
await page.getByTestId('experimental-features-trigger').click();
}