fix: connector label editing (#12282)
Fixes [BS-3373](https://linear.app/affine-design/issue/BS-3373/connector%E7%9A%84%E5%8F%8C%E5%87%BB%E6%B7%BB%E5%8A%A0note%E8%A1%8C%E4%B8%BA%E5%8F%97%E5%88%B0%E4%BA%86%E8%A6%86%E7%9B%96%E8%8C%83%E5%9B%B4%E7%9A%84%E5%BD%B1%E5%93%8D) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Connector label elements now include identity and creator metadata. - **Bug Fixes** - Improved hit-testing for pointer interactions, resulting in more accurate detection of hovered elements. - **Refactor** - Enhanced internal comparison logic for elements, improving sorting and ordering consistency. - Strengthened type definitions for search filters, improving result accuracy and clarity. - **Tests** - Added end-to-end tests to verify correct label entry and retrieval for multiple connectors. - Introduced utility functions to fetch connector labels and improved connector creation in test actions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -142,6 +142,8 @@ export class ConnectorElementView extends GfxElementModelView<ConnectorElementMo
|
|||||||
if (!curLabelElement) {
|
if (!curLabelElement) {
|
||||||
curLabelElement = labelElement;
|
curLabelElement = labelElement;
|
||||||
|
|
||||||
|
labelElement.id = `#${this.model.id}-label`;
|
||||||
|
labelElement.creator = this.model;
|
||||||
labelElement.fillColor = 'transparent';
|
labelElement.fillColor = 'transparent';
|
||||||
labelElement.strokeColor = 'transparent';
|
labelElement.strokeColor = 'transparent';
|
||||||
labelElement.strokeWidth = 0;
|
labelElement.strokeWidth = 0;
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ const typeFilters = {
|
|||||||
model instanceof GfxLocalElementModel,
|
model instanceof GfxLocalElementModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BuiltInFilterModelMap = {
|
||||||
|
block: GfxBlockElementModel;
|
||||||
|
canvas: GfxPrimitiveElementModel;
|
||||||
|
local: GfxLocalElementModel;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BuiltInFilterType = keyof typeof typeFilters;
|
||||||
|
|
||||||
type FilterFunc = (model: GfxModel | GfxLocalElementModel) => boolean;
|
type FilterFunc = (model: GfxModel | GfxLocalElementModel) => boolean;
|
||||||
|
|
||||||
export class GridManager extends GfxExtension {
|
export class GridManager extends GfxExtension {
|
||||||
@@ -280,7 +288,7 @@ export class GridManager extends GfxExtension {
|
|||||||
* @param bound
|
* @param bound
|
||||||
* @param options
|
* @param options
|
||||||
*/
|
*/
|
||||||
search<T extends keyof typeof typeFilters>(
|
search<T extends BuiltInFilterType = 'canvas' | 'block'>(
|
||||||
bound: IBound,
|
bound: IBound,
|
||||||
options?: {
|
options?: {
|
||||||
/**
|
/**
|
||||||
@@ -297,16 +305,16 @@ export class GridManager extends GfxExtension {
|
|||||||
*/
|
*/
|
||||||
filter?: (T | FilterFunc)[] | FilterFunc;
|
filter?: (T | FilterFunc)[] | FilterFunc;
|
||||||
}
|
}
|
||||||
): T extends 'local'[] ? (GfxModel | GfxLocalElementModel)[] : GfxModel[];
|
): BuiltInFilterModelMap[T][];
|
||||||
search<T extends keyof typeof typeFilters>(
|
search<T extends BuiltInFilterType = 'canvas' | 'block'>(
|
||||||
bound: IBound,
|
bound: IBound,
|
||||||
options: {
|
options: {
|
||||||
strict?: boolean | undefined;
|
strict?: boolean | undefined;
|
||||||
useSet: true;
|
useSet: true;
|
||||||
filter?: (T | FilterFunc)[] | FilterFunc;
|
filter?: (T | FilterFunc)[] | FilterFunc;
|
||||||
}
|
}
|
||||||
): T extends 'local'[] ? Set<GfxModel | GfxLocalElementModel> : Set<GfxModel>;
|
): Set<BuiltInFilterModelMap[T]>;
|
||||||
search<T extends keyof typeof typeFilters>(
|
search<T extends BuiltInFilterType = 'canvas' | 'block'>(
|
||||||
bound: IBound,
|
bound: IBound,
|
||||||
options: {
|
options: {
|
||||||
strict?: boolean;
|
strict?: boolean;
|
||||||
|
|||||||
@@ -66,7 +66,29 @@ export class GfxViewEventManager {
|
|||||||
.search(new Bound(x - 5, y - 5, 10, 10), {
|
.search(new Bound(x - 5, y - 5, 10, 10), {
|
||||||
filter: ['canvas', 'local'],
|
filter: ['canvas', 'local'],
|
||||||
})
|
})
|
||||||
.map(model => this.gfx.view.get(model)) as GfxElementModelView[];
|
.reduce((pre, model) => {
|
||||||
|
if (
|
||||||
|
model.includesPoint(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
{
|
||||||
|
hitThreshold: 10,
|
||||||
|
responsePadding: [5, 5],
|
||||||
|
},
|
||||||
|
this.gfx.std.host
|
||||||
|
) ||
|
||||||
|
('externalBound' in model
|
||||||
|
? model.externalBound?.isPointInBound([x, y])
|
||||||
|
: false)
|
||||||
|
) {
|
||||||
|
const view = this.gfx.view.get(model) as GfxElementModelView | null;
|
||||||
|
|
||||||
|
view && pre.push(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pre;
|
||||||
|
}, [] as GfxElementModelView[]);
|
||||||
|
|
||||||
const currentStackedViews = new Set(this._hoveredElementsStack);
|
const currentStackedViews = new Set(this._hoveredElementsStack);
|
||||||
const visited = new Set<GfxElementModelView>();
|
const visited = new Set<GfxElementModelView>();
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { mutex } from 'lib0';
|
|||||||
|
|
||||||
import type { EditorHost } from '../../../view/index.js';
|
import type { EditorHost } from '../../../view/index.js';
|
||||||
import type { GfxCompatibleInterface, PointTestOptions } from '../base.js';
|
import type { GfxCompatibleInterface, PointTestOptions } from '../base.js';
|
||||||
import type { GfxGroupModel } from '../model.js';
|
import type { GfxGroupModel, GfxModel } from '../model.js';
|
||||||
import type { SurfaceBlockModel } from './surface-model.js';
|
import type { SurfaceBlockModel } from './surface-model.js';
|
||||||
|
|
||||||
export function prop<V, T extends GfxLocalElementModel>() {
|
export function prop<V, T extends GfxLocalElementModel>() {
|
||||||
@@ -61,6 +61,8 @@ export abstract class GfxLocalElementModel implements GfxCompatibleInterface {
|
|||||||
|
|
||||||
abstract readonly type: string;
|
abstract readonly type: string;
|
||||||
|
|
||||||
|
creator: GfxModel | null = null;
|
||||||
|
|
||||||
get deserializedXYWH() {
|
get deserializedXYWH() {
|
||||||
if (!this._local.has('deserializedXYWH')) {
|
if (!this._local.has('deserializedXYWH')) {
|
||||||
const xywh = this.xywh;
|
const xywh = this.xywh;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { Store } from '@blocksuite/store';
|
import type { Store } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { GfxLocalElementModel } from '../gfx/index.js';
|
|
||||||
import type { Layer } from '../gfx/layer.js';
|
import type { Layer } from '../gfx/layer.js';
|
||||||
import {
|
import {
|
||||||
type GfxGroupCompatibleInterface,
|
type GfxGroupCompatibleInterface,
|
||||||
isGfxGroupCompatibleModel,
|
isGfxGroupCompatibleModel,
|
||||||
} from '../gfx/model/base.js';
|
} from '../gfx/model/base.js';
|
||||||
import type { GfxBlockElementModel } from '../gfx/model/gfx-block-model.js';
|
import { type GfxBlockElementModel } from '../gfx/model/gfx-block-model.js';
|
||||||
import type { GfxModel } from '../gfx/model/model.js';
|
import type { GfxModel } from '../gfx/model/model.js';
|
||||||
|
import { GfxLocalElementModel } from '../gfx/model/surface/local-element-model.js';
|
||||||
import type { SurfaceBlockModel } from '../gfx/model/surface/surface-model.js';
|
import type { SurfaceBlockModel } from '../gfx/model/surface/surface-model.js';
|
||||||
|
|
||||||
export function getLayerEndZIndex(layers: Layer[], layerIndex: number) {
|
export function getLayerEndZIndex(layers: Layer[], layerIndex: number) {
|
||||||
@@ -90,6 +90,39 @@ export function renderableInEdgeless(
|
|||||||
return parent === doc.root || parent === surface;
|
return parent === doc.root || parent === surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function compareIndex(aIndex: string, bIndex: string) {
|
||||||
|
return aIndex === bIndex
|
||||||
|
? SortOrder.SAME
|
||||||
|
: aIndex < bIndex
|
||||||
|
? SortOrder.BEFORE
|
||||||
|
: SortOrder.AFTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareLocal(
|
||||||
|
a: GfxModel | GfxLocalElementModel,
|
||||||
|
b: GfxModel | GfxLocalElementModel
|
||||||
|
) {
|
||||||
|
const isALocal = a instanceof GfxLocalElementModel;
|
||||||
|
const isBLocal = b instanceof GfxLocalElementModel;
|
||||||
|
|
||||||
|
if (isALocal && a.creator && a.creator === b) {
|
||||||
|
return SortOrder.AFTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBLocal && b.creator && b.creator === a) {
|
||||||
|
return SortOrder.BEFORE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isALocal && isBLocal && a.creator && a.creator === b.creator) {
|
||||||
|
return compareIndex(a.index, b.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
a: isALocal && a.creator ? a.creator : a,
|
||||||
|
b: isBLocal && b.creator ? b.creator : b,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A comparator function for sorting elements in the surface.
|
* A comparator function for sorting elements in the surface.
|
||||||
* SortOrder.AFTER means a should be rendered after b and so on.
|
* SortOrder.AFTER means a should be rendered after b and so on.
|
||||||
@@ -99,6 +132,15 @@ export function compare(
|
|||||||
a: GfxModel | GfxLocalElementModel,
|
a: GfxModel | GfxLocalElementModel,
|
||||||
b: GfxModel | GfxLocalElementModel
|
b: GfxModel | GfxLocalElementModel
|
||||||
) {
|
) {
|
||||||
|
const result = compareLocal(a, b);
|
||||||
|
|
||||||
|
if (typeof result === 'number') {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
a = result.a;
|
||||||
|
b = result.b;
|
||||||
|
|
||||||
if (isGfxGroupCompatibleModel(a) && b.groups.includes(a)) {
|
if (isGfxGroupCompatibleModel(a) && b.groups.includes(a)) {
|
||||||
return SortOrder.BEFORE;
|
return SortOrder.BEFORE;
|
||||||
} else if (isGfxGroupCompatibleModel(b) && a.groups.includes(b)) {
|
} else if (isGfxGroupCompatibleModel(b) && a.groups.includes(b)) {
|
||||||
@@ -128,10 +170,6 @@ export function compare(
|
|||||||
aGroup = aGroup ?? a;
|
aGroup = aGroup ?? a;
|
||||||
bGroup = bGroup ?? b;
|
bGroup = bGroup ?? b;
|
||||||
|
|
||||||
return aGroup.index === bGroup.index
|
return compareIndex(aGroup.index, bGroup.index);
|
||||||
? SortOrder.SAME
|
|
||||||
: aGroup.index < bGroup.index
|
|
||||||
? SortOrder.BEFORE
|
|
||||||
: SortOrder.AFTER;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
createShapeElement,
|
createShapeElement,
|
||||||
dragBetweenViewCoords,
|
dragBetweenViewCoords,
|
||||||
edgelessCommonSetup as commonSetup,
|
edgelessCommonSetup as commonSetup,
|
||||||
|
getConnectorLabel,
|
||||||
locatorComponentToolbar,
|
locatorComponentToolbar,
|
||||||
setEdgelessTool,
|
setEdgelessTool,
|
||||||
Shape,
|
Shape,
|
||||||
@@ -332,4 +333,30 @@ test.describe('connector label with straight shape', () => {
|
|||||||
await type(page, 'c');
|
await type(page, 'c');
|
||||||
await assertEdgelessCanvasText(page, 'c');
|
await assertEdgelessCanvasText(page, 'c');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should enter the correct label', async ({ page }) => {
|
||||||
|
await commonSetup(page);
|
||||||
|
const connector1 = await addBasicConnectorElement(
|
||||||
|
page,
|
||||||
|
{ x: 100, y: 200 },
|
||||||
|
{ x: 300, y: 300 }
|
||||||
|
);
|
||||||
|
const connector2 = await addBasicConnectorElement(
|
||||||
|
page,
|
||||||
|
{ x: 300, y: 200 },
|
||||||
|
{ x: 100, y: 300 }
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.mouse.dblclick(155, 207);
|
||||||
|
await type(page, 'Connector 1');
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
expect(await getConnectorLabel(page, connector1)).toBe('Connector 1');
|
||||||
|
|
||||||
|
await page.mouse.dblclick(245, 207);
|
||||||
|
await type(page, 'Connector 2');
|
||||||
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
await expect(await getConnectorLabel(page, connector2)).toBe('Connector 2');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { ConnectorTool } from '@blocksuite/affine/gfx/connector';
|
|||||||
import { ShapeTool } from '@blocksuite/affine/gfx/shape';
|
import { ShapeTool } from '@blocksuite/affine/gfx/shape';
|
||||||
import type { IPoint, IVec } from '@blocksuite/affine/global/gfx';
|
import type { IPoint, IVec } from '@blocksuite/affine/global/gfx';
|
||||||
import { sleep } from '@blocksuite/affine/global/utils';
|
import { sleep } from '@blocksuite/affine/global/utils';
|
||||||
import type { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine/model';
|
import type {
|
||||||
|
ConnectorElementModel,
|
||||||
|
NoteBlockModel,
|
||||||
|
NoteDisplayMode,
|
||||||
|
} from '@blocksuite/affine/model';
|
||||||
import type { ToolOptions } from '@blocksuite/affine/std/gfx';
|
import type { ToolOptions } from '@blocksuite/affine/std/gfx';
|
||||||
import type { Locator, Page } from '@playwright/test';
|
import type { Locator, Page } from '@playwright/test';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
@@ -418,6 +422,23 @@ export async function assertEdgelessTool(page: Page, mode: string) {
|
|||||||
expect(type).toEqual(mode);
|
expect(type).toEqual(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getConnectorLabel(page: Page, id: string) {
|
||||||
|
const text = await page.evaluate(id => {
|
||||||
|
const container = document.querySelector('affine-edgeless-root');
|
||||||
|
if (!container) {
|
||||||
|
throw new Error('Missing edgeless page');
|
||||||
|
}
|
||||||
|
const connector = container.gfx.getElementById(id) as ConnectorElementModel;
|
||||||
|
if (!connector) {
|
||||||
|
throw new Error('Missing connector');
|
||||||
|
}
|
||||||
|
|
||||||
|
return connector.text?.toString() ?? '';
|
||||||
|
}, id);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
export async function assertEdgelessConnectorToolMode(
|
export async function assertEdgelessConnectorToolMode(
|
||||||
page: Page,
|
page: Page,
|
||||||
mode: ConnectorMode
|
mode: ConnectorMode
|
||||||
@@ -524,6 +545,7 @@ export async function addBasicConnectorElement(
|
|||||||
) {
|
) {
|
||||||
await setEdgelessTool(page, 'connector');
|
await setEdgelessTool(page, 'connector');
|
||||||
await dragBetweenCoords(page, start, end, { steps: 100 });
|
await dragBetweenCoords(page, start, end, { steps: 100 });
|
||||||
|
return (await getSelectedIds(page))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addBasicFrameElement(
|
export async function addBasicFrameElement(
|
||||||
|
|||||||
Reference in New Issue
Block a user