mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-15 00:33:06 +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]);
|
}, [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 ? (
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
|
||||||
))}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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