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:
parent
2a80fbb993
commit
a828c74f87
@ -33,6 +33,7 @@
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-foundation": "workspace:*",
|
||||
"@blocksuite/affine-fragment-adapter-panel": "workspace:*",
|
||||
"@blocksuite/affine-fragment-doc-title": "workspace:*",
|
||||
"@blocksuite/affine-fragment-frame-panel": "workspace:*",
|
||||
"@blocksuite/affine-fragment-outline": "workspace:*",
|
||||
@ -209,6 +210,8 @@
|
||||
"./fragments/frame-panel/view": "./src/fragments/frame-panel/view.ts",
|
||||
"./fragments/outline": "./src/fragments/outline/index.ts",
|
||||
"./fragments/outline/view": "./src/fragments/outline/view.ts",
|
||||
"./fragments/adapter-panel": "./src/fragments/adapter-panel/index.ts",
|
||||
"./fragments/adapter-panel/view": "./src/fragments/adapter-panel/view.ts",
|
||||
"./gfx/text": "./src/gfx/text/index.ts",
|
||||
"./gfx/text/store": "./src/gfx/text/store.ts",
|
||||
"./gfx/text/view": "./src/gfx/text/view.ts",
|
||||
|
@ -19,6 +19,7 @@ import { SurfaceViewExtension } from '@blocksuite/affine-block-surface/view';
|
||||
import { SurfaceRefViewExtension } from '@blocksuite/affine-block-surface-ref/view';
|
||||
import { TableViewExtension } from '@blocksuite/affine-block-table/view';
|
||||
import { FoundationViewExtension } from '@blocksuite/affine-foundation/view';
|
||||
import { AdapterPanelViewExtension } from '@blocksuite/affine-fragment-adapter-panel/view';
|
||||
import { DocTitleViewExtension } from '@blocksuite/affine-fragment-doc-title/view';
|
||||
import { FramePanelViewExtension } from '@blocksuite/affine-fragment-frame-panel/view';
|
||||
import { OutlineViewExtension } from '@blocksuite/affine-fragment-outline/view';
|
||||
@ -124,5 +125,6 @@ export function getInternalViewExtensions() {
|
||||
DocTitleViewExtension,
|
||||
FramePanelViewExtension,
|
||||
OutlineViewExtension,
|
||||
AdapterPanelViewExtension,
|
||||
];
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-fragment-adapter-panel';
|
@ -0,0 +1 @@
|
||||
export * from '@blocksuite/affine-fragment-adapter-panel/view';
|
@ -30,6 +30,7 @@
|
||||
{ "path": "../components" },
|
||||
{ "path": "../ext-loader" },
|
||||
{ "path": "../foundation" },
|
||||
{ "path": "../fragments/adapter-panel" },
|
||||
{ "path": "../fragments/doc-title" },
|
||||
{ "path": "../fragments/frame-panel" },
|
||||
{ "path": "../fragments/outline" },
|
||||
|
39
blocksuite/affine/fragments/adapter-panel/package.json
Normal file
39
blocksuite/affine/fragments/adapter-panel/package.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@blocksuite/affine-fragment-adapter-panel",
|
||||
"description": "Adapter panel fragment for BlockSuite.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"keywords": [],
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/global": "workspace:*",
|
||||
"@blocksuite/icons": "^2.2.12",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"lit": "^3.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./view": "./src/view.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"!src/__tests__",
|
||||
"!dist/__tests__"
|
||||
],
|
||||
"version": "0.21.0"
|
||||
}
|
177
blocksuite/affine/fragments/adapter-panel/src/adapter-panel.ts
Normal file
177
blocksuite/affine/fragments/adapter-panel/src/adapter-panel.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import type { Store, TransformerMiddleware } from '@blocksuite/affine/store';
|
||||
import {
|
||||
type HtmlAdapter,
|
||||
HtmlAdapterFactoryIdentifier,
|
||||
type MarkdownAdapter,
|
||||
MarkdownAdapterFactoryIdentifier,
|
||||
type PlainTextAdapter,
|
||||
PlainTextAdapterFactoryIdentifier,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { provide } from '@lit/context';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { css, html, LitElement, type PropertyValues, unsafeCSS } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
import {
|
||||
type AdapterPanelContext,
|
||||
adapterPanelContext,
|
||||
ADAPTERS,
|
||||
} from './config';
|
||||
|
||||
export const AFFINE_ADAPTER_PANEL = 'affine-adapter-panel';
|
||||
|
||||
export class AdapterPanel extends SignalWatcher(WithDisposable(LitElement)) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.adapters-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-sizing: border-box;
|
||||
font-family: ${unsafeCSS(baseTheme.fontSansFamily)};
|
||||
}
|
||||
`;
|
||||
|
||||
get activeAdapter() {
|
||||
return this._context.activeAdapter$.value;
|
||||
}
|
||||
|
||||
private _createJob() {
|
||||
return this.store.getTransformer(this.transformerMiddlewares);
|
||||
}
|
||||
|
||||
private _getDocSnapshot() {
|
||||
const job = this._createJob();
|
||||
const result = job.docToSnapshot(this.store);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async _getHtmlContent() {
|
||||
try {
|
||||
const job = this._createJob();
|
||||
const htmlAdapterFactory = this.store.get(HtmlAdapterFactoryIdentifier);
|
||||
const htmlAdapter = htmlAdapterFactory.get(job) as HtmlAdapter;
|
||||
const result = await htmlAdapter.fromDoc(this.store);
|
||||
return result?.file;
|
||||
} catch (error) {
|
||||
console.error('Failed to get html content', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async _getMarkdownContent() {
|
||||
try {
|
||||
const job = this._createJob();
|
||||
const markdownAdapterFactory = this.store.get(
|
||||
MarkdownAdapterFactoryIdentifier
|
||||
);
|
||||
const markdownAdapter = markdownAdapterFactory.get(
|
||||
job
|
||||
) as MarkdownAdapter;
|
||||
const result = await markdownAdapter.fromDoc(this.store);
|
||||
return result?.file;
|
||||
} catch (error) {
|
||||
console.error('Failed to get markdown content', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private async _getPlainTextContent() {
|
||||
try {
|
||||
const job = this._createJob();
|
||||
const plainTextAdapterFactory = this.store.get(
|
||||
PlainTextAdapterFactoryIdentifier
|
||||
);
|
||||
const plainTextAdapter = plainTextAdapterFactory.get(
|
||||
job
|
||||
) as PlainTextAdapter;
|
||||
const result = await plainTextAdapter.fromDoc(this.store);
|
||||
return result?.file;
|
||||
} catch (error) {
|
||||
console.error('Failed to get plain text content', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private readonly _updateActiveContent = async () => {
|
||||
const activeId = this.activeAdapter.id;
|
||||
switch (activeId) {
|
||||
case 'markdown':
|
||||
this._context.markdownContent$.value =
|
||||
(await this._getMarkdownContent()) || '';
|
||||
break;
|
||||
case 'html':
|
||||
this._context.htmlContent$.value = (await this._getHtmlContent()) || '';
|
||||
break;
|
||||
case 'plaintext':
|
||||
this._context.plainTextContent$.value =
|
||||
(await this._getPlainTextContent()) || '';
|
||||
break;
|
||||
case 'snapshot':
|
||||
this._context.docSnapshot$.value = this._getDocSnapshot() || null;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._context = {
|
||||
activeAdapter$: signal(ADAPTERS[0]),
|
||||
isHtmlPreview$: signal(false),
|
||||
docSnapshot$: signal(null),
|
||||
htmlContent$: signal(''),
|
||||
markdownContent$: signal(''),
|
||||
plainTextContent$: signal(''),
|
||||
};
|
||||
}
|
||||
|
||||
override willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
if (changedProperties.has('store')) {
|
||||
this._updateActiveContent().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
this.disposables.add(
|
||||
effect(() => {
|
||||
if (this.activeAdapter) {
|
||||
this._updateActiveContent().catch(console.error);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="adapters-container">
|
||||
<affine-adapter-panel-header
|
||||
.updateActiveContent=${this._updateActiveContent}
|
||||
></affine-adapter-panel-header>
|
||||
<affine-adapter-panel-body></affine-adapter-panel-body>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor store!: Store;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor transformerMiddlewares: TransformerMiddleware[] = [];
|
||||
|
||||
@provide({ context: adapterPanelContext })
|
||||
private accessor _context!: AdapterPanelContext;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_ADAPTER_PANEL]: AdapterPanel;
|
||||
}
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
import { scrollbarStyle } from '@blocksuite/affine-shared/styles';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import {
|
||||
type AdapterItem,
|
||||
type AdapterPanelContext,
|
||||
adapterPanelContext,
|
||||
ADAPTERS,
|
||||
} from '../config';
|
||||
|
||||
export const AFFINE_ADAPTER_PANEL_BODY = 'affine-adapter-panel-body';
|
||||
|
||||
export class AdapterPanelBody extends SignalWatcher(LitElement) {
|
||||
static override styles = css`
|
||||
.adapter-panel-body {
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
${scrollbarStyle('.adapter-panel-body')}
|
||||
|
||||
.adapter-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: pre-wrap;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-size: var(--affine-font-sm);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.html-content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.html-preview-container,
|
||||
.html-panel-content {
|
||||
width: 100%;
|
||||
flex: 1 0 0;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: var(--affine-text-primary-color);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
${scrollbarStyle('.html-panel-content')}
|
||||
|
||||
.html-panel-footer {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.html-toggle-container {
|
||||
display: flex;
|
||||
background: ${unsafeCSSVarV2('segment/background')};
|
||||
justify-content: flex-start;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.html-toggle-item {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 0px 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
border-radius: 4px;
|
||||
color: ${unsafeCSSVarV2('text/primary')};
|
||||
}
|
||||
|
||||
.html-toggle-item:hover {
|
||||
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
}
|
||||
|
||||
.html-toggle-item[active] {
|
||||
background: ${unsafeCSSVarV2('segment/button')};
|
||||
box-shadow:
|
||||
var(--Shadow-buttonShadow-1-x, 0px) var(--Shadow-buttonShadow-1-y, 0px)
|
||||
var(--Shadow-buttonShadow-1-blur, 1px) 0px
|
||||
var(--Shadow-buttonShadow-1-color, rgba(0, 0, 0, 0.12)),
|
||||
var(--Shadow-buttonShadow-2-x, 0px) var(--Shadow-buttonShadow-2-y, 1px)
|
||||
var(--Shadow-buttonShadow-2-blur, 5px) 0px
|
||||
var(--Shadow-buttonShadow-2-color, rgba(0, 0, 0, 0.12));
|
||||
}
|
||||
|
||||
.adapter-container {
|
||||
display: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.adapter-container.active {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
get activeAdapter() {
|
||||
return this._context.activeAdapter$.value;
|
||||
}
|
||||
|
||||
get isHtmlPreview() {
|
||||
return this._context.isHtmlPreview$.value;
|
||||
}
|
||||
|
||||
get htmlContent() {
|
||||
return this._context.htmlContent$.value;
|
||||
}
|
||||
|
||||
get markdownContent() {
|
||||
return this._context.markdownContent$.value;
|
||||
}
|
||||
|
||||
get plainTextContent() {
|
||||
return this._context.plainTextContent$.value;
|
||||
}
|
||||
|
||||
get docSnapshot() {
|
||||
return this._context.docSnapshot$.value;
|
||||
}
|
||||
|
||||
private _renderHtmlPanel() {
|
||||
return html`
|
||||
${this.isHtmlPreview
|
||||
? html`<iframe
|
||||
class="html-preview-container"
|
||||
.srcdoc=${this.htmlContent}
|
||||
sandbox="allow-same-origin"
|
||||
></iframe>`
|
||||
: html`<div class="html-panel-content">${this.htmlContent}</div>`}
|
||||
<div class="html-panel-footer">
|
||||
<div class="html-toggle-container">
|
||||
<span
|
||||
class="html-toggle-item"
|
||||
?active=${!this.isHtmlPreview}
|
||||
@click=${() => (this._context.isHtmlPreview$.value = false)}
|
||||
>Source</span
|
||||
>
|
||||
<span
|
||||
class="html-toggle-item"
|
||||
?active=${this.isHtmlPreview}
|
||||
@click=${() => (this._context.isHtmlPreview$.value = true)}
|
||||
>Preview</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private readonly _renderAdapterContent = (adapter: AdapterItem) => {
|
||||
switch (adapter.id) {
|
||||
case 'html':
|
||||
return this._renderHtmlPanel();
|
||||
case 'markdown':
|
||||
return this.markdownContent;
|
||||
case 'plaintext':
|
||||
return this.plainTextContent;
|
||||
case 'snapshot':
|
||||
return this.docSnapshot
|
||||
? JSON.stringify(this.docSnapshot, null, 4)
|
||||
: '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _renderAdapterContainer = (adapter: AdapterItem) => {
|
||||
const containerClasses = classMap({
|
||||
'adapter-container': true,
|
||||
active: this.activeAdapter.id === adapter.id,
|
||||
});
|
||||
|
||||
const contentClasses = classMap({
|
||||
'adapter-content': true,
|
||||
[`${adapter.id}-content`]: true,
|
||||
});
|
||||
|
||||
const content = this._renderAdapterContent(adapter);
|
||||
|
||||
return html`
|
||||
<div class=${containerClasses}>
|
||||
<div class=${contentClasses}>${content}</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="adapter-panel-body">
|
||||
${ADAPTERS.map(adapter => this._renderAdapterContainer(adapter))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@consume({ context: adapterPanelContext })
|
||||
private accessor _context!: AdapterPanelContext;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_ADAPTER_PANEL_BODY]: AdapterPanelBody;
|
||||
}
|
||||
}
|
28
blocksuite/affine/fragments/adapter-panel/src/config.ts
Normal file
28
blocksuite/affine/fragments/adapter-panel/src/config.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { DocSnapshot } from '@blocksuite/store';
|
||||
import { createContext } from '@lit/context';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
|
||||
export type AdapterItem = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const ADAPTERS: AdapterItem[] = [
|
||||
{ id: 'markdown', label: 'Markdown' },
|
||||
{ id: 'plaintext', label: 'PlainText' },
|
||||
{ id: 'html', label: 'HTML' },
|
||||
{ id: 'snapshot', label: 'Snapshot' },
|
||||
];
|
||||
|
||||
export type AdapterPanelContext = {
|
||||
activeAdapter$: Signal<AdapterItem>;
|
||||
isHtmlPreview$: Signal<boolean>;
|
||||
docSnapshot$: Signal<DocSnapshot | null>;
|
||||
htmlContent$: Signal<string>;
|
||||
markdownContent$: Signal<string>;
|
||||
plainTextContent$: Signal<string>;
|
||||
};
|
||||
|
||||
export const adapterPanelContext = createContext<AdapterPanelContext>(
|
||||
'adapterPanelContext'
|
||||
);
|
17
blocksuite/affine/fragments/adapter-panel/src/effects.ts
Normal file
17
blocksuite/affine/fragments/adapter-panel/src/effects.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { AdapterPanel, AFFINE_ADAPTER_PANEL } from './adapter-panel';
|
||||
import {
|
||||
AdapterPanelBody,
|
||||
AFFINE_ADAPTER_PANEL_BODY,
|
||||
} from './body/adapter-panel-body';
|
||||
import { AdapterMenu, AFFINE_ADAPTER_MENU } from './header/adapter-menu';
|
||||
import {
|
||||
AdapterPanelHeader,
|
||||
AFFINE_ADAPTER_PANEL_HEADER,
|
||||
} from './header/adapter-panel-header';
|
||||
|
||||
export function effects() {
|
||||
customElements.define(AFFINE_ADAPTER_PANEL, AdapterPanel);
|
||||
customElements.define(AFFINE_ADAPTER_MENU, AdapterMenu);
|
||||
customElements.define(AFFINE_ADAPTER_PANEL_HEADER, AdapterPanelHeader);
|
||||
customElements.define(AFFINE_ADAPTER_PANEL_BODY, AdapterPanelBody);
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import {
|
||||
type AdapterItem,
|
||||
type AdapterPanelContext,
|
||||
adapterPanelContext,
|
||||
ADAPTERS,
|
||||
} from '../config';
|
||||
|
||||
export const AFFINE_ADAPTER_MENU = 'affine-adapter-menu';
|
||||
|
||||
export class AdapterMenu extends SignalWatcher(LitElement) {
|
||||
static override styles = css`
|
||||
.adapter-menu {
|
||||
min-width: 120px;
|
||||
padding: 4px;
|
||||
background: var(--affine-background-primary-color);
|
||||
border: 1px solid var(--affine-border-color);
|
||||
border-radius: 4px;
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
}
|
||||
.adapter-menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-family: var(--affine-font-family);
|
||||
font-size: var(--affine-font-xs);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.adapter-menu-item:hover {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
.adapter-menu-item.active {
|
||||
color: var(--affine-primary-color);
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
get activeAdapter() {
|
||||
return this._context.activeAdapter$.value;
|
||||
}
|
||||
|
||||
private readonly _handleAdapterChange = async (adapter: AdapterItem) => {
|
||||
this._context.activeAdapter$.value = adapter;
|
||||
this.abortController?.abort();
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`<div class="adapter-menu">
|
||||
${ADAPTERS.map(adapter => {
|
||||
const classes = classMap({
|
||||
'adapter-menu-item': true,
|
||||
active: this.activeAdapter.id === adapter.id,
|
||||
});
|
||||
return html`
|
||||
<button
|
||||
class=${classes}
|
||||
@click=${() => this._handleAdapterChange(adapter)}
|
||||
>
|
||||
${adapter.label}
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor abortController: AbortController | null = null;
|
||||
|
||||
@consume({ context: adapterPanelContext })
|
||||
private accessor _context!: AdapterPanelContext;
|
||||
}
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_ADAPTER_MENU]: AdapterMenu;
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
import { createLitPortal } from '@blocksuite/affine-components/portal';
|
||||
import { SignalWatcher } from '@blocksuite/global/lit';
|
||||
import { ArrowDownSmallIcon, FlipDirectionIcon } from '@blocksuite/icons/lit';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { consume } from '@lit/context';
|
||||
import { css, html, LitElement } from 'lit';
|
||||
import { property, query } from 'lit/decorators.js';
|
||||
|
||||
import { type AdapterPanelContext, adapterPanelContext } from '../config';
|
||||
|
||||
export const AFFINE_ADAPTER_PANEL_HEADER = 'affine-adapter-panel-header';
|
||||
|
||||
export class AdapterPanelHeader extends SignalWatcher(LitElement) {
|
||||
static override styles = css`
|
||||
.adapter-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: var(--affine-background-primary-color);
|
||||
}
|
||||
.adapter-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--affine-border-color);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.adapter-selector:hover {
|
||||
background: var(--affine-hover-color);
|
||||
}
|
||||
.adapter-selector-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--affine-text-primary-color);
|
||||
font-size: var(--affine-font-xs);
|
||||
}
|
||||
.update-button {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--affine-icon-color);
|
||||
}
|
||||
.update-button:hover {
|
||||
background-color: var(--affine-hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
get activeAdapter() {
|
||||
return this._context.activeAdapter$.value;
|
||||
}
|
||||
|
||||
private _adapterMenuAbortController: AbortController | null = null;
|
||||
private readonly _toggleAdapterMenu = () => {
|
||||
if (this._adapterMenuAbortController) {
|
||||
this._adapterMenuAbortController.abort();
|
||||
}
|
||||
this._adapterMenuAbortController = new AbortController();
|
||||
|
||||
createLitPortal({
|
||||
template: html`<affine-adapter-menu
|
||||
.abortController=${this._adapterMenuAbortController}
|
||||
></affine-adapter-menu>`,
|
||||
portalStyles: {
|
||||
zIndex: 'var(--affine-z-index-popover)',
|
||||
},
|
||||
container: this._adapterPanelHeader,
|
||||
computePosition: {
|
||||
referenceElement: this._adapterSelector,
|
||||
placement: 'bottom-start',
|
||||
middleware: [flip(), offset(4)],
|
||||
autoUpdate: { animationFrame: true },
|
||||
},
|
||||
abortController: this._adapterMenuAbortController,
|
||||
closeOnClickAway: true,
|
||||
});
|
||||
};
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="adapter-panel-header">
|
||||
<div class="adapter-selector" @click="${this._toggleAdapterMenu}">
|
||||
<span class="adapter-selector-label">
|
||||
${this.activeAdapter.label}
|
||||
</span>
|
||||
${ArrowDownSmallIcon({ width: '16px', height: '16px' })}
|
||||
</div>
|
||||
<div class="update-button" @click="${this.updateActiveContent}">
|
||||
${FlipDirectionIcon({ width: '16px', height: '16px' })}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@query('.adapter-panel-header')
|
||||
private accessor _adapterPanelHeader!: HTMLDivElement;
|
||||
|
||||
@query('.adapter-selector')
|
||||
private accessor _adapterSelector!: HTMLDivElement;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor updateActiveContent: () => void = () => {};
|
||||
|
||||
@consume({ context: adapterPanelContext })
|
||||
private accessor _context!: AdapterPanelContext;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
[AFFINE_ADAPTER_PANEL_HEADER]: AdapterPanelHeader;
|
||||
}
|
||||
}
|
4
blocksuite/affine/fragments/adapter-panel/src/index.ts
Normal file
4
blocksuite/affine/fragments/adapter-panel/src/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './adapter-panel.js';
|
||||
export * from './body/adapter-panel-body.js';
|
||||
export * from './header/adapter-menu.js';
|
||||
export * from './header/adapter-panel-header.js';
|
12
blocksuite/affine/fragments/adapter-panel/src/view.ts
Normal file
12
blocksuite/affine/fragments/adapter-panel/src/view.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ViewExtensionProvider } from '@blocksuite/affine-ext-loader';
|
||||
|
||||
import { effects } from './effects';
|
||||
|
||||
export class AdapterPanelViewExtension extends ViewExtensionProvider {
|
||||
override name = 'affine-adapter-panel-fragment';
|
||||
|
||||
override effect() {
|
||||
super.effect();
|
||||
effects();
|
||||
}
|
||||
}
|
18
blocksuite/affine/fragments/adapter-panel/tsconfig.json
Normal file
18
blocksuite/affine/fragments/adapter-panel/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
|
||||
},
|
||||
"include": ["./src"],
|
||||
"references": [
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../ext-loader" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../shared" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" },
|
||||
{ "path": "../../../framework/store" }
|
||||
]
|
||||
}
|
@ -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;
|
||||
|
@ -18,6 +18,7 @@ import { TrashPageFooter } from '@affine/core/components/pure/trash-page-footer'
|
||||
import { TopTip } from '@affine/core/components/top-tip';
|
||||
import { DocService } from '@affine/core/modules/doc';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import { RecentDocsService } from '@affine/core/modules/quicksearch';
|
||||
@ -36,6 +37,7 @@ import { DisposableGroup } from '@blocksuite/affine/global/disposable';
|
||||
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
|
||||
import {
|
||||
AiIcon,
|
||||
ExportIcon,
|
||||
FrameIcon,
|
||||
PropertyIcon,
|
||||
TocIcon,
|
||||
@ -57,6 +59,7 @@ import { PageNotFound } from '../../404';
|
||||
import * as styles from './detail-page.css';
|
||||
import { DetailPageHeader } from './detail-page-header';
|
||||
import { DetailPageWrapper } from './detail-page-wrapper';
|
||||
import { EditorAdapterPanel } from './tabs/adapter';
|
||||
import { EditorChatPanel } from './tabs/chat';
|
||||
import { EditorFramePanel } from './tabs/frame';
|
||||
import { EditorJournalPanel } from './tabs/journal';
|
||||
@ -103,6 +106,11 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
|
||||
const enableAI = useEnableAI();
|
||||
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableAdapterPanel = useLiveData(
|
||||
featureFlagService.flags.enable_adapter_panel.$
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveView) {
|
||||
setActiveBlockSuiteEditor(editorContainer);
|
||||
@ -360,6 +368,16 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
</Scrollable.Root>
|
||||
</ViewSidebarTab>
|
||||
|
||||
{enableAdapterPanel && (
|
||||
<ViewSidebarTab tabId="adapter" icon={<ExportIcon />}>
|
||||
<Scrollable.Root className={styles.sidebarScrollArea}>
|
||||
<Scrollable.Viewport>
|
||||
<EditorAdapterPanel host={editorContainer?.host ?? null} />
|
||||
</Scrollable.Viewport>
|
||||
</Scrollable.Root>
|
||||
</ViewSidebarTab>
|
||||
)}
|
||||
|
||||
<GlobalPageHistoryModal />
|
||||
{/* FIXME: wait for better ai, <PageAIOnboarding /> */}
|
||||
</FrameworkScope>
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const root = style({
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
});
|
@ -0,0 +1,74 @@
|
||||
import { ServerService } from '@affine/core/modules/cloud';
|
||||
import { AdapterPanel } from '@blocksuite/affine/fragments/adapter-panel';
|
||||
import {
|
||||
customImageProxyMiddleware,
|
||||
docLinkBaseURLMiddlewareBuilder,
|
||||
embedSyncedDocMiddleware,
|
||||
titleMiddleware,
|
||||
} from '@blocksuite/affine/shared/adapters';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import type { TransformerMiddleware } from '@blocksuite/affine/store';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import * as styles from './adapter.css';
|
||||
|
||||
const createImageProxyUrl = (baseUrl: string) => {
|
||||
try {
|
||||
return new URL(BUILD_CONFIG.imageProxyUrl, baseUrl).toString();
|
||||
} catch (error) {
|
||||
console.error('Failed to create image proxy url', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const createMiddlewares = (
|
||||
host: EditorHost,
|
||||
baseUrl: string
|
||||
): TransformerMiddleware[] => {
|
||||
const imageProxyUrl = createImageProxyUrl(baseUrl);
|
||||
|
||||
return [
|
||||
docLinkBaseURLMiddlewareBuilder(baseUrl, host.store.workspace.id).get(),
|
||||
titleMiddleware(host.store.workspace.meta.docMetas),
|
||||
embedSyncedDocMiddleware('content'),
|
||||
customImageProxyMiddleware(imageProxyUrl),
|
||||
];
|
||||
};
|
||||
|
||||
const getTransformerMiddlewares = (
|
||||
host: EditorHost | null,
|
||||
baseUrl: string
|
||||
) => {
|
||||
if (!host) return [];
|
||||
return createMiddlewares(host, baseUrl);
|
||||
};
|
||||
|
||||
// A wrapper for AdapterPanel
|
||||
export const EditorAdapterPanel = ({ host }: { host: EditorHost | null }) => {
|
||||
const server = useService(ServerService).server;
|
||||
const adapterPanelRef = useRef<AdapterPanel | null>(null);
|
||||
|
||||
const onRefChange = useCallback(
|
||||
(container: HTMLDivElement | null) => {
|
||||
if (container && host && container.children.length === 0) {
|
||||
adapterPanelRef.current = new AdapterPanel();
|
||||
adapterPanelRef.current.store = host.store;
|
||||
adapterPanelRef.current.transformerMiddlewares =
|
||||
getTransformerMiddlewares(host, server.baseUrl);
|
||||
container.append(adapterPanelRef.current);
|
||||
}
|
||||
},
|
||||
[host, server]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (host && adapterPanelRef.current) {
|
||||
adapterPanelRef.current.store = host.store;
|
||||
adapterPanelRef.current.transformerMiddlewares =
|
||||
getTransformerMiddlewares(host, server.baseUrl);
|
||||
}
|
||||
}, [host, server]);
|
||||
|
||||
return <div className={styles.root} ref={onRefChange} />;
|
||||
};
|
@ -327,6 +327,15 @@ export const AFFINE_FLAGS = {
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: true,
|
||||
},
|
||||
enable_adapter_panel: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-adapter-panel.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-adapter-panel.description',
|
||||
configurable: isCanaryBuild,
|
||||
defaultState: false,
|
||||
},
|
||||
} satisfies { [key in string]: FlagInfo };
|
||||
|
||||
// oxlint-disable-next-line no-redeclare
|
||||
|
@ -5890,6 +5890,14 @@ export function useAFFiNEI18N(): {
|
||||
* `Once enabled, you can preview HTML in code block.`
|
||||
*/
|
||||
["com.affine.settings.workspace.experimental-features.enable-code-block-html-preview.description"](): string;
|
||||
/**
|
||||
* `Adapter Panel`
|
||||
*/
|
||||
["com.affine.settings.workspace.experimental-features.enable-adapter-panel.name"](): string;
|
||||
/**
|
||||
* `Once enabled, you can preview adapter export content in the right side bar.`
|
||||
*/
|
||||
["com.affine.settings.workspace.experimental-features.enable-adapter-panel.description"](): string;
|
||||
/**
|
||||
* `Only an owner can edit the workspace avatar and name. Changes will be shown for everyone.`
|
||||
*/
|
||||
|
@ -1471,6 +1471,8 @@
|
||||
"com.affine.settings.workspace.experimental-features.enable-table-virtual-scroll.description": "Once enabled, switch table view to virtual scroll mode in Database Block.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-code-block-html-preview.name": "Code block HTML preview",
|
||||
"com.affine.settings.workspace.experimental-features.enable-code-block-html-preview.description": "Once enabled, you can preview HTML in code block.",
|
||||
"com.affine.settings.workspace.experimental-features.enable-adapter-panel.name": "Adapter Panel",
|
||||
"com.affine.settings.workspace.experimental-features.enable-adapter-panel.description": "Once enabled, you can preview adapter export content in the right side bar.",
|
||||
"com.affine.settings.workspace.not-owner": "Only an owner can edit the workspace avatar and name. Changes will be shown for everyone.",
|
||||
"com.affine.settings.workspace.preferences": "Preference",
|
||||
"com.affine.settings.workspace.billing": "Team's Billing",
|
||||
|
@ -28,6 +28,7 @@ export const PackageList = [
|
||||
'blocksuite/affine/components',
|
||||
'blocksuite/affine/ext-loader',
|
||||
'blocksuite/affine/foundation',
|
||||
'blocksuite/affine/fragments/adapter-panel',
|
||||
'blocksuite/affine/fragments/doc-title',
|
||||
'blocksuite/affine/fragments/frame-panel',
|
||||
'blocksuite/affine/fragments/outline',
|
||||
@ -475,6 +476,19 @@ export const PackageList = [
|
||||
'blocksuite/framework/store',
|
||||
],
|
||||
},
|
||||
{
|
||||
location: 'blocksuite/affine/fragments/adapter-panel',
|
||||
name: '@blocksuite/affine-fragment-adapter-panel',
|
||||
workspaceDependencies: [
|
||||
'blocksuite/affine/components',
|
||||
'blocksuite/affine/ext-loader',
|
||||
'blocksuite/affine/model',
|
||||
'blocksuite/affine/shared',
|
||||
'blocksuite/framework/global',
|
||||
'blocksuite/framework/std',
|
||||
'blocksuite/framework/store',
|
||||
],
|
||||
},
|
||||
{
|
||||
location: 'blocksuite/affine/fragments/doc-title',
|
||||
name: '@blocksuite/affine-fragment-doc-title',
|
||||
@ -1475,6 +1489,7 @@ export type PackageName =
|
||||
| '@blocksuite/data-view'
|
||||
| '@blocksuite/affine-ext-loader'
|
||||
| '@blocksuite/affine-foundation'
|
||||
| '@blocksuite/affine-fragment-adapter-panel'
|
||||
| '@blocksuite/affine-fragment-doc-title'
|
||||
| '@blocksuite/affine-fragment-frame-panel'
|
||||
| '@blocksuite/affine-fragment-outline'
|
||||
|
@ -75,6 +75,7 @@
|
||||
{ "path": "./blocksuite/affine/data-view" },
|
||||
{ "path": "./blocksuite/affine/ext-loader" },
|
||||
{ "path": "./blocksuite/affine/foundation" },
|
||||
{ "path": "./blocksuite/affine/fragments/adapter-panel" },
|
||||
{ "path": "./blocksuite/affine/fragments/doc-title" },
|
||||
{ "path": "./blocksuite/affine/fragments/frame-panel" },
|
||||
{ "path": "./blocksuite/affine/fragments/outline" },
|
||||
|
22
yarn.lock
22
yarn.lock
@ -3067,6 +3067,27 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@blocksuite/affine-fragment-adapter-panel@workspace:*, @blocksuite/affine-fragment-adapter-panel@workspace:blocksuite/affine/fragments/adapter-panel":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@blocksuite/affine-fragment-adapter-panel@workspace:blocksuite/affine/fragments/adapter-panel"
|
||||
dependencies:
|
||||
"@blocksuite/affine-components": "workspace:*"
|
||||
"@blocksuite/affine-ext-loader": "workspace:*"
|
||||
"@blocksuite/affine-model": "workspace:*"
|
||||
"@blocksuite/affine-shared": "workspace:*"
|
||||
"@blocksuite/global": "workspace:*"
|
||||
"@blocksuite/icons": "npm:^2.2.12"
|
||||
"@blocksuite/std": "workspace:*"
|
||||
"@blocksuite/store": "workspace:*"
|
||||
"@floating-ui/dom": "npm:^1.6.13"
|
||||
"@lit/context": "npm:^1.1.2"
|
||||
"@preact/signals-core": "npm:^1.8.0"
|
||||
"@toeverything/theme": "npm:^1.1.14"
|
||||
lit: "npm:^3.2.0"
|
||||
rxjs: "npm:^7.8.1"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@blocksuite/affine-fragment-doc-title@workspace:*, @blocksuite/affine-fragment-doc-title@workspace:blocksuite/affine/fragments/doc-title":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@blocksuite/affine-fragment-doc-title@workspace:blocksuite/affine/fragments/doc-title"
|
||||
@ -4151,6 +4172,7 @@ __metadata:
|
||||
"@blocksuite/affine-components": "workspace:*"
|
||||
"@blocksuite/affine-ext-loader": "workspace:*"
|
||||
"@blocksuite/affine-foundation": "workspace:*"
|
||||
"@blocksuite/affine-fragment-adapter-panel": "workspace:*"
|
||||
"@blocksuite/affine-fragment-doc-title": "workspace:*"
|
||||
"@blocksuite/affine-fragment-frame-panel": "workspace:*"
|
||||
"@blocksuite/affine-fragment-outline": "workspace:*"
|
||||
|
Loading…
x
Reference in New Issue
Block a user