Compare commits

..

1 Commits

Author SHA1 Message Date
Lakr
5415f1579b chore: gql update 2025-07-21 13:23:43 +08:00
412 changed files with 3102 additions and 10501 deletions

View File

@@ -18,19 +18,11 @@ services:
ports:
- 6379:6379
# https://mailpit.axllent.org/docs/install/docker/
mailpit:
image: axllent/mailpit:latest
mailhog:
image: mailhog/mailhog:latest
ports:
- 1025:1025
- 8025:8025
environment:
MP_MAX_MESSAGES: 5000
MP_DATABASE: /data/mailpit.db
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
volumes:
- mailpit_data:/data
# https://manual.manticoresearch.com/Starting_the_server/Docker
manticoresearch:
@@ -95,5 +87,4 @@ networks:
volumes:
postgres_data:
manticoresearch_data:
mailpit_data:
elasticsearch_data:

View File

@@ -219,41 +219,6 @@
"type": "boolean",
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false\n@environment `MAILER_IGNORE_TLS`",
"default": false
},
"fallbackDomains": {
"type": "array",
"description": "The emails from these domains are always sent using the fallback SMTP server.\n@default []",
"default": []
},
"fallbackSMTP.host": {
"type": "string",
"description": "Host of the email server (e.g. smtp.gmail.com)\n@default \"\"",
"default": ""
},
"fallbackSMTP.port": {
"type": "number",
"description": "Port of the email server (they commonly are 25, 465 or 587)\n@default 465",
"default": 465
},
"fallbackSMTP.username": {
"type": "string",
"description": "Username used to authenticate the email server\n@default \"\"",
"default": ""
},
"fallbackSMTP.password": {
"type": "string",
"description": "Password used to authenticate the email server\n@default \"\"",
"default": ""
},
"fallbackSMTP.sender": {
"type": "string",
"description": "Sender of all the emails (e.g. \"AFFiNE Team <noreply@affine.pro>\")\n@default \"\"",
"default": ""
},
"fallbackSMTP.ignoreTLS": {
"type": "boolean",
"description": "Whether ignore email server's TSL certification verification. Enable it for self-signed certificates.\n@default false",
"default": false
}
}
},
@@ -664,34 +629,14 @@
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether to enable the copilot plugin. <br> Document: <a href=\"https://docs.affine.pro/self-host-affine/administer/ai\" target=\"_blank\">https://docs.affine.pro/self-host-affine/administer/ai</a>\n@default false",
"description": "Whether to enable the copilot plugin.\n@default false",
"default": false
},
"scenarios": {
"type": "object",
"description": "Use custom models in scenarios and override default settings.\n@default {\"override_enabled\":false,\"scenarios\":{\"audio_transcribing\":\"gemini-2.5-flash\",\"chat\":\"claude-sonnet-4@20250514\",\"embedding\":\"gemini-embedding-001\",\"image\":\"gpt-image-1\",\"rerank\":\"gpt-4.1\",\"coding\":\"claude-sonnet-4@20250514\",\"complex_text_generation\":\"gpt-4o-2024-08-06\",\"quick_decision_making\":\"gpt-5-mini\",\"quick_text_generation\":\"gemini-2.5-flash\",\"polish_and_summarize\":\"gemini-2.5-flash\"}}",
"default": {
"override_enabled": false,
"scenarios": {
"audio_transcribing": "gemini-2.5-flash",
"chat": "claude-sonnet-4@20250514",
"embedding": "gemini-embedding-001",
"image": "gpt-image-1",
"rerank": "gpt-4.1",
"coding": "claude-sonnet-4@20250514",
"complex_text_generation": "gpt-4o-2024-08-06",
"quick_decision_making": "gpt-5-mini",
"quick_text_generation": "gemini-2.5-flash",
"polish_and_summarize": "gemini-2.5-flash"
}
}
},
"providers.openai": {
"type": "object",
"description": "The config for the openai provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.openai.com/v1\"}\n@link https://github.com/openai/openai-node",
"description": "The config for the openai provider.\n@default {\"apiKey\":\"\"}\n@link https://github.com/openai/openai-node",
"default": {
"apiKey": "",
"baseURL": "https://api.openai.com/v1"
"apiKey": ""
}
},
"providers.fal": {
@@ -703,10 +648,9 @@
},
"providers.gemini": {
"type": "object",
"description": "The config for the gemini provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://generativelanguage.googleapis.com/v1beta\"}",
"description": "The config for the gemini provider.\n@default {\"apiKey\":\"\"}",
"default": {
"apiKey": "",
"baseURL": "https://generativelanguage.googleapis.com/v1beta"
"apiKey": ""
}
},
"providers.geminiVertex": {
@@ -753,10 +697,9 @@
},
"providers.anthropic": {
"type": "object",
"description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\",\"baseURL\":\"https://api.anthropic.com/v1\"}",
"description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\"}",
"default": {
"apiKey": "",
"baseURL": "https://api.anthropic.com/v1"
"apiKey": ""
}
},
"providers.anthropicVertex": {

View File

@@ -29,25 +29,25 @@ const isInternal = buildType === 'internal';
const replicaConfig = {
stable: {
web: 2,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 2,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 2,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2,
web: 3,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 3,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 3,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 3,
},
beta: {
web: 1,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1,
sync: Number(process.env.BETA_SYNC_REPLICA) || 1,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 1,
doc: Number(process.env.BETA_DOC_REPLICA) || 1,
web: 2,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.BETA_SYNC_REPLICA) || 2,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 2,
doc: Number(process.env.BETA_DOC_REPLICA) || 2,
},
canary: {
web: 1,
graphql: 1,
sync: 1,
renderer: 1,
doc: 1,
web: 2,
graphql: 2,
sync: 2,
renderer: 2,
doc: 2,
},
};

View File

@@ -1,4 +1,4 @@
replicaCount: 2
replicaCount: 3
enabled: false
database:
connectionName: ""
@@ -33,11 +33,8 @@ service:
resources:
limits:
memory: "1Gi"
cpu: "1"
requests:
memory: "512Mi"
cpu: "100m"
memory: "4Gi"
cpu: "2"
volumes: []
volumeMounts: []

View File

@@ -74,7 +74,7 @@ jobs:
name: Wait for approval
with:
secret: ${{ secrets.GITHUB_TOKEN }}
approvers: forehalo,fengmk2,darkskygit
approvers: forehalo,fengmk2
minimum-approvals: 1
fail-on-denial: true
issue-title: Please confirm to release docker image
@@ -84,7 +84,7 @@ jobs:
Tag: ghcr.io/toeverything/affine:${{ needs.prepare.outputs.BUILD_TYPE }}
> comment with "approve", "approved", "lgtm", "yes" to approve
> comment with "deny", "denied", "no" to deny
> comment with "deny", "deny", "no" to deny
- name: Login to GitHub Container Registry
uses: docker/login-action@v3

View File

@@ -29,7 +29,7 @@ jobs:
shell: cmd
run: |
cd ${{ env.ARCHIVE_DIR }}/out
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
signtool sign /tr http://timestamp.sectigo.com /td sha256 /fd sha256 /a ${{ inputs.files }}
- name: zip file
shell: cmd
run: |

View File

@@ -2,7 +2,6 @@
**/node_modules
.yarn
.github/helm
.git
.vscode
.yarnrc.yml
.docker

24
Cargo.lock generated
View File

