mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-06 17:43:51 +00:00
Compare commits
10 Commits
zzj/fix/ta
...
v0.20.4-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbef681125 | ||
|
|
bf42a4ddb2 | ||
|
|
d57ef5c5b3 | ||
|
|
1fd3d618be | ||
|
|
7c8ba13aad | ||
|
|
b3821ad619 | ||
|
|
caa4dfedfc | ||
|
|
18dfad28d7 | ||
|
|
f43a848e18 | ||
|
|
8cec22cc64 |
@@ -1,5 +1,9 @@
|
||||
import type { Color, ColorScheme, Palette } from '@blocksuite/affine-model';
|
||||
import { isTransparent, resolveColor } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DefaultTheme,
|
||||
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';
|
||||
@@ -253,7 +257,7 @@ export class EdgelessColorPanel extends LitElement {
|
||||
accessor openColorPicker!: (e: MouseEvent) => void;
|
||||
|
||||
@property({ type: Array })
|
||||
accessor palettes: readonly Palette[] = [];
|
||||
accessor palettes: readonly Palette[] = DefaultTheme.Palettes;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor theme!: ColorScheme;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
type ColorScheme,
|
||||
DefaultTheme,
|
||||
type StrokeStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { type ColorScheme, 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';
|
||||
@@ -44,7 +40,6 @@ 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.StrokeColorPalettes}
|
||||
.palettes=${DefaultTheme.StrokeColorShortPalettes}
|
||||
.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.StrokeColorPalettes}
|
||||
.palettes=${DefaultTheme.StrokeColorShortPalettes}
|
||||
.hasTransparent=${!this.edgeless.doc
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_color_picker')}
|
||||
|
||||
@@ -75,9 +75,10 @@ export class EdgelessShapeMenu extends SignalWatcher(
|
||||
const filled = !isTransparent(value);
|
||||
const fillColor = value;
|
||||
const strokeColor = filled
|
||||
? DefaultTheme.StrokeColorPalettes.find(palette => palette.key === key)
|
||||
?.value
|
||||
: DefaultTheme.StrokeColorMap.Grey;
|
||||
? DefaultTheme.StrokeColorShortPalettes.find(
|
||||
palette => palette.key === key
|
||||
)?.value
|
||||
: DefaultTheme.StrokeColorShortMap.Grey;
|
||||
|
||||
const { shapeName } = this._props$.value;
|
||||
this.edgeless.std
|
||||
@@ -173,7 +174,7 @@ export class EdgelessShapeMenu extends SignalWatcher(
|
||||
class="one-way"
|
||||
.value=${fillColor}
|
||||
.theme=${this._theme$.value}
|
||||
.palettes=${DefaultTheme.FillColorPalettes}
|
||||
.palettes=${DefaultTheme.FillColorShortPalettes}
|
||||
.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.StrokeColorPalettes}
|
||||
.palettes=${DefaultTheme.StrokeColorShortPalettes}
|
||||
@select=${(e: ColorEvent) => this.onChange({ color: e.detail })}
|
||||
></edgeless-color-panel>
|
||||
</div>
|
||||
|
||||
@@ -134,13 +134,12 @@ 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>
|
||||
`;
|
||||
@@ -159,7 +158,6 @@ export class EdgelessChangeBrushButton extends WithDisposable(LitElement) {
|
||||
<edgeless-color-panel
|
||||
.value=${selectedColor}
|
||||
.theme=${colorScheme}
|
||||
.palettes=${DefaultTheme.Palettes}
|
||||
@select=${this._setBrushColor}
|
||||
>
|
||||
</edgeless-color-panel>
|
||||
|
||||
@@ -373,13 +373,12 @@ 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,7 +13,6 @@ import { renderToolbarSeparator } from '@blocksuite/affine-components/toolbar';
|
||||
import {
|
||||
type ColorScheme,
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
DefaultTheme,
|
||||
type FrameBlockModel,
|
||||
NoteBlockModel,
|
||||
NoteDisplayMode,
|
||||
@@ -201,13 +200,12 @@ 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>
|
||||
`;
|
||||
@@ -229,7 +227,6 @@ export class EdgelessChangeFrameButton extends WithDisposable(LitElement) {
|
||||
<edgeless-color-panel
|
||||
.value=${background}
|
||||
.theme=${colorScheme}
|
||||
.palettes=${DefaultTheme.Palettes}
|
||||
@select=${this._setFrameBackground}
|
||||
>
|
||||
</edgeless-color-panel>
|
||||
|
||||
@@ -338,7 +338,6 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
|
||||
.colors=${colors}
|
||||
.colorType=${type}
|
||||
.theme=${colorScheme}
|
||||
.palettes=${DefaultTheme.Palettes}
|
||||
>
|
||||
</edgeless-color-picker-button>
|
||||
`;
|
||||
@@ -362,7 +361,6 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
|
||||
aria-label="Fill colors"
|
||||
.value=${selectedFillColor}
|
||||
.theme=${colorScheme}
|
||||
.palettes=${DefaultTheme.Palettes}
|
||||
@select=${this._setShapeFillColor}
|
||||
>
|
||||
</edgeless-color-panel>
|
||||
@@ -390,7 +388,6 @@ export class EdgelessChangeShapeButton extends WithDisposable(LitElement) {
|
||||
.colors=${colors}
|
||||
.colorType=${type}
|
||||
.theme=${colorScheme}
|
||||
.palettes=${DefaultTheme.Palettes}
|
||||
.hollowCircle=${true}
|
||||
>
|
||||
<div
|
||||
@@ -453,8 +450,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()}
|
||||
@@ -465,7 +462,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,6 +344,10 @@ 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(
|
||||
[
|
||||
@@ -389,14 +393,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=${DefaultTheme.Palettes}
|
||||
.palettes=${palettes}
|
||||
>
|
||||
</edgeless-color-picker-button>
|
||||
`;
|
||||
@@ -418,7 +422,7 @@ export class EdgelessChangeTextMenu extends WithDisposable(LitElement) {
|
||||
<edgeless-color-panel
|
||||
.value=${selectedColor}
|
||||
.theme=${colorScheme}
|
||||
.palettes=${DefaultTheme.Palettes}
|
||||
.palettes=${palettes}
|
||||
@select=${this._setTextColor}
|
||||
></edgeless-color-panel>
|
||||
</editor-menu-button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ColorScheme, Palette } from '@blocksuite/affine-model';
|
||||
import { resolveColor } from '@blocksuite/affine-model';
|
||||
import { DefaultTheme, 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[] = [];
|
||||
accessor palettes: Palette[] = DefaultTheme.Palettes;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor pick!: (event: PickColorEvent) => void;
|
||||
|
||||
@@ -23,7 +23,7 @@ export const LINE_WIDTHS = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Use `DefaultTheme.StrokeColorMap` instead.
|
||||
* Use `DefaultTheme.StrokeColorShortMap` instead.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
@@ -44,7 +44,7 @@ export enum LineColor {
|
||||
export const LineColorMap = createEnumMap(LineColor);
|
||||
|
||||
/**
|
||||
* Use `DefaultTheme.StrokeColorPalettes` instead.
|
||||
* Use `DefaultTheme.StrokeColorShortPalettes` instead.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
|
||||
@@ -77,11 +77,11 @@ export abstract class MindmapStyleGetter {
|
||||
|
||||
export class StyleOne extends MindmapStyleGetter {
|
||||
private readonly _colorOrders = [
|
||||
DefaultTheme.StrokeColorMap.Purple,
|
||||
DefaultTheme.StrokeColorMap.Magenta,
|
||||
DefaultTheme.StrokeColorMap.Orange,
|
||||
DefaultTheme.StrokeColorMap.Yellow,
|
||||
DefaultTheme.StrokeColorMap.Green,
|
||||
DefaultTheme.StrokeColorShortMap.Purple,
|
||||
DefaultTheme.StrokeColorShortMap.Magenta,
|
||||
DefaultTheme.StrokeColorShortMap.Orange,
|
||||
DefaultTheme.StrokeColorShortMap.Yellow,
|
||||
DefaultTheme.StrokeColorShortMap.Green,
|
||||
'#7ae2d5',
|
||||
];
|
||||
|
||||
@@ -188,9 +188,9 @@ export const styleOne = new StyleOne();
|
||||
|
||||
export class StyleTwo extends MindmapStyleGetter {
|
||||
private readonly _colorOrders = [
|
||||
DefaultTheme.StrokeColorMap.Blue,
|
||||
DefaultTheme.StrokeColorShortMap.Blue,
|
||||
'#7ae2d5',
|
||||
DefaultTheme.StrokeColorMap.Yellow,
|
||||
DefaultTheme.StrokeColorShortMap.Yellow,
|
||||
];
|
||||
|
||||
readonly root = {
|
||||
@@ -207,7 +207,7 @@ export class StyleTwo extends MindmapStyleGetter {
|
||||
color: DefaultTheme.pureBlack,
|
||||
|
||||
filled: true,
|
||||
fillColor: DefaultTheme.StrokeColorMap.Yellow,
|
||||
fillColor: DefaultTheme.StrokeColorShortMap.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.StrokeColorMap.Yellow,
|
||||
DefaultTheme.StrokeColorMap.Green,
|
||||
DefaultTheme.StrokeColorShortMap.Yellow,
|
||||
DefaultTheme.StrokeColorShortMap.Green,
|
||||
'#5cc7ba',
|
||||
];
|
||||
|
||||
@@ -317,7 +317,7 @@ export class StyleThree extends MindmapStyleGetter {
|
||||
color: DefaultTheme.pureBlack,
|
||||
|
||||
filled: true,
|
||||
fillColor: DefaultTheme.StrokeColorMap.Yellow,
|
||||
fillColor: DefaultTheme.StrokeColorShortMap.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.StrokeColorMap.Purple,
|
||||
DefaultTheme.StrokeColorMap.Magenta,
|
||||
DefaultTheme.StrokeColorMap.Orange,
|
||||
DefaultTheme.StrokeColorMap.Yellow,
|
||||
DefaultTheme.StrokeColorMap.Green,
|
||||
DefaultTheme.StrokeColorMap.Blue,
|
||||
DefaultTheme.StrokeColorShortMap.Purple,
|
||||
DefaultTheme.StrokeColorShortMap.Magenta,
|
||||
DefaultTheme.StrokeColorShortMap.Orange,
|
||||
DefaultTheme.StrokeColorShortMap.Yellow,
|
||||
DefaultTheme.StrokeColorShortMap.Green,
|
||||
DefaultTheme.StrokeColorShortMap.Blue,
|
||||
];
|
||||
|
||||
readonly root = {
|
||||
|
||||
@@ -72,15 +72,44 @@ const NoteBackgroundColorPalettes: Palette[] = [
|
||||
...buildPalettes(NoteBackgroundColorMap),
|
||||
] as const;
|
||||
|
||||
const StrokeColorMap = { ...Medium, Black, White } as const;
|
||||
const StrokeColorShortMap = { ...Medium, Black, White } as const;
|
||||
|
||||
const StrokeColorPalettes: Palette[] = [
|
||||
...buildPalettes(StrokeColorMap),
|
||||
const StrokeColorShortPalettes: Palette[] = [
|
||||
...buildPalettes(StrokeColorShortMap),
|
||||
] as const;
|
||||
|
||||
const FillColorMap = { ...Medium, Black, White } as const;
|
||||
const FillColorShortMap = { ...Medium, Black, White } as const;
|
||||
|
||||
const FillColorPalettes: Palette[] = [...buildPalettes(FillColorMap)] 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;
|
||||
|
||||
export const DefaultTheme: Theme = {
|
||||
pureBlack,
|
||||
@@ -89,18 +118,19 @@ export const DefaultTheme: Theme = {
|
||||
white: White,
|
||||
transparent: Transparent,
|
||||
textColor: Medium.Blue,
|
||||
// Custom button should be selected by default,
|
||||
// add transparent `ff` to distinguish `#000000`.
|
||||
shapeTextColor: '#000000ff',
|
||||
shapeTextColor: pureBlack,
|
||||
shapeStrokeColor: Medium.Yellow,
|
||||
shapeFillColor: Medium.Yellow,
|
||||
connectorColor: Medium.Grey,
|
||||
noteBackgrounColor: NoteBackgroundColorMap.White,
|
||||
Palettes,
|
||||
StrokeColorMap,
|
||||
StrokeColorPalettes,
|
||||
FillColorMap,
|
||||
FillColorPalettes,
|
||||
ShapeTextColorPalettes,
|
||||
NoteBackgroundColorMap,
|
||||
NoteBackgroundColorPalettes,
|
||||
StrokeColorShortMap,
|
||||
StrokeColorShortPalettes,
|
||||
FillColorShortMap,
|
||||
FillColorShortPalettes,
|
||||
ShapeTextColorShortMap,
|
||||
ShapeTextColorShortPalettes,
|
||||
} as const;
|
||||
|
||||
@@ -21,16 +21,20 @@ export const ThemeSchema = z.object({
|
||||
shapeFillColor: ColorSchema,
|
||||
connectorColor: ColorSchema,
|
||||
noteBackgrounColor: ColorSchema,
|
||||
// Universal color palette
|
||||
|
||||
// Universal color palettes
|
||||
Palettes: 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),
|
||||
ShapeTextColorPalettes: 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;
|
||||
}
|
||||
|
||||
// With high enough zoom, fallback to DOM rendering
|
||||
const zoomThreshold = 1;
|
||||
const zoomThreshold = 1; // With high enough zoom, fallback to DOM rendering
|
||||
const debounceTime = 1000; // During this period, fallback to DOM
|
||||
const debug = false; // Toggle for debug logs
|
||||
|
||||
export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
||||
@@ -66,6 +66,10 @@ 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);
|
||||
@@ -134,7 +138,7 @@ export class ViewportTurboRendererExtension extends LifeCycleWatcher {
|
||||
() => {
|
||||
this.refresh().catch(console.error);
|
||||
},
|
||||
1000, // During this period, fallback to DOM
|
||||
debounceTime,
|
||||
{ leading: false, trailing: true }
|
||||
);
|
||||
|
||||
@@ -257,6 +261,13 @@ 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;
|
||||
@@ -281,13 +292,6 @@ 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 &&
|
||||
@@ -297,4 +301,11 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,14 @@ export function toDraftModel<Model extends BlockModel = BlockModel>(
|
||||
origin: Model
|
||||
): DraftModel<Model> {
|
||||
const { id, version, flavour, role, keys, text, children } = origin;
|
||||
|
||||
const isFlatData = origin.schema.model.isFlatData;
|
||||
const props = origin.keys.reduce((acc, key) => {
|
||||
const target = isFlatData ? origin.props : origin;
|
||||
const value = target[key as keyof typeof target];
|
||||
return {
|
||||
...acc,
|
||||
[key]: origin[key as keyof Model],
|
||||
[key]: value,
|
||||
};
|
||||
}, {} as ModelProps<Model>);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ type CreateProxyOptions = {
|
||||
transform?: Transform;
|
||||
onDispose: Slot;
|
||||
shouldByPassSignal: () => boolean;
|
||||
shouldByPassYjs: () => boolean;
|
||||
byPassSignalUpdate: (fn: () => void) => void;
|
||||
stashed: Set<string | number>;
|
||||
initialized: () => boolean;
|
||||
@@ -58,6 +59,7 @@ function createProxy(
|
||||
const {
|
||||
onDispose,
|
||||
shouldByPassSignal,
|
||||
shouldByPassYjs,
|
||||
byPassSignalUpdate,
|
||||
basePath,
|
||||
onChange,
|
||||
@@ -141,6 +143,9 @@ function createProxy(
|
||||
|
||||
if (isPureObject(value)) {
|
||||
const syncYMap = () => {
|
||||
if (shouldByPassYjs()) {
|
||||
return;
|
||||
}
|
||||
yMap.forEach((_, key) => {
|
||||
if (initialized() && keyWithoutPrefix(key).startsWith(fullPath)) {
|
||||
yMap.delete(key);
|
||||
@@ -185,7 +190,7 @@ function createProxy(
|
||||
|
||||
const yValue = native2Y(value);
|
||||
const next = transform(firstKey, value, yValue);
|
||||
if (!isStashed && initialized()) {
|
||||
if (!isStashed && initialized() && !shouldByPassYjs()) {
|
||||
yMap.doc?.transact(
|
||||
() => {
|
||||
yMap.set(keyWithPrefix(fullPath), yValue);
|
||||
@@ -238,7 +243,7 @@ function createProxy(
|
||||
});
|
||||
};
|
||||
|
||||
if (!isStashed && initialized()) {
|
||||
if (!isStashed && initialized() && !shouldByPassYjs()) {
|
||||
yMap.doc?.transact(
|
||||
() => {
|
||||
const fullKey = keyWithPrefix(fullPath);
|
||||
@@ -292,12 +297,17 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
|
||||
if (this._stashed.has(firstKey)) {
|
||||
return;
|
||||
}
|
||||
void keys.reduce((acc, key, index, arr) => {
|
||||
if (index === arr.length - 1) {
|
||||
acc[key] = y2Native(value);
|
||||
}
|
||||
return acc[key] as UnRecord;
|
||||
}, proxy as UnRecord);
|
||||
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);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type.action === 'delete') {
|
||||
@@ -307,12 +317,26 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
|
||||
if (this._stashed.has(firstKey)) {
|
||||
return;
|
||||
}
|
||||
void keys.reduce((acc, key, index, arr) => {
|
||||
if (index === arr.length - 1) {
|
||||
delete acc[key];
|
||||
}
|
||||
return acc[key] as UnRecord;
|
||||
}, proxy as UnRecord);
|
||||
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);
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
@@ -393,6 +417,8 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
|
||||
return root;
|
||||
};
|
||||
|
||||
private _byPassYjs = false;
|
||||
|
||||
private readonly _getProxy = (
|
||||
source: UnRecord,
|
||||
root: UnRecord,
|
||||
@@ -402,6 +428,7 @@ 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,
|
||||
@@ -410,6 +437,12 @@ 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,
|
||||
@@ -453,3 +486,9 @@ 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 type { BlockModel } from '../model/block/block-model';
|
||||
import type { DraftModel } from '../model/block/draft';
|
||||
import { BlockModel } from '../model/block/block-model';
|
||||
import { type DraftModel, toDraftModel } 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>>;
|
||||
model: DraftModel<BlockModel<Props>> | BlockModel<Props>;
|
||||
assets: AssetsManager;
|
||||
};
|
||||
|
||||
@@ -42,10 +42,16 @@ export class BaseBlockTransformer<Props extends object = object> {
|
||||
) as Props;
|
||||
}
|
||||
|
||||
protected _propsToSnapshot(model: DraftModel) {
|
||||
protected _propsToSnapshot(model: DraftModel | BlockModel) {
|
||||
let draftModel: DraftModel;
|
||||
if (model instanceof BlockModel) {
|
||||
draftModel = toDraftModel(model);
|
||||
} else {
|
||||
draftModel = model;
|
||||
}
|
||||
return Object.fromEntries(
|
||||
model.keys.map(key => {
|
||||
const value = model[key as keyof typeof model];
|
||||
draftModel.keys.map(key => {
|
||||
const value = draftModel[key as keyof typeof draftModel];
|
||||
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.FillColorMap.Orange,
|
||||
fillColor: DefaultTheme.FillColorShortMap.Orange,
|
||||
});
|
||||
expect(
|
||||
std.get(EditPropsStore).lastProps$.value[`shape:${ShapeType.Rect}`]
|
||||
.fillColor
|
||||
).toBe(DefaultTheme.FillColorMap.Orange);
|
||||
).toBe(DefaultTheme.FillColorShortMap.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.FillColorMap.Yellow);
|
||||
expect(diamondShape.fillColor).toBe(DefaultTheme.FillColorShortMap.Yellow);
|
||||
service.crud.updateElement(diamondId, {
|
||||
fillColor: DefaultTheme.FillColorMap.Blue,
|
||||
fillColor: DefaultTheme.FillColorShortMap.Blue,
|
||||
});
|
||||
expect(
|
||||
std.get(EditPropsStore).lastProps$.value[`shape:${ShapeType.Diamond}`]
|
||||
.fillColor
|
||||
).toBe(DefaultTheme.FillColorMap.Blue);
|
||||
).toBe(DefaultTheme.FillColorShortMap.Blue);
|
||||
|
||||
// rounded rect shape
|
||||
const roundedRectId = service.crud.addElement('shape', {
|
||||
@@ -81,13 +81,15 @@ describe('apply last props', () => {
|
||||
const roundedRectShape = service.crud.getElementById(
|
||||
roundedRectId
|
||||
) as ShapeElementModel;
|
||||
expect(roundedRectShape.fillColor).toBe(DefaultTheme.FillColorMap.Yellow);
|
||||
expect(roundedRectShape.fillColor).toBe(
|
||||
DefaultTheme.FillColorShortMap.Yellow
|
||||
);
|
||||
service.crud.updateElement(roundedRectId, {
|
||||
fillColor: DefaultTheme.FillColorMap.Green,
|
||||
fillColor: DefaultTheme.FillColorShortMap.Green,
|
||||
});
|
||||
expect(
|
||||
std.get(EditPropsStore).lastProps$.value['shape:roundedRect'].fillColor
|
||||
).toBe(DefaultTheme.FillColorMap.Green);
|
||||
).toBe(DefaultTheme.FillColorShortMap.Green);
|
||||
|
||||
// apply last props
|
||||
const rectId2 = service.crud.addElement('shape', {
|
||||
@@ -97,7 +99,7 @@ describe('apply last props', () => {
|
||||
const rectShape2 = service.crud.getElementById(
|
||||
rectId2
|
||||
) as ShapeElementModel;
|
||||
expect(rectShape2.fillColor).toBe(DefaultTheme.FillColorMap.Orange);
|
||||
expect(rectShape2.fillColor).toBe(DefaultTheme.FillColorShortMap.Orange);
|
||||
|
||||
const diamondId2 = service.crud.addElement('shape', {
|
||||
shapeType: ShapeType.Diamond,
|
||||
@@ -106,7 +108,7 @@ describe('apply last props', () => {
|
||||
const diamondShape2 = service.crud.getElementById(
|
||||
diamondId2
|
||||
) as ShapeElementModel;
|
||||
expect(diamondShape2.fillColor).toBe(DefaultTheme.FillColorMap.Blue);
|
||||
expect(diamondShape2.fillColor).toBe(DefaultTheme.FillColorShortMap.Blue);
|
||||
|
||||
const roundedRectId2 = service.crud.addElement('shape', {
|
||||
shapeType: ShapeType.Rect,
|
||||
@@ -116,7 +118,9 @@ describe('apply last props', () => {
|
||||
const roundedRectShape2 = service.crud.getElementById(
|
||||
roundedRectId2
|
||||
) as ShapeElementModel;
|
||||
expect(roundedRectShape2.fillColor).toBe(DefaultTheme.FillColorMap.Green);
|
||||
expect(roundedRectShape2.fillColor).toBe(
|
||||
DefaultTheme.FillColorShortMap.Green
|
||||
);
|
||||
});
|
||||
|
||||
test('connector', () => {
|
||||
@@ -204,14 +208,14 @@ describe('apply last props', () => {
|
||||
expect(text.color).toBe(DefaultTheme.textColor);
|
||||
expect(text.fontFamily).toBe(FontFamily.Inter);
|
||||
service.crud.updateElement(id, {
|
||||
color: DefaultTheme.StrokeColorMap.Green,
|
||||
color: DefaultTheme.StrokeColorShortMap.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.StrokeColorMap.Green);
|
||||
expect(text2.color).toBe(DefaultTheme.StrokeColorShortMap.Green);
|
||||
expect(text2.fontFamily).toBe(FontFamily.OrelegaOne);
|
||||
});
|
||||
|
||||
@@ -246,13 +250,13 @@ describe('apply last props', () => {
|
||||
const note = service.crud.getElementById(id) as FrameBlockModel;
|
||||
expect(note.background).toBe('transparent');
|
||||
service.crud.updateElement(id, {
|
||||
background: DefaultTheme.StrokeColorMap.Purple,
|
||||
background: DefaultTheme.StrokeColorShortMap.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.StrokeColorMap.Purple);
|
||||
expect(frame2.background).toBe(DefaultTheme.StrokeColorShortMap.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 } from '../utils';
|
||||
import { createTestingModule, sleep } from '../utils';
|
||||
import { Listeners } from './provider';
|
||||
|
||||
export const test = ava as TestFn<{
|
||||
@@ -201,3 +201,55 @@ 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,6 +8,7 @@ declare global {
|
||||
'__test__.event': { count: number };
|
||||
'__test__.event2': { count: number };
|
||||
'__test__.throw': { count: number };
|
||||
'__test__.suppressThrow': {};
|
||||
'__test__.requestId': {};
|
||||
}
|
||||
}
|
||||
@@ -32,6 +33,11 @@ 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,6 +19,12 @@ 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
|
||||
*/
|
||||
@@ -50,6 +56,9 @@ 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() {
|
||||
@@ -78,7 +87,16 @@ export class EventBus
|
||||
*/
|
||||
emit<T extends EventName>(event: T, payload: Events[T]) {
|
||||
this.logger.log(`Dispatch event: ${event}`);
|
||||
return this.emitter.emit(event, payload);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,10 +133,11 @@ export class EventBus
|
||||
return await listener(payload);
|
||||
} catch (e) {
|
||||
if (suppressError) {
|
||||
this.logger.error(
|
||||
`Error happened when handling event ${signature}`,
|
||||
e
|
||||
);
|
||||
this.emitter.emit('error', {
|
||||
event,
|
||||
payload,
|
||||
error: e,
|
||||
} as EventHandlerErrorPayload);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DynamicModule } from '@nestjs/common';
|
||||
|
||||
import { Config } from '../../config';
|
||||
import { QueueRedis } from '../../redis';
|
||||
import { QUEUES } from './def';
|
||||
import { Queue, QUEUES } from './def';
|
||||
import { JobExecutor } from './executor';
|
||||
import { JobQueue } from './queue';
|
||||
import { JobHandlerScanner } from './scanner';
|
||||
@@ -25,7 +25,15 @@ export class JobModule {
|
||||
},
|
||||
inject: [Config, QueueRedis],
|
||||
}),
|
||||
BullModule.registerQueue(...QUEUES.map(name => ({ name }))),
|
||||
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 };
|
||||
})
|
||||
),
|
||||
],
|
||||
providers: [JobQueue, JobExecutor, JobHandlerScanner],
|
||||
exports: [JobQueue],
|
||||
|
||||
@@ -92,6 +92,7 @@ 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',
|
||||
model: 'sonar-reasoning-pro',
|
||||
messages: [],
|
||||
},
|
||||
// use for believer plan
|
||||
|
||||
@@ -187,7 +187,7 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
|
||||
|
||||
async generateText(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'llama-3.1-sonar-small-128k-online',
|
||||
model: string = 'sonar',
|
||||
options: CopilotChatOptions = {}
|
||||
): Promise<string> {
|
||||
await this.checkParams({ messages, model, options });
|
||||
@@ -221,11 +221,12 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
|
||||
message: data.detail[0].msg || 'Unexpected perplexity response',
|
||||
});
|
||||
} else {
|
||||
const parser = new CitationParser();
|
||||
const citationParser = new CitationParser();
|
||||
const { content } = data.choices[0].message;
|
||||
const { citations } = data;
|
||||
let result = parser.parse(content, citations);
|
||||
result += parser.end();
|
||||
let result = content.replaceAll(/<\/?think>\n/g, '\n---\n');
|
||||
result = citationParser.parse(result, citations);
|
||||
result += citationParser.end();
|
||||
return result;
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -236,7 +237,7 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
|
||||
|
||||
async *generateTextStream(
|
||||
messages: PromptMessage[],
|
||||
model: string = 'llama-3.1-sonar-small-128k-online',
|
||||
model: string = 'sonar',
|
||||
options: CopilotChatOptions = {}
|
||||
): AsyncIterable<string> {
|
||||
await this.checkParams({ messages, model, options });
|
||||
@@ -264,7 +265,7 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
|
||||
params
|
||||
);
|
||||
if (response.body) {
|
||||
const parser = new CitationParser();
|
||||
const citationParser = new CitationParser();
|
||||
const provider = this.type;
|
||||
const eventStream = response.body
|
||||
.pipeThrough(new TextDecoderStream())
|
||||
@@ -289,12 +290,13 @@ export class PerplexityProvider implements CopilotTextToTextProvider {
|
||||
}
|
||||
const { content } = data.choices[0].delta;
|
||||
const { citations } = data;
|
||||
const result = parser.parse(content, citations);
|
||||
let result = content.replaceAll(/<\/?think>\n?/g, '\n---\n');
|
||||
result = citationParser.parse(result, citations);
|
||||
controller.enqueue(result);
|
||||
}
|
||||
},
|
||||
flush(controller) {
|
||||
controller.enqueue(parser.end());
|
||||
controller.enqueue(citationParser.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, OnEvent, OnJob } from '../../base';
|
||||
import { EventBus, JobQueue, OnJob } from '../../base';
|
||||
import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
@@ -126,6 +126,15 @@ 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,
|
||||
@@ -133,19 +142,4 @@ 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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||
import type { MindmapElementModel } from '@blocksuite/affine/blocks';
|
||||
|
||||
import { createTextRenderer } from '../components/text-renderer';
|
||||
import { createAIScrollableTextRenderer } from '../components/ai-scrollable-text-renderer';
|
||||
import {
|
||||
createMindmapExecuteRenderer,
|
||||
createMindmapRenderer,
|
||||
@@ -52,5 +52,5 @@ export function actionToAnswerRenderer<
|
||||
return createImageRenderer(host, { height: 300 });
|
||||
}
|
||||
|
||||
return createTextRenderer(host, { maxHeight: 320 });
|
||||
return createAIScrollableTextRenderer(host, {}, 320, true);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
replaceWithMarkdown,
|
||||
} from './actions/page-response';
|
||||
import type { AIItemConfig } from './components/ai-item/types';
|
||||
import { createTextRenderer } from './components/text-renderer';
|
||||
import { createAIScrollableTextRenderer } from './components/ai-scrollable-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: createTextRenderer(panel.host, { maxHeight: 320 }),
|
||||
answerRenderer: createAIScrollableTextRenderer(panel.host, {}, 320, true),
|
||||
finishStateConfig: buildFinishConfig(panel, 'chat', ctx),
|
||||
generatingStateConfig: buildGeneratingConfig(),
|
||||
errorStateConfig: buildErrorConfig(panel),
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
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,7 +88,6 @@ const customHeadingStyles = css`
|
||||
`;
|
||||
|
||||
export type TextRendererOptions = {
|
||||
maxHeight?: number;
|
||||
customHeading?: boolean;
|
||||
extensions?: ExtensionType[];
|
||||
additionalMiddlewares?: TransformerMiddleware[];
|
||||
@@ -284,18 +283,12 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const { maxHeight, customHeading } = this.options;
|
||||
const { 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,
|
||||
@@ -332,7 +325,6 @@ 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,6 +31,7 @@ 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';
|
||||
@@ -107,6 +108,10 @@ 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: strokeColorPalettes,
|
||||
palettes: StrokeColorShortPalettes,
|
||||
getCurrentColor: getCurrentStrokeColor,
|
||||
} = usePalettes(
|
||||
DefaultTheme.StrokeColorPalettes,
|
||||
DefaultTheme.StrokeColorShortPalettes,
|
||||
DefaultTheme.connectorColor
|
||||
);
|
||||
const { palettes: textColorPalettes, getCurrentColor: getCurrentTextColor } =
|
||||
usePalettes(DefaultTheme.StrokeColorPalettes, DefaultTheme.black);
|
||||
usePalettes(DefaultTheme.StrokeColorShortPalettes, DefaultTheme.black);
|
||||
|
||||
const connecterStyleItems = useMemo<RadioItem[]>(
|
||||
() => [
|
||||
@@ -165,7 +165,7 @@ export const ConnectorSettings = () => {
|
||||
|
||||
const colorItems = useMemo(() => {
|
||||
const { stroke } = settings.connector;
|
||||
return strokeColorPalettes.map(({ key, value, resolvedValue }) => {
|
||||
return StrokeColorShortPalettes.map(({ key, value, resolvedValue }) => {
|
||||
const handler = () => {
|
||||
editorSetting.set('connector', { stroke: value });
|
||||
};
|
||||
@@ -181,7 +181,7 @@ export const ConnectorSettings = () => {
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings, strokeColorPalettes]);
|
||||
}, [editorSetting, settings, StrokeColorShortPalettes]);
|
||||
|
||||
const startEndPointItems = useMemo(() => {
|
||||
const { frontEndpointStyle } = settings.connector;
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"e6t9tKz8Sy": {
|
||||
"index": "a5",
|
||||
"seed": 338503204,
|
||||
"color": "#000000ff",
|
||||
"color": "#000000",
|
||||
"fillColor": "#fcd34d",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
@@ -56,7 +56,7 @@
|
||||
"F8qB_zDC5Q": {
|
||||
"index": "a6",
|
||||
"seed": 1896265661,
|
||||
"color": "#000000ff",
|
||||
"color": "#000000",
|
||||
"fillColor": "#fcd34d",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
@@ -80,7 +80,7 @@
|
||||
"mPR44JBpcd": {
|
||||
"index": "a7",
|
||||
"seed": 2073974140,
|
||||
"color": "#000000ff",
|
||||
"color": "#000000",
|
||||
"fillColor": "#fcd34d",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
@@ -104,7 +104,7 @@
|
||||
"cmtluc3FWR": {
|
||||
"index": "a8",
|
||||
"seed": 1457248130,
|
||||
"color": "#000000ff",
|
||||
"color": "#000000",
|
||||
"fillColor": "#fcd34d",
|
||||
"filled": true,
|
||||
"fontFamily": "blocksuite:surface:Inter",
|
||||
@@ -128,7 +128,7 @@
|
||||
"knt_TKvACR": {
|
||||
"index": "a9",
|
||||
"seed": 1896265661,
|
||||
"color": "#000000ff",
|
||||
"color": "#000000",
|
||||
"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.FillColorPalettes,
|
||||
...DefaultTheme.FillColorShortPalettes,
|
||||
],
|
||||
DefaultTheme.transparent
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ export const PenSettings = () => {
|
||||
const { editorSetting } = framework.get(EditorSettingService);
|
||||
const settings = useLiveData(editorSetting.settings$);
|
||||
const { palettes, getCurrentColor } = usePalettes(
|
||||
DefaultTheme.StrokeColorPalettes,
|
||||
DefaultTheme.StrokeColorShortPalettes,
|
||||
DefaultTheme.black
|
||||
);
|
||||
|
||||
|
||||
@@ -59,11 +59,19 @@ export const ShapeSettings = () => {
|
||||
palettes: strokeColorPalettes,
|
||||
getCurrentColor: getCurrentStrokeColor,
|
||||
} = usePalettes(
|
||||
DefaultTheme.StrokeColorPalettes,
|
||||
DefaultTheme.StrokeColorShortPalettes,
|
||||
DefaultTheme.shapeStrokeColor
|
||||
);
|
||||
const { palettes: fillColorPalettes, getCurrentColor: getCurrentFillColor } =
|
||||
usePalettes(DefaultTheme.FillColorPalettes, DefaultTheme.shapeFillColor);
|
||||
usePalettes(
|
||||
DefaultTheme.FillColorShortPalettes,
|
||||
DefaultTheme.shapeFillColor
|
||||
);
|
||||
const { palettes: textColorPalettes, getCurrentColor: getCurrentTextColor } =
|
||||
usePalettes(
|
||||
DefaultTheme.ShapeTextColorShortPalettes,
|
||||
DefaultTheme.shapeTextColor
|
||||
);
|
||||
|
||||
const [currentShape, setCurrentShape] = useState<ShapeName>(ShapeType.Rect);
|
||||
|
||||
@@ -317,7 +325,7 @@ export const ShapeSettings = () => {
|
||||
|
||||
const textColorItems = useMemo(() => {
|
||||
const { color } = settings[`shape:${currentShape}`];
|
||||
return strokeColorPalettes.map(({ key, value, resolvedValue }) => {
|
||||
return textColorPalettes.map(({ key, value, resolvedValue }) => {
|
||||
const handler = () => {
|
||||
editorSetting.set(`shape:${currentShape}`, { color: value });
|
||||
};
|
||||
@@ -333,7 +341,7 @@ export const ShapeSettings = () => {
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
}, [editorSetting, settings, currentShape, strokeColorPalettes]);
|
||||
}, [editorSetting, settings, currentShape, textColorPalettes]);
|
||||
|
||||
const getElements = useCallback(
|
||||
(doc: Store) => {
|
||||
@@ -379,8 +387,8 @@ export const ShapeSettings = () => {
|
||||
|
||||
const textColor = useMemo(() => {
|
||||
const color = settings[`shape:${currentShape}`].color;
|
||||
return getCurrentStrokeColor(color);
|
||||
}, [currentShape, getCurrentStrokeColor, settings]);
|
||||
return getCurrentTextColor(color);
|
||||
}, [currentShape, getCurrentTextColor, 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.StrokeColorPalettes,
|
||||
DefaultTheme.StrokeColorShortPalettes,
|
||||
DefaultTheme.textColor
|
||||
);
|
||||
|
||||
|
||||
113
tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts
Normal file
113
tests/affine-local/e2e/blocksuite/edgeless/shape.spec.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
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');
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
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');
|
||||
@@ -236,3 +238,13 @@ 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');
|
||||
};
|
||||
|
||||
@@ -29,6 +29,10 @@ 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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user