mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 11:58:41 +00:00
Compare commits
51 Commits
v0.7.0-can
...
v0.7.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d07ff2390 | ||
|
|
42bab6990e | ||
|
|
89a566a645 | ||
|
|
7af5bd3894 | ||
|
|
a57c27679d | ||
|
|
68a72b2dfc | ||
|
|
602f795133 | ||
|
|
5df89a925b | ||
|
|
23126e1ff6 | ||
|
|
e1f715f837 | ||
|
|
fbcaed40e7 | ||
|
|
88757ce488 | ||
|
|
fc9462eee9 | ||
|
|
e1314730be | ||
|
|
36978dbed6 | ||
|
|
53d1991211 | ||
|
|
1ea445ab15 | ||
|
|
78410f531a | ||
|
|
96c0321696 | ||
|
|
0895f1fb30 | ||
|
|
b6188f4b11 | ||
|
|
2ed1a7b219 | ||
|
|
9bee6bd5cc | ||
|
|
198f30c86d | ||
|
|
454f1887cf | ||
|
|
4e1e4e9435 | ||
|
|
f7768563e1 | ||
|
|
6aa0e71b84 | ||
|
|
f1b3a10969 | ||
|
|
90e70ed986 | ||
|
|
094a479c2a | ||
|
|
20f1d487c8 | ||
|
|
4c9bda1406 | ||
|
|
d5debc0bf5 | ||
|
|
f5aee7c360 | ||
|
|
248cd9a8ab | ||
|
|
06abb702f5 | ||
|
|
ee289706ec | ||
|
|
6cbf310a5a | ||
|
|
855fd8a73a | ||
|
|
8dbd354659 | ||
|
|
1c7ae04f4f | ||
|
|
0bb6e362bf | ||
|
|
617350fc7d | ||
|
|
2713340532 | ||
|
|
31d552ab7e | ||
|
|
e11326f05f | ||
|
|
6648fe4dcc | ||
|
|
f669164674 | ||
|
|
c6d8904ca2 | ||
|
|
8c5a1e2de3 |
@@ -5,3 +5,4 @@ out
|
||||
storybook-static
|
||||
affine-out
|
||||
_next
|
||||
lib
|
||||
|
||||
51
.eslintrc.js
51
.eslintrc.js
@@ -1,3 +1,43 @@
|
||||
const createPattern = packageName => [
|
||||
{
|
||||
group: ['**/dist', '**/dist/**'],
|
||||
message: 'Do not import from dist',
|
||||
allowTypeImports: false,
|
||||
},
|
||||
{
|
||||
group: ['**/src', '**/src/**'],
|
||||
message: 'Do not import from src',
|
||||
allowTypeImports: false,
|
||||
},
|
||||
{
|
||||
group: [`@affine/${packageName}`],
|
||||
message: 'Do not import package itself',
|
||||
allowTypeImports: false,
|
||||
},
|
||||
{
|
||||
group: [`@toeverything/${packageName}`],
|
||||
message: 'Do not import package itself',
|
||||
allowTypeImports: false,
|
||||
},
|
||||
];
|
||||
|
||||
const allPackages = [
|
||||
'cli',
|
||||
'component',
|
||||
'debug',
|
||||
'env',
|
||||
'graphql',
|
||||
'hooks',
|
||||
'i18n',
|
||||
'jotai',
|
||||
'native',
|
||||
'plugin-infra',
|
||||
'templates',
|
||||
'theme',
|
||||
'workspace',
|
||||
'y-indexeddb',
|
||||
];
|
||||
|
||||
/**
|
||||
* @type {import('eslint').Linter.Config}
|
||||
*/
|
||||
@@ -96,6 +136,17 @@ const config = {
|
||||
'@typescript-eslint/no-var-requires': 0,
|
||||
},
|
||||
},
|
||||
...allPackages.map(pkg => ({
|
||||
files: [`packages/${pkg}/src/**/*.ts`, `packages/${pkg}/src/**/*.tsx`],
|
||||
rules: {
|
||||
'@typescript-eslint/no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
patterns: createPattern(pkg),
|
||||
},
|
||||
],
|
||||
},
|
||||
})),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
6
.github/labeler.yml
vendored
6
.github/labeler.yml
vendored
@@ -8,11 +8,17 @@ test:
|
||||
- '**/tests/**/*'
|
||||
- '**/__tests__/**/*'
|
||||
|
||||
plugin:copilot:
|
||||
- 'plugins/copilot/**/*'
|
||||
|
||||
mod:dev:
|
||||
- 'scripts/**/*'
|
||||
- 'packages/cli/**/*'
|
||||
- 'packages/debug/**/*'
|
||||
|
||||
mod:plugin-infra:
|
||||
- 'packages/plugin-infra/**/*'
|
||||
|
||||
mod:workspace: 'packages/workspace/**/*'
|
||||
|
||||
mod:i18n: 'packages/i18n/**/*'
|
||||
|
||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -35,7 +35,10 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- run: |
|
||||
- name: Run checks
|
||||
run: |
|
||||
yarn i18n-codegen gen
|
||||
yarn typecheck
|
||||
yarn lint --max-warnings=0
|
||||
yarn circular
|
||||
|
||||
@@ -84,7 +87,9 @@ jobs:
|
||||
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
|
||||
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
|
||||
API_SERVER_PROFILE: local
|
||||
ENABLE_DEBUG_PAGE: true
|
||||
ENABLE_DEBUG_PAGE: 1
|
||||
ENABLE_PLUGIN: true
|
||||
ENABLE_ALL_PAGE_FILTER: true
|
||||
ENABLE_LEGACY_PROVIDER: true
|
||||
COVERAGE: true
|
||||
|
||||
@@ -106,7 +111,9 @@ jobs:
|
||||
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
|
||||
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
|
||||
API_SERVER_PROFILE: affine
|
||||
ENABLE_DEBUG_PAGE: true
|
||||
ENABLE_DEBUG_PAGE: 1
|
||||
ENABLE_PLUGIN: true
|
||||
ENABLE_ALL_PAGE_FILTER: true
|
||||
ENABLE_LEGACY_PROVIDER: false
|
||||
COVERAGE: true
|
||||
|
||||
|
||||
2
.github/workflows/release-desktop-app.yml
vendored
2
.github/workflows/release-desktop-app.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
ENABLE_TEST_PROPERTIES: false
|
||||
ENABLE_IMAGE_PREVIEW_MODAL: false
|
||||
RELEASE_VERSION: ${{ github.event.inputs.version }}
|
||||
ENABLE_BOOKMARK_OPERATION: false
|
||||
ENABLE_BOOKMARK_OPERATION: true
|
||||
|
||||
- name: Upload Artifact (web-static)
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -70,3 +70,5 @@ next-env.d.ts
|
||||
# Rust
|
||||
target
|
||||
*.node
|
||||
tsconfig.node.tsbuildinfo
|
||||
lib
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
pnpm-lock.yaml
|
||||
target
|
||||
lib
|
||||
test-results
|
||||
|
||||
15
README.md
15
README.md
@@ -24,7 +24,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[?style=flat-square&logoColor=white&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAADAAAAAwAEwd99eAAABjElEQVRYhe1W0U3DMBB9RfyTDeoNyAYNG2QDOgJsECYgGxA26AZ4hIxgJqCZ4PjIGV+tUxK7raqiPsmKdXe5e3fOs7IiIlwSdxetfiNw7QRKAD0Ax/ssrI5QgQOw5v03AJOTJHcCL1x84LVmWzJyJlBg7P4BwCvb3pmIAbBPykZEqaulEU7YHNva1HypxUsKqIS9EvbynASs0n3ss+ciUIsuO8VvhL9emjdFBa3YO8XvALwpsZNYSqBB0PwUWgRZNksSL5GhlN0ngGd+dkpsD6AG8IGlslxwTh2fa09EBc3Dir32rRysuQlUAL54/wTAcpePPAXHPsOTGXhSEv69rAlYpZOt6DSO29J4D/TRRLJk6AvtaZSY9PkCFYVLqI9i/NF5YkkECgrXa6P4fVEn4iolrhNxRQqBZu7FqMNdZiMqAUPj2KdGZyicu1dHzlGqBHxn2sdTR53bmeJ+ebJd7LtXhGH4uQEwd0ttAPzMxGi5/6BdxTuMej41Bs59gGP+CU+Cq/4tvxH4HwR+Ab3Uqr/VGbqEAAAAAElFTkSuQmCC>)](https://app.affine.pro)
|
||||
[?style=flat-square&logoColor=white&logo=affine>)](https://app.affine.pro)
|
||||
[](https://affine.pro/download)
|
||||
[](https://affine.pro/download)
|
||||
[](https://affine.pro/download)
|
||||
@@ -45,7 +45,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAMAAAAPkIrYAAAAP1BMVEU8b9w8b9w+b947cNw7b9w6b908b909b9w8b9w7b9w8b9w7cN08b9w7b908b9w7b9w8b907cNw8b9w8b91HcEx3NJCJAAAAFXRSTlP/3QWSgA+lHPlu6Di4XtIrxk/xRADGudUoAAAB9UlEQVR42tWYwbKjIBREG0GJKkRj/v9bZ1ZvRC99rzib11tTB9qqnKoW3/+X38vy7ifzQ1b/wk/8Q1bCv3y6Z6wFh2x2llIRGB6xRhzz6p+wVhRJD1gRZZYHrADYSyqsjFPGZtYbuFESesUysZXlcMnYyJpxTW5keQh5N7G6CUJCE2uHFNfEGiBmbmB1H4jxDawNcqbuPmtAJTtj6RZ0lpIwiR5jNmgfNtHHwLXPWfFYcS2NMdxkjac/dNaNCJPo3yf9pFuseHbDrBsRFguGs8te8Q4rXzTjVSPCIHp3FePKWbzi30xE+4zlBMmoJaGLfpLUmAmLiN4Xyibahy76WZRQMLJ2WX27on2oFvQVac8yi4p+J2forA0V8W1c++AVS1f1H6p9KKLHxk9RWKmsyB+VLC76gV65DLjokdg5KmsEMXsiDwXWSmTc9ezSoKJHoi9zUVihbMHfQOSsXB7Mrz1S1huKPde69sEsiKgNt8hYTjiWlAyENeu7IFe1D15RSEBN+yCiXw17K1RZm/w7UtJVWYN8f1ZyLlkVb2bT4vIVVrINH1dqX2YttkHmIWsfVWs646wcRFYis6fIVGpfYq1kjpGSW8kSRD+xYSmXRM0Ang9eSZioVdy/5pWaLqzIRyIpuVxYozvGf1m67I7pf/s3UXv+AP61NI2Y+BbSAAAAAElFTkSuQmCC" height=25></a>
|
||||
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=affine" height=25></a>
|
||||
|
||||
<a href="https://community.affine.pro"><img src="https://img.shields.io/badge/-Community-424549?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAXNJREFUWEftlitLRUEURtdVEVExWUx2qxgNVouoXYtNDP4Tw20WtftAsItZrHaTYBJREZ98MAc248wcZxi4CGfSeezHmm/23kyPAa/egPPTAXQK/FsFBP7ldVDRZoqcgO9I+2bHy3ZIJBfTCPCZM1tqAxwBmzUBrNQNbEx+5b0B5oEN4NCBrAMnMaiUAuPAs3HU82TLEZwBqwGbaJ4UgKQ8CFR6SoEl4LIWwCJwZQCegKkWBWLHVKSActvdzgG3DqitDf3/VQBskBDALrDnAKXUo3ueAF5KinAf2DKOmnzD7l214bdbA6hC1XHZNQa8hSBC0hwDa57xDHDvvvWB7ciOZoE79+8CWPbsBGc769eFxJdWIKcuyIdRoG3W7AAC1dJkHDIOo8B78+4rEBo8r4AkLFk6Jk3HaeDBBTgHVmIAfpJUz+cAFXVBreQCvQYW/lqEjV1NAMUMqpAaxQMHyDnjYtuS+0BxstwaqJooFqxToFPgB5FuPCEB6XK2AAAAAElFTkSuQmCC" height=25></a>
|
||||
|
||||
@@ -74,7 +74,7 @@ Before we tell you how to get started with AFFiNE, we'd like to shamelessly plug
|
||||
|
||||
⚠️ Please note that AFFiNE is still under active development and is not yet ready for production use. ⚠️
|
||||
|
||||
[](https://app.affine.pro) No installation or registration required! Head over to our website and try it out now.
|
||||
[](https://app.affine.pro) No installation or registration required! Head over to our website and try it out now.
|
||||
|
||||
[](https://community.affine.pro) Our wonderful community, where you can meet and engage with the team, developers and other like-minded enthusiastic user of AFFiNE.
|
||||
|
||||
@@ -120,6 +120,15 @@ If you have questions, you are welcome to contact us. One of the best places to
|
||||
| [@toeverything/y-indexeddb](packages/y-indexeddb) | IndexedDB database adapter for Yjs | [](https://www.npmjs.com/package/@toeverything/y-indexeddb) |
|
||||
| [@toeverything/theme](packages/theme) | AFFiNE theme | [](https://www.npmjs.com/package/@toeverything/theme) |
|
||||
|
||||
## Plugins
|
||||
|
||||
> Plugins are a way to extend the functionality of AFFiNE.
|
||||
|
||||
| Name | |
|
||||
| ------------------------------------------------ | ----------------------------------------- |
|
||||
| [@affine/bookmark-block](plugins/bookmark-block) | A block for bookmarking a website |
|
||||
| [@affine/copilot](plugins/copilot) | AI Copilot that help you document writing |
|
||||
|
||||
## Thanks
|
||||
|
||||
We would also like to give thanks to open-source projects that make AFFiNE possible:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
||||
// This file contains the main process events
|
||||
// It will guide preload and main process on the correct event types and payloads
|
||||
|
||||
export type MainIPCHandlerMap = typeof import('./main/src/exposed').handlers;
|
||||
|
||||
export type MainIPCEventMap = typeof import('./main/src/exposed').events;
|
||||
@@ -6,7 +6,7 @@ import { v4 } from 'uuid';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { MainIPCHandlerMap } from '../../../constraints';
|
||||
import type { MainIPCHandlerMap } from '../exposed';
|
||||
|
||||
const registeredHandlers = new Map<
|
||||
string,
|
||||
@@ -176,7 +176,7 @@ describe('ensureSQLiteDB', () => {
|
||||
expect(fileExists).toBe(true);
|
||||
registeredHandlers.get('before-quit')?.forEach(fn => fn());
|
||||
await delay(100);
|
||||
expect(workspaceDB.db?.open).toBe(false);
|
||||
expect(workspaceDB.db).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,7 +197,7 @@ describe('workspace handlers', () => {
|
||||
const list = await dispatch('workspace', 'list');
|
||||
expect(list.map(([id]) => id)).toEqual([ids[0]]);
|
||||
// deleted db should be closed
|
||||
expect(dbs[1].db?.open).toBe(false);
|
||||
expect(dbs[1].db).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -436,6 +436,8 @@ describe('dialog handlers', () => {
|
||||
});
|
||||
|
||||
test('moveDBFile (valid)', async () => {
|
||||
const sendStub = vi.fn();
|
||||
browserWindow.webContents.send = sendStub;
|
||||
const newPath = path.join(SESSION_DATA_PATH, 'xxx');
|
||||
const showOpenDialog = vi.fn(() => {
|
||||
return { filePaths: [newPath] };
|
||||
@@ -444,13 +446,19 @@ describe('dialog handlers', () => {
|
||||
|
||||
const id = v4();
|
||||
const { ensureSQLiteDB } = await import('../db/ensure-db');
|
||||
await ensureSQLiteDB(id);
|
||||
const db = await ensureSQLiteDB(id);
|
||||
const res = await dispatch('dialog', 'moveDBFile', id);
|
||||
expect(showOpenDialog).toBeCalled();
|
||||
assert(res.filePath);
|
||||
expect(path.dirname(res.filePath)).toBe(newPath);
|
||||
expect(res.filePath.endsWith('.affine')).toBe(true);
|
||||
// should also send workspace meta change event
|
||||
expect(sendStub).toBeCalledWith('workspace:onMetaChange', {
|
||||
workspaceId: id,
|
||||
meta: { id, secondaryDBPath: res.filePath, mainDBPath: db.path },
|
||||
});
|
||||
electronModule.dialog = {};
|
||||
browserWindow.webContents.send = () => {};
|
||||
});
|
||||
|
||||
test('moveDBFile (canceled)', async () => {
|
||||
@@ -469,3 +477,18 @@ describe('dialog handlers', () => {
|
||||
electronModule.dialog = {};
|
||||
});
|
||||
});
|
||||
|
||||
describe('applicationMenu', () => {
|
||||
// test some basic IPC events
|
||||
test('applicationMenu event', async () => {
|
||||
const { applicationMenuSubjects } = await import('../application-menu');
|
||||
const sendStub = vi.fn();
|
||||
browserWindow.webContents.send = sendStub;
|
||||
applicationMenuSubjects.newPageAction.next();
|
||||
expect(sendStub).toHaveBeenCalledWith(
|
||||
'applicationMenu:onNewPageAction',
|
||||
undefined
|
||||
);
|
||||
browserWindow.webContents.send = () => {};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { app, Menu } from 'electron';
|
||||
|
||||
import { isMacOS } from '../../../utils';
|
||||
import { revealLogFile } from '../logger';
|
||||
import { checkForUpdatesAndNotify } from '../updater';
|
||||
import { isMacOS } from '../utils';
|
||||
import { applicationMenuSubjects } from './subject';
|
||||
|
||||
// Unique id for menuitems
|
||||
|
||||
1
apps/electron/layers/main/src/db/__tests__/.gitignore
vendored
Normal file
1
apps/electron/layers/main/src/db/__tests__/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tmp
|
||||
147
apps/electron/layers/main/src/db/__tests__/ensure-db.spec.ts
Normal file
147
apps/electron/layers/main/src/db/__tests__/ensure-db.spec.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { v4 } from 'uuid';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
|
||||
const registeredHandlers = new Map<
|
||||
string,
|
||||
((...args: any[]) => Promise<any>)[]
|
||||
>();
|
||||
|
||||
const SESSION_DATA_PATH = path.join(tmpDir, 'affine-test');
|
||||
const DOCUMENTS_PATH = path.join(tmpDir, 'affine-test-documents');
|
||||
|
||||
const electronModule = {
|
||||
app: {
|
||||
getPath: (name: string) => {
|
||||
if (name === 'sessionData') {
|
||||
return SESSION_DATA_PATH;
|
||||
} else if (name === 'documents') {
|
||||
return DOCUMENTS_PATH;
|
||||
}
|
||||
throw new Error('not implemented');
|
||||
},
|
||||
name: 'affine-test',
|
||||
on: (name: string, callback: (...args: any[]) => any) => {
|
||||
const handlers = registeredHandlers.get(name) || [];
|
||||
handlers.push(callback);
|
||||
registeredHandlers.set(name, handlers);
|
||||
},
|
||||
addEventListener: (...args: any[]) => {
|
||||
// @ts-ignore
|
||||
electronModule.app.on(...args);
|
||||
},
|
||||
removeEventListener: () => {},
|
||||
},
|
||||
shell: {} as Partial<Electron.Shell>,
|
||||
dialog: {} as Partial<Electron.Dialog>,
|
||||
};
|
||||
|
||||
const runHandler = (key: string) => {
|
||||
registeredHandlers.get(key)?.forEach(handler => handler());
|
||||
};
|
||||
|
||||
// dynamically import handlers so that we can inject local variables to mocks
|
||||
vi.doMock('electron', () => {
|
||||
return electronModule;
|
||||
});
|
||||
|
||||
const constructorStub = vi.fn();
|
||||
const destroyStub = vi.fn();
|
||||
|
||||
vi.doMock('../secondary-db', () => {
|
||||
return {
|
||||
SecondaryWorkspaceSQLiteDB: class {
|
||||
constructor(...args: any[]) {
|
||||
constructorStub(...args);
|
||||
}
|
||||
|
||||
destroy = destroyStub;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
runHandler('before-quit');
|
||||
await fs.remove(tmpDir);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('can get a valid WorkspaceSQLiteDB', async () => {
|
||||
const { ensureSQLiteDB } = await import('../ensure-db');
|
||||
const workspaceId = v4();
|
||||
const db0 = await ensureSQLiteDB(workspaceId);
|
||||
expect(db0).toBeDefined();
|
||||
expect(db0.workspaceId).toBe(workspaceId);
|
||||
|
||||
const db1 = await ensureSQLiteDB(v4());
|
||||
expect(db1).not.toBe(db0);
|
||||
expect(db1.workspaceId).not.toBe(db0.workspaceId);
|
||||
|
||||
// ensure that the db is cached
|
||||
expect(await ensureSQLiteDB(workspaceId)).toBe(db0);
|
||||
});
|
||||
|
||||
test('db should be destroyed when app quits', async () => {
|
||||
const { ensureSQLiteDB } = await import('../ensure-db');
|
||||
const workspaceId = v4();
|
||||
const db0 = await ensureSQLiteDB(workspaceId);
|
||||
const db1 = await ensureSQLiteDB(v4());
|
||||
|
||||
expect(db0.db).not.toBeNull();
|
||||
expect(db1.db).not.toBeNull();
|
||||
|
||||
runHandler('before-quit');
|
||||
|
||||
expect(db0.db).toBeNull();
|
||||
expect(db1.db).toBeNull();
|
||||
});
|
||||
|
||||
test('if db has a secondary db path, we should also poll that', async () => {
|
||||
const { ensureSQLiteDB } = await import('../ensure-db');
|
||||
const { appContext } = await import('../../context');
|
||||
const { storeWorkspaceMeta } = await import('../../workspace');
|
||||
const workspaceId = v4();
|
||||
await storeWorkspaceMeta(appContext, workspaceId, {
|
||||
secondaryDBPath: path.join(tmpDir, 'secondary.db'),
|
||||
});
|
||||
|
||||
const db = await ensureSQLiteDB(workspaceId);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
|
||||
// not sure why but we still need to wait with real timer
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(constructorStub).toBeCalledTimes(1);
|
||||
expect(constructorStub).toBeCalledWith(path.join(tmpDir, 'secondary.db'), db);
|
||||
|
||||
// if secondary meta is changed
|
||||
await storeWorkspaceMeta(appContext, workspaceId, {
|
||||
secondaryDBPath: path.join(tmpDir, 'secondary2.db'),
|
||||
});
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
expect(constructorStub).toBeCalledTimes(2);
|
||||
expect(destroyStub).toBeCalledTimes(1);
|
||||
|
||||
// if secondary meta is changed (but another workspace)
|
||||
await storeWorkspaceMeta(appContext, v4(), {
|
||||
secondaryDBPath: path.join(tmpDir, 'secondary3.db'),
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(1500);
|
||||
expect(constructorStub).toBeCalledTimes(2);
|
||||
expect(destroyStub).toBeCalledTimes(1);
|
||||
|
||||
// if primary is destroyed, secondary should also be destroyed
|
||||
db.destroy();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(destroyStub).toBeCalledTimes(2);
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { v4 } from 'uuid';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { AppContext } from '../../context';
|
||||
import { dbSubjects } from '../subjects';
|
||||
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
|
||||
const testAppContext: AppContext = {
|
||||
appDataPath: path.join(tmpDir, 'test-data'),
|
||||
appName: 'test',
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
if (process.platform !== 'win32') {
|
||||
// hmmm ....
|
||||
await fs.remove(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
function getTestUpdates() {
|
||||
const testYDoc = new Y.Doc();
|
||||
const yText = testYDoc.getText('test');
|
||||
yText.insert(0, 'hello');
|
||||
const updates = Y.encodeStateAsUpdate(testYDoc);
|
||||
|
||||
return updates;
|
||||
}
|
||||
test('can create new db file if not exists', async () => {
|
||||
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
|
||||
const workspaceId = v4();
|
||||
const db = await openWorkspaceDatabase(testAppContext, workspaceId);
|
||||
const dbPath = path.join(
|
||||
testAppContext.appDataPath,
|
||||
`workspaces/${workspaceId}`,
|
||||
`storage.db`
|
||||
);
|
||||
expect(await fs.exists(dbPath)).toBe(true);
|
||||
db.destroy();
|
||||
});
|
||||
|
||||
test('on applyUpdate (from self), will not trigger update', async () => {
|
||||
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
|
||||
const workspaceId = v4();
|
||||
const onUpdate = vi.fn();
|
||||
|
||||
const db = await openWorkspaceDatabase(testAppContext, workspaceId);
|
||||
db.update$.subscribe(onUpdate);
|
||||
db.applyUpdate(getTestUpdates(), 'self');
|
||||
expect(onUpdate).not.toHaveBeenCalled();
|
||||
db.destroy();
|
||||
});
|
||||
|
||||
test('on applyUpdate (from renderer), will trigger update', async () => {
|
||||
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
|
||||
const workspaceId = v4();
|
||||
const onUpdate = vi.fn();
|
||||
const onExternalUpdate = vi.fn();
|
||||
|
||||
const db = await openWorkspaceDatabase(testAppContext, workspaceId);
|
||||
db.update$.subscribe(onUpdate);
|
||||
const sub = dbSubjects.externalUpdate.subscribe(onExternalUpdate);
|
||||
db.applyUpdate(getTestUpdates(), 'renderer');
|
||||
expect(onUpdate).toHaveBeenCalled(); // not yet updated
|
||||
sub.unsubscribe();
|
||||
db.destroy();
|
||||
});
|
||||
|
||||
test('on applyUpdate (from external), will trigger update & send external update event', async () => {
|
||||
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
|
||||
const workspaceId = v4();
|
||||
const onUpdate = vi.fn();
|
||||
const onExternalUpdate = vi.fn();
|
||||
|
||||
const db = await openWorkspaceDatabase(testAppContext, workspaceId);
|
||||
db.update$.subscribe(onUpdate);
|
||||
const sub = dbSubjects.externalUpdate.subscribe(onExternalUpdate);
|
||||
db.applyUpdate(getTestUpdates(), 'external');
|
||||
expect(onUpdate).toHaveBeenCalled();
|
||||
expect(onExternalUpdate).toHaveBeenCalled();
|
||||
sub.unsubscribe();
|
||||
db.destroy();
|
||||
});
|
||||
|
||||
test('on destroy, check if resources have been released', async () => {
|
||||
const { openWorkspaceDatabase } = await import('../workspace-db-adapter');
|
||||
const workspaceId = v4();
|
||||
const db = await openWorkspaceDatabase(testAppContext, workspaceId);
|
||||
const updateSub = {
|
||||
complete: vi.fn(),
|
||||
next: vi.fn(),
|
||||
};
|
||||
db.update$ = updateSub as any;
|
||||
db.destroy();
|
||||
expect(db.db).toBe(null);
|
||||
expect(updateSub.complete).toHaveBeenCalled();
|
||||
});
|
||||
@@ -44,9 +44,9 @@ export abstract class BaseSQLiteAdapter {
|
||||
}
|
||||
|
||||
// todo: what if SQLite DB wrapper later is not sync?
|
||||
connect() {
|
||||
connect(): Database | undefined {
|
||||
if (this.db) {
|
||||
return;
|
||||
return this.db;
|
||||
}
|
||||
logger.log(`[SQLiteAdapter][${this.role}] open db`, this.path);
|
||||
const db = (this.db = sqlite(this.path));
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
from,
|
||||
fromEvent,
|
||||
interval,
|
||||
merge,
|
||||
Observable,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ function getWorkspaceDB$(id: string) {
|
||||
db$Map.set(
|
||||
id,
|
||||
from(openWorkspaceDatabase(appContext, id)).pipe(
|
||||
shareReplay(1),
|
||||
switchMap(db => {
|
||||
return startPollingSecondaryDB(db).pipe(
|
||||
ignoreElements(),
|
||||
@@ -57,7 +59,6 @@ function getWorkspaceDB$(id: string) {
|
||||
return db$Map.get(id)!;
|
||||
}
|
||||
|
||||
// fixme: this function has issue on registering multiple times...
|
||||
function startPollingSecondaryDB(db: WorkspaceSQLiteDB) {
|
||||
const meta$ = getWorkspaceMeta$(db.workspaceId);
|
||||
const secondaryDB$ = meta$.pipe(
|
||||
@@ -65,28 +66,43 @@ function startPollingSecondaryDB(db: WorkspaceSQLiteDB) {
|
||||
distinctUntilChanged(),
|
||||
filter((p): p is string => !!p),
|
||||
switchMap(path => {
|
||||
const secondaryDB = new SecondaryWorkspaceSQLiteDB(path, db);
|
||||
return new Observable<SecondaryWorkspaceSQLiteDB>(observer => {
|
||||
const secondaryDB = new SecondaryWorkspaceSQLiteDB(path, db);
|
||||
observer.next(secondaryDB);
|
||||
return () => {
|
||||
logger.info(
|
||||
'[ensureSQLiteDB] close secondary db connection',
|
||||
secondaryDB.path
|
||||
);
|
||||
secondaryDB.destroy();
|
||||
};
|
||||
});
|
||||
})
|
||||
}),
|
||||
takeUntil(db.update$.pipe(last())),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
const firstDelayedTick$ = defer(() => {
|
||||
return new Promise<number>(resolve =>
|
||||
setTimeout(() => {
|
||||
resolve(0);
|
||||
}, 1000)
|
||||
);
|
||||
});
|
||||
|
||||
// pull every 30 seconds
|
||||
const poll$ = interval(30000).pipe(
|
||||
const poll$ = merge(firstDelayedTick$, interval(30000)).pipe(
|
||||
switchMap(() => secondaryDB$),
|
||||
tap({
|
||||
next: secondaryDB => {
|
||||
secondaryDB.pull();
|
||||
},
|
||||
}),
|
||||
takeUntil(db.update$.pipe(last())),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
return poll$.pipe(takeUntil(db.update$.pipe(last())), shareReplay(1));
|
||||
return poll$;
|
||||
}
|
||||
|
||||
export function ensureSQLiteDB(id: string) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as Y from 'yjs';
|
||||
import type { AppContext } from '../context';
|
||||
import { logger } from '../logger';
|
||||
import type { YOrigin } from '../type';
|
||||
import { mergeUpdateWorker } from '../workers';
|
||||
import { getWorkspaceMeta } from '../workspace';
|
||||
import { BaseSQLiteAdapter } from './base-db-adapter';
|
||||
import type { WorkspaceSQLiteDB } from './workspace-db-adapter';
|
||||
@@ -26,6 +27,7 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
) {
|
||||
super(path);
|
||||
this.setupAndListen();
|
||||
logger.debug('[SecondaryWorkspaceSQLiteDB] created', this.workspaceId);
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -34,10 +36,10 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
this.flushUpdateQueue();
|
||||
this.unsubscribers.forEach(unsub => unsub());
|
||||
this.db?.close();
|
||||
this.yDoc.destroy();
|
||||
this.close();
|
||||
}
|
||||
|
||||
get workspaceId() {
|
||||
@@ -74,13 +76,13 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
|
||||
// wrap the fn with connect and close
|
||||
// it only works for sync functions
|
||||
run = (fn: () => void) => {
|
||||
run = <T extends (...args: any[]) => any>(fn: T) => {
|
||||
try {
|
||||
if (this.runCounter === 0) {
|
||||
this.connect();
|
||||
}
|
||||
this.runCounter++;
|
||||
fn();
|
||||
return fn();
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
} finally {
|
||||
@@ -166,19 +168,24 @@ export class SecondaryWorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
* - get blobs and put new blobs to upstream
|
||||
* - disconnect
|
||||
*/
|
||||
pull() {
|
||||
this.run(() => {
|
||||
async pull() {
|
||||
const start = performance.now();
|
||||
const updates = this.run(() => {
|
||||
// TODO: no need to get all updates, just get the latest ones (using a cursor, etc)?
|
||||
const updates = this.getUpdates().map(update => update.data);
|
||||
Y.transact(this.yDoc, () => {
|
||||
updates.forEach(update => {
|
||||
this.applyUpdate(update, 'self');
|
||||
});
|
||||
});
|
||||
logger.debug('pull external updates', this.path, updates.length);
|
||||
|
||||
this.syncBlobs();
|
||||
return this.getUpdates().map(update => update.data);
|
||||
});
|
||||
|
||||
const merged = await mergeUpdateWorker(updates);
|
||||
this.applyUpdate(merged, 'self');
|
||||
|
||||
logger.debug(
|
||||
'pull external updates',
|
||||
this.path,
|
||||
updates.length,
|
||||
(performance.now() - start).toFixed(2),
|
||||
'ms'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Database } from 'better-sqlite3';
|
||||
import { Subject } from 'rxjs';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import type { AppContext } from '../context';
|
||||
import { logger } from '../logger';
|
||||
import type { YOrigin } from '../type';
|
||||
import { mergeUpdateWorker } from '../workers';
|
||||
import { getWorkspaceMeta } from '../workspace';
|
||||
import { BaseSQLiteAdapter } from './base-db-adapter';
|
||||
import { dbSubjects } from './subjects';
|
||||
@@ -21,6 +23,7 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
|
||||
override destroy() {
|
||||
this.db?.close();
|
||||
this.db = null;
|
||||
this.yDoc.destroy();
|
||||
|
||||
// when db is closed, we can safely remove it from ensure-db list
|
||||
@@ -31,15 +34,15 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
return this.yDoc.getMap('space:meta').get('name') as string;
|
||||
};
|
||||
|
||||
override connect() {
|
||||
async init(): Promise<Database | undefined> {
|
||||
const db = super.connect();
|
||||
|
||||
if (!this.firstConnected) {
|
||||
this.yDoc.on('update', (update: Uint8Array, origin: YOrigin) => {
|
||||
if (origin !== 'self') {
|
||||
if (origin === 'renderer') {
|
||||
this.addUpdateToSQLite([update]);
|
||||
} else if (origin === 'external') {
|
||||
this.addUpdateToSQLite([update]);
|
||||
}
|
||||
if (origin === 'external') {
|
||||
logger.debug('external update', this.workspaceId);
|
||||
dbSubjects.externalUpdate.next({
|
||||
workspaceId: this.workspaceId,
|
||||
@@ -50,12 +53,10 @@ export class WorkspaceSQLiteDB extends BaseSQLiteAdapter {
|
||||
}
|
||||
|
||||
const updates = this.getUpdates();
|
||||
const merged = await mergeUpdateWorker(updates.map(update => update.data));
|
||||
|
||||
// to initialize the yDoc, we need to apply all updates from the db
|
||||
Y.transact(this.yDoc, () => {
|
||||
updates.forEach(update => {
|
||||
this.applyUpdate(update.data, 'self');
|
||||
});
|
||||
});
|
||||
this.applyUpdate(merged, 'self');
|
||||
|
||||
this.firstConnected = true;
|
||||
this.update$.next();
|
||||
@@ -100,6 +101,6 @@ export async function openWorkspaceDatabase(
|
||||
) {
|
||||
const meta = await getWorkspaceMeta(context, workspaceId);
|
||||
const db = new WorkspaceSQLiteDB(meta.mainDBPath, workspaceId);
|
||||
await db.connect();
|
||||
await db.init();
|
||||
return db;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function revealDBFile(workspaceId: string) {
|
||||
}
|
||||
|
||||
// provide a backdoor to set dialog path for testing in playwright
|
||||
interface FakeDialogResult {
|
||||
export interface FakeDialogResult {
|
||||
canceled?: boolean;
|
||||
filePath?: string;
|
||||
filePaths?: string[];
|
||||
@@ -63,7 +63,7 @@ const ErrorMessages = [
|
||||
|
||||
type ErrorMessage = (typeof ErrorMessages)[number];
|
||||
|
||||
interface SaveDBFileResult {
|
||||
export interface SaveDBFileResult {
|
||||
filePath?: string;
|
||||
canceled?: boolean;
|
||||
error?: ErrorMessage;
|
||||
@@ -122,7 +122,7 @@ export async function saveDBFileAs(
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectDBFileLocationResult {
|
||||
export interface SelectDBFileLocationResult {
|
||||
filePath?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
@@ -154,7 +154,7 @@ export async function selectDBFileLocation(): Promise<SelectDBFileLocationResult
|
||||
}
|
||||
}
|
||||
|
||||
interface LoadDBFileResult {
|
||||
export interface LoadDBFileResult {
|
||||
workspaceId?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
@@ -237,7 +237,7 @@ export async function loadDBFile(): Promise<LoadDBFileResult> {
|
||||
}
|
||||
}
|
||||
|
||||
interface MoveDBFileResult {
|
||||
export interface MoveDBFileResult {
|
||||
filePath?: string;
|
||||
error?: ErrorMessage;
|
||||
canceled?: boolean;
|
||||
|
||||
@@ -21,9 +21,18 @@ export function registerEvents() {
|
||||
// register events
|
||||
for (const [namespace, namespaceEvents] of Object.entries(allEvents)) {
|
||||
for (const [key, eventRegister] of Object.entries(namespaceEvents)) {
|
||||
const subscription = eventRegister((...args: any) => {
|
||||
const subscription = eventRegister((...args: any[]) => {
|
||||
const chan = `${namespace}:${key}`;
|
||||
logger.info('[ipc-event]', chan, args);
|
||||
logger.info(
|
||||
'[ipc-event]',
|
||||
chan,
|
||||
args.filter(
|
||||
a =>
|
||||
a !== undefined &&
|
||||
typeof a !== 'function' &&
|
||||
typeof a !== 'object'
|
||||
)
|
||||
);
|
||||
getActiveWindows().forEach(win => win.webContents.send(chan, ...args));
|
||||
});
|
||||
app.on('before-quit', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { logger } from '../logger';
|
||||
import type { ErrorMessage } from './utils';
|
||||
import { getFakedResult } from './utils';
|
||||
|
||||
interface SavePDFFileResult {
|
||||
export interface SavePDFFileResult {
|
||||
filePath?: string;
|
||||
canceled?: boolean;
|
||||
error?: ErrorMessage;
|
||||
|
||||
@@ -30,3 +30,7 @@ export const getExposedMeta = () => {
|
||||
events: eventsMeta,
|
||||
};
|
||||
};
|
||||
|
||||
export type MainIPCHandlerMap = typeof handlers;
|
||||
|
||||
export type MainIPCEventMap = typeof events;
|
||||
|
||||
@@ -2,9 +2,9 @@ import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import electronWindowState from 'electron-window-state';
|
||||
import { join } from 'path';
|
||||
|
||||
import { isMacOS, isWindows } from '../../utils';
|
||||
import { getExposedMeta } from './exposed';
|
||||
import { logger } from './logger';
|
||||
import { isMacOS, isWindows } from './utils';
|
||||
|
||||
const IS_DEV: boolean =
|
||||
process.env.NODE_ENV === 'development' && !process.env.CI;
|
||||
|
||||
49
apps/electron/layers/main/src/ui/get-meta-data/get-html.ts
Normal file
49
apps/electron/layers/main/src/ui/get-meta-data/get-html.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import type { GetHTMLOptions } from './types';
|
||||
|
||||
async function getHTMLFromWindow(win: BrowserWindow): Promise<string> {
|
||||
return win.webContents
|
||||
.executeJavaScript(`document.documentElement.outerHTML;`)
|
||||
.then(html => html);
|
||||
}
|
||||
|
||||
// For normal web pages, obtaining html can be done directly,
|
||||
// but for some dynamic web pages, obtaining html should wait for the complete loading of web pages. shouldReGetHTML should be used to judge whether to obtain html again
|
||||
export async function getHTMLByURL(
|
||||
url: string,
|
||||
options: GetHTMLOptions
|
||||
): Promise<string> {
|
||||
return new Promise(resolve => {
|
||||
const { timeout = 10000, shouldReGetHTML } = options;
|
||||
const window = new BrowserWindow({
|
||||
show: false,
|
||||
});
|
||||
let html = '';
|
||||
window.loadURL(url);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
resolve(html);
|
||||
window.close();
|
||||
}, timeout);
|
||||
|
||||
async function loopHandle() {
|
||||
html = await getHTMLFromWindow(window);
|
||||
if (!shouldReGetHTML) {
|
||||
return html;
|
||||
}
|
||||
|
||||
if (await shouldReGetHTML(html)) {
|
||||
setTimeout(loopHandle, 1000);
|
||||
} else {
|
||||
window.close();
|
||||
clearTimeout(timer);
|
||||
resolve(html);
|
||||
}
|
||||
}
|
||||
|
||||
window.webContents.on('did-finish-load', async () => {
|
||||
loopHandle();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,23 +1,15 @@
|
||||
import type { CheerioAPI, Element } from 'cheerio';
|
||||
import { load } from 'cheerio';
|
||||
import got from 'got';
|
||||
|
||||
import type { Context, MetaData, Options, RuleSet } from './types';
|
||||
|
||||
export * from './types';
|
||||
|
||||
import { getHTMLByURL } from './get-html';
|
||||
import { metaDataRules } from './rules';
|
||||
import type { GetMetaDataOptions } from './types';
|
||||
|
||||
const defaultOptions = {
|
||||
maxRedirects: 5,
|
||||
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36',
|
||||
lang: '*',
|
||||
timeout: 10000,
|
||||
forceImageHttps: true,
|
||||
customRules: {},
|
||||
};
|
||||
|
||||
const runRule = function (ruleSet: RuleSet, $: CheerioAPI, context: Context) {
|
||||
function runRule(ruleSet: RuleSet, $: CheerioAPI, context: Context) {
|
||||
let maxScore = 0;
|
||||
let value;
|
||||
|
||||
@@ -58,61 +50,31 @@ const runRule = function (ruleSet: RuleSet, $: CheerioAPI, context: Context) {
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
const getMetaData = async function (
|
||||
input: string | Partial<Options>,
|
||||
inputOptions: Partial<Options> = {}
|
||||
async function getMetaDataByHTML(
|
||||
html: string,
|
||||
url: string,
|
||||
options: GetMetaDataOptions
|
||||
) {
|
||||
let url;
|
||||
if (typeof input === 'object') {
|
||||
inputOptions = input;
|
||||
url = input.url || '';
|
||||
} else {
|
||||
url = input;
|
||||
}
|
||||
|
||||
const options = Object.assign({}, defaultOptions, inputOptions);
|
||||
|
||||
const { customRules = {} } = options;
|
||||
const rules: Record<string, RuleSet> = { ...metaDataRules };
|
||||
Object.keys(options.customRules).forEach((key: string) => {
|
||||
Object.keys(customRules).forEach((key: string) => {
|
||||
rules[key] = {
|
||||
rules: [...metaDataRules[key].rules, ...options.customRules[key].rules],
|
||||
rules: [...metaDataRules[key].rules, ...customRules[key].rules],
|
||||
defaultValue:
|
||||
options.customRules[key].defaultValue ||
|
||||
metaDataRules[key].defaultValue,
|
||||
processor:
|
||||
options.customRules[key].processor || metaDataRules[key].processor,
|
||||
customRules[key].defaultValue || metaDataRules[key].defaultValue,
|
||||
processor: customRules[key].processor || metaDataRules[key].processor,
|
||||
};
|
||||
});
|
||||
|
||||
let html;
|
||||
if (!options.html) {
|
||||
const response = await got(url, {
|
||||
headers: {
|
||||
'User-Agent': options.ua,
|
||||
'Accept-Language': options.lang,
|
||||
},
|
||||
timeout: options.timeout,
|
||||
...(options.maxRedirects === 0
|
||||
? { followRedirect: false }
|
||||
: { maxRedirects: options.maxRedirects }),
|
||||
});
|
||||
html = response.body;
|
||||
} else {
|
||||
html = options.html;
|
||||
}
|
||||
|
||||
const metadata: MetaData = {};
|
||||
const context: Context = {
|
||||
url,
|
||||
options,
|
||||
...options,
|
||||
};
|
||||
|
||||
const $ = load(html);
|
||||
// console.log('===============================');
|
||||
// console.log('html');
|
||||
// console.log(doc);
|
||||
|
||||
Object.keys(rules).forEach((key: string) => {
|
||||
const ruleSet = rules[key];
|
||||
@@ -120,6 +82,26 @@ const getMetaData = async function (
|
||||
});
|
||||
|
||||
return metadata;
|
||||
};
|
||||
}
|
||||
|
||||
export { getMetaData };
|
||||
export async function getMetaData(url: string, options: Options = {}) {
|
||||
const { customRules, forceImageHttps, shouldReGetHTML, ...other } = options;
|
||||
const html = await getHTMLByURL(url, {
|
||||
...other,
|
||||
shouldReGetHTML: async html => {
|
||||
const meta = await getMetaDataByHTML(html, url, {
|
||||
customRules,
|
||||
forceImageHttps,
|
||||
});
|
||||
return shouldReGetHTML ? await shouldReGetHTML(meta) : false;
|
||||
},
|
||||
}).catch(() => {
|
||||
// TODO: report error
|
||||
return '';
|
||||
});
|
||||
|
||||
return await getMetaDataByHTML(html, url, {
|
||||
customRules,
|
||||
forceImageHttps,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -591,7 +591,7 @@ export const metaDataRules: Record<string, RuleSet> = {
|
||||
],
|
||||
],
|
||||
processor: (imageUrl: any, context) =>
|
||||
context.options.forceImageHttps === true
|
||||
context.forceImageHttps === true
|
||||
? makeUrlSecure(makeUrlAbsolute(context.url, imageUrl))
|
||||
: makeUrlAbsolute(context.url, imageUrl),
|
||||
},
|
||||
@@ -625,7 +625,7 @@ export const metaDataRules: Record<string, RuleSet> = {
|
||||
},
|
||||
defaultValue: context => makeUrlAbsolute(context.url, '/favicon.ico'),
|
||||
processor: (iconUrl, context) =>
|
||||
context.options.forceImageHttps === true
|
||||
context.forceImageHttps === true
|
||||
? makeUrlSecure(makeUrlAbsolute(context.url, iconUrl))
|
||||
: makeUrlAbsolute(context.url, iconUrl),
|
||||
},
|
||||
@@ -654,7 +654,7 @@ export const metaDataRules: Record<string, RuleSet> = {
|
||||
['meta[name="og:video"][content]', element => element.attribs['content']],
|
||||
],
|
||||
processor: (imageUrl: any, context) =>
|
||||
context.options.forceImageHttps === true
|
||||
context.forceImageHttps === true
|
||||
? makeUrlSecure(makeUrlAbsolute(context.url, imageUrl))
|
||||
: makeUrlAbsolute(context.url, imageUrl),
|
||||
},
|
||||
@@ -683,7 +683,7 @@ export const metaDataRules: Record<string, RuleSet> = {
|
||||
['meta[name="og:audio"][content]', element => element.attribs['content']],
|
||||
],
|
||||
processor: (imageUrl: any, context) =>
|
||||
context.options.forceImageHttps === true
|
||||
context.forceImageHttps === true
|
||||
? makeUrlSecure(makeUrlAbsolute(context.url, imageUrl))
|
||||
: makeUrlAbsolute(context.url, imageUrl),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Element } from 'cheerio';
|
||||
|
||||
export interface MetaData {
|
||||
export type MetaData = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
@@ -12,29 +12,32 @@ export interface MetaData {
|
||||
provider?: string;
|
||||
|
||||
[x: string]: string | string[] | undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export type MetadataRule = [string, (el: Element) => string | null];
|
||||
|
||||
export interface Context {
|
||||
export type Context = {
|
||||
url: string;
|
||||
options: Options;
|
||||
}
|
||||
} & GetMetaDataOptions;
|
||||
|
||||
export interface RuleSet {
|
||||
export type RuleSet = {
|
||||
rules: MetadataRule[];
|
||||
defaultValue?: (context: Context) => string | string[];
|
||||
scorer?: (el: Element, score: any) => any;
|
||||
processor?: (input: any, context: Context) => any;
|
||||
}
|
||||
};
|
||||
|
||||
export interface Options {
|
||||
maxRedirects?: number;
|
||||
ua?: string;
|
||||
lang?: string;
|
||||
timeout?: number;
|
||||
forceImageHttps?: boolean;
|
||||
html?: string;
|
||||
url?: string;
|
||||
export type GetMetaDataOptions = {
|
||||
customRules?: Record<string, RuleSet>;
|
||||
}
|
||||
forceImageHttps?: boolean;
|
||||
};
|
||||
|
||||
export type GetHTMLOptions = {
|
||||
timeout?: number;
|
||||
shouldReGetHTML?: (currentHTML: string) => boolean | Promise<boolean>;
|
||||
};
|
||||
|
||||
export type Options = {
|
||||
shouldReGetHTML?: (metaData: MetaData) => boolean | Promise<boolean>;
|
||||
} & GetMetaDataOptions &
|
||||
Omit<GetHTMLOptions, 'shouldReGetHTML'>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { app, BrowserWindow, nativeTheme, session } from 'electron';
|
||||
import { app, BrowserWindow, nativeTheme } from 'electron';
|
||||
|
||||
import { isMacOS } from '../../../utils';
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import { isMacOS } from '../utils';
|
||||
import { getMetaData } from './get-meta-data';
|
||||
import { getGoogleOauthCode } from './google-auth';
|
||||
|
||||
@@ -42,7 +42,9 @@ export const uiHandlers = {
|
||||
},
|
||||
getBookmarkDataByLink: async (_, url: string) => {
|
||||
return getMetaData(url, {
|
||||
ua: session.defaultSession.getUserAgent(),
|
||||
shouldReGetHTML: metaData => {
|
||||
return !metaData.title && !metaData.description;
|
||||
},
|
||||
});
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { app } from 'electron';
|
||||
import type { AppUpdater } from 'electron-updater';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { isMacOS } from '../../../utils';
|
||||
import { logger } from '../logger';
|
||||
import { isMacOS } from '../utils';
|
||||
import { updaterSubjects } from './event';
|
||||
|
||||
export const ReleaseTypeSchema = z.enum([
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BehaviorSubject, Subject } from 'rxjs';
|
||||
|
||||
import type { MainEventListener } from '../type';
|
||||
|
||||
interface UpdateMeta {
|
||||
export interface UpdateMeta {
|
||||
version: string;
|
||||
allowAutoUpdate: boolean;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
export function getTime() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
export const isMacOS = () => {
|
||||
return process.platform === 'darwin';
|
||||
};
|
||||
|
||||
export const isWindows = () => {
|
||||
return process.platform === 'win32';
|
||||
};
|
||||
|
||||
35
apps/electron/layers/main/src/workers/index.ts
Normal file
35
apps/electron/layers/main/src/workers/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import path from 'node:path';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
import { mergeUpdate } from './merge-update';
|
||||
|
||||
export function mergeUpdateWorker(updates: Uint8Array[]) {
|
||||
// fallback to main thread if worker is disabled (in vitest)
|
||||
if (process.env.USE_WORKER !== 'true') {
|
||||
return mergeUpdate(updates);
|
||||
}
|
||||
return new Promise<Uint8Array>((resolve, reject) => {
|
||||
// it is intended to have "./workers" in the path
|
||||
const workerFile = path.join(__dirname, './workers/merge-update.worker.js');
|
||||
|
||||
// convert updates to SharedArrayBuffer[s]
|
||||
const sharedArrayBufferUpdates = updates.map(update => {
|
||||
const buffer = new SharedArrayBuffer(update.byteLength);
|
||||
const view = new Uint8Array(buffer);
|
||||
view.set(update);
|
||||
return view;
|
||||
});
|
||||
|
||||
const worker = new Worker(workerFile, {
|
||||
workerData: sharedArrayBufferUpdates,
|
||||
});
|
||||
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
worker.on('exit', code => {
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Worker stopped with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
11
apps/electron/layers/main/src/workers/merge-update.ts
Normal file
11
apps/electron/layers/main/src/workers/merge-update.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as Y from 'yjs';
|
||||
|
||||
export function mergeUpdate(updates: Uint8Array[]) {
|
||||
const yDoc = new Y.Doc();
|
||||
Y.transact(yDoc, () => {
|
||||
for (const update of updates) {
|
||||
Y.applyUpdate(yDoc, update);
|
||||
}
|
||||
});
|
||||
return Y.encodeStateAsUpdate(yDoc);
|
||||
}
|
||||
14
apps/electron/layers/main/src/workers/merge-update.worker.ts
Normal file
14
apps/electron/layers/main/src/workers/merge-update.worker.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { parentPort, workerData } from 'node:worker_threads';
|
||||
|
||||
import { mergeUpdate } from './merge-update';
|
||||
|
||||
function getMergeUpdate(updates: Uint8Array[]) {
|
||||
const update = mergeUpdate(updates);
|
||||
const buffer = new SharedArrayBuffer(update.byteLength);
|
||||
const view = new Uint8Array(buffer);
|
||||
view.set(update);
|
||||
|
||||
return update;
|
||||
}
|
||||
|
||||
parentPort?.postMessage(getMergeUpdate(workerData));
|
||||
1
apps/electron/layers/main/src/workspace/__tests__/.gitignore
vendored
Normal file
1
apps/electron/layers/main/src/workspace/__tests__/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
tmp
|
||||
@@ -0,0 +1,208 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { v4 } from 'uuid';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { AppContext } from '../../context';
|
||||
|
||||
const tmpDir = path.join(__dirname, 'tmp');
|
||||
|
||||
const testAppContext: AppContext = {
|
||||
appDataPath: path.join(tmpDir, 'test-data'),
|
||||
appName: 'test',
|
||||
};
|
||||
|
||||
vi.doMock('../../context', () => ({
|
||||
appContext: testAppContext,
|
||||
}));
|
||||
|
||||
vi.doMock('../../db/ensure-db', () => ({
|
||||
ensureSQLiteDB: async () => ({
|
||||
destroy: () => {},
|
||||
}),
|
||||
}));
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.remove(tmpDir);
|
||||
});
|
||||
|
||||
describe('list workspaces', () => {
|
||||
test('listWorkspaces (valid)', async () => {
|
||||
const { listWorkspaces } = await import('../handlers');
|
||||
const workspaceId = v4();
|
||||
const workspacePath = path.join(
|
||||
testAppContext.appDataPath,
|
||||
'workspaces',
|
||||
workspaceId
|
||||
);
|
||||
const meta = {
|
||||
id: workspaceId,
|
||||
};
|
||||
await fs.ensureDir(workspacePath);
|
||||
await fs.writeJSON(path.join(workspacePath, 'meta.json'), meta);
|
||||
const workspaces = await listWorkspaces(testAppContext);
|
||||
expect(workspaces).toEqual([[workspaceId, meta]]);
|
||||
});
|
||||
|
||||
test('listWorkspaces (without meta json file)', async () => {
|
||||
const { listWorkspaces } = await import('../handlers');
|
||||
const workspaceId = v4();
|
||||
const workspacePath = path.join(
|
||||
testAppContext.appDataPath,
|
||||
'workspaces',
|
||||
workspaceId
|
||||
);
|
||||
await fs.ensureDir(workspacePath);
|
||||
const workspaces = await listWorkspaces(testAppContext);
|
||||
expect(workspaces).toEqual([
|
||||
[
|
||||
workspaceId,
|
||||
// meta file will be created automatically
|
||||
{ id: workspaceId, mainDBPath: path.join(workspacePath, 'storage.db') },
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete workspace', () => {
|
||||
test('deleteWorkspace', async () => {
|
||||
const { deleteWorkspace } = await import('../handlers');
|
||||
const workspaceId = v4();
|
||||
const workspacePath = path.join(
|
||||
testAppContext.appDataPath,
|
||||
'workspaces',
|
||||
workspaceId
|
||||
);
|
||||
await fs.ensureDir(workspacePath);
|
||||
await deleteWorkspace(testAppContext, workspaceId);
|
||||
expect(await fs.pathExists(workspacePath)).toBe(false);
|
||||
// removed workspace will be moved to delete-workspaces
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
path.join(testAppContext.appDataPath, 'delete-workspaces', workspaceId)
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkspaceMeta', () => {
|
||||
test('can get meta', async () => {
|
||||
const { getWorkspaceMeta } = await import('../handlers');
|
||||
const workspaceId = v4();
|
||||
const workspacePath = path.join(
|
||||
testAppContext.appDataPath,
|
||||
'workspaces',
|
||||
workspaceId
|
||||
);
|
||||
const meta = {
|
||||
id: workspaceId,
|
||||
};
|
||||
await fs.ensureDir(workspacePath);
|
||||
await fs.writeJSON(path.join(workspacePath, 'meta.json'), meta);
|
||||
expect(await getWorkspaceMeta(testAppContext, workspaceId)).toEqual(meta);
|
||||
});
|
||||
|
||||
test('can create meta if not exists', async () => {
|
||||
const { getWorkspaceMeta } = await import('../handlers');
|
||||
const workspaceId = v4();
|
||||
const workspacePath = path.join(
|
||||
testAppContext.appDataPath,
|
||||
'workspaces',
|
||||
workspaceId
|
||||
);
|
||||
await fs.ensureDir(workspacePath);
|
||||
expect(await getWorkspaceMeta(testAppContext, workspaceId)).toEqual({
|
||||
id: workspaceId,
|
||||
mainDBPath: path.join(workspacePath, 'storage.db'),
|
||||
});
|
||||
expect(
|
||||
await fs.pathExists(path.join(workspacePath, 'meta.json'))
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('can migrate meta if db file is a link', async () => {
|
||||
const { getWorkspaceMeta } = await import('../handlers');
|
||||
const workspaceId = v4();
|
||||
const workspacePath = path.join(
|
||||
testAppContext.appDataPath,
|
||||
'workspaces',
|
||||
workspaceId
|
||||
);
|
||||
await fs.ensureDir(workspacePath);
|
||||
const sourcePath = path.join(tmpDir, 'source.db');
|
||||
await fs.writeFile(sourcePath, 'test');
|
||||
|
||||
await fs.ensureSymlink(sourcePath, path.join(workspacePath, 'storage.db'));
|
||||
|
||||
expect(await getWorkspaceMeta(testAppContext, workspaceId)).toEqual({
|
||||
id: workspaceId,
|
||||
mainDBPath: path.join(workspacePath, 'storage.db'),
|
||||
secondaryDBPath: sourcePath,
|
||||
});
|
||||
|
||||
expect(
|
||||
await fs.pathExists(path.join(workspacePath, 'meta.json'))
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test('storeWorkspaceMeta', async () => {
|
||||
const { storeWorkspaceMeta } = await import('../handlers');
|
||||
const workspaceId = v4();
|
||||
const workspacePath = path.join(
|
||||
testAppContext.appDataPath,
|
||||
'workspaces',
|
||||
workspaceId
|
||||
);
|
||||
await fs.ensureDir(workspacePath);
|
||||
const meta = {
|
||||
id: workspaceId,
|
||||
mainDBPath: path.join(workspacePath, 'storage.db'),
|
||||
};
|
||||
await storeWorkspaceMeta(testAppContext, workspaceId, meta);
|
||||
expect(await fs.readJSON(path.join(workspacePath, 'meta.json'))).toEqual(
|
||||
meta
|
||||
);
|
||||
await storeWorkspaceMeta(testAppContext, workspaceId, {
|
||||
secondaryDBPath: path.join(tmpDir, 'test.db'),
|
||||
});
|
||||
expect(await fs.readJSON(path.join(workspacePath, 'meta.json'))).toEqual({
|
||||
...meta,
|
||||
secondaryDBPath: path.join(tmpDir, 'test.db'),
|
||||
});
|
||||
});
|
||||
|
||||
test('getWorkspaceMeta observable', async () => {
|
||||
const { storeWorkspaceMeta } = await import('../handlers');
|
||||
const { getWorkspaceMeta$ } = await import('../index');
|
||||
|
||||
const workspaceId = v4();
|
||||
const workspacePath = path.join(
|
||||
testAppContext.appDataPath,
|
||||
'workspaces',
|
||||
workspaceId
|
||||
);
|
||||
|
||||
const metaChange = vi.fn();
|
||||
|
||||
const meta$ = getWorkspaceMeta$(workspaceId);
|
||||
|
||||
meta$.subscribe(metaChange);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
expect(metaChange).toHaveBeenCalledWith({
|
||||
id: workspaceId,
|
||||
mainDBPath: path.join(workspacePath, 'storage.db'),
|
||||
});
|
||||
|
||||
await storeWorkspaceMeta(testAppContext, workspaceId, {
|
||||
secondaryDBPath: path.join(tmpDir, 'test.db'),
|
||||
});
|
||||
|
||||
expect(metaChange).toHaveBeenCalledWith({
|
||||
id: workspaceId,
|
||||
mainDBPath: path.join(workspacePath, 'storage.db'),
|
||||
secondaryDBPath: path.join(tmpDir, 'test.db'),
|
||||
});
|
||||
});
|
||||
@@ -42,7 +42,6 @@ export async function deleteWorkspace(context: AppContext, id: string) {
|
||||
try {
|
||||
const db = await ensureSQLiteDB(id);
|
||||
db.destroy();
|
||||
// TODO: should remove DB connection first
|
||||
return await fs.move(basePath, movedPath, {
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { from, merge } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { merge } from 'rxjs';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
|
||||
import { appContext } from '../context';
|
||||
import type {
|
||||
@@ -35,7 +35,10 @@ export const workspaceHandlers = {
|
||||
// used internally. Get a stream of workspace id -> meta
|
||||
export const getWorkspaceMeta$ = (workspaceId: string) => {
|
||||
return merge(
|
||||
from(getWorkspaceMeta(appContext, workspaceId)),
|
||||
workspaceSubjects.meta.pipe(map(meta => meta.meta))
|
||||
getWorkspaceMeta(appContext, workspaceId),
|
||||
workspaceSubjects.meta.pipe(
|
||||
map(meta => meta.meta),
|
||||
filter(meta => meta.id === workspaceId)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
7
apps/electron/layers/preload/preload.d.ts
vendored
7
apps/electron/layers/preload/preload.d.ts
vendored
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/consistent-type-imports */
|
||||
|
||||
interface Window {
|
||||
apis: typeof import('./src/affine-apis').apis;
|
||||
events: typeof import('./src/affine-apis').events;
|
||||
appInfo: typeof import('./src/affine-apis').appInfo;
|
||||
declare interface Window {
|
||||
apis: import('./src/affine-apis').PreloadHandlers;
|
||||
events: import('./src/affine-apis').MainIPCEventMap;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
// NOTE: we will generate preload types from this file
|
||||
|
||||
// NOTE: we will generate preload types from this file
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import type { MainIPCEventMap, MainIPCHandlerMap } from '../../constraints';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import type {
|
||||
MainIPCEventMap,
|
||||
MainIPCHandlerMap,
|
||||
} from '../../main/src/exposed';
|
||||
|
||||
type WithoutFirstParameter<T> = T extends (_: any, ...args: infer P) => infer R
|
||||
? (...args: P) => R
|
||||
@@ -15,7 +19,7 @@ type HandlersMap<N extends keyof MainIPCHandlerMap> = {
|
||||
>;
|
||||
};
|
||||
|
||||
type PreloadHandlers = {
|
||||
export type PreloadHandlers = {
|
||||
[N in keyof MainIPCHandlerMap]: HandlersMap<N>;
|
||||
};
|
||||
|
||||
@@ -88,3 +92,6 @@ const appInfo = {
|
||||
};
|
||||
|
||||
export { apis, appInfo, events };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
export type { MainIPCEventMap } from '../../main/src/exposed';
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export const isMacOS = () => {
|
||||
return process.platform === 'darwin';
|
||||
};
|
||||
|
||||
export const isWindows = () => {
|
||||
return process.platform === 'win32';
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.2",
|
||||
"version": "0.7.0-canary.8",
|
||||
"author": "affine",
|
||||
"repository": {
|
||||
"url": "https://github.com/toeverything/AFFiNE",
|
||||
@@ -42,13 +42,13 @@
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "24.4.0",
|
||||
"electron": "25.0.0",
|
||||
"electron-log": "^5.0.0-beta.24",
|
||||
"electron-squirrel-startup": "1.0.0",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"esbuild": "^0.17.19",
|
||||
"fs-extra": "^11.1.1",
|
||||
"playwright": "^1.33.0",
|
||||
"playwright": "=1.33.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"undici": "^5.22.1",
|
||||
"uuid": "^9.0.0",
|
||||
|
||||
@@ -23,6 +23,7 @@ export const config = () => {
|
||||
JSON.stringify(process.env[key] ?? ''),
|
||||
]),
|
||||
['process.env.NODE_ENV', `"${mode}"`],
|
||||
['process.env.USE_WORKER', '"true"'],
|
||||
]);
|
||||
|
||||
if (DEV_SERVER_URL) {
|
||||
@@ -31,7 +32,10 @@ export const config = () => {
|
||||
|
||||
return {
|
||||
main: {
|
||||
entryPoints: [resolve(root, './layers/main/src/index.ts')],
|
||||
entryPoints: [
|
||||
resolve(root, './layers/main/src/index.ts'),
|
||||
resolve(root, './layers/main/src/workers/merge-update.worker.ts'),
|
||||
],
|
||||
outdir: resolve(root, './dist/layers/main'),
|
||||
bundle: true,
|
||||
target: `node${NODE_MAJOR_VERSION}`,
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"target": "ESNext"
|
||||
},
|
||||
"references": [{ "path": "../../../tests/kit" }],
|
||||
"include": ["**.spec.ts", "**.test.ts"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
@@ -10,17 +11,22 @@
|
||||
"outDir": "dist",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"noImplicitOverride": true
|
||||
"noImplicitOverride": true,
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx", "package.json"],
|
||||
"exclude": ["out", "dist", "node_modules"],
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules", "out", "dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/native"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "../../packages/env"
|
||||
},
|
||||
{ "path": "../../tests/kit" }
|
||||
],
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": false
|
||||
},
|
||||
"include": ["./scripts", "package.json"]
|
||||
"include": ["./scripts"]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.2",
|
||||
"version": "0.7.0-canary.8",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -21,14 +21,14 @@
|
||||
"@nestjs/graphql": "^11.0.6",
|
||||
"@nestjs/platform-express": "^9.4.2",
|
||||
"@node-rs/bcrypt": "^1.7.1",
|
||||
"@prisma/client": "^4.14.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"@prisma/client": "^4.15.0",
|
||||
"dotenv": "^16.1.1",
|
||||
"express": "^4.18.2",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"prisma": "^4.14.1",
|
||||
"prisma": "^4.15.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
@@ -39,12 +39,12 @@
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/node": "^18.16.16",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"c8": "^7.13.0",
|
||||
"c8": "^7.14.0",
|
||||
"nodemon": "^2.0.22",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4",
|
||||
"vitest": "^0.31.1"
|
||||
"vitest": "^0.31.2"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"exec": "node",
|
||||
|
||||
@@ -10,7 +10,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export const enum ExternalAccount {
|
||||
export enum ExternalAccount {
|
||||
github = 'github',
|
||||
google = 'google',
|
||||
firebase = 'firebase',
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"outDir": "dist/scripts"
|
||||
},
|
||||
"include": ["scripts", "package.json"]
|
||||
}
|
||||
|
||||
@@ -109,8 +109,10 @@ const nextConfig = {
|
||||
'@affine/templates',
|
||||
'@affine/workspace',
|
||||
'@affine/jotai',
|
||||
'@affine/copilot',
|
||||
'@toeverything/hooks',
|
||||
'@toeverything/y-indexeddb',
|
||||
'@toeverything/plugin-infra',
|
||||
],
|
||||
publicRuntimeConfig: {
|
||||
PROJECT_NAME: process.env.npm_package_name ?? 'AFFiNE',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/web",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.2",
|
||||
"version": "0.7.0-canary.8",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/copilot": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/graphql": "workspace:*",
|
||||
@@ -18,29 +19,30 @@
|
||||
"@affine/jotai": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230530061436-d0702cc0-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230530061436-d0702cc0-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230530061436-d0702cc0-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230601122821-16196c35-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230601122821-16196c35-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230601122821-16196c35-nightly",
|
||||
"@blocksuite/icons": "^2.1.19",
|
||||
"@blocksuite/lit": "0.0.0-20230530061436-d0702cc0-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230530061436-d0702cc0-nightly",
|
||||
"@blocksuite/lit": "0.0.0-20230601122821-16196c35-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230601122821-16196c35-nightly",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.0",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.13.2",
|
||||
"@mui/material": "^5.13.3",
|
||||
"@react-hookz/web": "^23.0.1",
|
||||
"@sentry/nextjs": "^7.53.1",
|
||||
"@toeverything/hooks": "workspace:*",
|
||||
"@toeverything/plugin-infra": "workspace:*",
|
||||
"cmdk": "^0.2.0",
|
||||
"css-spring": "^4.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"jotai": "^2.1.0",
|
||||
"jotai-devtools": "^0.5.3",
|
||||
"lit": "^2.7.4",
|
||||
"lottie-web": "^5.11.0",
|
||||
"lottie-web": "^5.12.0",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "18.3.0-canary-16d053d59-20230506",
|
||||
"react-dom": "18.3.0-canary-16d053d59-20230506",
|
||||
@@ -61,14 +63,14 @@
|
||||
"@swc-jotai/react-refresh": "^0.0.8",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@types/webpack-env": "^1.18.0",
|
||||
"@types/webpack-env": "^1.18.1",
|
||||
"@vanilla-extract/css": "^1.11.0",
|
||||
"@vanilla-extract/next-plugin": "=2.1.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"dotenv": "^16.1.1",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-next": "^13.4.4",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
"next": "^13.4.2",
|
||||
"next": "=13.4.2",
|
||||
"next-debug-local": "^0.1.5",
|
||||
"next-router-mock": "^0.9.3",
|
||||
"raw-loader": "^4.0.2",
|
||||
|
||||
@@ -18,6 +18,7 @@ export const blockSuiteFeatureFlags = {
|
||||
* @type {import('@affine/env').BuildFlags}
|
||||
*/
|
||||
export const buildFlags = {
|
||||
enablePlugin: process.env.ENABLE_PLUGIN === 'true',
|
||||
enableAllPageFilter:
|
||||
!!process.env.VERCEL ||
|
||||
(process.env.ENABLE_ALL_PAGE_FILTER
|
||||
@@ -39,6 +40,5 @@ export const buildFlags = {
|
||||
process.env.ENABLE_DEBUG_PAGE ?? process.env.NODE_ENV === 'development'
|
||||
),
|
||||
changelogUrl:
|
||||
process.env.CHANGELOG_URL ??
|
||||
'https://affine.pro/blog/whats-new-affine-0518',
|
||||
process.env.CHANGELOG_URL ?? 'http://affine.pro/blog/whats-new-affine-0601',
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { affineApis } from '@affine/workspace/affine/shared';
|
||||
import { rootStore } from '@affine/workspace/atom';
|
||||
import { createAffineProviders } from '@affine/workspace/providers';
|
||||
import type { AffineLegacyCloudWorkspace } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
|
||||
import { workspacesAtom } from '../../atoms';
|
||||
import { createAffineProviders } from '../../blocksuite';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
type Query = (typeof QueryKey)[keyof typeof QueryKey];
|
||||
|
||||
|
||||
@@ -14,8 +14,13 @@ import {
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { affineApis, affineAuth } from '@affine/workspace/affine/shared';
|
||||
import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers';
|
||||
import {
|
||||
createAffineProviders,
|
||||
createIndexedDBBackgroundProvider,
|
||||
} from '@affine/workspace/providers';
|
||||
import { createAffineDownloadProvider } from '@affine/workspace/providers';
|
||||
import type { AffineLegacyCloudWorkspace } from '@affine/workspace/type';
|
||||
import {
|
||||
LoadPriority,
|
||||
@@ -32,12 +37,9 @@ import { Suspense, useEffect } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createAffineProviders } from '../../blocksuite';
|
||||
import { createAffineDownloadProvider } from '../../blocksuite/providers/affine';
|
||||
import { PageLoading } from '../../components/pure/loading';
|
||||
import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh-auth-token';
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { affineApis, affineAuth } from '../../shared/apis';
|
||||
import { toast } from '../../utils';
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import {
|
||||
rootCurrentEditorAtom,
|
||||
rootCurrentPageIdAtom,
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
rootWorkspacesMetadataAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { atom } from 'jotai';
|
||||
@@ -17,11 +12,6 @@ import type { CreateWorkspaceMode } from '../components/affine/create-workspace-
|
||||
const logger = new DebugLogger('web:atoms');
|
||||
|
||||
// workspace necessary atoms
|
||||
/**
|
||||
* @deprecated Use `rootCurrentWorkspaceIdAtom` directly instead.
|
||||
*/
|
||||
export const currentWorkspaceIdAtom = rootCurrentWorkspaceIdAtom;
|
||||
|
||||
// todo(himself65): move this to the workspace package
|
||||
rootWorkspacesMetadataAtom.onMount = setAtom => {
|
||||
function createFirst(): RootWorkspaceMetadata[] {
|
||||
@@ -46,7 +36,11 @@ rootWorkspacesMetadataAtom.onMount = setAtom => {
|
||||
const id = setTimeout(() => {
|
||||
setAtom(metadata => {
|
||||
if (abortController.signal.aborted) return metadata;
|
||||
if (metadata.length === 0) {
|
||||
if (
|
||||
metadata.length === 0 &&
|
||||
localStorage.getItem('is-first-open') === null
|
||||
) {
|
||||
localStorage.setItem('is-first-open', 'false');
|
||||
const newMetadata = createFirst();
|
||||
logger.info('create first workspace', newMetadata);
|
||||
return newMetadata;
|
||||
@@ -77,15 +71,6 @@ rootWorkspacesMetadataAtom.onMount = setAtom => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use `rootCurrentPageIdAtom` directly instead.
|
||||
*/
|
||||
export const currentPageIdAtom = rootCurrentPageIdAtom;
|
||||
/**
|
||||
* @deprecated Use `rootCurrentEditorAtom` directly instead.
|
||||
*/
|
||||
export const currentEditorAtom = rootCurrentEditorAtom;
|
||||
|
||||
// modal atoms
|
||||
export const openWorkspacesModalAtom = atom(false);
|
||||
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
||||
@@ -139,3 +124,6 @@ export const workspaceRecentViresWriteAtom = atom<null, [string, View], View[]>(
|
||||
return record[id];
|
||||
}
|
||||
);
|
||||
|
||||
export type PageModeOption = 'all' | 'page' | 'edgeless';
|
||||
export const pageModeSelectAtom = atom<PageModeOption>('all');
|
||||
|
||||
34
apps/web/src/atoms/layout.ts
Normal file
34
apps/web/src/atoms/layout.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { ExpectedLayout } from '@toeverything/plugin-infra/type';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const contentLayoutBaseAtom = atom<ExpectedLayout>('editor');
|
||||
|
||||
type SetStateAction<Value> = Value | ((prev: Value) => Value);
|
||||
export const contentLayoutAtom = atom<
|
||||
ExpectedLayout,
|
||||
[SetStateAction<ExpectedLayout>],
|
||||
void
|
||||
>(
|
||||
get => get(contentLayoutBaseAtom),
|
||||
(get, set, layout) => {
|
||||
set(contentLayoutBaseAtom, prev => {
|
||||
let setV: (prev: ExpectedLayout) => ExpectedLayout;
|
||||
if (typeof layout !== 'function') {
|
||||
setV = () => layout;
|
||||
} else {
|
||||
setV = layout;
|
||||
}
|
||||
const nextValue = setV(prev);
|
||||
if (nextValue === 'editor') {
|
||||
return nextValue;
|
||||
}
|
||||
if (nextValue.first !== 'editor') {
|
||||
throw new Error('The first element of the layout should be editor.');
|
||||
}
|
||||
if (nextValue.splitPercentage && nextValue.splitPercentage < 70) {
|
||||
throw new Error('The split percentage should be greater than 70.');
|
||||
}
|
||||
return nextValue;
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { BlockSuiteFeatureFlags } from '@affine/env';
|
||||
import { config } from '@affine/env';
|
||||
import { affineApis } from '@affine/workspace/affine/shared';
|
||||
import type { AffinePublicWorkspace } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
function createPublicWorkspace(
|
||||
workspaceId: string,
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { config } from '@affine/env';
|
||||
import {
|
||||
createIndexedDBDownloadProvider,
|
||||
createLocalProviders,
|
||||
} from '@affine/workspace/providers';
|
||||
import {
|
||||
createAffineWebSocketProvider,
|
||||
createBroadCastChannelProvider,
|
||||
} from '@affine/workspace/providers';
|
||||
import type { Provider } from '@affine/workspace/type';
|
||||
|
||||
import type { BlockSuiteWorkspace } from '../shared';
|
||||
import { createAffineDownloadProvider } from './providers/affine';
|
||||
|
||||
export const createAffineProviders = (
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
): Provider[] => {
|
||||
return (
|
||||
[
|
||||
createAffineDownloadProvider(blockSuiteWorkspace),
|
||||
createAffineWebSocketProvider(blockSuiteWorkspace),
|
||||
config.enableBroadCastChannelProvider &&
|
||||
createBroadCastChannelProvider(blockSuiteWorkspace),
|
||||
createIndexedDBDownloadProvider(blockSuiteWorkspace),
|
||||
] as any[]
|
||||
).filter(v => Boolean(v));
|
||||
};
|
||||
|
||||
export { createLocalProviders };
|
||||
@@ -1,3 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
|
||||
export const providerLogger = new DebugLogger('provider');
|
||||
11
apps/web/src/bootstrap/index.ts
Normal file
11
apps/web/src/bootstrap/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { config, setupGlobal } from '@affine/env/config';
|
||||
|
||||
setupGlobal();
|
||||
|
||||
if (config.enablePlugin && !environment.isServer) {
|
||||
import('@affine/copilot');
|
||||
}
|
||||
|
||||
if (!environment.isServer) {
|
||||
import('@affine/bookmark-block');
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ProviderComposer 1`] = `
|
||||
<DocumentFragment>
|
||||
test1
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@@ -1,26 +0,0 @@
|
||||
export const SidebarSwitchIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11 5.00009V19.0001M6 7.62509H8M6 10.2501H8M6 12.8751H8M6.2 19.0001H17.8C18.9201 19.0001 19.4802 19.0001 19.908 18.8094C20.2843 18.6416 20.5903 18.3739 20.782 18.0446C21 17.6702 21 17.1802 21 16.2001V7.80009C21 6.82 21 6.32995 20.782 5.95561C20.5903 5.62632 20.2843 5.35861 19.908 5.19083C19.4802 5.00009 18.9201 5.00009 17.8 5.00009H6.2C5.0799 5.00009 4.51984 5.00009 4.09202 5.19083C3.71569 5.35861 3.40973 5.62632 3.21799 5.95561C3 6.32995 3 6.82 3 7.80009V16.2001C3 17.1802 3 17.6702 3.21799 18.0446C3.40973 18.3739 3.71569 18.6416 4.09202 18.8094C4.51984 19.0001 5.07989 19.0001 6.2 19.0001Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11 5.00009V19.0001M6 7.62509H8M6 10.2501H8M6 12.8751H8M6.2 19.0001H17.8C18.9201 19.0001 19.4802 19.0001 19.908 18.8094C20.2843 18.6416 20.5903 18.3739 20.782 18.0446C21 17.6702 21 17.1802 21 16.2001V7.80009C21 6.82 21 6.32995 20.782 5.95561C20.5903 5.62632 20.2843 5.35861 19.908 5.19083C19.4802 5.00009 18.9201 5.00009 17.8 5.00009H6.2C5.0799 5.00009 4.51984 5.00009 4.09202 5.19083C3.71569 5.35861 3.40973 5.62632 3.21799 5.95561C3 6.32995 3 6.82 3 7.80009V16.2001C3 17.1802 3 17.6702 3.21799 18.0446C3.40973 18.3739 3.71569 18.6416 4.09202 18.8094C4.51984 19.0001 5.07989 19.0001 6.2 19.0001Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { SidebarSwitchIcon } from './icons';
|
||||
import { StyledSidebarSwitch } from './style';
|
||||
type SidebarSwitchProps = {
|
||||
visible?: boolean;
|
||||
tooltipContent?: string;
|
||||
};
|
||||
|
||||
// fixme: the following code is not correct, SSR will fail because hydrate will not match the client side render
|
||||
// in `StyledSidebarSwitch` a component
|
||||
export const SidebarSwitch = ({
|
||||
visible = true,
|
||||
tooltipContent,
|
||||
...props
|
||||
}: SidebarSwitchProps) => {
|
||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
const checkIsMac = () => {
|
||||
const env = getEnvironment();
|
||||
return env.isBrowser && env.isMacOs;
|
||||
};
|
||||
const [isMac, setIsMac] = useState(false);
|
||||
const collapseKeyboardShortcuts = isMac ? ' ⌘+/' : ' Ctrl+/';
|
||||
|
||||
useEffect(() => {
|
||||
setIsMac(checkIsMac());
|
||||
}, []);
|
||||
|
||||
tooltipContent =
|
||||
tooltipContent || (open ? t['Collapse sidebar']() : t['Expand sidebar']());
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content={tooltipContent + ' ' + collapseKeyboardShortcuts}
|
||||
placement="right"
|
||||
zIndex={1000}
|
||||
visible={tooltipVisible}
|
||||
>
|
||||
<StyledSidebarSwitch
|
||||
{...props}
|
||||
visible={visible}
|
||||
disabled={!visible}
|
||||
onClick={useCallback(() => {
|
||||
setOpen(open => !open);
|
||||
setTooltipVisible(false);
|
||||
}, [setOpen])}
|
||||
onMouseEnter={useCallback(() => {
|
||||
setTooltipVisible(true);
|
||||
}, [])}
|
||||
onMouseLeave={useCallback(() => {
|
||||
setTooltipVisible(false);
|
||||
}, [])}
|
||||
>
|
||||
<SidebarSwitchIcon />
|
||||
</StyledSidebarSwitch>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { IconButton, styled } from '@affine/component';
|
||||
|
||||
export const StyledSidebarSwitch = styled(IconButton, {
|
||||
shouldForwardProp(propName: PropertyKey) {
|
||||
return propName !== 'visible';
|
||||
},
|
||||
})<{ visible: boolean }>(({ visible }) => {
|
||||
return {
|
||||
opacity: visible ? 1 : 0,
|
||||
WebkitAppRegion: visible ? 'no-drag' : 'drag',
|
||||
transition: 'all 0.2s ease-in-out',
|
||||
};
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Button, toast } from '@affine/component';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import {
|
||||
ArrowRightSmallIcon,
|
||||
DeleteIcon,
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -88,7 +86,6 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
|
||||
const isLastWorkspace = useAtomValue(rootWorkspacesMetadataAtom).length === 1;
|
||||
return (
|
||||
<>
|
||||
<div data-testid="avatar-row" className={style.row}>
|
||||
@@ -225,14 +222,6 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
<div className={style.settingItemLabelHint}>
|
||||
{t['Delete Workspace Label Hint']()}
|
||||
</div>
|
||||
{isOwner && isLastWorkspace && (
|
||||
<div
|
||||
className={style.settingsCannotDelete}
|
||||
data-testid="warn-cannot-delete-last-workspace"
|
||||
>
|
||||
{t['com.affine.workspace.cannot-delete']()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={style.col}></div>
|
||||
@@ -243,7 +232,6 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
type="warning"
|
||||
data-testid="delete-workspace-button"
|
||||
size="middle"
|
||||
disabled={isLastWorkspace}
|
||||
icon={<DeleteIcon />}
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Empty } from '@affine/component';
|
||||
import type {
|
||||
ListData,
|
||||
TrashListData,
|
||||
View,
|
||||
} from '@affine/component/page-list';
|
||||
import type { ListData, TrashListData } from '@affine/component/page-list';
|
||||
import {
|
||||
filterByView,
|
||||
filterByFilterList,
|
||||
PageList,
|
||||
PageListTrashView,
|
||||
} from '@affine/component/page-list';
|
||||
import type { View } from '@affine/component/page-list/filter/shared-types';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { getPagePreviewText } from '@toeverything/hooks/use-block-suite-page-preview';
|
||||
import { useAtom } from 'jotai';
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { pageModeSelectAtom } from '../../../atoms';
|
||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
@@ -80,32 +80,46 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
permanentlyDeletePage,
|
||||
cancelPublicPage,
|
||||
} = useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
const [filterMode] = useAtom(pageModeSelectAtom);
|
||||
const { createPage, createEdgeless, importFile, isPreferredEdgeless } =
|
||||
usePageHelper(blockSuiteWorkspace);
|
||||
const t = useAFFiNEI18N();
|
||||
const list = useMemo(
|
||||
() =>
|
||||
pageMetas.filter(pageMeta => {
|
||||
if (!filter[listType](pageMeta, pageMetas)) {
|
||||
return false;
|
||||
}
|
||||
if (!view) {
|
||||
pageMetas
|
||||
.filter(pageMeta => {
|
||||
if (filterMode === 'all') {
|
||||
return true;
|
||||
}
|
||||
if (filterMode === 'edgeless') {
|
||||
return isPreferredEdgeless(pageMeta.id);
|
||||
}
|
||||
if (filterMode === 'page') {
|
||||
return !isPreferredEdgeless(pageMeta.id);
|
||||
}
|
||||
console.error('unknown filter mode', pageMeta, filterMode);
|
||||
return true;
|
||||
}
|
||||
return filterByView(view, {
|
||||
Favorite: !!pageMeta.favorite,
|
||||
Created: pageMeta.createDate,
|
||||
Updated: pageMeta.updatedDate,
|
||||
});
|
||||
}),
|
||||
[pageMetas, listType, view]
|
||||
})
|
||||
.filter(pageMeta => {
|
||||
if (!filter[listType](pageMeta, pageMetas)) {
|
||||
return false;
|
||||
}
|
||||
if (!view) {
|
||||
return true;
|
||||
}
|
||||
return filterByFilterList(view.filterList, {
|
||||
'Is Favourited': !!pageMeta.favorite,
|
||||
Created: pageMeta.createDate,
|
||||
Updated: pageMeta.updatedDate ?? pageMeta.createDate,
|
||||
});
|
||||
}),
|
||||
[pageMetas, filterMode, isPreferredEdgeless, listType, view]
|
||||
);
|
||||
if (list.length === 0) {
|
||||
return <PageListEmpty listType={listType} />;
|
||||
}
|
||||
|
||||
if (listType === 'trash') {
|
||||
const pageList: TrashListData[] = list.map(pageMeta => {
|
||||
const page = blockSuiteWorkspace.getPage(pageMeta.id);
|
||||
const preview = page ? getPagePreviewText(page) : undefined;
|
||||
return {
|
||||
icon: isPreferredEdgeless(pageMeta.id) ? (
|
||||
<EdgelessIcon />
|
||||
@@ -114,6 +128,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
),
|
||||
pageId: pageMeta.id,
|
||||
title: pageMeta.title,
|
||||
preview,
|
||||
createDate: new Date(pageMeta.createDate),
|
||||
trashDate: pageMeta.trashDate
|
||||
? new Date(pageMeta.trashDate)
|
||||
@@ -132,14 +147,22 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
},
|
||||
};
|
||||
});
|
||||
return <PageListTrashView list={pageList} />;
|
||||
return (
|
||||
<PageListTrashView
|
||||
list={pageList}
|
||||
fallback={<PageListEmpty listType={listType} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const pageList: ListData[] = list.map(pageMeta => {
|
||||
const page = blockSuiteWorkspace.getPage(pageMeta.id);
|
||||
const preview = page ? getPagePreviewText(page) : undefined;
|
||||
return {
|
||||
icon: isPreferredEdgeless(pageMeta.id) ? <EdgelessIcon /> : <PageIcon />,
|
||||
pageId: pageMeta.id,
|
||||
title: pageMeta.title,
|
||||
preview,
|
||||
favorite: !!pageMeta.favorite,
|
||||
isPublicPage: !!pageMeta.isPublic,
|
||||
createDate: new Date(pageMeta.createDate),
|
||||
@@ -181,6 +204,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
onImportFile={importFile}
|
||||
isPublicWorkspace={isPublic}
|
||||
list={pageList}
|
||||
fallback={<PageListEmpty listType={listType} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { DownloadTips } from '@affine/component/affine-banner';
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { env } from '@affine/env';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
|
||||
|
||||
export const DownloadClientTip = () => {
|
||||
const env = getEnvironment();
|
||||
const [showDownloadClientTips, setShowDownloadClientTips] = useAtom(
|
||||
guideDownloadClientTipAtom
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
setLoginStorage,
|
||||
SignMethod,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { affineAuth } from '@affine/workspace/affine/shared';
|
||||
import type { LocalWorkspace } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import {
|
||||
@@ -20,7 +21,6 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
|
||||
import type { AffineOfficialWorkspace } from '../../../../shared';
|
||||
import { affineAuth } from '../../../../shared/apis';
|
||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||
|
||||
const IconWrapper = styled('div')(() => {
|
||||
|
||||
@@ -3,22 +3,18 @@ import {
|
||||
appSidebarFloatingAtom,
|
||||
appSidebarOpenAtom,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
|
||||
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
lazy,
|
||||
Suspense,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { forwardRef, memo, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
|
||||
import { contentLayoutAtom } from '../../../atoms/layout';
|
||||
import { useCurrentMode } from '../../../hooks/current/use-current-mode';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { DownloadClientTip } from './download-tips';
|
||||
@@ -31,12 +27,6 @@ import UserAvatar from './header-right-items/user-avatar';
|
||||
import * as styles from './styles.css';
|
||||
import { OSWarningMessage, shouldShowWarning } from './utils';
|
||||
|
||||
const SidebarSwitch = lazy(() =>
|
||||
import('../../affine/sidebar-switch').then(module => ({
|
||||
default: module.SidebarSwitch,
|
||||
}))
|
||||
);
|
||||
|
||||
export type BaseHeaderProps<
|
||||
Workspace extends AffineOfficialWorkspace = AffineOfficialWorkspace
|
||||
> = {
|
||||
@@ -46,7 +36,7 @@ export type BaseHeaderProps<
|
||||
leftSlot?: ReactNode;
|
||||
};
|
||||
|
||||
export const enum HeaderRightItemName {
|
||||
export enum HeaderRightItemName {
|
||||
EditorOptionMenu = 'editorOptionMenu',
|
||||
TrashButtonGroup = 'trashButtonGroup',
|
||||
SyncUser = 'syncUser',
|
||||
@@ -149,6 +139,43 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
|
||||
|
||||
export type HeaderProps = BaseHeaderProps;
|
||||
|
||||
const PluginHeaderItemAdapter = memo<{
|
||||
headerItem: PluginUIAdapter['headerItem'];
|
||||
}>(function PluginHeaderItemAdapter({ headerItem }) {
|
||||
return (
|
||||
<div>
|
||||
{headerItem({
|
||||
contentLayoutAtom,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const PluginHeader = () => {
|
||||
const affinePluginsMap = useAtomValue(affinePluginsAtom);
|
||||
const plugins = useMemo(
|
||||
() => Object.values(affinePluginsMap),
|
||||
[affinePluginsMap]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{plugins
|
||||
.filter(plugin => plugin.uiAdapter.headerItem != null)
|
||||
.map(plugin => {
|
||||
const headerItem = plugin.uiAdapter
|
||||
.headerItem as PluginUIAdapter['headerItem'];
|
||||
return (
|
||||
<PluginHeaderItemAdapter
|
||||
key={plugin.definition.id}
|
||||
headerItem={headerItem}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = forwardRef<
|
||||
HTMLDivElement,
|
||||
PropsWithChildren<HeaderProps> & HTMLAttributes<HTMLDivElement>
|
||||
@@ -164,11 +191,10 @@ export const Header = forwardRef<
|
||||
setShowGuideDownloadClientTip(shouldShowGuideDownloadClientTip);
|
||||
}, [shouldShowGuideDownloadClientTip]);
|
||||
const open = useAtomValue(appSidebarOpenAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||
|
||||
const mode = useCurrentMode();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.headerContainer}
|
||||
@@ -176,7 +202,6 @@ export const Header = forwardRef<
|
||||
data-has-warning={showWarning}
|
||||
data-open={open}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
{...props}
|
||||
>
|
||||
{showGuideDownloadClientTip ? (
|
||||
<DownloadClientTip />
|
||||
@@ -196,19 +221,14 @@ export const Header = forwardRef<
|
||||
data-testid="editor-header-items"
|
||||
data-is-edgeless={mode === 'edgeless'}
|
||||
>
|
||||
<Suspense>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<SidebarSwitch
|
||||
visible={!open}
|
||||
tooltipContent={t['Expand sidebar']()}
|
||||
data-testid="sliderBar-arrowButton-expand"
|
||||
/>
|
||||
{props.leftSlot}
|
||||
</div>
|
||||
</Suspense>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{!open && <SidebarSwitch />}
|
||||
{props.leftSlot}
|
||||
</div>
|
||||
|
||||
{props.children}
|
||||
<div className={styles.headerRightSide}>
|
||||
<PluginHeader />
|
||||
{useMemo(() => {
|
||||
return Object.entries(HeaderRightItems).map(
|
||||
([name, { availableWhen, Component }]) => {
|
||||
|
||||
@@ -21,6 +21,9 @@ export const headerContainer = style({
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
':has([data-popper-placement])': {
|
||||
WebkitAppRegion: 'no-drag',
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const header = style({
|
||||
@@ -146,7 +149,24 @@ export const pageListTitleWrapper = style({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const allPageListTitleWrapper = style({
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
'::after': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: 'var(--affine-border-color)',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
margin: '0 1px',
|
||||
},
|
||||
});
|
||||
export const pageListTitleIcon = style({
|
||||
fontSize: '20px',
|
||||
height: '1em',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { env } from '@affine/env';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type React from 'react';
|
||||
@@ -7,7 +7,6 @@ import { useEffect, useState } from 'react';
|
||||
const minimumChromeVersion = 102;
|
||||
|
||||
export const shouldShowWarning = () => {
|
||||
const env = getEnvironment();
|
||||
if (env.isDesktop) {
|
||||
// even though desktop have compatibility issues, we don't want to show the warning
|
||||
return false;
|
||||
@@ -28,7 +27,6 @@ export const OSWarningMessage: React.FC = () => {
|
||||
const [notChrome, setNotChrome] = useState(false);
|
||||
const [notGoodVersion, setNotGoodVersion] = useState(false);
|
||||
useEffect(() => {
|
||||
const env = getEnvironment();
|
||||
setNotChrome(env.isBrowser && !env.isChrome);
|
||||
setNotGoodVersion(
|
||||
env.isBrowser && env.isChrome && env.chromeVersion < minimumChromeVersion
|
||||
|
||||
18
apps/web/src/components/filter-container.css.ts
Normal file
18
apps/web/src/components/filter-container.css.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const filterContainerStyle = style({
|
||||
padding: '12px',
|
||||
display: 'flex',
|
||||
position: 'relative',
|
||||
'::after': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
height: '1px',
|
||||
background: 'var(--affine-border-color)',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
margin: '0 1px',
|
||||
},
|
||||
});
|
||||
@@ -1,18 +1,32 @@
|
||||
import './page-detail-editor.css';
|
||||
|
||||
import { PageNotFoundError, Unreachable } from '@affine/env/constant';
|
||||
import { rootCurrentEditorAtom } from '@affine/workspace/atom';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-block-suite-workspace-page-title';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
|
||||
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
|
||||
import type { ExpectedLayout } from '@toeverything/plugin-infra/type';
|
||||
import type { PluginBlockSuiteAdapter } from '@toeverything/plugin-infra/type';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import Head from 'next/head';
|
||||
import type React from 'react';
|
||||
import { lazy, memo, startTransition, useCallback } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import React, {
|
||||
lazy,
|
||||
memo,
|
||||
startTransition,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import type { MosaicNode } from 'react-mosaic-component';
|
||||
|
||||
import { currentEditorAtom, workspacePreferredModeAtom } from '../atoms';
|
||||
import { workspacePreferredModeAtom } from '../atoms';
|
||||
import { contentLayoutAtom } from '../atoms/layout';
|
||||
import type { AffineOfficialWorkspace } from '../shared';
|
||||
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
|
||||
|
||||
@@ -37,6 +51,11 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
onLoad,
|
||||
isPublic,
|
||||
}: PageDetailEditorProps) {
|
||||
const affinePluginsMap = useAtomValue(affinePluginsAtom);
|
||||
const plugins = useMemo(
|
||||
() => Object.values(affinePluginsMap),
|
||||
[affinePluginsMap]
|
||||
);
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
|
||||
if (!page) {
|
||||
@@ -47,7 +66,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
);
|
||||
const currentMode =
|
||||
useAtomValue(workspacePreferredModeAtom)[pageId] ?? 'page';
|
||||
const setEditor = useSetAtom(currentEditorAtom);
|
||||
const setEditor = useSetAtom(rootCurrentEditorAtom);
|
||||
assertExists(meta);
|
||||
return (
|
||||
<Editor
|
||||
@@ -75,18 +94,40 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
updatedDate: Date.now(),
|
||||
});
|
||||
localStorage.setItem('last_page_id', page.id);
|
||||
let dispose = () => {};
|
||||
if (onLoad) {
|
||||
return onLoad(page, editor);
|
||||
dispose = onLoad(page, editor);
|
||||
}
|
||||
return () => {};
|
||||
const uiDecorators = plugins
|
||||
.map(plugin => plugin.blockSuiteAdapter.uiDecorator)
|
||||
.filter((ui): ui is PluginBlockSuiteAdapter['uiDecorator'] =>
|
||||
Boolean(ui)
|
||||
);
|
||||
const disposes = uiDecorators.map(ui => ui(editor));
|
||||
return () => {
|
||||
disposes.map(fn => fn());
|
||||
dispose();
|
||||
};
|
||||
},
|
||||
[onLoad, setEditor]
|
||||
[plugins, onLoad, setEditor]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const PageDetailEditor: React.FC<PageDetailEditorProps> = props => {
|
||||
const PluginContentAdapter = memo<{
|
||||
detailContent: PluginUIAdapter['detailContent'];
|
||||
}>(function PluginContentAdapter({ detailContent }) {
|
||||
return (
|
||||
<div>
|
||||
{detailContent({
|
||||
contentLayoutAtom,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const PageDetailEditor: FC<PageDetailEditorProps> = props => {
|
||||
const { workspace, pageId } = props;
|
||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
|
||||
@@ -94,23 +135,66 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = props => {
|
||||
throw new PageNotFoundError(blockSuiteWorkspace, pageId);
|
||||
}
|
||||
const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, pageId);
|
||||
const affinePluginsMap = useAtomValue(affinePluginsAtom);
|
||||
const plugins = useMemo(
|
||||
() => Object.values(affinePluginsMap),
|
||||
[affinePluginsMap]
|
||||
);
|
||||
|
||||
const [layout, setLayout] = useAtom(contentLayoutAtom);
|
||||
|
||||
const onChange = useCallback(
|
||||
(_: MosaicNode<string | number> | null) => {
|
||||
// type cast
|
||||
const node = _ as MosaicNode<string> | null;
|
||||
if (node) {
|
||||
if (typeof node === 'string') {
|
||||
console.error('unexpected layout');
|
||||
} else {
|
||||
if (node.splitPercentage && node.splitPercentage < 70) {
|
||||
return;
|
||||
} else if (node.first !== 'editor') {
|
||||
return;
|
||||
}
|
||||
setLayout(node as ExpectedLayout);
|
||||
}
|
||||
}
|
||||
},
|
||||
[setLayout]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title}</title>
|
||||
</Head>
|
||||
<Mosaic
|
||||
onChange={useCallback(() => {}, [])}
|
||||
renderTile={id => {
|
||||
if (id === 'editor') {
|
||||
return <EditorWrapper {...props} />;
|
||||
} else {
|
||||
// @affine/copilot and other plugins will be added in the future
|
||||
{layout === 'editor' ? (
|
||||
<EditorWrapper {...props} />
|
||||
) : (
|
||||
<Mosaic
|
||||
onChange={onChange}
|
||||
renderTile={id => {
|
||||
if (id === 'editor') {
|
||||
return <EditorWrapper {...props} />;
|
||||
} else {
|
||||
const plugin = plugins.find(
|
||||
plugin => plugin.definition.id === id
|
||||
);
|
||||
if (plugin && plugin.uiAdapter.detailContent) {
|
||||
return (
|
||||
<Suspense>
|
||||
<PluginContentAdapter
|
||||
detailContent={plugin.uiAdapter.detailContent}
|
||||
/>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new Unreachable();
|
||||
}
|
||||
}}
|
||||
value="editor"
|
||||
/>
|
||||
}}
|
||||
value={layout}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MuiFade, Tooltip } from '@affine/component';
|
||||
import { config, getEnvironment } from '@affine/env';
|
||||
import { config, env } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, NewIcon, UserGuideIcon } from '@blocksuite/icons';
|
||||
import { useAtom } from 'jotai';
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
StyledIsland,
|
||||
StyledTriggerWrapper,
|
||||
} from './style';
|
||||
const env = getEnvironment();
|
||||
const ContactModal = lazy(() =>
|
||||
import('@affine/component/contact-modal').then(({ ContactModal }) => ({
|
||||
default: ContactModal,
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { MessageCode, Messages } from '@affine/env/constant';
|
||||
import { setLoginStorage, SignMethod } from '@affine/workspace/affine/login';
|
||||
import type React from 'react';
|
||||
import { affineAuth } from '@affine/workspace/affine/shared';
|
||||
import type { FC } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out';
|
||||
import { affineAuth } from '../../../shared/apis';
|
||||
import { toast } from '../../../utils';
|
||||
|
||||
declare global {
|
||||
interface DocumentEventMap {
|
||||
'affine-error': CustomEvent<{
|
||||
code: MessageCode;
|
||||
code: keyof typeof Messages;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
export const MessageCenter: React.FC = memo(function MessageCenter() {
|
||||
export const MessageCenter: FC = memo(function MessageCenter() {
|
||||
const [popup, setPopup] = useState(false);
|
||||
const onLogout = useAffineLogOut();
|
||||
useEffect(() => {
|
||||
const listener = (
|
||||
event: CustomEvent<{
|
||||
code: MessageCode;
|
||||
code: keyof typeof Messages;
|
||||
}>
|
||||
) => {
|
||||
// fixme: need refactor
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Modal, ModalWrapper } from '@affine/component';
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { env } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Command } from 'cmdk';
|
||||
import type { NextRouter } from 'next/router';
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
} from './style';
|
||||
|
||||
const isMac = () => {
|
||||
const env = getEnvironment();
|
||||
return env.isBrowser && env.isMacOs;
|
||||
};
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ export const Results: FC<ResultsProps> = ({
|
||||
>
|
||||
<StyledListItem>
|
||||
{result.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
|
||||
<span>{result.title}</span>
|
||||
<span>{result.title || UNTITLED_WORKSPACE_NAME}</span>
|
||||
</StyledListItem>
|
||||
</Command.Item>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
MuiClickAwayListener,
|
||||
MuiSlide,
|
||||
} from '@affine/component';
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { env } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -27,7 +27,6 @@ type ModalProps = {
|
||||
};
|
||||
|
||||
const checkIsMac = () => {
|
||||
const env = getEnvironment();
|
||||
return env.isBrowser && env.isMacOs;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { RadioButton, RadioButtonGroup } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { ReactNode } from 'react';
|
||||
import type React from 'react';
|
||||
|
||||
import { openQuickSearchModalAtom } from '../../../atoms';
|
||||
import { openQuickSearchModalAtom, pageModeSelectAtom } from '../../../atoms';
|
||||
import type { HeaderProps } from '../../blocksuite/workspace-header/header';
|
||||
import { Header } from '../../blocksuite/workspace-header/header';
|
||||
import * as styles from '../../blocksuite/workspace-header/styles.css';
|
||||
@@ -34,3 +37,30 @@ export const WorkspaceTitle: React.FC<WorkspaceTitleProps> = ({
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
export const WorkspaceModeFilterTab = ({ ...props }: WorkspaceTitleProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [value, setMode] = useAtom(pageModeSelectAtom);
|
||||
const handleValueChange = (value: string) => {
|
||||
if (value !== 'all' && value !== 'page' && value !== 'edgeless') {
|
||||
throw new Error('Invalid value for page mode option');
|
||||
}
|
||||
setMode(value);
|
||||
};
|
||||
return (
|
||||
<Header {...props}>
|
||||
<div className={styles.allPageListTitleWrapper}>
|
||||
<RadioButtonGroup
|
||||
defaultValue={value}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<RadioButton value="all" style={{ textTransform: 'capitalize' }}>
|
||||
{t['all']()}
|
||||
</RadioButton>
|
||||
<RadioButton value="page">{t['Page']()}</RadioButton>
|
||||
<RadioButton value="edgeless">{t['Edgeless']()}</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
</div>
|
||||
</Header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,19 +9,14 @@ import { config } from '@affine/env';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { WorkspaceHeaderProps } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/workspace/type';
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
FolderIcon,
|
||||
SettingsIcon,
|
||||
ShareIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { SettingsIcon } from '@blocksuite/icons';
|
||||
import { RESET } from 'jotai/utils';
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import { NIL } from 'uuid';
|
||||
|
||||
import { BlockSuiteEditorHeader } from './blocksuite/workspace-header';
|
||||
import { WorkspaceTitle } from './pure/workspace-title';
|
||||
import { filterContainerStyle } from './filter-container.css';
|
||||
import { WorkspaceModeFilterTab, WorkspaceTitle } from './pure/workspace-title';
|
||||
|
||||
export function WorkspaceHeader({
|
||||
currentWorkspace,
|
||||
@@ -31,23 +26,21 @@ export function WorkspaceHeader({
|
||||
const t = useAFFiNEI18N();
|
||||
if ('subPath' in currentEntry) {
|
||||
if (currentEntry.subPath === WorkspaceSubPath.ALL) {
|
||||
const leftSlot = config.enableAllPageFilter && (
|
||||
<ViewList setting={setting}></ViewList>
|
||||
);
|
||||
const filterContainer = config.enableAllPageFilter &&
|
||||
setting.currentView.filterList.length > 0 && (
|
||||
<div style={{ padding: 12, display: 'flex' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<FilterList
|
||||
value={setting.currentView.filterList}
|
||||
onChange={filterList => {
|
||||
setting.setCurrentView(view => ({
|
||||
...view,
|
||||
filterList,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
const leftSlot = <ViewList setting={setting}></ViewList>;
|
||||
const filterContainer = setting.currentView.filterList.length > 0 && (
|
||||
<div className={filterContainerStyle}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<FilterList
|
||||
value={setting.currentView.filterList}
|
||||
onChange={filterList => {
|
||||
setting.setCurrentView(view => ({
|
||||
...view,
|
||||
filterList,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{config.enableAllPageFilter && (
|
||||
<div>
|
||||
{setting.currentView.id !== NIL ||
|
||||
(setting.currentView.id === NIL &&
|
||||
@@ -62,20 +55,17 @@ export function WorkspaceHeader({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<WorkspaceTitle
|
||||
<WorkspaceModeFilterTab
|
||||
workspace={currentWorkspace}
|
||||
currentPage={null}
|
||||
isPublic={false}
|
||||
icon={<FolderIcon />}
|
||||
leftSlot={leftSlot}
|
||||
>
|
||||
{t['All pages']()}
|
||||
</WorkspaceTitle>
|
||||
/>
|
||||
{filterContainer}
|
||||
</>
|
||||
);
|
||||
@@ -92,25 +82,19 @@ export function WorkspaceHeader({
|
||||
);
|
||||
} else if (currentEntry.subPath === WorkspaceSubPath.SHARED) {
|
||||
return (
|
||||
<WorkspaceTitle
|
||||
<WorkspaceModeFilterTab
|
||||
workspace={currentWorkspace}
|
||||
currentPage={null}
|
||||
isPublic={false}
|
||||
icon={<ShareIcon />}
|
||||
>
|
||||
{t['Shared Pages']()}
|
||||
</WorkspaceTitle>
|
||||
/>
|
||||
);
|
||||
} else if (currentEntry.subPath === WorkspaceSubPath.TRASH) {
|
||||
return (
|
||||
<WorkspaceTitle
|
||||
<WorkspaceModeFilterTab
|
||||
workspace={currentWorkspace}
|
||||
currentPage={null}
|
||||
isPublic={false}
|
||||
icon={<DeleteTemporarilyIcon />}
|
||||
>
|
||||
{t['Trash']()}
|
||||
</WorkspaceTitle>
|
||||
/>
|
||||
);
|
||||
}
|
||||
} else if ('pageId' in currentEntry) {
|
||||
|
||||
@@ -23,11 +23,9 @@ import type React from 'react';
|
||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { workspacesAtom } from '../../atoms';
|
||||
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
useCurrentWorkspace,
|
||||
} from '../current/use-current-workspace';
|
||||
import { useCurrentWorkspace } from '../current/use-current-workspace';
|
||||
import { useAppHelper, useWorkspaces } from '../use-workspaces';
|
||||
|
||||
vi.mock(
|
||||
@@ -169,7 +167,7 @@ describe('useWorkspacesHelper', () => {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
store.set(rootCurrentWorkspaceIdAtom, workspacesHook.result.current[1].id);
|
||||
await store.get(currentWorkspaceAtom);
|
||||
await store.get(rootCurrentWorkspaceAtom);
|
||||
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
@@ -191,22 +189,20 @@ describe('useWorkspaces', () => {
|
||||
const { result } = renderHook(() => useAppHelper(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
// next tick
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
{
|
||||
const workspaces = await store.get(workspacesAtom);
|
||||
expect(workspaces.length).toEqual(1);
|
||||
expect(workspaces.length).toEqual(0);
|
||||
}
|
||||
await result.current.createLocalWorkspace('test');
|
||||
{
|
||||
const workspaces = await store.get(workspacesAtom);
|
||||
expect(workspaces.length).toEqual(2);
|
||||
expect(workspaces.length).toEqual(1);
|
||||
}
|
||||
const { result: result2 } = renderHook(() => useWorkspaces(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
expect(result2.current.length).toEqual(2);
|
||||
const firstWorkspace = result2.current[1];
|
||||
expect(result2.current.length).toEqual(1);
|
||||
const firstWorkspace = result2.current[0];
|
||||
expect(firstWorkspace.flavour).toBe('local');
|
||||
assert(firstWorkspace.flavour === WorkspaceFlavour.LOCAL);
|
||||
expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test');
|
||||
|
||||
@@ -20,11 +20,9 @@ import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { LocalAdapter } from '../../adapters/local';
|
||||
import { workspacesAtom } from '../../atoms';
|
||||
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import {
|
||||
currentWorkspaceAtom,
|
||||
useCurrentWorkspace,
|
||||
} from '../current/use-current-workspace';
|
||||
import { useCurrentWorkspace } from '../current/use-current-workspace';
|
||||
import {
|
||||
useRecentlyViewed,
|
||||
useSyncRecentViewsWithRouter,
|
||||
@@ -98,14 +96,14 @@ describe('useRecentlyViewed', () => {
|
||||
providers: [],
|
||||
} satisfies LocalWorkspace);
|
||||
store.set(rootCurrentWorkspaceIdAtom, blockSuiteWorkspace.id);
|
||||
const workspace = await store.get(currentWorkspaceAtom);
|
||||
const workspace = await store.get(rootCurrentWorkspaceAtom);
|
||||
expect(workspace?.id).toBe(blockSuiteWorkspace.id);
|
||||
const currentHook = renderHook(() => useCurrentWorkspace(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
expect(currentHook.result.current[0]?.id).toEqual(workspaceId);
|
||||
store.set(rootCurrentWorkspaceIdAtom, blockSuiteWorkspace.id);
|
||||
await store.get(currentWorkspaceAtom);
|
||||
await store.get(rootCurrentWorkspaceAtom);
|
||||
const recentlyViewedHook = renderHook(() => useRecentlyViewed(), {
|
||||
wrapper: ProviderWrapper,
|
||||
});
|
||||
|
||||
@@ -6,10 +6,9 @@ import {
|
||||
setLoginStorage,
|
||||
storageChangeSlot,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { affineAuth } from '@affine/workspace/affine/shared';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { affineAuth } from '../../shared/apis';
|
||||
|
||||
const logger = new DebugLogger('auth-token');
|
||||
|
||||
const revalidate = async () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Member } from '@affine/workspace/affine/api';
|
||||
import { affineApis } from '@affine/workspace/affine/shared';
|
||||
import { useCallback } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
export function useMembers(workspaceId: string) {
|
||||
const { data, mutate } = useSWR<Member[]>(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { affineApis } from '@affine/workspace/affine/shared';
|
||||
import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import type { AffineLegacyCloudWorkspace } from '@affine/workspace/type';
|
||||
import { useCallback } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
export function useToggleWorkspacePublish(
|
||||
workspace: AffineLegacyCloudWorkspace
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
|
||||
import { currentPageIdAtom, workspacePreferredModeAtom } from '../../atoms';
|
||||
import { workspacePreferredModeAtom } from '../../atoms';
|
||||
|
||||
const currentModeAtom = atom<'page' | 'edgeless'>(get => {
|
||||
const pageId = get(currentPageIdAtom);
|
||||
const pageId = get(rootCurrentPageIdAtom);
|
||||
const record = get(workspacePreferredModeAtom);
|
||||
if (pageId) return record[pageId] ?? 'page';
|
||||
else {
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import {
|
||||
rootCurrentPageIdAtom,
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms';
|
||||
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
|
||||
import type { AllWorkspace } from '../../shared';
|
||||
|
||||
/**
|
||||
* @deprecated use `rootCurrentWorkspaceAtom` instead
|
||||
*/
|
||||
export const currentWorkspaceAtom = rootCurrentWorkspaceAtom;
|
||||
|
||||
export function useCurrentWorkspace(): [
|
||||
AllWorkspace,
|
||||
(id: string | null) => void
|
||||
] {
|
||||
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
|
||||
const [, setId] = useAtom(currentWorkspaceIdAtom);
|
||||
const [, setPageId] = useAtom(currentPageIdAtom);
|
||||
const [, setId] = useAtom(rootCurrentWorkspaceIdAtom);
|
||||
const [, setPageId] = useAtom(rootCurrentPageIdAtom);
|
||||
return [
|
||||
currentWorkspace,
|
||||
useCallback(
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
SignMethod,
|
||||
storageChangeSlot,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { affineAuth } from '@affine/workspace/affine/shared';
|
||||
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
|
||||
import type { WorkspaceRegistry } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { affineAuth } from '../../shared/apis';
|
||||
import { useTransformWorkspace } from '../use-transform-workspace';
|
||||
|
||||
export function useOnTransformWorkspace() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { WorkspaceSubPath } from '@affine/workspace/type';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const enum RouteLogic {
|
||||
export enum RouteLogic {
|
||||
REPLACE = 'replace',
|
||||
PUSH = 'push',
|
||||
}
|
||||
|
||||
@@ -58,7 +58,10 @@ import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
import { useRouterHelper } from '../hooks/use-router-helper';
|
||||
import { useRouterTitle } from '../hooks/use-router-title';
|
||||
import { useWorkspaces } from '../hooks/use-workspaces';
|
||||
import { ModalProvider } from '../providers/modal-provider';
|
||||
import {
|
||||
AllWorkspaceModals,
|
||||
CurrentWorkspaceModals,
|
||||
} from '../providers/modal-provider';
|
||||
import { pathGenerator, publicPathGenerator } from '../shared';
|
||||
import { toast } from '../utils';
|
||||
|
||||
@@ -178,7 +181,10 @@ export const CurrentWorkspaceContext = ({
|
||||
return () => {
|
||||
clearTimeout(id);
|
||||
};
|
||||
}, [push, exist]);
|
||||
}, [push, exist, metadata.length]);
|
||||
if (metadata.length === 0) {
|
||||
return <WorkspaceFallback key="no-workspace" />;
|
||||
}
|
||||
if (!router.isReady) {
|
||||
return <WorkspaceFallback key="router-is-loading" />;
|
||||
}
|
||||
@@ -266,9 +272,10 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
|
||||
{/* load all workspaces is costly, do not block the whole UI */}
|
||||
<Suspense fallback={null}>
|
||||
<AllWorkspaceContext>
|
||||
<AllWorkspaceModals />
|
||||
<CurrentWorkspaceContext>
|
||||
{/* fixme(himself65): don't re-render whole modals */}
|
||||
<ModalProvider key={currentWorkspaceId} />
|
||||
<CurrentWorkspaceModals key={currentWorkspaceId} />
|
||||
</CurrentWorkspaceContext>
|
||||
</AllWorkspaceContext>
|
||||
</Suspense>
|
||||
@@ -375,7 +382,7 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
// Otherwise clicks would be intercepted
|
||||
useSensor(MouseSensor, {
|
||||
activationConstraint: {
|
||||
delay: 10,
|
||||
delay: 500,
|
||||
tolerance: 10,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
import 'react-mosaic-component/react-mosaic-component.css';
|
||||
// bootstrap code before everything
|
||||
import '../bootstrap';
|
||||
|
||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
||||
import { config, setupGlobal } from '@affine/env';
|
||||
import { config } from '@affine/env';
|
||||
import { createI18n, I18nextProvider } from '@affine/i18n';
|
||||
import { rootStore } from '@affine/workspace/atom';
|
||||
import type { EmotionCache } from '@emotion/cache';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { Provider } from 'jotai';
|
||||
import { AffinePluginContext } from '@toeverything/plugin-infra/react';
|
||||
import type { AppProps } from 'next/app';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -16,14 +17,10 @@ import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import React, { lazy, Suspense, useEffect, useMemo } from 'react';
|
||||
|
||||
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
|
||||
import { ProviderComposer } from '../components/provider-composer';
|
||||
import { MessageCenter } from '../components/pure/message-center';
|
||||
import { ThemeProvider } from '../providers/theme-provider';
|
||||
import type { NextPageWithLayout } from '../shared';
|
||||
import createEmotionCache from '../utils/create-emotion-cache';
|
||||
|
||||
setupGlobal();
|
||||
|
||||
type AppPropsWithLayout = AppProps & {
|
||||
Component: NextPageWithLayout;
|
||||
};
|
||||
@@ -68,17 +65,7 @@ const App = function App({
|
||||
<MessageCenter />
|
||||
<AffineErrorBoundary router={useRouter()}>
|
||||
<Suspense fallback={<WorkspaceFallback key="RootPageLoading" />}>
|
||||
<ProviderComposer
|
||||
contexts={useMemo(
|
||||
() =>
|
||||
[
|
||||
<Provider key="JotaiProvider" store={rootStore} />,
|
||||
<DebugProvider key="DebugProvider" />,
|
||||
<ThemeProvider key="ThemeProvider" />,
|
||||
].filter(Boolean),
|
||||
[]
|
||||
)}
|
||||
>
|
||||
<AffinePluginContext>
|
||||
<Head>
|
||||
<title>AFFiNE</title>
|
||||
<meta
|
||||
@@ -86,8 +73,10 @@ const App = function App({
|
||||
content="initial-scale=1, width=device-width"
|
||||
/>
|
||||
</Head>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</ProviderComposer>
|
||||
<DebugProvider>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</DebugProvider>
|
||||
</AffinePluginContext>
|
||||
</Suspense>
|
||||
</AffineErrorBoundary>
|
||||
</I18nextProvider>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceSubPath } from '@affine/workspace/type';
|
||||
import type { NextPage } from 'next';
|
||||
@@ -7,6 +8,8 @@ import { Suspense, useEffect } from 'react';
|
||||
import { PageLoading } from '../components/pure/loading';
|
||||
import { RouteLogic, useRouterHelper } from '../hooks/use-router-helper';
|
||||
import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces';
|
||||
import { AllWorkspaceContext } from '../layouts/workspace-layout';
|
||||
import { AllWorkspaceModals } from '../providers/modal-provider';
|
||||
|
||||
const logger = new DebugLogger('index-page');
|
||||
|
||||
@@ -62,7 +65,13 @@ const IndexPageInner = () => {
|
||||
}
|
||||
}, [helper, jumpToPage, jumpToSubPath, router, workspaces]);
|
||||
|
||||
return <PageLoading key="IndexPageInfinitePageLoading" />;
|
||||
return (
|
||||
<Suspense fallback={<WorkspaceFallback />}>
|
||||
<AllWorkspaceContext>
|
||||
<AllWorkspaceModals />
|
||||
</AllWorkspaceContext>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const IndexPage: NextPage = () => {
|
||||
|
||||
42
apps/web/src/pages/plugins.tsx
Normal file
42
apps/web/src/pages/plugins.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { AppContainer, MainContainer } from '@affine/component/workspace';
|
||||
import { config } from '@affine/env';
|
||||
import { NoSsr } from '@mui/material';
|
||||
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
const Plugins = () => {
|
||||
const plugins = useAtomValue(affinePluginsAtom);
|
||||
return (
|
||||
<NoSsr>
|
||||
<div>
|
||||
{Object.values(plugins).map(({ definition, uiAdapter }) => {
|
||||
const Content = uiAdapter.debugContent;
|
||||
return (
|
||||
<div key={definition.id}>
|
||||
{/* todo: support i18n */}
|
||||
{definition.name.fallback}
|
||||
{Content && <Content />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</NoSsr>
|
||||
);
|
||||
};
|
||||
|
||||
export default function PluginPage(): ReactElement {
|
||||
if (!config.enablePlugin) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<AppContainer>
|
||||
<MainContainer>
|
||||
<Suspense>
|
||||
<Plugins />
|
||||
</Suspense>
|
||||
</MainContainer>
|
||||
</AppContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Breadcrumbs, IconButton, ListSkeleton } from '@affine/component';
|
||||
import { StyledTableContainer } from '@affine/component/page-list';
|
||||
import { QueryParamError } from '@affine/env/constant';
|
||||
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
|
||||
import { SearchIcon } from '@blocksuite/icons';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
@@ -9,7 +10,7 @@ import { useRouter } from 'next/router';
|
||||
import type React from 'react';
|
||||
import { lazy, Suspense, useCallback, useEffect } from 'react';
|
||||
|
||||
import { currentWorkspaceIdAtom, openQuickSearchModalAtom } from '../../atoms';
|
||||
import { openQuickSearchModalAtom } from '../../atoms';
|
||||
import {
|
||||
publicWorkspaceAtom,
|
||||
publicWorkspaceIdAtom,
|
||||
@@ -96,7 +97,7 @@ const ListPage: NextPageWithLayout = () => {
|
||||
const workspaceId = router.query.workspaceId;
|
||||
const setWorkspaceId = useSetAtom(publicWorkspaceIdAtom);
|
||||
// todo: remove this atom usage here
|
||||
const setCurrentWorkspaceId = useSetAtom(currentWorkspaceIdAtom);
|
||||
const setCurrentWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
|
||||
useEffect(() => {
|
||||
if (!router.isReady) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import type { SettingPanel } from '@affine/workspace/type';
|
||||
import {
|
||||
settingPanel,
|
||||
@@ -7,7 +6,7 @@ import {
|
||||
WorkspaceSubPath,
|
||||
} from '@affine/workspace/type';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { useAtom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import Head from 'next/head';
|
||||
import type { NextRouter } from 'next/router';
|
||||
@@ -21,7 +20,6 @@ import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-wo
|
||||
import { useAppHelper } from '../../../hooks/use-workspaces';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
|
||||
const settingPanelAtom = atomWithStorage<SettingPanel>(
|
||||
'workspaceId',
|
||||
@@ -77,7 +75,6 @@ function useTabRouterSync(
|
||||
|
||||
const SettingPage: NextPageWithLayout = () => {
|
||||
const router = useRouter();
|
||||
const workspaceIds = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const t = useAFFiNEI18N();
|
||||
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
|
||||
@@ -102,13 +99,8 @@ const SettingPage: NextPageWithLayout = () => {
|
||||
const onDeleteWorkspace = useCallback(async () => {
|
||||
assertExists(currentWorkspace);
|
||||
const workspaceId = currentWorkspace.id;
|
||||
if (workspaceIds.length === 1 && workspaceId === workspaceIds[0].id) {
|
||||
toast(t['You cannot delete the last workspace']());
|
||||
throw new Error('You cannot delete the last workspace');
|
||||
} else {
|
||||
return await helper.deleteWorkspace(workspaceId);
|
||||
}
|
||||
}, [currentWorkspace, helper, t, workspaceIds]);
|
||||
return helper.deleteWorkspace(workspaceId);
|
||||
}, [currentWorkspace, helper]);
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
if (!router.isReady) {
|
||||
return <PageLoading />;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { env } from '@affine/env';
|
||||
import {
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
rootWorkspacesMetadataAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { WorkspaceSubPath } from '@affine/workspace/type';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback, useTransition } from 'react';
|
||||
|
||||
import {
|
||||
currentWorkspaceIdAtom,
|
||||
openCreateWorkspaceModalAtom,
|
||||
openDisableCloudAlertModalAtom,
|
||||
openOnboardingModalAtom,
|
||||
@@ -17,7 +19,6 @@ import {
|
||||
import { useAffineLogIn } from '../hooks/affine/use-affine-log-in';
|
||||
import { useAffineLogOut } from '../hooks/affine/use-affine-log-out';
|
||||
import { useCurrentUser } from '../hooks/current/use-current-user';
|
||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
import { useRouterHelper } from '../hooks/use-router-helper';
|
||||
import { useWorkspaces } from '../hooks/use-workspaces';
|
||||
|
||||
@@ -47,14 +48,7 @@ const OnboardingModal = lazy(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
export function Modals() {
|
||||
const [openWorkspacesModal, setOpenWorkspacesModal] = useAtom(
|
||||
openWorkspacesModalAtom
|
||||
);
|
||||
const [openCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom(
|
||||
openCreateWorkspaceModalAtom
|
||||
);
|
||||
|
||||
export function CurrentWorkspaceModals() {
|
||||
const [openDisableCloudAlertModal, setOpenDisableCloudAlertModal] = useAtom(
|
||||
openDisableCloudAlertModalAtom
|
||||
);
|
||||
@@ -62,15 +56,6 @@ export function Modals() {
|
||||
openOnboardingModalAtom
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const { jumpToSubPath } = useRouterHelper(router);
|
||||
const user = useCurrentUser();
|
||||
const workspaces = useWorkspaces();
|
||||
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||
const [, setCurrentWorkspace] = useCurrentWorkspace();
|
||||
const [transitioning, transition] = useTransition();
|
||||
const env = getEnvironment();
|
||||
const onCloseOnboardingModal = useCallback(() => {
|
||||
setOpenOnboardingModal(false);
|
||||
}, [setOpenOnboardingModal]);
|
||||
@@ -92,14 +77,39 @@ export function Modals() {
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const AllWorkspaceModals = (): ReactElement => {
|
||||
const [openWorkspacesModal, setOpenWorkspacesModal] = useAtom(
|
||||
openWorkspacesModalAtom
|
||||
);
|
||||
const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom(
|
||||
openCreateWorkspaceModalAtom
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const { jumpToSubPath } = useRouterHelper(router);
|
||||
const user = useCurrentUser();
|
||||
const workspaces = useWorkspaces();
|
||||
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
|
||||
rootCurrentWorkspaceIdAtom
|
||||
);
|
||||
const [transitioning, transition] = useTransition();
|
||||
return (
|
||||
<>
|
||||
<Suspense>
|
||||
<WorkspaceListModal
|
||||
disabled={transitioning}
|
||||
user={user}
|
||||
workspaces={workspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
open={openWorkspacesModal || workspaces.length === 0}
|
||||
open={
|
||||
(openWorkspacesModal || workspaces.length === 0) &&
|
||||
isOpenCreateWorkspaceModal === false
|
||||
}
|
||||
onClose={useCallback(() => {
|
||||
setOpenWorkspacesModal(false);
|
||||
}, [setOpenWorkspacesModal])}
|
||||
@@ -118,18 +128,18 @@ export function Modals() {
|
||||
onClickWorkspace={useCallback(
|
||||
workspace => {
|
||||
setOpenWorkspacesModal(false);
|
||||
setCurrentWorkspace(workspace.id);
|
||||
setCurrentWorkspaceId(workspace.id);
|
||||
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
|
||||
},
|
||||
[jumpToSubPath, setCurrentWorkspace, setOpenWorkspacesModal]
|
||||
[jumpToSubPath, setCurrentWorkspaceId, setOpenWorkspacesModal]
|
||||
)}
|
||||
onClickWorkspaceSetting={useCallback(
|
||||
workspace => {
|
||||
setOpenWorkspacesModal(false);
|
||||
setCurrentWorkspace(workspace.id);
|
||||
setCurrentWorkspaceId(workspace.id);
|
||||
jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
|
||||
},
|
||||
[jumpToSubPath, setCurrentWorkspace, setOpenWorkspacesModal]
|
||||
[jumpToSubPath, setCurrentWorkspaceId, setOpenWorkspacesModal]
|
||||
)}
|
||||
onClickLogin={useAffineLogIn()}
|
||||
onClickLogout={useAffineLogOut()}
|
||||
@@ -143,7 +153,7 @@ export function Modals() {
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<CreateWorkspaceModal
|
||||
mode={openCreateWorkspaceModal}
|
||||
mode={isOpenCreateWorkspaceModal}
|
||||
onClose={useCallback(() => {
|
||||
setOpenCreateWorkspaceModal(false);
|
||||
}, [setOpenCreateWorkspaceModal])}
|
||||
@@ -151,12 +161,12 @@ export function Modals() {
|
||||
async id => {
|
||||
setOpenCreateWorkspaceModal(false);
|
||||
setOpenWorkspacesModal(false);
|
||||
setCurrentWorkspace(id);
|
||||
setCurrentWorkspaceId(id);
|
||||
return jumpToSubPath(id, WorkspaceSubPath.ALL);
|
||||
},
|
||||
[
|
||||
jumpToSubPath,
|
||||
setCurrentWorkspace,
|
||||
setCurrentWorkspaceId,
|
||||
setOpenCreateWorkspaceModal,
|
||||
setOpenWorkspacesModal,
|
||||
]
|
||||
@@ -165,12 +175,4 @@ export function Modals() {
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const ModalProvider = (): ReactElement => {
|
||||
return (
|
||||
<>
|
||||
<Modals />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,16 +2,15 @@
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { loginResponseSchema } from '@affine/workspace/affine/login';
|
||||
import type { affineApis as API } from '@affine/workspace/affine/shared';
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { affineApis as API } from '../apis';
|
||||
|
||||
let affineApis: typeof API;
|
||||
|
||||
beforeAll(async () => {
|
||||
// @ts-expect-error
|
||||
globalThis.window = undefined;
|
||||
affineApis = (await import('../apis')).affineApis;
|
||||
affineApis = (await import('@affine/workspace/affine/shared')).affineApis;
|
||||
});
|
||||
|
||||
describe('apis', () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
} from '@affine/workspace/type';
|
||||
import type { AffinePublicWorkspace } from '@affine/workspace/type';
|
||||
import type { WorkspaceRegistry } from '@affine/workspace/type';
|
||||
import { WorkspaceSubPath } from '@affine/workspace/type';
|
||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import type { NextPage } from 'next';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
@@ -25,6 +24,13 @@ export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
|
||||
getLayout?: (page: ReactElement) => ReactNode;
|
||||
};
|
||||
|
||||
export enum WorkspaceSubPath {
|
||||
ALL = 'all',
|
||||
SETTING = 'setting',
|
||||
TRASH = 'trash',
|
||||
SHARED = 'shared',
|
||||
}
|
||||
|
||||
export const WorkspaceSubPathName = {
|
||||
[WorkspaceSubPath.ALL]: 'All Pages',
|
||||
[WorkspaceSubPath.SETTING]: 'Settings',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { env } from '@affine/env';
|
||||
import createCache from '@emotion/cache';
|
||||
|
||||
const isBrowser = getEnvironment().isBrowser;
|
||||
const isBrowser = env.isBrowser;
|
||||
|
||||
export default function createEmotionCache() {
|
||||
let insertionPoint;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"target": "ESNext",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -18,6 +17,33 @@
|
||||
"incremental": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/types/types.d.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"src/types/types.d.ts",
|
||||
"../../packages/graphql",
|
||||
"../electron/layers"
|
||||
],
|
||||
"exclude": ["node_modules"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/env"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/debug"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/component"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/i18n"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/jotai"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/hooks"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user