Compare commits
73 Commits
v0.7.0-can
...
v0.7.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b509302711 | ||
|
|
e51c98c1dd | ||
|
|
bbb1387469 | ||
|
|
4f88774999 | ||
|
|
3968deb6d4 | ||
|
|
37c8465af8 | ||
|
|
d88a21d24a | ||
|
|
6ad2d106bc | ||
|
|
8c1fcee135 | ||
|
|
0514da9759 | ||
|
|
b2fed03f30 | ||
|
|
f5e45573af | ||
|
|
ddb2931f38 | ||
|
|
acf17ebace | ||
|
|
7af3c05b8b | ||
|
|
01de2ae714 | ||
|
|
cfa18d1bc3 | ||
|
|
127c63601e | ||
|
|
f079b0b49a | ||
|
|
6caf934d47 | ||
|
|
2f910fbad0 | ||
|
|
dac4e390aa | ||
|
|
812e0e9c9a | ||
|
|
05291a8a36 | ||
|
|
8bcc4d6a57 | ||
|
|
e06d5e1c8d | ||
|
|
1c8895f23f | ||
|
|
8b5d997322 | ||
|
|
33644a68b2 | ||
|
|
bc85ad5b65 | ||
|
|
fe895905bd | ||
|
|
3c5ccd7231 | ||
|
|
da140b0b85 | ||
|
|
c4d53d59b5 | ||
|
|
a48726d088 | ||
|
|
b49306607b | ||
|
|
3d15c60cb1 | ||
|
|
283f0cd263 | ||
|
|
66152401be | ||
|
|
b12412a3c1 | ||
|
|
ce1e8d868c | ||
|
|
3294043180 | ||
|
|
152fbaabda | ||
|
|
5756bdf8d7 | ||
|
|
80ee33fd3e | ||
|
|
955d80e2c1 | ||
|
|
67fe7f04da | ||
|
|
6395521f09 | ||
|
|
822078e640 | ||
|
|
fafd93f7dc | ||
|
|
00ce086e79 | ||
|
|
28653d6892 | ||
|
|
e30c67482f | ||
|
|
bda28e0404 | ||
|
|
ce63364299 | ||
|
|
f468dff6aa | ||
|
|
fab03006e8 | ||
|
|
8a565b8633 | ||
|
|
e79a6a5d47 | ||
|
|
95c2e20cb5 | ||
|
|
2e0f410978 | ||
|
|
fa1cd87348 | ||
|
|
e95d28e136 | ||
|
|
87ba71e77e | ||
|
|
dec0c0d3d1 | ||
|
|
776172bc88 | ||
|
|
d582548ed8 | ||
|
|
70ac31b907 | ||
|
|
cff9fd1ead | ||
|
|
319febb00d | ||
|
|
72fa2da2d3 | ||
|
|
3084c427f1 | ||
|
|
9cd1f013f8 |
4
.github/workflows/build.yml
vendored
@@ -50,12 +50,12 @@ jobs:
|
||||
- name: Run Type Check
|
||||
run: yarn typecheck
|
||||
- name: Run ESLint
|
||||
run: yarn lint --max-warnings=0 --cache
|
||||
run: yarn lint:eslint --max-warnings=0
|
||||
- name: Run Prettier
|
||||
# Set nmMode in `actions/setup-node` will modify the .yarnrc.yml
|
||||
run: |
|
||||
git checkout .yarnrc.yml
|
||||
yarn prettier . --ignore-unknown --cache --check
|
||||
yarn lint:prettier
|
||||
- name: Run circular
|
||||
run: yarn circular
|
||||
- name: Upload server dist
|
||||
|
||||
6
.github/workflows/nx.yml
vendored
@@ -34,11 +34,11 @@ jobs:
|
||||
main-branch-name: master
|
||||
number-of-agents: 5
|
||||
init-commands: |
|
||||
yarn exec nx-cloud start-ci-run --stop-agents-after="build" --agent-count=3
|
||||
yarn exec nx-cloud start-ci-run --stop-agents-after="build" --agent-count=5
|
||||
environment-variables: |
|
||||
BUILD_TYPE=canary
|
||||
parallel-commands: |
|
||||
yarn exec nx-cloud record -- yarn exec nx format:check
|
||||
# parallel-commands: |
|
||||
# yarn exec nx-cloud record -- yarn exec nx format:check
|
||||
parallel-commands-on-agents: |
|
||||
yarn exec nx affected --target=build --parallel=5
|
||||
timeout: 60
|
||||
|
||||
21
.github/workflows/workers.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Deploy Cloudflare Worker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- packages/workers/**
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Publish
|
||||
uses: cloudflare/wrangler-action@2.0.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
workingDirectory: 'packages/workers'
|
||||
@@ -10,6 +10,9 @@ yarn -T run build:infra
|
||||
# generate prisma client type
|
||||
yarn workspace @affine/server prisma generate
|
||||
|
||||
# generate i18n
|
||||
yarn i18n-codegen gen
|
||||
|
||||
# lint staged files
|
||||
yarn exec lint-staged
|
||||
|
||||
|
||||
1
.npmrc
@@ -1,2 +1,3 @@
|
||||
shell-emulator=true
|
||||
electron_mirror="https://cdn.npmmirror.com/binaries/electron/"
|
||||
engine-strict=true
|
||||
|
||||
@@ -10,3 +10,5 @@ dist
|
||||
.yarn
|
||||
tests/affine-legacy/0.7.0-canary.18/static
|
||||
.github/helm
|
||||
_next
|
||||
storybook-static
|
||||
|
||||
@@ -123,6 +123,8 @@ If you have questions, you are welcome to contact us. One of the best places to
|
||||
## Plugins
|
||||
|
||||
> Plugins are a way to extend the functionality of AFFiNE.
|
||||
>
|
||||
> (Currently, plugins are under heavy development, and the SDK is not yet available.)
|
||||
|
||||
| Name | |
|
||||
| ------------------------------------------------ | ----------------------------------------- |
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/docs",
|
||||
"version": "0.7.0-canary.34",
|
||||
"version": "0.7.0-canary.41",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -10,14 +10,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"express": "^4.18.2",
|
||||
"jotai": "^2.2.1",
|
||||
"jotai": "^2.2.2",
|
||||
"react": "18.3.0-canary-1fdacbefd-20230630",
|
||||
"react-dom": "18.3.0-canary-1fdacbefd-20230630",
|
||||
"react-server-dom-webpack": "18.3.0-canary-1fdacbefd-20230630",
|
||||
|
||||
@@ -149,3 +149,35 @@ test('windows only check', async ({ page }) => {
|
||||
await expect(windowOnlyUI).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('delete workspace', async ({ page }) => {
|
||||
await page.getByTestId('current-workspace').click();
|
||||
await page.getByTestId('add-or-new-workspace').click();
|
||||
await page.getByTestId('new-workspace').click();
|
||||
await page.getByTestId('create-workspace-default-location-button').click();
|
||||
await page.getByTestId('create-workspace-input').type('Delete Me');
|
||||
await page.getByTestId('create-workspace-create-button').click();
|
||||
await page.getByTestId('create-workspace-continue-button').click();
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await page.getByTestId('current-workspace-label').click();
|
||||
expect(await page.getByTestId('workspace-name-input').inputValue()).toBe(
|
||||
'Delete Me'
|
||||
);
|
||||
const contentElement = await page.getByTestId('setting-modal-content');
|
||||
const boundingBox = await contentElement.boundingBox();
|
||||
if (!boundingBox) {
|
||||
throw new Error('boundingBox is null');
|
||||
}
|
||||
await page.mouse.move(
|
||||
boundingBox.x + boundingBox.width / 2,
|
||||
boundingBox.y + boundingBox.height / 2
|
||||
);
|
||||
await page.mouse.wheel(0, 500);
|
||||
await page.getByTestId('delete-workspace-button').click();
|
||||
await page.getByTestId('delete-workspace-input').type('Delete Me');
|
||||
await page.getByTestId('delete-workspace-confirm-button').click();
|
||||
await page.waitForTimeout(1000);
|
||||
expect(await page.getByTestId('workspace-name').textContent()).toBe(
|
||||
'Demo Workspace'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -19,9 +19,11 @@ test('check workspace has a DB file', async ({ appInfo, workspace }) => {
|
||||
|
||||
test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
|
||||
const w = await workspace.current();
|
||||
const settingButton = page.getByTestId('slider-bar-workspace-setting-button');
|
||||
// goto settings
|
||||
await settingButton.click();
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||
|
||||
// goto workspace setting
|
||||
await page.getByTestId('workspace-list-item').click();
|
||||
|
||||
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp-dir');
|
||||
|
||||
@@ -42,21 +44,26 @@ test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
|
||||
expect(files.some(f => f.endsWith('.affine'))).toBe(true);
|
||||
});
|
||||
|
||||
test.skip('export then add', async ({ page, appInfo, workspace }) => {
|
||||
test('export then add', async ({ page, appInfo, workspace }) => {
|
||||
const w = await workspace.current();
|
||||
const settingButton = page.getByTestId('slider-bar-workspace-setting-button');
|
||||
// goto settings
|
||||
await settingButton.click();
|
||||
|
||||
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||
await expect(page.getByTestId('setting-modal')).toBeVisible();
|
||||
|
||||
const originalId = w.id;
|
||||
|
||||
const newWorkspaceName = 'new-test-name';
|
||||
|
||||
// goto workspace setting
|
||||
await page.getByTestId('workspace-list-item').click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// change workspace name
|
||||
await page.getByTestId('workspace-name-input').fill(newWorkspaceName);
|
||||
await page.getByTestId('save-workspace-name').click();
|
||||
await page.waitForSelector('text="Update workspace name success"');
|
||||
await page.click('[data-tab-key="export"]');
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const tmpPath = path.join(appInfo.sessionData, w.id + '-tmp.db');
|
||||
|
||||
@@ -73,10 +80,11 @@ test.skip('export then add', async ({ page, appInfo, workspace }) => {
|
||||
|
||||
expect(await fs.exists(tmpPath)).toBe(true);
|
||||
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
|
||||
// add workspace
|
||||
// we are reusing the same db file so that we don't need to maintain one
|
||||
// in the codebase
|
||||
|
||||
await page.getByTestId('current-workspace').click();
|
||||
await page.getByTestId('add-or-new-workspace').click();
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.34",
|
||||
"version": "0.7.0-canary.41",
|
||||
"author": "affine",
|
||||
"repository": {
|
||||
"url": "https://github.com/toeverything/AFFiNE",
|
||||
@@ -12,11 +12,11 @@
|
||||
"scripts": {
|
||||
"dev": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
|
||||
"dev:prod": "yarn node scripts/dev.mjs",
|
||||
"build": "zx scripts/build-layers.mjs",
|
||||
"build": "NODE_ENV=production zx scripts/build-layers.mjs",
|
||||
"generate-assets": "zx scripts/generate-assets.mjs",
|
||||
"package": "electron-forge package",
|
||||
"make": "electron-forge make",
|
||||
"test": "DEBUG=pw:browser playwright test"
|
||||
"test": "DEBUG=pw:browser yarn -T run playwright test -c ./playwright.config.ts"
|
||||
},
|
||||
"config": {
|
||||
"forge": "./forge.config.js"
|
||||
@@ -24,11 +24,12 @@
|
||||
"main": "./dist/main.js",
|
||||
"devDependencies": {
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/native": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@electron-forge/cli": "^6.2.1",
|
||||
"@electron-forge/core": "^6.2.1",
|
||||
"@electron-forge/core-utils": "^6.2.1",
|
||||
@@ -48,8 +49,7 @@
|
||||
"electron-window-state": "^5.0.3",
|
||||
"esbuild": "^0.18.11",
|
||||
"fs-extra": "^11.1.1",
|
||||
"jotai": "^2.2.1",
|
||||
"playwright": "=1.33.0",
|
||||
"jotai": "^2.2.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"undici": "^5.22.1",
|
||||
"uuid": "^9.0.0",
|
||||
@@ -81,7 +81,6 @@
|
||||
"hoistingLimits": "workspaces"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"ts-node": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,12 @@ export const config = () => {
|
||||
bundle: true,
|
||||
target: `node${NODE_MAJOR_VERSION}`,
|
||||
platform: 'node',
|
||||
external: ['electron', 'electron-updater', '@toeverything/plugin-infra'],
|
||||
external: [
|
||||
'electron',
|
||||
'electron-updater',
|
||||
'@toeverything/plugin-infra',
|
||||
'yjs',
|
||||
],
|
||||
define: define,
|
||||
format: 'cjs',
|
||||
loader: {
|
||||
|
||||
69
apps/electron/src/helper/db/__tests__/migration.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { SqliteConnection } from '@affine/native';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { removeWithRetry } from '../../../../tests/utils';
|
||||
import { copyToTemp, migrateToSubdocAndReplaceDatabase } from '../migration';
|
||||
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
const testDBFilePath = path.resolve(__dirname, 'old-db.affine');
|
||||
|
||||
const appDataPath = path.join(tmpDir, 'app-data');
|
||||
|
||||
vi.mock('../../main-rpc', () => ({
|
||||
mainRPC: {
|
||||
getPath: async () => appDataPath,
|
||||
},
|
||||
}));
|
||||
|
||||
afterEach(async () => {
|
||||
await removeWithRetry(tmpDir);
|
||||
});
|
||||
|
||||
describe('migrateToSubdocAndReplaceDatabase', () => {
|
||||
it('should migrate and replace the database', async () => {
|
||||
const copiedDbFilePath = await copyToTemp(testDBFilePath);
|
||||
await migrateToSubdocAndReplaceDatabase(copiedDbFilePath);
|
||||
|
||||
const db = new SqliteConnection(copiedDbFilePath);
|
||||
await db.connect();
|
||||
|
||||
// check if db has two rows, one for root doc and one for subdoc
|
||||
const rows = await db.getAllUpdates();
|
||||
expect(rows.length).toBe(2);
|
||||
|
||||
const rootUpdate = rows.find(row => row.docId === undefined)!.data;
|
||||
const subdocUpdate = rows.find(row => row.docId !== undefined)!.data;
|
||||
|
||||
expect(rootUpdate).toBeDefined();
|
||||
expect(subdocUpdate).toBeDefined();
|
||||
|
||||
// apply updates
|
||||
const rootDoc = new Y.Doc();
|
||||
Y.applyUpdate(rootDoc, rootUpdate);
|
||||
|
||||
// check if root doc has one subdoc
|
||||
expect(rootDoc.subdocs.size).toBe(1);
|
||||
|
||||
// populates subdoc
|
||||
Y.applyUpdate(rootDoc.subdocs.values().next().value, subdocUpdate);
|
||||
|
||||
// check if root doc's meta is correct
|
||||
const meta = rootDoc.getMap('meta').toJSON();
|
||||
expect(meta.workspaceVersion).toBe(1);
|
||||
expect(meta.name).toBe('hiw');
|
||||
expect(meta.pages.length).toBe(1);
|
||||
const pageMeta = meta.pages[0];
|
||||
expect(pageMeta.title).toBe('Welcome to AFFiNEd');
|
||||
|
||||
// get the subdoc through id
|
||||
const subDoc = rootDoc
|
||||
.getMap('spaces')
|
||||
.get(`space:${pageMeta.id}`) as Y.Doc;
|
||||
expect(subDoc).toEqual(rootDoc.subdocs.values().next().value);
|
||||
|
||||
await db.close();
|
||||
});
|
||||
});
|
||||
BIN
apps/electron/src/helper/db/__tests__/old-db.affine
Normal file
@@ -119,6 +119,8 @@ export abstract class BaseSQLiteAdapter {
|
||||
`[SQLiteAdapter][${this.role}] addUpdateToSQLite`,
|
||||
'length:',
|
||||
updates.length,
|
||||
'docids',
|
||||
updates.map(u => u.docId),
|
||||
performance.now() - start,
|
||||
'ms'
|
||||
);
|
||||
|
||||
55
apps/electron/src/helper/db/migration.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { migrateToSubdoc } from '@affine/env/blocksuite';
|
||||
import { SqliteConnection } from '@affine/native';
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { mainRPC } from '../main-rpc';
|
||||
|
||||
export const migrateToSubdocAndReplaceDatabase = async (path: string) => {
|
||||
const db = new SqliteConnection(path);
|
||||
await db.connect();
|
||||
|
||||
const rows = await db.getAllUpdates();
|
||||
const originalDoc = new Y.Doc();
|
||||
|
||||
// 1. apply all updates to the root doc
|
||||
rows.forEach(row => {
|
||||
Y.applyUpdate(originalDoc, row.data);
|
||||
});
|
||||
|
||||
// 2. migrate using migrateToSubdoc
|
||||
const migratedDoc = migrateToSubdoc(originalDoc);
|
||||
|
||||
// 3. replace db rows with the migrated doc
|
||||
await replaceRows(db, migratedDoc, true);
|
||||
|
||||
// 4. close db
|
||||
await db.close();
|
||||
};
|
||||
|
||||
export const copyToTemp = async (path: string) => {
|
||||
const tmpDirPath = resolve(await mainRPC.getPath('sessionData'), 'tmp');
|
||||
const tmpFilePath = resolve(tmpDirPath, nanoid());
|
||||
await fs.ensureDir(tmpDirPath);
|
||||
await fs.copyFile(path, tmpFilePath);
|
||||
return tmpFilePath;
|
||||
};
|
||||
|
||||
async function replaceRows(
|
||||
db: SqliteConnection,
|
||||
doc: Y.Doc,
|
||||
isRoot: boolean
|
||||
): Promise<void> {
|
||||
const migratedUpdates = Y.encodeStateAsUpdate(doc);
|
||||
const docId = isRoot ? undefined : doc.guid;
|
||||
const rows = [{ data: migratedUpdates, docId: docId }];
|
||||
await db.replaceUpdates(docId, rows);
|
||||
await Promise.all(
|
||||
[...doc.subdocs].map(async subdoc => {
|
||||
await replaceRows(db, subdoc, false);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -115,19 +115,43 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
}
|
||||
|
||||
setupListener(docId?: string) {
|
||||
logger.debug(
|
||||
'SecondaryWorkspaceSQLiteDB:setupListener',
|
||||
this.workspaceId,
|
||||
docId
|
||||
);
|
||||
const doc = this.getDoc(docId);
|
||||
if (!doc) {
|
||||
const upstreamDoc = this.upstream.getDoc(docId);
|
||||
if (!doc || !upstreamDoc) {
|
||||
logger.warn(
|
||||
'[SecondaryWorkspaceSQLiteDB] setupListener: doc not found',
|
||||
docId
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const onUpstreamUpdate = (update: Uint8Array, origin: YOrigin) => {
|
||||
if (origin === 'renderer') {
|
||||
logger.debug(
|
||||
'SecondaryWorkspaceSQLiteDB:onUpstreamUpdate',
|
||||
origin,
|
||||
this.workspaceId,
|
||||
docId,
|
||||
update.length
|
||||
);
|
||||
if (origin === 'renderer' || origin === 'self') {
|
||||
// update to upstream yDoc should be replicated to self yDoc
|
||||
this.applyUpdate(update, 'upstream', docId);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelfUpdate = async (update: Uint8Array, origin: YOrigin) => {
|
||||
logger.debug(
|
||||
'SecondaryWorkspaceSQLiteDB:onSelfUpdate',
|
||||
origin,
|
||||
this.workspaceId,
|
||||
docId,
|
||||
update.length
|
||||
);
|
||||
// for self update from upstream, we need to push it to external DB
|
||||
if (origin === 'upstream') {
|
||||
await this.addUpdateToUpdateQueue({
|
||||
@@ -147,15 +171,19 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
});
|
||||
};
|
||||
|
||||
doc.subdocs.forEach(subdoc => {
|
||||
this.setupListener(subdoc.guid);
|
||||
});
|
||||
|
||||
// listen to upstream update
|
||||
this.upstream.yDoc.on('update', onUpstreamUpdate);
|
||||
this.yDoc.on('update', onSelfUpdate);
|
||||
this.yDoc.on('subdocs', onSubdocs);
|
||||
doc.on('update', onSelfUpdate);
|
||||
doc.on('subdocs', onSubdocs);
|
||||
|
||||
this.unsubscribers.add(() => {
|
||||
this.upstream.yDoc.off('update', onUpstreamUpdate);
|
||||
this.yDoc.off('update', onSelfUpdate);
|
||||
this.yDoc.off('subdocs', onSubdocs);
|
||||
doc.off('update', onSelfUpdate);
|
||||
doc.off('subdocs', onSubdocs);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -188,7 +216,10 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
if (doc) {
|
||||
Y.applyUpdate(this.yDoc, data, origin);
|
||||
} else {
|
||||
logger.warn('applyUpdate: doc not found', docId);
|
||||
logger.warn(
|
||||
'[SecondaryWorkspaceSQLiteDB] applyUpdate: doc not found',
|
||||
docId
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
|
||||
update$ = new Subject<void>();
|
||||
|
||||
constructor(public override path: string, public workspaceId: string) {
|
||||
constructor(
|
||||
public override path: string,
|
||||
public workspaceId: string
|
||||
) {
|
||||
super(path);
|
||||
}
|
||||
|
||||
@@ -49,9 +52,21 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
};
|
||||
|
||||
setupListener(docId?: string) {
|
||||
logger.debug(
|
||||
'WorkspaceSQLiteDB:setupListener',
|
||||
this.workspaceId,
|
||||
docId,
|
||||
this.getWorkspaceName()
|
||||
);
|
||||
const doc = this.getDoc(docId);
|
||||
if (doc) {
|
||||
const onUpdate = async (update: Uint8Array, origin: YOrigin) => {
|
||||
logger.debug(
|
||||
'WorkspaceSQLiteDB:onUpdate',
|
||||
this.workspaceId,
|
||||
docId,
|
||||
update.length
|
||||
);
|
||||
const insertRows = [{ data: update, docId }];
|
||||
if (origin === 'renderer') {
|
||||
await this.addUpdateToSQLite(insertRows);
|
||||
@@ -65,7 +80,11 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
logger.debug('external update', this.workspaceId);
|
||||
}
|
||||
};
|
||||
doc.subdocs.forEach(subdoc => {
|
||||
this.setupListener(subdoc.guid);
|
||||
});
|
||||
const onSubdocs = ({ added }: { added: Set<Y.Doc> }) => {
|
||||
logger.info('onSubdocs', this.workspaceId, docId, added);
|
||||
added.forEach(subdoc => {
|
||||
this.setupListener(subdoc.guid);
|
||||
});
|
||||
@@ -132,7 +151,7 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
if (doc) {
|
||||
Y.applyUpdate(doc, data, origin);
|
||||
} else {
|
||||
logger.warn('applyUpdate: doc not found', docId);
|
||||
logger.warn('[WorkspaceSQLiteDB] applyUpdate: doc not found', docId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import { ValidationResult } from '@affine/native';
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { ensureSQLiteDB } from '../db/ensure-db';
|
||||
import { copyToTemp, migrateToSubdocAndReplaceDatabase } from '../db/migration';
|
||||
import type { WorkspaceSQLiteDB } from '../db/workspace-db-adapter';
|
||||
import { logger } from '../logger';
|
||||
import { mainRPC } from '../main-rpc';
|
||||
@@ -55,6 +57,7 @@ const ErrorMessages = [
|
||||
'DB_FILE_ALREADY_LOADED',
|
||||
'DB_FILE_PATH_INVALID',
|
||||
'DB_FILE_INVALID',
|
||||
'DB_FILE_MIGRATION_FAILED',
|
||||
'FILE_ALREADY_EXISTS',
|
||||
'UNKNOWN_ERROR',
|
||||
] as const;
|
||||
@@ -191,27 +194,42 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
||||
],
|
||||
message: 'Load Workspace from a AFFiNE file',
|
||||
}));
|
||||
const filePath = ret.filePaths?.[0];
|
||||
if (ret.canceled || !filePath) {
|
||||
let originalPath = ret.filePaths?.[0];
|
||||
if (ret.canceled || !originalPath) {
|
||||
logger.info('loadDBFile canceled');
|
||||
return { canceled: true };
|
||||
}
|
||||
|
||||
// the imported file should not be in app data dir
|
||||
if (filePath.startsWith(await getWorkspacesBasePath())) {
|
||||
if (originalPath.startsWith(await getWorkspacesBasePath())) {
|
||||
logger.warn('loadDBFile: db file in app data dir');
|
||||
return { error: 'DB_FILE_PATH_INVALID' };
|
||||
}
|
||||
|
||||
if (await dbFileAlreadyLoaded(filePath)) {
|
||||
if (await dbFileAlreadyLoaded(originalPath)) {
|
||||
logger.warn('loadDBFile: db file already loaded');
|
||||
return { error: 'DB_FILE_ALREADY_LOADED' };
|
||||
}
|
||||
|
||||
const { SqliteConnection } = await import('@affine/native');
|
||||
|
||||
if (!(await SqliteConnection.validate(filePath))) {
|
||||
// TODO: report invalid db file error?
|
||||
const validationResult = await SqliteConnection.validate(originalPath);
|
||||
|
||||
if (validationResult === ValidationResult.MissingDocIdColumn) {
|
||||
try {
|
||||
const tmpDBPath = await copyToTemp(originalPath);
|
||||
await migrateToSubdocAndReplaceDatabase(tmpDBPath);
|
||||
originalPath = tmpDBPath;
|
||||
} catch (error) {
|
||||
logger.warn(`loadDBFile, migration failed: ${originalPath}`, error);
|
||||
return { error: 'DB_FILE_MIGRATION_FAILED' };
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
validationResult !== ValidationResult.MissingDocIdColumn &&
|
||||
validationResult !== ValidationResult.Valid
|
||||
) {
|
||||
return { error: 'DB_FILE_INVALID' }; // invalid db file
|
||||
}
|
||||
|
||||
@@ -220,14 +238,12 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
||||
const internalFilePath = await getWorkspaceDBPath(workspaceId);
|
||||
|
||||
await fs.ensureDir(await getWorkspacesBasePath());
|
||||
|
||||
await fs.copy(filePath, internalFilePath);
|
||||
logger.info(`loadDBFile, copy: ${filePath} -> ${internalFilePath}`);
|
||||
await fs.copy(originalPath, internalFilePath);
|
||||
logger.info(`loadDBFile, copy: ${originalPath} -> ${internalFilePath}`);
|
||||
|
||||
await storeWorkspaceMeta(workspaceId, {
|
||||
id: workspaceId,
|
||||
mainDBPath: internalFilePath,
|
||||
secondaryDBPath: filePath,
|
||||
});
|
||||
|
||||
return { workspaceId };
|
||||
|
||||
@@ -21,7 +21,7 @@ type WithoutFirstParameter<T> = T extends (_: any, ...args: infer P) => infer R
|
||||
// however this is too hard to be typed correctly
|
||||
async function dispatch<
|
||||
T extends keyof MainIPCHandlerMap,
|
||||
F extends keyof MainIPCHandlerMap[T]
|
||||
F extends keyof MainIPCHandlerMap[T],
|
||||
>(
|
||||
namespace: T,
|
||||
functionName: F,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { app, Menu } from 'electron';
|
||||
|
||||
import { revealLogFile } from '../logger';
|
||||
import { checkForUpdatesAndNotify } from '../updater';
|
||||
import { checkForUpdates } from '../updater';
|
||||
import { isMacOS } from '../utils';
|
||||
import { applicationMenuSubjects } from './subject';
|
||||
|
||||
@@ -125,7 +125,7 @@ export function createApplicationMenu() {
|
||||
{
|
||||
label: 'Check for Updates',
|
||||
click: async () => {
|
||||
await checkForUpdatesAndNotify(true);
|
||||
await checkForUpdates(true);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { app } from 'electron';
|
||||
import type { AppUpdater } from 'electron-updater';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { logger } from '../logger';
|
||||
@@ -20,56 +20,55 @@ export const buildType = ReleaseTypeSchema.parse(envBuildType);
|
||||
const mode = process.env.NODE_ENV;
|
||||
const isDev = mode === 'development';
|
||||
|
||||
let _autoUpdater: AppUpdater | null = null;
|
||||
|
||||
export const quitAndInstall = async () => {
|
||||
_autoUpdater?.quitAndInstall();
|
||||
autoUpdater.quitAndInstall();
|
||||
};
|
||||
|
||||
let lastCheckTime = 0;
|
||||
export const checkForUpdatesAndNotify = async (force = true) => {
|
||||
if (!_autoUpdater) {
|
||||
return void 0;
|
||||
}
|
||||
export const checkForUpdates = async (force = true) => {
|
||||
// check every 30 minutes (1800 seconds) at most
|
||||
if (force || lastCheckTime + 1000 * 1800 < Date.now()) {
|
||||
lastCheckTime = Date.now();
|
||||
return await _autoUpdater.checkForUpdatesAndNotify();
|
||||
return await autoUpdater.checkForUpdates();
|
||||
}
|
||||
return void 0;
|
||||
};
|
||||
|
||||
export const registerUpdater = async () => {
|
||||
// so we wrap it in a function
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
|
||||
_autoUpdater = autoUpdater;
|
||||
|
||||
// skip auto update in dev mode
|
||||
if (!_autoUpdater || isDev) {
|
||||
if (isDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: support auto update on windows and linux
|
||||
const allowAutoUpdate = isMacOS();
|
||||
|
||||
_autoUpdater.autoDownload = false;
|
||||
_autoUpdater.allowPrerelease = buildType !== 'stable';
|
||||
_autoUpdater.autoInstallOnAppQuit = false;
|
||||
_autoUpdater.autoRunAppAfterInstall = true;
|
||||
_autoUpdater.setFeedURL({
|
||||
autoUpdater.logger = logger;
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.allowPrerelease = buildType !== 'stable';
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
|
||||
const feedUrl: Parameters<typeof autoUpdater.setFeedURL>[0] = {
|
||||
channel: buildType,
|
||||
provider: 'github',
|
||||
repo: buildType !== 'internal' ? 'AFFiNE' : 'AFFiNE-Releases',
|
||||
owner: 'toeverything',
|
||||
releaseType: buildType === 'stable' ? 'release' : 'prerelease',
|
||||
});
|
||||
};
|
||||
|
||||
logger.debug('auto-updater feed config', feedUrl);
|
||||
|
||||
autoUpdater.setFeedURL(feedUrl);
|
||||
|
||||
// register events for checkForUpdatesAndNotify
|
||||
_autoUpdater.on('update-available', info => {
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
logger.info('Checking for update');
|
||||
});
|
||||
autoUpdater.on('update-available', info => {
|
||||
logger.info('Update available', info);
|
||||
if (allowAutoUpdate) {
|
||||
_autoUpdater?.downloadUpdate().catch(e => {
|
||||
autoUpdater?.downloadUpdate().catch(e => {
|
||||
logger.error('Failed to download update', e);
|
||||
});
|
||||
logger.info('Update available, downloading...', info);
|
||||
@@ -79,11 +78,14 @@ export const registerUpdater = async () => {
|
||||
allowAutoUpdate,
|
||||
});
|
||||
});
|
||||
_autoUpdater.on('download-progress', e => {
|
||||
autoUpdater.on('update-not-available', info => {
|
||||
logger.info('Update not available', info);
|
||||
});
|
||||
autoUpdater.on('download-progress', e => {
|
||||
logger.info(`Download progress: ${e.percent}`);
|
||||
updaterSubjects.downloadProgress.next(e.percent);
|
||||
});
|
||||
_autoUpdater.on('update-downloaded', e => {
|
||||
autoUpdater.on('update-downloaded', e => {
|
||||
updaterSubjects.updateReady.next({
|
||||
version: e.version,
|
||||
allowAutoUpdate,
|
||||
@@ -92,12 +94,12 @@ export const registerUpdater = async () => {
|
||||
// updaterSubjects.clientDownloadProgress.next(100);
|
||||
logger.info('Update downloaded, ready to install');
|
||||
});
|
||||
_autoUpdater.on('error', e => {
|
||||
autoUpdater.on('error', e => {
|
||||
logger.error('Error while updating client', e);
|
||||
});
|
||||
_autoUpdater.forceDevUpdateConfig = isDev;
|
||||
autoUpdater.forceDevUpdateConfig = isDev;
|
||||
|
||||
app.on('activate', async () => {
|
||||
await checkForUpdatesAndNotify(false);
|
||||
await checkForUpdates(false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { checkForUpdatesAndNotify, quitAndInstall } from './electron-updater';
|
||||
import { checkForUpdates, quitAndInstall } from './electron-updater';
|
||||
|
||||
export const updaterHandlers = {
|
||||
currentVersion: async () => {
|
||||
@@ -11,7 +11,7 @@ export const updaterHandlers = {
|
||||
return quitAndInstall();
|
||||
},
|
||||
checkForUpdatesAndNotify: async () => {
|
||||
const res = await checkForUpdatesAndNotify(true);
|
||||
const res = await checkForUpdates(true);
|
||||
if (res) {
|
||||
const { updateInfo } = res;
|
||||
return {
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
{
|
||||
"path": "../../packages/infra"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/env"
|
||||
},
|
||||
|
||||
// Tests
|
||||
{
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
SECRET_KEY="secret"
|
||||
DATABASE_URL="postgresql://affine@localhost:5432/affine"
|
||||
NEXTAUTH_URL="http://localhost:8080"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.34",
|
||||
"version": "0.7.0-canary.41",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -27,6 +27,7 @@
|
||||
"@node-rs/crc32": "^1.7.0",
|
||||
"@node-rs/jsonwebtoken": "^0.2.0",
|
||||
"@prisma/client": "^4.16.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"graphql": "^16.7.1",
|
||||
@@ -34,18 +35,22 @@
|
||||
"graphql-upload": "^16.0.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"next-auth": "^4.22.1",
|
||||
"nodemailer": "^6.9.3",
|
||||
"parse-duration": "^1.1.0",
|
||||
"prisma": "^4.16.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1"
|
||||
"rxjs": "^7.8.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@affine/storage": "workspace:*",
|
||||
"@napi-rs/image": "^1.6.1",
|
||||
"@nestjs/testing": "^10.0.4",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/node": "^18.16.19",
|
||||
"@types/nodemailer": "^6.4.8",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"c8": "^8.0.0",
|
||||
"nodemon": "^2.0.22",
|
||||
|
||||
@@ -233,5 +233,11 @@ export interface AFFiNEConfig {
|
||||
}
|
||||
>
|
||||
>;
|
||||
email: {
|
||||
server: string;
|
||||
port: number;
|
||||
sender: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret',
|
||||
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
|
||||
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
|
||||
OAUTH_EMAIL_SENDER: 'auth.email.sender',
|
||||
OAUTH_EMAIL_SERVER: 'auth.email.server',
|
||||
OAUTH_EMAIL_PORT: 'auth.email.port',
|
||||
OAUTH_EMAIL_PASSWORD: 'auth.email.password',
|
||||
} satisfies AFFiNEConfig['ENV_MAP'],
|
||||
env: process.env.NODE_ENV ?? 'development',
|
||||
get prod() {
|
||||
@@ -102,7 +106,6 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
},
|
||||
introspection: true,
|
||||
playground: true,
|
||||
debug: true,
|
||||
},
|
||||
auth: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
@@ -114,8 +117,16 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
publicKey: jwtKeyPair.publicKey,
|
||||
enableSignup: true,
|
||||
enableOauth: false,
|
||||
nextAuthSecret: '',
|
||||
get nextAuthSecret() {
|
||||
return this.privateKey;
|
||||
},
|
||||
oauthProviders: {},
|
||||
email: {
|
||||
server: 'smtp.gmail.com',
|
||||
port: 465,
|
||||
sender: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
objectStorage: {
|
||||
r2: {
|
||||
@@ -129,7 +140,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
|
||||
path: join(homedir(), '.affine-storage'),
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
} satisfies AFFiNEConfig;
|
||||
|
||||
applyEnvToConfig(defaultConfig);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/// <reference types="./global.d.ts" />
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import type { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { static as staticMiddleware } from 'express';
|
||||
// @ts-expect-error graphql-upload is not typed
|
||||
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
|
||||
@@ -27,6 +28,8 @@ app.use(
|
||||
})
|
||||
);
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
const config = app.get(Config);
|
||||
|
||||
const host = config.host ?? 'localhost';
|
||||
|
||||
@@ -41,7 +41,10 @@ export const CurrentUser = createParamDecorator(
|
||||
|
||||
@Injectable()
|
||||
class AuthGuard implements CanActivate {
|
||||
constructor(private auth: AuthService, private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private auth: AuthService,
|
||||
private prisma: PrismaService
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const { req } = getRequestResponseFromContext(context);
|
||||
|
||||
@@ -2,11 +2,10 @@ import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PrismaAdapter } from '@auth/prisma-adapter';
|
||||
import {
|
||||
All,
|
||||
BadRequestException,
|
||||
Controller,
|
||||
Get,
|
||||
Next,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
Res,
|
||||
@@ -15,6 +14,7 @@ import { Algorithm, sign, verify as jwtVerify } from '@node-rs/jsonwebtoken';
|
||||
import type { NextFunction, Request, Response } from 'express';
|
||||
import type { AuthAction, AuthOptions } from 'next-auth';
|
||||
import { AuthHandler } from 'next-auth/core';
|
||||
import Email from 'next-auth/providers/email';
|
||||
import Github from 'next-auth/providers/github';
|
||||
import Google from 'next-auth/providers/google';
|
||||
|
||||
@@ -28,16 +28,44 @@ const BASE_URL = '/api/auth/';
|
||||
export class NextAuthController {
|
||||
private readonly nextAuthOptions: AuthOptions;
|
||||
|
||||
constructor(readonly config: Config, readonly prisma: PrismaService) {
|
||||
constructor(
|
||||
readonly config: Config,
|
||||
readonly prisma: PrismaService
|
||||
) {
|
||||
const prismaAdapter = PrismaAdapter(prisma);
|
||||
// createUser exists in the adapter
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const createUser = prismaAdapter.createUser!.bind(prismaAdapter);
|
||||
prismaAdapter.createUser = async data => {
|
||||
if (data.email && !data.name) {
|
||||
data.name = data.email.split('@')[0];
|
||||
}
|
||||
return createUser(data);
|
||||
};
|
||||
this.nextAuthOptions = {
|
||||
providers: [],
|
||||
providers: [
|
||||
// @ts-expect-error esm interop issue
|
||||
Email.default({
|
||||
server: {
|
||||
host: config.auth.email.server,
|
||||
port: config.auth.email.port,
|
||||
auth: {
|
||||
user: config.auth.email.sender,
|
||||
pass: config.auth.email.password,
|
||||
},
|
||||
},
|
||||
from: `AFFiNE <no-reply@toeverything.info>`,
|
||||
}),
|
||||
],
|
||||
// @ts-expect-error Third part library type mismatch
|
||||
adapter: PrismaAdapter(prisma),
|
||||
adapter: prismaAdapter,
|
||||
debug: !config.prod,
|
||||
};
|
||||
|
||||
if (config.auth.oauthProviders.github) {
|
||||
this.nextAuthOptions.providers.push(
|
||||
Github({
|
||||
// @ts-expect-error esm interop issue
|
||||
Github.default({
|
||||
clientId: config.auth.oauthProviders.github.clientId,
|
||||
clientSecret: config.auth.oauthProviders.github.clientSecret,
|
||||
})
|
||||
@@ -46,12 +74,14 @@ export class NextAuthController {
|
||||
|
||||
if (config.auth.oauthProviders.google) {
|
||||
this.nextAuthOptions.providers.push(
|
||||
Google({
|
||||
// @ts-expect-error esm interop issue
|
||||
Google.default({
|
||||
clientId: config.auth.oauthProviders.google.clientId,
|
||||
clientSecret: config.auth.oauthProviders.google.clientSecret,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.nextAuthOptions.jwt = {
|
||||
encode: async ({ token, maxAge }) => {
|
||||
if (!token?.email) {
|
||||
@@ -108,8 +138,7 @@ export class NextAuthController {
|
||||
this.nextAuthOptions.secret ??= config.auth.nextAuthSecret;
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Post()
|
||||
@All('*')
|
||||
async auth(
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
|
||||
@@ -19,7 +19,10 @@ export const getUtcTimestamp = () => Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(private config: Config, private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private config: Config,
|
||||
private prisma: PrismaService
|
||||
) {}
|
||||
|
||||
sign(user: UserClaim) {
|
||||
const now = getUtcTimestamp();
|
||||
|
||||
@@ -101,7 +101,6 @@ type Query {
|
||||
type Mutation {
|
||||
register(name: String!, email: String!, password: String!): UserType!
|
||||
signIn(email: String!, password: String!): UserType!
|
||||
signUp(email: String!, password: String!, name: String!): UserType!
|
||||
|
||||
"""
|
||||
Create a new workspace
|
||||
|
||||
@@ -28,7 +28,7 @@ export type LeafPaths<
|
||||
T,
|
||||
Path extends string = '',
|
||||
MaxDepth extends string = '...',
|
||||
Depth extends string = ''
|
||||
Depth extends string = '',
|
||||
> = Depth extends MaxDepth
|
||||
? never
|
||||
: T extends Record<string | number, any>
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
"wait-on": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/block-std": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/icons": "^2.1.24",
|
||||
"@blocksuite/lit": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"react": "18.3.0-canary-1fdacbefd-20230630",
|
||||
"react-dom": "18.3.0-canary-1fdacbefd-20230630"
|
||||
},
|
||||
@@ -48,5 +48,5 @@
|
||||
"@blocksuite/lit": "*",
|
||||
"@blocksuite/store": "*"
|
||||
},
|
||||
"version": "0.7.0-canary.34"
|
||||
"version": "0.7.0-canary.41"
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { AffineLoading } from '@affine/component/affine-loading';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Loading',
|
||||
component: AffineLoading,
|
||||
};
|
||||
|
||||
export const Default: StoryFn = ({ width, loop, autoplay, autoReverse }) => (
|
||||
<div
|
||||
style={{
|
||||
width: width,
|
||||
height: width,
|
||||
}}
|
||||
>
|
||||
<AffineLoading loop={loop} autoplay={autoplay} autoReverse={autoReverse} />
|
||||
</div>
|
||||
);
|
||||
Default.args = {
|
||||
width: 100,
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
autoReverse: true,
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { BlockHubWrapper } from '@affine/component/block-hub';
|
||||
import type { EditorProps } from '@affine/component/block-suite-editor';
|
||||
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
|
||||
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
@@ -54,13 +56,13 @@ const Template: StoryFn<EditorProps> = (props: Partial<EditorProps>) => {
|
||||
}}
|
||||
>
|
||||
<BlockSuiteEditor onInit={initPage} page={page} mode="page" {...props} />
|
||||
<div
|
||||
<BlockHubWrapper
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
}}
|
||||
id="toolWrapper"
|
||||
blockHubAtom={rootBlockHubAtom}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,27 +2,27 @@ import { toast } from '@affine/component';
|
||||
import { BlockCard } from '@affine/component/card/block-card';
|
||||
import { WorkspaceCard } from '@affine/component/card/workspace-card';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Card',
|
||||
component: WorkspaceCard,
|
||||
};
|
||||
|
||||
const blockSuiteWorkspace = new Workspace({
|
||||
id: 'blocksuite-local',
|
||||
});
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
'blocksuite-local',
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
|
||||
blockSuiteWorkspace.meta.setName('Hello World');
|
||||
|
||||
export const AffineWorkspaceCard = () => {
|
||||
return (
|
||||
<WorkspaceCard
|
||||
workspace={{
|
||||
meta={{
|
||||
id: 'blocksuite-local',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace,
|
||||
}}
|
||||
onClick={() => {}}
|
||||
onSettingClick={() => {}}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Button } from '@affine/component';
|
||||
import type { ContactModalProps } from '@affine/component/contact-modal';
|
||||
import { ContactModal } from '@affine/component/contact-modal';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/ContactModal',
|
||||
component: ContactModal,
|
||||
};
|
||||
|
||||
export const Basic: StoryFn<ContactModalProps> = args => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
<ContactModal
|
||||
{...args}
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Basic.args = {
|
||||
logoSrc: '/imgs/affine-text-logo.png',
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { BlockHubWrapper } from '@affine/component/block-hub';
|
||||
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
|
||||
import { ImagePreviewModal } from '@affine/component/image-preview-modal';
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
@@ -54,13 +56,13 @@ export const Default = () => {
|
||||
>
|
||||
<BlockSuiteEditor mode="page" page={page} onInit={initEmptyPage} />
|
||||
</div>
|
||||
<div
|
||||
<BlockHubWrapper
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
}}
|
||||
id="toolWrapper"
|
||||
blockHubAtom={rootBlockHubAtom}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Empty } from '@affine/component';
|
||||
import { toast } from '@affine/component';
|
||||
import { AffineLoading } from '@affine/component/affine-loading';
|
||||
import type { OperationCellProps } from '@affine/component/page-list';
|
||||
import { PageListTrashView } from '@affine/component/page-list';
|
||||
import { PageList } from '@affine/component/page-list';
|
||||
@@ -13,7 +12,7 @@ import { userEvent } from '@storybook/testing-library';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/PageList',
|
||||
component: AffineLoading,
|
||||
component: PageList,
|
||||
};
|
||||
|
||||
export const AffineOperationCell: StoryFn<OperationCellProps> = ({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { WorkspaceAvatarProps } from '@affine/component/workspace-avatar';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
@@ -25,16 +24,7 @@ const basicBlockSuiteWorkspace = new Workspace({
|
||||
basicBlockSuiteWorkspace.meta.setName('Hello World');
|
||||
|
||||
export const Basic: StoryFn<WorkspaceAvatarProps> = props => {
|
||||
return (
|
||||
<WorkspaceAvatar
|
||||
{...props}
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace: basicBlockSuiteWorkspace,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <WorkspaceAvatar {...props} workspace={basicBlockSuiteWorkspace} />;
|
||||
};
|
||||
|
||||
Basic.args = {
|
||||
@@ -60,16 +50,7 @@ fetch(new URL('@affine-test/fixtures/smile.png', import.meta.url))
|
||||
});
|
||||
|
||||
export const BlobExample: StoryFn<WorkspaceAvatarProps> = props => {
|
||||
return (
|
||||
<WorkspaceAvatar
|
||||
{...props}
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace: avatarBlockSuiteWorkspace,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <WorkspaceAvatar {...props} workspace={avatarBlockSuiteWorkspace} />;
|
||||
};
|
||||
|
||||
BlobExample.args = {
|
||||
|
||||
@@ -13,6 +13,9 @@ import { blockSuiteFeatureFlags, buildFlags } from './preset.config.mjs';
|
||||
import { getCommitHash, getGitVersion } from './scripts/git-info.mjs';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJson = require('./package.json');
|
||||
const appVersion = packageJson.version;
|
||||
const editorVersion = packageJson.dependencies['@blocksuite/editor'];
|
||||
const { createVanillaExtractPlugin } = require('@vanilla-extract/next-plugin');
|
||||
const withVanillaExtract = createVanillaExtractPlugin();
|
||||
|
||||
@@ -102,6 +105,8 @@ const nextConfig = {
|
||||
publicRuntimeConfig: {
|
||||
PROJECT_NAME: process.env.npm_package_name ?? 'AFFiNE',
|
||||
BUILD_DATE: new Date().toISOString(),
|
||||
appVersion,
|
||||
editorVersion,
|
||||
gitVersion: getGitVersion(),
|
||||
hash: getCommitHash(),
|
||||
serverAPI: profileTarget.local,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/web",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.34",
|
||||
"version": "0.7.0-canary.41",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build && next export",
|
||||
@@ -19,13 +19,13 @@
|
||||
"@affine/jotai": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/icons": "^2.1.24",
|
||||
"@blocksuite/lit": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230704115754-19bc6fae-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230711103520-ce18dd84-nightly",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
@@ -42,7 +42,7 @@
|
||||
"cmdk": "^0.2.0",
|
||||
"css-spring": "^4.1.0",
|
||||
"graphql": "^16.7.1",
|
||||
"jotai": "^2.2.1",
|
||||
"jotai": "^2.2.2",
|
||||
"jotai-devtools": "^0.6.0",
|
||||
"lit": "^2.7.5",
|
||||
"lottie-web": "^5.12.2",
|
||||
|
||||
@@ -23,14 +23,13 @@ const buildPreset = {
|
||||
enableTestProperties: false,
|
||||
enableBroadcastChannelProvider: true,
|
||||
enableDebugPage: true,
|
||||
// never set this to true in stable, because legacy cloud has deprecated
|
||||
// and related code will be removed in the future
|
||||
enableLegacyCloud: false,
|
||||
changelogUrl: 'https://affine.pro/blog/whats-new-affine-0630',
|
||||
imageProxyUrl: 'https://workers.toeverything.workers.dev/proxy/image',
|
||||
enablePreloading: true,
|
||||
enableNewSettingModal: true,
|
||||
enableNewSettingUnstableApi: false,
|
||||
enableSQLiteProvider: true,
|
||||
enableMoveDatabase: false,
|
||||
enableNotificationCenter: false,
|
||||
enableCloud: false,
|
||||
},
|
||||
@@ -42,12 +41,13 @@ const buildPreset = {
|
||||
enableTestProperties: true,
|
||||
enableBroadcastChannelProvider: true,
|
||||
enableDebugPage: true,
|
||||
enableLegacyCloud: false,
|
||||
changelogUrl: 'https://affine.pro/blog/whats-new-affine-0630',
|
||||
imageProxyUrl: 'https://workers.toeverything.workers.dev/proxy/image',
|
||||
enablePreloading: true,
|
||||
enableNewSettingModal: true,
|
||||
enableNewSettingUnstableApi: false,
|
||||
enableSQLiteProvider: true,
|
||||
enableMoveDatabase: false,
|
||||
enableNotificationCenter: true,
|
||||
enableCloud: false,
|
||||
},
|
||||
@@ -72,9 +72,6 @@ const environmentPreset = {
|
||||
enableTestProperties: process.env.ENABLE_TEST_PROPERTIES
|
||||
? process.env.ENABLE_TEST_PROPERTIES === 'true'
|
||||
: currentBuildPreset.enableTestProperties,
|
||||
enableLegacyCloud: process.env.ENABLE_LEGACY_PROVIDER
|
||||
? process.env.ENABLE_LEGACY_PROVIDER === 'true'
|
||||
: currentBuildPreset.enableLegacyCloud,
|
||||
enableBroadcastChannelProvider: process.env.ENABLE_BC_PROVIDER
|
||||
? process.env.ENABLE_BC_PROVIDER !== 'false'
|
||||
: currentBuildPreset.enableBroadcastChannelProvider,
|
||||
@@ -97,6 +94,9 @@ const environmentPreset = {
|
||||
enableCloud: process.env.ENABLE_CLOUD
|
||||
? process.env.ENABLE_CLOUD === 'true'
|
||||
: currentBuildPreset.enableCloud,
|
||||
enableMoveDatabase: process.env.ENABLE_MOVE_DATABASE
|
||||
? process.env.ENABLE_MOVE_DATABASE === 'true'
|
||||
: currentBuildPreset.enableMoveDatabase,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
"build": {
|
||||
"executor": "nx:run-script",
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": [
|
||||
"{projectRoot}/**/*",
|
||||
"{workspaceRoot}/packages/component/src/**/*",
|
||||
"{workspaceRoot}/packages/debug/src/**/*",
|
||||
"{workspaceRoot}/packages/debug/graphql/**/*",
|
||||
"{workspaceRoot}/packages/hooks/src/**/*",
|
||||
"{workspaceRoot}/packages/jotai/src/**/*",
|
||||
"{workspaceRoot}/packages/templates/src/**/*",
|
||||
"{workspaceRoot}/packages/workspace/src/**/*"
|
||||
],
|
||||
"options": {
|
||||
"script": "build"
|
||||
},
|
||||
|
||||
|
After Width: | Height: | Size: 710 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 822 KiB |
|
After Width: | Height: | Size: 363 KiB |
|
After Width: | Height: | Size: 945 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 6.8 MiB |
@@ -19,6 +19,7 @@ import {
|
||||
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/hooks/use-block-suite-workspace';
|
||||
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
@@ -75,13 +76,11 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
Provider: ({ children }) => {
|
||||
return <>{children}</>;
|
||||
},
|
||||
PageDetail: ({ currentWorkspace, currentPageId, onLoadEditor }) => {
|
||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId);
|
||||
const page = workspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(
|
||||
currentWorkspace.blockSuiteWorkspace,
|
||||
currentPageId
|
||||
);
|
||||
throw new PageNotFoundError(workspace, currentPageId);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
@@ -89,7 +88,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
pageId={currentPageId}
|
||||
onInit={initEmptyPage}
|
||||
onLoad={onLoadEditor}
|
||||
workspace={currentWorkspace}
|
||||
workspace={workspace}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -105,14 +104,14 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspace,
|
||||
currentWorkspaceId,
|
||||
onDeleteWorkspace,
|
||||
onTransformWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
workspace={currentWorkspace}
|
||||
workspaceId={currentWorkspaceId}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,34 +3,14 @@
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import type {
|
||||
LocalIndexedDBBackgroundProvider,
|
||||
WorkspaceAdapter,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
|
||||
import {
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers';
|
||||
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 { WorkspaceAdapters } from '../../adapters/workspace';
|
||||
import {
|
||||
pageSettingFamily,
|
||||
pageSettingsAtom,
|
||||
recentPageSettingsAtom,
|
||||
} from '../index';
|
||||
import { rootCurrentWorkspaceAtom } from '../root';
|
||||
|
||||
describe('page mode atom', () => {
|
||||
test('basic', () => {
|
||||
@@ -63,66 +43,3 @@ describe('page mode atom', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentWorkspace atom', () => {
|
||||
test('should be defined', async () => {
|
||||
const store = createStore();
|
||||
store.set(
|
||||
workspaceAdaptersAtom,
|
||||
WorkspaceAdapters as Record<
|
||||
WorkspaceFlavour,
|
||||
WorkspaceAdapter<WorkspaceFlavour>
|
||||
>
|
||||
);
|
||||
let id: string;
|
||||
{
|
||||
const workspace = createEmptyBlockSuiteWorkspace(
|
||||
'test',
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
const page = workspace.createPage({ id: 'page0' });
|
||||
await initEmptyPage(page);
|
||||
const frameId = page.getBlockByFlavour('affine:note').at(0)?.id as string;
|
||||
id = page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('test 1'),
|
||||
},
|
||||
frameId
|
||||
);
|
||||
const provider = createIndexedDBBackgroundProvider(
|
||||
workspace.id,
|
||||
workspace.doc,
|
||||
{
|
||||
awareness: workspace.awarenessStore.awareness,
|
||||
}
|
||||
) as LocalIndexedDBBackgroundProvider;
|
||||
provider.connect();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
provider.disconnect();
|
||||
const workspaceId = await WorkspaceAdapters[
|
||||
WorkspaceFlavour.LOCAL
|
||||
].CRUD.create(workspace);
|
||||
await store.set(rootWorkspacesMetadataAtom, [
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
},
|
||||
]);
|
||||
_cleanupBlockSuiteWorkspaceCache();
|
||||
}
|
||||
store.set(
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
(await store.get(rootWorkspacesMetadataAtom))[0].id
|
||||
);
|
||||
const workspace = await store.get(rootCurrentWorkspaceAtom);
|
||||
expect(workspace).toBeDefined();
|
||||
const page = workspace.blockSuiteWorkspace.getPage('page0') as Page;
|
||||
await page.waitForLoaded();
|
||||
expect(page).not.toBeNull();
|
||||
const paragraphBlock = page.getBlockById(id) as ParagraphBlockModel;
|
||||
expect(paragraphBlock).not.toBeNull();
|
||||
expect(paragraphBlock.text.toString()).toBe('test 1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,18 +2,26 @@ import { atom } from 'jotai';
|
||||
import { atomFamily, atomWithStorage } from 'jotai/utils';
|
||||
|
||||
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
||||
import type { SettingProps } from '../components/affine/setting-modal';
|
||||
|
||||
// modal atoms
|
||||
export const openWorkspacesModalAtom = atom(false);
|
||||
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
||||
export const openQuickSearchModalAtom = atom(false);
|
||||
export const openOnboardingModalAtom = atom(false);
|
||||
export const openSettingModalAtom = atom(false);
|
||||
|
||||
export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspaceId'> & {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
export const openSettingModalAtom = atom<SettingAtom>({
|
||||
activeTab: 'appearance',
|
||||
workspaceId: null,
|
||||
open: false,
|
||||
});
|
||||
|
||||
export const openDisableCloudAlertModalAtom = atom(false);
|
||||
|
||||
export { workspacesAtom } from './root';
|
||||
|
||||
type PageMode = 'page' | 'edgeless';
|
||||
type PageLocalSetting = {
|
||||
mode: PageMode;
|
||||
@@ -47,9 +55,16 @@ export const recentPageSettingsAtom = atom<PartialPageLocalSettingWithPageId[]>(
|
||||
}
|
||||
);
|
||||
|
||||
const defaultPageSetting = {
|
||||
mode: 'page',
|
||||
} satisfies PageLocalSetting;
|
||||
|
||||
export const pageSettingFamily = atomFamily((pageId: string) =>
|
||||
atom(
|
||||
get => get(pageSettingsBaseAtom)[pageId],
|
||||
get =>
|
||||
get(pageSettingsBaseAtom)[pageId] ?? {
|
||||
...defaultPageSetting,
|
||||
},
|
||||
(
|
||||
get,
|
||||
set,
|
||||
@@ -61,11 +76,15 @@ export const pageSettingFamily = atomFamily((pageId: string) =>
|
||||
// pick 3 recent page ids
|
||||
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
|
||||
});
|
||||
const prevSetting = {
|
||||
...defaultPageSetting,
|
||||
...get(pageSettingsBaseAtom)[pageId],
|
||||
};
|
||||
set(pageSettingsBaseAtom, settings => ({
|
||||
...settings,
|
||||
[pageId]: {
|
||||
...settings[pageId],
|
||||
...(typeof patch === 'function' ? patch(settings[pageId]) : patch),
|
||||
...prevSetting,
|
||||
...(typeof patch === 'function' ? patch(prevSetting) : patch),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
//#region async atoms that to load the real workspace data
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type {
|
||||
WorkspaceAdapter,
|
||||
WorkspaceRegistry,
|
||||
} from '@affine/env/workspace';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
rootWorkspacesMetadataAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { ActiveDocProvider } from '@blocksuite/store';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import type { AllWorkspace } from '../shared';
|
||||
|
||||
const logger = new DebugLogger('web:atoms:root');
|
||||
|
||||
/**
|
||||
* Fetch all workspaces from the Plugin CRUD
|
||||
*/
|
||||
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
|
||||
async (get, { signal }) => {
|
||||
const { WorkspaceAdapters } = await import('../adapters/workspace');
|
||||
const flavours: string[] = Object.values(WorkspaceAdapters).map(
|
||||
plugin => plugin.flavour
|
||||
);
|
||||
const jotaiWorkspaces = (await get(rootWorkspacesMetadataAtom)).filter(
|
||||
workspace => flavours.includes(workspace.flavour)
|
||||
);
|
||||
if (jotaiWorkspaces.some(meta => !('version' in meta))) {
|
||||
// wait until all workspaces have migrated to v2
|
||||
await new Promise((resolve, reject) => {
|
||||
signal.addEventListener('abort', reject);
|
||||
setTimeout(resolve, 1000);
|
||||
}).catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
}
|
||||
const workspaces = await Promise.all(
|
||||
jotaiWorkspaces.map(workspace => {
|
||||
const adapter = WorkspaceAdapters[
|
||||
workspace.flavour
|
||||
] as WorkspaceAdapter<WorkspaceFlavour>;
|
||||
assertExists(adapter);
|
||||
const { CRUD } = adapter;
|
||||
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-cloud' | 'local'] =>
|
||||
workspace !== null
|
||||
)
|
||||
);
|
||||
const workspaceProviders = workspaces.map(workspace =>
|
||||
workspace.blockSuiteWorkspace.providers.filter(
|
||||
(provider): provider is ActiveDocProvider =>
|
||||
'active' in provider && provider.active
|
||||
)
|
||||
);
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const providers of workspaceProviders) {
|
||||
for (const provider of providers) {
|
||||
provider.sync();
|
||||
promises.push(provider.whenReady);
|
||||
}
|
||||
}
|
||||
// we will wait for all the necessary providers to be ready
|
||||
await Promise.all(promises);
|
||||
logger.info('workspaces', workspaces);
|
||||
return workspaces;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* This will throw an error if the workspace is not found,
|
||||
* should not be used on the root component,
|
||||
* use `rootCurrentWorkspaceIdAtom` instead
|
||||
*/
|
||||
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
|
||||
async (get, { signal }) => {
|
||||
const { WorkspaceAdapters } = await import('../adapters/workspace');
|
||||
const metadata = await get(rootWorkspacesMetadataAtom);
|
||||
const targetId = get(rootCurrentWorkspaceIdAtom);
|
||||
if (targetId === null) {
|
||||
throw new Error(
|
||||
'current workspace id is null. this should not happen. If you see this error, please report it to the developer.'
|
||||
);
|
||||
}
|
||||
const targetWorkspace = metadata.find(meta => meta.id === targetId);
|
||||
if (!targetWorkspace) {
|
||||
throw new Error(`cannot find the workspace with id ${targetId}.`);
|
||||
}
|
||||
|
||||
if (!('version' in targetWorkspace)) {
|
||||
// wait until the workspace has migrated to v2
|
||||
await new Promise((resolve, reject) => {
|
||||
signal.addEventListener('abort', reject);
|
||||
setTimeout(resolve, 1000);
|
||||
}).catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
}
|
||||
|
||||
const adapter = WorkspaceAdapters[
|
||||
targetWorkspace.flavour
|
||||
] as WorkspaceAdapter<WorkspaceFlavour>;
|
||||
assertExists(adapter);
|
||||
|
||||
const workspace = await adapter.CRUD.get(targetWorkspace.id);
|
||||
if (!workspace) {
|
||||
throw new Error(
|
||||
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
|
||||
);
|
||||
}
|
||||
|
||||
const providers = workspace.blockSuiteWorkspace.providers.filter(
|
||||
(provider): provider is ActiveDocProvider =>
|
||||
'active' in provider && provider.active === true
|
||||
);
|
||||
for (const provider of providers) {
|
||||
provider.sync();
|
||||
// we will wait for the necessary providers to be ready
|
||||
await provider.whenReady;
|
||||
}
|
||||
logger.info('current workspace', workspace);
|
||||
globalThis.currentWorkspace = workspace;
|
||||
globalThis.dispatchEvent(
|
||||
new CustomEvent('affine:workspace:change', {
|
||||
detail: { id: workspace.id },
|
||||
})
|
||||
);
|
||||
return workspace;
|
||||
}
|
||||
);
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* @internal debug only
|
||||
*/
|
||||
// eslint-disable-next-line no-var
|
||||
var currentWorkspace: AllWorkspace | undefined;
|
||||
interface WindowEventMap {
|
||||
'affine:workspace:change': CustomEvent<{ id: string }>;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not add `rootCurrentWorkspacePageAtom`, this is not needed.
|
||||
// It can be derived from `rootCurrentWorkspaceAtom` and `rootCurrentPageIdAtom`
|
||||
|
||||
//#endregion
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export type DateFormats =
|
||||
| 'MM/dd/YYYY'
|
||||
@@ -15,6 +15,7 @@ export type AppSetting = {
|
||||
clientBorder: boolean;
|
||||
fullWidthLayout: boolean;
|
||||
windowFrameStyle: 'frameless' | 'NativeTitleBar';
|
||||
fontStyle: FontFamily;
|
||||
dateFormat: DateFormats;
|
||||
startWeekOnMonday: boolean;
|
||||
enableBlurBackground: boolean;
|
||||
@@ -37,10 +38,22 @@ export const dateFormatOptions: DateFormats[] = [
|
||||
'dd MMMM YYYY',
|
||||
];
|
||||
|
||||
export const AppSettingAtom = atomWithStorage<AppSetting>('AFFiNE settings', {
|
||||
clientBorder: false,
|
||||
export type FontFamily = 'Sans' | 'Serif' | 'Mono';
|
||||
|
||||
export const fontStyleOptions = [
|
||||
{ key: 'Sans', value: 'var(--affine-font-sans-family)' },
|
||||
{ key: 'Serif', value: 'var(--affine-font-serif-family)' },
|
||||
{ key: 'Mono', value: 'var(--affine-font-mono-family)' },
|
||||
] satisfies {
|
||||
key: FontFamily;
|
||||
value: string;
|
||||
}[];
|
||||
|
||||
const appSettingBaseAtom = atomWithStorage<AppSetting>('affine-settings', {
|
||||
clientBorder: isDesktop,
|
||||
fullWidthLayout: false,
|
||||
windowFrameStyle: 'frameless',
|
||||
fontStyle: 'Sans',
|
||||
dateFormat: dateFormatOptions[0],
|
||||
startWeekOnMonday: false,
|
||||
enableBlurBackground: true,
|
||||
@@ -49,19 +62,21 @@ export const AppSettingAtom = atomWithStorage<AppSetting>('AFFiNE settings', {
|
||||
autoDownloadUpdate: true,
|
||||
});
|
||||
|
||||
export const useAppSetting = () => {
|
||||
const [settings, setSettings] = useAtom(AppSettingAtom);
|
||||
type SetStateAction<Value> = Value | ((prev: Value) => Value);
|
||||
|
||||
return [
|
||||
settings,
|
||||
useCallback(
|
||||
(patch: Partial<AppSetting>) => {
|
||||
setSettings((prev: AppSetting) => ({
|
||||
...prev,
|
||||
...patch,
|
||||
}));
|
||||
},
|
||||
[setSettings]
|
||||
),
|
||||
] as const;
|
||||
const appSettingAtom = atom<
|
||||
AppSetting,
|
||||
[SetStateAction<Partial<AppSetting>>],
|
||||
void
|
||||
>(
|
||||
get => get(appSettingBaseAtom),
|
||||
(get, set, apply) => {
|
||||
const prev = get(appSettingBaseAtom);
|
||||
const next = typeof apply === 'function' ? apply(prev) : apply;
|
||||
set(appSettingBaseAtom, { ...prev, ...next });
|
||||
}
|
||||
);
|
||||
|
||||
export const useAppSetting = () => {
|
||||
return useAtom(appSettingAtom);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { Generator } from '@blocksuite/store';
|
||||
import type React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -11,10 +10,7 @@ import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
|
||||
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
'test',
|
||||
WorkspaceFlavour.LOCAL,
|
||||
{
|
||||
idGenerator: Generator.AutoIncrement,
|
||||
}
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
|
||||
const page = blockSuiteWorkspace.createPage({ id: 'page0' });
|
||||
|
||||
@@ -4,8 +4,16 @@ import type {
|
||||
WorkspaceNotFoundError,
|
||||
} from '@affine/env/constant';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import {
|
||||
rootCurrentPageIdAtom,
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
rootWorkspacesMetadataAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { rootStore } from '@toeverything/plugin-infra/manager';
|
||||
import { useAtomValue } from 'jotai/react';
|
||||
import { Provider } from 'jotai/react';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import type { ErrorInfo, ReactNode } from 'react';
|
||||
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
|
||||
import type React from 'react';
|
||||
import { Component } from 'react';
|
||||
|
||||
@@ -24,6 +32,33 @@ interface AffineErrorBoundaryState {
|
||||
error: AffineError | null;
|
||||
}
|
||||
|
||||
export const DumpInfo = (props: Pick<AffineErrorBoundaryProps, 'router'>) => {
|
||||
const router = props.router;
|
||||
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
|
||||
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
|
||||
const path = router.asPath;
|
||||
const query = router.query;
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
Please copy the following information and send it to the developer.
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid red',
|
||||
}}
|
||||
>
|
||||
<div>path: {path}</div>
|
||||
<div>query: {JSON.stringify(query)}</div>
|
||||
<div>currentWorkspaceId: {currentWorkspaceId}</div>
|
||||
<div>currentPageId: {currentPageId}</div>
|
||||
<div>metadata: {JSON.stringify(metadata)}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export class AffineErrorBoundary extends Component<
|
||||
AffineErrorBoundaryProps,
|
||||
AffineErrorBoundaryState
|
||||
@@ -44,9 +79,10 @@ export class AffineErrorBoundary extends Component<
|
||||
|
||||
public override render(): ReactNode {
|
||||
if (this.state.error) {
|
||||
let errorDetail: ReactElement | null = null;
|
||||
const error = this.state.error;
|
||||
if (error instanceof PageNotFoundError) {
|
||||
return (
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
<>
|
||||
@@ -76,11 +112,20 @@ export class AffineErrorBoundary extends Component<
|
||||
</>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
{error.message ?? error.toString()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
{error.message ?? error.toString()}
|
||||
{errorDetail}
|
||||
<Provider key="JotaiProvider" store={rootStore}>
|
||||
<DumpInfo router={this.props.router} />
|
||||
</Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ export const CreateWorkspaceModal = ({
|
||||
const onConfirmEnableCloudSyncing = useCallback(
|
||||
(enableCloudSyncing: boolean) => {
|
||||
(async function () {
|
||||
if (!runtimeConfig.enableLegacyCloud && enableCloudSyncing) {
|
||||
if (!runtimeConfig.enableCloud && enableCloudSyncing) {
|
||||
setOpenDisableCloudAlertModal(true);
|
||||
} else {
|
||||
let id = addedId;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Menu, MenuItem, MenuTrigger, styled } from '@affine/component';
|
||||
import {
|
||||
type ButtonProps,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuTrigger,
|
||||
styled,
|
||||
} from '@affine/component';
|
||||
import { LOCALES } from '@affine/i18n';
|
||||
import { useI18N } from '@affine/i18n';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
@@ -41,7 +47,9 @@ const LanguageMenuContent: FC<{
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const LanguageMenu: FC = () => {
|
||||
export const LanguageMenu: FC<{ triggerProps: ButtonProps }> = ({
|
||||
triggerProps,
|
||||
}) => {
|
||||
const i18n = useI18N();
|
||||
|
||||
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
|
||||
@@ -62,6 +70,7 @@ export const LanguageMenu: FC = () => {
|
||||
<MenuTrigger
|
||||
data-testid="language-menu-button"
|
||||
style={{ textTransform: 'capitalize' }}
|
||||
{...triggerProps}
|
||||
>
|
||||
{currentLanguage?.originalName}
|
||||
</MenuTrigger>
|
||||
|
||||
@@ -20,7 +20,7 @@ interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspace: AffineOfficialWorkspace;
|
||||
onDeleteWorkspace: () => Promise<void>;
|
||||
onDeleteWorkspace: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const WorkspaceDeleteModal = ({
|
||||
@@ -30,23 +30,24 @@ export const WorkspaceDeleteModal = ({
|
||||
onDeleteWorkspace,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace ?? null
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
const [deleteStr, setDeleteStr] = useState<string>('');
|
||||
const allowDelete = deleteStr === workspaceName;
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
onDeleteWorkspace()
|
||||
onDeleteWorkspace(workspace.id)
|
||||
.then(() => {
|
||||
toast(t['Successfully deleted'](), {
|
||||
portal: document.body,
|
||||
});
|
||||
onClose();
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore error
|
||||
});
|
||||
}, [onDeleteWorkspace, t]);
|
||||
}, [onClose, onDeleteWorkspace, t, workspace.id]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
|
||||
@@ -9,9 +9,9 @@ import type {
|
||||
} from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import type { FC } from 'react';
|
||||
import { type FC, useMemo } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { useWorkspace } from '../../../hooks/use-workspace';
|
||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||
import { ExportPanel } from './export';
|
||||
import { ProfilePanel } from './profile';
|
||||
@@ -19,11 +19,11 @@ import { PublishPanel } from './publish';
|
||||
import { StoragePanel } from './storage';
|
||||
|
||||
export type WorkspaceSettingDetailProps = {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
onDeleteWorkspace: () => Promise<void>;
|
||||
workspaceId: string;
|
||||
onDeleteWorkspace: (id: string) => Promise<void>;
|
||||
onTransferWorkspace: <
|
||||
From extends WorkspaceFlavour,
|
||||
To extends WorkspaceFlavour
|
||||
To extends WorkspaceFlavour,
|
||||
>(
|
||||
from: From,
|
||||
to: To,
|
||||
@@ -32,13 +32,29 @@ export type WorkspaceSettingDetailProps = {
|
||||
};
|
||||
|
||||
export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
|
||||
workspace,
|
||||
workspaceId,
|
||||
onDeleteWorkspace,
|
||||
...props
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
|
||||
const storageAndExportSetting = useMemo(() => {
|
||||
if (environment.isDesktop) {
|
||||
return (
|
||||
<SettingWrapper title={t['Storage and Export']()}>
|
||||
{runtimeConfig.enableMoveDatabase ? (
|
||||
<StoragePanel workspace={workspace} />
|
||||
) : null}
|
||||
<ExportPanel workspace={workspace} />
|
||||
</SettingWrapper>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [t, workspace]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
@@ -63,13 +79,7 @@ export const WorkspaceSettingDetail: FC<WorkspaceSettingDetailProps> = ({
|
||||
{...props}
|
||||
/>
|
||||
</SettingWrapper>
|
||||
{environment.isDesktop ? (
|
||||
<SettingWrapper title={t['Storage and Export']()}>
|
||||
<StoragePanel workspace={workspace} />
|
||||
<ExportPanel workspace={workspace} />
|
||||
</SettingWrapper>
|
||||
) : null}
|
||||
|
||||
{storageAndExportSetting}
|
||||
<SettingWrapper>
|
||||
<DeleteLeaveWorkspace
|
||||
workspace={workspace}
|
||||
|
||||
@@ -37,6 +37,7 @@ export const ProfilePanel: FC<{
|
||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
|
||||
const [name, setName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
@@ -63,7 +64,10 @@ export const ProfilePanel: FC<{
|
||||
<div className="camera-icon-wrapper">
|
||||
<CameraIcon />
|
||||
</div>
|
||||
<WorkspaceAvatar size={56} workspace={workspace} />
|
||||
<WorkspaceAvatar
|
||||
size={56}
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
</>
|
||||
</Upload>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, FlexWrapper, Switch } from '@affine/component';
|
||||
import { Button, FlexWrapper, Switch, Tooltip } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type {
|
||||
@@ -18,13 +18,22 @@ import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
||||
import type { WorkspaceSettingDetailProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
export type PublishPanelProps = WorkspaceSettingDetailProps & {
|
||||
export type PublishPanelProps = Omit<
|
||||
WorkspaceSettingDetailProps,
|
||||
'workspaceId'
|
||||
> & {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
};
|
||||
export type PublishPanelLocalProps = WorkspaceSettingDetailProps & {
|
||||
export type PublishPanelLocalProps = Omit<
|
||||
WorkspaceSettingDetailProps,
|
||||
'workspaceId'
|
||||
> & {
|
||||
workspace: LocalWorkspace;
|
||||
};
|
||||
export type PublishPanelAffineProps = WorkspaceSettingDetailProps & {
|
||||
export type PublishPanelAffineProps = Omit<
|
||||
WorkspaceSettingDetailProps,
|
||||
'workspaceId'
|
||||
> & {
|
||||
workspace: AffineCloudWorkspace;
|
||||
};
|
||||
|
||||
@@ -83,30 +92,19 @@ const PublishPanelAffine: FC<PublishPanelAffineProps> = props => {
|
||||
|
||||
const FakePublishPanelAffine: FC<{
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}> = ({ workspace }) => {
|
||||
}> = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [origin, setOrigin] = useState('');
|
||||
const shareUrl = origin + '/public-workspace/' + workspace.id;
|
||||
|
||||
useEffect(() => {
|
||||
setOrigin(
|
||||
typeof window !== 'undefined' && window.location.origin
|
||||
? window.location.origin
|
||||
: ''
|
||||
);
|
||||
}, []);
|
||||
return (
|
||||
<div className={style.fakeWrapper}>
|
||||
<SettingRow name={t['Publish']()} desc={t['Unpublished hint']()}>
|
||||
<Switch checked={false} />
|
||||
</SettingRow>
|
||||
<FlexWrapper justifyContent="space-between">
|
||||
<Button className={style.urlButton} size="middle" title={shareUrl}>
|
||||
{shareUrl}
|
||||
</Button>
|
||||
<Button size="middle">{t['Copy']()}</Button>
|
||||
</FlexWrapper>
|
||||
</div>
|
||||
<Tooltip
|
||||
content={t['com.affine.settings.workspace.publish.local-tooltip']()}
|
||||
placement="top"
|
||||
>
|
||||
<div className={style.fakeWrapper}>
|
||||
<SettingRow name={t['Publish']()} desc={t['Unpublished hint']()}>
|
||||
<Switch checked={false} />
|
||||
</SettingRow>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
|
||||
@@ -141,7 +139,7 @@ const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<FakePublishPanelAffine workspace={workspace} />
|
||||
{runtimeConfig.enableLegacyCloud ? (
|
||||
{runtimeConfig.enableCloud ? (
|
||||
<EnableAffineCloudModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Button, toast } from '@affine/component';
|
||||
import { Button, FlexWrapper, toast, Tooltip } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMemo } from 'react';
|
||||
import { type FC, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import * as style from './style.css';
|
||||
|
||||
const useShowOpenDBFile = (workspaceId: string) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||
const [path, setPath] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (window.apis && window.events && environment.isDesktop) {
|
||||
window.apis?.workspace
|
||||
.getMeta(workspaceId)
|
||||
.then(meta => {
|
||||
setShow(!!meta.secondaryDBPath);
|
||||
setPath(meta.secondaryDBPath);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
@@ -20,12 +22,12 @@ const useShowOpenDBFile = (workspaceId: string) => {
|
||||
return window.events.workspace.onMetaChange((newMeta: any) => {
|
||||
if (newMeta.workspaceId === workspaceId) {
|
||||
const meta = newMeta.meta;
|
||||
setShow(!!meta.secondaryDBPath);
|
||||
setPath(meta.secondaryDBPath);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [workspaceId]);
|
||||
return show;
|
||||
return path;
|
||||
};
|
||||
|
||||
export const StoragePanel: FC<{
|
||||
@@ -33,7 +35,7 @@ export const StoragePanel: FC<{
|
||||
}> = ({ workspace }) => {
|
||||
const workspaceId = workspace.id;
|
||||
const t = useAFFiNEI18N();
|
||||
const showOpenFolder = useShowOpenDBFile(workspaceId);
|
||||
const secondaryPath = useDBFileSecondaryPath(workspaceId);
|
||||
|
||||
const [moveToInProgress, setMoveToInProgress] = useState<boolean>(false);
|
||||
const onRevealDBFile = useCallback(() => {
|
||||
@@ -65,23 +67,57 @@ export const StoragePanel: FC<{
|
||||
});
|
||||
}, [moveToInProgress, t, workspaceId]);
|
||||
|
||||
if (!showOpenFolder) {
|
||||
return null;
|
||||
}
|
||||
const rowContent = useMemo(
|
||||
() =>
|
||||
secondaryPath ? (
|
||||
<FlexWrapper justifyContent="space-between">
|
||||
<Tooltip
|
||||
zIndex={1000}
|
||||
content={t['com.affine.settings.storage.db-location.change-hint']()}
|
||||
placement="top-start"
|
||||
>
|
||||
<Button
|
||||
data-testid="move-folder"
|
||||
className={style.urlButton}
|
||||
size="middle"
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
{secondaryPath}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
data-testid="reveal-folder"
|
||||
data-disabled={moveToInProgress}
|
||||
onClick={onRevealDBFile}
|
||||
>
|
||||
{t['Open folder']()}
|
||||
</Button>
|
||||
</FlexWrapper>
|
||||
) : (
|
||||
<Button
|
||||
size="small"
|
||||
data-testid="move-folder"
|
||||
data-disabled={moveToInProgress}
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
{t['Move folder']()}
|
||||
</Button>
|
||||
),
|
||||
[handleMoveTo, moveToInProgress, onRevealDBFile, secondaryPath, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
name={t['Storage']()}
|
||||
desc={t['Storage Folder Hint']()}
|
||||
spreadCol={false}
|
||||
desc={t[
|
||||
secondaryPath
|
||||
? 'com.affine.settings.storage.description-alt'
|
||||
: 'com.affine.settings.storage.description'
|
||||
]()}
|
||||
spreadCol={!secondaryPath}
|
||||
>
|
||||
<Button
|
||||
data-testid="move-folder"
|
||||
data-disabled={moveToInProgress}
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
{t['Move folder']()}
|
||||
</Button>
|
||||
<Button onClick={onRevealDBFile}>{t['Open folder']()}</Button>
|
||||
{rowContent}
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,16 +43,22 @@ globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||
|
||||
export const urlButton = style({
|
||||
width: 'calc(100% - 64px - 15px)',
|
||||
justifyContent: 'left',
|
||||
textAlign: 'left',
|
||||
});
|
||||
globalStyle(`${urlButton} span`, {
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: 'var(--affine-placeholder-color)',
|
||||
fontWeight: '500',
|
||||
});
|
||||
|
||||
export const fakeWrapper = style({
|
||||
position: 'relative',
|
||||
opacity: 0.4,
|
||||
marginTop: '24px',
|
||||
selectors: {
|
||||
'&::after': {
|
||||
content: '""',
|
||||
@@ -61,7 +67,7 @@ export const fakeWrapper = style({
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
background: 'var(--affine-white-60)',
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
20
apps/web/src/components/affine/onboarding-modal.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { TourModal } from '@affine/component/tour-modal';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
import { openOnboardingModalAtom } from '../../atoms';
|
||||
import { guideOnboardingAtom } from '../../atoms/guide';
|
||||
|
||||
export const OnboardingModal: FC = memo(function OnboardingModal() {
|
||||
const [open, setOpen] = useAtom(openOnboardingModalAtom);
|
||||
const [guideOpen, setShowOnboarding] = useAtom(guideOnboardingAtom);
|
||||
const onCloseTourModal = useCallback(() => {
|
||||
setShowOnboarding(false);
|
||||
setOpen(false);
|
||||
}, [setOpen, setShowOnboarding]);
|
||||
|
||||
return (
|
||||
<TourModal open={!open ? guideOpen : open} onClose={onCloseTourModal} />
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
DiscordIcon,
|
||||
GithubIcon,
|
||||
RedditIcon,
|
||||
TelegramIcon,
|
||||
TwitterIcon,
|
||||
YouTubeIcon,
|
||||
} from './icons';
|
||||
|
||||
export const relatedLinks = [
|
||||
{
|
||||
icon: <GithubIcon />,
|
||||
title: 'GitHub',
|
||||
link: 'https://github.com/toeverything/AFFiNE',
|
||||
},
|
||||
{
|
||||
icon: <TwitterIcon />,
|
||||
title: 'Twitter',
|
||||
link: 'https://twitter.com/AffineOfficial',
|
||||
},
|
||||
{
|
||||
icon: <DiscordIcon />,
|
||||
title: 'Discord',
|
||||
link: 'https://discord.gg/Arn7TqJBvG',
|
||||
},
|
||||
{
|
||||
icon: <YouTubeIcon />,
|
||||
title: 'YouTube',
|
||||
link: 'https://www.youtube.com/@affinepro',
|
||||
},
|
||||
{
|
||||
icon: <TelegramIcon />,
|
||||
title: 'Telegram',
|
||||
link: 'https://t.me/affineworkos',
|
||||
},
|
||||
{
|
||||
icon: <RedditIcon />,
|
||||
title: 'Reddit',
|
||||
link: 'https://www.reddit.com/r/Affine/',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,189 @@
|
||||
export const LogoIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21.1996 0L4 50H14.0741L25.0146 15.4186L35.96 50H46L28.7978 0H21.1996Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const DocIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="50"
|
||||
height="50"
|
||||
viewBox="0 0 50 50"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 40.5353V9.46462C2 6.95444 2.99716 4.54708 4.77212 2.77212C6.54708 0.997163 8.95444 0 11.4646 0H37.7552C39.0224 0 40.0497 1.02726 40.0497 2.29445V33.3652C40.0497 33.4357 40.0465 33.5055 40.0403 33.5744C39.9882 34.1502 39.7234 34.6646 39.3251 35.0385C38.9147 35.4237 38.3625 35.6597 37.7552 35.6597H11.4646C11.0129 35.6597 10.5676 35.7224 10.1404 35.8429C8.60419 36.2781 7.37011 37.4505 6.85245 38.9541C6.67955 39.4584 6.58891 39.9922 6.58891 40.5354C6.58891 41.8285 7.1026 43.0687 8.01697 43.983C8.93134 44.8974 10.1715 45.4111 11.4646 45.4111H42.6309V4.68456C42.6309 3.41736 43.6582 2.3901 44.9254 2.3901C46.1926 2.3901 47.2198 3.41736 47.2198 4.68456V47.7055C47.2198 48.9727 46.1926 50 44.9254 50H11.4646C8.95445 50 6.54708 49.0028 4.77212 47.2279C2.99716 45.4529 2 43.0456 2 40.5353ZM12.6596 38.2409C11.3925 38.2409 10.3652 39.2682 10.3652 40.5354C10.3652 41.8026 11.3925 42.8298 12.6596 42.8298H36.5602C37.8274 42.8298 38.8546 41.8026 38.8546 40.5354C38.8546 39.2682 37.8274 38.2409 36.5602 38.2409H12.6596Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TwitterIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M22 5.88235C21.2639 6.21176 20.4704 6.42824 19.6482 6.53176C20.4895 6.03294 21.1396 5.24235 21.4455 4.29176C20.652 4.76235 19.7725 5.09176 18.8451 5.28C18.0899 4.47059 17.0287 4 15.8241 4C13.5774 4 11.7419 5.80706 11.7419 8.03765C11.7419 8.35765 11.7801 8.66824 11.847 8.96C8.4436 8.79059 5.413 7.18118 3.39579 4.74353C3.04207 5.33647 2.8413 6.03294 2.8413 6.76706C2.8413 8.16941 3.55832 9.41176 4.6673 10.1176C3.98853 10.1176 3.35755 9.92941 2.80306 9.64706V9.67529C2.80306 11.6329 4.21797 13.2706 6.09178 13.6376C5.49018 13.7997 4.8586 13.8223 4.24665 13.7035C4.50632 14.5059 5.01485 15.2079 5.70078 15.711C6.38671 16.2141 7.21553 16.4929 8.07075 16.5082C6.62106 17.6381 4.82409 18.2488 2.97514 18.24C2.6501 18.24 2.32505 18.2212 2 18.1835C3.81644 19.3318 5.97706 20 8.29063 20C15.8241 20 19.9637 13.8447 19.9637 8.50824C19.9637 8.32941 19.9637 8.16 19.9541 7.98118C20.7572 7.41647 21.4455 6.70118 22 5.88235Z"
|
||||
fill="#1D9BF0"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const GithubIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_3073_4801)">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.667 2C7.14199 2 2.66699 6.58819 2.66699 12.2529C2.66699 16.7899 5.52949 20.6219 9.50449 21.9804C10.0045 22.0701 10.192 21.7625 10.192 21.4934C10.192 21.2499 10.1795 20.4425 10.1795 19.5838C7.66699 20.058 7.01699 18.9558 6.81699 18.3791C6.70449 18.0843 6.21699 17.1743 5.79199 16.9308C5.44199 16.7386 4.94199 16.2644 5.77949 16.2516C6.56699 16.2388 7.12949 16.9949 7.31699 17.3025C8.21699 18.8533 9.65449 18.4175 10.2295 18.1484C10.317 17.4819 10.5795 17.0334 10.867 16.777C8.64199 16.5207 6.31699 15.6364 6.31699 11.7147C6.31699 10.5997 6.70449 9.67689 7.34199 8.95918C7.24199 8.70286 6.89199 7.65193 7.44199 6.24215C7.44199 6.24215 8.27949 5.97301 10.192 7.29308C10.992 7.06239 11.842 6.94704 12.692 6.94704C13.542 6.94704 14.392 7.06239 15.192 7.29308C17.1045 5.9602 17.942 6.24215 17.942 6.24215C18.492 7.65193 18.142 8.70286 18.042 8.95918C18.6795 9.67689 19.067 10.5868 19.067 11.7147C19.067 15.6492 16.7295 16.5207 14.5045 16.777C14.867 17.0975 15.1795 17.7126 15.1795 18.6738C15.1795 20.0452 15.167 21.1474 15.167 21.4934C15.167 21.7625 15.3545 22.0829 15.8545 21.9804C17.8396 21.2932 19.5646 19.9851 20.7867 18.2401C22.0088 16.4951 22.6664 14.4012 22.667 12.2529C22.667 6.58819 18.192 2 12.667 2Z"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3073_4801">
|
||||
<rect width="25" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const DiscordIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_3073_4801)">
|
||||
<path
|
||||
d="M19.2565 5.64663C17.9898 5.05614 16.6183 4.62755 15.1897 4.37993C15.1772 4.37953 15.1647 4.38188 15.1532 4.38681C15.1417 4.39175 15.1314 4.39915 15.1231 4.4085C14.9516 4.72279 14.7516 5.13233 14.6183 5.44662C13.103 5.21804 11.562 5.21804 10.0467 5.44662C9.9134 5.1228 9.71339 4.72279 9.53243 4.4085C9.52291 4.38945 9.49434 4.37993 9.46576 4.37993C8.03715 4.62755 6.67521 5.05614 5.39899 5.64663C5.38946 5.64663 5.37994 5.65615 5.37041 5.66568C2.77987 9.54197 2.06556 13.3135 2.41795 17.0469C2.41795 17.066 2.42748 17.085 2.44652 17.0946C4.16086 18.3517 5.80852 19.1137 7.43714 19.6184C7.46571 19.628 7.49428 19.6184 7.50381 19.5994C7.88477 19.0756 8.22764 18.5232 8.52288 17.9422C8.54193 17.9041 8.52288 17.866 8.48479 17.8565C7.94191 17.647 7.42761 17.3993 6.92284 17.1136C6.88474 17.0946 6.88474 17.0374 6.91331 17.0088C7.01808 16.9327 7.12284 16.8469 7.22761 16.7707C7.24666 16.7517 7.27523 16.7517 7.29428 16.7612C10.5706 18.2565 14.104 18.2565 17.3422 16.7612C17.3612 16.7517 17.3898 16.7517 17.4088 16.7707C17.5136 16.8565 17.6184 16.9327 17.7231 17.0184C17.7612 17.0469 17.7612 17.1041 17.7136 17.1231C17.2184 17.4184 16.6945 17.6565 16.1517 17.866C16.1136 17.8755 16.104 17.9232 16.1136 17.9517C16.4183 18.5327 16.7612 19.0851 17.1326 19.6089C17.1612 19.6184 17.1898 19.628 17.2184 19.6184C18.8565 19.1137 20.5042 18.3517 22.2185 17.0946C22.2375 17.085 22.2471 17.066 22.2471 17.0469C22.6661 12.7325 21.5518 8.98958 19.2946 5.66568C19.2851 5.65615 19.2756 5.64663 19.2565 5.64663ZM9.01813 14.7707C8.03715 14.7707 7.21808 13.8659 7.21808 12.7516C7.21808 11.6373 8.01811 10.7325 9.01813 10.7325C10.0277 10.7325 10.8277 11.6468 10.8182 12.7516C10.8182 13.8659 10.0182 14.7707 9.01813 14.7707ZM15.6564 14.7707C14.6754 14.7707 13.8564 13.8659 13.8564 12.7516C13.8564 11.6373 14.6564 10.7325 15.6564 10.7325C16.666 10.7325 17.466 11.6468 17.4565 12.7516C17.4565 13.8659 16.666 14.7707 15.6564 14.7707Z"
|
||||
fill="#5865F2"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3073_4801">
|
||||
<rect width="25" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TelegramIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2C9.34844 2 6.80312 3.05422 4.92969 4.92891C3.05432 6.80434 2.00052 9.34778 2 12C2 14.6511 3.05469 17.1964 4.92969 19.0711C6.80312 20.9458 9.34844 22 12 22C14.6516 22 17.1969 20.9458 19.0703 19.0711C20.9453 17.1964 22 14.6511 22 12C22 9.34891 20.9453 6.80359 19.0703 4.92891C17.1969 3.05422 14.6516 2 12 2Z"
|
||||
fill="url(#paint0_linear_8233_169329)"
|
||||
/>
|
||||
<path
|
||||
d="M6.5267 11.8943C9.44232 10.6243 11.3861 9.78694 12.3579 9.38241C15.1361 8.22726 15.7126 8.02663 16.0892 8.01983C16.172 8.01851 16.3564 8.03898 16.4767 8.13624C16.5767 8.21827 16.6048 8.32921 16.6189 8.4071C16.6314 8.48491 16.6486 8.66226 16.6345 8.80069C16.4845 10.3819 15.8329 14.2191 15.5017 15.9902C15.3626 16.7396 15.0861 16.9908 14.8189 17.0154C14.2376 17.0688 13.797 16.6316 13.2345 16.263C12.3548 15.686 11.8579 15.3269 11.0033 14.764C10.0158 14.1134 10.6564 13.7557 11.2189 13.1713C11.3658 13.0184 13.9251 10.691 13.9736 10.4799C13.9798 10.4535 13.9861 10.3551 13.9267 10.3032C13.8689 10.2512 13.7829 10.269 13.7204 10.283C13.6314 10.303 12.2267 11.2324 9.5017 13.071C9.10326 13.3451 8.74232 13.4787 8.41732 13.4716C8.06107 13.464 7.37357 13.2698 6.86264 13.1038C6.23764 12.9002 5.7392 12.7926 5.78295 12.4468C5.80482 12.2668 6.05326 12.0826 6.5267 11.8943Z"
|
||||
fill="white"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_8233_169329"
|
||||
x1="1002"
|
||||
y1="2"
|
||||
x2="1002"
|
||||
y2="2002"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#2AABEE" />
|
||||
<stop offset="1" stopColor="#229ED9" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const RedditIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12.334 22C17.8568 22 22.334 17.5228 22.334 12C22.334 6.47715 17.8568 2 12.334 2C6.81114 2 2.33398 6.47715 2.33398 12C2.33398 17.5228 6.81114 22 12.334 22Z"
|
||||
fill="#FF4500"
|
||||
/>
|
||||
<path
|
||||
d="M18.9863 12.0954C18.9863 11.2848 18.3308 10.641 17.5319 10.641C17.1545 10.6404 16.7915 10.7857 16.5186 11.0463C15.5172 10.331 14.1461 9.86611 12.6202 9.8065L13.2877 6.68299L15.4574 7.14783C15.4814 7.69627 15.9343 8.13744 16.4948 8.13744C17.067 8.13744 17.5319 7.6726 17.5319 7.1001C17.5319 6.52791 17.067 6.06299 16.4948 6.06299C16.0895 6.06299 15.7316 6.30143 15.5648 6.64721L13.1448 6.13455C13.0732 6.12252 13.0016 6.13455 12.9539 6.17033C12.8943 6.20611 12.8586 6.26564 12.8468 6.33721L12.1074 9.8183C10.5577 9.86611 9.16273 10.331 8.14945 11.0583C7.87653 10.7976 7.51349 10.6524 7.13609 10.653C6.32539 10.653 5.68164 11.3085 5.68164 12.1074C5.68164 12.7035 6.03922 13.2041 6.54008 13.4308C6.51576 13.5766 6.50379 13.7241 6.5043 13.8719C6.5043 16.113 9.11524 17.9372 12.3341 17.9372C15.553 17.9372 18.1639 16.125 18.1639 13.8719C18.1638 13.7241 18.1519 13.5766 18.1281 13.4308C18.6288 13.2041 18.9863 12.6914 18.9863 12.0954ZM8.99586 13.1325C8.99586 12.5603 9.4607 12.0954 10.0332 12.0954C10.6054 12.0954 11.0703 12.5603 11.0703 13.1325C11.0703 13.7048 10.6055 14.1699 10.0332 14.1699C9.46078 14.1816 8.99586 13.7048 8.99586 13.1325ZM14.8019 15.8865C14.0866 16.6019 12.7274 16.6496 12.3341 16.6496C11.9288 16.6496 10.5697 16.5899 9.86609 15.8865C9.75898 15.7792 9.75898 15.6123 9.86609 15.505C9.97344 15.3979 10.1403 15.3979 10.2477 15.505C10.7008 15.9581 11.6545 16.113 12.3341 16.113C13.0137 16.113 13.9792 15.9581 14.4203 15.505C14.5277 15.3979 14.6945 15.3979 14.8019 15.505C14.8972 15.6123 14.8972 15.7792 14.8019 15.8865ZM14.611 14.1817C14.0387 14.1817 13.5739 13.7168 13.5739 13.1446C13.5739 12.5723 14.0387 12.1074 14.611 12.1074C15.1834 12.1074 15.6483 12.5723 15.6483 13.1446C15.6483 13.7047 15.1834 14.1817 14.611 14.1817Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const LinkIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10.2917 1.33334C10.2917 0.988166 10.5715 0.708344 10.9167 0.708344H14.6667C15.0118 0.708344 15.2917 0.988166 15.2917 1.33334V5.08334C15.2917 5.42852 15.0118 5.70834 14.6667 5.70834C14.3215 5.70834 14.0417 5.42852 14.0417 5.08334V2.84223L8.44194 8.44195C8.19787 8.68603 7.80214 8.68603 7.55806 8.44195C7.31398 8.19787 7.31398 7.80215 7.55806 7.55807L13.1578 1.95834H10.9167C10.5715 1.95834 10.2917 1.67852 10.2917 1.33334ZM3.97464 1.54168L7.58334 1.54168C7.92851 1.54168 8.20834 1.8215 8.20834 2.16668C8.20834 2.51185 7.92851 2.79168 7.58334 2.79168H4C3.52298 2.79168 3.2028 2.79216 2.95623 2.81231C2.71697 2.83186 2.60256 2.86676 2.5271 2.90521C2.33109 3.00508 2.17174 3.16443 2.07187 3.36044C2.03342 3.4359 1.99852 3.55031 1.97897 3.78957C1.95882 4.03614 1.95834 4.35632 1.95834 4.83334V12C1.95834 12.477 1.95882 12.7972 1.97897 13.0438C1.99852 13.283 2.03342 13.3974 2.07187 13.4729C2.17174 13.6689 2.33109 13.8283 2.5271 13.9281C2.60256 13.9666 2.71697 14.0015 2.95623 14.021C3.2028 14.0412 3.52298 14.0417 4 14.0417H11.1667C11.6437 14.0417 11.9639 14.0412 12.2104 14.021C12.4497 14.0015 12.5641 13.9666 12.6396 13.9281C12.8356 13.8283 12.9949 13.6689 13.0948 13.4729C13.1333 13.3974 13.1682 13.283 13.1877 13.0438C13.2079 12.7972 13.2083 12.477 13.2083 12V8.41668C13.2083 8.0715 13.4882 7.79168 13.8333 7.79168C14.1785 7.79168 14.4583 8.0715 14.4583 8.41668V12.0254C14.4583 12.4705 14.4584 12.842 14.4336 13.1456C14.4077 13.4621 14.3518 13.7594 14.2086 14.0404C13.9888 14.4716 13.6383 14.8222 13.2071 15.0419C12.926 15.1851 12.6288 15.241 12.3122 15.2669C12.0087 15.2917 11.6372 15.2917 11.192 15.2917H3.97463C3.5295 15.2917 3.15797 15.2917 2.85444 15.2669C2.53787 15.241 2.24066 15.1851 1.95961 15.0419C1.5284 14.8222 1.17782 14.4716 0.958113 14.0404C0.81491 13.7594 0.758984 13.4621 0.733119 13.1456C0.70832 12.842 0.708327 12.4705 0.708336 12.0254V4.80798C0.708327 4.36285 0.70832 3.99131 0.733119 3.68779C0.758984 3.37121 0.81491 3.074 0.958113 2.79295C1.17782 2.36174 1.5284 2.01116 1.95961 1.79145C2.24066 1.64825 2.53787 1.59232 2.85444 1.56646C3.15797 1.54166 3.52951 1.54167 3.97464 1.54168Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export const YouTubeIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="25"
|
||||
height="24"
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M21.7477 7.19232C21.6387 6.76858 21.4261 6.38227 21.1311 6.07186C20.8361 5.76145 20.4689 5.53776 20.0662 5.42308C18.5917 5 12.6575 5 12.6575 5C12.6575 5 6.72304 5.01281 5.24858 5.43589C4.84583 5.55057 4.47865 5.77427 4.18363 6.0847C3.88861 6.39512 3.67602 6.78145 3.56705 7.2052C3.12106 9.96155 2.94806 14.1616 3.5793 16.8077C3.68828 17.2314 3.90087 17.6177 4.19589 17.9281C4.49092 18.2386 4.85808 18.4622 5.26083 18.5769C6.73528 19 12.6696 19 12.6696 19C12.6696 19 18.6039 19 20.0783 18.5769C20.481 18.4623 20.8482 18.2386 21.1432 17.9282C21.4383 17.6177 21.6509 17.2314 21.7599 16.8077C22.2303 14.0474 22.3752 9.85004 21.7477 7.1924V7.19232Z"
|
||||
fill="#FF0000"
|
||||
/>
|
||||
<path d="M10.667 15L15.667 12L10.667 9V15Z" fill="white" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Switch } from '@affine/component';
|
||||
import { relatedLinks } from '@affine/component/contact-modal';
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { SettingWrapper } from '@affine/component/setting-components';
|
||||
@@ -8,6 +7,7 @@ import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { type AppSetting, useAppSetting } from '../../../../../atoms/settings';
|
||||
import { relatedLinks } from './config';
|
||||
import { communityItem, communityWrapper, link } from './style.css';
|
||||
|
||||
export const AboutAffine = () => {
|
||||
@@ -26,45 +26,49 @@ export const AboutAffine = () => {
|
||||
subtitle={t['com.affine.settings.about.message']()}
|
||||
data-testid="about-title"
|
||||
/>
|
||||
{runtimeConfig.enableNewSettingUnstableApi && environment.isDesktop ? (
|
||||
<SettingWrapper title={t['Version']()}>
|
||||
<SettingRow
|
||||
name={t['Check for updates']()}
|
||||
desc={t['New version is ready']()}
|
||||
></SettingRow>
|
||||
<SettingRow
|
||||
name={t['Check for updates automatically']()}
|
||||
desc={t['com.affine.settings.about.update.check.message']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoCheckUpdate}
|
||||
onChange={checked => changeSwitch('autoCheckUpdate', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['Download updates automatically']()}
|
||||
desc={t['com.affine.settings.about.update.download.message']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoCheckUpdate}
|
||||
onChange={checked => changeSwitch('autoCheckUpdate', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[`Discover what's new`]()}
|
||||
desc={t['View the AFFiNE Changelog.']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://affine.pro/blog/whats-new-affine-0630',
|
||||
'_blank'
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
) : null}
|
||||
<SettingWrapper title={t['Version']()}>
|
||||
<SettingRow name="App Version" desc={runtimeConfig.appVersion} />
|
||||
<SettingRow name="Editor Version" desc={runtimeConfig.editorVersion} />
|
||||
{runtimeConfig.enableNewSettingUnstableApi && environment.isDesktop ? (
|
||||
<>
|
||||
<SettingRow
|
||||
name={t['Check for updates']()}
|
||||
desc={t['New version is ready']()}
|
||||
></SettingRow>
|
||||
<SettingRow
|
||||
name={t['Check for updates automatically']()}
|
||||
desc={t['com.affine.settings.about.update.check.message']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoCheckUpdate}
|
||||
onChange={checked => changeSwitch('autoCheckUpdate', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['Download updates automatically']()}
|
||||
desc={t['com.affine.settings.about.update.download.message']()}
|
||||
>
|
||||
<Switch
|
||||
checked={appSettings.autoCheckUpdate}
|
||||
onChange={checked => changeSwitch('autoCheckUpdate', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t[`Discover what's new`]()}
|
||||
desc={t['View the AFFiNE Changelog.']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://affine.pro/blog/whats-new-affine-0630',
|
||||
'_blank'
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
</>
|
||||
) : null}
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['Contact with us']()}>
|
||||
<a className={link} href="https://affine.pro" target="_blank">
|
||||
{t['Official Website']()}
|
||||
|
||||
@@ -23,23 +23,21 @@ globalStyle(`${link} .icon`, {
|
||||
|
||||
export const communityWrapper = style({
|
||||
display: 'grid',
|
||||
justifyContent: 'space-between',
|
||||
gridTemplateColumns: 'repeat(auto-fill, 84px)',
|
||||
gridGap: '6px',
|
||||
gridTemplateColumns: '15% 15% 15% 15% 15% 15%',
|
||||
gap: '2%',
|
||||
});
|
||||
export const communityItem = style({
|
||||
width: '84px',
|
||||
height: '58px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
cursor: 'pointer',
|
||||
padding: '6px 8px',
|
||||
});
|
||||
globalStyle(`${communityItem} svg`, {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
display: 'block',
|
||||
margin: '8px auto 4px',
|
||||
margin: '0 auto 2px',
|
||||
});
|
||||
globalStyle(`${communityItem} p`, {
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
type AppSetting,
|
||||
fontStyleOptions,
|
||||
useAppSetting,
|
||||
windowFrameStyleOptions,
|
||||
} from '../../../../../atoms/settings';
|
||||
@@ -21,6 +22,7 @@ export const ThemeSettings = () => {
|
||||
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
defaultValue={theme}
|
||||
onValueChange={useCallback(
|
||||
@@ -30,19 +32,56 @@ export const ThemeSettings = () => {
|
||||
[setTheme]
|
||||
)}
|
||||
>
|
||||
<RadioButton value="system" data-testid="system-theme-trigger">
|
||||
<RadioButton
|
||||
bold={true}
|
||||
value="system"
|
||||
data-testid="system-theme-trigger"
|
||||
>
|
||||
{t['system']()}
|
||||
</RadioButton>
|
||||
<RadioButton value="light" data-testid="light-theme-trigger">
|
||||
<RadioButton bold={true} value="light" data-testid="light-theme-trigger">
|
||||
{t['light']()}
|
||||
</RadioButton>
|
||||
<RadioButton value="dark" data-testid="dark-theme-trigger">
|
||||
<RadioButton bold={true} value="dark" data-testid="dark-theme-trigger">
|
||||
{t['dark']()}
|
||||
</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const FontFamilySettings = () => {
|
||||
const [appSettings, setAppSettings] = useAppSetting();
|
||||
return (
|
||||
<RadioButtonGroup
|
||||
width={250}
|
||||
className={settingWrapper}
|
||||
defaultValue={appSettings.fontStyle}
|
||||
onValueChange={useCallback(
|
||||
(key: AppSetting['fontStyle']) => {
|
||||
setAppSettings({ fontStyle: key });
|
||||
},
|
||||
[setAppSettings]
|
||||
)}
|
||||
>
|
||||
{fontStyleOptions.map(({ key, value }) => {
|
||||
return (
|
||||
<RadioButton
|
||||
key={key}
|
||||
bold={true}
|
||||
value={key}
|
||||
data-testid="system-font-style-trigger"
|
||||
style={{
|
||||
fontFamily: value,
|
||||
}}
|
||||
>
|
||||
{key}
|
||||
</RadioButton>
|
||||
);
|
||||
})}
|
||||
</RadioButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const AppearanceSettings = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
@@ -67,15 +106,21 @@ export const AppearanceSettings = () => {
|
||||
>
|
||||
<ThemeSettings />
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['Font Style']()}
|
||||
desc={t['Choose your font style']()}
|
||||
>
|
||||
<FontFamilySettings />
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['Display Language']()}
|
||||
desc={t['Select the language for the interface.']()}
|
||||
>
|
||||
<div className={settingWrapper}>
|
||||
<LanguageMenu />
|
||||
<LanguageMenu triggerProps={{ size: 'small' }} />
|
||||
</div>
|
||||
</SettingRow>
|
||||
{runtimeConfig.enableNewSettingUnstableApi && environment.isDesktop ? (
|
||||
{environment.isDesktop ? (
|
||||
<SettingRow
|
||||
name={t['Client Border Style']()}
|
||||
desc={t['Customize the appearance of the client.']()}
|
||||
@@ -104,6 +149,7 @@ export const AppearanceSettings = () => {
|
||||
>
|
||||
<RadioButtonGroup
|
||||
className={settingWrapper}
|
||||
width={250}
|
||||
defaultValue={appSettings.windowFrameStyle}
|
||||
onValueChange={(value: AppSetting['windowFrameStyle']) => {
|
||||
setAppSettings({ windowFrameStyle: value });
|
||||
|
||||
@@ -2,18 +2,11 @@ import {
|
||||
SettingModal as SettingModalBase,
|
||||
type SettingModalProps,
|
||||
} from '@affine/component/setting-components';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||
import type React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useWorkspaces } from '../../../hooks/use-workspaces';
|
||||
import { AccountSetting } from './account-setting';
|
||||
import {
|
||||
GeneralSetting,
|
||||
@@ -22,80 +15,71 @@ import {
|
||||
} from './general-setting';
|
||||
import { SettingSidebar } from './setting-sidebar';
|
||||
import { settingContent } from './style.css';
|
||||
import type { Workspace } from './type';
|
||||
import { WorkSpaceSetting } from './workspace-setting';
|
||||
import { WorkspaceSetting } from './workspace-setting';
|
||||
|
||||
export const SettingModal: React.FC<SettingModalProps> = ({
|
||||
type ActiveTab = GeneralSettingKeys | 'workspace' | 'account';
|
||||
export type SettingProps = {
|
||||
activeTab: ActiveTab;
|
||||
workspaceId: string | null;
|
||||
onSettingClick: (params: {
|
||||
activeTab: ActiveTab;
|
||||
workspaceId: string | null;
|
||||
}) => void;
|
||||
};
|
||||
export const SettingModal: React.FC<SettingModalProps & SettingProps> = ({
|
||||
open,
|
||||
setOpen,
|
||||
activeTab = 'appearance',
|
||||
workspaceId = null,
|
||||
onSettingClick,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspaces = useWorkspaces();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
|
||||
const generalSettingList = useGeneralSettingList();
|
||||
const workspaceList = useMemo(() => {
|
||||
return workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
||||
) as Workspace[];
|
||||
}, [workspaces]);
|
||||
|
||||
const [currentRef, setCurrentRef] = useState<{
|
||||
workspace: Workspace | null;
|
||||
generalKey: GeneralSettingKeys | null;
|
||||
isAccount: boolean;
|
||||
}>({
|
||||
workspace: null,
|
||||
generalKey: generalSettingList[0].key,
|
||||
isAccount: false,
|
||||
});
|
||||
|
||||
const onGeneralSettingClick = useCallback((key: GeneralSettingKeys) => {
|
||||
setCurrentRef({
|
||||
workspace: null,
|
||||
generalKey: key,
|
||||
isAccount: false,
|
||||
});
|
||||
}, []);
|
||||
const onWorkspaceSettingClick = useCallback((workspace: Workspace) => {
|
||||
setCurrentRef({
|
||||
workspace: workspace,
|
||||
generalKey: null,
|
||||
isAccount: false,
|
||||
});
|
||||
}, []);
|
||||
const onGeneralSettingClick = useCallback(
|
||||
(key: GeneralSettingKeys) => {
|
||||
onSettingClick({
|
||||
activeTab: key,
|
||||
workspaceId: null,
|
||||
});
|
||||
},
|
||||
[onSettingClick]
|
||||
);
|
||||
const onWorkspaceSettingClick = useCallback(
|
||||
(workspaceId: string) => {
|
||||
onSettingClick({
|
||||
activeTab: 'workspace',
|
||||
workspaceId,
|
||||
});
|
||||
},
|
||||
[onSettingClick]
|
||||
);
|
||||
const onAccountSettingClick = useCallback(() => {
|
||||
setCurrentRef({
|
||||
workspace: null,
|
||||
generalKey: null,
|
||||
isAccount: true,
|
||||
});
|
||||
}, []);
|
||||
onSettingClick({ activeTab: 'account', workspaceId: null });
|
||||
}, [onSettingClick]);
|
||||
|
||||
return (
|
||||
<SettingModalBase open={open} setOpen={setOpen}>
|
||||
<SettingSidebar
|
||||
generalSettingList={generalSettingList}
|
||||
onGeneralSettingClick={onGeneralSettingClick}
|
||||
currentWorkspace={
|
||||
currentWorkspace as AffineCloudWorkspace | LocalWorkspace
|
||||
}
|
||||
workspaceList={workspaceList}
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedGeneralKey={currentRef.generalKey}
|
||||
selectedWorkspace={currentRef.workspace}
|
||||
selectedGeneralKey={activeTab}
|
||||
selectedWorkspaceId={workspaceId}
|
||||
onAccountSettingClick={onAccountSettingClick}
|
||||
/>
|
||||
|
||||
<div className={settingContent}>
|
||||
<div data-testid="setting-modal-content" className={settingContent}>
|
||||
<div className="wrapper">
|
||||
<div className="content">
|
||||
{currentRef.workspace ? (
|
||||
<WorkSpaceSetting workspace={currentRef.workspace} />
|
||||
{activeTab === 'workspace' && workspaceId ? (
|
||||
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
|
||||
) : null}
|
||||
{currentRef.generalKey ? (
|
||||
<GeneralSetting generalKey={currentRef.generalKey} />
|
||||
{generalSettingList.find(v => v.key === activeTab) ? (
|
||||
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
|
||||
) : null}
|
||||
{currentRef.isAccount ? <AccountSetting /> : null}
|
||||
{activeTab === 'account' ? <AccountSetting /> : null}
|
||||
</div>
|
||||
<div className="footer">
|
||||
<ContactWithUsIcon />
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import {
|
||||
WorkspaceListItemSkeleton,
|
||||
WorkspaceListSkeleton,
|
||||
} from '@affine/component/setting-components';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/hooks/use-block-suite-workspace';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { FC } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import type {
|
||||
GeneralSettingKeys,
|
||||
GeneralSettingList,
|
||||
} from '../general-setting';
|
||||
import type { Workspace } from '../type';
|
||||
import {
|
||||
accountButton,
|
||||
settingSlideBar,
|
||||
sidebarItemsWrapper,
|
||||
sidebarSelectItem,
|
||||
@@ -22,27 +26,19 @@ import {
|
||||
sidebarTitle,
|
||||
} from './style.css';
|
||||
|
||||
export const SettingSidebar = ({
|
||||
generalSettingList,
|
||||
onGeneralSettingClick,
|
||||
currentWorkspace,
|
||||
workspaceList,
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspace,
|
||||
selectedGeneralKey,
|
||||
onAccountSettingClick,
|
||||
}: {
|
||||
export const SettingSidebar: FC<{
|
||||
generalSettingList: GeneralSettingList;
|
||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
||||
currentWorkspace: Workspace;
|
||||
workspaceList: Workspace[];
|
||||
onWorkspaceSettingClick: (
|
||||
workspace: AffineCloudWorkspace | LocalWorkspace
|
||||
) => void;
|
||||
|
||||
selectedWorkspace: Workspace | null;
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
selectedGeneralKey: string | null;
|
||||
onAccountSettingClick: () => void;
|
||||
}> = ({
|
||||
generalSettingList,
|
||||
onGeneralSettingClick,
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspaceId,
|
||||
selectedGeneralKey,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
@@ -74,67 +70,70 @@ export const SettingSidebar = ({
|
||||
{t['com.affine.settings.workspace']()}
|
||||
</div>
|
||||
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
|
||||
{workspaceList.map(workspace => {
|
||||
return (
|
||||
<WorkspaceListItem
|
||||
key={workspace.id}
|
||||
workspace={workspace}
|
||||
onClick={() => {
|
||||
onWorkspaceSettingClick(workspace);
|
||||
}}
|
||||
isCurrent={workspace.id === currentWorkspace.id}
|
||||
isActive={workspace.id === selectedWorkspace?.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{runtimeConfig.enableCloud && (
|
||||
<div className={accountButton} onClick={onAccountSettingClick}>
|
||||
<UserAvatar
|
||||
size={28}
|
||||
name="Account NameAccount Name"
|
||||
url={''}
|
||||
className="avatar"
|
||||
<Suspense fallback={<WorkspaceListSkeleton />}>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
/>
|
||||
|
||||
<div className="content">
|
||||
<div className="name" title="xxx">
|
||||
Account NameAccount Name
|
||||
</div>
|
||||
<div className="email" title="xxx">
|
||||
xxxxxxxx@gmail.comxxxxxxxx@gmail.com
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceList: FC<{
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
}> = ({ onWorkspaceSettingClick, selectedWorkspaceId }) => {
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
return (
|
||||
<>
|
||||
{workspaces.map(workspace => {
|
||||
return (
|
||||
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
|
||||
<WorkspaceListItem
|
||||
meta={workspace}
|
||||
onClick={() => {
|
||||
onWorkspaceSettingClick(workspace.id);
|
||||
}}
|
||||
isCurrent={workspace.id === currentWorkspace.id}
|
||||
isActive={workspace.id === selectedWorkspaceId}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkspaceListItem = ({
|
||||
workspace,
|
||||
meta,
|
||||
onClick,
|
||||
isCurrent,
|
||||
isActive,
|
||||
}: {
|
||||
workspace: AffineCloudWorkspace | LocalWorkspace;
|
||||
meta: RootWorkspaceMetadata;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
}) => {
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace ?? null
|
||||
);
|
||||
const workspace = useStaticBlockSuiteWorkspace(meta.id);
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
|
||||
return (
|
||||
<div
|
||||
className={clsx(sidebarSelectItem, { active: isActive })}
|
||||
title={workspaceName}
|
||||
onClick={onClick}
|
||||
data-testid="workspace-list-item"
|
||||
>
|
||||
<WorkspaceAvatar size={14} workspace={workspace} className="icon" />
|
||||
<span className="setting-name">{workspaceName}</span>
|
||||
{isCurrent ? <div className="current-label">Current</div> : null}
|
||||
{isCurrent ? (
|
||||
<div className="current-label" data-testid="current-workspace-label">
|
||||
Current
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,6 +34,7 @@ export const sidebarItemsWrapper = style({
|
||||
selectors: {
|
||||
'&.scroll': {
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ export const settingContent = style({
|
||||
flexGrow: '1',
|
||||
height: '100%',
|
||||
padding: '40px 15px 20px',
|
||||
overflowX: 'auto',
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .wrapper`, {
|
||||
@@ -13,7 +13,10 @@ globalStyle(`${settingContent} .wrapper`, {
|
||||
height: '100%',
|
||||
maxWidth: '560px',
|
||||
margin: '0 auto',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .wrapper::-webkit-scrollbar`, {
|
||||
display: 'none',
|
||||
});
|
||||
globalStyle(`${settingContent} .content`, {
|
||||
minHeight: '100%',
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
|
||||
export type Workspace = AffineCloudWorkspace | LocalWorkspace;
|
||||
@@ -1,28 +1,40 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import React, { Suspense, useCallback } from 'react';
|
||||
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
|
||||
import { usePassiveWorkspaceEffect } from '@toeverything/hooks/use-block-suite-workspace';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
|
||||
import { getUIAdapter } from '../../../../adapters/workspace';
|
||||
import { openSettingModalAtom } from '../../../../atoms';
|
||||
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||
import { useWorkspace } from '../../../../hooks/use-workspace';
|
||||
import { useAppHelper } from '../../../../hooks/use-workspaces';
|
||||
import type { Workspace } from '../type';
|
||||
|
||||
export const WorkSpaceSetting = ({ workspace }: { workspace: Workspace }) => {
|
||||
export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
usePassiveWorkspaceEffect(workspace.blockSuiteWorkspace);
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const helper = useAppHelper();
|
||||
const router = useRouter();
|
||||
|
||||
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
|
||||
|
||||
const onDeleteWorkspace = useCallback(async () => {
|
||||
assertExists(currentWorkspace);
|
||||
const workspaceId = currentWorkspace.id;
|
||||
return helper.deleteWorkspace(workspaceId);
|
||||
}, [helper]);
|
||||
const onDeleteWorkspace = useCallback(
|
||||
async (id: string) => {
|
||||
await helper.deleteWorkspace(id);
|
||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||
router.push('/').catch(console.error);
|
||||
},
|
||||
[helper, setSettingModal, router]
|
||||
);
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div>loading</div>}>
|
||||
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
||||
<NewSettingsDetail
|
||||
onTransformWorkspace={onTransformWorkspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
currentWorkspace={workspace}
|
||||
currentWorkspaceId={workspaceId}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -194,7 +194,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
title: pageMeta.title,
|
||||
preview,
|
||||
tags:
|
||||
page?.meta.tags.map(id => tagOptionMap[id]).filter(v => v != null) ??
|
||||
page?.meta.tags?.map(id => tagOptionMap[id]).filter(v => v != null) ??
|
||||
[],
|
||||
favorite: !!pageMeta.favorite,
|
||||
isPublicPage: !!pageMeta.isPublic,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useEffect } from 'react';
|
||||
import { pageSettingFamily } from '../../../../atoms';
|
||||
import type { BlockSuiteWorkspace } from '../../../../shared';
|
||||
import { toast } from '../../../../utils';
|
||||
import { StyledEditorModeSwitch } from './style';
|
||||
import { StyledEditorModeSwitch, StyledKeyboardItem } from './style';
|
||||
import { EdgelessSwitchItem, PageSwitchItem } from './switch-items';
|
||||
|
||||
export type EditorModeSwitchProps = {
|
||||
@@ -18,7 +18,17 @@ export type EditorModeSwitchProps = {
|
||||
pageId: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
const TooltipContent = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<div>
|
||||
{t['Switch']()}
|
||||
<StyledKeyboardItem>
|
||||
{!environment.isServer && environment.isMacOs ? '⌥ + S' : 'Alt + S'}
|
||||
</StyledKeyboardItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const EditorModeSwitch = ({
|
||||
style,
|
||||
blockSuiteWorkspace,
|
||||
@@ -34,7 +44,11 @@ export const EditorModeSwitch = ({
|
||||
const { trash } = pageMeta;
|
||||
useEffect(() => {
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
if ((e.key === 's' && e.metaKey) || (e.key === 's' && e.altKey)) {
|
||||
if (
|
||||
!environment.isServer && environment.isMacOs
|
||||
? e.key === 'ß'
|
||||
: e.key === 's' && e.altKey
|
||||
) {
|
||||
e.preventDefault();
|
||||
setSetting(setting => {
|
||||
if (setting?.mode !== 'page') {
|
||||
@@ -51,8 +65,9 @@ export const EditorModeSwitch = ({
|
||||
return () =>
|
||||
document.removeEventListener('keydown', keydown, { capture: true });
|
||||
}, [setSetting, t]);
|
||||
|
||||
return (
|
||||
<Tooltip content={'Switch ⌘ + S'}>
|
||||
<Tooltip content={<TooltipContent />}>
|
||||
<StyledEditorModeSwitch
|
||||
style={style}
|
||||
switchLeft={currentMode === 'page'}
|
||||
|
||||
@@ -53,3 +53,14 @@ export const StyledSwitchItem = styled('button')<{
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledKeyboardItem = styled('span')(() => {
|
||||
return {
|
||||
marginLeft: '10px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
paddingLeft: '5px',
|
||||
paddingRight: '5px',
|
||||
backgroundColor: 'var(--affine-white-10)',
|
||||
borderRadius: '4px',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ const LocalHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||
};
|
||||
|
||||
export const HeaderShareMenu: React.FC<BaseHeaderProps> = props => {
|
||||
if (!runtimeConfig.enableLegacyCloud) {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
return null;
|
||||
}
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
|
||||
@@ -35,7 +35,7 @@ import * as styles from './styles.css';
|
||||
import { OSWarningMessage, shouldShowWarning } from './utils';
|
||||
|
||||
export type BaseHeaderProps<
|
||||
Workspace extends AffineOfficialWorkspace = AffineOfficialWorkspace
|
||||
Workspace extends AffineOfficialWorkspace = AffineOfficialWorkspace,
|
||||
> = {
|
||||
workspace: Workspace;
|
||||
currentPage: Page | null;
|
||||
|
||||
@@ -210,6 +210,7 @@ export const windowAppControlsWrapper = style({
|
||||
display: 'flex',
|
||||
gap: '2px',
|
||||
transform: 'translateX(8px)',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export const windowAppControl = style({
|
||||
@@ -217,11 +218,17 @@ export const windowAppControl = style({
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
width: '42px',
|
||||
height: '32px',
|
||||
height: 'calc(100% - 10px)',
|
||||
paddingTop: '10px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '4px',
|
||||
borderRadius: '0',
|
||||
selectors: {
|
||||
'&[data-type="close"]': {
|
||||
width: '56px',
|
||||
paddingRight: '14px',
|
||||
marginRight: '-14px',
|
||||
},
|
||||
'&[data-type="close"]:hover': {
|
||||
background: 'var(--affine-error-color)',
|
||||
color: '#FFFFFF',
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
DEFAULT_HELLO_WORLD_PAGE_ID,
|
||||
PageNotFoundError,
|
||||
} from '@affine/env/constant';
|
||||
import { rootCurrentEditorAtom } from '@affine/workspace/atom';
|
||||
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { Page, Workspace } 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';
|
||||
@@ -21,27 +21,20 @@ import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import Head from 'next/head';
|
||||
import type { FC, ReactElement } from 'react';
|
||||
import React, {
|
||||
memo,
|
||||
startTransition,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import type { CSSProperties, FC, ReactElement } from 'react';
|
||||
import { memo, Suspense, useCallback, useMemo } from 'react';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import { pageSettingFamily } from '../atoms';
|
||||
import { contentLayoutAtom } from '../atoms/layout';
|
||||
import { useAppSetting } from '../atoms/settings';
|
||||
import type { AffineOfficialWorkspace } from '../shared';
|
||||
import { fontStyleOptions, useAppSetting } from '../atoms/settings';
|
||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||
import { editor } from './page-detail-editor.css';
|
||||
import { pluginContainer } from './page-detail-editor.css';
|
||||
|
||||
export type PageDetailEditorProps = {
|
||||
isPublic?: boolean;
|
||||
workspace: AffineOfficialWorkspace;
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
|
||||
onLoad?: (page: Page, editor: EditorContainer) => () => void;
|
||||
@@ -59,12 +52,11 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
() => Object.values(affinePluginsMap),
|
||||
[affinePluginsMap]
|
||||
);
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
|
||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(blockSuiteWorkspace, pageId);
|
||||
throw new PageNotFoundError(workspace, pageId);
|
||||
}
|
||||
const meta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
const meta = useBlockSuitePageMeta(workspace).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
const pageSettingAtom = pageSettingFamily(pageId);
|
||||
@@ -73,33 +65,40 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
pageSetting?.mode ??
|
||||
(DEFAULT_HELLO_WORLD_PAGE_ID === pageId ? 'edgeless' : 'page');
|
||||
|
||||
const setEditor = useSetAtom(rootCurrentEditorAtom);
|
||||
const setBlockHub = useSetAtom(rootBlockHubAtom);
|
||||
const [appSettings] = useAppSetting();
|
||||
|
||||
assertExists(meta);
|
||||
const value = useMemo(() => {
|
||||
const fontStyle = fontStyleOptions.find(
|
||||
option => option.key === appSettings.fontStyle
|
||||
);
|
||||
assertExists(fontStyle);
|
||||
return fontStyle.value;
|
||||
}, [appSettings.fontStyle]);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
className={clsx(editor, {
|
||||
'full-screen': appSettings?.fullWidthLayout,
|
||||
'full-screen': appSettings.fullWidthLayout,
|
||||
})}
|
||||
key={`${workspace.flavour}-${workspace.id}-${pageId}`}
|
||||
style={
|
||||
{
|
||||
'--affine-font-family': value,
|
||||
} as CSSProperties
|
||||
}
|
||||
key={`${workspace.id}-${pageId}`}
|
||||
mode={isPublic ? 'page' : currentMode}
|
||||
page={page}
|
||||
onInit={useCallback(
|
||||
(page: Page, editor: Readonly<EditorContainer>) => {
|
||||
startTransition(() => {
|
||||
setEditor(editor);
|
||||
});
|
||||
onInit(page, editor);
|
||||
},
|
||||
[onInit, setEditor]
|
||||
[onInit]
|
||||
)}
|
||||
setBlockHub={setBlockHub}
|
||||
onLoad={useCallback(
|
||||
(page: Page, editor: EditorContainer) => {
|
||||
startTransition(() => {
|
||||
setEditor(editor);
|
||||
});
|
||||
page.workspace.setPageMeta(page.id, {
|
||||
updatedDate: Date.now(),
|
||||
});
|
||||
@@ -119,7 +118,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
dispose();
|
||||
};
|
||||
},
|
||||
[plugins, onLoad, setEditor]
|
||||
[plugins, onLoad]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -192,12 +191,11 @@ const LayoutPanel = memo(function LayoutPanel(
|
||||
|
||||
export const PageDetailEditor: FC<PageDetailEditorProps> = props => {
|
||||
const { workspace, pageId } = props;
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
|
||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(blockSuiteWorkspace, pageId);
|
||||
throw new PageNotFoundError(workspace, pageId);
|
||||
}
|
||||
const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, pageId);
|
||||
const title = useBlockSuiteWorkspacePageTitle(workspace, pageId);
|
||||
|
||||
const layout = useAtomValue(contentLayoutAtom);
|
||||
const affinePluginsMap = useAtomValue(affinePluginsAtom);
|
||||
|
||||
@@ -22,7 +22,7 @@ export const Footer: FC = () => {
|
||||
</div>
|
||||
}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableLegacyCloud) {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { MuiFade, Tooltip } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, NewIcon, UserGuideIcon } from '@blocksuite/icons';
|
||||
import { useAtom } from 'jotai';
|
||||
import { lazy, Suspense, useState } from 'react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { openOnboardingModalAtom } from '../../../atoms';
|
||||
import { openOnboardingModalAtom, openSettingModalAtom } from '../../../atoms';
|
||||
import { useCurrentMode } from '../../../hooks/current/use-current-mode';
|
||||
import { ShortcutsModal } from '../shortcuts-modal';
|
||||
import { ContactIcon, HelpIcon, KeyboardIcon } from './icons';
|
||||
@@ -14,11 +14,7 @@ import {
|
||||
StyledIsland,
|
||||
StyledTriggerWrapper,
|
||||
} from './style';
|
||||
const ContactModal = lazy(() =>
|
||||
import('@affine/component/contact-modal').then(({ ContactModal }) => ({
|
||||
default: ContactModal,
|
||||
}))
|
||||
);
|
||||
|
||||
const DEFAULT_SHOW_LIST: IslandItemNames[] = [
|
||||
'whatNew',
|
||||
'contact',
|
||||
@@ -32,28 +28,23 @@ export const HelpIsland = ({
|
||||
showList?: IslandItemNames[];
|
||||
}) => {
|
||||
const mode = useCurrentMode();
|
||||
const [, setOpenOnboarding] = useAtom(openOnboardingModalAtom);
|
||||
const setOpenOnboarding = useSetAtom(openOnboardingModalAtom);
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
const [spread, setShowSpread] = useState(false);
|
||||
// const { triggerShortcutsModal, triggerContactModal } = useModal();
|
||||
// const blockHub = useGlobalState(store => store.blockHub);
|
||||
const t = useAFFiNEI18N();
|
||||
//
|
||||
// useEffect(() => {
|
||||
// blockHub?.blockHubStatusUpdated.on(status => {
|
||||
// if (status) {
|
||||
// setShowSpread(false);
|
||||
// }
|
||||
// });
|
||||
// return () => {
|
||||
// blockHub?.blockHubStatusUpdated.dispose();
|
||||
// };
|
||||
// }, [blockHub]);
|
||||
//
|
||||
// useEffect(() => {
|
||||
// spread && blockHub?.toggleMenu(false);
|
||||
// }, [blockHub, spread]);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [openShortCut, setOpenShortCut] = useState(false);
|
||||
|
||||
const openAbout = useCallback(() => {
|
||||
setShowSpread(false);
|
||||
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'about',
|
||||
workspaceId: null,
|
||||
});
|
||||
}, [setOpenSettingModalAtom]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledIsland
|
||||
@@ -83,10 +74,7 @@ export const HelpIsland = ({
|
||||
<Tooltip content={t['Contact Us']()} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
onClick={() => {
|
||||
setShowSpread(false);
|
||||
setOpen(true);
|
||||
}}
|
||||
onClick={openAbout}
|
||||
>
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
@@ -136,13 +124,6 @@ export const HelpIsland = ({
|
||||
</StyledTriggerWrapper>
|
||||
</MuiFade>
|
||||
</StyledIsland>
|
||||
<Suspense>
|
||||
<ContactModal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
logoSrc="/imgs/affine-text-logo.png"
|
||||
/>
|
||||
</Suspense>
|
||||
<ShortcutsModal
|
||||
open={openShortCut}
|
||||
onClose={() => setOpenShortCut(false)}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Loading } from './page-loading';
|
||||
export * from './page-loading';
|
||||
export default Loading;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { AffineLoading } from '@affine/component/affine-loading';
|
||||
import { memo, Suspense } from 'react';
|
||||
|
||||
export const Loading = memo(function Loading() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '180px',
|
||||
width: '180px',
|
||||
}}
|
||||
>
|
||||
<Suspense>
|
||||
<AffineLoading loop={true} autoplay={true} autoReverse={true} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated use skeleton instead
|
||||
*/
|
||||
export const PageLoading = () => {
|
||||
return null;
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
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;
|
||||
@@ -4,18 +4,23 @@ import {
|
||||
FolderIcon,
|
||||
SettingsIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { FC, SVGProps } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../../atoms';
|
||||
import { pathGenerator } from '../../../shared';
|
||||
|
||||
export const useSwitchToConfig = (
|
||||
workspaceId: string
|
||||
): {
|
||||
title: string;
|
||||
href: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
icon: FC<SVGProps<SVGSVGElement>>;
|
||||
}[] => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -25,7 +30,13 @@ export const useSwitchToConfig = (
|
||||
},
|
||||
{
|
||||
title: t['Workspace Settings'](),
|
||||
href: pathGenerator.setting(workspaceId),
|
||||
onClick: () => {
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'workspace',
|
||||
workspaceId,
|
||||
});
|
||||
},
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
@@ -34,6 +45,6 @@ export const useSwitchToConfig = (
|
||||
icon: DeleteTemporarilyIcon,
|
||||
},
|
||||
],
|
||||
[workspaceId, t]
|
||||
[t, workspaceId, setOpenSettingModalAtom]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
useTransition,
|
||||
} from 'react';
|
||||
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import { Footer } from './footer';
|
||||
import { PublishedResults } from './published-results';
|
||||
import { Results } from './results';
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from './style';
|
||||
|
||||
export type QuickSearchModalProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
workspace: AllWorkspace;
|
||||
open: boolean;
|
||||
setOpen: (value: boolean) => void;
|
||||
router: NextRouter;
|
||||
@@ -36,8 +36,9 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
||||
open,
|
||||
setOpen,
|
||||
router,
|
||||
blockSuiteWorkspace,
|
||||
workspace,
|
||||
}) => {
|
||||
const blockSuiteWorkspace = workspace?.blockSuiteWorkspace;
|
||||
const t = useAFFiNEI18N();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [loading, startTransition] = useTransition();
|
||||
@@ -171,7 +172,7 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
||||
query={query}
|
||||
onClose={handleClose}
|
||||
router={router}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
workspace={workspace}
|
||||
setShowCreatePage={setShowCreatePage}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -13,12 +13,12 @@ import { useEffect } from 'react';
|
||||
|
||||
import { recentPageSettingsAtom } from '../../../atoms';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import { useSwitchToConfig } from './config';
|
||||
import { StyledListItem, StyledNotFound } from './style';
|
||||
|
||||
export type ResultsProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
workspace: AllWorkspace;
|
||||
query: string;
|
||||
onClose: () => void;
|
||||
setShowCreatePage: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -26,15 +26,16 @@ export type ResultsProps = {
|
||||
};
|
||||
export const Results: FC<ResultsProps> = ({
|
||||
query,
|
||||
blockSuiteWorkspace,
|
||||
workspace,
|
||||
setShowCreatePage,
|
||||
router,
|
||||
onClose,
|
||||
}) => {
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
assertExists(blockSuiteWorkspace.id);
|
||||
const List = useSwitchToConfig(blockSuiteWorkspace.id);
|
||||
const List = useSwitchToConfig(workspace.id);
|
||||
|
||||
const recentPageSetting = useAtomValue(recentPageSettingsAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -100,7 +101,8 @@ export const Results: FC<ResultsProps> = ({
|
||||
value={link.title}
|
||||
onSelect={() => {
|
||||
onClose();
|
||||
router.push(link.href).catch(console.error);
|
||||
link.href && router.push(link.href).catch(console.error);
|
||||
link.onClick?.();
|
||||
}}
|
||||
>
|
||||
<StyledListItem>
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { useCallback, useRef } from 'react';
|
||||
@@ -40,12 +41,12 @@ import {
|
||||
|
||||
interface WorkspaceModalProps {
|
||||
disabled?: boolean;
|
||||
workspaces: AllWorkspace[];
|
||||
workspaces: RootWorkspaceMetadata[];
|
||||
currentWorkspaceId: AllWorkspace['id'] | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onClickWorkspace: (workspace: AllWorkspace) => void;
|
||||
onClickWorkspaceSetting: (workspace: AllWorkspace) => void;
|
||||
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
|
||||
onNewWorkspace: () => void;
|
||||
onAddWorkspace: () => void;
|
||||
onMoveWorkspace: (activeId: string, overId: string) => void;
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from './styles';
|
||||
|
||||
export type WorkspaceSelectorProps = {
|
||||
currentWorkspace: AllWorkspace | null;
|
||||
currentWorkspace: AllWorkspace;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const WorkspaceSelector: React.FC<WorkspaceSelectorProps> = ({
|
||||
onClick,
|
||||
}) => {
|
||||
const [name] = useBlockSuiteWorkspaceName(
|
||||
currentWorkspace?.blockSuiteWorkspace ?? null
|
||||
currentWorkspace?.blockSuiteWorkspace
|
||||
);
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
|
||||
@@ -57,7 +57,7 @@ export const WorkspaceSelector: React.FC<WorkspaceSelectorProps> = ({
|
||||
data-testid="workspace-avatar"
|
||||
className={workspaceAvatarStyle}
|
||||
size={40}
|
||||
workspace={currentWorkspace}
|
||||
workspace={currentWorkspace?.blockSuiteWorkspace ?? null}
|
||||
/>
|
||||
<StyledSelectorWrapper>
|
||||
<StyledWorkspaceName data-testid="workspace-name">
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
DeleteIcon,
|
||||
FilterIcon,
|
||||
@@ -51,6 +52,7 @@ const CollectionOperations = ({
|
||||
showUpdateCollection: () => void;
|
||||
setting: ReturnType<typeof useCollectionManager>;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const actions = useMemo<
|
||||
Array<
|
||||
| {
|
||||
@@ -68,12 +70,12 @@ const CollectionOperations = ({
|
||||
() => [
|
||||
{
|
||||
icon: <FilterIcon />,
|
||||
name: 'Edit Filter',
|
||||
name: t['Edit Filter'](),
|
||||
click: showUpdateCollection,
|
||||
},
|
||||
{
|
||||
icon: <UnpinIcon />,
|
||||
name: 'Unpin',
|
||||
name: t['Unpin'](),
|
||||
click: () => {
|
||||
return setting.updateCollection({
|
||||
...view,
|
||||
@@ -86,14 +88,14 @@ const CollectionOperations = ({
|
||||
},
|
||||
{
|
||||
icon: <DeleteIcon style={{ color: 'var(--affine-warning-color)' }} />,
|
||||
name: 'Delete',
|
||||
name: t['Delete'](),
|
||||
click: () => {
|
||||
return setting.deleteCollection(view.id);
|
||||
},
|
||||
className: styles.deleteFolder,
|
||||
},
|
||||
],
|
||||
[setting, showUpdateCollection, view]
|
||||
[setting, showUpdateCollection, t, view]
|
||||
);
|
||||
return (
|
||||
<div style={{ minWidth: 150 }}>
|
||||
|
||||