mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
feat: custom the items of linked menu by configuration (#7554)
Close issue [BS-719](https://linear.app/affine-design/issue/BS-719) and [BS-791](https://linear.app/affine-design/issue/BS-791). Related PR in [BlockSuite](https://github.com/toeverything/blocksuite/pull/7693). ### What Changed? - Support config in BlockSpec - AFFiNE’s Custom @Menu - ignore docs in trash - ignore unedited journal - customized journal icon  ### What's next? - getMenus returns an observable array - Add commands field to BlockSpec and encapsulated insertLinkedNode into a command
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { DocMode } from '@blocksuite/blocks';
|
||||
import {
|
||||
DocMetaTags,
|
||||
DocTitle,
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
} from '@blocksuite/presets';
|
||||
import type { Doc } from '@blocksuite/store';
|
||||
import {
|
||||
type DocMode,
|
||||
DocService,
|
||||
DocsService,
|
||||
useFramework,
|
||||
@@ -44,8 +44,8 @@ import {
|
||||
patchReferenceRenderer,
|
||||
type ReferenceReactRenderer,
|
||||
} from './specs/custom/spec-patchers';
|
||||
import { EdgelessModeSpecs } from './specs/edgeless';
|
||||
import { PageModeSpecs } from './specs/page';
|
||||
import { createEdgelessModeSpecs } from './specs/edgeless';
|
||||
import { createPageModeSpecs } from './specs/page';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const adapted = {
|
||||
@@ -88,7 +88,11 @@ const usePatchSpecs = (page: Doc, shared: boolean, mode: DocMode) => {
|
||||
};
|
||||
}, [page.collection]);
|
||||
|
||||
const specs = mode === 'page' ? PageModeSpecs : EdgelessModeSpecs;
|
||||
const specs = useMemo(() => {
|
||||
return mode === 'edgeless'
|
||||
? createEdgelessModeSpecs(framework)
|
||||
: createPageModeSpecs(framework);
|
||||
}, [mode, framework]);
|
||||
|
||||
const confirmModal = useConfirmModal();
|
||||
const patchedSpecs = useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { WorkspacePropertiesAdapter } from '@affine/core/modules/properties';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import type { EditorHost } from '@blocksuite/block-std';
|
||||
import type { AffineInlineEditor } from '@blocksuite/blocks';
|
||||
import { LinkedWidgetUtils } from '@blocksuite/blocks';
|
||||
import {
|
||||
LinkedEdgelessIcon,
|
||||
LinkedPageIcon,
|
||||
TodayIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import {
|
||||
DocsService,
|
||||
type FrameworkProvider,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
|
||||
export function createLinkedWidgetConfig(framework: FrameworkProvider) {
|
||||
return {
|
||||
getMenus: (
|
||||
query: string,
|
||||
abort: () => void,
|
||||
editorHost: EditorHost,
|
||||
inlineEditor: AffineInlineEditor
|
||||
) => {
|
||||
const currentWorkspace = framework.get(WorkspaceService).workspace;
|
||||
const rawMetas = currentWorkspace.docCollection.meta.docMetas;
|
||||
const adapter = framework.get(WorkspacePropertiesAdapter);
|
||||
const isJournal = (d: DocMeta) =>
|
||||
!!adapter.getJournalPageDateString(d.id);
|
||||
|
||||
const docMetas = rawMetas
|
||||
.filter(meta => {
|
||||
if (isJournal(meta) && !meta.updatedDate) {
|
||||
return false;
|
||||
}
|
||||
return !meta.trash;
|
||||
})
|
||||
.filter(({ title }) => isFuzzyMatch(title, query));
|
||||
|
||||
// TODO need i18n if BlockSuite supported
|
||||
const MAX_DOCS = 6;
|
||||
const DEFAULT_DOC_NAME = 'Untitled';
|
||||
const docsService = framework.get(DocsService);
|
||||
const isEdgeless = (d: DocMeta) =>
|
||||
docsService.list.getMode(d.id) === 'edgeless';
|
||||
return Promise.resolve([
|
||||
{
|
||||
name: 'Link to Doc',
|
||||
items: docMetas.map(doc => ({
|
||||
key: doc.id,
|
||||
name: doc.title || DEFAULT_DOC_NAME,
|
||||
icon: isJournal(doc)
|
||||
? TodayIcon
|
||||
: isEdgeless(doc)
|
||||
? LinkedEdgelessIcon
|
||||
: LinkedPageIcon,
|
||||
action: () => {
|
||||
abort();
|
||||
LinkedWidgetUtils.insertLinkedNode({
|
||||
inlineEditor,
|
||||
docId: doc.id,
|
||||
});
|
||||
mixpanel.track('LinkedDocCreated', {
|
||||
control: 'linked doc',
|
||||
module: 'inline @',
|
||||
type: 'doc',
|
||||
other: 'existing doc',
|
||||
});
|
||||
},
|
||||
})),
|
||||
maxDisplay: MAX_DOCS,
|
||||
overflowText: `${docMetas.length - MAX_DOCS} more docs`,
|
||||
},
|
||||
LinkedWidgetUtils.createNewDocMenuGroup(
|
||||
query,
|
||||
abort,
|
||||
editorHost,
|
||||
inlineEditor
|
||||
),
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the name is a fuzzy match of the query.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const name = 'John Smith';
|
||||
* const query = 'js';
|
||||
* const isMatch = isFuzzyMatch(name, query);
|
||||
* // isMatch: true
|
||||
* ```
|
||||
*/
|
||||
function isFuzzyMatch(name: string, query: string) {
|
||||
const pureName = name
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.split('')
|
||||
.filter(char => char !== ' ')
|
||||
.join('');
|
||||
|
||||
const regex = new RegExp(
|
||||
query
|
||||
.split('')
|
||||
.filter(char => char !== ' ')
|
||||
.map(item => `${escapeRegExp(item)}.*`)
|
||||
.join(''),
|
||||
'i'
|
||||
);
|
||||
return regex.test(pureName);
|
||||
}
|
||||
|
||||
function escapeRegExp(input: string) {
|
||||
// escape regex characters in the input string to prevent regex format errors
|
||||
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
@@ -3,13 +3,20 @@ import {
|
||||
AIPageRootBlockSpec,
|
||||
} from '@affine/core/blocksuite/presets/ai';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import type { BlockSpec } from '@blocksuite/block-std';
|
||||
import type { RootService, TelemetryEventMap } from '@blocksuite/blocks';
|
||||
import type {
|
||||
EdgelessRootBlockSpecType,
|
||||
PageRootBlockSpecType,
|
||||
RootService,
|
||||
TelemetryEventMap,
|
||||
} from '@blocksuite/blocks';
|
||||
import {
|
||||
AffineCanvasTextFonts,
|
||||
EdgelessRootService,
|
||||
PageRootService,
|
||||
} from '@blocksuite/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { createLinkedWidgetConfig } from './linked-widget';
|
||||
|
||||
function customLoadFonts(service: RootService): void {
|
||||
if (runtimeConfig.isSelfHosted) {
|
||||
@@ -41,12 +48,26 @@ function withAffineRootService(Service: typeof RootService) {
|
||||
};
|
||||
}
|
||||
|
||||
export const CustomPageRootBlockSpec: BlockSpec = {
|
||||
...AIPageRootBlockSpec,
|
||||
service: withAffineRootService(PageRootService),
|
||||
};
|
||||
export function createPageRootBlockSpec(
|
||||
framework: FrameworkProvider
|
||||
): PageRootBlockSpecType {
|
||||
return {
|
||||
...AIPageRootBlockSpec,
|
||||
service: withAffineRootService(PageRootService),
|
||||
config: {
|
||||
linkedWidget: createLinkedWidgetConfig(framework),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const CustomEdgelessRootBlockSpec: BlockSpec = {
|
||||
...AIEdgelessRootBlockSpec,
|
||||
service: withAffineRootService(EdgelessRootService),
|
||||
};
|
||||
export function createEdgelessRootBlockSpec(
|
||||
framework: FrameworkProvider
|
||||
): EdgelessRootBlockSpecType {
|
||||
return {
|
||||
...AIEdgelessRootBlockSpec,
|
||||
service: withAffineRootService(EdgelessRootService),
|
||||
config: {
|
||||
linkedWidget: createLinkedWidgetConfig(framework),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,17 +6,22 @@ import {
|
||||
EdgelessTextBlockSpec,
|
||||
FrameBlockSpec,
|
||||
} from '@blocksuite/blocks';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { CommonBlockSpecs } from './common';
|
||||
import { CustomEdgelessRootBlockSpec } from './custom/root-block';
|
||||
import { createEdgelessRootBlockSpec } from './custom/root-block';
|
||||
|
||||
export const EdgelessModeSpecs: BlockSpec[] = [
|
||||
...CommonBlockSpecs,
|
||||
EdgelessSurfaceBlockSpec,
|
||||
EdgelessSurfaceRefBlockSpec,
|
||||
FrameBlockSpec,
|
||||
EdgelessTextBlockSpec,
|
||||
EdgelessNoteBlockSpec,
|
||||
// special
|
||||
CustomEdgelessRootBlockSpec,
|
||||
];
|
||||
export function createEdgelessModeSpecs(
|
||||
framework: FrameworkProvider
|
||||
): BlockSpec[] {
|
||||
return [
|
||||
...CommonBlockSpecs,
|
||||
EdgelessSurfaceBlockSpec,
|
||||
EdgelessSurfaceRefBlockSpec,
|
||||
FrameBlockSpec,
|
||||
EdgelessTextBlockSpec,
|
||||
EdgelessNoteBlockSpec,
|
||||
// special
|
||||
createEdgelessRootBlockSpec(framework),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@ import {
|
||||
PageSurfaceBlockSpec,
|
||||
PageSurfaceRefBlockSpec,
|
||||
} from '@blocksuite/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { CommonBlockSpecs } from './common';
|
||||
import { CustomPageRootBlockSpec } from './custom/root-block';
|
||||
import { createPageRootBlockSpec } from './custom/root-block';
|
||||
|
||||
export const PageModeSpecs: BlockSpec[] = [
|
||||
...CommonBlockSpecs,
|
||||
PageSurfaceBlockSpec,
|
||||
PageSurfaceRefBlockSpec,
|
||||
// special
|
||||
CustomPageRootBlockSpec,
|
||||
];
|
||||
export function createPageModeSpecs(framework: FrameworkProvider): BlockSpec[] {
|
||||
return [
|
||||
...CommonBlockSpecs,
|
||||
PageSurfaceBlockSpec,
|
||||
PageSurfaceRefBlockSpec,
|
||||
// special
|
||||
createPageRootBlockSpec(framework),
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user