Compare commits

..

73 Commits

Author SHA1 Message Date
Alex Yang
0882d47dc9 v0.8.0-canary.9 2023-08-03 16:23:02 -07:00
Alex Yang
0c16eb1189 build: improve webpack config (#3561) 2023-08-03 23:05:46 +00:00
Alex Yang
f2ac4c7eda fix(core): editor wrapper css (#3563) 2023-08-03 21:40:43 +00:00
Alex Yang
0d531782ca ci: add dependabot.yml (#3562) 2023-08-03 12:24:23 -07:00
Alex Yang
47ff376195 ci: improve download @sentry/cli (#3560) 2023-08-03 17:26:30 +00:00
Alex Yang
33cc9d25a1 fix(core): use download atom (#3558) 2023-08-03 17:13:44 +00:00
Alex Yang
58ceeb9c5f chore: ignore output files (#3557) 2023-08-03 16:24:58 +00:00
Camol
3c00b69805 chore: remove repeated inreferences (#3551) 2023-08-03 16:11:51 +00:00
Chi Zhang
2678ca9330 feat: should hide downloadtip when it had been closed (#3555) 2023-08-03 23:50:08 +08:00
Peng Xiao
6f488d963b fix: a possible double connect issue (#3552) 2023-08-03 13:45:00 +00:00
JimmFly
8face25bdf fix: scrollbar position offset (#3538) 2023-08-03 08:52:04 +00:00
Alex Yang
0e32803247 refactor: merge plugin-infra into infra (#3540) 2023-08-03 08:48:59 +00:00
Pratik Kumar
3282344d4a fix: padding in the Switch button of Page/Edgeless (#3542) 2023-08-03 08:38:00 +00:00
Garfield Lee
78d23d86f5 feat: add tooltips for collection bar action buttons (#3545)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-03 08:26:17 +00:00
Garfield Lee
3a0797955c fix: editor-mode-switch animation should only run once (#3543)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-08-03 08:24:10 +00:00
Alex Yang
9449e66396 ci: fix upload artifact 2023-08-03 01:11:32 -07:00
Alex Yang
b6200ab56d ci: build storage 2023-08-03 00:32:07 -07:00
Alex Yang
ff23561e21 ci: fix needs 2023-08-03 00:25:31 -07:00
Alex Yang
e718428d50 ci: fix server build (#3541) 2023-08-03 07:10:02 +00:00
Alex Yang
ea34d66e14 feat: add @affine/sdk (#3536) 2023-08-03 04:47:05 +00:00
Alex Yang
d3c719d89a test: add test case for plugin bootstrap (#3529) 2023-08-03 01:48:35 +00:00
Alex Yang
dcd070b3e7 v0.8.0-canary.8 2023-08-02 16:51:34 -07:00
Alex Yang
32c08e49c5 feat: migrate to database v3 (#3528) 2023-08-02 16:50:10 -07:00
Garfield Lee
28a496bc67 feat: update editor mode switch icons (#3526) 2023-08-02 10:08:45 -07:00
Alex Yang
4386894e8a v0.8.0-canary.7 2023-08-02 10:06:05 -07:00
Alex Yang
33613c7041 fix(electron): check bundle (#3527) 2023-08-02 15:56:00 +00:00
Garfield Lee
db1b4d48b8 docs: update docs for build plugins (#3525) 2023-08-02 08:32:22 -07:00
Alex Yang
f007e2cecb ci: fix setup maker (#3519) 2023-08-02 05:03:05 +00:00
Peng Xiao
7e4df4c3d1 fix: stackoverflow issue in empty page (#3518) 2023-08-02 04:29:49 +00:00
Alex Yang
03b98b433b fix: drag workspace (#3513) 2023-08-01 23:29:17 +00:00
Alex Yang
1b17743ed3 feat: custom maker dmg (#3501) 2023-08-01 19:20:29 +00:00
Alex Yang
03f12f6aa4 feat: add filter schema (#3479) 2023-08-01 19:13:24 +00:00
Alex Yang
0176d66a94 v0.8.0-canary.6 2023-08-01 11:50:32 -07:00
Alex Yang
f078154b9b chore: bump version (#3489) 2023-08-01 11:30:56 -07:00
Peng Xiao
35a4c63c27 fix: flaky tests (#3507) 2023-08-01 14:13:04 +00:00
JimmFly
70f3508005 feat: brand new version of icons (#3496) 2023-08-01 05:51:30 +00:00
Alex Yang
16e22e614b feat: init @affine/worker (#3495) 2023-08-01 05:39:37 +00:00
Chi Zhang
c8b2728e27 feat: add placeholder for OPENAI_API_KEY input (#3486) 2023-07-31 17:35:53 +00:00
Alex Yang
452c780d40 refactor(i18n): language setup (#3484) 2023-07-31 09:21:12 +00:00
JimmFly
9567471e7f chore: adjustment options menu (#3455) 2023-07-31 07:56:51 +00:00
Alex Yang
4d4923cd37 v0.8.0-canary.5 2023-07-31 00:54:03 -07:00
Alex Yang
e85404a9c5 build: enable plugin system in production (#3480) 2023-07-31 06:52:11 +00:00
Alex Yang
1d43e46f99 chore: add noUnusedLocals and noUnusedParameters rules (#3476)
Co-authored-by: LongYinan <lynweklm@gmail.com>
2023-07-31 05:47:37 +00:00
Peng Xiao
5a8c1dcb57 fix: flaky test (#3478) 2023-07-31 05:47:13 +00:00
Alex Yang
9ffc523443 v0.8.0-canary.4 2023-07-30 20:33:52 -07:00
Alex Yang
39693a19bd chore: bump version (#3471) 2023-07-31 02:59:54 +00:00
Alex Yang
18fcaff5ee feat(plugin-cli): add cli af (#3465) 2023-07-30 18:10:45 +00:00
Alex Yang
568d5e4cdf fix(core): lockdown twice 2023-07-30 08:09:05 -07:00
fourdim
99c24b5cd8 chore: add the missing d.ts file for y-indexeddb (#3467) 2023-07-30 13:19:59 +00:00
Alex Yang
a3087d14d8 chore: remove unused files (#3466) 2023-07-30 06:35:00 +00:00
Alex Yang
cc7de52caf build: improve webpack config (#3463) 2023-07-30 06:34:52 +00:00
Alex Yang
05865d51c6 docs: update plugins section 2023-07-29 19:49:58 -07:00
Alex Yang
45089e176f v0.8.0-canary.3 2023-07-29 19:40:22 -07:00
Alex Yang
00a41b95b9 feat(plugin-infra): esm simulation in browser (#3464) 2023-07-30 02:23:00 +00:00
Alex Yang
765efd19da v0.8.0-canary.2 2023-07-29 14:33:38 -07:00
Alex Yang
ac59e28fcd feat(plugin-infra): support worker thread in server side (#3462) 2023-07-29 20:57:23 +00:00
Alex Yang
77dab70ff7 feat(plugin-infra): init permission control (#3461) 2023-07-29 20:10:50 +00:00
Alex Yang
0b66e911b1 feat(plugin-infra): support esm bundler (#3460) 2023-07-29 19:07:32 +00:00
JimmFly
6388a798c9 style: adjust active slider bar collection item active style (#3458)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-07-29 15:22:21 +00:00
Alex Yang
c45149b664 v0.8.0-canary.1 2023-07-29 08:23:38 -07:00
Alex Yang
ce0c1c39e2 feat: improve copilot plugin (#3459) 2023-07-29 07:37:01 +00:00
Alex Yang
52809a2783 refactor: image preview plugin (#3457) 2023-07-29 00:18:28 -07:00
Alex Yang
be3909370e refactor(plugin-infra): split functions (#3451) 2023-07-28 22:28:10 -07:00
Alex Yang
f79733e5df feat(plugin-infra): add package.json schema (#3456) 2023-07-29 05:07:25 +00:00
Alex Yang
2d95de06d6 docs: update rustc version 2023-07-28 21:36:43 -07:00
Alex Yang
97502231a3 v0.8.0-canary.0 2023-07-28 20:18:00 -07:00
Alex Yang
d20a6d2677 chore: bump version (#3449) 2023-07-29 02:53:29 +00:00
Alex Yang
9f43c0ddc8 refactor: plugin loading logic (#3448) 2023-07-29 02:43:52 +00:00
Peng Xiao
4cb1bf6a9f test: add test for sub doc (#3444) 2023-07-28 15:15:32 +00:00
JimmFly
d96263fde9 feat: add read only mode for page in trash (#3440) 2023-07-28 15:01:10 +00:00
JimmFly
ed8b2d9927 chore: update change log link (#3435) 2023-07-28 15:00:03 +00:00
Alex Yang
7b3be389d4 v0.7.0-canary.59 2023-07-27 22:03:23 -07:00
JimmFly
68755f4303 fix: bring back the lost WorkspaceDeleteModal style (#3434) 2023-07-27 21:32:46 -07:00
231 changed files with 7520 additions and 3257 deletions

View File

@@ -24,7 +24,8 @@
"debug",
"storage",
"infra",
"plugin-infra"
"plugin-cli",
"sdk"
]
]
}

View File

@@ -9,3 +9,6 @@ lib
.eslintrc.js
packages/i18n/src/i18n-generated.ts
e2e-dist-*
static
web-static
public

View File

@@ -43,7 +43,8 @@ const allPackages = [
'packages/i18n',
'packages/jotai',
'packages/native',
'packages/plugin-infra',
'packages/infra',
'packages/sdk',
'packages/templates',
'packages/theme',
'packages/workspace',

12
.github/actions/setup-maker/action.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Setup maker
description: 'Setup maker dmg for electron'
runs:
using: 'composite'
steps:
- name: 'Install create-dmg from brew'
if: runner.os == 'macos'
shell: bash
run: brew install create-dmg
- name: 'Build @affine/makder-dmg'
shell: bash
run: yarn nx build @affine/maker-dmg

View File

@@ -52,6 +52,7 @@ runs:
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
SENTRYCLI_SKIP_DOWNLOAD: '1'
- name: yarn install (try again)
if: ${{ steps.install.outcome == 'failure' }}
@@ -62,6 +63,7 @@ runs:
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
SENTRYCLI_SKIP_DOWNLOAD: '1'
- name: Get installed Playwright version
id: playwright-version

13
.github/actions/setup-sentry/action.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Setup @sentry/cli
description: 'Setup @sentry/cli'
runs:
using: 'composite'
steps:
- name: 'Install @sentry/cli from brew'
if: runner.os == 'macos'
shell: bash
run: brew install getsentry/tools/sentry-cli
- name: 'Install @sentry/cli from npm'
if: runner.os != 'macos'
shell: bash
run: sudo npm install -g @sentry/cli --unsafe-perm

10
.github/labeler.yml vendored
View File

@@ -22,8 +22,14 @@ plugin:bookmark-block:
plugin:copilot:
- 'plugins/copilot/**/*'
mod:plugin-infra:
- 'packages/plugin-infra/**/*'
mod:infra:
- 'packages/infra/**/*'
mod:sdk:
- 'packages/sdk/**/*'
mod:plugin-cli:
- 'packages/plugin-cli/**/*'
mod:workspace: 'packages/workspace/**/*'

View File

@@ -47,8 +47,6 @@ jobs:
electron-install: false
- name: Run i18n codegen
run: yarn i18n-codegen gen
- name: Run Type Check
run: yarn typecheck
- name: Run ESLint
run: yarn lint:eslint --max-warnings=0
- name: Run Prettier
@@ -58,6 +56,21 @@ jobs:
yarn lint:prettier
- name: Run circular
run: yarn circular
- name: Run Type Check
run: yarn typecheck
build-server:
name: Build Server
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Build Server
run: yarn nx build @affine/server
- name: Upload server dist
uses: actions/upload-artifact@v3
with:
@@ -110,6 +123,8 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
run: yarn run build:plugins
- name: Build Core
run: yarn nx build @affine/core
- name: Upload core artifact
@@ -119,10 +134,33 @@ jobs:
path: ./apps/core/dist
if-no-files-found: error
build-storage:
name: Build Storage
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
target: 'x86_64-unknown-linux-gnu'
- name: Build Storage
run: yarn build:storage
- name: Upload storage.node
uses: actions/upload-artifact@v3
with:
name: storage.node
path: ./packages/storage/storage.node
if-no-files-found: error
server-test:
name: Server Test
runs-on: ubuntu-latest
environment: development
needs: build-storage
services:
postgres:
image: postgres
@@ -158,24 +196,17 @@ jobs:
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Setup Rust
uses: ./.github/actions/setup-rust
- name: Download storage.node
uses: actions/download-artifact@v3
with:
target: 'x86_64-unknown-linux-gnu'
- name: Build Storage
run: yarn build:storage
name: storage.node
path: ./apps/server
- name: Run server tests
run: yarn test:coverage
working-directory: apps/server
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload storage.node
uses: actions/upload-artifact@v3
with:
name: storage.node
path: ./packages/storage/storage.node
if-no-files-found: error
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
@@ -207,6 +238,48 @@ jobs:
run: |
yarn exec concurrently -k -s first -n "SB,TEST" -c "magenta,blue" "yarn exec serve ./storybook-static -l 6006" "yarn exec wait-on tcp:6006 && yarn test"
e2e-plugin-test:
name: E2E Plugin Test
runs-on: ubuntu-latest
environment: development
needs: build-core
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Download core artifact
uses: actions/download-artifact@v3
with:
name: core
path: ./apps/core/dist
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: tests/affine-plugin
env:
COVERAGE: true
- name: Collect code coverage report
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2e-plugin-test
name: affine
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-plugin
path: ./test-results
if-no-files-found: ignore
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
@@ -261,7 +334,11 @@ jobs:
runs-on: ubuntu-latest
environment: development
needs: build-core
strategy:
matrix:
spec:
- { package: 0.7.0-canary.18 }
- { package: 0.8.0-canary.7 }
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
@@ -278,22 +355,18 @@ jobs:
- name: Unzip
run: yarn unzip
working-directory: ./tests/affine-legacy/0.7.0-canary.18
working-directory: ./tests/affine-legacy/${{ matrix.spec.package }}
- name: Run legacy playwright tests
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: ./tests/affine-legacy/0.7.0-canary.18
- name: Run vitest
run: yarn test
working-directory: ./tests/affine-legacy/0.7.0-canary.18
working-directory: ./tests/affine-legacy/${{ matrix.spec.package }}
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-migration
path: ./tests/affine-legacy/0.7.0-canary.18/test-results
name: test-results-e2e-migration-${{ matrix.spec.package }}
path: ./tests/affine-legacy/${{ matrix.spec.package }}/test-results
if-no-files-found: ignore
desktop-test:
@@ -342,6 +415,8 @@ jobs:
with:
playwright-install: true
hard-link-nm: false
- name: Setup Maker
uses: ./.github/actions/setup-maker
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:
@@ -365,6 +440,12 @@ jobs:
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
- name: Upload desktop dist
uses: actions/upload-artifact@v3
with:
name: dist-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: ./apps/electron/dist
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
@@ -379,12 +460,13 @@ jobs:
- name: Make bundle
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
env:
SKIP_BUNDLE: true
run: yarn workspace @affine/electron make --platform=darwin --arch=arm64
- name: Bundle output check
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
run: |
./scripts/unzip-macos-arm64.sh
yarn ts-node-esm ./scripts/macos-arm64-output-check.mts
working-directory: apps/electron
@@ -436,11 +518,11 @@ jobs:
build-docker:
if: github.ref == 'refs/heads/master'
name: Build Docker
needs:
- lint
- desktop-test
- server-test
runs-on: ubuntu-latest
needs:
- build-server
- build-core
- build-storage
steps:
- uses: actions/checkout@v3
- name: Download core artifact

9
.github/workflows/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'
versioning-strategy: increase
commit-message:
prefix: 'chore'

View File

@@ -50,6 +50,8 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
uses: ./.github/actions/setup-sentry
- name: Replace Version
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
- name: generate-assets
@@ -111,6 +113,8 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Maker
uses: ./.github/actions/setup-maker
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:

View File

@@ -47,6 +47,8 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
uses: ./.github/actions/setup-sentry
- name: Get canary version
id: get-canary-version
if: ${{ github.ref_type == 'tag' }}
@@ -113,6 +115,8 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Maker
uses: ./.github/actions/setup-maker
- name: Build AFFiNE native
uses: ./.github/actions/build-rust
with:

View File

@@ -126,10 +126,11 @@ If you have questions, you are welcome to contact us. One of the best places to
>
> (Currently, plugins are under heavy development, and the SDK is not yet available.)
| Name | |
| ------------------------------------ | ----------------------------------------- |
| [@affine/bookmark](plugins/bookmark) | A block for bookmarking a website |
| [@affine/copilot](plugins/copilot) | AI Copilot that help you document writing |
| Official Plugin | Description |
| ----------------------------------------------------- | ----------------------------------------- |
| [@affine/bookmark-plugin](plugins/bookmark) | A block for bookmarking a website |
| [@affine/copilot-plugin](plugins/copilot) | AI Copilot that help you document writing |
| [@affine/image-preview-plugin](plugins/image-preview) | Component for previewing an image |
## Thanks
@@ -197,7 +198,7 @@ See [LICENSE] for details.
[jobs available]: ./docs/jobs.md
[latest packages]: https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted
[contributor license agreement]: https://github.com/toeverything/affine/edit/master/.github/CLA.md
[rust-version-icon]: https://img.shields.io/badge/Rust-1.70.0-dea584
[rust-version-icon]: https://img.shields.io/badge/Rust-1.71.0-dea584
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
[codecov]: https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.1-success

View File

@@ -8,9 +8,9 @@ export const productionCacheGroups = {
test: /[\\/]node_modules[\\/]/,
name(module: any) {
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
const name =
module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)?.[1] ??
'unknown';
const name = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)?.[1];
return `npm-async-${name}`;
},
priority: Number.MAX_SAFE_INTEGER,

View File

@@ -1,7 +1,6 @@
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import HTMLPlugin from 'html-webpack-plugin';
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
import { PerfseePlugin } from '@perfsee/webpack';
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
@@ -32,6 +31,7 @@ const OptimizeOptionOptions: (
minimizer: [
new TerserPlugin({
minify: TerserPlugin.swcMinify,
exclude: [/plugins\/.+\/.+\.js$/, /plugins\/.+\/.+\.mjs$/],
parallel: true,
extractComments: true,
terserOptions: {
@@ -79,6 +79,10 @@ export const createConfiguration: (
name: 'affine',
// to set a correct base path for the source map
context: projectRoot,
experiments: {
topLevelAwait: true,
outputModule: false,
},
output: {
environment: {
module: true,
@@ -130,6 +134,10 @@ export const createConfiguration: (
module: {
parser: {
javascript: {
// Do not mock Node.js globals
node: false,
requireJs: false,
import: true,
// Treat as missing export as error
strictExportPresence: true,
},
@@ -137,6 +145,20 @@ export const createConfiguration: (
rules: [
{
test: /\.m?js?$/,
enforce: 'pre',
use: [
{
loader: require.resolve('source-map-loader'),
options: {
filterSourceMappingUrl: (
_url: string,
resourcePath: string
) => {
return resourcePath.includes('@blocksuite');
},
},
},
],
resolve: {
fullySpecified: false,
},
@@ -252,14 +274,6 @@ export const createConfiguration: (
ignoreOrder: true,
}),
]),
new HTMLPlugin({
template: join(rootPath, '.webpack', 'template.html'),
inject: 'body',
scriptLoading: 'defer',
minify: false,
chunks: ['index', 'plugin'],
filename: 'index.html',
}),
new VanillaExtractPlugin(),
new webpack.DefinePlugin({
'process.env': JSON.stringify({}),
@@ -269,7 +283,10 @@ export const createConfiguration: (
}),
new CopyPlugin({
patterns: [
{ from: resolve(rootPath, 'public'), to: resolve(rootPath, 'dist') },
{
from: resolve(rootPath, 'public'),
to: resolve(rootPath, 'dist'),
},
],
}),
],

View File

@@ -23,7 +23,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enableTestProperties: false,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
changelogUrl: 'https://affine.pro/blog/what-is-new-affine-0717',
changelogUrl: 'https://affine.pro/blog/what-is-new-affine-0728',
imageProxyUrl: 'https://workers.toeverything.workers.dev/proxy/image',
enablePreloading: true,
enableNewSettingModal: true,
@@ -43,7 +43,7 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enableTestProperties: true,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
changelogUrl: 'https://affine.pro/blog/what-is-new-affine-0717',
changelogUrl: 'https://github.com/toeverything/AFFiNE/releases',
imageProxyUrl: 'https://workers.toeverything.workers.dev/proxy/image',
enablePreloading: true,
enableNewSettingModal: true,

View File

@@ -1,8 +1,9 @@
import { createConfiguration, rootPath } from './config.js';
import { merge } from 'webpack-merge';
import { resolve } from 'node:path';
import { join, resolve } from 'node:path';
import type { BuildFlags } from '@affine/cli/config';
import { getRuntimeConfig } from './runtime-config.js';
import HTMLPlugin from 'html-webpack-plugin';
export default async function (cli_env: any, _: any) {
const flags: BuildFlags = JSON.parse(
@@ -14,15 +15,41 @@ export default async function (cli_env: any, _: any) {
const config = createConfiguration(flags, runtimeConfig);
return merge(config, {
entry: {
index: {
asyncChunks: false,
import: resolve(rootPath, 'src/index.tsx'),
'polyfill/ses': {
import: resolve(rootPath, 'src/polyfill/ses.ts'),
},
plugin: {
dependOn: ['index'],
asyncChunks: true,
dependOn: ['polyfill/ses'],
import: resolve(rootPath, 'src/bootstrap/register-plugins.ts'),
},
app: {
chunkLoading: 'import',
dependOn: ['polyfill/ses', 'plugin'],
import: resolve(rootPath, 'src/index.tsx'),
},
'_plugin/index.test': {
chunkLoading: 'import',
dependOn: ['polyfill/ses', 'plugin'],
import: resolve(rootPath, 'src/_plugin/index.test.tsx'),
},
},
plugins: [
new HTMLPlugin({
template: join(rootPath, '.webpack', 'template.html'),
inject: 'body',
scriptLoading: 'module',
minify: false,
chunks: ['app', 'plugin', 'polyfill/ses'],
filename: 'index.html',
}),
new HTMLPlugin({
template: join(rootPath, '.webpack', 'template.html'),
inject: 'body',
scriptLoading: 'module',
minify: false,
chunks: ['_plugin/index.test', 'plugin', 'polyfill/ses'],
filename: '_plugin/index.html',
}),
],
});
}

View File

@@ -2,7 +2,7 @@
"name": "@affine/core",
"type": "module",
"private": true,
"version": "0.7.0-canary.58",
"version": "0.8.0-canary.9",
"scripts": {
"build": "yarn -T run build-core",
"dev": "yarn -T run dev-core",
@@ -11,7 +11,6 @@
"dependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine/component": "workspace:*",
"@affine/copilot": "workspace:*",
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@affine/graphql": "workspace:*",
@@ -19,13 +18,13 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/blocks": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/editor": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/global": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/icons": "^2.1.27",
"@blocksuite/lit": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/store": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/block-std": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/icons": "^2.1.29",
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",
@@ -70,6 +69,7 @@
"express": "^4.18.2",
"html-webpack-plugin": "^5.5.3",
"raw-loader": "^4.0.2",
"source-map-loader": "^4.0.1",
"style-loader": "^3.3.3",
"swc-loader": "^0.2.3",
"swc-plugin-coverage-instrument": "^0.0.19",

View File

@@ -1,15 +1,21 @@
{
"name": "@affine/core",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"root": "apps/core",
"sourceRoot": "apps/core/src",
"targets": {
"build": {
"executor": "nx:run-script",
"dependsOn": ["^build"],
"dependsOn": [
{
"projects": ["tag:plugin"],
"target": "build",
"params": "ignore"
},
"^build"
],
"inputs": [
"{projectRoot}/.webpack/**/*",
"{projectRoot}/**/*",
"{projectRoot}/public/**/*",
"{workspaceRoot}/packages/component/src/**/*",
"{workspaceRoot}/packages/debug/src/**/*",
"{workspaceRoot}/packages/graphql/src/**/*",
@@ -45,7 +51,7 @@
"options": {
"script": "build"
},
"outputs": ["{projectRoot}/dist", "{projectRoot}/public/plugins"]
"outputs": ["{projectRoot}/dist"]
}
}
}

View File

@@ -7,6 +7,9 @@ const PORT = process.env.PORT || 8080;
app.use('/', express.static('dist'));
app.get('/*', (req, res) => {
if (req.url.startsWith('/plugins')) {
res.sendFile(req.url, { root: 'dist' });
}
res.sendFile('index.html', { root: 'dist' });
});

View File

@@ -0,0 +1,47 @@
import { assertExists } from '@blocksuite/global/utils';
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
import { use } from 'foxact/use';
import { useAtomValue } from 'jotai';
import { Provider } from 'jotai/react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { _pluginNestedImportsMap } from '../bootstrap/plugins/setup';
import { pluginRegisterPromise } from '../bootstrap/register-plugins';
async function main() {
const { setup } = await import('../bootstrap/setup');
await setup();
const root = document.getElementById('app');
assertExists(root);
const App = () => {
use(pluginRegisterPromise);
const plugins = useAtomValue(registeredPluginAtom);
_pluginNestedImportsMap.forEach(value => {
const exports = value.get('index.js');
assertExists(exports);
assertExists(exports?.get('entry'));
});
return (
<div>
<div data-plugins-load-status="success">
Successfully loaded plugins:
</div>
{plugins.map(plugin => {
return <div key={plugin}>{plugin}</div>;
})}
</div>
);
};
createRoot(root).render(
<StrictMode>
<Provider store={rootStore}>
<App />
</Provider>
</StrictMode>
);
}
await main();

View File

@@ -19,7 +19,7 @@ import {
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { nanoid } from '@blocksuite/store';
import { useStaticBlockSuiteWorkspace } from '@toeverything/plugin-infra/__internal__/react';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import {
BlockSuitePageList,

View File

@@ -3,16 +3,15 @@ import '@affine/component/theme/theme.css';
import { AffineContext } from '@affine/component/context';
import { WorkspaceFallback } from '@affine/component/workspace';
import { createI18n, setUpLanguage } from '@affine/i18n';
import { CacheProvider } from '@emotion/react';
import { use } from 'foxact/use';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, memo, Suspense, useEffect } from 'react';
import { lazy, memo, Suspense } from 'react';
import { RouterProvider } from 'react-router-dom';
import { router } from './router';
import createEmotionCache from './utils/create-emotion-cache';
const i18n = createI18n();
const cache = createEmotionCache();
const DevTools = lazy(() =>
@@ -32,14 +31,19 @@ const future = {
v7_startTransition: true,
} as const;
export const App = memo(function App() {
useEffect(() => {
async function loadLanguage() {
if (environment.isBrowser) {
const { createI18n, setUpLanguage } = await import('@affine/i18n');
const i18n = createI18n();
document.documentElement.lang = i18n.language;
// todo(himself65): this is a hack, we should use a better way to set the language
setUpLanguage(i18n)?.catch(error => {
console.error(error);
});
}, []);
await setUpLanguage(i18n);
}
}
const languageLoadingPromise = loadLanguage().catch(console.error);
export const App = memo(function App() {
use(languageLoadingPromise);
return (
<CacheProvider value={cache}>
<AffineContext>

View File

@@ -93,7 +93,7 @@ export const pageSettingFamily = atomFamily((pageId: string) =>
export const setPageModeAtom = atom(
void 0,
(get, set, pageId: string, mode: PageMode) => {
(_, set, pageId: string, mode: PageMode) => {
set(pageSettingFamily(pageId), { mode });
}
);

View File

@@ -1,4 +1,4 @@
import { currentPageIdAtom } from '@toeverything/plugin-infra/atom';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { atom } from 'jotai/vanilla';
import { pageSettingFamily } from './index';

View File

@@ -1,133 +0,0 @@
import { migrateToSubdoc } from '@affine/env/blocksuite';
import { setupGlobal } from '@affine/env/global';
import type {
LocalIndexedDBDownloadProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
type RootWorkspaceMetadataV2,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import {
migrateLocalBlobStorage,
upgradeV1ToV2,
} from '@affine/workspace/migration';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { assertExists } from '@blocksuite/global/utils';
import { rootStore } from '@toeverything/plugin-infra/atom';
import { WorkspaceAdapters } from '../adapters/workspace';
console.log('setup global');
setupGlobal();
rootStore.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
const value = localStorage.getItem('jotai-workspaces');
if (value) {
try {
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
const promises: Promise<void>[] = [];
const newMetadata = [...metadata];
metadata.forEach(oldMeta => {
if (!('version' in oldMeta)) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
const upgrade = async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (!workspace) {
console.warn('cannot find workspace', oldMeta.id);
return;
}
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
console.warn('not supported');
return;
}
const doc = workspace.blockSuiteWorkspace.doc;
const provider = createIndexedDBDownloadProvider(workspace.id, doc, {
awareness: workspace.blockSuiteWorkspace.awarenessStore.awareness,
}) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const newDoc = migrateToSubdoc(doc);
if (doc === newDoc) {
console.log('doc not changed');
return;
}
const newWorkspace = upgradeV1ToV2(workspace);
const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace
);
await adapter.CRUD.delete(workspace as any);
console.log('migrated', oldMeta.id, newId);
const index = newMetadata.findIndex(meta => meta.id === oldMeta.id);
newMetadata[index] = {
...oldMeta,
id: newId,
version: WorkspaceVersion.SubDoc,
};
await migrateLocalBlobStorage(workspace.id, newId);
};
// create a new workspace and push it to metadata
promises.push(upgrade());
}
});
await Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(() => {
console.error('migration failed');
})
.finally(() => {
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
window.dispatchEvent(new CustomEvent('migration-done'));
window.$migrationDone = true;
});
} catch (e) {
console.error('error when migrating data', e);
}
}
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
// new workspace should all support sub-doc feature
version: WorkspaceVersion.SubDoc,
}) satisfies RootWorkspaceMetadataV2
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
};
await rootStore
.get(rootWorkspacesMetadataAtom)
.then(meta => {
if (meta.length === 0 && localStorage.getItem('is-first-open') === null) {
const result = createFirst();
console.info('create first workspace', result);
localStorage.setItem('is-first-open', 'false');
rootStore.set(rootWorkspacesMetadataAtom, result).catch(console.error);
}
})
.catch(console.error);

View File

@@ -0,0 +1,86 @@
export interface FetchOptions {
fetch?: typeof fetch;
signal?: AbortSignal;
normalizeURL?(url: string): string;
/**
* Virtualize a url
* @param url URL to be rewrite
* @param direction Direction of this rewrite.
* 'in' means the url is from the outside world and should be virtualized.
* 'out' means the url is from the inside world and should be de-virtualized to fetch the real target.
*/
rewriteURL?(url: string, direction: 'in' | 'out'): string;
replaceRequest?(request: Request): Request | PromiseLike<Request>;
replaceResponse?(response: Response): Response | PromiseLike<Response>;
canConnect?(url: string): boolean | PromiseLike<boolean>;
}
export function createFetch(options: FetchOptions) {
const {
fetch: _fetch = fetch,
signal,
rewriteURL,
replaceRequest,
replaceResponse,
canConnect,
normalizeURL,
} = options;
return async function fetch(input: RequestInfo, init?: RequestInit) {
let request = new Request(input, {
...init,
signal: getMergedSignal(init?.signal, signal) || null,
});
if (normalizeURL) request = new Request(normalizeURL(request.url), request);
if (canConnect && !(await canConnect(request.url)))
throw new TypeError('Failed to fetch');
if (rewriteURL)
request = new Request(rewriteURL(request.url, 'out'), request);
if (replaceRequest) request = await replaceRequest(request);
let response = await _fetch(request);
if (rewriteURL) {
const { url, redirected, type } = response;
// Note: Response constructor does not allow us to set the url of a response.
// we have to define the own property on it. This is not a good simulation.
// To prevent get the original url by Response.prototype.[[get url]].call(response)
// we copy a response and set it's url to empty.
response = new Response(response.body, response);
Object.defineProperties(response, {
url: { value: url, configurable: true },
redirected: { value: redirected, configurable: true },
type: { value: type, configurable: true },
});
Object.defineProperty(response, 'url', {
configurable: true,
value: rewriteURL(url, 'in'),
});
}
if (replaceResponse) response = await replaceResponse(response);
return response;
};
}
function getMergedSignal(
signal: AbortSignal | undefined | null,
signal2: AbortSignal | undefined | null
) {
if (!signal) return signal2;
if (!signal2) return signal;
const abortController = new AbortController();
signal.addEventListener('abort', () => abortController.abort(), {
once: true,
});
signal2.addEventListener('abort', () => abortController.abort(), {
once: true,
});
return abortController.signal;
}

View File

@@ -0,0 +1,110 @@
type Handler = (...args: any[]) => void;
export interface Timers {
setTimeout: (handler: Handler, timeout?: number, ...args: any[]) => number;
clearTimeout: (handle: number) => void;
setInterval: (handler: Handler, timeout?: number, ...args: any[]) => number;
clearInterval: (handle: number) => void;
requestAnimationFrame: (callback: Handler) => number;
cancelAnimationFrame: (handle: number) => void;
requestIdleCallback?: typeof window.requestIdleCallback | undefined;
cancelIdleCallback?: typeof window.cancelIdleCallback | undefined;
queueMicrotask: typeof window.queueMicrotask;
}
export function createTimers(
abortSignal: AbortSignal,
originalTimes: Timers = {
requestAnimationFrame,
cancelAnimationFrame,
requestIdleCallback:
typeof requestIdleCallback === 'function'
? requestIdleCallback
: undefined,
cancelIdleCallback:
typeof cancelIdleCallback === 'function' ? cancelIdleCallback : undefined,
setTimeout,
clearTimeout,
setInterval,
clearInterval,
queueMicrotask,
}
): Timers {
const {
requestAnimationFrame: _requestAnimationFrame,
cancelAnimationFrame: _cancelAnimationFrame,
setInterval: _setInterval,
clearInterval: _clearInterval,
setTimeout: _setTimeout,
clearTimeout: _clearTimeout,
cancelIdleCallback: _cancelIdleCallback,
requestIdleCallback: _requestIdleCallback,
queueMicrotask: _queueMicrotask,
} = originalTimes;
const interval_timer_id: number[] = [];
const idle_id: number[] = [];
const raf_id: number[] = [];
abortSignal.addEventListener(
'abort',
() => {
raf_id.forEach(_cancelAnimationFrame);
interval_timer_id.forEach(_clearInterval);
_cancelIdleCallback && idle_id.forEach(_cancelIdleCallback);
},
{ once: true }
);
return {
// id is a positive number, it never repeats.
requestAnimationFrame(callback) {
raf_id[raf_id.length] = _requestAnimationFrame(callback);
return raf_id.length;
},
cancelAnimationFrame(handle) {
const id = raf_id[handle - 1];
if (!id) return;
_cancelAnimationFrame(id);
},
setInterval(handler, timeout) {
interval_timer_id[interval_timer_id.length] = (_setInterval as any)(
handler,
timeout
);
return interval_timer_id.length;
},
clearInterval(id) {
if (!id) return;
const handle = interval_timer_id[id - 1];
if (!handle) return;
_clearInterval(handle);
},
setTimeout(handler, timeout) {
idle_id[idle_id.length] = (_setTimeout as any)(handler, timeout);
return idle_id.length;
},
clearTimeout(id) {
if (!id) return;
const handle = idle_id[id - 1];
if (!handle) return;
_clearTimeout(handle);
},
requestIdleCallback: _requestIdleCallback
? function requestIdleCallback(callback, options) {
idle_id[idle_id.length] = _requestIdleCallback(callback, options);
return idle_id.length;
}
: undefined,
cancelIdleCallback: _cancelIdleCallback
? function cancelIdleCallback(handle) {
const id = idle_id[handle - 1];
if (!id) return;
_cancelIdleCallback(id);
}
: undefined,
queueMicrotask(callback) {
_queueMicrotask(() => abortSignal.aborted || callback());
},
};
}

View File

@@ -0,0 +1,449 @@
import * as AFFiNEComponent from '@affine/component';
import { DebugLogger } from '@affine/debug';
import type { CallbackMap, PluginContext } from '@affine/sdk/entry';
import { FormatQuickBar } from '@blocksuite/blocks';
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
import { assertExists } from '@blocksuite/global/utils';
import { DisposableGroup } from '@blocksuite/global/utils';
import * as Icons from '@blocksuite/icons';
import {
contentLayoutAtom,
currentPageAtom,
currentWorkspaceAtom,
editorItemsAtom,
headerItemsAtom,
rootStore,
settingItemsAtom,
windowItemsAtom,
} from '@toeverything/infra/atom';
import * as Jotai from 'jotai/index';
import { Provider } from 'jotai/react';
import * as JotaiUtils from 'jotai/utils';
import * as React from 'react';
import { createElement, type PropsWithChildren } from 'react';
import * as ReactJSXRuntime from 'react/jsx-runtime';
import * as ReactDom from 'react-dom';
import * as ReactDomClient from 'react-dom/client';
import * as SWR from 'swr';
import { createFetch } from './endowments/fercher';
import { createTimers } from './endowments/timer';
const dynamicImportKey = '$h_import';
const permissionLogger = new DebugLogger('plugins:permission');
const importLogger = new DebugLogger('plugins:import');
const setupRootImportsMap = () => {
_rootImportsMap.set('react', new Map(Object.entries(React)));
_rootImportsMap.set(
'react/jsx-runtime',
new Map(Object.entries(ReactJSXRuntime))
);
_rootImportsMap.set('react-dom', new Map(Object.entries(ReactDom)));
_rootImportsMap.set(
'react-dom/client',
new Map(Object.entries(ReactDomClient))
);
_rootImportsMap.set('@blocksuite/icons', new Map(Object.entries(Icons)));
_rootImportsMap.set(
'@affine/component',
new Map(Object.entries(AFFiNEComponent))
);
_rootImportsMap.set(
'@blocksuite/blocks/std',
new Map(Object.entries(BlockSuiteBlocksStd))
);
_rootImportsMap.set(
'@blocksuite/global/utils',
new Map(Object.entries(BlockSuiteGlobalUtils))
);
_rootImportsMap.set('jotai', new Map(Object.entries(Jotai)));
_rootImportsMap.set('jotai/utils', new Map(Object.entries(JotaiUtils)));
_rootImportsMap.set(
'@affine/sdk/entry',
new Map(
Object.entries({
rootStore: rootStore,
currentWorkspaceAtom: currentWorkspaceAtom,
currentPageAtom: currentPageAtom,
contentLayoutAtom: contentLayoutAtom,
})
)
);
_rootImportsMap.set('swr', new Map(Object.entries(SWR)));
};
// module -> importName -> updater[]
export const _rootImportsMap = new Map<string, Map<string, any>>();
setupRootImportsMap();
// pluginName -> module -> importName -> updater[]
export const _pluginNestedImportsMap = new Map<
string,
Map<string, Map<string, any>>
>();
const pluginImportsFunctionMap = new Map<string, (imports: any) => void>();
export const createImports = (pluginName: string) => {
if (pluginImportsFunctionMap.has(pluginName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return pluginImportsFunctionMap.get(pluginName)!;
}
const imports = (
newUpdaters: [string, [string, ((val: any) => void)[]][]][]
) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
importLogger.debug('currentImportMap', pluginName, currentImportMap);
for (const [module, moduleUpdaters] of newUpdaters) {
importLogger.debug('imports module', module, moduleUpdaters);
let moduleImports = _rootImportsMap.get(module);
if (!moduleImports) {
moduleImports = currentImportMap.get(module);
}
if (moduleImports) {
for (const [importName, importUpdaters] of moduleUpdaters) {
const updateImport = (value: any) => {
for (const importUpdater of importUpdaters) {
importUpdater(value);
}
};
if (moduleImports.has(importName)) {
const val = moduleImports.get(importName);
updateImport(val);
}
}
} else {
console.error(
'cannot find module in plugin import map',
module,
currentImportMap,
_pluginNestedImportsMap
);
}
}
};
pluginImportsFunctionMap.set(pluginName, imports);
return imports;
};
const abortController = new AbortController();
const pluginFetch = createFetch({});
const timer = createTimers(abortController.signal);
const sharedGlobalThis = Object.assign(Object.create(null), timer, {
fetch: pluginFetch,
});
const dynamicImportMap = new Map<
string,
(moduleName: string) => Promise<any>
>();
export const createOrGetDynamicImport = (
baseUrl: string,
pluginName: string
) => {
if (dynamicImportMap.has(pluginName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return dynamicImportMap.get(pluginName)!;
}
const dynamicImport = async (moduleName: string): Promise<any> => {
const codeUrl = `${baseUrl}/${moduleName}`;
const analysisUrl = `${baseUrl}/${moduleName}.json`;
const response = await fetch(codeUrl);
const analysisResponse = await fetch(analysisUrl);
const analysis = await analysisResponse.json();
const exports = analysis.exports as string[];
const code = await response.text();
const moduleCompartment = new Compartment(
createOrGetGlobalThis(
pluginName,
// use singleton here to avoid infinite loop
createOrGetDynamicImport(pluginName, baseUrl)
)
);
const entryPoint = moduleCompartment.evaluate(code, {
__evadeHtmlCommentTest__: true,
});
const moduleExports = {} as Record<string, any>;
const setVarProxy = new Proxy(
{},
{
get(_, p: string): any {
return (newValue: any) => {
moduleExports[p] = newValue;
};
},
}
);
entryPoint({
imports: createImports(pluginName),
liveVar: setVarProxy,
onceVar: setVarProxy,
});
importLogger.debug('import', moduleName, exports, moduleExports);
return moduleExports;
};
dynamicImportMap.set(pluginName, dynamicImport);
return dynamicImport;
};
const globalThisMap = new Map<string, any>();
export const createOrGetGlobalThis = (
pluginName: string,
dynamicImport: (moduleName: string) => Promise<any>
) => {
if (globalThisMap.has(pluginName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return globalThisMap.get(pluginName)!;
}
const pluginGlobalThis = Object.assign(
Object.create(null),
sharedGlobalThis,
{
process: Object.freeze({
env: {
NODE_ENV: process.env.NODE_ENV,
},
}),
// dynamic import function
[dynamicImportKey]: dynamicImport,
// UNSAFE: React will read `window` and `document`
window: new Proxy(
{},
{
get(_, key) {
permissionLogger.debug(`${pluginName} is accessing window`, key);
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
const result = Reflect.get(window, key);
if (typeof result === 'function') {
return function (...args: any[]) {
permissionLogger.debug(
`${pluginName} is calling window`,
key,
args
);
return result.apply(window, args);
};
}
permissionLogger.debug('window', key, result);
return result;
},
}
),
document: new Proxy(
{},
{
get(_, key) {
permissionLogger.debug(`${pluginName} is accessing document`, key);
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
const result = Reflect.get(document, key);
if (typeof result === 'function') {
return function (...args: any[]) {
permissionLogger.debug(
`${pluginName} is calling window`,
key,
args
);
return result.apply(document, args);
};
}
permissionLogger.debug('document', key, result);
return result;
},
}
),
navigator: {
userAgent: navigator.userAgent,
},
// safe to use for all plugins
Error: globalThis.Error,
TypeError: globalThis.TypeError,
RangeError: globalThis.RangeError,
console: globalThis.console,
crypto: globalThis.crypto,
// copilot uses these
CustomEvent: globalThis.CustomEvent,
Date: globalThis.Date,
Math: globalThis.Math,
URL: globalThis.URL,
URLSearchParams: globalThis.URLSearchParams,
Headers: globalThis.Headers,
TextEncoder: globalThis.TextEncoder,
TextDecoder: globalThis.TextDecoder,
Request: globalThis.Request,
// image-preview uses these
Blob: globalThis.Blob,
ClipboardItem: globalThis.ClipboardItem,
// fixme: use our own db api
indexedDB: globalThis.indexedDB,
IDBRequest: globalThis.IDBRequest,
IDBDatabase: globalThis.IDBDatabase,
IDBCursorWithValue: globalThis.IDBCursorWithValue,
IDBFactory: globalThis.IDBFactory,
IDBKeyRange: globalThis.IDBKeyRange,
IDBOpenDBRequest: globalThis.IDBOpenDBRequest,
IDBTransaction: globalThis.IDBTransaction,
IDBObjectStore: globalThis.IDBObjectStore,
IDBIndex: globalThis.IDBIndex,
IDBCursor: globalThis.IDBCursor,
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
}
);
globalThisMap.set(pluginName, pluginGlobalThis);
return pluginGlobalThis;
};
export const setupPluginCode = async (
baseUrl: string,
pluginName: string,
filename: string
) => {
if (!_pluginNestedImportsMap.has(pluginName)) {
_pluginNestedImportsMap.set(pluginName, new Map());
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
const isMissingPackage = (name: string) =>
_rootImportsMap.has(name) && !currentImportMap.has(name);
const bundleAnalysis = await fetch(`${baseUrl}/${filename}.json`).then(res =>
res.json()
);
const moduleExports = bundleAnalysis.exports as Record<string, [string]>;
const moduleImports = bundleAnalysis.imports as string[];
const moduleReexports = bundleAnalysis.reexports as Record<
string,
[localName: string, exportedName: string][]
>;
await Promise.all(
moduleImports.map(name => {
if (isMissingPackage(name)) {
return Promise.resolve();
} else {
importLogger.debug('missing package', name);
return setupPluginCode(baseUrl, pluginName, name);
}
})
);
const code = await fetch(`${baseUrl}/${filename.replace(/^\.\//, '')}`).then(
res => res.text()
);
importLogger.debug('evaluating', filename);
const moduleCompartment = new Compartment(
createOrGetGlobalThis(
pluginName,
// use singleton here to avoid infinite loop
createOrGetDynamicImport(baseUrl, pluginName)
)
);
const entryPoint = moduleCompartment.evaluate(code, {
__evadeHtmlCommentTest__: true,
});
const moduleExportsMap = new Map<string, any>();
const setVarProxy = new Proxy(
{},
{
get(_, p: string): any {
return (newValue: any) => {
moduleExportsMap.set(p, newValue);
};
},
}
);
currentImportMap.set(filename, moduleExportsMap);
entryPoint({
imports: createImports(pluginName),
liveVar: setVarProxy,
onceVar: setVarProxy,
});
for (const [newExport, [originalExport]] of Object.entries(moduleExports)) {
if (newExport === originalExport) continue;
const value = moduleExportsMap.get(originalExport);
moduleExportsMap.set(newExport, value);
moduleExportsMap.delete(originalExport);
}
for (const [name, reexports] of Object.entries(moduleReexports)) {
const targetExports = currentImportMap.get(filename);
const moduleExports = currentImportMap.get(name);
assertExists(targetExports);
assertExists(moduleExports);
for (const [exportedName, localName] of reexports) {
const exportedValue: any = moduleExports.get(exportedName);
assertExists(exportedValue);
targetExports.set(localName, exportedValue);
}
}
};
const PluginProvider = ({ children }: PropsWithChildren) =>
createElement(
Provider,
{
store: rootStore,
},
children
);
const group = new DisposableGroup();
const entryLogger = new DebugLogger('plugin:entry');
export const evaluatePluginEntry = (pluginName: string) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentImportMap = _pluginNestedImportsMap.get(pluginName)!;
const pluginExports = currentImportMap.get('index.js');
assertExists(pluginExports);
const entryFunction = pluginExports.get('entry');
const cleanup = entryFunction(<PluginContext>{
register: (part, callback) => {
entryLogger.info(`Registering ${pluginName} to ${part}`);
if (part === 'headerItem') {
rootStore.set(headerItemsAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['headerItem'],
}));
} else if (part === 'editor') {
rootStore.set(editorItemsAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['editor'],
}));
} else if (part === 'window') {
rootStore.set(windowItemsAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['window'],
}));
} else if (part === 'setting') {
rootStore.set(settingItemsAtom, items => ({
...items,
[pluginName]: callback as CallbackMap['setting'],
}));
} else if (part === 'formatBar') {
FormatQuickBar.customElements.push((page, getBlockRange) => {
const div = document.createElement('div');
(callback as CallbackMap['formatBar'])(div, page, getBlockRange);
return div;
});
} else {
throw new Error(`Unknown part: ${part}`);
}
},
utils: {
PluginProvider,
},
});
if (typeof cleanup !== 'function') {
throw new Error('Plugin entry must return a function');
}
group.add(cleanup);
};

View File

@@ -1,236 +1,77 @@
/// <reference types="@types/webpack-env" />
import 'ses';
import { DebugLogger } from '@affine/debug';
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
import * as AFFiNEComponent from '@affine/component';
import { FormatQuickBar } from '@blocksuite/blocks';
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
import { DisposableGroup } from '@blocksuite/global/utils';
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
import * as Icons from '@blocksuite/icons';
import * as Atom from '@toeverything/plugin-infra/atom';
import {
editorItemsAtom,
headerItemsAtom,
registeredPluginAtom,
rootStore,
settingItemsAtom,
windowItemsAtom,
} from '@toeverything/plugin-infra/atom';
import type {
CallbackMap,
PluginContext,
} from '@toeverything/plugin-infra/entry';
import * as Jotai from 'jotai';
import { Provider } from 'jotai/react';
import * as JotaiUtils from 'jotai/utils';
import type { PropsWithChildren } from 'react';
import * as React from 'react';
import * as ReactJSXRuntime from 'react/jsx-runtime';
import * as ReactDom from 'react-dom';
import * as ReactDomClient from 'react-dom/client';
import { evaluatePluginEntry, setupPluginCode } from './plugins/setup';
if (!process.env.COVERAGE) {
lockdown({
evalTaming: 'unsafeEval',
overrideTaming: 'severe',
consoleTaming: 'unsafe',
errorTaming: 'unsafe',
errorTrapping: 'platform',
unhandledRejectionTrapping: 'report',
});
const builtinPluginUrl = new Set([
'/plugins/bookmark',
'/plugins/copilot',
'/plugins/hello-world',
'/plugins/image-preview',
]);
const logger = new DebugLogger('register-plugins');
declare global {
// eslint-disable-next-line no-var
var __pluginPackageJson__: unknown[];
}
const PluginProvider = ({ children }: PropsWithChildren) =>
React.createElement(
Provider,
{
store: rootStore,
},
children
);
globalThis.__pluginPackageJson__ = [];
const customRequire = (id: string) => {
if (id === '@toeverything/plugin-infra/atom') {
return Atom;
}
if (id === 'react') {
return React;
}
if (id === 'react/jsx-runtime') {
return ReactJSXRuntime;
}
if (id === 'react-dom') {
return ReactDom;
}
if (id === 'react-dom/client') {
return ReactDomClient;
}
if (id === '@blocksuite/icons') {
return Icons;
}
if (id === '@affine/component') {
return AFFiNEComponent;
}
if (id === '@blocksuite/blocks/std') {
return BlockSuiteBlocksStd;
}
if (id === '@blocksuite/global/utils') {
return BlockSuiteGlobalUtils;
}
if (id === 'jotai') {
return Jotai;
}
if (id === 'jotai/utils') {
return JotaiUtils;
}
throw new Error(`Cannot find module '${id}'`);
};
const createGlobalThis = () => {
return {
process: Object.freeze({
env: {
NODE_ENV: process.env.NODE_ENV,
},
}),
// UNSAFE: React will read `window` and `document`
window,
document,
navigator,
userAgent: navigator.userAgent,
// todo(himself65): permission control
fetch: function (input: RequestInfo, init?: RequestInit) {
return globalThis.fetch(input, init);
},
setTimeout: function (callback: () => void, timeout: number) {
return globalThis.setTimeout(callback, timeout);
},
clearTimeout: function (id: number) {
return globalThis.clearTimeout(id);
},
// copilot uses these
crypto: globalThis.crypto,
CustomEvent: globalThis.CustomEvent,
Date: globalThis.Date,
Math: globalThis.Math,
URL: globalThis.URL,
URLSearchParams: globalThis.URLSearchParams,
Headers: globalThis.Headers,
TextEncoder: globalThis.TextEncoder,
TextDecoder: globalThis.TextDecoder,
Request: globalThis.Request,
Error: globalThis.Error,
// fixme: use our own db api
indexedDB: globalThis.indexedDB,
IDBRequest: globalThis.IDBRequest,
IDBDatabase: globalThis.IDBDatabase,
IDBCursorWithValue: globalThis.IDBCursorWithValue,
IDBFactory: globalThis.IDBFactory,
IDBKeyRange: globalThis.IDBKeyRange,
IDBOpenDBRequest: globalThis.IDBOpenDBRequest,
IDBTransaction: globalThis.IDBTransaction,
IDBObjectStore: globalThis.IDBObjectStore,
IDBIndex: globalThis.IDBIndex,
IDBCursor: globalThis.IDBCursor,
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
exports: {},
console: globalThis.console,
require: customRequire,
};
};
const group = new DisposableGroup();
const pluginList = (await (
await fetch(new URL(`./plugins/plugin-list.json`, window.location.origin))
).json()) as { name: string; assets: string[]; release: boolean }[];
await Promise.all(
pluginList.map(({ name: plugin, release, assets }) => {
if (!release && process.env.NODE_ENV !== 'development') {
return Promise.resolve();
}
const pluginCompartment = new Compartment(createGlobalThis(), {});
const pluginGlobalThis = pluginCompartment.globalThis;
const baseURL = new URL(`./plugins/${plugin}/`, window.location.origin);
const entryURL = new URL('index.js', baseURL);
rootStore.set(registeredPluginAtom, prev => [...prev, plugin]);
return fetch(entryURL).then(async res => {
if (assets.length > 0) {
await Promise.all(
assets.map(async asset => {
if (asset.endsWith('.css')) {
const res = await fetch(new URL(asset, baseURL));
if (res.ok) {
// todo: how to put css file into sandbox?
return res.text().then(text => {
const style = document.createElement('style');
style.setAttribute('plugin-id', plugin);
style.textContent = text;
document.head.appendChild(style);
});
}
return null;
} else {
return Promise.resolve();
}
})
);
}
const codeText = await res.text();
pluginCompartment.evaluate(codeText, {
__evadeHtmlCommentTest__: true,
});
pluginGlobalThis.__INTERNAL__ENTRY = {
register: (part, callback) => {
if (part === 'headerItem') {
rootStore.set(headerItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['headerItem'],
}));
} else if (part === 'editor') {
rootStore.set(editorItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['editor'],
}));
} else if (part === 'window') {
rootStore.set(windowItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['window'],
}));
} else if (part === 'setting') {
console.log('setting');
rootStore.set(settingItemsAtom, items => ({
...items,
[plugin]: callback as CallbackMap['setting'],
}));
} else if (part === 'formatBar') {
console.log('1');
FormatQuickBar.customElements.push((page, getBlockRange) => {
console.log('2');
const div = document.createElement('div');
(callback as CallbackMap['formatBar'])(div, page, getBlockRange);
return div;
});
} else {
throw new Error(`Unknown part: ${part}`);
export const pluginRegisterPromise = Promise.all(
[...builtinPluginUrl].map(url => {
return fetch(`${url}/package.json`)
.then(async res => {
const packageJson = await res.json();
const {
name: pluginName,
affinePlugin: {
release,
entry: { core },
assets,
},
} = packageJson;
globalThis.__pluginPackageJson__.push(packageJson);
logger.debug(`registering plugin ${pluginName}`);
logger.debug(`package.json: ${packageJson}`);
if (!release && !runtimeConfig.enablePlugin) {
return Promise.resolve();
}
const baseURL = url;
const entryURL = `${baseURL}/${core}`;
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
await setupPluginCode(baseURL, pluginName, core);
console.log(`prepareImports for ${pluginName} done`);
await fetch(entryURL).then(async () => {
if (assets.length > 0) {
await Promise.all(
assets.map(async (asset: string) => {
if (asset.endsWith('.css')) {
const res = await fetch(`${baseURL}/${asset}`);
if (res.ok) {
// todo: how to put css file into sandbox?
return res.text().then(text => {
const style = document.createElement('style');
style.setAttribute('plugin-id', pluginName);
style.textContent = text;
document.head.appendChild(style);
});
}
return null;
} else {
return Promise.resolve();
}
})
);
}
},
utils: {
PluginProvider,
},
} satisfies PluginContext;
const dispose = pluginCompartment.evaluate(
'exports.entry(__INTERNAL__ENTRY)'
);
if (typeof dispose !== 'function') {
throw new Error('Plugin entry must return a function');
}
pluginGlobalThis.__INTERNAL__ENTRY = undefined;
group.add(dispose);
});
evaluatePluginEntry(pluginName);
});
})
.catch(e => {
console.error(`error when fetch plugin from ${url}`, e);
});
})
);
console.log('register plugins finished');
).then(() => {
console.info('All plugins loaded');
});

View File

@@ -0,0 +1,180 @@
import {
migrateDatabaseBlockTo3,
migrateToSubdoc,
} from '@affine/env/blocksuite';
import { setupGlobal } from '@affine/env/global';
import type {
LocalIndexedDBDownloadProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
type RootWorkspaceMetadataV2,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import {
migrateLocalBlobStorage,
upgradeV1ToV2,
} from '@affine/workspace/migration';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { assertExists } from '@blocksuite/global/utils';
import { rootStore } from '@toeverything/infra/atom';
import { WorkspaceAdapters } from '../adapters/workspace';
async function tryMigration() {
const value = localStorage.getItem('jotai-workspaces');
if (value) {
try {
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
const promises: Promise<void>[] = [];
const newMetadata = [...metadata];
metadata.forEach(oldMeta => {
if (!('version' in oldMeta)) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
const upgrade = async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (!workspace) {
console.warn('cannot find workspace', oldMeta.id);
return;
}
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
console.warn('not supported');
return;
}
const doc = workspace.blockSuiteWorkspace.doc;
const provider = createIndexedDBDownloadProvider(
workspace.id,
doc,
{
awareness:
workspace.blockSuiteWorkspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const newDoc = migrateToSubdoc(doc);
if (doc === newDoc) {
console.log('doc not changed');
return;
}
const newWorkspace = upgradeV1ToV2(workspace);
await migrateDatabaseBlockTo3(newWorkspace.blockSuiteWorkspace.doc);
const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace
);
await adapter.CRUD.delete(workspace as any);
console.log('migrated', oldMeta.id, newId);
const index = newMetadata.findIndex(meta => meta.id === oldMeta.id);
newMetadata[index] = {
...oldMeta,
id: newId,
version: WorkspaceVersion.DatabaseV3,
};
await migrateLocalBlobStorage(workspace.id, newId);
console.log('migrate to v2');
};
// create a new workspace and push it to metadata
promises.push(upgrade());
} else if (oldMeta.version < WorkspaceVersion.DatabaseV3) {
console.log('migrate to v3');
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
promises.push(
(async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (workspace) {
const provider = createIndexedDBDownloadProvider(
workspace.id,
workspace.blockSuiteWorkspace.doc,
{
awareness:
workspace.blockSuiteWorkspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
await migrateDatabaseBlockTo3(
workspace.blockSuiteWorkspace.doc
);
}
const index = newMetadata.findIndex(
meta => meta.id === oldMeta.id
);
console.log('migrate to v3');
newMetadata[index] = {
...oldMeta,
version: WorkspaceVersion.DatabaseV3,
};
})()
);
}
});
await Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(() => {
console.error('migration failed');
})
.finally(() => {
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
window.dispatchEvent(new CustomEvent('migration-done'));
window.$migrationDone = true;
});
} catch (e) {
console.error('error when migrating data', e);
}
}
}
function createFirstAppData() {
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
<RootWorkspaceMetadataV2>{
id,
flavour: Plugin.flavour,
version: WorkspaceVersion.DatabaseV3,
}
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
};
if (localStorage.getItem('is-first-open') !== null) {
return;
}
const result = createFirst();
console.info('create first workspace', result);
localStorage.setItem('is-first-open', 'false');
rootStore.set(rootWorkspacesMetadataAtom, result);
}
export async function setup() {
rootStore.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
console.log('setup global');
setupGlobal();
createFirstAppData();
await tryMigration();
await rootStore.get(rootWorkspacesMetadataAtom);
console.log('setup done');
}

View File

@@ -9,7 +9,7 @@ import {
currentPageIdAtom,
currentWorkspaceIdAtom,
rootStore,
} from '@toeverything/plugin-infra/atom';
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react';
import { Provider } from 'jotai/react';
import type { ErrorInfo, ReactElement, ReactNode } from 'react';

View File

@@ -92,15 +92,15 @@ export const WorkspaceDeleteModal = ({
/>
</StyledInputContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
<Button onClick={onClose} size="large">
{t['Cancel']()}
</Button>
<Button
data-testid="delete-workspace-confirm-button"
disabled={!allowDelete}
onClick={handleDelete}
size="large"
type="error"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t['Delete']()}

View File

@@ -5,7 +5,7 @@ export const StyledModalWrapper = styled('div')(() => {
position: 'relative',
padding: '0px',
width: '560px',
background: 'var(--affine-white)',
background: 'var(--affine-background-overlay-panel-color)',
borderRadius: '12px',
// height: '312px',
};

View File

@@ -61,10 +61,7 @@ export const AboutAffine = () => {
desc={t['View the AFFiNE Changelog.']()}
style={{ cursor: 'pointer' }}
onClick={() => {
window.open(
'https://affine.pro/blog/what-is-new-affine-0717',
'_blank'
);
window.open(runtimeConfig.changelogUrl, '_blank');
}}
>
<ArrowRightSmallIcon />

View File

@@ -3,11 +3,13 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
registeredPluginAtom,
settingItemsAtom,
} from '@toeverything/plugin-infra/atom';
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
import type { FC, ReactNode } from 'react';
import { useRef } from 'react';
import { pluginItem } from './style.css';
const PluginSettingWrapper: FC<{
id: string;
title?: ReactNode;
@@ -45,7 +47,9 @@ export const Plugins = () => {
data-testid="plugins-title"
/>
{allowedPlugins.map(plugin => (
<PluginSettingWrapper key={plugin} id={plugin} title={plugin} />
<div className={pluginItem} key={plugin}>
<PluginSettingWrapper key={plugin} id={plugin} title={plugin} />
</div>
))}
</>
);

View File

@@ -7,3 +7,10 @@ export const settingWrapper = style({
minWidth: '150px',
maxWidth: '250px',
});
export const pluginItem = style({
borderBottom: '1px solid var(--affine-border-color)',
transition: '0.3s',
padding: '24px 8px',
fontSize: 'var(--affine-font-sm)',
});

View File

@@ -8,7 +8,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useStaticBlockSuiteWorkspace } from '@toeverything/plugin-infra/__internal__/react';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import clsx from 'clsx';
import { useAtomValue } from 'jotai';
import type { FC } from 'react';

View File

@@ -1,5 +1,5 @@
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react';
import { useSetAtom } from 'jotai';
import { Suspense, useCallback } from 'react';

View File

@@ -12,7 +12,7 @@ import { useBlockSuitePagePreview } from '@toeverything/hooks/use-block-suite-pa
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { useAtom, useAtomValue } from 'jotai';
import type React from 'react';
import { Suspense, useMemo } from 'react';
import { Suspense, useCallback, useMemo } from 'react';
import { allPageModeSelectAtom } from '../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
@@ -70,16 +70,20 @@ const PagePreview = ({
};
const PageListEmpty = (props: {
createPage?: () => void;
createPage?: ReturnType<typeof usePageHelper>['createPage'];
listType: BlockSuitePageListProps['listType'];
}) => {
const { listType, createPage } = props;
const t = useAFFiNEI18N();
const onCreatePage = useCallback(() => {
createPage?.();
}, [createPage]);
const getEmptyDescription = () => {
if (listType === 'all') {
const CreateNewPageButton = () => (
<button className={emptyDescButton} onClick={createPage}>
const createNewPageButton = (
<button className={emptyDescButton} onClick={onCreatePage}>
New Page
</button>
);
@@ -87,7 +91,7 @@ const PageListEmpty = (props: {
const shortcut = environment.isMacOs ? '⌘ + N' : 'Ctrl + N';
return (
<Trans i18nKey="emptyAllPagesClient">
Click on the <CreateNewPageButton /> button Or press
Click on the {createNewPageButton} button Or press
<kbd className={emptyDescKbd}>{{ shortcut } as any}</kbd> to create
your first page.
</Trans>
@@ -96,7 +100,7 @@ const PageListEmpty = (props: {
return (
<Trans i18nKey="emptyAllPages">
Click on the
<CreateNewPageButton />
{createNewPageButton}
button to create your first page.
</Trans>
);

View File

@@ -5,14 +5,15 @@ export const StyledEditorModeSwitch = styled('div')<{
showAlone?: boolean;
}>(({ switchLeft, showAlone }) => {
return {
width: showAlone ? '40px' : '78px',
maxWidth: showAlone ? '40px' : '70px',
gap: '8px',
height: '32px',
background: showAlone
? 'transparent'
: 'var(--affine-background-secondary-color)',
borderRadius: '12px',
...displayFlex('space-between', 'center'),
padding: '0 8px',
padding: '4px 4px',
position: 'relative',
'::after': {
@@ -25,7 +26,7 @@ export const StyledEditorModeSwitch = styled('div')<{
borderRadius: '8px',
zIndex: 1,
position: 'absolute',
transform: `translateX(${switchLeft ? '0' : '38px'})`,
transform: `translateX(${switchLeft ? '0' : '32px'})`,
transition: 'all .15s',
},
};
@@ -54,7 +55,7 @@ export const StyledSwitchItem = styled('button')<{
zIndex: 2,
fontSize: '20px',
path: {
fill: 'currentColor',
stroke: 'currentColor',
},
};
});

View File

@@ -35,7 +35,7 @@ const HoverAnimateController = ({
>
{cloneElement(children, {
isStopped: !startAnimate,
speed: 5,
speed: 1,
width: 20,
height: 20,
})}

View File

@@ -14,9 +14,9 @@ import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { currentPageIdAtom } from '@toeverything/plugin-infra/atom';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { useAtom, useAtomValue } from 'jotai';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { useParams } from 'react-router-dom';
import { pageSettingFamily } from '../../../../atoms';
@@ -24,7 +24,6 @@ import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suit
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { toast } from '../../../../utils';
import { MenuThemeModeSwitch } from '../header-right-items/theme-mode-switch';
import * as styles from '../styles.css';
import { LanguageMenu } from './language-menu';
const CommonMenu = () => {
const content = (
@@ -71,61 +70,56 @@ const PageMenu = () => {
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [openConfirm, setOpenConfirm] = useState(false);
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const handleFavorite = useCallback(() => {
setPageMeta(pageId, { favorite: !favorite });
toast(favorite ? t['Removed from Favorites']() : t['Added to Favorites']());
}, [favorite, pageId, setPageMeta, t]);
const handleSwitchMode = useCallback(() => {
setSetting(setting => ({
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
}));
toast(
mode === 'page'
? t['com.affine.edgelessMode']()
: t['com.affine.pageMode']()
);
}, [mode, setSetting, t]);
const handleOnConfirm = useCallback(() => {
removeToTrash(pageMeta.id);
toast(t['Moved to Trash']());
setOpenConfirm(false);
}, [pageMeta.id, removeToTrash, t]);
const EditMenu = (
<>
<>
<MenuItem
data-testid="editor-option-menu-favorite"
onClick={() => {
setPageMeta(pageId, { favorite: !favorite });
toast(
favorite
? t['Removed from Favorites']()
: t['Added to Favorites']()
);
}}
icon={
favorite ? (
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
) : (
<FavoriteIcon />
)
}
>
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
data-testid="editor-option-menu-edgeless"
onClick={() => {
setSetting(setting => ({
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
}));
}}
>
{t['Convert to ']()}
{mode === 'page' ? t['Edgeless']() : t['Page']()}
</MenuItem>
<Export />
<MoveToTrash
data-testid="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
<div className={styles.horizontalDividerContainer}>
<div className={styles.horizontalDivider} />
</div>
</>
<div
onClick={e => {
e.stopPropagation();
}}
<MenuItem
data-testid="editor-option-menu-favorite"
onClick={handleFavorite}
icon={
favorite ? (
<FavoritedIcon style={{ color: 'var(--affine-primary-color)' }} />
) : (
<FavoriteIcon />
)
}
>
<MenuThemeModeSwitch />
<LanguageMenu />
</div>
{favorite ? t['Remove from favorites']() : t['Add to Favorites']()}
</MenuItem>
<MenuItem
icon={mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
data-testid="editor-option-menu-edgeless"
onClick={handleSwitchMode}
>
{t['Convert to ']()}
{mode === 'page' ? t['Edgeless']() : t['Page']()}
</MenuItem>
<Export />
<MoveToTrash
data-testid="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
</>
);
@@ -145,11 +139,7 @@ const PageMenu = () => {
<MoveToTrash.ConfirmModal
open={openConfirm}
title={pageMeta.title}
onConfirm={() => {
removeToTrash(pageMeta.id);
toast(t['Moved to Trash']());
setOpenConfirm(false);
}}
onConfirm={handleOnConfirm}
onCancel={() => {
setOpenConfirm(false);
}}

View File

@@ -3,7 +3,7 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { currentPageIdAtom } from '@toeverything/plugin-infra/atom';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
import { useCallback, useState } from 'react';

View File

@@ -7,8 +7,8 @@ import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
import { isDesktop } from '@affine/env/constant';
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { headerItemsAtom } from '@toeverything/plugin-infra/atom';
import { useAtomValue } from 'jotai';
import { headerItemsAtom } from '@toeverything/infra/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import {
forwardRef,
@@ -19,6 +19,7 @@ import {
useState,
} from 'react';
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
import { currentModeAtom } from '../../../atoms/mode';
import type { AffineOfficialWorkspace } from '../../../shared';
import DownloadClientTip from './download-tips';
@@ -57,7 +58,9 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
[HeaderRightItemName.EditorOptionMenu]: {
Component: EditorOptionMenu,
availableWhen: (_, currentPage, { isPublic }) => {
return !isPublic && currentPage?.meta.trash !== true;
return (
!isPublic && currentPage?.meta.trash !== true && currentPage !== null
);
},
},
[HeaderRightItemName.WindowsAppControls]: {
@@ -152,10 +155,9 @@ export const Header = forwardRef<
PropsWithChildren<HeaderProps> & HTMLAttributes<HTMLDivElement>
>((props, ref) => {
const [showWarning, setShowWarning] = useState(false);
const [showDownloadTip, setShowDownloadTip] = useState(true);
// const [shouldShowGuideDownloadClientTip] = useAtom(
// guideDownloadClientTipAtom
// );
const [showDownloadTip, setShowDownloadTip] = useAtom(
guideDownloadClientTipAtom
);
useEffect(() => {
setShowWarning(shouldShowWarning());
}, []);
@@ -175,7 +177,10 @@ export const Header = forwardRef<
{showDownloadTip ? (
<DownloadClientTip
show={showDownloadTip}
onClose={() => setShowDownloadTip(false)}
onClose={() => {
setShowDownloadTip(false);
localStorage.setItem('affine-is-dt-hide', '1');
}}
/>
) : (
<BrowserWarning

View File

@@ -1,6 +1,7 @@
import './page-detail-editor.css';
import { PageNotFoundError } from '@affine/env/constant';
import type { CallbackMap, LayoutNode } from '@affine/sdk//entry';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
@@ -12,9 +13,7 @@ import {
editorItemsAtom,
rootStore,
windowItemsAtom,
} from '@toeverything/plugin-infra/atom';
import type { CallbackMap } from '@toeverything/plugin-infra/entry';
import type { LayoutNode } from '@toeverything/plugin-infra/type';
} from '@toeverything/infra/atom';
import clsx from 'clsx';
import { useAtomValue, useSetAtom } from 'jotai';
import type { CSSProperties, FC, ReactElement } from 'react';
@@ -191,7 +190,12 @@ const LayoutPanel = memo(function LayoutPanel(
</Suspense>
</Panel>
<PanelResizeHandle />
<Panel defaultSize={100 - node.splitPercentage}>
<Panel
defaultSize={100 - node.splitPercentage}
style={{
overflow: 'scroll',
}}
>
<Suspense>
<LayoutPanel node={node.second} editorProps={props.editorProps} />
</Suspense>

View File

@@ -150,6 +150,7 @@ export const Page = ({
<Collapsible.Root open={!collapsed}>
<MenuItem
data-testid="collection-page"
data-type="collection-list-item"
icon={icon}
onClick={clickPage}
className={styles.title}

View File

@@ -10,7 +10,8 @@ import { useReferenceLinkHelper } from './use-reference-link-helper';
export function useBlockSuiteMetaHelper(
blockSuiteWorkspace: BlockSuiteWorkspace
) {
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const { setPageMeta, getPageMeta, setPageReadonly } =
usePageMetaHelper(blockSuiteWorkspace);
const { addReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
@@ -56,8 +57,9 @@ export function useBlockSuiteMetaHelper(
trashDate: +new Date(),
trashRelate: isRoot ? parentMeta?.id : undefined,
});
setPageReadonly(pageId, true);
},
[getPageMeta, metas, setPageMeta]
[getPageMeta, metas, setPageMeta, setPageReadonly]
);
const restoreFromTrash = useCallback(
@@ -73,11 +75,12 @@ export function useBlockSuiteMetaHelper(
trashDate: undefined,
trashRelate: undefined,
});
setPageReadonly(pageId, false);
subpageIds.forEach(id => {
restoreFromTrash(id);
});
},
[addReferenceLink, getPageMeta, setPageMeta]
[addReferenceLink, getPageMeta, setPageMeta, setPageReadonly]
);
const permanentlyDeletePage = useCallback(

View File

@@ -2,7 +2,7 @@ import { assertExists } from '@blocksuite/global/utils';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/plugin-infra/atom';
} from '@toeverything/infra/atom';
import { useAtom, useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react';

View File

@@ -1,6 +1,6 @@
import type { WorkspaceRegistry } from '@affine/env/workspace';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import { currentPageIdAtom } from '@toeverything/plugin-infra/atom';
import { currentPageIdAtom } from '@toeverything/infra/atom';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';

View File

@@ -2,7 +2,7 @@ import type { WorkspaceSubPath } from '@affine/env/workspace';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/plugin-infra/atom';
} from '@toeverything/infra/atom';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports

View File

@@ -25,12 +25,12 @@ export function useTransformWorkspace() {
workspace.blockSuiteWorkspace
);
await WorkspaceAdapters[from].CRUD.delete(workspace as any);
await set(workspaces => {
set(workspaces => {
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
workspaces.splice(idx, 1, {
id: newId,
flavour: to,
version: WorkspaceVersion.SubDoc,
version: WorkspaceVersion.DatabaseV3,
});
return [...workspaces];
});

View File

@@ -1,7 +1,7 @@
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils';
import type { Workspace } from '@blocksuite/store';
import { useStaticBlockSuiteWorkspace } from '@toeverything/plugin-infra/__internal__/react';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import type { Atom } from 'jotai';
import { atom, useAtomValue } from 'jotai';

View File

@@ -7,7 +7,7 @@ import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { assertEquals } from '@blocksuite/global/utils';
import { nanoid } from '@blocksuite/store';
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
@@ -27,12 +27,12 @@ export function useAppHelper() {
async (workspaceId: string): Promise<string> => {
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
saveWorkspaceToLocalStorage(workspaceId);
await set(workspaces => [
set(workspaces => [
...workspaces,
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.SubDoc,
version: WorkspaceVersion.DatabaseV3,
},
]);
logger.debug('imported local workspace', workspaceId);
@@ -72,12 +72,12 @@ export function useAppHelper() {
jumpOnce: true,
});
}
await set(workspaces => [
set(workspaces => [
...workspaces,
{
id,
flavour: WorkspaceFlavour.LOCAL,
version: WorkspaceVersion.SubDoc,
version: WorkspaceVersion.DatabaseV3,
},
]);
logger.debug('created local workspace', id);

View File

@@ -1,16 +1,20 @@
import { WorkspaceFallback } from '@affine/component/workspace';
import { assertExists } from '@blocksuite/global/utils';
import { StrictMode } from 'react';
import { StrictMode, Suspense } from 'react';
import { createRoot } from 'react-dom/client';
async function main() {
await import('./bootstrap/before-app');
const { setup } = await import('./bootstrap/setup');
await setup();
const { App } = await import('./app');
const root = document.getElementById('app');
assertExists(root);
createRoot(root).render(
<StrictMode>
<App />
<Suspense fallback={<WorkspaceFallback key="AppLoading" />}>
<App />
</Suspense>
</StrictMode>
);
}

View File

@@ -26,8 +26,8 @@ import {
useSensor,
useSensors,
} from '@dnd-kit/core';
import { usePassiveWorkspaceEffect } from '@toeverything/plugin-infra/__internal__/react';
import { currentWorkspaceIdAtom } from '@toeverything/plugin-infra/atom';
import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react';
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useMemo } from 'react';

View File

@@ -1,7 +1,7 @@
import { DebugLogger } from '@affine/debug';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
import { rootStore } from '@toeverything/plugin-infra/atom';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { rootStore } from '@toeverything/infra/atom';
import { lazy } from 'react';
import type { LoaderFunction } from 'react-router-dom';
import { redirect } from 'react-router-dom';

View File

@@ -2,8 +2,8 @@ import { useCollectionManager } from '@affine/component/page-list';
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { assertExists } from '@blocksuite/global/utils';
import { getActiveBlockSuiteWorkspaceAtom } from '@toeverything/plugin-infra/__internal__/workspace';
import { currentPageIdAtom, rootStore } from '@toeverything/plugin-infra/atom';
import { getActiveBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import { currentPageIdAtom, rootStore } from '@toeverything/infra/atom';
import { useAtom } from 'jotai/react';
import { useCallback, useEffect } from 'react';
import type { LoaderFunction } from 'react-router-dom';

View File

@@ -7,7 +7,7 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import { currentPageIdAtom, rootStore } from '@toeverything/plugin-infra/atom';
import { currentPageIdAtom, rootStore } from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai';
import { useAtom } from 'jotai/react';
import { type ReactElement, useCallback, useEffect } from 'react';
@@ -27,7 +27,7 @@ const DetailPageImpl = (): ReactElement => {
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const collectionManager = useCollectionManager(currentWorkspace.id);
const onLoad = useCallback(
(page: Page, editor: EditorContainer) => {
(_: Page, editor: EditorContainer) => {
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
return openPage(blockSuiteWorkspace.id, pageId);
});

View File

@@ -1,8 +1,5 @@
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import {
currentWorkspaceIdAtom,
rootStore,
} from '@toeverything/plugin-infra/atom';
import { currentWorkspaceIdAtom, rootStore } from '@toeverything/infra/atom';
import type { ReactElement } from 'react';
import { type LoaderFunction, Outlet, redirect } from 'react-router-dom';

View File

@@ -0,0 +1,19 @@
import 'ses';
if (!process.env.COVERAGE) {
lockdown({
evalTaming: 'unsafeEval',
overrideTaming: 'severe',
consoleTaming: 'unsafe',
errorTaming: 'unsafe',
errorTrapping: 'platform',
unhandledRejectionTrapping: 'report',
});
console.log('SES lockdown complete');
} else {
Object.defineProperty(globalThis, 'harden', {
value: (x: any) => Object.freeze(x),
writable: false,
});
}

View File

@@ -5,7 +5,7 @@ import { arrayMove } from '@dnd-kit/sortable';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/plugin-infra/atom';
} from '@toeverything/infra/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { FC, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useTransition } from 'react';
@@ -124,7 +124,7 @@ export const AllWorkspaceModals = (): ReactElement => {
currentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(currentPageIdAtom);
const [transitioning, transition] = useTransition();
const [isPending, startTransition] = useTransition();
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
const handleOpenSettingModal = useCallback(
@@ -143,7 +143,7 @@ export const AllWorkspaceModals = (): ReactElement => {
<>
<Suspense>
<WorkspaceListModal
disabled={transitioning}
disabled={isPending}
workspaces={workspaces}
currentWorkspaceId={currentWorkspaceId}
open={
@@ -157,10 +157,10 @@ export const AllWorkspaceModals = (): ReactElement => {
(activeId, overId) => {
const oldIndex = workspaces.findIndex(w => w.id === activeId);
const newIndex = workspaces.findIndex(w => w.id === overId);
transition(() => {
startTransition(() => {
setWorkspaces(workspaces =>
arrayMove(workspaces, oldIndex, newIndex)
).catch(console.error);
);
});
},
[setWorkspaces, workspaces]
@@ -195,11 +195,13 @@ export const AllWorkspaceModals = (): ReactElement => {
setOpenCreateWorkspaceModal(false);
}, [setOpenCreateWorkspaceModal])}
onCreate={useCallback(
async id => {
setOpenCreateWorkspaceModal(false);
setOpenWorkspacesModal(false);
setCurrentWorkspaceId(id);
return jumpToSubPath(id, WorkspaceSubPath.ALL);
id => {
startTransition(() => {
setOpenCreateWorkspaceModal(false);
setOpenWorkspacesModal(false);
setCurrentWorkspaceId(id);
jumpToSubPath(id, WorkspaceSubPath.ALL);
});
},
[
jumpToSubPath,

View File

@@ -16,7 +16,7 @@
"jsxImportSource": "@emotion/react",
"incremental": true,
"experimentalDecorators": true,
"types": ["webpack-env"]
"types": ["webpack-env", "ses"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"],

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/docs",
"version": "0.7.0-canary.58",
"version": "0.8.0-canary.9",
"type": "module",
"private": true,
"scripts": {
@@ -10,12 +10,12 @@
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/blocks": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/editor": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/global": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/lit": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/store": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/block-std": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
"express": "^4.18.2",
"jotai": "^2.2.2",
"react": "18.3.0-canary-1fdacbefd-20230630",

View File

@@ -96,9 +96,10 @@ test('app theme', async ({ page, electronApp }) => {
}
{
await page.getByTestId('editor-option-menu').click();
await page.getByTestId('change-theme-dark').click();
await page.getByTestId('settings-modal-trigger').click();
await page.getByTestId('appearance-panel-trigger').click();
await page.waitForTimeout(50);
await page.getByTestId('dark-theme-trigger').click();
const themeMode = await root.evaluate(element =>
element.getAttribute('data-theme')
);

View File

@@ -47,6 +47,9 @@ test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
test('export then add', async ({ page, appInfo, workspace }) => {
const w = await workspace.current();
await page.focus('.affine-default-page-block-title');
await page.fill('.affine-default-page-block-title', 'test1');
await page.getByTestId('slider-bar-workspace-setting-button').click();
await expect(page.getByTestId('setting-modal')).toBeVisible();
@@ -108,4 +111,11 @@ test('export then add', async ({ page, appInfo, workspace }) => {
expect(newWorkspace.id).not.toBe(originalId);
// check its name is correct
await expect(page.getByTestId('workspace-name')).toHaveText(newWorkspaceName);
// find button which has the title "test1"
const test1PageButton = await page.waitForSelector(`text="test1"`);
await test1PageButton.click();
const title = page.locator('[data-block-is-title] >> text="test1"');
await expect(title).toBeVisible();
});

View File

@@ -28,6 +28,59 @@ const arch =
const windowsIconUrl = `https://cdn.affine.pro/app-icons/icon_${buildType}.ico`;
const makers = [
!process.env.SKIP_BUNDLE && {
name: '@affine/maker-dmg',
config: {
format: 'ULFO',
icon: icnsPath,
name: 'AFFiNE',
'icon-size': 128,
background: path.resolve(
__dirname,
'./resources/icons/dmg-background.png'
),
file: path.resolve(
__dirname,
'out',
buildType,
`${productName}-darwin-${arch}`,
`${productName}.app`
),
},
},
{
name: '@electron-forge/maker-zip',
config: {
name: 'affine',
iconUrl: icoPath,
setupIcon: icoPath,
platforms: ['darwin', 'linux', 'win32'],
},
},
!process.env.SKIP_BUNDLE && {
name: '@electron-forge/maker-squirrel',
config: {
name: 'AFFiNE',
setupIcon: icoPath,
iconUrl: windowsIconUrl,
loadingGif: './resources/icons/affine_installing.gif',
},
},
!process.env.SKIP_BUNDLE && {
name: '@reforged/maker-appimage',
config: {
name: 'AFFiNE',
iconUrl: icoPath,
setupIcon: icoPath,
platforms: ['linux'],
options: {
bin: productName,
},
},
},
].filter(Boolean);
/**
* @type {import('@electron-forge/shared-types').ForgeConfig}
*/
@@ -57,63 +110,7 @@ module.exports = {
// We need the following line for updater
extraResource: ['./resources/app-update.yml'],
},
makers: [
{
name: '@electron-forge/maker-dmg',
config: {
format: 'ULFO',
icon: icnsPath,
name: 'AFFiNE',
'icon-size': 128,
background: './resources/icons/dmg-background.png',
contents: [
{
x: 176,
y: 192,
type: 'file',
path: path.resolve(
__dirname,
'out',
buildType,
`${productName}-darwin-${arch}`,
`${productName}.app`
),
},
{ x: 432, y: 192, type: 'link', path: '/Applications' },
],
},
},
{
name: '@electron-forge/maker-zip',
config: {
name: 'affine',
iconUrl: icoPath,
setupIcon: icoPath,
platforms: ['darwin', 'linux', 'win32'],
},
},
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'AFFiNE',
setupIcon: icoPath,
iconUrl: windowsIconUrl,
loadingGif: './resources/icons/affine_installing.gif',
},
},
{
name: '@reforged/maker-appimage',
config: {
name: 'AFFiNE',
iconUrl: icoPath,
setupIcon: icoPath,
platforms: ['linux'],
options: {
bin: productName,
},
},
},
],
makers,
hooks: {
readPackageJson: async (_, packageJson) => {
// we want different package name for canary build
@@ -124,17 +121,28 @@ module.exports = {
const { rm, cp } = require('node:fs/promises');
const { resolve } = require('node:path');
await rm(
resolve(__dirname, './node_modules/@toeverything/plugin-infra'),
await rm(resolve(__dirname, './node_modules/@toeverything/infra'), {
recursive: true,
force: true,
});
await cp(
resolve(__dirname, '../../packages/infra'),
resolve(__dirname, './node_modules/@toeverything/infra'),
{
recursive: true,
force: true,
}
);
await rm(resolve(__dirname, './node_modules/@affine/sdk'), {
recursive: true,
force: true,
});
await cp(
resolve(__dirname, '../../packages/plugin-infra'),
resolve(__dirname, './node_modules/@toeverything/plugin-infra'),
resolve(__dirname, '../../packages/sdk'),
resolve(__dirname, './node_modules/@affine/sdk'),
{
recursive: true,
force: true,

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.7.0-canary.58",
"version": "0.8.0-canary.9",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -10,6 +10,7 @@
"description": "AFFiNE App",
"homepage": "https://github.com/toeverything/AFFiNE",
"scripts": {
"start": "electron .",
"dev": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"dev:prod": "yarn node scripts/dev.mjs",
"build": "NODE_ENV=production zx scripts/build-layers.mjs",
@@ -25,22 +26,22 @@
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/env": "workspace:*",
"@affine/maker-dmg": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/editor": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/lit": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/store": "0.0.0-20230720073515-bea92e0f-nightly",
"@affine/sdk": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
"@electron-forge/cli": "^6.2.1",
"@electron-forge/core": "^6.2.1",
"@electron-forge/core-utils": "^6.2.1",
"@electron-forge/maker-deb": "^6.2.1",
"@electron-forge/maker-dmg": "^6.2.1",
"@electron-forge/maker-squirrel": "^6.2.1",
"@electron-forge/maker-zip": "^6.2.1",
"@electron-forge/shared-types": "^6.2.1",
"@electron/remote": "2.0.10",
"@reforged/maker-appimage": "^3.3.1",
"@toeverything/infra": "workspace:*",
"@types/fs-extra": "^11.0.1",
"@types/uuid": "^9.0.2",
"cross-env": "7.0.3",
@@ -59,7 +60,7 @@
"zx": "^7.2.3"
},
"dependencies": {
"@toeverything/plugin-infra": "workspace:*",
"@toeverything/infra": "workspace:*",
"async-call-rpc": "^6.3.1",
"electron-updater": "^6.0.0",
"link-preview-js": "^3.0.4",

View File

@@ -9,7 +9,7 @@
"executor": "nx:run-script",
"dependsOn": [
{
"projects": ["@affine/bookmark-block"],
"projects": ["tag:plugin"],
"target": "build",
"params": "ignore"
},

View File

@@ -32,6 +32,7 @@ export const config = () => {
resolve(electronDir, './src/main/index.ts'),
resolve(electronDir, './src/preload/index.ts'),
resolve(electronDir, './src/helper/index.ts'),
resolve(electronDir, './src/worker/plugin.ts'),
],
entryNames: '[dir]',
outdir: resolve(electronDir, './dist'),

View File

@@ -3,10 +3,11 @@ import { readdir } from 'node:fs/promises';
const outputRoot = fileURLToPath(
new URL(
'../zip-out/AFFiNE-canary.app/Contents/Resources/app',
'../out/canary/AFFiNE-canary-darwin-arm64/AFFiNE-canary.app/Contents/Resources/app',
import.meta.url
)
);
const outputList = [
[
'dist',
@@ -19,7 +20,7 @@ const outputList = [
],
],
['dist/plugins', ['bookmark']],
['dist/plugins/bookmark', ['index.js']],
['dist/plugins/bookmark', ['index.cjs']],
] as [entry: string, expected: string[]][];
await Promise.all(

View File

@@ -1,16 +0,0 @@
#!/bin/bash
# Set the directory
dir="./out/canary/make/zip/darwin/arm64"
# Get the first file
file=$(ls -1 $dir | head -n 1)
# Check if file exists and is a zip file
if [ -f "$dir/$file" ] && [ ${file: -4} == ".zip" ]
then
# Unzip the file
unzip "$dir/$file" -d "zip-out"
else
echo "No zip file found"
fi

View File

@@ -1,8 +1,8 @@
import { app, Menu } from 'electron';
import { isMacOS } from '../../shared/utils';
import { revealLogFile } from '../logger';
import { checkForUpdates } from '../updater';
import { isMacOS } from '../utils';
import { applicationMenuSubjects } from './subject';
// Unique id for menuitems

View File

@@ -15,8 +15,8 @@ import {
type WebContents,
} from 'electron';
import { MessageEventChannel } from '../shared/utils';
import { logger } from './logger';
import { MessageEventChannel } from './utils';
const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js');

View File

@@ -2,6 +2,7 @@ import { shell } from 'electron';
import log from 'electron-log';
export const logger = log.scope('main');
export const pluginLogger = log.scope('plugin');
log.initialize();
export function getLogFilePath() {

View File

@@ -4,10 +4,10 @@ import { BrowserWindow, nativeTheme } from 'electron';
import electronWindowState from 'electron-window-state';
import { join } from 'path';
import { isMacOS, isWindows } from '../shared/utils';
import { getExposedMeta } from './exposed';
import { ensureHelperProcess } from './helper-process';
import { logger } from './logger';
import { isMacOS, isWindows } from './utils';
const IS_DEV: boolean =
process.env.NODE_ENV === 'development' && !process.env.CI;
@@ -114,6 +114,7 @@ async function createWindow() {
// singleton
let browserWindow: Electron.BrowserWindow | undefined;
/**
* Restore existing BrowserWindow or Create new BrowserWindow
*/

View File

@@ -1,7 +1,14 @@
import { join, resolve } from 'node:path';
import { Worker } from 'node:worker_threads';
import { logger } from '@affine/electron/main/logger';
import { logger, pluginLogger } from '@affine/electron/main/logger';
import { AsyncCall } from 'async-call-rpc';
import { ipcMain } from 'electron';
import { readFile } from 'fs/promises';
import { MessageEventChannel } from '../shared/utils';
const builtInPlugins = ['bookmark'];
declare global {
// fixme(himself65):
@@ -10,26 +17,41 @@ declare global {
var asyncCall: Record<string, (...args: any) => PromiseLike<any>>;
}
export function registerPlugin() {
export async function registerPlugin() {
logger.info('import plugin manager');
globalThis.asyncCall = {};
const bookmarkPluginPath = join(
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
'./bookmark/index.js'
const asyncCall = AsyncCall<
Record<string, (...args: any) => PromiseLike<any>>
>(
{
log: (...args: any[]) => {
pluginLogger.log(...args);
},
},
{
channel: new MessageEventChannel(
new Worker(resolve(__dirname, './worker.js'), {})
),
}
);
globalThis.asyncCall = asyncCall;
await Promise.all(
builtInPlugins.map(async plugin => {
const pluginPackageJsonPath = join(
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
`./${plugin}/package.json`
);
logger.info(`${plugin} plugin path:`, pluginPackageJsonPath);
const packageJson = JSON.parse(
await readFile(pluginPackageJsonPath, 'utf-8')
);
console.log('packageJson', packageJson);
const serverCommand: string[] = packageJson.affinePlugin.serverCommand;
serverCommand.forEach(command => {
ipcMain.handle(command, async (_, ...args) => {
logger.info(`plugin ${plugin} called`);
return asyncCall[command](...args);
});
});
})
);
logger.info('bookmark plugin path:', bookmarkPluginPath);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { entry } = require(bookmarkPluginPath);
entry({
registerCommand: (command: string, handler: (...args: any[]) => any) => {
logger.info('register plugin command', command);
ipcMain.handle(command, (event, ...args) => handler(...args));
globalThis.asyncCall[command] = handler;
},
registerCommands: (command: string) => {
ipcMain.removeHandler(command);
delete globalThis.asyncCall[command];
},
});
}

View File

@@ -1,7 +1,7 @@
import { app, BrowserWindow, nativeTheme } from 'electron';
import { isMacOS } from '../../shared/utils';
import type { NamespaceHandlers } from '../type';
import { isMacOS } from '../utils';
import { getGoogleOauthCode } from './google-auth';
export const uiHandlers = {

View File

@@ -2,8 +2,8 @@ import { app } from 'electron';
import { autoUpdater } from 'electron-updater';
import { z } from 'zod';
import { isMacOS } from '../../shared/utils';
import { logger } from '../logger';
import { isMacOS } from '../utils';
import { updaterSubjects } from './event';
export const ReleaseTypeSchema = z.enum([

View File

@@ -0,0 +1,45 @@
import { join, resolve } from 'node:path';
import { parentPort } from 'node:worker_threads';
import type { ServerContext } from '@affine/sdk/server';
import { AsyncCall } from 'async-call-rpc';
import { MessageEventChannel } from '../shared/utils';
if (!parentPort) {
throw new Error('parentPort is null');
}
const commandProxy: Record<string, (...args: any[]) => Promise<any>> = {};
parentPort.start();
const mainThread = AsyncCall<{
log: (...args: any[]) => Promise<void>;
}>(commandProxy, {
channel: new MessageEventChannel(parentPort),
});
globalThis.console.log = mainThread.log;
globalThis.console.error = mainThread.log;
globalThis.console.info = mainThread.log;
globalThis.console.debug = mainThread.log;
globalThis.console.warn = mainThread.log;
// eslint-disable-next-line @typescript-eslint/no-var-requires
const bookmarkPluginModule = require(join(
process.env.PLUGIN_DIR ?? resolve(__dirname, './plugins'),
'./bookmark/index.cjs'
));
const serverContext: ServerContext = {
registerCommand: (command, fn) => {
console.log('register command', command);
commandProxy[command] = fn;
},
unregisterCommand: command => {
console.log('unregister command', command);
delete commandProxy[command];
},
};
bookmarkPluginModule.entry(serverContext);

View File

@@ -17,13 +17,13 @@
"exclude": ["node_modules", "out", "dist", "**/__tests__/**/*"],
"references": [
{
"path": "../../packages/plugin-infra"
"path": "../../packages/infra"
},
{
"path": "../../packages/native"
},
{
"path": "../../packages/infra"
"path": "../../packages/sdk"
},
{
"path": "../../packages/env"

View File

@@ -1,13 +1,14 @@
{
"name": "@affine/server",
"private": true,
"version": "0.7.0-canary.58",
"version": "0.8.0-canary.9",
"description": "Affine Node.js server",
"type": "module",
"bin": {
"run-test": "./scripts/run-test.ts"
},
"scripts": {
"build": "tsc",
"dev": "nodemon ./src/index.ts",
"test": "yarn exec ts-node-esm ./scripts/run-test.ts all",
"test:watch": "yarn exec ts-node-esm ./scripts/run-test.ts all --watch",

View File

@@ -32,7 +32,7 @@ export default {
framework: {
name: '@storybook/react-vite',
},
async viteFinal(config, { configType }) {
async viteFinal(config, _) {
return mergeConfig(config, {
assetsInclude: ['**/*.md'],
plugins: [
@@ -50,13 +50,6 @@ export default {
coverage: false,
}),
},
resolve: {
alias: {
'dotenv/config': fileURLToPath(
new URL('../../../scripts/vitest/dotenv-config.ts', import.meta.url)
),
},
},
});
},
} as StorybookConfig;

View File

@@ -30,13 +30,13 @@
"wait-on": "^7.0.1"
},
"devDependencies": {
"@blocksuite/block-std": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/blocks": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/editor": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/global": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/icons": "^2.1.27",
"@blocksuite/lit": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/store": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/block-std": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/icons": "^2.1.29",
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
"react": "18.2.0",
"react-dom": "18.2.0"
},
@@ -48,5 +48,5 @@
"@blocksuite/lit": "*",
"@blocksuite/store": "*"
},
"version": "0.7.0-canary.58"
"version": "0.8.0-canary.9"
}

View File

@@ -1,11 +1,13 @@
import { BlockHubWrapper } from '@affine/component/block-hub';
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
import { ImagePreviewModal } from '@affine/component/image-preview-modal';
import { initEmptyPage } from '@affine/env/blocksuite';
import { WorkspaceFlavour } from '@affine/env/workspace';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { ImagePreviewModal } from '@affine/image-preview-plugin/src/component';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { Meta } from '@storybook/react';
import { createPortal } from 'react-dom';
export default {
title: 'Component/ImagePreviewModal',
@@ -52,6 +54,10 @@ export const Default = () => {
}}
>
<BlockSuiteEditor mode="page" page={page} onInit={initEmptyPage} />
{createPortal(
<ImagePreviewModal pageId={page.id} workspace={page.workspace} />,
document.body
)}
</div>
<BlockHubWrapper
style={{

View File

@@ -18,6 +18,9 @@
{
"path": "../../packages/workspace"
},
{
"path": "../../plugins/image-preview"
},
{
"path": "./tsconfig.node.json"
}

View File

@@ -56,13 +56,19 @@ yarn install
### Build Native Dependencies
Run the following script. It will build the native module at [`/packages/native`](/packages/native) and build Node.js binding using [NAPI.rs](https://napi.rs/).
This could take a while if you build it for the first time.
This could take a while if you build it for the first time.
Note: use `strip` from system instead of `binutils` if you are runinng MacOS. [see problem here](https://github.com/toeverything/AFFiNE/discussions/2840)
```
yarn workspace @affine/native build
```
### Build Plugins
```
yarn run build:plugins
```
## Debugging the Electron App
You need to run two scripts to run the app in development mode

View File

@@ -36,6 +36,8 @@
"{workspaceRoot}/packages/i18n/src/i18n-generated.ts"
],
"inputs": [
"{workspaceRoot}/infra/**/*",
"{workspaceRoot}/sdk/**/*",
{
"runtime": "node -v"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/monorepo",
"version": "0.7.0-canary.58",
"version": "0.8.0-canary.9",
"private": true,
"author": "toeverything",
"license": "MPL-2.0",
@@ -12,7 +12,8 @@
"tests/fixtures",
"tests/kit",
"tests/affine-legacy/*",
"tests/affine-local"
"tests/affine-local",
"tests/affine-plugin"
],
"engines": {
"node": ">=18.16.1 <19.0.0"
@@ -20,12 +21,11 @@
"scripts": {
"dev": "dev-core",
"dev:electron": "yarn workspace @affine/electron dev:app",
"dev:plugins": "./apps/electron/scripts/plugins/dev-plugins.mjs",
"build": "yarn nx build @affine/core",
"build:electron": "yarn nx build @affine/electron",
"build:storage": "yarn nx run-many -t build -p @affine/storage",
"build:infra": "yarn nx run-many -t build -p plugin-infra infra",
"build:plugins": "node ./scripts/build-plugins.mjs",
"build:infra": "yarn nx run-many -t build -p infra sdk",
"build:plugins": "yarn nx run-many -t build --projects=tag:plugin",
"build:storybook": "yarn nx build @affine/storybook",
"start:web-static": "yarn workspace @affine/core static-server",
"start:storybook": "yarn exec serve apps/storybook/storybook-static -l 6006",
@@ -58,6 +58,7 @@
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/cli": "workspace:*",
"@affine/plugin-cli": "workspace:*",
"@commitlint/cli": "^17.6.7",
"@commitlint/config-conventional": "^17.6.7",
"@faker-js/faker": "^8.0.2",
@@ -69,7 +70,6 @@
"@taplo/cli": "^0.5.2",
"@testing-library/react": "^14.0.0",
"@toeverything/infra": "workspace:*",
"@toeverything/plugin-infra": "workspace:*",
"@types/eslint": "^8.44.1",
"@types/node": "^18.17.1",
"@typescript-eslint/eslint-plugin": "^6.2.0",

View File

@@ -4,8 +4,7 @@
"private": true,
"bin": {
"build-core": "./src/bin/build-core.mjs",
"dev-core": "./src/bin/dev-core.mjs",
"dev-plugin": "./src/bin/dev-plugin.mjs"
"dev-core": "./src/bin/dev-core.mjs"
},
"exports": {
"./config": "./src/config/index.ts"
@@ -21,5 +20,5 @@
"peerDependencies": {
"ts-node": "*"
},
"version": "0.7.0-canary.58"
"version": "0.8.0-canary.9"
}

View File

@@ -1,203 +0,0 @@
import { ok } from 'node:assert';
import { existsSync } from 'node:fs';
import { mkdir, open, readFile } from 'node:fs/promises';
import path from 'node:path';
import { parseArgs } from 'node:util';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import react from '@vitejs/plugin-react-swc';
import { build } from 'vite';
import { z } from 'zod';
import { projectRoot } from '../config/index.js';
const args = process.argv.splice(2);
const result = parseArgs({
args,
options: {
watch: {
type: 'boolean',
default: false,
},
plugin: {
type: 'string',
},
},
});
const plugin = result.values.plugin;
if (typeof plugin !== 'string') {
throw new Error('plugin is required');
}
const isWatch = result.values.watch;
ok(typeof isWatch === 'boolean');
const packageJsonSchema = z.object({
name: z.string(),
affinePlugin: z.object({
release: z.boolean(),
entry: z.object({
core: z.string(),
server: z.string().optional(),
}),
}),
});
const external = [
// built-in packages
/^@affine/,
/^@blocksuite/,
/^@toeverything/,
// react
/^react/,
/^react-dom/,
// store
/^jotai/,
// css
/^@vanilla-extract/,
// remove this when bookmark plugin is ready
'link-preview-js',
];
const allPluginDir = path.resolve(projectRoot, 'plugins');
const getPluginDir = (plugin: string) => path.resolve(allPluginDir, plugin);
const pluginDir = getPluginDir(plugin);
const packageJsonFile = path.resolve(pluginDir, 'package.json');
const json: z.infer<typeof packageJsonSchema> = await readFile(
packageJsonFile,
{
encoding: 'utf-8',
}
)
.then(text => JSON.parse(text))
.then(async json => {
const { success } = await packageJsonSchema.safeParseAsync(json);
if (success) {
return json;
} else {
throw new Error('invalid package.json');
}
});
type Metadata = {
assets: Set<string>;
};
const metadata: Metadata = {
assets: new Set(),
};
const outDir = path.resolve(projectRoot, 'apps', 'core', 'public', 'plugins');
const coreOutDir = path.resolve(outDir, plugin);
const serverOutDir = path.resolve(
projectRoot,
'apps',
'electron',
'dist',
'plugins',
plugin
);
const pluginListJsonPath = path.resolve(outDir, 'plugin-list.json');
const coreEntry = path.resolve(pluginDir, json.affinePlugin.entry.core);
if (json.affinePlugin.entry.server) {
const serverEntry = path.resolve(pluginDir, json.affinePlugin.entry.server);
await build({
build: {
watch: isWatch ? {} : undefined,
minify: false,
outDir: serverOutDir,
emptyOutDir: true,
lib: {
entry: serverEntry,
fileName: 'index',
formats: ['cjs'],
},
rollupOptions: {
external,
},
},
});
}
await build({
build: {
watch: isWatch ? {} : undefined,
minify: false,
outDir: coreOutDir,
emptyOutDir: true,
lib: {
entry: coreEntry,
fileName: 'index',
formats: ['cjs'],
},
rollupOptions: {
output: {
assetFileNames: chunkInfo => {
if (chunkInfo.name) {
metadata.assets.add(chunkInfo.name);
return chunkInfo.name;
} else {
throw new Error('no name');
}
},
manualChunks: () => 'plugin',
},
external,
},
},
plugins: [
vanillaExtractPlugin(),
react(),
{
name: 'generate-list-json',
async generateBundle() {
if (!existsSync(outDir)) {
await mkdir(outDir, { recursive: true });
}
const file = await open(pluginListJsonPath, 'as+', 0o777);
const txt = await file.readFile({
encoding: 'utf-8',
});
if (!txt) {
console.log('generate plugin-list.json');
await file.write(
JSON.stringify([
{
release: json.affinePlugin.release,
name: plugin,
assets: [...metadata.assets],
},
])
);
} else {
console.log('modify plugin-list.json');
const list = JSON.parse(txt);
const index = list.findIndex((item: any) => item.name === plugin);
if (index === -1) {
list.push({
release: json.affinePlugin.release,
name: plugin,
assets: [...metadata.assets],
});
} else {
list[index].assets = [...metadata.assets];
}
await file.write(JSON.stringify(list), 0);
}
},
},
],
});

View File

@@ -3,8 +3,13 @@
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node16",
"moduleResolution": "Node",
"outDir": "lib"
},
"include": ["src/bin", "src"]
"include": ["src"],
"references": [
{
"path": "../infra"
}
]
}

View File

@@ -36,7 +36,6 @@
"@radix-ui/react-scroll-area": "^1.0.4",
"@radix-ui/react-toast": "^1.1.4",
"@toeverything/hooks": "workspace:*",
"@toeverything/plugin-infra": "workspace:*",
"@toeverything/theme": "^0.7.9",
"@vanilla-extract/dynamic": "^2.0.3",
"clsx": "^2.0.0",
@@ -52,12 +51,12 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
"@blocksuite/blocks": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/editor": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/global": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/icons": "^2.1.27",
"@blocksuite/lit": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/store": "0.0.0-20230720073515-bea92e0f-nightly",
"@blocksuite/blocks": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/editor": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/global": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/icons": "^2.1.29",
"@blocksuite/lit": "0.0.0-20230802200139-381599c0-nightly",
"@blocksuite/store": "0.0.0-20230802200139-381599c0-nightly",
"@types/react": "^18.2.17",
"@types/react-datepicker": "^4.15.0",
"@types/react-dnd": "^3.0.2",
@@ -67,5 +66,5 @@
"vite": "^4.4.7",
"yjs": "^13.6.7"
},
"version": "0.7.0-canary.58"
"version": "0.8.0-canary.9"
}

View File

@@ -1,5 +1,4 @@
import { style } from '@vanilla-extract/css';
export const root = style({
display: 'inline-flex',
alignItems: 'center',
@@ -35,18 +34,13 @@ export const root = style({
paddingLeft: '4px',
paddingRight: '12px',
},
'&[data-type="collection-list-item"][data-collapsible="false"]:hover': {
width: 'calc(100% + 8px)',
transform: 'translateX(-8px)',
paddingLeft: '20px',
paddingRight: '12px',
},
'&[data-type="favorite-list-item"][data-collapsible="false"]:hover': {
width: 'calc(100% + 8px)',
transform: 'translateX(-8px)',
paddingLeft: '20px',
paddingRight: '12px',
},
'&[data-type="collection-list-item"][data-collapsible="false"][data-active="true"],&[data-type="favorite-list-item"][data-collapsible="false"][data-active="true"], &[data-type="favorite-list-item"][data-collapsible="false"]:hover, &[data-type="collection-list-item"][data-collapsible="false"]:hover':
{
width: 'calc(100% + 8px)',
transform: 'translateX(-8px)',
paddingLeft: '20px',
paddingRight: '12px',
},
},
});

View File

@@ -8,8 +8,7 @@ import { Skeleton } from '@mui/material';
import { use } from 'foxact/use';
import { useAtomValue } from 'jotai';
import type { CSSProperties, ReactElement } from 'react';
import { lazy, memo, Suspense, useCallback, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { memo, Suspense, useCallback, useEffect, useRef } from 'react';
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
@@ -41,12 +40,6 @@ declare global {
var currentEditor: EditorContainer | undefined;
}
const ImagePreviewModal = lazy(() =>
import('../image-preview-modal').then(module => ({
default: module.ImagePreviewModal,
}))
);
const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
const { onLoad, page, mode, style } = props;
if (!page.loaded) {
@@ -191,17 +184,6 @@ export const BlockSuiteEditor = memo(function BlockSuiteEditor(
<Suspense fallback={<BlockSuiteFallback />}>
<BlockSuiteEditorImpl {...props} />
</Suspense>
{props.page && (
<Suspense fallback={null}>
{createPortal(
<ImagePreviewModal
workspace={props.page.workspace}
pageId={props.page.id}
/>,
document.body
)}
</Suspense>
)}
</ErrorBoundary>
);
});

View File

@@ -9,7 +9,7 @@ import {
LocalWorkspaceIcon as DefaultLocalWorkspaceIcon,
} from '@blocksuite/icons';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useStaticBlockSuiteWorkspace } from '@toeverything/plugin-infra/__internal__/react';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import type { FC } from 'react';
import { useCallback } from 'react';

View File

@@ -1,6 +1,6 @@
import { ProviderComposer } from '@affine/component/provider-composer';
import { ThemeProvider } from '@affine/component/theme-provider';
import { rootStore } from '@toeverything/plugin-infra/atom';
import { rootStore } from '@toeverything/infra/atom';
import { Provider } from 'jotai';
import type { PropsWithChildren } from 'react';
import { useMemo } from 'react';

View File

@@ -1,19 +0,0 @@
import { isBrowser } from '@affine/env/constant';
import type { EmbedBlockDoubleClickData } from '@blocksuite/blocks';
import { atom } from 'jotai';
export const previewBlockIdAtom = atom<string | null>(null);
export const hasAnimationPlayedAtom = atom<boolean | null>(true);
previewBlockIdAtom.onMount = set => {
if (isBrowser) {
const callback = (event: CustomEvent<EmbedBlockDoubleClickData>) => {
set(event.detail.blockId);
};
window.addEventListener('affine.embed-block-db-click', callback);
return () => {
window.removeEventListener('affine.embed-block-db-click', callback);
};
}
return;
};

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