mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
fix: workspace storage settings issues (#3055)
(cherry picked from commit 00ce086e79)
This commit is contained in:
@@ -10,6 +10,9 @@ yarn -T run build:infra
|
|||||||
# generate prisma client type
|
# generate prisma client type
|
||||||
yarn workspace @affine/server prisma generate
|
yarn workspace @affine/server prisma generate
|
||||||
|
|
||||||
|
# generate i18n
|
||||||
|
yarn i18n-codegen gen
|
||||||
|
|
||||||
# lint staged files
|
# lint staged files
|
||||||
yarn exec lint-staged
|
yarn exec lint-staged
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ test('check workspace has a DB file', async ({ appInfo, workspace }) => {
|
|||||||
expect(await fs.exists(dbPath)).toBe(true);
|
expect(await fs.exists(dbPath)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
|
test('move workspace db file', async ({ page, appInfo, workspace }) => {
|
||||||
const w = await workspace.current();
|
const w = await workspace.current();
|
||||||
const settingButton = page.getByTestId('slider-bar-workspace-setting-button');
|
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||||
// goto settings
|
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||||
await settingButton.click();
|
|
||||||
|
// goto workspace setting
|
||||||
|
await page.getByTestId('workspace-list-item').click();
|
||||||
|
|
||||||
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp-dir');
|
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp-dir');
|
||||||
|
|
||||||
@@ -42,21 +44,24 @@ test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
|
|||||||
expect(files.some(f => f.endsWith('.affine'))).toBe(true);
|
expect(files.some(f => f.endsWith('.affine'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('export then add', async ({ page, appInfo, workspace }) => {
|
test('export then add', async ({ page, appInfo, workspace }) => {
|
||||||
const w = await workspace.current();
|
const w = await workspace.current();
|
||||||
const settingButton = page.getByTestId('slider-bar-workspace-setting-button');
|
|
||||||
// goto settings
|
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||||
await settingButton.click();
|
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||||
|
|
||||||
const originalId = w.id;
|
const originalId = w.id;
|
||||||
|
|
||||||
const newWorkspaceName = 'new-test-name';
|
const newWorkspaceName = 'new-test-name';
|
||||||
|
|
||||||
|
// goto workspace setting
|
||||||
|
await page.getByTestId('workspace-list-item').click();
|
||||||
|
|
||||||
// change workspace name
|
// change workspace name
|
||||||
await page.getByTestId('workspace-name-input').fill(newWorkspaceName);
|
await page.getByTestId('workspace-name-input').fill(newWorkspaceName);
|
||||||
await page.getByTestId('save-workspace-name').click();
|
await page.getByTestId('save-workspace-name').click();
|
||||||
await page.waitForSelector('text="Update workspace name success"');
|
await page.waitForSelector('text="Update workspace name success"');
|
||||||
await page.click('[data-tab-key="export"]');
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp.db');
|
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp.db');
|
||||||
|
|
||||||
@@ -73,10 +78,11 @@ test.skip('export then add', async ({ page, appInfo, workspace }) => {
|
|||||||
|
|
||||||
expect(await fs.exists(tmpPath)).toBe(true);
|
expect(await fs.exists(tmpPath)).toBe(true);
|
||||||
|
|
||||||
|
await page.getByTestId('modal-close-button').click();
|
||||||
|
|
||||||
// add workspace
|
// add workspace
|
||||||
// we are reusing the same db file so that we don't need to maintain one
|
// we are reusing the same db file so that we don't need to maintain one
|
||||||
// in the codebase
|
// in the codebase
|
||||||
|
|
||||||
await page.getByTestId('current-workspace').click();
|
await page.getByTestId('current-workspace').click();
|
||||||
await page.getByTestId('add-or-new-workspace').click();
|
await page.getByTestId('add-or-new-workspace').click();
|
||||||
|
|
||||||
|
|||||||
@@ -52,9 +52,11 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setupListener(docId?: string) {
|
setupListener(docId?: string) {
|
||||||
|
logger.debug('WorkspaceSQLiteDB: setupListener', this.workspaceId, docId);
|
||||||
const doc = this.getDoc(docId);
|
const doc = this.getDoc(docId);
|
||||||
if (doc) {
|
if (doc) {
|
||||||
const onUpdate = async (update: Uint8Array, origin: YOrigin) => {
|
const onUpdate = async (update: Uint8Array, origin: YOrigin) => {
|
||||||
|
logger.debug('onUpdate', this.workspaceId, docId, update.length);
|
||||||
const insertRows = [{ data: update, docId }];
|
const insertRows = [{ data: update, docId }];
|
||||||
if (origin === 'renderer') {
|
if (origin === 'renderer') {
|
||||||
await this.addUpdateToSQLite(insertRows);
|
await this.addUpdateToSQLite(insertRows);
|
||||||
@@ -68,7 +70,11 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
|||||||
logger.debug('external update', this.workspaceId);
|
logger.debug('external update', this.workspaceId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
doc.subdocs.forEach(subdoc => {
|
||||||
|
this.setupListener(subdoc.guid);
|
||||||
|
});
|
||||||
const onSubdocs = ({ added }: { added: Set<Y.Doc> }) => {
|
const onSubdocs = ({ added }: { added: Set<Y.Doc> }) => {
|
||||||
|
logger.info('onSubdocs', this.workspaceId, docId, added);
|
||||||
added.forEach(subdoc => {
|
added.forEach(subdoc => {
|
||||||
this.setupListener(subdoc.guid);
|
this.setupListener(subdoc.guid);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { Button, toast } from '@affine/component';
|
import { Button, FlexWrapper, toast, Tooltip } from '@affine/component';
|
||||||
import { SettingRow } from '@affine/component/setting-components';
|
import { SettingRow } from '@affine/component/setting-components';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { type FC, useCallback, useEffect, useState } from 'react';
|
import { type FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||||
|
import * as style from './style.css';
|
||||||
|
|
||||||
const useShowOpenDBFile = (workspaceId: string) => {
|
const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||||
const [show, setShow] = useState(false);
|
const [path, setPath] = useState<string | undefined>(undefined);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.apis && window.events && environment.isDesktop) {
|
if (window.apis && window.events && environment.isDesktop) {
|
||||||
window.apis?.workspace
|
window.apis?.workspace
|
||||||
.getMeta(workspaceId)
|
.getMeta(workspaceId)
|
||||||
.then(meta => {
|
.then(meta => {
|
||||||
setShow(!!meta.secondaryDBPath);
|
setPath(meta.secondaryDBPath);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -20,12 +22,12 @@ const useShowOpenDBFile = (workspaceId: string) => {
|
|||||||
return window.events.workspace.onMetaChange((newMeta: any) => {
|
return window.events.workspace.onMetaChange((newMeta: any) => {
|
||||||
if (newMeta.workspaceId === workspaceId) {
|
if (newMeta.workspaceId === workspaceId) {
|
||||||
const meta = newMeta.meta;
|
const meta = newMeta.meta;
|
||||||
setShow(!!meta.secondaryDBPath);
|
setPath(meta.secondaryDBPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [workspaceId]);
|
}, [workspaceId]);
|
||||||
return show;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StoragePanel: FC<{
|
export const StoragePanel: FC<{
|
||||||
@@ -33,7 +35,7 @@ export const StoragePanel: FC<{
|
|||||||
}> = ({ workspace }) => {
|
}> = ({ workspace }) => {
|
||||||
const workspaceId = workspace.id;
|
const workspaceId = workspace.id;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const showOpenFolder = useShowOpenDBFile(workspaceId);
|
const secondaryPath = useDBFileSecondaryPath(workspaceId);
|
||||||
|
|
||||||
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
|
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
|
||||||
const onRevealDBFile = useCallback(() => {
|
const onRevealDBFile = useCallback(() => {
|
||||||
@@ -65,23 +67,57 @@ export const StoragePanel: FC<{
|
|||||||
});
|
});
|
||||||
}, [moveToInProgress, t, workspaceId]);
|
}, [moveToInProgress, t, workspaceId]);
|
||||||
|
|
||||||
if (!showOpenFolder) {
|
const rowContent = useMemo(
|
||||||
return null;
|
() =>
|
||||||
}
|
secondaryPath ? (
|
||||||
|
<FlexWrapper justifyContent="space-between">
|
||||||
|
<Tooltip
|
||||||
|
zIndex={1000}
|
||||||
|
content={t['com.affine.settings.storage.db-location.change-hint']()}
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
data-testid="move-folder"
|
||||||
|
className={style.urlButton}
|
||||||
|
size="middle"
|
||||||
|
onClick={handleMoveTo}
|
||||||
|
>
|
||||||
|
{secondaryPath}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
data-testid="reveal-folder"
|
||||||
|
data-disabled={moveToInProgress}
|
||||||
|
onClick={onRevealDBFile}
|
||||||
|
>
|
||||||
|
{t['Open folder']()}
|
||||||
|
</Button>
|
||||||
|
</FlexWrapper>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
data-testid="move-folder"
|
||||||
|
data-disabled={moveToInProgress}
|
||||||
|
onClick={handleMoveTo}
|
||||||
|
>
|
||||||
|
{t['Move folder']()}
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
[handleMoveTo, moveToInProgress, onRevealDBFile, secondaryPath, t]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingRow
|
<SettingRow
|
||||||
name={t['Storage']()}
|
name={t['Storage']()}
|
||||||
desc={t['Storage Folder Hint']()}
|
desc={t[
|
||||||
spreadCol={false}
|
secondaryPath
|
||||||
|
? 'com.affine.settings.storage.description-alt'
|
||||||
|
: 'com.affine.settings.storage.description'
|
||||||
|
]()}
|
||||||
|
spreadCol={!secondaryPath}
|
||||||
>
|
>
|
||||||
<Button
|
{rowContent}
|
||||||
data-testid="move-folder"
|
|
||||||
data-disabled={moveToInProgress}
|
|
||||||
onClick={handleMoveTo}
|
|
||||||
>
|
|
||||||
{t['Move folder']()}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onRevealDBFile}>{t['Open folder']()}</Button>
|
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,12 +43,15 @@ globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
|||||||
|
|
||||||
export const urlButton = style({
|
export const urlButton = style({
|
||||||
width: 'calc(100% - 64px - 15px)',
|
width: 'calc(100% - 64px - 15px)',
|
||||||
|
justifyContent: 'left',
|
||||||
|
textAlign: 'left',
|
||||||
});
|
});
|
||||||
globalStyle(`${urlButton} span`, {
|
globalStyle(`${urlButton} span`, {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
fontWeight: '500',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fakeWrapper = style({
|
export const fakeWrapper = style({
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ const WorkspaceListItem = ({
|
|||||||
className={clsx(sidebarSelectItem, { active: isActive })}
|
className={clsx(sidebarSelectItem, { active: isActive })}
|
||||||
title={workspaceName}
|
title={workspaceName}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
data-testid="workspace-list-item"
|
||||||
>
|
>
|
||||||
<WorkspaceAvatar size={14} workspace={workspace} className="icon" />
|
<WorkspaceAvatar size={14} workspace={workspace} className="icon" />
|
||||||
<span className="setting-name">{workspaceName}</span>
|
<span className="setting-name">{workspaceName}</span>
|
||||||
|
|||||||
@@ -184,7 +184,11 @@ export const RootAppSidebar = ({
|
|||||||
</RouteMenuLinkItem>
|
</RouteMenuLinkItem>
|
||||||
)}
|
)}
|
||||||
{runtimeConfig.enableNewSettingModal ? (
|
{runtimeConfig.enableNewSettingModal ? (
|
||||||
<MenuItem icon={<SettingsIcon />} onClick={onOpenSettingModal}>
|
<MenuItem
|
||||||
|
data-testid="slider-bar-workspace-setting-button"
|
||||||
|
icon={<SettingsIcon />}
|
||||||
|
onClick={onOpenSettingModal}
|
||||||
|
>
|
||||||
<span data-testid="settings-modal-trigger">
|
<span data-testid="settings-modal-trigger">
|
||||||
{t['Settings']()}
|
{t['Settings']()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ export const ModalCloseButton = ({
|
|||||||
...props
|
...props
|
||||||
}: ModalCloseButtonProps) => {
|
}: ModalCloseButtonProps) => {
|
||||||
return absolute ? (
|
return absolute ? (
|
||||||
<StyledIconButton {...props}>
|
<StyledIconButton data-testid="modal-close-button" {...props}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</StyledIconButton>
|
</StyledIconButton>
|
||||||
) : (
|
) : (
|
||||||
<IconButton {...props}>
|
<IconButton data-testid="modal-close-button" {...props}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -280,7 +280,8 @@
|
|||||||
"UNKNOWN_ERROR": "Unbekannter Fehler",
|
"UNKNOWN_ERROR": "Unbekannter Fehler",
|
||||||
"Open folder hint": "Prüfe, wo sich der Speicherordner befindet.",
|
"Open folder hint": "Prüfe, wo sich der Speicherordner befindet.",
|
||||||
"Storage Folder": "Speicherordner",
|
"Storage Folder": "Speicherordner",
|
||||||
"Storage Folder Hint": "Speicherort überprüfen oder ändern.",
|
"com.affine.settings.storage.description": "Speicherort überprüfen oder ändern.",
|
||||||
|
"com.affine.settings.storage.description-alt": "Speicherort überprüfen oder ändern.",
|
||||||
"Sync across devices with AFFiNE Cloud": "Geräteübergreifende Synchronisierung mit AFFiNE Cloud",
|
"Sync across devices with AFFiNE Cloud": "Geräteübergreifende Synchronisierung mit AFFiNE Cloud",
|
||||||
"Name Your Workspace": "Workspace benennen",
|
"Name Your Workspace": "Workspace benennen",
|
||||||
"Update Available": "Update verfügbar",
|
"Update Available": "Update verfügbar",
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
"com.affine.settings.about.update.download.message": "Automatically download updates (to this device).",
|
"com.affine.settings.about.update.download.message": "Automatically download updates (to this device).",
|
||||||
"com.affine.settings.about.update.check.message": "Automatically check for new updates periodically.",
|
"com.affine.settings.about.update.check.message": "Automatically check for new updates periodically.",
|
||||||
"com.affine.settings.about.message": "Information about AFFiNE",
|
"com.affine.settings.about.message": "Information about AFFiNE",
|
||||||
|
"com.affine.settings.storage.description": "Check or change storage location",
|
||||||
|
"com.affine.settings.storage.description-alt": "Check or change storage location. Click path to edit location.",
|
||||||
|
"com.affine.settings.storage.db-location.change-hint": "Click to move storage location.",
|
||||||
"com.affine.pageMode": "Page Mode",
|
"com.affine.pageMode": "Page Mode",
|
||||||
"com.affine.edgelessMode": "Edgeless Mode",
|
"com.affine.edgelessMode": "Edgeless Mode",
|
||||||
"com.affine.onboarding.title1": "Hyper merged whiteboard and docs",
|
"com.affine.onboarding.title1": "Hyper merged whiteboard and docs",
|
||||||
@@ -281,7 +284,6 @@
|
|||||||
"Loading Page": "Loading Page",
|
"Loading Page": "Loading Page",
|
||||||
"Favorite pages for easy access": "Favourite pages for easy access",
|
"Favorite pages for easy access": "Favourite pages for easy access",
|
||||||
"emptySharedPages": "Shared pages will appear here.",
|
"emptySharedPages": "Shared pages will appear here.",
|
||||||
"Storage Folder Hint": "Check or change storage location. Click path to edit location.",
|
|
||||||
"You cannot delete the last workspace": "You cannot delete the last workspace",
|
"You cannot delete the last workspace": "You cannot delete the last workspace",
|
||||||
"Synced with AFFiNE Cloud": "Synced with AFFiNE Cloud",
|
"Synced with AFFiNE Cloud": "Synced with AFFiNE Cloud",
|
||||||
"Recent": "Recent",
|
"Recent": "Recent",
|
||||||
@@ -305,8 +307,8 @@
|
|||||||
"DB_FILE_ALREADY_LOADED": "Database file already loaded",
|
"DB_FILE_ALREADY_LOADED": "Database file already loaded",
|
||||||
"UNKNOWN_ERROR": "Unknown error",
|
"UNKNOWN_ERROR": "Unknown error",
|
||||||
"Default Location": "Default Location",
|
"Default Location": "Default Location",
|
||||||
"Open folder": "Open folder",
|
"Open folder": "Open",
|
||||||
"Move folder": "Move folder",
|
"Move folder": "Move",
|
||||||
"Set database location": "Set database location",
|
"Set database location": "Set database location",
|
||||||
"Move folder hint": "Select a new storage location.",
|
"Move folder hint": "Select a new storage location.",
|
||||||
"Storage Folder": "Storage Folder",
|
"Storage Folder": "Storage Folder",
|
||||||
|
|||||||
@@ -298,7 +298,8 @@
|
|||||||
"UNKNOWN_ERROR": "Erreur inconnue",
|
"UNKNOWN_ERROR": "Erreur inconnue",
|
||||||
"Restart Install Client Update": "Redémarrez pour installer la mise à jour",
|
"Restart Install Client Update": "Redémarrez pour installer la mise à jour",
|
||||||
"Save": "Enregistrer",
|
"Save": "Enregistrer",
|
||||||
"Storage Folder Hint": "Vérifier ou changer l'emplacement du lieu de stockage",
|
"com.affine.settings.storage.description": "Vérifier ou changer l'emplacement du lieu de stockage",
|
||||||
|
"com.affine.settings.storage.description-alt": "Vérifier ou changer l'emplacement du lieu de stockage",
|
||||||
"Use on current device only": "Utiliser seulement sur l'appareil actuel",
|
"Use on current device only": "Utiliser seulement sur l'appareil actuel",
|
||||||
"Default db location hint": "Par défaut, elle sera enregistrée sous {{location}}",
|
"Default db location hint": "Par défaut, elle sera enregistrée sous {{location}}",
|
||||||
"FILE_ALREADY_EXISTS": "Fichier déjà existant",
|
"FILE_ALREADY_EXISTS": "Fichier déjà existant",
|
||||||
|
|||||||
@@ -212,7 +212,8 @@
|
|||||||
"Sync across devices with AFFiNE Cloud": "AFFiNEクラウドでデバイス間を同期する",
|
"Sync across devices with AFFiNE Cloud": "AFFiNEクラウドでデバイス間を同期する",
|
||||||
"Stop publishing": "公開をやめる",
|
"Stop publishing": "公開をやめる",
|
||||||
"Storage Folder": "ストレージフォルダー",
|
"Storage Folder": "ストレージフォルダー",
|
||||||
"Storage Folder Hint": "保存場所を確認または変更する",
|
"com.affine.settings.storage.description": "保存場所を確認または変更する",
|
||||||
|
"com.affine.settings.storage.description-alt": "保存場所を確認または変更する",
|
||||||
"Upload": "アップロード",
|
"Upload": "アップロード",
|
||||||
"Update Available": "アップデート可能",
|
"Update Available": "アップデート可能",
|
||||||
"Update workspace name success": "ワークスペース名の更新に成功",
|
"Update workspace name success": "ワークスペース名の更新に成功",
|
||||||
|
|||||||
@@ -377,7 +377,8 @@
|
|||||||
"Shared Pages Description": "公开分享页面需要 AFFiNE Cloud 服务。",
|
"Shared Pages Description": "公开分享页面需要 AFFiNE Cloud 服务。",
|
||||||
"Shared Pages In Public Workspace Description": "整个工作区已在网络上发布,可以通过<1>工作区设置</1>进行编辑。",
|
"Shared Pages In Public Workspace Description": "整个工作区已在网络上发布,可以通过<1>工作区设置</1>进行编辑。",
|
||||||
"Storage Folder": "存储文件夹",
|
"Storage Folder": "存储文件夹",
|
||||||
"Storage Folder Hint": "检查或更改存储位置。",
|
"com.affine.settings.storage.description": "检查或更改存储位置。",
|
||||||
|
"com.affine.settings.storage.description-alt": "检查或更改存储位置。点击路径调整存储位置。",
|
||||||
"Successfully deleted": "成功删除。",
|
"Successfully deleted": "成功删除。",
|
||||||
"Sync across devices with AFFiNE Cloud": "使用 AFFiNE Cloud 在多个设备间进行同步",
|
"Sync across devices with AFFiNE Cloud": "使用 AFFiNE Cloud 在多个设备间进行同步",
|
||||||
"Synced with AFFiNE Cloud": "AFFiNE Cloud 同步完成",
|
"Synced with AFFiNE Cloud": "AFFiNE Cloud 同步完成",
|
||||||
|
|||||||
Reference in New Issue
Block a user