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]);
return (
<Modal
contentOptions={{ className: styles.confirmModalContainer }}
contentOptions={{
className: styles.confirmModalContainer,
onPointerDownOutside: e => {
e.stopPropagation();
onCancel?.();
},
}}
width={width}
closeButtonOptions={{
onClick: onCancel,
}}
{...props}
>
{children ? (

View File

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

View File

@ -10,14 +10,11 @@ import {
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { AffinePageReference } from '../../affine/reference-link';
import { BlocksuiteEditorContainer } from './blocksuite-editor-container';
import { NoPageRootError } from './no-page-error';
import type { ReferenceReactRenderer } from './specs/custom/patch-reference-renderer';
export type ErrorBoundaryProps = {
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 (
<BlocksuiteEditorContainer
mode={mode}
@ -108,7 +92,6 @@ const BlockSuiteEditorImpl = forwardRef<AffineEditorContainer, EditorProps>(
ref={onRefChange}
className={className}
style={style}
referenceRenderer={referenceRenderer}
defaultSelectedBlockId={defaultSelectedBlockId}
/>
);

View File

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

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;
}