mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 01:56:27 +08:00
refactor(editor): reorg blocksuite ai code (#10372)
### TL;DR Relocated AI-related code from `presets` directory to a dedicated `ai` directory for better organization and maintainability. ### What changed? - Moved AI-related code from `blocksuite/presets/ai` to `blocksuite/ai` - Relocated AI chat block code from `blocksuite/blocks` to `blocksuite/ai/blocks` - Updated imports across files to reflect new directory structure - Renamed `registerBlocksuitePresetsCustomComponents` to `registerAIEffects` - Fixed path references in GitHub workflow file ### How to test? 1. Build and run the application 2. Verify AI functionality works as expected: - Test AI chat blocks - Check AI panel functionality - Verify AI copilot features - Ensure AI-related UI components render correctly ### Why make this change? This restructuring improves code organization by: - Giving AI features a dedicated directory that better reflects their importance - Making the codebase more maintainable by grouping related AI functionality - Reducing confusion by removing AI code from the more general `presets` directory - Creating clearer boundaries between AI and non-AI related code
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { NoPageRootError } from '@affine/core/components/blocksuite/block-suite-editor/no-page-error';
|
||||
import { NoPageRootError } from '@affine/core/blocksuite/block-suite-editor/no-page-error';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
|
||||
import { ContactUS, ErrorDetail } from '../error-basic/error-detail';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AllDocsIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { type MouseEvent, useCallback } from 'react';
|
||||
|
||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
||||
import { ActionButton } from './action-button';
|
||||
import docsIllustrationDark from './assets/docs.dark.png';
|
||||
import docsIllustrationLight from './assets/docs.light.png';
|
||||
|
||||
@@ -32,9 +32,9 @@ import {
|
||||
} from 'react';
|
||||
import { encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { BlockSuiteEditor } from '../../../blocksuite/block-suite-editor';
|
||||
import { PureEditorModeSwitch } from '../../../blocksuite/block-suite-mode-switch';
|
||||
import { pageHistoryModalAtom } from '../../atoms/page-history';
|
||||
import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
|
||||
import { PureEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch';
|
||||
import { AffineErrorBoundary } from '../affine-error-boundary';
|
||||
import {
|
||||
historyListGroupByDay,
|
||||
|
||||
-349
@@ -1,349 +0,0 @@
|
||||
import {
|
||||
GeneralNetworkError,
|
||||
PaymentRequiredError,
|
||||
UnauthorizedError,
|
||||
} from '@affine/core/blocksuite/presets/ai/_common/components/ai-item/types';
|
||||
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
|
||||
import {
|
||||
addContextDocMutation,
|
||||
cleanupCopilotSessionMutation,
|
||||
createCopilotContextMutation,
|
||||
createCopilotMessageMutation,
|
||||
createCopilotSessionMutation,
|
||||
forkCopilotSessionMutation,
|
||||
getCopilotHistoriesQuery,
|
||||
getCopilotHistoryIdsQuery,
|
||||
getCopilotSessionsQuery,
|
||||
GraphQLError,
|
||||
type GraphQLQuery,
|
||||
listContextDocsAndFilesQuery,
|
||||
listContextQuery,
|
||||
type QueryOptions,
|
||||
type QueryResponse,
|
||||
removeContextDocMutation,
|
||||
type RequestOptions,
|
||||
updateCopilotSessionMutation,
|
||||
UserFriendlyError,
|
||||
} from '@affine/graphql';
|
||||
import { getCurrentStore } from '@toeverything/infra';
|
||||
|
||||
type OptionsField<T extends GraphQLQuery> =
|
||||
RequestOptions<T>['variables'] extends { options: infer U } ? U : never;
|
||||
|
||||
function codeToError(error: UserFriendlyError) {
|
||||
switch (error.status) {
|
||||
case 401:
|
||||
return new UnauthorizedError();
|
||||
case 402:
|
||||
return new PaymentRequiredError();
|
||||
default:
|
||||
return new GeneralNetworkError(
|
||||
error.code
|
||||
? `${error.code}: ${error.message}\nIdentify: ${error.name}`
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveError(err: any) {
|
||||
const standardError =
|
||||
err instanceof GraphQLError
|
||||
? new UserFriendlyError(err.extensions)
|
||||
: UserFriendlyError.fromAnyError(err);
|
||||
|
||||
return codeToError(standardError);
|
||||
}
|
||||
|
||||
export function handleError(src: any) {
|
||||
const err = resolveError(src);
|
||||
if (err instanceof UnauthorizedError) {
|
||||
getCurrentStore().set(showAILoginRequiredAtom, true);
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
export class CopilotClient {
|
||||
constructor(
|
||||
readonly gql: <Query extends GraphQLQuery>(
|
||||
options: QueryOptions<Query>
|
||||
) => Promise<QueryResponse<Query>>,
|
||||
readonly fetcher: (input: string, init?: RequestInit) => Promise<Response>,
|
||||
readonly eventSource: (
|
||||
url: string,
|
||||
eventSourceInitDict?: EventSourceInit
|
||||
) => EventSource
|
||||
) {}
|
||||
|
||||
async createSession(
|
||||
options: OptionsField<typeof createCopilotSessionMutation>
|
||||
) {
|
||||
try {
|
||||
const res = await this.gql({
|
||||
query: createCopilotSessionMutation,
|
||||
variables: {
|
||||
options,
|
||||
},
|
||||
});
|
||||
return res.createCopilotSession;
|
||||
} catch (err) {
|
||||
throw resolveError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async updateSession(
|
||||
options: OptionsField<typeof updateCopilotSessionMutation>
|
||||
) {
|
||||
try {
|
||||
const res = await this.gql({
|
||||
query: updateCopilotSessionMutation,
|
||||
variables: {
|
||||
options,
|
||||
},
|
||||
});
|
||||
return res.updateCopilotSession;
|
||||
} catch (err) {
|
||||
throw resolveError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async forkSession(options: OptionsField<typeof forkCopilotSessionMutation>) {
|
||||
try {
|
||||
const res = await this.gql({
|
||||
query: forkCopilotSessionMutation,
|
||||
variables: {
|
||||
options,
|
||||
},
|
||||
});
|
||||
return res.forkCopilotSession;
|
||||
} catch (err) {
|
||||
throw resolveError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async createMessage(
|
||||
options: OptionsField<typeof createCopilotMessageMutation>
|
||||
) {
|
||||
try {
|
||||
const res = await this.gql({
|
||||
query: createCopilotMessageMutation,
|
||||
variables: {
|
||||
options,
|
||||
},
|
||||
});
|
||||
return res.createCopilotMessage;
|
||||
} catch (err) {
|
||||
throw resolveError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getSessionIds(
|
||||
workspaceId: string,
|
||||
docId?: string,
|
||||
options?: RequestOptions<
|
||||
typeof getCopilotSessionsQuery
|
||||
>['variables']['options']
|
||||
) {
|
||||
try {
|
||||
const res = await this.gql({
|
||||
query: getCopilotSessionsQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
docId,
|
||||
options,
|
||||
},
|
||||
});
|
||||
return res.currentUser?.copilot?.sessionIds;
|
||||
} catch (err) {
|
||||
throw resolveError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getHistories(
|
||||
workspaceId: string,
|
||||
docId?: string,
|
||||
options?: RequestOptions<
|
||||
typeof getCopilotHistoriesQuery
|
||||
>['variables']['options']
|
||||
) {
|
||||
try {
|
||||
const res = await this.gql({
|
||||
query: getCopilotHistoriesQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
docId,
|
||||
options,
|
||||
},
|
||||
});
|
||||
|
||||
return res.currentUser?.copilot?.histories;
|
||||
} catch (err) {
|
||||
throw resolveError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async getHistoryIds(
|
||||
workspaceId: string,
|
||||
docId?: string,
|
||||
options?: RequestOptions<
|
||||
typeof getCopilotHistoriesQuery
|
||||
>['variables']['options']
|
||||
) {
|
||||
try {
|
||||
const res = await this.gql({
|
||||
query: getCopilotHistoryIdsQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
docId,
|
||||
options,
|
||||
},
|
||||
});
|
||||
|
||||
return res.currentUser?.copilot?.histories;
|
||||
} catch (err) {
|
||||
throw resolveError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async cleanupSessions(input: {
|
||||
workspaceId: string;
|
||||
docId: string;
|
||||
sessionIds: string[];
|
||||
}) {
|
||||
try {
|
||||
const res = await this.gql({
|
||||
query: cleanupCopilotSessionMutation,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
return res.cleanupCopilotSession;
|
||||
} catch (err) {
|
||||
throw resolveError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async createContext(workspaceId: string, sessionId: string) {
|
||||
const res = await this.gql({
|
||||
query: createCopilotContextMutation,
|
||||
variables: {
|
||||
workspaceId,
|
||||
sessionId,
|
||||
},
|
||||
});
|
||||
return res.createCopilotContext;
|
||||
}
|
||||
|
||||
async getContextId(workspaceId: string, sessionId: string) {
|
||||
const res = await this.gql({
|
||||
query: listContextQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
sessionId,
|
||||
},
|
||||
});
|
||||
return res.currentUser?.copilot?.contexts?.[0]?.id;
|
||||
}
|
||||
|
||||
async addContextDoc(options: OptionsField<typeof addContextDocMutation>) {
|
||||
const res = await this.gql({
|
||||
query: addContextDocMutation,
|
||||
variables: {
|
||||
options,
|
||||
},
|
||||
});
|
||||
return res.addContextDoc;
|
||||
}
|
||||
|
||||
async removeContextDoc(
|
||||
options: OptionsField<typeof removeContextDocMutation>
|
||||
) {
|
||||
const res = await this.gql({
|
||||
query: removeContextDocMutation,
|
||||
variables: {
|
||||
options,
|
||||
},
|
||||
});
|
||||
return res.removeContextDoc;
|
||||
}
|
||||
|
||||
async addContextFile() {
|
||||
return;
|
||||
}
|
||||
|
||||
async removeContextFile() {
|
||||
return;
|
||||
}
|
||||
|
||||
async getContextDocsAndFiles(
|
||||
workspaceId: string,
|
||||
sessionId: string,
|
||||
contextId: string
|
||||
) {
|
||||
const res = await this.gql({
|
||||
query: listContextDocsAndFilesQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
sessionId,
|
||||
contextId,
|
||||
},
|
||||
});
|
||||
return res.currentUser?.copilot?.contexts?.[0];
|
||||
}
|
||||
|
||||
async chatText({
|
||||
sessionId,
|
||||
messageId,
|
||||
signal,
|
||||
}: {
|
||||
sessionId: string;
|
||||
messageId?: string;
|
||||
signal?: AbortSignal;
|
||||
}) {
|
||||
let url = `/api/copilot/chat/${sessionId}`;
|
||||
if (messageId) {
|
||||
url += `?messageId=${encodeURIComponent(messageId)}`;
|
||||
}
|
||||
const response = await this.fetcher(url.toString(), { signal });
|
||||
return response.text();
|
||||
}
|
||||
|
||||
// Text or image to text
|
||||
chatTextStream(
|
||||
{
|
||||
sessionId,
|
||||
messageId,
|
||||
}: {
|
||||
sessionId: string;
|
||||
messageId?: string;
|
||||
},
|
||||
endpoint = 'stream'
|
||||
) {
|
||||
let url = `/api/copilot/chat/${sessionId}/${endpoint}`;
|
||||
if (messageId) {
|
||||
url += `?messageId=${encodeURIComponent(messageId)}`;
|
||||
}
|
||||
return this.eventSource(url);
|
||||
}
|
||||
|
||||
// Text or image to images
|
||||
imagesStream(
|
||||
sessionId: string,
|
||||
messageId?: string,
|
||||
seed?: string,
|
||||
endpoint = 'images'
|
||||
) {
|
||||
let url = `/api/copilot/chat/${sessionId}/${endpoint}`;
|
||||
|
||||
if (messageId || seed) {
|
||||
url += '?';
|
||||
url += new URLSearchParams(
|
||||
Object.fromEntries(
|
||||
Object.entries({ messageId, seed }).filter(
|
||||
([_, v]) => v !== undefined
|
||||
)
|
||||
) as Record<string, string>
|
||||
).toString();
|
||||
}
|
||||
return this.eventSource(url);
|
||||
}
|
||||
}
|
||||
-105
@@ -1,105 +0,0 @@
|
||||
import { handleError } from './copilot-client';
|
||||
|
||||
export function delay(ms: number) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export type AffineTextEvent = {
|
||||
type: 'attachment' | 'message';
|
||||
data: string;
|
||||
};
|
||||
|
||||
type AffineTextStream = AsyncIterable<AffineTextEvent>;
|
||||
|
||||
type toTextStreamOptions = {
|
||||
timeout?: number;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
||||
// todo(@Peng): may need to extend the error type
|
||||
const safeParseError = (data: string): { status: number } => {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return {
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export function toTextStream(
|
||||
eventSource: EventSource,
|
||||
{ timeout, signal }: toTextStreamOptions = {}
|
||||
): AffineTextStream {
|
||||
return {
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
const messageQueue: AffineTextEvent[] = [];
|
||||
let resolveMessagePromise: () => void;
|
||||
let rejectMessagePromise: (err: Error) => void;
|
||||
|
||||
function resetMessagePromise() {
|
||||
if (resolveMessagePromise) {
|
||||
resolveMessagePromise();
|
||||
}
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
resolveMessagePromise = resolve;
|
||||
rejectMessagePromise = reject;
|
||||
});
|
||||
}
|
||||
let messagePromise = resetMessagePromise();
|
||||
|
||||
function messageListener(event: MessageEvent) {
|
||||
messageQueue.push({
|
||||
type: event.type as 'attachment' | 'message',
|
||||
data: event.data as string,
|
||||
});
|
||||
messagePromise = resetMessagePromise();
|
||||
}
|
||||
|
||||
eventSource.addEventListener('message', messageListener);
|
||||
eventSource.addEventListener('attachment', messageListener);
|
||||
|
||||
eventSource.addEventListener('error', event => {
|
||||
const errorMessage = (event as unknown as { data: string }).data;
|
||||
// if there is data in Error event, it means the server sent an error message
|
||||
// otherwise, the stream is finished successfully
|
||||
if (event.type === 'error' && errorMessage) {
|
||||
// try to parse the error message as a JSON object
|
||||
const error = safeParseError(errorMessage);
|
||||
rejectMessagePromise(handleError(error));
|
||||
} else {
|
||||
resolveMessagePromise();
|
||||
}
|
||||
eventSource.close();
|
||||
});
|
||||
|
||||
try {
|
||||
while (
|
||||
eventSource.readyState !== EventSource.CLOSED &&
|
||||
!signal?.aborted
|
||||
) {
|
||||
if (messageQueue.length === 0) {
|
||||
// Wait for the next message or timeout
|
||||
await (timeout
|
||||
? Promise.race([
|
||||
messagePromise,
|
||||
delay(timeout).then(() => {
|
||||
if (!signal?.aborted) {
|
||||
throw new Error('Timeout');
|
||||
}
|
||||
}),
|
||||
])
|
||||
: messagePromise);
|
||||
} else if (messageQueue.length > 0) {
|
||||
const top = messageQueue.shift();
|
||||
if (top) {
|
||||
yield top;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
eventSource.close();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// manually synced with packages/backend/server/src/data/migrations/utils/prompts.ts
|
||||
// TODO(@Peng): automate this
|
||||
export const promptKeys = [
|
||||
'debug:chat:gpt4',
|
||||
'debug:action:dalle3',
|
||||
'debug:action:fal-sd15',
|
||||
'debug:action:fal-upscaler',
|
||||
'debug:action:fal-remove-bg',
|
||||
'debug:action:fal-face-to-sticker',
|
||||
'Chat With AFFiNE AI',
|
||||
'Search With AFFiNE AI',
|
||||
'Summary',
|
||||
'Generate a caption',
|
||||
'Summary the webpage',
|
||||
'Explain this',
|
||||
'Explain this image',
|
||||
'Explain this code',
|
||||
'Translate to',
|
||||
'Write an article about this',
|
||||
'Write a twitter about this',
|
||||
'Write a poem about this',
|
||||
'Write a blog post about this',
|
||||
'Write outline',
|
||||
'Change tone to',
|
||||
'Brainstorm ideas about this',
|
||||
'Expand mind map',
|
||||
'Improve writing for it',
|
||||
'Improve grammar for it',
|
||||
'Fix spelling for it',
|
||||
'Find action items from it',
|
||||
'Check code error',
|
||||
'Create headings',
|
||||
'Make it real',
|
||||
'Make it real with text',
|
||||
'Make it longer',
|
||||
'Make it shorter',
|
||||
'Continue writing',
|
||||
'workflow:presentation',
|
||||
'workflow:brainstorm',
|
||||
'workflow:image-sketch',
|
||||
'workflow:image-clay',
|
||||
'workflow:image-anime',
|
||||
'workflow:image-pixel',
|
||||
] as const;
|
||||
|
||||
export type PromptKey = (typeof promptKeys)[number];
|
||||
@@ -1,313 +0,0 @@
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets';
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import { partition } from 'lodash-es';
|
||||
|
||||
import type { CopilotClient } from './copilot-client';
|
||||
import { delay, toTextStream } from './event-source';
|
||||
import type { PromptKey } from './prompt';
|
||||
|
||||
const TIMEOUT = 50000;
|
||||
|
||||
export type TextToTextOptions = {
|
||||
client: CopilotClient;
|
||||
docId: string;
|
||||
workspaceId: string;
|
||||
promptName?: PromptKey;
|
||||
sessionId?: string | Promise<string>;
|
||||
content?: string;
|
||||
attachments?: (string | Blob | File)[];
|
||||
params?: Record<string, any>;
|
||||
timeout?: number;
|
||||
stream?: boolean;
|
||||
signal?: AbortSignal;
|
||||
retry?: boolean;
|
||||
workflow?: boolean;
|
||||
isRootSession?: boolean;
|
||||
postfix?: (text: string) => string;
|
||||
};
|
||||
|
||||
export type ToImageOptions = TextToTextOptions & {
|
||||
seed?: string;
|
||||
};
|
||||
|
||||
async function resizeImage(blob: Blob | File): Promise<Blob | null> {
|
||||
let src = '';
|
||||
try {
|
||||
src = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
await new Promise(resolve => {
|
||||
img.onload = resolve;
|
||||
});
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
// keep aspect ratio
|
||||
const scale = Math.min(1024 / img.width, 1024 / img.height);
|
||||
canvas.width = Math.floor(img.width * scale);
|
||||
canvas.height = Math.floor(img.height * scale);
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
return await new Promise(resolve =>
|
||||
canvas.toBlob(blob => resolve(blob), 'image/jpeg', 0.8)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
if (src) URL.revokeObjectURL(src);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function createSessionMessage({
|
||||
client,
|
||||
docId,
|
||||
workspaceId,
|
||||
promptName = 'Chat With AFFiNE AI',
|
||||
content,
|
||||
sessionId: providedSessionId,
|
||||
attachments,
|
||||
params,
|
||||
retry = false,
|
||||
}: TextToTextOptions) {
|
||||
if (!promptName && !providedSessionId) {
|
||||
throw new Error('promptName or sessionId is required');
|
||||
}
|
||||
const hasAttachments = attachments && attachments.length > 0;
|
||||
const sessionId = await (providedSessionId ??
|
||||
client.createSession({
|
||||
workspaceId,
|
||||
docId,
|
||||
promptName,
|
||||
}));
|
||||
|
||||
const options: Parameters<CopilotClient['createMessage']>[0] = {
|
||||
sessionId,
|
||||
content,
|
||||
params,
|
||||
};
|
||||
|
||||
if (hasAttachments) {
|
||||
const [stringAttachments, blobs] = partition(
|
||||
attachments,
|
||||
attachment => typeof attachment === 'string'
|
||||
) as [string[], (Blob | File)[]];
|
||||
options.attachments = stringAttachments;
|
||||
options.blobs = (
|
||||
await Promise.all(
|
||||
blobs.map(resizeImage).map(async blob => {
|
||||
const file = await blob;
|
||||
if (!file) return null;
|
||||
return new File([file], sessionId, {
|
||||
type: file.type,
|
||||
});
|
||||
})
|
||||
)
|
||||
).filter(Boolean) as File[];
|
||||
}
|
||||
if (retry)
|
||||
return {
|
||||
sessionId,
|
||||
};
|
||||
|
||||
const messageId = await client.createMessage(options);
|
||||
return {
|
||||
messageId,
|
||||
sessionId,
|
||||
};
|
||||
}
|
||||
|
||||
export function textToText({
|
||||
client,
|
||||
docId,
|
||||
workspaceId,
|
||||
promptName,
|
||||
content,
|
||||
attachments,
|
||||
params,
|
||||
sessionId,
|
||||
stream,
|
||||
signal,
|
||||
timeout = TIMEOUT,
|
||||
retry = false,
|
||||
workflow = false,
|
||||
isRootSession = false,
|
||||
postfix,
|
||||
}: TextToTextOptions) {
|
||||
let _sessionId: string;
|
||||
let _messageId: string | undefined;
|
||||
|
||||
if (stream) {
|
||||
return {
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
if (retry) {
|
||||
const retrySessionId =
|
||||
(await sessionId) ?? AIProvider.LAST_ACTION_SESSIONID;
|
||||
assertExists(retrySessionId, 'retry sessionId is required');
|
||||
_sessionId = retrySessionId;
|
||||
_messageId = undefined;
|
||||
} else {
|
||||
const message = await createSessionMessage({
|
||||
client,
|
||||
docId,
|
||||
workspaceId,
|
||||
promptName,
|
||||
content,
|
||||
attachments,
|
||||
params,
|
||||
sessionId,
|
||||
retry,
|
||||
});
|
||||
_sessionId = message.sessionId;
|
||||
_messageId = message.messageId;
|
||||
}
|
||||
|
||||
const eventSource = client.chatTextStream(
|
||||
{
|
||||
sessionId: _sessionId,
|
||||
messageId: _messageId,
|
||||
},
|
||||
workflow ? 'workflow' : undefined
|
||||
);
|
||||
AIProvider.LAST_ACTION_SESSIONID = _sessionId;
|
||||
if (isRootSession) {
|
||||
AIProvider.LAST_ROOT_SESSION_ID = _sessionId;
|
||||
}
|
||||
|
||||
if (signal) {
|
||||
if (signal.aborted) {
|
||||
eventSource.close();
|
||||
return;
|
||||
}
|
||||
signal.onabort = () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}
|
||||
if (postfix) {
|
||||
const messages: string[] = [];
|
||||
for await (const event of toTextStream(eventSource, {
|
||||
timeout,
|
||||
signal,
|
||||
})) {
|
||||
if (event.type === 'message') {
|
||||
messages.push(event.data);
|
||||
}
|
||||
}
|
||||
yield postfix(messages.join(''));
|
||||
} else {
|
||||
for await (const event of toTextStream(eventSource, {
|
||||
timeout,
|
||||
signal,
|
||||
})) {
|
||||
if (event.type === 'message') {
|
||||
yield event.data;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return Promise.race([
|
||||
timeout
|
||||
? delay(timeout).then(() => {
|
||||
throw new Error('Timeout');
|
||||
})
|
||||
: null,
|
||||
(async function () {
|
||||
if (retry) {
|
||||
const retrySessionId =
|
||||
(await sessionId) ?? AIProvider.LAST_ACTION_SESSIONID;
|
||||
assertExists(retrySessionId, 'retry sessionId is required');
|
||||
_sessionId = retrySessionId;
|
||||
_messageId = undefined;
|
||||
} else {
|
||||
const message = await createSessionMessage({
|
||||
client,
|
||||
docId,
|
||||
workspaceId,
|
||||
promptName,
|
||||
content,
|
||||
attachments,
|
||||
params,
|
||||
sessionId,
|
||||
});
|
||||
_sessionId = message.sessionId;
|
||||
_messageId = message.messageId;
|
||||
}
|
||||
|
||||
AIProvider.LAST_ACTION_SESSIONID = _sessionId;
|
||||
if (isRootSession) {
|
||||
AIProvider.LAST_ROOT_SESSION_ID = _sessionId;
|
||||
}
|
||||
|
||||
return client.chatText({
|
||||
sessionId: _sessionId,
|
||||
messageId: _messageId,
|
||||
});
|
||||
})(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Only one image is currently being processed
|
||||
export function toImage({
|
||||
docId,
|
||||
workspaceId,
|
||||
promptName,
|
||||
content,
|
||||
attachments,
|
||||
params,
|
||||
seed,
|
||||
sessionId,
|
||||
signal,
|
||||
timeout = TIMEOUT,
|
||||
retry = false,
|
||||
workflow = false,
|
||||
client,
|
||||
}: ToImageOptions) {
|
||||
let _sessionId: string;
|
||||
let _messageId: string | undefined;
|
||||
return {
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
if (retry) {
|
||||
const retrySessionId =
|
||||
(await sessionId) ?? AIProvider.LAST_ACTION_SESSIONID;
|
||||
assertExists(retrySessionId, 'retry sessionId is required');
|
||||
_sessionId = retrySessionId;
|
||||
_messageId = undefined;
|
||||
} else {
|
||||
const { messageId, sessionId } = await createSessionMessage({
|
||||
docId,
|
||||
workspaceId,
|
||||
promptName,
|
||||
content,
|
||||
attachments,
|
||||
params,
|
||||
client,
|
||||
});
|
||||
_sessionId = sessionId;
|
||||
_messageId = messageId;
|
||||
}
|
||||
|
||||
const eventSource = client.imagesStream(
|
||||
_sessionId,
|
||||
_messageId,
|
||||
seed,
|
||||
workflow ? 'workflow' : undefined
|
||||
);
|
||||
AIProvider.LAST_ACTION_SESSIONID = _sessionId;
|
||||
|
||||
for await (const event of toTextStream(eventSource, {
|
||||
timeout,
|
||||
signal,
|
||||
})) {
|
||||
if (event.type === 'attachment') {
|
||||
yield event.data;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
-538
@@ -1,538 +0,0 @@
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets';
|
||||
import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis';
|
||||
import type { GlobalDialogService } from '@affine/core/modules/dialogs';
|
||||
import {
|
||||
type ChatHistoryOrder,
|
||||
type getCopilotHistoriesQuery,
|
||||
type RequestOptions,
|
||||
} from '@affine/graphql';
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { CopilotClient } from './copilot-client';
|
||||
import type { PromptKey } from './prompt';
|
||||
import { textToText, toImage } from './request';
|
||||
import { setupTracker } from './tracker';
|
||||
|
||||
const filterStyleToPromptName = new Map(
|
||||
Object.entries({
|
||||
'Clay style': 'workflow:image-clay',
|
||||
'Pixel style': 'workflow:image-pixel',
|
||||
'Sketch style': 'workflow:image-sketch',
|
||||
'Anime style': 'workflow:image-anime',
|
||||
})
|
||||
);
|
||||
|
||||
const processTypeToPromptName = new Map(
|
||||
Object.entries({
|
||||
Clearer: 'debug:action:fal-upscaler',
|
||||
'Remove background': 'debug:action:fal-remove-bg',
|
||||
'Convert to sticker': 'debug:action:fal-face-to-sticker',
|
||||
})
|
||||
);
|
||||
|
||||
export function setupAIProvider(
|
||||
client: CopilotClient,
|
||||
globalDialogService: GlobalDialogService
|
||||
) {
|
||||
//#region actions
|
||||
AIProvider.provide('chat', options => {
|
||||
const { input, docs, ...rest } = options;
|
||||
const params = docs?.length
|
||||
? {
|
||||
docs: docs.map((doc, i) => ({
|
||||
docId: doc.docId,
|
||||
markdown: doc.markdown,
|
||||
index: i + 1,
|
||||
})),
|
||||
}
|
||||
: undefined;
|
||||
return textToText({
|
||||
...rest,
|
||||
client,
|
||||
content: input,
|
||||
params,
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('summary', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Summary',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('translate', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
promptName: 'Translate to',
|
||||
content: options.input,
|
||||
params: {
|
||||
language: options.lang,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('changeTone', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
params: {
|
||||
tone: options.tone.toLowerCase(),
|
||||
},
|
||||
content: options.input,
|
||||
promptName: 'Change tone to',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('improveWriting', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Improve writing for it',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('improveGrammar', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Improve grammar for it',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('fixSpelling', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Fix spelling for it',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('createHeadings', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Create headings',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('makeLonger', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Make it longer',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('makeShorter', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Make it shorter',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('checkCodeErrors', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Check code error',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('explainCode', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Explain this code',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('writeArticle', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Write an article about this',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('writeTwitterPost', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Write a twitter about this',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('writePoem', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Write a poem about this',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('writeOutline', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Write outline',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('writeBlogPost', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Write a blog post about this',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('brainstorm', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Brainstorm ideas about this',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('findActions', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Find action items from it',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('brainstormMindmap', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'workflow:brainstorm',
|
||||
workflow: true,
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('expandMindmap', options => {
|
||||
assertExists(options.input, 'expandMindmap action requires input');
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
params: {
|
||||
mindmap: options.mindmap,
|
||||
node: options.input,
|
||||
},
|
||||
content: options.input,
|
||||
promptName: 'Expand mind map',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('explain', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Explain this',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('explainImage', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Explain this image',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('makeItReal', options => {
|
||||
let promptName: PromptKey = 'Make it real';
|
||||
let content = options.input || '';
|
||||
|
||||
// wireframes
|
||||
if (options.attachments?.length) {
|
||||
content = `Here are the latest wireframes. Could you make a new website based on these wireframes and notes and send back just the html file?
|
||||
Here are our design notes:\n ${content}.`;
|
||||
} else {
|
||||
// notes
|
||||
promptName = 'Make it real with text';
|
||||
content = `Here are the latest notes: \n ${content}.
|
||||
Could you make a new website based on these notes and send back just the html file?`;
|
||||
}
|
||||
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content,
|
||||
promptName,
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('createSlides', options => {
|
||||
const SlideSchema = z.object({
|
||||
page: z.number(),
|
||||
type: z.enum(['name', 'title', 'content']),
|
||||
content: z.string(),
|
||||
});
|
||||
type Slide = z.infer<typeof SlideSchema>;
|
||||
const parseJson = (json: string) => {
|
||||
try {
|
||||
return SlideSchema.parse(JSON.parse(json));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
// TODO(@darkskygit): move this to backend's workflow after workflow support custom code action
|
||||
const postfix = (text: string): string => {
|
||||
const slides = text
|
||||
.split('\n')
|
||||
.map(parseJson)
|
||||
.filter((v): v is Slide => !!v);
|
||||
return slides
|
||||
.map(slide => {
|
||||
if (slide.type === 'name') {
|
||||
return `- ${slide.content}`;
|
||||
} else if (slide.type === 'title') {
|
||||
return ` - ${slide.content}`;
|
||||
} else if (slide.content.includes('\n')) {
|
||||
return slide.content
|
||||
.split('\n')
|
||||
.map(c => ` - ${c}`)
|
||||
.join('\n');
|
||||
} else {
|
||||
return ` - ${slide.content}`;
|
||||
}
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'workflow:presentation',
|
||||
workflow: true,
|
||||
postfix,
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('createImage', options => {
|
||||
// test to image
|
||||
let promptName: PromptKey = 'debug:action:dalle3';
|
||||
// image to image
|
||||
if (options.attachments?.length) {
|
||||
promptName = 'debug:action:fal-sd15';
|
||||
}
|
||||
return toImage({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName,
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('filterImage', options => {
|
||||
// test to image
|
||||
const promptName = filterStyleToPromptName.get(options.style as string);
|
||||
return toImage({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
timeout: 120000,
|
||||
promptName: promptName as PromptKey,
|
||||
workflow: !!promptName?.startsWith('workflow:'),
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('processImage', options => {
|
||||
// test to image
|
||||
const promptName = processTypeToPromptName.get(
|
||||
options.type as string
|
||||
) as PromptKey;
|
||||
return toImage({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
timeout: 120000,
|
||||
promptName,
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('generateCaption', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Generate a caption',
|
||||
});
|
||||
});
|
||||
|
||||
AIProvider.provide('continueWriting', options => {
|
||||
return textToText({
|
||||
...options,
|
||||
client,
|
||||
content: options.input,
|
||||
promptName: 'Continue writing',
|
||||
});
|
||||
});
|
||||
//#endregion
|
||||
|
||||
AIProvider.provide('session', {
|
||||
createSession: async (
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
promptName = 'Chat With AFFiNE AI'
|
||||
) => {
|
||||
return client.createSession({
|
||||
workspaceId,
|
||||
docId,
|
||||
promptName,
|
||||
});
|
||||
},
|
||||
getSessionIds: async (
|
||||
workspaceId: string,
|
||||
docId?: string,
|
||||
options?: { action?: boolean }
|
||||
) => {
|
||||
return client.getSessionIds(workspaceId, docId, options);
|
||||
},
|
||||
updateSession: async (sessionId: string, promptName: string) => {
|
||||
return client.updateSession({
|
||||
sessionId,
|
||||
promptName,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
AIProvider.provide('context', {
|
||||
createContext: async (workspaceId: string, sessionId: string) => {
|
||||
return client.createContext(workspaceId, sessionId);
|
||||
},
|
||||
getContextId: async (workspaceId: string, sessionId: string) => {
|
||||
return client.getContextId(workspaceId, sessionId);
|
||||
},
|
||||
addContextDoc: async (options: { contextId: string; docId: string }) => {
|
||||
return client.addContextDoc(options);
|
||||
},
|
||||
removeContextDoc: async (options: { contextId: string; docId: string }) => {
|
||||
return client.removeContextDoc(options);
|
||||
},
|
||||
addContextFile: async () => {
|
||||
return client.addContextFile();
|
||||
},
|
||||
removeContextFile: async () => {
|
||||
return client.removeContextFile();
|
||||
},
|
||||
getContextDocsAndFiles: async (
|
||||
workspaceId: string,
|
||||
sessionId: string,
|
||||
contextId: string
|
||||
) => {
|
||||
return client.getContextDocsAndFiles(workspaceId, sessionId, contextId);
|
||||
},
|
||||
});
|
||||
|
||||
AIProvider.provide('histories', {
|
||||
actions: async (
|
||||
workspaceId: string,
|
||||
docId?: string
|
||||
): Promise<BlockSuitePresets.AIHistory[]> => {
|
||||
// @ts-expect-error - 'action' is missing in server impl
|
||||
return (
|
||||
(await client.getHistories(workspaceId, docId, {
|
||||
action: true,
|
||||
withPrompt: true,
|
||||
})) ?? []
|
||||
);
|
||||
},
|
||||
chats: async (
|
||||
workspaceId: string,
|
||||
docId?: string,
|
||||
options?: {
|
||||
sessionId?: string;
|
||||
messageOrder?: ChatHistoryOrder;
|
||||
}
|
||||
): Promise<BlockSuitePresets.AIHistory[]> => {
|
||||
// @ts-expect-error - 'action' is missing in server impl
|
||||
return (await client.getHistories(workspaceId, docId, options)) ?? [];
|
||||
},
|
||||
cleanup: async (
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
sessionIds: string[]
|
||||
) => {
|
||||
await client.cleanupSessions({ workspaceId, docId, sessionIds });
|
||||
},
|
||||
ids: async (
|
||||
workspaceId: string,
|
||||
docId?: string,
|
||||
options?: RequestOptions<
|
||||
typeof getCopilotHistoriesQuery
|
||||
>['variables']['options']
|
||||
): Promise<BlockSuitePresets.AIHistoryIds[]> => {
|
||||
// @ts-expect-error - 'role' is missing type in server impl
|
||||
return await client.getHistoryIds(workspaceId, docId, options);
|
||||
},
|
||||
});
|
||||
|
||||
AIProvider.provide('photoEngine', {
|
||||
async searchImages(options): Promise<string[]> {
|
||||
let url = '/api/copilot/unsplash/photos';
|
||||
if (options.query) {
|
||||
url += `?query=${encodeURIComponent(options.query)}`;
|
||||
}
|
||||
const result: {
|
||||
results?: {
|
||||
urls: {
|
||||
regular: string;
|
||||
};
|
||||
}[];
|
||||
} = await client.fetcher(url.toString()).then(res => res.json());
|
||||
if (!result.results) return [];
|
||||
return result.results.map(r => {
|
||||
const url = new URL(r.urls.regular);
|
||||
url.searchParams.set('fit', 'crop');
|
||||
url.searchParams.set('crop', 'edges');
|
||||
url.searchParams.set('dpr', (window.devicePixelRatio ?? 2).toString());
|
||||
url.searchParams.set('w', `${options.width}`);
|
||||
url.searchParams.set('h', `${options.height}`);
|
||||
return url.toString();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
AIProvider.provide('onboarding', toggleGeneralAIOnboarding);
|
||||
|
||||
AIProvider.provide('forkChat', options => {
|
||||
return client.forkSession(options);
|
||||
});
|
||||
|
||||
const disposeRequestLoginHandler = AIProvider.slots.requestLogin.on(() => {
|
||||
globalDialogService.open('sign-in', {});
|
||||
});
|
||||
|
||||
setupTracker();
|
||||
|
||||
return () => {
|
||||
disposeRequestLoginHandler.dispose();
|
||||
};
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets';
|
||||
import { mixpanel, track } from '@affine/track';
|
||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||
import type { GfxPrimitiveElementModel } from '@blocksuite/affine/block-std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/affine/store';
|
||||
import { lowerCase, omit } from 'lodash-es';
|
||||
|
||||
type ElementModel = GfxPrimitiveElementModel;
|
||||
|
||||
type AIActionEventName =
|
||||
| 'AI action invoked'
|
||||
| 'AI action aborted'
|
||||
| 'AI result discarded'
|
||||
| 'AI result accepted';
|
||||
|
||||
type AIActionEventProperties = {
|
||||
page: 'doc' | 'edgeless';
|
||||
segment:
|
||||
| 'AI action panel'
|
||||
| 'right side bar'
|
||||
| 'inline chat panel'
|
||||
| 'AI result panel'
|
||||
| 'AI chat block';
|
||||
module:
|
||||
| 'exit confirmation'
|
||||
| 'AI action panel'
|
||||
| 'AI chat panel'
|
||||
| 'inline chat panel'
|
||||
| 'AI result panel'
|
||||
| 'AI chat block';
|
||||
control:
|
||||
| 'stop button'
|
||||
| 'format toolbar'
|
||||
| 'AI chat send button'
|
||||
| 'Block action bar'
|
||||
| 'paywall'
|
||||
| 'policy wall'
|
||||
| 'server error'
|
||||
| 'login required'
|
||||
| 'insert'
|
||||
| 'replace'
|
||||
| 'use as caption'
|
||||
| 'discard'
|
||||
| 'retry'
|
||||
| 'add note'
|
||||
| 'add page'
|
||||
| 'continue in chat';
|
||||
type:
|
||||
| 'doc' // synced doc
|
||||
| 'note' // note shape
|
||||
| 'text'
|
||||
| 'image'
|
||||
| 'draw object'
|
||||
| 'chatbox text'
|
||||
| 'other';
|
||||
category: string;
|
||||
other: Record<string, unknown>;
|
||||
docId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
type BlocksuiteActionEvent = Parameters<
|
||||
Parameters<typeof AIProvider.slots.actions.on>[0]
|
||||
>[0];
|
||||
|
||||
const trackAction = ({
|
||||
eventName,
|
||||
properties,
|
||||
}: {
|
||||
eventName: AIActionEventName;
|
||||
properties: AIActionEventProperties;
|
||||
}) => {
|
||||
mixpanel.track(eventName, properties);
|
||||
};
|
||||
|
||||
const inferPageMode = (host: EditorHost) => {
|
||||
return host.querySelector('affine-page-root') ? 'doc' : 'edgeless';
|
||||
};
|
||||
|
||||
const defaultActionOptions = [
|
||||
'stream',
|
||||
'input',
|
||||
'content',
|
||||
'stream',
|
||||
'attachments',
|
||||
'signal',
|
||||
'docId',
|
||||
'workspaceId',
|
||||
'host',
|
||||
'models',
|
||||
'control',
|
||||
'where',
|
||||
'seed',
|
||||
];
|
||||
|
||||
function isElementModel(
|
||||
model: BlockModel | ElementModel
|
||||
): model is ElementModel {
|
||||
return !isBlockModel(model);
|
||||
}
|
||||
|
||||
function isBlockModel(model: BlockModel | ElementModel): model is BlockModel {
|
||||
return 'flavour' in model;
|
||||
}
|
||||
|
||||
function inferObjectType(event: BlocksuiteActionEvent) {
|
||||
const models: (BlockModel | ElementModel)[] | undefined =
|
||||
event.options.models;
|
||||
if (!models) {
|
||||
if (event.action === 'chat') {
|
||||
return 'chatbox text';
|
||||
} else if (event.options.attachments?.length) {
|
||||
return 'image';
|
||||
} else {
|
||||
return 'text';
|
||||
}
|
||||
} else if (models.every(isElementModel)) {
|
||||
return 'draw object';
|
||||
} else if (models.every(isBlockModel)) {
|
||||
const flavour = models[0].flavour;
|
||||
if (flavour === 'affine:note') {
|
||||
return 'note';
|
||||
} else if (
|
||||
['affine:paragraph', 'affine:list', 'affine:code'].includes(flavour)
|
||||
) {
|
||||
return 'text';
|
||||
} else if (flavour === 'affine:image') {
|
||||
return 'image';
|
||||
}
|
||||
}
|
||||
return 'other';
|
||||
}
|
||||
|
||||
function inferSegment(
|
||||
event: BlocksuiteActionEvent
|
||||
): AIActionEventProperties['segment'] {
|
||||
if (event.options.where === 'inline-chat-panel') {
|
||||
return 'inline chat panel';
|
||||
} else if (event.event.startsWith('result:')) {
|
||||
return 'AI result panel';
|
||||
} else if (event.options.where === 'chat-panel') {
|
||||
return 'right side bar';
|
||||
} else if (event.options.where === 'ai-chat-block') {
|
||||
return 'AI chat block';
|
||||
} else {
|
||||
return 'AI action panel';
|
||||
}
|
||||
}
|
||||
|
||||
function inferModule(
|
||||
event: BlocksuiteActionEvent
|
||||
): AIActionEventProperties['module'] {
|
||||
if (event.options.where === 'chat-panel') {
|
||||
return 'AI chat panel';
|
||||
} else if (event.event === 'result:discard') {
|
||||
return 'exit confirmation';
|
||||
} else if (event.event.startsWith('result:')) {
|
||||
return 'AI result panel';
|
||||
} else if (event.options.where === 'inline-chat-panel') {
|
||||
return 'inline chat panel';
|
||||
} else if (event.options.where === 'ai-chat-block') {
|
||||
return 'AI chat block';
|
||||
} else {
|
||||
return 'AI action panel';
|
||||
}
|
||||
}
|
||||
|
||||
function inferEventName(
|
||||
event: BlocksuiteActionEvent
|
||||
): AIActionEventName | null {
|
||||
if (['result:discard', 'result:retry'].includes(event.event)) {
|
||||
return 'AI result discarded';
|
||||
} else if (event.event.startsWith('result:')) {
|
||||
return 'AI result accepted';
|
||||
} else if (event.event.startsWith('aborted:')) {
|
||||
return 'AI action aborted';
|
||||
} else if (event.event === 'started') {
|
||||
return 'AI action invoked';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inferControl(
|
||||
event: BlocksuiteActionEvent
|
||||
): AIActionEventProperties['control'] {
|
||||
if (event.event === 'aborted:stop') {
|
||||
return 'stop button';
|
||||
} else if (event.event === 'aborted:paywall') {
|
||||
return 'paywall';
|
||||
} else if (event.event === 'aborted:server-error') {
|
||||
return 'server error';
|
||||
} else if (event.event === 'aborted:login-required') {
|
||||
return 'login required';
|
||||
} else if (event.options.control === 'chat-send') {
|
||||
return 'AI chat send button';
|
||||
} else if (event.options.control === 'block-action-bar') {
|
||||
return 'Block action bar';
|
||||
} else if (event.event === 'result:add-note') {
|
||||
return 'add note';
|
||||
} else if (event.event === 'result:add-page') {
|
||||
return 'add page';
|
||||
} else if (event.event === 'result:continue-in-chat') {
|
||||
return 'continue in chat';
|
||||
} else if (event.event === 'result:insert') {
|
||||
return 'insert';
|
||||
} else if (event.event === 'result:replace') {
|
||||
return 'replace';
|
||||
} else if (event.event === 'result:use-as-caption') {
|
||||
return 'use as caption';
|
||||
} else if (event.event === 'result:discard') {
|
||||
return 'discard';
|
||||
} else if (event.event === 'result:retry') {
|
||||
return 'retry';
|
||||
} else {
|
||||
return 'format toolbar';
|
||||
}
|
||||
}
|
||||
|
||||
const toTrackedOptions = (
|
||||
event: BlocksuiteActionEvent
|
||||
): {
|
||||
eventName: AIActionEventName;
|
||||
properties: AIActionEventProperties;
|
||||
} | null => {
|
||||
const eventName = inferEventName(event);
|
||||
|
||||
if (!eventName) return null;
|
||||
|
||||
const pageMode = inferPageMode(event.options.host);
|
||||
const otherProperties = omit(event.options, defaultActionOptions);
|
||||
const type = inferObjectType(event);
|
||||
const segment = inferSegment(event);
|
||||
const module = inferModule(event);
|
||||
const control = inferControl(event);
|
||||
const category = lowerCase(event.action);
|
||||
|
||||
return {
|
||||
eventName,
|
||||
properties: {
|
||||
page: pageMode,
|
||||
segment,
|
||||
category,
|
||||
module,
|
||||
control,
|
||||
type,
|
||||
other: otherProperties,
|
||||
docId: event.options.docId,
|
||||
workspaceId: event.options.workspaceId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function setupTracker() {
|
||||
AIProvider.slots.requestUpgradePlan.on(() => {
|
||||
track.$.paywall.aiAction.viewPlans();
|
||||
});
|
||||
|
||||
AIProvider.slots.requestLogin.on(() => {
|
||||
track.doc.editor.aiActions.requestSignIn();
|
||||
});
|
||||
|
||||
AIProvider.slots.actions.on(event => {
|
||||
const properties = toTrackedOptions(event);
|
||||
if (properties) {
|
||||
trackAction(properties);
|
||||
}
|
||||
});
|
||||
}
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
width: '100%',
|
||||
maxWidth: cssVar('--affine-editor-width'),
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
paddingLeft: cssVar('--affine-editor-side-padding', '24'),
|
||||
paddingRight: cssVar('--affine-editor-side-padding', '24'),
|
||||
fontSize: cssVar('--affine-font-base'),
|
||||
'@container': {
|
||||
[`viewport (width <= 640px)`]: {
|
||||
padding: '0 24px',
|
||||
},
|
||||
},
|
||||
'@media': {
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const titleLine = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
fontWeight: 500,
|
||||
fontSize: '15px',
|
||||
lineHeight: '24px',
|
||||
color: cssVar('--affine-text-primary-color'),
|
||||
});
|
||||
|
||||
export const showButton = style({
|
||||
height: '28px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid ' + cssVar('--affine-border-color'),
|
||||
backgroundColor: cssVar('--affine-white'),
|
||||
textAlign: 'center',
|
||||
fontSize: '12px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: '500',
|
||||
color: cssVar('--affine-text-primary-color'),
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
export const linksContainer = style({
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
export const linksTitles = style({
|
||||
color: cssVar('--affine-text-secondary-color'),
|
||||
height: '32px',
|
||||
lineHeight: '32px',
|
||||
});
|
||||
|
||||
export const link = style({
|
||||
width: '100%',
|
||||
height: '30px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
whiteSpace: 'nowrap',
|
||||
borderRadius: '4px',
|
||||
':hover': {
|
||||
backgroundColor: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${link} .affine-reference-title`, {
|
||||
borderBottom: 'none',
|
||||
});
|
||||
|
||||
globalStyle(`${link} svg`, {
|
||||
color: cssVarV2('icon/secondary'),
|
||||
});
|
||||
|
||||
globalStyle(`${link}:hover svg`, {
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
|
||||
export const linkPreviewContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '12px',
|
||||
marginTop: '4px',
|
||||
marginBottom: '16px',
|
||||
});
|
||||
|
||||
export const linkPreview = style({
|
||||
cursor: 'default',
|
||||
border: `0.5px solid ${cssVarV2('backlinks/blockBorder')}`,
|
||||
borderRadius: '8px',
|
||||
padding: '8px',
|
||||
color: cssVarV2('text/primary'),
|
||||
vars: {
|
||||
[cssVar('fontFamily')]: cssVar('fontSansFamily'),
|
||||
},
|
||||
backgroundColor: cssVarV2('backlinks/blockBackgroundColor'),
|
||||
':hover': {
|
||||
backgroundColor: cssVarV2('backlinks/blockHover'),
|
||||
},
|
||||
});
|
||||
|
||||
globalStyle(`${linkPreview} *`, {
|
||||
cursor: 'default',
|
||||
});
|
||||
|
||||
export const linkPreviewRenderer = style({
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
export const collapsedIcon = style({
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: 20,
|
||||
selectors: {
|
||||
'&[data-collapsed="true"]': {
|
||||
transform: 'rotate(90deg)',
|
||||
color: cssVarV2('icon/secondary'),
|
||||
},
|
||||
},
|
||||
});
|
||||
-382
@@ -1,382 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
createReactComponentFromLit,
|
||||
Divider,
|
||||
useLitPortalFactory,
|
||||
} from '@affine/component';
|
||||
import { TextRenderer } from '@affine/core/blocksuite/presets';
|
||||
import { DocService } from '@affine/core/modules/doc';
|
||||
import {
|
||||
type Backlink,
|
||||
DocLinksService,
|
||||
type Link,
|
||||
} from '@affine/core/modules/doc-link';
|
||||
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||
import { GlobalSessionStateService } from '@affine/core/modules/storage';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import {
|
||||
getAFFiNEWorkspaceSchema,
|
||||
WorkspaceService,
|
||||
} from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import type { TransformerMiddleware } from '@blocksuite/affine/store';
|
||||
import { ToggleDownIcon } from '@blocksuite/icons/rc';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import {
|
||||
LiveData,
|
||||
useFramework,
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import React, {
|
||||
Fragment,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
AffinePageReference,
|
||||
AffineSharedPageReference,
|
||||
} from '../../affine/reference-link';
|
||||
import * as styles from './bi-directional-link-panel.css';
|
||||
import {
|
||||
patchReferenceRenderer,
|
||||
type ReferenceReactRenderer,
|
||||
} from './specs/custom/spec-patchers';
|
||||
import { createPageModeSpecs } from './specs/page';
|
||||
|
||||
const BlocksuiteTextRenderer = createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: TextRenderer,
|
||||
});
|
||||
|
||||
const PREFIX = 'bi-directional-link-panel-collapse:';
|
||||
|
||||
const useBiDirectionalLinkPanelCollapseState = (
|
||||
docId: string,
|
||||
linkDocId?: string
|
||||
) => {
|
||||
const { globalSessionStateService } = useServices({
|
||||
GlobalSessionStateService,
|
||||
});
|
||||
|
||||
const path = linkDocId ? docId + ':' + linkDocId : docId;
|
||||
|
||||
const [open, setOpen] = useState(
|
||||
globalSessionStateService.globalSessionState.get(PREFIX + path) ?? false
|
||||
);
|
||||
|
||||
const wrappedSetOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
setOpen(open);
|
||||
globalSessionStateService.globalSessionState.set(PREFIX + path, open);
|
||||
},
|
||||
[path, globalSessionStateService]
|
||||
);
|
||||
|
||||
return [open, wrappedSetOpen] as const;
|
||||
};
|
||||
|
||||
const CollapsibleSection = ({
|
||||
title,
|
||||
children,
|
||||
length,
|
||||
docId,
|
||||
linkDocId,
|
||||
}: {
|
||||
title: ReactNode;
|
||||
children: ReactNode;
|
||||
length?: number;
|
||||
docId: string;
|
||||
linkDocId?: string;
|
||||
}) => {
|
||||
const [open, setOpen] = useBiDirectionalLinkPanelCollapseState(
|
||||
docId,
|
||||
linkDocId
|
||||
);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setOpen(!open);
|
||||
track.doc.biDirectionalLinksPanel.$.toggle({
|
||||
type: open ? 'collapse' : 'expand',
|
||||
});
|
||||
}, [open, setOpen]);
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={open} onOpenChange={handleToggle}>
|
||||
<Collapsible.Trigger className={styles.link}>
|
||||
{title}
|
||||
{length ? (
|
||||
<ToggleDownIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={!open}
|
||||
/>
|
||||
) : null}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>{children}</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const usePreviewExtensions = () => {
|
||||
const [reactToLit, portals] = useLitPortalFactory();
|
||||
const framework = useFramework();
|
||||
|
||||
const { workspaceService } = useServices({
|
||||
WorkspaceService,
|
||||
});
|
||||
|
||||
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
|
||||
return function customReference(reference) {
|
||||
const data = reference.delta.attributes?.reference;
|
||||
if (!data) return <span />;
|
||||
|
||||
const pageId = data.pageId;
|
||||
if (!pageId) return <span />;
|
||||
|
||||
const params = toURLSearchParams(data.params);
|
||||
|
||||
if (workspaceService.workspace.openOptions.isSharedMode) {
|
||||
return (
|
||||
<AffineSharedPageReference
|
||||
docCollection={workspaceService.workspace.docCollection}
|
||||
pageId={pageId}
|
||||
params={params}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <AffinePageReference pageId={pageId} params={params} />;
|
||||
};
|
||||
}, [workspaceService]);
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
const specs = createPageModeSpecs(framework);
|
||||
specs.extend([patchReferenceRenderer(reactToLit, referenceRenderer)]);
|
||||
return specs.value;
|
||||
}, [reactToLit, referenceRenderer, framework]);
|
||||
|
||||
return [extensions, portals] as const;
|
||||
};
|
||||
|
||||
const useBacklinkGroups = () => {
|
||||
const { docLinksService } = useServices({
|
||||
DocLinksService,
|
||||
});
|
||||
|
||||
const backlinkGroups = useLiveData(
|
||||
LiveData.computed(get => {
|
||||
const links = get(docLinksService.backlinks.backlinks$);
|
||||
|
||||
// group by docId
|
||||
const groupedLinks = links.reduce(
|
||||
(acc, link) => {
|
||||
acc[link.docId] = [...(acc[link.docId] || []), link];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, Backlink[]>
|
||||
);
|
||||
|
||||
return Object.entries(groupedLinks).map(([docId, links]) => ({
|
||||
docId,
|
||||
title: links[0].title, // title should be the same for all blocks
|
||||
links,
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
return backlinkGroups;
|
||||
};
|
||||
|
||||
export const BacklinkGroups = () => {
|
||||
const [extensions, portals] = usePreviewExtensions();
|
||||
const { workspaceService, docService } = useServices({
|
||||
WorkspaceService,
|
||||
DocService,
|
||||
});
|
||||
|
||||
const backlinkGroups = useBacklinkGroups();
|
||||
const textRendererOptions = useMemo(() => {
|
||||
const docLinkBaseURLMiddleware: TransformerMiddleware = ({
|
||||
adapterConfigs,
|
||||
}) => {
|
||||
adapterConfigs.set(
|
||||
'docLinkBaseUrl',
|
||||
`/workspace/${workspaceService.workspace.id}`
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
customHeading: true,
|
||||
extensions,
|
||||
additionalMiddlewares: [docLinkBaseURLMiddleware],
|
||||
};
|
||||
}, [extensions, workspaceService.workspace.id]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{backlinkGroups.map(linkGroup => (
|
||||
<CollapsibleSection
|
||||
key={linkGroup.docId}
|
||||
title={
|
||||
<AffinePageReference
|
||||
pageId={linkGroup.docId}
|
||||
onClick={() => {
|
||||
track.doc.biDirectionalLinksPanel.backlinkTitle.navigate();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
length={linkGroup.links.length}
|
||||
docId={docService.doc.id}
|
||||
linkDocId={linkGroup.docId}
|
||||
>
|
||||
<div className={styles.linkPreviewContainer}>
|
||||
{linkGroup.links.map(link => {
|
||||
if (!link.markdownPreview) {
|
||||
return null;
|
||||
}
|
||||
const searchParams = new URLSearchParams();
|
||||
const displayMode = link.displayMode || 'page';
|
||||
searchParams.set('mode', displayMode);
|
||||
|
||||
let blockId = link.blockId;
|
||||
if (
|
||||
link.parentFlavour === 'affine:database' &&
|
||||
link.parentBlockId
|
||||
) {
|
||||
// if parentBlockFlavour is 'affine:database',
|
||||
// we will fallback to the database block instead
|
||||
blockId = link.parentBlockId;
|
||||
} else if (displayMode === 'edgeless' && link.noteBlockId) {
|
||||
// if note has displayMode === 'edgeless' && has noteBlockId,
|
||||
// set noteBlockId as blockId
|
||||
blockId = link.noteBlockId;
|
||||
}
|
||||
|
||||
searchParams.set('blockIds', blockId);
|
||||
|
||||
const to = {
|
||||
pathname: '/' + linkGroup.docId,
|
||||
search: '?' + searchParams.toString(),
|
||||
hash: '',
|
||||
};
|
||||
|
||||
// if this backlink has no noteBlock && displayMode is edgeless, we will render
|
||||
// the link as a page link
|
||||
const edgelessLink =
|
||||
displayMode === 'edgeless' && !link.noteBlockId;
|
||||
|
||||
return (
|
||||
<WorkbenchLink
|
||||
to={to}
|
||||
key={link.blockId}
|
||||
className={styles.linkPreview}
|
||||
onClick={() => {
|
||||
track.doc.biDirectionalLinksPanel.backlinkPreview.navigate();
|
||||
}}
|
||||
>
|
||||
{edgelessLink ? (
|
||||
<>
|
||||
[Edgeless]
|
||||
<AffinePageReference
|
||||
key={link.blockId}
|
||||
pageId={linkGroup.docId}
|
||||
params={searchParams}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<BlocksuiteTextRenderer
|
||||
className={styles.linkPreviewRenderer}
|
||||
answer={link.markdownPreview}
|
||||
schema={getAFFiNEWorkspaceSchema()}
|
||||
options={textRendererOptions}
|
||||
/>
|
||||
)}
|
||||
</WorkbenchLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
))}
|
||||
<>
|
||||
{portals.map(p => (
|
||||
<Fragment key={p.id}>{p.portal}</Fragment>
|
||||
))}
|
||||
</>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const BiDirectionalLinkPanel = () => {
|
||||
const { docLinksService, docService } = useServices({
|
||||
DocLinksService,
|
||||
DocService,
|
||||
});
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const [show, setShow] = useBiDirectionalLinkPanelCollapseState(
|
||||
docService.doc.id
|
||||
);
|
||||
|
||||
const links = useLiveData(
|
||||
show ? docLinksService.links.links$ : new LiveData([] as Link[])
|
||||
);
|
||||
|
||||
const backlinkGroups = useBacklinkGroups();
|
||||
|
||||
const backlinkCount = useMemo(() => {
|
||||
return backlinkGroups.reduce((acc, link) => acc + link.links.length, 0);
|
||||
}, [backlinkGroups]);
|
||||
|
||||
const handleClickShow = useCallback(() => {
|
||||
setShow(!show);
|
||||
track.doc.biDirectionalLinksPanel.$.toggle({
|
||||
type: show ? 'collapse' : 'expand',
|
||||
});
|
||||
}, [show, setShow]);
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{!show && <Divider size="thinner" />}
|
||||
|
||||
<div className={styles.titleLine}>
|
||||
<div className={styles.title}>Bi-Directional Links</div>
|
||||
<Button className={styles.showButton} onClick={handleClickShow}>
|
||||
{show
|
||||
? t['com.affine.editor.bi-directional-link-panel.hide']()
|
||||
: t['com.affine.editor.bi-directional-link-panel.show']()}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{show && (
|
||||
<>
|
||||
<Divider size="thinner" />
|
||||
|
||||
<div className={styles.linksContainer}>
|
||||
<div className={styles.linksTitles}>
|
||||
{t['com.affine.page-properties.backlinks']()} · {backlinkCount}
|
||||
</div>
|
||||
<BacklinkGroups />
|
||||
</div>
|
||||
<div className={styles.linksContainer}>
|
||||
<div className={styles.linksTitles}>
|
||||
{t['com.affine.page-properties.outgoing-links']()} ·{' '}
|
||||
{links.length}
|
||||
</div>
|
||||
{links.map((link, i) => (
|
||||
<div
|
||||
key={`${link.docId}-${link.params?.toString()}-${i}`}
|
||||
className={styles.link}
|
||||
>
|
||||
<AffinePageReference pageId={link.docId} params={link.params} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-194
@@ -1,194 +0,0 @@
|
||||
import type {
|
||||
EdgelessEditor,
|
||||
PageEditor,
|
||||
} from '@affine/core/blocksuite/editors';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import type { BlockStdScope, EditorHost } from '@blocksuite/affine/block-std';
|
||||
import {
|
||||
appendParagraphCommand,
|
||||
type DocMode,
|
||||
type DocTitle,
|
||||
focusBlockEnd,
|
||||
getLastNoteBlock,
|
||||
type RootBlockModel,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { type Store } from '@blocksuite/affine/store';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type React from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import type { DefaultOpenProperty } from '../../doc-properties';
|
||||
import { BlocksuiteDocEditor, BlocksuiteEdgelessEditor } from './lit-adaper';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
interface BlocksuiteEditorContainerProps {
|
||||
page: Store;
|
||||
mode: DocMode;
|
||||
shared?: boolean;
|
||||
readonly?: boolean;
|
||||
className?: string;
|
||||
defaultOpenProperty?: DefaultOpenProperty;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface AffineEditorContainer extends HTMLElement {
|
||||
page: Store;
|
||||
doc: Store;
|
||||
docTitle: DocTitle;
|
||||
host?: EditorHost;
|
||||
model: RootBlockModel | null;
|
||||
updateComplete: Promise<boolean>;
|
||||
mode: DocMode;
|
||||
origin: HTMLDivElement;
|
||||
std: BlockStdScope;
|
||||
}
|
||||
|
||||
export const BlocksuiteEditorContainer = forwardRef<
|
||||
AffineEditorContainer,
|
||||
BlocksuiteEditorContainerProps
|
||||
>(function AffineEditorContainer(
|
||||
{ page, mode, className, style, shared, readonly, defaultOpenProperty },
|
||||
ref
|
||||
) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const docRef = useRef<PageEditor>(null);
|
||||
const docTitleRef = useRef<DocTitle>(null);
|
||||
const edgelessRef = useRef<EdgelessEditor>(null);
|
||||
const featureFlags = useService(FeatureFlagService).flags;
|
||||
const enableEditorRTL = useLiveData(featureFlags.enable_editor_rtl.$);
|
||||
|
||||
/**
|
||||
* mimic an AffineEditorContainer using proxy
|
||||
*/
|
||||
const affineEditorContainerProxy = useMemo(() => {
|
||||
const api = {
|
||||
get page() {
|
||||
return page;
|
||||
},
|
||||
get doc() {
|
||||
return page;
|
||||
},
|
||||
get docTitle() {
|
||||
return docTitleRef.current;
|
||||
},
|
||||
get host() {
|
||||
return (
|
||||
(mode === 'page'
|
||||
? docRef.current?.host
|
||||
: edgelessRef.current?.host) ?? null
|
||||
);
|
||||
},
|
||||
get model() {
|
||||
return page.root as any;
|
||||
},
|
||||
get updateComplete() {
|
||||
return mode === 'page'
|
||||
? docRef.current?.updateComplete
|
||||
: edgelessRef.current?.updateComplete;
|
||||
},
|
||||
get mode() {
|
||||
return mode;
|
||||
},
|
||||
get origin() {
|
||||
return rootRef.current;
|
||||
},
|
||||
get std() {
|
||||
return mode === 'page' ? docRef.current?.std : edgelessRef.current?.std;
|
||||
},
|
||||
};
|
||||
|
||||
const proxy = new Proxy(api, {
|
||||
has(_, prop) {
|
||||
return (
|
||||
Reflect.has(api, prop) ||
|
||||
(rootRef.current ? Reflect.has(rootRef.current, prop) : false)
|
||||
);
|
||||
},
|
||||
get(_, prop) {
|
||||
if (Reflect.has(api, prop)) {
|
||||
return api[prop as keyof typeof api];
|
||||
}
|
||||
if (rootRef.current && Reflect.has(rootRef.current, prop)) {
|
||||
const maybeFn = Reflect.get(rootRef.current, prop);
|
||||
if (typeof maybeFn === 'function') {
|
||||
return maybeFn.bind(rootRef.current);
|
||||
} else {
|
||||
return maybeFn;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}) as AffineEditorContainer;
|
||||
|
||||
return proxy;
|
||||
}, [mode, page]);
|
||||
|
||||
useImperativeHandle(ref, () => affineEditorContainerProxy, [
|
||||
affineEditorContainerProxy,
|
||||
]);
|
||||
|
||||
const handleClickPageModeBlank = useCallback(() => {
|
||||
if (shared || readonly || page.readonly) return;
|
||||
const std = affineEditorContainerProxy.host?.std;
|
||||
if (!std) {
|
||||
return;
|
||||
}
|
||||
const note = getLastNoteBlock(page);
|
||||
if (note) {
|
||||
const lastBlock = note.lastChild();
|
||||
if (
|
||||
lastBlock &&
|
||||
lastBlock.flavour === 'affine:paragraph' &&
|
||||
lastBlock.text?.length === 0
|
||||
) {
|
||||
const focusBlock = std.view.getBlock(lastBlock.id) ?? undefined;
|
||||
std.command.exec(focusBlockEnd, {
|
||||
focusBlock,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std.command.exec(appendParagraphCommand);
|
||||
}, [affineEditorContainerProxy.host?.std, page, readonly, shared]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={`editor-${page.id}`}
|
||||
dir={enableEditorRTL ? 'rtl' : 'ltr'}
|
||||
className={clsx(
|
||||
`editor-wrapper ${mode}-mode`,
|
||||
styles.docEditorRoot,
|
||||
className
|
||||
)}
|
||||
data-affine-editor-container
|
||||
style={style}
|
||||
ref={rootRef}
|
||||
>
|
||||
{mode === 'page' ? (
|
||||
<BlocksuiteDocEditor
|
||||
shared={shared}
|
||||
page={page}
|
||||
ref={docRef}
|
||||
readonly={readonly}
|
||||
titleRef={docTitleRef}
|
||||
onClickBlank={handleClickPageModeBlank}
|
||||
defaultOpenProperty={defaultOpenProperty}
|
||||
/>
|
||||
) : (
|
||||
<BlocksuiteEdgelessEditor
|
||||
shared={shared}
|
||||
page={page}
|
||||
ref={edgelessRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
import { useRefEffect } from '@affine/component';
|
||||
import { EditorLoading } from '@affine/component/page-detail-skeleton';
|
||||
import { ServerService } from '@affine/core/modules/cloud';
|
||||
import {
|
||||
EditorSettingService,
|
||||
fontStyleOptions,
|
||||
} from '@affine/core/modules/editor-setting';
|
||||
import {
|
||||
customImageProxyMiddleware,
|
||||
type DocMode,
|
||||
ImageProxyService,
|
||||
LinkPreviewerService,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { DisposableGroup } from '@blocksuite/affine/global/utils';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { DefaultOpenProperty } from '../../doc-properties';
|
||||
import {
|
||||
type AffineEditorContainer,
|
||||
BlocksuiteEditorContainer,
|
||||
} from './blocksuite-editor-container';
|
||||
import { NoPageRootError } from './no-page-error';
|
||||
|
||||
export type EditorProps = {
|
||||
page: Store;
|
||||
mode: DocMode;
|
||||
shared?: boolean;
|
||||
readonly?: boolean;
|
||||
defaultOpenProperty?: DefaultOpenProperty;
|
||||
// on Editor ready
|
||||
onEditorReady?: (editor: AffineEditorContainer) => (() => void) | void;
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const BlockSuiteEditorImpl = ({
|
||||
mode,
|
||||
page,
|
||||
className,
|
||||
shared,
|
||||
readonly,
|
||||
style,
|
||||
onEditorReady,
|
||||
defaultOpenProperty,
|
||||
}: EditorProps) => {
|
||||
useEffect(() => {
|
||||
const disposable = page.slots.blockUpdated.once(() => {
|
||||
page.workspace.meta.setDocMeta(page.id, {
|
||||
updatedDate: Date.now(),
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [page]);
|
||||
|
||||
const server = useService(ServerService).server;
|
||||
|
||||
const editorRef = useRefEffect(
|
||||
(editor: AffineEditorContainer) => {
|
||||
globalThis.currentEditor = editor;
|
||||
let canceled = false;
|
||||
const disposableGroup = new DisposableGroup();
|
||||
|
||||
// Invoke onLoad once the editor has been mounted to the DOM.
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// provide image proxy endpoint to blocksuite
|
||||
const imageProxyUrl = new URL(
|
||||
BUILD_CONFIG.imageProxyUrl,
|
||||
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
|
||||
.then(() => {
|
||||
if (onEditorReady) {
|
||||
const dispose = onEditorReady(editor);
|
||||
if (dispose) {
|
||||
disposableGroup.add(dispose);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating editor', error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
disposableGroup.dispose();
|
||||
};
|
||||
},
|
||||
[onEditorReady, page, server]
|
||||
);
|
||||
|
||||
return (
|
||||
<BlocksuiteEditorContainer
|
||||
mode={mode}
|
||||
page={page}
|
||||
shared={shared}
|
||||
readonly={readonly}
|
||||
defaultOpenProperty={defaultOpenProperty}
|
||||
ref={editorRef}
|
||||
className={className}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const BlockSuiteEditor = (props: EditorProps) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const editorSetting = useService(EditorSettingService).editorSetting;
|
||||
const settings = useLiveData(
|
||||
editorSetting.settings$.selector(s => ({
|
||||
fontFamily: s.fontFamily,
|
||||
customFontFamily: s.customFontFamily,
|
||||
fullWidthLayout: s.fullWidthLayout,
|
||||
}))
|
||||
);
|
||||
const fontFamily = useMemo(() => {
|
||||
const fontStyle = fontStyleOptions.find(
|
||||
option => option.key === settings.fontFamily
|
||||
);
|
||||
if (!fontStyle) {
|
||||
return cssVar('fontSansFamily');
|
||||
}
|
||||
const customFontFamily = settings.customFontFamily;
|
||||
|
||||
return customFontFamily && fontStyle.key === 'Custom'
|
||||
? `${customFontFamily}, ${fontStyle.value}`
|
||||
: fontStyle.value;
|
||||
}, [settings.customFontFamily, settings.fontFamily]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.page.root) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
disposable.dispose();
|
||||
setError(new NoPageRootError(props.page));
|
||||
}, 20 * 1000);
|
||||
const disposable = props.page.slots.rootAdded.once(() => {
|
||||
setIsLoading(false);
|
||||
clearTimeout(timer);
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [props.page]);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<Slot style={{ '--affine-font-family': fontFamily } as CSSProperties}>
|
||||
{isLoading ? (
|
||||
<EditorLoading />
|
||||
) : (
|
||||
<BlockSuiteEditorImpl key={props.page.id} {...props} />
|
||||
)}
|
||||
</Slot>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { effects as editorEffects } from '@affine/core/blocksuite/editors';
|
||||
import { registerBlocksuitePresetsCustomComponents } from '@affine/core/blocksuite/presets/effects';
|
||||
import { effects as bsEffects } from '@blocksuite/affine/effects';
|
||||
|
||||
import { effects as edgelessEffects } from './specs/edgeless';
|
||||
import { effects as patchEffects } from './specs/preview';
|
||||
|
||||
bsEffects();
|
||||
patchEffects();
|
||||
editorEffects();
|
||||
edgelessEffects();
|
||||
registerBlocksuitePresetsCustomComponents();
|
||||
|
||||
export * from './blocksuite-editor';
|
||||
export * from './blocksuite-editor-container';
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { i18nTime, useI18n } from '@affine/i18n';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const BlocksuiteEditorJournalDocTitle = ({ page }: { page: Store }) => {
|
||||
const journalService = useService(JournalService);
|
||||
const journalDateStr = useLiveData(journalService.journalDate$(page.id));
|
||||
const journalDate = journalDateStr ? dayjs(journalDateStr) : null;
|
||||
const isTodayJournal = useLiveData(journalService.journalToday$(page.id));
|
||||
const localizedJournalDate = i18nTime(journalDateStr, {
|
||||
absolute: { accuracy: 'day' },
|
||||
});
|
||||
const t = useI18n();
|
||||
|
||||
// TODO(catsjuice): i18n
|
||||
const day = journalDate?.format('dddd') ?? null;
|
||||
|
||||
return (
|
||||
<div className="doc-title-container" data-testid="journal-title">
|
||||
<span data-testid="date">{localizedJournalDate}</span>
|
||||
{isTodayJournal ? (
|
||||
<span className={styles.titleTodayTag} data-testid="date-today-label">
|
||||
{t['com.affine.today']()}
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.titleDayTag}>{day}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,398 +0,0 @@
|
||||
import {
|
||||
createReactComponentFromLit,
|
||||
useConfirmModal,
|
||||
useLitPortalFactory,
|
||||
} from '@affine/component';
|
||||
import { EdgelessEditor, PageEditor } from '@affine/core/blocksuite/editors';
|
||||
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import type {
|
||||
DatabaseRow,
|
||||
DatabaseValueCell,
|
||||
} from '@affine/core/modules/doc-info/types';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { toURLSearchParams } from '@affine/core/modules/navigation';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import track from '@affine/track';
|
||||
import {
|
||||
codeToolbarWidget,
|
||||
type DocMode,
|
||||
DocTitle,
|
||||
embedCardToolbarWidget,
|
||||
formatBarWidget,
|
||||
imageToolbarWidget,
|
||||
slashMenuWidget,
|
||||
surfaceRefToolbarWidget,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
useFramework,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import React, {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
AffinePageReference,
|
||||
AffineSharedPageReference,
|
||||
} from '../../affine/reference-link';
|
||||
import {
|
||||
type DefaultOpenProperty,
|
||||
DocPropertiesTable,
|
||||
} from '../../doc-properties';
|
||||
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
|
||||
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
|
||||
import { extendEdgelessPreviewSpec } from './specs/custom/root-block';
|
||||
import {
|
||||
patchDocModeService,
|
||||
patchEdgelessClipboard,
|
||||
patchForAttachmentEmbedViews,
|
||||
patchForClipboardInElectron,
|
||||
patchForEdgelessNoteConfig,
|
||||
patchForMobile,
|
||||
patchGenerateDocUrlExtension,
|
||||
patchNotificationService,
|
||||
patchOpenDocExtension,
|
||||
patchParseDocUrlExtension,
|
||||
patchPeekViewService,
|
||||
patchQuickSearchService,
|
||||
patchReferenceRenderer,
|
||||
patchSideBarService,
|
||||
type ReferenceReactRenderer,
|
||||
} from './specs/custom/spec-patchers';
|
||||
import { createEdgelessModeSpecs } from './specs/edgeless';
|
||||
import { createPageModeSpecs } from './specs/page';
|
||||
import { StarterBar } from './starter-bar';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
const adapted = {
|
||||
DocEditor: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: PageEditor,
|
||||
}),
|
||||
DocTitle: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: DocTitle,
|
||||
}),
|
||||
EdgelessEditor: createReactComponentFromLit({
|
||||
react: React,
|
||||
elementClass: EdgelessEditor,
|
||||
}),
|
||||
};
|
||||
|
||||
interface BlocksuiteEditorProps {
|
||||
page: Store;
|
||||
readonly?: boolean;
|
||||
shared?: boolean;
|
||||
defaultOpenProperty?: DefaultOpenProperty;
|
||||
}
|
||||
|
||||
const usePatchSpecs = (mode: DocMode) => {
|
||||
const [reactToLit, portals] = useLitPortalFactory();
|
||||
const {
|
||||
peekViewService,
|
||||
docService,
|
||||
docsService,
|
||||
editorService,
|
||||
workspaceService,
|
||||
featureFlagService,
|
||||
} = useServices({
|
||||
PeekViewService,
|
||||
DocService,
|
||||
DocsService,
|
||||
WorkspaceService,
|
||||
EditorService,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const framework = useFramework();
|
||||
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
|
||||
return function customReference(reference) {
|
||||
const data = reference.delta.attributes?.reference;
|
||||
if (!data) return <span />;
|
||||
|
||||
const pageId = data.pageId;
|
||||
if (!pageId) return <span />;
|
||||
|
||||
// title alias
|
||||
const title = data.title;
|
||||
const params = toURLSearchParams(data.params);
|
||||
|
||||
if (workspaceService.workspace.openOptions.isSharedMode) {
|
||||
return (
|
||||
<AffineSharedPageReference
|
||||
docCollection={workspaceService.workspace.docCollection}
|
||||
pageId={pageId}
|
||||
params={params}
|
||||
title={title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AffinePageReference pageId={pageId} params={params} title={title} />
|
||||
);
|
||||
};
|
||||
}, [workspaceService]);
|
||||
|
||||
useMemo(() => {
|
||||
extendEdgelessPreviewSpec(framework);
|
||||
}, [framework]);
|
||||
|
||||
const confirmModal = useConfirmModal();
|
||||
|
||||
const patchedSpecs = useMemo(() => {
|
||||
const builder =
|
||||
mode === 'edgeless'
|
||||
? createEdgelessModeSpecs(framework)
|
||||
: createPageModeSpecs(framework);
|
||||
|
||||
builder.extend(
|
||||
[
|
||||
patchReferenceRenderer(reactToLit, referenceRenderer),
|
||||
patchForEdgelessNoteConfig(framework, reactToLit),
|
||||
patchNotificationService(confirmModal),
|
||||
patchPeekViewService(peekViewService),
|
||||
patchOpenDocExtension(),
|
||||
patchEdgelessClipboard(),
|
||||
patchParseDocUrlExtension(framework),
|
||||
patchGenerateDocUrlExtension(framework),
|
||||
patchQuickSearchService(framework),
|
||||
patchSideBarService(framework),
|
||||
patchDocModeService(docService, docsService, editorService),
|
||||
].flat()
|
||||
);
|
||||
|
||||
if (featureFlagService.flags.enable_pdf_embed_preview.value) {
|
||||
builder.extend([patchForAttachmentEmbedViews(reactToLit)]);
|
||||
}
|
||||
if (BUILD_CONFIG.isMobileEdition) {
|
||||
builder.omit(formatBarWidget);
|
||||
builder.omit(embedCardToolbarWidget);
|
||||
builder.omit(slashMenuWidget);
|
||||
builder.omit(codeToolbarWidget);
|
||||
builder.omit(imageToolbarWidget);
|
||||
builder.omit(surfaceRefToolbarWidget);
|
||||
builder.extend([patchForMobile()].flat());
|
||||
}
|
||||
if (BUILD_CONFIG.isElectron) {
|
||||
builder.extend([patchForClipboardInElectron(framework)].flat());
|
||||
}
|
||||
|
||||
return builder.value;
|
||||
}, [
|
||||
mode,
|
||||
confirmModal,
|
||||
docService,
|
||||
docsService,
|
||||
editorService,
|
||||
framework,
|
||||
peekViewService,
|
||||
reactToLit,
|
||||
referenceRenderer,
|
||||
featureFlagService,
|
||||
]);
|
||||
|
||||
return [
|
||||
patchedSpecs,
|
||||
useMemo(
|
||||
() => (
|
||||
<>
|
||||
{portals.map(p => (
|
||||
<Fragment key={p.id}>{p.portal}</Fragment>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
[portals]
|
||||
),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const BlocksuiteDocEditor = forwardRef<
|
||||
PageEditor,
|
||||
BlocksuiteEditorProps & {
|
||||
onClickBlank?: () => void;
|
||||
titleRef?: React.Ref<DocTitle>;
|
||||
}
|
||||
>(function BlocksuiteDocEditor(
|
||||
{
|
||||
page,
|
||||
shared,
|
||||
onClickBlank,
|
||||
titleRef: externalTitleRef,
|
||||
defaultOpenProperty,
|
||||
readonly,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const titleRef = useRef<DocTitle | null>(null);
|
||||
const docRef = useRef<PageEditor | null>(null);
|
||||
const journalService = useService(JournalService);
|
||||
const isJournal = !!useLiveData(journalService.journalDate$(page.id));
|
||||
|
||||
const editorSettingService = useService(EditorSettingService);
|
||||
|
||||
const onDocRef = useCallback(
|
||||
(el: PageEditor) => {
|
||||
docRef.current = el;
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(el);
|
||||
} else {
|
||||
ref.current = el;
|
||||
}
|
||||
}
|
||||
},
|
||||
[ref]
|
||||
);
|
||||
|
||||
const onTitleRef = useCallback(
|
||||
(el: DocTitle) => {
|
||||
titleRef.current = el;
|
||||
if (externalTitleRef) {
|
||||
if (typeof externalTitleRef === 'function') {
|
||||
externalTitleRef(el);
|
||||
} else {
|
||||
(externalTitleRef as any).current = el;
|
||||
}
|
||||
}
|
||||
},
|
||||
[externalTitleRef]
|
||||
);
|
||||
|
||||
const [specs, portals] = usePatchSpecs('page');
|
||||
|
||||
const displayBiDirectionalLink = useLiveData(
|
||||
editorSettingService.editorSetting.settings$.selector(
|
||||
s => s.displayBiDirectionalLink
|
||||
)
|
||||
);
|
||||
|
||||
const displayDocInfo = useLiveData(
|
||||
editorSettingService.editorSetting.settings$.selector(s => s.displayDocInfo)
|
||||
);
|
||||
|
||||
const onPropertyChange = useCallback((property: DocCustomPropertyInfo) => {
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: property.type,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onPropertyAdded = useCallback((property: DocCustomPropertyInfo) => {
|
||||
track.doc.inlineDocInfo.property.addProperty({
|
||||
type: property.type,
|
||||
control: 'at menu',
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onDatabasePropertyChange = useCallback(
|
||||
(_row: DatabaseRow, cell: DatabaseValueCell) => {
|
||||
track.doc.inlineDocInfo.databaseProperty.editProperty({
|
||||
type: cell.property.type$.value,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onPropertyInfoChange = useCallback(
|
||||
(property: DocCustomPropertyInfo, field: string) => {
|
||||
track.doc.inlineDocInfo.property.editPropertyMeta({
|
||||
type: property.type,
|
||||
field,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.affineDocViewport}>
|
||||
{!isJournal ? (
|
||||
<adapted.DocTitle doc={page} ref={onTitleRef} />
|
||||
) : (
|
||||
<BlocksuiteEditorJournalDocTitle page={page} />
|
||||
)}
|
||||
{!shared && displayDocInfo ? (
|
||||
<div className={styles.docPropertiesTableContainer}>
|
||||
<DocPropertiesTable
|
||||
className={styles.docPropertiesTable}
|
||||
onDatabasePropertyChange={onDatabasePropertyChange}
|
||||
onPropertyChange={onPropertyChange}
|
||||
onPropertyAdded={onPropertyAdded}
|
||||
onPropertyInfoChange={onPropertyInfoChange}
|
||||
defaultOpenProperty={defaultOpenProperty}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<adapted.DocEditor
|
||||
className={styles.docContainer}
|
||||
ref={onDocRef}
|
||||
doc={page}
|
||||
specs={specs}
|
||||
/>
|
||||
<div
|
||||
className={styles.docEditorGap}
|
||||
data-testid="page-editor-blank"
|
||||
onClick={onClickBlank}
|
||||
></div>
|
||||
{!readonly && !BUILD_CONFIG.isMobileEdition && (
|
||||
<StarterBar doc={page} />
|
||||
)}
|
||||
{!shared && displayBiDirectionalLink ? (
|
||||
<BiDirectionalLinkPanel />
|
||||
) : null}
|
||||
</div>
|
||||
{portals}
|
||||
</>
|
||||
);
|
||||
});
|
||||
export const BlocksuiteEdgelessEditor = forwardRef<
|
||||
EdgelessEditor,
|
||||
BlocksuiteEditorProps
|
||||
>(function BlocksuiteEdgelessEditor({ page }, ref) {
|
||||
const [specs, portals] = usePatchSpecs('edgeless');
|
||||
const editorRef = useRef<EdgelessEditor | null>(null);
|
||||
|
||||
const onDocRef = useCallback(
|
||||
(el: EdgelessEditor) => {
|
||||
editorRef.current = el;
|
||||
if (ref) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(el);
|
||||
} else {
|
||||
ref.current = el;
|
||||
}
|
||||
}
|
||||
},
|
||||
[ref]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.updateComplete
|
||||
.then(() => {
|
||||
// make sure editor can get keyboard events on showing up
|
||||
editorRef.current?.querySelector('affine-edgeless-root')?.click();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.affineEdgelessDocViewport}>
|
||||
<adapted.EdgelessEditor ref={onDocRef} doc={page} specs={specs} />
|
||||
{portals}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import type { Doc as YDoc, Map as YMap } from 'yjs';
|
||||
|
||||
/**
|
||||
* TODO(@eyhn): Define error to unexpected state together in the future.
|
||||
*/
|
||||
export class NoPageRootError extends Error {
|
||||
constructor(public page: Store) {
|
||||
super('Page root not found when render editor!');
|
||||
|
||||
// Log info to let sentry collect more message
|
||||
const hasExpectSpace = Array.from(
|
||||
page.rootDoc.getMap<YDoc>('spaces').values()
|
||||
).some(doc => page.spaceDoc.guid === doc.guid);
|
||||
const blocks = page.spaceDoc.getMap('blocks') as YMap<YMap<any>>;
|
||||
const havePageBlock = Array.from(blocks.values()).some(
|
||||
block => block.get('sys:flavour') === 'affine:page'
|
||||
);
|
||||
console.info(
|
||||
'NoPageRootError current data: %s',
|
||||
JSON.stringify({
|
||||
expectPageId: page.id,
|
||||
expectGuid: page.spaceDoc.guid,
|
||||
hasExpectSpace,
|
||||
blockSize: blocks.size,
|
||||
havePageBlock,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { AIChatBlockSpec } from '@affine/core/blocksuite/blocks';
|
||||
import {
|
||||
AICodeBlockSpec,
|
||||
AIImageBlockSpec,
|
||||
AIParagraphBlockSpec,
|
||||
} from '@affine/core/blocksuite/presets';
|
||||
import {
|
||||
AdapterFactoryExtensions,
|
||||
AttachmentBlockSpec,
|
||||
BookmarkBlockSpec,
|
||||
CodeBlockSpec,
|
||||
DatabaseBlockSpec,
|
||||
DataViewBlockSpec,
|
||||
DefaultOpenDocExtension,
|
||||
DividerBlockSpec,
|
||||
EditPropsStore,
|
||||
EmbedExtensions,
|
||||
FontLoaderService,
|
||||
ImageBlockSpec,
|
||||
LatexBlockSpec,
|
||||
ListBlockSpec,
|
||||
ParagraphBlockSpec,
|
||||
RefNodeSlotsExtension,
|
||||
RichTextExtensions,
|
||||
TableBlockSpec,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
|
||||
const CommonBlockSpecs: ExtensionType[] = [
|
||||
RefNodeSlotsExtension,
|
||||
EditPropsStore,
|
||||
RichTextExtensions,
|
||||
LatexBlockSpec,
|
||||
ListBlockSpec,
|
||||
DatabaseBlockSpec,
|
||||
TableBlockSpec,
|
||||
DataViewBlockSpec,
|
||||
DividerBlockSpec,
|
||||
EmbedExtensions,
|
||||
BookmarkBlockSpec,
|
||||
AttachmentBlockSpec,
|
||||
AdapterFactoryExtensions,
|
||||
FontLoaderService,
|
||||
DefaultOpenDocExtension,
|
||||
].flat();
|
||||
|
||||
export const DefaultBlockSpecs: ExtensionType[] = [
|
||||
CodeBlockSpec,
|
||||
ImageBlockSpec,
|
||||
ParagraphBlockSpec,
|
||||
...CommonBlockSpecs,
|
||||
].flat();
|
||||
|
||||
export const AIBlockSpecs: ExtensionType[] = [
|
||||
AICodeBlockSpec,
|
||||
AIImageBlockSpec,
|
||||
AIParagraphBlockSpec,
|
||||
AIChatBlockSpec,
|
||||
...CommonBlockSpecs,
|
||||
].flat();
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import { notify } from '@affine/component';
|
||||
import {
|
||||
generateUrl,
|
||||
type UseSharingUrl,
|
||||
} from '@affine/core/components/hooks/affine/use-share-url';
|
||||
import { ServerService } from '@affine/core/modules/cloud';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type {
|
||||
DatabaseBlockModel,
|
||||
MenuOptions,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { menu } from '@blocksuite/affine/blocks';
|
||||
import { LinkIcon } from '@blocksuite/icons/lit';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
export function createDatabaseOptionsConfig(framework: FrameworkProvider) {
|
||||
return {
|
||||
configure: (model: DatabaseBlockModel, options: MenuOptions) => {
|
||||
const items = options.items;
|
||||
|
||||
items.splice(2, 0, createCopyLinkToBlockMenuItem(framework, model));
|
||||
|
||||
return options;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createCopyLinkToBlockMenuItem(
|
||||
framework: FrameworkProvider,
|
||||
model: DatabaseBlockModel
|
||||
) {
|
||||
return menu.action({
|
||||
name: 'Copy link to block',
|
||||
prefix: LinkIcon({ width: '20', height: '20' }),
|
||||
hide: () => {
|
||||
const { editor } = framework.get(EditorService);
|
||||
const mode = editor.mode$.value;
|
||||
return mode === 'edgeless';
|
||||
},
|
||||
select: () => {
|
||||
const serverService = framework.get(ServerService);
|
||||
const pageId = model.doc.id;
|
||||
const { editor } = framework.get(EditorService);
|
||||
const mode = editor.mode$.value;
|
||||
|
||||
if (mode === 'edgeless') return;
|
||||
|
||||
const workspaceId = editor.doc.workspace.id;
|
||||
const options: UseSharingUrl = {
|
||||
workspaceId,
|
||||
pageId,
|
||||
mode,
|
||||
blockIds: [model.id],
|
||||
};
|
||||
|
||||
const str = generateUrl({
|
||||
...options,
|
||||
baseUrl: serverService.server.baseUrl,
|
||||
});
|
||||
if (!str) return;
|
||||
|
||||
const type = model.flavour;
|
||||
const page = editor.editorContainer$.value;
|
||||
|
||||
copyLinkToBlockStdScopeClipboard(str, page?.host?.std.clipboard)
|
||||
.then(success => {
|
||||
if (!success) return;
|
||||
|
||||
notify.success({ title: I18n['Copied link to clipboard']() });
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
track.doc.editor.toolbar.copyBlockToLink({ type });
|
||||
},
|
||||
});
|
||||
}
|
||||
-276
@@ -1,276 +0,0 @@
|
||||
import { AIChatBlockSpec } from '@affine/core/blocksuite/blocks';
|
||||
import {
|
||||
AICodeBlockSpec,
|
||||
AIImageBlockSpec,
|
||||
AIParagraphBlockSpec,
|
||||
} from '@affine/core/blocksuite/presets';
|
||||
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { AppThemeService } from '@affine/core/modules/theme';
|
||||
import { mixpanel } from '@affine/track';
|
||||
import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/block-std';
|
||||
import type {
|
||||
DocDisplayMetaExtension,
|
||||
DocDisplayMetaParams,
|
||||
Signal,
|
||||
SpecBuilder,
|
||||
TelemetryEventMap,
|
||||
ThemeExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
CodeBlockSpec,
|
||||
ColorScheme,
|
||||
createSignalFromObservable,
|
||||
DatabaseConfigExtension,
|
||||
DocDisplayMetaProvider,
|
||||
EditorSettingExtension,
|
||||
ImageBlockSpec,
|
||||
ParagraphBlockSpec,
|
||||
referenceToNode,
|
||||
RootBlockConfigExtension,
|
||||
SpecProvider,
|
||||
TelemetryProvider,
|
||||
ThemeExtensionIdentifier,
|
||||
ToolbarMoreMenuConfigExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import { LinkedPageIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import type { Observable } from 'rxjs';
|
||||
import { combineLatest, map } from 'rxjs';
|
||||
|
||||
import { getFontConfigExtension } from '../font-extension';
|
||||
import { createDatabaseOptionsConfig } from './database-block';
|
||||
import { createLinkedWidgetConfig } from './widgets/linked';
|
||||
import { createToolbarMoreMenuConfig } from './widgets/toolbar';
|
||||
|
||||
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>);
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getThemeExtension(framework: FrameworkProvider) {
|
||||
class AffineThemeExtension
|
||||
extends LifeCycleWatcher
|
||||
implements ThemeExtension
|
||||
{
|
||||
static override readonly key = 'affine-theme';
|
||||
|
||||
private readonly themes: Map<string, Signal<ColorScheme>> = new Map();
|
||||
|
||||
protected readonly disposables: (() => void)[] = [];
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
di.override(ThemeExtensionIdentifier, AffineThemeExtension, [
|
||||
StdIdentifier,
|
||||
]);
|
||||
}
|
||||
|
||||
getAppTheme() {
|
||||
const keyName = 'app-theme';
|
||||
const cache = this.themes.get(keyName);
|
||||
if (cache) return cache;
|
||||
|
||||
const theme$: Observable<ColorScheme> = framework
|
||||
.get(AppThemeService)
|
||||
.appTheme.theme$.map(theme => {
|
||||
return theme === ColorScheme.Dark
|
||||
? ColorScheme.Dark
|
||||
: ColorScheme.Light;
|
||||
});
|
||||
const { signal: themeSignal, cleanup } =
|
||||
createSignalFromObservable<ColorScheme>(theme$, ColorScheme.Light);
|
||||
this.disposables.push(cleanup);
|
||||
this.themes.set(keyName, themeSignal);
|
||||
return themeSignal;
|
||||
}
|
||||
|
||||
getEdgelessTheme(docId?: string) {
|
||||
const doc =
|
||||
(docId && framework.get(DocsService).list.doc$(docId).getValue()) ||
|
||||
framework.get(DocService).doc;
|
||||
|
||||
const cache = this.themes.get(doc.id);
|
||||
if (cache) return cache;
|
||||
|
||||
const appTheme$ = framework.get(AppThemeService).appTheme.theme$;
|
||||
const docTheme$ = doc.properties$.map(
|
||||
props => props.edgelessColorTheme || 'system'
|
||||
);
|
||||
const theme$: Observable<ColorScheme> = combineLatest([
|
||||
appTheme$,
|
||||
docTheme$,
|
||||
]).pipe(
|
||||
map(([appTheme, docTheme]) => {
|
||||
const theme = docTheme === 'system' ? appTheme : docTheme;
|
||||
return theme === ColorScheme.Dark
|
||||
? ColorScheme.Dark
|
||||
: ColorScheme.Light;
|
||||
})
|
||||
);
|
||||
const { signal: themeSignal, cleanup } =
|
||||
createSignalFromObservable<ColorScheme>(theme$, ColorScheme.Light);
|
||||
this.disposables.push(cleanup);
|
||||
this.themes.set(doc.id, themeSignal);
|
||||
return themeSignal;
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(dispose => dispose());
|
||||
}
|
||||
}
|
||||
|
||||
return AffineThemeExtension;
|
||||
}
|
||||
|
||||
export function buildDocDisplayMetaExtension(framework: FrameworkProvider) {
|
||||
const docDisplayMetaService = framework.get(DocDisplayMetaService);
|
||||
|
||||
function iconBuilder(
|
||||
icon: typeof PageIcon,
|
||||
size = '1.25em',
|
||||
style = 'user-select:none;flex-shrink:0;vertical-align:middle;font-size:inherit;margin-bottom:0.1em;'
|
||||
) {
|
||||
return icon({
|
||||
width: size,
|
||||
height: size,
|
||||
style,
|
||||
});
|
||||
}
|
||||
|
||||
class AffineDocDisplayMetaService
|
||||
extends LifeCycleWatcher
|
||||
implements DocDisplayMetaExtension
|
||||
{
|
||||
static override key = 'doc-display-meta';
|
||||
|
||||
readonly disposables: (() => void)[] = [];
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
di.override(DocDisplayMetaProvider, this, [StdIdentifier]);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
while (this.disposables.length > 0) {
|
||||
this.disposables.pop()?.();
|
||||
}
|
||||
}
|
||||
|
||||
icon(
|
||||
docId: string,
|
||||
{ params, title, referenced }: DocDisplayMetaParams = {}
|
||||
): Signal<TemplateResult> {
|
||||
const icon$ = docDisplayMetaService
|
||||
.icon$(docId, {
|
||||
type: 'lit',
|
||||
title,
|
||||
reference: referenced,
|
||||
referenceToNode: referenceToNode({ pageId: docId, params }),
|
||||
})
|
||||
.map(iconBuilder);
|
||||
|
||||
const { signal: iconSignal, cleanup } = createSignalFromObservable(
|
||||
icon$,
|
||||
iconBuilder(referenced ? LinkedPageIcon : PageIcon)
|
||||
);
|
||||
|
||||
this.disposables.push(cleanup);
|
||||
|
||||
return iconSignal;
|
||||
}
|
||||
|
||||
title(
|
||||
docId: string,
|
||||
{ title, referenced }: DocDisplayMetaParams = {}
|
||||
): Signal<string> {
|
||||
const title$ = docDisplayMetaService.title$(docId, {
|
||||
title,
|
||||
reference: referenced,
|
||||
});
|
||||
|
||||
const { signal: titleSignal, cleanup } =
|
||||
createSignalFromObservable<string>(title$, title ?? '');
|
||||
|
||||
this.disposables.push(cleanup);
|
||||
|
||||
return titleSignal;
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
return AffineDocDisplayMetaService;
|
||||
}
|
||||
|
||||
function getEditorConfigExtension(
|
||||
framework: FrameworkProvider
|
||||
): ExtensionType[] {
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
return [
|
||||
EditorSettingExtension(editorSettingService.editorSetting.settingSignal),
|
||||
DatabaseConfigExtension(createDatabaseOptionsConfig(framework)),
|
||||
RootBlockConfigExtension({
|
||||
linkedWidget: createLinkedWidgetConfig(framework),
|
||||
}),
|
||||
ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)),
|
||||
];
|
||||
}
|
||||
|
||||
export const extendEdgelessPreviewSpec = (function () {
|
||||
let _extension: ExtensionType;
|
||||
let _framework: FrameworkProvider;
|
||||
return function (framework: FrameworkProvider) {
|
||||
if (framework === _framework && _extension) {
|
||||
return _extension;
|
||||
} else {
|
||||
_extension && SpecProvider._.omitSpec('preview:edgeless', _extension);
|
||||
_extension = getThemeExtension(framework);
|
||||
_framework = framework;
|
||||
SpecProvider._.extendSpec('preview:edgeless', [_extension]);
|
||||
return _extension;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
export function enableAffineExtension(
|
||||
framework: FrameworkProvider,
|
||||
specBuilder: SpecBuilder
|
||||
): void {
|
||||
specBuilder.extend(
|
||||
[
|
||||
getThemeExtension(framework),
|
||||
getFontConfigExtension(),
|
||||
getTelemetryExtension(),
|
||||
getEditorConfigExtension(framework),
|
||||
buildDocDisplayMetaExtension(framework),
|
||||
].flat()
|
||||
);
|
||||
}
|
||||
|
||||
export function enableAIExtension(specBuilder: SpecBuilder): void {
|
||||
specBuilder.replace(CodeBlockSpec, AICodeBlockSpec);
|
||||
specBuilder.replace(ImageBlockSpec, AIImageBlockSpec);
|
||||
specBuilder.replace(ParagraphBlockSpec, AIParagraphBlockSpec);
|
||||
specBuilder.extend(AIChatBlockSpec);
|
||||
}
|
||||
-761
@@ -1,761 +0,0 @@
|
||||
import {
|
||||
type ElementOrFactory,
|
||||
Input,
|
||||
notify,
|
||||
toast,
|
||||
type ToastOptions,
|
||||
toReactNode,
|
||||
type useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { AIChatBlockSchema } from '@affine/core/blocksuite/blocks';
|
||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import { DesktopApiService } from '@affine/core/modules/desktop-api';
|
||||
import { type DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import type { EditorService } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { resolveLinkToDoc } from '@affine/core/modules/navigation';
|
||||
import type { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import {
|
||||
CreationQuickSearchSession,
|
||||
DocsQuickSearchSession,
|
||||
LinksQuickSearchSession,
|
||||
QuickSearchService,
|
||||
RecentDocsQuickSearchSession,
|
||||
} from '@affine/core/modules/quicksearch';
|
||||
import { ExternalLinksQuickSearchSession } from '@affine/core/modules/quicksearch/impls/external-links';
|
||||
import { JournalsQuickSearchSession } from '@affine/core/modules/quicksearch/impls/journals';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
BlockServiceWatcher,
|
||||
type BlockStdScope,
|
||||
BlockViewIdentifier,
|
||||
ConfigIdentifier,
|
||||
LifeCycleWatcher,
|
||||
type WidgetComponent,
|
||||
} from '@blocksuite/affine/block-std';
|
||||
import type {
|
||||
AffineReference,
|
||||
CodeBlockConfig,
|
||||
DocMode,
|
||||
DocModeProvider,
|
||||
OpenDocConfig,
|
||||
OpenDocConfigItem,
|
||||
PeekOptions,
|
||||
PeekViewService as BSPeekViewService,
|
||||
QuickSearchResult,
|
||||
ReferenceNodeConfig,
|
||||
RootBlockConfig,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
AffineSlashMenuWidget,
|
||||
AttachmentEmbedConfigIdentifier,
|
||||
DocModeExtension,
|
||||
EdgelessRootBlockComponent,
|
||||
EmbedLinkedDocBlockComponent,
|
||||
FeatureFlagService,
|
||||
GenerateDocUrlExtension,
|
||||
insertLinkByQuickSearchCommand,
|
||||
NativeClipboardExtension,
|
||||
NoteConfigExtension,
|
||||
NotificationExtension,
|
||||
OpenDocExtension,
|
||||
ParagraphBlockService,
|
||||
ParseDocUrlExtension,
|
||||
PeekViewExtension,
|
||||
QuickSearchExtension,
|
||||
ReferenceNodeConfigExtension,
|
||||
ReferenceNodeConfigIdentifier,
|
||||
RootBlockConfigExtension,
|
||||
SidebarExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import { Bound } from '@blocksuite/affine/global/utils';
|
||||
import {
|
||||
type BlockSnapshot,
|
||||
type ExtensionType,
|
||||
Text,
|
||||
} from '@blocksuite/affine/store';
|
||||
import type { ReferenceParams } from '@blocksuite/affine-model';
|
||||
import {
|
||||
CenterPeekIcon,
|
||||
ExpandFullIcon,
|
||||
OpenInNewIcon,
|
||||
SplitViewIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
import { pick } from 'lodash-es';
|
||||
|
||||
import type { DocProps } from '../../../../../blocksuite/initialization';
|
||||
import { AttachmentEmbedPreview } from '../../../../attachment-viewer/pdf-viewer-embedded';
|
||||
import { generateUrl } from '../../../../hooks/affine/use-share-url';
|
||||
import { BlocksuiteEditorJournalDocTitle } from '../../journal-doc-title';
|
||||
import { EdgelessNoteHeader } from './widgets/edgeless-note-header';
|
||||
import { createKeyboardToolbarConfig } from './widgets/keyboard-toolbar';
|
||||
|
||||
export type ReferenceReactRenderer = (
|
||||
reference: AffineReference
|
||||
) => React.ReactElement;
|
||||
|
||||
const logger = new DebugLogger('affine::spec-patchers');
|
||||
|
||||
function patchSpecService(
|
||||
flavour: string,
|
||||
onWidgetConnected?: (component: WidgetComponent) => void
|
||||
) {
|
||||
class TempServiceWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = flavour;
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
const disposableGroup = this.blockService.disposables;
|
||||
if (onWidgetConnected) {
|
||||
disposableGroup.add(
|
||||
this.blockService.specSlots.widgetConnected.on(({ component }) => {
|
||||
onWidgetConnected(component);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return TempServiceWatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the block specs with custom renderers.
|
||||
*/
|
||||
export function patchReferenceRenderer(
|
||||
reactToLit: (element: ElementOrFactory) => TemplateResult,
|
||||
reactRenderer: ReferenceReactRenderer
|
||||
): ExtensionType {
|
||||
const customContent = (reference: AffineReference) => {
|
||||
const node = reactRenderer(reference);
|
||||
return reactToLit(node);
|
||||
};
|
||||
|
||||
return ReferenceNodeConfigExtension({
|
||||
customContent,
|
||||
});
|
||||
}
|
||||
|
||||
export function patchNotificationService({
|
||||
closeConfirmModal,
|
||||
openConfirmModal,
|
||||
}: ReturnType<typeof useConfirmModal>) {
|
||||
return NotificationExtension({
|
||||
confirm: async ({ title, message, confirmText, cancelText, abort }) => {
|
||||
return new Promise<boolean>(resolve => {
|
||||
openConfirmModal({
|
||||
title: toReactNode(title),
|
||||
description: toReactNode(message),
|
||||
confirmText,
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelText,
|
||||
onConfirm: () => {
|
||||
resolve(true);
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
abort?.addEventListener('abort', () => {
|
||||
resolve(false);
|
||||
closeConfirmModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
prompt: async ({
|
||||
title,
|
||||
message,
|
||||
confirmText,
|
||||
placeholder,
|
||||
cancelText,
|
||||
autofill,
|
||||
abort,
|
||||
}) => {
|
||||
return new Promise<string | null>(resolve => {
|
||||
let value = autofill || '';
|
||||
const description = (
|
||||
<div>
|
||||
<span style={{ marginBottom: 12 }}>{toReactNode(message)}</span>
|
||||
<Input
|
||||
autoSelect={true}
|
||||
placeholder={placeholder}
|
||||
defaultValue={value}
|
||||
onChange={e => (value = e)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
openConfirmModal({
|
||||
title: toReactNode(title),
|
||||
description: description,
|
||||
confirmText: confirmText ?? 'Confirm',
|
||||
confirmButtonOptions: {
|
||||
variant: 'primary',
|
||||
},
|
||||
cancelText: cancelText ?? 'Cancel',
|
||||
onConfirm: () => {
|
||||
resolve(value);
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve(null);
|
||||
},
|
||||
autoFocusConfirm: false,
|
||||
});
|
||||
abort?.addEventListener('abort', () => {
|
||||
resolve(null);
|
||||
closeConfirmModal();
|
||||
});
|
||||
});
|
||||
},
|
||||
toast: (message: string, options: ToastOptions) => {
|
||||
return toast(message, options);
|
||||
},
|
||||
notify: notification => {
|
||||
const accentToNotify = {
|
||||
error: notify.error,
|
||||
success: notify.success,
|
||||
warning: notify.warning,
|
||||
info: notify,
|
||||
};
|
||||
|
||||
const fn = accentToNotify[notification.accent || 'info'];
|
||||
if (!fn) {
|
||||
throw new Error('Invalid notification accent');
|
||||
}
|
||||
|
||||
const toastId = fn(
|
||||
{
|
||||
title: toReactNode(notification.title),
|
||||
message: toReactNode(notification.message),
|
||||
footer: toReactNode(notification.footer),
|
||||
action: notification.action?.onClick
|
||||
? {
|
||||
label: toReactNode(notification.action?.label),
|
||||
onClick: notification.action.onClick,
|
||||
}
|
||||
: undefined,
|
||||
onDismiss: notification.onClose,
|
||||
},
|
||||
{
|
||||
duration: notification.duration || 0,
|
||||
onDismiss: notification.onClose,
|
||||
onAutoClose: notification.onClose,
|
||||
}
|
||||
);
|
||||
|
||||
notification.abort?.addEventListener('abort', () => {
|
||||
notify.dismiss(toastId);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function patchOpenDocExtension() {
|
||||
const openDocConfig: OpenDocConfig = {
|
||||
items: [
|
||||
{
|
||||
type: 'open-in-active-view',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc'](),
|
||||
icon: ExpandFullIcon(),
|
||||
},
|
||||
BUILD_CONFIG.isElectron
|
||||
? {
|
||||
type: 'open-in-new-view',
|
||||
label:
|
||||
I18n['com.affine.peek-view-controls.open-doc-in-split-view'](),
|
||||
icon: SplitViewIcon(),
|
||||
}
|
||||
: null,
|
||||
{
|
||||
type: 'open-in-new-tab',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](),
|
||||
icon: OpenInNewIcon(),
|
||||
},
|
||||
{
|
||||
type: 'open-in-center-peek',
|
||||
label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](),
|
||||
icon: CenterPeekIcon(),
|
||||
},
|
||||
].filter((item): item is OpenDocConfigItem => item !== null),
|
||||
};
|
||||
return OpenDocExtension(openDocConfig);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function patchDocModeService(
|
||||
docService: DocService,
|
||||
docsService: DocsService,
|
||||
editorService: EditorService
|
||||
): ExtensionType {
|
||||
const DEFAULT_MODE = 'page';
|
||||
class AffineDocModeService implements DocModeProvider {
|
||||
setEditorMode = (mode: DocMode) => {
|
||||
editorService.editor.setMode(mode);
|
||||
};
|
||||
getEditorMode = () => {
|
||||
return editorService.editor.mode$.value;
|
||||
};
|
||||
setPrimaryMode = (mode: DocMode, id?: string) => {
|
||||
if (id) {
|
||||
docsService.list.setPrimaryMode(id, mode);
|
||||
} else {
|
||||
docService.doc.setPrimaryMode(mode);
|
||||
}
|
||||
};
|
||||
getPrimaryMode = (id?: string) => {
|
||||
const mode = id
|
||||
? docsService.list.getPrimaryMode(id)
|
||||
: docService.doc.getPrimaryMode();
|
||||
return (mode || DEFAULT_MODE) as DocMode;
|
||||
};
|
||||
togglePrimaryMode = (id?: string) => {
|
||||
const mode = id
|
||||
? docsService.list.togglePrimaryMode(id)
|
||||
: docService.doc.togglePrimaryMode();
|
||||
return (mode || DEFAULT_MODE) as DocMode;
|
||||
};
|
||||
onPrimaryModeChange = (handler: (mode: DocMode) => void, id?: string) => {
|
||||
const mode$ = id
|
||||
? docsService.list.primaryMode$(id)
|
||||
: docService.doc.primaryMode$;
|
||||
const sub = mode$.subscribe(m => handler((m || DEFAULT_MODE) as DocMode));
|
||||
return {
|
||||
dispose: sub.unsubscribe,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const docModeExtension = DocModeExtension(new AffineDocModeService());
|
||||
|
||||
return docModeExtension;
|
||||
}
|
||||
|
||||
export function patchQuickSearchService(framework: FrameworkProvider) {
|
||||
const QuickSearch = QuickSearchExtension({
|
||||
async openQuickSearch() {
|
||||
let searchResult: QuickSearchResult = null;
|
||||
searchResult = await new Promise((resolve, reject) =>
|
||||
framework.get(QuickSearchService).quickSearch.show(
|
||||
[
|
||||
framework.get(RecentDocsQuickSearchSession),
|
||||
framework.get(CreationQuickSearchSession),
|
||||
framework.get(DocsQuickSearchSession),
|
||||
framework.get(LinksQuickSearchSession),
|
||||
framework.get(ExternalLinksQuickSearchSession),
|
||||
framework.get(JournalsQuickSearchSession),
|
||||
],
|
||||
result => {
|
||||
if (result === null) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'docs') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'recent-doc') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'link') {
|
||||
resolve({
|
||||
docId: result.payload.docId,
|
||||
params: pick(result.payload, [
|
||||
'mode',
|
||||
'blockIds',
|
||||
'elementIds',
|
||||
]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'date-picker') {
|
||||
result.payload
|
||||
.getDocId()
|
||||
.then(docId => {
|
||||
if (docId) {
|
||||
resolve({ docId });
|
||||
}
|
||||
})
|
||||
.catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'external-link') {
|
||||
const externalUrl = result.payload.url;
|
||||
resolve({ externalUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.source === 'creation') {
|
||||
const docsService = framework.get(DocsService);
|
||||
const editorSettingService = framework.get(EditorSettingService);
|
||||
const mode =
|
||||
result.id === 'creation:create-edgeless' ? 'edgeless' : 'page';
|
||||
const docProps: DocProps = {
|
||||
page: { title: new Text(result.payload.title) },
|
||||
note: editorSettingService.editorSetting.get('affine:note'),
|
||||
};
|
||||
const newDoc = docsService.createDoc({
|
||||
primaryMode: mode,
|
||||
docProps,
|
||||
});
|
||||
|
||||
resolve({ docId: newDoc.id });
|
||||
return;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: {
|
||||
i18nKey: 'com.affine.cmdk.insert-links',
|
||||
},
|
||||
placeholder: {
|
||||
i18nKey: 'com.affine.cmdk.docs.placeholder',
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return searchResult;
|
||||
},
|
||||
});
|
||||
|
||||
const SlashMenuQuickSearchExtension = patchSpecService(
|
||||
'affine:page',
|
||||
(component: WidgetComponent) => {
|
||||
if (component instanceof AffineSlashMenuWidget) {
|
||||
component.config.items.forEach(item => {
|
||||
if (
|
||||
'action' in item &&
|
||||
(item.name === 'Linked Doc' || item.name === 'Link')
|
||||
) {
|
||||
item.action = async ({ rootComponent }) => {
|
||||
const [success, { insertedLinkType }] =
|
||||
rootComponent.std.command.exec(insertLinkByQuickSearchCommand);
|
||||
|
||||
if (!success) return;
|
||||
|
||||
insertedLinkType
|
||||
?.then(type => {
|
||||
const flavour = type?.flavour;
|
||||
if (!flavour) return;
|
||||
|
||||
if (flavour === 'affine:bookmark') {
|
||||
track.doc.editor.slashMenu.bookmark();
|
||||
return;
|
||||
}
|
||||
|
||||
if (flavour === 'affine:embed-linked-doc') {
|
||||
track.doc.editor.slashMenu.linkDoc({
|
||||
control: 'linkDoc',
|
||||
});
|
||||
return;
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
return [QuickSearch, SlashMenuQuickSearchExtension];
|
||||
}
|
||||
|
||||
export function patchParseDocUrlExtension(framework: FrameworkProvider) {
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const ParseDocUrl = ParseDocUrlExtension({
|
||||
parseDocUrl(url) {
|
||||
const info = resolveLinkToDoc(url);
|
||||
if (!info || info.workspaceId !== workspaceService.workspace.id) return;
|
||||
|
||||
delete info.refreshKey;
|
||||
|
||||
return info;
|
||||
},
|
||||
});
|
||||
|
||||
return [ParseDocUrl];
|
||||
}
|
||||
|
||||
export function patchGenerateDocUrlExtension(framework: FrameworkProvider) {
|
||||
const workspaceService = framework.get(WorkspaceService);
|
||||
const workspaceServerService = framework.get(WorkspaceServerService);
|
||||
const GenerateDocUrl = GenerateDocUrlExtension({
|
||||
generateDocUrl(pageId: string, params?: ReferenceParams) {
|
||||
return generateUrl({
|
||||
...params,
|
||||
pageId,
|
||||
workspaceId: workspaceService.workspace.id,
|
||||
baseUrl: workspaceServerService.server?.baseUrl ?? location.origin,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return [GenerateDocUrl];
|
||||
}
|
||||
|
||||
export function patchEdgelessClipboard() {
|
||||
class EdgelessClipboardWatcher extends BlockServiceWatcher {
|
||||
static override readonly flavour = 'affine:page';
|
||||
|
||||
override mounted() {
|
||||
super.mounted();
|
||||
this.blockService.disposables.add(
|
||||
this.blockService.specSlots.viewConnected.on(view => {
|
||||
const { component } = view;
|
||||
if (component instanceof EdgelessRootBlockComponent) {
|
||||
const AIChatBlockFlavour = AIChatBlockSchema.model.flavour;
|
||||
const createFunc = (block: BlockSnapshot) => {
|
||||
const {
|
||||
xywh,
|
||||
scale,
|
||||
messages,
|
||||
sessionId,
|
||||
rootDocId,
|
||||
rootWorkspaceId,
|
||||
} = block.props;
|
||||
const blockId = component.service.crud.addBlock(
|
||||
AIChatBlockFlavour,
|
||||
{
|
||||
xywh,
|
||||
scale,
|
||||
messages,
|
||||
sessionId,
|
||||
rootDocId,
|
||||
rootWorkspaceId,
|
||||
},
|
||||
component.surface.model.id
|
||||
);
|
||||
return blockId;
|
||||
};
|
||||
component.clipboardController.registerBlock(
|
||||
AIChatBlockFlavour,
|
||||
createFunc
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return EdgelessClipboardWatcher;
|
||||
}
|
||||
|
||||
@customElement('affine-linked-doc-ref-block')
|
||||
export class LinkedDocBlockComponent extends EmbedLinkedDocBlockComponent {
|
||||
override getInitialState() {
|
||||
return {
|
||||
loading: false,
|
||||
isBannerEmpty: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function patchForSharedPage() {
|
||||
const extension: ExtensionType = {
|
||||
setup: di => {
|
||||
di.override(
|
||||
BlockViewIdentifier('affine:embed-linked-doc'),
|
||||
() => literal`affine-linked-doc-ref-block`
|
||||
);
|
||||
di.override(
|
||||
BlockViewIdentifier('affine:embed-synced-doc'),
|
||||
() => literal`affine-linked-doc-ref-block`
|
||||
);
|
||||
},
|
||||
};
|
||||
return extension;
|
||||
}
|
||||
|
||||
export function patchForMobile() {
|
||||
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);
|
||||
}
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
|
||||
// Hide reference popup on mobile.
|
||||
{
|
||||
const prev = di.getFactory(ReferenceNodeConfigIdentifier);
|
||||
di.override(ReferenceNodeConfigIdentifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
hidePopup: true,
|
||||
} satisfies ReferenceNodeConfig;
|
||||
});
|
||||
}
|
||||
|
||||
// Hide number lines for code block on mobile.
|
||||
{
|
||||
const codeConfigIdentifier = ConfigIdentifier('affine:code');
|
||||
const prev = di.getFactory(codeConfigIdentifier);
|
||||
di.override(codeConfigIdentifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
showLineNumbers: false,
|
||||
} satisfies CodeBlockConfig;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override mounted() {
|
||||
// remove slash placeholder for mobile: `type / ...`
|
||||
{
|
||||
const paragraphService = this.std.get(ParagraphBlockService);
|
||||
if (!paragraphService) return;
|
||||
|
||||
paragraphService.placeholderGenerator = 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.type];
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const extensions: ExtensionType[] = [
|
||||
{
|
||||
setup: di => {
|
||||
const prev = di.getFactory(RootBlockConfigExtension.identifier);
|
||||
|
||||
di.override(RootBlockConfigExtension.identifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
keyboardToolbar: createKeyboardToolbarConfig(),
|
||||
} satisfies RootBlockConfig;
|
||||
});
|
||||
},
|
||||
},
|
||||
MobileSpecsPatches,
|
||||
];
|
||||
return extensions;
|
||||
}
|
||||
|
||||
export function patchForAttachmentEmbedViews(
|
||||
reactToLit: (
|
||||
element: ElementOrFactory,
|
||||
rerendering?: boolean
|
||||
) => TemplateResult
|
||||
): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({
|
||||
name: 'pdf',
|
||||
check: (model, maxFileSize) =>
|
||||
model.type === 'application/pdf' && model.size <= maxFileSize,
|
||||
action: model => {
|
||||
const bound = Bound.deserialize(model.xywh);
|
||||
bound.w = 537 + 24 + 2;
|
||||
bound.h = 759 + 46 + 24 + 2;
|
||||
model.doc.updateBlock(model, {
|
||||
embed: true,
|
||||
style: 'pdf',
|
||||
xywh: bound.serialize(),
|
||||
});
|
||||
},
|
||||
template: (model, _blobUrl) =>
|
||||
reactToLit(<AttachmentEmbedPreview model={model} />, false),
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function patchForClipboardInElectron(framework: FrameworkProvider) {
|
||||
const desktopApi = framework.get(DesktopApiService);
|
||||
return NativeClipboardExtension({
|
||||
copyAsPNG: desktopApi.handler.clipboard.copyAsPNG,
|
||||
});
|
||||
}
|
||||
|
||||
export function patchForEdgelessNoteConfig(
|
||||
framework: FrameworkProvider,
|
||||
reactToLit: (element: ElementOrFactory) => TemplateResult
|
||||
) {
|
||||
return NoteConfigExtension({
|
||||
edgelessNoteHeader: ({ note }) =>
|
||||
reactToLit(<EdgelessNoteHeader note={note} />),
|
||||
pageBlockTitle: ({ note }) => {
|
||||
const journalService = framework.get(JournalService);
|
||||
const isJournal = !!journalService.journalDate$(note.doc.id).value;
|
||||
if (isJournal) {
|
||||
return reactToLit(<BlocksuiteEditorJournalDocTitle page={note.doc} />);
|
||||
} else {
|
||||
return html`<doc-title .doc=${note.doc}></doc-title>`;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function patchSideBarService(framework: FrameworkProvider) {
|
||||
const { workbench } = framework.get(WorkbenchService);
|
||||
|
||||
return SidebarExtension({
|
||||
open: (tabId?: string) => {
|
||||
workbench.openSidebar();
|
||||
workbench.activeView$.value.activeSidebarTab(tabId ?? null);
|
||||
},
|
||||
close: () => {
|
||||
workbench.closeSidebar();
|
||||
},
|
||||
getTabIds: () => {
|
||||
return workbench.activeView$.value.sidebarTabs$.value.map(tab => tab.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
-227
@@ -1,227 +0,0 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { isMindmapChild, isMindMapRoot } from '@affine/core/blocksuite/presets';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { apis } from '@affine/electron-api';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import type { BlockStdScope } from '@blocksuite/affine/block-std';
|
||||
import {
|
||||
type GfxBlockElementModel,
|
||||
GfxControllerIdentifier,
|
||||
type GfxModel,
|
||||
GfxPrimitiveElementModel,
|
||||
isGfxGroupCompatibleModel,
|
||||
} from '@blocksuite/affine/block-std/gfx';
|
||||
import type { MenuContext } from '@blocksuite/affine/blocks';
|
||||
import { Bound, getCommonBound } from '@blocksuite/affine/global/utils';
|
||||
import { CopyAsImgaeIcon } from '@blocksuite/icons/lit';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
const snapshotStyle = `
|
||||
affine-edgeless-root .widgets-container,
|
||||
.copy-as-image-transparent {
|
||||
opacity: 0;
|
||||
}
|
||||
.edgeless-background {
|
||||
background-image: none;
|
||||
}
|
||||
`;
|
||||
|
||||
function getSelectedRect() {
|
||||
const selected = document
|
||||
.querySelector('edgeless-selected-rect')
|
||||
?.shadowRoot?.querySelector('.affine-edgeless-selected-rect');
|
||||
if (!selected) {
|
||||
throw new Error('Missing edgeless selected rect');
|
||||
}
|
||||
return selected.getBoundingClientRect();
|
||||
}
|
||||
|
||||
function expandBound(bound: Bound, margin: number) {
|
||||
const x = bound.x - margin;
|
||||
const y = bound.y - margin;
|
||||
const w = bound.w + margin * 2;
|
||||
const h = bound.h + margin * 2;
|
||||
return new Bound(x, y, w, h);
|
||||
}
|
||||
|
||||
function isOverlap(target: Bound, source: Bound) {
|
||||
const { x, y, w, h } = source;
|
||||
const left = target.x;
|
||||
const top = target.y;
|
||||
const right = target.x + target.w;
|
||||
const bottom = target.y + target.h;
|
||||
|
||||
return x < right && y < bottom && x + w > left && y + h > top;
|
||||
}
|
||||
|
||||
function isInside(target: Bound, source: Bound) {
|
||||
const { x, y, w, h } = source;
|
||||
const left = target.x;
|
||||
const top = target.y;
|
||||
const right = target.x + target.w;
|
||||
const bottom = target.y + target.h;
|
||||
|
||||
return x >= left && y >= top && x + w <= right && y + h <= bottom;
|
||||
}
|
||||
|
||||
function hideEdgelessElements(elements: GfxModel[], std: BlockStdScope) {
|
||||
elements.forEach(ele => {
|
||||
if (ele instanceof GfxPrimitiveElementModel) {
|
||||
(ele as any).lastOpacity = ele.opacity;
|
||||
ele.opacity = 0;
|
||||
} else {
|
||||
const block = std.view.getBlock(ele.id);
|
||||
if (!block) return;
|
||||
block.classList.add('copy-as-image-transparent');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showEdgelessElements(elements: GfxModel[], std: BlockStdScope) {
|
||||
elements.forEach(ele => {
|
||||
if (ele instanceof GfxPrimitiveElementModel) {
|
||||
ele.opacity = (ele as any).lastOpacity;
|
||||
delete (ele as any).lastOpacity;
|
||||
} else {
|
||||
const block = std.view.getBlock(ele.id);
|
||||
if (!block) return;
|
||||
block.classList.remove('copy-as-image-transparent');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function withDescendantElements(elements: GfxModel[]) {
|
||||
const set = new Set<GfxModel>();
|
||||
elements.forEach(element => {
|
||||
if (set.has(element)) return;
|
||||
set.add(element);
|
||||
if (isGfxGroupCompatibleModel(element)) {
|
||||
element.descendantElements.forEach(descendant => set.add(descendant));
|
||||
}
|
||||
});
|
||||
return [...set];
|
||||
}
|
||||
|
||||
const MARGIN = 20;
|
||||
|
||||
export function createCopyAsPngMenuItem(framework: FrameworkProvider) {
|
||||
return {
|
||||
icon: CopyAsImgaeIcon({ width: '20', height: '20' }),
|
||||
label: 'Copy as Image',
|
||||
type: 'copy-as-image',
|
||||
when: (ctx: MenuContext) => {
|
||||
if (ctx.isEmpty()) return false;
|
||||
const { editor } = framework.get(EditorService);
|
||||
const mode = editor.mode$.value;
|
||||
return mode === 'edgeless';
|
||||
},
|
||||
action: async (ctx: MenuContext) => {
|
||||
if (!apis) {
|
||||
notify.error({
|
||||
title: I18n.t('com.affine.copy.asImage.notAvailable.title'),
|
||||
message: I18n.t('com.affine.copy.asImage.notAvailable.message'),
|
||||
action: {
|
||||
label: I18n.t('com.affine.copy.asImage.notAvailable.action'),
|
||||
onClick: () => {
|
||||
window.open('https://affine.pro/download');
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const gfx = ctx.host.std.get(GfxControllerIdentifier);
|
||||
|
||||
let selected = gfx.selection.selectedElements;
|
||||
// select mindmap if root node selected
|
||||
const maybeMindmap = selected[0];
|
||||
const mindmapId = maybeMindmap.group?.id;
|
||||
if (
|
||||
selected.length === 1 &&
|
||||
mindmapId &&
|
||||
(isMindMapRoot(maybeMindmap) || isMindmapChild(maybeMindmap))
|
||||
) {
|
||||
gfx.selection.set({ elements: [mindmapId] });
|
||||
}
|
||||
|
||||
// select bound
|
||||
selected = gfx.selection.selectedElements;
|
||||
const elements = withDescendantElements(selected);
|
||||
const bounds = elements.map(element => Bound.deserialize(element.xywh));
|
||||
const bound = getCommonBound(bounds);
|
||||
if (!bound) return;
|
||||
const { zoom } = gfx.viewport;
|
||||
const exBound = expandBound(bound, MARGIN * zoom);
|
||||
|
||||
// fit to screen
|
||||
if (
|
||||
!isInside(gfx.viewport.viewportBounds, exBound) ||
|
||||
gfx.viewport.zoom < 1
|
||||
) {
|
||||
gfx.viewport.setViewportByBound(bound, [20, 20, 20, 20], false);
|
||||
if (gfx.viewport.zoom > 1) {
|
||||
gfx.viewport.setZoom(1);
|
||||
}
|
||||
}
|
||||
|
||||
// hide unselected overlap elements
|
||||
const overlapElements = gfx.gfxElements.filter(ele => {
|
||||
const eleBound = Bound.deserialize(ele.xywh);
|
||||
const exEleBound = expandBound(eleBound, MARGIN * zoom);
|
||||
const isSelected = elements.includes(ele);
|
||||
return !isSelected && isOverlap(exBound, exEleBound);
|
||||
});
|
||||
hideEdgelessElements(overlapElements, ctx.host.std);
|
||||
|
||||
// add css style
|
||||
const styleEle = document.createElement('style');
|
||||
styleEle.innerHTML = snapshotStyle;
|
||||
document.head.append(styleEle);
|
||||
|
||||
// capture image
|
||||
setTimeout(() => {
|
||||
if (!apis) return;
|
||||
try {
|
||||
const domRect = getSelectedRect();
|
||||
const { zoom } = gfx.viewport;
|
||||
const isFrameSelected =
|
||||
selected.length === 1 &&
|
||||
(selected[0] as GfxBlockElementModel).flavour === 'affine:frame';
|
||||
const margin = isFrameSelected ? -2 : MARGIN * zoom;
|
||||
|
||||
gfx.selection.clear();
|
||||
|
||||
apis.ui
|
||||
.captureArea({
|
||||
x: domRect.left - margin,
|
||||
y: domRect.top - margin,
|
||||
width: domRect.width + margin * 2,
|
||||
height: domRect.height + margin * 2,
|
||||
})
|
||||
.then(() => {
|
||||
notify.success({
|
||||
title: I18n.t('com.affine.copy.asImage.success'),
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
notify.error({
|
||||
title: I18n.t('com.affine.copy.asImage.failed'),
|
||||
message: String(e),
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
styleEle.remove();
|
||||
showEdgelessElements(overlapElements, ctx.host.std);
|
||||
});
|
||||
} catch (e) {
|
||||
styleEle.remove();
|
||||
showEdgelessElements(overlapElements, ctx.host.std);
|
||||
notify.error({
|
||||
title: I18n.t('com.affine.copy.asImage.failed'),
|
||||
message: String(e),
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
};
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const headerPadding = 8;
|
||||
export const header = style({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
padding: headerPadding,
|
||||
zIndex: 2, // should have higher z-index than the note mask
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const title = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
flex: 1,
|
||||
color: cssVarV2('text/primary'),
|
||||
fontFamily: 'Inter',
|
||||
fontWeight: 600,
|
||||
lineHeight: '30px',
|
||||
});
|
||||
|
||||
export const iconSize = 24;
|
||||
const buttonPadding = 4;
|
||||
export const button = style({
|
||||
padding: buttonPadding,
|
||||
pointerEvents: 'auto',
|
||||
color: cssVarV2('icon/transparentBlack'),
|
||||
});
|
||||
|
||||
export const headerHeight = 2 * headerPadding + iconSize + 2 * buttonPadding;
|
||||
-194
@@ -1,194 +0,0 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { useSharingUrl } from '@affine/core/components/hooks/affine/use-share-url';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { DocService } from '@affine/core/modules/doc';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { useInsidePeekView } from '@affine/core/modules/peek-view/view/modal-container';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { extractEmojiIcon } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx';
|
||||
import { type NoteBlockModel } from '@blocksuite/affine/blocks';
|
||||
import { Bound } from '@blocksuite/affine/global/utils';
|
||||
import {
|
||||
InformationIcon,
|
||||
LinkedPageIcon,
|
||||
LinkIcon,
|
||||
ToggleDownIcon,
|
||||
ToggleRightIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService, useServices } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import * as styles from './edgeless-note-header.css';
|
||||
|
||||
const EdgelessNoteToggleButton = ({ note }: { note: NoteBlockModel }) => {
|
||||
const t = useI18n();
|
||||
const [collapsed, setCollapsed] = useState(note.edgeless.collapse);
|
||||
const editor = useService(EditorService).editor;
|
||||
const editorContainer = useLiveData(editor.editorContainer$);
|
||||
const gfx = editorContainer?.std.get(GfxControllerIdentifier);
|
||||
const { doc } = useService(DocService);
|
||||
|
||||
const title = useLiveData(doc.title$);
|
||||
// only render emoji if it exists (mode or journal icon will not be rendered)
|
||||
const { emoji, rest: titleWithoutEmoji } = useMemo(
|
||||
() => extractEmojiIcon(title),
|
||||
[title]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return note.edgeless$.subscribe(({ collapse, collapsedHeight }) => {
|
||||
if (
|
||||
collapse &&
|
||||
collapsedHeight &&
|
||||
Math.abs(collapsedHeight - styles.headerHeight) < 1
|
||||
) {
|
||||
setCollapsed(true);
|
||||
} else {
|
||||
setCollapsed(false);
|
||||
}
|
||||
});
|
||||
}, [note.edgeless$]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!gfx) return;
|
||||
|
||||
const { selection } = gfx;
|
||||
|
||||
const dispose = selection.slots.updated.on(() => {
|
||||
if (selection.has(note.id) && selection.editing) {
|
||||
note.doc.transact(() => {
|
||||
note.edgeless.collapse = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return () => dispose.dispose();
|
||||
}, [gfx, note]);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
note.doc.transact(() => {
|
||||
if (collapsed) {
|
||||
note.edgeless.collapse = false;
|
||||
} else {
|
||||
const bound = Bound.deserialize(note.xywh);
|
||||
bound.h = styles.headerHeight * (note.edgeless.scale ?? 1);
|
||||
note.xywh = bound.serialize();
|
||||
note.edgeless.collapse = true;
|
||||
note.edgeless.collapsedHeight = styles.headerHeight;
|
||||
gfx?.selection.clear();
|
||||
}
|
||||
});
|
||||
}, [collapsed, gfx, note]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
className={styles.button}
|
||||
size={styles.iconSize}
|
||||
tooltip={t['com.affine.editor.edgeless-note-header.fold-page-block']()}
|
||||
data-testid="edgeless-note-toggle-button"
|
||||
onClick={toggle}
|
||||
>
|
||||
{collapsed ? <ToggleRightIcon /> : <ToggleDownIcon />}
|
||||
</IconButton>
|
||||
<div className={styles.title} data-testid="edgeless-note-title">
|
||||
{collapsed && (
|
||||
<>
|
||||
{emoji && <span>{emoji}</span>}
|
||||
<span>{titleWithoutEmoji}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ViewInPageButton = () => {
|
||||
const t = useI18n();
|
||||
const editor = useService(EditorService).editor;
|
||||
|
||||
const viewInPage = useCallback(() => {
|
||||
editor.setMode('page');
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
className={styles.button}
|
||||
size={styles.iconSize}
|
||||
tooltip={t['com.affine.editor.edgeless-note-header.view-in-page']()}
|
||||
data-testid="edgeless-note-view-in-page-button"
|
||||
onClick={viewInPage}
|
||||
>
|
||||
<LinkedPageIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
const InfoButton = ({ note }: { note: NoteBlockModel }) => {
|
||||
const t = useI18n();
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
|
||||
const onOpenInfoModal = useCallback(() => {
|
||||
track.doc.editor.pageBlockHeader.openDocInfo();
|
||||
workspaceDialogService.open('doc-info', { docId: note.doc.id });
|
||||
}, [note.doc.id, workspaceDialogService]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
className={styles.button}
|
||||
size={styles.iconSize}
|
||||
tooltip={t['com.affine.page-properties.page-info.view']()}
|
||||
data-testid="edgeless-note-info-button"
|
||||
onClick={onOpenInfoModal}
|
||||
>
|
||||
<InformationIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkButton = ({ note }: { note: NoteBlockModel }) => {
|
||||
const t = useI18n();
|
||||
const { workspaceService, editorService } = useServices({
|
||||
WorkspaceService,
|
||||
EditorService,
|
||||
});
|
||||
|
||||
const { onClickCopyLink } = useSharingUrl({
|
||||
workspaceId: workspaceService.workspace.id,
|
||||
pageId: editorService.editor.doc.id,
|
||||
});
|
||||
|
||||
const copyLink = useCallback(() => {
|
||||
onClickCopyLink('edgeless', [note.id]);
|
||||
}, [note.id, onClickCopyLink]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
className={styles.button}
|
||||
size={styles.iconSize}
|
||||
tooltip={t['com.affine.share-menu.copy']()}
|
||||
data-testid="edgeless-note-link-button"
|
||||
onClick={copyLink}
|
||||
>
|
||||
<LinkIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const EdgelessNoteHeader = ({ note }: { note: NoteBlockModel }) => {
|
||||
const insidePeekView = useInsidePeekView();
|
||||
|
||||
if (!note.isPageBlock()) return null;
|
||||
|
||||
return (
|
||||
<div className={styles.header} data-testid="edgeless-page-block-header">
|
||||
<EdgelessNoteToggleButton note={note} />
|
||||
<ViewInPageButton />
|
||||
{!insidePeekView && <InfoButton note={note} />}
|
||||
<LinkButton note={note} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { type KeyboardToolbarConfig } from '@blocksuite/affine/blocks';
|
||||
|
||||
export function createKeyboardToolbarConfig(): Partial<KeyboardToolbarConfig> {
|
||||
return {
|
||||
// TODO(@L-Sun): check android following the PR
|
||||
// https://github.com/toeverything/blocksuite/pull/8645
|
||||
useScreenHeight: BUILD_CONFIG.isIOS,
|
||||
};
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { AtMenuConfigService } from '@affine/core/modules/at-menu-config/services';
|
||||
import type { LinkedWidgetConfig } from '@blocksuite/affine/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
export function createLinkedWidgetConfig(
|
||||
framework: FrameworkProvider
|
||||
): Partial<LinkedWidgetConfig> {
|
||||
return framework.get(AtMenuConfigService).getConfig();
|
||||
}
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
import { notify } from '@affine/component';
|
||||
import {
|
||||
generateUrl,
|
||||
type UseSharingUrl,
|
||||
} from '@affine/core/components/hooks/affine/use-share-url';
|
||||
import { WorkspaceServerService } from '@affine/core/modules/cloud';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type {
|
||||
GfxBlockElementModel,
|
||||
GfxPrimitiveElementModel,
|
||||
} from '@blocksuite/affine/block-std/gfx';
|
||||
import type { MenuContext, MenuItemGroup } from '@blocksuite/affine/blocks';
|
||||
import { LinkIcon } from '@blocksuite/icons/lit';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { createCopyAsPngMenuItem } from './copy-as-image';
|
||||
|
||||
export function createToolbarMoreMenuConfig(framework: FrameworkProvider) {
|
||||
return {
|
||||
configure: <T extends MenuContext>(groups: MenuItemGroup<T>[]) => {
|
||||
const clipboardGroup = groups.find(group => group.type === 'clipboard');
|
||||
|
||||
if (clipboardGroup) {
|
||||
let copyIndex = clipboardGroup.items.findIndex(
|
||||
item => item.type === 'copy'
|
||||
);
|
||||
if (copyIndex === -1) {
|
||||
copyIndex = clipboardGroup.items.findIndex(
|
||||
item => item.type === 'duplicate'
|
||||
);
|
||||
if (copyIndex !== -1) {
|
||||
copyIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// after `copy` or before `duplicate`
|
||||
clipboardGroup.items.splice(
|
||||
copyIndex + 1,
|
||||
0,
|
||||
createCopyLinkToBlockMenuItem(framework)
|
||||
);
|
||||
|
||||
clipboardGroup.items.splice(
|
||||
copyIndex + 1,
|
||||
0,
|
||||
createCopyAsPngMenuItem(framework)
|
||||
);
|
||||
}
|
||||
|
||||
return groups;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createCopyLinkToBlockMenuItem(
|
||||
framework: FrameworkProvider,
|
||||
item = {
|
||||
icon: LinkIcon({ width: '20', height: '20' }),
|
||||
label: 'Copy link to block',
|
||||
type: 'copy-link-to-block',
|
||||
when: (ctx: MenuContext) => {
|
||||
if (ctx.isEmpty()) return false;
|
||||
|
||||
const { editor } = framework.get(EditorService);
|
||||
const mode = editor.mode$.value;
|
||||
|
||||
if (mode === 'edgeless') {
|
||||
// linking blocks in notes is currently not supported in edgeless mode.
|
||||
if (ctx.selectedBlockModels.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// linking single block/element in edgeless mode.
|
||||
if (ctx.isMultiple()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}
|
||||
) {
|
||||
return {
|
||||
...item,
|
||||
action: async (ctx: MenuContext) => {
|
||||
const workspaceServerService = framework.get(WorkspaceServerService);
|
||||
|
||||
const { editor } = framework.get(EditorService);
|
||||
const mode = editor.mode$.value;
|
||||
const pageId = editor.doc.id;
|
||||
const workspaceId = editor.doc.workspace.id;
|
||||
const options: UseSharingUrl = { workspaceId, pageId, mode };
|
||||
let type = '';
|
||||
|
||||
if (mode === 'page') {
|
||||
// maybe multiple blocks
|
||||
const blockIds = ctx.selectedBlockModels.map(model => model.id);
|
||||
options.blockIds = blockIds;
|
||||
type = ctx.selectedBlockModels[0].flavour;
|
||||
} else if (mode === 'edgeless' && ctx.firstElement) {
|
||||
// single block/element
|
||||
const id = ctx.firstElement.id;
|
||||
if (ctx.isElement()) {
|
||||
options.elementIds = [id];
|
||||
type = (ctx.firstElement as GfxPrimitiveElementModel).type;
|
||||
} else {
|
||||
options.blockIds = [id];
|
||||
type = (ctx.firstElement as GfxBlockElementModel).flavour;
|
||||
}
|
||||
}
|
||||
|
||||
const str = generateUrl({
|
||||
...options,
|
||||
baseUrl: workspaceServerService.server?.baseUrl ?? location.origin,
|
||||
});
|
||||
if (!str) {
|
||||
ctx.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await copyLinkToBlockStdScopeClipboard(
|
||||
str,
|
||||
ctx.std.clipboard
|
||||
);
|
||||
|
||||
if (success) {
|
||||
notify.success({ title: I18n['Copied link to clipboard']() });
|
||||
}
|
||||
|
||||
track.doc.editor.toolbar.copyBlockToLink({ type });
|
||||
|
||||
ctx.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { createAIEdgelessRootBlockSpec } from '@affine/core/blocksuite/presets';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { builtInTemplates as builtInEdgelessTemplates } from '@affine/templates/edgeless';
|
||||
import { builtInTemplates as builtInStickersTemplates } from '@affine/templates/stickers';
|
||||
import type { SpecBuilder, TemplateManager } from '@blocksuite/affine/blocks';
|
||||
import {
|
||||
EdgelessRootBlockSpec,
|
||||
EdgelessTemplatePanel,
|
||||
SpecProvider,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { enableAffineExtension, enableAIExtension } from './custom/root-block';
|
||||
|
||||
export function createEdgelessModeSpecs(
|
||||
framework: FrameworkProvider
|
||||
): SpecBuilder {
|
||||
const featureFlagService = framework.get(FeatureFlagService);
|
||||
const enableAI = featureFlagService.flags.enable_ai.value;
|
||||
const edgelessSpec = SpecProvider._.getSpec('edgeless');
|
||||
enableAffineExtension(framework, edgelessSpec);
|
||||
if (enableAI) {
|
||||
enableAIExtension(edgelessSpec);
|
||||
edgelessSpec.replace(
|
||||
EdgelessRootBlockSpec,
|
||||
createAIEdgelessRootBlockSpec(framework)
|
||||
);
|
||||
}
|
||||
|
||||
return edgelessSpec;
|
||||
}
|
||||
|
||||
export function effects() {
|
||||
EdgelessTemplatePanel.templates.extend(
|
||||
builtInStickersTemplates as TemplateManager
|
||||
);
|
||||
EdgelessTemplatePanel.templates.extend(
|
||||
builtInEdgelessTemplates as TemplateManager
|
||||
);
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
import {
|
||||
AffineCanvasTextFonts,
|
||||
FontConfigExtension,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
|
||||
export function getFontConfigExtension() {
|
||||
return FontConfigExtension(
|
||||
AffineCanvasTextFonts.map(font => ({
|
||||
...font,
|
||||
url: environment.publicPath + 'fonts/' + font.url.split('/').pop(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { createAIPageRootBlockSpec } from '@affine/core/blocksuite/presets';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import {
|
||||
PageRootBlockSpec,
|
||||
type SpecBuilder,
|
||||
SpecProvider,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { type FrameworkProvider } from '@toeverything/infra';
|
||||
|
||||
import { enableAffineExtension, enableAIExtension } from './custom/root-block';
|
||||
|
||||
export function createPageModeSpecs(framework: FrameworkProvider): SpecBuilder {
|
||||
const featureFlagService = framework.get(FeatureFlagService);
|
||||
const enableAI = featureFlagService.flags.enable_ai.value;
|
||||
const provider = SpecProvider._;
|
||||
const pageSpec = provider.getSpec('page');
|
||||
enableAffineExtension(framework, pageSpec);
|
||||
if (enableAI) {
|
||||
enableAIExtension(pageSpec);
|
||||
pageSpec.replace(PageRootBlockSpec, createAIPageRootBlockSpec(framework));
|
||||
}
|
||||
return pageSpec;
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import { AIChatBlockSpec } from '@affine/core/blocksuite/blocks';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||
import { AppThemeService } from '@affine/core/modules/theme';
|
||||
import {
|
||||
type BlockStdScope,
|
||||
LifeCycleWatcher,
|
||||
StdIdentifier,
|
||||
} from '@blocksuite/affine/block-std';
|
||||
import {
|
||||
ColorScheme,
|
||||
createSignalFromObservable,
|
||||
type Signal,
|
||||
type SpecBuilder,
|
||||
SpecProvider,
|
||||
type ThemeExtension,
|
||||
ThemeExtensionIdentifier,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { Container } from '@blocksuite/affine/global/di';
|
||||
import type { ExtensionType } from '@blocksuite/affine/store';
|
||||
import type { FrameworkProvider } from '@toeverything/infra';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
import { buildDocDisplayMetaExtension } from './custom/root-block';
|
||||
import { patchPeekViewService } from './custom/spec-patchers';
|
||||
import { getFontConfigExtension } from './font-extension';
|
||||
|
||||
const CustomSpecs: ExtensionType[] = [
|
||||
AIChatBlockSpec,
|
||||
getFontConfigExtension(),
|
||||
].flat();
|
||||
|
||||
function patchPreviewSpec(
|
||||
id: 'preview:edgeless' | 'preview:page',
|
||||
specs: ExtensionType[]
|
||||
) {
|
||||
const specProvider = SpecProvider._;
|
||||
specProvider.extendSpec(id, specs);
|
||||
}
|
||||
|
||||
export function effects() {
|
||||
// Patch edgeless preview spec for blocksuite surface-ref and embed-synced-doc
|
||||
patchPreviewSpec('preview:edgeless', CustomSpecs);
|
||||
}
|
||||
|
||||
export function getPagePreviewThemeExtension(framework: FrameworkProvider) {
|
||||
class AffinePagePreviewThemeExtension
|
||||
extends LifeCycleWatcher
|
||||
implements ThemeExtension
|
||||
{
|
||||
static override readonly key = 'affine-page-preview-theme';
|
||||
|
||||
readonly theme: Signal<ColorScheme>;
|
||||
|
||||
readonly disposables: (() => void)[] = [];
|
||||
|
||||
static override setup(di: Container) {
|
||||
super.setup(di);
|
||||
di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [
|
||||
StdIdentifier,
|
||||
]);
|
||||
}
|
||||
|
||||
constructor(std: BlockStdScope) {
|
||||
super(std);
|
||||
const theme$: Observable<ColorScheme> = framework
|
||||
.get(AppThemeService)
|
||||
.appTheme.theme$.map(theme => {
|
||||
return theme === ColorScheme.Dark
|
||||
? ColorScheme.Dark
|
||||
: ColorScheme.Light;
|
||||
});
|
||||
const { signal, cleanup } = createSignalFromObservable<ColorScheme>(
|
||||
theme$,
|
||||
ColorScheme.Light
|
||||
);
|
||||
this.theme = signal;
|
||||
this.disposables.push(cleanup);
|
||||
}
|
||||
|
||||
getAppTheme() {
|
||||
return this.theme;
|
||||
}
|
||||
|
||||
getEdgelessTheme() {
|
||||
return this.theme;
|
||||
}
|
||||
|
||||
override unmounted() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(dispose => dispose());
|
||||
}
|
||||
}
|
||||
|
||||
return AffinePagePreviewThemeExtension;
|
||||
}
|
||||
|
||||
export function createPageModePreviewSpecs(
|
||||
framework: FrameworkProvider
|
||||
): SpecBuilder {
|
||||
const specProvider = SpecProvider._;
|
||||
const pagePreviewSpec = specProvider.getSpec('preview:page');
|
||||
// Enable theme extension, doc display meta extension and peek view service
|
||||
const peekViewService = framework.get(PeekViewService);
|
||||
pagePreviewSpec.extend([
|
||||
getPagePreviewThemeExtension(framework),
|
||||
buildDocDisplayMetaExtension(framework),
|
||||
patchPeekViewService(peekViewService),
|
||||
]);
|
||||
return pagePreviewSpec;
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
import { container } from './bi-directional-link-panel.css';
|
||||
|
||||
export const root = style([
|
||||
container,
|
||||
{
|
||||
paddingBottom: 6,
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
alignItems: 'center',
|
||||
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
lineHeight: '20px',
|
||||
color: cssVarV2.text.primary,
|
||||
},
|
||||
]);
|
||||
|
||||
export const badges = style({
|
||||
display: 'flex',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const badge = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
padding: '2px 8px',
|
||||
borderRadius: 40,
|
||||
backgroundColor: cssVarV2.layer.background.secondary,
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
position: 'relative',
|
||||
|
||||
':before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0,0,0,.04)',
|
||||
borderRadius: 'inherit',
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s ease',
|
||||
},
|
||||
|
||||
selectors: {
|
||||
'&:hover:before': {
|
||||
opacity: 1,
|
||||
},
|
||||
'&[data-active="true"]:before': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const badgeIcon = style({
|
||||
fontSize: 20,
|
||||
lineHeight: 0,
|
||||
color: cssVarV2.icon.primary,
|
||||
});
|
||||
|
||||
export const aiIcon = style({
|
||||
color: cssVarV2.icon.activated,
|
||||
});
|
||||
|
||||
export const badgeText = style({
|
||||
fontSize: 15,
|
||||
lineHeight: '24px',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
@@ -1,180 +0,0 @@
|
||||
import {
|
||||
handleInlineAskAIAction,
|
||||
pageAIGroups,
|
||||
} from '@affine/core/blocksuite/presets';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { TemplateDocService } from '@affine/core/modules/template-doc';
|
||||
import { TemplateListMenu } from '@affine/core/modules/template-doc/view/template-list-menu';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { PageRootBlockComponent } from '@blocksuite/affine/blocks';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
AiIcon,
|
||||
EdgelessIcon,
|
||||
TemplateColoredIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useAsyncCallback } from '../../hooks/affine-async-hooks';
|
||||
import * as styles from './starter-bar.css';
|
||||
|
||||
const Badge = forwardRef<
|
||||
HTMLLIElement,
|
||||
HTMLAttributes<HTMLLIElement> & {
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
active?: boolean;
|
||||
}
|
||||
>(function Badge({ icon, text, className, active, ...attrs }, ref) {
|
||||
return (
|
||||
<li
|
||||
data-active={active}
|
||||
className={clsx(styles.badge, className)}
|
||||
ref={ref}
|
||||
{...attrs}
|
||||
>
|
||||
<span className={styles.badgeText}>{text}</span>
|
||||
<span className={styles.badgeIcon}>{icon}</span>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
const StarterBarNotEmpty = ({ doc }: { doc: Store }) => {
|
||||
const t = useI18n();
|
||||
|
||||
const templateDocService = useService(TemplateDocService);
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const docsService = useService(DocsService);
|
||||
const editorService = useService(EditorService);
|
||||
|
||||
const [templateMenuOpen, setTemplateMenuOpen] = useState(false);
|
||||
|
||||
const isTemplate = useLiveData(
|
||||
useMemo(
|
||||
() => templateDocService.list.isTemplate$(doc.id),
|
||||
[doc.id, templateDocService.list]
|
||||
)
|
||||
);
|
||||
const enableAI = useLiveData(featureFlagService.flags.enable_ai.$);
|
||||
|
||||
const handleSelectTemplate = useAsyncCallback(
|
||||
async (templateId: string) => {
|
||||
await docsService.duplicateFromTemplate(templateId, doc.id);
|
||||
track.doc.editor.starterBar.quickStart({ with: 'template' });
|
||||
},
|
||||
[doc.id, docsService]
|
||||
);
|
||||
|
||||
const startWithEdgeless = useCallback(() => {
|
||||
const record = docsService.list.doc$(doc.id).value;
|
||||
record?.setPrimaryMode('edgeless');
|
||||
editorService.editor.setMode('edgeless');
|
||||
}, [doc.id, docsService.list, editorService.editor]);
|
||||
|
||||
const onTemplateMenuOpenChange = useCallback((open: boolean) => {
|
||||
if (open) track.doc.editor.starterBar.openTemplateListMenu();
|
||||
setTemplateMenuOpen(open);
|
||||
}, []);
|
||||
|
||||
const startWithAI = useCallback(() => {
|
||||
const std = editorService.editor.editorContainer$.value?.std;
|
||||
if (!std) return;
|
||||
|
||||
const rootBlockId = std.host.doc.root?.id;
|
||||
if (!rootBlockId) return;
|
||||
|
||||
const rootComponent = std.view.getBlock(rootBlockId);
|
||||
if (!(rootComponent instanceof PageRootBlockComponent)) return;
|
||||
|
||||
const { id, created } = rootComponent.focusFirstParagraph();
|
||||
if (created) {
|
||||
std.view.viewUpdated.once(v => {
|
||||
if (v.id === id) handleInlineAskAIAction(std.host, pageAIGroups);
|
||||
});
|
||||
} else {
|
||||
handleInlineAskAIAction(std.host, pageAIGroups);
|
||||
}
|
||||
}, [editorService.editor]);
|
||||
|
||||
const showTemplate = !isTemplate;
|
||||
|
||||
if (!enableAI && !showTemplate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{t['com.affine.page-starter-bar.start']()}
|
||||
<ul className={styles.badges}>
|
||||
{enableAI ? (
|
||||
<Badge
|
||||
data-testid="start-with-ai-badge"
|
||||
icon={<AiIcon className={styles.aiIcon} />}
|
||||
text={t['com.affine.page-starter-bar.ai']()}
|
||||
onClick={startWithAI}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{showTemplate ? (
|
||||
<TemplateListMenu
|
||||
onSelect={handleSelectTemplate}
|
||||
rootOptions={{
|
||||
open: templateMenuOpen,
|
||||
onOpenChange: onTemplateMenuOpenChange,
|
||||
}}
|
||||
>
|
||||
<Badge
|
||||
data-testid="template-docs-badge"
|
||||
icon={<TemplateColoredIcon />}
|
||||
text={t['com.affine.page-starter-bar.template']()}
|
||||
active={templateMenuOpen}
|
||||
/>
|
||||
</TemplateListMenu>
|
||||
) : null}
|
||||
|
||||
<Badge
|
||||
icon={<EdgelessIcon />}
|
||||
text={t['com.affine.page-starter-bar.edgeless']()}
|
||||
onClick={startWithEdgeless}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const StarterBar = ({ doc }: { doc: Store }) => {
|
||||
const [isEmpty, setIsEmpty] = useState(doc.isEmpty);
|
||||
const templateDocService = useService(TemplateDocService);
|
||||
|
||||
const isTemplate = useLiveData(
|
||||
useMemo(
|
||||
() => templateDocService.list.isTemplate$(doc.id),
|
||||
[doc.id, templateDocService.list]
|
||||
)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = doc.slots.blockUpdated.on(() => {
|
||||
setIsEmpty(doc.isEmpty);
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
}, [doc]);
|
||||
|
||||
if (!isEmpty || isTemplate) return null;
|
||||
|
||||
return <StarterBarNotEmpty doc={doc} />;
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style, type StyleRule } from '@vanilla-extract/css';
|
||||
|
||||
export const docEditorRoot = style({
|
||||
overflowX: 'clip',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
});
|
||||
|
||||
export const affineDocViewport = style({
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingBottom: '100px',
|
||||
});
|
||||
export const affineEdgelessDocViewport = style({
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const docContainer = style({
|
||||
display: 'block',
|
||||
selectors: ['generating', 'finished', 'error'].reduce<
|
||||
NonNullable<StyleRule['selectors']>
|
||||
>((rules, state) => {
|
||||
rules[`&:has(affine-ai-panel-widget[data-state='${state}'])`] = {
|
||||
paddingBottom: '980px',
|
||||
};
|
||||
return rules;
|
||||
}, {}),
|
||||
});
|
||||
|
||||
export const docEditorGap = style({
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
margin: '0 auto',
|
||||
paddingTop: 50,
|
||||
paddingBottom: 50,
|
||||
cursor: 'text',
|
||||
flexGrow: 1,
|
||||
});
|
||||
|
||||
const titleTagBasic = style({
|
||||
fontSize: cssVar('fontH4'),
|
||||
fontWeight: 600,
|
||||
padding: '0 4px',
|
||||
borderRadius: '4px',
|
||||
marginLeft: '4px',
|
||||
lineHeight: '0px',
|
||||
});
|
||||
export const titleDayTag = style([
|
||||
titleTagBasic,
|
||||
{
|
||||
color: cssVar('textSecondaryColor'),
|
||||
},
|
||||
]);
|
||||
export const titleTodayTag = style([
|
||||
titleTagBasic,
|
||||
{
|
||||
color: cssVar('brandColor'),
|
||||
},
|
||||
]);
|
||||
export const pageReferenceIcon = style({
|
||||
verticalAlign: 'middle',
|
||||
fontSize: '1.1em',
|
||||
transform: 'translate(2px, -1px)',
|
||||
});
|
||||
|
||||
export const docPropertiesTableContainer = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
});
|
||||
|
||||
export const docPropertiesTable = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
width: '100%',
|
||||
maxWidth: cssVar('editorWidth'),
|
||||
padding: `0 ${cssVar('editorSidePadding', '24px')}`,
|
||||
'@container': {
|
||||
[`viewport (width <= 640px)`]: {
|
||||
padding: '0 16px',
|
||||
},
|
||||
},
|
||||
});
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
import { FavoriteTag } from '@affine/core/components/page-list';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { toast } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export interface FavoriteButtonProps {
|
||||
pageId: string;
|
||||
}
|
||||
|
||||
export const useFavorite = (pageId: string) => {
|
||||
const t = useI18n();
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
|
||||
const favorite = useLiveData(favAdapter.isFavorite$(pageId, 'doc'));
|
||||
|
||||
const toggleFavorite = useCallback(() => {
|
||||
favAdapter.toggle(pageId, 'doc');
|
||||
toast(
|
||||
favorite
|
||||
? t['com.affine.toastMessage.removedFavorites']()
|
||||
: t['com.affine.toastMessage.addedFavorites']()
|
||||
);
|
||||
}, [favorite, pageId, t, favAdapter]);
|
||||
|
||||
return { favorite, toggleFavorite };
|
||||
};
|
||||
|
||||
export const FavoriteButton = ({ pageId }: FavoriteButtonProps) => {
|
||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||
|
||||
const handleFavorite = useCallback(() => {
|
||||
track.$.header.actions.toggleFavorite();
|
||||
toggleFavorite();
|
||||
}, [toggleFavorite]);
|
||||
|
||||
return (
|
||||
<FavoriteTag
|
||||
data-testid="pin-button"
|
||||
active={!!favorite}
|
||||
onClick={handleFavorite}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { InformationIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const InfoButton = ({ docId }: { docId: string }) => {
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
const t = useI18n();
|
||||
|
||||
const onOpenInfoModal = useCallback(() => {
|
||||
track.$.header.actions.openDocInfo();
|
||||
workspaceDialogService.open('doc-info', { docId });
|
||||
}, [docId, workspaceDialogService]);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
size="20"
|
||||
tooltip={t['com.affine.page-properties.page-info.view']()}
|
||||
data-testid="header-info-button"
|
||||
onClick={onOpenInfoModal}
|
||||
>
|
||||
<InformationIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import type { WeekDatePickerHandle } from '@affine/component';
|
||||
import { WeekDatePicker } from '@affine/component';
|
||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export interface JournalWeekDatePickerProps {
|
||||
page: Store;
|
||||
}
|
||||
|
||||
const weekStyle = { maxWidth: 800, width: '100%' };
|
||||
export const JournalWeekDatePicker = ({ page }: JournalWeekDatePickerProps) => {
|
||||
const handleRef = useRef<WeekDatePickerHandle>(null);
|
||||
const journalService = useService(JournalService);
|
||||
const journalDateStr = useLiveData(journalService.journalDate$(page.id));
|
||||
const journalDate = journalDateStr ? dayjs(journalDateStr) : null;
|
||||
const { openJournal } = useJournalRouteHelper();
|
||||
const [date, setDate] = useState(
|
||||
(journalDate ?? dayjs()).format('YYYY-MM-DD')
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!journalDate) return;
|
||||
setDate(journalDate.format('YYYY-MM-DD'));
|
||||
handleRef.current?.setCursor?.(journalDate);
|
||||
}, [journalDate]);
|
||||
|
||||
return (
|
||||
<WeekDatePicker
|
||||
data-testid="journal-week-picker"
|
||||
handleRef={handleRef}
|
||||
style={weekStyle}
|
||||
value={date}
|
||||
onChange={openJournal}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const JournalTodayButton = () => {
|
||||
const t = useI18n();
|
||||
const journalHelper = useJournalRouteHelper();
|
||||
|
||||
const onToday = useCallback(() => {
|
||||
journalHelper.openToday({
|
||||
replaceHistory: true,
|
||||
});
|
||||
}, [journalHelper]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
size="default"
|
||||
onClick={onToday}
|
||||
style={{ height: 32, padding: '0px 8px' }}
|
||||
>
|
||||
{t['com.affine.today']()}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import { OverlayModal } from '@affine/component';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import TopSvg from './top-svg';
|
||||
|
||||
export const HistoryTipsModal = ({
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
setOpen(false);
|
||||
confirmEnableCloud(currentWorkspace);
|
||||
}, [confirmEnableCloud, currentWorkspace, setOpen]);
|
||||
|
||||
return (
|
||||
<OverlayModal
|
||||
open={open}
|
||||
topImage={<TopSvg />}
|
||||
title={t['com.affine.history-vision.tips-modal.title']()}
|
||||
onOpenChange={setOpen}
|
||||
description={t['com.affine.history-vision.tips-modal.description']()}
|
||||
cancelText={t['com.affine.history-vision.tips-modal.cancel']()}
|
||||
confirmButtonOptions={{
|
||||
variant: 'primary',
|
||||
}}
|
||||
onConfirm={handleConfirm}
|
||||
confirmText={t['com.affine.history-vision.tips-modal.confirm']()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-299
File diff suppressed because one or more lines are too long
@@ -1,456 +0,0 @@
|
||||
import { notify, toast, useConfirmModal } from '@affine/component';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
MenuSub,
|
||||
} from '@affine/component/ui/menu';
|
||||
import { PageHistoryModal } from '@affine/core/components/affine/page-history-modal';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { ShareMenuContent } from '@affine/core/modules/share-menu';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type { Store } from '@blocksuite/affine/store';
|
||||
import {
|
||||
DuplicateIcon,
|
||||
EdgelessIcon,
|
||||
EditIcon,
|
||||
FrameIcon,
|
||||
HistoryIcon,
|
||||
ImportIcon,
|
||||
InformationIcon,
|
||||
LocalWorkspaceIcon,
|
||||
OpenInNewIcon,
|
||||
PageIcon,
|
||||
ShareIcon,
|
||||
SplitViewIcon,
|
||||
TocIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { HeaderDropDownButton } from '../../../pure/header-drop-down-button';
|
||||
import { useFavorite } from '../favorite';
|
||||
import { HistoryTipsModal } from './history-tips-modal';
|
||||
|
||||
type PageMenuProps = {
|
||||
rename?: () => void;
|
||||
page: Store;
|
||||
isJournal?: boolean;
|
||||
containerWidth: number;
|
||||
};
|
||||
|
||||
export const PageHeaderMenuButton = ({
|
||||
rename,
|
||||
page,
|
||||
isJournal,
|
||||
containerWidth,
|
||||
}: PageMenuProps) => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const editorService = useService(EditorService);
|
||||
const isInTrash = useLiveData(
|
||||
editorService.editor.doc.meta$.map(meta => meta.trash)
|
||||
);
|
||||
|
||||
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||
const [openHistoryTipsModal, setOpenHistoryTipsModal] = useState(false);
|
||||
|
||||
const handleMenuOpenChange = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
track.$.header.docOptions.open();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
track.$.header.history.open();
|
||||
if (workspace.flavour === 'affine-cloud') {
|
||||
return setHistoryModalOpen(true);
|
||||
}
|
||||
return setOpenHistoryTipsModal(true);
|
||||
}, [setOpenHistoryTipsModal, workspace.flavour]);
|
||||
|
||||
if (isInTrash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
items={
|
||||
<PageHeaderMenuItem
|
||||
page={page}
|
||||
containerWidth={containerWidth}
|
||||
rename={rename}
|
||||
isJournal={isJournal}
|
||||
openHistoryModal={openHistoryModal}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
align: 'center',
|
||||
}}
|
||||
rootOptions={{
|
||||
onOpenChange: handleMenuOpenChange,
|
||||
}}
|
||||
>
|
||||
<HeaderDropDownButton />
|
||||
</Menu>
|
||||
{workspace.flavour !== 'local' ? (
|
||||
<PageHistoryModal
|
||||
docCollection={workspace.docCollection}
|
||||
open={historyModalOpen}
|
||||
pageId={page.id}
|
||||
onOpenChange={setHistoryModalOpen}
|
||||
/>
|
||||
) : null}
|
||||
<HistoryTipsModal
|
||||
open={openHistoryTipsModal}
|
||||
setOpen={setOpenHistoryTipsModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// fixme: refactor this file
|
||||
const PageHeaderMenuItem = ({
|
||||
rename,
|
||||
page,
|
||||
isJournal,
|
||||
containerWidth,
|
||||
openHistoryModal,
|
||||
}: PageMenuProps & {
|
||||
openHistoryModal: () => void;
|
||||
}) => {
|
||||
const pageId = page?.id;
|
||||
const t = useI18n();
|
||||
const { hideShare } = useDetailPageHeaderResponsive(containerWidth);
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const guardService = useService(GuardService);
|
||||
const editorService = useService(EditorService);
|
||||
const currentMode = useLiveData(editorService.editor.mode$);
|
||||
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
|
||||
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const openInAppService = useServiceOptional(OpenInAppService);
|
||||
|
||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||
|
||||
const { duplicate } = useBlockSuiteMetaHelper();
|
||||
|
||||
const view = useService(ViewService).view;
|
||||
|
||||
const openSidePanel = useCallback(
|
||||
(id: string) => {
|
||||
workbench.openSidebar();
|
||||
view.activeSidebarTab(id);
|
||||
},
|
||||
[workbench, view]
|
||||
);
|
||||
|
||||
const openAllFrames = useCallback(() => {
|
||||
openSidePanel('frame');
|
||||
}, [openSidePanel]);
|
||||
|
||||
const openOutlinePanel = useCallback(() => {
|
||||
openSidePanel('outline');
|
||||
}, [openSidePanel]);
|
||||
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
const openInfoModal = useCallback(() => {
|
||||
track.$.header.pageInfo.open();
|
||||
workspaceDialogService.open('doc-info', { docId: pageId });
|
||||
}, [workspaceDialogService, pageId]);
|
||||
|
||||
const handleOpenInNewTab = useCallback(() => {
|
||||
workbench.openDoc(pageId, {
|
||||
at: 'new-tab',
|
||||
});
|
||||
}, [pageId, workbench]);
|
||||
|
||||
const handleOpenInSplitView = useCallback(() => {
|
||||
workbench.openDoc(pageId, {
|
||||
at: 'tail',
|
||||
});
|
||||
}, [pageId, workbench]);
|
||||
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
|
||||
const handleOpenTrashModal = useCallback(() => {
|
||||
track.$.header.docOptions.deleteDoc();
|
||||
openConfirmModal({
|
||||
title: t['com.affine.moveToTrash.confirmModal.title'](),
|
||||
description: t['com.affine.moveToTrash.confirmModal.description']({
|
||||
title: editorService.editor.doc.title$.value || t['Untitled'](),
|
||||
}),
|
||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||
confirmText: t.Delete(),
|
||||
confirmButtonOptions: {
|
||||
variant: 'error',
|
||||
},
|
||||
onConfirm: async () => {
|
||||
const canTrash = await guardService.can('Doc_Trash', pageId);
|
||||
if (!canTrash) {
|
||||
toast(t['com.affine.no-permission']());
|
||||
return;
|
||||
}
|
||||
editorService.editor.doc.moveToTrash();
|
||||
},
|
||||
});
|
||||
}, [editorService.editor.doc, guardService, openConfirmModal, pageId, t]);
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
rename?.();
|
||||
track.$.header.docOptions.renameDoc();
|
||||
}, [rename]);
|
||||
|
||||
const handleSwitchMode = useCallback(() => {
|
||||
const mode = primaryMode === 'page' ? 'edgeless' : 'page';
|
||||
editorService.editor.setMode(mode);
|
||||
editorService.editor.doc.setPrimaryMode(mode);
|
||||
track.$.header.docOptions.switchPageMode({
|
||||
mode,
|
||||
});
|
||||
notify.success({
|
||||
title:
|
||||
primaryMode === 'page'
|
||||
? t['com.affine.toastMessage.defaultMode.edgeless.title']()
|
||||
: t['com.affine.toastMessage.defaultMode.page.title'](),
|
||||
message:
|
||||
primaryMode === 'page'
|
||||
? t['com.affine.toastMessage.defaultMode.edgeless.message']()
|
||||
: t['com.affine.toastMessage.defaultMode.page.message'](),
|
||||
});
|
||||
}, [primaryMode, editorService, t]);
|
||||
|
||||
const exportHandler = useExportPage();
|
||||
|
||||
const handleDuplicate = useCallback(() => {
|
||||
duplicate(pageId);
|
||||
track.$.header.docOptions.createDoc({
|
||||
control: 'duplicate',
|
||||
});
|
||||
}, [duplicate, pageId]);
|
||||
|
||||
const handleOpenDocs = useCallback(
|
||||
(result: {
|
||||
docIds: string[];
|
||||
entryId?: string;
|
||||
isWorkspaceFile?: boolean;
|
||||
}) => {
|
||||
const { docIds, entryId, isWorkspaceFile } = result;
|
||||
// If the imported file is a workspace file, open the entry page.
|
||||
if (isWorkspaceFile && entryId) {
|
||||
workbench.openDoc(entryId);
|
||||
} else if (!docIds.length) {
|
||||
return;
|
||||
}
|
||||
// Open all the docs when there are multiple docs imported.
|
||||
if (docIds.length > 1) {
|
||||
workbench.openAll();
|
||||
} else {
|
||||
// Otherwise, open the only doc.
|
||||
workbench.openDoc(docIds[0]);
|
||||
}
|
||||
},
|
||||
[workbench]
|
||||
);
|
||||
|
||||
const handleOpenImportModal = useCallback(() => {
|
||||
track.$.header.importModal.open();
|
||||
workspaceDialogService.open('import', undefined, payload => {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
handleOpenDocs(payload);
|
||||
});
|
||||
}, [workspaceDialogService, handleOpenDocs]);
|
||||
|
||||
const handleShareMenuOpenChange = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
track.$.sharePanel.$.open();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleToggleFavorite = useCallback(() => {
|
||||
track.$.header.docOptions.toggleFavorite();
|
||||
toggleFavorite();
|
||||
}, [toggleFavorite]);
|
||||
|
||||
const showResponsiveMenu = hideShare;
|
||||
const ResponsiveMenuItems = (
|
||||
<>
|
||||
{hideShare ? (
|
||||
<MenuSub
|
||||
subContentOptions={{
|
||||
sideOffset: 12,
|
||||
alignOffset: -8,
|
||||
}}
|
||||
items={
|
||||
<div style={{ padding: 4 }}>
|
||||
<ShareMenuContent
|
||||
workspaceMetadata={workspace.meta}
|
||||
currentPage={page}
|
||||
onEnableAffineCloud={() =>
|
||||
confirmEnableCloud(workspace, {
|
||||
openPageId: page.id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
triggerOptions={{
|
||||
prefixIcon: <ShareIcon />,
|
||||
}}
|
||||
subOptions={{
|
||||
onOpenChange: handleShareMenuOpenChange,
|
||||
}}
|
||||
>
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</MenuSub>
|
||||
) : null}
|
||||
<MenuSeparator />
|
||||
</>
|
||||
);
|
||||
|
||||
const onOpenInDesktop = useCallback(() => {
|
||||
openInAppService?.showOpenInAppPage();
|
||||
}, [openInAppService]);
|
||||
|
||||
const canEdit = useLiveData(guardService.can$('Doc_Update', pageId));
|
||||
const canMoveToTrash = useLiveData(guardService.can$('Doc_Trash', pageId));
|
||||
|
||||
return (
|
||||
<>
|
||||
{showResponsiveMenu ? ResponsiveMenuItems : null}
|
||||
{!isJournal && (
|
||||
<MenuItem
|
||||
prefixIcon={<EditIcon />}
|
||||
data-testid="editor-option-menu-rename"
|
||||
onSelect={handleRename}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
{t['Rename']()}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
prefixIcon={primaryMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onSelect={handleSwitchMode}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
{primaryMode === 'page'
|
||||
? t['com.affine.editorDefaultMode.edgeless']()
|
||||
: t['com.affine.editorDefaultMode.page']()}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
data-testid="editor-option-menu-favorite"
|
||||
onSelect={handleToggleFavorite}
|
||||
prefixIcon={<IsFavoriteIcon favorite={favorite} />}
|
||||
>
|
||||
{favorite
|
||||
? t['com.affine.favoritePageOperation.remove']()
|
||||
: t['com.affine.favoritePageOperation.add']()}
|
||||
</MenuItem>
|
||||
<MenuSeparator />
|
||||
<MenuItem
|
||||
prefixIcon={<OpenInNewIcon />}
|
||||
data-testid="editor-option-menu-open-in-new-tab"
|
||||
onSelect={handleOpenInNewTab}
|
||||
>
|
||||
{t['com.affine.workbench.tab.page-menu-open']()}
|
||||
</MenuItem>
|
||||
{BUILD_CONFIG.isElectron && (
|
||||
<MenuItem
|
||||
prefixIcon={<SplitViewIcon />}
|
||||
data-testid="editor-option-menu-open-in-split-new"
|
||||
onSelect={handleOpenInSplitView}
|
||||
>
|
||||
{t['com.affine.workbench.split-view.page-menu-open']()}
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuSeparator />
|
||||
<MenuItem
|
||||
prefixIcon={<InformationIcon />}
|
||||
data-testid="editor-option-menu-info"
|
||||
onSelect={openInfoModal}
|
||||
>
|
||||
{t['com.affine.page-properties.page-info.view']()}
|
||||
</MenuItem>
|
||||
{currentMode === 'page' ? (
|
||||
<MenuItem
|
||||
prefixIcon={<TocIcon />}
|
||||
data-testid="editor-option-toc"
|
||||
onSelect={openOutlinePanel}
|
||||
>
|
||||
{t['com.affine.header.option.view-toc']()}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
prefixIcon={<FrameIcon />}
|
||||
data-testid="editor-option-frame"
|
||||
onSelect={openAllFrames}
|
||||
>
|
||||
{t['com.affine.header.option.view-frame']()}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
prefixIcon={<HistoryIcon />}
|
||||
data-testid="editor-option-menu-history"
|
||||
onSelect={openHistoryModal}
|
||||
>
|
||||
{t['com.affine.history.view-history-version']()}
|
||||
</MenuItem>
|
||||
<MenuSeparator />
|
||||
{!isJournal && (
|
||||
<MenuItem
|
||||
prefixIcon={<DuplicateIcon />}
|
||||
data-testid="editor-option-menu-duplicate"
|
||||
onSelect={handleDuplicate}
|
||||
>
|
||||
{t['com.affine.header.option.duplicate']()}
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem
|
||||
prefixIcon={<ImportIcon />}
|
||||
data-testid="editor-option-menu-import"
|
||||
onSelect={handleOpenImportModal}
|
||||
>
|
||||
{t['Import']()}
|
||||
</MenuItem>
|
||||
<Export exportHandler={exportHandler} pageMode={currentMode} />
|
||||
<MenuSeparator />
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
onSelect={handleOpenTrashModal}
|
||||
disabled={!canMoveToTrash}
|
||||
/>
|
||||
{BUILD_CONFIG.isWeb && workspace.flavour === 'affine-cloud' ? (
|
||||
<MenuItem
|
||||
prefixIcon={<LocalWorkspaceIcon />}
|
||||
data-testid="editor-option-menu-link"
|
||||
onSelect={onOpenInDesktop}
|
||||
>
|
||||
{t['com.affine.header.option.open-in-desktop']()}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { PresentationIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
|
||||
export const DetailPageHeaderPresentButton = () => {
|
||||
const editorService = useService(EditorService);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
style={{ flexShrink: 0 }}
|
||||
size="24"
|
||||
onClick={() => editorService.editor.togglePresentation()}
|
||||
>
|
||||
<PresentationIcon />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import type { InlineEditProps } from '@affine/component';
|
||||
import { InlineEdit } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
|
||||
export interface BlockSuiteHeaderTitleProps {
|
||||
/** if set, title cannot be edited */
|
||||
inputHandleRef?: InlineEditProps['handleRef'];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const inputAttrs = {
|
||||
'data-testid': 'title-content',
|
||||
} as HTMLAttributes<HTMLInputElement>;
|
||||
export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||
const { inputHandleRef } = props;
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const isSharedMode = workspaceService.workspace.openOptions.isSharedMode;
|
||||
const docsService = useService(DocsService);
|
||||
const guardService = useService(GuardService);
|
||||
const docService = useService(DocService);
|
||||
const docTitle = useLiveData(docService.doc.record.title$);
|
||||
|
||||
const onChange = useAsyncCallback(
|
||||
async (v: string) => {
|
||||
await docsService.changeDocTitle(docService.doc.id, v);
|
||||
track.$.header.actions.renameDoc();
|
||||
},
|
||||
[docService.doc.id, docsService]
|
||||
);
|
||||
|
||||
const canEdit = useLiveData(
|
||||
guardService.can$('Doc_Update', docService.doc.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<InlineEdit
|
||||
className={clsx(styles.title, props.className)}
|
||||
value={docTitle}
|
||||
onChange={onChange}
|
||||
editable={!isSharedMode && canEdit}
|
||||
exitible={true}
|
||||
placeholder="Untitled"
|
||||
data-testid="title-edit-button"
|
||||
handleRef={inputHandleRef}
|
||||
inputAttrs={inputAttrs}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const title = style({
|
||||
fontWeight: 500,
|
||||
color: cssVar('textPrimaryColor'),
|
||||
selectors: {
|
||||
'&[data-editing="true"]': {
|
||||
['WebkitAppRegion' as string]: 'no-drag',
|
||||
},
|
||||
},
|
||||
});
|
||||
-919
@@ -1,919 +0,0 @@
|
||||
{
|
||||
"v": "5.12.1",
|
||||
"fr": 120,
|
||||
"ip": 0,
|
||||
"op": 76,
|
||||
"w": 240,
|
||||
"h": 240,
|
||||
"nm": "Edgeless",
|
||||
"ddd": 0,
|
||||
"assets": [],
|
||||
"layers": [
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 1,
|
||||
"ty": 4,
|
||||
"nm": "“图层 2”轮廓",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 11
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 10
|
||||
},
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [97.5, 138, 0],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [10.35, 13.5, 0],
|
||||
"ix": 1,
|
||||
"l": 2
|
||||
},
|
||||
"s": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": {
|
||||
"x": [0.772, 0.772, 0.667],
|
||||
"y": [1, 1, 1.219]
|
||||
},
|
||||
"o": {
|
||||
"x": [0.462, 0.462, 0.333],
|
||||
"y": [0, 0, 0]
|
||||
},
|
||||
"t": 5,
|
||||
"s": [1100, 1100, 100]
|
||||
},
|
||||
{
|
||||
"i": {
|
||||
"x": [0.562, 0.562, 0.667],
|
||||
"y": [1, 1, 1]
|
||||
},
|
||||
"o": {
|
||||
"x": [0.455, 0.455, 0.333],
|
||||
"y": [0, 0, -0.238]
|
||||
},
|
||||
"t": 26.562,
|
||||
"s": [1070, 1070, 100]
|
||||
},
|
||||
{
|
||||
"t": 50,
|
||||
"s": [1100, 1100, 100]
|
||||
}
|
||||
],
|
||||
"ix": 6,
|
||||
"l": 2
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, 0],
|
||||
[1.369, 1.18],
|
||||
[1.875, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, -1.885],
|
||||
[-2.094, -1.571],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[3.665, 3.299],
|
||||
[1.57, -1.728],
|
||||
[-3.665, -3.299]
|
||||
],
|
||||
"c": false
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "路径 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
|
||||
"ix": 3
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 4
|
||||
},
|
||||
"w": {
|
||||
"a": 0,
|
||||
"k": 1.5,
|
||||
"ix": 5
|
||||
},
|
||||
"lc": 1,
|
||||
"lj": 1,
|
||||
"ml": 4,
|
||||
"bm": 0,
|
||||
"nm": "描边 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [13.626, 10.441],
|
||||
"ix": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [0, 0],
|
||||
"ix": 1
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [100, 100],
|
||||
"ix": 3
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 6
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 7
|
||||
},
|
||||
"sk": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 4
|
||||
},
|
||||
"sa": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 5
|
||||
},
|
||||
"nm": "变换"
|
||||
}
|
||||
],
|
||||
"nm": "组 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 240,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 2,
|
||||
"ty": 4,
|
||||
"nm": "“图层 3”轮廓",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 11
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 10
|
||||
},
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [69, 119, 0],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [6.9, 11.9, 0],
|
||||
"ix": 1,
|
||||
"l": 2
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [1000, 1000, 100],
|
||||
"ix": 6,
|
||||
"l": 2
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[6.818, 9.76],
|
||||
[6.818, 13.949]
|
||||
],
|
||||
"c": false
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "路径 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
|
||||
"ix": 3
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 4
|
||||
},
|
||||
"w": {
|
||||
"a": 0,
|
||||
"k": 1.5,
|
||||
"ix": 5
|
||||
},
|
||||
"lc": 1,
|
||||
"lj": 1,
|
||||
"ml": 4,
|
||||
"bm": 0,
|
||||
"nm": "描边 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [0, 0],
|
||||
"ix": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [0, 0],
|
||||
"ix": 1
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [100, 100],
|
||||
"ix": 3
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 6
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 7
|
||||
},
|
||||
"sk": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 4
|
||||
},
|
||||
"sa": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 5
|
||||
},
|
||||
"nm": "变换"
|
||||
}
|
||||
],
|
||||
"nm": "组 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 240,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 3,
|
||||
"ty": 4,
|
||||
"nm": "“图层 4”轮廓",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 11
|
||||
},
|
||||
"r": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": {
|
||||
"x": [0.363],
|
||||
"y": [1]
|
||||
},
|
||||
"o": {
|
||||
"x": [0.675],
|
||||
"y": [-0.111]
|
||||
},
|
||||
"t": 5,
|
||||
"s": [0]
|
||||
},
|
||||
{
|
||||
"t": 50,
|
||||
"s": [90]
|
||||
}
|
||||
],
|
||||
"ix": 10
|
||||
},
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": {
|
||||
"x": 0.363,
|
||||
"y": 1
|
||||
},
|
||||
"o": {
|
||||
"x": 0.675,
|
||||
"y": 0
|
||||
},
|
||||
"t": 5,
|
||||
"s": [173.5, 171, 0],
|
||||
"to": [0, -1.333, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": {
|
||||
"x": 0.667,
|
||||
"y": 1
|
||||
},
|
||||
"o": {
|
||||
"x": 0.647,
|
||||
"y": 0
|
||||
},
|
||||
"t": 26.562,
|
||||
"s": [173.5, 163, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [0, -1.333, 0]
|
||||
},
|
||||
{
|
||||
"t": 50,
|
||||
"s": [173.5, 171, 0]
|
||||
}
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [17.35, 16.9, 0],
|
||||
"ix": 1,
|
||||
"l": 2
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [1000, 1000, 100],
|
||||
"ix": 6,
|
||||
"l": 2
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"o": [
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0],
|
||||
[0, 0]
|
||||
],
|
||||
"v": [
|
||||
[-2.357, -2.357],
|
||||
[2.357, -2.357],
|
||||
[2.357, 2.357],
|
||||
[-2.357, 2.357]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "路径 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
|
||||
"ix": 3
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 4
|
||||
},
|
||||
"w": {
|
||||
"a": 0,
|
||||
"k": 1.5,
|
||||
"ix": 5
|
||||
},
|
||||
"lc": 1,
|
||||
"lj": 2,
|
||||
"bm": 0,
|
||||
"nm": "描边 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [17.344, 16.829],
|
||||
"ix": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [0, 0],
|
||||
"ix": 1
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [100, 100],
|
||||
"ix": 3
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 6
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 7
|
||||
},
|
||||
"sk": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 4
|
||||
},
|
||||
"sa": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 5
|
||||
},
|
||||
"nm": "变换"
|
||||
}
|
||||
],
|
||||
"nm": "组 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 240,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 4,
|
||||
"ty": 4,
|
||||
"nm": "“图层 5”轮廓",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 11
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 10
|
||||
},
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": {
|
||||
"x": 0.363,
|
||||
"y": 1
|
||||
},
|
||||
"o": {
|
||||
"x": 0.675,
|
||||
"y": 0
|
||||
},
|
||||
"t": 5,
|
||||
"s": [68.5, 170, 0],
|
||||
"to": [0, -1.333, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": {
|
||||
"x": 0.667,
|
||||
"y": 1
|
||||
},
|
||||
"o": {
|
||||
"x": 0.647,
|
||||
"y": 0
|
||||
},
|
||||
"t": 26.562,
|
||||
"s": [68.5, 162, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [0, -1.333, 0]
|
||||
},
|
||||
{
|
||||
"t": 50,
|
||||
"s": [68.5, 170, 0]
|
||||
}
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [6.85, 17.05, 0],
|
||||
"ix": 1,
|
||||
"l": 2
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [1000, 1000, 100],
|
||||
"ix": 6,
|
||||
"l": 2
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[-1.446, 0],
|
||||
[0, -1.446],
|
||||
[1.446, 0],
|
||||
[0, 1.446]
|
||||
],
|
||||
"o": [
|
||||
[1.446, 0],
|
||||
[0, 1.446],
|
||||
[-1.446, 0],
|
||||
[0, -1.446]
|
||||
],
|
||||
"v": [
|
||||
[0, -2.618],
|
||||
[2.618, 0],
|
||||
[0, 2.618],
|
||||
[-2.618, 0]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "路径 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
|
||||
"ix": 3
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 4
|
||||
},
|
||||
"w": {
|
||||
"a": 0,
|
||||
"k": 1.5,
|
||||
"ix": 5
|
||||
},
|
||||
"lc": 1,
|
||||
"lj": 1,
|
||||
"ml": 4,
|
||||
"bm": 0,
|
||||
"nm": "描边 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [6.818, 17.091],
|
||||
"ix": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [0, 0],
|
||||
"ix": 1
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [100, 100],
|
||||
"ix": 3
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 6
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 7
|
||||
},
|
||||
"sk": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 4
|
||||
},
|
||||
"sa": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 5
|
||||
},
|
||||
"nm": "变换"
|
||||
}
|
||||
],
|
||||
"nm": "组 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 240,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
},
|
||||
{
|
||||
"ddd": 0,
|
||||
"ind": 5,
|
||||
"ty": 4,
|
||||
"nm": "“图层 6”轮廓",
|
||||
"sr": 1,
|
||||
"ks": {
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 11
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 10
|
||||
},
|
||||
"p": {
|
||||
"a": 1,
|
||||
"k": [
|
||||
{
|
||||
"i": {
|
||||
"x": 0.363,
|
||||
"y": 1
|
||||
},
|
||||
"o": {
|
||||
"x": 0.675,
|
||||
"y": 0
|
||||
},
|
||||
"t": 5,
|
||||
"s": [68, 65, 0],
|
||||
"to": [0, 1.333, 0],
|
||||
"ti": [0, 0, 0]
|
||||
},
|
||||
{
|
||||
"i": {
|
||||
"x": 0.667,
|
||||
"y": 1
|
||||
},
|
||||
"o": {
|
||||
"x": 0.647,
|
||||
"y": 0
|
||||
},
|
||||
"t": 26.562,
|
||||
"s": [68, 73, 0],
|
||||
"to": [0, 0, 0],
|
||||
"ti": [0, 1.333, 0]
|
||||
},
|
||||
{
|
||||
"t": 50,
|
||||
"s": [68, 65, 0]
|
||||
}
|
||||
],
|
||||
"ix": 2,
|
||||
"l": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [6.8, 6.6, 0],
|
||||
"ix": 1,
|
||||
"l": 2
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [1000, 1000, 100],
|
||||
"ix": 6,
|
||||
"l": 2
|
||||
}
|
||||
},
|
||||
"ao": 0,
|
||||
"shapes": [
|
||||
{
|
||||
"ty": "gr",
|
||||
"it": [
|
||||
{
|
||||
"ind": 0,
|
||||
"ty": "sh",
|
||||
"ix": 1,
|
||||
"ks": {
|
||||
"a": 0,
|
||||
"k": {
|
||||
"i": [
|
||||
[-1.446, 0],
|
||||
[0, -1.446],
|
||||
[1.446, 0],
|
||||
[0, 1.446]
|
||||
],
|
||||
"o": [
|
||||
[1.446, 0],
|
||||
[0, 1.446],
|
||||
[-1.446, 0],
|
||||
[0, -1.446]
|
||||
],
|
||||
"v": [
|
||||
[0, -2.618],
|
||||
[2.618, 0],
|
||||
[0, 2.618],
|
||||
[-2.618, 0]
|
||||
],
|
||||
"c": true
|
||||
},
|
||||
"ix": 2
|
||||
},
|
||||
"nm": "路径 1",
|
||||
"mn": "ADBE Vector Shape - Group",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "st",
|
||||
"c": {
|
||||
"a": 0,
|
||||
"k": [0.466666696586, 0.458823559331, 0.490196108351, 1],
|
||||
"ix": 3
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 4
|
||||
},
|
||||
"w": {
|
||||
"a": 0,
|
||||
"k": 1.5,
|
||||
"ix": 5
|
||||
},
|
||||
"lc": 1,
|
||||
"lj": 1,
|
||||
"ml": 4,
|
||||
"bm": 0,
|
||||
"nm": "描边 1",
|
||||
"mn": "ADBE Vector Graphic - Stroke",
|
||||
"hd": false
|
||||
},
|
||||
{
|
||||
"ty": "tr",
|
||||
"p": {
|
||||
"a": 0,
|
||||
"k": [6.818, 6.618],
|
||||
"ix": 2
|
||||
},
|
||||
"a": {
|
||||
"a": 0,
|
||||
"k": [0, 0],
|
||||
"ix": 1
|
||||
},
|
||||
"s": {
|
||||
"a": 0,
|
||||
"k": [100, 100],
|
||||
"ix": 3
|
||||
},
|
||||
"r": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 6
|
||||
},
|
||||
"o": {
|
||||
"a": 0,
|
||||
"k": 100,
|
||||
"ix": 7
|
||||
},
|
||||
"sk": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 4
|
||||
},
|
||||
"sa": {
|
||||
"a": 0,
|
||||
"k": 0,
|
||||
"ix": 5
|
||||
},
|
||||
"nm": "变换"
|
||||
}
|
||||
],
|
||||
"nm": "组 1",
|
||||
"np": 2,
|
||||
"cix": 2,
|
||||
"bm": 0,
|
||||
"ix": 1,
|
||||
"mn": "ADBE Vector Group",
|
||||
"hd": false
|
||||
}
|
||||
],
|
||||
"ip": 0,
|
||||
"op": 240,
|
||||
"st": 0,
|
||||
"ct": 1,
|
||||
"bm": 0
|
||||
}
|
||||
],
|
||||
"markers": []
|
||||
}
|
||||
-2068
File diff suppressed because it is too large
Load Diff
@@ -1,136 +0,0 @@
|
||||
import { RadioGroup, type RadioItem } from '@affine/component';
|
||||
import { registerAffineCommand } from '@affine/core/commands';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { ViewService, WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { switchItem } from './style.css';
|
||||
import { EdgelessSwitchItem, PageSwitchItem } from './switch-items';
|
||||
|
||||
export interface EditorModeSwitchProps {
|
||||
pageId: string;
|
||||
isPublic?: boolean;
|
||||
publicMode?: DocMode;
|
||||
}
|
||||
|
||||
const EdgelessRadioItem: RadioItem = {
|
||||
value: 'edgeless',
|
||||
label: <EdgelessSwitchItem />,
|
||||
testId: 'switch-edgeless-mode-button',
|
||||
className: switchItem,
|
||||
};
|
||||
const PageRadioItem: RadioItem = {
|
||||
value: 'page',
|
||||
label: <PageSwitchItem />,
|
||||
testId: 'switch-page-mode-button',
|
||||
className: switchItem,
|
||||
};
|
||||
|
||||
export const EditorModeSwitch = () => {
|
||||
const t = useI18n();
|
||||
const editor = useService(EditorService).editor;
|
||||
const trash = useLiveData(editor.doc.trash$);
|
||||
const isSharedMode = editor.isSharedMode;
|
||||
const currentMode = useLiveData(editor.mode$);
|
||||
const view = useServiceOptional(ViewService)?.view;
|
||||
const workbench = useServiceOptional(WorkbenchService)?.workbench;
|
||||
const activeView = useLiveData(workbench?.activeView$);
|
||||
const isActiveView = activeView?.id && activeView?.id === view?.id;
|
||||
|
||||
const togglePage = useCallback(() => {
|
||||
if (currentMode === 'page' || isSharedMode || trash) return;
|
||||
editor.setMode('page');
|
||||
editor.setSelector(undefined);
|
||||
track.$.header.actions.switchPageMode({ mode: 'page' });
|
||||
}, [currentMode, editor, isSharedMode, trash]);
|
||||
|
||||
const toggleEdgeless = useCallback(() => {
|
||||
if (currentMode === 'edgeless' || isSharedMode || trash) return;
|
||||
editor.setMode('edgeless');
|
||||
editor.setSelector(undefined);
|
||||
track.$.header.actions.switchPageMode({ mode: 'edgeless' });
|
||||
}, [currentMode, editor, isSharedMode, trash]);
|
||||
|
||||
const onModeChange = useCallback(
|
||||
(mode: DocMode) => {
|
||||
mode === 'page' ? togglePage() : toggleEdgeless();
|
||||
},
|
||||
[toggleEdgeless, togglePage]
|
||||
);
|
||||
|
||||
const shouldHide = useCallback(
|
||||
(mode: DocMode) => (trash || isSharedMode) && currentMode !== mode,
|
||||
[currentMode, isSharedMode, trash]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (trash || isSharedMode || currentMode === undefined || !isActiveView)
|
||||
return;
|
||||
return registerAffineCommand({
|
||||
id: 'affine:doc-mode-switch',
|
||||
category: 'editor:page',
|
||||
label:
|
||||
currentMode === 'page'
|
||||
? t['com.affine.cmdk.switch-to-edgeless']()
|
||||
: t['com.affine.cmdk.switch-to-page'](),
|
||||
icon: currentMode === 'page' ? <EdgelessIcon /> : <PageIcon />,
|
||||
keyBinding: {
|
||||
binding: 'Alt+KeyS',
|
||||
capture: true,
|
||||
},
|
||||
run: () => onModeChange(currentMode === 'edgeless' ? 'page' : 'edgeless'),
|
||||
});
|
||||
}, [currentMode, isActiveView, isSharedMode, onModeChange, t, trash]);
|
||||
|
||||
return (
|
||||
<PureEditorModeSwitch
|
||||
mode={currentMode}
|
||||
setMode={onModeChange}
|
||||
hidePage={shouldHide('page')}
|
||||
hideEdgeless={shouldHide('edgeless')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export interface PureEditorModeSwitchProps {
|
||||
mode?: DocMode;
|
||||
setMode?: (mode: DocMode) => void;
|
||||
hidePage?: boolean;
|
||||
hideEdgeless?: boolean;
|
||||
}
|
||||
|
||||
export const PureEditorModeSwitch = ({
|
||||
mode,
|
||||
setMode,
|
||||
hidePage,
|
||||
hideEdgeless,
|
||||
}: PureEditorModeSwitchProps) => {
|
||||
const items = useMemo(
|
||||
() => [
|
||||
...(hidePage ? [] : [PageRadioItem]),
|
||||
...(hideEdgeless ? [] : [EdgelessRadioItem]),
|
||||
],
|
||||
[hideEdgeless, hidePage]
|
||||
);
|
||||
return (
|
||||
<RadioGroup
|
||||
iconMode
|
||||
itemHeight={24}
|
||||
borderRadius={8}
|
||||
padding={4}
|
||||
gap={8}
|
||||
value={mode}
|
||||
items={items}
|
||||
onChange={setMode}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const switchItem = style({
|
||||
width: 24,
|
||||
height: 24,
|
||||
});
|
||||
// a workaround to override lottie svg stroke with default radio button color schemes
|
||||
globalStyle(`${switchItem} svg path`, {
|
||||
stroke: 'currentColor',
|
||||
});
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
import { Tooltip } from '@affine/component';
|
||||
import {
|
||||
type CustomLottieProps,
|
||||
InternalLottie,
|
||||
} from '@affine/component/internal-lottie';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type React from 'react';
|
||||
import { cloneElement, useState } from 'react';
|
||||
|
||||
import edgelessHover from './animation-data/edgeless-hover.json';
|
||||
import pageHover from './animation-data/page-hover.json';
|
||||
|
||||
type HoverAnimateControllerProps = {
|
||||
active?: boolean;
|
||||
hide?: boolean;
|
||||
trash?: boolean;
|
||||
children: React.ReactElement<CustomLottieProps>;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
const HoverAnimateController = ({
|
||||
children,
|
||||
...props
|
||||
}: HoverAnimateControllerProps) => {
|
||||
const [startAnimate, setStartAnimate] = useState(false);
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setStartAnimate(true)}
|
||||
onMouseLeave={() => setStartAnimate(false)}
|
||||
{...props}
|
||||
>
|
||||
{cloneElement(children, {
|
||||
isStopped: !startAnimate,
|
||||
speed: 1,
|
||||
width: 20,
|
||||
height: 20,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const pageLottieOptions = {
|
||||
loop: false,
|
||||
autoplay: false,
|
||||
animationData: pageHover,
|
||||
rendererSettings: {
|
||||
preserveAspectRatio: 'xMidYMid slice',
|
||||
},
|
||||
};
|
||||
|
||||
const edgelessLottieOptions = {
|
||||
loop: false,
|
||||
autoplay: false,
|
||||
animationData: edgelessHover,
|
||||
rendererSettings: {
|
||||
preserveAspectRatio: 'xMidYMid slice',
|
||||
},
|
||||
};
|
||||
|
||||
export const PageSwitchItem = (
|
||||
props: Omit<HoverAnimateControllerProps, 'children'>
|
||||
) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<Tooltip
|
||||
content={t['com.affine.header.mode-switch.page']()}
|
||||
shortcut={['$alt', 'S']}
|
||||
side="bottom"
|
||||
>
|
||||
<HoverAnimateController {...props}>
|
||||
<InternalLottie options={pageLottieOptions} />
|
||||
</HoverAnimateController>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const EdgelessSwitchItem = (
|
||||
props: Omit<HoverAnimateControllerProps, 'children'>
|
||||
) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<Tooltip
|
||||
content={t['com.affine.header.mode-switch.edgeless']()}
|
||||
shortcut={['$alt', 'S']}
|
||||
side="bottom"
|
||||
>
|
||||
<HoverAnimateController {...props}>
|
||||
<InternalLottie options={edgelessLottieOptions} />
|
||||
</HoverAnimateController>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const pageListEmptyStyle = style({
|
||||
height: 'calc(100% - 52px)',
|
||||
});
|
||||
export const emptyDescButton = style({
|
||||
cursor: 'pointer',
|
||||
color: cssVar('textSecondaryColor'),
|
||||
background: cssVar('backgroundCodeBlock'),
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
borderRadius: '4px',
|
||||
padding: '0 6px',
|
||||
boxSizing: 'border-box',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
export const emptyDescKbd = style([
|
||||
emptyDescButton,
|
||||
{
|
||||
cursor: 'text',
|
||||
},
|
||||
]);
|
||||
@@ -1,136 +0,0 @@
|
||||
import { toast } from '@affine/component';
|
||||
import type { DocProps } from '@affine/core/blocksuite/initialization';
|
||||
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { type DocMode } from '@blocksuite/affine/blocks';
|
||||
import type { Workspace } from '@blocksuite/affine/store';
|
||||
import { useServices } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const usePageHelper = (docCollection: Workspace) => {
|
||||
const {
|
||||
docsService,
|
||||
workbenchService,
|
||||
editorSettingService,
|
||||
appSidebarService,
|
||||
} = useServices({
|
||||
DocsService,
|
||||
WorkbenchService,
|
||||
EditorSettingService,
|
||||
AppSidebarService,
|
||||
});
|
||||
const workbench = workbenchService.workbench;
|
||||
const docRecordList = docsService.list;
|
||||
const appSidebar = appSidebarService.sidebar;
|
||||
|
||||
const createPageAndOpen = useCallback(
|
||||
(
|
||||
mode?: DocMode,
|
||||
options: {
|
||||
at?: 'new-tab' | 'tail' | 'active';
|
||||
show?: boolean;
|
||||
} = {
|
||||
at: 'active',
|
||||
show: true,
|
||||
}
|
||||
) => {
|
||||
appSidebar.setHovering(false);
|
||||
const docProps: DocProps = {
|
||||
note: editorSettingService.editorSetting.get('affine:note'),
|
||||
};
|
||||
const page = docsService.createDoc({ docProps });
|
||||
|
||||
if (mode) {
|
||||
docRecordList.doc$(page.id).value?.setPrimaryMode(mode);
|
||||
}
|
||||
|
||||
if (options.show !== false) {
|
||||
workbench.openDoc(page.id, {
|
||||
at: options.at,
|
||||
show: options.show,
|
||||
});
|
||||
}
|
||||
return page;
|
||||
},
|
||||
[
|
||||
appSidebar,
|
||||
docRecordList,
|
||||
docsService,
|
||||
editorSettingService.editorSetting,
|
||||
workbench,
|
||||
]
|
||||
);
|
||||
|
||||
const createEdgelessAndOpen = useCallback(
|
||||
(
|
||||
options: {
|
||||
at?: 'new-tab' | 'tail' | 'active';
|
||||
show?: boolean;
|
||||
} = {
|
||||
at: 'active',
|
||||
show: true,
|
||||
}
|
||||
) => {
|
||||
return createPageAndOpen('edgeless', options);
|
||||
},
|
||||
[createPageAndOpen]
|
||||
);
|
||||
|
||||
const importFileAndOpen = useMemo(
|
||||
() => async () => {
|
||||
const { showImportModal } = await import('@blocksuite/affine/blocks');
|
||||
const { promise, resolve, reject } =
|
||||
Promise.withResolvers<
|
||||
Parameters<
|
||||
NonNullable<Parameters<typeof showImportModal>[0]['onSuccess']>
|
||||
>[1]
|
||||
>();
|
||||
const onSuccess = (
|
||||
pageIds: string[],
|
||||
options: { isWorkspaceFile: boolean; importedCount: number }
|
||||
) => {
|
||||
resolve(options);
|
||||
toast(
|
||||
`Successfully imported ${options.importedCount} Page${
|
||||
options.importedCount > 1 ? 's' : ''
|
||||
}.`
|
||||
);
|
||||
if (options.isWorkspaceFile) {
|
||||
workbench.openAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
const pageId = pageIds[0];
|
||||
workbench.openDoc(pageId);
|
||||
};
|
||||
showImportModal({
|
||||
collection: docCollection,
|
||||
onSuccess,
|
||||
onFail: message => {
|
||||
reject(new Error(message));
|
||||
},
|
||||
});
|
||||
return await promise;
|
||||
},
|
||||
[docCollection, workbench]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
createPage: (
|
||||
mode?: DocMode,
|
||||
options?: {
|
||||
at?: 'new-tab' | 'tail' | 'active';
|
||||
show?: boolean;
|
||||
}
|
||||
) => createPageAndOpen(mode, options),
|
||||
createEdgeless: createEdgelessAndOpen,
|
||||
importFile: importFileAndOpen,
|
||||
};
|
||||
}, [createEdgelessAndOpen, createPageAndOpen, importFileAndOpen]);
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
import type { EditorHost } from '@blocksuite/affine/block-std';
|
||||
import { OutlineViewer } from '@blocksuite/affine/blocks';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import * as styles from './outline-viewer.css';
|
||||
|
||||
export const EditorOutlineViewer = ({
|
||||
editor,
|
||||
show,
|
||||
openOutlinePanel,
|
||||
}: {
|
||||
editor: EditorHost | null;
|
||||
show: boolean;
|
||||
openOutlinePanel?: () => void;
|
||||
}) => {
|
||||
const outlineViewerRef = useRef<OutlineViewer | null>(null);
|
||||
|
||||
const onRefChange = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container) {
|
||||
if (outlineViewerRef.current === null) {
|
||||
console.error('outline viewer should be initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
container.append(outlineViewerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!editor || !show) return;
|
||||
|
||||
if (!outlineViewerRef.current) {
|
||||
outlineViewerRef.current = new OutlineViewer();
|
||||
}
|
||||
if (outlineViewerRef.current.editor !== editor) {
|
||||
outlineViewerRef.current.editor = editor;
|
||||
}
|
||||
if (
|
||||
outlineViewerRef.current.toggleOutlinePanel !== openOutlinePanel &&
|
||||
openOutlinePanel
|
||||
) {
|
||||
outlineViewerRef.current.toggleOutlinePanel = openOutlinePanel;
|
||||
}
|
||||
|
||||
return <div className={styles.root} ref={onRefChange} />;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
const top = 256 - 52; // 52 is the height of the header
|
||||
const bottom = 76;
|
||||
|
||||
export const root = style({
|
||||
position: 'absolute',
|
||||
top,
|
||||
right: 22,
|
||||
maxHeight: `calc(100% - ${top}px - ${bottom}px)`,
|
||||
display: 'flex',
|
||||
'@container': {
|
||||
'(width <= 640px)': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -26,7 +26,7 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import type { AffineEditorContainer } from '../../blocksuite/block-suite-editor/blocksuite-editor-container';
|
||||
import type { AffineEditorContainer } from '../../../blocksuite/block-suite-editor/blocksuite-editor-container';
|
||||
import { useAsyncCallback } from '../affine-async-hooks';
|
||||
|
||||
type ExportType = 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SetStateAction } from 'jotai';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
|
||||
import type { AffineEditorContainer } from '../blocksuite/block-suite-editor';
|
||||
import type { AffineEditorContainer } from '../../blocksuite/block-suite-editor';
|
||||
|
||||
const activeEditorContainerAtom = atom<AffineEditorContainer | null>(null);
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import { useStore } from 'jotai';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import type { AffineEditorContainer } from '../../blocksuite/block-suite-editor';
|
||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||
import {
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
@@ -25,10 +27,8 @@ import {
|
||||
registerAffineSettingsCommands,
|
||||
registerAffineUpdatesCommands,
|
||||
} from '../../commands';
|
||||
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
|
||||
import { EditorSettingService } from '../../modules/editor-setting';
|
||||
import { CMDKQuickSearchService } from '../../modules/quicksearch/services/cmdk';
|
||||
import type { AffineEditorContainer } from '../blocksuite/block-suite-editor';
|
||||
import { useActiveBlocksuiteEditor } from './use-block-suite-editor';
|
||||
import { useNavigateHelper } from './use-navigate-helper';
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import type { AffineEditorContainer } from '../blocksuite/block-suite-editor';
|
||||
import { BlockSuiteEditor } from '../blocksuite/block-suite-editor';
|
||||
import { DocService } from '../modules/doc';
|
||||
import { EditorService } from '../modules/editor';
|
||||
import { EditorSettingService } from '../modules/editor-setting';
|
||||
import type { AffineEditorContainer } from './blocksuite/block-suite-editor';
|
||||
import { BlockSuiteEditor } from './blocksuite/block-suite-editor';
|
||||
import * as styles from './page-detail-editor.css';
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -28,8 +28,8 @@ import clsx from 'clsx';
|
||||
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 { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||
import { createTagFilter } from '../filter/utils';
|
||||
import { SaveAsCollectionButton } from '../view';
|
||||
import * as styles from './page-list-header.css';
|
||||
|
||||
@@ -39,8 +39,8 @@ 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 { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||
import { IsFavoriteIcon } from '../pure/icons';
|
||||
import { FavoriteTag } from './components/favorite-tag';
|
||||
import * as styles from './list.css';
|
||||
|
||||
@@ -3,7 +3,11 @@ import {
|
||||
pushGlobalLoadingEventAtom,
|
||||
resolveGlobalLoadingEventAtom,
|
||||
} from '@affine/component/global-loading';
|
||||
import { AIProvider } from '@affine/core/blocksuite/presets';
|
||||
import {
|
||||
AIProvider,
|
||||
CopilotClient,
|
||||
setupAIProvider,
|
||||
} from '@affine/core/blocksuite/ai';
|
||||
import { SyncAwareness } from '@affine/core/components/affine/awareness';
|
||||
import { useRegisterFindInPageCommands } from '@affine/core/components/hooks/affine/use-register-find-in-page-commands';
|
||||
import { useRegisterWorkspaceCommands } from '@affine/core/components/hooks/use-register-workspace-commands';
|
||||
@@ -45,9 +49,6 @@ import {
|
||||
timeout,
|
||||
} from 'rxjs';
|
||||
|
||||
import { CopilotClient } from '../blocksuite/block-suite-editor/ai/copilot-client';
|
||||
import { setupAIProvider } from '../blocksuite/block-suite-editor/ai/setup-provider';
|
||||
|
||||
/**
|
||||
* @deprecated just for legacy code, will be removed in the future
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user