mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 03:56:23 +08:00
Merge branch 'canary' into zzj/feat/database-block/upgrade-prompt
This commit is contained in:
@@ -135,6 +135,7 @@ declare global {
|
||||
isRootSession?: boolean;
|
||||
webSearch?: boolean;
|
||||
reasoning?: boolean;
|
||||
modelId?: string;
|
||||
contexts?: {
|
||||
docs: AIDocContextOption[];
|
||||
files: AIFileContextOption[];
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Peekable } from '@blocksuite/affine/components/peek';
|
||||
import { ViewExtensionManagerIdentifier } from '@blocksuite/affine/ext-loader';
|
||||
import { BlockComponent } from '@blocksuite/affine/std';
|
||||
import { computed } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
|
||||
import { ChatMessagesSchema } from '../../components/ai-chat-messages';
|
||||
import type { TextRendererOptions } from '../../components/text-renderer';
|
||||
import { ChatWithAIIcon } from './components/icon';
|
||||
import { type AIChatBlockModel } from './model';
|
||||
import { AIChatBlockStyles } from './styles';
|
||||
@@ -17,6 +19,8 @@ import { AIChatBlockStyles } from './styles';
|
||||
export class AIChatBlockComponent extends BlockComponent<AIChatBlockModel> {
|
||||
static override styles = AIChatBlockStyles;
|
||||
|
||||
private _textRendererOptions: TextRendererOptions = {};
|
||||
|
||||
// Deserialize messages from JSON string and verify the type using zod
|
||||
private readonly _deserializeChatMessages = computed(() => {
|
||||
const messages = this.model.props.messages$.value;
|
||||
@@ -32,18 +36,23 @@ export class AIChatBlockComponent extends BlockComponent<AIChatBlockModel> {
|
||||
}
|
||||
});
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._textRendererOptions = {
|
||||
customHeading: true,
|
||||
extensions: this.previewExtensions,
|
||||
};
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
const messages = this._deserializeChatMessages.value.slice(-2);
|
||||
const textRendererOptions = {
|
||||
customHeading: true,
|
||||
};
|
||||
|
||||
return html`<div class="affine-ai-chat-block-container">
|
||||
<div class="ai-chat-messages-container">
|
||||
<ai-chat-messages
|
||||
.host=${this.host}
|
||||
.messages=${messages}
|
||||
.textRendererOptions=${textRendererOptions}
|
||||
.textRendererOptions=${this._textRendererOptions}
|
||||
.withMask=${true}
|
||||
></ai-chat-messages>
|
||||
</div>
|
||||
@@ -52,6 +61,10 @@ export class AIChatBlockComponent extends BlockComponent<AIChatBlockModel> {
|
||||
</div>
|
||||
</div> `;
|
||||
}
|
||||
|
||||
get previewExtensions() {
|
||||
return this.std.get(ViewExtensionManagerIdentifier).get('preview-page');
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
+66
-1
@@ -1,9 +1,11 @@
|
||||
import { Bound } from '@blocksuite/affine/global/gfx';
|
||||
import { Bound, clamp } from '@blocksuite/affine/global/gfx';
|
||||
import { toGfxBlockComponent } from '@blocksuite/affine/std';
|
||||
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||
import { html } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { AIChatBlockComponent } from './ai-chat-block';
|
||||
import { AIChatBlockSchema } from './model';
|
||||
|
||||
export class EdgelessAIChatBlockComponent extends toGfxBlockComponent(
|
||||
AIChatBlockComponent
|
||||
@@ -31,6 +33,69 @@ export class EdgelessAIChatBlockComponent extends toGfxBlockComponent(
|
||||
}
|
||||
}
|
||||
|
||||
export const EdgelessAIChatBlockInteraction =
|
||||
GfxViewInteractionExtension<EdgelessAIChatBlockComponent>(
|
||||
AIChatBlockSchema.model.flavour,
|
||||
{
|
||||
resizeConstraint: {
|
||||
minWidth: 260,
|
||||
minHeight: 160,
|
||||
maxWidth: 320,
|
||||
maxHeight: 300,
|
||||
},
|
||||
|
||||
handleRotate() {
|
||||
return {
|
||||
beforeRotate(context) {
|
||||
context.set({
|
||||
rotatable: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
handleResize({ model }) {
|
||||
const initialScale = model.props.scale$.peek();
|
||||
|
||||
return {
|
||||
onResizeStart(context) {
|
||||
context.default(context);
|
||||
model.stash('scale');
|
||||
},
|
||||
|
||||
onResizeMove(context) {
|
||||
const { newBound, originalBound, lockRatio, constraint } = context;
|
||||
const { minWidth, maxWidth, minHeight, maxHeight } = constraint;
|
||||
|
||||
let scale = initialScale;
|
||||
const originalRealWidth = originalBound.w / scale;
|
||||
|
||||
// update scale if resize is proportional
|
||||
if (lockRatio) {
|
||||
scale = newBound.w / originalRealWidth;
|
||||
}
|
||||
|
||||
let newRealWidth = clamp(newBound.w / scale, minWidth, maxWidth);
|
||||
let newRealHeight = clamp(newBound.h / scale, minHeight, maxHeight);
|
||||
|
||||
newBound.w = newRealWidth * scale;
|
||||
newBound.h = newRealHeight * scale;
|
||||
|
||||
model.props.xywh = newBound.serialize();
|
||||
if (scale !== initialScale) {
|
||||
model.props.scale = scale;
|
||||
}
|
||||
},
|
||||
|
||||
onResizeEnd(context) {
|
||||
context.default(context);
|
||||
model.pop('scale');
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-edgeless-ai-chat': EdgelessAIChatBlockComponent;
|
||||
|
||||
@@ -158,6 +158,9 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@query('.chat-panel-messages-container')
|
||||
accessor messagesContainer: HTMLDivElement | null = null;
|
||||
|
||||
@@ -271,6 +274,7 @@ export class ChatPanelMessages extends WithDisposable(ShadowlessElement) {
|
||||
.status=${isLast ? status : 'idle'}
|
||||
.error=${isLast ? error : null}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
.getSessionId=${this.getSessionId}
|
||||
.retry=${() => this.retry()}
|
||||
></chat-message-assistant>`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
@@ -20,11 +21,15 @@ export class ChatContentRichText extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
protected override render() {
|
||||
const { text, host } = this;
|
||||
return html`${createTextRenderer(host, {
|
||||
customHeading: true,
|
||||
extensions: this.extensions,
|
||||
affineFeatureFlagService: this.affineFeatureFlagService,
|
||||
})(text, this.state)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import './chat-panel-messages';
|
||||
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { ContextEmbedStatus } from '@affine/graphql';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
@@ -205,6 +206,9 @@ export class ChatPanel extends SignalWatcher(
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@state()
|
||||
accessor isLoading = false;
|
||||
|
||||
@@ -399,6 +403,7 @@ export class ChatPanel extends SignalWatcher(
|
||||
.host=${this.host}
|
||||
.isLoading=${this.isLoading}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
></chat-panel-messages>
|
||||
<ai-chat-composer
|
||||
.host=${this.host}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '../content/assistant-avatar';
|
||||
import '../content/rich-text';
|
||||
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { isInsidePageEditor } from '@blocksuite/affine/shared/utils';
|
||||
import type { EditorHost } from '@blocksuite/affine/std';
|
||||
@@ -48,6 +49,9 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
@property({ attribute: false })
|
||||
accessor extensions!: ExtensionType[];
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor getSessionId!: () => Promise<string | undefined>;
|
||||
|
||||
@@ -93,6 +97,7 @@ export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {
|
||||
.text=${item.content}
|
||||
.state=${state}
|
||||
.extensions=${this.extensions}
|
||||
.affineFeatureFlagService=${this.affineFeatureFlagService}
|
||||
></chat-content-rich-text>
|
||||
${shouldRenderError ? AIChatErrorRenderer(host, error) : nothing}
|
||||
${this.renderEditorActions()}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { toast } from '@affine/component';
|
||||
import type {
|
||||
CollectionMeta,
|
||||
TagMeta,
|
||||
} from '@affine/core/components/page-list';
|
||||
import type { TagMeta } from '@affine/core/components/page-list';
|
||||
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||
import track from '@affine/track';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
import type { TagMeta } from '@affine/core/components/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { createLitPortal } from '@blocksuite/affine/components/portal';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
@@ -128,7 +127,7 @@ export class ChatPanelChips extends SignalWatcher(
|
||||
|
||||
private _tags: Signal<TagMeta[]> = signal([]);
|
||||
|
||||
private _collections: Signal<Collection[]> = signal([]);
|
||||
private _collections: Signal<{ id: string; name: string }[]> = signal([]);
|
||||
|
||||
private _cleanup: (() => void) | null = null;
|
||||
|
||||
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||
import { CollectionsIcon } from '@blocksuite/icons/lit';
|
||||
@@ -18,7 +17,7 @@ export class ChatPanelCollectionChip extends SignalWatcher(
|
||||
accessor removeChip!: (chip: CollectionChip) => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor collection!: Collection;
|
||||
accessor collection!: { id: string; name: string };
|
||||
|
||||
override render() {
|
||||
const { state } = this.chip;
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
SearchDocMenuAction,
|
||||
SearchTagMenuAction,
|
||||
} from '@affine/core/modules/search-menu/services';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { DocMeta, Store } from '@blocksuite/affine/store';
|
||||
import type { LinkedMenuGroup } from '@blocksuite/affine/widgets/linked-doc';
|
||||
import type { Signal } from '@preact/signals-core';
|
||||
@@ -71,7 +70,7 @@ export interface DocDisplayConfig {
|
||||
getTagTitle: (tagId: string) => string;
|
||||
getTagPageIds: (tagId: string) => string[];
|
||||
getCollections: () => {
|
||||
signal: Signal<Collection[]>;
|
||||
signal: Signal<{ id: string; name: string }[]>;
|
||||
cleanup: () => void;
|
||||
};
|
||||
getCollectionPageIds: (collectionId: string) => string[];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createReactComponentFromLit } from '@affine/component';
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { Container, type ServiceProvider } from '@blocksuite/affine/global/di';
|
||||
import { WithDisposable } from '@blocksuite/affine/global/lit';
|
||||
import {
|
||||
@@ -6,10 +7,7 @@ import {
|
||||
defaultImageProxyMiddleware,
|
||||
ImageProxyService,
|
||||
} from '@blocksuite/affine/shared/adapters';
|
||||
import {
|
||||
LinkPreviewerService,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import { ThemeProvider } from '@blocksuite/affine/shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||
import {
|
||||
BlockStdScope,
|
||||
@@ -104,6 +102,7 @@ export type TextRendererOptions = {
|
||||
extensions?: ExtensionType[];
|
||||
additionalMiddlewares?: TransformerMiddleware[];
|
||||
testId?: string;
|
||||
affineFeatureFlagService?: FeatureFlagService;
|
||||
};
|
||||
|
||||
// todo: refactor it for more general purpose usage instead of AI only?
|
||||
@@ -266,7 +265,14 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
||||
codeBlockWrapMiddleware(true),
|
||||
...(this.options.additionalMiddlewares ?? []),
|
||||
];
|
||||
markDownToDoc(provider, schema, latestAnswer, middlewares)
|
||||
const affineFeatureFlagService = this.options.affineFeatureFlagService;
|
||||
markDownToDoc(
|
||||
provider,
|
||||
schema,
|
||||
latestAnswer,
|
||||
middlewares,
|
||||
affineFeatureFlagService
|
||||
)
|
||||
.then(doc => {
|
||||
this.disposeDoc();
|
||||
this._doc = doc.doc.getStore({
|
||||
@@ -278,16 +284,10 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) {
|
||||
this._doc.readonly = true;
|
||||
this.requestUpdate();
|
||||
if (this.state !== 'generating') {
|
||||
// LinkPreviewerService & ImageProxyService config should read from host settings
|
||||
const linkPreviewerService =
|
||||
this.host?.std.store.get(LinkPreviewerService);
|
||||
this._doc.load();
|
||||
// LinkPreviewService & ImageProxyService config should read from host settings
|
||||
const imageProxyService =
|
||||
this.host?.std.store.get(ImageProxyService);
|
||||
if (linkPreviewerService) {
|
||||
this._doc
|
||||
?.get(LinkPreviewerService)
|
||||
.setEndpoint(linkPreviewerService.endpoint);
|
||||
}
|
||||
if (imageProxyService) {
|
||||
this._doc
|
||||
?.get(ImageProxyService)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { ContextEmbedStatus } from '@affine/graphql';
|
||||
import {
|
||||
CanvasElementType,
|
||||
@@ -445,7 +446,10 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
.get(ViewExtensionManagerIdentifier)
|
||||
.get('preview-page');
|
||||
|
||||
this._textRendererOptions = { extensions };
|
||||
this._textRendererOptions = {
|
||||
extensions,
|
||||
affineFeatureFlagService: this.affineFeatureFlagService,
|
||||
};
|
||||
this._historyMessages = this._deserializeHistoryChatMessages(
|
||||
this.historyMessagesString
|
||||
);
|
||||
@@ -556,6 +560,9 @@ export class AIChatBlockPeekView extends LitElement {
|
||||
@property({ attribute: false })
|
||||
accessor searchMenuConfig!: SearchMenuConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor affineFeatureFlagService!: FeatureFlagService;
|
||||
|
||||
@state()
|
||||
accessor _historyMessages: ChatMessage[] = [];
|
||||
|
||||
@@ -584,7 +591,8 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
docDisplayConfig: DocDisplayConfig,
|
||||
searchMenuConfig: SearchMenuConfig,
|
||||
networkSearchConfig: AINetworkSearchConfig,
|
||||
reasoningConfig: AIReasoningConfig
|
||||
reasoningConfig: AIReasoningConfig,
|
||||
affineFeatureFlagService: FeatureFlagService
|
||||
) => {
|
||||
return html`<ai-chat-block-peek-view
|
||||
.blockModel=${blockModel}
|
||||
@@ -593,5 +601,6 @@ export const AIChatBlockPeekViewTemplate = (
|
||||
.docDisplayConfig=${docDisplayConfig}
|
||||
.searchMenuConfig=${searchMenuConfig}
|
||||
.reasoningConfig=${reasoningConfig}
|
||||
.affineFeatureFlagService=${affineFeatureFlagService}
|
||||
></ai-chat-block-peek-view>`;
|
||||
};
|
||||
|
||||
@@ -352,12 +352,14 @@ export class CopilotClient {
|
||||
messageId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
signal,
|
||||
}: {
|
||||
sessionId: string;
|
||||
messageId?: string;
|
||||
reasoning?: boolean;
|
||||
webSearch?: boolean;
|
||||
modelId?: string;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
let url = `/api/copilot/chat/${sessionId}`;
|
||||
@@ -365,6 +367,7 @@ export class CopilotClient {
|
||||
messageId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
});
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
@@ -380,11 +383,13 @@ export class CopilotClient {
|
||||
messageId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
}: {
|
||||
sessionId: string;
|
||||
messageId?: string;
|
||||
reasoning?: boolean;
|
||||
webSearch?: boolean;
|
||||
modelId?: string;
|
||||
},
|
||||
endpoint = 'stream'
|
||||
) {
|
||||
@@ -393,6 +398,7 @@ export class CopilotClient {
|
||||
messageId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
});
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
|
||||
@@ -21,6 +21,7 @@ export type TextToTextOptions = {
|
||||
postfix?: (text: string) => string;
|
||||
reasoning?: boolean;
|
||||
webSearch?: boolean;
|
||||
modelId?: string;
|
||||
};
|
||||
|
||||
export type ToImageOptions = TextToTextOptions & {
|
||||
@@ -117,6 +118,7 @@ export function textToText({
|
||||
postfix,
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
}: TextToTextOptions) {
|
||||
let messageId: string | undefined;
|
||||
|
||||
@@ -138,6 +140,7 @@ export function textToText({
|
||||
messageId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
},
|
||||
workflow ? 'workflow' : undefined
|
||||
);
|
||||
@@ -199,6 +202,7 @@ export function textToText({
|
||||
messageId,
|
||||
reasoning,
|
||||
webSearch,
|
||||
modelId,
|
||||
});
|
||||
})(),
|
||||
]);
|
||||
|
||||
@@ -82,7 +82,7 @@ export function setupAIProvider(
|
||||
|
||||
//#region actions
|
||||
AIProvider.provide('chat', async options => {
|
||||
const { input, contexts, webSearch } = options;
|
||||
const { input, contexts, webSearch, reasoning } = options;
|
||||
|
||||
const sessionId = await createSession({
|
||||
promptName: 'Chat With AFFiNE AI',
|
||||
@@ -90,6 +90,7 @@ export function setupAIProvider(
|
||||
});
|
||||
return textToText({
|
||||
...options,
|
||||
modelId: options.modelId ?? (reasoning ? 'o4-mini' : undefined),
|
||||
client,
|
||||
sessionId,
|
||||
content: input,
|
||||
|
||||
+4
-2
@@ -165,11 +165,13 @@ const usePreviewExtensions = () => {
|
||||
const extensions = useMemo(() => {
|
||||
const manager = getViewManager()
|
||||
.config.init()
|
||||
.common(framework, enableAI)
|
||||
.foundation(framework)
|
||||
.ai(enableAI, framework)
|
||||
.theme(framework)
|
||||
.database(framework)
|
||||
.linkedDoc(framework)
|
||||
.paragraph(enableAI).value;
|
||||
.paragraph(enableAI)
|
||||
.linkPreview(framework).value;
|
||||
const specs = manager.get('preview-page');
|
||||
return [...specs, patchReferenceRenderer(reactToLit, referenceRenderer)];
|
||||
}, [reactToLit, referenceRenderer, framework, enableAI]);
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
ImageProxyService,
|
||||
} from '@blocksuite/affine/shared/adapters';
|
||||
import { focusBlockEnd } from '@blocksuite/affine/shared/commands';
|
||||
import { LinkPreviewerService } from '@blocksuite/affine/shared/services';
|
||||
import { getLastNoteBlock } from '@blocksuite/affine/shared/utils';
|
||||
import type { BlockStdScope, EditorHost } from '@blocksuite/affine/std';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
@@ -212,13 +211,7 @@ const BlockSuiteEditorImpl = ({
|
||||
server.baseUrl
|
||||
).toString();
|
||||
|
||||
const linkPreviewUrl = new URL(
|
||||
BUILD_CONFIG.linkPreviewUrl,
|
||||
server.baseUrl
|
||||
).toString();
|
||||
|
||||
editor.std.clipboard.use(customImageProxyMiddleware(imageProxyUrl));
|
||||
page.get(LinkPreviewerService).setEndpoint(linkPreviewUrl);
|
||||
page.get(ImageProxyService).setImageProxyURL(imageProxyUrl);
|
||||
|
||||
editor.updateComplete
|
||||
|
||||
@@ -80,7 +80,8 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
const patchedSpecs = useMemo(() => {
|
||||
const manager = getViewManager()
|
||||
.config.init()
|
||||
.common(framework, enableAI)
|
||||
.foundation(framework)
|
||||
.ai(enableAI, framework)
|
||||
.theme(framework)
|
||||
.editorConfig(framework)
|
||||
.editorView({
|
||||
@@ -98,7 +99,10 @@ const usePatchSpecs = (mode: DocMode) => {
|
||||
})
|
||||
.database(framework)
|
||||
.linkedDoc(framework)
|
||||
.paragraph(enableAI).value;
|
||||
.paragraph(enableAI)
|
||||
.mobile(framework)
|
||||
.electron(framework)
|
||||
.linkPreview(framework).value;
|
||||
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
if (mode === 'page') {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Avatar, uniReactRoot } from '@affine/component';
|
||||
import {
|
||||
createGroupByConfig,
|
||||
type GroupRenderProps,
|
||||
t,
|
||||
ungroups,
|
||||
} from '@blocksuite/affine/blocks/database';
|
||||
import type { UserService } from '@blocksuite/affine-shared/services';
|
||||
|
||||
import { useMemberInfo } from '../hooks/use-member-info';
|
||||
import {
|
||||
avatar,
|
||||
memberName,
|
||||
memberPreviewContainer,
|
||||
} from '../properties/member/style.css';
|
||||
|
||||
const MemberPreview = ({
|
||||
memberId,
|
||||
userService,
|
||||
}: {
|
||||
memberId: string;
|
||||
userService: UserService | null | undefined;
|
||||
}) => {
|
||||
const userInfo = useMemberInfo(memberId, userService);
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={memberPreviewContainer}>
|
||||
<Avatar
|
||||
name={userInfo.removed ? undefined : (userInfo.name ?? undefined)}
|
||||
className={avatar}
|
||||
url={!userInfo.removed ? userInfo.avatar : undefined}
|
||||
size={20}
|
||||
/>
|
||||
<div className={memberName}>
|
||||
{userInfo.removed ? 'Deleted user' : userInfo.name || 'Unnamed'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const MemberGroupView = (props: GroupRenderProps<string | null, {}>) => {
|
||||
const tType = props.group.tType;
|
||||
if (!t.user.is(tType)) return 'Ungroup';
|
||||
const memberId = props.group.value;
|
||||
if (memberId == null) return 'Ungroup';
|
||||
|
||||
return (
|
||||
<MemberPreview
|
||||
memberId={memberId}
|
||||
userService={tType.data?.userService}
|
||||
></MemberPreview>
|
||||
);
|
||||
};
|
||||
|
||||
const MultiMemberGroupView = (props: GroupRenderProps<string | null, {}>) => {
|
||||
const tType = props.group.tType;
|
||||
if (!t.array.is(tType) || !t.user.is(tType.element)) return 'Ungroup';
|
||||
const memberId = props.group.value;
|
||||
if (memberId == null) return 'Ungroup';
|
||||
|
||||
return (
|
||||
<MemberPreview
|
||||
memberId={memberId}
|
||||
userService={tType.element.data?.userService}
|
||||
></MemberPreview>
|
||||
);
|
||||
};
|
||||
|
||||
export const groupByConfigList = [
|
||||
createGroupByConfig({
|
||||
name: 'member',
|
||||
matchType: t.user.instance(),
|
||||
groupName: (type, value: string | null) => {
|
||||
if (t.user.is(type) && typeof value === 'string') {
|
||||
const userService = type.data?.userService;
|
||||
if (userService) {
|
||||
const userInfo = userService.userInfo$(value).value;
|
||||
if (userInfo && !userInfo?.removed) {
|
||||
return userInfo.name ?? 'Unnamed';
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
defaultKeys: () => {
|
||||
return [ungroups];
|
||||
},
|
||||
valuesGroup: value => {
|
||||
if (typeof value !== 'string') {
|
||||
return [ungroups];
|
||||
}
|
||||
return [
|
||||
{
|
||||
key: value,
|
||||
value: value,
|
||||
},
|
||||
];
|
||||
},
|
||||
view: uniReactRoot.createUniComponent(MemberGroupView),
|
||||
}),
|
||||
createGroupByConfig({
|
||||
name: 'multi-member',
|
||||
matchType: t.array.instance(t.user.instance()),
|
||||
groupName: (_type, value: string | null) => {
|
||||
if (
|
||||
t.array.is(_type) &&
|
||||
t.user.is(_type.element) &&
|
||||
typeof value === 'string'
|
||||
) {
|
||||
const userService = _type.element.data?.userService;
|
||||
if (userService) {
|
||||
const userInfo = userService.userInfo$(value).value;
|
||||
if (userInfo && !userInfo?.removed) {
|
||||
return userInfo.name ?? 'Unnamed';
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
},
|
||||
defaultKeys: _type => {
|
||||
return [ungroups];
|
||||
},
|
||||
valuesGroup: (value, _type) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return [ungroups];
|
||||
}
|
||||
return value.map(id => ({
|
||||
key: id,
|
||||
value: id,
|
||||
}));
|
||||
},
|
||||
addToGroup: (value, old) => {
|
||||
if (value == null) {
|
||||
return old;
|
||||
}
|
||||
return Array.isArray(old) ? [...old, value] : [value];
|
||||
},
|
||||
removeFromGroup: (value, old) => {
|
||||
if (Array.isArray(old)) {
|
||||
return old.filter(v => v !== value);
|
||||
}
|
||||
return old;
|
||||
},
|
||||
view: uniReactRoot.createUniComponent(MultiMemberGroupView),
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { UserService } from '@blocksuite/affine-shared/services';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useSignalValue } from '../../../modules/doc-info/utils';
|
||||
|
||||
export const useMemberInfo = (
|
||||
id: string,
|
||||
userService: UserService | null | undefined
|
||||
) => {
|
||||
useEffect(() => {
|
||||
userService?.revalidateUserInfo(id);
|
||||
}, [id, userService]);
|
||||
return useSignalValue(userService?.userInfo$(id));
|
||||
};
|
||||
+23
-2
@@ -1,4 +1,12 @@
|
||||
import { propertyType, t } from '@blocksuite/affine/blocks/database';
|
||||
import {
|
||||
EditorHostKey,
|
||||
propertyType,
|
||||
t,
|
||||
} from '@blocksuite/affine/blocks/database';
|
||||
import {
|
||||
UserListProvider,
|
||||
UserProvider,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import zod from 'zod';
|
||||
|
||||
export const createdByColumnType = propertyType('created-by');
|
||||
@@ -26,6 +34,19 @@ export const createdByPropertyModelConfig = createdByColumnType.modelConfig({
|
||||
jsonValue: {
|
||||
schema: zod.string().nullable(),
|
||||
isEmpty: () => false,
|
||||
type: () => t.string.instance(),
|
||||
type: ({ dataSource }) => {
|
||||
const host = dataSource.serviceGet(EditorHostKey);
|
||||
const userService = host?.std.getOptional(UserProvider);
|
||||
const userListService = host?.std.getOptional(UserListProvider);
|
||||
|
||||
return t.user.instance(
|
||||
userListService && userService
|
||||
? {
|
||||
userService,
|
||||
userListService,
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+3
-13
@@ -3,7 +3,7 @@ import {
|
||||
type CellRenderProps,
|
||||
createIcon,
|
||||
type DataViewCellLifeCycle,
|
||||
HostContextKey,
|
||||
EditorHostKey,
|
||||
} from '@blocksuite/affine/blocks/database';
|
||||
import {
|
||||
UserProvider,
|
||||
@@ -14,11 +14,11 @@ import {
|
||||
forwardRef,
|
||||
type ForwardRefRenderFunction,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
} from 'react';
|
||||
|
||||
import { useSignalValue } from '../../../../modules/doc-info/utils';
|
||||
import { useMemberInfo } from '../../hooks/use-member-info';
|
||||
import { createdByPropertyModelConfig } from './define';
|
||||
|
||||
const cellContainer = css({
|
||||
@@ -64,7 +64,7 @@ const CreatedByCellComponent: ForwardRefRenderFunction<
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const host = props.cell.view.contextGet(HostContextKey);
|
||||
const host = props.cell.view.serviceGet(EditorHostKey);
|
||||
const userService = host?.std.getOptional(UserProvider);
|
||||
const memberId = useSignalValue(props.cell.value$);
|
||||
if (!memberId) {
|
||||
@@ -83,16 +83,6 @@ const CreatedByCellComponent: ForwardRefRenderFunction<
|
||||
);
|
||||
};
|
||||
|
||||
const useMemberInfo = (
|
||||
id: string,
|
||||
userService: UserService | null | undefined
|
||||
) => {
|
||||
useEffect(() => {
|
||||
userService?.revalidateUserInfo(id);
|
||||
}, [id, userService]);
|
||||
return useSignalValue(userService?.userInfo$(id));
|
||||
};
|
||||
|
||||
const MemberPreview = ({
|
||||
memberId,
|
||||
userService,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type CellRenderProps,
|
||||
createIcon,
|
||||
type DataViewCellLifeCycle,
|
||||
HostContextKey,
|
||||
EditorHostKey,
|
||||
} from '@blocksuite/affine/blocks/database';
|
||||
import { openFileOrFiles } from '@blocksuite/affine/shared/utils';
|
||||
import type { BlobEngine } from '@blocksuite/affine/sync';
|
||||
@@ -226,8 +226,8 @@ class FileCellManager {
|
||||
this.cell = props.cell;
|
||||
this.selectCurrentCell = props.selectCurrentCell;
|
||||
this.isEditing = props.isEditing$;
|
||||
this.blobSync = this.cell?.view?.contextGet
|
||||
? this.cell.view.contextGet(HostContextKey)?.store.blobSync
|
||||
this.blobSync = this.cell?.view?.serviceGet
|
||||
? this.cell.view.serviceGet(EditorHostKey)?.store.blobSync
|
||||
: undefined;
|
||||
|
||||
this.fileUploadManager = this.blobSync
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { propertyType, t } from '@blocksuite/affine/blocks/database';
|
||||
import {
|
||||
EditorHostKey,
|
||||
propertyType,
|
||||
t,
|
||||
} from '@blocksuite/affine/blocks/database';
|
||||
import {
|
||||
UserListProvider,
|
||||
UserProvider,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import zod from 'zod';
|
||||
|
||||
export const memberColumnType = propertyType('member');
|
||||
@@ -31,7 +39,21 @@ export const memberPropertyModelConfig = memberColumnType.modelConfig({
|
||||
},
|
||||
jsonValue: {
|
||||
schema: MemberCellJsonValueTypeSchema,
|
||||
type: () => t.array.instance(t.string.instance()),
|
||||
type: ({ dataSource }) => {
|
||||
const host = dataSource.serviceGet(EditorHostKey);
|
||||
const userService = host?.std.getOptional(UserProvider);
|
||||
const userListService = host?.std.getOptional(UserListProvider);
|
||||
return t.array.instance(
|
||||
t.user.instance(
|
||||
userListService && userService
|
||||
? {
|
||||
userService: userService,
|
||||
userListService: userListService,
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
},
|
||||
isEmpty: ({ value }) => value.length === 0,
|
||||
},
|
||||
});
|
||||
|
||||
+2
-8
@@ -16,6 +16,7 @@ import {
|
||||
|
||||
import { useSignalValue } from '../../../../../modules/doc-info/utils';
|
||||
import { Spinner } from '../../../components/loading';
|
||||
import { useMemberInfo } from '../../../hooks/use-member-info';
|
||||
import * as styles from './style.css';
|
||||
|
||||
type BaseOptions = {
|
||||
@@ -172,13 +173,6 @@ class MemberManager {
|
||||
};
|
||||
}
|
||||
|
||||
export const useMemberInfo = (id: string, memberManager: MemberManager) => {
|
||||
useEffect(() => {
|
||||
memberManager.userService?.revalidateUserInfo(id);
|
||||
}, [id, memberManager.userService]);
|
||||
return useSignalValue(memberManager.userService?.userInfo$(id));
|
||||
};
|
||||
|
||||
export const MemberListItem = (props: {
|
||||
member: ExistedUserInfo;
|
||||
memberManager: MemberManager;
|
||||
@@ -225,7 +219,7 @@ export const MemberPreview = ({
|
||||
memberManager: MemberManager;
|
||||
onDelete?: () => void;
|
||||
}) => {
|
||||
const userInfo = useMemberInfo(memberId, memberManager);
|
||||
const userInfo = useMemberInfo(memberId, memberManager.userService);
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const memberPopoverContainer = style({
|
||||
@@ -10,45 +9,10 @@ export const memberPopoverContent = style({
|
||||
padding: '0',
|
||||
});
|
||||
|
||||
export const searchContainer = style({
|
||||
padding: '12px 12px 8px 12px',
|
||||
});
|
||||
|
||||
export const searchInput = style({
|
||||
width: '100%',
|
||||
});
|
||||
|
||||
export const memberListContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxHeight: '300px',
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
export const memberItem = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '8px 12px',
|
||||
gap: '8px',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
transition: 'background-color 0.2s ease',
|
||||
':hover': {
|
||||
backgroundColor: cssVarV2.layer.background.hoverOverlay,
|
||||
},
|
||||
':active': {
|
||||
backgroundColor: cssVarV2.layer.background.secondary,
|
||||
},
|
||||
});
|
||||
|
||||
export const memberItemContent = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const memberName = style({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
@@ -70,21 +34,6 @@ export const avatar = style({
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
export const loadingContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
});
|
||||
|
||||
export const noResultContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: '16px',
|
||||
color: cssVarV2.text.secondary,
|
||||
});
|
||||
|
||||
export const memberPreviewContainer = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type CellRenderProps,
|
||||
createIcon,
|
||||
type DataViewCellLifeCycle,
|
||||
HostContextKey,
|
||||
EditorHostKey,
|
||||
} from '@blocksuite/affine/blocks/database';
|
||||
import {
|
||||
UserListProvider,
|
||||
@@ -17,12 +17,12 @@ import {
|
||||
forwardRef,
|
||||
type ForwardRefRenderFunction,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import { useSignalValue } from '../../../../modules/doc-info/utils';
|
||||
import { useMemberInfo } from '../../hooks/use-member-info';
|
||||
import type {
|
||||
MemberCellJsonValueType,
|
||||
MemberCellRawValueType,
|
||||
@@ -55,7 +55,7 @@ class MemberManager {
|
||||
this.cell = props.cell;
|
||||
this.selectCurrentCell = props.selectCurrentCell;
|
||||
this.isEditing = props.isEditing$;
|
||||
const host = this.cell.view.contextGet(HostContextKey);
|
||||
const host = this.cell.view.serviceGet(EditorHostKey);
|
||||
this.userService = host?.std.getOptional(UserProvider);
|
||||
this.userListService = host?.std.getOptional(UserListProvider);
|
||||
}
|
||||
@@ -140,13 +140,6 @@ const MemberCellComponent: ForwardRefRenderFunction<
|
||||
);
|
||||
};
|
||||
|
||||
const useMemberInfo = (id: string, memberManager: MemberManager) => {
|
||||
useEffect(() => {
|
||||
memberManager.userService?.revalidateUserInfo(id);
|
||||
}, [id, memberManager.userService]);
|
||||
return useSignalValue(memberManager.userService?.userInfo$(id));
|
||||
};
|
||||
|
||||
const MemberPreview = ({
|
||||
memberId,
|
||||
memberManager,
|
||||
@@ -154,7 +147,7 @@ const MemberPreview = ({
|
||||
memberId: string;
|
||||
memberManager: MemberManager;
|
||||
}) => {
|
||||
const userInfo = useMemberInfo(memberId, memberManager);
|
||||
const userInfo = useMemberInfo(memberId, memberManager.userService);
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+24
-46
@@ -10,12 +10,6 @@ import { AiSlashMenuConfigExtension } from '@affine/core/blocksuite/ai/extension
|
||||
import { CopilotTool } from '@affine/core/blocksuite/ai/tool/copilot-tool';
|
||||
import { aiPanelWidget } from '@affine/core/blocksuite/ai/widgets/ai-panel/ai-panel';
|
||||
import { edgelessCopilotWidget } from '@affine/core/blocksuite/ai/widgets/edgeless-copilot';
|
||||
import { buildDocDisplayMetaExtension } from '@affine/core/blocksuite/extensions/display-meta';
|
||||
import { patchFileSizeLimitExtension } from '@affine/core/blocksuite/extensions/file-size-limit';
|
||||
import { getFontConfigExtension } from '@affine/core/blocksuite/extensions/font-config';
|
||||
import { patchPeekViewService } from '@affine/core/blocksuite/extensions/peek-view-service';
|
||||
import { getTelemetryExtension } from '@affine/core/blocksuite/extensions/telemetry';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
@@ -25,31 +19,37 @@ import { BlockFlavourIdentifier } from '@blocksuite/affine/std';
|
||||
import { FrameworkProvider } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { EdgelessClipboardAIChatConfig } from './edgeless-clipboard';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
enableAI: z.boolean().optional(),
|
||||
enable: z.boolean().optional(),
|
||||
framework: z.instanceof(FrameworkProvider).optional(),
|
||||
});
|
||||
|
||||
export class AffineCommonViewExtension extends ViewExtensionProvider<
|
||||
z.infer<typeof optionsSchema>
|
||||
> {
|
||||
override name = 'affine-view-extensions';
|
||||
type AIViewOptions = z.infer<typeof optionsSchema>;
|
||||
|
||||
export class AIViewExtension extends ViewExtensionProvider<AIViewOptions> {
|
||||
override name = 'affine-ai-view-extension';
|
||||
|
||||
override schema = optionsSchema;
|
||||
|
||||
private _setupAI(
|
||||
context: ViewExtensionContext,
|
||||
framework: FrameworkProvider
|
||||
) {
|
||||
context.register(AIChatBlockSpec);
|
||||
context.register(AITranscriptionBlockSpec);
|
||||
context.register([
|
||||
AICodeBlockWatcher,
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier('custom:affine:image'),
|
||||
config: imageToolbarAIEntryConfig(),
|
||||
}),
|
||||
]);
|
||||
override setup(context: ViewExtensionContext, options?: AIViewOptions) {
|
||||
super.setup(context, options);
|
||||
if (!options?.enable) return;
|
||||
const framework = options.framework;
|
||||
if (!framework) return;
|
||||
|
||||
context
|
||||
.register(AIChatBlockSpec)
|
||||
.register(AITranscriptionBlockSpec)
|
||||
.register(EdgelessClipboardAIChatConfig)
|
||||
.register(AICodeBlockWatcher)
|
||||
.register(
|
||||
ToolbarModuleExtension({
|
||||
id: BlockFlavourIdentifier('custom:affine:image'),
|
||||
config: imageToolbarAIEntryConfig(),
|
||||
})
|
||||
);
|
||||
if (context.scope === 'edgeless' || context.scope === 'page') {
|
||||
context.register([
|
||||
aiPanelWidget,
|
||||
@@ -76,26 +76,4 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
|
||||
context.register(getAIPageRootWatcher(framework));
|
||||
}
|
||||
}
|
||||
|
||||
override setup(
|
||||
context: ViewExtensionContext,
|
||||
options?: z.infer<typeof optionsSchema>
|
||||
) {
|
||||
super.setup(context, options);
|
||||
const { framework, enableAI } = options || {};
|
||||
if (framework) {
|
||||
context.register(patchPeekViewService(framework.get(PeekViewService)));
|
||||
context.register([
|
||||
getFontConfigExtension(),
|
||||
buildDocDisplayMetaExtension(framework),
|
||||
]);
|
||||
context.register(getTelemetryExtension());
|
||||
if (context.scope === 'edgeless' || context.scope === 'page') {
|
||||
context.register(patchFileSizeLimitExtension(framework));
|
||||
}
|
||||
if (enableAI) {
|
||||
this._setupAI(context, framework);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@ import type { ExtensionType } from '@blocksuite/store';
|
||||
export function patchForAudioEmbedView(reactToLit: ReactToLit): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
// do not show audio block on mobile
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
return;
|
||||
}
|
||||
di.override(AttachmentEmbedConfigIdentifier('audio'), () => ({
|
||||
name: 'audio',
|
||||
check: (model, maxFileSize) =>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import { DatabaseBlockDataSource } from '@blocksuite/affine/blocks/database';
|
||||
import {
|
||||
DatabaseBlockDataSource,
|
||||
ExternalGroupByConfigProvider,
|
||||
} from '@blocksuite/affine/blocks/database';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
|
||||
import { groupByConfigList } from '../database-block/group-by';
|
||||
import { propertiesPresets } from '../database-block/properties';
|
||||
|
||||
export function patchDatabaseBlockConfigService(): ExtensionType {
|
||||
//TODO use service
|
||||
DatabaseBlockDataSource.externalProperties.value = propertiesPresets;
|
||||
return {
|
||||
setup: () => {},
|
||||
setup: di => {
|
||||
groupByConfigList.forEach(config => {
|
||||
di.addValue(ExternalGroupByConfigProvider(config.name), config);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine/ext-loader';
|
||||
import { FrameworkProvider } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { patchForClipboardInElectron } from './electron-clipboard';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
framework: z.instanceof(FrameworkProvider).optional(),
|
||||
});
|
||||
|
||||
type ElectronViewExtensionOptions = z.infer<typeof optionsSchema>;
|
||||
|
||||
export class ElectronViewExtension extends ViewExtensionProvider<ElectronViewExtensionOptions> {
|
||||
override name = 'electron-view-extensions';
|
||||
|
||||
override schema = optionsSchema;
|
||||
|
||||
override setup(
|
||||
context: ViewExtensionContext,
|
||||
options?: ElectronViewExtensionOptions
|
||||
) {
|
||||
super.setup(context, options);
|
||||
if (!BUILD_CONFIG.isElectron) return;
|
||||
|
||||
const framework = options?.framework;
|
||||
if (!framework) return;
|
||||
|
||||
context.register(patchForClipboardInElectron(framework));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import {
|
||||
AffineCanvasTextFonts,
|
||||
FontConfigExtension,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
|
||||
export function getFontConfigExtension() {
|
||||
return FontConfigExtension(
|
||||
AffineCanvasTextFonts.map(font => ({
|
||||
...font,
|
||||
url: environment.publicPath + 'fonts/' + font.url.split('/').pop(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine/ext-loader';
|
||||
import { FrameworkProvider } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { patchLinkPreviewService } from './link-preview-service';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
framework: z.instanceof(FrameworkProvider).optional(),
|
||||
});
|
||||
|
||||
type AffineLinkPreviewViewOptions = z.infer<typeof optionsSchema>;
|
||||
|
||||
export class AffineLinkPreviewExtension extends ViewExtensionProvider<AffineLinkPreviewViewOptions> {
|
||||
override name = 'affine-link-preview-extension';
|
||||
|
||||
override schema = optionsSchema;
|
||||
|
||||
override setup(
|
||||
context: ViewExtensionContext,
|
||||
options?: AffineLinkPreviewViewOptions
|
||||
) {
|
||||
super.setup(context, options);
|
||||
if (!options?.framework) {
|
||||
return;
|
||||
}
|
||||
const { framework } = options;
|
||||
context.register(patchLinkPreviewService(framework));
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { DEFAULT_LINK_PREVIEW_ENDPOINT } from '@blocksuite/affine/shared/consts';
|
||||
import {
|
||||
LinkPreviewCacheIdentifier,
|
||||
type LinkPreviewCacheProvider,
|
||||
LinkPreviewService,
|
||||
LinkPreviewServiceIdentifier,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import { type BlockStdScope, StdIdentifier } from '@blocksuite/affine/std';
|
||||
import { type ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { ServerService } from '../../../modules/cloud/services/server';
|
||||
|
||||
class AffineLinkPreviewService extends LinkPreviewService {
|
||||
constructor(
|
||||
endpoint: string,
|
||||
std: BlockStdScope,
|
||||
cache: LinkPreviewCacheProvider
|
||||
) {
|
||||
super(std, cache);
|
||||
this.setEndpoint(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the link preview service, set the endpoint and cache
|
||||
* @param framework
|
||||
* @returns
|
||||
*/
|
||||
export function patchLinkPreviewService(
|
||||
framework: FrameworkProvider
|
||||
): ExtensionType {
|
||||
// get link preview service endpoint from server and BUILD_CONFIG
|
||||
let linkPreviewUrl: string;
|
||||
try {
|
||||
const server = framework.get(ServerService).server;
|
||||
linkPreviewUrl = new URL(
|
||||
BUILD_CONFIG.linkPreviewUrl || '/',
|
||||
server.baseUrl
|
||||
).toString();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
'Invalid BUILD_CONFIG.linkPreviewUrl, falling back to default',
|
||||
err
|
||||
);
|
||||
linkPreviewUrl = DEFAULT_LINK_PREVIEW_ENDPOINT;
|
||||
}
|
||||
|
||||
return {
|
||||
setup: (di: Container) => {
|
||||
di.override(LinkPreviewServiceIdentifier, provider => {
|
||||
return new AffineLinkPreviewService(
|
||||
linkPreviewUrl,
|
||||
provider.get(StdIdentifier),
|
||||
provider.get(LinkPreviewCacheIdentifier)
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { KeyboardToolbarExtension } from '@affine/core/blocksuite/extensions/mobile/keyboard-toolbar-extension';
|
||||
import { MobileFeatureFlagControl } from '@affine/core/blocksuite/extensions/mobile/mobile-feature-flag-control';
|
||||
import {
|
||||
type ViewExtensionContext,
|
||||
ViewExtensionProvider,
|
||||
} from '@blocksuite/affine/ext-loader';
|
||||
import { FrameworkProvider } from '@toeverything/infra';
|
||||
import { z } from 'zod';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
framework: z.instanceof(FrameworkProvider).optional(),
|
||||
});
|
||||
|
||||
type MobileViewOptions = z.infer<typeof optionsSchema>;
|
||||
|
||||
export class MobileViewExtension extends ViewExtensionProvider<MobileViewOptions> {
|
||||
override name = 'mobile-view-extension';
|
||||
|
||||
override schema = optionsSchema;
|
||||
|
||||
override setup(context: ViewExtensionContext, options?: MobileViewOptions) {
|
||||
super.setup(context, options);
|
||||
const isMobile = BUILD_CONFIG.isMobileEdition;
|
||||
if (!isMobile) return;
|
||||
|
||||
const framework = options?.framework;
|
||||
if (framework) {
|
||||
context.register(KeyboardToolbarExtension(framework));
|
||||
}
|
||||
|
||||
context.register(MobileFeatureFlagControl);
|
||||
}
|
||||
}
|
||||
+3
-36
@@ -1,50 +1,15 @@
|
||||
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||
import { CodeBlockConfigExtension } from '@blocksuite/affine/blocks/code';
|
||||
import { ParagraphBlockConfigExtension } from '@blocksuite/affine/blocks/paragraph';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import { DisposableGroup } from '@blocksuite/affine/global/disposable';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
VirtualKeyboardProvider as BSVirtualKeyboardProvider,
|
||||
type VirtualKeyboardProviderWithAction,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import { type BlockStdScope, LifeCycleWatcher } from '@blocksuite/affine/std';
|
||||
import { LifeCycleWatcher } from '@blocksuite/affine/std';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import { batch, signal } from '@preact/signals-core';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
export class MobileSpecsPatches extends LifeCycleWatcher {
|
||||
static override key = 'mobile-patches';
|
||||
|
||||
constructor(std: BlockStdScope) {
|
||||
super(std);
|
||||
const featureFlagService = std.get(FeatureFlagService);
|
||||
|
||||
featureFlagService.setFlag('enable_mobile_keyboard_toolbar', true);
|
||||
featureFlagService.setFlag('enable_mobile_linked_doc_menu', true);
|
||||
}
|
||||
}
|
||||
|
||||
export const mobileParagraphConfig = ParagraphBlockConfigExtension({
|
||||
getPlaceholder: model => {
|
||||
const placeholders = {
|
||||
text: '',
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
quote: '',
|
||||
};
|
||||
return placeholders[model.props.type];
|
||||
},
|
||||
});
|
||||
|
||||
export const mobileCodeConfig = CodeBlockConfigExtension({
|
||||
showLineNumbers: false,
|
||||
});
|
||||
|
||||
export function KeyboardToolbarExtension(
|
||||
framework: FrameworkProvider
|
||||
): ExtensionType {
|
||||
@@ -89,6 +54,7 @@ export function KeyboardToolbarExtension(
|
||||
|
||||
if ('show' in affineVirtualKeyboardProvider) {
|
||||
const providerWithAction = affineVirtualKeyboardProvider;
|
||||
|
||||
class BSVirtualKeyboardServiceWithShowAndHide
|
||||
extends BSVirtualKeyboardService
|
||||
implements VirtualKeyboardProviderWithAction
|
||||
@@ -96,6 +62,7 @@ export function KeyboardToolbarExtension(
|
||||
show() {
|
||||
providerWithAction.show();
|
||||
}
|
||||
|
||||
hide() {
|
||||
providerWithAction.hide();
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { FeatureFlagService } from '@blocksuite/affine/shared/services';
|
||||
import { type BlockStdScope, LifeCycleWatcher } from '@blocksuite/affine/std';
|
||||
|
||||
export class MobileFeatureFlagControl extends LifeCycleWatcher {
|
||||
static override key = 'mobile-patches';
|
||||
|
||||
constructor(std: BlockStdScope) {
|
||||
super(std);
|
||||
const featureFlagService = std.get(FeatureFlagService);
|
||||
|
||||
featureFlagService.setFlag('enable_mobile_keyboard_toolbar', true);
|
||||
featureFlagService.setFlag('enable_mobile_linked_doc_menu', true);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type {
|
||||
PeekOptions,
|
||||
PeekViewService as BSPeekViewService,
|
||||
} from '@blocksuite/affine/components/peek';
|
||||
import { PeekViewExtension } from '@blocksuite/affine/components/peek';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
const logger = new DebugLogger('affine::patch-peek-view-service');
|
||||
|
||||
export function patchPeekViewService(service: PeekViewService) {
|
||||
return PeekViewExtension({
|
||||
peek: (
|
||||
element: {
|
||||
target: HTMLElement;
|
||||
docId: string;
|
||||
blockIds?: string[];
|
||||
template?: TemplateResult;
|
||||
},
|
||||
options?: PeekOptions
|
||||
) => {
|
||||
logger.debug('center peek', element);
|
||||
const { template, target, ...props } = element;
|
||||
|
||||
return service.peekView.open(
|
||||
{
|
||||
element: target,
|
||||
docRef: props,
|
||||
},
|
||||
template,
|
||||
options?.abortSignal
|
||||
);
|
||||
},
|
||||
} satisfies BSPeekViewService);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { mixpanel } from '@affine/track';
|
||||
import {
|
||||
type TelemetryEventMap,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine/shared/services';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
|
||||
export function getTelemetryExtension(): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.addImpl(TelemetryProvider, () => ({
|
||||
track: <T extends keyof TelemetryEventMap>(
|
||||
eventName: T,
|
||||
props: TelemetryEventMap[T]
|
||||
) => {
|
||||
mixpanel.track(eventName as string, props as Record<string, unknown>);
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ConfirmModalProps, ElementOrFactory } from '@affine/component';
|
||||
import { patchForAudioEmbedView } from '@affine/core/blocksuite/extensions/audio/audio-view';
|
||||
import { patchDatabaseBlockConfigService } from '@affine/core/blocksuite/extensions/database-block-config-service';
|
||||
import { buildDocDisplayMetaExtension } from '@affine/core/blocksuite/extensions/display-meta';
|
||||
import { patchDocModeService } from '@affine/core/blocksuite/extensions/doc-mode-service';
|
||||
import { patchDocUrlExtensions } from '@affine/core/blocksuite/extensions/doc-url';
|
||||
import { EdgelessClipboardAIChatConfig } from '@affine/core/blocksuite/extensions/edgeless-clipboard';
|
||||
import { patchForClipboardInElectron } from '@affine/core/blocksuite/extensions/electron-clipboard';
|
||||
import { patchFileSizeLimitExtension } from '@affine/core/blocksuite/extensions/file-size-limit';
|
||||
import { patchNotificationService } from '@affine/core/blocksuite/extensions/notification-service';
|
||||
import { patchOpenDocExtension } from '@affine/core/blocksuite/extensions/open-doc';
|
||||
import { patchQuickSearchService } from '@affine/core/blocksuite/extensions/quick-search-service';
|
||||
@@ -29,13 +29,6 @@ import { FrameworkProvider } from '@toeverything/infra';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
KeyboardToolbarExtension,
|
||||
mobileCodeConfig,
|
||||
mobileParagraphConfig,
|
||||
MobileSpecsPatches,
|
||||
} from '../extensions/mobile-config';
|
||||
|
||||
const optionsSchema = z.object({
|
||||
// services
|
||||
framework: z.instanceof(FrameworkProvider),
|
||||
@@ -106,8 +99,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
|
||||
reactToLit,
|
||||
confirmModal,
|
||||
} = options;
|
||||
const isMobileEdition = BUILD_CONFIG.isMobileEdition;
|
||||
const isElectron = BUILD_CONFIG.isElectron;
|
||||
|
||||
const docService = framework.get(DocService);
|
||||
const docsService = framework.get(DocsService);
|
||||
@@ -115,30 +106,21 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
|
||||
|
||||
const referenceRenderer = this._getCustomReferenceRenderer(framework);
|
||||
|
||||
context.register([
|
||||
patchReferenceRenderer(reactToLit, referenceRenderer),
|
||||
patchNotificationService(confirmModal),
|
||||
patchOpenDocExtension(),
|
||||
EdgelessClipboardAIChatConfig,
|
||||
patchSideBarService(framework),
|
||||
patchDocModeService(docService, docsService, editorService),
|
||||
]);
|
||||
context.register(patchDocUrlExtensions(framework));
|
||||
context.register(patchQuickSearchService(framework));
|
||||
context.register([
|
||||
patchDatabaseBlockConfigService(),
|
||||
patchForAudioEmbedView(reactToLit),
|
||||
]);
|
||||
if (isMobileEdition) {
|
||||
context.register([
|
||||
KeyboardToolbarExtension(framework),
|
||||
MobileSpecsPatches,
|
||||
mobileParagraphConfig,
|
||||
mobileCodeConfig,
|
||||
context
|
||||
.register([
|
||||
patchReferenceRenderer(reactToLit, referenceRenderer),
|
||||
patchNotificationService(confirmModal),
|
||||
patchOpenDocExtension(),
|
||||
patchSideBarService(framework),
|
||||
patchDocModeService(docService, docsService, editorService),
|
||||
patchFileSizeLimitExtension(framework),
|
||||
buildDocDisplayMetaExtension(framework),
|
||||
])
|
||||
.register(patchDocUrlExtensions(framework))
|
||||
.register(patchQuickSearchService(framework))
|
||||
.register([
|
||||
patchDatabaseBlockConfigService(),
|
||||
patchForAudioEmbedView(reactToLit),
|
||||
]);
|
||||
}
|
||||
if (isElectron) {
|
||||
context.register(patchForClipboardInElectron(framework));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ class MigratingAffineStoreExtension extends StoreExtensionProvider {
|
||||
|
||||
interface Configure {
|
||||
init: () => Configure;
|
||||
|
||||
featureFlag: (featureFlagService?: FeatureFlagService) => Configure;
|
||||
|
||||
value: StoreExtensionManager;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ReactToLit } from '@affine/component';
|
||||
import { AIViewExtension } from '@affine/core/blocksuite/extensions/ai';
|
||||
import { CloudViewExtension } from '@affine/core/blocksuite/extensions/cloud';
|
||||
import {
|
||||
EdgelessBlockHeaderConfigViewExtension,
|
||||
@@ -7,23 +8,59 @@ import {
|
||||
import { AffineEditorConfigViewExtension } from '@affine/core/blocksuite/extensions/editor-config';
|
||||
import { createDatabaseOptionsConfig } from '@affine/core/blocksuite/extensions/editor-config/database';
|
||||
import { createLinkedWidgetConfig } from '@affine/core/blocksuite/extensions/editor-config/linked';
|
||||
import { ElectronViewExtension } from '@affine/core/blocksuite/extensions/electron';
|
||||
import { AffineLinkPreviewExtension } from '@affine/core/blocksuite/extensions/link-preview-service';
|
||||
import { MobileViewExtension } from '@affine/core/blocksuite/extensions/mobile';
|
||||
import { PdfViewExtension } from '@affine/core/blocksuite/extensions/pdf';
|
||||
import { AffineThemeViewExtension } from '@affine/core/blocksuite/extensions/theme';
|
||||
import { TurboRendererViewExtension } from '@affine/core/blocksuite/extensions/turbo-renderer';
|
||||
import { AffineCommonViewExtension } from '@affine/core/blocksuite/manager/common-view';
|
||||
import {
|
||||
AffineEditorViewExtension,
|
||||
type AffineEditorViewOptions,
|
||||
} from '@affine/core/blocksuite/manager/editor-view';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { mixpanel } from '@affine/track';
|
||||
import { DatabaseViewExtension } from '@blocksuite/affine/blocks/database/view';
|
||||
import { ParagraphViewExtension } from '@blocksuite/affine/blocks/paragraph/view';
|
||||
import type {
|
||||
PeekOptions,
|
||||
PeekViewService as BSPeekViewService,
|
||||
} from '@blocksuite/affine/components/peek';
|
||||
import { ViewExtensionManager } from '@blocksuite/affine/ext-loader';
|
||||
import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view';
|
||||
import { FoundationViewExtension } from '@blocksuite/affine/foundation/view';
|
||||
import { AffineCanvasTextFonts } from '@blocksuite/affine/shared/services';
|
||||
import { LinkedDocViewExtension } from '@blocksuite/affine/widgets/linked-doc/view';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import type { TemplateResult } from 'lit';
|
||||
|
||||
import { CodeBlockPreviewViewExtension } from './code-block-preview';
|
||||
|
||||
type Configure = {
|
||||
init: () => Configure;
|
||||
|
||||
foundation: (framework?: FrameworkProvider) => Configure;
|
||||
editorView: (options?: AffineEditorViewOptions) => Configure;
|
||||
theme: (framework?: FrameworkProvider) => Configure;
|
||||
editorConfig: (framework?: FrameworkProvider) => Configure;
|
||||
edgelessBlockHeader: (options?: EdgelessBlockHeaderViewOptions) => Configure;
|
||||
database: (framework?: FrameworkProvider) => Configure;
|
||||
linkedDoc: (framework?: FrameworkProvider) => Configure;
|
||||
paragraph: (enableAI?: boolean) => Configure;
|
||||
cloud: (framework?: FrameworkProvider, enableCloud?: boolean) => Configure;
|
||||
turboRenderer: (enableTurboRenderer?: boolean) => Configure;
|
||||
pdf: (enablePDFEmbedPreview?: boolean, reactToLit?: ReactToLit) => Configure;
|
||||
mobile: (framework?: FrameworkProvider) => Configure;
|
||||
ai: (enable?: boolean, framework?: FrameworkProvider) => Configure;
|
||||
electron: (framework?: FrameworkProvider) => Configure;
|
||||
linkPreview: (framework?: FrameworkProvider) => Configure;
|
||||
|
||||
value: ViewExtensionManager;
|
||||
};
|
||||
|
||||
const peekViewLogger = new DebugLogger('affine::patch-peek-view-service');
|
||||
|
||||
class ViewProvider {
|
||||
static instance: ViewProvider | null = null;
|
||||
static getInstance() {
|
||||
@@ -40,7 +77,6 @@ class ViewProvider {
|
||||
...getInternalViewExtensions(),
|
||||
|
||||
AffineThemeViewExtension,
|
||||
AffineCommonViewExtension,
|
||||
AffineEditorViewExtension,
|
||||
AffineEditorConfigViewExtension,
|
||||
CodeBlockPreviewViewExtension,
|
||||
@@ -48,6 +84,10 @@ class ViewProvider {
|
||||
TurboRendererViewExtension,
|
||||
CloudViewExtension,
|
||||
PdfViewExtension,
|
||||
MobileViewExtension,
|
||||
AIViewExtension,
|
||||
ElectronViewExtension,
|
||||
AffineLinkPreviewExtension,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -55,10 +95,10 @@ class ViewProvider {
|
||||
return this._manager;
|
||||
}
|
||||
|
||||
get config() {
|
||||
get config(): Configure {
|
||||
return {
|
||||
init: this._initDefaultConfig,
|
||||
common: this._configureCommon,
|
||||
foundation: this._configureFoundation,
|
||||
editorView: this._configureEditorView,
|
||||
theme: this._configureTheme,
|
||||
editorConfig: this._configureEditorConfig,
|
||||
@@ -69,13 +109,17 @@ class ViewProvider {
|
||||
cloud: this._configureCloud,
|
||||
turboRenderer: this._configureTurboRenderer,
|
||||
pdf: this._configurePdf,
|
||||
mobile: this._configureMobile,
|
||||
ai: this._configureAI,
|
||||
electron: this._configureElectron,
|
||||
linkPreview: this._configureLinkPreview,
|
||||
value: this._manager,
|
||||
};
|
||||
}
|
||||
|
||||
private readonly _initDefaultConfig = () => {
|
||||
this.config
|
||||
.common()
|
||||
.foundation()
|
||||
.theme()
|
||||
.editorView()
|
||||
.editorConfig()
|
||||
@@ -85,19 +129,55 @@ class ViewProvider {
|
||||
.paragraph()
|
||||
.cloud()
|
||||
.turboRenderer()
|
||||
.pdf();
|
||||
.pdf()
|
||||
.mobile()
|
||||
.ai()
|
||||
.electron()
|
||||
.linkPreview();
|
||||
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureCommon = (
|
||||
framework?: FrameworkProvider,
|
||||
enableAI?: boolean
|
||||
) => {
|
||||
this._manager.configure(AffineCommonViewExtension, {
|
||||
framework,
|
||||
enableAI,
|
||||
private readonly _configureFoundation = (framework?: FrameworkProvider) => {
|
||||
const peekViewService = framework?.get(PeekViewService);
|
||||
|
||||
this._manager.configure(FoundationViewExtension, {
|
||||
telemetry: {
|
||||
track: (eventName, props) => {
|
||||
mixpanel.track(eventName, props);
|
||||
},
|
||||
},
|
||||
fontConfig: AffineCanvasTextFonts.map(font => ({
|
||||
...font,
|
||||
url: environment.publicPath + 'fonts/' + font.url.split('/').pop(),
|
||||
})),
|
||||
peekView: !peekViewService
|
||||
? undefined
|
||||
: ({
|
||||
peek: (
|
||||
element: {
|
||||
target: HTMLElement;
|
||||
docId: string;
|
||||
blockIds?: string[];
|
||||
template?: TemplateResult;
|
||||
},
|
||||
options?: PeekOptions
|
||||
) => {
|
||||
peekViewLogger.debug('center peek', element);
|
||||
const { template, target, ...props } = element;
|
||||
|
||||
return peekViewService.peekView.open(
|
||||
{
|
||||
element: target,
|
||||
docRef: props,
|
||||
},
|
||||
template,
|
||||
options?.abortSignal
|
||||
);
|
||||
},
|
||||
} satisfies BSPeekViewService),
|
||||
});
|
||||
|
||||
return this.config;
|
||||
};
|
||||
|
||||
@@ -146,7 +226,23 @@ class ViewProvider {
|
||||
};
|
||||
|
||||
private readonly _configureParagraph = (enableAI?: boolean) => {
|
||||
if (enableAI) {
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
this._manager.configure(ParagraphViewExtension, {
|
||||
getPlaceholder: model => {
|
||||
const placeholders = {
|
||||
text: '',
|
||||
h1: 'Heading 1',
|
||||
h2: 'Heading 2',
|
||||
h3: 'Heading 3',
|
||||
h4: 'Heading 4',
|
||||
h5: 'Heading 5',
|
||||
h6: 'Heading 6',
|
||||
quote: '',
|
||||
};
|
||||
return placeholders[model.props.type] ?? '';
|
||||
},
|
||||
});
|
||||
} else if (enableAI) {
|
||||
this._manager.configure(ParagraphViewExtension, {
|
||||
getPlaceholder: model => {
|
||||
const placeholders = {
|
||||
@@ -193,6 +289,29 @@ class ViewProvider {
|
||||
});
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureMobile = (framework?: FrameworkProvider) => {
|
||||
this._manager.configure(MobileViewExtension, { framework });
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureAI = (
|
||||
enable?: boolean,
|
||||
framework?: FrameworkProvider
|
||||
) => {
|
||||
this._manager.configure(AIViewExtension, { framework, enable });
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureElectron = (framework?: FrameworkProvider) => {
|
||||
this._manager.configure(ElectronViewExtension, { framework });
|
||||
return this.config;
|
||||
};
|
||||
|
||||
private readonly _configureLinkPreview = (framework?: FrameworkProvider) => {
|
||||
this._manager.configure(AffineLinkPreviewExtension, { framework });
|
||||
return this.config;
|
||||
};
|
||||
}
|
||||
|
||||
export function getViewManager() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { WorkspaceImpl } from '@affine/core/modules/workspace/impls/workspace';
|
||||
import type { ServiceProvider } from '@blocksuite/affine/global/di';
|
||||
import {
|
||||
@@ -161,11 +162,13 @@ export async function markDownToDoc(
|
||||
provider: ServiceProvider,
|
||||
schema: Schema,
|
||||
answer: string,
|
||||
middlewares?: TransformerMiddleware[]
|
||||
middlewares?: TransformerMiddleware[],
|
||||
affineFeatureFlagService?: FeatureFlagService
|
||||
) {
|
||||
// Should not create a new doc in the original collection
|
||||
const collection = new WorkspaceImpl({
|
||||
rootDoc: new YDoc({ guid: 'markdownToDoc' }),
|
||||
featureFlagService: affineFeatureFlagService,
|
||||
});
|
||||
collection.meta.initialize();
|
||||
const transformer = new Transformer({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Collection } from '@affine/core/modules/collection';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { AllDocsIcon, FilterIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
|
||||
@@ -5,10 +5,8 @@ import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ViewLayersIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { createEmptyCollection } from '../../page-list';
|
||||
import { ActionButton } from './action-button';
|
||||
import collectionListDark from './assets/collection-list.dark.png';
|
||||
import collectionListLight from './assets/collection-list.light.png';
|
||||
@@ -39,8 +37,7 @@ export const EmptyCollections = (props: UniversalEmptyProps) => {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
const id = nanoid();
|
||||
collectionService.addCollection(createEmptyCollection(id, { name }));
|
||||
const id = collectionService.createCollection({ name });
|
||||
navigateHelper.jumpToCollection(currentWorkspace.id, id);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IconButton, Menu, MenuItem, MenuSeparator } from '@affine/component';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||
import { WorkspacePropertyService } from '@affine/core/modules/workspace-property';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { FavoriteIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import { WorkspacePropertyIcon, WorkspacePropertyName } from '../properties';
|
||||
@@ -24,6 +24,36 @@ export const AddFilterMenu = ({
|
||||
{t['com.affine.filter']()}
|
||||
</div>
|
||||
<MenuSeparator />
|
||||
<MenuItem
|
||||
prefixIcon={<FavoriteIcon className={styles.filterTypeItemIcon} />}
|
||||
key={'favorite'}
|
||||
onClick={() => {
|
||||
onAdd({
|
||||
type: 'system',
|
||||
key: 'favorite',
|
||||
method: 'is',
|
||||
value: 'true',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className={styles.filterTypeItemName}>{t['Favorited']()}</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
prefixIcon={<FavoriteIcon className={styles.filterTypeItemIcon} />}
|
||||
key={'shared'}
|
||||
onClick={() => {
|
||||
onAdd({
|
||||
type: 'system',
|
||||
key: 'shared',
|
||||
method: 'is',
|
||||
value: 'true',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span className={styles.filterTypeItemName}>
|
||||
{t['com.affine.filter.is-public']()}
|
||||
</span>
|
||||
</MenuItem>
|
||||
{workspaceProperties.map(property => {
|
||||
const type = WorkspacePropertyTypes[property.type];
|
||||
const defaultFilter = type?.defaultFilter;
|
||||
|
||||
@@ -81,13 +81,13 @@ export function useAIChatConfig() {
|
||||
return tag$.value?.pageIds$.value ?? [];
|
||||
},
|
||||
getCollections: () => {
|
||||
const collections$ = collectionService.collections$;
|
||||
return createSignalFromObservable(collections$, []);
|
||||
const collectionMetas$ = collectionService.collectionMetas$;
|
||||
return createSignalFromObservable(collectionMetas$, []);
|
||||
},
|
||||
getCollectionPageIds: (collectionId: string) => {
|
||||
const collection$ = collectionService.collection$(collectionId);
|
||||
// TODO: lack of documents that meet the collection rules
|
||||
return collection$?.value?.allowList ?? [];
|
||||
return collection$?.value?.info$.value.allowList ?? [];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import type { DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useDeleteCollectionInfo = () => {
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const user = useLiveData(authService.session.account$);
|
||||
|
||||
return useMemo<DeleteCollectionInfo | null>(
|
||||
() => (user ? { userName: user.label, userId: user.id } : null),
|
||||
[user]
|
||||
);
|
||||
};
|
||||
@@ -1,201 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import type {
|
||||
Filter,
|
||||
LiteralValue,
|
||||
PropertiesMeta,
|
||||
Ref,
|
||||
VariableMap,
|
||||
} from '@affine/env/filter';
|
||||
import { getOrCreateI18n, I18nextProvider } from '@affine/i18n';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { Condition } from '../filter/condition';
|
||||
import { tBoolean, tDate } from '../filter/logical/custom-type';
|
||||
import { toLiteral } from '../filter/shared-types';
|
||||
import type { FilterMatcherDataType } from '../filter/vars';
|
||||
import { filterMatcher } from '../filter/vars';
|
||||
import { filterByFilterList } from '../use-collection-manager';
|
||||
const ref = (name: keyof VariableMap): Ref => {
|
||||
return {
|
||||
type: 'ref',
|
||||
name,
|
||||
};
|
||||
};
|
||||
const mockVariableMap = (vars: Partial<VariableMap>): VariableMap => {
|
||||
return {
|
||||
Created: 0,
|
||||
Updated: 0,
|
||||
'Is Favourited': false,
|
||||
'Is Public': false,
|
||||
Tags: [],
|
||||
...vars,
|
||||
};
|
||||
};
|
||||
const mockPropertiesMeta = (meta: Partial<PropertiesMeta>): PropertiesMeta => {
|
||||
return {
|
||||
tags: {
|
||||
options: [],
|
||||
},
|
||||
...meta,
|
||||
};
|
||||
};
|
||||
const filter = (
|
||||
matcherData: FilterMatcherDataType,
|
||||
left: Ref,
|
||||
args: LiteralValue[]
|
||||
): Filter => {
|
||||
return {
|
||||
type: 'filter',
|
||||
left,
|
||||
funcName: matcherData.name,
|
||||
args: args.map(toLiteral),
|
||||
};
|
||||
};
|
||||
describe('match filter', () => {
|
||||
test('boolean variable will match `is` filter', () => {
|
||||
const is = filterMatcher
|
||||
.allMatchedData(tBoolean.create())
|
||||
.find(v => v.name === 'is');
|
||||
expect(is?.name).toBe('is');
|
||||
});
|
||||
test('Date variable will match `before` filter', () => {
|
||||
const before = filterMatcher
|
||||
.allMatchedData(tDate.create())
|
||||
.find(v => v.name === 'before');
|
||||
expect(before?.name).toBe('before');
|
||||
});
|
||||
});
|
||||
|
||||
describe('eval filter', () => {
|
||||
test('before', async () => {
|
||||
const before = filterMatcher.findData(v => v.name === 'before');
|
||||
if (!before) {
|
||||
throw new Error('before is not found');
|
||||
}
|
||||
const filter1 = filter(before, ref('Created'), [
|
||||
new Date(2023, 5, 28).getTime(),
|
||||
]);
|
||||
const filter2 = filter(before, ref('Created'), [
|
||||
new Date(2023, 5, 30).getTime(),
|
||||
]);
|
||||
const filter3 = filter(before, ref('Created'), [
|
||||
new Date(2023, 5, 29).getTime(),
|
||||
]);
|
||||
const varMap = mockVariableMap({
|
||||
Created: new Date(2023, 5, 29).getTime(),
|
||||
});
|
||||
expect(filterByFilterList([filter1], varMap)).toBe(false);
|
||||
expect(filterByFilterList([filter2], varMap)).toBe(true);
|
||||
expect(filterByFilterList([filter3], varMap)).toBe(false);
|
||||
});
|
||||
test('after', async () => {
|
||||
const after = filterMatcher.findData(v => v.name === 'after');
|
||||
if (!after) {
|
||||
throw new Error('after is not found');
|
||||
}
|
||||
const filter1 = filter(after, ref('Created'), [
|
||||
new Date(2023, 5, 28).getTime(),
|
||||
]);
|
||||
const filter2 = filter(after, ref('Created'), [
|
||||
new Date(2023, 5, 30).getTime(),
|
||||
]);
|
||||
const filter3 = filter(after, ref('Created'), [
|
||||
new Date(2023, 5, 29).getTime(),
|
||||
]);
|
||||
const varMap = mockVariableMap({
|
||||
Created: new Date(2023, 5, 29).getTime(),
|
||||
});
|
||||
expect(filterByFilterList([filter1], varMap)).toBe(true);
|
||||
expect(filterByFilterList([filter2], varMap)).toBe(false);
|
||||
expect(filterByFilterList([filter3], varMap)).toBe(false);
|
||||
});
|
||||
test('is', async () => {
|
||||
const is = filterMatcher.findData(v => v.name === 'is');
|
||||
if (!is) {
|
||||
throw new Error('is is not found');
|
||||
}
|
||||
const filter1 = filter(is, ref('Is Favourited'), [false]);
|
||||
const filter2 = filter(is, ref('Is Favourited'), [true]);
|
||||
const varMap = mockVariableMap({
|
||||
'Is Favourited': true,
|
||||
});
|
||||
expect(filterByFilterList([filter1], varMap)).toBe(false);
|
||||
expect(filterByFilterList([filter2], varMap)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('render filter', () => {
|
||||
test('boolean condition value change', async () => {
|
||||
const is = filterMatcher.match(tBoolean.create());
|
||||
const i18n = getOrCreateI18n();
|
||||
if (!is) {
|
||||
throw new Error('is is not found');
|
||||
}
|
||||
const Wrapper = () => {
|
||||
const [value, onChange] = useState(
|
||||
filter(is, ref('Is Favourited'), [true])
|
||||
);
|
||||
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Condition
|
||||
propertiesMeta={mockPropertiesMeta({})}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</I18nextProvider>
|
||||
);
|
||||
};
|
||||
const result = render(<Wrapper />);
|
||||
const dom = await result.findByText('true');
|
||||
dom.click();
|
||||
await result.findByText('false');
|
||||
result.unmount();
|
||||
});
|
||||
|
||||
const WrapperCreator = (fn: FilterMatcherDataType) =>
|
||||
function Wrapper(): ReactElement {
|
||||
const [value, onChange] = useState(
|
||||
filter(fn, ref('Created'), [new Date(2023, 5, 29).getTime()])
|
||||
);
|
||||
return (
|
||||
<Condition
|
||||
propertiesMeta={mockPropertiesMeta({})}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
test('date condition function change', async () => {
|
||||
const dateFunction = filterMatcher.match(tDate.create());
|
||||
if (!dateFunction) {
|
||||
throw new Error('dateFunction is not found');
|
||||
}
|
||||
const Wrapper = WrapperCreator(dateFunction);
|
||||
const result = render(<Wrapper />);
|
||||
const dom = await result.findByTestId('filter-name');
|
||||
dom.click();
|
||||
await result.findByTestId('filter-name');
|
||||
result.unmount();
|
||||
});
|
||||
test('date condition variable change', async () => {
|
||||
const dateFunction = filterMatcher.match(tDate.create());
|
||||
if (!dateFunction) {
|
||||
throw new Error('dateFunction is not found');
|
||||
}
|
||||
const Wrapper = WrapperCreator(dateFunction);
|
||||
const result = render(<Wrapper />);
|
||||
const dom = await result.findByTestId('variable-name');
|
||||
dom.click();
|
||||
await result.findByTestId('variable-name');
|
||||
result.unmount();
|
||||
});
|
||||
});
|
||||
+19
-49
@@ -1,51 +1,25 @@
|
||||
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { CollectionService } from '../../../modules/collection';
|
||||
import {
|
||||
type CollectionMeta,
|
||||
CollectionService,
|
||||
} from '../../../modules/collection';
|
||||
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
||||
import { collectionHeaderColsDef } from '../header-col-def';
|
||||
import { CollectionOperationCell } from '../operation-cell';
|
||||
import { CollectionListItemRenderer } from '../page-group';
|
||||
import { ListTableHeader } from '../page-header';
|
||||
import type { CollectionMeta, ItemListHandle, ListItem } from '../types';
|
||||
import type { ItemListHandle, ListItem } from '../types';
|
||||
import { VirtualizedList } from '../virtualized-list';
|
||||
import { CollectionListHeader } from './collection-list-header';
|
||||
|
||||
const useCollectionOperationsRenderer = ({
|
||||
info,
|
||||
service,
|
||||
}: {
|
||||
info: DeleteCollectionInfo;
|
||||
service: CollectionService;
|
||||
}) => {
|
||||
const collectionOperationsRenderer = useCallback(
|
||||
(collection: Collection) => {
|
||||
return (
|
||||
<CollectionOperationCell
|
||||
info={info}
|
||||
collection={collection}
|
||||
service={service}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[info, service]
|
||||
);
|
||||
|
||||
return collectionOperationsRenderer;
|
||||
};
|
||||
|
||||
export const VirtualizedCollectionList = ({
|
||||
collections,
|
||||
collectionMetas,
|
||||
setHideHeaderCreateNewCollection,
|
||||
handleCreateCollection,
|
||||
}: {
|
||||
collections: Collection[];
|
||||
collectionMetas: CollectionMeta[];
|
||||
handleCreateCollection: () => void;
|
||||
setHideHeaderCreateNewCollection: (hide: boolean) => void;
|
||||
}) => {
|
||||
@@ -55,30 +29,24 @@ export const VirtualizedCollectionList = ({
|
||||
[]
|
||||
);
|
||||
const collectionService = useService(CollectionService);
|
||||
const collectionMetas = useLiveData(collectionService.collectionMetas$);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const info = useDeleteCollectionInfo();
|
||||
|
||||
const collectionOperations = useCollectionOperationsRenderer({
|
||||
info,
|
||||
service: collectionService,
|
||||
});
|
||||
|
||||
const filteredSelectedCollectionIds = useMemo(() => {
|
||||
const ids = new Set(collections.map(collection => collection.id));
|
||||
const ids = new Set(collectionMetas.map(collection => collection.id));
|
||||
return selectedCollectionIds.filter(id => ids.has(id));
|
||||
}, [collections, selectedCollectionIds]);
|
||||
}, [collectionMetas, selectedCollectionIds]);
|
||||
|
||||
const hideFloatingToolbar = useCallback(() => {
|
||||
listRef.current?.toggleSelectable();
|
||||
}, []);
|
||||
|
||||
const collectionOperationRenderer = useCallback(
|
||||
(item: ListItem) => {
|
||||
const collection = item as CollectionMeta;
|
||||
return collectionOperations(collection);
|
||||
},
|
||||
[collectionOperations]
|
||||
);
|
||||
const collectionOperationRenderer = useCallback((item: ListItem) => {
|
||||
const collection = item;
|
||||
return (
|
||||
<CollectionOperationCell collectionMeta={collection as CollectionMeta} />
|
||||
);
|
||||
}, []);
|
||||
|
||||
const collectionHeaderRenderer = useCallback(() => {
|
||||
return <ListTableHeader headerCols={collectionHeaderColsDef} />;
|
||||
@@ -92,9 +60,11 @@ export const VirtualizedCollectionList = ({
|
||||
if (selectedCollectionIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
collectionService.deleteCollection(info, ...selectedCollectionIds);
|
||||
for (const collectionId of selectedCollectionIds) {
|
||||
collectionService.deleteCollection(collectionId);
|
||||
}
|
||||
hideFloatingToolbar();
|
||||
}, [collectionService, hideFloatingToolbar, info, selectedCollectionIds]);
|
||||
}, [collectionService, hideFloatingToolbar, selectedCollectionIds]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { TagService } from '@affine/core/modules/tag';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { inferOpenMode } from '@affine/core/utils';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type { DocMode } from '@blocksuite/affine/model';
|
||||
@@ -29,8 +28,10 @@ import { useCallback, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
||||
import { CollectionService } from '../../../modules/collection';
|
||||
import { createTagFilter } from '../filter/utils';
|
||||
import {
|
||||
type Collection,
|
||||
CollectionService,
|
||||
} from '../../../modules/collection';
|
||||
import { SaveAsCollectionButton } from '../view';
|
||||
import * as styles from './page-list-header.css';
|
||||
import { PageListNewPageButton } from './page-list-new-page-button';
|
||||
@@ -133,11 +134,12 @@ export const CollectionPageListHeader = ({
|
||||
const workspace = workspaceService.workspace;
|
||||
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const name = useLiveData(collection.name$);
|
||||
|
||||
const createAndAddDocument = useCallback(
|
||||
(createDocumentFn: () => DocRecord) => {
|
||||
const newDoc = createDocumentFn();
|
||||
collectionService.addPageToCollection(collection.id, newDoc.id);
|
||||
collectionService.addDocToCollection(collection.id, newDoc.id);
|
||||
},
|
||||
[collection.id, collectionService]
|
||||
);
|
||||
@@ -183,7 +185,7 @@ export const CollectionPageListHeader = ({
|
||||
<div className={styles.titleIcon}>
|
||||
<ViewLayersIcon />
|
||||
</div>
|
||||
<div className={styles.titleCollectionName}>{collection.name}</div>
|
||||
<div className={styles.titleCollectionName}>{name}</div>
|
||||
</div>
|
||||
<div className={styles.rightButtonGroup}>
|
||||
<Button onClick={handleEdit}>{t['Edit']()}</Button>
|
||||
@@ -221,12 +223,21 @@ export const TagPageListHeader = ({
|
||||
}, [jumpToTags, workspaceId]);
|
||||
|
||||
const saveToCollection = useCallback(
|
||||
(collection: Collection) => {
|
||||
collectionService.addCollection({
|
||||
...collection,
|
||||
filterList: [createTagFilter(tag.id)],
|
||||
(collectionName: string) => {
|
||||
const id = collectionService.createCollection({
|
||||
name: collectionName,
|
||||
rules: {
|
||||
filters: [
|
||||
{
|
||||
type: 'system',
|
||||
key: 'tags',
|
||||
method: 'include-all',
|
||||
value: tag.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
jumpToCollection(workspaceId, collection.id);
|
||||
jumpToCollection(workspaceId, id);
|
||||
},
|
||||
[collectionService, tag.id, jumpToCollection, workspaceId]
|
||||
);
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
import { IconButton, Menu, toast } from '@affine/component';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
CollectionRulesService,
|
||||
type FilterParams,
|
||||
} from '@affine/core/modules/collection-rules';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { PublicDocMode } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { FilterIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { type ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { Filters } from '../../filter';
|
||||
import { AddFilterMenu } from '../../filter/add-filter';
|
||||
import { AffineShapeIcon, FavoriteTag } from '..';
|
||||
import { FilterList } from '../filter';
|
||||
import { VariableSelect } from '../filter/vars';
|
||||
import { usePageHeaderColsDef } from '../header-col-def';
|
||||
import { PageListItemRenderer } from '../page-group';
|
||||
import { ListTableHeader } from '../page-header';
|
||||
@@ -20,7 +29,6 @@ import { SelectorLayout } from '../selector/selector-layout';
|
||||
import type { ListItem } from '../types';
|
||||
import { VirtualizedList } from '../virtualized-list';
|
||||
import * as styles from './select-page.css';
|
||||
import { useFilter } from './use-filter';
|
||||
import { useSearch } from './use-search';
|
||||
|
||||
export const SelectPage = ({
|
||||
@@ -58,12 +66,13 @@ export const SelectPage = ({
|
||||
workspaceService,
|
||||
compatibleFavoriteItemsAdapter,
|
||||
shareDocsListService,
|
||||
collectionRulesService,
|
||||
} = useServices({
|
||||
ShareDocsListService,
|
||||
WorkspaceService,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
CollectionRulesService,
|
||||
});
|
||||
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
||||
const workspace = workspaceService.workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||
@@ -73,20 +82,6 @@ export const SelectPage = ({
|
||||
shareDocsListService.shareDocs?.revalidate();
|
||||
}, [shareDocsListService.shareDocs]);
|
||||
|
||||
const getPublicMode = useCallback(
|
||||
(id: string) => {
|
||||
const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode;
|
||||
if (mode === PublicDocMode.Edgeless) {
|
||||
return 'edgeless';
|
||||
} else if (mode === PublicDocMode.Page) {
|
||||
return 'page';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
[shareDocs]
|
||||
);
|
||||
|
||||
const isFavorite = useCallback(
|
||||
(meta: DocMeta) => favourites.some(fav => fav.id === meta.id),
|
||||
[favourites]
|
||||
@@ -106,22 +101,41 @@ export const SelectPage = ({
|
||||
);
|
||||
|
||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||
const {
|
||||
clickFilter,
|
||||
createFilter,
|
||||
filters,
|
||||
showFilter,
|
||||
updateFilters,
|
||||
filteredList,
|
||||
} = useFilter(
|
||||
pageMetas.map(meta => ({
|
||||
meta,
|
||||
publicMode: getPublicMode(meta.id),
|
||||
favorite: isFavorite(meta),
|
||||
}))
|
||||
);
|
||||
const [filters, setFilters] = useState<FilterParams[]>([]);
|
||||
|
||||
const [filteredDocIds, setFilteredDocIds] = useState<string[]>([]);
|
||||
const filteredPageMetas = useMemo(() => {
|
||||
const idSet = new Set(filteredDocIds);
|
||||
return pageMetas.filter(page => idSet.has(page.id));
|
||||
}, [pageMetas, filteredDocIds]);
|
||||
|
||||
const { searchText, updateSearchText, searchedList } =
|
||||
useSearch(filteredList);
|
||||
useSearch(filteredPageMetas);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
.watch([
|
||||
...filters,
|
||||
{
|
||||
type: 'system',
|
||||
key: 'empty-journal',
|
||||
method: 'is',
|
||||
value: 'false',
|
||||
},
|
||||
{
|
||||
type: 'system',
|
||||
key: 'trash',
|
||||
method: 'is',
|
||||
value: 'false',
|
||||
},
|
||||
])
|
||||
.subscribe(result => {
|
||||
setFilteredDocIds(result.groups.flatMap(group => group.items));
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [collectionRulesService, filters]);
|
||||
|
||||
const operationsRenderer = useCallback(
|
||||
(item: ListItem) => {
|
||||
@@ -162,29 +176,21 @@ export const SelectPage = ({
|
||||
{t['com.affine.selectPage.title']()}
|
||||
</div>
|
||||
)}
|
||||
{!showFilter && filters.length === 0 ? (
|
||||
{filters.length === 0 ? (
|
||||
<Menu
|
||||
items={
|
||||
<VariableSelect
|
||||
propertiesMeta={docCollection.meta.properties}
|
||||
selected={filters}
|
||||
onSelect={createFilter}
|
||||
<AddFilterMenu
|
||||
onAdd={params => setFilters([...filters, params])}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<IconButton icon={<FilterIcon />} onClick={clickFilter} />
|
||||
<IconButton icon={<FilterIcon />} />
|
||||
</Menu>
|
||||
) : (
|
||||
<IconButton icon={<FilterIcon />} onClick={clickFilter} />
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
{showFilter ? (
|
||||
{filters.length !== 0 ? (
|
||||
<div style={{ padding: '12px 16px 16px' }}>
|
||||
<FilterList
|
||||
propertiesMeta={docCollection.meta.properties}
|
||||
value={filters}
|
||||
onChange={updateFilters}
|
||||
/>
|
||||
<Filters filters={filters} onChange={setFilters} />
|
||||
</div>
|
||||
) : null}
|
||||
{searchedList.length ? (
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import {
|
||||
filterPageByRules,
|
||||
type PageDataForFilter,
|
||||
} from '../use-collection-manager';
|
||||
|
||||
export const useFilter = (list: PageDataForFilter[]) => {
|
||||
const [filters, changeFilters] = useState<Filter[]>([]);
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
const clickFilter = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (showFilter || filters.length !== 0) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setShowFilter(!showFilter);
|
||||
}
|
||||
},
|
||||
[filters.length, showFilter]
|
||||
);
|
||||
const onCreateFilter = useCallback(
|
||||
(filter: Filter) => {
|
||||
changeFilters([...filters, filter]);
|
||||
setShowFilter(true);
|
||||
},
|
||||
[filters]
|
||||
);
|
||||
return {
|
||||
showFilter,
|
||||
filters,
|
||||
updateFilters: changeFilters,
|
||||
clickFilter,
|
||||
createFilter: onCreateFilter,
|
||||
filteredList: list
|
||||
.filter(pageData => {
|
||||
if (pageData.meta.trash) {
|
||||
return false;
|
||||
}
|
||||
return filterPageByRules(filters, [], pageData);
|
||||
})
|
||||
.map(pageData => pageData.meta),
|
||||
};
|
||||
};
|
||||
@@ -1,14 +1,13 @@
|
||||
import { toast, useConfirmModal } from '@affine/component';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { type Collection } from '@affine/core/modules/collection';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import type { Collection, Filter } from '@affine/env/filter';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { ListFloatingToolbar } from '../components/list-floating-toolbar';
|
||||
import { usePageItemGroupDefinitions } from '../group-definitions';
|
||||
@@ -17,7 +16,6 @@ import { PageOperationCell } from '../operation-cell';
|
||||
import { PageListItemRenderer } from '../page-group';
|
||||
import { ListTableHeader } from '../page-header';
|
||||
import type { ItemListHandle, ListItem } from '../types';
|
||||
import { useFilteredPageMetas } from '../use-filtered-page-metas';
|
||||
import { VirtualizedList } from '../virtualized-list';
|
||||
import {
|
||||
CollectionPageListHeader,
|
||||
@@ -25,15 +23,14 @@ import {
|
||||
TagPageListHeader,
|
||||
} from './page-list-header';
|
||||
|
||||
const usePageOperationsRenderer = () => {
|
||||
const usePageOperationsRenderer = (collection?: Collection) => {
|
||||
const t = useI18n();
|
||||
const collectionService = useService(CollectionService);
|
||||
const removeFromAllowList = useCallback(
|
||||
(id: string) => {
|
||||
collectionService.deletePagesFromCollections([id]);
|
||||
collection?.removeDoc(id);
|
||||
toast(t['com.affine.collection.removePage.success']());
|
||||
},
|
||||
[collectionService, t]
|
||||
[collection, t]
|
||||
);
|
||||
const pageOperationsRenderer = useCallback(
|
||||
(page: DocMeta, isInAllowList?: boolean) => {
|
||||
@@ -53,14 +50,12 @@ const usePageOperationsRenderer = () => {
|
||||
export const VirtualizedPageList = memo(function VirtualizedPageList({
|
||||
tag,
|
||||
collection,
|
||||
filters,
|
||||
listItem,
|
||||
setHideHeaderCreateNewPage,
|
||||
disableMultiDelete,
|
||||
}: {
|
||||
tag?: Tag;
|
||||
collection?: Collection;
|
||||
filters?: Filter[];
|
||||
listItem?: DocMeta[];
|
||||
setHideHeaderCreateNewPage?: (hide: boolean) => void;
|
||||
disableMultiDelete?: boolean;
|
||||
@@ -72,19 +67,28 @@ export const VirtualizedPageList = memo(function VirtualizedPageList({
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const docsService = useService(DocsService);
|
||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||
const pageOperations = usePageOperationsRenderer();
|
||||
const pageOperations = usePageOperationsRenderer(collection);
|
||||
const pageHeaderColsDef = usePageHeaderColsDef();
|
||||
|
||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
||||
filters,
|
||||
collection,
|
||||
});
|
||||
const [filteredPageIds, setFilteredPageIds] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
const subscription = collection?.watch().subscribe(docIds => {
|
||||
setFilteredPageIds(docIds);
|
||||
});
|
||||
return () => subscription?.unsubscribe();
|
||||
}, [collection]);
|
||||
const allowList = useLiveData(collection?.info$.map(info => info.allowList));
|
||||
const pageMetasToRender = useMemo(() => {
|
||||
if (listItem) {
|
||||
return listItem;
|
||||
}
|
||||
return filteredPageMetas;
|
||||
}, [filteredPageMetas, listItem]);
|
||||
if (collection) {
|
||||
return pageMetas.filter(
|
||||
page => filteredPageIds.includes(page.id) && !page.trash
|
||||
);
|
||||
}
|
||||
return pageMetas.filter(page => !page.trash);
|
||||
}, [collection, filteredPageIds, listItem, pageMetas]);
|
||||
|
||||
const filteredSelectedPageIds = useMemo(() => {
|
||||
const ids = new Set(pageMetasToRender.map(page => page.id));
|
||||
@@ -98,10 +102,10 @@ export const VirtualizedPageList = memo(function VirtualizedPageList({
|
||||
const pageOperationRenderer = useCallback(
|
||||
(item: ListItem) => {
|
||||
const page = item as DocMeta;
|
||||
const isInAllowList = collection?.allowList?.includes(page.id);
|
||||
const isInAllowList = allowList?.includes(page.id);
|
||||
return pageOperations(page, isInAllowList);
|
||||
},
|
||||
[collection, pageOperations]
|
||||
[allowList, pageOperations]
|
||||
);
|
||||
|
||||
const pageHeaderRenderer = useCallback(() => {
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
import { Menu, MenuItem, Tooltip } from '@affine/component';
|
||||
import type { Filter, Literal, PropertiesMeta } from '@affine/env/filter';
|
||||
import clsx from 'clsx';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { FilterTag } from './filter-tag-translation';
|
||||
import * as styles from './index.css';
|
||||
import { literalMatcher } from './literal-matcher';
|
||||
import { tBoolean } from './logical/custom-type';
|
||||
import type { TFunction, TType } from './logical/typesystem';
|
||||
import { typesystem } from './logical/typesystem';
|
||||
import { variableDefineMap } from './shared-types';
|
||||
import { filterMatcher, VariableSelect, vars } from './vars';
|
||||
|
||||
export const Condition = ({
|
||||
value,
|
||||
onChange,
|
||||
propertiesMeta,
|
||||
}: {
|
||||
value: Filter;
|
||||
onChange: (filter: Filter) => void;
|
||||
propertiesMeta: PropertiesMeta;
|
||||
}) => {
|
||||
const data = useMemo(() => {
|
||||
const data = filterMatcher.find(v => v.data.name === value.funcName);
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const instance = typesystem.instance(
|
||||
{},
|
||||
[variableDefineMap[value.left.name].type(propertiesMeta)],
|
||||
tBoolean.create(),
|
||||
data.type
|
||||
);
|
||||
return {
|
||||
render: data.data.render,
|
||||
type: instance,
|
||||
};
|
||||
}, [propertiesMeta, value.funcName, value.left.name]);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const render =
|
||||
data.render ??
|
||||
(({ ast }) => {
|
||||
const args = renderArgs(value, onChange, data.type);
|
||||
return (
|
||||
<div className={styles.filterContainerStyle}>
|
||||
<Menu
|
||||
items={
|
||||
<VariableSelect
|
||||
propertiesMeta={propertiesMeta}
|
||||
selected={[]}
|
||||
onSelect={onChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
data-testid="variable-name"
|
||||
className={clsx(styles.filterTypeStyle, styles.ellipsisTextStyle)}
|
||||
>
|
||||
<Tooltip content={ast.left.name}>
|
||||
<div className={styles.filterTypeIconStyle}>
|
||||
{variableDefineMap[ast.left.name].icon}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<FilterTag name={ast.left.name} />
|
||||
</div>
|
||||
</Menu>
|
||||
<Menu
|
||||
items={
|
||||
<FunctionSelect
|
||||
propertiesMeta={propertiesMeta}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={clsx(styles.switchStyle, styles.ellipsisTextStyle)}
|
||||
data-testid="filter-name"
|
||||
>
|
||||
<FilterTag name={ast.funcName} />
|
||||
</div>
|
||||
</Menu>
|
||||
{args}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return <>{render({ ast: value })}</>;
|
||||
};
|
||||
|
||||
const FunctionSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
propertiesMeta,
|
||||
}: {
|
||||
value: Filter;
|
||||
onChange: (value: Filter) => void;
|
||||
propertiesMeta: PropertiesMeta;
|
||||
}) => {
|
||||
const list = useMemo(() => {
|
||||
const type = vars.find(v => v.name === value.left.name)?.type;
|
||||
if (!type) {
|
||||
return [];
|
||||
}
|
||||
return filterMatcher.allMatchedData(type(propertiesMeta));
|
||||
}, [propertiesMeta, value.left.name]);
|
||||
return (
|
||||
<div data-testid="filter-name-select">
|
||||
{list.map(v => (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...value,
|
||||
funcName: v.name,
|
||||
args: v.defaultArgs().map(v => ({ type: 'literal', value: v })),
|
||||
});
|
||||
}}
|
||||
key={v.name}
|
||||
>
|
||||
<FilterTag name={v.name} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Arg = ({
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
type: TType;
|
||||
value: Literal;
|
||||
onChange: (lit: Literal) => void;
|
||||
}) => {
|
||||
const data = useMemo(() => literalMatcher.match(type), [type]);
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
data-testid="filter-arg"
|
||||
className={clsx(styles.argStyle, styles.ellipsisTextStyle)}
|
||||
>
|
||||
{data.render({
|
||||
type,
|
||||
value: value?.value,
|
||||
onChange: v => onChange({ type: 'literal', value: v }),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const renderArgs = (
|
||||
filter: Filter,
|
||||
onChange: (value: Filter) => void,
|
||||
type: TFunction
|
||||
): ReactNode => {
|
||||
const rest = type.args.slice(1);
|
||||
return rest.map((argType, i) => {
|
||||
const value = filter.args[i];
|
||||
return (
|
||||
<Arg
|
||||
key={`${argType.type}-${i}`}
|
||||
type={argType}
|
||||
value={value}
|
||||
onChange={value => {
|
||||
const args = type.args.map((_, index) =>
|
||||
i === index ? value : filter.args[index]
|
||||
);
|
||||
onChange({
|
||||
...filter,
|
||||
args,
|
||||
});
|
||||
}}
|
||||
></Arg>
|
||||
);
|
||||
});
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const datePickerTriggerInput = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
width: '50px',
|
||||
fontWeight: '600',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '22px',
|
||||
textAlign: 'center',
|
||||
':hover': {
|
||||
background: cssVar('hoverColor'),
|
||||
borderRadius: '4px',
|
||||
},
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
import type { PopoverProps } from '@affine/component';
|
||||
import { DatePicker, Popover } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { datePickerTriggerInput } from './date-select.css';
|
||||
|
||||
const datePickerPopperContentOptions: PopoverProps['contentOptions'] = {
|
||||
style: { padding: 20, marginTop: 10 },
|
||||
};
|
||||
|
||||
export const DateSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onDateChange = useCallback(
|
||||
(e: string) => {
|
||||
setOpen(false);
|
||||
onChange(dayjs(e, 'YYYY-MM-DD').valueOf());
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
contentOptions={datePickerPopperContentOptions}
|
||||
content={
|
||||
<DatePicker
|
||||
weekDays={t['com.affine.calendar-date-picker.week-days']()}
|
||||
monthNames={t['com.affine.calendar-date-picker.month-names']()}
|
||||
todayLabel={t['com.affine.calendar-date-picker.today']()}
|
||||
value={dayjs(value as number).format('YYYY-MM-DD')}
|
||||
onChange={onDateChange}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<input
|
||||
value={dayjs(value as number).format('MMM DD')}
|
||||
className={datePickerTriggerInput}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Filter, Literal, Ref, VariableMap } from '@affine/env/filter';
|
||||
|
||||
import { filterMatcher } from './vars';
|
||||
|
||||
const evalRef = (ref: Ref, variableMap: VariableMap) => {
|
||||
return variableMap[ref.name];
|
||||
};
|
||||
const evalLiteral = (lit?: Literal) => {
|
||||
return lit?.value;
|
||||
};
|
||||
const evalFilter = (filter: Filter, variableMap: VariableMap): boolean => {
|
||||
const impl = filterMatcher.findData(v => v.name === filter.funcName)?.impl;
|
||||
if (!impl) {
|
||||
throw new Error('No function implementation found');
|
||||
}
|
||||
const leftValue = evalRef(filter.left, variableMap);
|
||||
const args = filter.args.map(evalLiteral);
|
||||
return impl(leftValue, ...args);
|
||||
};
|
||||
export const evalFilterList = (
|
||||
filterList: Filter[],
|
||||
variableMap: VariableMap
|
||||
) => {
|
||||
return filterList.every(filter => evalFilter(filter, variableMap));
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import { Button, IconButton, Menu } from '@affine/component';
|
||||
import type { Filter, PropertiesMeta } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CloseIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { Condition } from './condition';
|
||||
import * as styles from './index.css';
|
||||
import { CreateFilterMenu } from './vars';
|
||||
|
||||
export const FilterList = ({
|
||||
value,
|
||||
onChange,
|
||||
propertiesMeta,
|
||||
}: {
|
||||
value: Filter[];
|
||||
onChange: (value: Filter[]) => void;
|
||||
propertiesMeta: PropertiesMeta;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{value.map((filter, i) => {
|
||||
return (
|
||||
<div className={styles.filterItemStyle} key={i}>
|
||||
<Condition
|
||||
propertiesMeta={propertiesMeta}
|
||||
value={filter}
|
||||
onChange={filter => {
|
||||
onChange(
|
||||
value.map((old, oldIndex) => (oldIndex === i ? filter : old))
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.filterItemCloseStyle}
|
||||
onClick={() => {
|
||||
onChange(value.filter((_, index) => i !== index));
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Menu
|
||||
key={value.length} // hack to force menu to rerender (disable unmount animation)
|
||||
items={
|
||||
<CreateFilterMenu
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
propertiesMeta={propertiesMeta}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{value.length === 0 ? (
|
||||
<Button suffix={<PlusIcon />}>
|
||||
{t['com.affine.filterList.button.add']()}
|
||||
</Button>
|
||||
) : (
|
||||
<IconButton size="16">
|
||||
<PlusIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
|
||||
import { ellipsisTextStyle } from './index.css';
|
||||
type FilterTagProps = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
const useFilterTag = ({ name }: FilterTagProps) => {
|
||||
const t = useI18n();
|
||||
switch (name) {
|
||||
case 'Created':
|
||||
return t['Created']();
|
||||
case 'Updated':
|
||||
return t['Updated']();
|
||||
case 'Tags':
|
||||
return t['Tags']();
|
||||
case 'Is Favourited':
|
||||
return t['com.affine.filter.is-favourited']();
|
||||
case 'Is Public':
|
||||
return t['com.affine.filter.is-public']();
|
||||
case 'after':
|
||||
return t['com.affine.filter.after']();
|
||||
case 'before':
|
||||
return t['com.affine.filter.before']();
|
||||
case 'last':
|
||||
return t['com.affine.filter.last']();
|
||||
case 'is':
|
||||
return t['com.affine.filter.is']();
|
||||
case 'is not empty':
|
||||
return t['com.affine.filter.is not empty']();
|
||||
case 'is empty':
|
||||
return t['com.affine.filter.is empty']();
|
||||
case 'contains all':
|
||||
return t['com.affine.filter.contains all']();
|
||||
case 'contains one of':
|
||||
return t['com.affine.filter.contains one of']();
|
||||
case 'does not contains all':
|
||||
return t['com.affine.filter.does not contains all']();
|
||||
case 'does not contains one of':
|
||||
return t['com.affine.filter.does not contains one of']();
|
||||
case 'true':
|
||||
return t['com.affine.filter.true']();
|
||||
case 'false':
|
||||
return t['com.affine.filter.false']();
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
};
|
||||
|
||||
export const FilterTag = ({ name }: FilterTagProps) => {
|
||||
const tag = useFilterTag({ name });
|
||||
|
||||
return (
|
||||
<Tooltip content={tag}>
|
||||
<span className={ellipsisTextStyle} data-testid={`filler-tag-${tag}`}>
|
||||
{tag}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const filterContainerStyle = style({
|
||||
display: 'flex',
|
||||
userSelect: 'none',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const menuItemStyle = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
export const variableSelectTitleStyle = style({
|
||||
margin: '7px 16px',
|
||||
fontWeight: 500,
|
||||
lineHeight: '20px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
export const variableSelectDividerStyle = style({
|
||||
marginTop: '2px',
|
||||
marginBottom: '2px',
|
||||
marginLeft: '12px',
|
||||
marginRight: '8px',
|
||||
height: '1px',
|
||||
background: cssVar('borderColor'),
|
||||
});
|
||||
export const menuItemTextStyle = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
export const filterItemStyle = style({
|
||||
display: 'flex',
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
borderRadius: '8px',
|
||||
background: cssVar('white'),
|
||||
padding: '4px 8px',
|
||||
overflow: 'hidden',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
export const filterItemCloseStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
marginLeft: '4px',
|
||||
});
|
||||
export const inputStyle = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
padding: '2px 4px',
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
background: cssVar('hoverColor'),
|
||||
borderRadius: '4px',
|
||||
},
|
||||
});
|
||||
export const switchStyle = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
padding: '2px 4px',
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flex: '3 1 auto',
|
||||
minWidth: '28px',
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
background: cssVar('hoverColor'),
|
||||
borderRadius: '4px',
|
||||
},
|
||||
});
|
||||
export const filterTypeStyle = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '2px 4px',
|
||||
transition: 'all 0.15s ease-in-out',
|
||||
marginRight: '6px',
|
||||
flex: '1 0 auto',
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
background: cssVar('hoverColor'),
|
||||
borderRadius: '4px',
|
||||
},
|
||||
});
|
||||
export const filterTypeIconStyle = style({
|
||||
fontSize: cssVar('fontBase'),
|
||||
marginRight: '6px',
|
||||
padding: '1px 0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: cssVar('iconColor'),
|
||||
});
|
||||
|
||||
export const argStyle = style({
|
||||
marginLeft: 4,
|
||||
fontWeight: 600,
|
||||
flex: '1 0 auto',
|
||||
});
|
||||
|
||||
export const ellipsisTextStyle = style({
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './eval';
|
||||
export * from './filter-list';
|
||||
export * from './utils';
|
||||
@@ -1,97 +0,0 @@
|
||||
import { Input, Menu, MenuItem } from '@affine/component';
|
||||
import type { LiteralValue } from '@affine/env/filter';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import type { TagMeta } from '../types';
|
||||
import { DateSelect } from './date-select';
|
||||
import { FilterTag } from './filter-tag-translation';
|
||||
import { inputStyle } from './index.css';
|
||||
import { tBoolean, tDate, tDateRange, tTag } from './logical/custom-type';
|
||||
import { Matcher } from './logical/matcher';
|
||||
import type { TType } from './logical/typesystem';
|
||||
import { tArray, typesystem } from './logical/typesystem';
|
||||
import { MultiSelect } from './multi-select';
|
||||
|
||||
export const literalMatcher = new Matcher<{
|
||||
render: (props: {
|
||||
type: TType;
|
||||
value: LiteralValue;
|
||||
onChange: (lit: LiteralValue) => void;
|
||||
}) => ReactNode;
|
||||
}>((type, target) => {
|
||||
return typesystem.isSubtype(type, target);
|
||||
});
|
||||
|
||||
literalMatcher.register(tDateRange.create(), {
|
||||
render: ({ value, onChange }) => (
|
||||
<Menu
|
||||
items={
|
||||
<div>
|
||||
<Input
|
||||
type="number"
|
||||
// Handle the input change and update the value accordingly
|
||||
onChange={i => (i ? onChange(parseInt(i)) : onChange(0))}
|
||||
/>
|
||||
{[1, 2, 3, 7, 14, 30].map(i => (
|
||||
<MenuItem
|
||||
key={i}
|
||||
onClick={() => {
|
||||
// Handle the menu item click and update the value accordingly
|
||||
onChange(i);
|
||||
}}
|
||||
>
|
||||
{i} {i > 1 ? 'days' : 'day'}
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<span>{value.toString()}</span> {(value as number) > 1 ? 'days' : 'day'}
|
||||
</div>
|
||||
</Menu>
|
||||
),
|
||||
});
|
||||
|
||||
literalMatcher.register(tBoolean.create(), {
|
||||
render: ({ value, onChange }) => (
|
||||
<div
|
||||
className={inputStyle}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
onChange(!value);
|
||||
}}
|
||||
>
|
||||
<FilterTag name={value?.toString()} />
|
||||
</div>
|
||||
),
|
||||
});
|
||||
literalMatcher.register(tDate.create(), {
|
||||
render: ({ value, onChange }) => (
|
||||
<DateSelect value={value as number} onChange={onChange} />
|
||||
),
|
||||
});
|
||||
const getTagsOfArrayTag = (type: TType): TagMeta[] => {
|
||||
if (type.type === 'array') {
|
||||
if (tTag.is(type.ele)) {
|
||||
return type.ele.data?.tags ?? [];
|
||||
}
|
||||
return [];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
literalMatcher.register(tArray(tTag.create()), {
|
||||
render: ({ type, value, onChange }) => {
|
||||
return (
|
||||
<MultiSelect
|
||||
value={(value ?? []) as string[]}
|
||||
onChange={value => onChange(value)}
|
||||
options={getTagsOfArrayTag(type).map((v: any) => ({
|
||||
label: v.name,
|
||||
value: v.id,
|
||||
}))}
|
||||
></MultiSelect>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import type { TagMeta } from '../../types';
|
||||
import { DataHelper, typesystem } from './typesystem';
|
||||
|
||||
export const tNumber = typesystem.defineData(
|
||||
DataHelper.create<{ value: number }>('Number')
|
||||
);
|
||||
export const tString = typesystem.defineData(
|
||||
DataHelper.create<{ value: string }>('String')
|
||||
);
|
||||
export const tBoolean = typesystem.defineData(
|
||||
DataHelper.create<{ value: boolean }>('Boolean')
|
||||
);
|
||||
export const tDate = typesystem.defineData(
|
||||
DataHelper.create<{ value: number }>('Date')
|
||||
);
|
||||
|
||||
export const tTag = typesystem.defineData<{ tags: TagMeta[] }>({
|
||||
name: 'Tag',
|
||||
supers: [],
|
||||
});
|
||||
|
||||
export const tDateRange = typesystem.defineData(
|
||||
DataHelper.create<{ value: number }>('DateRange')
|
||||
);
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { TType } from './typesystem';
|
||||
import { typesystem } from './typesystem';
|
||||
|
||||
type MatcherData<Data, Type extends TType = TType> = { type: Type; data: Data };
|
||||
|
||||
export class Matcher<Data, Type extends TType = TType> {
|
||||
private readonly list: MatcherData<Data, Type>[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly _match?: (type: Type, target: TType) => boolean
|
||||
) {}
|
||||
|
||||
register(type: Type, data: Data) {
|
||||
this.list.push({ type, data });
|
||||
}
|
||||
|
||||
match(type: TType) {
|
||||
const match = this._match ?? typesystem.isSubtype.bind(typesystem);
|
||||
for (const t of this.list) {
|
||||
if (match(t.type, type)) {
|
||||
return t.data;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
allMatched(type: TType): MatcherData<Data>[] {
|
||||
const match = this._match ?? typesystem.isSubtype.bind(typesystem);
|
||||
const result: MatcherData<Data>[] = [];
|
||||
for (const t of this.list) {
|
||||
if (match(t.type, type)) {
|
||||
result.push(t);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
allMatchedData(type: TType): Data[] {
|
||||
return this.allMatched(type).map(v => v.data);
|
||||
}
|
||||
|
||||
findData(f: (data: Data) => boolean): Data | undefined {
|
||||
return this.list.find(data => f(data.data))?.data;
|
||||
}
|
||||
|
||||
find(
|
||||
f: (data: MatcherData<Data, Type>) => boolean
|
||||
): MatcherData<Data, Type> | undefined {
|
||||
return this.list.find(f);
|
||||
}
|
||||
|
||||
all(): MatcherData<Data, Type>[] {
|
||||
return this.list;
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
/**
|
||||
* This file will be moved to a separate package soon.
|
||||
*/
|
||||
|
||||
export interface TUnion {
|
||||
type: 'union';
|
||||
title: 'union';
|
||||
list: TType[];
|
||||
}
|
||||
|
||||
export const tUnion = (list: TType[]): TUnion => ({
|
||||
type: 'union',
|
||||
title: 'union',
|
||||
list,
|
||||
});
|
||||
|
||||
// TODO treat as data type
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export interface TArray<Ele extends TType = TType> {
|
||||
type: 'array';
|
||||
ele: Ele;
|
||||
title: 'array';
|
||||
}
|
||||
|
||||
export const tArray = <const T extends TType>(ele: T): TArray<T> => {
|
||||
return {
|
||||
type: 'array',
|
||||
title: 'array',
|
||||
ele,
|
||||
};
|
||||
};
|
||||
export type TTypeVar = {
|
||||
type: 'typeVar';
|
||||
title: 'typeVar';
|
||||
name: string;
|
||||
bound: TType;
|
||||
};
|
||||
export const tTypeVar = (name: string, bound: TType): TTypeVar => {
|
||||
return {
|
||||
type: 'typeVar',
|
||||
title: 'typeVar',
|
||||
name,
|
||||
bound,
|
||||
};
|
||||
};
|
||||
export type TTypeRef = {
|
||||
type: 'typeRef';
|
||||
title: 'typeRef';
|
||||
name: string;
|
||||
};
|
||||
export const tTypeRef = (name: string): TTypeRef => {
|
||||
return {
|
||||
type: 'typeRef',
|
||||
title: 'typeRef',
|
||||
name,
|
||||
};
|
||||
};
|
||||
|
||||
export type TFunction = {
|
||||
type: 'function';
|
||||
title: 'function';
|
||||
typeVars: TTypeVar[];
|
||||
args: TType[];
|
||||
rt: TType;
|
||||
};
|
||||
|
||||
export const tFunction = (fn: {
|
||||
typeVars?: TTypeVar[];
|
||||
args: TType[];
|
||||
rt: TType;
|
||||
}): TFunction => {
|
||||
return {
|
||||
type: 'function',
|
||||
title: 'function',
|
||||
typeVars: fn.typeVars ?? [],
|
||||
args: fn.args,
|
||||
rt: fn.rt,
|
||||
};
|
||||
};
|
||||
|
||||
export type TType = TDataType | TArray | TUnion | TTypeRef | TFunction;
|
||||
|
||||
export type DataTypeShape = Record<string, unknown>;
|
||||
export type TDataType<Data extends DataTypeShape = Record<string, unknown>> = {
|
||||
type: 'data';
|
||||
name: string;
|
||||
data?: Data;
|
||||
};
|
||||
export type ValueOfData<T extends DataDefine> =
|
||||
T extends DataDefine<infer R> ? R : never;
|
||||
|
||||
export class DataDefine<Data extends DataTypeShape = Record<string, unknown>> {
|
||||
constructor(
|
||||
private readonly config: DataDefineConfig<Data>,
|
||||
private readonly dataMap: Map<string, DataDefine>
|
||||
) {}
|
||||
|
||||
create(data?: Data): TDataType<Data> {
|
||||
return {
|
||||
type: 'data',
|
||||
name: this.config.name,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
is(data: TType): data is TDataType<Data> {
|
||||
if (data.type !== 'data') {
|
||||
return false;
|
||||
}
|
||||
return data.name === this.config.name;
|
||||
}
|
||||
|
||||
private isByName(name: string): boolean {
|
||||
return name === this.config.name;
|
||||
}
|
||||
|
||||
isSubOf(superType: TDataType): boolean {
|
||||
if (this.is(superType)) {
|
||||
return true;
|
||||
}
|
||||
return this.config.supers.some(sup => sup.isSubOf(superType));
|
||||
}
|
||||
|
||||
private isSubOfByName(superType: string): boolean {
|
||||
if (this.isByName(superType)) {
|
||||
return true;
|
||||
}
|
||||
return this.config.supers.some(sup => sup.isSubOfByName(superType));
|
||||
}
|
||||
|
||||
isSuperOf(subType: TDataType): boolean {
|
||||
const dataDefine = this.dataMap.get(subType.name);
|
||||
if (!dataDefine) {
|
||||
throw new Error('bug');
|
||||
}
|
||||
return dataDefine.isSubOfByName(this.config.name);
|
||||
}
|
||||
}
|
||||
|
||||
// type DataTypeVar = {};
|
||||
|
||||
// TODO support generic data type
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface DataDefineConfig<T extends DataTypeShape> {
|
||||
name: string;
|
||||
supers: DataDefine[];
|
||||
_phantom?: T;
|
||||
}
|
||||
|
||||
interface DataHelper<T extends DataTypeShape> {
|
||||
create<V = Record<string, unknown>>(name: string): DataDefineConfig<T & V>;
|
||||
|
||||
extends<V extends DataTypeShape>(
|
||||
dataDefine: DataDefine<V>
|
||||
): DataHelper<T & V>;
|
||||
}
|
||||
|
||||
const createDataHelper = <T extends DataTypeShape = Record<string, unknown>>(
|
||||
...supers: DataDefine[]
|
||||
): DataHelper<T> => {
|
||||
return {
|
||||
create(name: string) {
|
||||
return {
|
||||
name,
|
||||
supers,
|
||||
};
|
||||
},
|
||||
extends(dataDefine) {
|
||||
return createDataHelper(...supers, dataDefine);
|
||||
},
|
||||
};
|
||||
};
|
||||
export const DataHelper = createDataHelper();
|
||||
|
||||
export class Typesystem {
|
||||
dataMap = new Map<string, DataDefine<any>>();
|
||||
|
||||
defineData<T extends DataTypeShape>(
|
||||
config: DataDefineConfig<T>
|
||||
): DataDefine<T> {
|
||||
const result = new DataDefine(config, this.dataMap);
|
||||
this.dataMap.set(config.name, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
isDataType(t: TType): t is TDataType {
|
||||
return t.type === 'data';
|
||||
}
|
||||
|
||||
isSubtype(
|
||||
superType: TType,
|
||||
sub: TType,
|
||||
context?: Record<string, TType>
|
||||
): boolean {
|
||||
if (superType.type === 'typeRef') {
|
||||
// TODO both are ref
|
||||
if (context && sub.type !== 'typeRef') {
|
||||
context[superType.name] = sub;
|
||||
}
|
||||
// TODO bound
|
||||
return true;
|
||||
}
|
||||
if (sub.type === 'typeRef') {
|
||||
// TODO both are ref
|
||||
if (context) {
|
||||
context[sub.name] = superType;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (tUnknown.is(superType)) {
|
||||
return true;
|
||||
}
|
||||
if (superType.type === 'union') {
|
||||
return superType.list.some(type => this.isSubtype(type, sub, context));
|
||||
}
|
||||
if (sub.type === 'union') {
|
||||
return sub.list.every(type => this.isSubtype(superType, type, context));
|
||||
}
|
||||
|
||||
if (this.isDataType(sub)) {
|
||||
const dataDefine = this.dataMap.get(sub.name);
|
||||
if (!dataDefine) {
|
||||
throw new Error('bug');
|
||||
}
|
||||
if (!this.isDataType(superType)) {
|
||||
return false;
|
||||
}
|
||||
return dataDefine.isSubOf(superType);
|
||||
}
|
||||
|
||||
if (superType.type === 'array' || sub.type === 'array') {
|
||||
if (superType.type !== 'array' || sub.type !== 'array') {
|
||||
return false;
|
||||
}
|
||||
return this.isSubtype(superType.ele, sub.ele, context);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
subst(context: Record<string, TType>, template: TFunction): TFunction {
|
||||
const subst = (type: TType): TType => {
|
||||
if (this.isDataType(type)) {
|
||||
return type;
|
||||
}
|
||||
switch (type.type) {
|
||||
case 'typeRef':
|
||||
return { ...context[type.name] };
|
||||
case 'union':
|
||||
return tUnion(type.list.map(type => subst(type)));
|
||||
case 'array':
|
||||
return tArray(subst(type.ele));
|
||||
case 'function':
|
||||
throw new Error('TODO');
|
||||
}
|
||||
};
|
||||
const result = tFunction({
|
||||
args: template.args.map(type => subst(type)),
|
||||
rt: subst(template.rt),
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
instance(
|
||||
context: Record<string, TType>,
|
||||
realArgs: TType[],
|
||||
realRt: TType,
|
||||
template: TFunction
|
||||
): TFunction {
|
||||
const ctx = { ...context };
|
||||
template.args.forEach((arg, i) => {
|
||||
const realArg = realArgs[i];
|
||||
if (realArg) {
|
||||
this.isSubtype(arg, realArg, ctx);
|
||||
}
|
||||
});
|
||||
this.isSubtype(realRt, template.rt);
|
||||
return this.subst(ctx, template);
|
||||
}
|
||||
}
|
||||
|
||||
export const typesystem = new Typesystem();
|
||||
export const tUnknown = typesystem.defineData(DataHelper.create('Unknown'));
|
||||
@@ -1,62 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const content = style({
|
||||
fontSize: 12,
|
||||
color: cssVar('textPrimaryColor'),
|
||||
borderRadius: 8,
|
||||
padding: '3px 4px',
|
||||
cursor: 'pointer',
|
||||
overflow: 'hidden',
|
||||
':hover': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
});
|
||||
export const text = style({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: 350,
|
||||
selectors: {
|
||||
'&.empty': {
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const optionList = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
padding: '0 4px',
|
||||
maxHeight: '220px',
|
||||
});
|
||||
export const scrollbar = style({
|
||||
vars: {
|
||||
'--scrollbar-width': '4px',
|
||||
},
|
||||
});
|
||||
export const selectOption = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: 14,
|
||||
height: 26,
|
||||
borderRadius: 5,
|
||||
maxWidth: 240,
|
||||
minWidth: 100,
|
||||
padding: '0 12px',
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
});
|
||||
export const optionLabel = style({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1,
|
||||
});
|
||||
export const done = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: cssVar('primaryColor'),
|
||||
marginLeft: 8,
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
import { Menu, MenuItem, Scrollable, Tooltip } from '@affine/component';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import clsx from 'clsx';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import * as styles from './multi-select.css';
|
||||
|
||||
export const MultiSelect = ({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
}: {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const optionMap = useMemo(
|
||||
() => Object.fromEntries(options.map(v => [v.value, v])),
|
||||
[options]
|
||||
);
|
||||
|
||||
const content = useMemo(
|
||||
() => value.map(id => optionMap[id]?.label).join(', '),
|
||||
[optionMap, value]
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
return (
|
||||
<Scrollable.Root>
|
||||
<Scrollable.Viewport
|
||||
data-testid="multi-select"
|
||||
className={styles.optionList}
|
||||
>
|
||||
{options.length === 0 ? (
|
||||
<MenuItem checked={true}>
|
||||
{t['com.affine.filter.empty-tag']()}
|
||||
</MenuItem>
|
||||
) : (
|
||||
options.map(option => {
|
||||
const selected = value.includes(option.value);
|
||||
const click = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (selected) {
|
||||
onChange(value.filter(v => v !== option.value));
|
||||
} else {
|
||||
onChange([...value, option.value]);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<MenuItem
|
||||
data-testid={`multi-select-${option.label}`}
|
||||
checked={selected}
|
||||
onClick={click}
|
||||
key={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</MenuItem>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar className={styles.scrollbar} />
|
||||
</Scrollable.Root>
|
||||
);
|
||||
}, [onChange, options, t, value]);
|
||||
|
||||
return (
|
||||
<Menu items={items}>
|
||||
<div className={styles.content}>
|
||||
<Tooltip
|
||||
content={
|
||||
content.length ? content : t['com.affine.filter.empty-tag']()
|
||||
}
|
||||
>
|
||||
{value.length ? (
|
||||
<div className={styles.text}>{content}</div>
|
||||
) : (
|
||||
<div className={clsx(styles.text, 'empty')}>
|
||||
{t['com.affine.filter.empty-tag']()}
|
||||
</div>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import type {
|
||||
Literal,
|
||||
LiteralValue,
|
||||
PropertiesMeta,
|
||||
VariableMap,
|
||||
} from '@affine/env/filter';
|
||||
import {
|
||||
CloudWorkspaceIcon,
|
||||
CreatedIcon,
|
||||
FavoriteIcon,
|
||||
TagsIcon,
|
||||
UpdatedIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { tBoolean, tDate, tTag } from './logical/custom-type';
|
||||
import type { TType } from './logical/typesystem';
|
||||
import { tArray } from './logical/typesystem';
|
||||
|
||||
export const toLiteral = (value: LiteralValue): Literal => ({
|
||||
type: 'literal',
|
||||
value,
|
||||
});
|
||||
|
||||
export type FilterVariable = {
|
||||
name: keyof VariableMap;
|
||||
type: (propertiesMeta: PropertiesMeta) => TType;
|
||||
icon: ReactElement;
|
||||
};
|
||||
|
||||
export const variableDefineMap = {
|
||||
Created: {
|
||||
type: () => tDate.create(),
|
||||
icon: <CreatedIcon />,
|
||||
},
|
||||
Updated: {
|
||||
type: () => tDate.create(),
|
||||
icon: <UpdatedIcon />,
|
||||
},
|
||||
'Is Favourited': {
|
||||
type: () => tBoolean.create(),
|
||||
icon: <FavoriteIcon />,
|
||||
},
|
||||
Tags: {
|
||||
type: meta =>
|
||||
tArray(
|
||||
tTag.create({
|
||||
tags:
|
||||
meta.tags?.options.map(t => ({
|
||||
id: t.id,
|
||||
name: t.value,
|
||||
color: t.color,
|
||||
})) ?? [],
|
||||
})
|
||||
),
|
||||
icon: <TagsIcon />,
|
||||
},
|
||||
'Is Public': {
|
||||
type: () => tBoolean.create(),
|
||||
icon: <CloudWorkspaceIcon />,
|
||||
},
|
||||
// Imported: {
|
||||
// type: tBoolean.create(),
|
||||
// },
|
||||
// 'Daily Note': {
|
||||
// type: tBoolean.create(),
|
||||
// },
|
||||
} satisfies Record<string, Omit<FilterVariable, 'name'>>;
|
||||
|
||||
export type InternalVariableMap = {
|
||||
[K in keyof typeof variableDefineMap]: LiteralValue;
|
||||
};
|
||||
|
||||
declare module '@affine/env/filter' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface VariableMap extends InternalVariableMap {}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
|
||||
export const createTagFilter = (id: string): Filter => {
|
||||
return {
|
||||
type: 'filter',
|
||||
left: { type: 'ref', name: 'Tags' },
|
||||
funcName: 'contains all',
|
||||
args: [{ type: 'literal', value: [id] }],
|
||||
};
|
||||
};
|
||||
@@ -1,305 +0,0 @@
|
||||
import { MenuItem, MenuSeparator } from '@affine/component';
|
||||
import type {
|
||||
Filter,
|
||||
LiteralValue,
|
||||
PropertiesMeta,
|
||||
VariableMap,
|
||||
} from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import dayjs from 'dayjs';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { FilterTag } from './filter-tag-translation';
|
||||
import * as styles from './index.css';
|
||||
import { tBoolean, tDate, tDateRange, tTag } from './logical/custom-type';
|
||||
import { Matcher } from './logical/matcher';
|
||||
import type { TFunction } from './logical/typesystem';
|
||||
import {
|
||||
tArray,
|
||||
tFunction,
|
||||
tTypeRef,
|
||||
tTypeVar,
|
||||
typesystem,
|
||||
} from './logical/typesystem';
|
||||
import type { FilterVariable } from './shared-types';
|
||||
import { variableDefineMap } from './shared-types';
|
||||
|
||||
export const vars: FilterVariable[] = Object.entries(variableDefineMap).map(
|
||||
([key, value]) => ({
|
||||
name: key as keyof VariableMap,
|
||||
type: value.type,
|
||||
icon: value.icon,
|
||||
})
|
||||
);
|
||||
|
||||
export const createDefaultFilter = (
|
||||
variable: FilterVariable,
|
||||
propertiesMeta: PropertiesMeta
|
||||
): Filter => {
|
||||
const data = filterMatcher.match(variable.type(propertiesMeta));
|
||||
if (!data) {
|
||||
throw new Error('No matching function found');
|
||||
}
|
||||
return {
|
||||
type: 'filter',
|
||||
left: {
|
||||
type: 'ref',
|
||||
name: variable.name,
|
||||
},
|
||||
funcName: data.name,
|
||||
args: data.defaultArgs().map(value => ({
|
||||
type: 'literal',
|
||||
value,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
export const CreateFilterMenu = ({
|
||||
value,
|
||||
onChange,
|
||||
propertiesMeta,
|
||||
}: {
|
||||
value: Filter[];
|
||||
onChange: (value: Filter[]) => void;
|
||||
propertiesMeta: PropertiesMeta;
|
||||
}) => {
|
||||
return (
|
||||
<VariableSelect
|
||||
propertiesMeta={propertiesMeta}
|
||||
selected={value}
|
||||
onSelect={filter => {
|
||||
onChange([...value, filter]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const VariableSelect = ({
|
||||
onSelect,
|
||||
propertiesMeta,
|
||||
}: {
|
||||
selected: Filter[];
|
||||
onSelect: (value: Filter) => void;
|
||||
propertiesMeta: PropertiesMeta;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<div data-testid="variable-select">
|
||||
<div className={styles.variableSelectTitleStyle}>
|
||||
{t['com.affine.filter']()}
|
||||
</div>
|
||||
<MenuSeparator />
|
||||
{vars
|
||||
// .filter(v => !selected.find(filter => filter.left.name === v.name))
|
||||
.map(v => (
|
||||
<MenuItem
|
||||
prefixIcon={variableDefineMap[v.name].icon}
|
||||
key={v.name}
|
||||
onClick={() => {
|
||||
onSelect(createDefaultFilter(v, propertiesMeta));
|
||||
}}
|
||||
className={styles.menuItemStyle}
|
||||
>
|
||||
<div
|
||||
data-testid="variable-select-item"
|
||||
className={styles.menuItemTextStyle}
|
||||
>
|
||||
<FilterTag name={v.name} />
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export type FilterMatcherDataType = {
|
||||
name: string;
|
||||
defaultArgs: () => LiteralValue[];
|
||||
render?: (props: { ast: Filter }) => ReactNode;
|
||||
impl: (...args: (LiteralValue | undefined)[]) => boolean;
|
||||
};
|
||||
export const filterMatcher = new Matcher<FilterMatcherDataType, TFunction>(
|
||||
(type, target) => {
|
||||
const staticType = typesystem.subst(
|
||||
Object.fromEntries(type.typeVars?.map(v => [v.name, v.bound]) ?? []),
|
||||
type
|
||||
);
|
||||
const firstArg = staticType.args[0];
|
||||
return firstArg && typesystem.isSubtype(firstArg, target);
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
args: [tBoolean.create(), tBoolean.create()],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'is',
|
||||
defaultArgs: () => [true],
|
||||
impl: (value, target) => {
|
||||
return value === target;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
args: [tDate.create(), tDate.create()],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'after',
|
||||
defaultArgs: () => {
|
||||
return [dayjs().subtract(1, 'day').endOf('day').valueOf()];
|
||||
},
|
||||
impl: (date, target) => {
|
||||
if (typeof date !== 'number' || typeof target !== 'number') {
|
||||
throw new Error('argument type error');
|
||||
}
|
||||
return dayjs(date).isAfter(dayjs(target).endOf('day'));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
args: [tDate.create(), tDateRange.create()],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'last',
|
||||
defaultArgs: () => [30], // Default to the last 30 days
|
||||
impl: (date, n) => {
|
||||
if (typeof date !== 'number' || typeof n !== 'number') {
|
||||
throw new Error('Argument type error: date and n must be numbers');
|
||||
}
|
||||
const startDate = dayjs().subtract(n, 'day').startOf('day').valueOf();
|
||||
return date > startDate;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
args: [tDate.create(), tDate.create()],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'before',
|
||||
defaultArgs: () => [dayjs().endOf('day').valueOf()],
|
||||
impl: (date, target) => {
|
||||
if (typeof date !== 'number' || typeof target !== 'number') {
|
||||
throw new Error('argument type error');
|
||||
}
|
||||
return dayjs(date).isBefore(dayjs(target).startOf('day'));
|
||||
},
|
||||
}
|
||||
);
|
||||
const safeArray = (arr: unknown): LiteralValue[] => {
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
};
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
args: [tArray(tTag.create())],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'is not empty',
|
||||
defaultArgs: () => [],
|
||||
impl: tags => {
|
||||
const safeTags = safeArray(tags);
|
||||
return safeTags.length > 0;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
args: [tArray(tTag.create())],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'is empty',
|
||||
defaultArgs: () => [],
|
||||
impl: tags => {
|
||||
const safeTags = safeArray(tags);
|
||||
return safeTags.length === 0;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
typeVars: [tTypeVar('T', tTag.create())],
|
||||
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'contains all',
|
||||
defaultArgs: () => [],
|
||||
impl: (tags, target) => {
|
||||
if (!Array.isArray(target)) {
|
||||
return true;
|
||||
}
|
||||
const safeTags = safeArray(tags);
|
||||
return target.every(id => safeTags.includes(id));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
typeVars: [tTypeVar('T', tTag.create())],
|
||||
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'contains one of',
|
||||
defaultArgs: () => [],
|
||||
impl: (tags, target) => {
|
||||
if (!Array.isArray(target)) {
|
||||
return true;
|
||||
}
|
||||
const safeTags = safeArray(tags);
|
||||
return target.some(id => safeTags.includes(id));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
typeVars: [tTypeVar('T', tTag.create())],
|
||||
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'does not contains all',
|
||||
defaultArgs: () => [],
|
||||
impl: (tags, target) => {
|
||||
if (!Array.isArray(target)) {
|
||||
return true;
|
||||
}
|
||||
const safeTags = safeArray(tags);
|
||||
return !target.every(id => safeTags.includes(id));
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
filterMatcher.register(
|
||||
tFunction({
|
||||
typeVars: [tTypeVar('T', tTag.create())],
|
||||
args: [tArray(tTypeRef('T')), tArray(tTypeRef('T'))],
|
||||
rt: tBoolean.create(),
|
||||
}),
|
||||
{
|
||||
name: 'does not contains one of',
|
||||
defaultArgs: () => [],
|
||||
impl: (tags, target) => {
|
||||
if (!Array.isArray(target)) {
|
||||
return true;
|
||||
}
|
||||
const safeTags = safeArray(tags);
|
||||
return !target.some(id => safeTags.includes(id));
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -6,7 +6,6 @@ export * from './components/page-display-menu';
|
||||
export * from './docs';
|
||||
export * from './docs/page-list-item';
|
||||
export * from './docs/page-tags';
|
||||
export * from './filter';
|
||||
export * from './group-definitions';
|
||||
export * from './header-col-def';
|
||||
export * from './list';
|
||||
@@ -17,8 +16,6 @@ export * from './page-header';
|
||||
export * from './tags';
|
||||
export * from './types';
|
||||
export * from './use-all-doc-display-properties';
|
||||
export * from './use-collection-manager';
|
||||
export * from './use-filtered-page-metas';
|
||||
export * from './utils';
|
||||
export * from './view';
|
||||
export * from './virtualized-list';
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from '@affine/core/modules/favorite';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
@@ -38,8 +37,10 @@ import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||
import type { CollectionService } from '../../modules/collection';
|
||||
import {
|
||||
type CollectionMeta,
|
||||
CollectionService,
|
||||
} from '../../modules/collection';
|
||||
import { useGuard } from '../guard';
|
||||
import { IsFavoriteIcon } from '../pure/icons';
|
||||
import { FavoriteTag } from './components/favorite-tag';
|
||||
@@ -328,31 +329,28 @@ export const TrashOperationCell = ({
|
||||
};
|
||||
|
||||
export interface CollectionOperationCellProps {
|
||||
collection: Collection;
|
||||
info: DeleteCollectionInfo;
|
||||
service: CollectionService;
|
||||
collectionMeta: CollectionMeta;
|
||||
}
|
||||
|
||||
export const CollectionOperationCell = ({
|
||||
collection,
|
||||
service,
|
||||
info,
|
||||
collectionMeta,
|
||||
}: CollectionOperationCellProps) => {
|
||||
const t = useI18n();
|
||||
const {
|
||||
compatibleFavoriteItemsAdapter: favAdapter,
|
||||
workspaceService,
|
||||
workspaceDialogService,
|
||||
collectionService,
|
||||
docsService,
|
||||
} = useServices({
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
WorkspaceService,
|
||||
WorkspaceDialogService,
|
||||
CollectionService,
|
||||
DocsService,
|
||||
});
|
||||
const docCollection = workspaceService.workspace.docCollection;
|
||||
const { createPage } = usePageHelper(docCollection);
|
||||
const collectionId = collectionMeta.id;
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const favourite = useLiveData(
|
||||
favAdapter.isFavorite$(collection.id, 'collection')
|
||||
favAdapter.isFavorite$(collectionId, 'collection')
|
||||
);
|
||||
|
||||
const { openPromptModal } = usePromptModal();
|
||||
@@ -377,44 +375,43 @@ export const CollectionOperationCell = ({
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
service.updateCollection(collection.id, () => ({
|
||||
...collection,
|
||||
collectionService.updateCollection(collectionId, {
|
||||
name,
|
||||
}));
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
[collection, handlePropagation, openPromptModal, service, t]
|
||||
[collectionId, collectionService, handlePropagation, openPromptModal, t]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
handlePropagation(event);
|
||||
workspaceDialogService.open('collection-editor', {
|
||||
collectionId: collection.id,
|
||||
collectionId: collectionId,
|
||||
});
|
||||
},
|
||||
[handlePropagation, workspaceDialogService, collection.id]
|
||||
[handlePropagation, workspaceDialogService, collectionId]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
return service.deleteCollection(info, collection.id);
|
||||
}, [service, info, collection]);
|
||||
return collectionService.deleteCollection(collectionId);
|
||||
}, [collectionId, collectionService]);
|
||||
|
||||
const onToggleFavoriteCollection = useCallback(() => {
|
||||
const status = favAdapter.isFavorite(collection.id, 'collection');
|
||||
favAdapter.toggle(collection.id, 'collection');
|
||||
const status = favAdapter.isFavorite(collectionId, 'collection');
|
||||
favAdapter.toggle(collectionId, 'collection');
|
||||
toast(
|
||||
status
|
||||
? t['com.affine.toastMessage.removedFavorites']()
|
||||
: t['com.affine.toastMessage.addedFavorites']()
|
||||
);
|
||||
}, [favAdapter, collection.id, t]);
|
||||
}, [favAdapter, collectionId, t]);
|
||||
|
||||
const createAndAddDocument = useCallback(() => {
|
||||
const newDoc = createPage();
|
||||
service.addPageToCollection(collection.id, newDoc.id);
|
||||
}, [collection.id, createPage, service]);
|
||||
const newDoc = docsService.createDoc();
|
||||
collectionService.addDocToCollection(collectionId, newDoc.id);
|
||||
}, [docsService, collectionService, collectionId]);
|
||||
|
||||
const onConfirmAddDocToCollection = useCallback(() => {
|
||||
openConfirmModal({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { shallowEqual } from '@affine/component';
|
||||
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
import { TagListItem } from './tags/tag-list-item';
|
||||
import type {
|
||||
CollectionListItemProps,
|
||||
CollectionMeta,
|
||||
ItemGroupProps,
|
||||
ListItem,
|
||||
ListProps,
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { CollectionMeta } from '@affine/core/modules/collection';
|
||||
import type { DocMeta, Workspace } from '@blocksuite/affine/store';
|
||||
import type { JSX, PropsWithChildren, ReactNode } from 'react';
|
||||
import type { To } from 'react-router-dom';
|
||||
|
||||
export type ListItem = DocMeta | CollectionMeta | TagMeta;
|
||||
|
||||
export interface CollectionMeta extends Collection {
|
||||
title: string;
|
||||
createDate?: Date | number;
|
||||
updatedDate?: Date | number;
|
||||
}
|
||||
export type ListItem =
|
||||
| DocMeta
|
||||
| (CollectionMeta & {
|
||||
createDate?: Date | number;
|
||||
updatedDate?: Date | number;
|
||||
})
|
||||
| TagMeta;
|
||||
|
||||
export type TagMeta = {
|
||||
id: string;
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { Collection, Filter, VariableMap } from '@affine/env/filter';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
|
||||
import { evalFilterList } from './filter';
|
||||
|
||||
export const createEmptyCollection = (
|
||||
id: string,
|
||||
data?: Partial<Omit<Collection, 'id'>>
|
||||
): Collection => {
|
||||
return {
|
||||
id,
|
||||
name: '',
|
||||
filterList: [],
|
||||
allowList: [],
|
||||
...data,
|
||||
};
|
||||
};
|
||||
|
||||
export const filterByFilterList = (filterList: Filter[], varMap: VariableMap) =>
|
||||
evalFilterList(filterList, varMap);
|
||||
|
||||
export type PageDataForFilter = {
|
||||
meta: DocMeta;
|
||||
favorite: boolean;
|
||||
publicMode: undefined | 'page' | 'edgeless';
|
||||
};
|
||||
|
||||
export const filterPage = (collection: Collection, page: PageDataForFilter) => {
|
||||
if (collection.filterList.length === 0) {
|
||||
return collection.allowList.includes(page.meta.id);
|
||||
}
|
||||
return filterPageByRules(collection.filterList, collection.allowList, page);
|
||||
};
|
||||
export const filterPageByRules = (
|
||||
rules: Filter[],
|
||||
allowList: string[],
|
||||
{ meta, publicMode, favorite }: PageDataForFilter
|
||||
) => {
|
||||
if (allowList?.includes(meta.id)) {
|
||||
return true;
|
||||
}
|
||||
return filterByFilterList(rules, {
|
||||
'Is Favourited': !!favorite,
|
||||
'Is Public': !!publicMode,
|
||||
Created: meta.createDate,
|
||||
Updated: meta.updatedDate ?? meta.createDate,
|
||||
Tags: meta.tags,
|
||||
});
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||
import type { Collection, Filter } from '@affine/env/filter';
|
||||
import { PublicDocMode } from '@affine/graphql';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { filterPage, filterPageByRules } from './use-collection-manager';
|
||||
|
||||
export const useFilteredPageMetas = (
|
||||
pageMetas: DocMeta[],
|
||||
options: {
|
||||
trash?: boolean;
|
||||
filters?: Filter[];
|
||||
collection?: Collection;
|
||||
} = {}
|
||||
) => {
|
||||
const shareDocsListService = useService(ShareDocsListService);
|
||||
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
||||
|
||||
const getPublicMode = useCallback(
|
||||
(id: string) => {
|
||||
const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode;
|
||||
return mode
|
||||
? mode === PublicDocMode.Edgeless
|
||||
? ('edgeless' as const)
|
||||
: ('page' as const)
|
||||
: undefined;
|
||||
},
|
||||
[shareDocs]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO(@eyhn): loading & error UI
|
||||
shareDocsListService.shareDocs?.revalidate();
|
||||
}, [shareDocsListService]);
|
||||
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const favoriteItems = useLiveData(favAdapter.favorites$);
|
||||
|
||||
const filteredPageMetas = useMemo(
|
||||
() =>
|
||||
pageMetas.filter(pageMeta => {
|
||||
if (options.trash) {
|
||||
if (!pageMeta.trash) {
|
||||
return false;
|
||||
}
|
||||
} else if (pageMeta.trash) {
|
||||
return false;
|
||||
}
|
||||
const pageData = {
|
||||
meta: pageMeta,
|
||||
favorite: favoriteItems.some(fav => fav.id === pageMeta.id),
|
||||
publicMode: getPublicMode(pageMeta.id),
|
||||
};
|
||||
if (
|
||||
options.filters &&
|
||||
!filterPageByRules(options.filters, [], pageData)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.collection && !filterPage(options.collection, pageData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
[
|
||||
pageMetas,
|
||||
options.trash,
|
||||
options.filters,
|
||||
options.collection,
|
||||
favoriteItems,
|
||||
getPublicMode,
|
||||
]
|
||||
);
|
||||
|
||||
return filteredPageMetas;
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const menuTitleStyle = style({
|
||||
marginLeft: '12px',
|
||||
marginTop: '10px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
});
|
||||
export const menuDividerStyle = style({
|
||||
marginTop: '2px',
|
||||
marginBottom: '2px',
|
||||
marginLeft: '12px',
|
||||
marginRight: '8px',
|
||||
height: '1px',
|
||||
background: cssVar('borderColor'),
|
||||
});
|
||||
export const viewMenu = style({});
|
||||
export const viewOption = style({
|
||||
borderRadius: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 6,
|
||||
width: 24,
|
||||
height: 24,
|
||||
opacity: 0,
|
||||
':hover': {
|
||||
backgroundColor: cssVar('hoverColor'),
|
||||
},
|
||||
selectors: {
|
||||
[`${viewMenu}:hover &`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const filterMenuTrigger = style({
|
||||
padding: '6px 8px',
|
||||
selectors: {
|
||||
[`&[data-is-hidden="true"]`]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Button, FlexWrapper, Menu } from '@affine/component';
|
||||
import type { Filter, PropertiesMeta } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { FilterIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { CreateFilterMenu } from '../filter/vars';
|
||||
import * as styles from './collection-list.css';
|
||||
|
||||
export const AllPageListOperationsMenu = ({
|
||||
propertiesMeta,
|
||||
filterList,
|
||||
onChangeFilterList,
|
||||
}: {
|
||||
propertiesMeta: PropertiesMeta;
|
||||
filterList: Filter[];
|
||||
onChangeFilterList: (filterList: Filter[]) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<FlexWrapper alignItems="center">
|
||||
<Menu
|
||||
items={
|
||||
<CreateFilterMenu
|
||||
propertiesMeta={propertiesMeta}
|
||||
value={filterList}
|
||||
onChange={onChangeFilterList}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
className={styles.filterMenuTrigger}
|
||||
prefix={<FilterIcon />}
|
||||
data-testid="create-first-filter"
|
||||
>
|
||||
{t['com.affine.filter']()}
|
||||
</Button>
|
||||
</Menu>
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,8 @@
|
||||
import type { MenuItemProps } from '@affine/component';
|
||||
import { Menu, MenuItem, usePromptModal } from '@affine/component';
|
||||
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
DeleteIcon,
|
||||
@@ -18,7 +16,10 @@ import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { CollectionService } from '../../../modules/collection';
|
||||
import {
|
||||
type Collection,
|
||||
CollectionService,
|
||||
} from '../../../modules/collection';
|
||||
import { IsFavoriteIcon } from '../../pure/icons';
|
||||
import * as styles from './collection-operations.css';
|
||||
|
||||
@@ -41,7 +42,6 @@ export const CollectionOperations = ({
|
||||
WorkbenchService,
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
const deleteInfo = useDeleteCollectionInfo();
|
||||
const workbench = workbenchService.workbench;
|
||||
const t = useI18n();
|
||||
const { openPromptModal } = usePromptModal();
|
||||
@@ -63,10 +63,9 @@ export const CollectionOperations = ({
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
service.updateCollection(collection.id, () => ({
|
||||
...collection,
|
||||
service.updateCollection(collection.id, {
|
||||
name,
|
||||
}));
|
||||
});
|
||||
},
|
||||
});
|
||||
}, [openRenameModal, openPromptModal, t, service, collection]);
|
||||
@@ -160,7 +159,7 @@ export const CollectionOperations = ({
|
||||
icon: <DeleteIcon />,
|
||||
name: t['Delete'](),
|
||||
click: () => {
|
||||
service.deleteCollection(deleteInfo, collection.id);
|
||||
service.deleteCollection(collection.id);
|
||||
},
|
||||
type: 'danger',
|
||||
},
|
||||
@@ -175,7 +174,6 @@ export const CollectionOperations = ({
|
||||
openCollectionNewTab,
|
||||
openCollectionSplitView,
|
||||
service,
|
||||
deleteInfo,
|
||||
collection.id,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './affine-shape';
|
||||
export * from './collection-list';
|
||||
export * from './collection-operations';
|
||||
export * from './create-collection';
|
||||
export * from './save-as-collection-button';
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Button, usePromptModal } from '@affine/component';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { SaveIcon } from '@blocksuite/icons/rc';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { createEmptyCollection } from '../use-collection-manager';
|
||||
import * as styles from './save-as-collection-button.css';
|
||||
|
||||
interface SaveAsCollectionButtonProps {
|
||||
onConfirm: (collection: Collection) => void;
|
||||
onConfirm: (collectionName: string) => void;
|
||||
}
|
||||
|
||||
export const SaveAsCollectionButton = ({
|
||||
@@ -35,7 +32,7 @@ export const SaveAsCollectionButton = ({
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
onConfirm(createEmptyCollection(nanoid(), { name }));
|
||||
onConfirm(name);
|
||||
},
|
||||
});
|
||||
}, [openPromptModal, t, onConfirm]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { Collection } from '@affine/core/modules/collection';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { DeleteIcon, FilterIcon } from '@blocksuite/icons/rc';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { toast, useConfirmModal } from '@affine/component';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { LiveData, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { ListFloatingToolbar } from './components/list-floating-toolbar';
|
||||
@@ -14,7 +15,6 @@ import { TrashOperationCell } from './operation-cell';
|
||||
import { PageListItemRenderer } from './page-group';
|
||||
import { ListTableHeader } from './page-header';
|
||||
import type { ItemListHandle, ListItem } from './types';
|
||||
import { useFilteredPageMetas } from './use-filtered-page-metas';
|
||||
import { VirtualizedList } from './virtualized-list';
|
||||
|
||||
export const VirtualizedTrashList = ({
|
||||
@@ -25,13 +25,17 @@ export const VirtualizedTrashList = ({
|
||||
disableMultiRestore?: boolean;
|
||||
}) => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const docsService = useService(DocsService);
|
||||
const guardService = useService(GuardService);
|
||||
const docCollection = currentWorkspace.docCollection;
|
||||
const { restoreFromTrash, permanentlyDeletePage } = useBlockSuiteMetaHelper();
|
||||
const allTrashPageIds = useLiveData(
|
||||
LiveData.from(docsService.allTrashDocIds$(), [])
|
||||
);
|
||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
||||
trash: true,
|
||||
});
|
||||
const filteredPageMetas = useMemo(() => {
|
||||
return pageMetas.filter(page => allTrashPageIds.includes(page.id));
|
||||
}, [pageMetas, allTrashPageIds]);
|
||||
|
||||
const listRef = useRef<ItemListHandle>(null);
|
||||
const [showFloatingToolbar, setShowFloatingToolbar] = useState(false);
|
||||
|
||||
@@ -83,7 +83,7 @@ const AccountMenu = () => {
|
||||
const openSignOutModal = useSignOut();
|
||||
const serverService = useService(ServerService);
|
||||
const userFeatureService = useService(UserFeatureService);
|
||||
const isAdmin = useLiveData(userFeatureService.userFeature.isAdmin$);
|
||||
const isAFFiNEAdmin = useLiveData(userFeatureService.userFeature.isAdmin$);
|
||||
|
||||
const onOpenAccountSetting = useCallback(() => {
|
||||
track.$.navigationPanel.profileAndBadge.openSettings({ to: 'account' });
|
||||
@@ -111,7 +111,7 @@ const AccountMenu = () => {
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.settings']()}
|
||||
</MenuItem>
|
||||
{isAdmin ? (
|
||||
{isAFFiNEAdmin ? (
|
||||
<MenuItem
|
||||
prefixIcon={<AdminIcon />}
|
||||
data-testid="workspace-modal-account-admin-option"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||
|
||||
export const FavoriteFilterValue = ({
|
||||
filter,
|
||||
onChange,
|
||||
}: {
|
||||
filter: FilterParams;
|
||||
onChange: (filter: FilterParams) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Menu
|
||||
items={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...filter,
|
||||
value: 'true',
|
||||
});
|
||||
}}
|
||||
selected={filter.value === 'true'}
|
||||
>
|
||||
{'True'}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...filter,
|
||||
value: 'false',
|
||||
});
|
||||
}}
|
||||
selected={filter.value !== 'true'}
|
||||
>
|
||||
{'False'}
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span>{filter.value === 'true' ? 'True' : 'False'}</span>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||
import type { I18nString } from '@affine/i18n';
|
||||
import { TagIcon } from '@blocksuite/icons/rc';
|
||||
import { FavoriteIcon, ShareIcon, TagIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { FavoriteFilterValue } from './favorite';
|
||||
import { SharedFilterValue } from './shared';
|
||||
import { TagsFilterValue } from './tags';
|
||||
|
||||
export const SystemPropertyTypes = {
|
||||
@@ -15,6 +17,22 @@ export const SystemPropertyTypes = {
|
||||
},
|
||||
filterValue: TagsFilterValue,
|
||||
},
|
||||
favorite: {
|
||||
icon: FavoriteIcon,
|
||||
name: 'Favorited',
|
||||
filterMethod: {
|
||||
is: 'com.affine.filter.is',
|
||||
},
|
||||
filterValue: FavoriteFilterValue,
|
||||
},
|
||||
shared: {
|
||||
icon: ShareIcon,
|
||||
name: 'Shared',
|
||||
filterMethod: {
|
||||
is: 'com.affine.filter.is',
|
||||
},
|
||||
filterValue: SharedFilterValue,
|
||||
},
|
||||
} satisfies {
|
||||
[type: string]: {
|
||||
icon: React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import type { FilterParams } from '@affine/core/modules/collection-rules';
|
||||
|
||||
export const SharedFilterValue = ({
|
||||
filter,
|
||||
onChange,
|
||||
}: {
|
||||
filter: FilterParams;
|
||||
onChange: (filter: FilterParams) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Menu
|
||||
items={
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...filter,
|
||||
value: 'true',
|
||||
});
|
||||
}}
|
||||
selected={filter.value === 'true'}
|
||||
>
|
||||
{'True'}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...filter,
|
||||
value: 'false',
|
||||
});
|
||||
}}
|
||||
selected={filter.value !== 'true'}
|
||||
>
|
||||
{'False'}
|
||||
</MenuItem>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<span>{filter.value === 'true' ? 'True' : 'False'}</span>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -102,7 +102,10 @@ export const WorkspacePropertyTypes = {
|
||||
renameable: false,
|
||||
description: 'com.affine.page-properties.property.tags.tooltips',
|
||||
filterMethod: {
|
||||
include: 'com.affine.filter.contains all',
|
||||
'include-all': 'com.affine.filter.contains all',
|
||||
'include-any-of': 'com.affine.filter.contains one of',
|
||||
'not-include-all': 'com.affine.filter.does not contains all',
|
||||
'not-include-any-of': 'com.affine.filter.does not contains one of',
|
||||
'is-not-empty': 'com.affine.filter.is not empty',
|
||||
'is-empty': 'com.affine.filter.is empty',
|
||||
},
|
||||
|
||||
+30
-72
@@ -5,26 +5,17 @@ import {
|
||||
MenuItem,
|
||||
toast,
|
||||
} from '@affine/component';
|
||||
import { filterPage } from '@affine/core/components/page-list';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import {
|
||||
type Collection,
|
||||
CollectionService,
|
||||
} from '@affine/core/modules/collection';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { ShareDocsListService } from '@affine/core/modules/share-doc';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { PublicDocMode } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { FilterMinusIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
LiveData,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import {
|
||||
@@ -71,6 +62,7 @@ export const NavigationPanelCollectionNode = ({
|
||||
|
||||
const collectionService = useService(CollectionService);
|
||||
const collection = useLiveData(collectionService.collection$(collectionId));
|
||||
const name = useLiveData(collection?.name$);
|
||||
|
||||
const dndData = useMemo(() => {
|
||||
return {
|
||||
@@ -89,11 +81,10 @@ export const NavigationPanelCollectionNode = ({
|
||||
|
||||
const handleRename = useCallback(
|
||||
(name: string) => {
|
||||
if (collection && collection.name !== name) {
|
||||
collectionService.updateCollection(collectionId, () => ({
|
||||
...collection,
|
||||
if (collection && collection.name$.value !== name) {
|
||||
collectionService.updateCollection(collectionId, {
|
||||
name,
|
||||
}));
|
||||
});
|
||||
|
||||
track.$.navigationPanel.organize.renameOrganizeItem({
|
||||
type: 'collection',
|
||||
@@ -109,10 +100,10 @@ export const NavigationPanelCollectionNode = ({
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
if (collection.allowList.includes(docId)) {
|
||||
if (collection.allowList$.value.includes(docId)) {
|
||||
toast(t['com.affine.collection.addPage.alreadyExists']());
|
||||
} else {
|
||||
collectionService.addPageToCollection(collection.id, docId);
|
||||
collectionService.addDocToCollection(collection.id, docId);
|
||||
}
|
||||
},
|
||||
[collection, collectionService, t]
|
||||
@@ -210,7 +201,7 @@ export const NavigationPanelCollectionNode = ({
|
||||
return (
|
||||
<NavigationPanelTreeNode
|
||||
icon={CollectionIcon}
|
||||
name={collection.name || t['Untitled']()}
|
||||
name={name || t['Untitled']()}
|
||||
dndData={dndData}
|
||||
onDrop={handleDropOnCollection}
|
||||
renameable
|
||||
@@ -237,84 +228,51 @@ const NavigationPanelCollectionNodeChildren = ({
|
||||
collection: Collection;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const {
|
||||
docsService,
|
||||
compatibleFavoriteItemsAdapter,
|
||||
shareDocsListService,
|
||||
collectionService,
|
||||
} = useServices({
|
||||
DocsService,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
ShareDocsListService,
|
||||
const { collectionService } = useServices({
|
||||
CollectionService,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// TODO(@eyhn): loading & error UI
|
||||
shareDocsListService.shareDocs?.revalidate();
|
||||
}, [shareDocsListService]);
|
||||
|
||||
const docMetas = useLiveData(
|
||||
useMemo(
|
||||
() =>
|
||||
LiveData.computed(get => {
|
||||
return get(docsService.list.docs$).map(
|
||||
doc => get(doc.meta$) as DocMeta
|
||||
);
|
||||
}),
|
||||
[docsService]
|
||||
)
|
||||
const allowList = useLiveData(
|
||||
collection.allowList$.map(list => new Set(list))
|
||||
);
|
||||
const favourites = useLiveData(compatibleFavoriteItemsAdapter.favorites$);
|
||||
const allowList = useMemo(
|
||||
() => new Set(collection.allowList),
|
||||
[collection.allowList]
|
||||
);
|
||||
const shareDocs = useLiveData(shareDocsListService.shareDocs?.list$);
|
||||
|
||||
const handleRemoveFromAllowList = useCallback(
|
||||
(id: string) => {
|
||||
track.$.navigationPanel.collections.removeOrganizeItem({ type: 'doc' });
|
||||
collectionService.deletePageFromCollection(collection.id, id);
|
||||
collectionService.removeDocFromCollection(collection.id, id);
|
||||
toast(t['com.affine.collection.removePage.success']());
|
||||
},
|
||||
[collection.id, collectionService, t]
|
||||
);
|
||||
|
||||
const filtered = docMetas.filter(meta => {
|
||||
if (meta.trash) return false;
|
||||
const publicMode = shareDocs?.find(d => d.id === meta.id)?.mode;
|
||||
const pageData = {
|
||||
meta: meta as DocMeta,
|
||||
publicMode:
|
||||
publicMode === PublicDocMode.Edgeless
|
||||
? ('edgeless' as const)
|
||||
: publicMode === PublicDocMode.Page
|
||||
? ('page' as const)
|
||||
: undefined,
|
||||
favorite: favourites.some(fav => fav.id === meta.id),
|
||||
};
|
||||
return filterPage(collection, pageData);
|
||||
});
|
||||
const [filteredDocIds, setFilteredDocIds] = useState<string[]>([]);
|
||||
|
||||
return filtered.map(doc => (
|
||||
useEffect(() => {
|
||||
const subscription = collection.watch().subscribe(docIds => {
|
||||
setFilteredDocIds(docIds);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [collection]);
|
||||
|
||||
return filteredDocIds.map(docId => (
|
||||
<NavigationPanelDocNode
|
||||
key={doc.id}
|
||||
docId={doc.id}
|
||||
key={docId}
|
||||
docId={docId}
|
||||
reorderable={false}
|
||||
location={{
|
||||
at: 'navigation-panel:collection:filtered-docs',
|
||||
collectionId: collection.id,
|
||||
}}
|
||||
operations={
|
||||
allowList
|
||||
allowList.has(docId)
|
||||
? [
|
||||
{
|
||||
index: 99,
|
||||
view: (
|
||||
<MenuItem
|
||||
prefixIcon={<FilterMinusIcon />}
|
||||
onClick={() => handleRemoveFromAllowList(doc.id)}
|
||||
onClick={() => handleRemoveFromAllowList(docId)}
|
||||
>
|
||||
{t['Remove special filter']()}
|
||||
</MenuItem>
|
||||
|
||||
+3
-5
@@ -5,7 +5,6 @@ import {
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
|
||||
import { useDeleteCollectionInfo } from '@affine/core/components/hooks/affine/use-delete-collection-info';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
@@ -42,7 +41,6 @@ export const useNavigationPanelCollectionNodeOperations = (
|
||||
CollectionService,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
});
|
||||
const deleteInfo = useDeleteCollectionInfo();
|
||||
|
||||
const { createPage } = usePageHelper(
|
||||
workspaceService.workspace.docCollection
|
||||
@@ -59,7 +57,7 @@ export const useNavigationPanelCollectionNodeOperations = (
|
||||
|
||||
const createAndAddDocument = useCallback(() => {
|
||||
const newDoc = createPage();
|
||||
collectionService.addPageToCollection(collectionId, newDoc.id);
|
||||
collectionService.addDocToCollection(collectionId, newDoc.id);
|
||||
track.$.navigationPanel.collections.createDoc();
|
||||
track.$.navigationPanel.collections.addDocToCollection({
|
||||
control: 'button',
|
||||
@@ -100,11 +98,11 @@ export const useNavigationPanelCollectionNodeOperations = (
|
||||
}, [collectionId, workbenchService.workbench]);
|
||||
|
||||
const handleDeleteCollection = useCallback(() => {
|
||||
collectionService.deleteCollection(deleteInfo, collectionId);
|
||||
collectionService.deleteCollection(collectionId);
|
||||
track.$.navigationPanel.organize.deleteOrganizeItem({
|
||||
type: 'collection',
|
||||
});
|
||||
}, [collectionId, collectionService, deleteInfo]);
|
||||
}, [collectionId, collectionService]);
|
||||
|
||||
const handleShowEdit = useCallback(() => {
|
||||
onOpenEdit();
|
||||
|
||||
+2
-5
@@ -1,5 +1,4 @@
|
||||
import { IconButton, usePromptModal } from '@affine/component';
|
||||
import { createEmptyCollection } from '@affine/core/components/page-list/use-collection-manager';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { NavigationPanelService } from '@affine/core/modules/navigation-panel';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
@@ -7,7 +6,6 @@ import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { AddCollectionIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { CollapsibleSection } from '../../layouts/collapsible-section';
|
||||
@@ -46,8 +44,7 @@ export const NavigationPanelCollections = () => {
|
||||
variant: 'primary',
|
||||
},
|
||||
onConfirm(name) {
|
||||
const id = nanoid();
|
||||
collectionService.addCollection(createEmptyCollection(id, { name }));
|
||||
const id = collectionService.createCollection({ name });
|
||||
track.$.navigationPanel.organize.createOrganizeItem({
|
||||
type: 'collection',
|
||||
});
|
||||
@@ -84,7 +81,7 @@ export const NavigationPanelCollections = () => {
|
||||
<NavigationPanelTreeRoot
|
||||
placeholder={<RootEmpty onClickCreate={handleCreateCollection} />}
|
||||
>
|
||||
{collections.map(collection => (
|
||||
{Array.from(collections.values()).map(collection => (
|
||||
<NavigationPanelCollectionNode
|
||||
key={collection.id}
|
||||
collectionId={collection.id}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, RadioGroup } from '@affine/component';
|
||||
import { useAllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
||||
import { SelectPage } from '@affine/core/components/page-list/docs/select-page';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { CollectionInfo } from '@affine/core/modules/collection';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
@@ -12,10 +12,10 @@ export type EditCollectionMode = 'page' | 'rule';
|
||||
|
||||
export interface EditCollectionProps {
|
||||
onConfirmText?: string;
|
||||
init: Collection;
|
||||
init: CollectionInfo;
|
||||
mode?: EditCollectionMode;
|
||||
onCancel: () => void;
|
||||
onConfirm: (collection: Collection) => void;
|
||||
onConfirm: (collection: CollectionInfo) => void;
|
||||
}
|
||||
|
||||
export const EditCollection = ({
|
||||
@@ -27,9 +27,9 @@ export const EditCollection = ({
|
||||
}: EditCollectionProps) => {
|
||||
const t = useI18n();
|
||||
const config = useAllPageListConfig();
|
||||
const [value, onChange] = useState<Collection>(init);
|
||||
const [value, onChange] = useState<CollectionInfo>(init);
|
||||
const [mode, setMode] = useState<'page' | 'rule'>(
|
||||
initMode ?? (init.filterList.length === 0 ? 'page' : 'rule')
|
||||
initMode ?? (init.rules.filters.length === 0 ? 'page' : 'rule')
|
||||
);
|
||||
const isNameEmpty = useMemo(() => value.name.trim().length === 0, [value]);
|
||||
const onSaveCollection = useCallback(() => {
|
||||
@@ -40,10 +40,10 @@ export const EditCollection = ({
|
||||
const reset = useCallback(() => {
|
||||
onChange({
|
||||
...value,
|
||||
filterList: init.filterList,
|
||||
rules: init.rules,
|
||||
allowList: init.allowList,
|
||||
});
|
||||
}, [init.allowList, init.filterList, value]);
|
||||
}, [init, value]);
|
||||
const onIdsChange = useCallback(
|
||||
(ids: string[]) => {
|
||||
onChange({ ...value, allowList: ids });
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Modal } from '@affine/component';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import {
|
||||
type CollectionInfo,
|
||||
CollectionService,
|
||||
} from '@affine/core/modules/collection';
|
||||
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
|
||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
@@ -18,17 +20,18 @@ export const CollectionEditorDialog = ({
|
||||
const collectionService = useService(CollectionService);
|
||||
const collection = useLiveData(collectionService.collection$(collectionId));
|
||||
const onConfirmOnCollection = useCallback(
|
||||
(collection: Collection) => {
|
||||
collectionService.updateCollection(collection.id, () => collection);
|
||||
(collection: CollectionInfo) => {
|
||||
collectionService.updateCollection(collection.id, collection);
|
||||
close();
|
||||
},
|
||||
[close, collectionService]
|
||||
);
|
||||
const info = useLiveData(collection?.info$);
|
||||
const onCancel = useCallback(() => {
|
||||
close();
|
||||
}, [close]);
|
||||
|
||||
if (!collection) {
|
||||
if (!collection || !info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -50,7 +53,7 @@ export const CollectionEditorDialog = ({
|
||||
>
|
||||
<EditCollection
|
||||
onConfirmText={t['com.affine.editCollection.save']()}
|
||||
init={collection}
|
||||
init={info}
|
||||
mode={mode}
|
||||
onCancel={onCancel}
|
||||
onConfirm={onConfirmOnCollection}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Button, IconButton, Tooltip } from '@affine/component';
|
||||
import { Filters } from '@affine/core/components/filter';
|
||||
import type { AllPageListConfig } from '@affine/core/components/hooks/affine/use-all-page-list-config';
|
||||
import {
|
||||
AffineShapeIcon,
|
||||
FilterList,
|
||||
filterPageByRules,
|
||||
List,
|
||||
type ListItem,
|
||||
ListScrollContainer,
|
||||
} from '@affine/core/components/page-list';
|
||||
import type { CollectionInfo } from '@affine/core/modules/collection';
|
||||
import { CollectionRulesService } from '@affine/core/modules/collection-rules';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import {
|
||||
@@ -19,11 +18,11 @@ import {
|
||||
PageIcon,
|
||||
ToggleRightIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import clsx from 'clsx';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import * as styles from './edit-collection.css';
|
||||
|
||||
@@ -35,8 +34,8 @@ export const RulesMode = ({
|
||||
switchMode,
|
||||
allPageListConfig,
|
||||
}: {
|
||||
collection: Collection;
|
||||
updateCollection: (collection: Collection) => void;
|
||||
collection: CollectionInfo;
|
||||
updateCollection: (collection: CollectionInfo) => void;
|
||||
reset: () => void;
|
||||
buttons: ReactNode;
|
||||
switchMode: ReactNode;
|
||||
@@ -44,30 +43,50 @@ export const RulesMode = ({
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [showPreview, setShowPreview] = useState(true);
|
||||
const allowListPages: DocMeta[] = [];
|
||||
const rulesPages: DocMeta[] = [];
|
||||
const docsService = useService(DocsService);
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const favorites = useLiveData(favAdapter.favorites$);
|
||||
allPageListConfig.allPages.forEach(meta => {
|
||||
if (meta.trash) {
|
||||
return;
|
||||
}
|
||||
const pageData = {
|
||||
meta,
|
||||
publicMode: allPageListConfig.getPublicMode(meta.id),
|
||||
favorite: favorites.some(f => f.id === meta.id),
|
||||
const collectionRulesService = useService(CollectionRulesService);
|
||||
const [rulesPageIds, setRulesPageIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = collectionRulesService
|
||||
.watch(
|
||||
collection.rules.filters.length > 0
|
||||
? [
|
||||
...collection.rules.filters,
|
||||
{
|
||||
type: 'system',
|
||||
key: 'trash',
|
||||
method: 'is',
|
||||
value: 'false',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
.subscribe(rules => {
|
||||
setRulesPageIds(rules.groups.flatMap(group => group.items));
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
if (
|
||||
collection.filterList.length &&
|
||||
filterPageByRules(collection.filterList, [], pageData)
|
||||
) {
|
||||
rulesPages.push(meta);
|
||||
}
|
||||
if (collection.allowList.includes(meta.id)) {
|
||||
allowListPages.push(meta);
|
||||
}
|
||||
});
|
||||
}, [collection, collectionRulesService]);
|
||||
|
||||
const rulesPages = useMemo(() => {
|
||||
return allPageListConfig.allPages.filter(meta => {
|
||||
return rulesPageIds.includes(meta.id);
|
||||
});
|
||||
}, [allPageListConfig.allPages, rulesPageIds]);
|
||||
|
||||
const allowListPages = useMemo(() => {
|
||||
return allPageListConfig.allPages.filter(meta => {
|
||||
return (
|
||||
collection.allowList.includes(meta.id) &&
|
||||
!rulesPageIds.includes(meta.id)
|
||||
);
|
||||
});
|
||||
}, [allPageListConfig.allPages, collection.allowList, rulesPageIds]);
|
||||
|
||||
const [expandInclude, setExpandInclude] = useState(
|
||||
collection.allowList.length > 0
|
||||
);
|
||||
@@ -113,13 +132,17 @@ export const RulesMode = ({
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<FilterList
|
||||
propertiesMeta={allPageListConfig.docCollection.meta.properties}
|
||||
value={collection.filterList}
|
||||
onChange={useCallback(
|
||||
filterList => updateCollection({ ...collection, filterList }),
|
||||
[collection, updateCollection]
|
||||
)}
|
||||
<Filters
|
||||
filters={collection.rules.filters}
|
||||
onChange={filters => {
|
||||
updateCollection({
|
||||
...collection,
|
||||
rules: {
|
||||
...collection.rules,
|
||||
filters,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className={styles.rulesContainerLeftContentInclude}>
|
||||
{collection.allowList.length > 0 ? (
|
||||
@@ -215,7 +238,7 @@ export const RulesMode = ({
|
||||
></List>
|
||||
) : (
|
||||
<RulesEmpty
|
||||
noRules={collection.filterList.length === 0}
|
||||
noRules={collection.rules.filters.length === 0}
|
||||
fullHeight={allowListPages.length === 0}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -2,14 +2,16 @@ import { Modal, toast } from '@affine/component';
|
||||
import {
|
||||
collectionHeaderColsDef,
|
||||
CollectionListItemRenderer,
|
||||
type CollectionMeta,
|
||||
FavoriteTag,
|
||||
type ListItem,
|
||||
ListTableHeader,
|
||||
VirtualizedList,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { SelectorLayout } from '@affine/core/components/page-list/selector/selector-layout';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import {
|
||||
type CollectionMeta,
|
||||
CollectionService,
|
||||
} from '@affine/core/modules/collection';
|
||||
import type { DialogComponentProps } from '@affine/core/modules/dialogs';
|
||||
import type { WORKSPACE_DIALOG_SCHEMA } from '@affine/core/modules/dialogs/constant';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
@@ -52,22 +54,15 @@ export const CollectionSelectorDialog = ({
|
||||
const collectionService = useService(CollectionService);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const collections = useLiveData(collectionService.collections$);
|
||||
const collections = useLiveData(collectionService.collectionMetas$);
|
||||
const [selection, setSelection] = useState(selectedCollectionIds);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const collectionMetas = useMemo(() => {
|
||||
const collectionsList: CollectionMeta[] = collections
|
||||
.map(collection => {
|
||||
return {
|
||||
...collection,
|
||||
title: collection.name,
|
||||
};
|
||||
})
|
||||
.filter(meta => {
|
||||
const reg = new RegExp(keyword, 'i');
|
||||
return reg.test(meta.title);
|
||||
});
|
||||
const collectionsList: CollectionMeta[] = collections.filter(meta => {
|
||||
const reg = new RegExp(keyword, 'i');
|
||||
return reg.test(meta.title);
|
||||
});
|
||||
return collectionsList;
|
||||
}, [collections, keyword]);
|
||||
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ export const EdgelessSnapshot = (props: Props) => {
|
||||
const extensions = useMemo(() => {
|
||||
const manager = getViewManager()
|
||||
.config.init()
|
||||
.common(framework)
|
||||
.foundation(framework)
|
||||
.theme(framework)
|
||||
.database(framework)
|
||||
.linkedDoc(framework).value;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user