Compare commits

..

48 Commits

Author SHA1 Message Date
himself65
10cd000822 v0.5.4-canary.25 2023-05-05 23:57:10 -05:00
Himself65
496225a92e chore: bump version (#2249) 2023-05-05 23:41:51 -05:00
JimmFly
1ef408c9ad chore: update the style of help island in edgeless mode (#2244) 2023-05-05 15:35:05 -05:00
JimmFly
8d8119b39b chore: update theme color (#2242) 2023-05-05 15:34:01 -05:00
JimmFly
80c1f9e546 chore: disable navigation path (#2243) 2023-05-05 15:33:36 -05:00
Whitewater
dbd3249ae5 chore: clean all page list (#2245) 2023-05-05 14:46:58 -05:00
himself65
fbbcb4bad9 v0.5.4-canary.24 2023-05-04 23:30:02 -05:00
himself65
33069c87d0 build(theme): generate css file 2023-05-04 23:29:32 -05:00
himself65
637b8203d3 v0.5.4-canary.23 2023-05-04 23:20:02 -05:00
阿良仔
92859bf8b9 perf: remove data-testid in production (#2228)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-05 04:18:54 +00:00
夏宇航
8a617f91e6 style: fix popover z-index (#2215) 2023-05-05 04:13:56 +00:00
Whitewater
84b36c1d35 refactor: clean all pages component (#2176)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-04 22:59:16 -05:00
三咲智子 Kevin Deng
2c49c774af feat(y-indexeddb): add connected (#2208)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-05 03:42:49 +00:00
JimmFly
de0b300aca chore: optimize onboarding component style (#2235) 2023-05-04 22:26:40 -05:00
Himself65
4a50fe584c fix(electron): system theme (#2237) 2023-05-05 03:22:53 +00:00
Himself65
f7d1d922fa fix: cleanup page id in time (#2236) 2023-05-04 22:22:11 -05:00
Himself65
1b12972afd fix(electron): theme sync (#2231) 2023-05-04 21:00:05 -05:00
himself65
33c48eed79 v0.5.4-canary.22 2023-05-04 18:50:20 -05:00
Himself65
9631c99f7b chore: bump version (#2229) 2023-05-04 18:49:08 -05:00
Himself65
097cce34b5 fix: reduce useState and useEffect (#2223) 2023-05-04 17:53:52 -05:00
三咲智子 Kevin Deng
52b9734a7b feat(y-indexeddb): cleanup (#2207)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-04 20:25:58 +00:00
JimmFly
6d7f06c1c3 feat: add onboarding for client (#2144)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-04 15:29:16 +08:00
Fangdun Tsai
238f69b4e7 fix(component): click area of the item (#2221)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-04 05:46:56 +00:00
Himself65
3d43e61087 feat(i18n): static type on i18n (#2225) 2023-05-04 05:35:09 +00:00
Himself65
66c3b09c67 fix(hooks): reduce unused assertExists (#2224) 2023-05-04 03:55:45 +00:00
Himself65
1e84ad1484 fix: reduce pageMeta instance (#2222) 2023-05-03 22:09:43 -05:00
himself65
b3a3911cea v0.5.4-canary.21 2023-05-03 18:58:22 -05:00
Himself65
86988bd6e8 fix: dock to blocksuite latest API (#2219) 2023-05-03 18:57:59 -05:00
Himself65
9096ac2960 refactor: workspace provider (#2218) 2023-05-03 18:16:22 -05:00
himself65
ec39c23fb7 fix(web): add meta description 2023-05-03 18:15:52 -05:00
himself65
b036fe8502 chore: add codecov.yml 2023-05-03 00:47:43 -05:00
himself65
71142a3f1d v0.5.4-canary.20 2023-05-03 00:29:58 -05:00
Himself65
aace740df5 fix: prohibit delete last workspace (#2212) 2023-05-03 04:31:04 +00:00
Horus
f42d656cfa feat: add mac release zip file and release info yml (#2185) 2023-05-03 12:13:40 +08:00
Himself65
88124994e1 chore: bump version (#2211) 2023-05-02 22:40:53 -05:00
Fangdun Tsai
5a881ec223 fix(electron): ignore .DS_Store on MacOS (#2203) 2023-05-03 03:00:09 +00:00
Himself65
12b61d34c3 chore: bump version (#2210) 2023-05-02 16:50:58 -05:00
三咲智子 Kevin Deng
4eff5f3c38 chore: upgrade jotai devtools (#2209) 2023-05-02 21:27:01 +00:00
Himself65
648fad65e0 chore: bump version (#2206) 2023-04-30 20:03:40 -05:00
himself65
a2844e54d2 chore(y-indexeddb): add types fields 2023-04-30 18:40:34 -05:00
Fangdun Tsai
850cfe1187 fix: theme button width (#2202) 2023-04-30 01:51:33 -05:00
himself65
9030767d16 v0.5.4-canary.19 2023-04-29 05:23:48 -05:00
LongYinan
a4e7d0d0c3 fix(electron): remove disableHardwareAcceleration (#2199) 2023-04-29 05:22:59 -05:00
himself65
99898b2260 v0.5.4-canary.18 2023-04-28 16:00:57 -05:00
Himself65
1031fbc7ec refactor: guide atoms (#2196) 2023-04-28 15:59:59 -05:00
Himself65
31cccafb40 fix: sidebar regression (#2195) 2023-04-28 15:02:47 -05:00
Himself65
73a7c01580 revert: resize in app sidebar (#2193) 2023-04-28 05:41:17 -05:00
Whitewater
f9b012cac9 feat: add breakpoints (#2191) 2023-04-28 05:21:14 -05:00
200 changed files with 4384 additions and 3185 deletions

2
.github/CLA.md vendored
View File

@@ -56,5 +56,5 @@ Example:
- Skye Sun, @skyesun, 2023/04/14
- Jordy Delgado, @Jdelgad8, 2023/04/17
- Howard Do, @howarddo2208, 2023/04/20
- Kevin Deng, @sxzz, 2023/04/21
- 三咲智子 Kevin Deng, @sxzz, 2023/04/21
- Moeyua, @moeyua, 2023/04/22

View File

@@ -63,6 +63,7 @@ jobs:
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
ENABLE_TEST_PROPERTIES: false
- name: Upload Artifact (web-static)
uses: actions/upload-artifact@v3
@@ -76,6 +77,12 @@ jobs:
name: before-make-electron-dist
path: apps/electron/dist
- name: Upload YML Build Script
uses: actions/upload-artifact@v3
with:
name: release-yml-build-script
path: apps/electron/scripts/generate-yml.js
make-distribution:
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
strategy:
@@ -122,7 +129,7 @@ jobs:
run: |
mkdir -p builds
mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
mv apps/electron/out/*/make/zip/darwin/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
- name: Save artifacts (windows)
if: ${{ matrix.spec.platform == 'windows' }}
run: |
@@ -169,7 +176,17 @@ jobs:
with:
name: affine-linux-x64-builds
path: ./
- name: Download Artifacts
uses: actions/download-artifact@v3
with:
name: release-yml-build-script
path: ./
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Generate Release yml
run: |
RELEASE_VERSION=${{ github.event.inputs.version }} node generate-yml.js
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:
@@ -188,3 +205,4 @@ jobs:
./RELEASES
./*.AppImage
./*.apk
./*.yml

1
.gitignore vendored
View File

@@ -57,6 +57,7 @@ Thumbs.db
.next
out/
storybook-static
i18n_generated.ts
/test-results/
/playwright-report/

21
.i18n-codegen.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "./node_modules/@magic-works/i18n-codegen/schema.json",
"version": 1,
"list": [
{
"input": "./packages/i18n/src/resources/en.json",
"output": "./packages/i18n/src/i18n_generated",
"parser": {
"type": "i18next",
"contextSeparator": "$",
"pluralSeparator": "_"
},
"generator": {
"type": "i18next/react-hooks",
"hooks": "useAFFiNEI18N",
"emitTS": true,
"shouldUnescape": true
}
}
]
}

View File

@@ -8,7 +8,11 @@ import type { AppContext } from '../context';
export async function listWorkspaces(context: AppContext) {
const basePath = path.join(context.appDataPath, 'workspaces');
try {
return fs.readdir(basePath);
return fs
.readdir(basePath, {
withFileTypes: true,
})
.then(dirs => dirs.filter(dir => dir.isDirectory()).map(dir => dir.name));
} catch (error) {
logger.error('listWorkspaces', error);
return [];

View File

@@ -36,11 +36,6 @@ app.on('open-url', async (_, _url) => {
// todo: handle `affine://...` urls
});
/**
* Disable Hardware Acceleration for more power-save
*/
app.disableHardwareAcceleration();
/**
* Shout down background process if all windows was closed
*/

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.5.4-canary.17",
"version": "0.5.4-canary.25",
"author": "affine",
"description": "AFFiNE App",
"homepage": "https://github.com/toeverything/AFFiNE",
@@ -38,7 +38,7 @@
"@types/better-sqlite3": "^7.6.4",
"@types/fs-extra": "^11.0.1",
"cross-env": "7.0.3",
"electron": "24.1.3",
"electron": "24.2.0",
"electron-log": "^5.0.0-beta.23",
"electron-squirrel-startup": "1.0.0",
"electron-window-state": "^5.0.3",
@@ -47,11 +47,11 @@
"playwright": "^1.33.0",
"ts-node": "^10.9.1",
"undici": "^5.22.0",
"zx": "^7.2.1"
"zx": "^7.2.2"
},
"dependencies": {
"better-sqlite3": "^8.3.0",
"yjs": "^13.6.0"
"yjs": "^13.6.1"
},
"build": {
"protocols": [

View File

@@ -0,0 +1,63 @@
// do not run in your local machine
/* eslint-disable */
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
/* eslint-enable */
const yml = {
version: process.env.RELEASE_VERSION ?? '0.0.0',
files: [],
};
let fileList = [];
// TODO: maybe add `beta` and `stable`
const BUILD_TYPE = process.env.BUILD_TYPE || 'canary';
const generateYml = async () => {
fileList = [
`affine-${BUILD_TYPE}-macos-arm64.dmg`,
`affine-${BUILD_TYPE}-macos-arm64.zip`,
`affine-${BUILD_TYPE}-macos-x64.zip`,
`affine-${BUILD_TYPE}-macos-x64.dmg`,
];
fileList.forEach(fileName => {
const filePath = path.join(__dirname, './', fileName);
try {
const fileData = fs.readFileSync(filePath);
const hash = crypto
.createHash('sha512')
.update(fileData)
.digest('base64');
const size = fs.statSync(filePath).size;
yml.files.push({
url: fileName,
sha512: hash,
size: size,
});
} catch (e) {}
});
yml.path = yml.files[0].url;
yml.sha512 = yml.files[0].sha512;
yml.releaseDate = new Date().toISOString();
const ymlStr =
`version: ${yml.version}\n` +
`files:\n` +
yml.files
.map(file => {
return (
` - url: ${file.url}\n` +
` sha512: ${file.sha512}\n` +
` size: ${file.size}\n`
);
})
.join('') +
`path: ${yml.path}\n` +
`sha512: ${yml.sha512}\n` +
`releaseDate: ${yml.releaseDate}\n`;
fs.writeFileSync(`./latest-mac.yml`, ymlStr);
};
generateYml();

View File

@@ -16,6 +16,9 @@ test.beforeEach(async () => {
colorScheme: 'light',
});
page = await electronApp.firstWindow();
await page.getByTestId('onboarding-modal-close-button').click({
delay: 100,
});
// cleanup page data
await page.evaluate(() => localStorage.clear());
});
@@ -76,3 +79,21 @@ test('affine cloud disabled', async () => {
state: 'visible',
});
});
test('affine onboarding button', async () => {
await page.getByTestId('help-island').click();
await page.getByTestId('easy-guide').click();
const onboardingModal = page.locator('[data-testid=onboarding-modal]');
expect(await onboardingModal.isVisible()).toEqual(true);
const switchVideo = page.locator(
'[data-testid=onboarding-modal-switch-video]'
);
expect(await switchVideo.isVisible()).toEqual(true);
await page.getByTestId('onboarding-modal-next-button').click();
const editingVideo = page.locator(
'[data-testid=onboarding-modal-editing-video]'
);
expect(await editingVideo.isVisible()).toEqual(true);
await page.getByTestId('onboarding-modal-ok-button').click();
expect(await onboardingModal.isVisible()).toEqual(false);
});

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.5.4-canary.17",
"version": "0.5.4-canary.25",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -14,7 +14,7 @@
"postinstall": "prisma generate"
},
"dependencies": {
"@apollo/server": "^4.7.0",
"@apollo/server": "^4.7.1",
"@nestjs/apollo": "^11.0.5",
"@nestjs/common": "^9.4.0",
"@nestjs/core": "^9.4.0",
@@ -38,14 +38,14 @@
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/lodash-es": "^4.17.7",
"@types/node": "^18.16.2",
"@types/node": "^18.16.5",
"@types/supertest": "^2.0.12",
"c8": "^7.13.0",
"nodemon": "^2.0.22",
"supertest": "^6.3.3",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"vitest": "^0.30.1"
"vitest": "^0.31.0"
},
"nodemonConfig": {
"exec": "node",

View File

@@ -2,10 +2,12 @@
import { createRequire } from 'node:module';
import path from 'node:path';
import { runCli } from '@magic-works/i18n-codegen';
import { PerfseePlugin } from '@perfsee/webpack';
import { withSentryConfig } from '@sentry/nextjs';
import SentryWebpackPlugin from '@sentry/webpack-plugin';
import debugLocal from 'next-debug-local';
import { fileURLToPath } from 'url';
import { blockSuiteFeatureFlags, buildFlags } from './preset.config.mjs';
import { getCommitHash, getGitVersion } from './scripts/gitInfo.mjs';
@@ -17,6 +19,21 @@ const withVanillaExtract = createVanillaExtractPlugin();
console.info('Build Flags', buildFlags);
console.info('Editor Flags', blockSuiteFeatureFlags);
if (process.env.NODE_ENV !== 'development') {
await runCli(
{
config: fileURLToPath(
new URL('../../.i18n-codegen.json', import.meta.url)
),
watch: false,
},
error => {
console.error(error);
process.exit(1);
}
);
}
const enableDebugLocal = path.isAbsolute(process.env.LOCAL_BLOCK_SUITE ?? '');
if (enableDebugLocal) {
@@ -63,6 +80,11 @@ const nextConfig = {
removeConsole: {
exclude: ['error', 'log', 'warn', 'info'],
},
reactRemoveProperties: !buildFlags.enableTestProperties
? {
properties: ['^data-testid$'],
}
: false,
emotion: {
sourceMap: true,
},

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/web",
"private": true,
"version": "0.5.4-canary.17",
"version": "0.5.4-canary.25",
"scripts": {
"dev": "next dev",
"build": "next build",
@@ -19,28 +19,28 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230428070346-4bed88f0-nightly",
"@blocksuite/editor": "0.0.0-20230428070346-4bed88f0-nightly",
"@blocksuite/global": "0.0.0-20230428070346-4bed88f0-nightly",
"@blocksuite/icons": "^2.1.14",
"@blocksuite/store": "0.0.0-20230428070346-4bed88f0-nightly",
"@blocksuite/blocks": "0.0.0-20230505225643-03f75e5e-nightly",
"@blocksuite/editor": "0.0.0-20230505225643-03f75e5e-nightly",
"@blocksuite/global": "0.0.0-20230505225643-03f75e5e-nightly",
"@blocksuite/icons": "^2.1.15",
"@blocksuite/store": "0.0.0-20230505225643-03f75e5e-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.10.7",
"@emotion/react": "^11.10.6",
"@emotion/cache": "^11.10.8",
"@emotion/react": "^11.10.8",
"@emotion/server": "^11.10.0",
"@emotion/styled": "^11.10.6",
"@mui/material": "^5.12.2",
"@emotion/styled": "^11.10.8",
"@mui/material": "^5.12.3",
"@react-hookz/web": "^23.0.0",
"@sentry/nextjs": "^7.50.0",
"@sentry/nextjs": "^7.51.0",
"@toeverything/hooks": "workspace:*",
"cmdk": "^0.2.0",
"css-spring": "^4.1.0",
"dayjs": "^1.11.7",
"graphql": "^16.6.0",
"jotai": "^2.0.4",
"jotai-devtools": "^0.4.0",
"lit": "^2.7.3",
"jotai": "^2.1.0",
"jotai-devtools": "^0.5.2",
"lit": "^2.7.4",
"lottie-web": "^5.11.0",
"next-themes": "^0.2.1",
"react": "^18.2.0",
@@ -48,7 +48,7 @@
"react-is": "^18.2.0",
"swr": "^2.1.5",
"y-protocols": "^1.0.5",
"yjs": "^13.6.0",
"yjs": "^13.6.1",
"zod": "^3.21.4"
},
"devDependencies": {
@@ -56,24 +56,24 @@
"@redux-devtools/extension": "^3.2.5",
"@rich-data/viewer": "^2.15.6",
"@sentry/webpack-plugin": "^1.20.1",
"@swc-jotai/debug-label": "^0.0.9",
"@swc-jotai/react-refresh": "^0.0.7",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.1",
"@swc-jotai/debug-label": "^0.0.10",
"@swc-jotai/react-refresh": "^0.0.8",
"@types/react": "^18.2.5",
"@types/react-dom": "^18.2.4",
"@types/webpack-env": "^1.18.0",
"@vanilla-extract/css": "^1.11.0",
"@vanilla-extract/next-plugin": "^2.1.2",
"dotenv": "^16.0.3",
"eslint": "^8.39.0",
"eslint-config-next": "^13.3.1",
"next": "=13.2.3",
"eslint": "^8.40.0",
"eslint-config-next": "^13.4.1",
"next": "^13.4.1",
"next-debug-local": "^0.1.5",
"next-router-mock": "^0.9.3",
"raw-loader": "^4.0.2",
"redux": "^4.2.1",
"swc-plugin-coverage-instrument": "=0.0.14",
"swc-plugin-coverage-instrument": "^0.0.18",
"typescript": "^5.0.4",
"webpack": "^5.81.0"
"webpack": "^5.82.0"
},
"stableVersion": "0.0.0"
}

View File

@@ -18,6 +18,9 @@ export const blockSuiteFeatureFlags = {
* @type {import('@affine/env').BuildFlags}
*/
export const buildFlags = {
enableTestProperties: process.env.ENABLE_TEST_PROPERTIES
? process.env.ENABLE_TEST_PROPERTIES === 'true'
: true,
enableLegacyCloud: process.env.ENABLE_LEGACY_PROVIDER
? process.env.ENABLE_LEGACY_PROVIDER === 'true'
: true,

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,71 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import { initPage } from '@affine/env/blocksuite';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { WorkspaceFlavour } from '@affine/workspace/type';
import {
_cleanupBlockSuiteWorkspaceCache,
createEmptyBlockSuiteWorkspace,
} from '@affine/workspace/utils';
import type { ParagraphBlockModel } from '@blocksuite/blocks/models';
import type { Page } from '@blocksuite/store';
import { createStore } from 'jotai';
import { describe, expect, test } from 'vitest';
import { WorkspacePlugins } from '../../plugins';
import { rootCurrentWorkspaceAtom } from '../root';
describe('currentWorkspace atom', () => {
test('should be defined', async () => {
const store = createStore();
let id: string;
{
const workspace = createEmptyBlockSuiteWorkspace(
'test',
WorkspaceFlavour.LOCAL
);
const page = workspace.createPage({ id: 'page0' });
initPage(page);
const frameId = page.getBlockByFlavour('affine:frame').at(0)
?.id as string;
id = page.addBlock(
'affine:paragraph',
{
text: new page.Text('test 1'),
},
frameId
);
const provider = createIndexedDBDownloadProvider(workspace);
provider.sync();
await provider.whenReady;
const workspaceId = await WorkspacePlugins[
WorkspaceFlavour.LOCAL
].CRUD.create(workspace);
store.set(rootWorkspacesMetadataAtom, [
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
},
]);
_cleanupBlockSuiteWorkspaceCache();
}
store.set(
rootCurrentWorkspaceIdAtom,
store.get(rootWorkspacesMetadataAtom)[0].id
);
const workspace = await store.get(rootCurrentWorkspaceAtom);
expect(workspace).toBeDefined();
const page = workspace.blockSuiteWorkspace.getPage('page0') as Page;
expect(page).not.toBeNull();
const paragraphBlock = page.getBlockById(id) as ParagraphBlockModel;
expect(paragraphBlock).not.toBeNull();
expect(paragraphBlock.text.toString()).toBe('test 1');
});
});

View File

@@ -1,29 +0,0 @@
import { atomWithStorage } from 'jotai/utils';
export type Visibility = Record<string, boolean>;
const DEFAULT_VALUE = '0.0.0';
//atomWithStorage always uses initial value when first render
//https://github.com/pmndrs/jotai/discussions/1737
function getInitialValue() {
if (typeof window !== 'undefined') {
const storedValue = window.localStorage.getItem('lastVersion');
if (storedValue) {
return JSON.parse(storedValue);
}
}
return DEFAULT_VALUE;
}
export const lastVersionAtom = atomWithStorage(
'lastVersion',
getInitialValue()
);
export const guideHiddenAtom = atomWithStorage<Visibility>('guideHidden', {});
export const guideHiddenUntilNextUpdateAtom = atomWithStorage<Visibility>(
'guideHiddenUntilNextUpdate',
{}
);

View File

@@ -0,0 +1,69 @@
// these atoms cannot be moved to @affine/jotai since they use atoms from @affine/component
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
export type Guide = {
// should show quick search tips
quickSearchTips: boolean;
// should show change log
changeLog: boolean;
// should show recording tips
onBoarding: boolean;
};
const guidePrimitiveAtom = atomWithStorage<Guide>('helper-guide', {
quickSearchTips: true,
changeLog: true,
onBoarding: true,
});
export const guideQuickSearchTipsAtom = atom<
Guide['quickSearchTips'],
[open: boolean],
void
>(
get => {
const open = get(appSidebarOpenAtom);
const guide = get(guidePrimitiveAtom);
// only show the tips when the sidebar is closed
return guide.quickSearchTips && open === false;
},
(_, set, open) => {
set(guidePrimitiveAtom, tips => ({
...tips,
quickSearchTips: open,
}));
}
);
export const guideChangeLogAtom = atom<
Guide['changeLog'],
[open: boolean],
void
>(
get => {
return get(guidePrimitiveAtom).changeLog;
},
(_, set, open) => {
set(guidePrimitiveAtom, tips => ({
...tips,
changeLog: open,
}));
}
);
export const guideOnboardingAtom = atom<
Guide['onBoarding'],
[open: boolean],
void
>(
get => {
return get(guidePrimitiveAtom).onBoarding;
},
(_, set, open) => {
set(guidePrimitiveAtom, tips => ({
...tips,
onBoarding: open,
}));
}
);

View File

@@ -77,6 +77,7 @@ export const currentEditorAtom = rootCurrentEditorAtom;
export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom(false);
export const openQuickSearchModalAtom = atom(false);
export const openOnboardingModalAtom = atom(false);
export const openDisableCloudAlertModalAtom = atom(false);

View File

@@ -5,6 +5,10 @@ import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type {
NecessaryProvider,
WorkspaceRegistry,
} from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
@@ -37,18 +41,38 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
return CRUD.get(workspace.id).then(workspace => {
if (workspace === null) {
console.warn(
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
);
}
return workspace;
});
})
).then(workspaces =>
workspaces.filter(
(workspace): workspace is WorkspaceRegistry['affine' | 'local'] =>
workspace !== null
)
);
logger.info('workspaces', workspaces);
workspaces.forEach(workspace => {
if (workspace === null) {
console.warn(
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
);
const workspaceProviders = workspaces.map(workspace =>
workspace.providers.filter(
(provider): provider is NecessaryProvider =>
'necessary' in provider && provider.necessary
)
);
const promises: Promise<void>[] = [];
for (const providers of workspaceProviders) {
for (const provider of providers) {
provider.sync();
promises.push(provider.whenReady);
}
});
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
}
// we will wait for all the necessary providers to be ready
await Promise.all(promises);
logger.info('workspaces', workspaces);
return workspaces;
});
/**
@@ -77,6 +101,15 @@ export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
);
}
const providers = workspace.providers.filter(
(provider): provider is NecessaryProvider =>
'necessary' in provider && provider.necessary === true
);
for (const provider of providers) {
provider.sync();
// we will wait for the necessary providers to be ready
await provider.whenReady;
}
return workspace;
}
);

View File

@@ -1,30 +0,0 @@
/**
* @vitest-environment happy-dom
*/
import 'fake-indexeddb/auto';
import { beforeEach, describe, expect, test } from 'vitest';
import { BlockSuiteWorkspace } from '../../shared';
import { createAffineProviders, createLocalProviders } from '..';
let blockSuiteWorkspace: BlockSuiteWorkspace;
beforeEach(() => {
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test' });
});
describe('blocksuite providers', () => {
test('should be valid provider', () => {
[createLocalProviders, createAffineProviders].forEach(createProviders => {
createProviders(blockSuiteWorkspace).forEach(provider => {
expect(provider).toBeTypeOf('object');
expect(provider).toHaveProperty('flavour');
expect(provider).toHaveProperty('connect');
expect(provider.connect).toBeTypeOf('function');
expect(provider).toHaveProperty('disconnect');
expect(provider.disconnect).toBeTypeOf('function');
});
});
});
});

View File

@@ -1,13 +1,15 @@
import { config } from '@affine/env';
import {
createIndexedDBProvider,
createIndexedDBDownloadProvider,
createLocalProviders,
} from '@affine/workspace/providers';
import { createBroadCastChannelProvider } from '@affine/workspace/providers';
import {
createAffineWebSocketProvider,
createBroadCastChannelProvider,
} from '@affine/workspace/providers';
import type { Provider } from '@affine/workspace/type';
import type { BlockSuiteWorkspace } from '../shared';
import { createAffineWebSocketProvider } from './providers';
import { createAffineDownloadProvider } from './providers/affine';
export const createAffineProviders = (
@@ -19,7 +21,7 @@ export const createAffineProviders = (
createAffineWebSocketProvider(blockSuiteWorkspace),
config.enableBroadCastChannelProvider &&
createBroadCastChannelProvider(blockSuiteWorkspace),
createIndexedDBProvider(blockSuiteWorkspace),
createIndexedDBDownloadProvider(blockSuiteWorkspace),
] as any[]
).filter(v => Boolean(v));
};

View File

@@ -12,9 +12,15 @@ export const createAffineDownloadProvider = (
): AffineDownloadProvider => {
assertExists(blockSuiteWorkspace.id);
const id = blockSuiteWorkspace.id;
let connected = false;
const callbacks = new Set<() => void>();
return {
flavour: 'affine-download',
background: true,
get connected() {
return connected;
},
callbacks,
connect: () => {
providerLogger.info('connect download provider', id);
if (hashMap.has(id)) {
@@ -23,6 +29,7 @@ export const createAffineDownloadProvider = (
blockSuiteWorkspace.doc,
new Uint8Array(hashMap.get(id) as ArrayBuffer)
);
connected = true;
return;
}
affineApis
@@ -41,6 +48,7 @@ export const createAffineDownloadProvider = (
},
disconnect: () => {
providerLogger.info('disconnect download provider', id);
connected = false;
},
cleanup: () => {
hashMap.delete(id);

View File

@@ -1,48 +0,0 @@
import { websocketPrefixUrl } from '@affine/env';
import { KeckProvider } from '@affine/workspace/affine/keck';
import { getLoginStorage } from '@affine/workspace/affine/login';
import type { AffineWebSocketProvider } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import type { BlockSuiteWorkspace } from '../../shared';
import { providerLogger } from '../logger';
const createAffineWebSocketProvider = (
blockSuiteWorkspace: BlockSuiteWorkspace
): AffineWebSocketProvider => {
let webSocketProvider: KeckProvider | null = null;
return {
flavour: 'affine-websocket',
background: false,
cleanup: () => {
assertExists(webSocketProvider);
webSocketProvider.destroy();
webSocketProvider = null;
},
connect: () => {
webSocketProvider = new KeckProvider(
websocketPrefixUrl + '/api/sync/',
blockSuiteWorkspace.id,
blockSuiteWorkspace.doc,
{
params: { token: getLoginStorage()?.token ?? '' },
awareness: blockSuiteWorkspace.awarenessStore.awareness,
// we maintain broadcast channel by ourselves
// @ts-expect-error
disableBc: true,
connect: false,
}
);
providerLogger.info('connect', webSocketProvider.url);
webSocketProvider.connect();
},
disconnect: () => {
assertExists(webSocketProvider);
providerLogger.info('disconnect', webSocketProvider.url);
webSocketProvider.destroy();
webSocketProvider = null;
},
};
};
export { createAffineWebSocketProvider };

View File

@@ -16,7 +16,7 @@ const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
}
);
const page = blockSuiteWorkspace.createPage('page0');
const page = blockSuiteWorkspace.createPage({ id: 'page0' });
const Editor: React.FC<{
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;

View File

@@ -1,5 +1,5 @@
import { IconButton, Modal, ModalWrapper } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import type React from 'react';
@@ -17,7 +17,7 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
open,
onClose,
}) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const user = useCurrentUser();
return (
@@ -33,8 +33,8 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
</IconButton>
</Header>
<Content>
<ContentTitle>{t('Enable AFFiNE Cloud')}?</ContentTitle>
<StyleTips>{t('Enable AFFiNE Cloud Description')}</StyleTips>
<ContentTitle>{t['Enable AFFiNE Cloud']()}?</ContentTitle>
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips>
{/* <StyleTips>{t('Retain cached cloud data')}</StyleTips> */}
<div>
<StyleButton
@@ -45,7 +45,7 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
onConfirm();
}}
>
{user ? t('Enable') : t('Sign in and Enable')}
{user ? t.Enable() : t['Sign in and Enable']()}
</StyleButton>
<StyleButton
shape="round"
@@ -53,7 +53,7 @@ export const EnableAffineCloudModal: React.FC<EnableAffineCloudModalProps> = ({
onClose();
}}
>
{t('Not now')}
{t['Not now']()}
</StyleButton>
</div>
</Content>

View File

@@ -1,60 +0,0 @@
import { MenuItem } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { ArrowRightSmallIcon, MoveToIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useRef, useState } from 'react';
import type { BlockSuiteWorkspace } from '../../../shared';
import { PinboardMenu } from '../pinboard';
import type { CommonMenuItemProps } from './types';
export type MoveToProps = CommonMenuItemProps<{
dragId: string;
dropId: string;
}> & {
metas: PageMeta[];
currentMeta: PageMeta;
blockSuiteWorkspace: BlockSuiteWorkspace;
};
/**
* @deprecated
*/
export const MoveTo = ({
metas,
currentMeta,
blockSuiteWorkspace,
onSelect,
onItemClick,
}: MoveToProps) => {
const { t } = useTranslation();
const ref = useRef<HTMLButtonElement>(null);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const open = anchorEl !== null;
return (
<>
<MenuItem
ref={ref}
onClick={e => {
e.stopPropagation();
setAnchorEl(ref.current);
onItemClick?.();
}}
icon={<MoveToIcon />}
endIcon={<ArrowRightSmallIcon />}
data-testid="move-to-menu-item"
>
{t('Move to')}
</MenuItem>
<PinboardMenu
anchorEl={anchorEl}
open={open}
placement="left"
metas={metas}
currentMeta={currentMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
onPinboardClick={onSelect}
/>
</>
);
};

View File

@@ -1,5 +1,5 @@
import { FlexWrapper } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
@@ -16,14 +16,14 @@ export const SearchContent = ({
results: PageMeta[];
onClick?: (dropId: string) => void;
}) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const record = useAtomValue(workspacePreferredModeAtom);
if (results.length) {
return (
<>
<StyledMenuSubTitle>
{t('Find results', { number: results.length })}
{t['Find results']({ number: `${results.length}` })}
</StyledMenuSubTitle>
{results.map(meta => {
return (
@@ -45,7 +45,7 @@ export const SearchContent = ({
return (
<>
<StyledMenuSubTitle>{t('Find 0 result')}</StyledMenuSubTitle>
<StyledMenuSubTitle>{t['Find 0 result']()}</StyledMenuSubTitle>
<FlexWrapper
alignItems="center"
justifyContent="center"

View File

@@ -1,6 +1,6 @@
import type { PureMenuProps } from '@affine/component';
import { Input, PureMenu, TreeView } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { RemoveIcon, SearchIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import React, { useCallback, useMemo, useState } from 'react';
@@ -40,7 +40,7 @@ export const PinboardMenu = ({
() => propsMetas.filter(m => m.id !== currentMeta.id),
[currentMeta.id, propsMetas]
);
const { t } = useTranslation();
const t = useAFFiNEI18N();
const [query, setQuery] = useState('');
const isSearching = query.length > 0;
@@ -92,7 +92,7 @@ export const PinboardMenu = ({
<Input
value={query}
onChange={setQuery}
placeholder={t('Move page to...')}
placeholder={t['Move page to...']()}
height={32}
noBorder={true}
onClick={e => e.stopPropagation()}
@@ -121,9 +121,9 @@ export const PinboardMenu = ({
}}
>
<RemoveIcon />
{t('Remove from Pinboard')}
{t['Remove from Pivots']()}
</StyledPinboard>
<p>{t('RFP')}</p>
<p>{t['RFP']()}</p>
</StyledMenuFooter>
)}
</PureMenu>

View File

@@ -1,12 +1,12 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { StyledPinboard } from '../styles';
export const EmptyItem = () => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<StyledPinboard disable={true} textWrap={true}>
{t('Organize pages to build knowledge')}
{t['Organize pages to build knowledge']()}
</StyledPinboard>
);
};

View File

@@ -1,5 +1,6 @@
import { MenuItem, MuiClickAwayListener, PureMenu } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { CopyLink, MoveToTrash } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
MoreVerticalIcon,
MoveToIcon,
@@ -13,7 +14,6 @@ import { useMemo, useRef, useState } from 'react';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { CopyLink, MoveToTrash } from '../../operation-menu-items';
import { PinboardMenu } from '../pinboard-menu/';
import { StyledOperationButton } from '../styles';
@@ -39,7 +39,7 @@ export const OperationButton = ({
onMenuClose,
onRename,
}: OperationButtonProps) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const timer = useRef<ReturnType<typeof setTimeout>>();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
@@ -100,7 +100,7 @@ export const OperationButton = ({
}}
icon={<PlusIcon />}
>
{t('Add a subpage inside')}
{t['Add a subpage inside']()}
</MenuItem>
{!isRoot && (
<MenuItem
@@ -111,7 +111,7 @@ export const OperationButton = ({
}}
icon={<MoveToIcon />}
>
{t('Move to')}
{t['Move to']()}
</MenuItem>
)}
{!isRoot && (
@@ -124,7 +124,7 @@ export const OperationButton = ({
}}
icon={<PenIcon />}
>
{t('Rename')}
{t['Rename']()}
</MenuItem>
)}
{!isRoot && (
@@ -152,9 +152,9 @@ export const OperationButton = ({
/>
<MoveToTrash.ConfirmModal
open={confirmModalOpen}
meta={currentMeta}
title={currentMeta.title}
onConfirm={() => {
toast(t('Moved to Trash'));
toast(t['Moved to Trash']());
removeToTrash(currentMeta.id);
onDelete();
}}

View File

@@ -1,15 +1,10 @@
import { Tooltip } from '@affine/component';
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
import { getEnvironment } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtom } from 'jotai';
import { useCallback, useEffect, useState } from 'react';
import {
useGuideHidden,
useGuideHiddenUntilNextUpdate,
useUpdateTipsOnVersionChange,
} from '../../../hooks/use-is-first-load';
import { SidebarSwitchIcon } from './icons';
import { StyledSidebarSwitch } from './style';
type SidebarSwitchProps = {
@@ -18,19 +13,15 @@ type SidebarSwitchProps = {
};
// fixme: the following code is not correct, SSR will fail because hydrate will not match the client side render
// in `StyledSidebarSwitch` component
// in `StyledSidebarSwitch` a component
export const SidebarSwitch = ({
visible = true,
tooltipContent,
...props
}: SidebarSwitchProps) => {
useUpdateTipsOnVersionChange();
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const [tooltipVisible, setTooltipVisible] = useState(false);
const [guideHidden, setGuideHidden] = useGuideHidden();
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
useGuideHiddenUntilNextUpdate();
const { t } = useTranslation();
const t = useAFFiNEI18N();
const checkIsMac = () => {
const env = getEnvironment();
return env.isBrowser && env.isMacOs;
@@ -43,7 +34,7 @@ export const SidebarSwitch = ({
}, []);
tooltipContent =
tooltipContent || (open ? t('Collapse sidebar') : t('Expand sidebar'));
tooltipContent || (open ? t['Collapse sidebar']() : t['Expand sidebar']());
return (
<Tooltip
@@ -59,22 +50,7 @@ export const SidebarSwitch = ({
onClick={useCallback(() => {
setOpen(open => !open);
setTooltipVisible(false);
if (!guideHiddenUntilNextUpdate['quickSearchTips']) {
setGuideHiddenUntilNextUpdate({
...guideHiddenUntilNextUpdate,
quickSearchTips: true,
});
setTimeout(() => {
setGuideHidden({ ...guideHidden, quickSearchTips: false });
}, 200);
}
}, [
guideHidden,
guideHiddenUntilNextUpdate,
setGuideHidden,
setGuideHiddenUntilNextUpdate,
setOpen,
])}
}, [setOpen])}
onMouseEnter={useCallback(() => {
setTooltipVisible(true);
}, [])}

View File

@@ -1,5 +1,5 @@
import { IconButton, Modal, ModalWrapper } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import type React from 'react';
@@ -15,7 +15,7 @@ export type TransformWorkspaceToAffineModalProps = {
export const TransformWorkspaceToAffineModal: React.FC<
TransformWorkspaceToAffineModalProps
> = ({ open, onClose, onConform }) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const user = useCurrentUser();
return (
@@ -35,8 +35,8 @@ export const TransformWorkspaceToAffineModal: React.FC<
</IconButton>
</Header>
<Content>
<ContentTitle>{t('Enable AFFiNE Cloud')}?</ContentTitle>
<StyleTips>{t('Enable AFFiNE Cloud Description')}</StyleTips>
<ContentTitle>{t['Enable AFFiNE Cloud']()}?</ContentTitle>
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips>
{/* <StyleTips>{t('Retain cached cloud data')}</StyleTips> */}
<div>
<StyleButton
@@ -61,7 +61,7 @@ export const TransformWorkspaceToAffineModal: React.FC<
// setLoading(false);
}}
>
{user ? t('Enable') : t('Sign in and Enable')}
{user ? t['Enable']() : t['Sign in and Enable']()}
</StyleButton>
<StyleButton
shape="round"
@@ -69,7 +69,7 @@ export const TransformWorkspaceToAffineModal: React.FC<
onClose();
}}
>
{t('Not now')}
{t['Not now']()}
</StyleButton>
</div>
</Content>

View File

@@ -1,4 +1,4 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { SettingPanel, WorkspaceRegistry } from '@affine/workspace/type';
import { settingPanel, WorkspaceFlavour } from '@affine/workspace/type';
import type { MouseEvent } from 'react';
@@ -39,6 +39,7 @@ export type WorkspaceSettingDetailProps = {
export type PanelProps = WorkspaceSettingDetailProps;
type Name = 'General' | 'Sync' | 'Collaboration' | 'Publish' | 'Export';
const panelMap = {
[settingPanel.General]: {
name: 'General',
@@ -63,7 +64,7 @@ const panelMap = {
},
} satisfies {
[Key in SettingPanel]: {
name: string;
name: Name;
enable?: (flavour: WorkspaceFlavour) => boolean;
ui: React.FC<PanelProps>;
};
@@ -96,7 +97,7 @@ export const WorkspaceSettingDetail: React.FC<
if (!(currentTab in panelMap)) {
throw new Error('Invalid activeTab: ' + currentTab);
}
const { t } = useTranslation();
const t = useAFFiNEI18N();
const workspaceId = workspace.id;
useEffect(() => {
if (isAffine && isOwner) {
@@ -148,7 +149,7 @@ export const WorkspaceSettingDetail: React.FC<
data-tab-key={key}
onClick={handleTabClick}
>
{t(value.name)}
{t[value.name]()}
</WorkspaceSettingTagItem>
);
})}

View File

@@ -1,6 +1,6 @@
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { PermissionType } from '@affine/workspace/affine/api';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
@@ -41,7 +41,7 @@ const AffineRemoteCollaborationPanel: React.FC<
}
> = ({ workspace }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const { t } = useTranslation();
const t = useAFFiNEI18N();
const { members, removeMember } = useMembers(workspace.id);
return (
<>
@@ -49,11 +49,11 @@ const AffineRemoteCollaborationPanel: React.FC<
<ul>
<StyledMemberTitleContainer>
<StyledMemberNameContainer>
{t('Users')} (
{t['Users']()} (
<span data-testid="member-length">{members.length}</span>)
</StyledMemberNameContainer>
<StyledMemberRoleContainer>
{t('Access level')}
{t['Access level']()}
</StyledMemberRoleContainer>
<div style={{ width: '24px', paddingRight: '48px' }}></div>
</StyledMemberTitleContainer>
@@ -91,9 +91,9 @@ const AffineRemoteCollaborationPanel: React.FC<
<StyledMemberRoleContainer>
{member.accepted
? member.type !== PermissionType.Owner
? t('Member')
: t('Owner')
: t('Pending')}
? t['Member']()
: t['Owner']()
: t['Pending']()}
</StyledMemberRoleContainer>
{member.type === PermissionType.Owner ? (
<StyledMoreVerticalDiv />
@@ -109,14 +109,14 @@ const AffineRemoteCollaborationPanel: React.FC<
// @ts-ignore
await removeMember(member.id);
toast(
t('Member has been removed', {
t['Member has been removed']({
name: user.name,
})
);
}}
icon={<DeleteTemporarilyIcon />}
>
{t('Remove from workspace')}
{t['Remove from workspace']()}
</MenuItem>
</>
}
@@ -145,7 +145,7 @@ const AffineRemoteCollaborationPanel: React.FC<
data-testid="invite-members"
shape="circle"
>
{t('Invite Members')}
{t['Invite Members']()}
</Button>
</StyledMemberButtonContainer>
</StyledMemberContainer>
@@ -168,11 +168,11 @@ const LocalCollaborationPanel: React.FC<
workspace: LocalWorkspace;
}
> = ({ workspace, onTransferWorkspace }) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
return (
<>
<Wrapper marginBottom="42px">{t('Collaboration Description')}</Wrapper>
<Wrapper marginBottom="42px">{t['Collaboration Description']()}</Wrapper>
<Button
data-testid="local-workspace-enable-cloud-button"
@@ -182,7 +182,7 @@ const LocalCollaborationPanel: React.FC<
setOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
{t['Enable AFFiNE Cloud']()}
</Button>
{config.enableLegacyCloud ? (
<TransformWorkspaceToAffineModal

View File

@@ -7,7 +7,7 @@ import {
MuiAvatar,
styled,
} from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EmailIcon } from '@blocksuite/icons';
import type React from 'react';
import { Suspense, useCallback, useState } from 'react';
@@ -60,7 +60,7 @@ export const InviteMemberModal = ({
const { inviteMember } = useMembers(workspaceId);
const [email, setEmail] = useState<string>('');
const [showMemberPreview, setShowMemberPreview] = useState(false);
const { t } = useTranslation();
const t = useAFFiNEI18N();
const inputChange = useCallback((value: string) => {
setEmail(value);
}, []);
@@ -77,7 +77,7 @@ export const InviteMemberModal = ({
/>
</Header>
<Content>
<ContentTitle>{t('Invite Members')}</ContentTitle>
<ContentTitle>{t['Invite Members']()}</ContentTitle>
<InviteBox>
<Input
data-testid="invite-member-input"
@@ -90,7 +90,7 @@ export const InviteMemberModal = ({
onBlur={useCallback(() => {
setShowMemberPreview(false);
}, [])}
placeholder={t('Invite placeholder')}
placeholder={t['Invite placeholder']()}
/>
{showMemberPreview && gmailReg.test(email) && (
<Suspense fallback="loading...">
@@ -112,7 +112,7 @@ export const InviteMemberModal = ({
onInviteSuccess();
}}
>
{t('Invite')}
{t['Invite']()}
</Button>
</Footer>
</ModalWrapper>

View File

@@ -1,11 +1,11 @@
import { Button, Wrapper } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
export const ExportPanel = () => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<>
<Wrapper marginBottom="42px"> {t('Export Description')}</Wrapper>
<Wrapper marginBottom="42px"> {t['Export Description']()}</Wrapper>
<Button
type="light"
shape="circle"
@@ -14,7 +14,7 @@ export const ExportPanel = () => {
window.apis.openSaveDBFileDialog();
}}
>
{t('Export AFFiNE backup file')}
{t['Export AFFiNE backup file']()}
</Button>
</>
);

View File

@@ -1,5 +1,6 @@
import { Button, Input, Modal, ModalCloseButton } from '@affine/component';
import { Trans, useTranslation } from '@affine/i18n';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useCallback, useState } from 'react';
@@ -33,11 +34,11 @@ export const WorkspaceDeleteModal = ({
);
const [deleteStr, setDeleteStr] = useState<string>('');
const allowDelete = deleteStr === workspaceName;
const { t } = useTranslation();
const t = useAFFiNEI18N();
const handleDelete = useCallback(() => {
onDeleteWorkspace().then(() => {
toast(t('Successfully deleted'), {
toast(t['Successfully deleted'](), {
portal: document.body,
});
});
@@ -47,7 +48,7 @@ export const WorkspaceDeleteModal = ({
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t('Delete Workspace')}?</StyledModalHeader>
<StyledModalHeader>{t['Delete Workspace']()}?</StyledModalHeader>
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description">
@@ -80,7 +81,7 @@ export const WorkspaceDeleteModal = ({
}}
onChange={setDeleteStr}
data-testid="delete-workspace-input"
placeholder={t('Placeholder of delete workspace')}
placeholder={t['Placeholder of delete workspace']()}
value={deleteStr}
width={315}
height={42}
@@ -88,7 +89,7 @@ export const WorkspaceDeleteModal = ({
</StyledInputContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t('Cancel')}
{t['Cancel']()}
</Button>
<Button
data-testid="delete-workspace-confirm-button"
@@ -98,7 +99,7 @@ export const WorkspaceDeleteModal = ({
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Delete')}
{t['Delete']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>

View File

@@ -1,6 +1,6 @@
import { Button, FlexWrapper, MuiFade } from '@affine/component';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
@@ -38,7 +38,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
const [input, setInput] = useState<string>(name);
const isOwner = useIsWorkspaceOwner(workspace);
const [showEditInput, setShowEditInput] = useState(false);
const { t } = useTranslation();
const t = useAFFiNEI18N();
const handleUpdateWorkspaceName = (name: string) => {
setName(name);
@@ -50,7 +50,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
return (
<>
<StyledRow>
<StyledSettingKey>{t('Workspace Avatar')}</StyledSettingKey>
<StyledSettingKey>{t['Workspace Avatar']()}</StyledSettingKey>
<StyledAvatar disabled={!isOwner}>
{isOwner ? (
<Upload
@@ -72,7 +72,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
</StyledRow>
<StyledRow>
<StyledSettingKey>{t('Workspace Name')}</StyledSettingKey>
<StyledSettingKey>{t['Workspace Name']()}</StyledSettingKey>
<div style={{ position: 'relative' }}>
<MuiFade in={!showEditInput}>
@@ -84,7 +84,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
setShowEditInput(true);
}}
>
{t('Edit')}
{t['Edit']()}
</StyledEditButton>
)}
</FlexWrapper>
@@ -97,7 +97,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
width={284}
height={38}
value={input}
placeholder={t('Workspace Name')}
placeholder={t['Workspace Name']()}
maxLength={50}
minLength={0}
onChange={newName => {
@@ -114,7 +114,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
setShowEditInput(false);
}}
>
{t('Confirm')}
{t['Confirm']()}
</Button>
<Button
type="default"
@@ -125,7 +125,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
setShowEditInput(false);
}}
>
{t('Cancel')}
{t['Cancel']()}
</Button>
</FlexWrapper>
</MuiFade>
@@ -165,29 +165,29 @@ export const GeneralPanel: React.FC<PanelProps> = ({
}
}}
>
<StyledSettingKey>{t('Workspace Type')}</StyledSettingKey>
<StyledSettingKey>{t['Workspace Type']()}</StyledSettingKey>
{isOwner ? (
workspace.flavour === WorkspaceFlavour.LOCAL ? (
<StyledWorkspaceInfo>
<LocalWorkspaceIcon />
<span>{t('Local Workspace')}</span>
<span>{t['Local Workspace']()}</span>
</StyledWorkspaceInfo>
) : (
<StyledWorkspaceInfo>
<CloudWorkspaceIcon />
<span>{t('Cloud Workspace')}</span>
<span>{t['Cloud Workspace']()}</span>
</StyledWorkspaceInfo>
)
) : (
<StyledWorkspaceInfo>
<JoinedWorkspaceIcon />
<span>{t('Joined Workspace')}</span>
<span>{t['Joined Workspace']()}</span>
</StyledWorkspaceInfo>
)}
</StyledRow>
<StyledRow>
<StyledSettingKey> {t('Delete Workspace')}</StyledSettingKey>
<StyledSettingKey> {t['Delete Workspace']()}</StyledSettingKey>
{isOwner ? (
<>
<Button
@@ -199,7 +199,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
setShowDelete(true);
}}
>
{t('Delete Workspace')}
{t['Delete Workspace']()}
</Button>
<WorkspaceDeleteModal
onDeleteWorkspace={onDeleteWorkspace}
@@ -219,7 +219,7 @@ export const GeneralPanel: React.FC<PanelProps> = ({
setShowLeave(true);
}}
>
{t('Leave Workspace')}
{t['Leave Workspace']()}
</Button>
<WorkspaceLeave
open={showLeave}

View File

@@ -1,7 +1,7 @@
import { Modal } from '@affine/component';
import { ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
StyledButtonContent,
@@ -17,7 +17,7 @@ interface WorkspaceDeleteProps {
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
// const { leaveWorkSpace } = useWorkspaceHelper();
const { t } = useTranslation();
const t = useAFFiNEI18N();
const handleLeave = async () => {
// await leaveWorkSpace();
onClose();
@@ -27,13 +27,13 @@ export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t('Leave Workspace')}</StyledModalHeader>
<StyledModalHeader>{t['Leave Workspace']()}</StyledModalHeader>
<StyledTextContent>
{t('Leave Workspace Description')}
{t['Leave Workspace Description']()}
</StyledTextContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t('Cancel')}
{t['Cancel']()}
</Button>
<Button
onClick={handleLeave}
@@ -41,7 +41,7 @@ export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Leave')}
{t['Leave']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>

View File

@@ -6,7 +6,7 @@ import {
Wrapper,
} from '@affine/component';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { Box } from '@mui/material';
@@ -41,20 +41,20 @@ const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
);
}, []);
const shareUrl = origin + '/public-workspace/' + workspace.id;
const { t } = useTranslation();
const t = useAFFiNEI18N();
const publishWorkspace = useToggleWorkspacePublish(workspace);
const copyUrl = useCallback(() => {
navigator.clipboard.writeText(shareUrl);
toast(t('Copied link to clipboard'));
toast(t['Copied link to clipboard']());
}, [shareUrl, t]);
if (workspace.public) {
return (
<>
<Wrapper marginBottom="42px">{t('Published Description')}</Wrapper>
<Wrapper marginBottom="42px">{t['Published Description']()}</Wrapper>
<Wrapper marginBottom="12px">
<Content weight="500">{t('Share with link')}</Content>
<Content weight="500">{t['Share with link']()}</Content>
</Wrapper>
<FlexWrapper>
<Input
@@ -69,7 +69,7 @@ const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Copy Link')}
{t['Copy Link']()}
</Button>
</FlexWrapper>
<Button
@@ -81,14 +81,14 @@ const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
shape="circle"
style={{ marginTop: '38px' }}
>
{t('Stop publishing')}
{t['Stop publishing']()}
</Button>
</>
);
}
return (
<>
<Wrapper marginBottom="42px">{t('Publishing Description')}</Wrapper>
<Wrapper marginBottom="42px">{t['Publishing Description']()}</Wrapper>
<Button
data-testid="publish-to-web-button"
onClick={async () => {
@@ -97,7 +97,7 @@ const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
type="light"
shape="circle"
>
{t('Publish to web')}
{t['Publish to web']()}
</Button>
</>
);
@@ -111,7 +111,7 @@ const PublishPanelLocal: React.FC<PublishPanelLocalProps> = ({
workspace,
onTransferWorkspace,
}) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const [open, setOpen] = useState(false);
return (
<>
@@ -120,7 +120,7 @@ const PublishPanelLocal: React.FC<PublishPanelLocalProps> = ({
marginBottom: '42px',
}}
>
{t('Publishing')}
{t['Publishing']()}
</Box>
{/* TmpDisableAffineCloudModal */}
@@ -132,7 +132,7 @@ const PublishPanelLocal: React.FC<PublishPanelLocalProps> = ({
setOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
{t['Enable AFFiNE Cloud']()}
</Button>
{config.enableLegacyCloud ? (
<EnableAffineCloudModal

View File

@@ -1,5 +1,6 @@
import { Content, FlexWrapper, styled } from '@affine/component';
import { Trans, useTranslation } from '@affine/i18n';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
@@ -25,7 +26,7 @@ export const SyncPanel: React.FC<PanelProps> = ({ workspace }) => {
workspace.blockSuiteWorkspace
);
const user = useCurrentUser();
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<>
<FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
@@ -37,7 +38,7 @@ export const SyncPanel: React.FC<PanelProps> = ({ workspace }) => {
/>
<StyledWorkspaceName>{name}</StyledWorkspaceName>
&nbsp;
<Content weight={500}>{t('is a Cloud Workspace')}</Content>
<Content weight={500}>{t['is a Cloud Workspace']()}</Content>
</FlexWrapper>
<Trans i18nKey="Cloud Workspace Description">
All data will be synchronised and saved to the AFFiNE account

View File

@@ -0,0 +1,5 @@
import { style } from '@vanilla-extract/css';
export const pageListEmptyStyle = style({
height: 'calc(100% - 52px)',
});

View File

@@ -1,35 +1,171 @@
import { Empty } from '@affine/component';
import type { ListData, TrashListData } from '@affine/component/page-list';
import { PageList, PageListTrashView } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { useAtomValue } from 'jotai';
import type React from 'react';
import { useMemo } from 'react';
import { workspacePreferredModeAtom } from '../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
import PageList from './page-list';
import { toast } from '../../../utils';
import { pageListEmptyStyle } from './index.css';
export type BlockSuitePageListProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
listType: 'all' | 'trash' | 'favorite' | 'shared' | 'public';
isPublic?: true;
onOpenPage: (pageId: string, newTab?: boolean) => void;
};
const filter = {
all: (pageMeta: PageMeta) => !pageMeta.trash,
public: (pageMeta: PageMeta) => !pageMeta.trash,
trash: (pageMeta: PageMeta, allMetas: PageMeta[]) => {
const parentMeta = allMetas.find(m => m.subpageIds?.includes(pageMeta.id));
return !parentMeta?.trash && pageMeta.trash;
},
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
};
dayjs.extend(localizedFormat);
const formatDate = (date?: number | unknown) => {
const dateStr =
typeof date === 'number' ? dayjs(date).format('YYYY-MM-DD HH:mm') : '--';
return dateStr;
};
const PageListEmpty = (props: {
listType: BlockSuitePageListProps['listType'];
}) => {
const { listType } = props;
const t = useAFFiNEI18N();
const getEmptyDescription = () => {
if (listType === 'all') {
return t['emptyAllPages']();
}
if (listType === 'favorite') {
return t['emptyFavorite']();
}
if (listType === 'trash') {
return t['emptyTrash']();
}
if (listType === 'shared') {
return t['emptySharedPages']();
}
};
return (
<div className={pageListEmptyStyle}>
<Empty description={getEmptyDescription()} />
</div>
);
};
export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
blockSuiteWorkspace,
onOpenPage,
listType,
isPublic = false,
}) => {
return (
<PageList
blockSuiteWorkspace={blockSuiteWorkspace}
onClickPage={onOpenPage}
listType="all"
/>
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
const {
toggleFavorite,
removeToTrash,
restoreFromTrash,
permanentlyDeletePage,
cancelPublicPage,
} = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N();
const list = useMemo(
() => pageMetas.filter(pageMeta => filter[listType](pageMeta, pageMetas)),
[pageMetas, listType]
);
};
const record = useAtomValue(workspacePreferredModeAtom);
if (list.length === 0) {
return <PageListEmpty listType={listType} />;
}
if (listType === 'trash') {
const pageList: TrashListData[] = list.map(pageMeta => {
return {
icon:
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
pageId: pageMeta.id,
title: pageMeta.title,
createDate: formatDate(pageMeta.createDate),
updatedDate: formatDate(pageMeta.updatedDate),
onClickPage: () => onOpenPage(pageMeta.id),
onClickRestore: () => {
restoreFromTrash(pageMeta.id);
},
onRestorePage: () => {
restoreFromTrash(pageMeta.id);
toast(t['restored']({ title: pageMeta.title || 'Untitled' }));
},
onPermanentlyDeletePage: () => {
permanentlyDeletePage(pageMeta.id);
toast(t['Permanently deleted']());
},
};
});
return <PageListTrashView list={pageList} />;
}
const pageList: ListData[] = list.map(pageMeta => {
return {
icon:
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
pageId: pageMeta.id,
title: pageMeta.title,
favorite: !!pageMeta.favorite,
isPublicPage: !!pageMeta.isPublic,
createDate: formatDate(pageMeta.createDate),
updatedDate: formatDate(pageMeta.updatedDate),
onClickPage: () => onOpenPage(pageMeta.id),
onOpenPageInNewTab: () => onOpenPage(pageMeta.id, true),
onClickRestore: () => {
restoreFromTrash(pageMeta.id);
},
removeToTrash: () => {
removeToTrash(pageMeta.id);
toast(t['Successfully deleted']());
},
onRestorePage: () => {
restoreFromTrash(pageMeta.id);
toast(t['restored']({ title: pageMeta.title || 'Untitled' }));
},
bookmarkPage: () => {
toggleFavorite(pageMeta.id);
toast(
pageMeta.favorite
? t['Removed from Favorites']()
: t['Added to Favorites']()
);
},
onDisablePublicSharing: () => {
cancelPublicPage(pageMeta.id);
toast('Successfully disabled', {
portal: document.body,
});
},
};
});
export const BlockSuitePublicPageList: React.FC<BlockSuitePageListProps> = ({
blockSuiteWorkspace,
onOpenPage,
}) => {
return (
<PageList
isPublic={true}
blockSuiteWorkspace={blockSuiteWorkspace}
onClickPage={onOpenPage}
isPublicWorkspace={isPublic}
list={pageList}
listType={listType}
/>
);
};

View File

@@ -1,30 +0,0 @@
import type { TableCellProps } from '@affine/component';
import { TableCell } from '@affine/component';
import type { PageMeta } from '@blocksuite/store';
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import React from 'react';
dayjs.extend(localizedFormat);
export const DateCell = ({
pageMeta,
dateKey,
backupKey = '',
...props
}: {
pageMeta: PageMeta;
dateKey: keyof PageMeta;
backupKey?: keyof PageMeta;
} & Omit<TableCellProps, 'children'>) => {
const value = pageMeta[dateKey] ?? pageMeta[backupKey];
return (
<TableCell ellipsis={true} {...props}>
{typeof value === 'number'
? dayjs(value).format('YYYY-MM-DD HH:mm')
: '--'}
</TableCell>
);
};
export default DateCell;

View File

@@ -1,30 +0,0 @@
import { Empty } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import React from 'react';
export const PageListEmpty = (props: { listType?: string }) => {
const { listType } = props;
const { t } = useTranslation();
const getEmptyDescription = () => {
if (listType === 'all') {
return t('emptyAllPages');
}
if (listType === 'favorite') {
return t('emptyFavorite');
}
if (listType === 'trash') {
return t('emptyTrash');
}
if (listType === 'shared') {
return t('emptySharedPages');
}
};
return (
<div style={{ height: 'calc(100% - 52px)' }}>
<Empty description={getEmptyDescription()} />
</div>
);
};
export default PageListEmpty;

View File

@@ -1,252 +0,0 @@
import {
Content,
IconButton,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Tooltip,
} from '@affine/component';
import { useTranslation } from '@affine/i18n';
import {
EdgelessIcon,
FavoritedIcon,
FavoriteIcon,
PageIcon,
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useMediaQuery, useTheme } from '@mui/material';
import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import type React from 'react';
import { useMemo } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import DateCell from './DateCell';
import Empty from './Empty';
import { OperationCell, TrashOperationCell } from './OperationCell';
import {
StyledTableContainer,
StyledTableRow,
StyledTitleLink,
StyledTitleWrapper,
} from './styles';
export type FavoriteTagProps = {
pageMeta: PageMeta;
onClick: () => void;
};
const FavoriteTag: React.FC<FavoriteTagProps> = ({
pageMeta: { favorite },
onClick,
}) => {
const { t } = useTranslation();
return (
<Tooltip
content={favorite ? t('Favorited') : t('Favorite')}
placement="top-start"
>
<IconButton
iconSize={[20, 20]}
onClick={e => {
e.stopPropagation();
onClick();
toast(
favorite ? t('Removed from Favorites') : t('Added to Favorites')
);
}}
style={{
color: favorite
? 'var(--affine-primary-color)'
: 'var(--affine-icon-color)',
}}
className={favorite ? '' : 'favorite-button'}
>
{favorite ? (
<FavoritedIcon data-testid="favorited-icon" />
) : (
<FavoriteIcon />
)}
</IconButton>
</Tooltip>
);
};
type PageListProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
isPublic?: boolean;
listType?: 'all' | 'trash' | 'favorite' | 'shared';
onClickPage: (pageId: string, newTab?: boolean) => void;
};
const filter = {
all: (pageMeta: PageMeta) => !pageMeta.trash,
trash: (pageMeta: PageMeta, allMetas: PageMeta[]) => {
const parentMeta = allMetas.find(m => m.subpageIds?.includes(pageMeta.id));
return !parentMeta?.trash && pageMeta.trash;
},
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
};
export const PageList: React.FC<PageListProps> = ({
blockSuiteWorkspace,
isPublic = false,
listType,
onClickPage,
}) => {
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
const helper = usePageMetaHelper(blockSuiteWorkspace);
const { removeToTrash, restoreFromTrash } =
useBlockSuiteMetaHelper(blockSuiteWorkspace);
const { t } = useTranslation();
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
const isTrash = listType === 'trash';
const isShared = listType === 'shared';
const record = useAtomValue(workspacePreferredModeAtom);
const list = useMemo(
() =>
pageList.filter(pageMeta =>
filter[listType ?? 'all'](pageMeta, pageList)
),
[pageList, listType]
);
if (list.length === 0) {
return <Empty listType={listType} />;
}
return (
<StyledTableContainer>
<Table>
<TableHead>
<TableRow>
{matches && (
<>
<TableCell proportion={0.5}>{t('Title')}</TableCell>
<TableCell proportion={0.2}>{t('Created')}</TableCell>
<TableCell proportion={0.2}>
{isTrash
? t('Moved to Trash')
: isShared
? 'Shared'
: t('Updated')}
</TableCell>
<TableCell proportion={0.1}></TableCell>
</>
)}
</TableRow>
</TableHead>
<TableBody>
{list.map((pageMeta, index) => {
return (
<StyledTableRow
data-testid={`page-list-item-${pageMeta.id}`}
key={`${pageMeta.id}-${index}`}
>
<TableCell
onClick={() => {
onClickPage(pageMeta.id);
}}
>
<StyledTitleWrapper>
<StyledTitleLink>
{record[pageMeta.id] === 'edgeless' ? (
<EdgelessIcon />
) : (
<PageIcon />
)}
<Content ellipsis={true} color="inherit">
{pageMeta.title || t('Untitled')}
</Content>
</StyledTitleLink>
{listType && !isTrash && (
<FavoriteTag
onClick={() => {
helper.setPageMeta(pageMeta.id, {
favorite: !pageMeta.favorite,
});
}}
pageMeta={pageMeta}
/>
)}
</StyledTitleWrapper>
</TableCell>
{matches && (
<>
<DateCell
pageMeta={pageMeta}
dateKey="createDate"
onClick={() => {
onClickPage(pageMeta.id);
}}
/>
<DateCell
pageMeta={pageMeta}
dateKey={isTrash ? 'trashDate' : 'updatedDate'}
backupKey={isTrash ? 'trashDate' : 'createDate'}
onClick={() => {
onClickPage(pageMeta.id);
}}
/>
{!isPublic && (
<TableCell
style={{ padding: 0 }}
data-testid={`more-actions-${pageMeta.id}`}
>
{isTrash ? (
<TrashOperationCell
pageMeta={pageMeta}
onPermanentlyDeletePage={pageId => {
blockSuiteWorkspace.removePage(pageId);
}}
onRestorePage={() => {
restoreFromTrash(pageMeta.id);
}}
onOpenPage={pageId => {
onClickPage(pageId, false);
}}
/>
) : (
<OperationCell
pageMeta={pageMeta}
metas={pageList}
blockSuiteWorkspace={blockSuiteWorkspace}
onOpenPageInNewTab={pageId => {
onClickPage(pageId, true);
}}
onToggleFavoritePage={(pageId: string) => {
helper.setPageMeta(pageId, {
favorite: !pageMeta.favorite,
});
}}
onToggleTrashPage={(pageId, isTrash) => {
if (isTrash) {
removeToTrash(pageId);
} else {
restoreFromTrash(pageId);
}
}}
/>
)}
</TableCell>
)}
</>
)}
</StyledTableRow>
);
})}
</TableBody>
</Table>
</StyledTableContainer>
);
};
export default PageList;

View File

@@ -21,7 +21,7 @@ export const StyledEditorModeSwitch = styled('div')<{
width: '24px',
height: '24px',
background: 'var(--affine-background-primary-color)',
boxShadow: 'var(--affine-shadow)',
boxShadow: 'var(--affine-shadow-1)',
borderRadius: '8px',
zIndex: 1,
position: 'absolute',
@@ -40,7 +40,7 @@ export const StyledSwitchItem = styled('button')<{
height: '24px',
borderRadius: '8px',
WebkitAppRegion: 'no-drag',
boxShadow: active ? 'var(--affine-shadow)' : 'none',
boxShadow: active ? 'var(--affine-shadow-1)' : 'none',
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
display: hide ? 'none' : 'inline-flex',
alignItems: 'center',

View File

@@ -1,6 +1,7 @@
// fixme(himself65): refactor this file
import { FlexWrapper, IconButton, Menu, MenuItem } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { Export, MoveToTrash } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
EdgelessIcon,
FavoritedIcon,
@@ -22,7 +23,6 @@ import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suit
import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { toast } from '../../../../utils';
import { Export, MoveToTrash } from '../../../affine/operation-menu-items';
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
import {
StyledHorizontalDivider,
@@ -45,7 +45,7 @@ const CommonMenu = () => {
<Menu
width={276}
content={content}
// placement="bottom-end"
placement="bottom"
disablePortal={true}
trigger="click"
>
@@ -57,7 +57,7 @@ const CommonMenu = () => {
);
};
const PageMenu = () => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
// fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
const [pageId] = useCurrentPageId();
@@ -83,7 +83,9 @@ const PageMenu = () => {
onClick={() => {
setPageMeta(pageId, { favorite: !favorite });
toast(
favorite ? t('Removed from Favorites') : t('Added to Favorites')
favorite
? t['Removed from Favorites']()
: t['Added to Favorites']()
);
}}
icon={
@@ -94,7 +96,7 @@ const PageMenu = () => {
)
}
>
{favorite ? t('Remove from favorites') : t('Add to Favorites')}
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
@@ -106,8 +108,8 @@ const PageMenu = () => {
}));
}}
>
{t('Convert to ')}
{mode === 'page' ? t('Edgeless') : t('Page')}
{t['Convert to ']()}
{mode === 'page' ? t['Edgeless']() : t['Page']()}
</MenuItem>
<Export />
{!pageMeta.isRootPinboard && (
@@ -150,10 +152,10 @@ const PageMenu = () => {
</Menu>
<MoveToTrash.ConfirmModal
open={openConfirm}
meta={pageMeta}
title={pageMeta.title}
onConfirm={() => {
removeToTrash(pageMeta.id);
toast(t('Moved to Trash'));
toast(t['Moved to Trash']());
}}
onCancel={() => {
setOpenConfirm(false);

View File

@@ -1,12 +1,12 @@
import { Button, displayFlex, Menu, MenuItem, styled } from '@affine/component';
import { LOCALES } from '@affine/i18n';
import { useTranslation } from '@affine/i18n';
import { useI18N } from '@affine/i18n';
import { ArrowDownSmallIcon, PublishIcon } from '@blocksuite/icons';
import type { FC, ReactElement } from 'react';
import { useCallback } from 'react';
const LanguageMenuContent: FC = () => {
const { i18n } = useTranslation();
const i18n = useI18N();
const changeLanguage = useCallback(
(event: string) => {
i18n.changeLanguage(event);
@@ -32,7 +32,7 @@ const LanguageMenuContent: FC = () => {
);
};
export const LanguageMenu: React.FC = () => {
const { i18n } = useTranslation();
const i18n = useI18N();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);

View File

@@ -1,6 +1,6 @@
import { displayFlex, IconButton, styled, Tooltip } from '@affine/component';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
getLoginStorage,
setLoginStorage,
@@ -79,7 +79,7 @@ export const SyncUser = () => {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
const t = useAFFiNEI18N();
const transformWorkspace = useTransformWorkspace();
if (!config.enableLegacyCloud) {
@@ -89,7 +89,7 @@ export const SyncUser = () => {
if (status === 'offline') {
return (
<Tooltip
content={t('Please make sure you are online')}
content={t['Please make sure you are online']()}
placement="bottom-end"
>
<IconWrapper>
@@ -103,7 +103,7 @@ export const SyncUser = () => {
return (
<>
<Tooltip
content={t('Saved then enable AFFiNE Cloud')}
content={t['Saved then enable AFFiNE Cloud']()}
placement="bottom-end"
>
<IconButton
@@ -156,7 +156,7 @@ export const SyncUser = () => {
}
return (
<Tooltip content={t('Synced with AFFiNE Cloud')} placement="bottom-end">
<Tooltip content={t['Synced with AFFiNE Cloud']()} placement="bottom-end">
<IconWrapper>
<CloudWorkspaceIcon />
</IconWrapper>

View File

@@ -1,5 +1,5 @@
import { Button, Confirm } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useRouter } from 'next/router';
@@ -20,7 +20,7 @@ export const TrashButtonGroup = () => {
meta => meta.id === pageId
);
assertExists(pageMeta);
const { t } = useTranslation();
const t = useAFFiNEI18N();
const router = useRouter();
const { restoreFromTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
@@ -36,7 +36,7 @@ export const TrashButtonGroup = () => {
restoreFromTrash(pageId);
}}
>
{t('Restore it')}
{t['Restore it']()}
</Button>
<Button
bold={true}
@@ -46,12 +46,12 @@ export const TrashButtonGroup = () => {
setOpen(true);
}}
>
{t('Delete permanently')}
{t['Delete permanently']()}
</Button>
<Confirm
title={t('TrashButtonGroupTitle')}
content={t('TrashButtonGroupDescription')}
confirmText={t('Delete')}
title={t['TrashButtonGroupTitle']()}
content={t['TrashButtonGroupDescription']()}
confirmText={t['Delete']()}
confirmType="danger"
open={open}
onConfirm={() => {

View File

@@ -1,6 +1,5 @@
import { DarkModeIcon, LightModeIcon } from '@blocksuite/icons';
import { useTheme } from 'next-themes';
import { useEffect } from 'react';
import {
StyledSwitchItem,
@@ -13,11 +12,6 @@ import {
export const MenuThemeModeSwitch = () => {
const { setTheme, resolvedTheme, theme } = useTheme();
useEffect(() => {
if (environment.isDesktop) {
window.apis?.onThemeChange(resolvedTheme === 'dark' ? 'dark' : 'light');
}
}, [resolvedTheme]);
return (
<StyledThemeModeContainer>
<StyledThemeModeSwitch data-testid="change-theme-container" inMenu={true}>

View File

@@ -17,6 +17,7 @@ export const StyledThemeModeContainer = styled('div')(() => {
});
export const StyledThemeButtonContainer = styled('div')(() => {
return {
height: '32px',
border: `1px solid var(--affine-border-color)`,
borderRadius: '4px',
cursor: 'pointer',
@@ -29,6 +30,8 @@ export const StyledThemeButton = styled('button')<{
active: boolean;
}>(({ active }) => {
return {
height: '100%',
flex: 1,
cursor: 'pointer',
color: active ? 'var(--affine-primary-color)' : 'var(--affine-icon-color)',
};
@@ -36,7 +39,7 @@ export const StyledThemeButton = styled('button')<{
export const StyledVerticalDivider = styled('div')(() => {
return {
width: '1px',
height: '32px',
height: '100%',
borderLeft: `1px solid var(--affine-border-color)`,
};
});

View File

@@ -1,5 +1,5 @@
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { CloseIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
@@ -133,7 +133,7 @@ export const Header = forwardRef<
setShowWarning(shouldShowWarning());
}, []);
const [open] = useAtom(appSidebarOpenAtom);
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<StyledHeaderContainer
@@ -156,7 +156,7 @@ export const Header = forwardRef<
<Suspense>
<SidebarSwitch
visible={!open}
tooltipContent={t('Expand sidebar')}
tooltipContent={t['Expand sidebar']()}
data-testid="sliderBar-arrowButton-expand"
/>
</Suspense>

View File

@@ -4,12 +4,12 @@ import { getEnvironment } from '@affine/env';
import { ArrowDownSmallIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue, useSetAtom } from 'jotai';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { forwardRef, useCallback, useRef } from 'react';
import { currentEditorAtom, openQuickSearchModalAtom } from '../../../atoms';
import { useGuideHidden } from '../../../hooks/use-is-first-load';
import { guideQuickSearchTipsAtom } from '../../../atoms/guide';
import { useElementResizeEffect } from '../../../hooks/use-workspaces';
import { QuickSearchButton } from '../../pure/quick-search-button';
import { EditorModeSwitch } from './editor-mode-switch';
@@ -39,7 +39,6 @@ export const WorkspaceHeader = forwardRef<
);
assertExists(pageMeta);
const title = pageMeta.title;
const [isTipsHidden, setTipsHidden] = useGuideHidden();
const isMac = () => {
const env = getEnvironment();
return env.isBrowser && env.isMacOs;
@@ -47,14 +46,18 @@ export const WorkspaceHeader = forwardRef<
const popperRef: PopperProps['popperRef'] = useRef(null);
const [showQuickSearchTips, setShowQuickSearchTips] = useAtom(
guideQuickSearchTipsAtom
);
useElementResizeEffect(
useAtomValue(currentEditorAtom),
useCallback(() => {
if (isTipsHidden.quickSearchTips || !popperRef.current) {
if (showQuickSearchTips || !popperRef.current) {
return;
}
popperRef.current.update();
}, [isTipsHidden.quickSearchTips])
}, [showQuickSearchTips])
);
const TipsContent = (
@@ -77,9 +80,7 @@ export const WorkspaceHeader = forwardRef<
</div>
<StyledQuickSearchTipButton
data-testid="quick-search-got-it"
onClick={() =>
setTipsHidden({ ...isTipsHidden, quickSearchTips: true })
}
onClick={() => setShowQuickSearchTips(false)}
>
Got it
</StyledQuickSearchTipButton>
@@ -107,7 +108,7 @@ export const WorkspaceHeader = forwardRef<
content={TipsContent}
placement="bottom"
popperRef={popperRef}
open={!isTipsHidden.quickSearchTips}
open={showQuickSearchTips}
offset={[0, -5]}
>
<StyledSearchArrowWrapper>

View File

@@ -15,7 +15,7 @@ export const StyledHeaderContainer = styled('div')<{
top: 0,
background: 'var(--affine-background-primary-color)',
WebkitAppRegion: 'drag',
zIndex: 1,
zIndex: 'var(--affine-z-index-popover)',
'@media (max-width: 768px)': {
'&[data-open="true"]': {
WebkitAppRegion: 'no-drag',

View File

@@ -1,5 +1,6 @@
import { getEnvironment } from '@affine/env';
import { Trans, useTranslation } from '@affine/i18n';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type React from 'react';
import { useEffect, useState } from 'react';
@@ -23,7 +24,7 @@ export const shouldShowWarning = () => {
};
export const OSWarningMessage: React.FC = () => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const [notChrome, setNotChrome] = useState(false);
const [notGoodVersion, setNotGoodVersion] = useState(false);
useEffect(() => {
@@ -44,7 +45,7 @@ export const OSWarningMessage: React.FC = () => {
</span>
);
} else if (notGoodVersion) {
return <span>{t('upgradeBrowser')}</span>;
return <span>{t['upgradeBrowser']()}</span>;
}
return null;
};

View File

@@ -2,6 +2,7 @@ import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-block-suite-workspace-page-title';
import { useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head';
@@ -34,7 +35,7 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
isPreview,
}) => {
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const page = blockSuiteWorkspace.getPage(pageId);
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
if (!page) {
throw new PageNotFoundError(blockSuiteWorkspace, pageId);
}
@@ -63,7 +64,7 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
style={{
height: 'calc(100% - 52px)',
}}
key={`${workspace.flavour}-${workspace.id}-${[pageId]}`}
key={`${workspace.flavour}-${workspace.id}-${pageId}`}
mode={isPublic ? 'page' : currentMode}
page={page}
onInit={useCallback(

View File

@@ -0,0 +1,45 @@
import { TourModal } from '@affine/component/tour-modal';
import { useAtom } from 'jotai';
import { useCallback, useEffect, useMemo } from 'react';
import { openOnboardingModalAtom } from '../../atoms';
import { guideOnboardingAtom } from '../../atoms/guide';
type OnboardingModalProps = {
onClose: () => void;
open: boolean;
};
const getHelperGuide = (): { onBoarding: boolean } | null => {
const helperGuide = localStorage.getItem('helper-guide');
if (helperGuide) {
return JSON.parse(helperGuide);
}
return null;
};
export const OnboardingModal: React.FC<OnboardingModalProps> = ({
open,
onClose,
}) => {
const [, setShowOnboarding] = useAtom(guideOnboardingAtom);
const [, setOpenOnboarding] = useAtom(openOnboardingModalAtom);
const onCloseTourModal = useCallback(() => {
setShowOnboarding(false);
onClose();
}, [onClose, setShowOnboarding]);
const shouldShow = useMemo(() => {
const helperGuide = getHelperGuide();
return helperGuide?.onBoarding ?? true;
}, []);
useEffect(() => {
if (shouldShow) {
setOpenOnboarding(true);
}
}, [shouldShow, setOpenOnboarding]);
return <TourModal open={open} onClose={onCloseTourModal} />;
};
export default OnboardingModal;

View File

@@ -6,7 +6,7 @@ import {
ModalWrapper,
styled,
} from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { KeyboardEvent } from 'react';
import { useCallback, useRef, useState } from 'react';
@@ -35,7 +35,7 @@ export const CreateWorkspaceModal = ({
},
[handleCreateWorkspace, workspaceName]
);
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<Modal open={open} onClose={onClose}>
<ModalWrapper width={560} height={342} style={{ padding: '10px' }}>
@@ -49,8 +49,8 @@ export const CreateWorkspaceModal = ({
/>
</Header>
<Content>
<ContentTitle>{t('New Workspace')}</ContentTitle>
<p>{t('Workspace description')}</p>
<ContentTitle>{t['New Workspace']()}</ContentTitle>
<p>{t['Workspace description']()}</p>
<Input
ref={ref => {
if (ref) {
@@ -59,7 +59,7 @@ export const CreateWorkspaceModal = ({
}}
data-testid="create-workspace-input"
onKeyDown={handleKeyDown}
placeholder={t('Set a Workspace name')}
placeholder={t['Set a Workspace name']()}
maxLength={15}
minLength={0}
onChange={value => {
@@ -86,7 +86,7 @@ export const CreateWorkspaceModal = ({
handleCreateWorkspace();
}}
>
{t('Create')}
{t['Create']()}
</Button>
</Content>
</ModalWrapper>

View File

@@ -1,6 +1,6 @@
import { Button } from '@affine/component';
import { styled } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { ChangeEvent } from 'react';
import type React from 'react';
import { useRef } from 'react';
@@ -17,7 +17,7 @@ export const Upload: React.FC<UploadProps> = ({
children,
...props
}) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const input_ref = useRef<HTMLInputElement>(null);
const _chooseFile = () => {
if (input_ref.current) {
@@ -37,7 +37,7 @@ export const Upload: React.FC<UploadProps> = ({
};
return (
<UploadStyle onClick={_chooseFile}>
{children ?? <Button>{t('Upload')}</Button>}
{children ?? <Button>{t['Upload']()}</Button>}
<input
ref={input_ref}
type="file"

View File

@@ -2,7 +2,7 @@ import { FlexWrapper } from '@affine/component';
import { IconButton } from '@affine/component';
import { Tooltip } from '@affine/component';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
import { useSetAtom } from 'jotai';
@@ -21,7 +21,7 @@ export type FooterProps = {
};
export const Footer: React.FC<FooterProps> = ({ user, onLogin, onLogout }) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
return (
<StyledFooter data-testid="workspace-list-modal-footer">
@@ -38,7 +38,7 @@ export const Footer: React.FC<FooterProps> = ({ user, onLogin, onLogout }) => {
<p>{user.email}</p>
</StyleUserInfo>
</FlexWrapper>
<Tooltip content={t('Sign out')} disablePortal={true}>
<Tooltip content={t['Sign out']()} disablePortal={true}>
<IconButton
data-testid="workspace-list-modal-sign-out"
onClick={() => {
@@ -69,7 +69,7 @@ export const Footer: React.FC<FooterProps> = ({ user, onLogin, onLogout }) => {
}
}}
>
{t('Sign in')}
{t['Sign in']()}
</StyledSignInButton>
)}
</StyledFooter>

View File

@@ -1,8 +1,12 @@
import { MuiFade, Tooltip } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { getEnvironment } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon, NewIcon } from '@blocksuite/icons';
import { useAtom } from 'jotai';
import { lazy, Suspense, useState } from 'react';
import { openOnboardingModalAtom } from '../../../atoms';
import { useCurrentMode } from '../../../hooks/current/use-current-mode';
import { ShortcutsModal } from '../shortcuts-modal';
import { ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
import {
@@ -11,23 +15,30 @@ import {
StyledIsland,
StyledTriggerWrapper,
} from './style';
const env = getEnvironment();
const ContactModal = lazy(() =>
import('@affine/component/contact-modal').then(({ ContactModal }) => ({
default: ContactModal,
}))
);
export type IslandItemNames = 'whatNew' | 'contact' | 'shortcuts';
const DEFAULT_SHOW_LIST: IslandItemNames[] = [
'whatNew',
'contact',
'shortcuts',
];
const DESKTOP_SHOW_LIST: IslandItemNames[] = [...DEFAULT_SHOW_LIST, 'guide'];
export type IslandItemNames = 'whatNew' | 'contact' | 'shortcuts' | 'guide';
export const HelpIsland = ({
showList = ['whatNew', 'contact', 'shortcuts'],
showList = env.isDesktop ? DESKTOP_SHOW_LIST : DEFAULT_SHOW_LIST,
}: {
showList?: IslandItemNames[];
}) => {
const mode = useCurrentMode();
const [, setOpenOnboarding] = useAtom(openOnboardingModalAtom);
const [spread, setShowSpread] = useState(false);
// const { triggerShortcutsModal, triggerContactModal } = useModal();
// const blockHub = useGlobalState(store => store.blockHub);
const { t } = useTranslation();
const t = useAFFiNEI18N();
//
// useEffect(() => {
// blockHub?.blockHubStatusUpdated.on(status => {
@@ -53,12 +64,13 @@ export const HelpIsland = ({
onClick={() => {
setShowSpread(!spread);
}}
inEdgelessPage={mode === 'edgeless'}
>
<StyledAnimateWrapper
style={{ height: spread ? `${showList.length * 44}px` : 0 }}
>
{showList.includes('whatNew') && (
<Tooltip content={t("Discover what's new!")} placement="left-end">
<Tooltip content={t["Discover what's new!"]()} placement="left-end">
<StyledIconWrapper
data-testid="right-bottom-change-log-icon"
onClick={() => {
@@ -73,7 +85,7 @@ export const HelpIsland = ({
</Tooltip>
)}
{showList.includes('contact') && (
<Tooltip content={t('Contact Us')} placement="left-end">
<Tooltip content={t['Contact Us']()} placement="left-end">
<StyledIconWrapper
data-testid="right-bottom-contact-us-icon"
onClick={() => {
@@ -86,7 +98,7 @@ export const HelpIsland = ({
</Tooltip>
)}
{showList.includes('shortcuts') && (
<Tooltip content={t('Keyboard Shortcuts')} placement="left-end">
<Tooltip content={t['Keyboard Shortcuts']()} placement="left-end">
<StyledIconWrapper
data-testid="shortcuts-icon"
onClick={() => {
@@ -98,9 +110,22 @@ export const HelpIsland = ({
</StyledIconWrapper>
</Tooltip>
)}
{showList.includes('guide') && (
<Tooltip content={'Easy Guide'} placement="left-end">
<StyledIconWrapper
data-testid="easy-guide"
onClick={() => {
setShowSpread(false);
setOpenOnboarding(true);
}}
>
<HelpIcon />
</StyledIconWrapper>
</Tooltip>
)}
</StyledAnimateWrapper>
<Tooltip content={t('Help and Feedback')} placement="left-end">
<Tooltip content={t['Help and Feedback']()} placement="left-end">
<MuiFade in={!spread} data-testid="faq-icon">
<StyledTriggerWrapper>
<HelpIcon />

View File

@@ -2,12 +2,17 @@ import { displayFlex, positionAbsolute, styled } from '@affine/component';
export const StyledIsland = styled('div')<{
spread: boolean;
}>(({ spread }) => {
inEdgelessPage?: boolean;
}>(({ spread, inEdgelessPage }) => {
return {
transition: 'box-shadow 0.2s',
width: '44px',
position: 'relative',
boxShadow: spread ? 'var(--affine-menu-shadow)' : 'unset',
boxShadow: spread
? 'var(--affine-menu-shadow)'
: inEdgelessPage
? 'var(--affine-menu-shadow)'
: 'unset',
padding: '0 4px 44px',
borderRadius: '10px',
background: spread
@@ -15,6 +20,7 @@ export const StyledIsland = styled('div')<{
: 'var(--affine-background-primary-color)',
':hover': {
background: spread ? null : 'var(--affine-white)',
boxShadow: spread ? null : 'var(--affine-menu-shadow)',
},
'::after': {
content: '""',

View File

@@ -1,6 +1,6 @@
import { styled } from '@affine/component';
import { AffineLoading } from '@affine/component/affine-loading';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { memo, Suspense } from 'react';
export const Loading = memo(function Loading() {
@@ -36,11 +36,11 @@ const StyledLoadingContainer = styled('div')(() => {
});
export const PageLoading = ({ text }: { text?: string }) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<StyledLoadingContainer>
<Loading />
<h1>{text ? text : t('Loading')}</h1>
<h1>{text ? text : t['Loading']()}</h1>
</StyledLoadingContainer>
);
};

View File

@@ -1,5 +1,5 @@
import { initPage } from '@affine/env/blocksuite';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageBlockModel } from '@blocksuite/blocks';
import { PlusIcon } from '@blocksuite/icons';
import { assertEquals, nanoid } from '@blocksuite/store';
@@ -27,7 +27,7 @@ export const Footer: React.FC<FooterProps> = ({
router,
}) => {
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { t } = useTranslation();
const t = useAFFiNEI18N();
const { jumpToPage } = useRouterHelper(router);
const MAX_QUERY_SHOW_LENGTH = 20;
const normalizedQuery =
@@ -60,9 +60,9 @@ export const Footer: React.FC<FooterProps> = ({
<StyledModalFooterContent>
<PlusIcon />
{query ? (
<span>{t('New Keyword Page', { query: normalizedQuery })}</span>
<span>{t['New Keyword Page']({ query: normalizedQuery })}</span>
) : (
<span>{t('New Page')}</span>
<span>{t['New Page']()}</span>
)}
</StyledModalFooterContent>
</Command.Item>

View File

@@ -1,4 +1,4 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { Command } from 'cmdk';
@@ -41,7 +41,7 @@ export const PublishedResults: FC<PublishedResultsProps> = ({
// router.push('/404');
// });
// }, [router, dataCenter, setPublishWorkspaceName]);
const { t } = useTranslation();
const t = useAFFiNEI18N();
useEffect(() => {
setResults(blockSuiteWorkspace.search(query));
//Save the Map<BlockId, PageId> obtained from the search as state
@@ -57,7 +57,7 @@ export const PublishedResults: FC<PublishedResultsProps> = ({
{query ? (
resultsPageMeta.length ? (
<Command.Group
heading={t('Find results', { number: resultsPageMeta.length })}
heading={t['Find results']({ number: `${resultsPageMeta.length}` })}
>
{resultsPageMeta.map(result => {
return (
@@ -85,7 +85,7 @@ export const PublishedResults: FC<PublishedResultsProps> = ({
</Command.Group>
) : (
<StyledNotFound>
<span>{t('Find 0 result')}</span>
<span>{t['Find 0 result']()}</span>
<Image
src="/imgs/no-result.svg"
alt="no result"

View File

@@ -1,5 +1,5 @@
import { UNTITLED_WORKSPACE_NAME } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
@@ -36,7 +36,7 @@ export const Results: FC<ResultsProps> = ({
const List = useSwitchToConfig(blockSuiteWorkspace.id);
const recentlyViewed = useRecentlyViewed();
const { t } = useTranslation();
const t = useAFFiNEI18N();
const { jumpToPage } = useRouterHelper(router);
const results = blockSuiteWorkspace.search(query);
@@ -61,7 +61,7 @@ export const Results: FC<ResultsProps> = ({
return (
<>
{recentlyViewedItem.length > 0 && (
<Command.Group heading={t('Recent')}>
<Command.Group heading={t['Recent']()}>
{recentlyViewedItem.map(recent => {
const page = pageList.find(page => recent.id === page.id);
assertExists(page);
@@ -87,7 +87,7 @@ export const Results: FC<ResultsProps> = ({
})}
</Command.Group>
)}
<Command.Group heading={t('Jump to')}>
<Command.Group heading={t['Jump to']()}>
{List.map(link => {
return (
<Command.Item
@@ -112,7 +112,7 @@ export const Results: FC<ResultsProps> = ({
if (!resultsPageMeta.length) {
return (
<StyledNotFound>
<span>{t('Find 0 result')}</span>
<span>{t['Find 0 result']()}</span>
<Image
src="/imgs/no-result.svg"
alt="no result"
@@ -124,7 +124,7 @@ export const Results: FC<ResultsProps> = ({
}
return (
<Command.Group
heading={t('Find results', { number: resultsPageMeta.length })}
heading={t['Find results']({ number: `${resultsPageMeta.length}` })}
>
{resultsPageMeta.map(result => {
return (

View File

@@ -1,4 +1,4 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteTemporarilyIcon,
FavoriteIcon,
@@ -16,26 +16,26 @@ export const useSwitchToConfig = (
href: string;
icon: FC<SVGProps<SVGSVGElement>>;
}[] => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return useMemo(
() => [
{
title: t('All pages'),
title: t['All pages'](),
href: pathGenerator.all(workspaceId),
icon: FolderIcon,
},
{
title: t('Favorites'),
title: t['Favorites'](),
href: pathGenerator.favorite(workspaceId),
icon: FavoriteIcon,
},
{
title: t('Workspace Settings'),
title: t['Workspace Settings'](),
href: pathGenerator.setting(workspaceId),
icon: SettingsIcon,
},
{
title: t('Trash'),
title: t['Trash'](),
href: pathGenerator.trash(workspaceId),
icon: DeleteTemporarilyIcon,
},

View File

@@ -1,6 +1,6 @@
import { Modal, ModalWrapper } from '@affine/component';
import { getEnvironment } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Command } from 'cmdk';
import type { NextRouter } from 'next/router';
import type React from 'react';
@@ -15,7 +15,6 @@ import {
import type { BlockSuiteWorkspace } from '../../../shared';
import { Footer } from './Footer';
import { NavigationPath } from './navigation-path';
import { PublishedResults } from './PublishedResults';
import { Results } from './Results';
import { SearchInput } from './SearchInput';
@@ -45,7 +44,7 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
router,
blockSuiteWorkspace,
}) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const inputRef = useRef<HTMLInputElement>(null);
const [loading, startTransition] = useTransition();
const [query, _setQuery] = useState('');
@@ -110,12 +109,12 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
overflow: 'hidden',
}}
>
<NavigationPath
{/* <NavigationPath
blockSuiteWorkspace={blockSuiteWorkspace}
onJumpToPage={() => {
setOpen(false);
}}
/>
/> */}
<Command
shouldFilter={false}
//Handle KeyboardEvent conflicts with blocksuite
@@ -145,10 +144,10 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
}}
placeholder={
isPublicWorkspace
? t('Quick search placeholder2', {
? t['Quick search placeholder2']({
workspace: publishWorkspaceName,
})
: t('Quick search placeholder')
: t['Quick search placeholder']()
}
/>
<StyledShortcut>{isMac() ? '⌘ + K' : 'Ctrl + K'}</StyledShortcut>

View File

@@ -1,5 +1,5 @@
import { IconButton, Tooltip, TreeView } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
ArrowRightSmallIcon,
CollapseIcon,
@@ -35,7 +35,7 @@ export const NavigationPath = ({
}) => {
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
const router = useRouter();
const { t } = useTranslation();
const t = useAFFiNEI18N();
const [openExtend, setOpenExtend] = useState(false);
const pageId = propsPageId ?? router.query.pageId;
@@ -59,7 +59,7 @@ export const NavigationPath = ({
<>
<StyledNavigationPathContainer data-testid="navigation-path">
{openExtend ? (
<span>{t('Navigation Path')}</span>
<span>{t['Navigation Path']()}</span>
) : (
pathData.path.map((meta, index) => {
const isLast = index === pathData.path.length - 1;
@@ -96,7 +96,9 @@ export const NavigationPath = ({
)}
<Tooltip
content={
openExtend ? t('Back to Quick Search') : t('View Navigation Path')
openExtend
? t['Back to Quick Search']()
: t['View Navigation Path']()
}
placement="top"
disablePortal={true}

View File

@@ -1,90 +1,91 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
interface ShortcutTip {
[x: string]: string;
}
export const useMacKeyboardShortcuts = (): ShortcutTip => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return {
[t('Undo')]: '⌘+Z',
[t('Redo')]: '⌘+⇧+Z',
[t('Bold')]: '⌘+B',
[t('Italic')]: '⌘+I',
[t('Underline')]: '⌘+U',
[t('Strikethrough')]: '⌘+⇧+S',
[t('Inline code')]: ' ⌘+E',
[t('Code block')]: '⌘+⌥+C',
[t('Link')]: '⌘+K',
[t('Quick search')]: '⌘+K',
[t('Body text')]: '⌘+⌥+0',
[t('Heading', { number: '1' })]: '⌘+⌥+1',
[t('Heading', { number: '2' })]: '⌘+⌥+2',
[t('Heading', { number: '3' })]: '⌘+⌥+3',
[t('Heading', { number: '4' })]: '⌘+⌥+4',
[t('Heading', { number: '5' })]: '⌘+⌥+5',
[t('Heading', { number: '6' })]: '⌘+⌥+6',
[t('Increase indent')]: 'Tab',
[t('Reduce indent')]: '⇧+Tab',
[t['Undo']()]: '⌘+Z',
[t['Redo']()]: '⌘+⇧+Z',
[t['Bold']()]: '⌘+B',
[t['Italic']()]: '⌘+I',
[t['Underline']()]: '⌘+U',
[t['Strikethrough']()]: '⌘+⇧+S',
[t['Inline code']()]: ' ⌘+E',
[t['Code block']()]: '⌘+⌥+C',
[t['Link']()]: '⌘+K',
[t['Quick search']()]: '⌘+K',
[t['Body text']()]: '⌘+⌥+0',
[t['Heading']({ number: '1' })]: '⌘+⌥+1',
[t['Heading']({ number: '2' })]: '⌘+⌥+2',
[t['Heading']({ number: '3' })]: '⌘+⌥+3',
[t['Heading']({ number: '4' })]: '⌘+⌥+4',
[t['Heading']({ number: '5' })]: '⌘+⌥+5',
[t['Heading']({ number: '6' })]: '⌘+⌥+6',
[t['Increase indent']()]: 'Tab',
[t['Reduce indent']()]: '⇧+Tab',
};
};
export const useMacMarkdownShortcuts = (): ShortcutTip => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return {
[t('Bold')]: '**Text** ',
[t('Italic')]: '*Text* ',
[t('Underline')]: '~Text~ ',
[t('Strikethrough')]: '~~Text~~ ',
[t('Divider')]: '***',
[t('Inline code')]: '`Text` ',
[t('Code block')]: '``` Space',
[t('Heading', { number: '1' })]: '# Text',
[t('Heading', { number: '2' })]: '## Text',
[t('Heading', { number: '3' })]: '### Text',
[t('Heading', { number: '4' })]: '#### Text',
[t('Heading', { number: '5' })]: '##### Text',
[t('Heading', { number: '6' })]: '###### Text',
[t['Bold']()]: '**Text** ',
[t['Italic']()]: '*Text* ',
[t['Underline']()]: '~Text~ ',
[t['Strikethrough']()]: '~~Text~~ ',
[t['Divider']()]: '***',
[t['Inline code']()]: '`Text` ',
[t['Code block']()]: '``` Space',
[t['Heading']({ number: '1' })]: '# Text',
[t['Heading']({ number: '2' })]: '## Text',
[t['Heading']({ number: '3' })]: '### Text',
[t['Heading']({ number: '4' })]: '#### Text',
[t['Heading']({ number: '5' })]: '##### Text',
[t['Heading']({ number: '6' })]: '###### Text',
};
};
export const useWindowsKeyboardShortcuts = (): ShortcutTip => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return {
[t('Undo')]: 'Ctrl+Z',
[t('Redo')]: 'Ctrl+Y',
[t('Bold')]: 'Ctrl+B',
[t('Italic')]: 'Ctrl+I',
[t('Underline')]: 'Ctrl+U',
[t('Strikethrough')]: 'Ctrl+Shift+S',
[t('Inline code')]: ' Ctrl+E',
[t('Code block')]: 'Ctrl+Alt+C',
[t('Link')]: 'Ctrl+K',
[t('Quick search')]: 'Ctrl+K',
[t('Body text')]: 'Ctrl+Shift+0',
[t('Heading', { number: '1' })]: 'Ctrl+Shift+1',
[t('Heading', { number: '2' })]: 'Ctrl+Shift+2',
[t('Heading', { number: '3' })]: 'Ctrl+Shift+3',
[t('Heading', { number: '4' })]: 'Ctrl+Shift+4',
[t('Heading', { number: '5' })]: 'Ctrl+Shift+5',
[t('Heading', { number: '6' })]: 'Ctrl+Shift+6',
[t('Increase indent')]: 'Tab',
[t('Reduce indent')]: 'Shift+Tab',
[t['Undo']()]: 'Ctrl+Z',
[t['Redo']()]: 'Ctrl+Y',
[t['Bold']()]: 'Ctrl+B',
[t['Italic']()]: 'Ctrl+I',
[t['Underline']()]: 'Ctrl+U',
[t['Strikethrough']()]: 'Ctrl+Shift+S',
[t['Inline code']()]: ' Ctrl+E',
[t['Code block']()]: 'Ctrl+Alt+C',
[t['Link']()]: 'Ctrl+K',
[t['Quick search']()]: 'Ctrl+K',
[t['Body text']()]: 'Ctrl+Shift+0',
[t['Heading']({ number: '1' })]: 'Ctrl+Shift+1',
[t['Heading']({ number: '2' })]: 'Ctrl+Shift+2',
[t['Heading']({ number: '3' })]: 'Ctrl+Shift+3',
[t['Heading']({ number: '4' })]: 'Ctrl+Shift+4',
[t['Heading']({ number: '5' })]: 'Ctrl+Shift+5',
[t['Heading']({ number: '6' })]: 'Ctrl+Shift+6',
[t['Increase indent']()]: 'Tab',
[t['Reduce indent']()]: 'Shift+Tab',
};
};
export const useWinMarkdownShortcuts = (): ShortcutTip => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return {
[t('Bold')]: '**Text** ',
[t('Italic')]: '*Text* ',
[t('Underline')]: '~Text~ ',
[t('Strikethrough')]: '~~Text~~ ',
[t('Divider')]: '***',
[t('Inline code')]: '`Text` ',
[t('Code block')]: '``` Text',
[t('Heading', { number: '1' })]: '# Text',
[t('Heading', { number: '2' })]: '## Text',
[t('Heading', { number: '3' })]: '### Text',
[t('Heading', { number: '4' })]: '#### Text',
[t('Heading', { number: '5' })]: '##### Text',
[t('Heading', { number: '6' })]: '###### Text',
[t['Bold']()]: '**Text** ',
[t['Italic']()]: '*Text* ',
[t['Underline']()]: '~Text~ ',
[t['Strikethrough']()]: '~~Text~~ ',
[t['Divider']()]: '***',
[t['Inline code']()]: '`Text` ',
[t['Code block']()]: '``` Text',
[t['Heading']({ number: '1' })]: '# Text',
[t['Heading']({ number: '2' })]: '## Text',
[t['Heading']({ number: '3' })]: '### Text',
[t['Heading']({ number: '4' })]: '#### Text',
[t['Heading']({ number: '5' })]: '##### Text',
[t['Heading']({ number: '6' })]: '###### Text',
};
};

View File

@@ -4,7 +4,7 @@ import {
MuiSlide,
} from '@affine/component';
import { getEnvironment } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useEffect, useState } from 'react';
import {
@@ -32,7 +32,7 @@ const checkIsMac = () => {
};
export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const macMarkdownShortcuts = useMacMarkdownShortcuts();
const winMarkdownShortcuts = useWinMarkdownShortcuts();
const macKeyboardShortcuts = useMacKeyboardShortcuts();
@@ -59,7 +59,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
<StyledModalHeader>
<StyledTitle>
<KeyboardIcon />
{t('Shortcuts')}
{t['Shortcuts']()}
</StyledTitle>
<ModalCloseButton
@@ -73,7 +73,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
/>
</StyledModalHeader>
<StyledSubTitle style={{ marginTop: 0 }}>
{t('Keyboard Shortcuts')}
{t['Keyboard Shortcuts']()}
</StyledSubTitle>
{Object.entries(keyboardShortcuts).map(([title, shortcuts]) => {
return (
@@ -83,7 +83,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
</StyledListItem>
);
})}
<StyledSubTitle>{t('Markdown Syntax')}</StyledSubTitle>
<StyledSubTitle>{t['Markdown Syntax']()}</StyledSubTitle>
{Object.entries(markdownShortcuts).map(([title, shortcuts]) => {
return (
<StyledListItem key={title}>

View File

@@ -5,7 +5,7 @@ import {
Tooltip,
} from '@affine/component';
import { WorkspaceList } from '@affine/component/workspace-list';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
@@ -57,7 +57,7 @@ export const WorkspaceListModal = ({
currentWorkspaceId,
onMoveWorkspace,
}: WorkspaceModalProps) => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<Modal open={open} onClose={onClose}>
@@ -71,9 +71,9 @@ export const WorkspaceListModal = ({
>
<StyledModalHeader>
<StyledModalHeaderLeft>
<StyledModalTitle>{t('My Workspaces')}</StyledModalTitle>
<StyledModalTitle>{t['My Workspaces']()}</StyledModalTitle>
<Tooltip
content={t('Workspace description')}
content={t['Workspace description']()}
placement="top-start"
disablePortal={true}
>
@@ -124,8 +124,8 @@ export const WorkspaceListModal = ({
</StyleWorkspaceAdd>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>{t('New Workspace')}</StyleWorkspaceTitle>
<p>{t('Create Or Import')}</p>
<StyleWorkspaceTitle>{t['New Workspace']()}</StyleWorkspaceTitle>
<p>{t['Create Or Import']()}</p>
</StyleWorkspaceInfo>
</StyledCreateWorkspaceCard>
</StyledModalContent>

View File

@@ -47,7 +47,7 @@ export const StyledCreateWorkspaceCard = styled('div')(() => {
height: '124px',
cursor: 'pointer',
padding: '16px',
boxShadow: 'var(--affine-shadow)',
boxShadow: 'var(--affine-shadow-1)',
borderRadius: '12px',
transition: 'all .1s',
background: 'var(--affine-white-80)',

View File

@@ -1,30 +1,15 @@
import ChangeLogComponent from '@affine/component/changeLog';
import { useAtom } from 'jotai';
import { useCallback } from 'react';
import {
useGuideHidden,
useGuideHiddenUntilNextUpdate,
} from '../../../../hooks/use-is-first-load';
import { guideChangeLogAtom } from '../../../../atoms/guide';
export const ChangeLog = () => {
const [guideHidden, setGuideHidden] = useGuideHidden();
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
useGuideHiddenUntilNextUpdate();
const [showChangeLogTips, setShowChangeLogTips] = useAtom(guideChangeLogAtom);
const onCloseWhatsNew = useCallback(() => {
setTimeout(() => {
setGuideHiddenUntilNextUpdate({
...guideHiddenUntilNextUpdate,
changeLog: true,
});
setGuideHidden({ ...guideHidden, changeLog: true });
}, 300);
}, [
guideHidden,
guideHiddenUntilNextUpdate,
setGuideHidden,
setGuideHiddenUntilNextUpdate,
]);
if (guideHiddenUntilNextUpdate.changeLog) {
setShowChangeLogTips(false);
}, [setShowChangeLogTips]);
if (!showChangeLogTips) {
return <></>;
}
return <ChangeLogComponent onCloseWhatsNew={onCloseWhatsNew} />;

View File

@@ -1,12 +1,12 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { StyledCollapseItem } from '../shared-styles';
export const EmptyItem = () => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<StyledCollapseItem disable={true} textWrap={true}>
{t('Favorite pages for easy access')}
{t['Favorite pages for easy access']()}
</StyledCollapseItem>
);
};

View File

@@ -1,4 +1,4 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowDownSmallIcon, FavoriteIcon } from '@blocksuite/icons';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useCallback, useState } from 'react';
@@ -26,7 +26,7 @@ export const Favorite = ({
const [showSubFavorite, setOpenSubFavorite] = useState(true);
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<>
@@ -50,7 +50,7 @@ export const Favorite = ({
}}
>
<FavoriteIcon />
{t('Favorites')}
{t['Favorites']()}
</StyledLink>
</StyledListItem>
<FavoriteList

View File

@@ -9,7 +9,7 @@ export const StyledListItem = styled('div')<{
color: active
? 'var(--affine-primary-color)'
: 'var(--affine-text-primary-color)',
paddingLeft: '16px',
paddingLeft: '2px',
paddingRight: '2px',
borderRadius: '8px',
cursor: 'pointer',
@@ -17,7 +17,7 @@ export const StyledListItem = styled('div')<{
position: 'relative',
flexShrink: 0,
userSelect: 'none',
...displayFlex('flex-start', 'center'),
...displayFlex('flex-start', 'stretch'),
...(disabled
? {
cursor: 'not-allowed',
@@ -25,8 +25,9 @@ export const StyledListItem = styled('div')<{
}
: {}),
'> svg, a > svg': {
'a > svg, div > svg': {
fontSize: '20px',
marginLeft: '14px',
marginRight: '12px',
color: active
? 'var(--affine-primary-color)'

View File

@@ -4,7 +4,7 @@ import {
ResizeIndicator,
} from '@affine/component/app-sidebar';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import {
DeleteTemporarilyIcon,
@@ -17,7 +17,7 @@ import {
import type { Page } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import type { ReactElement, UIEvent } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import type { AllWorkspace } from '../../shared';
import ChangeLog from '../pure/workspace-slider-bar/changeLog';
@@ -67,7 +67,7 @@ export const RootAppSidebar = ({
}: RootAppSidebarProps): ReactElement => {
const currentWorkspaceId = currentWorkspace?.id || null;
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const { t } = useTranslation();
const t = useAFFiNEI18N();
const [isScrollAtTop, setIsScrollAtTop] = useState(true);
const onClickNewPage = useCallback(async () => {
const page = await createPage();
@@ -79,17 +79,17 @@ export const RootAppSidebar = ({
window.apis?.onSidebarVisibilityChange(sidebarOpen);
}
}, [sidebarOpen]);
const ref = useRef<HTMLElement>(null);
const [ref, setRef] = useState<HTMLElement | null>(null);
return (
<>
<AppSidebar
ref={ref}
ref={setRef}
footer={
<StyledNewPageButton
data-testid="new-page-button"
onClick={onClickNewPage}
>
<PlusIcon /> {t('New Page')}
<PlusIcon /> {t['New Page']()}
</StyledNewPageButton>
}
>
@@ -105,8 +105,17 @@ export const RootAppSidebar = ({
onOpenQuickSearchModal();
}, [onOpenQuickSearchModal])}
>
<SearchIcon />
{t('Quick search')}
<div
style={{
display: 'flex',
flex: 1,
alignItems: 'center',
justifyContent: 'flex-start',
}}
>
<SearchIcon />
{t['Quick search']()}
</div>
</StyledListItem>
<StyledListItem
active={
@@ -125,7 +134,7 @@ export const RootAppSidebar = ({
}}
>
<SettingsIcon />
<div>{t('Workspace Settings')}</div>
<div>{t['Workspace Settings']()}</div>
</StyledLink>
</StyledListItem>
<StyledListItem
@@ -140,7 +149,7 @@ export const RootAppSidebar = ({
}}
>
<FolderIcon />
<span data-testid="all-pages">{t('All pages')}</span>
<span data-testid="all-pages">{t['All pages']()}</span>
</StyledLink>
</StyledListItem>
<StyledScrollWrapper
@@ -190,7 +199,7 @@ export const RootAppSidebar = ({
}}
>
<ShareIcon />
<span data-testid="shared-pages">{t('Shared Pages')}</span>
<span data-testid="shared-pages">{t['Shared Pages']()}</span>
</StyledLink>
</StyledListItem>
))}
@@ -205,7 +214,7 @@ export const RootAppSidebar = ({
pathname: currentWorkspaceId && paths.trash(currentWorkspaceId),
}}
>
<DeleteTemporarilyIcon /> {t('Trash')}
<DeleteTemporarilyIcon /> {t['Trash']()}
</StyledLink>
</StyledListItem>
</StyledSliderBarInnerWrapper>

View File

@@ -34,12 +34,6 @@ import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../current/use-current-workspace';
import {
useGuideHidden,
useGuideHiddenUntilNextUpdate,
useLastVersion,
useTipsDisplayStatus,
} from '../use-is-first-load';
import {
useRecentlyViewed,
useSyncRecentViewsWithRouter,
@@ -98,9 +92,13 @@ beforeEach(async () => {
const frameId = page.addBlock('affine:frame', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId);
};
initPage(blockSuiteWorkspace.createPage('page0'));
initPage(blockSuiteWorkspace.createPage('page1'));
initPage(blockSuiteWorkspace.createPage('page2'));
initPage(
blockSuiteWorkspace.createPage({
id: 'page0',
})
);
initPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
});
describe('usePageMetas', async () => {
@@ -278,47 +276,3 @@ describe('useRecentlyViewed', () => {
]);
});
});
describe('useIsFirstLoad', () => {
test('useLastVersion', async () => {
const lastVersion = renderHook(() => useLastVersion());
const setLastVersion = lastVersion.result.current[1];
expect(lastVersion.result.current[0]).toEqual('0.0.0');
setLastVersion('testVersion');
lastVersion.rerender();
expect(lastVersion.result.current[0]).toEqual('testVersion');
});
test('useGuideHidden', async () => {
const guideHidden = renderHook(() => useGuideHidden());
const setGuideHidden = guideHidden.result.current[1];
expect(guideHidden.result.current[0]).toEqual({});
setGuideHidden({ test: true });
guideHidden.rerender();
expect(guideHidden.result.current[0]).toEqual({ test: true });
});
test('useGuideHiddenUntilNextUpdate', async () => {
const guideHiddenUntilNextUpdate = renderHook(() =>
useGuideHiddenUntilNextUpdate()
);
const setGuideHiddenUntilNextUpdate =
guideHiddenUntilNextUpdate.result.current[1];
expect(guideHiddenUntilNextUpdate.result.current[0]).toEqual({});
setGuideHiddenUntilNextUpdate({ test: true });
guideHiddenUntilNextUpdate.rerender();
expect(guideHiddenUntilNextUpdate.result.current[0]).toEqual({
test: true,
});
});
test('useTipsDisplayStatus', async () => {
const tipsDisplayStatus = renderHook(() => useTipsDisplayStatus());
expect(tipsDisplayStatus.result.current).toEqual({
quickSearchTips: {
permanentlyHidden: true,
hiddenUntilNextUpdate: true,
},
changeLog: {
permanentlyHidden: true,
hiddenUntilNextUpdate: true,
},
});
});
});

View File

@@ -15,6 +15,32 @@ export function useBlockSuiteMetaHelper(
useReferenceLinkHelper(blockSuiteWorkspace);
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
const addToFavorite = useCallback(
(pageId: string) => {
setPageMeta(pageId, {
favorite: true,
});
},
[setPageMeta]
);
const removeFromFavorite = useCallback(
(pageId: string) => {
setPageMeta(pageId, {
favorite: false,
});
},
[setPageMeta]
);
const toggleFavorite = useCallback(
(pageId: string) => {
const { favorite } = getPageMeta(pageId) ?? {};
setPageMeta(pageId, {
favorite: !favorite,
});
},
[getPageMeta, setPageMeta]
);
const removeToTrash = useCallback(
(pageId: string, isRoot = true) => {
const parentMeta = metas.find(m => m.subpageIds?.includes(pageId));
@@ -58,8 +84,47 @@ export function useBlockSuiteMetaHelper(
[addReferenceLink, getPageMeta, setPageMeta]
);
const permanentlyDeletePage = useCallback(
(pageId: string) => {
blockSuiteWorkspace.removePage(pageId);
},
[blockSuiteWorkspace]
);
/**
* see {@link useBlockSuiteWorkspacePageIsPublic}
*/
const publicPage = useCallback(
(pageId: string) => {
setPageMeta(pageId, {
isPublic: true,
});
},
[setPageMeta]
);
/**
* see {@link useBlockSuiteWorkspacePageIsPublic}
*/
const cancelPublicPage = useCallback(
(pageId: string) => {
setPageMeta(pageId, {
isPublic: false,
});
},
[setPageMeta]
);
return {
publicPage,
cancelPublicPage,
addToFavorite,
removeFromFavorite,
toggleFavorite,
removeToTrash,
restoreFromTrash,
permanentlyDeletePage,
};
}

View File

@@ -1,74 +0,0 @@
import { config } from '@affine/env';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useEffect } from 'react';
import {
guideHiddenAtom,
guideHiddenUntilNextUpdateAtom,
lastVersionAtom,
} from '../atoms/first-load';
export function useLastVersion() {
return useAtom(lastVersionAtom);
}
export function useGuideHidden() {
return useAtom(guideHiddenAtom);
}
export function useGuideHiddenUntilNextUpdate() {
return useAtom(guideHiddenUntilNextUpdateAtom);
}
const TIPS = {
quickSearchTips: true,
changeLog: true,
};
export function useTipsDisplayStatus() {
const permanentlyHiddenTips = useAtomValue(guideHiddenAtom);
const hiddenUntilNextUpdateTips = useAtomValue(
guideHiddenUntilNextUpdateAtom
);
return {
quickSearchTips: {
permanentlyHidden: permanentlyHiddenTips.quickSearchTips || true,
hiddenUntilNextUpdate: hiddenUntilNextUpdateTips.quickSearchTips || true,
},
changeLog: {
permanentlyHidden: permanentlyHiddenTips.changeLog || true,
hiddenUntilNextUpdate: hiddenUntilNextUpdateTips.changeLog || true,
},
};
}
export function useUpdateTipsOnVersionChange() {
const [lastVersion, setLastVersion] = useLastVersion();
const currentVersion = config.gitVersion;
const tipsDisplayStatus = useTipsDisplayStatus();
const setPermanentlyHiddenTips = useSetAtom(guideHiddenAtom);
const setHiddenUntilNextUpdateTips = useSetAtom(
guideHiddenUntilNextUpdateAtom
);
useEffect(() => {
if (lastVersion !== currentVersion) {
setLastVersion(currentVersion);
const newHiddenUntilNextUpdateTips = { ...TIPS };
const newPermanentlyHiddenTips = { ...TIPS, changeLog: false };
Object.keys(tipsDisplayStatus).forEach(tipKey => {
newHiddenUntilNextUpdateTips[tipKey as keyof typeof TIPS] = false;
});
setHiddenUntilNextUpdateTips(newHiddenUntilNextUpdateTips);
setPermanentlyHiddenTips(newPermanentlyHiddenTips);
}
}, [
currentVersion,
lastVersion,
setLastVersion,
setPermanentlyHiddenTips,
setHiddenUntilNextUpdateTips,
tipsDisplayStatus,
]);
}

View File

@@ -50,6 +50,8 @@ function flattenToTree(
[]
);
};
// Unreachable code, we have removed the root pinboard
// @ts-expect-error
return helper(rootMeta ? [{ ...rootMeta, renderTopLine: false }] : []);
}

View File

@@ -22,6 +22,7 @@ export function useRouterWithWorkspaceIdDefense(router: NextRouter) {
}
const exist = metadata.find(m => m.id === currentWorkspaceId);
if (!exist) {
console.warn('workspace not exist, redirect to first one');
// clean up
setCurrentWorkspaceId(null);
setCurrentPageId(null);

View File

@@ -13,6 +13,9 @@ export function useSyncRouterWithCurrentPageId(router: NextRouter) {
if (typeof pageId === 'string') {
console.log('set page id', pageId);
setCurrentPageId(pageId);
} else if (pageId === undefined) {
console.log('cleanup page');
setCurrentPageId(null);
}
}, [router.isReady, router.query.pageId, setCurrentPageId]);
}

View File

@@ -45,8 +45,9 @@ export function useSyncRouterWithCurrentWorkspaceId(router: NextRouter) {
window.apis?.onWorkspaceChange(targetWorkspace.id);
}
void router.push({
pathname: '/workspace/[workspaceId]/all',
pathname: router.pathname,
query: {
...router.query,
workspaceId: targetWorkspace.id,
},
});
@@ -56,8 +57,9 @@ export function useSyncRouterWithCurrentWorkspaceId(router: NextRouter) {
console.log('set workspace id', workspaceId);
setCurrentWorkspaceId(targetWorkspace.id);
void router.push({
pathname: '/workspace/[workspaceId]/all',
pathname: router.pathname,
query: {
...router.query,
workspaceId: targetWorkspace.id,
},
});

View File

@@ -32,7 +32,7 @@ export function useAppHelper() {
ws => ws.id === workspaceId
) as LocalWorkspace;
if (workspace && 'blockSuiteWorkspace' in workspace) {
workspace.blockSuiteWorkspace.createPage(pageId);
workspace.blockSuiteWorkspace.createPage({ id: pageId });
} else {
throw new Error('cannot create page. blockSuiteWorkspace not found');
}

View File

@@ -1,7 +1,8 @@
import { DebugLogger } from '@affine/debug';
import { DEFAULT_HELLO_WORLD_PAGE_ID } from '@affine/env';
import { ensureRootPinboard, initPage } from '@affine/env/blocksuite';
import { setUpLanguage, useTranslation } from '@affine/i18n';
import { initPage } from '@affine/env/blocksuite';
import { setUpLanguage, useI18N } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { createAffineGlobalChannel } from '@affine/workspace/affine/sync';
import {
rootCurrentPageIdAtom,
@@ -9,7 +10,7 @@ import {
rootStore,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
import type { BackgroundProvider } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
@@ -17,14 +18,7 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head';
import { useRouter } from 'next/router';
import type { FC, PropsWithChildren, ReactElement } from 'react';
import {
lazy,
Suspense,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
import {
@@ -127,7 +121,10 @@ export const AllWorkspaceContext = ({
// ignore current workspace
.filter(workspace => workspace.id !== currentWorkspaceId)
.flatMap(workspace =>
workspace.providers.filter(provider => provider.background)
workspace.providers.filter(
(provider): provider is BackgroundProvider =>
'background' in provider && provider.background
)
);
providers.forEach(provider => {
provider.connect();
@@ -151,22 +148,23 @@ export const CurrentWorkspaceContext = ({
useRouterWithWorkspaceIdDefense(router);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
const { t } = useTranslation();
const t = useAFFiNEI18N();
if (!router.isReady) {
return <PageLoading text={t('Router is Loading')} />;
return <PageLoading text={t['Router is Loading']()} />;
}
if (!workspaceId) {
return <PageLoading text={t('Finding Workspace ID')} />;
return <PageLoading text={t['Finding Workspace ID']()} />;
}
if (!exist) {
return <PageLoading text={t('Workspace Not Found')} />;
return <PageLoading text={t['Workspace Not Found']()} />;
}
return <>{children}</>;
};
export const WorkspaceLayout: FC<PropsWithChildren> =
function WorkspacesSuspense({ children }) {
const { i18n, t } = useTranslation();
const i18n = useI18N();
const t = useAFFiNEI18N();
useEffect(() => {
document.documentElement.lang = i18n.language;
// todo(himself65): this is a hack, we should use a better way to set the language
@@ -243,7 +241,7 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
</AllWorkspaceContext>
<CurrentWorkspaceContext>
<Suspense
fallback={<PageLoading text={t('Finding Current Workspace')} />}
fallback={<PageLoading text={t['Finding Current Workspace']()} />}
>
<Provider>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
@@ -260,69 +258,50 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [isLoading, setIsLoading] = useState(true);
const { t } = useTranslation();
const t = useAFFiNEI18N();
useEffect(() => {
logger.info('currentWorkspace: ', currentWorkspace);
globalThis.currentWorkspace = currentWorkspace;
}, [currentWorkspace]);
useEffect(() => {
if (currentWorkspace) {
globalThis.currentWorkspace = currentWorkspace;
//#region init workspace
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
// this is a new workspace, so we should redirect to the new page
const pageId = nanoid();
const page = currentWorkspace.blockSuiteWorkspace.createPage({
id: pageId,
});
assertEquals(page.id, pageId);
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
initPage(page);
if (!router.query.pageId) {
setCurrentPageId(pageId);
void jumpToPage(currentWorkspace.id, pageId);
}
}, [currentWorkspace]);
}
// fixme: pinboard has been removed,
// the related code should be removed in the future.
// no matter the workspace is empty, ensure the root pinboard exists
// ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
//#endregion
useEffect(() => {
if (currentWorkspace) {
currentWorkspace.providers.forEach(provider => {
provider.connect();
});
return () => {
currentWorkspace.providers.forEach(provider => {
provider.disconnect();
});
};
}
}, [currentWorkspace]);
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspace) {
return;
}
const localProvider = currentWorkspace.providers.find(
provider => provider.flavour === 'local-indexeddb'
const backgroundProviders = currentWorkspace.providers.filter(
(provider): provider is BackgroundProvider => 'background' in provider
);
if (localProvider && localProvider.flavour === 'local-indexeddb') {
const provider = localProvider as LocalIndexedDBProvider;
const callback = () => {
setIsLoading(false);
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
// this is a new workspace, so we should redirect to the new page
const pageId = nanoid();
const page = currentWorkspace.blockSuiteWorkspace.createPage(pageId);
assertEquals(page.id, pageId);
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
initPage(page);
if (!router.query.pageId) {
setCurrentPageId(pageId);
void jumpToPage(currentWorkspace.id, pageId);
}
}
// no matter the workspace is empty, ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
};
provider.callbacks.add(callback);
return () => {
provider.callbacks.delete(callback);
};
}
}, [currentWorkspace, jumpToPage, router, setCurrentPageId]);
backgroundProviders.forEach(provider => {
provider.connect();
});
return () => {
backgroundProviders.forEach(provider => {
provider.disconnect();
});
};
}, [currentWorkspace]);
useEffect(() => {
if (!currentWorkspace) {
@@ -394,12 +373,8 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
/>
<MainContainerWrapper>
<MainContainer className="main-container">
<Suspense fallback={<PageLoading text={t('Page is Loading')} />}>
{isLoading ? (
<PageLoading text={t('Page is Loading')} />
) : (
children
)}
<Suspense fallback={<PageLoading text={t['Page is Loading']()} />}>
{children}
</Suspense>
<StyledToolWrapper>
{/* fixme(himself65): remove this */}

View File

@@ -1,5 +1,5 @@
import { Button, displayFlex, styled } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import Head from 'next/head';
import Image from 'next/legacy/image';
import { useRouter } from 'next/router';
@@ -24,20 +24,20 @@ export const StyledContainer = styled('div')(() => {
});
export const NotfoundPage = () => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
const router = useRouter();
return (
<StyledContainer data-testid="notFound">
<Image alt="404" src="/imgs/invite-error.svg" width={360} height={270} />
<p>{t('404 - Page Not Found')}</p>
<p>{t['404 - Page Not Found']()}</p>
<Button
shape="round"
onClick={() => {
router.push('/');
}}
>
{t('Back Home')}
{t['Back Home']()}
</Button>
</StyledContainer>
);

View File

@@ -9,7 +9,7 @@ import { Typography } from '@mui/material';
import type React from 'react';
import { useEffect, useMemo, useState } from 'react';
import PageList from '../../components/blocksuite/block-suite-page-list/page-list';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { StyledPage, StyledWrapper } from '../../layouts/styles';
import { toast } from '../../utils';
@@ -52,14 +52,15 @@ const BroadcastPage: React.FC = () => {
data-testid="create-page"
onClick={() => {
logger.info('create page');
blockSuiteWorkspace.createPage(nanoid());
blockSuiteWorkspace.createPage({ id: nanoid() });
}}
>
Create Page
</Button>
<PageList
<BlockSuitePageList
blockSuiteWorkspace={blockSuiteWorkspace}
onClickPage={() => {
listType="all"
onOpenPage={() => {
toast('do nothing');
}}
/>

View File

@@ -6,6 +6,9 @@ import * as React from 'react';
import createEmotionCache from '../utils/create-emotion-cache';
const description =
'There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together.';
export default class AppDocument extends Document<{
emotionStyleTags: EmotionJSX.Element[];
}> {
@@ -51,16 +54,14 @@ export default class AppDocument extends Document<{
/>
<link rel="icon" sizes="192x192" href="/chrome-192x192.png" />
<meta name="emotion-insertion-point" content="" />
<meta property="description" content={description} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://app.affine.pro/" />
<meta
name="twitter:title"
content="AFFiNEThere can be more than Notion and Miro."
/>
<meta
name="twitter:description"
content="There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together."
/>
<meta name="twitter:description" content={description} />
<meta name="twitter:site" content="@AffineOfficial" />
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
<meta

View File

@@ -1,5 +1,5 @@
import { DebugLogger } from '@affine/debug';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { NextPage } from 'next';
import { useRouter } from 'next/router';
import { Suspense, useEffect } from 'react';
@@ -63,9 +63,9 @@ const IndexPageInner = () => {
};
const IndexPage: NextPage = () => {
const { t } = useTranslation();
const t = useAFFiNEI18N();
return (
<Suspense fallback={<PageLoading text={t('Loading All Workspaces')} />}>
<Suspense fallback={<PageLoading text={t['Loading All Workspaces']()} />}>
<IndexPageInner />
</Suspense>
);

View File

@@ -1,4 +1,5 @@
import { Breadcrumbs, IconButton, ListSkeleton } from '@affine/component';
import { StyledTableContainer } from '@affine/component/page-list';
import { SearchIcon } from '@blocksuite/icons';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
@@ -13,7 +14,6 @@ import {
publicWorkspaceIdAtom,
} from '../../atoms/public-workspace';
import { QueryParamError } from '../../components/affine/affine-error-eoundary';
import { StyledTableContainer } from '../../components/blocksuite/block-suite-page-list/page-list/styles';
import { WorkspaceAvatar } from '../../components/pure/footer';
import { PageLoading } from '../../components/pure/loading';
import {
@@ -23,9 +23,9 @@ import {
import type { NextPageWithLayout } from '../../shared';
import { NavContainer, StyledBreadcrumbs } from './[workspaceId]/[pageId]';
const BlockSuitePublicPageList = lazy(() =>
const BlockSuitePageList = lazy(() =>
import('../../components/blocksuite/block-suite-page-list').then(module => ({
default: module.BlockSuitePublicPageList,
default: module.BlockSuitePageList,
}))
);
@@ -79,7 +79,9 @@ const ListPageInner: React.FC<{
</StyledTableContainer>
}
>
<BlockSuitePublicPageList
<BlockSuitePageList
listType="public"
isPublic={true}
onOpenPage={handleClickPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>

View File

@@ -1,6 +1,6 @@
import { Breadcrumbs, displayFlex, styled } from '@affine/component';
import { initPage } from '@affine/env/blocksuite';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { PageIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
@@ -72,7 +72,7 @@ const PublicWorkspaceDetailPageInner = (): ReactElement => {
[blockSuiteWorkspace.id, openPage]
),
});
const { t } = useTranslation();
const t = useAFFiNEI18N();
const [name] = useBlockSuiteWorkspaceName(blockSuiteWorkspace);
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(blockSuiteWorkspace);
const pageTitle = blockSuiteWorkspace.meta.getPageMeta(pageId)?.title;
@@ -101,7 +101,7 @@ const PublicWorkspaceDetailPageInner = (): ReactElement => {
href={`/public-workspace/${blockSuiteWorkspace.id}/${pageId}`}
>
<PageIcon fontSize={24} />
<span>{pageTitle ? pageTitle : t('Untitled')}</span>
<span>{pageTitle ? pageTitle : t['Untitled']()}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
</NavContainer>

View File

@@ -1,6 +1,6 @@
import type { BlockSuiteFeatureFlags } from '@affine/env';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
@@ -41,7 +41,7 @@ const WorkspaceDetail: React.FC = () => {
const { openPage } = useRouterHelper(router);
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
const t = useAFFiNEI18N();
assertExists(currentWorkspace);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
@@ -86,7 +86,7 @@ const WorkspaceDetail: React.FC = () => {
}
}, [currentWorkspace]);
if (!currentPageId) {
return <PageLoading text={t('Loading Page')} />;
return <PageLoading text={t['Loading Page']()} />;
}
if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].UI.PageDetail;
@@ -112,16 +112,16 @@ const WorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter();
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const { t } = useTranslation();
const t = useAFFiNEI18N();
useRouterAndWorkspaceWithPageIdDefense(router);
const page = useBlockSuiteWorkspacePage(
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
if (!router.isReady) {
return <PageLoading text={t('Router is Loading')} />;
return <PageLoading text={t['Router is Loading']()} />;
} else if (!currentPageId || !page) {
return <PageLoading text={t('Page is Loading')} />;
return <PageLoading text={t['Page is Loading']()} />;
}
return <WorkspaceDetail />;
};

View File

@@ -1,4 +1,4 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { FolderIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
@@ -23,7 +23,7 @@ const AllPage: NextPageWithLayout = () => {
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
const t = useAFFiNEI18N();
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
@@ -47,7 +47,7 @@ const AllPage: NextPageWithLayout = () => {
return (
<>
<Head>
<title>{t('All Pages')} - AFFiNE</title>
<title>{t['All pages']()} - AFFiNE</title>
</Head>
<WorkspaceTitle
workspace={currentWorkspace}
@@ -56,7 +56,7 @@ const AllPage: NextPageWithLayout = () => {
isPublic={false}
icon={<FolderIcon />}
>
{t('All pages')}
{t['All pages']()}
</WorkspaceTitle>
<PageList
onOpenPage={onClickPage}
@@ -69,7 +69,7 @@ const AllPage: NextPageWithLayout = () => {
return (
<>
<Head>
<title>{t('All Pages')} - AFFiNE</title>
<title>{t['All pages']()} - AFFiNE</title>
</Head>
<WorkspaceTitle
workspace={currentWorkspace}
@@ -78,7 +78,7 @@ const AllPage: NextPageWithLayout = () => {
isPublic={false}
icon={<FolderIcon />}
>
{t('All pages')}
{t['All pages']()}
</WorkspaceTitle>
<PageList
onOpenPage={onClickPage}

View File

@@ -1,11 +1,11 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { FavoriteIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import PageList from '../../../components/blocksuite/block-suite-page-list/page-list';
import { BlockSuitePageList } from '../../../components/blocksuite/block-suite-page-list';
import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
@@ -18,7 +18,7 @@ const FavouritePage: NextPageWithLayout = () => {
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
const t = useAFFiNEI18N();
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
@@ -39,7 +39,7 @@ const FavouritePage: NextPageWithLayout = () => {
return (
<>
<Head>
<title>{t('Favorites')} - AFFiNE</title>
<title>{t['Favorites']()} - AFFiNE</title>
</Head>
<WorkspaceTitle
workspace={currentWorkspace}
@@ -48,11 +48,11 @@ const FavouritePage: NextPageWithLayout = () => {
isPublic={false}
icon={<FavoriteIcon />}
>
{t('Favorites')}
{t['Favorites']()}
</WorkspaceTitle>
<PageList
<BlockSuitePageList
blockSuiteWorkspace={blockSuiteWorkspace}
onClickPage={onClickPage}
onOpenPage={onClickPage}
listType="favorite"
/>
</>

View File

@@ -1,5 +1,6 @@
import { useTranslation } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { atomWithSyncStorage } from '@affine/jotai';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { SettingPanel } from '@affine/workspace/type';
import {
settingPanel,
@@ -8,7 +9,7 @@ import {
} from '@affine/workspace/type';
import { SettingsIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useAtom } from 'jotai';
import { useAtom, useAtomValue } from 'jotai';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect } from 'react';
@@ -23,6 +24,7 @@ import { useAppHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared';
import { toast } from '../../../utils';
const settingPanelAtom = atomWithSyncStorage<SettingPanel>(
'workspaceId',
@@ -31,8 +33,9 @@ const settingPanelAtom = atomWithSyncStorage<SettingPanel>(
const SettingPage: NextPageWithLayout = () => {
const router = useRouter();
const workspaceIds = useAtomValue(rootWorkspacesMetadataAtom);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
const t = useAFFiNEI18N();
useSyncRouterWithCurrentWorkspaceId(router);
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
useEffect(() => {});
@@ -97,8 +100,12 @@ const SettingPage: NextPageWithLayout = () => {
const onDeleteWorkspace = useCallback(() => {
assertExists(currentWorkspace);
const workspaceId = currentWorkspace.id;
if (workspaceIds.length === 1 && workspaceId === workspaceIds[0].id) {
toast(t['You cannot delete the last workspace']());
throw new Error('You cannot delete the last workspace');
}
return helper.deleteWorkspace(workspaceId);
}, [currentWorkspace, helper]);
}, [currentWorkspace, helper, t, workspaceIds]);
const onTransformWorkspace = useOnTransformWorkspace();
if (!router.isReady) {
return <PageLoading />;
@@ -112,7 +119,7 @@ const SettingPage: NextPageWithLayout = () => {
return (
<>
<Head>
<title>{t('Settings')} - AFFiNE</title>
<title>{t['Settings']()} - AFFiNE</title>
</Head>
<WorkspaceTitle
workspace={currentWorkspace}
@@ -121,7 +128,7 @@ const SettingPage: NextPageWithLayout = () => {
isPublic={false}
icon={<SettingsIcon />}
>
{t('Workspace Settings')}
{t['Workspace Settings']()}
</WorkspaceTitle>
<Setting
onTransformWorkspace={onTransformWorkspace}
@@ -138,7 +145,7 @@ const SettingPage: NextPageWithLayout = () => {
return (
<>
<Head>
<title>{t('Settings')} - AFFiNE</title>
<title>{t['Settings']()} - AFFiNE</title>
</Head>
<WorkspaceTitle
workspace={currentWorkspace}
@@ -147,7 +154,7 @@ const SettingPage: NextPageWithLayout = () => {
isPublic={false}
icon={<SettingsIcon />}
>
{t('Workspace Settings')}
{t['Workspace Settings']()}
</WorkspaceTitle>
<Setting
onTransformWorkspace={onTransformWorkspace}

Some files were not shown because too many files have changed in this diff Show More