mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 02:21:51 +08:00
fix(editor): typecheck for tests and playground (#9897)
This commit is contained in:
@@ -1,635 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
|
||||||
import '@shoelace-style/shoelace/dist/components/alert/alert.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/button/button.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/button-group/button-group.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/color-picker/color-picker.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/divider/divider.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/dropdown/dropdown.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/input/input.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/menu/menu.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/menu-item/menu-item.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/select/select.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/tab/tab.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/tab-group/tab-group.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/components/tooltip/tooltip.js';
|
|
||||||
import '@shoelace-style/shoelace/dist/themes/light.css';
|
|
||||||
import '@shoelace-style/shoelace/dist/themes/dark.css';
|
|
||||||
|
|
||||||
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
|
||||||
import { ShadowlessElement } from '@blocksuite/block-std';
|
|
||||||
import {
|
|
||||||
ColorScheme,
|
|
||||||
type DocMode,
|
|
||||||
DocModeProvider,
|
|
||||||
EdgelessRootService,
|
|
||||||
ExportManager,
|
|
||||||
printToPdf,
|
|
||||||
} from '@blocksuite/blocks';
|
|
||||||
import { type SerializedXYWH, SignalWatcher } from '@blocksuite/global/utils';
|
|
||||||
import type { DeltaInsert } from '@blocksuite/inline';
|
|
||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
|
||||||
import { Text, type Workspace } from '@blocksuite/store';
|
|
||||||
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js';
|
|
||||||
import { css, html, nothing } from 'lit';
|
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
|
||||||
|
|
||||||
import { notify } from '../../default/utils/notify.js';
|
|
||||||
import { mockEdgelessTheme } from '../mock-services.js';
|
|
||||||
import { generateRoomId } from '../sync/websocket/utils.js';
|
|
||||||
import type { DocsPanel } from './docs-panel.js';
|
|
||||||
import type { LeftSidePanel } from './left-side-panel.js';
|
|
||||||
|
|
||||||
const basePath =
|
|
||||||
'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.11.2/dist/';
|
|
||||||
setBasePath(basePath);
|
|
||||||
|
|
||||||
@customElement('collab-debug-menu')
|
|
||||||
export class CollabDebugMenu extends SignalWatcher(ShadowlessElement) {
|
|
||||||
static override styles = css`
|
|
||||||
:root {
|
|
||||||
--sl-font-size-medium: var(--affine-font-xs);
|
|
||||||
--sl-input-font-size-small: var(--affine-font-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dg.ac {
|
|
||||||
z-index: 1001 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
private readonly _darkModeChange = (e: MediaQueryListEvent) => {
|
|
||||||
this._setThemeMode(!!e.matches);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _handleDocsPanelClose = () => {
|
|
||||||
this.leftSidePanel.toggle(this.docsPanel);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _keydown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'F1') {
|
|
||||||
this._switchEditorMode();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _startCollaboration = async () => {
|
|
||||||
if (window.wsProvider) {
|
|
||||||
notify('There is already a websocket provider exists', 'neutral').catch(
|
|
||||||
console.error
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams(location.search);
|
|
||||||
const id = params.get('room') || (await generateRoomId());
|
|
||||||
|
|
||||||
params.set('room', id);
|
|
||||||
const url = new URL(location.href);
|
|
||||||
url.search = params.toString();
|
|
||||||
location.href = url.href;
|
|
||||||
};
|
|
||||||
|
|
||||||
get doc() {
|
|
||||||
return this.editor.doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
get editorMode() {
|
|
||||||
return this.editor.mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
set editorMode(value: DocMode) {
|
|
||||||
this.editor.mode = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get rootService() {
|
|
||||||
try {
|
|
||||||
return this.editor.std.getService('affine:page');
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _addNote() {
|
|
||||||
const rootModel = this.doc.root;
|
|
||||||
if (!rootModel) return;
|
|
||||||
const rootId = rootModel.id;
|
|
||||||
|
|
||||||
this.doc.captureSync();
|
|
||||||
|
|
||||||
const count = rootModel.children.length;
|
|
||||||
const xywh: SerializedXYWH = `[0,${count * 60},800,95]`;
|
|
||||||
|
|
||||||
const noteId = this.doc.addBlock('affine:note', { xywh }, rootId);
|
|
||||||
this.doc.addBlock('affine:paragraph', {}, noteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _clearSiteData() {
|
|
||||||
await fetch('/Clear-Site-Data');
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _exportHtml() {
|
|
||||||
const htmlTransformer = this.rootService?.transformers.html;
|
|
||||||
htmlTransformer?.exportDoc(this.doc).catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _exportMarkDown() {
|
|
||||||
const markdownTransformer = this.rootService?.transformers.markdown;
|
|
||||||
markdownTransformer?.exportDoc(this.doc).catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _exportPdf() {
|
|
||||||
this.editor.std.get(ExportManager).exportPdf().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _exportPng() {
|
|
||||||
this.editor.std.get(ExportManager).exportPng().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _exportSnapshot() {
|
|
||||||
if (!this.rootService) return;
|
|
||||||
const zipTransformer = this.rootService.transformers.zip;
|
|
||||||
await zipTransformer.exportDocs(
|
|
||||||
this.collection,
|
|
||||||
Array.from(this.collection.docs.values()).map(collection =>
|
|
||||||
collection.getStore()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _importSnapshot() {
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.setAttribute('type', 'file');
|
|
||||||
input.setAttribute('accept', '.zip');
|
|
||||||
input.multiple = false;
|
|
||||||
input.onchange = async () => {
|
|
||||||
const file = input.files?.item(0);
|
|
||||||
if (!file) return;
|
|
||||||
if (!this.rootService) return;
|
|
||||||
try {
|
|
||||||
const zipTransformer = this.rootService.transformers.zip;
|
|
||||||
const docs = await zipTransformer.importDocs(this.collection, file);
|
|
||||||
for (const doc of docs) {
|
|
||||||
let noteBlockId;
|
|
||||||
const noteBlocks = window.doc.getBlocksByFlavour('affine:note');
|
|
||||||
if (noteBlocks.length) {
|
|
||||||
noteBlockId = noteBlocks[0].id;
|
|
||||||
} else {
|
|
||||||
noteBlockId = this.doc.addBlock(
|
|
||||||
'affine:note',
|
|
||||||
{
|
|
||||||
xywh: '[-200,-48,400,96]',
|
|
||||||
},
|
|
||||||
this.doc.root?.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!doc) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.doc.addBlock(
|
|
||||||
'affine:paragraph',
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: new Text([
|
|
||||||
{
|
|
||||||
insert: ' ',
|
|
||||||
attributes: {
|
|
||||||
reference: {
|
|
||||||
type: 'LinkedPage',
|
|
||||||
pageId: doc.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as DeltaInsert<AffineTextAttributes>,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
noteBlockId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.requestUpdate();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Invalid snapshot.');
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
input.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _insertTransitionStyle(classKey: string, duration: number) {
|
|
||||||
const $html = document.documentElement;
|
|
||||||
const $style = document.createElement('style');
|
|
||||||
const slCSSKeys = ['sl-transition-x-fast'];
|
|
||||||
$style.innerHTML = `html.${classKey} * { transition: all ${duration}ms 0ms linear !important; } :root { ${slCSSKeys.map(
|
|
||||||
key => `--${key}: ${duration}ms`
|
|
||||||
)} }`;
|
|
||||||
|
|
||||||
$html.append($style);
|
|
||||||
$html.classList.add(classKey);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
$style.remove();
|
|
||||||
$html.classList.remove(classKey);
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _print() {
|
|
||||||
printToPdf().catch(console.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setThemeMode(dark: boolean) {
|
|
||||||
const html = document.querySelector('html');
|
|
||||||
|
|
||||||
this._dark = dark;
|
|
||||||
localStorage.setItem('blocksuite:dark', dark ? 'true' : 'false');
|
|
||||||
if (!html) return;
|
|
||||||
html.dataset.theme = dark ? 'dark' : 'light';
|
|
||||||
|
|
||||||
this._insertTransitionStyle('color-transition', 0);
|
|
||||||
|
|
||||||
if (dark) {
|
|
||||||
html.classList.add('dark');
|
|
||||||
html.classList.add('sl-theme-dark');
|
|
||||||
} else {
|
|
||||||
html.classList.remove('dark');
|
|
||||||
html.classList.remove('sl-theme-dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
const theme = dark ? ColorScheme.Dark : ColorScheme.Light;
|
|
||||||
mockEdgelessTheme.setTheme(theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _switchEditorMode() {
|
|
||||||
if (!this.editor.host) return;
|
|
||||||
const newMode = this._docMode === 'page' ? 'edgeless' : 'page';
|
|
||||||
const docModeService = this.editor.host.std.get(DocModeProvider);
|
|
||||||
if (docModeService) {
|
|
||||||
docModeService.setPrimaryMode(newMode, this.editor.doc.id);
|
|
||||||
}
|
|
||||||
this._docMode = newMode;
|
|
||||||
this.editor.mode = newMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleDarkMode() {
|
|
||||||
this._setThemeMode(!this._dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _toggleDocsPanel() {
|
|
||||||
this.docsPanel.onClose = this._handleDocsPanelClose;
|
|
||||||
this.leftSidePanel.toggle(this.docsPanel);
|
|
||||||
}
|
|
||||||
|
|
||||||
override connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
|
|
||||||
this._docMode = this.editor.mode;
|
|
||||||
this.editor.slots.docUpdated.on(({ newDocId }) => {
|
|
||||||
const newDocMode = this.editor.std
|
|
||||||
.get(DocModeProvider)
|
|
||||||
.getPrimaryMode(newDocId);
|
|
||||||
this._docMode = newDocMode;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.addEventListener('keydown', this._keydown);
|
|
||||||
}
|
|
||||||
|
|
||||||
override createRenderRoot() {
|
|
||||||
const matchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
this._setThemeMode(this._dark && matchMedia.matches);
|
|
||||||
matchMedia.addEventListener('change', this._darkModeChange);
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
override disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
|
|
||||||
const matchMedia = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
matchMedia.removeEventListener('change', this._darkModeChange);
|
|
||||||
document.body.removeEventListener('keydown', this._keydown);
|
|
||||||
}
|
|
||||||
|
|
||||||
override firstUpdated() {
|
|
||||||
this.doc.slots.historyUpdated.on(() => {
|
|
||||||
this._canUndo = this.doc.canUndo;
|
|
||||||
this._canRedo = this.doc.canRedo;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
override render() {
|
|
||||||
return html`
|
|
||||||
<style>
|
|
||||||
.collab-debug-menu {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
z-index: 1000; /* for debug visibility */
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.collab-debug-menu {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-toolbar {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
padding: 8px 8px 8px 16px;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 390px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-toolbar sl-button.dots-menu::part(base) {
|
|
||||||
color: var(--sl-color-neutral-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-toolbar sl-button.dots-menu::part(label) {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-toolbar > * {
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edgeless-toolbar {
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 17px;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edgeless-toolbar sl-select,
|
|
||||||
.edgeless-toolbar sl-color-picker,
|
|
||||||
.edgeless-toolbar sl-button {
|
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="collab-debug-menu default">
|
|
||||||
<div class="default-toolbar">
|
|
||||||
<div class="top-container">
|
|
||||||
<sl-dropdown placement="bottom" hoist>
|
|
||||||
<sl-button
|
|
||||||
class="dots-menu"
|
|
||||||
variant="text"
|
|
||||||
size="small"
|
|
||||||
slot="trigger"
|
|
||||||
>
|
|
||||||
<sl-icon
|
|
||||||
style="font-size: 14px"
|
|
||||||
name="three-dots-vertical"
|
|
||||||
label="Menu"
|
|
||||||
></sl-icon>
|
|
||||||
</sl-button>
|
|
||||||
<sl-menu>
|
|
||||||
<sl-menu-item>
|
|
||||||
<sl-icon
|
|
||||||
slot="prefix"
|
|
||||||
name="terminal"
|
|
||||||
label="Test operations"
|
|
||||||
></sl-icon>
|
|
||||||
<span>Test operations</span>
|
|
||||||
<sl-menu slot="submenu">
|
|
||||||
<sl-menu-item @click="${this._print}"> Print </sl-menu-item>
|
|
||||||
<sl-menu-item @click=${this._addNote}>
|
|
||||||
Add Note</sl-menu-item
|
|
||||||
>
|
|
||||||
<sl-menu-item @click=${this._exportMarkDown}>
|
|
||||||
Export Markdown
|
|
||||||
</sl-menu-item>
|
|
||||||
<sl-menu-item @click=${this._exportHtml}>
|
|
||||||
Export HTML
|
|
||||||
</sl-menu-item>
|
|
||||||
<sl-menu-item @click=${this._exportPdf}>
|
|
||||||
Export PDF
|
|
||||||
</sl-menu-item>
|
|
||||||
<sl-menu-item @click=${this._exportPng}>
|
|
||||||
Export PNG
|
|
||||||
</sl-menu-item>
|
|
||||||
<sl-menu-item @click=${this._exportSnapshot}>
|
|
||||||
Export Snapshot
|
|
||||||
</sl-menu-item>
|
|
||||||
<sl-menu-item @click=${this._importSnapshot}>
|
|
||||||
Import Snapshot
|
|
||||||
</sl-menu-item>
|
|
||||||
</sl-menu>
|
|
||||||
</sl-menu-item>
|
|
||||||
<sl-menu-item @click=${this._clearSiteData}>
|
|
||||||
Clear Site Data
|
|
||||||
<sl-icon slot="prefix" name="trash"></sl-icon>
|
|
||||||
</sl-menu-item>
|
|
||||||
<sl-menu-item @click=${this._toggleDarkMode}>
|
|
||||||
Toggle ${this._dark ? 'Light' : 'Dark'} Mode
|
|
||||||
<sl-icon
|
|
||||||
slot="prefix"
|
|
||||||
name=${this._dark ? 'moon' : 'brightness-high'}
|
|
||||||
></sl-icon>
|
|
||||||
</sl-menu-item>
|
|
||||||
<sl-divider></sl-divider>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/toeverything/blocksuite"
|
|
||||||
>
|
|
||||||
<sl-menu-item>
|
|
||||||
<sl-icon slot="prefix" name="github"></sl-icon>
|
|
||||||
GitHub
|
|
||||||
</sl-menu-item>
|
|
||||||
</a>
|
|
||||||
</sl-menu>
|
|
||||||
</sl-dropdown>
|
|
||||||
|
|
||||||
<!-- undo/redo group -->
|
|
||||||
<sl-button-group label="History">
|
|
||||||
<!-- undo -->
|
|
||||||
<sl-tooltip content="Undo" placement="bottom" hoist>
|
|
||||||
<sl-button
|
|
||||||
pill
|
|
||||||
size="small"
|
|
||||||
content="Undo"
|
|
||||||
.disabled=${!this._canUndo}
|
|
||||||
@click=${() => {
|
|
||||||
this.doc.undo();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<sl-icon name="arrow-counterclockwise" label="Undo"></sl-icon>
|
|
||||||
</sl-button>
|
|
||||||
</sl-tooltip>
|
|
||||||
<!-- redo -->
|
|
||||||
<sl-tooltip content="Redo" placement="bottom" hoist>
|
|
||||||
<sl-button
|
|
||||||
pill
|
|
||||||
size="small"
|
|
||||||
content="Redo"
|
|
||||||
.disabled=${!this._canRedo}
|
|
||||||
@click=${() => {
|
|
||||||
this.doc.redo();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<sl-icon name="arrow-clockwise" label="Redo"></sl-icon>
|
|
||||||
</sl-button>
|
|
||||||
</sl-tooltip>
|
|
||||||
</sl-button-group>
|
|
||||||
|
|
||||||
<sl-tooltip content="Start Collaboration" placement="bottom" hoist>
|
|
||||||
<sl-button @click=${this._startCollaboration} size="small" circle>
|
|
||||||
<sl-icon name="people" label="Collaboration"></sl-icon>
|
|
||||||
</sl-button>
|
|
||||||
</sl-tooltip>
|
|
||||||
<sl-tooltip content="Docs" placement="bottom" hoist>
|
|
||||||
<sl-button
|
|
||||||
@click=${this._toggleDocsPanel}
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
data-docs-panel-toggle
|
|
||||||
>
|
|
||||||
<sl-icon name="filetype-doc" label="Doc"></sl-icon>
|
|
||||||
</sl-button>
|
|
||||||
</sl-tooltip>
|
|
||||||
|
|
||||||
${new URLSearchParams(location.search).get('room')
|
|
||||||
? html`<sl-input
|
|
||||||
placeholder="Your name in room"
|
|
||||||
clearable
|
|
||||||
size="small"
|
|
||||||
@blur=${(e: Event) => {
|
|
||||||
if ((e.target as HTMLInputElement).value.length > 0) {
|
|
||||||
this.collection.awarenessStore.awareness.setLocalStateField(
|
|
||||||
'user',
|
|
||||||
{
|
|
||||||
name: (e.target as HTMLInputElement).value ?? '',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.collection.awarenessStore.awareness.setLocalStateField(
|
|
||||||
'user',
|
|
||||||
{
|
|
||||||
name: 'Unknown',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></sl-input
|
|
||||||
></sl-tooltip>`
|
|
||||||
: nothing}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display: flex; gap: 12px">
|
|
||||||
<!-- Edgeless Theme button -->
|
|
||||||
${this._docMode === 'edgeless'
|
|
||||||
? html`<sl-tooltip
|
|
||||||
content="Edgeless Theme"
|
|
||||||
placement="bottom"
|
|
||||||
hoist
|
|
||||||
>
|
|
||||||
<sl-button
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
@click=${() => mockEdgelessTheme.toggleTheme()}
|
|
||||||
>
|
|
||||||
<sl-icon
|
|
||||||
name="${mockEdgelessTheme.theme$.value === 'dark'
|
|
||||||
? 'moon'
|
|
||||||
: 'brightness-high'}"
|
|
||||||
label="Edgeless Theme"
|
|
||||||
></sl-icon>
|
|
||||||
</sl-button>
|
|
||||||
</sl-tooltip>`
|
|
||||||
: nothing}
|
|
||||||
<!-- Present button -->
|
|
||||||
${this._docMode === 'edgeless'
|
|
||||||
? html`<sl-tooltip content="Present" placement="bottom" hoist>
|
|
||||||
<sl-button
|
|
||||||
size="small"
|
|
||||||
circle
|
|
||||||
@click=${() => {
|
|
||||||
if (this.rootService instanceof EdgelessRootService) {
|
|
||||||
this.rootService.gfx.tool.setTool('frameNavigator', {
|
|
||||||
mode: 'fit',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<sl-icon name="easel" label="Present"></sl-icon>
|
|
||||||
</sl-button>
|
|
||||||
</sl-tooltip>`
|
|
||||||
: nothing}
|
|
||||||
<sl-button-group label="Mode" style="margin-right: 12px">
|
|
||||||
<!-- switch to page -->
|
|
||||||
<sl-tooltip content="Page" placement="bottom" hoist>
|
|
||||||
<sl-button
|
|
||||||
pill
|
|
||||||
size="small"
|
|
||||||
content="Page"
|
|
||||||
.disabled=${this._docMode !== 'edgeless'}
|
|
||||||
@click=${this._switchEditorMode}
|
|
||||||
>
|
|
||||||
<sl-icon name="filetype-doc" label="Page"></sl-icon>
|
|
||||||
</sl-button>
|
|
||||||
</sl-tooltip>
|
|
||||||
<!-- switch to edgeless -->
|
|
||||||
<sl-tooltip content="Edgeless" placement="bottom" hoist>
|
|
||||||
<sl-button
|
|
||||||
pill
|
|
||||||
size="small"
|
|
||||||
content="Edgeless"
|
|
||||||
.disabled=${this._docMode !== 'page'}
|
|
||||||
@click=${this._switchEditorMode}
|
|
||||||
>
|
|
||||||
<sl-icon name="palette" label="Edgeless"></sl-icon>
|
|
||||||
</sl-button>
|
|
||||||
</sl-tooltip>
|
|
||||||
</sl-button-group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor _canRedo = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor _canUndo = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor _dark = localStorage.getItem('blocksuite:dark') === 'true';
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor _docMode: DocMode = 'page';
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor collection!: Workspace;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor docsPanel!: DocsPanel;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor editor!: AffineEditorContainer;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor leftSidePanel!: LeftSidePanel;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
accessor readonly = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'collab-debug-menu': CollabDebugMenu;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -35,7 +35,6 @@ function renderToImageData(index: number, scale: number) {
|
|||||||
bitmap.fill(0, 0, width, height);
|
bitmap.fill(0, 0, width, height);
|
||||||
page.render(bitmap, 0, 0, width, height, 0, flags);
|
page.render(bitmap, 0, 0, width, height, 0, flags);
|
||||||
|
|
||||||
// @ts-expect-error ignore
|
|
||||||
const data = new Uint8ClampedArray(bitmap.toUint8Array());
|
const data = new Uint8ClampedArray(bitmap.toUint8Array());
|
||||||
|
|
||||||
bitmap.close();
|
bitmap.close();
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { DocModeProvider } from '@blocksuite/blocks';
|
import type { DocModeProvider } from '@blocksuite/blocks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import type { Blocks, Doc, Workspace } from '@blocksuite/store';
|
import type { Doc, Store, Workspace } from '@blocksuite/store';
|
||||||
import type { LitElement } from 'lit';
|
import type { LitElement } from 'lit';
|
||||||
|
|
||||||
export function getDocFromUrlParams(collection: Workspace, url: URL) {
|
export function getDocFromUrlParams(collection: Workspace, url: URL) {
|
||||||
let doc: Blocks | null = null;
|
let doc: Store | null = null;
|
||||||
|
|
||||||
const docId = decodeURIComponent(url.hash.slice(1));
|
const docId = decodeURIComponent(url.hash.slice(1));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AffineSchemas, SpecProvider, TestUtils } from '@blocksuite/blocks';
|
import { AffineSchemas, SpecProvider, TestUtils } from '@blocksuite/blocks';
|
||||||
import type { BlockSuiteFlags } from '@blocksuite/global/types';
|
|
||||||
import { nanoid, Schema, Transformer } from '@blocksuite/store';
|
import { nanoid, Schema, Transformer } from '@blocksuite/store';
|
||||||
import {
|
import {
|
||||||
createAutoIncrementIdGenerator,
|
createAutoIncrementIdGenerator,
|
||||||
@@ -48,27 +47,10 @@ export function createStarterDocCollection() {
|
|||||||
blobSources.shadows.push(new IndexedDBBlobSource(collectionId));
|
blobSources.shadows.push(new IndexedDBBlobSource(collectionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
const flags: Partial<BlockSuiteFlags> = Object.fromEntries(
|
|
||||||
Array.from(params.entries())
|
|
||||||
.filter(([key]) => key.startsWith('enable_'))
|
|
||||||
.map(([k, v]) => [k, v === 'true'])
|
|
||||||
);
|
|
||||||
|
|
||||||
const options: DocCollectionOptions = {
|
const options: DocCollectionOptions = {
|
||||||
id: collectionId,
|
id: collectionId,
|
||||||
schema,
|
schema,
|
||||||
idGenerator,
|
idGenerator,
|
||||||
defaultFlags: {
|
|
||||||
enable_synced_doc_block: true,
|
|
||||||
enable_pie_menu: true,
|
|
||||||
enable_lasso_tool: true,
|
|
||||||
enable_edgeless_text: true,
|
|
||||||
enable_color_picker: true,
|
|
||||||
enable_mind_map_import: true,
|
|
||||||
enable_advanced_block_visibility: true,
|
|
||||||
enable_shape_shadow_blur: false,
|
|
||||||
...flags,
|
|
||||||
},
|
|
||||||
awarenessSources: [new BroadcastChannelAwarenessSource(id)],
|
awarenessSources: [new BroadcastChannelAwarenessSource(id)],
|
||||||
docSources,
|
docSources,
|
||||||
blobSources,
|
blobSources,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DeltaInsert } from '@inline/types.js';
|
import type { DeltaInsert } from '@blocksuite/inline';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import './utils/declare-test-window.js';
|
import './utils/declare-test-window.js';
|
||||||
|
|
||||||
|
import type { BlockSnapshot } from '@blocksuite/store';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import type { BlockSnapshot } from '@store/index.js';
|
|
||||||
import { ignoreSnapshotId } from 'utils/ignore.js';
|
import { ignoreSnapshotId } from 'utils/ignore.js';
|
||||||
import { getEmbedCardToolbar } from 'utils/query.js';
|
import { getEmbedCardToolbar } from 'utils/query.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import type { BlockSnapshot } from '@blocksuite/store';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import type { BlockSnapshot } from '@store/index.js';
|
|
||||||
import { lightThemeV2 } from '@toeverything/theme/v2';
|
import { lightThemeV2 } from '@toeverything/theme/v2';
|
||||||
|
|
||||||
import { initDatabaseColumn } from '../database/actions.js';
|
import { initDatabaseColumn } from '../database/actions.js';
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type {
|
import type {
|
||||||
RichTextCell,
|
RichTextCell,
|
||||||
RichTextCellEditing,
|
RichTextCellEditing,
|
||||||
} from '@blocks/database-block/properties/rich-text/cell-renderer.js';
|
} from '@blocksuite/affine-block-database';
|
||||||
import { press } from '@inline/__tests__/utils.js';
|
import { ZERO_WIDTH_SPACE } from '@blocksuite/inline';
|
||||||
import { ZERO_WIDTH_SPACE } from '@inline/consts.js';
|
|
||||||
import { expect, type Locator, type Page } from '@playwright/test';
|
import { expect, type Locator, type Page } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -18,6 +17,11 @@ import {
|
|||||||
waitNextFrame,
|
waitNextFrame,
|
||||||
} from '../utils/actions/misc.js';
|
} from '../utils/actions/misc.js';
|
||||||
|
|
||||||
|
export async function press(page: Page, content: string) {
|
||||||
|
await page.keyboard.press(content, { delay: 50 });
|
||||||
|
await page.waitForTimeout(50);
|
||||||
|
}
|
||||||
|
|
||||||
export async function initDatabaseColumn(page: Page, title = '') {
|
export async function initDatabaseColumn(page: Page, title = '') {
|
||||||
const editor = getEditorLocator(page);
|
const editor = getEditorLocator(page);
|
||||||
await editor.locator('affine-data-view-table-group').first().hover();
|
await editor.locator('affine-data-view-table-group').first().hover();
|
||||||
@@ -265,7 +269,7 @@ export async function assertDatabaseCellLink(
|
|||||||
'affine-database-link-cell-editing'
|
'affine-database-link-cell-editing'
|
||||||
);
|
);
|
||||||
if (!richText) throw new Error('Missing database rich text cell');
|
if (!richText) throw new Error('Missing database rich text cell');
|
||||||
return richText.inlineEditor.yText.toString();
|
return richText.inlineEditor!.yText.toString();
|
||||||
},
|
},
|
||||||
{ rowIndex, columnIndex }
|
{ rowIndex, columnIndex }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { press } from '@inline/__tests__/utils.js';
|
|
||||||
import { expect, type Page } from '@playwright/test';
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
import { type } from '../utils/actions/index.js';
|
import { type } from '../utils/actions/index.js';
|
||||||
@@ -9,7 +8,12 @@ import {
|
|||||||
waitNextFrame,
|
waitNextFrame,
|
||||||
} from '../utils/actions/misc.js';
|
} from '../utils/actions/misc.js';
|
||||||
import { test } from '../utils/playwright.js';
|
import { test } from '../utils/playwright.js';
|
||||||
import { changeColumnType, moveToCenterOf, pressKey } from './actions.js';
|
import {
|
||||||
|
changeColumnType,
|
||||||
|
moveToCenterOf,
|
||||||
|
press,
|
||||||
|
pressKey,
|
||||||
|
} from './actions.js';
|
||||||
|
|
||||||
const addRow = async (page: Page, count: number = 1) => {
|
const addRow = async (page: Page, count: number = 1) => {
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { press } from '@inline/__tests__/utils.js';
|
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -7,6 +6,7 @@ import {
|
|||||||
initEmptyDatabaseState,
|
initEmptyDatabaseState,
|
||||||
} from '../utils/actions/misc.js';
|
} from '../utils/actions/misc.js';
|
||||||
import { test } from '../utils/playwright.js';
|
import { test } from '../utils/playwright.js';
|
||||||
|
import { press } from './actions.js';
|
||||||
|
|
||||||
test.describe('title', () => {
|
test.describe('title', () => {
|
||||||
test('should able to link doc by press @', async ({ page }) => {
|
test('should able to link doc by press @', async ({ page }) => {
|
||||||
|
|||||||
@@ -138,7 +138,9 @@ test('zoom by mouse without ctrl pressed when edgelessScrollZoom is enabled', as
|
|||||||
|
|
||||||
// enable edgelessScrollZoom
|
// enable edgelessScrollZoom
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
|
// @ts-expect-error set a setting
|
||||||
window.editorSetting$.value = {
|
window.editorSetting$.value = {
|
||||||
|
// @ts-expect-error set a setting
|
||||||
...window.editorSetting$.value,
|
...window.editorSetting$.value,
|
||||||
edgelessScrollZoom: true,
|
edgelessScrollZoom: true,
|
||||||
};
|
};
|
||||||
@@ -153,7 +155,9 @@ test('zoom by mouse without ctrl pressed when edgelessScrollZoom is enabled', as
|
|||||||
|
|
||||||
// disable edgelessScrollZoom
|
// disable edgelessScrollZoom
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
|
// @ts-expect-error set a setting
|
||||||
window.editorSetting$.value = {
|
window.editorSetting$.value = {
|
||||||
|
// @ts-expect-error set a setting
|
||||||
...window.editorSetting$.value,
|
...window.editorSetting$.value,
|
||||||
edgelessScrollZoom: false,
|
edgelessScrollZoom: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { EdgelessTextBlockComponent } from '@blocks/edgeless-text-block/edgeless-text-block.js';
|
import type { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text';
|
||||||
import { Bound } from '@blocksuite/global/utils';
|
import { Bound } from '@blocksuite/global/utils';
|
||||||
import { expect, type Page } from '@playwright/test';
|
import { expect, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DeltaInsert } from '@inline/types.js';
|
import type { DeltaInsert } from '@blocksuite/inline';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,17 +1,158 @@
|
|||||||
import {
|
import type {
|
||||||
assertSelection,
|
DeltaInsert,
|
||||||
enterInlineEditorPlayground,
|
InlineEditor,
|
||||||
focusInlineRichText,
|
InlineRange,
|
||||||
getDeltaFromInlineRichText,
|
} from '@blocksuite/inline';
|
||||||
getInlineRangeIndexRect,
|
import { ZERO_WIDTH_SPACE } from '@blocksuite/inline';
|
||||||
getInlineRichTextLine,
|
import { expect, type Page, test } from '@playwright/test';
|
||||||
press,
|
|
||||||
setInlineRichTextRange,
|
// FIXME(mirone): copy paste from framework/inline/__tests__/utils.ts
|
||||||
type,
|
const defaultPlaygroundURL = new URL(
|
||||||
} from '@inline/__tests__/utils.js';
|
`http://localhost:${process.env.CI ? 4173 : 5173}/`
|
||||||
import { ZERO_WIDTH_SPACE } from '@inline/consts.js';
|
);
|
||||||
import type { InlineEditor } from '@inline/index.js';
|
|
||||||
import { expect, test } from '@playwright/test';
|
async function type(page: Page, content: string) {
|
||||||
|
await page.keyboard.type(content, { delay: 50 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function press(page: Page, content: string) {
|
||||||
|
await page.keyboard.press(content, { delay: 50 });
|
||||||
|
await page.waitForTimeout(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enterInlineEditorPlayground(page: Page) {
|
||||||
|
const url = new URL('examples/inline/index.html', defaultPlaygroundURL);
|
||||||
|
await page.goto(url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function focusInlineRichText(page: Page, index = 0): Promise<void> {
|
||||||
|
await page.evaluate(index => {
|
||||||
|
const richTexts = document
|
||||||
|
.querySelector('test-page')
|
||||||
|
?.querySelectorAll('test-rich-text');
|
||||||
|
|
||||||
|
if (!richTexts) {
|
||||||
|
throw new Error('Cannot find test-rich-text');
|
||||||
|
}
|
||||||
|
|
||||||
|
(richTexts[index] as any).inlineEditor.focusEnd();
|
||||||
|
}, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDeltaFromInlineRichText(
|
||||||
|
page: Page,
|
||||||
|
index = 0
|
||||||
|
): Promise<DeltaInsert> {
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
return page.evaluate(index => {
|
||||||
|
const richTexts = document
|
||||||
|
.querySelector('test-page')
|
||||||
|
?.querySelectorAll('test-rich-text');
|
||||||
|
|
||||||
|
if (!richTexts) {
|
||||||
|
throw new Error('Cannot find test-rich-text');
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = (richTexts[index] as any).inlineEditor as InlineEditor;
|
||||||
|
return editor.yText.toDelta();
|
||||||
|
}, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setInlineRichTextRange(
|
||||||
|
page: Page,
|
||||||
|
inlineRange: InlineRange,
|
||||||
|
index = 0
|
||||||
|
): Promise<void> {
|
||||||
|
await page.evaluate(
|
||||||
|
([inlineRange, index]) => {
|
||||||
|
const richTexts = document
|
||||||
|
.querySelector('test-page')
|
||||||
|
?.querySelectorAll('test-rich-text');
|
||||||
|
|
||||||
|
if (!richTexts) {
|
||||||
|
throw new Error('Cannot find test-rich-text');
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = (richTexts[index as number] as any)
|
||||||
|
.inlineEditor as InlineEditor;
|
||||||
|
editor.setInlineRange(inlineRange as InlineRange);
|
||||||
|
},
|
||||||
|
[inlineRange, index]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInlineRichTextLine(
|
||||||
|
page: Page,
|
||||||
|
index: number,
|
||||||
|
i = 0
|
||||||
|
): Promise<readonly [string, number]> {
|
||||||
|
return page.evaluate(
|
||||||
|
([index, i]) => {
|
||||||
|
const richTexts = document.querySelectorAll('test-rich-text');
|
||||||
|
|
||||||
|
if (!richTexts) {
|
||||||
|
throw new Error('Cannot find test-rich-text');
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = (richTexts[i] as any).inlineEditor as InlineEditor;
|
||||||
|
const result = editor.getLine(index);
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Cannot find line');
|
||||||
|
}
|
||||||
|
const { line, rangeIndexRelatedToLine } = result;
|
||||||
|
return [line.vTextContent, rangeIndexRelatedToLine] as const;
|
||||||
|
},
|
||||||
|
[index, i]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInlineRangeIndexRect(
|
||||||
|
page: Page,
|
||||||
|
[richTextIndex, inlineIndex]: [number, number],
|
||||||
|
coordOffSet: { x: number; y: number } = { x: 0, y: 0 }
|
||||||
|
) {
|
||||||
|
const rect = await page.evaluate(
|
||||||
|
({ richTextIndex, inlineIndex: vIndex, coordOffSet }) => {
|
||||||
|
const richText = document.querySelectorAll('test-rich-text')[
|
||||||
|
richTextIndex
|
||||||
|
] as any;
|
||||||
|
const domRange = richText.inlineEditor.toDomRange({
|
||||||
|
index: vIndex,
|
||||||
|
length: 0,
|
||||||
|
});
|
||||||
|
const pointBound = domRange.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: pointBound.left + coordOffSet.x,
|
||||||
|
y: pointBound.top + pointBound.height / 2 + coordOffSet.y,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
richTextIndex,
|
||||||
|
inlineIndex,
|
||||||
|
coordOffSet,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertSelection(
|
||||||
|
page: Page,
|
||||||
|
richTextIndex: number,
|
||||||
|
rangeIndex: number,
|
||||||
|
rangeLength = 0
|
||||||
|
) {
|
||||||
|
const actual = await page.evaluate(
|
||||||
|
([richTextIndex]) => {
|
||||||
|
const richText =
|
||||||
|
document?.querySelectorAll('test-rich-text')[richTextIndex];
|
||||||
|
// @ts-expect-error getInlineRange
|
||||||
|
const inlineEditor = richText.inlineEditor;
|
||||||
|
return inlineEditor?.getInlineRange();
|
||||||
|
},
|
||||||
|
[richTextIndex]
|
||||||
|
);
|
||||||
|
expect(actual).toEqual({ index: rangeIndex, length: rangeLength });
|
||||||
|
}
|
||||||
|
|
||||||
test('basic input', async ({ page }) => {
|
test('basic input', async ({ page }) => {
|
||||||
await enterInlineEditorPlayground(page);
|
await enterInlineEditorPlayground(page);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ZERO_WIDTH_SPACE } from '@inline/consts.js';
|
import { ZERO_WIDTH_SPACE } from '@blocksuite/inline';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import {
|
import {
|
||||||
assertRichTextInlineDeltas,
|
assertRichTextInlineDeltas,
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
"@blocksuite/affine-shared": "workspace:*",
|
"@blocksuite/affine-shared": "workspace:*",
|
||||||
"@blocksuite/block-std": "workspace:*",
|
"@blocksuite/block-std": "workspace:*",
|
||||||
"@blocksuite/global": "workspace:*",
|
"@blocksuite/global": "workspace:*",
|
||||||
|
"@blocksuite/inline": "workspace:*",
|
||||||
"@blocksuite/presets": "workspace:*",
|
"@blocksuite/presets": "workspace:*",
|
||||||
|
"@blocksuite/store": "workspace:*",
|
||||||
"@playwright/test": "=1.49.1",
|
"@playwright/test": "=1.49.1",
|
||||||
"@toeverything/theme": "^1.1.7"
|
"@toeverything/theme": "^1.1.7"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DeltaInsert } from '@inline/types.js';
|
import type { DeltaInsert } from '@blocksuite/inline';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -835,7 +835,7 @@ test.describe('slash menu with customize menu', () => {
|
|||||||
const SlashMenuWidget = window.$blocksuite.blocks.AffineSlashMenuWidget;
|
const SlashMenuWidget = window.$blocksuite.blocks.AffineSlashMenuWidget;
|
||||||
|
|
||||||
class CustomSlashMenu extends SlashMenuWidget {
|
class CustomSlashMenu extends SlashMenuWidget {
|
||||||
config = {
|
override config = {
|
||||||
...SlashMenuWidget.DEFAULT_CONFIG,
|
...SlashMenuWidget.DEFAULT_CONFIG,
|
||||||
items: [
|
items: [
|
||||||
{ groupName: 'Custom Menu' },
|
{ groupName: 'Custom Menu' },
|
||||||
|
|||||||
@@ -2,13 +2,7 @@
|
|||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"outDir": "./dist",
|
"outDir": "./dist"
|
||||||
"paths": {
|
|
||||||
"@blocks/*": ["../blocks/src/*"],
|
|
||||||
"@inline/*": ["../framework/inline/src/*"],
|
|
||||||
"@store/*": ["../framework/store/src/*"],
|
|
||||||
"@playground/*": ["../playground/*"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["**.spec.ts", "**.test.ts", "**/**.ts"],
|
"include": ["**.spec.ts", "**.test.ts", "**/**.ts"],
|
||||||
"exclude": ["dist"],
|
"exclude": ["dist"],
|
||||||
@@ -18,6 +12,8 @@
|
|||||||
{ "path": "../affine/shared" },
|
{ "path": "../affine/shared" },
|
||||||
{ "path": "../framework/block-std" },
|
{ "path": "../framework/block-std" },
|
||||||
{ "path": "../framework/global" },
|
{ "path": "../framework/global" },
|
||||||
{ "path": "../presets" }
|
{ "path": "../framework/inline" },
|
||||||
|
{ "path": "../presets" },
|
||||||
|
{ "path": "../framework/store" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ export async function updateBlockType(
|
|||||||
) {
|
) {
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
([flavour, type]) => {
|
([flavour, type]) => {
|
||||||
window.host.std.command
|
(window.host.std.command.chain() as any)
|
||||||
.chain()
|
|
||||||
.updateBlockType({
|
.updateBlockType({
|
||||||
flavour,
|
flavour,
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { IPoint } from '@blocksuite/global/utils';
|
import type { IPoint } from '@blocksuite/global/utils';
|
||||||
|
import type { Store } from '@blocksuite/store';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import type { Store } from '@store/index.js';
|
|
||||||
|
|
||||||
import { toViewCoord } from './edgeless.js';
|
import { toViewCoord } from './edgeless.js';
|
||||||
import { waitNextFrame } from './misc.js';
|
import { waitNextFrame } from './misc.js';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import '../declare-test-window.js';
|
import '../declare-test-window.js';
|
||||||
|
|
||||||
import type { NoteBlockModel, NoteDisplayMode } from '@blocks/index.js';
|
import type { NoteBlockModel, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||||
import type { IPoint, IVec } from '@blocksuite/global/utils';
|
import type { IPoint, IVec } from '@blocksuite/global/utils';
|
||||||
import { assertExists, sleep } from '@blocksuite/global/utils';
|
import { assertExists, sleep } from '@blocksuite/global/utils';
|
||||||
import type { Locator, Page } from '@playwright/test';
|
import type { Locator, Page } from '@playwright/test';
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import '../declare-test-window.js';
|
import '../declare-test-window.js';
|
||||||
|
|
||||||
import type { DatabaseBlockModel, ListType, RichText } from '@blocks/index.js';
|
import type { EditorHost } from '@blocksuite/block-std';
|
||||||
import type { EditorHost, ExtensionType } from '@blocksuite/block-std';
|
import type {
|
||||||
import type { BlockSuiteFlags } from '@blocksuite/global/types';
|
BlockSuiteFlags,
|
||||||
|
DatabaseBlockModel,
|
||||||
|
ListType,
|
||||||
|
RichText,
|
||||||
|
} from '@blocksuite/blocks';
|
||||||
|
import type { Container } from '@blocksuite/global/di';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import type { InlineRange, InlineRootElement } from '@blocksuite/inline';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import type { InlineRange, InlineRootElement } from '@inline/index.js';
|
import type { BlockModel, ExtensionType } from '@blocksuite/store';
|
||||||
import type { CustomFramePanel } from '@playground/apps/_common/components/custom-frame-panel.js';
|
import { uuidv4 } from '@blocksuite/store';
|
||||||
import type { CustomOutlinePanel } from '@playground/apps/_common/components/custom-outline-panel.js';
|
|
||||||
import type { CustomOutlineViewer } from '@playground/apps/_common/components/custom-outline-viewer.js';
|
|
||||||
import type { DocsPanel } from '@playground/apps/_common/components/docs-panel.js';
|
|
||||||
import type { StarterDebugMenu } from '@playground/apps/_common/components/starter-debug-menu.js';
|
|
||||||
import type { ConsoleMessage, Locator, Page } from '@playwright/test';
|
import type { ConsoleMessage, Locator, Page } from '@playwright/test';
|
||||||
import { expect } from '@playwright/test';
|
import { expect } from '@playwright/test';
|
||||||
import type { BlockModel } from '@store/schema/index.js';
|
|
||||||
import { uuidv4 } from '@store/utils/id-generator.js';
|
|
||||||
import lz from 'lz-string';
|
import lz from 'lz-string';
|
||||||
|
|
||||||
import { currentEditorIndex, multiEditor } from '../multiple-editor.js';
|
import { currentEditorIndex, multiEditor } from '../multiple-editor.js';
|
||||||
@@ -95,7 +95,7 @@ async function initEmptyEditor({
|
|||||||
for (const [key, value] of Object.entries(flags)) {
|
for (const [key, value] of Object.entries(flags)) {
|
||||||
doc
|
doc
|
||||||
.get(window.$blocksuite.blocks.FeatureFlagService)
|
.get(window.$blocksuite.blocks.FeatureFlagService)
|
||||||
.setFlag(key, value);
|
.setFlag(key as keyof BlockSuiteFlags, value);
|
||||||
}
|
}
|
||||||
doc
|
doc
|
||||||
.get(window.$blocksuite.blocks.FeatureFlagService)
|
.get(window.$blocksuite.blocks.FeatureFlagService)
|
||||||
@@ -105,7 +105,7 @@ async function initEmptyEditor({
|
|||||||
const defaultExtensions: ExtensionType[] = [
|
const defaultExtensions: ExtensionType[] = [
|
||||||
...window.$blocksuite.defaultExtensions(),
|
...window.$blocksuite.defaultExtensions(),
|
||||||
{
|
{
|
||||||
setup: di => {
|
setup: (di: Container) => {
|
||||||
di.addImpl(window.$blocksuite.identifiers.ParseDocUrlService, {
|
di.addImpl(window.$blocksuite.identifiers.ParseDocUrlService, {
|
||||||
parseDocUrl() {
|
parseDocUrl() {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -114,12 +114,13 @@ async function initEmptyEditor({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setup: di => {
|
setup: (di: Container) => {
|
||||||
di.override(
|
di.override(
|
||||||
window.$blocksuite.identifiers.DocModeProvider,
|
window.$blocksuite.identifiers.DocModeProvider,
|
||||||
|
// @ts-expect-error set mock service
|
||||||
window.$blocksuite.mockServices.mockDocModeService(
|
window.$blocksuite.mockServices.mockDocModeService(
|
||||||
() => editor.mode,
|
() => editor.mode,
|
||||||
mode => editor.switchEditor(mode)
|
(mode: 'page' | 'edgeless') => editor.switchEditor(mode)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -149,28 +150,35 @@ async function initEmptyEditor({
|
|||||||
|
|
||||||
editor.updateComplete
|
editor.updateComplete
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const debugMenu: StarterDebugMenu =
|
const debugMenu = document.createElement('starter-debug-menu');
|
||||||
document.createElement('starter-debug-menu');
|
const docsPanel = document.createElement('docs-panel');
|
||||||
const docsPanel: DocsPanel = document.createElement('docs-panel');
|
const framePanel = document.createElement('custom-frame-panel');
|
||||||
const framePanel: CustomFramePanel =
|
const outlinePanel = document.createElement('custom-outline-panel');
|
||||||
document.createElement('custom-frame-panel');
|
const outlineViewer = document.createElement(
|
||||||
const outlinePanel: CustomOutlinePanel = document.createElement(
|
|
||||||
'custom-outline-panel'
|
|
||||||
);
|
|
||||||
const outlineViewer: CustomOutlineViewer = document.createElement(
|
|
||||||
'custom-outline-viewer'
|
'custom-outline-viewer'
|
||||||
);
|
);
|
||||||
|
// @ts-expect-error set test editor
|
||||||
docsPanel.editor = editor;
|
docsPanel.editor = editor;
|
||||||
|
// @ts-expect-error set test editor
|
||||||
framePanel.editor = editor;
|
framePanel.editor = editor;
|
||||||
|
// @ts-expect-error set test editor
|
||||||
outlinePanel.editor = editor;
|
outlinePanel.editor = editor;
|
||||||
|
// @ts-expect-error set test editor
|
||||||
outlineViewer.editor = editor;
|
outlineViewer.editor = editor;
|
||||||
|
// @ts-expect-error set test collection
|
||||||
debugMenu.collection = collection;
|
debugMenu.collection = collection;
|
||||||
|
// @ts-expect-error set test editor
|
||||||
debugMenu.editor = editor;
|
debugMenu.editor = editor;
|
||||||
|
// @ts-expect-error set test docsPanel
|
||||||
debugMenu.docsPanel = docsPanel;
|
debugMenu.docsPanel = docsPanel;
|
||||||
|
// @ts-expect-error set test framePanel
|
||||||
debugMenu.framePanel = framePanel;
|
debugMenu.framePanel = framePanel;
|
||||||
|
// @ts-expect-error set test outlineViewer
|
||||||
debugMenu.outlineViewer = outlineViewer;
|
debugMenu.outlineViewer = outlineViewer;
|
||||||
|
// @ts-expect-error set test outlinePanel
|
||||||
debugMenu.outlinePanel = outlinePanel;
|
debugMenu.outlinePanel = outlinePanel;
|
||||||
const leftSidePanel = document.createElement('left-side-panel');
|
const leftSidePanel = document.createElement('left-side-panel');
|
||||||
|
// @ts-expect-error set test leftSidePanel
|
||||||
debugMenu.leftSidePanel = leftSidePanel;
|
debugMenu.leftSidePanel = leftSidePanel;
|
||||||
document.body.append(debugMenu);
|
document.body.append(debugMenu);
|
||||||
document.body.append(leftSidePanel);
|
document.body.append(leftSidePanel);
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import './declare-test-window.js';
|
import './declare-test-window.js';
|
||||||
|
|
||||||
import type {
|
|
||||||
AffineInlineEditor,
|
|
||||||
NoteBlockModel,
|
|
||||||
RichText,
|
|
||||||
RootBlockModel,
|
|
||||||
} from '@blocks/index.js';
|
|
||||||
import type {
|
import type {
|
||||||
BlockComponent,
|
BlockComponent,
|
||||||
EditorHost,
|
EditorHost,
|
||||||
TextSelection,
|
TextSelection,
|
||||||
} from '@blocksuite/block-std';
|
} from '@blocksuite/block-std';
|
||||||
|
import type {
|
||||||
|
AffineInlineEditor,
|
||||||
|
NoteBlockModel,
|
||||||
|
RichText,
|
||||||
|
RootBlockModel,
|
||||||
|
} from '@blocksuite/blocks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { InlineRootElement } from '@inline/inline-editor.js';
|
import type { InlineRootElement } from '@blocksuite/inline';
|
||||||
|
import type { BlockModel } from '@blocksuite/store';
|
||||||
import { expect, type Locator, type Page } from '@playwright/test';
|
import { expect, type Locator, type Page } from '@playwright/test';
|
||||||
import type { BlockModel } from '@store/index.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_NOTE_HEIGHT,
|
DEFAULT_NOTE_HEIGHT,
|
||||||
@@ -462,7 +462,9 @@ export async function assertBlockChildrenFlavours(
|
|||||||
) {
|
) {
|
||||||
const actual = await page.evaluate(
|
const actual = await page.evaluate(
|
||||||
({ blockId }) => {
|
({ blockId }) => {
|
||||||
const element = document.querySelector(`[data-block-id="${blockId}"]`);
|
const element = document.querySelector<BlockComponent>(
|
||||||
|
`[data-block-id="${blockId}"]`
|
||||||
|
);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const model = element.model as BlockModel;
|
const model = element.model as BlockModel;
|
||||||
return model.children.map(child => child.flavour);
|
return model.children.map(child => child.flavour);
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import type { RefNodeSlotsProvider, TestUtils } from '@blocks/index.js';
|
import type {
|
||||||
|
DocModeProvider,
|
||||||
|
DocModeService,
|
||||||
|
ParseDocUrlProvider,
|
||||||
|
QuickSearchProvider,
|
||||||
|
ThemeProvider,
|
||||||
|
} from '@blocksuite/affine-shared/services';
|
||||||
import type {
|
import type {
|
||||||
EditorHost,
|
EditorHost,
|
||||||
ExtensionType,
|
WidgetViewMapExtension,
|
||||||
WidgetViewMapIdentifier,
|
WidgetViewMapIdentifier,
|
||||||
} from '@blocksuite/block-std';
|
} from '@blocksuite/block-std';
|
||||||
|
import type { RefNodeSlotsProvider, TestUtils } from '@blocksuite/blocks';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import type { StarterDebugMenu } from '@playground/apps/_common/components/starter-debug-menu.js';
|
|
||||||
import type {
|
import type {
|
||||||
BlockModel,
|
BlockModel,
|
||||||
Blocks,
|
ExtensionType,
|
||||||
|
Store,
|
||||||
Transformer,
|
Transformer,
|
||||||
Workspace,
|
Workspace,
|
||||||
} from '@store/index.js';
|
} from '@blocksuite/store';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -19,32 +26,32 @@ declare global {
|
|||||||
* the following instance are initialized in `packages/playground/apps/starter/main.ts`
|
* the following instance are initialized in `packages/playground/apps/starter/main.ts`
|
||||||
*/
|
*/
|
||||||
$blocksuite: {
|
$blocksuite: {
|
||||||
store: typeof import('../../framework/store/src/index.js');
|
store: typeof import('@blocksuite/store');
|
||||||
blocks: typeof import('../../blocks/src/index.js');
|
blocks: typeof import('@blocksuite/blocks');
|
||||||
global: {
|
global: {
|
||||||
utils: typeof import('../../framework/global/src/utils.js');
|
utils: typeof import('@blocksuite/global/utils');
|
||||||
};
|
};
|
||||||
editor: typeof import('../../presets/src/index.js');
|
editor: typeof import('@blocksuite/presets');
|
||||||
identifiers: {
|
identifiers: {
|
||||||
WidgetViewMapIdentifier: typeof WidgetViewMapIdentifier;
|
WidgetViewMapIdentifier: typeof WidgetViewMapIdentifier;
|
||||||
QuickSearchProvider: typeof import('../../affine/shared/src/services/quick-search-service.js').QuickSearchProvider;
|
QuickSearchProvider: typeof QuickSearchProvider;
|
||||||
DocModeProvider: typeof import('../../affine/shared/src/services/doc-mode-service.js').DocModeProvider;
|
DocModeProvider: typeof DocModeProvider;
|
||||||
ThemeProvider: typeof import('../../affine/shared/src/services/theme-service.js').ThemeProvider;
|
ThemeProvider: typeof ThemeProvider;
|
||||||
RefNodeSlotsProvider: typeof RefNodeSlotsProvider;
|
RefNodeSlotsProvider: typeof RefNodeSlotsProvider;
|
||||||
ParseDocUrlService: typeof import('../../affine/shared/src/services/parse-url-service.js').ParseDocUrlProvider;
|
ParseDocUrlService: typeof ParseDocUrlProvider;
|
||||||
};
|
};
|
||||||
defaultExtensions: () => ExtensionType[];
|
defaultExtensions: () => ExtensionType[];
|
||||||
extensions: {
|
extensions: {
|
||||||
WidgetViewMapExtension: typeof import('../../framework/block-std/src/extension/widget-view-map.js').WidgetViewMapExtension;
|
WidgetViewMapExtension: typeof WidgetViewMapExtension;
|
||||||
};
|
};
|
||||||
mockServices: {
|
mockServices: {
|
||||||
mockDocModeService: typeof import('../../playground/apps/_common/mock-services.js').mockDocModeService;
|
mockDocModeService: typeof DocModeService;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
collection: Workspace;
|
collection: Workspace;
|
||||||
blockSchema: Record<string, typeof BlockModel>;
|
blockSchema: Record<string, typeof BlockModel>;
|
||||||
doc: Blocks;
|
doc: Store;
|
||||||
debugMenu: StarterDebugMenu;
|
debugMenu: HTMLElement;
|
||||||
editor: AffineEditorContainer;
|
editor: AffineEditorContainer;
|
||||||
host: EditorHost;
|
host: EditorHost;
|
||||||
testUtils: TestUtils;
|
testUtils: TestUtils;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { BlockSnapshot } from '@store/index.js';
|
import type { BlockSnapshot } from '@blocksuite/store';
|
||||||
|
|
||||||
export function ignoreFields(target: unknown, keys: string[]): unknown {
|
export function ignoreFields(target: unknown, keys: string[]): unknown {
|
||||||
if (Array.isArray(target)) {
|
if (Array.isArray(target)) {
|
||||||
|
|||||||
@@ -107,13 +107,6 @@ export class InitCommand extends Command {
|
|||||||
['references'],
|
['references'],
|
||||||
this.workspace.packages
|
this.workspace.packages
|
||||||
.filter(p => p.isTsProject)
|
.filter(p => p.isTsProject)
|
||||||
.filter(
|
|
||||||
p =>
|
|
||||||
// NOTE(@forehalo): there two packages including outdated types, will be fixed when merged with affine-e2e
|
|
||||||
!['@blocksuite/playground', '@blocksuite/legacy-e2e'].includes(
|
|
||||||
p.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.map(p => ({ path: p.path.relativePath })),
|
.map(p => ({ path: p.path.relativePath })),
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -452,7 +452,9 @@ export const PackageList = [
|
|||||||
'blocksuite/affine/shared',
|
'blocksuite/affine/shared',
|
||||||
'blocksuite/framework/block-std',
|
'blocksuite/framework/block-std',
|
||||||
'blocksuite/framework/global',
|
'blocksuite/framework/global',
|
||||||
|
'blocksuite/framework/inline',
|
||||||
'blocksuite/presets',
|
'blocksuite/presets',
|
||||||
|
'blocksuite/framework/store',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -82,7 +82,9 @@
|
|||||||
{ "path": "./blocksuite/framework/inline" },
|
{ "path": "./blocksuite/framework/inline" },
|
||||||
{ "path": "./blocksuite/framework/store" },
|
{ "path": "./blocksuite/framework/store" },
|
||||||
{ "path": "./blocksuite/framework/sync" },
|
{ "path": "./blocksuite/framework/sync" },
|
||||||
|
{ "path": "./blocksuite/playground" },
|
||||||
{ "path": "./blocksuite/presets" },
|
{ "path": "./blocksuite/presets" },
|
||||||
|
{ "path": "./blocksuite/tests-legacy" },
|
||||||
{ "path": "./packages/backend/native" },
|
{ "path": "./packages/backend/native" },
|
||||||
{ "path": "./packages/backend/server" },
|
{ "path": "./packages/backend/server" },
|
||||||
{ "path": "./packages/common/debug" },
|
{ "path": "./packages/common/debug" },
|
||||||
|
|||||||
@@ -4011,7 +4011,9 @@ __metadata:
|
|||||||
"@blocksuite/affine-shared": "workspace:*"
|
"@blocksuite/affine-shared": "workspace:*"
|
||||||
"@blocksuite/block-std": "workspace:*"
|
"@blocksuite/block-std": "workspace:*"
|
||||||
"@blocksuite/global": "workspace:*"
|
"@blocksuite/global": "workspace:*"
|
||||||
|
"@blocksuite/inline": "workspace:*"
|
||||||
"@blocksuite/presets": "workspace:*"
|
"@blocksuite/presets": "workspace:*"
|
||||||
|
"@blocksuite/store": "workspace:*"
|
||||||
"@playwright/test": "npm:=1.49.1"
|
"@playwright/test": "npm:=1.49.1"
|
||||||
"@toeverything/theme": "npm:^1.1.7"
|
"@toeverything/theme": "npm:^1.1.7"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
|
|||||||
Reference in New Issue
Block a user