feat: provide notification to bs (#7002)

upstream https://github.com/toeverything/blocksuite/pull/7101
fix AFF-1120
This commit is contained in:
pengx17 2024-05-24 10:36:49 +00:00
parent 919e40f28e
commit 88d4351c28
No known key found for this signature in database
GPG Key ID: 23F23D9E8B3971ED
6 changed files with 202 additions and 104 deletions

View File

@ -37,8 +37,17 @@ export const ConfirmModal = ({
}, [onConfirm]); }, [onConfirm]);
return ( return (
<Modal <Modal
contentOptions={{ className: styles.confirmModalContainer }} contentOptions={{
className: styles.confirmModalContainer,
onPointerDownOutside: e => {
e.stopPropagation();
onCancel?.();
},
}}
width={width} width={width}
closeButtonOptions={{
onClick: onCancel,
}}
{...props} {...props}
> >
{children ? ( {children ? (

View File

@ -22,7 +22,6 @@ import {
} from 'react'; } from 'react';
import { BlocksuiteDocEditor, BlocksuiteEdgelessEditor } from './lit-adaper'; import { BlocksuiteDocEditor, BlocksuiteEdgelessEditor } from './lit-adaper';
import type { ReferenceReactRenderer } from './specs/custom/patch-reference-renderer';
import * as styles from './styles.css'; import * as styles from './styles.css';
// copy forwardSlot from blocksuite, but it seems we need to dispose the pipe // copy forwardSlot from blocksuite, but it seems we need to dispose the pipe
@ -46,7 +45,6 @@ interface BlocksuiteEditorContainerProps {
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
defaultSelectedBlockId?: string; defaultSelectedBlockId?: string;
referenceRenderer?: ReferenceReactRenderer;
} }
// mimic the interface of the webcomponent and expose slots & host // mimic the interface of the webcomponent and expose slots & host
@ -100,7 +98,7 @@ export const BlocksuiteEditorContainer = forwardRef<
AffineEditorContainer, AffineEditorContainer,
BlocksuiteEditorContainerProps BlocksuiteEditorContainerProps
>(function AffineEditorContainer( >(function AffineEditorContainer(
{ page, mode, className, style, defaultSelectedBlockId, referenceRenderer }, { page, mode, className, style, defaultSelectedBlockId },
ref ref
) { ) {
const scrolledRef = useRef(false); const scrolledRef = useRef(false);
@ -303,17 +301,9 @@ export const BlocksuiteEditorContainer = forwardRef<
ref={rootRef} ref={rootRef}
> >
{mode === 'page' ? ( {mode === 'page' ? (
<BlocksuiteDocEditor <BlocksuiteDocEditor page={page} ref={docRef} />
page={page}
ref={docRef}
referenceRenderer={referenceRenderer}
/>
) : ( ) : (
<BlocksuiteEdgelessEditor <BlocksuiteEdgelessEditor page={page} ref={edgelessRef} />
page={page}
ref={edgelessRef}
referenceRenderer={referenceRenderer}
/>
)} )}
</div> </div>
); );

View File

@ -10,14 +10,11 @@ import {
Suspense, Suspense,
useCallback, useCallback,
useEffect, useEffect,
useMemo,
useRef, useRef,
} from 'react'; } from 'react';
import { AffinePageReference } from '../../affine/reference-link';
import { BlocksuiteEditorContainer } from './blocksuite-editor-container'; import { BlocksuiteEditorContainer } from './blocksuite-editor-container';
import { NoPageRootError } from './no-page-error'; import { NoPageRootError } from './no-page-error';
import type { ReferenceReactRenderer } from './specs/custom/patch-reference-renderer';
export type ErrorBoundaryProps = { export type ErrorBoundaryProps = {
onReset?: () => void; onReset?: () => void;
@ -88,19 +85,6 @@ const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
}; };
}, []); }, []);
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
return function customReference(reference) {
const pageId = reference.delta.attributes?.reference?.pageId;
if (!pageId) return <span />;
return (
<AffinePageReference
docCollection={page.collection}
pageId={pageId}
/>
);
};
}, [page.collection]);
return ( return (
<BlocksuiteEditorContainer <BlocksuiteEditorContainer
mode={mode} mode={mode}
@ -108,7 +92,6 @@ const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
ref={onRefChange} ref={onRefChange}
className={className} className={className}
style={style} style={style}
referenceRenderer={referenceRenderer}
defaultSelectedBlockId={defaultSelectedBlockId} defaultSelectedBlockId={defaultSelectedBlockId}
/> />
); );

View File

