mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-06 17:43:51 +00:00
Compare commits
66 Commits
graphite-b
...
v0.22.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
512a908fd4 | ||
|
|
71be1d424a | ||
|
|
d6a26b8093 | ||
|
|
5e05952f6e | ||
|
|
c1930c5937 | ||
|
|
b7ebd33389 | ||
|
|
de9a3e1428 | ||
|
|
374eee9196 | ||
|
|
1bdccdbd57 | ||
|
|
053efb61f0 | ||
|
|
c7aebd0412 | ||
|
|
01aa6979eb | ||
|
|
c32f7c7964 | ||
|
|
d219c92e98 | ||
|
|
063072457c | ||
|
|
13fa4f922a | ||
|
|
f54bc0c047 | ||
|
|
1f0cc51462 | ||
|
|
160e4c2a38 | ||
|
|
99198e246b | ||
|
|
44e1eb503f | ||
|
|
2288cbe54d | ||
|
|
23ff398994 | ||
|
|
ee931d546e | ||
|
|
a02eed382d | ||
|
|
ab78b8e3ab | ||
|
|
3fe2ac4e46 | ||
|
|
d02aa8c7e0 | ||
|
|
cce756365a | ||
|
|
a88dcc0951 | ||
|
|
57208a3de4 | ||
|
|
d8cbeb1bb1 | ||
|
|
418b38e8de | ||
|
|
00ff373c01 | ||
|
|
39830a410a | ||
|
|
ef3be4a816 | ||
|
|
658393159b | ||
|
|
ac3f247f01 | ||
|
|
065d9c3b73 | ||
|
|
2e58c11799 | ||
|
|
10da3ad28e | ||
|
|
887a496f8b | ||
|
|
ada69c80f6 | ||
|
|
7b82dd656b | ||
|
|
5c96566dd8 | ||
|
|
a35e1b1882 | ||
|
|
756847d3cb | ||
|
|
3c3a8bb107 | ||
|
|
88eec2cdfb | ||
|
|
52777b0064 | ||
|
|
00ccd2d865 | ||
|
|
5d94bd41a4 | ||
|
|
20d8d6131a | ||
|
|
94539ac0d0 | ||
|
|
e1ce42a6fc | ||
|
|
2a7f0162cf | ||
|
|
34a5d9dec3 | ||
|
|
c68598c0e0 | ||
|
|
9c81c24fbe | ||
|
|
517aec79ba | ||
|
|
31a1841e25 | ||
|
|
625e8392a6 | ||
|
|
f616bd29d3 | ||
|
|
d6b9e9c60a | ||
|
|
bc67766bb9 | ||
|
|
9a96cfded0 |
@@ -886,8 +886,8 @@
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable indexer plugin\n@default true\n@environment `AFFINE_INDEXER_ENABLED`",
|
||||
"default": true
|
||||
"description": "Enable indexer plugin\n@default false\n@environment `AFFINE_INDEXER_ENABLED`",
|
||||
"default": false
|
||||
},
|
||||
"provider.type": {
|
||||
"type": "string",
|
||||
|
||||
1
.github/workflows/build-images.yml
vendored
1
.github/workflows/build-images.yml
vendored
@@ -113,6 +113,7 @@ jobs:
|
||||
build-server-native:
|
||||
name: Build Server native - ${{ matrix.targets.name }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
6
.github/workflows/build-test.yml
vendored
6
.github/workflows/build-test.yml
vendored
@@ -20,6 +20,7 @@ env:
|
||||
COVERAGE: true
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||
DEPLOYMENT_TYPE: affine
|
||||
AFFINE_INDEXER_ENABLED: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -151,7 +152,8 @@ jobs:
|
||||
- name: Clippy
|
||||
run: |
|
||||
rustup component add clippy
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
cargo clippy --workspace --exclude affine_server_native --all-targets --all-features -- -D warnings
|
||||
cargo clippy -p affine_server_native --all-targets --all-features -- -D warnings
|
||||
|
||||
check-git-status:
|
||||
name: Check Git Status
|
||||
@@ -923,7 +925,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --release --no-fail-fast
|
||||
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
||||
|
||||
copilot-api-test:
|
||||
name: Server Copilot Api Test
|
||||
|
||||
25
.github/workflows/release-mobile.yml
vendored
25
.github/workflows/release-mobile.yml
vendored
@@ -117,31 +117,10 @@ jobs:
|
||||
name: android
|
||||
path: packages/frontend/apps/android/dist
|
||||
|
||||
determine-ios-runner:
|
||||
runs-on: ubuntu-latest
|
||||
ios:
|
||||
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
|
||||
needs:
|
||||
- build-ios-web
|
||||
outputs:
|
||||
RUNNER: ${{ steps.runner.outputs.RUNNER }}
|
||||
steps:
|
||||
- name: Determine Runner
|
||||
id: runner
|
||||
# Randomly pick runner with 80% chance for blaze/macos-14 and 20% chance for namespace-profile-macos
|
||||
# blaze/macos-14 is free but has limited concurrency
|
||||
run: |
|
||||
RANDOM_NUMBER=$(( $RANDOM % 100 + 1 ))
|
||||
if [ $RANDOM_NUMBER -le 20 ]; then
|
||||
echo "Selected namespace-profile-macos (20% probability)"
|
||||
echo "RUNNER=namespace-profile-macos" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Selected blaze/macos-14 (80% probability)"
|
||||
echo "RUNNER=blaze/macos-14" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
ios:
|
||||
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || needs.determine-ios-runner.outputs.RUNNER }}
|
||||
needs:
|
||||
- determine-ios-runner
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download mobile artifact
|
||||
|
||||
611
Cargo.lock
generated
611
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@@ -47,9 +47,9 @@ log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
mimalloc = "0.1"
|
||||
nanoid = "0.4"
|
||||
napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-alpha.28" }
|
||||
napi-derive = { version = "3.0.0-beta.3" }
|
||||
nom = "8"
|
||||
notify = { version = "8", features = ["serde"] }
|
||||
objc2 = "0.6"
|
||||
@@ -77,12 +77,12 @@ smol_str = "0.3"
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
strum_macros = "0.27.0"
|
||||
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
||||
text-splitter = "0.25"
|
||||
text-splitter = "0.27"
|
||||
thiserror = "2"
|
||||
tiktoken-rs = "0.6"
|
||||
tokio = "1.37"
|
||||
tiktoken-rs = "0.7"
|
||||
tokio = "1.45"
|
||||
tree-sitter = { version = "0.25" }
|
||||
tree-sitter-c = { version = "0.23" }
|
||||
tree-sitter-c = { version = "0.24" }
|
||||
tree-sitter-c-sharp = { version = "0.23" }
|
||||
tree-sitter-cpp = { version = "0.23" }
|
||||
tree-sitter-go = { version = "0.23" }
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
AttachmentBlockStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocModeProvider,
|
||||
FileSizeLimitProvider,
|
||||
TelemetryProvider,
|
||||
@@ -37,6 +38,7 @@ import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
import { AttachmentEmbedProvider } from './embed';
|
||||
import { styles } from './styles';
|
||||
@@ -79,8 +81,12 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
return this.std.get(FileSizeLimitProvider).maxFileSize;
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return !!this.model.props.footnoteIdentifier;
|
||||
return this.citationService.isCitationModel(this.model);
|
||||
}
|
||||
|
||||
convertTo = () => {
|
||||
@@ -139,6 +145,34 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
}
|
||||
|
||||
private readonly _trackCitationDeleteEvent = () => {
|
||||
// Check citation delete event
|
||||
this._disposables.add(
|
||||
this.std.store.slots.blockUpdated
|
||||
.pipe(
|
||||
filter(payload => {
|
||||
if (!payload.isLocal) return false;
|
||||
|
||||
const { flavour, id, type } = payload;
|
||||
if (
|
||||
type !== 'delete' ||
|
||||
flavour !== this.model.flavour ||
|
||||
id !== this.model.id
|
||||
)
|
||||
return false;
|
||||
|
||||
const { model } = payload;
|
||||
if (!this.citationService.isCitationModel(model)) return false;
|
||||
|
||||
return true;
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.citationService.trackEvent('Delete');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -162,6 +196,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._trackCitationDeleteEvent();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ConfirmIcon } from '@blocksuite/affine-components/icons';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import type { AttachmentBlockModel } from '@blocksuite/affine-model';
|
||||
import { CitationProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { EditorHost } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { createRef, ref } from 'lit/directives/ref.js';
|
||||
@@ -33,6 +34,7 @@ export const RenameModal = ({
|
||||
|
||||
let fileName = includeExtension ? nameWithoutExtension : originalName;
|
||||
const extension = includeExtension ? originalExtension : '';
|
||||
const citationService = editorHost.std.get(CitationProvider);
|
||||
|
||||
const abort = () => abortController.abort();
|
||||
const onConfirm = () => {
|
||||
@@ -44,6 +46,9 @@ export const RenameModal = ({
|
||||
model.store.updateBlock(model, {
|
||||
name: newFileName,
|
||||
});
|
||||
if (citationService.isCitationModel(model)) {
|
||||
citationService.trackEvent('Edit');
|
||||
}
|
||||
abort();
|
||||
};
|
||||
const onInput = (e: InputEvent) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocModeProvider,
|
||||
LinkPreviewServiceIdentifier,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
@@ -18,6 +19,7 @@ import { html } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
import { filter } from 'rxjs/operators';
|
||||
|
||||
import { refreshBookmarkUrlData } from './utils.js';
|
||||
|
||||
@@ -114,11 +116,12 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
);
|
||||
};
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return (
|
||||
!!this.model.props.footnoteIdentifier &&
|
||||
this.model.props.style === 'citation'
|
||||
);
|
||||
return this.citationService.isCitationModel(this.model);
|
||||
}
|
||||
|
||||
get imageProxyService() {
|
||||
@@ -166,6 +169,31 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
></bookmark-card>`;
|
||||
};
|
||||
|
||||
private readonly _trackCitationDeleteEvent = () => {
|
||||
// Check citation delete event
|
||||
this._disposables.add(
|
||||
this.std.store.slots.blockUpdated
|
||||
.pipe(
|
||||
filter(payload => {
|
||||
if (!payload.isLocal) return false;
|
||||
const { flavour, id, type } = payload;
|
||||
if (
|
||||
type !== 'delete' ||
|
||||
flavour !== this.model.flavour ||
|
||||
id !== this.model.id
|
||||
)
|
||||
return false;
|
||||
const { model } = payload;
|
||||
if (!this.citationService.isCitationModel(model)) return false;
|
||||
return true;
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.citationService.trackEvent('Delete');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -203,6 +231,8 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._trackCitationDeleteEvent();
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
|
||||
@@ -407,7 +407,7 @@ const builtinSurfaceToolbarConfig = {
|
||||
if (options?.viewType !== 'embed') return;
|
||||
|
||||
const { flavour, styles } = options;
|
||||
let { style } = model.props;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style = styles[0];
|
||||
@@ -482,24 +482,26 @@ const builtinSurfaceToolbarConfig = {
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'b.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => BookmarkStyles.includes(action.id)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
@@ -40,6 +40,16 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
|
||||
private _inlineRangeProvider: InlineRangeProvider | null = null;
|
||||
|
||||
private readonly _localPreview$ = signal<boolean | null>(null);
|
||||
|
||||
preview$: Signal<boolean> = computed(() => {
|
||||
const modelPreview = !!this.model.props.preview$.value;
|
||||
if (this.store.readonly) {
|
||||
return this._localPreview$.value ?? modelPreview;
|
||||
}
|
||||
return modelPreview;
|
||||
});
|
||||
|
||||
highlightTokens$: Signal<ThemedToken[][]> = signal([]);
|
||||
|
||||
languageName$: Signal<string> = computed(() => {
|
||||
@@ -393,7 +403,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
true) &&
|
||||
(this.model.props.lineNumber ?? true);
|
||||
|
||||
const preview = !!this.model.props.preview;
|
||||
const preview = this.preview$.value;
|
||||
const previewContext = this.std.getOptional(
|
||||
CodeBlockPreviewIdentifier(this.model.props.language ?? '')
|
||||
);
|
||||
@@ -461,6 +471,14 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
override accessor useCaptionEditor = true;
|
||||
|
||||
override accessor useZeroWidth = true;
|
||||
|
||||
setPreviewState(preview: boolean) {
|
||||
if (this.store.readonly) {
|
||||
this._localPreview$.value = preview;
|
||||
} else {
|
||||
this.store.updateBlock(this.model, { preview });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -58,11 +58,7 @@ export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
|
||||
`;
|
||||
|
||||
private readonly _toggle = (value: boolean) => {
|
||||
if (this.blockComponent.store.readonly) return;
|
||||
|
||||
this.blockComponent.store.updateBlock(this.blockComponent.model, {
|
||||
preview: value,
|
||||
});
|
||||
this.blockComponent.setPreviewState(value);
|
||||
|
||||
const std = this.blockComponent.std;
|
||||
const mode = std.getOptional(DocModeProvider)?.getEditorMode() ?? 'page';
|
||||
@@ -77,7 +73,7 @@ export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
|
||||
};
|
||||
|
||||
get preview() {
|
||||
return !!this.blockComponent.model.props.preview$.value;
|
||||
return this.blockComponent.preview$.value;
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -259,18 +259,18 @@ const builtinToolbarConfig = {
|
||||
conversionsActionGroup,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
@@ -368,26 +368,26 @@ const builtinSurfaceToolbarConfig = {
|
||||
conversionsActionGroup,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedLinkedDocStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedLinkedDocStyles.includes(action.id)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(EmbedLinkedDocModel);
|
||||
if (!model) return null;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
REFERENCE_NODE,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocDisplayMetaProvider,
|
||||
DocModeProvider,
|
||||
OpenDocExtensionIdentifier,
|
||||
@@ -43,6 +44,7 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { renderLinkedDocInCard } from '../common/render-linked-doc';
|
||||
@@ -254,11 +256,12 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
return this.store.readonly;
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get isCitation() {
|
||||
return (
|
||||
!!this.model.props.footnoteIdentifier &&
|
||||
this.model.props.style === 'citation'
|
||||
);
|
||||
return this.citationService.isCitationModel(this.model);
|
||||
}
|
||||
|
||||
private readonly _handleDoubleClick = (event: MouseEvent) => {
|
||||
@@ -454,6 +457,31 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
);
|
||||
};
|
||||
|
||||
private readonly _trackCitationDeleteEvent = () => {
|
||||
// Check citation delete event
|
||||
this._disposables.add(
|
||||
this.std.store.slots.blockUpdated
|
||||
.pipe(
|
||||
filter(payload => {
|
||||
if (!payload.isLocal) return false;
|
||||
const { flavour, id, type } = payload;
|
||||
if (
|
||||
type !== 'delete' ||
|
||||
flavour !== this.model.flavour ||
|
||||
id !== this.model.id
|
||||
)
|
||||
return false;
|
||||
const { model } = payload;
|
||||
if (!this.citationService.isCitationModel(model)) return false;
|
||||
return true;
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.citationService.trackEvent('Delete');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -532,6 +560,8 @@ export class EmbedLinkedDocBlockComponent extends EmbedBlockComponent<EmbedLinke
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
this._trackCitationDeleteEvent();
|
||||
}
|
||||
|
||||
getInitialState(): {
|
||||
|
||||
@@ -153,7 +153,7 @@ function createBuiltinToolbarConfigForExternal(
|
||||
.get(EmbedOptionProvider)
|
||||
.getEmbedBlockOptions(url);
|
||||
|
||||
let { style } = model.props;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
let flavour = 'affine:bookmark';
|
||||
|
||||
if (options?.viewType === 'card') {
|
||||
@@ -227,7 +227,7 @@ function createBuiltinToolbarConfigForExternal(
|
||||
if (options?.viewType !== 'embed') return;
|
||||
|
||||
const { flavour, styles } = options;
|
||||
let { style } = model.props;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style =
|
||||
@@ -441,7 +441,11 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
|
||||
let { style } = model.props;
|
||||
let flavour = 'affine:bookmark';
|
||||
|
||||
if (!BookmarkStyles.includes(style)) {
|
||||
if (
|
||||
!BookmarkStyles.includes(
|
||||
style as (typeof BookmarkStyles)[number]
|
||||
)
|
||||
) {
|
||||
style = BookmarkStyles[0];
|
||||
}
|
||||
|
||||
@@ -517,26 +521,26 @@ const createBuiltinSurfaceToolbarConfigForExternal = (
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'c.style',
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action =>
|
||||
EmbedGithubStyles.includes(action.id as EmbedCardStyle)
|
||||
),
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => EmbedGithubStyles.includes(action.id)),
|
||||
when(ctx) {
|
||||
return Boolean(ctx.getCurrentModelByType(EmbedGithubModel));
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { html } from 'lit';
|
||||
import { state } from 'lit/decorators.js';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import {
|
||||
@@ -87,6 +88,12 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||
this.gfx.tool.currentToolName$.value === 'frameNavigator';
|
||||
const frameIndex = this.gfx.layer.getZIndex(model);
|
||||
|
||||
const widgets = html`${repeat(
|
||||
Object.entries(this.widgets),
|
||||
([id]) => id,
|
||||
([_, widget]) => widget
|
||||
)}`;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="affine-frame-container"
|
||||
@@ -102,6 +109,7 @@ export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||
: `1px solid ${cssVarV2('edgeless/frame/border/default')}`,
|
||||
})}
|
||||
></div>
|
||||
${widgets}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -178,11 +186,22 @@ export const FrameBlockInteraction =
|
||||
selectable(context) {
|
||||
const { model } = context;
|
||||
|
||||
const onTitle =
|
||||
model.externalBound?.containsPoint([
|
||||
context.position.x,
|
||||
context.position.y,
|
||||
]) ?? false;
|
||||
|
||||
return (
|
||||
context.default(context) &&
|
||||
(model.isLocked() || !isTransparent(model.props.background))
|
||||
(model.isLocked() ||
|
||||
!isTransparent(model.props.background) ||
|
||||
onTitle)
|
||||
);
|
||||
},
|
||||
onSelect(context) {
|
||||
return context.default(context);
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
@@ -241,20 +241,35 @@ export class EdgelessFrameManager extends GfxExtension {
|
||||
surfaceModel.elementAdded.subscribe(({ id, local }) => {
|
||||
const element = surfaceModel.getElementById(id);
|
||||
if (element && local) {
|
||||
const frame = this.getFrameFromPoint(element.elementBound.center);
|
||||
|
||||
// if the container created with a frame, skip it.
|
||||
if (
|
||||
isGfxGroupCompatibleModel(element) &&
|
||||
frame &&
|
||||
element.hasChild(frame)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// new element may intended to be added to other group
|
||||
// so we need to wait for the next microtask to check if the element can be added to the frame
|
||||
// The entire frame detection logic must be in microtask for timing reasons:
|
||||
//
|
||||
// 1. For connectors: When elementAdded fires, connectors have invalid bounds [0,0,0,0]
|
||||
// because their path/bounds are calculated in a separate microtask of updateConnectorPath by connector-watcher.
|
||||
// We need to wait for that calculation to complete before frame detection.
|
||||
//
|
||||
// 2. For shapes: Although they have valid bounds immediately, processing them in microtask
|
||||
// ensures consistent timing and allows other initialization to complete first.
|
||||
//
|
||||
// 3. Group compatibility: Some elements may need to establish their group relationships
|
||||
// before being considered for frame membership.
|
||||
//
|
||||
// By embedding the entire logic in microtask, we ensure:
|
||||
// - Connectors have proper bounds calculated (not [0,0,0,0])
|
||||
// - getFrameFromPoint() works correctly with valid element centers
|
||||
// - All element initialization is complete before frame detection
|
||||
queueMicrotask(() => {
|
||||
const frame = this.getFrameFromPoint(element.elementBound.center);
|
||||
|
||||
// if the container created with a frame, skip it.
|
||||
if (
|
||||
isGfxGroupCompatibleModel(element) &&
|
||||
frame &&
|
||||
element.hasChild(frame)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add elements that aren't already grouped and have a valid frame
|
||||
if (!element.group && frame) {
|
||||
this.addElementsToFrame(frame, [element]);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
BLOCK_CHILDREN_CONTAINER_PADDING_LEFT,
|
||||
EDGELESS_TOP_CONTENTEDITABLE_SELECTOR,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
CitationProvider,
|
||||
DocModeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
calculateCollapsedSiblings,
|
||||
getNearestHeadingBefore,
|
||||
@@ -63,6 +66,10 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
?.getPlaceholder(this.model);
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
get attributeRenderer() {
|
||||
return this.inlineManager.getRenderer();
|
||||
}
|
||||
@@ -94,6 +101,12 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
return this.std.get(DefaultInlineManagerExtension.identifier);
|
||||
}
|
||||
|
||||
get hasCitationSiblings() {
|
||||
return this.collapsedSiblings.some(sibling =>
|
||||
this.citationService.isCitationModel(sibling)
|
||||
);
|
||||
}
|
||||
|
||||
override get topContenteditableElement() {
|
||||
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
|
||||
return this.closest<BlockComponent>(
|
||||
@@ -286,6 +299,13 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
||||
collapsed: value,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hasCitationSiblings) {
|
||||
this.citationService.trackEvent('Expand', {
|
||||
control: 'Source Button',
|
||||
type: value ? 'Hide' : 'Show',
|
||||
});
|
||||
}
|
||||
}}
|
||||
></blocksuite-toggle-button>
|
||||
`
|
||||
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
getSurfaceComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { splitIntoLines } from '@blocksuite/affine-gfx-text';
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import type {
|
||||
EmbedCardStyle,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
BookmarkStyles,
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
@@ -236,7 +239,7 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
const options: Record<string, unknown> = {};
|
||||
|
||||
let flavour = 'affine:bookmark';
|
||||
let style = BookmarkStyles[0];
|
||||
let style: EmbedCardStyle = BookmarkStyles[0];
|
||||
let isInternalLink = false;
|
||||
let isLinkedBlock = false;
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
) as SurfaceBlockModel;
|
||||
}
|
||||
|
||||
private get _viewportElement(): HTMLElement {
|
||||
get viewportElement(): HTMLElement {
|
||||
return this.std.get(ViewportElementProvider).viewportElement;
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
this.gfx.viewport.onResize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(this._viewportElement);
|
||||
resizeObserver.observe(this.viewportElement);
|
||||
this._resizeObserver = resizeObserver;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,25 @@ type RendererOptions = {
|
||||
surfaceModel: SurfaceBlockModel;
|
||||
};
|
||||
|
||||
const UpdateType = {
|
||||
ELEMENT_ADDED: 'element-added',
|
||||
ELEMENT_REMOVED: 'element-removed',
|
||||
ELEMENT_UPDATED: 'element-updated',
|
||||
VIEWPORT_CHANGED: 'viewport-changed',
|
||||
SIZE_CHANGED: 'size-changed',
|
||||
ZOOM_STATE_CHANGED: 'zoom-state-changed',
|
||||
} as const;
|
||||
|
||||
type UpdateType = (typeof UpdateType)[keyof typeof UpdateType];
|
||||
|
||||
interface IncrementalUpdateState {
|
||||
dirtyElementIds: Set<string>;
|
||||
viewportDirty: boolean;
|
||||
sizeDirty: boolean;
|
||||
usePlaceholderDirty: boolean;
|
||||
pendingUpdates: Map<string, UpdateType[]>;
|
||||
}
|
||||
|
||||
const PLACEHOLDER_RESET_STYLES = {
|
||||
border: 'none',
|
||||
borderRadius: '0',
|
||||
@@ -141,6 +160,18 @@ export class DomRenderer {
|
||||
|
||||
private _sizeUpdatedRafId: number | null = null;
|
||||
|
||||
private readonly _updateState: IncrementalUpdateState = {
|
||||
dirtyElementIds: new Set(),
|
||||
viewportDirty: false,
|
||||
sizeDirty: false,
|
||||
usePlaceholderDirty: false,
|
||||
pendingUpdates: new Map(),
|
||||
};
|
||||
|
||||
private _lastViewportBounds: Bound | null = null;
|
||||
private _lastZoom: number | null = null;
|
||||
private _lastUsePlaceholder: boolean = false;
|
||||
|
||||
rootElement: HTMLElement;
|
||||
|
||||
private readonly _elementsMap = new Map<string, HTMLElement>();
|
||||
@@ -186,6 +217,7 @@ export class DomRenderer {
|
||||
private _initViewport() {
|
||||
this._disposables.add(
|
||||
this.viewport.viewportUpdated.subscribe(() => {
|
||||
this._markViewportDirty();
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
@@ -195,6 +227,7 @@ export class DomRenderer {
|
||||
if (this._sizeUpdatedRafId) return;
|
||||
this._sizeUpdatedRafId = requestConnectedFrame(() => {
|
||||
this._sizeUpdatedRafId = null;
|
||||
this._markSizeDirty();
|
||||
this._resetSize();
|
||||
this._render();
|
||||
this.refresh();
|
||||
@@ -208,6 +241,7 @@ export class DomRenderer {
|
||||
|
||||
if (this.usePlaceholder !== shouldRenderPlaceholders) {
|
||||
this.usePlaceholder = shouldRenderPlaceholders;
|
||||
this._markUsePlaceholderDirty();
|
||||
this.refresh();
|
||||
}
|
||||
})
|
||||
@@ -307,6 +341,292 @@ export class DomRenderer {
|
||||
}
|
||||
|
||||
private _render() {
|
||||
this._renderIncremental();
|
||||
}
|
||||
|
||||
private _watchSurface(surfaceModel: SurfaceBlockModel) {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_ADDED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(payload => {
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_REMOVED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(payload => {
|
||||
this._markElementDirty(payload.model.id, UpdateType.ELEMENT_UPDATED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
surfaceModel.elementUpdated.subscribe(payload => {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this._markElementDirty(payload.id, UpdateType.ELEMENT_UPDATED);
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay = (overlay: Overlay) => {
|
||||
overlay.setRenderer(null);
|
||||
this._overlays.add(overlay);
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
attach = (container: HTMLElement) => {
|
||||
this._container = container;
|
||||
container.append(this.rootElement);
|
||||
|
||||
this._resetSize();
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
dispose = () => {
|
||||
this._overlays.forEach(overlay => overlay.dispose());
|
||||
this._overlays.clear();
|
||||
this._disposables.dispose();
|
||||
|
||||
if (this._refreshRafId) {
|
||||
cancelAnimationFrame(this._refreshRafId);
|
||||
this._refreshRafId = null;
|
||||
}
|
||||
if (this._sizeUpdatedRafId) {
|
||||
cancelAnimationFrame(this._sizeUpdatedRafId);
|
||||
this._sizeUpdatedRafId = null;
|
||||
}
|
||||
|
||||
this.rootElement.remove();
|
||||
this._elementsMap.clear();
|
||||
};
|
||||
|
||||
generateColorProperty = (color: Color, fallback?: Color) => {
|
||||
return (
|
||||
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
|
||||
);
|
||||
};
|
||||
|
||||
getColorScheme = () => {
|
||||
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
|
||||
};
|
||||
|
||||
getColorValue = (color: Color, fallback?: Color, real?: boolean) => {
|
||||
return (
|
||||
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
|
||||
);
|
||||
};
|
||||
|
||||
getPropertyValue = (property: string) => {
|
||||
return this.provider.getPropertyValue?.(property) ?? '';
|
||||
};
|
||||
|
||||
refresh = () => {
|
||||
if (this._refreshRafId !== null) return;
|
||||
|
||||
this._refreshRafId = requestConnectedFrame(() => {
|
||||
this._refreshRafId = null;
|
||||
this._render();
|
||||
}, this._container);
|
||||
};
|
||||
|
||||
removeOverlay = (overlay: Overlay) => {
|
||||
if (!this._overlays.has(overlay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlays.delete(overlay);
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
/**
|
||||
* Mark a specific element as dirty for incremental updates
|
||||
* @param elementId - The ID of the element to mark as dirty
|
||||
* @param updateType - The type of update (optional, defaults to ELEMENT_UPDATED)
|
||||
*/
|
||||
markElementDirty = (
|
||||
elementId: string,
|
||||
updateType: UpdateType = UpdateType.ELEMENT_UPDATED
|
||||
) => {
|
||||
this._markElementDirty(elementId, updateType);
|
||||
};
|
||||
|
||||
/**
|
||||
* Force a full re-render of all elements
|
||||
*/
|
||||
forceFullRender = () => {
|
||||
this._updateState.viewportDirty = true;
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
private _markElementDirty(elementId: string, updateType: UpdateType) {
|
||||
this._updateState.dirtyElementIds.add(elementId);
|
||||
const currentUpdates =
|
||||
this._updateState.pendingUpdates.get(elementId) || [];
|
||||
if (!currentUpdates.includes(updateType)) {
|
||||
currentUpdates.push(updateType);
|
||||
this._updateState.pendingUpdates.set(elementId, currentUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private _markViewportDirty() {
|
||||
this._updateState.viewportDirty = true;
|
||||
}
|
||||
|
||||
private _markSizeDirty() {
|
||||
this._updateState.sizeDirty = true;
|
||||
}
|
||||
|
||||
private _markUsePlaceholderDirty() {
|
||||
this._updateState.usePlaceholderDirty = true;
|
||||
}
|
||||
|
||||
private _clearUpdateState() {
|
||||
this._updateState.dirtyElementIds.clear();
|
||||
this._updateState.viewportDirty = false;
|
||||
this._updateState.sizeDirty = false;
|
||||
this._updateState.usePlaceholderDirty = false;
|
||||
this._updateState.pendingUpdates.clear();
|
||||
}
|
||||
|
||||
private _isViewportChanged(): boolean {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
|
||||
if (!this._lastViewportBounds || !this._lastZoom) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
this._lastViewportBounds.x !== viewportBounds.x ||
|
||||
this._lastViewportBounds.y !== viewportBounds.y ||
|
||||
this._lastViewportBounds.w !== viewportBounds.w ||
|
||||
this._lastViewportBounds.h !== viewportBounds.h ||
|
||||
this._lastZoom !== zoom
|
||||
);
|
||||
}
|
||||
|
||||
private _isUsePlaceholderChanged(): boolean {
|
||||
return this._lastUsePlaceholder !== this.usePlaceholder;
|
||||
}
|
||||
|
||||
private _updateLastState() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
this._lastViewportBounds = {
|
||||
x: viewportBounds.x,
|
||||
y: viewportBounds.y,
|
||||
w: viewportBounds.w,
|
||||
h: viewportBounds.h,
|
||||
} as Bound;
|
||||
this._lastZoom = zoom;
|
||||
this._lastUsePlaceholder = this.usePlaceholder;
|
||||
}
|
||||
|
||||
private _renderIncremental() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
const addedElements: HTMLElement[] = [];
|
||||
const elementsToRemove: HTMLElement[] = [];
|
||||
|
||||
const needsFullRender =
|
||||
this._isViewportChanged() ||
|
||||
this._isUsePlaceholderChanged() ||
|
||||
this._updateState.sizeDirty ||
|
||||
this._updateState.viewportDirty ||
|
||||
this._updateState.usePlaceholderDirty;
|
||||
|
||||
if (needsFullRender) {
|
||||
this._renderFull();
|
||||
this._updateLastState();
|
||||
this._clearUpdateState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update dirty elements
|
||||
const elementsFromGrid = this.grid.search(viewportBounds, {
|
||||
filter: ['canvas', 'local'],
|
||||
}) as SurfaceElementModel[];
|
||||
|
||||
const visibleElementIds = new Set<string>();
|
||||
|
||||
// 1. Update dirty elements
|
||||
for (const elementModel of elementsFromGrid) {
|
||||
const display = (elementModel.display ?? true) && !elementModel.hidden;
|
||||
if (
|
||||
display &&
|
||||
intersects(getBoundWithRotation(elementModel), viewportBounds)
|
||||
) {
|
||||
visibleElementIds.add(elementModel.id);
|
||||
|
||||
// Only update dirty elements
|
||||
if (this._updateState.dirtyElementIds.has(elementModel.id)) {
|
||||
if (
|
||||
this.usePlaceholder &&
|
||||
!(elementModel as GfxCompatibleInterface).forceFullRender
|
||||
) {
|
||||
this._renderOrUpdatePlaceholder(
|
||||
elementModel,
|
||||
viewportBounds,
|
||||
zoom,
|
||||
addedElements
|
||||
);
|
||||
} else {
|
||||
this._renderOrUpdateFullElement(
|
||||
elementModel,
|
||||
viewportBounds,
|
||||
zoom,
|
||||
addedElements
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove elements that are no longer in the grid
|
||||
for (const elementId of this._updateState.dirtyElementIds) {
|
||||
const updateTypes = this._updateState.pendingUpdates.get(elementId) || [];
|
||||
if (
|
||||
updateTypes.includes(UpdateType.ELEMENT_REMOVED) ||
|
||||
!visibleElementIds.has(elementId)
|
||||
) {
|
||||
const domElem = this._elementsMap.get(elementId);
|
||||
if (domElem) {
|
||||
domElem.remove();
|
||||
this._elementsMap.delete(elementId);
|
||||
elementsToRemove.push(domElem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Notify changes
|
||||
if (addedElements.length > 0 || elementsToRemove.length > 0) {
|
||||
this.elementsUpdated.next({
|
||||
elements: Array.from(this._elementsMap.values()),
|
||||
added: addedElements,
|
||||
removed: elementsToRemove,
|
||||
});
|
||||
}
|
||||
|
||||
this._updateLastState();
|
||||
this._clearUpdateState();
|
||||
}
|
||||
|
||||
private _renderFull() {
|
||||
const { viewportBounds, zoom } = this.viewport;
|
||||
const addedElements: HTMLElement[] = [];
|
||||
const elementsToRemove: HTMLElement[] = [];
|
||||
@@ -387,100 +707,4 @@ export class DomRenderer {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _watchSurface(surfaceModel: SurfaceBlockModel) {
|
||||
this._disposables.add(
|
||||
surfaceModel.elementAdded.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.elementRemoved.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementAdded.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementDeleted.subscribe(() => this.refresh())
|
||||
);
|
||||
this._disposables.add(
|
||||
surfaceModel.localElementUpdated.subscribe(() => this.refresh())
|
||||
);
|
||||
|
||||
this._disposables.add(
|
||||
surfaceModel.elementUpdated.subscribe(payload => {
|
||||
// ignore externalXYWH update cause it's updated by the renderer
|
||||
if (payload.props['externalXYWH']) return;
|
||||
this.refresh();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
addOverlay(overlay: Overlay) {
|
||||
overlay.setRenderer(null);
|
||||
this._overlays.add(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
attach(container: HTMLElement) {
|
||||
this._container = container;
|
||||
container.append(this.rootElement);
|
||||
|
||||
this._resetSize();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._overlays.forEach(overlay => overlay.dispose());
|
||||
this._overlays.clear();
|
||||
this._disposables.dispose();
|
||||
|
||||
if (this._refreshRafId) {
|
||||
cancelAnimationFrame(this._refreshRafId);
|
||||
this._refreshRafId = null;
|
||||
}
|
||||
if (this._sizeUpdatedRafId) {
|
||||
cancelAnimationFrame(this._sizeUpdatedRafId);
|
||||
this._sizeUpdatedRafId = null;
|
||||
}
|
||||
|
||||
this.rootElement.remove();
|
||||
this._elementsMap.clear();
|
||||
}
|
||||
|
||||
generateColorProperty(color: Color, fallback?: Color) {
|
||||
return (
|
||||
this.provider.generateColorProperty?.(color, fallback) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getColorScheme() {
|
||||
return this.provider.getColorScheme?.() ?? ColorScheme.Light;
|
||||
}
|
||||
|
||||
getColorValue(color: Color, fallback?: Color, real?: boolean) {
|
||||
return (
|
||||
this.provider.getColorValue?.(color, fallback, real) ?? 'transparent'
|
||||
);
|
||||
}
|
||||
|
||||
getPropertyValue(property: string) {
|
||||
return this.provider.getPropertyValue?.(property) ?? '';
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this._refreshRafId !== null) return;
|
||||
|
||||
this._refreshRafId = requestConnectedFrame(() => {
|
||||
this._refreshRafId = null;
|
||||
this._render();
|
||||
}, this._container);
|
||||
}
|
||||
|
||||
removeOverlay(overlay: Overlay) {
|
||||
if (!this._overlays.has(overlay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._overlays.delete(overlay);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,6 @@ export class OpenDocDropdownMenu extends SignalWatcher(
|
||||
gap: unset !important;
|
||||
}
|
||||
|
||||
editor-icon-button {
|
||||
.label {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
div[data-orientation] {
|
||||
width: 264px;
|
||||
gap: 4px;
|
||||
|
||||
@@ -9,6 +9,7 @@ const toolbarColorKeys: Array<keyof AffineCssVariables> = [
|
||||
'--affine-background-overlay-panel-color',
|
||||
'--affine-v2-layer-background-overlayPanel' as never,
|
||||
'--affine-v2-layer-insideBorder-blackBorder' as never,
|
||||
'--affine-v2-icon-primary' as never,
|
||||
'--affine-background-error-color',
|
||||
'--affine-background-primary-color',
|
||||
'--affine-background-tertiary-color',
|
||||
|
||||
@@ -16,5 +16,6 @@ export const renderFilterBar = (props: DataViewWidgetProps) => {
|
||||
.vars="${filterTrait.view.vars$}"
|
||||
.filterGroup="${filterTrait.filter$}"
|
||||
.onChange="${filterTrait.filterSet}"
|
||||
.dataViewLogic="${props.dataViewLogic}"
|
||||
></filter-bar>`;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { property } from 'lit/decorators.js';
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
|
||||
import { popCreateFilter } from '../../../core/index.js';
|
||||
import type { DataViewUILogicBase } from '../../../core/view/data-view-base.js';
|
||||
import { popFilterGroup } from './group-panel-view.js';
|
||||
|
||||
export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
@@ -99,6 +100,7 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
requestAnimationFrame(() => {
|
||||
this.expandGroup(element, index);
|
||||
});
|
||||
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -206,6 +208,9 @@ export class FilterBar extends SignalWatcher(ShadowlessElement) {
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor vars!: ReadonlySignal<Variable[]>;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor dataViewLogic!: DataViewUILogicBase;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -26,7 +26,10 @@ import { repeat } from 'lit/directives/repeat.js';
|
||||
import type { Variable } from '../../../core/expression/types.js';
|
||||
import type { FilterTrait } from '../../../core/filter/trait.js';
|
||||
import type { Filter, FilterGroup } from '../../../core/filter/types.js';
|
||||
import { popCreateFilter } from '../../../core/index.js';
|
||||
import {
|
||||
type DataViewUILogicBase,
|
||||
popCreateFilter,
|
||||
} from '../../../core/index.js';
|
||||
import {
|
||||
type FilterGroupView,
|
||||
getDepth,
|
||||
@@ -375,6 +378,7 @@ export const popFilterRoot = (
|
||||
props: {
|
||||
filterTrait: FilterTrait;
|
||||
onBack: () => void;
|
||||
dataViewLogic: DataViewUILogicBase;
|
||||
}
|
||||
) => {
|
||||
const filterTrait = props.filterTrait;
|
||||
@@ -414,6 +418,10 @@ export const popFilterRoot = (
|
||||
...value,
|
||||
conditions: [...value.conditions, filter],
|
||||
});
|
||||
props.dataViewLogic.eventTrace(
|
||||
'CreateDatabaseFilter',
|
||||
{}
|
||||
);
|
||||
},
|
||||
},
|
||||
{ middleware: subMenuMiddleware }
|
||||
|
||||
@@ -75,6 +75,7 @@ export class DataViewHeaderToolsFilter extends WidgetBase {
|
||||
conditions: [filter],
|
||||
};
|
||||
this.toggleShowFilter(true);
|
||||
this.dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -145,13 +145,16 @@ const createSettingMenus = (
|
||||
popFilterRoot(target, {
|
||||
filterTrait: filterTrait,
|
||||
onBack: reopen,
|
||||
dataViewLogic: dataViewLogic,
|
||||
});
|
||||
dataViewLogic.eventTrace('CreateDatabaseFilter', {});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
popFilterRoot(target, {
|
||||
filterTrait: filterTrait,
|
||||
onBack: reopen,
|
||||
dataViewLogic: dataViewLogic,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from '@blocksuite/affine-ext-loader';
|
||||
import {
|
||||
AutoClearSelectionService,
|
||||
CitationService,
|
||||
DefaultOpenDocExtension,
|
||||
DNDAPIExtension,
|
||||
DocDisplayMetaService,
|
||||
@@ -76,6 +77,7 @@ export class FoundationViewExtension extends ViewExtensionProvider<FoundationVie
|
||||
FileSizeLimitService,
|
||||
LinkPreviewCache,
|
||||
LinkPreviewService,
|
||||
CitationService,
|
||||
]);
|
||||
context.register(clipboardConfigs);
|
||||
if (this.isEdgeless(context.scope)) {
|
||||
|
||||
@@ -192,10 +192,14 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
|
||||
this._updateTitleInMeta();
|
||||
this.requestUpdate();
|
||||
};
|
||||
this._rootModel?.props.title.yText.observe(updateMetaTitle);
|
||||
this._disposables.add(() => {
|
||||
this._rootModel?.props.title.yText.unobserve(updateMetaTitle);
|
||||
});
|
||||
|
||||
if (this._rootModel) {
|
||||
const rootModel = this._rootModel;
|
||||
rootModel.props.title.yText.observe(updateMetaTitle);
|
||||
this._disposables.add(() => {
|
||||
rootModel.props.title.yText.unobserve(updateMetaTitle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { type Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
type DragExtensionInitializeContext,
|
||||
type ExtensionDragMoveContext,
|
||||
@@ -74,47 +74,63 @@ export class SnapExtension extends InteractivityExtension {
|
||||
return {};
|
||||
}
|
||||
|
||||
let alignBound: Bound | null = null;
|
||||
|
||||
return {
|
||||
onResizeStart(context) {
|
||||
alignBound = snapOverlay.setMovingElements(context.elements);
|
||||
snapOverlay.setMovingElements(context.elements);
|
||||
},
|
||||
onResizeMove(context) {
|
||||
if (!alignBound || alignBound.w === 0 || alignBound.h === 0) {
|
||||
return;
|
||||
const {
|
||||
handle,
|
||||
originalBound,
|
||||
scaleX,
|
||||
scaleY,
|
||||
handleSign,
|
||||
currentHandlePos,
|
||||
elements,
|
||||
} = context;
|
||||
const rotate = elements.length > 1 ? 0 : elements[0].rotate;
|
||||
const alignDirection: ('vertical' | 'horizontal')[] = [];
|
||||
let switchDirection = false;
|
||||
let nx = handleSign.x;
|
||||
let ny = handleSign.y;
|
||||
|
||||
if (handle.length > 6) {
|
||||
alignDirection.push('vertical', 'horizontal');
|
||||
} else if (rotate % 90 === 0) {
|
||||
nx =
|
||||
handleSign.x * Math.cos((rotate / 180) * Math.PI) -
|
||||
handleSign.y * Math.sin((rotate / 180) * Math.PI);
|
||||
ny =
|
||||
handleSign.x * Math.sin((rotate / 180) * Math.PI) +
|
||||
handleSign.y * Math.cos((rotate / 180) * Math.PI);
|
||||
|
||||
if (Math.abs(nx) > Math.abs(ny)) {
|
||||
alignDirection.push('horizontal');
|
||||
} else {
|
||||
alignDirection.push('vertical');
|
||||
}
|
||||
|
||||
if (rotate % 180 !== 0) {
|
||||
switchDirection = true;
|
||||
}
|
||||
}
|
||||
|
||||
const { handle, handleSign, lockRatio } = context;
|
||||
let { dx, dy } = context;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(
|
||||
Math.abs(dx / alignBound.w),
|
||||
Math.abs(dy / alignBound.h)
|
||||
if (alignDirection.length > 0) {
|
||||
const rst = snapOverlay.alignResize(
|
||||
currentHandlePos,
|
||||
alignDirection
|
||||
);
|
||||
|
||||
dx = min * Math.sign(dx) * alignBound.w;
|
||||
dy = min * Math.sign(dy) * alignBound.h;
|
||||
const dx = switchDirection ? ny * rst.dy : nx * rst.dx;
|
||||
const dy = switchDirection ? nx * rst.dx : ny * rst.dy;
|
||||
|
||||
context.suggest({
|
||||
scaleX: scaleX + dx / originalBound.w,
|
||||
scaleY: scaleY + dy / originalBound.h,
|
||||
});
|
||||
}
|
||||
|
||||
const currentBound = new Bound(
|
||||
alignBound.x +
|
||||
(handle.includes('left') ? -dx * handleSign.xSign : 0),
|
||||
alignBound.y +
|
||||
(handle.includes('top') ? -dy * handleSign.ySign : 0),
|
||||
Math.abs(alignBound.w + dx * handleSign.xSign),
|
||||
Math.abs(alignBound.h + dy * handleSign.ySign)
|
||||
);
|
||||
const alignRst = snapOverlay.align(currentBound);
|
||||
|
||||
context.suggest({
|
||||
dx: alignRst.dx + context.dx,
|
||||
dy: alignRst.dy + context.dy,
|
||||
});
|
||||
},
|
||||
onResizeEnd() {
|
||||
alignBound = null;
|
||||
snapOverlay.clear();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ConnectorElementModel,
|
||||
MindmapElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { almostEqual, Bound, Point } from '@blocksuite/global/gfx';
|
||||
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
|
||||
interface Distance {
|
||||
@@ -586,6 +586,60 @@ export class SnapOverlay extends Overlay {
|
||||
);
|
||||
}
|
||||
|
||||
alignResize(position: IVec, direction: ('vertical' | 'horizontal')[]) {
|
||||
const rst = { dx: 0, dy: 0 };
|
||||
|
||||
const { viewport } = this.gfx;
|
||||
const threshold = ALIGN_THRESHOLD / viewport.zoom;
|
||||
const searchBound = new Bound(
|
||||
position[0] - threshold / 2,
|
||||
position[1] - threshold / 2,
|
||||
threshold,
|
||||
threshold
|
||||
);
|
||||
const alignBound = new Bound(position[0], position[1], 0, 0);
|
||||
|
||||
this._intraGraphicAlignLines = {
|
||||
horizontal: [],
|
||||
vertical: [],
|
||||
};
|
||||
this._distributedAlignLines = [];
|
||||
this._updateAlignCandidates(searchBound);
|
||||
|
||||
for (const other of this._referenceBounds.all) {
|
||||
const closestDistances = this._calculateClosestDistances(
|
||||
alignBound,
|
||||
other
|
||||
);
|
||||
|
||||
if (
|
||||
direction.includes('horizontal') &&
|
||||
closestDistances.horiz &&
|
||||
(!this._intraGraphicAlignLines.horizontal.length ||
|
||||
Math.abs(closestDistances.horiz.distance) < Math.abs(rst.dx))
|
||||
) {
|
||||
this._updateXAlignPoint(rst, alignBound, other, closestDistances);
|
||||
}
|
||||
|
||||
if (
|
||||
direction.includes('vertical') &&
|
||||
closestDistances.vert &&
|
||||
(!this._intraGraphicAlignLines.vertical.length ||
|
||||
Math.abs(closestDistances.vert.distance) < Math.abs(rst.dy))
|
||||
) {
|
||||
this._updateYAlignPoint(rst, alignBound, other, closestDistances);
|
||||
}
|
||||
}
|
||||
|
||||
this._intraGraphicAlignLines.horizontal =
|
||||
this._intraGraphicAlignLines.horizontal.slice(0, 1);
|
||||
this._intraGraphicAlignLines.vertical =
|
||||
this._intraGraphicAlignLines.vertical.slice(0, 1);
|
||||
this._renderer?.refresh();
|
||||
|
||||
return rst;
|
||||
}
|
||||
|
||||
align(bound: Bound): { dx: number; dy: number } {
|
||||
const rst = { dx: 0, dy: 0 };
|
||||
const threshold = ALIGN_THRESHOLD / this.gfx.viewport.zoom;
|
||||
|
||||
@@ -9,18 +9,35 @@ function applyShapeSpecificStyles(
|
||||
element: HTMLElement,
|
||||
zoom: number
|
||||
) {
|
||||
if (model.shapeType === 'rect') {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
} else if (model.shapeType === 'ellipse') {
|
||||
element.style.borderRadius = '50%';
|
||||
} else {
|
||||
element.style.borderRadius = '';
|
||||
// Reset properties that might be set by different shape types
|
||||
element.style.removeProperty('clip-path');
|
||||
element.style.removeProperty('border-radius');
|
||||
// Clear DOM for shapes that don't use SVG, or if type changes from SVG-based to non-SVG-based
|
||||
if (model.shapeType !== 'diamond' && model.shapeType !== 'triangle') {
|
||||
while (element.firstChild) element.firstChild.remove();
|
||||
}
|
||||
|
||||
switch (model.shapeType) {
|
||||
case 'rect': {
|
||||
const w = model.w * zoom;
|
||||
const h = model.h * zoom;
|
||||
const r = model.radius ?? 0;
|
||||
const borderRadius =
|
||||
r < 1 ? `${Math.min(w * r, h * r)}px` : `${r * zoom}px`;
|
||||
element.style.borderRadius = borderRadius;
|
||||
break;
|
||||
}
|
||||
case 'ellipse':
|
||||
element.style.borderRadius = '50%';
|
||||
break;
|
||||
case 'diamond':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)';
|
||||
break;
|
||||
case 'triangle':
|
||||
element.style.clipPath = 'polygon(50% 0%, 100% 100%, 0% 100%)';
|
||||
break;
|
||||
}
|
||||
// No 'else' needed to clear styles, as they are reset at the beginning of the function.
|
||||
}
|
||||
|
||||
function applyBorderStyles(
|
||||
@@ -78,6 +95,9 @@ export const shapeDomRenderer = (
|
||||
renderer: DomRenderer
|
||||
): void => {
|
||||
const { zoom } = renderer.viewport;
|
||||
const unscaledWidth = model.w;
|
||||
const unscaledHeight = model.h;
|
||||
|
||||
const fillColor = renderer.getColorValue(
|
||||
model.fillColor,
|
||||
DefaultTheme.shapeFillColor,
|
||||
@@ -89,17 +109,80 @@ export const shapeDomRenderer = (
|
||||
true
|
||||
);
|
||||
|
||||
element.style.width = `${model.w * zoom}px`;
|
||||
element.style.height = `${model.h * zoom}px`;
|
||||
element.style.width = `${unscaledWidth * zoom}px`;
|
||||
element.style.height = `${unscaledHeight * zoom}px`;
|
||||
element.style.boxSizing = 'border-box';
|
||||
|
||||
// Apply shape-specific clipping, border-radius, and potentially clear innerHTML
|
||||
applyShapeSpecificStyles(model, element, zoom);
|
||||
|
||||
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
|
||||
if (model.shapeType === 'diamond' || model.shapeType === 'triangle') {
|
||||
// For diamond and triangle, fill and border are handled by inline SVG
|
||||
element.style.border = 'none'; // Ensure no standard CSS border interferes
|
||||
element.style.backgroundColor = 'transparent'; // Host element is transparent
|
||||
|
||||
const strokeW = model.strokeWidth;
|
||||
const halfStroke = strokeW / 2; // Calculate half stroke width for point adjustment
|
||||
|
||||
let svgPoints = '';
|
||||
if (model.shapeType === 'diamond') {
|
||||
// Adjusted points for diamond
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight / 2}`,
|
||||
`${unscaledWidth / 2},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight / 2}`,
|
||||
].join(' ');
|
||||
} else {
|
||||
// triangle
|
||||
// Adjusted points for triangle
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight - halfStroke}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
// Determine if stroke should be visible and its color
|
||||
const finalStrokeColor =
|
||||
model.strokeStyle !== 'none' && strokeW > 0 ? strokeColor : 'transparent';
|
||||
// Determine dash array, only if stroke is visible and style is 'dash'
|
||||
const finalStrokeDasharray =
|
||||
model.strokeStyle === 'dash' && finalStrokeColor !== 'transparent'
|
||||
? '12, 12'
|
||||
: 'none';
|
||||
// Determine fill color
|
||||
const finalFillColor = model.filled ? fillColor : 'transparent';
|
||||
|
||||
// Build SVG safely with DOM-API
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
|
||||
const polygon = document.createElementNS(SVG_NS, 'polygon');
|
||||
polygon.setAttribute('points', svgPoints);
|
||||
polygon.setAttribute('fill', finalFillColor);
|
||||
polygon.setAttribute('stroke', finalStrokeColor);
|
||||
polygon.setAttribute('stroke-width', String(strokeW));
|
||||
if (finalStrokeDasharray !== 'none') {
|
||||
polygon.setAttribute('stroke-dasharray', finalStrokeDasharray);
|
||||
}
|
||||
svg.append(polygon);
|
||||
|
||||
// Replace existing children to avoid memory leaks
|
||||
element.replaceChildren(svg);
|
||||
} else {
|
||||
// Standard rendering for other shapes (e.g., rect, ellipse)
|
||||
// innerHTML was already cleared by applyShapeSpecificStyles if necessary
|
||||
element.style.backgroundColor = model.filled ? fillColor : 'transparent';
|
||||
applyBorderStyles(model, element, strokeColor, zoom); // Uses standard CSS border
|
||||
}
|
||||
|
||||
applyBorderStyles(model, element, strokeColor, zoom);
|
||||
applyTransformStyles(model, element);
|
||||
|
||||
element.style.boxSizing = 'border-box';
|
||||
element.style.zIndex = renderer.layerManager.getZIndex(model).toString();
|
||||
|
||||
manageClassNames(model, element);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HoverController } from '@blocksuite/affine-components/hover';
|
||||
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
|
||||
import type { FootNote } from '@blocksuite/affine-model';
|
||||
import { CitationProvider } from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||
import { WithDisposable } from '@blocksuite/global/lit';
|
||||
@@ -117,6 +118,10 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
return this.std.store.readonly;
|
||||
}
|
||||
|
||||
get citationService() {
|
||||
return this.std.get(CitationProvider);
|
||||
}
|
||||
|
||||
onFootnoteClick = () => {
|
||||
if (!this.footnote) {
|
||||
return;
|
||||
@@ -215,6 +220,10 @@ export class AffineFootnoteNode extends WithDisposable(ShadowlessElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.citationService.trackEvent('Hover', {
|
||||
control: 'Source Footnote',
|
||||
});
|
||||
|
||||
return {
|
||||
template: this._FootNotePopup(footnote, abortController),
|
||||
container: this.std.host,
|
||||
|
||||
@@ -188,6 +188,8 @@ export class AffineLatexNode extends SignalWatcher(
|
||||
this._editorAbortController?.abort();
|
||||
this._editorAbortController = new AbortController();
|
||||
|
||||
blockComponent.selection.setGroup('note', []);
|
||||
|
||||
const portal = createLitPortal({
|
||||
template: html`<latex-editor-menu
|
||||
.std=${this.std}
|
||||
|
||||
@@ -30,11 +30,12 @@ import { AttachmentBlockTransformer } from './attachment-transformer.js';
|
||||
*/
|
||||
type BackwardCompatibleUndefined = undefined;
|
||||
|
||||
export const AttachmentBlockStyles: EmbedCardStyle[] = [
|
||||
export const AttachmentBlockStyles = [
|
||||
'cubeThick',
|
||||
'horizontalThin',
|
||||
'pdf',
|
||||
] as const;
|
||||
'citation',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type AttachmentBlockProps = {
|
||||
name: string;
|
||||
|
||||
@@ -15,13 +15,13 @@ import type {
|
||||
LinkPreviewData,
|
||||
} from '../../utils/index.js';
|
||||
|
||||
export const BookmarkStyles: EmbedCardStyle[] = [
|
||||
export const BookmarkStyles = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
'citation',
|
||||
] as const;
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type BookmarkBlockProps = {
|
||||
style: (typeof BookmarkStyles)[number];
|
||||
|
||||
@@ -8,7 +8,7 @@ export type EmbedFigmaBlockUrlData = {
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export const EmbedFigmaStyles: EmbedCardStyle[] = ['figma'] as const;
|
||||
export const EmbedFigmaStyles = ['figma'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedFigmaBlockProps = {
|
||||
style: (typeof EmbedFigmaStyles)[number];
|
||||
|
||||
@@ -13,12 +13,12 @@ export type EmbedGithubBlockUrlData = {
|
||||
assignees: string[] | null;
|
||||
};
|
||||
|
||||
export const EmbedGithubStyles: EmbedCardStyle[] = [
|
||||
export const EmbedGithubStyles = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
] as const;
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedGithubBlockProps = {
|
||||
style: (typeof EmbedGithubStyles)[number];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BlockModel } from '@blocksuite/store';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedHtmlStyles: EmbedCardStyle[] = ['html'] as const;
|
||||
export const EmbedHtmlStyles = ['html'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedHtmlBlockProps = {
|
||||
style: (typeof EmbedHtmlStyles)[number];
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { type EmbedCardStyle } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedIframeStyles: EmbedCardStyle[] = ['figma'] as const;
|
||||
export const EmbedIframeStyles = ['figma'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedIframeBlockProps = {
|
||||
url: string; // the original url that user input
|
||||
|
||||
@@ -4,17 +4,17 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedLinkedDocStyles: EmbedCardStyle[] = [
|
||||
export const EmbedLinkedDocStyles = [
|
||||
'vertical',
|
||||
'horizontal',
|
||||
'list',
|
||||
'cube',
|
||||
'horizontalThin',
|
||||
'citation',
|
||||
];
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedLinkedDocBlockProps = {
|
||||
style: EmbedCardStyle;
|
||||
style: (typeof EmbedLinkedDocStyles)[number];
|
||||
caption: string | null;
|
||||
footnoteIdentifier: string | null;
|
||||
} & ReferenceInfo;
|
||||
|
||||
@@ -10,7 +10,7 @@ export type EmbedLoomBlockUrlData = {
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export const EmbedLoomStyles: EmbedCardStyle[] = ['video'] as const;
|
||||
export const EmbedLoomStyles = ['video'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedLoomBlockProps = {
|
||||
style: (typeof EmbedLoomStyles)[number];
|
||||
|
||||
@@ -5,7 +5,9 @@ import type { ReferenceInfo } from '../../../consts/doc.js';
|
||||
import type { EmbedCardStyle } from '../../../utils/index.js';
|
||||
import { defineEmbedModel } from '../../../utils/index.js';
|
||||
|
||||
export const EmbedSyncedDocStyles: EmbedCardStyle[] = ['syncedDoc'];
|
||||
export const EmbedSyncedDocStyles = [
|
||||
'syncedDoc',
|
||||
] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedSyncedDocBlockProps = {
|
||||
style: EmbedCardStyle;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { BookmarkBlockModel } from '../bookmark';
|
||||
import { EmbedFigmaModel } from './figma';
|
||||
import { EmbedGithubModel } from './github';
|
||||
import type { EmbedHtmlModel } from './html';
|
||||
@@ -30,7 +31,10 @@ export type EmbedCardModel = InstanceType<
|
||||
ExternalEmbedModel | InternalEmbedModel
|
||||
>;
|
||||
|
||||
export type LinkableEmbedModel = EmbedCardModel | EmbedIframeBlockModel;
|
||||
export type LinkableEmbedModel =
|
||||
| EmbedCardModel
|
||||
| EmbedIframeBlockModel
|
||||
| BookmarkBlockModel;
|
||||
|
||||
export type BuiltInEmbedModel = EmbedCardModel | EmbedHtmlModel;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export type EmbedYoutubeBlockUrlData = {
|
||||
creatorImage: string | null;
|
||||
};
|
||||
|
||||
export const EmbedYoutubeStyles: EmbedCardStyle[] = ['video'] as const;
|
||||
export const EmbedYoutubeStyles = ['video'] as const satisfies EmbedCardStyle[];
|
||||
|
||||
export type EmbedYoutubeBlockProps = {
|
||||
style: (typeof EmbedYoutubeStyles)[number];
|
||||
|
||||
@@ -80,4 +80,8 @@ describe('isValidUrl: determining whether a URL is valid is very complicated', (
|
||||
// See also https://stackoverflow.com/questions/9238640/how-long-can-a-tld-possibly-be#:~:text=Longest%20TLD%20up%20to%20date,17%20when%20decoded%20%5Bverm%C3%B6gensberatung%5D.
|
||||
expect(isValidUrl('example.xn--vermgensberatung-pwb')).toEqual(false);
|
||||
});
|
||||
|
||||
test('should allow ip address url when origin is same', () => {
|
||||
expect(isValidUrl('http://127.0.0.1', 'http://127.0.0.1')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ const handlePoint = (
|
||||
};
|
||||
|
||||
const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
|
||||
slots.afterExport.subscribe(payload => {
|
||||
const afterExportSubscription = slots.afterExport.subscribe(payload => {
|
||||
if (payload.type === 'block') {
|
||||
const snapshot = payload.snapshot;
|
||||
|
||||
@@ -53,10 +53,14 @@ const sliceText = (slots: TransformerSlots, std: EditorHost['std']) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
afterExportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
export const copyMiddleware = (std: BlockStdScope): TransformerMiddleware => {
|
||||
return ({ slots }) => {
|
||||
sliceText(slots, std);
|
||||
return sliceText(slots, std);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
export const fileNameMiddleware =
|
||||
(fileName?: string): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type !== 'page') {
|
||||
return;
|
||||
}
|
||||
@@ -20,4 +20,8 @@ export const fileNameMiddleware =
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeImportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -528,7 +528,7 @@ export const pasteMiddleware = (
|
||||
): TransformerMiddleware => {
|
||||
return ({ slots }) => {
|
||||
let tr: PasteTr | undefined;
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
const beforeImportSubscription = slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
const { snapshot } = payload;
|
||||
flatNote(snapshot);
|
||||
@@ -543,13 +543,18 @@ export const pasteMiddleware = (
|
||||
}
|
||||
}
|
||||
});
|
||||
slots.afterImport.subscribe(payload => {
|
||||
const afterImportSubscription = slots.afterImport.subscribe(payload => {
|
||||
if (tr && payload.type === 'slice') {
|
||||
tr.pasted();
|
||||
tr.focusPasted();
|
||||
tr.convertToLinkedDoc();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeImportSubscription.unsubscribe();
|
||||
afterImportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const replaceIdMiddleware =
|
||||
map(({ model }) => model)
|
||||
);
|
||||
|
||||
afterImportBlock$
|
||||
const afterImportBlockSubscription = afterImportBlock$
|
||||
.pipe(filter(model => matchModels(model, [DatabaseBlockModel])))
|
||||
.subscribe(model => {
|
||||
Object.keys(model.props.cells).forEach(cellId => {
|
||||
@@ -44,7 +44,7 @@ export const replaceIdMiddleware =
|
||||
});
|
||||
|
||||
// replace LinkedPage pageId with new id in paragraph blocks
|
||||
afterImportBlock$
|
||||
const replaceLinkedPageIdSubscription = afterImportBlock$
|
||||
.pipe(
|
||||
filter(model =>
|
||||
matchModels(model, [ParagraphBlockModel, ListBlockModel])
|
||||
@@ -84,7 +84,7 @@ export const replaceIdMiddleware =
|
||||
}
|
||||
});
|
||||
|
||||
afterImportBlock$
|
||||
const replaceSurfaceRefIdSubscription = afterImportBlock$
|
||||
.pipe(filter(model => matchModels(model, [SurfaceRefBlockModel])))
|
||||
.subscribe(model => {
|
||||
const original = model.props.reference;
|
||||
@@ -105,7 +105,7 @@ export const replaceIdMiddleware =
|
||||
});
|
||||
|
||||
// TODO(@fundon): process linked block/element
|
||||
afterImportBlock$
|
||||
const replaceLinkedDocIdSubscription = afterImportBlock$
|
||||
.pipe(
|
||||
filter(model =>
|
||||
matchModels(model, [EmbedLinkedDocModel, EmbedSyncedDocModel])
|
||||
@@ -128,7 +128,7 @@ export const replaceIdMiddleware =
|
||||
|
||||
// Before Import
|
||||
|
||||
slots.beforeImport
|
||||
const beforeImportPageSubscription = slots.beforeImport
|
||||
.pipe(filter(payload => payload.type === 'page'))
|
||||
.subscribe(payload => {
|
||||
if (idMap.has(payload.snapshot.meta.id)) {
|
||||
@@ -140,7 +140,7 @@ export const replaceIdMiddleware =
|
||||
payload.snapshot.meta.id = newId;
|
||||
});
|
||||
|
||||
slots.beforeImport
|
||||
const beforeImportBlockSubscription = slots.beforeImport
|
||||
.pipe(
|
||||
filter(
|
||||
(payload): payload is BeforeImportBlockPayload =>
|
||||
@@ -244,4 +244,13 @@ export const replaceIdMiddleware =
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
afterImportBlockSubscription.unsubscribe();
|
||||
replaceLinkedPageIdSubscription.unsubscribe();
|
||||
replaceSurfaceRefIdSubscription.unsubscribe();
|
||||
replaceLinkedDocIdSubscription.unsubscribe();
|
||||
beforeImportPageSubscription.unsubscribe();
|
||||
beforeImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,33 +5,42 @@ export const surfaceRefToEmbed =
|
||||
(std: BlockStdScope): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
let pageId: string | null = null;
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
pageId = payload.snapshot.pageId;
|
||||
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
|
||||
payload => {
|
||||
if (payload.type === 'slice') {
|
||||
pageId = payload.snapshot.pageId;
|
||||
}
|
||||
}
|
||||
});
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
// only handle surface-ref block snapshot
|
||||
if (
|
||||
payload.type !== 'block' ||
|
||||
payload.snapshot.flavour !== 'affine:surface-ref'
|
||||
)
|
||||
return;
|
||||
);
|
||||
const beforeImportBlockSubscription = slots.beforeImport.subscribe(
|
||||
payload => {
|
||||
// only handle surface-ref block snapshot
|
||||
if (
|
||||
payload.type !== 'block' ||
|
||||
payload.snapshot.flavour !== 'affine:surface-ref'
|
||||
)
|
||||
return;
|
||||
|
||||
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
|
||||
const isNotSameDoc = pageId !== std.store.doc.id;
|
||||
if (pageId && isNotSameDoc) {
|
||||
// The blockId of the original surface-ref block
|
||||
const blockId = payload.snapshot.id;
|
||||
payload.snapshot.id = std.workspace.idGenerator();
|
||||
payload.snapshot.flavour = 'affine:embed-linked-doc';
|
||||
payload.snapshot.props = {
|
||||
pageId,
|
||||
params: {
|
||||
mode: 'page',
|
||||
blockIds: [blockId],
|
||||
},
|
||||
};
|
||||
// turn into embed-linked-doc if the current doc is different from the pageId of the surface-ref block
|
||||
const isNotSameDoc = pageId !== std.store.doc.id;
|
||||
if (pageId && isNotSameDoc) {
|
||||
// The blockId of the original surface-ref block
|
||||
const blockId = payload.snapshot.id;
|
||||
payload.snapshot.id = std.workspace.idGenerator();
|
||||
payload.snapshot.flavour = 'affine:embed-linked-doc';
|
||||
payload.snapshot.props = {
|
||||
pageId,
|
||||
params: {
|
||||
mode: 'page',
|
||||
blockIds: [blockId],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return () => {
|
||||
beforeImportSliceSubscription.unsubscribe();
|
||||
beforeImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,9 +3,13 @@ import type { DocMeta, TransformerMiddleware } from '@blocksuite/store';
|
||||
export const titleMiddleware =
|
||||
(metas: DocMeta[]): TransformerMiddleware =>
|
||||
({ slots, adapterConfigs }) => {
|
||||
slots.beforeExport.subscribe(() => {
|
||||
const beforeExportSubscription = slots.beforeExport.subscribe(() => {
|
||||
for (const meta of metas) {
|
||||
adapterConfigs.set('title:' + meta.id, meta.title);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeExportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -79,7 +79,7 @@ export const uploadMiddleware = (
|
||||
}
|
||||
}
|
||||
|
||||
blockView$
|
||||
const blockViewSubscription = blockView$
|
||||
.pipe(
|
||||
map(payload => {
|
||||
if (assetsManager.uploadingAssetsMap.size === 0) return null;
|
||||
@@ -110,5 +110,9 @@ export const uploadMiddleware = (
|
||||
)
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
return () => {
|
||||
blockViewSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { type Container, createIdentifier } from '@blocksuite/global/di';
|
||||
import { type BlockStdScope, StdIdentifier } from '@blocksuite/std';
|
||||
import { type BlockModel, Extension } from '@blocksuite/store';
|
||||
|
||||
import { DocModeProvider } from '../doc-mode-service';
|
||||
import type {
|
||||
CitationEvents,
|
||||
CitationEventType,
|
||||
} from '../telemetry-service/citation';
|
||||
import { TelemetryProvider } from '../telemetry-service/telemetry-service';
|
||||
|
||||
const CitationEventTypeMap = {
|
||||
Hover: 'AICitationHoverSource',
|
||||
Expand: 'AICitationExpandSource',
|
||||
Delete: 'AICitationDelete',
|
||||
Edit: 'AICitationEdit',
|
||||
} as const;
|
||||
|
||||
type EventType = keyof typeof CitationEventTypeMap;
|
||||
|
||||
type EventTypeMapping = {
|
||||
[K in EventType]: CitationEventType;
|
||||
};
|
||||
|
||||
export interface CitationViewService {
|
||||
/**
|
||||
* Tracks citation-related events
|
||||
* @param type - The type of citation event to track
|
||||
* @param properties - The properties of the event
|
||||
*/
|
||||
trackEvent<T extends EventType>(
|
||||
type: T,
|
||||
properties?: CitationEvents[EventTypeMapping[T]]
|
||||
): void;
|
||||
/**
|
||||
* Checks if the model is a citation model
|
||||
* @param model - The model to check
|
||||
* @returns True if the model is a citation model, false otherwise
|
||||
*/
|
||||
isCitationModel(model: BlockModel): boolean;
|
||||
}
|
||||
|
||||
export const CitationProvider =
|
||||
createIdentifier<CitationViewService>('CitationService');
|
||||
|
||||
export class CitationService extends Extension implements CitationViewService {
|
||||
constructor(private readonly std: BlockStdScope) {
|
||||
super();
|
||||
}
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(CitationProvider, CitationService, [StdIdentifier]);
|
||||
}
|
||||
|
||||
get docModeService() {
|
||||
return this.std.getOptional(DocModeProvider);
|
||||
}
|
||||
|
||||
get telemetryService() {
|
||||
return this.std.getOptional(TelemetryProvider);
|
||||
}
|
||||
|
||||
isCitationModel = (model: BlockModel) => {
|
||||
return (
|
||||
'footnoteIdentifier' in model.props &&
|
||||
!!model.props.footnoteIdentifier &&
|
||||
'style' in model.props &&
|
||||
model.props.style === 'citation'
|
||||
);
|
||||
};
|
||||
|
||||
trackEvent<T extends EventType>(
|
||||
type: T,
|
||||
properties?: CitationEvents[EventTypeMapping[T]]
|
||||
) {
|
||||
const editorMode = this.docModeService?.getEditorMode() ?? 'page';
|
||||
this.telemetryService?.track(CitationEventTypeMap[type], {
|
||||
page: editorMode === 'page' ? 'doc editor' : 'whiteboard editor',
|
||||
module: 'AI Result',
|
||||
control: 'Source',
|
||||
...properties,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './citation-service';
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './auto-clear-selection-service';
|
||||
export * from './block-meta-service';
|
||||
export * from './citation-service';
|
||||
export * from './doc-display-meta-service';
|
||||
export * from './doc-mode-service';
|
||||
export * from './drag-handle-config';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { TelemetryEvent } from './types';
|
||||
export type CitationEventType =
|
||||
| 'AICitationHoverSource'
|
||||
| 'AICitationExpandSource'
|
||||
| 'AICitationDelete'
|
||||
| 'AICitationEdit';
|
||||
|
||||
export type CitationEvents = Record<CitationEventType, TelemetryEvent>;
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './citation.js';
|
||||
export * from './database.js';
|
||||
export * from './link.js';
|
||||
export * from './telemetry-service.js';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import type { CitationEvents } from './citation.js';
|
||||
import type { CodeBlockEvents } from './code-block.js';
|
||||
import type { OutDatabaseAllEvents } from './database.js';
|
||||
import type { LinkToolbarEvents } from './link.js';
|
||||
@@ -28,7 +29,8 @@ export type TelemetryEventMap = OutDatabaseAllEvents &
|
||||
LinkToolbarEvents &
|
||||
SlashMenuEvents &
|
||||
CodeBlockEvents &
|
||||
NoteEvents & {
|
||||
NoteEvents &
|
||||
CitationEvents & {
|
||||
DocCreated: DocCreatedEvent;
|
||||
Link: TelemetryEvent;
|
||||
LinkedDocCreated: LinkedDocCreatedEvent;
|
||||
|
||||
@@ -11,6 +11,9 @@ const ALLOWED_SCHEMES = new Set([
|
||||
// https://publicsuffix.org/
|
||||
const TLD_REGEXP = /(?:\.[a-zA-Z]+)?(\.[a-zA-Z]{2,})$/;
|
||||
|
||||
const IPV4_ADDR_REGEXP =
|
||||
/^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$/;
|
||||
|
||||
const toURL = (str: string) => {
|
||||
try {
|
||||
if (!URL.canParse(str)) return null;
|
||||
@@ -21,16 +24,20 @@ const toURL = (str: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
function resolveURL(str: string) {
|
||||
function resolveURL(str: string, baseUrl: string, padded = false) {
|
||||
const url = toURL(str);
|
||||
if (!url) return null;
|
||||
|
||||
const protocol = url.protocol.substring(0, url.protocol.length - 1);
|
||||
const hostname = url.hostname;
|
||||
const origin = url.origin;
|
||||
|
||||
let allowed = ALLOWED_SCHEMES.has(protocol);
|
||||
if (allowed && hostname.includes('.')) {
|
||||
allowed = TLD_REGEXP.test(hostname);
|
||||
allowed =
|
||||
origin === baseUrl ||
|
||||
TLD_REGEXP.test(hostname) ||
|
||||
(padded ? false : IPV4_ADDR_REGEXP.test(hostname));
|
||||
}
|
||||
|
||||
return { url, allowed };
|
||||
@@ -68,10 +75,10 @@ export function normalizeUrl(str: string) {
|
||||
*
|
||||
* For more detail see https://www.ietf.org/rfc/rfc1738.txt
|
||||
*/
|
||||
export function isValidUrl(str: string) {
|
||||
export function isValidUrl(str: string, baseUrl = location.origin) {
|
||||
str = str.trim();
|
||||
|
||||
let result = resolveURL(str);
|
||||
let result = resolveURL(str, baseUrl);
|
||||
|
||||
if (result && !result.allowed) return false;
|
||||
|
||||
@@ -80,7 +87,7 @@ export function isValidUrl(str: string) {
|
||||
if (!hasScheme) {
|
||||
const dotIdx = str.indexOf('.');
|
||||
if (dotIdx > 0 && dotIdx < str.length - 1) {
|
||||
result = resolveURL(`https://${str}`);
|
||||
result = resolveURL(`https://${str}`, baseUrl, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export const gfxBlocksFilter = (
|
||||
}
|
||||
|
||||
return ({ slots, transformerConfigs }) => {
|
||||
slots.beforeExport.subscribe(payload => {
|
||||
const beforeExportSubscription = slots.beforeExport.subscribe(payload => {
|
||||
if (payload.type !== 'block') {
|
||||
return;
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export const gfxBlocksFilter = (
|
||||
}
|
||||
});
|
||||
|
||||
slots.afterExport.subscribe(payload => {
|
||||
const afterExportSubscription = slots.afterExport.subscribe(payload => {
|
||||
if (payload.type !== 'block') {
|
||||
return;
|
||||
}
|
||||
@@ -110,5 +110,10 @@ export const gfxBlocksFilter = (
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
beforeExportSubscription.unsubscribe();
|
||||
afterExportSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
AttachmentBlockModel,
|
||||
BookmarkBlockModel,
|
||||
EmbedGithubModel,
|
||||
EmbedLinkedDocModel,
|
||||
NoteBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { matchModels } from '@blocksuite/affine-shared/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
|
||||
export const cardStyleUpdater =
|
||||
(std: BlockStdScope): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type !== 'block' || !payload.parent) return;
|
||||
const parentModel = std.store.getModelById(payload.parent);
|
||||
if (!matchModels(parentModel, [NoteBlockModel])) return;
|
||||
|
||||
// TODO(@L-Sun): Refactor this after refactor `store.moveBlocks`
|
||||
// Currently, drag a block will use store.moveBlocks to update the tree of blocks
|
||||
// but the instance of it is not changed.
|
||||
// So change the style of snapshot.props in the middleware is not working.
|
||||
// Instead, we can change the style of the model instance in the middleware,
|
||||
const model = std.store.getModelById(payload.snapshot.id);
|
||||
if (!model) return;
|
||||
|
||||
if (model instanceof AttachmentBlockModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontalThin',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (model instanceof BookmarkBlockModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontal',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (model instanceof EmbedGithubModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontal',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (model instanceof EmbedLinkedDocModel) {
|
||||
std.store.updateBlock(model, {
|
||||
style: 'horizontal',
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -9,36 +9,45 @@ export const newIdCrossDoc =
|
||||
let samePage = false;
|
||||
const oldToNewIdMap = new Map<string, string>();
|
||||
|
||||
slots.beforeImport.subscribe(payload => {
|
||||
if (payload.type === 'slice') {
|
||||
samePage = payload.snapshot.pageId === std.store.id;
|
||||
const beforeImportSliceSubscription = slots.beforeImport.subscribe(
|
||||
payload => {
|
||||
if (payload.type === 'slice') {
|
||||
samePage = payload.snapshot.pageId === std.store.id;
|
||||
}
|
||||
if (payload.type === 'block' && !samePage) {
|
||||
const newId = std.workspace.idGenerator();
|
||||
|
||||
oldToNewIdMap.set(payload.snapshot.id, newId);
|
||||
payload.snapshot.id = newId;
|
||||
}
|
||||
}
|
||||
if (payload.type === 'block' && !samePage) {
|
||||
const newId = std.workspace.idGenerator();
|
||||
);
|
||||
|
||||
oldToNewIdMap.set(payload.snapshot.id, newId);
|
||||
payload.snapshot.id = newId;
|
||||
const afterImportBlockSubscription = slots.afterImport.subscribe(
|
||||
payload => {
|
||||
if (
|
||||
!samePage &&
|
||||
payload.type === 'block' &&
|
||||
matchModels(payload.model, [DatabaseBlockModel])
|
||||
) {
|
||||
const originalCells = payload.model.props.cells;
|
||||
const newCells = {
|
||||
...originalCells,
|
||||
};
|
||||
|
||||
Object.keys(originalCells).forEach(cellId => {
|
||||
if (oldToNewIdMap.has(cellId)) {
|
||||
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
|
||||
}
|
||||
});
|
||||
|
||||
payload.model.props.cells$.value = newCells;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
slots.afterImport.subscribe(payload => {
|
||||
if (
|
||||
!samePage &&
|
||||
payload.type === 'block' &&
|
||||
matchModels(payload.model, [DatabaseBlockModel])
|
||||
) {
|
||||
const originalCells = payload.model.props.cells;
|
||||
const newCells = {
|
||||
...originalCells,
|
||||
};
|
||||
|
||||
Object.keys(originalCells).forEach(cellId => {
|
||||
if (oldToNewIdMap.has(cellId)) {
|
||||
newCells[oldToNewIdMap.get(cellId)!] = originalCells[cellId];
|
||||
}
|
||||
});
|
||||
|
||||
payload.model.props.cells$.value = newCells;
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
beforeImportSliceSubscription.unsubscribe();
|
||||
afterImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,19 +7,25 @@ import type { TransformerMiddleware } from '@blocksuite/store';
|
||||
export const reorderList =
|
||||
(std: BlockStdScope): TransformerMiddleware =>
|
||||
({ slots }) => {
|
||||
slots.afterImport.subscribe(payload => {
|
||||
if (payload.type === 'block') {
|
||||
const model = payload.model;
|
||||
if (
|
||||
matchModels(model, [ListBlockModel]) &&
|
||||
model.props.type === 'numbered'
|
||||
) {
|
||||
const next = std.store.getNext(model);
|
||||
correctNumberedListsOrderToPrev(std.store, model);
|
||||
if (next) {
|
||||
correctNumberedListsOrderToPrev(std.store, next);
|
||||
const afterImportBlockSubscription = slots.afterImport.subscribe(
|
||||
payload => {
|
||||
if (payload.type === 'block') {
|
||||
const model = payload.model;
|
||||
if (
|
||||
matchModels(model, [ListBlockModel]) &&
|
||||
model.props.type === 'numbered'
|
||||
) {
|
||||
const next = std.store.getNext(model);
|
||||
correctNumberedListsOrderToPrev(std.store, model);
|
||||
if (next) {
|
||||
correctNumberedListsOrderToPrev(std.store, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
return () => {
|
||||
afterImportBlockSubscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
|
||||
@@ -76,6 +76,7 @@ import last from 'lodash-es/last';
|
||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||
import { PreviewHelper } from '../helpers/preview-helper.js';
|
||||
import { gfxBlocksFilter } from '../middleware/blocks-filter.js';
|
||||
import { cardStyleUpdater } from '../middleware/card-style-updater.js';
|
||||
import { newIdCrossDoc } from '../middleware/new-id-cross-doc.js';
|
||||
import { reorderList } from '../middleware/reorder-list';
|
||||
import {
|
||||
@@ -1433,6 +1434,7 @@ export class DragEventWatcher {
|
||||
newIdCrossDoc(std),
|
||||
reorderList(std),
|
||||
surfaceRefToEmbed(std),
|
||||
cardStyleUpdater(std),
|
||||
];
|
||||
|
||||
if (selectedIds) {
|
||||
|
||||
@@ -374,6 +374,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
type: 'resize' | 'rotate';
|
||||
angle: number;
|
||||
handle: ResizeHandle;
|
||||
flipX?: boolean;
|
||||
flipY?: boolean;
|
||||
pure?: boolean;
|
||||
}) => {
|
||||
if (!options) {
|
||||
@@ -381,8 +383,25 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
return 'default';
|
||||
}
|
||||
|
||||
const { type, angle, handle } = options;
|
||||
const { type, angle, flipX, flipY } = options;
|
||||
let cursor: CursorType = 'default';
|
||||
let handle: ResizeHandle = options.handle;
|
||||
|
||||
if (flipX) {
|
||||
handle = (
|
||||
handle.includes('left')
|
||||
? handle.replace('left', 'right')
|
||||
: handle.replace('right', 'left')
|
||||
) as ResizeHandle;
|
||||
}
|
||||
|
||||
if (flipY) {
|
||||
handle = (
|
||||
handle.includes('top')
|
||||
? handle.replace('top', 'bottom')
|
||||
: handle.replace('bottom', 'top')
|
||||
) as ResizeHandle;
|
||||
}
|
||||
|
||||
if (type === 'rotate') {
|
||||
cursor = generateCursorUrl(angle, handle);
|
||||
@@ -626,7 +645,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
onResizeStart: () => {
|
||||
this._mode = 'resize';
|
||||
},
|
||||
onResizeUpdate: ({ lockRatio, scaleX, exceed }) => {
|
||||
onResizeUpdate: ({ lockRatio, scaleX, scaleY, exceed }) => {
|
||||
if (lockRatio) {
|
||||
this._scaleDirection = handle;
|
||||
this._scalePercent = `${Math.round(scaleX * 100)}%`;
|
||||
@@ -642,6 +661,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
type: 'resize',
|
||||
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
|
||||
handle,
|
||||
flipX: scaleX < 0,
|
||||
flipY: scaleY < 0,
|
||||
});
|
||||
},
|
||||
onResizeEnd: () => {
|
||||
@@ -652,6 +673,14 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
}
|
||||
},
|
||||
option => {
|
||||
if (
|
||||
['resize', 'rotate'].includes(
|
||||
interaction.activeInteraction$.value?.type ?? ''
|
||||
)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this._updateCursor({
|
||||
...option,
|
||||
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model';
|
||||
import { type FrameBlockModel } from '@blocksuite/affine-model';
|
||||
import { WidgetComponent, WidgetViewExtension } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { repeat } from 'lit/directives/repeat.js';
|
||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||
|
||||
import type { AffineFrameTitle } from './frame-title.js';
|
||||
|
||||
export const AFFINE_FRAME_TITLE_WIDGET = 'affine-frame-title-widget';
|
||||
|
||||
export class AffineFrameTitleWidget extends WidgetComponent<RootBlockModel> {
|
||||
private get _frames() {
|
||||
return Object.values(this.store.blocks.value)
|
||||
.map(({ model }) => model)
|
||||
.filter(model => model instanceof FrameBlockModel);
|
||||
}
|
||||
|
||||
getFrameTitle(frame: FrameBlockModel | string) {
|
||||
const id = typeof frame === 'string' ? frame : frame.id;
|
||||
const frameTitle = this.shadowRoot?.querySelector(
|
||||
`affine-frame-title[data-id="${id}"]`
|
||||
) as AffineFrameTitle | null;
|
||||
return frameTitle;
|
||||
}
|
||||
|
||||
export class AffineFrameTitleWidget extends WidgetComponent<FrameBlockModel> {
|
||||
override render() {
|
||||
return repeat(
|
||||
this._frames,
|
||||
({ id }) => id,
|
||||
frame =>
|
||||
html`<affine-frame-title
|
||||
.model=${frame}
|
||||
data-id=${frame.id}
|
||||
></affine-frame-title>`
|
||||
);
|
||||
return html`<affine-frame-title
|
||||
.model=${this.model}
|
||||
data-id=${this.model.id}
|
||||
></affine-frame-title>`;
|
||||
}
|
||||
}
|
||||
|
||||
export const frameTitleWidget = WidgetViewExtension(
|
||||
'affine:page',
|
||||
'affine:frame',
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}`
|
||||
);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
type AffineFrameTitleWidget,
|
||||
} from './affine-frame-title-widget';
|
||||
import type { AffineFrameTitle } from './frame-title';
|
||||
import { frameTitleStyleVars } from './styles';
|
||||
|
||||
export class EdgelessFrameTitleEditor extends WithDisposable(
|
||||
@@ -135,12 +136,13 @@ export class EdgelessFrameTitleEditor extends WithDisposable(
|
||||
|
||||
const frameTitleWidget = this.edgeless.std.view.getWidget(
|
||||
AFFINE_FRAME_TITLE_WIDGET,
|
||||
rootBlockId
|
||||
this.frameModel.id
|
||||
) as AffineFrameTitleWidget | null;
|
||||
|
||||
if (!frameTitleWidget) return nothing;
|
||||
|
||||
const frameTitle = frameTitleWidget.getFrameTitle(this.frameModel);
|
||||
const frameTitle =
|
||||
frameTitleWidget.querySelector<AffineFrameTitle>('affine-frame-title');
|
||||
|
||||
const colors = frameTitle?.colors ?? {
|
||||
background: cssVarV2('edgeless/frame/background/white'),
|
||||
|
||||
@@ -142,12 +142,10 @@ export class AffineFrameTitle extends SignalWatcher(
|
||||
}px)`,
|
||||
];
|
||||
|
||||
const anchor = this.gfx.viewport.toViewCoord(bound.x, bound.y);
|
||||
|
||||
this.style.display = '';
|
||||
this.style.setProperty('--bg-color', this.colors.background);
|
||||
this.style.left = `${anchor[0]}px`;
|
||||
this.style.top = `${anchor[1]}px`;
|
||||
this.style.left = '0px';
|
||||
this.style.top = '0px';
|
||||
this.style.display = hidden ? 'none' : 'flex';
|
||||
this.style.transform = transformOperation.join(' ');
|
||||
this.style.maxWidth = `${maxWidth}px`;
|
||||
@@ -205,18 +203,6 @@ export class AffineFrameTitle extends SignalWatcher(
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
on(this, 'click', evt => {
|
||||
if (evt.shiftKey) {
|
||||
this.gfx.selection.toggle(this.model);
|
||||
} else {
|
||||
this.gfx.selection.set({
|
||||
elements: [this.model.id],
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
_disposables.add(
|
||||
on(this, 'dblclick', () => {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root?.id || '');
|
||||
|
||||
@@ -566,23 +566,29 @@ Optional flag to insert before sibling
|
||||
|
||||
### updateBlock()
|
||||
|
||||
> **updateBlock**(`modelOrId`, `callBackOrProps`): `void`
|
||||
> **updateBlock**\<`T`\>(`modelOrId`, `callBackOrProps`): `void`
|
||||
|
||||
Updates a block's properties or executes a callback in a transaction
|
||||
|
||||
#### Type Parameters
|
||||
|
||||
##### T
|
||||
|
||||
`T` *extends* `BlockModel`\<`object`\> = `BlockModel`\<`object`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
##### modelOrId
|
||||
|
||||
The block model or block ID to update
|
||||
|
||||
`string` | `BlockModel`\<`object`\>
|
||||
`string` | `T`
|
||||
|
||||
##### callBackOrProps
|
||||
|
||||
Either a callback function to execute or properties to update
|
||||
|
||||
`Partial`\<`BlockProps`\> | () => `void`
|
||||
() => `void` | `Partial`\<`BlockProps` \| `PropsOfModel`\<`T`\> & `BlockSysProps`\>
|
||||
|
||||
#### Returns
|
||||
|
||||
|
||||
@@ -220,6 +220,12 @@ export class UIEventDispatcher extends LifeCycleWatcher {
|
||||
|
||||
this._setActive(false);
|
||||
});
|
||||
// When the document is hidden, the event dispatcher should be inactive
|
||||
this.disposables.addFromEvent(document, 'visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
this._setActive(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _buildEventScopeBySelection(name: EventName) {
|
||||
|
||||
@@ -947,23 +947,34 @@ export class InteractivityManager extends GfxExtension {
|
||||
...options,
|
||||
lockRatio,
|
||||
elements,
|
||||
onResizeMove: ({ dx, dy, handleSign, lockRatio }) => {
|
||||
onResizeMove: ({
|
||||
scaleX,
|
||||
scaleY,
|
||||
originalBound,
|
||||
handleSign,
|
||||
handlePos,
|
||||
currentHandlePos,
|
||||
lockRatio,
|
||||
}) => {
|
||||
const suggested: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
priority?: number;
|
||||
}[] = [];
|
||||
const suggest = (distance: { dx: number; dy: number }) => {
|
||||
const suggest = (distance: { scaleX: number; scaleY: number }) => {
|
||||
suggested.push(distance);
|
||||
};
|
||||
|
||||
extensionHandlers.forEach(ext => {
|
||||
ext.onResizeMove?.({
|
||||
dx,
|
||||
dy,
|
||||
scaleX,
|
||||
scaleY,
|
||||
elements,
|
||||
handleSign,
|
||||
handle,
|
||||
handleSign,
|
||||
handlePos,
|
||||
originalBound,
|
||||
currentHandlePos,
|
||||
lockRatio,
|
||||
suggest,
|
||||
});
|
||||
@@ -973,9 +984,9 @@ export class InteractivityManager extends GfxExtension {
|
||||
return (a.priority ?? 0) - (b.priority ?? 0);
|
||||
});
|
||||
|
||||
return last(suggested) ?? { dx, dy };
|
||||
return last(suggested) ?? { scaleX, scaleY };
|
||||
},
|
||||
onResizeStart: ({ data }) => {
|
||||
onResizeStart: ({ handleSign, handlePos, data }) => {
|
||||
this.activeInteraction$.value = {
|
||||
type: 'resize',
|
||||
elements,
|
||||
@@ -984,6 +995,8 @@ export class InteractivityManager extends GfxExtension {
|
||||
ext.onResizeStart?.({
|
||||
elements,
|
||||
handle,
|
||||
handlePos,
|
||||
handleSign,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1045,13 +1058,15 @@ export class InteractivityManager extends GfxExtension {
|
||||
|
||||
options.onResizeUpdate?.({ scaleX, scaleY, lockRatio, exceed });
|
||||
},
|
||||
onResizeEnd: ({ data }) => {
|
||||
onResizeEnd: ({ handleSign, handlePos, data }) => {
|
||||
this.activeInteraction$.value = null;
|
||||
|
||||
extensionHandlers.forEach(ext => {
|
||||
ext.onResizeEnd?.({
|
||||
elements,
|
||||
handle,
|
||||
handlePos,
|
||||
handleSign,
|
||||
});
|
||||
});
|
||||
options.onResizeEnd?.();
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Bound,
|
||||
getCommonBoundWithRotation,
|
||||
type IBound,
|
||||
type IPoint,
|
||||
type IVec,
|
||||
} from '@blocksuite/global/gfx';
|
||||
|
||||
@@ -29,7 +30,7 @@ export const DEFAULT_HANDLES: ResizeHandle[] = [
|
||||
'bottom',
|
||||
];
|
||||
|
||||
type ElementInitialSnapshot = Readonly<Required<IBound>>;
|
||||
type ReadonlyIBound = Readonly<Required<IBound>>;
|
||||
|
||||
export interface OptionResize {
|
||||
elements: GfxModel[];
|
||||
@@ -37,16 +38,18 @@ export interface OptionResize {
|
||||
lockRatio: boolean;
|
||||
event: PointerEvent;
|
||||
onResizeMove: (payload: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
|
||||
handleSign: {
|
||||
xSign: number;
|
||||
ySign: number;
|
||||
};
|
||||
originalBound: IBound;
|
||||
|
||||
handleSign: IPoint;
|
||||
|
||||
handlePos: IVec;
|
||||
currentHandlePos: IVec;
|
||||
|
||||
lockRatio: boolean;
|
||||
}) => { dx: number; dy: number };
|
||||
}) => { scaleX: number; scaleY: number };
|
||||
onResizeUpdate: (payload: {
|
||||
lockRatio: boolean;
|
||||
scaleX: number;
|
||||
@@ -59,8 +62,16 @@ export interface OptionResize {
|
||||
matrix: DOMMatrix;
|
||||
}[];
|
||||
}) => void;
|
||||
onResizeStart?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||
onResizeEnd?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||
onResizeStart?: (payload: {
|
||||
handlePos: IVec;
|
||||
handleSign: IPoint;
|
||||
data: { model: GfxModel }[];
|
||||
}) => void;
|
||||
onResizeEnd?: (payload: {
|
||||
handlePos: IVec;
|
||||
handleSign: IPoint;
|
||||
data: { model: GfxModel }[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export type RotateOption = {
|
||||
@@ -95,11 +106,102 @@ export class ResizeController {
|
||||
this.gfx = option.gfx;
|
||||
}
|
||||
|
||||
getCoordsTransform(originalBound: IBound, handle: ResizeHandle) {
|
||||
const { x: xSign, y: ySign } = this.getHandleSign(handle);
|
||||
const pivot = new DOMPoint(
|
||||
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
|
||||
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
|
||||
);
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
const toLocalRotatedM = new DOMMatrix()
|
||||
.translate(-pivot.x, -pivot.y)
|
||||
.translate(
|
||||
originalBound.w / 2 + originalBound.x,
|
||||
originalBound.h / 2 + originalBound.y
|
||||
)
|
||||
.rotate(-(originalBound.rotate ?? 0))
|
||||
.translate(
|
||||
-(originalBound.w / 2 + originalBound.x),
|
||||
-(originalBound.h / 2 + originalBound.y)
|
||||
);
|
||||
|
||||
const toLocal = (p: DOMPoint, withRotation: boolean = false) =>
|
||||
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
|
||||
const toModel = (p: DOMPoint) =>
|
||||
p.matrixTransform(toLocalRotatedM.inverse());
|
||||
|
||||
const handlePos = toModel(
|
||||
new DOMPoint(originalBound.w * xSign, originalBound.h * ySign)
|
||||
);
|
||||
|
||||
return {
|
||||
xSign,
|
||||
ySign,
|
||||
originalBound,
|
||||
toLocalM,
|
||||
toLocalRotatedM,
|
||||
toLocal,
|
||||
toModel,
|
||||
handlePos: [handlePos.x, handlePos.y] as IVec,
|
||||
};
|
||||
}
|
||||
|
||||
getScaleFromDelta(
|
||||
transform: ReturnType<ResizeController['getCoordsTransform']>,
|
||||
delta: { dx: number; dy: number },
|
||||
handleStartPos: IVec,
|
||||
lockRatio: boolean
|
||||
) {
|
||||
const { originalBound, xSign, ySign, toModel, toLocal } = transform;
|
||||
const currentPos = toLocal(
|
||||
new DOMPoint(handleStartPos[0] + delta.dx, handleStartPos[1] + delta.dy),
|
||||
true
|
||||
);
|
||||
|
||||
let scaleX = xSign ? currentPos.x / (originalBound.w * xSign) : 1;
|
||||
let scaleY = ySign ? currentPos.y / (originalBound.h * ySign) : 1;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
const finalHandlePos = toModel(
|
||||
new DOMPoint(
|
||||
originalBound.w * xSign * scaleX,
|
||||
originalBound.h * ySign * scaleY
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
scaleX,
|
||||
scaleY,
|
||||
handlePos: [finalHandlePos.x, finalHandlePos.y] as IVec,
|
||||
};
|
||||
}
|
||||
|
||||
getScaleMatrix(
|
||||
{ scaleX, scaleY }: { scaleX: number; scaleY: number },
|
||||
lockRatio: boolean
|
||||
) {
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
return {
|
||||
scaleX,
|
||||
scaleY,
|
||||
scaleM: new DOMMatrix().scaleSelf(scaleX, scaleY),
|
||||
};
|
||||
}
|
||||
|
||||
startResize(options: OptionResize) {
|
||||
const {
|
||||
elements,
|
||||
handle,
|
||||
lockRatio,
|
||||
onResizeStart,
|
||||
onResizeMove,
|
||||
onResizeUpdate,
|
||||
@@ -107,19 +209,32 @@ export class ResizeController {
|
||||
event,
|
||||
} = options;
|
||||
|
||||
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||
const originals: ReadonlyIBound[] = elements.map(el => ({
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
w: el.w,
|
||||
h: el.h,
|
||||
rotate: el.rotate,
|
||||
}));
|
||||
const originalBound = getCommonBoundWithRotation(originals);
|
||||
const originalBound: IBound =
|
||||
originals.length > 1
|
||||
? getCommonBoundWithRotation(originals)
|
||||
: {
|
||||
x: originals[0].x,
|
||||
y: originals[0].y,
|
||||
w: originals[0].w,
|
||||
h: originals[0].h,
|
||||
rotate: originals[0].rotate,
|
||||
};
|
||||
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
]);
|
||||
const handleSign = this.getHandleSign(handle);
|
||||
const transform = this.getCoordsTransform(originalBound, handle);
|
||||
const handleSign = {
|
||||
x: transform.xSign,
|
||||
y: transform.ySign,
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const currPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||
@@ -130,45 +245,69 @@ export class ResizeController {
|
||||
dx: currPt[0] - startPt[0],
|
||||
dy: currPt[1] - startPt[1],
|
||||
};
|
||||
const shouldLockRatio = lockRatio || e.shiftKey;
|
||||
const shouldLockRatio =
|
||||
options.lockRatio || e.shiftKey || elements.length > 1;
|
||||
const {
|
||||
scaleX,
|
||||
scaleY,
|
||||
handlePos: currentHandlePos,
|
||||
} = this.getScaleFromDelta(
|
||||
transform,
|
||||
delta,
|
||||
transform.handlePos,
|
||||
shouldLockRatio
|
||||
);
|
||||
|
||||
const scale = onResizeMove({
|
||||
scaleX,
|
||||
scaleY,
|
||||
|
||||
originalBound,
|
||||
|
||||
delta = onResizeMove({
|
||||
dx: delta.dx,
|
||||
dy: delta.dy,
|
||||
handleSign,
|
||||
|
||||
handlePos: transform.handlePos,
|
||||
currentHandlePos,
|
||||
|
||||
lockRatio: shouldLockRatio,
|
||||
});
|
||||
const scaleInfo = this.getScaleMatrix(scale, shouldLockRatio);
|
||||
|
||||
if (elements.length === 1) {
|
||||
this.resizeSingle(
|
||||
originals[0],
|
||||
elements[0],
|
||||
shouldLockRatio,
|
||||
startPt,
|
||||
delta,
|
||||
handleSign,
|
||||
transform,
|
||||
scaleInfo,
|
||||
onResizeUpdate
|
||||
);
|
||||
} else {
|
||||
this.resizeMulti(
|
||||
originalBound,
|
||||
originals,
|
||||
elements,
|
||||
startPt,
|
||||
delta,
|
||||
handleSign,
|
||||
transform,
|
||||
scaleInfo,
|
||||
onResizeUpdate
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onResizeStart?.({ data: elements.map(model => ({ model })) });
|
||||
onResizeStart?.({
|
||||
handleSign,
|
||||
handlePos: transform.handlePos,
|
||||
data: elements.map(model => ({ model })),
|
||||
});
|
||||
|
||||
const onPointerUp = () => {
|
||||
this.host.removeEventListener('pointermove', onPointerMove);
|
||||
this.host.removeEventListener('pointerup', onPointerUp);
|
||||
|
||||
onResizeEnd?.({ data: elements.map(model => ({ model })) });
|
||||
onResizeEnd?.({
|
||||
handleSign,
|
||||
handlePos: transform.handlePos,
|
||||
data: elements.map(model => ({ model })),
|
||||
});
|
||||
};
|
||||
|
||||
this.host.addEventListener('pointermove', onPointerMove);
|
||||
@@ -176,55 +315,15 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private resizeSingle(
|
||||
orig: ElementInitialSnapshot,
|
||||
orig: ReadonlyIBound,
|
||||
model: GfxModel,
|
||||
lockRatio: boolean,
|
||||
startPt: IVec,
|
||||
delta: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
},
|
||||
handleSign: { xSign: number; ySign: number },
|
||||
transform: ReturnType<typeof ResizeController.prototype.getCoordsTransform>,
|
||||
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
|
||||
updateCallback: OptionResize['onResizeUpdate']
|
||||
) {
|
||||
const { xSign, ySign } = handleSign;
|
||||
|
||||
const pivot = new DOMPoint(
|
||||
orig.x + (-xSign === 1 ? orig.w : 0),
|
||||
orig.y + (-ySign === 1 ? orig.h : 0)
|
||||
);
|
||||
const toLocalRotatedM = new DOMMatrix()
|
||||
.translate(-pivot.x, -pivot.y)
|
||||
.translate(orig.w / 2 + orig.x, orig.h / 2 + orig.y)
|
||||
.rotate(-orig.rotate)
|
||||
.translate(-(orig.w / 2 + orig.x), -(orig.h / 2 + orig.y));
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
|
||||
const toLocal = (p: DOMPoint, withRotation: boolean) =>
|
||||
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
|
||||
const toModel = (p: DOMPoint) =>
|
||||
p.matrixTransform(toLocalRotatedM.inverse());
|
||||
|
||||
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]), true);
|
||||
const currPtLocal = toLocal(
|
||||
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy),
|
||||
true
|
||||
);
|
||||
|
||||
let scaleX = xSign
|
||||
? (xSign * (currPtLocal.x - handleLocal.x) + orig.w) / orig.w
|
||||
: 1;
|
||||
let scaleY = ySign
|
||||
? (ySign * (currPtLocal.y - handleLocal.y) + orig.h) / orig.h
|
||||
: 1;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||
const { toLocalM, toLocalRotatedM, toLocal, toModel } = transform;
|
||||
const { scaleX, scaleY, scaleM } = scale;
|
||||
|
||||
const [visualTopLeft, visualBottomRight] = [
|
||||
new DOMPoint(orig.x, orig.y),
|
||||
@@ -282,45 +381,14 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private resizeMulti(
|
||||
originalBound: Bound,
|
||||
originals: ElementInitialSnapshot[],
|
||||
originals: ReadonlyIBound[],
|
||||
elements: GfxModel[],
|
||||
startPt: IVec,
|
||||
delta: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
},
|
||||
handleSign: { xSign: number; ySign: number },
|
||||
transform: ReturnType<ResizeController['getCoordsTransform']>,
|
||||
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
|
||||
updateCallback: OptionResize['onResizeUpdate']
|
||||
) {
|
||||
const { xSign, ySign } = handleSign;
|
||||
const pivot = new DOMPoint(
|
||||
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
|
||||
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
|
||||
);
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
|
||||
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
|
||||
|
||||
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]));
|
||||
const currPtLocal = toLocal(
|
||||
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy)
|
||||
);
|
||||
|
||||
let scaleX = xSign
|
||||
? (xSign * (currPtLocal.x - handleLocal.x) + originalBound.w) /
|
||||
originalBound.w
|
||||
: 1;
|
||||
let scaleY = ySign
|
||||
? (ySign * (currPtLocal.y - handleLocal.y) + originalBound.h) /
|
||||
originalBound.h
|
||||
: 1;
|
||||
|
||||
const min = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
|
||||
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||
const { toLocalM } = transform;
|
||||
const { scaleX, scaleY, scaleM } = scale;
|
||||
|
||||
const data = elements.map((model, i) => {
|
||||
const orig = originals[i];
|
||||
@@ -357,7 +425,7 @@ export class ResizeController {
|
||||
startRotate(option: RotateOption) {
|
||||
const { event, elements, onRotateUpdate } = option;
|
||||
|
||||
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||
const originals: ReadonlyIBound[] = elements.map(el => ({
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
w: el.w,
|
||||
@@ -429,7 +497,7 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private rotateSingle(option: {
|
||||
orig: ElementInitialSnapshot;
|
||||
orig: ReadonlyIBound;
|
||||
model: GfxModel;
|
||||
startPt: IVec;
|
||||
currentPt: IVec;
|
||||
@@ -481,7 +549,7 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private rotateMulti(option: {
|
||||
origs: ElementInitialSnapshot[];
|
||||
origs: ReadonlyIBound[];
|
||||
models: GfxModel[];
|
||||
startPt: IVec;
|
||||
currentPt: IVec;
|
||||
@@ -567,23 +635,23 @@ export class ResizeController {
|
||||
private getHandleSign(handle: ResizeHandle) {
|
||||
switch (handle) {
|
||||
case 'top-left':
|
||||
return { xSign: -1, ySign: -1 };
|
||||
return { x: -1, y: -1 };
|
||||
case 'top':
|
||||
return { xSign: 0, ySign: -1 };
|
||||
return { x: 0, y: -1 };
|
||||
case 'top-right':
|
||||
return { xSign: 1, ySign: -1 };
|
||||
return { x: 1, y: -1 };
|
||||
case 'right':
|
||||
return { xSign: 1, ySign: 0 };
|
||||
return { x: 1, y: 0 };
|
||||
case 'bottom-right':
|
||||
return { xSign: 1, ySign: 1 };
|
||||
return { x: 1, y: 1 };
|
||||
case 'bottom':
|
||||
return { xSign: 0, ySign: 1 };
|
||||
return { x: 0, y: 1 };
|
||||
case 'bottom-left':
|
||||
return { xSign: -1, ySign: 1 };
|
||||
return { x: -1, y: 1 };
|
||||
case 'left':
|
||||
return { xSign: -1, ySign: 0 };
|
||||
return { x: -1, y: 0 };
|
||||
default:
|
||||
return { xSign: 0, ySign: 0 };
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { IBound, IPoint, IVec } from '@blocksuite/global/gfx';
|
||||
|
||||
import type { GfxModel } from '../../model/model';
|
||||
import type { ResizeHandle } from '../resize/manager';
|
||||
|
||||
@@ -8,6 +10,16 @@ export type ExtensionElementResizeContext = {
|
||||
export type ExtensionElementResizeStartContext = {
|
||||
elements: GfxModel[];
|
||||
|
||||
/**
|
||||
* The position of the handle in the browser coordinate space.
|
||||
*/
|
||||
handlePos: IVec;
|
||||
|
||||
/**
|
||||
* The sign (or normal vector) of the handle.
|
||||
*/
|
||||
handleSign: IPoint;
|
||||
|
||||
handle: ResizeHandle;
|
||||
};
|
||||
|
||||
@@ -16,15 +28,14 @@ export type ExtensionElementResizeEndContext =
|
||||
|
||||
export type ExtensionElementResizeMoveContext =
|
||||
ExtensionElementResizeStartContext & {
|
||||
dx: number;
|
||||
dy: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
|
||||
originalBound: IBound;
|
||||
|
||||
currentHandlePos: IVec;
|
||||
|
||||
lockRatio: boolean;
|
||||
|
||||
handleSign: {
|
||||
xSign: number;
|
||||
ySign: number;
|
||||
};
|
||||
|
||||
suggest: (distance: { dx: number; dy: number }) => void;
|
||||
suggest: (distance: { scaleX: number; scaleY: number }) => void;
|
||||
};
|
||||
|
||||
@@ -825,7 +825,7 @@ export class LayerManager extends GfxExtension {
|
||||
const block = store.getModelById(payload.id);
|
||||
|
||||
if (block instanceof GfxBlockElementModel) {
|
||||
this.delete(block as GfxBlockElementModel);
|
||||
this.delete(block);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -834,20 +834,29 @@ export class LayerManager extends GfxExtension {
|
||||
const watchSurface = (surface: SurfaceBlockModel) => {
|
||||
let lastChildMap = new Map(surface.childMap.peek());
|
||||
this._disposable.add(
|
||||
surface.childMap.subscribe(val => {
|
||||
val.forEach((_, id) => {
|
||||
surface.childMap.subscribe(currentChildMap => {
|
||||
currentChildMap.forEach((_, id) => {
|
||||
if (lastChildMap.has(id)) {
|
||||
lastChildMap.delete(id);
|
||||
return;
|
||||
}
|
||||
});
|
||||
lastChildMap.forEach((_, id) => {
|
||||
const block = this._doc.getBlock(id);
|
||||
if (block?.model) {
|
||||
this.delete(block.model as GfxBlockElementModel);
|
||||
const model = this._doc.getModelById(id);
|
||||
if (model instanceof GfxBlockElementModel) {
|
||||
this.delete(model);
|
||||
}
|
||||
});
|
||||
lastChildMap = new Map(val);
|
||||
currentChildMap.forEach((_, id) => {
|
||||
const model = store.getModelById(id);
|
||||
if (
|
||||
model instanceof GfxBlockElementModel &&
|
||||
!this.blocks.includes(model)
|
||||
) {
|
||||
this.add(model);
|
||||
}
|
||||
});
|
||||
lastChildMap = new Map(currentChildMap);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -19,3 +19,5 @@ export type BlockSysProps = {
|
||||
children?: BlockModel[];
|
||||
};
|
||||
export type BlockProps = BlockSysProps & Record<string, unknown>;
|
||||
|
||||
export type PropsOfModel<T> = T extends BlockModel<infer P> ? P : never;
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
type BlockModel,
|
||||
type BlockOptions,
|
||||
type BlockProps,
|
||||
type BlockSysProps,
|
||||
type PropsOfModel,
|
||||
type YBlock,
|
||||
} from '../block/index.js';
|
||||
import { DocCRUD } from './crud.js';
|
||||
@@ -852,9 +854,12 @@ export class Store {
|
||||
*
|
||||
* @category Block CRUD
|
||||
*/
|
||||
updateBlock(
|
||||
modelOrId: BlockModel | string,
|
||||
callBackOrProps: (() => void) | Partial<BlockProps>
|
||||
|
||||
updateBlock<T extends BlockModel = BlockModel>(
|
||||
modelOrId: T | string,
|
||||
callBackOrProps:
|
||||
| (() => void)
|
||||
| Partial<(PropsOfModel<T> & BlockSysProps) | BlockProps>
|
||||
) {
|
||||
if (this.readonly) {
|
||||
console.error('cannot modify data in readonly mode');
|
||||
|
||||
@@ -113,6 +113,8 @@ type TransformerMiddlewareOptions = {
|
||||
transformerConfigs: Map<string, unknown>;
|
||||
};
|
||||
|
||||
type TransformerMiddlewareCleanup = () => void;
|
||||
|
||||
export type TransformerMiddleware = (
|
||||
options: TransformerMiddlewareOptions
|
||||
) => void;
|
||||
) => void | TransformerMiddlewareCleanup;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { nextTick } from '@blocksuite/global/utils';
|
||||
import { Subject } from 'rxjs';
|
||||
@@ -67,6 +68,8 @@ export class Transformer {
|
||||
|
||||
private readonly _docCRUD: DocCRUD;
|
||||
|
||||
private readonly _disposables: DisposableGroup = new DisposableGroup();
|
||||
|
||||
private readonly _slots: TransformerSlots = {
|
||||
beforeImport: new Subject<BeforeImportPayload>(),
|
||||
afterImport: new Subject<AfterImportPayload>(),
|
||||
@@ -366,13 +369,16 @@ export class Transformer {
|
||||
this._docCRUD = docCRUD;
|
||||
|
||||
middlewares.forEach(middleware => {
|
||||
middleware({
|
||||
const cleanup = middleware({
|
||||
slots: this._slots,
|
||||
docCRUD: this._docCRUD,
|
||||
assetsManager: this._assetsManager,
|
||||
adapterConfigs: this._adapterConfigs,
|
||||
transformerConfigs: this._transformerConfigs,
|
||||
});
|
||||
if (cleanup) {
|
||||
this._disposables.add(cleanup);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -646,4 +652,9 @@ export class Transformer {
|
||||
reset() {
|
||||
this._assetsManager.cleanup();
|
||||
}
|
||||
|
||||
[Symbol.dispose]() {
|
||||
this._disposables.dispose();
|
||||
this._assetsManager.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,12 +132,13 @@ export class DocEngine {
|
||||
this.logger
|
||||
);
|
||||
|
||||
cleanUp.push(
|
||||
state.mainPeer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
}).unsubscribe
|
||||
);
|
||||
const subscriber = state.mainPeer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
});
|
||||
cleanUp.push(() => {
|
||||
subscriber.unsubscribe();
|
||||
});
|
||||
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
|
||||
@@ -152,12 +153,15 @@ export class DocEngine {
|
||||
this.priorityTarget,
|
||||
this.logger
|
||||
);
|
||||
cleanUp.push(
|
||||
peer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
}).unsubscribe
|
||||
);
|
||||
|
||||
const subscriber = peer.onStatusChange.subscribe(() => {
|
||||
if (!signal.aborted)
|
||||
this.updateSyncingState(state.mainPeer, state.shadowPeers);
|
||||
});
|
||||
cleanUp.push(() => {
|
||||
subscriber.unsubscribe();
|
||||
});
|
||||
|
||||
return peer;
|
||||
});
|
||||
|
||||
|
||||
@@ -31,12 +31,15 @@ describe('frame', () => {
|
||||
);
|
||||
await wait();
|
||||
|
||||
const frameTitleWidget = service.std.view.getWidget(
|
||||
'affine-frame-title-widget',
|
||||
doc.root!.id
|
||||
) as AffineFrameTitleWidget | null;
|
||||
const getFrameTitle = (frameId: string) => {
|
||||
const frameTitleWidget = service.std.view.getWidget(
|
||||
'affine-frame-title-widget',
|
||||
frameId
|
||||
) as AffineFrameTitleWidget | null;
|
||||
return frameTitleWidget?.shadowRoot?.querySelector('affine-frame-title');
|
||||
};
|
||||
|
||||
const frameTitle = frameTitleWidget?.getFrameTitle(frame);
|
||||
const frameTitle = getFrameTitle(frame);
|
||||
const rect = frameTitle?.getBoundingClientRect();
|
||||
|
||||
expect(frameTitle).toBeTruthy();
|
||||
@@ -58,7 +61,7 @@ describe('frame', () => {
|
||||
);
|
||||
await wait();
|
||||
|
||||
const nestedTitle = frameTitleWidget?.getFrameTitle(nestedFrame);
|
||||
const nestedTitle = getFrameTitle(nestedFrame);
|
||||
expect(nestedTitle).toBeTruthy();
|
||||
if (!nestedTitle) return;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
fill: '#ff0000',
|
||||
stroke: '#000000',
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps as any);
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector(
|
||||
@@ -73,7 +73,7 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
subType: 'ellipse',
|
||||
xywh: '[200, 200, 50, 50]',
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps as any);
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
@@ -91,4 +91,48 @@ describe('Shape rendering with DOM renderer', () => {
|
||||
);
|
||||
expect(shapeElement).toBeNull();
|
||||
});
|
||||
|
||||
test('should correctly render diamond shape', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
const shapeProps = {
|
||||
type: 'shape',
|
||||
subType: 'diamond',
|
||||
xywh: '[150, 150, 80, 60]',
|
||||
fillColor: '#ff0000',
|
||||
strokeColor: '#000000',
|
||||
filled: true,
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
await wait(100);
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
|
||||
`[data-element-id="${shapeId}"]`
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
});
|
||||
|
||||
test('should correctly render triangle shape', async () => {
|
||||
const surfaceView = getSurface(window.doc, window.editor);
|
||||
const surfaceModel = surfaceView.model;
|
||||
const shapeProps = {
|
||||
type: 'shape',
|
||||
subType: 'triangle',
|
||||
xywh: '[150, 150, 80, 60]',
|
||||
fillColor: '#ff0000',
|
||||
strokeColor: '#000000',
|
||||
filled: true,
|
||||
};
|
||||
const shapeId = surfaceModel.addElement(shapeProps);
|
||||
await wait(100);
|
||||
const shapeElement = surfaceView?.renderRoot.querySelector<HTMLElement>(
|
||||
`[data-element-id="${shapeId}"]`
|
||||
);
|
||||
|
||||
expect(shapeElement).not.toBeNull();
|
||||
expect(shapeElement?.style.width).toBe('80px');
|
||||
expect(shapeElement?.style.height).toBe('60px');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ describe('basic', () => {
|
||||
xywh: '[100, 0, 100, 100]',
|
||||
index: service.generateIndex(),
|
||||
})!;
|
||||
await wait(0); // wait next frame
|
||||
frameId = service.crud.addBlock(
|
||||
'affine:frame',
|
||||
{
|
||||
|
||||
@@ -7,30 +7,40 @@ import type { InitFn } from './utils.js';
|
||||
const presetMarkdown = `Click the 🔁 button to switch between editors dynamically - they are fully compatible!`;
|
||||
|
||||
export const preset: InitFn = async (collection: Workspace, id: string) => {
|
||||
const doc = collection.createDoc(id).getStore({ id });
|
||||
doc.load();
|
||||
// Add root block and surface block at root level
|
||||
const rootId = doc.addBlock('affine:page', {
|
||||
title: new Text('BlockSuite Playground'),
|
||||
});
|
||||
doc.addBlock('affine:surface', {}, rootId);
|
||||
let doc = collection.getDoc(id);
|
||||
const hasDoc = !!doc;
|
||||
if (!doc) {
|
||||
doc = collection.createDoc(id);
|
||||
}
|
||||
|
||||
// Add note block inside root block
|
||||
const noteId = doc.addBlock(
|
||||
'affine:note',
|
||||
{ xywh: '[0, 100, 800, 640]' },
|
||||
rootId
|
||||
);
|
||||
const store = doc.getStore({ id });
|
||||
store.load();
|
||||
|
||||
// Import preset markdown content inside note block
|
||||
await MarkdownTransformer.importMarkdownToBlock({
|
||||
doc,
|
||||
blockId: noteId,
|
||||
markdown: presetMarkdown,
|
||||
extensions: getTestStoreManager().get('store'),
|
||||
});
|
||||
// Run only once on all clients.
|
||||
let noteId: string;
|
||||
if (!hasDoc) {
|
||||
// Add root block and surface block at root level
|
||||
const rootId = store.addBlock('affine:page', {
|
||||
title: new Text('BlockSuite Playground'),
|
||||
});
|
||||
store.addBlock('affine:surface', {}, rootId);
|
||||
|
||||
doc.resetHistory();
|
||||
// Add note block inside root block
|
||||
noteId = store.addBlock(
|
||||
'affine:note',
|
||||
{ xywh: '[0, 100, 800, 640]' },
|
||||
rootId
|
||||
);
|
||||
// Import preset markdown content inside note block
|
||||
await MarkdownTransformer.importMarkdownToBlock({
|
||||
doc: store,
|
||||
blockId: noteId,
|
||||
markdown: presetMarkdown,
|
||||
extensions: getTestStoreManager().get('store'),
|
||||
});
|
||||
}
|
||||
|
||||
store.resetHistory();
|
||||
};
|
||||
|
||||
preset.id = 'preset';
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@types/katex": "^0.16.7",
|
||||
"browser-fs-access": "^0.37.0",
|
||||
"jszip": "^3.10.1",
|
||||
"katex": "^0.16.11",
|
||||
"lit": "^3.2.0",
|
||||
"lz-string": "^1.5.0",
|
||||
"rxjs": "^7.8.1",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
@import '@toeverything/theme/style.css';
|
||||
@import '@toeverything/theme/fonts.css';
|
||||
|
||||
@import 'katex/dist/katex.min.css';
|
||||
|
||||
@font-face {
|
||||
font-family: 'color-emoji';
|
||||
src:
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"@vitest/coverage-istanbul": "3.1.3",
|
||||
"@vitest/ui": "3.1.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "^36.0.0",
|
||||
"electron": "^35.0.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.0.0",
|
||||
|
||||
12
packages/backend/native/index.d.ts
vendored
12
packages/backend/native/index.d.ts
vendored
@@ -8,6 +8,11 @@ export const AFFINE_PRO_LICENSE_AES_KEY: string | undefined | null
|
||||
|
||||
export const AFFINE_PRO_PUBLIC_KEY: string | undefined | null
|
||||
|
||||
export interface Chunk {
|
||||
index: number
|
||||
content: string
|
||||
}
|
||||
|
||||
export declare function fromModelName(modelName: string): Tokenizer | null
|
||||
|
||||
export declare function getMime(input: Uint8Array): string
|
||||
@@ -22,6 +27,11 @@ export declare function mergeUpdatesInApplyWay(updates: Array<Buffer>): Buffer
|
||||
|
||||
export declare function mintChallengeResponse(resource: string, bits?: number | undefined | null): Promise<string>
|
||||
|
||||
export declare function parseDoc(filePath: string, doc: Buffer): Promise<{ name: string, chunks: Array<{index: number, content: string}> }>
|
||||
export interface ParsedDoc {
|
||||
name: string
|
||||
chunks: Array<Chunk>
|
||||
}
|
||||
|
||||
export declare function parseDoc(filePath: string, doc: Buffer): Promise<ParsedDoc>
|
||||
|
||||
export declare function verifyChallengeResponse(response: string, bits: number, resource: string): Promise<boolean>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"build:debug": "napi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "3.0.0-alpha.78",
|
||||
"@napi-rs/cli": "3.0.0-alpha.81",
|
||||
"lib0": "^0.2.99",
|
||||
"tiktoken": "^1.0.17",
|
||||
"tinybench": "^4.0.0",
|
||||
|
||||
@@ -2,9 +2,21 @@ use affine_common::doc_loader::Doc;
|
||||
use napi::{
|
||||
anyhow::anyhow,
|
||||
bindgen_prelude::{AsyncTask, Buffer},
|
||||
Env, JsObject, Result, Task,
|
||||
Env, Result, Task,
|
||||
};
|
||||
|
||||
#[napi(object)]
|
||||
pub struct Chunk {
|
||||
pub index: i64,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ParsedDoc {
|
||||
pub name: String,
|
||||
pub chunks: Vec<Chunk>,
|
||||
}
|
||||
|
||||
pub struct Document {
|
||||
inner: Doc,
|
||||
}
|
||||
@@ -14,24 +26,20 @@ impl Document {
|
||||
self.inner.name.clone()
|
||||
}
|
||||
|
||||
fn chunks(&self, env: Env) -> Result<JsObject> {
|
||||
let mut array = env.create_array_with_length(self.inner.chunks.len())?;
|
||||
for (i, chunk) in self.inner.chunks.iter().enumerate() {
|
||||
let content = crate::utils::clean_content(&chunk.content);
|
||||
|
||||
let mut obj = env.create_object()?;
|
||||
obj.set_named_property("index", i as i64)?;
|
||||
obj.set_named_property("content", content)?;
|
||||
array.set_element(i as u32, obj)?;
|
||||
}
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
fn resolve(self, env: Env) -> Result<JsObject> {
|
||||
let mut obj = env.create_object()?;
|
||||
obj.set_named_property("name", self.name())?;
|
||||
obj.set_named_property("chunks", self.chunks(env)?)?;
|
||||
Ok(obj)
|
||||
fn chunks(&self) -> Vec<Chunk> {
|
||||
self
|
||||
.inner
|
||||
.chunks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, chunk)| {
|
||||
let content = crate::utils::clean_content(&chunk.content);
|
||||
Chunk {
|
||||
index: i as i64,
|
||||
content,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<Chunk>>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,21 +51,22 @@ pub struct AsyncParseDocResponse {
|
||||
#[napi]
|
||||
impl Task for AsyncParseDocResponse {
|
||||
type Output = Document;
|
||||
type JsValue = JsObject;
|
||||
type JsValue = ParsedDoc;
|
||||
|
||||
fn compute(&mut self) -> Result<Self::Output> {
|
||||
let doc = Doc::new(&self.file_path, &self.doc).map_err(|e| anyhow!(e))?;
|
||||
Ok(Document { inner: doc })
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, doc: Document) -> Result<Self::JsValue> {
|
||||
doc.resolve(env)
|
||||
fn resolve(&mut self, _: Env, doc: Document) -> Result<Self::JsValue> {
|
||||
Ok(ParsedDoc {
|
||||
name: doc.name(),
|
||||
chunks: doc.chunks(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(
|
||||
ts_return_type = "Promise<{ name: string, chunks: Array<{index: number, content: string}> }>"
|
||||
)]
|
||||
#[napi]
|
||||
pub fn parse_doc(file_path: String, doc: Buffer) -> AsyncTask<AsyncParseDocResponse> {
|
||||
AsyncTask::new(AsyncParseDocResponse {
|
||||
file_path,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use affine_common::hashcash::Stamp;
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, Result as NapiResult, Task};
|
||||
use napi_derive::napi;
|
||||
|
||||
pub struct AsyncVerifyChallengeResponse {
|
||||
@@ -13,7 +13,7 @@ pub struct AsyncVerifyChallengeResponse {
|
||||
#[napi]
|
||||
impl Task for AsyncVerifyChallengeResponse {
|
||||
type Output = bool;
|
||||
type JsValue = JsBoolean;
|
||||
type JsValue = bool;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(if let Ok(stamp) = Stamp::try_from(self.response.as_str()) {
|
||||
@@ -23,8 +23,8 @@ impl Task for AsyncVerifyChallengeResponse {
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
env.get_boolean(output)
|
||||
fn resolve(&mut self, _: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,14 +49,14 @@ pub struct AsyncMintChallengeResponse {
|
||||
#[napi]
|
||||
impl Task for AsyncMintChallengeResponse {
|
||||
type Output = String;
|
||||
type JsValue = JsString;
|
||||
type JsValue = String;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
env.create_string(&output)
|
||||
fn resolve(&mut self, _: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,16 +15,25 @@ pub fn from_model_name(model_name: String) -> Option<Tokenizer> {
|
||||
impl Tokenizer {
|
||||
#[napi]
|
||||
pub fn count(&self, content: String, allowed_special: Option<Vec<String>>) -> u32 {
|
||||
self
|
||||
.inner
|
||||
.encode(
|
||||
&content,
|
||||
if let Some(allowed_special) = &allowed_special {
|
||||
HashSet::from_iter(allowed_special.iter().map(|s| s.as_str()))
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
)
|
||||
.len() as u32
|
||||
let allowed_special = if let Some(allowed_special) = &allowed_special {
|
||||
HashSet::from_iter(allowed_special.iter().map(|s| s.as_str()))
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
|
||||
self.inner.encode(&content, &allowed_special).0.len() as u32
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_tokenizer() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,6 @@
|
||||
# MAILER_SENDER="noreply@toeverything.info"
|
||||
# MAILER_USER="noreply@toeverything.info"
|
||||
# MAILER_PASSWORD="affine"
|
||||
# MAILER_SECURE=false
|
||||
# MAILER_SECURE=false
|
||||
|
||||
# AFFINE_INDEXER_ENABLED=true
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_files" ADD COLUMN "blob_id" VARCHAR NOT NULL DEFAULT '';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'ai_workspace_files') THEN
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_files"
|
||||
ADD COLUMN "blob_id" VARCHAR NOT NULL DEFAULT '';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
@@ -5,16 +5,25 @@
|
||||
- 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.
|
||||
|
||||
*/
|
||||
-- DropIndex
|
||||
DROP INDEX "ai_workspace_embeddings_workspace_id_doc_id_chunk_key";
|
||||
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
|
||||
-- DropIndex
|
||||
DROP INDEX "ai_workspace_embeddings_workspace_id_doc_id_chunk_key";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "ai_workspace_file_embeddings_workspace_id_file_id_chunk_key";
|
||||
-- DropIndex
|
||||
DROP INDEX "ai_workspace_file_embeddings_workspace_id_file_id_chunk_key";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_embeddings" DROP CONSTRAINT "ai_workspace_embeddings_pkey",
|
||||
ADD CONSTRAINT "ai_workspace_embeddings_pkey" PRIMARY KEY ("workspace_id", "doc_id", "chunk");
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_embeddings"
|
||||
DROP CONSTRAINT "ai_workspace_embeddings_pkey",
|
||||
ADD CONSTRAINT "ai_workspace_embeddings_pkey" PRIMARY KEY ("workspace_id", "doc_id", "chunk");
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_file_embeddings" DROP CONSTRAINT "ai_workspace_file_embeddings_pkey",
|
||||
ADD CONSTRAINT "ai_workspace_file_embeddings_pkey" PRIMARY KEY ("workspace_id", "file_id", "chunk");
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_workspace_file_embeddings"
|
||||
DROP CONSTRAINT "ai_workspace_file_embeddings_pkey",
|
||||
ADD CONSTRAINT "ai_workspace_file_embeddings_pkey" PRIMARY KEY ("workspace_id", "file_id", "chunk");
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
"dependencies": {
|
||||
"@affine/reader": "workspace:*",
|
||||
"@affine/server-native": "workspace:*",
|
||||
"@ai-sdk/anthropic": "^1.2.10",
|
||||
"@ai-sdk/anthropic": "^1.2.12",
|
||||
"@ai-sdk/google": "^1.2.18",
|
||||
"@ai-sdk/google-vertex": "^2.2.22",
|
||||
"@ai-sdk/openai": "^1.3.21",
|
||||
"@ai-sdk/perplexity": "^1.1.6",
|
||||
"@ai-sdk/google-vertex": "^2.2.23",
|
||||
"@ai-sdk/openai": "^1.3.22",
|
||||
"@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",
|
||||
@@ -100,7 +100,7 @@
|
||||
"nanoid": "^5.0.9",
|
||||
"nest-commander": "^3.15.0",
|
||||
"nest-winston": "^1.9.7",
|
||||
"nestjs-cls": "^5.0.0",
|
||||
"nestjs-cls": "^6.0.0",
|
||||
"nodemailer": "^7.0.0",
|
||||
"on-headers": "^1.0.2",
|
||||
"piscina": "^5.0.0-alpha.0",
|
||||
|
||||
@@ -47,5 +47,42 @@ function runPredeployScript() {
|
||||
});
|
||||
}
|
||||
|
||||
function fixFailedMigrations() {
|
||||
console.log('fixing failed migrations.');
|
||||
const maybeFailedMigrations = [
|
||||
'20250521083048_fix_workspace_embedding_chunk_primary_key',
|
||||
];
|
||||
for (const migration of maybeFailedMigrations) {
|
||||
try {
|
||||
execSync(`yarn prisma migrate resolve --rolled-back ${migration}`, {
|
||||
encoding: 'utf-8',
|
||||
env: process.env,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
console.log(`migration [${migration}] has been rolled back.`);
|
||||
} catch (err) {
|
||||
if (
|
||||
err.message.includes(
|
||||
'cannot be rolled back because it is not in a failed state'
|
||||
) ||
|
||||
err.message.includes(
|
||||
'cannot be rolled back because it was never applied'
|
||||
) ||
|
||||
err.message.includes(
|
||||
'called markMigrationRolledBack on a database without migrations table'
|
||||
)
|
||||
) {
|
||||
// migration has been rolled back, skip it
|
||||
continue;
|
||||
}
|
||||
// ignore other errors
|
||||
console.log(
|
||||
`migration [${migration}] rolled back failed. ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepare();
|
||||
fixFailedMigrations();
|
||||
runPredeployScript();
|
||||
|
||||
@@ -272,13 +272,68 @@ test('should validate markdown list', t => {
|
||||
// ==================== action ====================
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: 'Should chat with histories',
|
||||
promptName: ['Chat With AFFiNE AI'],
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: `
|
||||
Hi! I’m going to send you a technical term related to real-time collaborative editing (e.g., CRDT, Operational Transformation, OT Composer, etc.). Whenever I send you a term:
|
||||
1. Translate it into Chinese (send me the Chinese version).
|
||||
2. Then translate that Chinese back into English (send me the retranslated English).
|
||||
3. Provide a brief, English-language introduction and context for this concept.
|
||||
4. In that English explanation, annotate any niche terms with their Chinese equivalents in parentheses (for example: “Conflict-Free Replicated Data Type (无冲突复制数据类型)”).
|
||||
5. Finally, give the origin or “term history” (e.g., who introduced it, in which paper or year).
|
||||
|
||||
If you understand, please proceed by explaining the term “CRDT.”
|
||||
`.trim(),
|
||||
},
|
||||
{
|
||||
role: 'assistant' as const,
|
||||
content: `
|
||||
1. **Chinese Translation:**
|
||||
“CRDT” → **无冲突复制数据类型**
|
||||
|
||||
2. **Back-Translation to English:**
|
||||
无冲突复制数据类型 → **Conflict-Free Replicated Data Type**
|
||||
|
||||
3. **English Introduction & Context:**
|
||||
A **Conflict-Free Replicated Data Type (无冲突复制数据类型)** is an abstract data type designed for distributed systems where replicas of shared state may be modified concurrently without requiring coordination. CRDTs allow multiple users or processes to update the same data structure (for example, a shared document in a collaborative editor) at the same time.
|
||||
- **Key Terms (with Chinese equivalents):**
|
||||
- **Replica (副本):** Each node or client maintains its own copy of the data.
|
||||
- **State-based (状态型) vs. Operation-based (操作型):** Two main CRDT classes; state-based CRDTs exchange entire state snapshots occasionally, whereas operation-based CRDTs broadcast only incremental operations.
|
||||
- **Merge Function (合并函数):** A deterministic function that resolves differences between two replicas without conflicts.
|
||||
|
||||
CRDTs enable **eventual consistency (最终一致性)** in real-time collaborative editors by ensuring that, after all updates propagate, every replica converges to the same state, even if operations arrive in different orders. This approach removes the need for a centralized server to resolve conflicts, making offline or peer-to-peer editing possible.
|
||||
|
||||
4. **Origin / Term History:**
|
||||
The term **“CRDT”** was first introduced by Marc Shapiro, Nuno Preguiça, Carlos Baquero, and Marek Zawirski in their 2011 paper titled “Conflict-free Replicated Data Types” (published in the _Stabilization, Safety, and Security of Distributed Systems (SSS)_ conference). They formalized two families of CRDTs—state-based (“Convergent Replicated Data Types” or CvRDTs) and operation-based (“Commutative Replicated Data Types” or CmRDTs)—and proved their convergence properties under asynchronous, unreliable networks.
|
||||
`.trim(),
|
||||
},
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: `Thanks! Now please just tell me the **Chinese translation** and the **back-translated English term** that you provided previously for “CRDT.” Do not reprint the full introduction—only those two lines.`,
|
||||
},
|
||||
],
|
||||
verifier: (t: ExecutionContext<Tester>, result: string) => {
|
||||
assertNotWrappedInCodeBlock(t, result);
|
||||
const lower = result.toLowerCase();
|
||||
t.assert(
|
||||
lower.includes('无冲突复制数据类型') &&
|
||||
lower.includes('conflict-free replicated data type'),
|
||||
'The response should include “无冲突复制数据类型” and “Conflict-Free Replicated Data Type”'
|
||||
);
|
||||
},
|
||||
type: 'text' as const,
|
||||
},
|
||||
{
|
||||
name: 'Should not have citation',
|
||||
promptName: ['Chat With AFFiNE AI'],
|
||||
messages: [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: 'what is ssot',
|
||||
content: 'what is AFFiNE AI?',
|
||||
params: {
|
||||
files: [
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ export class MockCopilotProvider extends OpenAIProvider {
|
||||
capabilities: [
|
||||
{
|
||||
input: [ModelInputType.Text, ModelInputType.Image],
|
||||
output: [ModelOutputType.Text],
|
||||
output: [ModelOutputType.Text, ModelOutputType.Structured],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user