@@ -93,7 +93,7 @@ dependencies = [
"symphonia",
"thiserror 2.0.12",
"uuid",
"windows 0.61.3",
"windows 0.61.1",
"windows-core 0.61.2",
]
@@ -1691,7 +1691,7 @@ dependencies = [
"libc",
"log",
"rustversion",
"windows 0.61.3",
"windows 0.61.1",
]
[[package]]
@@ -2284,7 +2284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -4732,9 +4732,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.25.8"
version = "0.25.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7b8994f367f16e6fa14b5aebbcb350de5d7cbea82dc5b00ae997dd71680dd2"
checksum = "ac5fff5c47490dfdf473b5228039bfacad9d765d9b6939d26bf7cc064c1c7822"
dependencies = [
"cc",
"regex",
@@ -4842,9 +4842,9 @@ dependencies = [
[[package]]
name = "tree-sitter-scala"
version = "0.24.0"
version = "0.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7516aeb3d1f40ede8e3045b163e86993b3434514dd06c34c0b75e782d9a0b251"
checksum = "efde5e68b4736e9eac17bfa296c6f104a26bffab363b365eb898c40a63c15d2f"
dependencies = [
"cc",
"tree-sitter-language",
@@ -5334,7 +5334,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -5365,9 +5365,9 @@ dependencies = [
[[package]]
name = "windows"
version = "0.61.3"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections",
"windows-core 0.61.2",
@@ -5477,9 +5477,9 @@ dependencies = [
[[package]]
name = "windows-link"
version = "0.1.3"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-numerics"

View File

@@ -93,7 +93,7 @@ tree-sitter-javascript = { version = "0.23" }
tree-sitter-kotlin-ng = { version = "1.1" }
tree-sitter-python = { version = "0.23" }
tree-sitter-rust = { version = "0.24" }
tree-sitter-scala = { version = "0.24" }
tree-sitter-scala = { version = "0.23" }
tree-sitter-typescript = { version = "0.23" }
uniffi = "0.29"
url = { version = "2.5" }

View File

@@ -164,10 +164,8 @@ export class DatabaseBlockDataSource extends DataSourceBase {
readonly$: ReadonlySignal<boolean> = computed(() => {
return (
this._model.store.readonly ||
(IS_MOBILE &&
!this._model.store.provider
.get(FeatureFlagService)
.getFlag('enable_mobile_database_editing'))
// TODO(@L-Sun): use block level readonly
IS_MOBILE
);
});

View File

@@ -13,7 +13,6 @@ import {
BlockElementCommentManager,
CommentProviderIdentifier,
DocModeProvider,
FeatureFlagService,
NotificationProvider,
type TelemetryEventMap,
TelemetryProvider,
@@ -35,7 +34,6 @@ import {
uniMap,
} from '@blocksuite/data-view';
import { widgetPresets } from '@blocksuite/data-view/widget-presets';
import { IS_MOBILE } from '@blocksuite/global/env';
import { Rect } from '@blocksuite/global/gfx';
import {
CommentIcon,
@@ -50,7 +48,6 @@ import { autoUpdate } from '@floating-ui/dom';
import { computed, signal } from '@preact/signals-core';
import { html, nothing } from 'lit';
import { repeat } from 'lit/directives/repeat.js';
import { styleMap } from 'lit/directives/style-map.js';
import { popSideDetail } from './components/layout.js';
import { DatabaseConfigExtension } from './config.js';
@@ -352,7 +349,6 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
this.classList.add(databaseBlockStyles);
this.listenFullWidthChange();
this.handleMobileEditing();
}
listenFullWidthChange() {
@@ -368,41 +364,6 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
})
);
}
handleMobileEditing() {
if (!IS_MOBILE) return;
let notifyClosed = true;
const handler = () => {
if (
!this.std
.get(FeatureFlagService)
.getFlag('enable_mobile_database_editing')
) {
const notification = this.std.getOptional(NotificationProvider);
if (notification && notifyClosed) {
notifyClosed = false;
notification.notify({
title: html`<div
style=${styleMap({
whiteSpace: 'wrap',
})}
>
Mobile database editing is not supported yet. You can open it in
experimental features, or edit it in desktop mode.
</div>`,
accent: 'warning',
onClose: () => {
notifyClosed = true;
},
});
}
}
};
this.disposables.addFromEvent(this, 'click', handler);
}
private readonly dataViewRootLogic = lazy(
() =>
new DataViewRootUILogic({

View File

@@ -1,7 +1,6 @@
import { ImageBlockModel } from '@blocksuite/affine-model';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
ToolbarModuleExtension,
} from '@blocksuite/affine-shared/services';
@@ -50,10 +49,6 @@ const builtinToolbarConfig = {
});
},
},
{
id: 'c.comment',
...blockCommentToolbarButton,
},
{
placement: ActionPlacement.More,
id: 'a.clipboard',

View File

@@ -24,7 +24,6 @@ import {
getPrevContentBlock,
matchModels,
} from '@blocksuite/affine-shared/utils';
import { IS_ANDROID, IS_MOBILE } from '@blocksuite/global/env';
import { BlockSelection, type EditorHost } from '@blocksuite/std';
import type { BlockModel, Text } from '@blocksuite/store';
@@ -79,28 +78,6 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) {
index: lengthBeforeJoin,
length: 0,
}).catch(console.error);
// due to some IME like Microsoft Swift IME on Android will reset range after join text,
// for example:
//
// $ZERO_WIDTH_FOR_EMPTY_LINE <--- p1
// |aaa <--- p2
//
// after pressing backspace, during beforeinput event, the native range is (p1, 1) -> (p2, 0)
// and after browser and IME handle the event, the native range is (p1, 1) -> (p1, 1)
//
// a|aa <--- p1
//
// so we need to set range again after join text.
if (IS_ANDROID) {
setTimeout(() => {
asyncSetInlineRange(editorHost.std, prevBlock, {
index: lengthBeforeJoin,
length: 0,
}).catch(console.error);
});
}
return true;
}
@@ -114,17 +91,10 @@ export function mergeWithPrev(editorHost: EditorHost, model: BlockModel) {
...EMBED_BLOCK_MODEL_LIST,
])
) {
// due to create a block selection will clear text selection, which lead
// the virtual keyboard to be auto closed on mobile. This behavior breaks
// the user experience.
if (!IS_MOBILE) {
const selection = editorHost.selection.create(BlockSelection, {
blockId: prevBlock.id,
});
editorHost.selection.setGroup('note', [selection]);
} else {
doc.deleteBlock(prevBlock);
}
const selection = editorHost.selection.create(BlockSelection, {
blockId: prevBlock.id,
});
editorHost.selection.setGroup('note', [selection]);
if (model.text?.length === 0) {
doc.deleteBlock(model, {

View File

@@ -634,9 +634,9 @@ export class EdgelessPageKeyboardManager extends PageKeyboardManager {
const movedElements = new Set([
...selectedElements,
...selectedElements.flatMap(el =>
isGfxGroupCompatibleModel(el) ? el.descendantElements : []
),
...selectedElements
.map(el => (isGfxGroupCompatibleModel(el) ? el.descendantElements : []))
.flat(),
]);
movedElements.forEach(element => {

View File

@@ -4,6 +4,6 @@ export * from './clipboard/command';
export * from './edgeless-root-block.js';
export { EdgelessRootService } from './edgeless-root-service.js';
export * from './utils/clipboard-utils.js';
export { getElementProps, sortEdgelessElements } from './utils/clone-utils.js';
export { sortEdgelessElements } from './utils/clone-utils.js';
export { isCanvasElement } from './utils/query.js';
export { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';

View File

@@ -5,7 +5,6 @@ import {
} from '@blocksuite/affine-shared/commands';
import {
ActionPlacement,
blockCommentToolbarButton,
type ToolbarModuleConfig,
} from '@blocksuite/affine-shared/services';
import { CaptionIcon, CopyIcon, DeleteIcon } from '@blocksuite/icons/lit';
@@ -62,10 +61,6 @@ export const surfaceRefToolbarModuleConfig: ToolbarModuleConfig = {
surfaceRefBlock.captionElement.show();
},
},
{
id: 'e.comment',
...blockCommentToolbarButton,
},
{
id: 'a.clipboard',
placement: ActionPlacement.More,

View File

@@ -11,7 +11,7 @@ import {
getBoundWithRotation,
intersects,
} from '@blocksuite/global/gfx';
import { type BlockStdScope, SurfaceSelection } from '@blocksuite/std';
import type { BlockStdScope } from '@blocksuite/std';
import type {
GfxCompatibleInterface,
GridManager,
@@ -298,10 +298,7 @@ export class DomRenderer {
viewportBounds,
zoom
);
const zIndexStyle = {
'z-index': this.layerManager.getZIndex(elementModel),
};
Object.assign(domElement.style, geometricStyles, zIndexStyle);
Object.assign(domElement.style, geometricStyles);
Object.assign(domElement.style, PLACEHOLDER_RESET_STYLES);
// Clear classes specific to shapes, if applicable
@@ -338,10 +335,7 @@ export class DomRenderer {
zoom
);
const opacityStyle = getOpacity(elementModel);
const zIndexStyle = {
'z-index': this.layerManager.getZIndex(elementModel),
};
Object.assign(domElement.style, geometricStyles, opacityStyle, zIndexStyle);
Object.assign(domElement.style, geometricStyles, opacityStyle);
this._renderElement(elementModel, domElement);
}
@@ -390,36 +384,6 @@ export class DomRenderer {
this.refresh();
})
);
// Workaround for the group rendering reactive update when selection changed
let lastSet = new Set<string>();
this._disposables.add(
this.std.selection.filter$(SurfaceSelection).subscribe(selections => {
const groupRelatedSelection = new Set(
selections.flatMap(s =>
s.elements.flatMap(e => {
const element = surfaceModel.getElementById(e);
if (
element &&
(element.type === 'group' || element.groups.length !== 0)
) {
return [element.id, ...element.groups.map(g => g.id)];
}
return [];
})
)
);
if (lastSet.symmetricDifference(groupRelatedSelection).size !== 0) {
lastSet.union(groupRelatedSelection).forEach(g => {
this._markElementDirty(g, UpdateType.ELEMENT_UPDATED);
});
this.refresh();
}
lastSet = groupRelatedSelection;
})
);
}
addOverlay = (overlay: Overlay) => {

View File

@@ -65,7 +65,7 @@ export abstract class DataViewUILogicBase<
return handler(context);
});
}
setSelection(selection?: Selection) {
setSelection(selection?: Selection): void {
this.root.setSelection(selection);
}

View File

@@ -73,9 +73,7 @@ export class MobileKanbanCell extends SignalWatcher(
if (this.view.readonly$.value) {
return;
}
const setSelection = this.kanbanViewLogic.setSelection.bind(
this.kanbanViewLogic
);
const setSelection = this.kanbanViewLogic.setSelection;
const viewId = this.kanbanViewLogic.view.id;
if (setSelection && viewId) {
if (editing && this.cell?.beforeEnterEditMode() === false) {
@@ -103,12 +101,12 @@ export class MobileKanbanCell extends SignalWatcher(
this.disposables.add(
effect(() => {
const isEditing = this.isSelectionEditing$.value;
if (isEditing && !this.isEditing$.peek()) {
if (isEditing) {
this.isEditing$.value = true;
requestAnimationFrame(() => {
this._cell.value?.afterEnterEditingMode();
});
} else if (!isEditing && this.isEditing$.peek()) {
} else {
this._cell.value?.beforeExitEditingMode();
this.isEditing$.value = false;
}

View File

@@ -86,9 +86,6 @@ export class MobileKanbanViewUILogic extends DataViewUILogicBase<
}
renderAddGroup = () => {
if (this.readonly) {
return;
}
const addGroup = this.groupManager.addGroup;
if (!addGroup) {
return;

View File

@@ -68,9 +68,7 @@ export class MobileTableCell extends SignalWatcher(
if (this.view.readonly$.value) {
return;
}
const setSelection = this.tableViewLogic.setSelection.bind(
this.tableViewLogic
);
const setSelection = this.tableViewLogic.setSelection;
const viewId = this.tableViewLogic.view.id;
if (setSelection && viewId) {
if (editing && this.cell?.beforeEnterEditMode() === false) {
@@ -105,13 +103,13 @@ export class MobileTableCell extends SignalWatcher(
this.disposables.add(
effect(() => {
const isEditing = this.isSelectionEditing$.value;
if (isEditing && !this.isEditing$.peek()) {
if (isEditing) {
this.isEditing$.value = true;
const cell = this._cell.value;
requestAnimationFrame(() => {
cell?.afterEnterEditingMode();
});
} else if (!isEditing && this.isEditing$.peek()) {
} else {
this._cell.value?.beforeExitEditingMode();
this.isEditing$.value = false;
}

View File

@@ -5,6 +5,12 @@ export const mobileTableViewWrapper = css({
position: 'relative',
width: '100%',
paddingBottom: '4px',
/**
* Disable horizontal scrolling to prevent crashes on iOS Safari
* See https://github.com/toeverything/AFFiNE/pull/12203
* and https://github.com/toeverything/blocksuite/pull/8784
*/
overflowX: 'hidden',
overflowY: 'hidden',
});

View File

@@ -88,9 +88,6 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
};
private readonly addFilter = (e: MouseEvent) => {
if (this.dataViewLogic.root.config.dataSource.readonly$.peek()) {
return;
}
const element = popupTargetFromElement(e.target as HTMLElement);
popCreateFilter(element, {
vars: this.vars,

View File

@@ -68,5 +68,5 @@ export function getHeadingBlocksFromDoc(
ignoreEmpty = false
) {
const notes = getNotesFromStore(store, modes);
return notes.flatMap(note => getHeadingBlocksFromNote(note, ignoreEmpty));
return notes.map(note => getHeadingBlocksFromNote(note, ignoreEmpty)).flat();
}

View File

@@ -1,7 +1,7 @@
export * from './adapter';
export * from './brush-tool';
export * from './element-renderer';
export * from './eraser-tool';
export * from './highlighter-tool';
export * from './renderer';
export * from './toolbar/configs';
export * from './toolbar/senior-tool';

View File

@@ -1,69 +0,0 @@
import {
DomElementRendererExtension,
type DomRenderer,
} from '@blocksuite/affine-block-surface';
import type { BrushElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
export const BrushDomRendererExtension = DomElementRendererExtension(
'brush',
(
model: BrushElementModel,
domElement: HTMLElement,
renderer: DomRenderer
) => {
const { zoom } = renderer.viewport;
const [, , w, h] = model.deserializedXYWH;
// Early return if invalid dimensions
if (w <= 0 || h <= 0) {
return;
}
// Early return if no commands
if (!model.commands) {
return;
}
// Clear previous content
domElement.innerHTML = '';
// Get color value
const color = renderer.getColorValue(model.color, DefaultTheme.black, true);
// Create SVG element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.position = 'absolute';
svg.style.left = '0';
svg.style.top = '0';
svg.style.width = `${w * zoom}px`;
svg.style.height = `${h * zoom}px`;
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
// Apply rotation transform
if (model.rotate !== 0) {
svg.style.transform = `rotate(${model.rotate}deg)`;
svg.style.transformOrigin = 'center';
}
// Create path element for the brush stroke
const pathElement = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
pathElement.setAttribute('d', model.commands);
pathElement.setAttribute('fill', color);
pathElement.setAttribute('stroke', 'none');
svg.append(pathElement);
domElement.replaceChildren(svg);
// Set element size and position
domElement.style.width = `${w * zoom}px`;
domElement.style.height = `${h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
}
);

View File

@@ -1,73 +0,0 @@
import {
DomElementRendererExtension,
type DomRenderer,
} from '@blocksuite/affine-block-surface';
import type { HighlighterElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
export const HighlighterDomRendererExtension = DomElementRendererExtension(
'highlighter',
(
model: HighlighterElementModel,
domElement: HTMLElement,
renderer: DomRenderer
) => {
const { zoom } = renderer.viewport;
const [, , w, h] = model.deserializedXYWH;
// Early return if invalid dimensions
if (w <= 0 || h <= 0) {
return;
}
// Early return if no commands
if (!model.commands) {
return;
}
// Clear previous content
domElement.innerHTML = '';
// Get color value
const color = renderer.getColorValue(
model.color,
DefaultTheme.hightlighterColor,
true
);
// Create SVG element
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.style.position = 'absolute';
svg.style.left = '0';
svg.style.top = '0';
svg.style.width = `${w * zoom}px`;
svg.style.height = `${h * zoom}px`;
svg.style.overflow = 'visible';
svg.style.pointerEvents = 'none';
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
// Apply rotation transform
if (model.rotate !== 0) {
svg.style.transform = `rotate(${model.rotate}deg)`;
svg.style.transformOrigin = 'center';
}
// Create path element for the highlighter stroke
const pathElement = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
pathElement.setAttribute('d', model.commands);
pathElement.setAttribute('fill', color);
pathElement.setAttribute('stroke', 'none');
svg.append(pathElement);
domElement.replaceChildren(svg);
// Set element size and position
domElement.style.width = `${w * zoom}px`;
domElement.style.height = `${h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
}
);

View File

@@ -1,2 +0,0 @@
export { BrushDomRendererExtension } from './brush';
export { HighlighterDomRendererExtension } from './highlighter';

View File

@@ -1,2 +0,0 @@
export { BrushElementRendererExtension } from './brush';
export { HighlighterElementRendererExtension } from './highlighter';

View File

@@ -1,2 +0,0 @@
export * from './dom';
export * from './element';

View File

@@ -5,14 +5,9 @@ import {
import { BrushTool } from './brush-tool';
import { effects } from './effects';
import { BrushElementRendererExtension } from './element-renderer';
import { EraserTool } from './eraser-tool';
import { HighlighterTool } from './highlighter-tool';
import {
BrushDomRendererExtension,
BrushElementRendererExtension,
HighlighterDomRendererExtension,
HighlighterElementRendererExtension,
} from './renderer';
import {
brushToolbarExtension,
highlighterToolbarExtension,
@@ -35,9 +30,6 @@ export class BrushViewExtension extends ViewExtensionProvider {
context.register(HighlighterTool);
context.register(BrushElementRendererExtension);
context.register(BrushDomRendererExtension);
context.register(HighlighterElementRendererExtension);
context.register(HighlighterDomRendererExtension);
context.register(brushToolbarExtension);
context.register(highlighterToolbarExtension);

View File

@@ -0,0 +1,11 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import { connectorDomRenderer } from './connector-dom/index.js';
/**
* Extension to register the DOM-based renderer for 'connector' elements.
*/
export const ConnectorDomRendererExtension = DomElementRendererExtension(
'connector',
connectorDomRenderer
);

View File

@@ -1,18 +1,14 @@
import {
DomElementRendererExtension,
type DomRenderer,
} from '@blocksuite/affine-block-surface';
import type { DomRenderer } from '@blocksuite/affine-block-surface';
import {
type ConnectorElementModel,
ConnectorMode,
DefaultTheme,
type LocalConnectorElementModel,
type PointStyle,
} from '@blocksuite/affine-model';
import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx';
import { isConnectorWithLabel } from '../connector-manager';
import { DEFAULT_ARROW_SIZE } from './utils';
import { isConnectorWithLabel } from '../../connector-manager.js';
import { DEFAULT_ARROW_SIZE } from '../utils.js';
interface PathBounds {
minX: number;
@@ -225,8 +221,8 @@ function renderConnectorLabel(
* @param element - The HTMLElement to apply the connector's styles to.
* @param renderer - The main DOMRenderer instance, providing access to viewport and color utilities.
*/
export const connectorBaseDomRenderer = (
model: ConnectorElementModel | LocalConnectorElementModel,
export const connectorDomRenderer = (
model: ConnectorElementModel,
element: HTMLElement,
renderer: DomRenderer
): void => {
@@ -362,21 +358,10 @@ export const connectorBaseDomRenderer = (
element.style.height = `${model.h * zoom}px`;
element.style.overflow = 'visible';
element.style.pointerEvents = 'none';
};
export const connectorDomRenderer = (
model: ConnectorElementModel,
element: HTMLElement,
renderer: DomRenderer
): void => {
connectorBaseDomRenderer(model, element, renderer);
renderConnectorLabel(model, element, renderer, renderer.viewport.zoom);
};
// Set z-index for layering
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
/**
* Extension to register the DOM-based renderer for 'connector' elements.
*/
export const ConnectorDomRendererExtension = DomElementRendererExtension(
'connector',
connectorDomRenderer
);
// Render label if present
renderConnectorLabel(model, element, renderer, zoom);
};

View File

@@ -25,7 +25,7 @@ import {
} from '@blocksuite/global/gfx';
import { deltaInsertsToChunks } from '@blocksuite/std/inline';
import { isConnectorWithLabel } from '../connector-manager';
import { isConnectorWithLabel } from '../connector-manager.js';
import {
DEFAULT_ARROW_SIZE,
getArrowOptions,
@@ -33,7 +33,7 @@ import {
renderCircle,
renderDiamond,
renderTriangle,
} from './utils';
} from './utils.js';
export const connector: ElementRenderer<
ConnectorElementModel | LocalConnectorElementModel

View File

@@ -1,8 +1,9 @@
export * from './adapter';
export * from './connector-manager';
export * from './connector-tool';
export * from './element-renderer';
export { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
export * from './element-transform';
export * from './renderer';
export * from './text';
export * from './toolbar/config';
export * from './toolbar/quick-tool';

View File

@@ -1,2 +0,0 @@
export * from './dom-renderer';
export * from './element-renderer';

View File

@@ -6,11 +6,9 @@ import {
import { ConnectionOverlay } from './connector-manager';
import { ConnectorTool } from './connector-tool';
import { effects } from './effects';
import { ConnectorElementRendererExtension } from './element-renderer';
import { ConnectorDomRendererExtension } from './element-renderer/connector-dom';
import { ConnectorFilter } from './element-transform';
import {
ConnectorDomRendererExtension,
ConnectorElementRendererExtension,
} from './renderer';
import { connectorToolbarExtension } from './toolbar/config';
import { connectorQuickTool } from './toolbar/quick-tool';
import { ConnectorElementView, ConnectorInteraction } from './view/view';

View File

@@ -6,7 +6,7 @@ import {
import type { GroupElementModel } from '@blocksuite/affine-model';
import { Bound } from '@blocksuite/global/gfx';
import { titleRenderParams } from './utils';
import { titleRenderParams } from './utils.js';
export const group: ElementRenderer<GroupElementModel> = (
model,

View File

@@ -13,7 +13,7 @@ import {
GROUP_TITLE_FONT_SIZE,
GROUP_TITLE_OFFSET,
GROUP_TITLE_PADDING,
} from './consts';
} from './consts.js';
export function titleRenderParams(group: GroupElementModel, zoom: number) {
let text = group.title.toString().trim();

View File

@@ -1,6 +1,6 @@
export * from './adapter';
export * from './command';
export * from './element-renderer';
export * from './element-view';
export * from './renderer';
export * from './text/text';
export * from './toolbar/config';

View File

@@ -1,62 +0,0 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import { FontWeight, type GroupElementModel } from '@blocksuite/affine-model';
import {
GROUP_TITLE_FONT,
GROUP_TITLE_FONT_SIZE,
GROUP_TITLE_PADDING,
} from './consts';
import { titleRenderParams } from './utils';
export const GroupDomRendererExtension = DomElementRendererExtension(
'group',
(model: GroupElementModel, domElement, renderer) => {
const { zoom } = renderer.viewport;
const [, , w, h] = model.deserializedXYWH;
const renderParams = titleRenderParams(model, zoom);
model.externalXYWH = renderParams.titleBound.serialize();
domElement.innerHTML = '';
domElement.style.outlineColor = '';
domElement.style.outlineWidth = '';
domElement.style.outlineStyle = '';
const elements = renderer.provider.selectedElements?.() || [];
const renderTitle = () => {
const { text } = renderParams;
const titleElement = document.createElement('div');
titleElement.style.transform = `translate(0, -100%)`;
titleElement.style.fontFamily = GROUP_TITLE_FONT;
titleElement.style.fontWeight = `${FontWeight.Regular}`;
titleElement.style.fontStyle = 'normal';
titleElement.style.fontSize = `${GROUP_TITLE_FONT_SIZE}px`;
titleElement.style.color = renderer.getPropertyValue('--affine-blue');
titleElement.style.textAlign = 'left';
titleElement.style.padding = `${GROUP_TITLE_PADDING[0]}px ${GROUP_TITLE_PADDING[1]}px`;
titleElement.textContent = text;
domElement.replaceChildren(titleElement);
};
if (elements.includes(model.id)) {
if (model.showTitle) {
renderTitle();
} else {
domElement.style.outlineColor =
renderer.getPropertyValue('--affine-blue');
domElement.style.outlineWidth = '2px';
domElement.style.outlineStyle = 'solid';
}
} else if (model.childElements.some(child => elements.includes(child.id))) {
domElement.style.outlineColor = '#8FD1FF';
domElement.style.outlineWidth = '2px';
domElement.style.outlineStyle = 'solid';
}
domElement.style.width = `${w * zoom}px`;
domElement.style.height = `${h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
}
);

View File

@@ -1,2 +0,0 @@
export * from './dom-renderer';
export * from './element-renderer';

View File

@@ -21,7 +21,7 @@ import {
GROUP_TITLE_FONT_SIZE,
GROUP_TITLE_OFFSET,
GROUP_TITLE_PADDING,
} from '../renderer/consts';
} from '../element-renderer/consts';
export function mountGroupTitleEditor(
group: GroupElementModel,

View File

@@ -4,12 +4,9 @@ import {
} from '@blocksuite/affine-ext-loader';
import { effects } from './effects';
import { GroupElementRendererExtension } from './element-renderer';
import { GroupElementView, GroupInteraction } from './element-view';
import { GroupInteractionExtension } from './interaction-ext';
import {
GroupDomRendererExtension,
GroupElementRendererExtension,
} from './renderer';
import { groupToolbarExtension } from './toolbar/config';
export class GroupViewExtension extends ViewExtensionProvider {
@@ -23,7 +20,6 @@ export class GroupViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) {
super.setup(context);
context.register(GroupElementRendererExtension);
context.register(GroupDomRendererExtension);
context.register(GroupElementView);
if (this.isEdgeless(context.scope)) {
context.register(groupToolbarExtension);

View File

@@ -1,7 +1,7 @@
export * from './adapter';
export * from './element-renderer';
export * from './indicator-overlay';
export * from './interactivity';
export * from './renderer';
export * from './toolbar/config';
export * from './toolbar/senior-tool';
export * from './utils';

View File

@@ -1,65 +0,0 @@
import { DomElementRendererExtension } from '@blocksuite/affine-block-surface';
import {
connectorBaseDomRenderer,
ConnectorPathGenerator,
} from '@blocksuite/affine-gfx-connector';
import type {
MindmapElementModel,
MindmapNode,
} from '@blocksuite/affine-model';
import type { GfxModel } from '@blocksuite/std/gfx';
export const MindmapDomRendererExtension = DomElementRendererExtension(
'mindmap',
(model: MindmapElementModel, domElement, renderer) => {
const bound = model.elementBound;
const { zoom } = renderer.viewport;
// Set element size and position
domElement.style.width = `${bound.w * zoom}px`;
domElement.style.height = `${bound.h * zoom}px`;
domElement.style.overflow = 'visible';
domElement.style.pointerEvents = 'none';
const newChildren: HTMLDivElement[] = [];
const traverse = (node: MindmapNode) => {
const connectors = model.getConnectors(node);
if (!connectors) return;
connectors.reverse().forEach(result => {
const { connector, outdated } = result;
const elementGetter = (id: string) =>
model.surface.getElementById(id) ??
(model.surface.store.getModelById(id) as GfxModel);
if (outdated) {
ConnectorPathGenerator.updatePath(connector, null, elementGetter);
}
const connectorContainer = document.createElement('div');
connectorContainer.style.position = 'absolute';
connectorContainer.style.transformOrigin = 'top left';
const geometricStyles = {
left: `${(connector.x - bound.x) * zoom}px`,
top: `${(connector.y - bound.y) * zoom}px`,
};
const opacityStyle = { opacity: node.element.opacity };
Object.assign(connectorContainer.style, geometricStyles, opacityStyle);
connectorBaseDomRenderer(connector, connectorContainer, renderer);
newChildren.push(connectorContainer);
});
if (node.detail.collapsed) {
return;
} else {
node.children.forEach(traverse);
}
};
model.tree && traverse(model.tree);
domElement.replaceChildren(...newChildren);
}
);

View File

@@ -1,2 +0,0 @@
export * from './dom-renderer';
export * from './element-renderer';

View File

@@ -4,12 +4,9 @@ import {
} from '@blocksuite/affine-ext-loader';
import { effects } from './effects';
import { MindmapElementRendererExtension } from './element-renderer';
import { MindMapIndicatorOverlay } from './indicator-overlay';
import { MindMapDragExtension } from './interactivity';
import {
MindmapDomRendererExtension,
MindmapElementRendererExtension,
} from './renderer';
import {
mindmapToolbarExtension,
shapeMindmapToolbarExtension,
@@ -28,7 +25,6 @@ export class MindmapViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) {
super.setup(context);
context.register(MindmapElementRendererExtension);
context.register(MindmapDomRendererExtension);
context.register(mindMapSeniorTool);
context.register(mindmapToolbarExtension);
context.register(shapeMindmapToolbarExtension);

View File

@@ -1,7 +1,4 @@
import {
DefaultTool,
EdgelessLegacySlotIdentifier,
} from '@blocksuite/affine-block-surface';
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import { on } from '@blocksuite/affine-shared/utils';
import type { PointerEventState } from '@blocksuite/std';
import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';
@@ -67,15 +64,12 @@ export class PanTool extends BaseTool<PanToolOption> {
const { toolType, options: originalToolOptions } = currentTool;
const selectionToRestore = this.gfx.selection.surfaceSelections;
if (!toolType) return;
// restore to DefaultTool if previous tool is CopilotTool
if (toolType.toolName === 'copilot') {
this.controller.setTool(DefaultTool);
return;
}
let finalOptions: ToolOptions<BaseTool<any>> | undefined =
originalToolOptions;
if (toolType.toolName === 'frameNavigator') {
const PRESENT_TOOL_NAME = 'frameNavigator';
if (toolType.toolName === PRESENT_TOOL_NAME) {
// When restoring PresentTool (frameNavigator) after a temporary pan (e.g., via middle mouse button),
// set 'restoredAfterPan' to true. This allows PresentTool to avoid an unwanted viewport reset
// and maintain the panned position.
@@ -99,17 +93,15 @@ export class PanTool extends BaseTool<PanToolOption> {
});
}
requestAnimationFrame(() => {
this.controller.setTool(PanTool, {
panning: true,
});
this.controller.setTool(PanTool, {
panning: true,
});
const dispose = on(document, 'pointerup', evt => {
if (evt.button === MouseButton.MIDDLE) {
restoreToPrevious();
dispose();
}
dispose();
});
return false;

View File

@@ -1 +1,2 @@
export * from './highlighter';
export * from './shape';

View File

@@ -1,5 +1,4 @@
import type { DomRenderer } from '@blocksuite/affine-block-surface';
import { isRTL } from '@blocksuite/affine-gfx-text';
import type { ShapeElementModel } from '@blocksuite/affine-model';
import { DefaultTheme } from '@blocksuite/affine-model';
import { SVGShapeBuilder } from '@blocksuite/global/gfx';
@@ -100,8 +99,6 @@ export const shapeDomRenderer = (
const unscaledWidth = model.w;
const unscaledHeight = model.h;
const newChildren: Element[] = [];
const fillColor = renderer.getColorValue(
model.fillColor,
DefaultTheme.shapeFillColor,
@@ -173,7 +170,8 @@ export const shapeDomRenderer = (
}
svg.append(polygon);
newChildren.push(svg);
// 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
@@ -181,43 +179,10 @@ export const shapeDomRenderer = (
applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border
}
if (model.textDisplay && model.text) {
const str = model.text.toString();
const textElement = document.createElement('div');
if (isRTL(str)) {
textElement.dir = 'rtl';
}
textElement.style.position = 'absolute';
textElement.style.inset = '0';
textElement.style.display = 'flex';
textElement.style.flexDirection = 'column';
textElement.style.justifyContent =
model.textVerticalAlign === 'center'
? 'center'
: model.textVerticalAlign === 'top'
? 'flex-start'
: 'flex-end';
textElement.style.whiteSpace = 'pre-wrap';
textElement.style.wordBreak = 'break-word';
textElement.style.textAlign = model.textAlign;
textElement.style.alignmentBaseline = 'alphabetic';
textElement.style.fontFamily = model.fontFamily;
textElement.style.fontSize = `${model.fontSize * zoom}px`;
textElement.style.fontWeight = model.fontWeight;
textElement.style.color = renderer.getColorValue(
model.color,
DefaultTheme.shapeTextColor,
true
);
textElement.textContent = str;
newChildren.push(textElement);
}
// Replace existing children to avoid memory leaks
element.replaceChildren(...newChildren);
applyTransformStyles(model, element);
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
manageClassNames(model, element);
applyShadowStyles(model, element, renderer);
};

View File

@@ -4,7 +4,10 @@ import {
} from '@blocksuite/affine-ext-loader';
import { effects } from './effects';
import { ShapeElementRendererExtension } from './element-renderer';
import {
HighlighterElementRendererExtension,
ShapeElementRendererExtension,
} from './element-renderer';
import { ShapeDomRendererExtension } from './element-renderer/shape-dom';
import { ShapeElementView, ShapeViewInteraction } from './element-view';
import { ShapeTool } from './shape-tool';
@@ -21,6 +24,7 @@ export class ShapeViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) {
super.setup(context);
if (this.isEdgeless(context.scope)) {
context.register(HighlighterElementRendererExtension);
context.register(ShapeElementRendererExtension);
context.register(ShapeDomRendererExtension);
context.register(ShapeElementView);

View File

@@ -103,52 +103,54 @@ export class InlineCommentManager extends LifeCycleWatcher {
id: CommentId,
selections: BaseSelection[]
) => {
const needCommentTexts = selections.flatMap(selection => {
if (!selection.is(TextSelection)) return [];
const [_, { selectedBlocks }] = this.std.command
.chain()
.pipe(getSelectedBlocksCommand, {
textSelection: selection,
})
.run();
const needCommentTexts = selections
.map(selection => {
if (!selection.is(TextSelection)) return [];
const [_, { selectedBlocks }] = this.std.command
.chain()
.pipe(getSelectedBlocksCommand, {
textSelection: selection,
})
.run();
if (!selectedBlocks) return [];
if (!selectedBlocks) return [];
type MakeRequired<T, K extends keyof T> = T & {
[key in K]: NonNullable<T[key]>;
};
type MakeRequired<T, K extends keyof T> = T & {
[key in K]: NonNullable<T[key]>;
};
return selectedBlocks
.map(
({ model }) =>
[model, getInlineEditorByModel(this.std, model)] as const
)
.filter(
(
pair
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
!!pair[0].text && !!pair[1]
)
.map(([model, inlineEditor]) => {
let from: TextRangePoint;
let to: TextRangePoint | null;
if (model.id === selection.from.blockId) {
from = selection.from;
to = null;
} else if (model.id === selection.to?.blockId) {
from = selection.to;
to = null;
} else {
from = {
blockId: model.id,
index: 0,
length: model.text.yText.length,
};
to = null;
}
return [new TextSelection({ from, to }), inlineEditor] as const;
});
});
return selectedBlocks
.map(
({ model }) =>
[model, getInlineEditorByModel(this.std, model)] as const
)
.filter(
(
pair
): pair is [MakeRequired<BlockModel, 'text'>, AffineInlineEditor] =>
!!pair[0].text && !!pair[1]
)
.map(([model, inlineEditor]) => {
let from: TextRangePoint;
let to: TextRangePoint | null;
if (model.id === selection.from.blockId) {
from = selection.from;
to = null;
} else if (model.id === selection.to?.blockId) {
from = selection.to;
to = null;
} else {
from = {
blockId: model.id,
index: 0,
length: model.text.yText.length,
};
to = null;
}
return [new TextSelection({ from, to }), inlineEditor] as const;
});
})
.flat();
if (needCommentTexts.length === 0) return;

View File

@@ -150,9 +150,6 @@ export class AffineReference extends WithDisposable(ShadowlessElement) {
readonly open = (event?: Partial<DocLinkClickedEvent>) => {
if (!this.config.interactable) return;
if (event?.event?.button === 2) {
return;
}
this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.next({
...this.referenceInfo,

View File

@@ -131,7 +131,7 @@ export class HighlighterElementModel extends GfxPrimitiveElementModel<Highlighte
instance['_local'].delete('commands');
})
@derive((lineWidth: number, instance: Instance) => {
const oldBound = Bound.fromXYWH(instance.deserializedXYWH);
const oldBound = instance.elementBound;
if (
lineWidth === instance.lineWidth ||

View File

@@ -64,7 +64,7 @@ export const blockCommentToolbarButton: Omit<ToolbarAction, 'id'> = {
// may be hover on a block or element, in this case
// the selection is empty, so we need to get the current model
if (model) {
if (model && selections.length === 0) {
if (model instanceof BlockModel) {
commentProvider.addComment([
new BlockSelection({

View File

@@ -15,7 +15,6 @@ export interface BlockSuiteFlags {
enable_shape_shadow_blur: boolean;
enable_mobile_keyboard_toolbar: boolean;
enable_mobile_linked_doc_menu: boolean;
enable_mobile_database_editing: boolean;
enable_block_meta: boolean;
enable_callout: boolean;
enable_edgeless_scribbled_style: boolean;
@@ -42,7 +41,6 @@ export class FeatureFlagService extends StoreExtension {
enable_mobile_keyboard_toolbar: false,
enable_mobile_linked_doc_menu: false,
enable_block_meta: true,
enable_mobile_database_editing: false,
enable_callout: false,
enable_edgeless_scribbled_style: false,
enable_table_virtual_scroll: false,

View File

@@ -5,7 +5,6 @@ import type { Signal } from '@preact/signals-core';
import type { AffineUserInfo } from './types';
export interface UserService {
currentUserInfo$: Signal<AffineUserInfo | null>;
userInfo$(id: string): Signal<AffineUserInfo | null>;
isLoading$(id: string): Signal<boolean>;
error$(id: string): Signal<string | null>; // user friendly error string

View File

@@ -4,14 +4,6 @@ import type { ReadonlySignal } from '@preact/signals-core';
export interface VirtualKeyboardProvider {
readonly visible$: ReadonlySignal<boolean>;
readonly height$: ReadonlySignal<number>;
/**
* The static height of the keyboard, it should record the last non-zero height of virtual keyboard
*/
readonly staticHeight$: ReadonlySignal<number>;
/**
* The safe area of the app tab, it will be used when the keyboard is open or closed
*/
readonly appTabSafeArea$: ReadonlySignal<string>;
}
export interface VirtualKeyboardProviderWithAction

View File

@@ -11,12 +11,14 @@ export function getSelectedRect(selected: GfxModel[]): DOMRect {
return new DOMRect();
}
const lockedElementsByFrame = selected.flatMap(selectable => {
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
return selectable.descendantElements;
}
return [];
});
const lockedElementsByFrame = selected
.map(selectable => {
if (selectable instanceof FrameBlockModel && selectable.isLocked()) {
return selectable.descendantElements;
}
return [];
})
.flat();
selected = [...new Set([...selected, ...lockedElementsByFrame])];

View File

@@ -114,7 +114,6 @@ export class PreviewHelper {
});
let width: number = 500;
// oxlint-disable-next-line no-unassigned-vars
let height;
const noteBlock = this.widget.host.querySelector('affine-note');

View File

@@ -168,6 +168,10 @@ export type KeyboardSubToolbarConfig = {
export type KeyboardToolbarContext = {
std: BlockStdScope;
rootComponent: BlockComponent;
/**
* Close tool bar, and blur the focus if blur is true, default is false
*/
closeToolbar: (blur?: boolean) => void;
/**
* Close current tool panel and show virtual keyboard
*/

View File

@@ -4,7 +4,7 @@ import {
requiredProperties,
ShadowlessElement,
} from '@blocksuite/std';
import { html, nothing } from 'lit';
import { html, nothing, type PropertyValues } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
@@ -71,13 +71,22 @@ export class AffineKeyboardToolPanel extends SignalWatcher(
.map(group => (typeof group === 'function' ? group(this.context) : group))
.filter((group): group is KeyboardToolPanelGroup => group !== null);
return html`<div class="affine-keyboard-tool-panel-container">
${repeat(
groups,
group => group.name,
group => this._renderGroup(group)
)}
</div>`;
return repeat(
groups,
group => group.name,
group => this._renderGroup(group)
);
}
protected override willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has('height')) {
this.style.height = `${this.height}px`;
if (this.height === 0) {
this.style.padding = '0';
} else {
this.style.padding = '';
}
}
}
@property({ attribute: false })
@@ -85,4 +94,7 @@ export class AffineKeyboardToolPanel extends SignalWatcher(
@property({ attribute: false })
accessor context!: KeyboardToolbarContext;
@property({ attribute: false })
accessor height = 0;
}

View File

@@ -8,7 +8,7 @@ import {
requiredProperties,
ShadowlessElement,
} from '@blocksuite/std';
import { effect, type Signal, signal } from '@preact/signals-core';
import { effect, type Signal, signal, untracked } from '@preact/signals-core';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
@@ -22,6 +22,7 @@ import type {
KeyboardToolbarItem,
KeyboardToolPanelConfig,
} from './config';
import { PositionController } from './position-controller';
import { keyboardToolbarStyles } from './styles';
import {
isKeyboardSubToolBarConfig,
@@ -40,7 +41,10 @@ export class AffineKeyboardToolbar extends SignalWatcher(
) {
static override styles = keyboardToolbarStyles;
private readonly _expanded$ = signal(false);
/** This field records the panel static height same as the virtual keyboard height */
panelHeight$ = signal(0);
positionController = new PositionController(this);
get std() {
return this.rootComponent.std;
@@ -50,31 +54,9 @@ export class AffineKeyboardToolbar extends SignalWatcher(
return this._currentPanelIndex$.value !== -1;
}
private get panelHeight() {
return this._expanded$.value
? `${
this.keyboard.staticHeight$.value !== 0
? this.keyboard.staticHeight$.value
: 330
}px`
: this.keyboard.appTabSafeArea$.value;
}
/**
* Prevent flickering during keyboard opening
*/
private _resetPanelIndexTimeoutId: ReturnType<typeof setTimeout> | null =
null;
private readonly _closeToolPanel = () => {
this._currentPanelIndex$.value = -1;
if (!this.keyboard.visible$.peek()) this.keyboard.show();
if (this._resetPanelIndexTimeoutId) {
clearTimeout(this._resetPanelIndexTimeoutId);
this._resetPanelIndexTimeoutId = null;
}
this._resetPanelIndexTimeoutId = setTimeout(() => {
this._currentPanelIndex$.value = -1;
}, 100);
};
private readonly _currentPanelIndex$ = signal(-1);
@@ -101,10 +83,6 @@ export class AffineKeyboardToolbar extends SignalWatcher(
if (this._currentPanelIndex$.value === index) {
this._closeToolPanel();
} else {
if (this._resetPanelIndexTimeoutId) {
clearTimeout(this._resetPanelIndexTimeoutId);
this._resetPanelIndexTimeoutId = null;
}
this._currentPanelIndex$.value = index;
this.keyboard.hide();
this._scrollCurrentBlockIntoView();
@@ -145,6 +123,9 @@ export class AffineKeyboardToolbar extends SignalWatcher(
return {
std: this.std,
rootComponent: this.rootComponent,
closeToolbar: (blur = false) => {
this.close(blur);
},
closeToolPanel: () => {
this._closeToolPanel();
},
@@ -221,7 +202,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
}
private _renderItems() {
if (!this.std.event.active$.value)
if (document.activeElement !== this.rootComponent)
return html`<div class="item-container"></div>`;
const goPrevToolbarAction = when(
@@ -245,15 +226,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
<icon-button
size="36px"
@click=${() => {
if (this.keyboard.staticHeight$.value === 0) {
this._closeToolPanel();
return;
}
if (this.keyboard.visible$.peek()) {
this.keyboard.hide();
} else {
this.keyboard.show();
}
this.close(true);
}}
>
${KeyboardIcon()}
@@ -264,23 +237,6 @@ export class AffineKeyboardToolbar extends SignalWatcher(
override connectedCallback() {
super.connectedCallback();
// There are two cases that `_expanded$` will be true:
// 1. when virtual keyboard is opened, the panel need to be expanded and overlapped by the keyboard,
// so that the toolbar will be on the top of the keyboard.
// 2. the panel is opened, whether the keyboard is closed or not exists (e.g. a physical keyboard connected)
//
// There is one case that `_expanded$` will be false:
// 1. the panel is closed, and the keyboard is closed, the toolbar will be rendered at the bottom of the viewport
this._disposables.add(
effect(() => {
if (this.keyboard.visible$.value || this.panelOpened) {
this._expanded$.value = true;
} else {
this._expanded$.value = false;
}
})
);
// prevent editor blur when click item in toolbar
this.disposables.addFromEvent(this, 'pointerdown', e => {
e.preventDefault();
@@ -304,17 +260,15 @@ export class AffineKeyboardToolbar extends SignalWatcher(
if (this.keyboard.visible$.value) {
this._closeToolPanel();
}
// when keyboard is closed and the panel is not opened, we need to close the toolbar,
// this usually happens when user close keyboard from system side
else if (this.hasUpdated && untracked(() => !this.panelOpened)) {
this.close(true);
}
})
);
this._watchAutoShow();
this.disposables.add(() => {
if (this._resetPanelIndexTimeoutId) {
clearTimeout(this._resetPanelIndexTimeoutId);
this._resetPanelIndexTimeoutId = null;
}
});
}
private _watchAutoShow() {
@@ -377,10 +331,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
<affine-keyboard-tool-panel
.config=${this._currentPanelConfig}
.context=${this._context}
style=${styleMap({
height: this.panelHeight,
paddingBottom: this.keyboard.appTabSafeArea$.value,
})}
.height=${this.panelHeight$.value}
></affine-keyboard-tool-panel>
`;
}
@@ -388,6 +339,9 @@ export class AffineKeyboardToolbar extends SignalWatcher(
@property({ attribute: false })
accessor keyboard!: VirtualKeyboardProviderWithAction;
@property({ attribute: false })
accessor close: (blur: boolean) => void = () => {};
@property({ attribute: false })
accessor config!: KeyboardToolbarConfig;

View File

@@ -0,0 +1,42 @@
import { type VirtualKeyboardProvider } from '@blocksuite/affine-shared/services';
import { DisposableGroup } from '@blocksuite/global/disposable';
import type { BlockStdScope, ShadowlessElement } from '@blocksuite/std';
import { effect, type Signal } from '@preact/signals-core';
import type { ReactiveController, ReactiveControllerHost } from 'lit';
/**
* This controller is used to control the keyboard toolbar position
*/
export class PositionController implements ReactiveController {
private readonly _disposables = new DisposableGroup();
host: ReactiveControllerHost &
ShadowlessElement & {
std: BlockStdScope;
panelHeight$: Signal<number>;
keyboard: VirtualKeyboardProvider;
panelOpened: boolean;
};
constructor(host: PositionController['host']) {
(this.host = host).addController(this);
}
hostConnected() {
const { keyboard } = this.host;
this._disposables.add(
effect(() => {
if (keyboard.visible$.value) {
this.host.panelHeight$.value = keyboard.height$.value;
}
})
);
this.host.style.bottom = '0px';
}
hostDisconnected() {
this._disposables.dispose();
}
}

View File

@@ -7,7 +7,6 @@ export const keyboardToolbarStyles = css`
position: fixed;
display: block;
width: 100vw;
bottom: 0;
}
.keyboard-toolbar {
@@ -61,18 +60,14 @@ export const keyboardToolbarStyles = css`
export const keyboardToolPanelStyles = css`
affine-keyboard-tool-panel {
display: block;
overflow-y: auto;
box-sizing: border-box;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
}
.affine-keyboard-tool-panel-container {
display: flex;
flex-direction: column;
gap: 24px;
width: 100%;
padding: 16px 4px 8px 8px;
overflow-y: auto;
box-sizing: border-box;
background-color: ${unsafeCSSVarV2('layer/background/primary')};
}
${scrollbarStyle('affine-keyboard-tool-panel')}

View File

@@ -20,6 +20,18 @@ import {
export const AFFINE_KEYBOARD_TOOLBAR_WIDGET = 'affine-keyboard-toolbar-widget';
export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel> {
private readonly _close = (blur: boolean) => {
if (blur) {
if (document.activeElement === this._docTitle?.inlineEditorContainer) {
this._docTitle?.inlineEditor?.setInlineRange(null);
this._docTitle?.inlineEditor?.eventSource?.blur();
} else if (document.activeElement === this.block?.rootComponent) {
this.std.selection.clear();
}
}
this._show$.value = false;
};
private readonly _show$ = signal(false);
private _initialInputMode: string = '';
@@ -61,26 +73,29 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
override connectedCallback(): void {
super.connectedCallback();
this.disposables.add(
effect(() => {
this._show$.value = this.std.event.active$.value;
})
);
const rootComponent = this.block?.rootComponent;
if (rootComponent && this.keyboard.fallback) {
this._initialInputMode = rootComponent.inputMode;
this.disposables.add(() => {
rootComponent.inputMode = this._initialInputMode;
if (rootComponent) {
this.disposables.addFromEvent(rootComponent, 'focus', () => {
this._show$.value = true;
});
this.disposables.add(
effect(() => {
// recover input mode when keyboard toolbar is hidden
if (!this._show$.value) {
rootComponent.inputMode = this._initialInputMode;
}
})
);
this.disposables.addFromEvent(rootComponent, 'blur', () => {
this._show$.value = false;
});
if (this.keyboard.fallback) {
this._initialInputMode = rootComponent.inputMode;
this.disposables.add(() => {
rootComponent.inputMode = this._initialInputMode;
});
this.disposables.add(
effect(() => {
// recover input mode when keyboard toolbar is hidden
if (!this._show$.value) {
rootComponent.inputMode = this._initialInputMode;
}
})
);
}
}
if (this._docTitle) {
@@ -114,6 +129,7 @@ export class AffineKeyboardToolbarWidget extends WidgetComponent<RootBlockModel>
.keyboard=${this.keyboard}
.config=${this.config}
.rootComponent=${this.block.rootComponent}
.close=${this._close}
></affine-keyboard-toolbar>`}
></blocksuite-portal>`;
}

View File

@@ -113,9 +113,11 @@ export class LinkedDocPopover extends SignalWatcher(
}
private get _flattenActionList() {
return this._actionGroup.flatMap(group =>
group.items.map(item => ({ ...item, groupName: group.name }))
);
return this._actionGroup
.map(group =>
group.items.map(item => ({ ...item, groupName: group.name }))
)
.flat();
}
private get _query() {

View File

@@ -65,98 +65,6 @@ export class Unzip {
this.unzipped = fflate.unzipSync(new Uint8Array(await blob.arrayBuffer()));
}
private fixFileNameEncoding(fileName: string): string {
try {
// check if contains non-ASCII characters
if (fileName.split('').some(char => char.charCodeAt(0) > 127)) {
// try different encodings
const fixedName = this.tryDifferentEncodings(fileName);
if (fixedName && fixedName !== fileName) {
return fixedName;
}
}
return fileName;
} catch {
return fileName;
}
}
// try different encodings
private tryDifferentEncodings(fileName: string): string | null {
try {
// convert string to bytes
const bytes = new Uint8Array(fileName.length);
for (let i = 0; i < fileName.length; i++) {
bytes[i] = fileName.charCodeAt(i);
}
// try different encodings
// The macOS system zip tool creates archives with UTF-8 encoded filenames.
// However, this implementation doesn't strictly adhere to the ZIP specification.
// Simply forcing UTF-8 encoding when unzipping should resolve filename corruption issues.
const encodings = ['utf-8'];
for (const encoding of encodings) {
try {
const decoder = new TextDecoder(encoding);
const result = decoder.decode(bytes);
// check if decoded result is valid
if (result && this.isValidDecodedString(result)) {
return result;
}
} catch {
// ignore encoding error, try next encoding
}
}
} catch {
// ignore conversion error
}
return null;
}
// check if decoded string is valid
private isValidDecodedString(str: string): boolean {
// check if contains control characters
const controlCharCodes = new Set([
0x00,
0x01,
0x02,
0x03,
0x04,
0x05,
0x06,
0x07,
0x08, // \x00-\x08
0x0b,
0x0c, // \x0B, \x0C
0x0e,
0x0f,
0x10,
0x11,
0x12,
0x13,
0x14,
0x15,
0x16,
0x17,
0x18,
0x19,
0x1a,
0x1b,
0x1c,
0x1d,
0x1e,
0x1f, // \x0E-\x1F
0x7f, // \x7F
]);
return !str
.split('')
.some(char => controlCharCodes.has(char.charCodeAt(0)));
}
*[Symbol.iterator]() {
const keys = Object.keys(this.unzipped ?? {});
let index = 0;
@@ -173,10 +81,7 @@ export class Unzip {
const content = new File([this.unzipped![path]], fileName, {
type: mime ?? '',
}) as Blob;
const fixedPath = this.fixFileNameEncoding(path);
yield { path: fixedPath, content, index };
yield { path, content, index };
index++;
}
}

View File

@@ -142,13 +142,15 @@ export class SlashMenu extends WithDisposable(LitElement) {
// We search first and second layer
if (this._filteredItems.length !== 0 && depth >= 1) break;
queue = queue.flatMap(item => {
if (isSubMenuItem(item)) {
return item.subMenu;
} else {
return [];
}
});
queue = queue
.map<typeof queue>(item => {
if (isSubMenuItem(item)) {
return item.subMenu;
} else {
return [];
}
})
.flat();
depth++;
}

View File

@@ -418,9 +418,9 @@ export class AffineToolbarWidget extends WidgetComponent {
return;
}
const elementIds = selections.flatMap(s =>
s.editing || s.inoperable ? [] : s.elements
);
const elementIds = selections
.map(s => (s.editing || s.inoperable ? [] : s.elements))
.flat();
const count = elementIds.length;
const activated = context.activated && Boolean(count);

View File

@@ -229,7 +229,8 @@ export function renderToolbar(
? module.config.when(context)
: (module.config.when ?? true)
)
.flatMap(module => module.config.actions);
.map<ToolbarActions>(module => module.config.actions)
.flat();
const combined = combine(actions, context);

View File

@@ -91,11 +91,15 @@ export class KeyboardControl {
const disposables = new DisposableGroup();
if (IS_ANDROID) {
disposables.add(
this._dispatcher.add('beforeInput', ctx => {
if (this.composition) return false;
const binding = androidBindKeymapPatch(keymap);
return binding(ctx);
})
this._dispatcher.add(
'beforeInput',
ctx => {
if (this.composition) return false;
const binding = androidBindKeymapPatch(keymap);
return binding(ctx);
},
options
)
);
}

View File

@@ -226,18 +226,6 @@ export class UIEventDispatcher extends LifeCycleWatcher {
this._setActive(false);
}
});
// When the selection is outside the host, the event dispatcher should be inactive
this.disposables.addFromEvent(document, 'selectionchange', () => {
const sel = document.getSelection();
if (!sel || sel.rangeCount === 0) return;
const { anchorNode, focusNode } = sel;
if (
(anchorNode && !this.host.contains(anchorNode)) ||
(focusNode && !this.host.contains(focusNode))
) {
this._setActive(false);
}
});
}
private _buildEventScopeBySelection(name: EventName) {

View File

@@ -104,7 +104,7 @@ export function bindKeymap(
};
}
// In some IME of Android like, the keypress event dose not contain
// In Android, the keypress event dose not contain
// the information about what key is pressed. See
// https://stackoverflow.com/a/68188679
// https://stackoverflow.com/a/66724830

View File

@@ -57,7 +57,7 @@ export type CanvasLayer = BaseLayer<GfxPrimitiveElementModel> & {
type: 'canvas';
/**
* The z-index of the first element in this canvas layer.
* The z-index of canvas layer.
*
* A canvas layer renders all the elements in a single canvas,
* this property is used to render the canvas with correct z-index.
@@ -165,7 +165,8 @@ export class LayerManager extends GfxExtension {
];
curLayer.zIndex = currentCSSZindex;
layers.push(curLayer as LayerManager['layers'][number]);
currentCSSZindex += curLayer.elements.length;
currentCSSZindex +=
curLayer.type === 'block' ? curLayer.elements.length : 1;
}
};
const addLayer = (type: 'canvas' | 'block') => {

View File

@@ -1,4 +1,3 @@
import { IS_ANDROID } from '@blocksuite/global/env';
import type { BaseTextAttributes } from '@blocksuite/store';
import type { InlineEditor } from '../inline-editor.js';
@@ -42,10 +41,11 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
}
};
private readonly _onBeforeInput = async (event: InputEvent) => {
private readonly _onBeforeInput = (event: InputEvent) => {
const range = this.editor.rangeService.getNativeRange();
if (
this.editor.isReadonly ||
this._isComposing ||
!range ||
!this._isRangeCompletelyInRoot(range)
)
@@ -54,29 +54,33 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
let inlineRange = this.editor.toInlineRange(range);
if (!inlineRange) return;
if (this._isComposing) {
if (IS_ANDROID && event.inputType === 'insertCompositionText') {
this._compositionInlineRange = inlineRange;
}
return;
}
let ifHandleTargetRange = true;
if (
event.inputType.startsWith('delete') &&
(isInEmbedGap(range.commonAncestorContainer) ||
if (event.inputType.startsWith('delete')) {
if (
isInEmbedGap(range.commonAncestorContainer) &&
inlineRange.length === 0 &&
inlineRange.index > 0
) {
inlineRange = {
index: inlineRange.index - 1,
length: 1,
};
ifHandleTargetRange = false;
} else if (
isInEmptyLine(range.commonAncestorContainer) &&
inlineRange.length === 0 &&
inlineRange.index > 0
// eslint-disable-next-line sonarjs/no-duplicated-branches
) {
// do not use target range when deleting across lines
// https://github.com/toeverything/blocksuite/issues/5381
isInEmptyLine(range.commonAncestorContainer)) &&
inlineRange.length === 0 &&
inlineRange.index > 0
) {
// do not use target range when deleting across lines
inlineRange = {
index: inlineRange.index - 1,
length: 1,
};
ifHandleTargetRange = false;
inlineRange = {
index: inlineRange.index - 1,
length: 1,
};
ifHandleTargetRange = false;
}
}
if (ifHandleTargetRange) {
@@ -93,24 +97,11 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
}
}
}
if (!inlineRange) return;
event.preventDefault();
if (IS_ANDROID) {
this.editor.rerenderWholeEditor();
await this.editor.waitForUpdate();
if (
event.inputType === 'deleteContentBackward' &&
!(inlineRange.index === 0 && inlineRange.length === 0)
) {
// when press backspace at offset 1, double characters will be removed.
// because we mock backspace key event `androidBindKeymapPatch` in blocksuite/framework/std/src/event/keymap.ts
// so we need to stop the event propagation to prevent the double characters removal.
event.stopPropagation();
}
}
const ctx: BeforeinputHookCtx<TextAttributes> = {
inlineEditor: this.editor,
raw: event,
@@ -355,9 +346,11 @@ export class EventService<TextAttributes extends BaseTextAttributes> {
return;
}
this.editor.disposables.addFromEvent(eventSource, 'beforeinput', e => {
this._onBeforeInput(e).catch(console.error);
});
this.editor.disposables.addFromEvent(
eventSource,
'beforeinput',
this._onBeforeInput
);
this.editor.disposables.addFromEvent(
eventSource,
'compositionstart',

View File

@@ -12,7 +12,11 @@ import type { SurfaceBlockModel } from '../gfx/model/surface/surface-model.js';
export function getLayerEndZIndex(layers: Layer[], layerIndex: number) {
const layer = layers[layerIndex];
return layer ? layer.zIndex + layer.elements.length - 1 : 0;
return layer
? layer.type === 'block'
? layer.zIndex + layer.elements.length - 1
: layer.zIndex
: 0;
}
export function updateLayersZIndex(layers: Layer[], startIdx: number) {
@@ -23,7 +27,7 @@ export function updateLayersZIndex(layers: Layer[], startIdx: number) {
const curLayer = layers[i];
curLayer.zIndex = curIndex;
curIndex += curLayer.elements.length;
curIndex += curLayer.type === 'block' ? curLayer.elements.length : 1;
}
}

View File

@@ -200,7 +200,7 @@ test('layer zindex should update correctly when elements changed', async () => {
expect(service.layer.layers[1].zIndex).toBe(3);
expect(service.layer.layers[2].type).toBe('block');
expect(service.layer.layers[2].zIndex).toBe(5);
expect(service.layer.layers[2].zIndex).toBe(4);
};
assert2StepState();

View File

@@ -9,7 +9,6 @@
"**/node_modules",
".yarn",
".github/helm",
".git",
".vscode",
".yarnrc.yml",
".docker",
@@ -160,7 +159,6 @@
}
],
"unicorn/prefer-array-some": "error",
"unicorn/prefer-array-flat-map": "off",
"unicorn/no-useless-promise-resolve-reject": "error",
"unicorn/no-unnecessary-await": "error",
"unicorn/no-useless-fallback-in-spread": "error",

View File

@@ -82,7 +82,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.0.0",
"msw": "^2.6.8",
"oxlint": "^1.15.0",
"oxlint": "^1.1.0",
"prettier": "^3.4.2",
"semver": "^7.6.3",
"serve": "^14.2.4",
@@ -135,7 +135,6 @@
"object.fromentries": "npm:@nolyfill/object.fromentries@^1",
"object.hasown": "npm:@nolyfill/object.hasown@^1",
"object.values": "npm:@nolyfill/object.values@^1",
"on-headers": "npm:on-headers@^1.1.0",
"reflect.getprototypeof": "npm:@nolyfill/reflect.getprototypeof@^1",
"regexp.prototype.flags": "npm:@nolyfill/regexp.prototype.flags@^1",
"safe-array-concat": "npm:@nolyfill/safe-array-concat@^1",

View File

@@ -1,7 +1,5 @@
use std::collections::HashSet;
use tiktoken_rs::{get_bpe_from_tokenizer, tokenizer::Tokenizer as TiktokenTokenizer};
#[napi]
pub struct Tokenizer {
inner: tiktoken_rs::CoreBPE,
@@ -9,10 +7,6 @@ pub struct Tokenizer {
#[napi]
pub fn from_model_name(model_name: String) -> Option<Tokenizer> {
if model_name.starts_with("gpt-5") {
let bpe = get_bpe_from_tokenizer(TiktokenTokenizer::O200kBase).ok()?;
return Some(Tokenizer { inner: bpe });
}
let bpe = tiktoken_rs::get_bpe_from_model(&model_name).ok()?;
Some(Tokenizer { inner: bpe })
}
@@ -37,7 +31,7 @@ mod tests {
#[test]
fn test_tokenizer() {
let tokenizer = from_model_name("gpt-5".to_string()).unwrap();
let tokenizer = from_model_name("gpt-4.1".to_string()).unwrap();
let content = "Hello, world!";
let count = tokenizer.count(content.to_string(), None);
assert!(count > 0);

View File

@@ -1,37 +0,0 @@
-- CreateTable
/*
Warnings:
- The primary key for the `ai_workspace_embeddings` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The primary key for the `ai_workspace_file_embeddings` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'ai_workspace_embeddings') AND
EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'ai_workspace_file_embeddings') THEN
CREATE TABLE "ai_workspace_blob_embeddings" (
"workspace_id" VARCHAR NOT NULL,
"blob_id" VARCHAR NOT NULL,
"chunk" INTEGER NOT NULL,
"content" VARCHAR NOT NULL,
"embedding" vector(1024) NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ai_workspace_blob_embeddings_pkey" PRIMARY KEY ("workspace_id","blob_id","chunk")
);
-- CreateIndex
CREATE INDEX "ai_workspace_blob_embeddings_idx" ON "ai_workspace_blob_embeddings"
USING hnsw (embedding vector_cosine_ops);
-- AddForeignKey
ALTER TABLE "ai_workspace_blob_embeddings"
ADD CONSTRAINT "ai_workspace_blob_embeddings_workspace_id_blob_id_fkey"
FOREIGN KEY ("workspace_id", "blob_id")
REFERENCES "blobs"("workspace_id", "key")
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END
$$;

View File

@@ -1,20 +0,0 @@
-- CreateTable
CREATE TABLE "access_tokens" (
"id" VARCHAR NOT NULL,
"name" VARCHAR NOT NULL,
"token" VARCHAR NOT NULL,
"user_id" VARCHAR NOT NULL,
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMPTZ(3),
CONSTRAINT "access_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "access_tokens_token_key" ON "access_tokens"("token");
-- CreateIndex
CREATE INDEX "access_tokens_user_id_idx" ON "access_tokens"("user_id");
-- AddForeignKey
ALTER TABLE "access_tokens" ADD CONSTRAINT "access_tokens_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "workspaces" ADD COLUMN "last_check_embeddings" TIMESTAMPTZ(3) NOT NULL DEFAULT '1970-01-01 00:00:00 +00:00';
-- CreateIndex
CREATE INDEX "workspaces_last_check_embeddings_idx" ON "workspaces"("last_check_embeddings");

View File

@@ -28,19 +28,18 @@
"dependencies": {
"@affine/reader": "workspace:*",
"@affine/server-native": "workspace:*",
"@ai-sdk/anthropic": "^2.0.1",
"@ai-sdk/google": "^2.0.4",
"@ai-sdk/google-vertex": "^3.0.5",
"@ai-sdk/openai": "^2.0.10",
"@ai-sdk/openai-compatible": "^1.0.5",
"@ai-sdk/perplexity": "^2.0.1",
"@ai-sdk/anthropic": "^1.2.12",
"@ai-sdk/google": "^1.2.18",
"@ai-sdk/google-vertex": "^2.2.23",
"@ai-sdk/openai": "^1.3.22",
"@ai-sdk/openai-compatible": "^0.2.14",
"@ai-sdk/perplexity": "^1.1.9",
"@apollo/server": "^4.11.3",
"@aws-sdk/client-s3": "^3.779.0",
"@aws-sdk/s3-request-presigner": "^3.779.0",
"@fal-ai/serverless-client": "^0.15.0",
"@google-cloud/opentelemetry-cloud-trace-exporter": "^2.4.1",
"@google-cloud/opentelemetry-resource-util": "^2.4.0",
"@modelcontextprotocol/sdk": "^1.16.0",
"@nestjs-cls/transactional": "^2.6.1",
"@nestjs-cls/transactional-adapter-prisma": "^1.2.19",
"@nestjs/apollo": "^13.0.4",
@@ -75,7 +74,7 @@
"@prisma/instrumentation": "^6.7.0",
"@react-email/components": "0.0.38",
"@socket.io/redis-adapter": "^8.3.0",
"ai": "^5.0.10",
"ai": "^4.3.4",
"bullmq": "^5.40.2",
"cookie-parser": "^1.4.7",
"cross-env": "^7.0.3",
@@ -86,7 +85,6 @@
"express": "^5.0.1",
"fast-xml-parser": "^5.0.0",
"get-stream": "^9.0.1",
"google-auth-library": "^10.2.0",
"graphql": "^16.9.0",
"graphql-scalars": "^1.24.0",
"graphql-upload": "^17.0.0",
@@ -105,7 +103,7 @@
"nest-winston": "^1.9.7",
"nestjs-cls": "^6.0.0",
"nodemailer": "^7.0.0",
"on-headers": "^1.1.0",
"on-headers": "^1.0.2",
"piscina": "^5.0.0-alpha.0",
"prisma": "^6.6.0",
"react": "19.1.0",

View File

@@ -49,7 +49,6 @@ model User {
comments Comment[]
replies Reply[]
commentAttachments CommentAttachment[] @relation("createdCommentAttachments")
AccessToken AccessToken[]
@@index([email])
@@map("users")
@@ -111,18 +110,17 @@ model VerificationToken {
model Workspace {
// NOTE: manually set this column type to identity in migration file
sid Int @unique @default(autoincrement())
id String @id @default(uuid()) @db.VarChar
public Boolean
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
sid Int @unique @default(autoincrement())
id String @id @default(uuid()) @db.VarChar
public Boolean
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
// workspace level feature flags
enableAi Boolean @default(true) @map("enable_ai")
enableUrlPreview Boolean @default(false) @map("enable_url_preview")
enableDocEmbedding Boolean @default(true) @map("enable_doc_embedding")
name String? @db.VarChar
avatarKey String? @map("avatar_key") @db.VarChar
indexed Boolean @default(false)
lastCheckEmbeddings DateTime @default("1970-01-01T00:00:00-00:00") @map("last_check_embeddings") @db.Timestamptz(3)
enableAi Boolean @default(true) @map("enable_ai")
enableUrlPreview Boolean @default(false) @map("enable_url_preview")
enableDocEmbedding Boolean @default(true) @map("enable_doc_embedding")
name String? @db.VarChar
avatarKey String? @map("avatar_key") @db.VarChar
indexed Boolean @default(false)
features WorkspaceFeature[]
docs WorkspaceDoc[]
@@ -134,7 +132,6 @@ model Workspace {
comments Comment[]
commentAttachments CommentAttachment[]
@@index([lastCheckEmbeddings])
@@map("workspaces")
}
@@ -571,23 +568,6 @@ model AiWorkspaceFileEmbedding {
@@map("ai_workspace_file_embeddings")
}
model AiWorkspaceBlobEmbedding {
workspaceId String @map("workspace_id") @db.VarChar
blobId String @map("blob_id") @db.VarChar
// a file can be divided into multiple chunks and embedded separately.
chunk Int @db.Integer
content String @db.VarChar
embedding Unsupported("vector(1024)")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
blob Blob @relation(fields: [workspaceId, blobId], references: [workspaceId, key], onDelete: Cascade)
@@id([workspaceId, blobId, chunk])
@@index([embedding], map: "ai_workspace_blob_embeddings_idx")
@@map("ai_workspace_blob_embeddings")
}
enum AiJobStatus {
pending
running
@@ -827,8 +807,7 @@ model Blob {
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
AiWorkspaceBlobEmbedding AiWorkspaceBlobEmbedding[]
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@id([workspaceId, key])
@@map("blobs")
@@ -952,17 +931,3 @@ model CommentAttachment {
@@id([workspaceId, docId, key])
@@map("comment_attachments")
}
model AccessToken {
id String @id @default(uuid()) @db.VarChar
name String @db.VarChar
token String @unique @db.VarChar
userId String @map("user_id") @db.VarChar
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
expiresAt DateTime? @map("expires_at") @db.Timestamptz(3)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@map("access_tokens")
}

View File

@@ -96,21 +96,6 @@ test('should be able to visit private api if signed in', async t => {
t.is(res.body.user.id, u1.id);
});
test('should be able to visit private api with access token', async t => {
const models = t.context.app.get(Models);
const token = await models.accessToken.create({
userId: u1.id,
name: 'test',
});
const res = await request(server)
.get('/private')
.set('Authorization', `Bearer ${token.token}`)
.expect(HttpStatus.OK);
t.is(res.body.user.id, u1.id);
});
test('should be able to parse session cookie', async t => {
const spy = Sinon.spy(auth, 'getUserSession');
await request(server)

View File

@@ -1,5 +1,3 @@
import { randomUUID } from 'node:crypto';
import type { ExecutionContext, TestFn } from 'ava';
import ava from 'ava';
import { z } from 'zod';
@@ -7,7 +5,6 @@ import { z } from 'zod';
import { ServerFeature, ServerService } from '../core';
import { AuthService } from '../core/auth';
import { QuotaModule } from '../core/quota';
import { Models } from '../models';
import { CopilotModule } from '../plugins/copilot';
import { prompts, PromptService } from '../plugins/copilot/prompt';
import {
@@ -33,8 +30,6 @@ import { TestAssets } from './utils/copilot';
type Tester = {
auth: AuthService;
module: TestingModule;
models: Models;
service: ServerService;
prompt: PromptService;
factory: CopilotProviderFactory;
workflow: CopilotWorkflowService;
@@ -71,15 +66,12 @@ test.serial.before(async t => {
isCopilotConfigured = service.features.includes(ServerFeature.Copilot);
const auth = module.get(AuthService);
const models = module.get(Models);
const prompt = module.get(PromptService);
const factory = module.get(CopilotProviderFactory);
const workflow = module.get(CopilotWorkflowService);
t.context.module = module;
t.context.auth = auth;
t.context.service = service;
t.context.models = models;
t.context.prompt = prompt;
t.context.factory = factory;
t.context.workflow = workflow;
@@ -92,7 +84,7 @@ test.serial.before(async t => {
});
test.serial.before(async t => {
const { prompt, executors, models, service } = t.context;
const { prompt, executors } = t.context;
executors.image.register();
executors.text.register();
@@ -106,28 +98,6 @@ test.serial.before(async t => {
for (const p of prompts) {
await prompt.set(p.name, p.model, p.messages, p.config);
}
const user = await models.user.create({
email: `${randomUUID()}@affine.pro`,
});
await service.updateConfig(user.id, [
{
module: 'copilot',
key: 'scenarios',
value: {
enabled: true,
scenarios: {
image: 'flux-1/schnell',
rerank: 'gpt-5-mini',
complex_text_generation: 'gpt-5-mini',
coding: 'gpt-5-mini',
quick_decision_making: 'gpt-5-mini',
quick_text_generation: 'gpt-5-mini',
polish_and_summarize: 'gemini-2.5-flash',
},
},
},
]);
});
test.after(async t => {
@@ -414,12 +384,12 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
role: 'user' as const,
content: 'what is ssot',
params: {
docs: [
files: [
{
docId: 'SSOT',
docTitle: 'Single source of truth - Wikipedia',
blobId: 'SSOT',
fileName: 'Single source of truth - Wikipedia',
fileType: 'text/markdown',
docContent: TestAssets.SSOT,
fileContent: TestAssets.SSOT,
},
],
},
@@ -560,8 +530,9 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
'Create headings',
'Make it longer',
'Make it shorter',
'Section Edit',
'Continue writing',
'Chat With AFFiNE AI',
'Search With AFFiNE AI',
],
messages: [{ role: 'user' as const, content: TestAssets.SSOT }],
verifier: (t: ExecutionContext<Tester>, result: string) => {
@@ -576,18 +547,9 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
},
type: 'text' as const,
},
{
promptName: ['Continue writing'],
messages: [{ role: 'user' as const, content: TestAssets.AFFiNE }],
verifier: (t: ExecutionContext<Tester>, result: string) => {
assertNotWrappedInCodeBlock(t, result);
t.assert(result.length > 0, 'should not be empty');
},
type: 'text' as const,
},
{
promptName: ['Brainstorm ideas about this', 'Brainstorm mindmap'],
messages: [{ role: 'user' as const, content: TestAssets.AFFiNE }],
messages: [{ role: 'user' as const, content: TestAssets.SSOT }],
verifier: (t: ExecutionContext<Tester>, result: string) => {
assertNotWrappedInCodeBlock(t, result);
t.assert(checkMDList(result), 'should be a markdown list');
@@ -684,7 +646,20 @@ The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Ca
type: 'image' as const,
},
{
promptName: ['Generate image'],
promptName: ['debug:action:dalle3'],
messages: [
{
role: 'user' as const,
content: 'Panda',
},
],
verifier: (t: ExecutionContext<Tester>, link: string) => {
t.truthy(checkUrl(link), 'should be a valid url');
},
type: 'image' as const,
},
{
promptName: ['debug:action:gpt-image-1'],
messages: [
{
role: 'user' as const,
@@ -732,7 +707,7 @@ for (const {
[
...prompt.finish(
messages.reduce(
// @ts-expect-error params not typed
// @ts-expect-error
(acc, m) => Object.assign(acc, m.params),
{}
)
@@ -802,7 +777,7 @@ for (const {
[
...prompt.finish(
finalMessage.reduce(
// @ts-expect-error params not typed
// @ts-expect-error
(acc, m) => Object.assign(acc, m.params),
params
)

View File

@@ -111,7 +111,7 @@ test.before(async t => {
m.overrideProvider(OpenAIProvider).useClass(MockCopilotProvider);
m.overrideProvider(GeminiGenerativeProvider).useClass(
class MockGenerativeProvider extends MockCopilotProvider {
// @ts-expect-error type not typed
// @ts-expect-error
override type: CopilotProviderType = CopilotProviderType.Gemini;
}
);

View File

@@ -5,14 +5,12 @@ import { ProjectRoot } from '@affine-tools/utils/path';
import { PrismaClient } from '@prisma/client';
import type { TestFn } from 'ava';
import ava from 'ava';
import { nanoid } from 'nanoid';
import Sinon from 'sinon';
import { EventBus, JobQueue } from '../base';
import { ConfigModule } from '../base/config';
import { AuthService } from '../core/auth';
import { QuotaModule } from '../core/quota';
import { StorageModule, WorkspaceBlobStorage } from '../core/storage';
import {
ContextCategories,
CopilotSessionModel,
@@ -70,7 +68,6 @@ type Context = {
db: PrismaClient;
event: EventBus;
workspace: WorkspaceModel;
workspaceStorage: WorkspaceBlobStorage;
copilotSession: CopilotSessionModel;
context: CopilotContextService;
prompt: PromptService;
@@ -117,7 +114,6 @@ test.before(async t => {
},
}),
QuotaModule,
StorageModule,
CopilotModule,
],
tapModule: builder => {
@@ -131,7 +127,6 @@ test.before(async t => {
const db = module.get(PrismaClient);
const event = module.get(EventBus);
const workspace = module.get(WorkspaceModel);
const workspaceStorage = module.get(WorkspaceBlobStorage);
const copilotSession = module.get(CopilotSessionModel);
const prompt = module.get(PromptService);
const factory = module.get(CopilotProviderFactory);
@@ -151,7 +146,6 @@ test.before(async t => {
t.context.db = db;
t.context.event = event;
t.context.workspace = workspace;
t.context.workspaceStorage = workspaceStorage;
t.context.copilotSession = copilotSession;
t.context.prompt = prompt;
t.context.factory = factory;
@@ -212,9 +206,7 @@ test('should be able to manage prompt', async t => {
'should have two messages'
);
await prompt.update(promptName, {
messages: [{ role: 'system', content: 'hello' }],
});
await prompt.update(promptName, [{ role: 'system', content: 'hello' }]);
t.is(
(await prompt.get(promptName))!.finish({}).length,
1,
@@ -373,7 +365,7 @@ test('should be able to update chat session prompt', async t => {
// Update the session
const updatedSessionId = await session.update({
sessionId,
promptName: 'Chat With AFFiNE AI',
promptName: 'Search With AFFiNE AI',
userId,
});
t.is(updatedSessionId, sessionId, 'should update session with same id');
@@ -383,7 +375,7 @@ test('should be able to update chat session prompt', async t => {
t.truthy(updatedSession, 'should retrieve updated session');
t.is(
updatedSession?.config.promptName,
'Chat With AFFiNE AI',
'Search With AFFiNE AI',
'should have updated prompt name'
);
});
@@ -412,7 +404,7 @@ test('should be able to fork chat session', async t => {
// fork session
const s1 = (await session.get(sessionId))!;
// @ts-expect-error find maybe return undefined
// @ts-expect-error
const latestMessageId = s1.finish({}).find(m => m.role === 'assistant')!.id;
const forkedSessionId1 = await session.fork({
userId,
@@ -1341,16 +1333,16 @@ test('TextStreamParser should format different types of chunks correctly', t =>
textDelta: {
chunk: {
type: 'text-delta' as const,
text: 'Hello world',
},
textDelta: 'Hello world',
} as any,
expected: 'Hello world',
description: 'should format text-delta correctly',
},
reasoning: {
chunk: {
type: 'reasoning-delta' as const,
text: 'I need to think about this',
},
type: 'reasoning' as const,
textDelta: 'I need to think about this',
} as any,
expected: '\n> [!]\n> I need to think about this',
description: 'should format reasoning as callout',
},
@@ -1359,8 +1351,8 @@ test('TextStreamParser should format different types of chunks correctly', t =>
type: 'tool-call' as const,
toolName: 'web_search_exa' as const,
toolCallId: 'test-id-1',
input: { query: 'test query', mode: 'AUTO' as const },
},
args: { query: 'test query', mode: 'AUTO' as const },
} as any,
expected: '\n> [!]\n> \n> Searching the web "test query"\n> ',
description: 'should format web search tool call correctly',
},
@@ -1369,8 +1361,8 @@ test('TextStreamParser should format different types of chunks correctly', t =>
type: 'tool-call' as const,
toolName: 'web_crawl_exa' as const,
toolCallId: 'test-id-2',
input: { url: 'https://example.com' },
},
args: { url: 'https://example.com' },
} as any,
expected: '\n> [!]\n> \n> Crawling the web "https://example.com"\n> ',
description: 'should format web crawl tool call correctly',
},
@@ -1379,8 +1371,8 @@ test('TextStreamParser should format different types of chunks correctly', t =>
type: 'tool-result' as const,
toolName: 'web_search_exa' as const,
toolCallId: 'test-id-1',
input: { query: 'test query', mode: 'AUTO' as const },
output: [
args: { query: 'test query', mode: 'AUTO' as const },
result: [
{
title: 'Test Title',
url: 'https://test.com',
@@ -1407,7 +1399,7 @@ test('TextStreamParser should format different types of chunks correctly', t =>
chunk: {
type: 'error' as const,
error: { type: 'testError', message: 'Test error message' },
},
} as any,
errorMessage: 'Test error message',
description: 'should throw error for error chunks',
},
@@ -1437,85 +1429,78 @@ test('TextStreamParser should process a sequence of message chunks', t => {
chunks: [
// Reasoning chunks
{
id: nanoid(),
type: 'reasoning-delta' as const,
text: 'The user is asking about',
},
type: 'reasoning' as const,
textDelta: 'The user is asking about',
} as any,
{
id: nanoid(),
type: 'reasoning-delta' as const,
text: ' recent advances in quantum computing',
},
type: 'reasoning' as const,
textDelta: ' recent advances in quantum computing',
} as any,
{
id: nanoid(),
type: 'reasoning-delta' as const,
text: ' and how it might impact',
},
type: 'reasoning' as const,
textDelta: ' and how it might impact',
} as any,
{
id: nanoid(),
type: 'reasoning-delta' as const,
text: ' cryptography and data security.',
},
type: 'reasoning' as const,
textDelta: ' cryptography and data security.',
} as any,
{
id: nanoid(),
type: 'reasoning-delta' as const,
text: ' I should provide information on quantum supremacy achievements',
},
type: 'reasoning' as const,
textDelta:
' I should provide information on quantum supremacy achievements',
} as any,
// Text delta
{
id: nanoid(),
type: 'text-delta' as const,
text: 'Let me search for the latest breakthroughs in quantum computing and their ',
},
textDelta:
'Let me search for the latest breakthroughs in quantum computing and their ',
} as any,
// Tool call
{
type: 'tool-call' as const,
toolCallId: 'toolu_01ABCxyz123456789',
toolName: 'web_search_exa' as const,
input: {
args: {
query: 'latest quantum computing breakthroughs cryptography impact',
},
},
} as any,
// Tool result
{
type: 'tool-result' as const,
toolCallId: 'toolu_01ABCxyz123456789',
toolName: 'web_search_exa' as const,
input: {
args: {
query: 'latest quantum computing breakthroughs cryptography impact',
},
output: [
result: [
{
title: 'IBM Unveils 1000-Qubit Quantum Processor',
url: 'https://example.com/tech/quantum-computing-milestone',
},
],
},
} as any,
// More text deltas
{
id: nanoid(),
type: 'text-delta' as const,
text: 'implications for security.',
},
textDelta: 'implications for security.',
} as any,
{
id: nanoid(),
type: 'text-delta' as const,
text: '\n\nQuantum computing has made ',
},
textDelta: '\n\nQuantum computing has made ',
} as any,
{
id: nanoid(),
type: 'text-delta' as const,
text: 'remarkable progress in the past year. ',
},
textDelta: 'remarkable progress in the past year. ',
} as any,
{
id: nanoid(),
type: 'text-delta' as const,
text: 'The development of more stable qubits has accelerated research significantly.',
},
textDelta:
'The development of more stable qubits has accelerated research significantly.',
} as any,
],
expected:
'\n> [!]\n> The user is asking about recent advances in quantum computing and how it might impact cryptography and data security. I should provide information on quantum supremacy achievements\n\nLet me search for the latest breakthroughs in quantum computing and their \n> [!]\n> \n> Searching the web "latest quantum computing breakthroughs cryptography impact"\n> \n> \n> \n> [IBM Unveils 1000-Qubit Quantum Processor](https://example.com/tech/quantum-computing-milestone)\n> \n> \n> \n\nimplications for security.\n\nQuantum computing has made remarkable progress in the past year. The development of more stable qubits has accelerated research significantly.',
@@ -1535,25 +1520,14 @@ test('TextStreamParser should process a sequence of message chunks', t => {
// ==================== context ====================
test('should be able to manage context', async t => {
const {
context,
event,
jobs,
prompt,
session,
storage,
workspace,
workspaceStorage,
} = t.context;
const ws = await workspace.create(userId);
const { context, prompt, session, event, jobs, storage } = t.context;
await prompt.set(promptName, 'model', [
{ role: 'system', content: 'hello {{word}}' },
]);
const chatSession = await session.create({
docId: 'test',
workspaceId: ws.id,
workspaceId: 'test',
userId,
promptName,
pinned: false,
@@ -1634,24 +1608,6 @@ test('should be able to manage context', async t => {
t.is(result[0].fileId, file.id, 'should match file id');
}
// blob record
{
const blobId = 'test-blob';
await workspaceStorage.put(session.workspaceId, blobId, buffer);
await jobs.embedPendingBlob({ workspaceId: session.workspaceId, blobId });
const result = await t.context.context.matchWorkspaceBlobs(
session.workspaceId,
'test',
1,
undefined,
1
);
t.is(result.length, 1, 'should match blob embedding');
t.is(result[0].blobId, blobId, 'should match blob id');
}
// doc record
const addDoc = async () => {

View File

@@ -13,45 +13,74 @@ Generated by [AVA](https://avajs.dev).
# You own your data, with no compromises␊
## Local-first & Real-time collaborative␊
We love the idea proposed by Ink & Switch in the famous article about you owning your data, despite the cloud. Furthermore, AFFiNE is the first all-in-one workspace that keeps your data ownership with no compromises on real-time collaboration and editing experience.␊
AFFiNE is a local-first application upon CRDTs with real-time collaboration support. Your data is always stored locally while multiple nodes remain synced in real-time.␊
### Blocks that assemble your next docs, tasks kanban or whiteboard␊
There is a large overlap of their atomic "building blocks" between these apps. They are neither open source nor have a plugin system like VS Code for contributors to customize. We want to have something that contains all the features we love and goes one step further.␊
We are building AFFiNE to be a fundamental open source platform that contains all the building blocks for docs, task management and visual collaboration, hoping you can shape your next workflow with us that can make your life better and also connect others, too.␊
If you want to learn more about the product design of AFFiNE, here goes the concepts:␊
To Shape, not to adapt. AFFiNE is built for individuals & teams who care about their data, who refuse vendor lock-in, and who want to have control over their essential tools.␊
## A true canvas for blocks in any form␊
[Many editor apps](http://notion.so) claimed to be a canvas for productivity. Since _the Mother of All Demos,_ Douglas Engelbart, a creative and programable digital workspace has been a pursuit and an ultimate mission for generations of tool makers.␊
"We shape our tools and thereafter our tools shape us”. A lot of pioneers have inspired us a long the way, e.g.:␊
* Quip & Notion with their great concept of "everything is a block"␊
* Trello with their Kanban␊
* Airtable & Miro with their no-code programable datasheets␊
* Miro & Whimiscal with their edgeless visual whiteboard␊
* Remnote & Capacities with their object-based tag system␊
For more details, please refer to our [RoadMap](https://docs.affine.pro/docs/core-concepts/roadmap)␊
## Self Host␊
Self host AFFiNE␊
||Title|Tag|␊
|---|---|---|␊
|Affine Development|Affine Development|<span data-affine-option data-value="AxSe-53xjX" data-option-color="var(--affine-tag-pink)">AFFiNE</span>|␊
@@ -62,12 +91,16 @@ Generated by [AVA](https://avajs.dev).
|Miro & Whimiscal with their edgeless visual whiteboard|Miro & Whimiscal with their edgeless visual whiteboard|<span data-affine-option data-value="HgHsKOUINZ" data-option-color="var(--affine-tag-blue)">Reference</span>|␊
|Remnote & Capacities with their object-based tag system|Remnote & Capacities with their object-based tag system||␊
## Affine Development␊
For developer or installation guides, please go to [AFFiNE Development](https://docs.affine.pro/docs/development/quick-start)␊
`,
title: 'Write, Draw, Plan all at Once.',
}

View File

@@ -1,27 +0,0 @@
import { faker } from '@faker-js/faker';
import type { AccessToken } from '@prisma/client';
import { Prisma } from '@prisma/client';
import { Mocker } from './factory';
export type MockAccessTokenInput = Omit<
Prisma.AccessTokenUncheckedCreateInput,
'token'
>;
export type MockedAccessToken = AccessToken;
export class MockAccessToken extends Mocker<
MockAccessTokenInput,
MockedAccessToken
> {
override async create(input: MockAccessTokenInput) {
return await this.db.accessToken.create({
data: {
...input,
name: input.name ?? faker.lorem.word(),
token: 'ut_' + faker.string.hexadecimal({ length: 37 }),
},
});
}
}

View File

@@ -57,6 +57,15 @@ export class MockCopilotProvider extends OpenAIProvider {
},
],
},
{
id: 'gpt-4.1',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Object],
},
],
},
{
id: 'gpt-4.1-2025-04-14',
capabilities: [
@@ -67,25 +76,7 @@ export class MockCopilotProvider extends OpenAIProvider {
],
},
{
id: 'gpt-5',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Object],
},
],
},
{
id: 'gpt-5-2025-08-07',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],
output: [ModelOutputType.Text, ModelOutputType.Object],
},
],
},
{
id: 'gpt-5-mini',
id: 'gpt-4.1-mini',
capabilities: [
{
input: [ModelInputType.Text, ModelInputType.Image],

Some files were not shown because too many files have changed in this diff Show More