mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-03-26 09:08:41 +08:00
Compare commits
1 Commits
l-sun/enab
...
apple-inte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5415f1579b |
@@ -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:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
30
.github/actions/deploy/deploy.mjs
vendored
30
.github/actions/deploy/deploy.mjs
vendored
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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: []
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/windows-signer.yml
vendored
2
.github/workflows/windows-signer.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
**/node_modules
|
||||
.yarn
|
||||
.github/helm
|
||||
.git
|
||||
.vscode
|
||||
.yarnrc.yml
|
||||
.docker
|
||||
|
||||
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -65,7 +65,7 @@ export abstract class DataViewUILogicBase<
|
||||
return handler(context);
|
||||
});
|
||||
}
|
||||
setSelection(selection?: Selection) {
|
||||
setSelection(selection?: Selection): void {
|
||||
this.root.setSelection(selection);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -86,9 +86,6 @@ export class MobileKanbanViewUILogic extends DataViewUILogicBase<
|
||||
}
|
||||
|
||||
renderAddGroup = () => {
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
const addGroup = this.groupManager.addGroup;
|
||||
if (!addGroup) {
|
||||
return;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
);
|
||||
@@ -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';
|
||||
}
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
export { BrushDomRendererExtension } from './brush';
|
||||
export { HighlighterDomRendererExtension } from './highlighter';
|
||||
@@ -1,2 +0,0 @@
|
||||
export { BrushElementRendererExtension } from './brush';
|
||||
export { HighlighterElementRendererExtension } from './highlighter';
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './dom';
|
||||
export * from './element';
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
|
||||
@@ -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';
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './dom-renderer';
|
||||
export * from './element-renderer';
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
@@ -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();
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './dom-renderer';
|
||||
export * from './element-renderer';
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './dom-renderer';
|
||||
export * from './element-renderer';
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './highlighter';
|
||||
export * from './shape';
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])];
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
$$;
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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.',
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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 }),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user