feat(editor): add experimental feature adapter panel to AFFiNE canary (#12489)
Closes: [BS-2539](https://linear.app/affine-design/issue/BS-2539/为-affine-添加-ef,并且支持在-affine-预览对应的功能) > [!warning] > This feature is only available in the canary build and is intended for debugging purposes. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced an "Adapter Panel" feature with a new sidebar tab for previewing document content in multiple formats (Markdown, PlainText, HTML, Snapshot), controllable via a feature flag. - Added a fully integrated adapter panel component with reactive UI elements for selecting adapters, toggling HTML preview modes, and updating content. - Provided a customizable adapter panel for both main app and playground environments, supporting content transformation pipelines and export previews. - Enabled seamless toggling and live updating of adapter panel content through intuitive menus and controls. - **Localization** - Added English translations and descriptive settings for the Adapter Panel feature. - **Chores** - Added new package and workspace dependencies along with TypeScript project references to support the Adapter Panel modules and components. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,285 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
||||
import '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js';
|
||||
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import {
|
||||
defaultImageProxyMiddleware,
|
||||
docLinkBaseURLMiddlewareBuilder,
|
||||
embedSyncedDocMiddleware,
|
||||
type HtmlAdapter,
|
||||
HtmlAdapterFactoryIdentifier,
|
||||
type MarkdownAdapter,
|
||||
MarkdownAdapterFactoryIdentifier,
|
||||
type PlainTextAdapter,
|
||||
PlainTextAdapterFactoryIdentifier,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine/shared/adapters';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import type { DocSnapshot } from '@blocksuite/affine/store';
|
||||
import type { TestAffineEditorContainer } from '@blocksuite/integration-test';
|
||||
import { effect } from '@preact/signals-core';
|
||||
import type SlTabPanel from '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js';
|
||||
import { css, html, type PropertyValues } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('adapters-panel')
|
||||
export class AdaptersPanel extends WithDisposable(ShadowlessElement) {
|
||||
static override styles = css`
|
||||
adapters-panel {
|
||||
width: 36vw;
|
||||
}
|
||||
.adapters-container {
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
.adapter-container {
|
||||
padding: 0px 16px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 80px);
|
||||
white-space: pre-wrap;
|
||||
color: var(--affine-text-primary-color);
|
||||
overflow: auto;
|
||||
}
|
||||
.update-button {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 12px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
font-family: var(--affine-font-family);
|
||||
color: var(--affine-text-primary-color);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
}
|
||||
.update-button:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
.html-panel {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
}
|
||||
.html-preview-container,
|
||||
.html-panel-content {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: var(--affine-text-primary-color);
|
||||
overflow: auto;
|
||||
}
|
||||
.html-panel-footer {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
font-family: var(--affine-font-family);
|
||||
color: var(--affine-text-primary-color);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
line-height: 20px;
|
||||
}
|
||||
span[active] {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
get doc() {
|
||||
return this.editor.doc;
|
||||
}
|
||||
|
||||
private _createJob() {
|
||||
return this.doc.getTransformer([
|
||||
docLinkBaseURLMiddlewareBuilder(
|
||||
'https://example.com',
|
||||
this.doc.workspace.id
|
||||
).get(),
|
||||
titleMiddleware(this.doc.workspace.meta.docMetas),
|
||||
embedSyncedDocMiddleware('content'),
|
||||
defaultImageProxyMiddleware,
|
||||
]);
|
||||
}
|
||||
|
||||
private _getDocSnapshot() {
|
||||
const job = this._createJob();
|
||||
const result = job.docToSnapshot(this.doc);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _getHtmlContent() {
|
||||
const job = this._createJob();
|
||||
const htmlAdapterFactory = this.editor.std.provider.get(
|
||||
HtmlAdapterFactoryIdentifier
|
||||
);
|
||||
const htmlAdapter = htmlAdapterFactory.get(job) as HtmlAdapter;
|
||||
const result = await htmlAdapter.fromDoc(this.doc);
|
||||
return result?.file;
|
||||
}
|
||||
|
||||
private async _getMarkdownContent() {
|
||||
const job = this._createJob();
|
||||
const markdownAdapterFactory = this.editor.std.provider.get(
|
||||
MarkdownAdapterFactoryIdentifier
|
||||
);
|
||||
const markdownAdapter = markdownAdapterFactory.get(job) as MarkdownAdapter;
|
||||
const result = await markdownAdapter.fromDoc(this.doc);
|
||||
return result?.file;
|
||||
}
|
||||
|
||||
private async _getPlainTextContent() {
|
||||
const job = this._createJob();
|
||||
const plainTextAdapterFactory = this.editor.std.provider.get(
|
||||
PlainTextAdapterFactoryIdentifier
|
||||
);
|
||||
const plainTextAdapter = plainTextAdapterFactory.get(
|
||||
job
|
||||
) as PlainTextAdapter;
|
||||
const result = await plainTextAdapter.fromDoc(this.doc);
|
||||
return result?.file;
|
||||
}
|
||||
|
||||
private async _handleTabShow(name: string) {
|
||||
switch (name) {
|
||||
case 'markdown':
|
||||
this._markdownContent = (await this._getMarkdownContent()) || '';
|
||||
break;
|
||||
case 'html':
|
||||
this._htmlContent = (await this._getHtmlContent()) || '';
|
||||
break;
|
||||
case 'plaintext':
|
||||
this._plainTextContent = (await this._getPlainTextContent()) || '';
|
||||
break;
|
||||
case 'snapshot':
|
||||
this._docSnapshot = this._getDocSnapshot() || null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderHtmlPanel() {
|
||||
return html`
|
||||
${this._isHtmlPreview
|
||||
? html`<iframe
|
||||
class="html-preview-container"
|
||||
.srcdoc=${this._htmlContent}
|
||||
></iframe>`
|
||||
: html`<div class="html-panel-content">${this._htmlContent}</div>`}
|
||||
<div class="html-panel-footer">
|
||||
<span
|
||||
class="html-panel-footer-item"
|
||||
?active=${!this._isHtmlPreview}
|
||||
@click=${() => (this._isHtmlPreview = false)}
|
||||
>Source</span
|
||||
>
|
||||
<span
|
||||
class="html-panel-footer-item"
|
||||
?active=${this._isHtmlPreview}
|
||||
@click=${() => (this._isHtmlPreview = true)}
|
||||
>Preview</span
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _updateActiveTabContent() {
|
||||
if (!this._activeTab) return;
|
||||
const activeTabName = this._activeTab.name;
|
||||
await this._handleTabShow(activeTabName);
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
const doc = this.doc;
|
||||
if (doc) {
|
||||
this._updateActiveTabContent().catch(console.error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
const snapshotString = this._docSnapshot
|
||||
? JSON.stringify(this._docSnapshot, null, 4)
|
||||
: '';
|
||||
return html`
|
||||
<div class="adapters-container">
|
||||
<sl-tab-group
|
||||
activation="auto"
|
||||
@sl-tab-show=${(e: CustomEvent) => this._handleTabShow(e.detail.name)}
|
||||
>
|
||||
<sl-tab slot="nav" panel="markdown">Markdown</sl-tab>
|
||||
<sl-tab slot="nav" panel="plaintext">PlainText</sl-tab>
|
||||
<sl-tab slot="nav" panel="html">HTML</sl-tab>
|
||||
<sl-tab slot="nav" panel="snapshot">Snapshot</sl-tab>
|
||||
|
||||
<sl-tab-panel name="markdown">
|
||||
<div class="adapter-container">${this._markdownContent}</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="html">
|
||||
<div class="adapter-container html-panel">
|
||||
${this._renderHtmlPanel()}
|
||||
</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="plaintext">
|
||||
<div class="adapter-container">${this._plainTextContent}</div>
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="snapshot">
|
||||
<div class="adapter-container">${snapshotString}</div>
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
<sl-tooltip content="Update Adapter Content" placement="left" hoist>
|
||||
<div class="update-button" @click="${this._updateActiveTabContent}">
|
||||
Update
|
||||
</div>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
override willUpdate(_changedProperties: PropertyValues) {
|
||||
if (_changedProperties.has('editor')) {
|
||||
requestIdleCallback(() => {
|
||||
this._updateActiveTabContent().catch(console.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@query('sl-tab-panel[active]')
|
||||
private accessor _activeTab!: SlTabPanel;
|
||||
|
||||
@state()
|
||||
private accessor _docSnapshot: DocSnapshot | null = null;
|
||||
|
||||
@state()
|
||||
private accessor _htmlContent = '';
|
||||
|
||||
@state()
|
||||
private accessor _isHtmlPreview = false;
|
||||
|
||||
@state()
|
||||
private accessor _markdownContent = '';
|
||||
|
||||
@state()
|
||||
private accessor _plainTextContent = '';
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: TestAffineEditorContainer;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'adapters-panel': AdaptersPanel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import type { TransformerMiddleware } from '@blocksuite/affine/store';
|
||||
import type { TestAffineEditorContainer } from '@blocksuite/integration-test';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('custom-adapter-panel')
|
||||
export class CustomAdapterPanel extends SignalWatcher(
|
||||
WithDisposable(ShadowlessElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
.custom-adapter-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 16px;
|
||||
border: 1px solid var(--affine-border-color, #e3e2e4);
|
||||
background: var(--affine-background-overlay-panel-color);
|
||||
height: 100vh;
|
||||
width: 30vw;
|
||||
box-sizing: border-box;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
private _renderPanel() {
|
||||
return html`<affine-adapter-panel
|
||||
.store=${this.editor.doc}
|
||||
.transformerMiddlewares=${this.transformerMiddlewares}
|
||||
></affine-adapter-panel>`;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${this._show
|
||||
? html`
|
||||
<div class="custom-adapter-container">${this._renderPanel()}</div>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
toggleDisplay() {
|
||||
this._show = !this._show;
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _show = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor editor!: TestAffineEditorContainer;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor transformerMiddlewares: TransformerMiddleware[] = [];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'custom-adapter-panel': CustomAdapterPanel;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ import type { Pane } from 'tweakpane';
|
||||
import type { CommentPanel } from '../../comment/index.js';
|
||||
import { createTestEditor } from '../../starter/utils/extensions.js';
|
||||
import { mockEdgelessTheme } from '../mock-services.js';
|
||||
import { AdaptersPanel } from './adapters-panel.js';
|
||||
import type { CustomAdapterPanel } from './custom-adapter-panel.js';
|
||||
import type { CustomFramePanel } from './custom-frame-panel.js';
|
||||
import type { CustomOutlinePanel } from './custom-outline-panel.js';
|
||||
import type { CustomOutlineViewer } from './custom-outline-viewer.js';
|
||||
@@ -612,26 +612,6 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
this._hasOffset = !this._hasOffset;
|
||||
}
|
||||
|
||||
private _toggleAdaptersPanel() {
|
||||
const app = document.querySelector('#app');
|
||||
if (!app) return;
|
||||
|
||||
const currentAdaptersPanel = app.querySelector('adapters-panel');
|
||||
if (currentAdaptersPanel) {
|
||||
currentAdaptersPanel.remove();
|
||||
(app as HTMLElement).style.display = 'block';
|
||||
this.editor.style.width = '100%';
|
||||
this.editor.style.flex = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const adaptersPanel = new AdaptersPanel();
|
||||
adaptersPanel.editor = this.editor;
|
||||
app.append(adaptersPanel);
|
||||
this.editor.style.flex = '1';
|
||||
(app as HTMLElement).style.display = 'flex';
|
||||
}
|
||||
|
||||
private _toggleCommentPanel() {
|
||||
document.body.append(this.commentPanel);
|
||||
}
|
||||
@@ -649,6 +629,10 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
this.framePanel.toggleDisplay();
|
||||
}
|
||||
|
||||
private _toggleAdapterPanel() {
|
||||
this.adapterPanel.toggleDisplay();
|
||||
}
|
||||
|
||||
private _toggleMultipleEditors() {
|
||||
const app = document.querySelector('#app');
|
||||
if (app) {
|
||||
@@ -926,8 +910,8 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
<sl-menu-item @click="${this._toggleMultipleEditors}">
|
||||
Toggle Multiple Editors
|
||||
</sl-menu-item>
|
||||
<sl-menu-item @click="${this._toggleAdaptersPanel}">
|
||||
Toggle Adapters Panel
|
||||
<sl-menu-item @click="${this._toggleAdapterPanel}">
|
||||
Toggle Adapter Panel
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
@@ -1032,6 +1016,9 @@ export class StarterDebugMenu extends ShadowlessElement {
|
||||
@property({ attribute: false })
|
||||
accessor framePanel!: CustomFramePanel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor adapterPanel!: CustomAdapterPanel;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor leftSidePanel!: LeftSidePanel;
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { Store, Workspace } from '@blocksuite/affine/store';
|
||||
import {
|
||||
defaultImageProxyMiddleware,
|
||||
docLinkBaseURLMiddlewareBuilder,
|
||||
embedSyncedDocMiddleware,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
import { AttachmentViewerPanel } from '../../_common/components/attachment-viewer-panel';
|
||||
import { CustomAdapterPanel } from '../../_common/components/custom-adapter-panel';
|
||||
import { CustomFramePanel } from '../../_common/components/custom-frame-panel';
|
||||
import { CustomOutlinePanel } from '../../_common/components/custom-outline-panel';
|
||||
import { CustomOutlineViewer } from '../../_common/components/custom-outline-viewer';
|
||||
@@ -24,6 +31,7 @@ export async function createTestApp(doc: Store, collection: Workspace) {
|
||||
const docsPanel = new DocsPanel();
|
||||
const framePanel = new CustomFramePanel();
|
||||
const outlinePanel = new CustomOutlinePanel();
|
||||
const adapterPanel = new CustomAdapterPanel();
|
||||
const outlineViewer = new CustomOutlineViewer();
|
||||
const leftSidePanel = new LeftSidePanel();
|
||||
const commentPanel = new CommentPanel();
|
||||
@@ -36,6 +44,16 @@ export async function createTestApp(doc: Store, collection: Workspace) {
|
||||
outlineViewer.toggleOutlinePanel = () => {
|
||||
outlinePanel.toggleDisplay();
|
||||
};
|
||||
adapterPanel.editor = editor;
|
||||
adapterPanel.transformerMiddlewares = [
|
||||
docLinkBaseURLMiddlewareBuilder(
|
||||
'https://example.com',
|
||||
editor.doc.workspace.id
|
||||
).get(),
|
||||
titleMiddleware(editor.doc.workspace.meta.docMetas),
|
||||
embedSyncedDocMiddleware('content'),
|
||||
defaultImageProxyMiddleware,
|
||||
];
|
||||
|
||||
debugMenu.collection = collection;
|
||||
debugMenu.editor = editor;
|
||||
@@ -44,6 +62,7 @@ export async function createTestApp(doc: Store, collection: Workspace) {
|
||||
debugMenu.framePanel = framePanel;
|
||||
debugMenu.leftSidePanel = leftSidePanel;
|
||||
debugMenu.docsPanel = docsPanel;
|
||||
debugMenu.adapterPanel = adapterPanel;
|
||||
|
||||
debugMenu.commentPanel = commentPanel;
|
||||
|
||||
@@ -55,6 +74,7 @@ export async function createTestApp(doc: Store, collection: Workspace) {
|
||||
document.body.append(framePanel);
|
||||
document.body.append(leftSidePanel);
|
||||
document.body.append(debugMenu);
|
||||
document.body.append(adapterPanel);
|
||||
|
||||
window.editor = editor;
|
||||
window.doc = doc;
|
||||
|
||||
Reference in New Issue
Block a user