mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-04 19:15:33 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f723d41bd8 | |||
| cd753dcd83 | |||
| f1608d4298 | |||
| a4dd931b71 | |||
| ddc9cb7a3d | |||
| 0f19a506ac | |||
| 7223d35c89 | |||
| 3f753eddf5 | |||
| a828c74f87 | |||
| 2a80fbb993 | |||
| 262f1a47a4 | |||
| fe99e51d5b | |||
| a7e6c511a9 | |||
| bd72c931c4 | |||
| cb88156188 | |||
| 952650e1bc | |||
| 797646442d | |||
| 4b9313ce37 |
@@ -6,6 +6,7 @@ yarn install
|
||||
|
||||
# Build Server Dependencies
|
||||
yarn affine @affine/server-native build
|
||||
yarn affine @affine/reader build
|
||||
|
||||
# Create database
|
||||
yarn affine @affine/server prisma migrate reset -f
|
||||
|
||||
@@ -10,6 +10,7 @@ services:
|
||||
environment:
|
||||
DATABASE_URL: postgresql://affine:affine@db:5432/affine
|
||||
REDIS_SERVER_HOST: redis
|
||||
AFFINE_INDEXER_SEARCH_ENDPOINT: http://indexer:9308
|
||||
|
||||
db:
|
||||
image: pgvector/pgvector:pg16
|
||||
@@ -23,5 +24,19 @@ services:
|
||||
redis:
|
||||
image: redis
|
||||
|
||||
indexer:
|
||||
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.3.2}
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 65535
|
||||
hard: 65535
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
volumes:
|
||||
- manticoresearch_data:/var/lib/manticore
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
manticoresearch_data:
|
||||
|
||||
@@ -12,4 +12,4 @@ DB_DATABASE_NAME=affine
|
||||
# ELASTIC_PLATFORM=linux/arm64
|
||||
|
||||
# manticoresearch
|
||||
MANTICORE_VERSION=9.2.14
|
||||
MANTICORE_VERSION=9.3.2
|
||||
|
||||
@@ -26,7 +26,7 @@ services:
|
||||
|
||||
# https://manual.manticoresearch.com/Starting_the_server/Docker
|
||||
manticoresearch:
|
||||
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.2.14}
|
||||
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.3.2}
|
||||
ports:
|
||||
- 9308:9308
|
||||
ulimits:
|
||||
|
||||
@@ -21,8 +21,3 @@ CONFIG_LOCATION=~/.affine/self-host/config
|
||||
DB_USERNAME=affine
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=affine
|
||||
|
||||
# indexer search provider manticoresearch version
|
||||
MANTICORE_VERSION=9.2.14
|
||||
# position of the manticoresearch data to persist
|
||||
MANTICORE_DATA_LOCATION=~/.affine/self-host/manticore
|
||||
|
||||
@@ -10,8 +10,6 @@ services:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
indexer:
|
||||
condition: service_healthy
|
||||
affine_migration:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
@@ -23,7 +21,7 @@ services:
|
||||
environment:
|
||||
- REDIS_SERVER_HOST=redis
|
||||
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
|
||||
- AFFINE_INDEXER_SEARCH_ENDPOINT=http://indexer:9308
|
||||
- AFFINE_INDEXER_ENABLED=false
|
||||
restart: unless-stopped
|
||||
|
||||
affine_migration:
|
||||
@@ -39,14 +37,12 @@ services:
|
||||
environment:
|
||||
- REDIS_SERVER_HOST=redis
|
||||
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
|
||||
- AFFINE_INDEXER_SEARCH_ENDPOINT=http://indexer:9308
|
||||
- AFFINE_INDEXER_ENABLED=false
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
indexer:
|
||||
condition: service_healthy
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
@@ -78,24 +74,3 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
indexer:
|
||||
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.2.14}
|
||||
container_name: affine_indexer
|
||||
volumes:
|
||||
- ${MANTICORE_DATA_LOCATION}:/var/lib/manticore
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 65535
|
||||
hard: 65535
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
healthcheck:
|
||||
test:
|
||||
['CMD', 'wget', '-O-', 'http://127.0.0.1:9308']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -125,6 +125,7 @@ jobs:
|
||||
- name: Run BS Docs Build
|
||||
run: |
|
||||
yarn affine bs-docs build
|
||||
git checkout packages/frontend/i18n/src/i18n-completenesses.json
|
||||
git status --porcelain | grep . && {
|
||||
echo "Run 'yarn typecheck && yarn affine bs-docs build' and make sure all changes are submitted"
|
||||
exit 1
|
||||
@@ -183,6 +184,7 @@ jobs:
|
||||
yarn affine gql build
|
||||
yarn affine i18n build
|
||||
yarn affine server genconfig
|
||||
git checkout packages/frontend/i18n/src/i18n-completenesses.json
|
||||
git status --porcelain | grep . && {
|
||||
echo "Run 'yarn affine init && yarn affine gql build && yarn affine i18n build && yarn affine server genconfig' and make sure all changes are submitted"
|
||||
exit 1
|
||||
@@ -583,7 +585,7 @@ jobs:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.2.14
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -733,7 +735,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.2.14
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -957,7 +959,7 @@ jobs:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.2.14
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -1056,7 +1058,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.2.14
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -1183,7 +1185,7 @@ jobs:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.2.14
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.2.14
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
ports:
|
||||
- 6379:6379
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.2.14
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-foundation": "workspace:*",
|
||||
"@blocksuite/affine-fragment-adapter-panel": "workspace:*",
|
||||
"@blocksuite/affine-fragment-doc-title": "workspace:*",
|
||||
"@blocksuite/affine-fragment-frame-panel": "workspace:*",
|
||||
"@blocksuite/affine-fragment-outline": "workspace:*",
|
||||
@@ -209,6 +210,8 @@
|
||||
"./fragments/frame-panel/view": "./src/fragments/frame-panel/view.ts",
|
||||
"./fragments/outline": "./src/fragments/outline/index.ts",
|
||||
"./fragments/outline/view": "./src/fragments/outline/view.ts",
|
||||
"./fragments/adapter-panel": "./src/fragments/adapter-panel/index.ts",
|
||||
"./fragments/adapter-panel/view": "./src/fragments/adapter-panel/view.ts",
|
||||
"./gfx/text": "./src/gfx/text/index.ts",
|
||||
"./gfx/text/store": "./src/gfx/text/store.ts",
|
||||
"./gfx/text/view": "./src/gfx/text/view.ts",
|
||||
|
||||
@@ -19,6 +19,7 @@ import { SurfaceViewExtension } from '@blocksuite/affine-block-surface/view';
|
||||
import { SurfaceRefViewExtension } from '@blocksuite/affine-block-surface-ref/view';
|
||||
import { TableViewExtension } from '@blocksuite/affine-block-table/view';
|
||||
import { FoundationViewExtension } from '@blocksuite/affine-foundation/view';
|
||||
import { AdapterPanelViewExtension } from '@blocksuite/affine-fragment-adapter-panel/view';
|
||||
import { DocTitleViewExtension } from '@blocksuite/affine-fragment-doc-title/view';
|
||||
import { FramePanelViewExtension } from '@blocksuite/affine-fragment-frame-panel/view';
|
||||
import { OutlineViewExtension } from '@blocksuite/affine-fragment-outline/view';
|
||||
@@ -124,5 +125,6 @@ export function getInternalViewExtensions() {
|
||||
DocTitleViewExtension,
|
||||
FramePanelViewExtension,
|
||||
OutlineViewExtension,
|
||||
AdapterPanelViewExtension,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-fragment-adapter-panel';
|
||||
@@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-fragment-adapter-panel/view';
|
||||
@@ -30,6 +30,7 @@
|
||||
{ "path": "../components" },
|
||||
{ "path": "../ext-loader" },
|
||||
{ "path": "../foundation" },
|
||||
{ "path": "../fragments/adapter-panel" },
|
||||
{ "path": "../fragments/doc-title" },
|
||||
{ "path": "../fragments/frame-panel" },
|
||||
{ "path": "../fragments/outline" },
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"file-type": "^20.0.0",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"file-type": "^20.0.0",
|
||||
"file-type": "^21.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -66,7 +66,6 @@ import {
|
||||
DEFAULT_NOTE_TIP,
|
||||
} from './utils/consts.js';
|
||||
import { deleteElements } from './utils/crud.js';
|
||||
import { getNextShapeType } from './utils/hotkey-utils.js';
|
||||
import { isCanvasElement } from './utils/query.js';
|
||||
|
||||
export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
@@ -216,10 +215,8 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const { shapeName } = controller.activatedOption;
|
||||
const nextShapeName = getNextShapeType(shapeName);
|
||||
this._setEdgelessTool(ShapeTool, {
|
||||
shapeName: nextShapeName,
|
||||
shapeName: controller.cycleShapeName('prev'),
|
||||
});
|
||||
|
||||
controller.createOverlay();
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import type { ShapeToolOption } from '@blocksuite/affine-gfx-shape';
|
||||
import { ShapeType } from '@blocksuite/affine-model';
|
||||
|
||||
const shapeMap: Record<ShapeToolOption['shapeName'], number> = {
|
||||
[ShapeType.Rect]: 0,
|
||||
[ShapeType.Ellipse]: 1,
|
||||
[ShapeType.Diamond]: 2,
|
||||
[ShapeType.Triangle]: 3,
|
||||
roundedRect: 4,
|
||||
};
|
||||
const shapes = Object.keys(shapeMap) as ShapeToolOption['shapeName'][];
|
||||
|
||||
export function getNextShapeType(cur: ShapeToolOption['shapeName']) {
|
||||
return shapes[(shapeMap[cur] + 1) % shapes.length];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { ConnectorDomRendererExtension } from '../renderer/dom-elements/index.js';
|
||||
|
||||
export { ConnectorDomRendererExtension };
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './clipboard-config';
|
||||
export * from './connector-dom-renderer';
|
||||
export * from './crud-extension';
|
||||
export * from './dom-element-renderer';
|
||||
export * from './edit-props-middleware-builder';
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||
import { ConnectorMode, DefaultTheme } from '@blocksuite/affine-model';
|
||||
import {
|
||||
getBezierParameters,
|
||||
type PointLocation,
|
||||
} from '@blocksuite/global/gfx';
|
||||
|
||||
import { DomElementRendererExtension } from '../../extensions/dom-element-renderer.js';
|
||||
import type { DomRenderer } from '../dom-renderer.js';
|
||||
import type { DomElementRenderer } from './index.js';
|
||||
|
||||
/**
|
||||
* DOM renderer for connector elements.
|
||||
* Uses SVG to render connector paths, endpoints, and labels.
|
||||
*/
|
||||
export const connectorDomRenderer: DomElementRenderer<ConnectorElementModel> = (
|
||||
elementModel,
|
||||
domElement,
|
||||
renderer
|
||||
) => {
|
||||
const {
|
||||
mode,
|
||||
path: points,
|
||||
strokeStyle,
|
||||
frontEndpointStyle,
|
||||
rearEndpointStyle,
|
||||
strokeWidth,
|
||||
stroke,
|
||||
w,
|
||||
h,
|
||||
} = elementModel;
|
||||
|
||||
// Clear previous content
|
||||
domElement.innerHTML = '';
|
||||
|
||||
// Points might not be built yet in some scenarios (undo/redo, copy/paste)
|
||||
if (!points.length || points.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create SVG element
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.style.width = `${w * renderer.viewport.zoom}px`;
|
||||
svg.style.height = `${h * renderer.viewport.zoom}px`;
|
||||
svg.style.position = 'absolute';
|
||||
svg.style.top = '0';
|
||||
svg.style.left = '0';
|
||||
svg.style.pointerEvents = 'none';
|
||||
svg.style.overflow = 'visible';
|
||||
|
||||
const strokeColor = renderer.getColorValue(
|
||||
stroke,
|
||||
DefaultTheme.connectorColor,
|
||||
true
|
||||
);
|
||||
|
||||
// Render connector path
|
||||
renderConnectorPath(
|
||||
svg,
|
||||
points,
|
||||
mode,
|
||||
strokeStyle,
|
||||
strokeWidth,
|
||||
strokeColor,
|
||||
renderer.viewport.zoom
|
||||
);
|
||||
|
||||
// Render endpoints
|
||||
if (frontEndpointStyle && frontEndpointStyle !== 'None') {
|
||||
renderEndpoint(
|
||||
svg,
|
||||
points,
|
||||
frontEndpointStyle,
|
||||
'front',
|
||||
strokeWidth,
|
||||
strokeColor,
|
||||
mode,
|
||||
renderer.viewport.zoom
|
||||
);
|
||||
}
|
||||
|
||||
if (rearEndpointStyle && rearEndpointStyle !== 'None') {
|
||||
renderEndpoint(
|
||||
svg,
|
||||
points,
|
||||
rearEndpointStyle,
|
||||
'rear',
|
||||
strokeWidth,
|
||||
strokeColor,
|
||||
mode,
|
||||
renderer.viewport.zoom
|
||||
);
|
||||
}
|
||||
|
||||
// Render label if exists
|
||||
if (elementModel.hasLabel()) {
|
||||
renderConnectorLabel(elementModel, domElement, renderer);
|
||||
}
|
||||
|
||||
domElement.appendChild(svg);
|
||||
};
|
||||
|
||||
function renderConnectorPath(
|
||||
svg: SVGSVGElement,
|
||||
points: PointLocation[],
|
||||
mode: ConnectorMode,
|
||||
strokeStyle: string,
|
||||
strokeWidth: number,
|
||||
strokeColor: string,
|
||||
zoom: number
|
||||
) {
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
|
||||
let pathData = '';
|
||||
|
||||
if (mode === ConnectorMode.Curve) {
|
||||
// Bezier curve
|
||||
const bezierParams = getBezierParameters(points);
|
||||
const [p0, p1, p2, p3] = bezierParams;
|
||||
pathData = `M ${p0[0]} ${p0[1]} C ${p1[0]} ${p1[1]} ${p2[0]} ${p2[1]} ${p3[0]} ${p3[1]}`;
|
||||
} else {
|
||||
// Straight or orthogonal lines
|
||||
pathData = `M ${points[0][0]} ${points[0][1]}`;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
pathData += ` L ${points[i][0]} ${points[i][1]}`;
|
||||
}
|
||||
}
|
||||
|
||||
path.setAttribute('d', pathData);
|
||||
path.setAttribute('stroke', strokeColor);
|
||||
path.setAttribute('stroke-width', (strokeWidth * zoom).toString());
|
||||
path.setAttribute('fill', 'none');
|
||||
path.setAttribute('stroke-linecap', 'round');
|
||||
path.setAttribute('stroke-linejoin', 'round');
|
||||
|
||||
if (strokeStyle === 'dash') {
|
||||
const dashArray = `${12 * zoom},${12 * zoom}`;
|
||||
path.setAttribute('stroke-dasharray', dashArray);
|
||||
}
|
||||
|
||||
svg.appendChild(path);
|
||||
}
|
||||
|
||||
function renderEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
points: PointLocation[],
|
||||
endpointStyle: string,
|
||||
position: 'front' | 'rear',
|
||||
strokeWidth: number,
|
||||
strokeColor: string,
|
||||
mode: ConnectorMode,
|
||||
zoom: number
|
||||
) {
|
||||
const pointIndex = position === 'rear' ? points.length - 1 : 0;
|
||||
const point = points[pointIndex];
|
||||
const size = 15 * (strokeWidth / 2) * zoom;
|
||||
|
||||
// Calculate tangent direction for endpoint orientation
|
||||
let tangent: [number, number];
|
||||
if (mode === ConnectorMode.Curve) {
|
||||
const bezierParams = getBezierParameters(points);
|
||||
// For curve mode, use bezier tangent
|
||||
if (position === 'rear') {
|
||||
const lastIdx = points.length - 1;
|
||||
const prevPoint = points[lastIdx - 1];
|
||||
tangent = [point[0] - prevPoint[0], point[1] - prevPoint[1]];
|
||||
} else {
|
||||
const nextPoint = points[1];
|
||||
tangent = [nextPoint[0] - point[0], nextPoint[1] - point[1]];
|
||||
}
|
||||
} else {
|
||||
// For straight/orthogonal mode
|
||||
if (position === 'rear') {
|
||||
const prevPoint = points[points.length - 2];
|
||||
tangent = [point[0] - prevPoint[0], point[1] - prevPoint[1]];
|
||||
} else {
|
||||
const nextPoint = points[1];
|
||||
tangent = [nextPoint[0] - point[0], nextPoint[1] - point[1]];
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize tangent
|
||||
const length = Math.sqrt(tangent[0] * tangent[0] + tangent[1] * tangent[1]);
|
||||
if (length > 0) {
|
||||
tangent[0] /= length;
|
||||
tangent[1] /= length;
|
||||
}
|
||||
|
||||
// Adjust tangent direction for front endpoint
|
||||
if (position === 'front') {
|
||||
tangent[0] = -tangent[0];
|
||||
tangent[1] = -tangent[1];
|
||||
}
|
||||
|
||||
switch (endpointStyle) {
|
||||
case 'Arrow':
|
||||
renderArrowEndpoint(svg, point, tangent, size, strokeColor, zoom);
|
||||
break;
|
||||
case 'Triangle':
|
||||
renderTriangleEndpoint(svg, point, tangent, size, strokeColor, zoom);
|
||||
break;
|
||||
case 'Circle':
|
||||
renderCircleEndpoint(svg, point, tangent, size, strokeColor, zoom);
|
||||
break;
|
||||
case 'Diamond':
|
||||
renderDiamondEndpoint(svg, point, tangent, size, strokeColor, zoom);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function renderArrowEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
point: PointLocation,
|
||||
tangent: [number, number],
|
||||
size: number,
|
||||
color: string,
|
||||
zoom: number
|
||||
) {
|
||||
const angle = Math.PI / 4; // 45 degrees
|
||||
const arrowPath = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
|
||||
// Calculate arrow points
|
||||
const cos1 = Math.cos(angle);
|
||||
const sin1 = Math.sin(angle);
|
||||
const cos2 = Math.cos(-angle);
|
||||
const sin2 = Math.sin(-angle);
|
||||
|
||||
const x1 = point[0] + size * (tangent[0] * cos1 - tangent[1] * sin1);
|
||||
const y1 = point[1] + size * (tangent[0] * sin1 + tangent[1] * cos1);
|
||||
const x2 = point[0] + size * (tangent[0] * cos2 - tangent[1] * sin2);
|
||||
const y2 = point[1] + size * (tangent[0] * sin2 + tangent[1] * cos2);
|
||||
|
||||
const pathData = `M ${x1} ${y1} L ${point[0]} ${point[1]} L ${x2} ${y2}`;
|
||||
arrowPath.setAttribute('d', pathData);
|
||||
arrowPath.setAttribute('stroke', color);
|
||||
arrowPath.setAttribute('stroke-width', (2 * zoom).toString());
|
||||
arrowPath.setAttribute('fill', 'none');
|
||||
arrowPath.setAttribute('stroke-linecap', 'round');
|
||||
arrowPath.setAttribute('stroke-linejoin', 'round');
|
||||
|
||||
svg.appendChild(arrowPath);
|
||||
}
|
||||
|
||||
function renderTriangleEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
point: PointLocation,
|
||||
tangent: [number, number],
|
||||
size: number,
|
||||
color: string,
|
||||
zoom: number
|
||||
) {
|
||||
const triangle = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'polygon'
|
||||
);
|
||||
|
||||
const angle = Math.PI / 3; // 60 degrees
|
||||
const cos1 = Math.cos(angle);
|
||||
const sin1 = Math.sin(angle);
|
||||
const cos2 = Math.cos(-angle);
|
||||
const sin2 = Math.sin(-angle);
|
||||
|
||||
const x1 = point[0] + size * (tangent[0] * cos1 - tangent[1] * sin1);
|
||||
const y1 = point[1] + size * (tangent[0] * sin1 + tangent[1] * cos1);
|
||||
const x2 = point[0] + size * (tangent[0] * cos2 - tangent[1] * sin2);
|
||||
const y2 = point[1] + size * (tangent[0] * sin2 + tangent[1] * cos2);
|
||||
|
||||
const points = `${point[0]},${point[1]} ${x1},${y1} ${x2},${y2}`;
|
||||
triangle.setAttribute('points', points);
|
||||
triangle.setAttribute('fill', color);
|
||||
triangle.setAttribute('stroke', color);
|
||||
triangle.setAttribute('stroke-width', (1 * zoom).toString());
|
||||
|
||||
svg.appendChild(triangle);
|
||||
}
|
||||
|
||||
function renderCircleEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
point: PointLocation,
|
||||
tangent: [number, number],
|
||||
size: number,
|
||||
color: string,
|
||||
zoom: number
|
||||
) {
|
||||
const circle = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'circle'
|
||||
);
|
||||
|
||||
const radius = size * 0.5;
|
||||
const centerX = point[0] + radius * tangent[0];
|
||||
const centerY = point[1] + radius * tangent[1];
|
||||
|
||||
circle.setAttribute('cx', centerX.toString());
|
||||
circle.setAttribute('cy', centerY.toString());
|
||||
circle.setAttribute('r', radius.toString());
|
||||
circle.setAttribute('fill', color);
|
||||
circle.setAttribute('stroke', color);
|
||||
circle.setAttribute('stroke-width', (1 * zoom).toString());
|
||||
|
||||
svg.appendChild(circle);
|
||||
}
|
||||
|
||||
function renderDiamondEndpoint(
|
||||
svg: SVGSVGElement,
|
||||
point: PointLocation,
|
||||
tangent: [number, number],
|
||||
size: number,
|
||||
color: string,
|
||||
zoom: number
|
||||
) {
|
||||
const diamond = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'polygon'
|
||||
);
|
||||
|
||||
// Calculate diamond points
|
||||
const perpX = -tangent[1]; // Perpendicular to tangent
|
||||
const perpY = tangent[0];
|
||||
|
||||
const halfSize = size * 0.5;
|
||||
const x1 = point[0] + halfSize * tangent[0]; // Front point
|
||||
const y1 = point[1] + halfSize * tangent[1];
|
||||
const x2 = point[0] + halfSize * perpX; // Right point
|
||||
const y2 = point[1] + halfSize * perpY;
|
||||
const x3 = point[0] - halfSize * tangent[0]; // Back point
|
||||
const y3 = point[1] - halfSize * tangent[1];
|
||||
const x4 = point[0] - halfSize * perpX; // Left point
|
||||
const y4 = point[1] - halfSize * perpY;
|
||||
|
||||
const points = `${x1},${y1} ${x2},${y2} ${x3},${y3} ${x4},${y4}`;
|
||||
diamond.setAttribute('points', points);
|
||||
diamond.setAttribute('fill', color);
|
||||
diamond.setAttribute('stroke', color);
|
||||
diamond.setAttribute('stroke-width', (1 * zoom).toString());
|
||||
|
||||
svg.appendChild(diamond);
|
||||
}
|
||||
|
||||
function renderConnectorLabel(
|
||||
elementModel: ConnectorElementModel,
|
||||
domElement: HTMLElement,
|
||||
renderer: DomRenderer
|
||||
) {
|
||||
if (!elementModel.text || !elementModel.labelXYWH) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelElement = document.createElement('div');
|
||||
const [lx, ly, lw, lh] = elementModel.labelXYWH;
|
||||
const { x, y } = elementModel;
|
||||
|
||||
// Position label relative to the connector
|
||||
const relativeX = (lx - x) * renderer.viewport.zoom;
|
||||
const relativeY = (ly - y) * renderer.viewport.zoom;
|
||||
|
||||
labelElement.style.position = 'absolute';
|
||||
labelElement.style.left = `${relativeX}px`;
|
||||
labelElement.style.top = `${relativeY}px`;
|
||||
labelElement.style.width = `${lw * renderer.viewport.zoom}px`;
|
||||
labelElement.style.height = `${lh * renderer.viewport.zoom}px`;
|
||||
labelElement.style.pointerEvents = 'auto';
|
||||
labelElement.style.display = 'flex';
|
||||
labelElement.style.alignItems = 'center';
|
||||
labelElement.style.justifyContent = 'center';
|
||||
labelElement.style.backgroundColor = 'white';
|
||||
labelElement.style.border = '1px solid #e0e0e0';
|
||||
labelElement.style.borderRadius = '4px';
|
||||
labelElement.style.padding = '2px 4px';
|
||||
labelElement.style.fontSize = `${(elementModel.labelStyle?.fontSize || 16) * renderer.viewport.zoom}px`;
|
||||
labelElement.style.fontFamily =
|
||||
elementModel.labelStyle?.fontFamily || 'Inter';
|
||||
labelElement.style.color = renderer.getColorValue(
|
||||
elementModel.labelStyle?.color || DefaultTheme.black,
|
||||
DefaultTheme.black,
|
||||
true
|
||||
);
|
||||
labelElement.style.textAlign = elementModel.labelStyle?.textAlign || 'center';
|
||||
labelElement.style.overflow = 'hidden';
|
||||
labelElement.style.whiteSpace = 'nowrap';
|
||||
labelElement.style.textOverflow = 'ellipsis';
|
||||
|
||||
// Set label text content
|
||||
labelElement.textContent = elementModel.text.toString();
|
||||
|
||||
domElement.appendChild(labelElement);
|
||||
}
|
||||
|
||||
// Export the extension
|
||||
import { DomElementRendererExtension } from '../../extensions/dom-element-renderer.js';
|
||||
|
||||
export const ConnectorDomRendererExtension = DomElementRendererExtension(
|
||||
'connector',
|
||||
connectorDomRenderer
|
||||
);
|
||||
@@ -29,3 +29,9 @@ export const DomElementRendererIdentifier = (type: string) =>
|
||||
export type DomElementRenderer<
|
||||
T extends SurfaceElementModel = SurfaceElementModel,
|
||||
> = (elementModel: T, domElement: HTMLElement, renderer: DomRenderer) => void;
|
||||
|
||||
// Export the connector DOM renderer
|
||||
export {
|
||||
connectorDomRenderer,
|
||||
ConnectorDomRendererExtension,
|
||||
} from './connector.js';
|
||||
|
||||
@@ -81,53 +81,6 @@ function getOpacity(elementModel: SurfaceElementModel) {
|
||||
return { opacity: `${elementModel.opacity ?? 1}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* @class DomRenderer
|
||||
* Renders surface elements directly to the DOM using HTML elements and CSS.
|
||||
*
|
||||
* This renderer supports an extension mechanism to handle different types of surface elements.
|
||||
* To add rendering support for a new element type (e.g., 'my-custom-element'), follow these steps:
|
||||
*
|
||||
* 1. **Define the Renderer Function**:
|
||||
* Create a function that implements the rendering logic for your element.
|
||||
* This function will receive the element's model, the target HTMLElement, and the DomRenderer instance.
|
||||
* Signature: `(model: MyCustomElementModel, domElement: HTMLElement, renderer: DomRenderer) => void;`
|
||||
* Example: `shapeDomRenderer` in `blocksuite/affine/gfx/shape/src/element-renderer/shape-dom/index.ts`.
|
||||
* In this function, you'll apply styles and attributes to the `domElement` based on the `model`.
|
||||
*
|
||||
* 2. **Create the Renderer Extension**:
|
||||
* Create a new file (e.g., `my-custom-element-dom-renderer.extension.ts`).
|
||||
* Import `DomElementRendererExtension` (e.g., from `@blocksuite/affine-block-surface` or its source location
|
||||
* `blocksuite/affine/blocks/surface/src/extensions/dom-element-renderer.ts`).
|
||||
* Import your renderer function (from step 1).
|
||||
* Use the factory to create your extension:
|
||||
* `export const MyCustomElementDomRendererExtension = DomElementRendererExtension('my-custom-element', myCustomElementRendererFn);`
|
||||
* Example: `ShapeDomRendererExtension` in `blocksuite/affine/gfx/shape/src/element-renderer/shape-dom.ts`.
|
||||
*
|
||||
* 3. **Register the Extension**:
|
||||
* In your application setup where BlockSuite services and view extensions are registered (e.g., a `ViewExtensionProvider`
|
||||
* or a central DI configuration place), import your new extension (from step 2) and register it with the
|
||||
* dependency injection container.
|
||||
* Example: `context.register(MyCustomElementDomRendererExtension);`
|
||||
* As seen with `ShapeDomRendererExtension` being registered in `blocksuite/affine/gfx/shape/src/view.ts`.
|
||||
*
|
||||
* 4. **Core Infrastructure (Provided by DomRenderer System)**:
|
||||
* - `DomElementRenderer` (type): The function signature for renderers, defined in
|
||||
* `blocksuite/affine/blocks/surface/src/renderer/dom-elements/index.ts`.
|
||||
* - `DomElementRendererIdentifier` (function): Creates unique service identifiers for DI,
|
||||
* used by `DomRenderer` to look up specific renderers. Defined in the same file.
|
||||
* - `DomElementRendererExtension` (factory): A helper to create extension objects for easy registration.
|
||||
* (e.g., from `@blocksuite/affine-block-surface` or its source).
|
||||
* - `DomRenderer._renderElement()`: This method automatically looks up the registered renderer using
|
||||
* `DomElementRendererIdentifier(elementType)` and calls it if found.
|
||||
*
|
||||
* 5. **Ensure Exports**:
|
||||
* - The `DomRenderer` class itself should be accessible (e.g., exported from `@blocksuite/affine/blocks/surface`).
|
||||
* - The `DomElementRendererExtension` factory should be accessible.
|
||||
*
|
||||
* By following these steps, `DomRenderer` will automatically pick up and use your custom rendering logic
|
||||
* when it encounters elements of 'my-custom-element' type.
|
||||
*/
|
||||
export class DomRenderer {
|
||||
private _container!: HTMLElement;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { effects } from './effects';
|
||||
import {
|
||||
ConnectorDomRendererExtension,
|
||||
EdgelessCRUDExtension,
|
||||
EdgelessLegacySlotExtension,
|
||||
EditPropsMiddlewareBuilder,
|
||||
@@ -26,6 +27,7 @@ export class SurfaceViewExtension extends ViewExtensionProvider {
|
||||
super.setup(context);
|
||||
context.register([
|
||||
FlavourExtension('affine:surface'),
|
||||
ConnectorDomRendererExtension,
|
||||
EdgelessCRUDExtension,
|
||||
EdgelessLegacySlotExtension,
|
||||
ExportManagerExtension,
|
||||
|
||||
@@ -745,7 +745,7 @@ export class TableCell extends SignalWatcher(
|
||||
padding: '8px 12px',
|
||||
})}
|
||||
.yText="${this.text}"
|
||||
.inlineEventSource="${this.topContenteditableElement}"
|
||||
.inlineEventSource="${this.topContenteditableElement ?? nothing}"
|
||||
.attributesSchema="${this.inlineManager?.getSchema()}"
|
||||
.attributeRenderer="${this.inlineManager?.getRenderer()}"
|
||||
.embedChecker="${this.inlineManager?.embedChecker}"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@blocksuite/affine-fragment-adapter-panel",
|
||||
"description": "Adapter panel fragment for BlockSuite.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.12",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./view": "./src/view.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.21.0"
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { Store, TransformerMiddleware } from '@blocksuite/affine/store';
|
||||
import {
|
||||
type HtmlAdapter,
|
||||
HtmlAdapterFactoryIdentifier,
|
||||
type MarkdownAdapter,
|
||||
MarkdownAdapterFactoryIdentifier,
|
||||
type PlainTextAdapter,
|
||||
PlainTextAdapterFactoryIdentifier,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { provide } from '@lit/context';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, LitElement, type PropertyValues, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import {
|
||||
type AdapterPanelContext,
|
||||
adapterPanelContext,
|
||||
ADAPTERS,
|
||||
} from './config';
|
||||
|
||||
export const AFFINE_ADAPTER_PANEL = 'affine-adapter-panel';
|
||||
|
||||
export class AdapterPanel extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.adapters-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-sizing: border-box;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
}
|
||||
`;
|
||||
|
||||
get activeAdapter() {
|
||||
return this._context.activeAdapter$.value;
|
||||
}
|
||||
|
||||
private _createJob() {
|
||||
return this.store.getTransformer(this.transformerMiddlewares);
|
||||
}
|
||||
|
||||
private _getDocSnapshot() {
|
||||
const job = this._createJob();
|
||||
const result = job.docToSnapshot(this.store);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _getHtmlContent() {
|
||||
try {
|
||||
const job = this._createJob();
|
||||
const htmlAdapterFactory = this.store.get(HtmlAdapterFactoryIdentifier);
|
||||
const htmlAdapter = htmlAdapterFactory.get(job) as HtmlAdapter;
|
||||
const result = await htmlAdapter.fromDoc(this.store);
|
||||
return result?.file;
|
||||
} catch (error) {
|
||||
console.error('Failed to get html content', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async _getMarkdownContent() {
|
||||
try {
|
||||
const job = this._createJob();
|
||||
const markdownAdapterFactory = this.store.get(
|
||||
MarkdownAdapterFactoryIdentifier
|
||||
);
|
||||
const markdownAdapter = markdownAdapterFactory.get(
|
||||
job
|
||||
) as MarkdownAdapter;
|
||||
const result = await markdownAdapter.fromDoc(this.store);
|
||||
return result?.file;
|
||||
} catch (error) {
|
||||
console.error('Failed to get markdown content', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async _getPlainTextContent() {
|
||||
try {
|
||||
const job = this._createJob();
|
||||
const plainTextAdapterFactory = this.store.get(
|
||||
PlainTextAdapterFactoryIdentifier
|
||||
);
|
||||
const plainTextAdapter = plainTextAdapterFactory.get(
|
||||
job
|
||||
) as PlainTextAdapter;
|
||||
const result = await plainTextAdapter.fromDoc(this.store);
|
||||
return result?.file;
|
||||
} catch (error) {
|
||||
console.error('Failed to get plain text content', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _updateActiveContent = async () => {
|
||||
const activeId = this.activeAdapter.id;
|
||||
switch (activeId) {
|
||||
case 'markdown':
|
||||
this._context.markdownContent$.value =
|
||||
(await this._getMarkdownContent()) || '';
|
||||
break;
|
||||
case 'html':
|
||||
this._context.htmlContent$.value = (await this._getHtmlContent()) || '';
|
||||
break;
|
||||
case 'plaintext':
|
||||
this._context.plainTextContent$.value =
|
||||
(await this._getPlainTextContent()) || '';
|
||||
break;
|
||||
case 'snapshot':
|
||||
this._context.docSnapshot$.value = this._getDocSnapshot() || null;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._context = {
|
||||
activeAdapter$: signal(ADAPTERS[0]),
|
||||
isHtmlPreview$: signal(false),
|
||||
docSnapshot$: signal(null),
|
||||
htmlContent$: signal(''),
|
||||
markdownContent$: signal(''),
|
||||
plainTextContent$: signal(''),
|
||||
};
|
||||
}
|
||||
|
||||
override willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has('store')) {
|
||||
this._updateActiveContent().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
if (this.activeAdapter) {
|
||||
this._updateActiveContent().catch(console.error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="adapters-container">
|
||||
<affine-adapter-panel-header
|
||||
.updateActiveContent=${this._updateActiveContent}
|
||||
></affine-adapter-panel-header>
|
||||
<affine-adapter-panel-body></affine-adapter-panel-body>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor store!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor transformerMiddlewares: TransformerMiddleware[] = [];
|
||||
|
||||
@provide({ context: adapterPanelContext })
|
||||
private accessor _context!: AdapterPanelContext;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_ADAPTER_PANEL]: AdapterPanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import { scrollbarStyle } from '@blocksuite/affine-shared/styles';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import {
|
||||
type AdapterItem,
|
||||
type AdapterPanelContext,
|
||||
adapterPanelContext,
|
||||
ADAPTERS,
|
||||
} from '../config';
|
||||
|
||||
export const AFFINE_ADAPTER_PANEL_BODY = 'affine-adapter-panel-body';
|
||||
|
||||
export class AdapterPanelBody extends SignalWatcher(LitElement) {
|
||||
static override styles = css`
|
||||
.adapter-panel-body {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
${scrollbarStyle('.adapter-panel-body')}
|
||||
|
||||
.adapter-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: pre-wrap;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-size: var(--affine-font-sm);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.html-content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.html-preview-container,
|
||||
.html-panel-content {
|
||||
width: 100%;
|
||||
flex: 1 0 0;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: var(--affine-text-primary-color);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
${scrollbarStyle('.html-panel-content')}
|
||||
|
||||
.html-panel-footer {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.html-toggle-container {
|
||||
display: flex;
|
||||
background: ${unsafeCSSVarV2('segment/background')};
|
||||
justify-content: flex-start;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.html-toggle-item {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 0px 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
border-radius: 4px;
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
}
|
||||
|
||||
.html-toggle-item:hover {
|
||||
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
}
|
||||
|
||||
.html-toggle-item[active] {
|
||||
background: ${unsafeCSSVarV2('segment/button')};
|
||||
box-shadow:
|
||||
var(--Shadow-buttonShadow-1-x, 0px) var(--Shadow-buttonShadow-1-y, 0px)
|
||||
var(--Shadow-buttonShadow-1-blur, 1px) 0px
|
||||
var(--Shadow-buttonShadow-1-color, rgba(0, 0, 0, 0.12)),
|
||||
var(--Shadow-buttonShadow-2-x, 0px) var(--Shadow-buttonShadow-2-y, 1px)
|
||||
var(--Shadow-buttonShadow-2-blur, 5px) 0px
|
||||
var(--Shadow-buttonShadow-2-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.adapter-container {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.adapter-container.active {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
get activeAdapter() {
|
||||
return this._context.activeAdapter$.value;
|
||||
}
|
||||
|
||||
get isHtmlPreview() {
|
||||
return this._context.isHtmlPreview$.value;
|
||||
}
|
||||
|
||||
get htmlContent() {
|
||||
return this._context.htmlContent$.value;
|
||||
}
|
||||
|
||||
get markdownContent() {
|
||||
return this._context.markdownContent$.value;
|
||||
}
|
||||
|
||||
get plainTextContent() {
|
||||
return this._context.plainTextContent$.value;
|
||||
}
|
||||
|
||||
get docSnapshot() {
|
||||
return this._context.docSnapshot$.value;
|
||||
}
|
||||
|
||||
private _renderHtmlPanel() {
|
||||
return html`
|
||||
${this.isHtmlPreview
|
||||
? html`<iframe
|
||||
class="html-preview-container"
|
||||
.srcdoc=${this.htmlContent}
|
||||
sandbox="allow-same-origin"
|
||||
></iframe>`
|
||||
: html`<div class="html-panel-content">${this.htmlContent}</div>`}
|
||||
<div class="html-panel-footer">
|
||||
<div class="html-toggle-container">
|
||||
<span
|
||||
class="html-toggle-item"
|
||||
?active=${!this.isHtmlPreview}
|
||||
@click=${() => (this._context.isHtmlPreview$.value = false)}
|
||||
>Source</span
|
||||
>
|
||||
<span
|
||||
class="html-toggle-item"
|
||||
?active=${this.isHtmlPreview}
|
||||
@click=${() => (this._context.isHtmlPreview$.value = true)}
|
||||
>Preview</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly _renderAdapterContent = (adapter: AdapterItem) => {
|
||||
switch (adapter.id) {
|
||||
case 'html':
|
||||
return this._renderHtmlPanel();
|
||||
case 'markdown':
|
||||
return this.markdownContent;
|
||||
case 'plaintext':
|
||||
return this.plainTextContent;
|
||||
case 'snapshot':
|
||||
return this.docSnapshot
|
||||
? JSON.stringify(this.docSnapshot, null, 4)
|
||||
: '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _renderAdapterContainer = (adapter: AdapterItem) => {
|
||||
const containerClasses = classMap({
|
||||
'adapter-container': true,
|
||||
active: this.activeAdapter.id === adapter.id,
|
||||
});
|
||||
|
||||
const contentClasses = classMap({
|
||||
'adapter-content': true,
|
||||
[`${adapter.id}-content`]: true,
|
||||
});
|
||||
|
||||
const content = this._renderAdapterContent(adapter);
|
||||
|
||||
return html`
|
||||
<div class=${containerClasses}>
|
||||
<div class=${contentClasses}>${content}</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="adapter-panel-body">
|
||||
${ADAPTERS.map(adapter => this._renderAdapterContainer(adapter))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@consume({ context: adapterPanelContext })
|
||||
private accessor _context!: AdapterPanelContext;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_ADAPTER_PANEL_BODY]: AdapterPanelBody;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { DocSnapshot } from '@blocksuite/store';
|
||||
import { createContext } from '@lit/context';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
|
||||
export type AdapterItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const ADAPTERS: AdapterItem[] = [
|
||||
{ id: 'markdown', label: 'Markdown' },
|
||||
{ id: 'plaintext', label: 'PlainText' },
|
||||
{ id: 'html', label: 'HTML' },
|
||||
{ id: 'snapshot', label: 'Snapshot' },
|
||||
];
|
||||
|
||||
export type AdapterPanelContext = {
|
||||
activeAdapter$: Signal<AdapterItem>;
|
||||
isHtmlPreview$: Signal<boolean>;
|
||||
docSnapshot$: Signal<DocSnapshot | null>;
|
||||
htmlContent$: Signal<string>;
|
||||
markdownContent$: Signal<string>;
|
||||
plainTextContent$: Signal<string>;
|
||||
};
|
||||
|
||||
export const adapterPanelContext = createContext<AdapterPanelContext>(
|
||||
'adapterPanelContext'
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AdapterPanel, AFFINE_ADAPTER_PANEL } from './adapter-panel';
|
||||
import {
|
||||
AdapterPanelBody,
|
||||
AFFINE_ADAPTER_PANEL_BODY,
|
||||
} from './body/adapter-panel-body';
|
||||
import { AdapterMenu, AFFINE_ADAPTER_MENU } from './header/adapter-menu';
|
||||
import {
|
||||
AdapterPanelHeader,
|
||||
AFFINE_ADAPTER_PANEL_HEADER,
|
||||
} from './header/adapter-panel-header';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(AFFINE_ADAPTER_PANEL, AdapterPanel);
|
||||
customElements.define(AFFINE_ADAPTER_MENU, AdapterMenu);
|
||||
customElements.define(AFFINE_ADAPTER_PANEL_HEADER, AdapterPanelHeader);
|
||||
customElements.define(AFFINE_ADAPTER_PANEL_BODY, AdapterPanelBody);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import {
|
||||
type AdapterItem,
|
||||
type AdapterPanelContext,
|
||||
adapterPanelContext,
|
||||
ADAPTERS,
|
||||
} from '../config';
|
||||
|
||||
export const AFFINE_ADAPTER_MENU = 'affine-adapter-menu';
|
||||
|
||||
export class AdapterMenu extends SignalWatcher(LitElement) {
|
||||
static override styles = css`
|
||||
.adapter-menu {
|
||||
min-width: 120px;
|
||||
padding: 4px;
|
||||
background: var(--affine-background-primary-color);
|
||||
border: 1px solid var(--affine-border-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
}
|
||||
.adapter-menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.adapter-menu-item:hover {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
.adapter-menu-item.active {
|
||||
color: var(--affine-primary-color);
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
get activeAdapter() {
|
||||
return this._context.activeAdapter$.value;
|
||||
}
|
||||
|
||||
private readonly _handleAdapterChange = async (adapter: AdapterItem) => {
|
||||
this._context.activeAdapter$.value = adapter;
|
||||
this.abortController?.abort();
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`<div class="adapter-menu">
|
||||
${ADAPTERS.map(adapter => {
|
||||
const classes = classMap({
|
||||
'adapter-menu-item': true,
|
||||
active: this.activeAdapter.id === adapter.id,
|
||||
});
|
||||
return html`
|
||||
<button
|
||||
class=${classes}
|
||||
@click=${() => this._handleAdapterChange(adapter)}
|
||||
>
|
||||
${adapter.label}
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor abortController: AbortController | null = null;
|
||||
|
||||
@consume({ context: adapterPanelContext })
|
||||
private accessor _context!: AdapterPanelContext;
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_ADAPTER_MENU]: AdapterMenu;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||
import { ArrowDownSmallIcon, FlipDirectionIcon } from '@blocksuite/icons/lit';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
|
||||
import { type AdapterPanelContext, adapterPanelContext } from '../config';
|
||||
|
||||
export const AFFINE_ADAPTER_PANEL_HEADER = 'affine-adapter-panel-header';
|
||||
|
||||
export class AdapterPanelHeader extends SignalWatcher(LitElement) {
|
||||
static override styles = css`
|
||||
.adapter-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--affine-background-primary-color);
|
||||
}
|
||||
.adapter-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.adapter-selector:hover {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
.adapter-selector-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-size: var(--affine-font-xs);
|
||||
}
|
||||
.update-button {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--affine-icon-color);
|
||||
}
|
||||
.update-button:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
get activeAdapter() {
|
||||
return this._context.activeAdapter$.value;
|
||||
}
|
||||
|
||||
private _adapterMenuAbortController: AbortController | null = null;
|
||||
private readonly _toggleAdapterMenu = () => {
|
||||
if (this._adapterMenuAbortController) {
|
||||
this._adapterMenuAbortController.abort();
|
||||
}
|
||||
this._adapterMenuAbortController = new AbortController();
|
||||
|
||||
createLitPortal({
|
||||
template: html`<affine-adapter-menu
|
||||
.abortController=${this._adapterMenuAbortController}
|
||||
></affine-adapter-menu>`,
|
||||
portalStyles: {
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
},
|
||||
container: this._adapterPanelHeader,
|
||||
computePosition: {
|
||||
referenceElement: this._adapterSelector,
|
||||
placement: 'bottom-start',
|
||||
middleware: [flip(), offset(4)],
|
||||
autoUpdate: { animationFrame: true },
|
||||
},
|
||||
abortController: this._adapterMenuAbortController,
|
||||
closeOnClickAway: true,
|
||||
});
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="adapter-panel-header">
|
||||
<div class="adapter-selector" @click="${this._toggleAdapterMenu}">
|
||||
<span class="adapter-selector-label">
|
||||
${this.activeAdapter.label}
|
||||
</span>
|
||||
${ArrowDownSmallIcon({ width: '16px', height: '16px' })}
|
||||
</div>
|
||||
<div class="update-button" @click="${this.updateActiveContent}">
|
||||
${FlipDirectionIcon({ width: '16px', height: '16px' })}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.adapter-panel-header')
|
||||
private accessor _adapterPanelHeader!: HTMLDivElement;
|
||||
|
||||
@query('.adapter-selector')
|
||||
private accessor _adapterSelector!: HTMLDivElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor updateActiveContent: () => void = () => {};
|
||||
|
||||
@consume({ context: adapterPanelContext })
|
||||
private accessor _context!: AdapterPanelContext;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_ADAPTER_PANEL_HEADER]: AdapterPanelHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './adapter-panel.js';
|
||||
export * from './body/adapter-panel-body.js';
|
||||
export * from './header/adapter-menu.js';
|
||||
export * from './header/adapter-panel-header.js';
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ViewExtensionProvider } from '@blocksuite/affine-ext-loader';
|
||||
|
||||
import { effects } from './effects';
|
||||
|
||||
export class AdapterPanelViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-adapter-panel-fragment';
|
||||
|
||||
override effect() {
|
||||
super.effect();
|
||||
effects();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" },
|
||||
{ "path": "../../../framework/store" }
|
||||
]
|
||||
}
|
||||
@@ -223,13 +223,15 @@ export class ConnectorTool extends BaseTool<ConnectorToolOptions> {
|
||||
}
|
||||
|
||||
getNextMode() {
|
||||
switch (this.activatedOption.mode) {
|
||||
case ConnectorMode.Curve:
|
||||
return ConnectorMode.Orthogonal;
|
||||
case ConnectorMode.Orthogonal:
|
||||
return ConnectorMode.Straight;
|
||||
case ConnectorMode.Straight:
|
||||
return ConnectorMode.Curve;
|
||||
}
|
||||
// reorder the enum values
|
||||
const modes = [
|
||||
ConnectorMode.Curve,
|
||||
ConnectorMode.Orthogonal,
|
||||
ConnectorMode.Straight,
|
||||
];
|
||||
|
||||
const currentIndex = modes.indexOf(this.activatedOption.mode);
|
||||
const nextIndex = (currentIndex + 1) % modes.length;
|
||||
return modes[nextIndex];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,21 +235,18 @@ export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
|
||||
const locked = this.gfx.viewport.locked;
|
||||
const selection = this.gfx.selection;
|
||||
if (locked || selection.editing) return;
|
||||
if (
|
||||
this.gfx.tool.dragging$.peek() &&
|
||||
this.gfx.tool.currentToolName$.peek() === 'shape'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeIndex = shapes.findIndex(
|
||||
s => s.name === this.draggingShape
|
||||
);
|
||||
const nextIndex = (activeIndex + 1) % shapes.length;
|
||||
const next = shapes[nextIndex];
|
||||
this.draggingShape = next.name;
|
||||
const currentTool = this.gfx.tool.currentToolName$.peek();
|
||||
|
||||
if (this.readyToDrop) {
|
||||
if (currentTool === ShapeTool.toolName) {
|
||||
const activeIndex = shapes.findIndex(
|
||||
s => s.name === this.draggingShape
|
||||
);
|
||||
const nextIndex = (activeIndex + 1) % shapes.length;
|
||||
const next = shapes[nextIndex];
|
||||
this.draggingShape = next.name;
|
||||
}
|
||||
this.draggableController.cancelWithoutAnimation();
|
||||
const el = this.shapeContainer.querySelector(
|
||||
`.shape.${this.draggingShape}`
|
||||
@@ -264,8 +261,14 @@ export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
|
||||
const clientPos = { x: x + left, y: y + top };
|
||||
this.draggableController.dragAndMoveTo(el, clientPos);
|
||||
} else {
|
||||
if (this.gfx.tool.dragging$.peek()) return;
|
||||
let shapeName =
|
||||
this.gfx.tool.get(ShapeTool).activatedOption.shapeName;
|
||||
if (currentTool === ShapeTool.toolName) {
|
||||
shapeName = this.gfx.tool.get(ShapeTool).cycleShapeName('next');
|
||||
}
|
||||
this.setEdgelessTool(ShapeTool, {
|
||||
shapeName: this.draggingShape,
|
||||
shapeName,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,18 +9,35 @@ function applyShapeSpecificStyles(
|
||||
element: HTMLElement,
|
||||
zoom: number
|
||||
) {
|
||||
if (model.shapeType === 'rect') {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
} else if (model.shapeType === 'ellipse') {
|
||||
element.style.borderRadius = '50%';
|
||||
} else {
|
||||
element.style.borderRadius = '';
|
||||
// Reset properties that might be set by different shape types
|
||||
element.style.removeProperty('clip-path');
|
||||
element.style.removeProperty('border-radius');
|
||||
// Clear DOM for shapes that don't use SVG, or if type changes from SVG-based to non-SVG-based
|
||||
if (model.shapeType !== 'diamond' && model.shapeType !== 'triangle') {
|
||||
while (element.firstChild) element.firstChild.remove();
|
||||
}
|
||||
|
||||
switch (model.shapeType) {
|
||||
case 'rect': {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
break;
|
||||
}
|
||||
case 'ellipse':
|
||||
element.style.borderRadius = '50%';
|
||||
break;
|
||||
case 'diamond':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)';
|
||||
break;
|
||||
case 'triangle':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 100%, 0% 100%)';
|
||||
break;
|
||||
}
|
||||
// No 'else' needed to clear styles, as they are reset at the beginning of the function.
|
||||
}
|
||||
|
||||
function applyBorderStyles(
|
||||
@@ -78,6 +95,9 @@ export const shapeDomRenderer = (
|
||||
renderer: DomRenderer
|
||||
): void => {
|
||||
const { zoom } = renderer.viewport;
|
||||
const unscaledWidth = model.w;
|
||||
const unscaledHeight = model.h;
|
||||
|
||||
const fillColor = renderer.getColorValue(
|
||||
model.fillColor,
|
||||
DefaultTheme.shapeFillColor,
|
||||
@@ -89,17 +109,80 @@ export const shapeDomRenderer = (
|
||||
true
|
||||
);
|
||||
|
||||
element.style.width = `${model.w * zoom}px`;
|
||||
element.style.height = `${model.h * zoom}px`;
|
||||
element.style.width = `${unscaledWidth * zoom}px`;
|
||||
element.style.height = `${unscaledHeight * zoom}px`;
|
||||
element.style.boxSizing = 'border-box';
|
||||
|
||||
// Apply shape-specific clipping, border-radius, and potentially clear innerHTML
|
||||
applyShapeSpecificStyles(model, element, zoom);
|
||||
|
||||
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
|
||||
if (model.shapeType === 'diamond' || model.shapeType === 'triangle') {
|
||||
// For diamond and triangle, fill and border are handled by inline SVG
|
||||
element.style.border = 'none'; // Ensure no standard CSS border interferes
|
||||
element.style.backgroundColor = 'transparent'; // Host element is transparent
|
||||
|
||||
const strokeW = model.strokeWidth;
|
||||
const halfStroke = strokeW / 2; // Calculate half stroke width for point adjustment
|
||||
|
||||
let svgPoints = '';
|
||||
if (model.shapeType === 'diamond') {
|
||||
// Adjusted points for diamond
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight / 2}`,
|
||||
`${unscaledWidth / 2},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight / 2}`,
|
||||
].join(' ');
|
||||
} else {
|
||||
// triangle
|
||||
// Adjusted points for triangle
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight - halfStroke}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
// Determine if stroke should be visible and its color
|
||||
const finalStrokeColor =
|
||||
model.strokeStyle !== 'none' && strokeW > 0 ? strokeColor : 'transparent';
|
||||
// Determine dash array, only if stroke is visible and style is 'dash'
|
||||
const finalStrokeDasharray =
|
||||
model.strokeStyle === 'dash' && finalStrokeColor !== 'transparent'
|
||||
? '12, 12'
|
||||
: 'none';
|
||||
// Determine fill color
|
||||
const finalFillColor = model.filled ? fillColor : 'transparent';
|
||||
|
||||
// Build SVG safely with DOM-API
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
|
||||
const polygon = document.createElementNS(SVG_NS, 'polygon');
|
||||
polygon.setAttribute('points', svgPoints);
|
||||
polygon.setAttribute('fill', finalFillColor);
|
||||
polygon.setAttribute('stroke', finalStrokeColor);
|
||||
polygon.setAttribute('stroke-width', String(strokeW));
|
||||
if (finalStrokeDasharray !== 'none') {
|
||||
polygon.setAttribute('stroke-dasharray', finalStrokeDasharray);
|
||||
}
|
||||
svg.append(polygon);
|
||||
|
||||
// Replace existing children to avoid memory leaks
|
||||
element.replaceChildren(svg);
|
||||
} else {
|
||||
// Standard rendering for other shapes (e.g., rect, ellipse)
|
||||
// innerHTML was already cleared by applyShapeSpecificStyles if necessary
|
||||
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
|
||||
applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border
|
||||
}
|
||||
|
||||
applyBorderStyles(model, element, strokeColor, zoom);
|
||||
applyTransformStyles(model, element);
|
||||
|
||||
element.style.boxSizing = 'border-box';
|
||||
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
|
||||
|
||||
manageClassNames(model, element);
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
type SurfaceBlockComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { ShapeElementModel, ShapeName } from '@blocksuite/affine-model';
|
||||
import { DefaultTheme, getShapeType } from '@blocksuite/affine-model';
|
||||
import {
|
||||
DefaultTheme,
|
||||
getShapeType,
|
||||
ShapeType,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EditPropsStore,
|
||||
TelemetryProvider,
|
||||
@@ -15,7 +19,7 @@ import { hasClassNameInList } from '@blocksuite/affine-shared/utils';
|
||||
import type { IBound } from '@blocksuite/global/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import type { PointerEventState } from '@blocksuite/std';
|
||||
import { BaseTool } from '@blocksuite/std/gfx';
|
||||
import { BaseTool, type GfxController } from '@blocksuite/std/gfx';
|
||||
import { effect } from '@preact/signals-core';
|
||||
|
||||
import {
|
||||
@@ -159,6 +163,13 @@ export class ShapeTool extends BaseTool<ShapeToolOption> {
|
||||
this._surfaceComponent?.refresh();
|
||||
}
|
||||
|
||||
constructor(gfx: GfxController) {
|
||||
super(gfx);
|
||||
this.activatedOption = {
|
||||
shapeName: ShapeType.Rect,
|
||||
};
|
||||
}
|
||||
|
||||
override activate() {
|
||||
this.createOverlay();
|
||||
}
|
||||
@@ -337,4 +348,20 @@ export class ShapeTool extends BaseTool<ShapeToolOption> {
|
||||
setDisableOverlay(disable: boolean) {
|
||||
this._disableOverlay = disable;
|
||||
}
|
||||
|
||||
cycleShapeName(dir: 'prev' | 'next' = 'next'): ShapeName {
|
||||
const shapeNames: ShapeName[] = [
|
||||
ShapeType.Rect,
|
||||
ShapeType.Ellipse,
|
||||
ShapeType.Diamond,
|
||||
ShapeType.Triangle,
|
||||
'roundedRect',
|
||||
];
|
||||
|
||||
const currentIndex = shapeNames.indexOf(this.activatedOption.shapeName);
|
||||
const nextIndex =
|
||||
(currentIndex + (dir === 'prev' ? -1 : 1) + shapeNames.length) %
|
||||
shapeNames.length;
|
||||
return shapeNames[nextIndex];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ export enum ShapeTextFontSize {
|
||||
}
|
||||
|
||||
export enum ShapeType {
|
||||
Diamond = 'diamond',
|
||||
Ellipse = 'ellipse',
|
||||
Rect = 'rect',
|
||||
Ellipse = 'ellipse',
|
||||
Diamond = 'diamond',
|
||||
Triangle = 'triangle',
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mdast": "^4.0.4",
|
||||
@@ -71,7 +72,6 @@
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/bytes": "^3.1.5",
|
||||
"vitest": "3.1.3"
|
||||
},
|
||||
"version": "0.21.0"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"@blocksuite/sync": "workspace:*",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@types/lodash.ismatch": "^4.4.9",
|
||||
"file-type": "^20.0.0",
|
||||
"file-type": "^21.0.0",
|
||||
"lib0": "^0.2.97",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.ismatch": "^4.4.0",
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
fill: '#ff0000',
|
||||
stroke: '#000000',
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps as any);
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector(
|
||||
@@ -73,7 +73,7 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
subType: 'ellipse',
|
||||
xywh: '[200, 200, 50, 50]',
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps as any);
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
@@ -91,4 +91,48 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
);
|
||||
expect(shapeElement).toBeNull();
|
||||
});
|
||||
|
||||
test('should correctly render diamond shape', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
const shapeProps = {
|
||||
type: 'shape',
|
||||
subType: 'diamond',
|
||||
xywh: '[150, 150, 80, 60]',
|
||||
fillColor: '#ff0000',
|
||||
strokeColor: '#000000',
|
||||
filled: true,
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
await wait(100);
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
|
||||
`[data-element-id="${shapeId}"]`
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
});
|
||||
|
||||
test('should correctly render triangle shape', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
const shapeProps = {
|
||||
type: 'shape',
|
||||
subType: 'triangle',
|
||||
xywh: '[150, 150, 80, 60]',
|
||||
fillColor: '#ff0000',
|
||||
strokeColor: '#000000',
|
||||
filled: true,
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
await wait(100);
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
|
||||
`[data-element-id="${shapeId}"]`
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
||||
import '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js';
|
||||
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import {
|
||||
defaultImageProxyMiddleware,
|
||||
docLinkBaseURLMiddlewareBuilder,
|
||||
embedSyncedDocMiddleware,
|
||||
type HtmlAdapter,
|
||||
HtmlAdapterFactoryIdentifier,
|
||||
type MarkdownAdapter,
|
||||
MarkdownAdapterFactoryIdentifier,
|
||||
type PlainTextAdapter,
|
||||
PlainTextAdapterFactoryIdentifier,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine/shared/adapters';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import type { DocSnapshot } from '@blocksuite/affine/store';
|
||||
import type { TestAffineEditorContainer } from '@blocksuite/integration-test';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import type SlTabPanel from '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js';
|
||||
import { css, html, type PropertyValues } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('adapters-panel')
|
||||
export class AdaptersPanel extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
adapters-panel {
|
||||
width: 36vw;
|
||||
}
|
||||
.adapters-container {
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.adapter-container {
|
||||
padding: 0px 16px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 80px);
|
||||
white-space: pre-wrap;
|
||||
color: var(--affine-text-primary-color);
|
||||
overflow: auto;
|
||||
}
|
||||
.update-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
font-family: var(--affine-font-family);
|
||||
color: var(--affine-text-primary-color);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
}
|
||||
.update-button:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
.html-panel {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.html-preview-container,
|
||||
.html-panel-content {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: var(--affine-text-primary-color);
|
||||
overflow: auto;
|
||||
}
|
||||
.html-panel-footer {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
font-family: var(--affine-font-family);
|
||||
color: var(--affine-text-primary-color);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
line-height: 20px;
|
||||
}
|
||||
span[active] {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
get doc() {
|
||||
return this.editor.doc;
|
||||
}
|
||||
|
||||
private _createJob() {
|
||||
return this.doc.getTransformer([
|
||||
docLinkBaseURLMiddlewareBuilder(
|
||||
'https://example.com',
|
||||
this.doc.workspace.id
|
||||
).get(),
|
||||
titleMiddleware(this.doc.workspace.meta.docMetas),
|
||||
embedSyncedDocMiddleware('content'),
|
||||
defaultImageProxyMiddleware,
|
||||
]);
|
||||
}
|
||||
|
||||
private _getDocSnapshot() {
|
||||
const job = this._createJob();
|
||||
const result = job.docToSnapshot(this.doc);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _getHtmlContent() {
|
||||
const job = this._createJob();
|
||||
const htmlAdapterFactory = this.editor.std.provider.get(
|
||||
HtmlAdapterFactoryIdentifier
|
||||
);
|
||||
const htmlAdapter = htmlAdapterFactory.get(job) as HtmlAdapter;
|
||||
const result = await htmlAdapter.fromDoc(this.doc);
|
||||
return result?.file;
|
||||
}
|
||||
|
||||
private async _getMarkdownContent() {
|
||||
const job = this._createJob();
|
||||
const markdownAdapterFactory = this.editor.std.provider.get(
|
||||
MarkdownAdapterFactoryIdentifier
|
||||
);
|
||||
const markdownAdapter = markdownAdapterFactory.get(job) as MarkdownAdapter;
|
||||
const result = await markdownAdapter.fromDoc(this.doc);
|
||||
return result?.file;
|
||||
}
|
||||
|
||||
private async _getPlainTextContent() {
|
||||
const job = this._createJob();
|
||||
const plainTextAdapterFactory = this.editor.std.provider.get(
|
||||
PlainTextAdapterFactoryIdentifier
|
||||
);
|
||||
const plainTextAdapter = plainTextAdapterFactory.get(
|
||||
job
|
||||
) as PlainTextAdapter;
|
||||
const result = await plainTextAdapter.fromDoc(this.doc);
|
||||
return result?.file;
|
||||
}
|
||||
|
||||
private async _handleTabShow(name: string) {
|
||||
switch (name) {
|
||||
case 'markdown':
|
||||
this._markdownContent = (await this._getMarkdownContent()) || '';
|
||||
break;
|
||||
case 'html':
|
||||
this._htmlContent = (await this._getHtmlContent()) || '';
|
||||
break;
|
||||
case 'plaintext':
|
||||
this._plainTextContent = (await this._getPlainTextContent()) || '';
|
||||
break;
|
||||
case 'snapshot':
|
||||
this._docSnapshot = this._getDocSnapshot() || null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderHtmlPanel() {
|
||||
return html`
|
||||
${this._isHtmlPreview
|
||||
? html`<iframe
|
||||
class="html-preview-container"
|
||||
.srcdoc=${this._htmlContent}
|
||||
></iframe>`
|
||||
: html`<div class="html-panel-content">${this._htmlContent}</div>`}
|
||||
<div class="html-panel-footer">
|
||||
<span
|
||||
class="html-panel-footer-item"
|
||||
?active=${!this._isHtmlPreview}
|
||||
@click=${() => (this._isHtmlPreview = false)}
|
||||
>Source</span
|
||||
>
|
||||
<span
|
||||
class="html-panel-footer-item"
|
||||
?active=${this._isHtmlPreview}
|
||||
@click=${() => (this._isHtmlPreview = true)}
|
||||
>Preview</span
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _updateActiveTabContent() {
|
||||
if (!this._activeTab) return;
|
||||
const activeTabName = this._activeTab.name;
|
||||
await this._handleTabShow(activeTabName);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const doc = this.doc;
|
||||
if (doc) {
|
||||
this._updateActiveTabContent().catch(console.error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const snapshotString = this._docSnapshot
|
||||
? JSON.stringify(this._docSnapshot, null, 4)
|
||||
: '';
|
||||
return html`
|
||||
<div class="adapters-container">
|
||||
<sl-tab-group
|
||||
activation="auto"
|
||||
@sl-tab-show=${(e: CustomEvent) => this._handleTabShow(e.detail.name)}
|
||||
>
|
||||
<sl-tab slot="nav" panel="markdown">Markdown</sl-tab>
|
||||
<sl-tab slot="nav" panel="plaintext">PlainText</sl-tab>
|
||||
<sl-tab slot="nav" panel="html">HTML</sl-tab>
|
||||
<sl-tab slot="nav" panel="snapshot">Snapshot</sl-tab>
|
||||
|
||||
<sl-tab-panel name="markdown">
|
||||
<div class="adapter-container">${this._markdownContent}</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="html">
|
||||
<div class="adapter-container html-panel">
|
||||
${this._renderHtmlPanel()}
|
||||
</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="plaintext">
|
||||
<div class="adapter-container">${this._plainTextContent}</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="snapshot">
|
||||
<div class="adapter-container">${snapshotString}</div>
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
<sl-tooltip content="Update Adapter Content" placement="left" hoist>
|
||||
<div class="update-button" @click="${this._updateActiveTabContent}">
|
||||
Update
|
||||
</div>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
override willUpdate(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('editor')) {
|
||||
requestIdleCallback(() => {
|
||||
this._updateActiveTabContent().catch(console.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@query('sl-tab-panel[active]')
|
||||
private accessor _activeTab!: SlTabPanel;
|
||||
|
||||
@state()
|
||||
private accessor _docSnapshot: DocSnapshot | null = null;
|
||||
|
||||
@state()
|
||||
private accessor _htmlContent = '';
|
||||
|
||||
@state()
|
||||
private accessor _isHtmlPreview = false;
|
||||
|
||||
@state()
|
||||
private accessor _markdownContent = '';
|
||||
|
||||
@state()
|
||||
private accessor _plainTextContent = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: TestAffineEditorContainer;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'adapters-panel': AdaptersPanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import type { TransformerMiddleware } from '@blocksuite/affine/store';
|
||||
import type { TestAffineEditorContainer } from '@blocksuite/integration-test';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('custom-adapter-panel')
|
||||
export class CustomAdapterPanel extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.custom-adapter-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 16px;
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
height: 100vh;
|
||||
width: 30vw;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
private _renderPanel() {
|
||||
return html`<affine-adapter-panel
|
||||
.store=${this.editor.doc}
|
||||
.transformerMiddlewares=${this.transformerMiddlewares}
|
||||
></affine-adapter-panel>`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${this._show
|
||||
? html`
|
||||
<div class="custom-adapter-container">${this._renderPanel()}</div>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
toggleDisplay() {
|
||||
this._show = !this._show;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _show = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: TestAffineEditorContainer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor transformerMiddlewares: TransformerMiddleware[] = [];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'custom-adapter-panel': CustomAdapterPanel;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ import type { Pane } from 'tweakpane';
|
||||
import type { CommentPanel } from '../../comment/index.js';
|
||||
import { createTestEditor } from '../../starter/utils/extensions.js';
|
||||
import { mockEdgelessTheme } from '../mock-services.js';
|
||||
import { AdaptersPanel } from './adapters-panel.js';
|
||||
import type { CustomAdapterPanel } from './custom-adapter-panel.js';
|
||||
import type { CustomFramePanel } from './custom-frame-panel.js';
|
||||
import type { CustomOutlinePanel } from './custom-outline-panel.js';
|
||||
import type { CustomOutlineViewer } from './custom-outline-viewer.js';
|
||||
@@ -612,26 +612,6 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
this._hasOffset = !this._hasOffset;
|
||||
}
|
||||
|
||||
private _toggleAdaptersPanel() {
|
||||
const app = document.querySelector('#app');
|
||||
if (!app) return;
|
||||
|
||||
const currentAdaptersPanel = app.querySelector('adapters-panel');
|
||||
if (currentAdaptersPanel) {
|
||||
currentAdaptersPanel.remove();
|
||||
(app as HTMLElement).style.display = 'block';
|
||||
this.editor.style.width = '100%';
|
||||
this.editor.style.flex = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const adaptersPanel = new AdaptersPanel();
|
||||
adaptersPanel.editor = this.editor;
|
||||
app.append(adaptersPanel);
|
||||
this.editor.style.flex = '1';
|
||||
(app as HTMLElement).style.display = 'flex';
|
||||
}
|
||||
|
||||
private _toggleCommentPanel() {
|
||||
document.body.append(this.commentPanel);
|
||||
}
|
||||
@@ -649,6 +629,10 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
this.framePanel.toggleDisplay();
|
||||
}
|
||||
|
||||
private _toggleAdapterPanel() {
|
||||
this.adapterPanel.toggleDisplay();
|
||||
}
|
||||
|
||||
private _toggleMultipleEditors() {
|
||||
const app = document.querySelector('#app');
|
||||
if (app) {
|
||||
@@ -926,8 +910,8 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
<sl-menu-item @click="${this._toggleMultipleEditors}">
|
||||
Toggle Multiple Editors
|
||||
</sl-menu-item>
|
||||
<sl-menu-item @click="${this._toggleAdaptersPanel}">
|
||||
Toggle Adapters Panel
|
||||
<sl-menu-item @click="${this._toggleAdapterPanel}">
|
||||
Toggle Adapter Panel
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
@@ -1032,6 +1016,9 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
@property({ attribute: false })
|
||||
accessor framePanel!: CustomFramePanel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor adapterPanel!: CustomAdapterPanel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor leftSidePanel!: LeftSidePanel;
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { Store, Workspace } from '@blocksuite/affine/store';
|
||||
import {
|
||||
defaultImageProxyMiddleware,
|
||||
docLinkBaseURLMiddlewareBuilder,
|
||||
embedSyncedDocMiddleware,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
import { AttachmentViewerPanel } from '../../_common/components/attachment-viewer-panel';
|
||||
import { CustomAdapterPanel } from '../../_common/components/custom-adapter-panel';
|
||||
import { CustomFramePanel } from '../../_common/components/custom-frame-panel';
|
||||
import { CustomOutlinePanel } from '../../_common/components/custom-outline-panel';
|
||||
import { CustomOutlineViewer } from '../../_common/components/custom-outline-viewer';
|
||||
@@ -24,6 +31,7 @@ export async function createTestApp(doc: Store, collection: Workspace) {
|
||||
const docsPanel = new DocsPanel();
|
||||
const framePanel = new CustomFramePanel();
|
||||
const outlinePanel = new CustomOutlinePanel();
|
||||
const adapterPanel = new CustomAdapterPanel();
|
||||
const outlineViewer = new CustomOutlineViewer();
|
||||
const leftSidePanel = new LeftSidePanel();
|
||||
const commentPanel = new CommentPanel();
|
||||
@@ -36,6 +44,16 @@ export async function createTestApp(doc: Store, collection: Workspace) {
|
||||
outlineViewer.toggleOutlinePanel = () => {
|
||||
outlinePanel.toggleDisplay();
|
||||
};
|
||||
adapterPanel.editor = editor;
|
||||
adapterPanel.transformerMiddlewares = [
|
||||
docLinkBaseURLMiddlewareBuilder(
|
||||
'https://example.com',
|
||||
editor.doc.workspace.id
|
||||
).get(),
|
||||
titleMiddleware(editor.doc.workspace.meta.docMetas),
|
||||
embedSyncedDocMiddleware('content'),
|
||||
defaultImageProxyMiddleware,
|
||||
];
|
||||
|
||||
debugMenu.collection = collection;
|
||||
debugMenu.editor = editor;
|
||||
@@ -44,6 +62,7 @@ export async function createTestApp(doc: Store, collection: Workspace) {
|
||||
debugMenu.framePanel = framePanel;
|
||||
debugMenu.leftSidePanel = leftSidePanel;
|
||||
debugMenu.docsPanel = docsPanel;
|
||||
debugMenu.adapterPanel = adapterPanel;
|
||||
|
||||
debugMenu.commentPanel = commentPanel;
|
||||
|
||||
@@ -55,6 +74,7 @@ export async function createTestApp(doc: Store, collection: Workspace) {
|
||||
document.body.append(framePanel);
|
||||
document.body.append(leftSidePanel);
|
||||
document.body.append(debugMenu);
|
||||
document.body.append(adapterPanel);
|
||||
|
||||
window.editor = editor;
|
||||
window.doc = doc;
|
||||
|
||||
@@ -22,7 +22,7 @@ cp ./.docker/dev/.env.example ./.docker/dev/.env
|
||||
docker compose -f ./.docker/dev/compose.yml up
|
||||
```
|
||||
|
||||
#### Notify
|
||||
### Notify
|
||||
|
||||
> Starting from AFFiNE 0.20, compose.yml includes a breaking change: the default database image has switched from `postgres:16` to `pgvector/pgvector:pg16`. If you were previously using another major version of Postgres, please change the number after `pgvector/pgvector:pg` to the major version you are using.
|
||||
|
||||
@@ -108,6 +108,6 @@ yarn affine server prisma studio
|
||||
|
||||
### Seed the db
|
||||
|
||||
```
|
||||
```sh
|
||||
yarn affine server seed -h
|
||||
```
|
||||
|
||||
+36
-3
@@ -4,19 +4,52 @@ The actual snapshot is saved in `copilot-context.spec.ts.snap`.
|
||||
|
||||
Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should get null for non-exist job
|
||||
|
||||
> should return null for non-exist job
|
||||
|
||||
null
|
||||
|
||||
## should insert embedding by doc id
|
||||
|
||||
> should match file embedding
|
||||
|
||||
[
|
||||
{
|
||||
chunk: 0,
|
||||
content: 'content',
|
||||
distance: 0,
|
||||
fileId: 'file-id',
|
||||
},
|
||||
]
|
||||
|
||||
> should return empty array when embedding is deleted
|
||||
|
||||
[]
|
||||
|
||||
> should match workspace embedding
|
||||
|
||||
[
|
||||
{
|
||||
docId: 'doc1',
|
||||
},
|
||||
]
|
||||
|
||||
> should return empty array when doc is ignored
|
||||
|
||||
[]
|
||||
|
||||
> should return workspace embedding
|
||||
|
||||
[
|
||||
{
|
||||
docId: 'doc1',
|
||||
},
|
||||
]
|
||||
|
||||
> should return empty array when embedding deleted
|
||||
|
||||
[]
|
||||
|
||||
## should check embedding table
|
||||
|
||||
> should return true when embedding table is available
|
||||
|
||||
true
|
||||
|
||||
BIN
Binary file not shown.
@@ -6,9 +6,11 @@ import ava, { TestFn } from 'ava';
|
||||
import { Config } from '../../base';
|
||||
import { CopilotContextModel } from '../../models/copilot-context';
|
||||
import { CopilotSessionModel } from '../../models/copilot-session';
|
||||
import { CopilotWorkspaceConfigModel } from '../../models/copilot-workspace';
|
||||
import { UserModel } from '../../models/user';
|
||||
import { WorkspaceModel } from '../../models/workspace';
|
||||
import { createTestingModule, type TestingModule } from '../utils';
|
||||
import { cleanObject } from '../utils/copilot';
|
||||
|
||||
interface Context {
|
||||
config: Config;
|
||||
@@ -18,6 +20,7 @@ interface Context {
|
||||
workspace: WorkspaceModel;
|
||||
copilotSession: CopilotSessionModel;
|
||||
copilotContext: CopilotContextModel;
|
||||
copilotWorkspace: CopilotWorkspaceConfigModel;
|
||||
}
|
||||
|
||||
const test = ava as TestFn<Context>;
|
||||
@@ -28,6 +31,7 @@ test.before(async t => {
|
||||
t.context.workspace = module.get(WorkspaceModel);
|
||||
t.context.copilotSession = module.get(CopilotSessionModel);
|
||||
t.context.copilotContext = module.get(CopilotContextModel);
|
||||
t.context.copilotWorkspace = module.get(CopilotWorkspaceConfigModel);
|
||||
t.context.db = module.get(PrismaClient);
|
||||
t.context.config = module.get(Config);
|
||||
t.context.module = module;
|
||||
@@ -74,7 +78,7 @@ test('should create a copilot context', async t => {
|
||||
|
||||
test('should get null for non-exist job', async t => {
|
||||
const job = await t.context.copilotContext.get('non-exist');
|
||||
t.is(job, null);
|
||||
t.snapshot(job, 'should return null for non-exist job');
|
||||
});
|
||||
|
||||
test('should update context', async t => {
|
||||
@@ -111,7 +115,10 @@ test('should insert embedding by doc id', async t => {
|
||||
1,
|
||||
1
|
||||
);
|
||||
t.snapshot(ret, 'should match file embedding');
|
||||
t.snapshot(
|
||||
cleanObject(ret, ['chunk', 'content', 'distance']),
|
||||
'should match file embedding'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -122,7 +129,7 @@ test('should insert embedding by doc id', async t => {
|
||||
1,
|
||||
1
|
||||
);
|
||||
t.is(ret.length, 0);
|
||||
t.snapshot(ret, 'should return empty array when embedding is deleted');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +162,7 @@ test('should insert embedding by doc id', async t => {
|
||||
workspace.id,
|
||||
[docId]
|
||||
);
|
||||
t.true(ret.has(docId), 'should return true when embedding exists');
|
||||
t.true(ret.has(docId), 'should return doc id when embedding is inserted');
|
||||
}
|
||||
|
||||
{
|
||||
@@ -165,8 +172,39 @@ test('should insert embedding by doc id', async t => {
|
||||
1,
|
||||
1
|
||||
);
|
||||
t.is(ret.length, 1);
|
||||
t.is(ret[0].content, 'content');
|
||||
t.snapshot(
|
||||
cleanObject(ret, ['chunk', 'content', 'distance']),
|
||||
'should match workspace embedding'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
await t.context.copilotWorkspace.updateIgnoredDocs(workspace.id, [docId]);
|
||||
const ret = await t.context.copilotContext.matchWorkspaceEmbedding(
|
||||
Array.from({ length: 1024 }, () => 0.9),
|
||||
workspace.id,
|
||||
1,
|
||||
1
|
||||
);
|
||||
t.snapshot(ret, 'should return empty array when doc is ignored');
|
||||
}
|
||||
|
||||
{
|
||||
await t.context.copilotWorkspace.updateIgnoredDocs(
|
||||
workspace.id,
|
||||
undefined,
|
||||
[docId]
|
||||
);
|
||||
const ret = await t.context.copilotContext.matchWorkspaceEmbedding(
|
||||
Array.from({ length: 1024 }, () => 0.9),
|
||||
workspace.id,
|
||||
1,
|
||||
1
|
||||
);
|
||||
t.snapshot(
|
||||
cleanObject(ret, ['chunk', 'content', 'distance']),
|
||||
'should return workspace embedding'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -188,7 +226,7 @@ test('should insert embedding by doc id', async t => {
|
||||
test('should check embedding table', async t => {
|
||||
{
|
||||
const ret = await t.context.copilotContext.checkEmbeddingAvailable();
|
||||
t.true(ret, 'should return true when embedding table is available');
|
||||
t.snapshot(ret, 'should return true when embedding table is available');
|
||||
}
|
||||
|
||||
// {
|
||||
|
||||
@@ -201,6 +201,68 @@ test('should insert and search embedding', async t => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should check need to be embedded', async t => {
|
||||
const docId = randomUUID();
|
||||
|
||||
await t.context.doc.upsert({
|
||||
spaceId: workspace.id,
|
||||
docId,
|
||||
blob: Uint8Array.from([1, 2, 3]),
|
||||
timestamp: Date.now(),
|
||||
editorId: user.id,
|
||||
});
|
||||
|
||||
{
|
||||
let needsEmbedding = await t.context.copilotWorkspace.checkDocNeedEmbedded(
|
||||
workspace.id,
|
||||
docId
|
||||
);
|
||||
t.true(needsEmbedding, 'document with no embedding should need embedding');
|
||||
}
|
||||
|
||||
{
|
||||
await t.context.copilotContext.insertWorkspaceEmbedding(
|
||||
workspace.id,
|
||||
docId,
|
||||
[
|
||||
{
|
||||
index: 0,
|
||||
content: 'content',
|
||||
embedding: Array.from({ length: 1024 }, () => 1),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
let needsEmbedding = await t.context.copilotWorkspace.checkDocNeedEmbedded(
|
||||
workspace.id,
|
||||
docId
|
||||
);
|
||||
t.false(
|
||||
needsEmbedding,
|
||||
'document with recent embedding should not need embedding'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
await t.context.doc.upsert({
|
||||
spaceId: workspace.id,
|
||||
docId,
|
||||
blob: Uint8Array.from([4, 5, 6]),
|
||||
timestamp: Date.now() + 1000, // Ensure timestamp is later
|
||||
editorId: user.id,
|
||||
});
|
||||
|
||||
let needsEmbedding = await t.context.copilotWorkspace.checkDocNeedEmbedded(
|
||||
workspace.id,
|
||||
docId
|
||||
);
|
||||
t.true(
|
||||
needsEmbedding,
|
||||
'document updated after embedding should need embedding'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should check embedding table', async t => {
|
||||
{
|
||||
const ret = await t.context.copilotWorkspace.checkEmbeddingAvailable();
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
CallMetric,
|
||||
DocNotFound,
|
||||
DocUpdateBlocked,
|
||||
EventBus,
|
||||
GatewayErrorWrapper,
|
||||
metrics,
|
||||
NotInSpace,
|
||||
@@ -144,6 +145,7 @@ export class SpaceSyncGateway
|
||||
|
||||
constructor(
|
||||
private readonly ac: AccessController,
|
||||
private readonly event: EventBus,
|
||||
private readonly workspace: PgWorkspaceDocStorageAdapter,
|
||||
private readonly userspace: PgUserspaceDocStorageAdapter,
|
||||
private readonly docReader: DocReader,
|
||||
@@ -201,6 +203,7 @@ export class SpaceSyncGateway
|
||||
await client.join(room);
|
||||
}
|
||||
} else {
|
||||
this.event.emit('workspace.embedding', { workspaceId: spaceId });
|
||||
await this.selectAdapter(client, spaceType).join(user.id, spaceId);
|
||||
}
|
||||
|
||||
|
||||
@@ -175,6 +175,55 @@ export class CopilotWorkspaceConfigModel extends BaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async checkDocNeedEmbedded(workspaceId: string, docId: string) {
|
||||
// NOTE: check if the document needs re-embedding.
|
||||
// 1. check if there have been any recent updates to the document snapshot and update
|
||||
// 2. check if the embedding is older than the snapshot and update
|
||||
// 3. check if the embedding is older than 10 minutes (avoid frequent updates)
|
||||
// if all conditions are met, re-embedding is required.
|
||||
const result = await this.db.$queryRaw<{ needs_embedding: boolean }[]>`
|
||||
SELECT
|
||||
EXISTS (
|
||||
WITH docs AS (
|
||||
SELECT
|
||||
s.workspace_id,
|
||||
s.guid AS doc_id,
|
||||
s.updated_at
|
||||
FROM
|
||||
snapshots s
|
||||
WHERE
|
||||
s.workspace_id = ${workspaceId}
|
||||
AND s.guid = ${docId}
|
||||
UNION
|
||||
ALL
|
||||
SELECT
|
||||
u.workspace_id,
|
||||
u.guid AS doc_id,
|
||||
u.created_at AS updated_at
|
||||
FROM
|
||||
"updates" u
|
||||
WHERE
|
||||
u.workspace_id = ${workspaceId}
|
||||
AND u.guid = ${docId}
|
||||
)
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
docs
|
||||
LEFT JOIN ai_workspace_embeddings e
|
||||
ON e.workspace_id = docs.workspace_id
|
||||
AND e.doc_id = docs.doc_id
|
||||
WHERE
|
||||
e.updated_at IS NULL
|
||||
OR docs.updated_at > e.updated_at
|
||||
OR e.updated_at < NOW() - INTERVAL '10 minutes'
|
||||
) AS needs_embedding;
|
||||
`;
|
||||
|
||||
return result[0]?.needs_embedding ?? false;
|
||||
}
|
||||
|
||||
// ================ embeddings ================
|
||||
|
||||
async checkEmbeddingAvailable(): Promise<boolean> {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Models } from '../../../models';
|
||||
import { CopilotStorage } from '../storage';
|
||||
import { readStream } from '../utils';
|
||||
import { OpenAIEmbeddingClient } from './embedding';
|
||||
import type { Chunk, DocFragment } from './types';
|
||||
import { EMBEDDING_DIMENSIONS, EmbeddingClient } from './types';
|
||||
|
||||
@Injectable()
|
||||
@@ -78,16 +79,23 @@ export class CopilotContextDocJob {
|
||||
@OnEvent('workspace.doc.embedding')
|
||||
async addDocEmbeddingQueue(
|
||||
docs: Events['workspace.doc.embedding'],
|
||||
contextId?: string
|
||||
options?: { contextId: string; priority: number }
|
||||
) {
|
||||
if (!this.supportEmbedding) return;
|
||||
|
||||
for (const { workspaceId, docId } of docs) {
|
||||
await this.queue.add('copilot.embedding.docs', {
|
||||
contextId,
|
||||
workspaceId,
|
||||
docId,
|
||||
});
|
||||
await this.queue.add(
|
||||
'copilot.embedding.docs',
|
||||
{
|
||||
contextId: options?.contextId,
|
||||
workspaceId,
|
||||
docId,
|
||||
},
|
||||
{
|
||||
jobId: `workspace:embedding:${workspaceId}:${docId}`,
|
||||
priority: options?.priority ?? 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,14 +118,26 @@ export class CopilotContextDocJob {
|
||||
}: Events['workspace.embedding']) {
|
||||
if (!this.supportEmbedding || !this.embeddingClient) return;
|
||||
|
||||
if (enableDocEmbedding === undefined) {
|
||||
enableDocEmbedding =
|
||||
await this.models.workspace.allowEmbedding(workspaceId);
|
||||
}
|
||||
|
||||
if (enableDocEmbedding) {
|
||||
const toBeEmbedDocIds =
|
||||
await this.models.copilotWorkspace.findDocsToEmbed(workspaceId);
|
||||
for (const docId of toBeEmbedDocIds) {
|
||||
await this.queue.add('copilot.embedding.docs', {
|
||||
workspaceId,
|
||||
docId,
|
||||
});
|
||||
await this.queue.add(
|
||||
'copilot.embedding.docs',
|
||||
{
|
||||
workspaceId,
|
||||
docId,
|
||||
},
|
||||
{
|
||||
jobId: `workspace:embedding:${workspaceId}:${docId}`,
|
||||
priority: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const controller = this.workspaceJobAbortController.get(workspaceId);
|
||||
@@ -132,14 +152,25 @@ export class CopilotContextDocJob {
|
||||
async addDocEmbeddingQueueFromEvent(doc: Events['doc.indexer.updated']) {
|
||||
if (!this.supportEmbedding || !this.embeddingClient) return;
|
||||
|
||||
await this.queue.add('copilot.embedding.docs', {
|
||||
workspaceId: doc.workspaceId,
|
||||
docId: doc.workspaceId,
|
||||
});
|
||||
await this.queue.add(
|
||||
'copilot.embedding.docs',
|
||||
{
|
||||
workspaceId: doc.workspaceId,
|
||||
docId: doc.docId,
|
||||
},
|
||||
{
|
||||
jobId: `workspace:embedding:${doc.workspaceId}:${doc.docId}`,
|
||||
priority: 2,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@OnEvent('doc.indexer.deleted')
|
||||
async deleteDocEmbeddingQueueFromEvent(doc: Events['doc.indexer.deleted']) {
|
||||
await this.queue.remove(
|
||||
`workspace:embedding:${doc.workspaceId}:${doc.docId}`,
|
||||
'copilot.embedding.docs'
|
||||
);
|
||||
await this.models.copilotContext.deleteWorkspaceEmbedding(
|
||||
doc.workspaceId,
|
||||
doc.docId
|
||||
@@ -221,6 +252,43 @@ export class CopilotContextDocJob {
|
||||
}
|
||||
}
|
||||
|
||||
private async getDocFragment(
|
||||
workspaceId: string,
|
||||
docId: string
|
||||
): Promise<DocFragment | null> {
|
||||
const docContent = await this.doc.getFullDocContent(workspaceId, docId);
|
||||
const authors = await this.models.doc.getAuthors(workspaceId, docId);
|
||||
if (docContent?.summary && authors) {
|
||||
const { title = 'Untitled', summary } = docContent;
|
||||
const { createdAt, updatedAt, createdByUser, updatedByUser } = authors;
|
||||
return {
|
||||
title,
|
||||
summary,
|
||||
createdAt: createdAt.toDateString(),
|
||||
updatedAt: updatedAt.toDateString(),
|
||||
createdBy: createdByUser?.name,
|
||||
updatedBy: updatedByUser?.name,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private formatDocChunks(chunks: Chunk[], fragment: DocFragment): Chunk[] {
|
||||
return chunks.map(chunk => ({
|
||||
index: chunk.index,
|
||||
content: [
|
||||
`Title: ${fragment.title}`,
|
||||
`Created at: ${fragment.createdAt}`,
|
||||
`Updated at: ${fragment.updatedAt}`,
|
||||
fragment.createdBy ? `Created by: ${fragment.createdBy}` : undefined,
|
||||
fragment.updatedBy ? `Updated by: ${fragment.updatedBy}` : undefined,
|
||||
chunk.content,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
}));
|
||||
}
|
||||
|
||||
private getWorkspaceSignal(workspaceId: string) {
|
||||
let controller = this.workspaceJobAbortController.get(workspaceId);
|
||||
if (!controller) {
|
||||
@@ -241,39 +309,49 @@ export class CopilotContextDocJob {
|
||||
const signal = this.getWorkspaceSignal(workspaceId);
|
||||
|
||||
try {
|
||||
const content = await this.doc.getFullDocContent(workspaceId, docId);
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
} else if (content) {
|
||||
// fast fall for empty doc, journal is easily to create a empty doc
|
||||
if (content.summary) {
|
||||
const embeddings = await this.embeddingClient.getFileEmbeddings(
|
||||
new File([content.summary], `${content.title || 'Untitled'}.md`),
|
||||
signal
|
||||
);
|
||||
const needEmbedding =
|
||||
await this.models.copilotWorkspace.checkDocNeedEmbedded(
|
||||
workspaceId,
|
||||
docId
|
||||
);
|
||||
if (needEmbedding) {
|
||||
if (signal.aborted) return;
|
||||
const fragment = await this.getDocFragment(workspaceId, docId);
|
||||
if (fragment) {
|
||||
// fast fall for empty doc, journal is easily to create a empty doc
|
||||
if (fragment.summary) {
|
||||
const embeddings = await this.embeddingClient.getFileEmbeddings(
|
||||
new File(
|
||||
[fragment.summary],
|
||||
`${fragment.title || 'Untitled'}.md`
|
||||
),
|
||||
chunks => this.formatDocChunks(chunks, fragment),
|
||||
signal
|
||||
);
|
||||
|
||||
for (const chunks of embeddings) {
|
||||
for (const chunks of embeddings) {
|
||||
await this.models.copilotContext.insertWorkspaceEmbedding(
|
||||
workspaceId,
|
||||
docId,
|
||||
chunks
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// for empty doc, insert empty embedding
|
||||
const emptyEmbedding = {
|
||||
index: 0,
|
||||
content: '',
|
||||
embedding: Array.from({ length: EMBEDDING_DIMENSIONS }, () => 0),
|
||||
};
|
||||
await this.models.copilotContext.insertWorkspaceEmbedding(
|
||||
workspaceId,
|
||||
docId,
|
||||
chunks
|
||||
[emptyEmbedding]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// for empty doc, insert empty embedding
|
||||
const emptyEmbedding = {
|
||||
index: 0,
|
||||
content: '',
|
||||
embedding: Array.from({ length: EMBEDDING_DIMENSIONS }, () => 0),
|
||||
};
|
||||
await this.models.copilotContext.insertWorkspaceEmbedding(
|
||||
workspaceId,
|
||||
docId,
|
||||
[emptyEmbedding]
|
||||
);
|
||||
} else if (contextId) {
|
||||
throw new DocNotFound({ spaceId: workspaceId, docId });
|
||||
}
|
||||
} else if (contextId) {
|
||||
throw new DocNotFound({ spaceId: workspaceId, docId });
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (contextId) {
|
||||
|
||||
@@ -498,7 +498,7 @@ export class CopilotContextResolver {
|
||||
workspaceId: session.workspaceId,
|
||||
docId,
|
||||
})),
|
||||
session.id
|
||||
{ contextId: session.id, priority: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -559,7 +559,7 @@ export class CopilotContextResolver {
|
||||
|
||||
await this.jobs.addDocEmbeddingQueue(
|
||||
[{ workspaceId: session.workspaceId, docId: options.docId }],
|
||||
session.id
|
||||
{ contextId: session.id, priority: 0 }
|
||||
);
|
||||
|
||||
return { ...record, status: record.status || null };
|
||||
|
||||
@@ -3,6 +3,7 @@ import { File } from 'node:buffer';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CopilotContextFileNotSupported } from '../../../base';
|
||||
import type { PageDocContent } from '../../../core/utils/blocksuite';
|
||||
import { ChunkSimilarity, Embedding } from '../../../models';
|
||||
import { parseDoc } from '../../../native';
|
||||
|
||||
@@ -10,7 +11,7 @@ declare global {
|
||||
interface Events {
|
||||
'workspace.embedding': {
|
||||
workspaceId: string;
|
||||
enableDocEmbedding: boolean;
|
||||
enableDocEmbedding?: boolean;
|
||||
};
|
||||
|
||||
'workspace.doc.embedding': Array<{
|
||||
@@ -53,6 +54,13 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export type DocFragment = PageDocContent & {
|
||||
createdAt: string;
|
||||
createdBy?: string;
|
||||
updatedAt: string;
|
||||
updatedBy?: string;
|
||||
};
|
||||
|
||||
export type Chunk = {
|
||||
index: number;
|
||||
content: string;
|
||||
@@ -63,11 +71,12 @@ export const EMBEDDING_DIMENSIONS = 1024;
|
||||
export abstract class EmbeddingClient {
|
||||
async getFileEmbeddings(
|
||||
file: File,
|
||||
chunkMapper: (chunk: Chunk[]) => Chunk[],
|
||||
signal?: AbortSignal
|
||||
): Promise<Embedding[][]> {
|
||||
const chunks = await this.getFileChunks(file, signal);
|
||||
const chunkedEmbeddings = await Promise.all(
|
||||
chunks.map(chunk => this.generateEmbeddings(chunk))
|
||||
chunks.map(chunk => this.generateEmbeddings(chunkMapper(chunk)))
|
||||
);
|
||||
return chunkedEmbeddings;
|
||||
}
|
||||
|
||||
@@ -3,26 +3,24 @@ import { gqlFetcherFactory } from '@affine/graphql';
|
||||
|
||||
import { DummyConnection } from '../../connection';
|
||||
|
||||
const TIMEOUT = 15000;
|
||||
|
||||
export class HttpConnection extends DummyConnection {
|
||||
readonly fetch = async (input: string, init?: RequestInit) => {
|
||||
const externalSignal = init?.signal;
|
||||
if (externalSignal?.aborted) {
|
||||
throw externalSignal.reason;
|
||||
}
|
||||
const abortController = new AbortController();
|
||||
externalSignal?.addEventListener('abort', reason => {
|
||||
abortController.abort(reason);
|
||||
});
|
||||
|
||||
const timeout = 15000;
|
||||
const timeoutId = setTimeout(() => {
|
||||
abortController.abort(new Error('request timeout'));
|
||||
}, timeout);
|
||||
const signals = [AbortSignal.timeout(TIMEOUT)];
|
||||
if (externalSignal) signals.push(externalSignal);
|
||||
|
||||
const combinedSignal = AbortSignal.any(signals);
|
||||
|
||||
const res = await globalThis
|
||||
.fetch(new URL(input, this.serverBaseUrl), {
|
||||
...init,
|
||||
signal: abortController.signal,
|
||||
signal: combinedSignal,
|
||||
headers: {
|
||||
...this.requestHeaders,
|
||||
...init?.headers,
|
||||
@@ -30,16 +28,18 @@ export class HttpConnection extends DummyConnection {
|
||||
},
|
||||
})
|
||||
.catch(err => {
|
||||
const message =
|
||||
err.name === 'TimeoutError' ? 'request timeout' : err.message;
|
||||
|
||||
throw new UserFriendlyError({
|
||||
status: 504,
|
||||
code: 'NETWORK_ERROR',
|
||||
type: 'NETWORK_ERROR',
|
||||
name: 'NETWORK_ERROR',
|
||||
message: `Network error: ${err.message}`,
|
||||
message: `Network error: ${message}`,
|
||||
stacktrace: err.stack,
|
||||
});
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
if (!res.ok && res.status !== 404) {
|
||||
if (res.status === 413) {
|
||||
throw new UserFriendlyError({
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.54.1",
|
||||
"react-resizable-panels": "^3.0.0",
|
||||
"react-router": "^7.6.0",
|
||||
"react-router-dom": "^7.5.1",
|
||||
"sonner": "^2.0.0",
|
||||
"swr": "^2.2.5",
|
||||
"vaul": "^1.1.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Toaster } from '@affine/admin/components/ui/sonner';
|
||||
import { FACTORIES, lazy, RELATIVE_ROUTES } from '@affine/routes';
|
||||
import { lazy, ROUTES } from '@affine/routes';
|
||||
import { withSentryReactRouterV7Routing } from '@sentry/react';
|
||||
import { useEffect } from 'react';
|
||||
import {
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Route,
|
||||
Routes as ReactRouterRoutes,
|
||||
useLocation,
|
||||
} from 'react-router';
|
||||
} from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
@@ -18,24 +18,22 @@ import { isAdmin, useCurrentUser, useServerConfig } from './modules/common';
|
||||
import { Layout } from './modules/layout';
|
||||
|
||||
export const Setup = lazy(
|
||||
async () => await import(/* webpackChunkName: "setup" */ './modules/setup')
|
||||
() => import(/* webpackChunkName: "setup" */ './modules/setup')
|
||||
);
|
||||
export const Accounts = lazy(
|
||||
async () =>
|
||||
await import(/* webpackChunkName: "accounts" */ './modules/accounts')
|
||||
() => import(/* webpackChunkName: "accounts" */ './modules/accounts')
|
||||
);
|
||||
export const AI = lazy(
|
||||
async () => await import(/* webpackChunkName: "ai" */ './modules/ai')
|
||||
() => import(/* webpackChunkName: "ai" */ './modules/ai')
|
||||
);
|
||||
export const About = lazy(
|
||||
async () => await import(/* webpackChunkName: "about" */ './modules/about')
|
||||
() => import(/* webpackChunkName: "about" */ './modules/about')
|
||||
);
|
||||
export const Settings = lazy(
|
||||
async () =>
|
||||
await import(/* webpackChunkName: "settings" */ './modules/settings')
|
||||
() => import(/* webpackChunkName: "settings" */ './modules/settings')
|
||||
);
|
||||
export const Auth = lazy(
|
||||
async () => await import(/* webpackChunkName: "auth" */ './modules/auth')
|
||||
() => import(/* webpackChunkName: "auth" */ './modules/auth')
|
||||
);
|
||||
|
||||
const Routes = window.SENTRY_RELEASE
|
||||
@@ -52,7 +50,7 @@ function AuthenticatedRoutes() {
|
||||
}, [user]);
|
||||
|
||||
if (!user || !isAdmin(user)) {
|
||||
return <Navigate to={FACTORIES.admin.auth()} />;
|
||||
return <Navigate to="/admin/auth" />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -88,21 +86,19 @@ export const App = () => {
|
||||
>
|
||||
<BrowserRouter basename={environment.subPath}>
|
||||
<Routes>
|
||||
<Route path={RELATIVE_ROUTES.admin.index}>
|
||||
<Route index element={<RootRoutes />} />
|
||||
<Route path={RELATIVE_ROUTES.admin.auth} element={<Auth />} />
|
||||
<Route path={RELATIVE_ROUTES.admin.setup} element={<Setup />} />
|
||||
<Route path={ROUTES.admin.index} element={<RootRoutes />}>
|
||||
<Route path={ROUTES.admin.auth} element={<Auth />} />
|
||||
<Route path={ROUTES.admin.setup} element={<Setup />} />
|
||||
<Route element={<AuthenticatedRoutes />}>
|
||||
<Route path={ROUTES.admin.accounts} element={<Accounts />} />
|
||||
<Route path={ROUTES.admin.ai} element={<AI />} />
|
||||
<Route path={ROUTES.admin.about} element={<About />} />
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.admin.accounts}
|
||||
element={<Accounts />}
|
||||
/>
|
||||
<Route path={RELATIVE_ROUTES.admin.ai} element={<AI />} />
|
||||
<Route path={RELATIVE_ROUTES.admin.about} element={<About />} />
|
||||
<Route path={RELATIVE_ROUTES.admin.settings.index}>
|
||||
<Route index element={<Settings />} />
|
||||
path={ROUTES.admin.settings.index}
|
||||
element={<Settings />}
|
||||
>
|
||||
<Route
|
||||
path={RELATIVE_ROUTES.admin.settings.module}
|
||||
path={ROUTES.admin.settings.module}
|
||||
element={<Settings />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Label } from '@affine/admin/components/ui/label';
|
||||
import { FeatureType, getUserFeaturesQuery } from '@affine/graphql';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Navigate } from 'react-router';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { affineFetch } from '../../fetch-utils';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { NavLink } from 'react-router';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { buttonVariants } from '../../components/ui/button';
|
||||
import { cn } from '../../utils';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||
import { cn } from '@affine/admin/utils';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { NavLink } from 'react-router';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
interface NavItemProps {
|
||||
icon: React.ReactNode;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { buttonVariants } from '@affine/admin/components/ui/button';
|
||||
import { cn } from '@affine/admin/utils';
|
||||
import { AccountIcon, AiOutlineIcon, SelfhostIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { NavLink } from 'react-router';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { ServerVersion } from './server-version';
|
||||
import { SettingsItem } from './settings-item';
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SettingsIcon } from '@blocksuite/icons/rc';
|
||||
import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu';
|
||||
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { NavLink } from 'react-router';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { KNOWN_CONFIG_GROUPS, UNKNOWN_CONFIG_GROUPS } from '../settings/config';
|
||||
import { NormalSubItem } from './collapsible-item';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { validateEmailAndPassword } from '@affine/admin/utils';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { affineFetch } from '../../fetch-utils';
|
||||
@@ -149,7 +149,7 @@ export const Form = () => {
|
||||
}
|
||||
|
||||
if (current === count) {
|
||||
return await navigate('/', { replace: true });
|
||||
return navigate('/', { replace: true });
|
||||
}
|
||||
|
||||
api?.scrollNext();
|
||||
@@ -168,7 +168,7 @@ export const Form = () => {
|
||||
const onPrevious = useAsyncCallback(async () => {
|
||||
if (current === count) {
|
||||
if (serverConfig.initialized === true) {
|
||||
return await navigate('/admin', { replace: true });
|
||||
return navigate('/admin', { replace: true });
|
||||
}
|
||||
toast.error('Goto Admin Panel failed, please try again.');
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Navigate } from 'react-router';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { useServerConfig } from '../common';
|
||||
import { Form } from './form';
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.6.0"
|
||||
"react-router-dom": "^6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/cli": "^7.0.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getStoreManager } from '@affine/core/blocksuite/manager/store';
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||
import { router } from '@affine/core/mobile/router';
|
||||
@@ -45,7 +46,7 @@ import { OpClient } from '@toeverything/infra/op';
|
||||
import { AsyncCall } from 'async-call-rpc';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import { RouterProvider } from 'react-router/dom';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { AffineTheme } from './plugins/affine-theme';
|
||||
import { AIButton } from './plugins/ai-button';
|
||||
@@ -59,6 +60,10 @@ window.addEventListener('beforeunload', () => {
|
||||
storeManagerClient.dispose();
|
||||
});
|
||||
|
||||
const future = {
|
||||
v7_startTransition: true,
|
||||
} as const;
|
||||
|
||||
const framework = new Framework();
|
||||
configureCommonModules(framework);
|
||||
configureBrowserWorkbenchModule(framework);
|
||||
@@ -337,7 +342,11 @@ export function App() {
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<ThemeProvider />
|
||||
<RouterProvider router={router} />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</AffineContext>
|
||||
</I18nProvider>
|
||||
</FrameworkRoot>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.6.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"uuid": "^11.0.3",
|
||||
"webm-muxer": "^5.0.3"
|
||||
},
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { WindowsAppControls } from '@affine/core/components/pure/header/windows-app-controls';
|
||||
import { Router } from '@affine/core/desktop/router';
|
||||
import { AppContainer } from '@affine/core/desktop/components/app-container';
|
||||
import { router } from '@affine/core/desktop/router';
|
||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
import createEmotionCache from '@affine/core/utils/create-emotion-cache';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { FrameworkRoot, getCurrentStore } from '@toeverything/infra';
|
||||
import { Suspense } from 'react';
|
||||
import { BrowserRouter } from 'react-router';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { setupEffects } from './effects';
|
||||
import { DesktopThemeSync } from './theme-sync';
|
||||
@@ -33,6 +34,10 @@ if (
|
||||
|
||||
const cache = createEmotionCache();
|
||||
|
||||
const future = {
|
||||
v7_startTransition: true,
|
||||
} as const;
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<Suspense>
|
||||
@@ -41,9 +46,11 @@ export function App() {
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<DesktopThemeSync />
|
||||
<BrowserRouter basename={environment.subPath}>
|
||||
<Router />
|
||||
</BrowserRouter>
|
||||
<RouterProvider
|
||||
fallbackElement={<AppContainer fallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
{environment.isWindows && (
|
||||
<div style={{ position: 'fixed', right: 0, top: 0, zIndex: 5 }}>
|
||||
<WindowsAppControls />
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.6.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"yjs": "^13.6.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getStoreManager } from '@affine/core/blocksuite/manager/store';
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||
import { HapticProvider } from '@affine/core/mobile/modules/haptics';
|
||||
import { NavigationGestureProvider } from '@affine/core/mobile/modules/navigation-gesture';
|
||||
@@ -55,7 +56,7 @@ import { AsyncCall } from 'async-call-rpc';
|
||||
import { AppTrackingTransparency } from 'capacitor-plugin-app-tracking-transparency';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
import { RouterProvider } from 'react-router/dom';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import { BlocksuiteMenuConfigProvider } from './bs-menu-config';
|
||||
import { ModalConfigProvider } from './modal-config';
|
||||
@@ -71,6 +72,10 @@ window.addEventListener('beforeunload', () => {
|
||||
storeManagerClient.dispose();
|
||||
});
|
||||
|
||||
const future = {
|
||||
v7_startTransition: true,
|
||||
} as const;
|
||||
|
||||
const framework = new Framework();
|
||||
configureCommonModules(framework);
|
||||
configureBrowserWorkbenchModule(framework);
|
||||
@@ -406,7 +411,11 @@ export function App() {
|
||||
<KeyboardThemeProvider />
|
||||
<ModalConfigProvider>
|
||||
<BlocksuiteMenuConfigProvider>
|
||||
<RouterProvider router={router} />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</BlocksuiteMenuConfigProvider>
|
||||
</ModalConfigProvider>
|
||||
</AffineContext>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.6.0"
|
||||
"react-router-dom": "^6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.1",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { AppFallback } from '@affine/core/mobile/components/app-fallback';
|
||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||
import { HapticProvider } from '@affine/core/mobile/modules/haptics';
|
||||
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||
@@ -18,7 +19,7 @@ import { StoreManagerClient } from '@affine/nbstore/worker/client';
|
||||
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
|
||||
import { OpClient } from '@toeverything/infra/op';
|
||||
import { Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router/dom';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
let storeManagerClient: StoreManagerClient;
|
||||
|
||||
@@ -34,6 +35,10 @@ window.addEventListener('beforeunload', () => {
|
||||
storeManagerClient.dispose();
|
||||
});
|
||||
|
||||
const future = {
|
||||
v7_startTransition: true,
|
||||
} as const;
|
||||
|
||||
const framework = new Framework();
|
||||
configureCommonModules(framework);
|
||||
configureBrowserWorkbenchModule(framework);
|
||||
@@ -144,7 +149,11 @@ export function App() {
|
||||
<FrameworkRoot framework={frameworkProvider}>
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<RouterProvider router={router} />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</AffineContext>
|
||||
</I18nProvider>
|
||||
</FrameworkRoot>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@toeverything/infra": "workspace:*",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router": "^7.6.0"
|
||||
"react-router-dom": "^6.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { Router } from '@affine/core/desktop/router';
|
||||
import { AppContainer } from '@affine/core/desktop/components/app-container';
|
||||
import { router } from '@affine/core/desktop/router';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
import { I18nProvider } from '@affine/core/modules/i18n';
|
||||
import { LifecycleService } from '@affine/core/modules/lifecycle';
|
||||
@@ -17,7 +18,7 @@ import { CacheProvider } from '@emotion/react';
|
||||
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
|
||||
import { OpClient } from '@toeverything/infra/op';
|
||||
import { Suspense } from 'react';
|
||||
import { BrowserRouter } from 'react-router';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
const cache = createEmotionCache();
|
||||
|
||||
@@ -41,6 +42,10 @@ window.addEventListener('beforeunload', () => {
|
||||
storeManagerClient.dispose();
|
||||
});
|
||||
|
||||
const future = {
|
||||
v7_startTransition: true,
|
||||
} as const;
|
||||
|
||||
const framework = new Framework();
|
||||
configureCommonModules(framework);
|
||||
configureBrowserWorkbenchModule(framework);
|
||||
@@ -85,9 +90,11 @@ export function App() {
|
||||
<CacheProvider value={cache}>
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<BrowserRouter basename={environment.subPath}>
|
||||
<Router />
|
||||
</BrowserRouter>
|
||||
<RouterProvider
|
||||
fallbackElement={<AppContainer fallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</AffineContext>
|
||||
</I18nProvider>
|
||||
</CacheProvider>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-paginate": "^8.2.0",
|
||||
"react-router": "^7.6.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-transition-state": "^2.2.0",
|
||||
"sonner": "^2.0.0",
|
||||
"swr": "^2.2.5",
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { Location } from 'react-router';
|
||||
import { useLocation, useNavigate } from 'react-router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { Location } from 'react-router-dom';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { Button } from '../../ui/button';
|
||||
import { Checkbox } from '../../ui/checkbox';
|
||||
import { Divider } from '../../ui/divider';
|
||||
import Input from '../../ui/input';
|
||||
import { notify } from '../../ui/notification';
|
||||
import { ScrollableContainer } from '../../ui/scrollbar';
|
||||
import * as styles from './onboarding-page.css';
|
||||
import type { User } from './type';
|
||||
@@ -120,21 +118,6 @@ export const OnboardingPage = ({
|
||||
() => questions?.[questionIdx],
|
||||
[questionIdx, questions]
|
||||
);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
if (callbackUrl) {
|
||||
const result = navigate(callbackUrl);
|
||||
if (result instanceof Promise) {
|
||||
result.catch((err: Error) => {
|
||||
const error = UserFriendlyError.fromAny(err);
|
||||
console.error(error);
|
||||
notify.error(error);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onOpenAffine();
|
||||
}
|
||||
}, [callbackUrl, navigate, onOpenAffine]);
|
||||
const isMacosDesktop = BUILD_CONFIG.isElectron && environment.isMacOs;
|
||||
const isWindowsDesktop = BUILD_CONFIG.isElectron && environment.isWindows;
|
||||
|
||||
@@ -280,7 +263,13 @@ export const OnboardingPage = ({
|
||||
className={clsx(styles.button, styles.openAFFiNEButton)}
|
||||
variant="primary"
|
||||
size="extraLarge"
|
||||
onClick={onClick}
|
||||
onClick={() => {
|
||||
if (callbackUrl) {
|
||||
navigate(callbackUrl);
|
||||
} else {
|
||||
onOpenAffine();
|
||||
}
|
||||
}}
|
||||
suffix={<ArrowRightSmallIcon />}
|
||||
>
|
||||
Get Started
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DialogTrigger } from '@radix-ui/react-dialog';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ButtonProps } from '../button';
|
||||
import { Button } from '../button';
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"@affine/graphql": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/nbstore": "workspace:*",
|
||||
"@affine/routes": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/track": "workspace:*",
|
||||
"@blocksuite/affine": "workspace:*",
|
||||
@@ -50,7 +49,7 @@
|
||||
"core-js": "^3.39.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"file-type": "^20.0.0",
|
||||
"file-type": "^21.0.0",
|
||||
"filesize": "^10.1.6",
|
||||
"foxact": "^0.2.43",
|
||||
"fuse.js": "^7.0.0",
|
||||
@@ -75,7 +74,7 @@
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "^6.0.0",
|
||||
"react-router": "^7.6.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-transition-state": "^2.2.0",
|
||||
"react-virtuoso": "^4.12.3",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import clsx from 'clsx';
|
||||
import { Provider } from 'jotai/react';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useRouteError } from 'react-router';
|
||||
import { useRouteError } from 'react-router-dom';
|
||||
|
||||
import * as styles from './affine-error-fallback.css';
|
||||
import { ErrorDetail } from './error-basic/error-detail';
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
export interface DumpInfoProps {
|
||||
error: any;
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useNavigateHelper } from '../use-navigate-helper';
|
||||
|
||||
export function useBlockSuiteMetaHelper() {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
const { openPage } = useNavigateHelper();
|
||||
const docsService = useService(DocsService);
|
||||
const docRecordList = useService(DocsService).list;
|
||||
|
||||
@@ -45,9 +45,9 @@ export function useBlockSuiteMetaHelper() {
|
||||
async (pageId: string, openPageAfterDuplication: boolean = true) => {
|
||||
const newPageId = await docsService.duplicate(pageId);
|
||||
openPageAfterDuplication &&
|
||||
jumpToPage(workspace.docCollection.id, newPageId);
|
||||
openPage(workspace.docCollection.id, newPageId);
|
||||
},
|
||||
[docsService, jumpToPage, workspace.docCollection.id]
|
||||
[docsService, openPage, workspace.docCollection.id]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useSignOut = ({
|
||||
}: ConfirmModalProps = {}) => {
|
||||
const t = useI18n();
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const { jumpToAll } = useNavigateHelper();
|
||||
const { openPage } = useNavigateHelper();
|
||||
|
||||
const serverService = useService(ServerService);
|
||||
const authService = useService(AuthService);
|
||||
@@ -56,14 +56,14 @@ export const useSignOut = ({
|
||||
w => w.flavour !== serverService.server.id
|
||||
);
|
||||
if (localWorkspace) {
|
||||
jumpToAll(localWorkspace.id);
|
||||
openPage(localWorkspace.id, 'all');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
authService,
|
||||
currentWorkspaceFlavour,
|
||||
jumpToAll,
|
||||
onConfirm,
|
||||
openPage,
|
||||
serverService.server.id,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { notify } from '@affine/component';
|
||||
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
||||
import { toDocSearchParams } from '@affine/core/modules/navigation';
|
||||
import { getOpenUrlInDesktopAppLink } from '@affine/core/modules/open-in-app';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { FACTORIES } from '@affine/routes';
|
||||
import type { DocMode } from '@blocksuite/affine/model';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
import type { NavigateFunction, NavigateOptions, To } from 'react-router';
|
||||
import type { NavigateFunction, NavigateOptions } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* In workbench, we use nested react-router, so default `useNavigate` can't get correct navigate function in workbench.
|
||||
@@ -25,27 +22,11 @@ export enum RouteLogic {
|
||||
* Use this for over workbench navigate, for navigate in workbench, use `WorkbenchService`.
|
||||
*/
|
||||
export function useNavigateHelper() {
|
||||
const navigateFunction = useContext(NavigateContext);
|
||||
const navigate = useContext(NavigateContext);
|
||||
|
||||
const navigate = useCallback(
|
||||
(to: To, options?: NavigateOptions) => {
|
||||
if (!navigateFunction) {
|
||||
throw new Error(
|
||||
'useNavigateHelper must be used within a NavigateProvider'
|
||||
);
|
||||
}
|
||||
const result = navigateFunction(to, options);
|
||||
if (result instanceof Promise) {
|
||||
result.catch((err: Error) => {
|
||||
const error = UserFriendlyError.fromAny(err);
|
||||
console.error(error);
|
||||
notify.error(error);
|
||||
});
|
||||
}
|
||||
return;
|
||||
},
|
||||
[navigateFunction]
|
||||
);
|
||||
if (!navigate) {
|
||||
throw new Error('useNavigateHelper must be used within a NavigateProvider');
|
||||
}
|
||||
|
||||
const jumpToPage = useCallback(
|
||||
(
|
||||
@@ -53,7 +34,7 @@ export function useNavigateHelper() {
|
||||
pageId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(FACTORIES.workspace.doc({ workspaceId, docId: pageId }), {
|
||||
return navigate(`/workspace/${workspaceId}/${pageId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -75,18 +56,15 @@ export function useNavigateHelper() {
|
||||
refreshKey: nanoid(),
|
||||
});
|
||||
const query = search?.size ? `?${search.toString()}` : '';
|
||||
return navigate(
|
||||
FACTORIES.workspace.doc({ workspaceId, docId: pageId }) + query,
|
||||
{
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
}
|
||||
);
|
||||
return navigate(`/workspace/${workspaceId}/${pageId}${query}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToCollections = useCallback(
|
||||
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate(FACTORIES.workspace.collections({ workspaceId }), {
|
||||
return navigate(`/workspace/${workspaceId}/collection`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -94,7 +72,7 @@ export function useNavigateHelper() {
|
||||
);
|
||||
const jumpToTags = useCallback(
|
||||
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate(FACTORIES.workspace.tags({ workspaceId }), {
|
||||
return navigate(`/workspace/${workspaceId}/tag`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -106,7 +84,7 @@ export function useNavigateHelper() {
|
||||
tagId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(FACTORIES.workspace.tags.tag({ workspaceId, tagId }), {
|
||||
return navigate(`/workspace/${workspaceId}/tag/${tagId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -118,28 +96,20 @@ export function useNavigateHelper() {
|
||||
collectionId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(
|
||||
FACTORIES.workspace.collections.collection({
|
||||
workspaceId,
|
||||
collectionId,
|
||||
}),
|
||||
{
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
}
|
||||
);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const jumpToAll = useCallback(
|
||||
(workspaceId: string, logic?: RouteLogic) => {
|
||||
return navigate(FACTORIES.workspace.all({ workspaceId }), {
|
||||
return navigate(`/workspace/${workspaceId}/collection/${collectionId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const openPage = useCallback(
|
||||
(workspaceId: string, pageId: string, logic?: RouteLogic) => {
|
||||
return jumpToPage(workspaceId, pageId, logic);
|
||||
},
|
||||
[jumpToPage]
|
||||
);
|
||||
|
||||
const jumpToIndex = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH, opt?: { search?: string }) => {
|
||||
return navigate(
|
||||
@@ -154,7 +124,7 @@ export function useNavigateHelper() {
|
||||
|
||||
const jumpTo404 = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate(FACTORIES.notFound(), {
|
||||
return navigate('/404', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -162,7 +132,7 @@ export function useNavigateHelper() {
|
||||
);
|
||||
const jumpToExpired = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate(FACTORIES.expired(), {
|
||||
return navigate('/expired', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
@@ -186,7 +156,7 @@ export function useNavigateHelper() {
|
||||
}
|
||||
|
||||
return navigate(
|
||||
FACTORIES.signIn() +
|
||||
'/sign-in' +
|
||||
(searchParams.toString() ? '?' + searchParams.toString() : ''),
|
||||
{
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
@@ -206,7 +176,7 @@ export function useNavigateHelper() {
|
||||
}
|
||||
|
||||
const encodedUrl = encodeURIComponent(deeplink);
|
||||
return navigate(FACTORIES.openApp({ action: `url?url=${encodedUrl}` }));
|
||||
return navigate(`/open-app/url?url=${encodedUrl}`);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
@@ -214,8 +184,7 @@ export function useNavigateHelper() {
|
||||
const jumpToImportTemplate = useCallback(
|
||||
(name: string, snapshotUrl: string) => {
|
||||
return navigate(
|
||||
FACTORIES.template.import() +
|
||||
`?name=${encodeURIComponent(name)}&snapshotUrl=${encodeURIComponent(snapshotUrl)}`
|
||||
`/template/import?name=${encodeURIComponent(name)}&snapshotUrl=${encodeURIComponent(snapshotUrl)}`
|
||||
);
|
||||
},
|
||||
[navigate]
|
||||
@@ -232,8 +201,7 @@ export function useNavigateHelper() {
|
||||
searchParams.set('tab', tab);
|
||||
}
|
||||
return navigate(
|
||||
FACTORIES.workspace.settings({ workspaceId }) +
|
||||
(searchParams.toString() ? `?${searchParams.toString()}` : ''),
|
||||
`/workspace/${workspaceId}/settings?${searchParams.toString()}`,
|
||||
{
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
}
|
||||
@@ -247,7 +215,7 @@ export function useNavigateHelper() {
|
||||
jumpToPageBlock,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
jumpToAll,
|
||||
openPage,
|
||||
jumpToExpired,
|
||||
jumpToSignIn,
|
||||
jumpToCollection,
|
||||
@@ -263,7 +231,7 @@ export function useNavigateHelper() {
|
||||
jumpToPageBlock,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
jumpToAll,
|
||||
openPage,
|
||||
jumpToExpired,
|
||||
jumpToSignIn,
|
||||
jumpToCollection,
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||
import type { DocMeta, Workspace } from '@blocksuite/affine/store';
|
||||
import type { JSX, PropsWithChildren, ReactNode } from 'react';
|
||||
import type { To } from 'react-router';
|
||||
import type { To } from 'react-router-dom';
|
||||
|
||||
export type ListItem =
|
||||
| DocMeta
|
||||
|
||||
@@ -70,7 +70,12 @@ import {
|
||||
JournalGroupHeader,
|
||||
JournalValue,
|
||||
} from './journal';
|
||||
import { NumberDocListProperty, NumberValue } from './number';
|
||||
import {
|
||||
NumberDocListProperty,
|
||||
NumberFilterValue,
|
||||
NumberGroupHeader,
|
||||
NumberValue,
|
||||
} from './number';
|
||||
import {
|
||||
PageWidthDocListProperty,
|
||||
PageWidthFilterValue,
|
||||
@@ -158,8 +163,23 @@ export const WorkspacePropertyTypes = {
|
||||
value: NumberValue,
|
||||
name: 'com.affine.page-properties.property.number',
|
||||
description: 'com.affine.page-properties.property.number.tooltips',
|
||||
filterMethod: {
|
||||
'<': '<',
|
||||
'=': '=',
|
||||
'≠': '≠',
|
||||
'≥': '≥',
|
||||
'≤': '≤',
|
||||
'>': '>',
|
||||
'is-not-empty': 'com.affine.filter.is not empty',
|
||||
'is-empty': 'com.affine.filter.is empty',
|
||||
},
|
||||
allowInGroupBy: true,
|
||||
allowInOrderBy: true,
|
||||
filterValue: NumberFilterValue,
|
||||
defaultFilter: { method: 'is-not-empty' },
|
||||
showInDocList: 'stack',
|
||||
docListProperty: NumberDocListProperty,
|
||||
groupHeader: NumberGroupHeader,
|
||||
},
|
||||
checkbox: {
|
||||
icon: CheckBoxCheckLinearIcon,
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { PropertyValue } from '@affine/component';
|
||||
import { Input, Menu, type MenuRef, PropertyValue } from '@affine/component';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { NumberIcon } from '@blocksuite/icons/rc';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import {
|
||||
type ChangeEventHandler,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { PlainTextDocGroupHeader } from '../explorer/docs-view/group-header';
|
||||
import { StackProperty } from '../explorer/docs-view/stack-property';
|
||||
import type { GroupHeaderProps } from '../explorer/types';
|
||||
import type { PropertyValueProps } from '../properties/types';
|
||||
import * as styles from './number.css';
|
||||
|
||||
@@ -58,6 +64,109 @@ export const NumberValue = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const NumberFilterValue = ({
|
||||
filter,
|
||||
isDraft,
|
||||
onDraftCompleted,
|
||||
onChange,
|
||||
}: {
|
||||
filter: FilterParams;
|
||||
isDraft?: boolean;
|
||||
onDraftCompleted?: () => void;
|
||||
onChange?: (filter: FilterParams) => void;
|
||||
}) => {
|
||||
const [tempValue, setTempValue] = useState(filter.value || '');
|
||||
const [valueMenuOpen, setValueMenuOpen] = useState(false);
|
||||
const menuRef = useRef<MenuRef>(null);
|
||||
const t = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
if (isDraft) {
|
||||
menuRef.current?.changeOpen(true);
|
||||
}
|
||||
}, [isDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
// update temp value with new filter value
|
||||
setTempValue(filter.value || '');
|
||||
}, [filter.value]);
|
||||
|
||||
const submitTempValue = useCallback(() => {
|
||||
if (tempValue !== (filter.value || '')) {
|
||||
onChange?.({
|
||||
...filter,
|
||||
value: tempValue,
|
||||
});
|
||||
}
|
||||
}, [filter, onChange, tempValue]);
|
||||
|
||||
const handleInputKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
submitTempValue();
|
||||
setValueMenuOpen(false);
|
||||
onDraftCompleted?.();
|
||||
},
|
||||
[submitTempValue, onDraftCompleted]
|
||||
);
|
||||
|
||||
const handleInputEnter = useCallback(() => {
|
||||
submitTempValue();
|
||||
setValueMenuOpen(false);
|
||||
onDraftCompleted?.();
|
||||
}, [submitTempValue, onDraftCompleted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDraft &&
|
||||
(filter.method === 'is-not-empty' || filter.method === 'is-empty')
|
||||
) {
|
||||
onDraftCompleted?.();
|
||||
}
|
||||
}, [isDraft, filter.method, onDraftCompleted]);
|
||||
|
||||
return filter.method !== 'is-not-empty' && filter.method !== 'is-empty' ? (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
rootOptions={{
|
||||
open: valueMenuOpen,
|
||||
onOpenChange: setValueMenuOpen,
|
||||
onClose: onDraftCompleted,
|
||||
}}
|
||||
contentOptions={{
|
||||
onPointerDownOutside: submitTempValue,
|
||||
sideOffset: -28,
|
||||
}}
|
||||
items={
|
||||
<Input
|
||||
inputStyle={{
|
||||
fontSize: cssVar('fontBase'),
|
||||
}}
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
autoFocus
|
||||
autoSelect
|
||||
value={tempValue}
|
||||
onChange={value => {
|
||||
setTempValue(value);
|
||||
}}
|
||||
onEnter={handleInputEnter}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
style={{ height: 34, borderRadius: 4 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{filter.value ? (
|
||||
<span>{filter.value}</span>
|
||||
) : (
|
||||
<span style={{ color: cssVarV2('text/placeholder') }}>
|
||||
{t['com.affine.filter.empty']()}
|
||||
</span>
|
||||
)}
|
||||
</Menu>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const NumberDocListProperty = ({ value }: { value: number }) => {
|
||||
if (value !== 0 && !value) {
|
||||
return null;
|
||||
@@ -65,3 +174,13 @@ export const NumberDocListProperty = ({ value }: { value: number }) => {
|
||||
|
||||
return <StackProperty icon={<NumberIcon />}>{value}</StackProperty>;
|
||||
};
|
||||
|
||||
export const NumberGroupHeader = ({ groupId, docCount }: GroupHeaderProps) => {
|
||||
const t = useI18n();
|
||||
const number = groupId || t['com.affine.filter.empty']();
|
||||
return (
|
||||
<PlainTextDocGroupHeader groupId={groupId} docCount={docCount}>
|
||||
{number}
|
||||
</PlainTextDocGroupHeader>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -98,6 +98,16 @@ export const TagsFilterValue = ({
|
||||
},
|
||||
[filter, onChange, selectedTags]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDraft &&
|
||||
(filter.method === 'is-not-empty' || filter.method === 'is-empty')
|
||||
) {
|
||||
onDraftCompleted?.();
|
||||
}
|
||||
}, [isDraft, filter.method, onDraftCompleted]);
|
||||
|
||||
return filter.method !== 'is-not-empty' && filter.method !== 'is-empty' ? (
|
||||
<WorkspaceTagsInlineEditor
|
||||
placeholder={
|
||||
|
||||
@@ -216,14 +216,25 @@ export const TextFilterValue = ({
|
||||
if (e.key !== 'Escape') return;
|
||||
submitTempValue();
|
||||
setValueMenuOpen(false);
|
||||
onDraftCompleted?.();
|
||||
},
|
||||
[submitTempValue]
|
||||
[submitTempValue, onDraftCompleted]
|
||||
);
|
||||
|
||||
const handleInputEnter = useCallback(() => {
|
||||
submitTempValue();
|
||||
setValueMenuOpen(false);
|
||||
}, [submitTempValue]);
|
||||
onDraftCompleted?.();
|
||||
}, [submitTempValue, onDraftCompleted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isDraft &&
|
||||
(filter.method === 'is-not-empty' || filter.method === 'is-empty')
|
||||
) {
|
||||
onDraftCompleted?.();
|
||||
}
|
||||
}, [isDraft, filter.method, onDraftCompleted]);
|
||||
|
||||
return filter.method !== 'is-not-empty' && filter.method !== 'is-empty' ? (
|
||||
<Menu
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ const CloudWorkSpaceList = ({
|
||||
if (currentWorkspaceFlavour === server.id) {
|
||||
const otherWorkspace = workspaces.find(w => w.flavour !== server.id);
|
||||
if (otherWorkspace) {
|
||||
navigateHelper.jumpToAll(otherWorkspace.id);
|
||||
navigateHelper.openPage(otherWorkspace.id, 'all');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
||||
@@ -49,7 +49,7 @@ const Dialog = ({
|
||||
? workspacesService.open({ metadata: workspaceMeta })
|
||||
: { workspace: undefined };
|
||||
|
||||
const { jumpToPage, jumpToAll } = useNavigateHelper();
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
|
||||
const enableCloud = useCallback(async () => {
|
||||
try {
|
||||
@@ -61,11 +61,7 @@ const Dialog = ({
|
||||
account.id,
|
||||
selectedServer.id
|
||||
);
|
||||
if (openPageId) {
|
||||
jumpToPage(newId, openPageId);
|
||||
} else {
|
||||
jumpToAll(newId);
|
||||
}
|
||||
jumpToPage(newId, openPageId || 'all');
|
||||
close?.();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -78,10 +74,9 @@ const Dialog = ({
|
||||
account,
|
||||
workspacesService,
|
||||
selectedServer.id,
|
||||
jumpToPage,
|
||||
openPageId,
|
||||
close,
|
||||
jumpToPage,
|
||||
jumpToAll,
|
||||
t,
|
||||
]);
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ const Dialog = ({
|
||||
workspaces.find(w => w.flavour !== 'local') ??
|
||||
workspaces.at(0);
|
||||
const selectedWorkspaceName = useWorkspaceName(selectedWorkspace);
|
||||
const { jumpToPage, jumpToSignIn } = useNavigateHelper();
|
||||
const { openPage, jumpToSignIn } = useNavigateHelper();
|
||||
|
||||
const noWorkspace = workspaces.length === 0;
|
||||
|
||||
@@ -119,7 +119,7 @@ const Dialog = ({
|
||||
templateDownloader.data$.value,
|
||||
templateMode
|
||||
);
|
||||
jumpToPage(selectedWorkspace.id, docId);
|
||||
openPage(selectedWorkspace.id, docId);
|
||||
onClose?.();
|
||||
} catch (err) {
|
||||
setImportingError(err);
|
||||
@@ -129,8 +129,8 @@ const Dialog = ({
|
||||
}
|
||||
}, [
|
||||
importTemplateService,
|
||||
jumpToPage,
|
||||
onClose,
|
||||
openPage,
|
||||
selectedWorkspace,
|
||||
templateDownloader.data$.value,
|
||||
templateMode,
|
||||
@@ -149,7 +149,7 @@ const Dialog = ({
|
||||
'Workspace',
|
||||
templateDownloader.data$.value
|
||||
);
|
||||
jumpToPage(workspaceId, docId);
|
||||
openPage(workspaceId, docId);
|
||||
onClose?.();
|
||||
} catch (err) {
|
||||
setImportingError(err);
|
||||
@@ -158,8 +158,8 @@ const Dialog = ({
|
||||
}
|
||||
}, [
|
||||
importTemplateService,
|
||||
jumpToPage,
|
||||
onClose,
|
||||
openPage,
|
||||
templateDownloader.data$.value,
|
||||
]);
|
||||
|
||||
|
||||
+6
@@ -1,6 +1,7 @@
|
||||
import { Button, Input, Modal, notify } from '@affine/component';
|
||||
import { IntegrationService } from '@affine/core/modules/integration';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { PlusIcon, TodayIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
@@ -72,6 +73,11 @@ const AddSubscription = () => {
|
||||
.then(() => {
|
||||
setOpen(false);
|
||||
setUrl('');
|
||||
track.$.settingsPanel.integrationList.connectIntegration({
|
||||
type: 'calendar',
|
||||
control: 'Calendar Setting',
|
||||
result: 'success',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notify.error({
|
||||
|
||||
+5
@@ -4,6 +4,7 @@ import {
|
||||
IntegrationService,
|
||||
} from '@affine/core/modules/integration';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
@@ -114,6 +115,10 @@ const UnsubscribeButton = ({ url, name }: { url: string; name: string }) => {
|
||||
}),
|
||||
onConfirm: () => {
|
||||
calendar.deleteSubscription(url);
|
||||
track.$.settingsPanel.integrationList.disconnectIntegration({
|
||||
type: 'calendar',
|
||||
control: 'Calendar Setting',
|
||||
});
|
||||
},
|
||||
confirmText: t['com.affine.integration.calendar.unsubscribe'](),
|
||||
confirmButtonOptions: {
|
||||
|
||||
+1
@@ -40,6 +40,7 @@ export const cardIcon = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: cssVarV2.icon.monotone,
|
||||
});
|
||||
export const cardContent = style([
|
||||
spaceY,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AuthPageContainer } from '@affine/component/auth-components';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useCallback } from 'react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import {
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import { useParams, useSearchParams } from 'react-router';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useMutation } from '../../../components/hooks/use-mutation';
|
||||
import {
|
||||
@@ -26,6 +28,18 @@ import { AppContainer } from '../../components/app-container';
|
||||
import { ConfirmChangeEmail } from './confirm-change-email';
|
||||
import { ConfirmVerifiedEmail } from './email-verified-email';
|
||||
|
||||
const authTypeSchema = z.enum([
|
||||
'onboarding',
|
||||
'setPassword',
|
||||
'signIn',
|
||||
'changePassword',
|
||||
'signUp',
|
||||
'changeEmail',
|
||||
'confirm-change-email',
|
||||
'subscription-redirect',
|
||||
'verify-email',
|
||||
]);
|
||||
|
||||
export const Component = () => {
|
||||
const authService = useService(AuthService);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
@@ -145,3 +159,14 @@ export const Component = () => {
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
if (!args.params.authType) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (!authTypeSchema.safeParse(args.params.authType).success) {
|
||||
return redirect('/404');
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user