mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-14 12:14:13 +03:00
feat: provide notification to bs (#7002)
upstream https://github.com/toeverything/blocksuite/pull/7101 fix AFF-1120
This commit is contained in:
parent
919e40f28e
commit
88d4351c28
@ -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 ? (
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user