@ -1,9 +1,11 @@
import { import {
createReactComponentFromLit, createReactComponentFromLit,
useConfirmModal,
useLitPortalFactory, useLitPortalFactory,
} from '@affine/component'; } from '@affine/component';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import type { BlockSpec } from '@blocksuite/block-std';
import { import {
BiDirectionalLinkPanel, BiDirectionalLinkPanel,
DocMetaTags, DocMetaTags,
@ -24,11 +26,13 @@ import React, {
} from 'react'; } from 'react';
import { PagePropertiesTable } from '../../affine/page-properties'; import { PagePropertiesTable } from '../../affine/page-properties';
import { AffinePageReference } from '../../affine/reference-link';
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title'; import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
import { import {
patchNotificationService,
patchReferenceRenderer, patchReferenceRenderer,
type ReferenceReactRenderer, type ReferenceReactRenderer,
} from './specs/custom/patch-reference-renderer'; } from './specs/custom/spec-patchers';
import { EdgelessModeSpecs } from './specs/edgeless'; import { EdgelessModeSpecs } from './specs/edgeless';
import { PageModeSpecs } from './specs/page'; import { PageModeSpecs } from './specs/page';
import * as styles from './styles.css'; import * as styles from './styles.css';
@ -58,14 +62,49 @@ const adapted = {
interface BlocksuiteEditorProps { interface BlocksuiteEditorProps {
page: Doc; page: Doc;
referenceRenderer?: ReferenceReactRenderer;
// todo: add option to replace docTitle with custom component (e.g., for journal page)
} }
const usePatchSpecs = (page: Doc, specs: BlockSpec[]) => {
const [reactToLit, portals] = useLitPortalFactory();
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
return function customReference(reference) {
const pageId = reference.delta.attributes?.reference?.pageId;
if (!pageId) return <span />;
return (
<AffinePageReference docCollection={page.collection} pageId={pageId} />
);
};
}, [page.collection]);
const confirmModal = useConfirmModal();
const patchedSpecs = useMemo(() => {
let patched = patchReferenceRenderer(specs, reactToLit, referenceRenderer);
patched = patchNotificationService(
patchReferenceRenderer(patched, reactToLit, referenceRenderer),
confirmModal
);
return patched;
}, [confirmModal, reactToLit, referenceRenderer, specs]);
return [
patchedSpecs,
useMemo(
() => (
<>
{portals.map(p => (
<Fragment key={p.id}>{p.portal}</Fragment>
))}
</>
),
[portals]
),
] as const;
};
export const BlocksuiteDocEditor = forwardRef< export const BlocksuiteDocEditor = forwardRef<
PageEditor, PageEditor,
BlocksuiteEditorProps BlocksuiteEditorProps
>(function BlocksuiteDocEditor({ page, referenceRenderer }, ref) { >(function BlocksuiteDocEditor({ page }, ref) {
const titleRef = useRef<DocTitle>(null); const titleRef = useRef<DocTitle>(null);
const docRef = useRef<PageEditor | null>(null); const docRef = useRef<PageEditor | null>(null);
const [docPage, setDocPage] = const [docPage, setDocPage] =
@ -90,13 +129,6 @@ export const BlocksuiteDocEditor = forwardRef<
[ref] [ref]
); );
const [reactToLit, portals] = useLitPortalFactory();
const specs = useMemo(() => {
if (!referenceRenderer) return PageModeSpecs;
return patchReferenceRenderer(PageModeSpecs, reactToLit, referenceRenderer);
}, [reactToLit, referenceRenderer]);
useEffect(() => { useEffect(() => {
// auto focus the title // auto focus the title
setTimeout(() => { setTimeout(() => {
@ -114,6 +146,8 @@ export const BlocksuiteDocEditor = forwardRef<
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const [specs, portals] = usePatchSpecs(page, PageModeSpecs);
return ( return (
<> <>
<div className={styles.affineDocViewport} style={{ height: '100%' }}> <div className={styles.affineDocViewport} style={{ height: '100%' }}>
@ -142,32 +176,19 @@ export const BlocksuiteDocEditor = forwardRef<
<adapted.BiDirectionalLinkPanel doc={page} pageRoot={docPage} /> <adapted.BiDirectionalLinkPanel doc={page} pageRoot={docPage} />
) : null} ) : null}
</div> </div>
{portals.map(p => ( {portals}
<Fragment key={p.id}>{p.portal}</Fragment>
))}
</> </>
); );
}); });
export const BlocksuiteEdgelessEditor = forwardRef< export const BlocksuiteEdgelessEditor = forwardRef<
EdgelessEditor, EdgelessEditor,
BlocksuiteEditorProps BlocksuiteEditorProps
>(function BlocksuiteEdgelessEditor({ page, referenceRenderer }, ref) { >(function BlocksuiteEdgelessEditor({ page }, ref) {
const [reactToLit, portals] = useLitPortalFactory(); const [specs, portals] = usePatchSpecs(page, EdgelessModeSpecs);
const specs = useMemo(() => {
if (!referenceRenderer) return EdgelessModeSpecs;
return patchReferenceRenderer(
EdgelessModeSpecs,
reactToLit,
referenceRenderer
);
}, [reactToLit, referenceRenderer]);
return ( return (
<> <>
<adapted.EdgelessEditor ref={ref} doc={page} specs={specs} /> <adapted.EdgelessEditor ref={ref} doc={page} specs={specs} />
{portals.map(p => ( {portals}
<Fragment key={p.id}>{p.portal}</Fragment>
))}
</> </>
); );
}); });

View File

@ -1,45 +0,0 @@
import type { ElementOrFactory } from '@affine/component';
import type { BlockSpec } from '@blocksuite/block-std';
import type {
AffineReference,
ParagraphBlockService,
} from '@blocksuite/blocks';
import type { TemplateResult } from 'lit';
export type ReferenceReactRenderer = (
reference: AffineReference
) => React.ReactElement;
/**
* Patch the block specs with custom renderers.
*/
export function patchReferenceRenderer(
specs: BlockSpec[],
reactToLit: (element: ElementOrFactory) => TemplateResult,
reactRenderer: ReferenceReactRenderer
) {
const litRenderer = (reference: AffineReference) => {
const node = reactRenderer(reference);
return reactToLit(node);
};
return specs.map(spec => {
if (
['affine:paragraph', 'affine:list', 'affine:database'].includes(
spec.schema.model.flavour
)
) {
// todo: remove these type assertions
spec.service = class extends (
(spec.service as typeof ParagraphBlockService)
) {
override mounted() {
super.mounted();
this.referenceNodeConfig.setCustomContent(litRenderer);
}
};
}
return spec;
});
}

View File

@ -0,0 +1,140 @@
import {
createReactComponentFromLit,
type ElementOrFactory,
toast,
type ToastOptions,
type useConfirmModal,
} from '@affine/component';
import type { BlockSpec } from '@blocksuite/block-std';
import type {
AffineReference,
ParagraphBlockService,
RootService,
} from '@blocksuite/blocks';
import { LitElement, type TemplateResult } from 'lit';
import React from 'react';
export type ReferenceReactRenderer = (
reference: AffineReference
) => React.ReactElement;
export class LitTemplateWrapper extends LitElement {
static override get properties() {
return {
template: { type: Object },
};
}
template: TemplateResult | null = null;
// do not enable shadow root
override createRenderRoot() {
return this;
}
override render() {
return this.template;
}
}
window.customElements.define('affine-lit-template-wrapper', LitTemplateWrapper);
const TemplateWrapper = createReactComponentFromLit({
elementClass: LitTemplateWrapper,
react: React,
});
/**
* Patch the block specs with custom renderers.
*/
export function patchReferenceRenderer(
specs: BlockSpec[],
reactToLit: (element: ElementOrFactory) => TemplateResult,
reactRenderer: ReferenceReactRenderer
) {
const litRenderer = (reference: AffineReference) => {
const node = reactRenderer(reference);
return reactToLit(node);
};
return specs.map(spec => {
if (
['affine:paragraph', 'affine:list', 'affine:database'].includes(
spec.schema.model.flavour
)
) {
// todo: remove these type assertions
spec.service = class extends (
(spec.service as typeof ParagraphBlockService)
) {
override mounted() {
super.mounted();
this.referenceNodeConfig.setCustomContent(litRenderer);
}
};
}
return spec;
});
}
export function patchNotificationService(
specs: BlockSpec[],
{ closeConfirmModal, openConfirmModal }: ReturnType<typeof useConfirmModal>
) {
const rootSpec = specs.find(
spec => spec.schema.model.flavour === 'affine:page'
);
if (!rootSpec) {
return specs;
}
rootSpec.service = class extends (rootSpec.service as typeof RootService) {
override notificationService = {
confirm: async ({
title,
message,
confirmText,
cancelText,
abort,
}: {
title: string;
message: string | TemplateResult;
confirmText: string;
cancelText: string;
abort?: AbortSignal;
}) => {
return new Promise<boolean>(resolve => {
openConfirmModal({
title,
description:
typeof message === 'string' ? (
message
) : (
<TemplateWrapper template={message} />
),
confirmButtonOptions: {
children: confirmText,
type: 'primary',
},
cancelText,
onConfirm: () => {
resolve(true);
},
onCancel: () => {
resolve(false);
},
});
abort?.addEventListener('abort', () => {
resolve(false);
closeConfirmModal();
});
});
},
toast: (message: string, options: ToastOptions) => {
return toast(message, options);
},
notify: async () => {},
};
};
return specs;
}