Compare commits

...

55 Commits

Author SHA1 Message Date
dependabot[bot]
aa7e0dd85b chore: bump @vitest/ui from 0.34.5 to 0.34.6 (#4553)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 03:47:44 +00:00
liuyi
0092a19812 refactor(server): deprecate unstable redis manager (#4567) 2023-10-10 03:23:12 +00:00
dependabot[bot]
4a6cfedc4a chore: bump react-i18next from 13.2.1 to 13.2.2 (#4562)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 03:11:07 +00:00
dependabot[bot]
8c97fd1d28 chore: bump sinon from 16.0.0 to 16.1.0 (#4563)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-10 03:10:21 +00:00
Peng Xiao
d9fe3e73d5 fix: list page storybook not rendering issue (#4560) 2023-10-09 07:54:05 +00:00
dependabot[bot]
59a4b3bc31 chore: bump electron from 26.2.2 to 26.3.0 (#4564)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-09 07:14:50 +00:00
Zero King
0161c98d65 chore: reword template galleries introduction (#4548) 2023-10-06 16:11:08 +08:00
dependabot[bot]
d3ffa2c5f2 chore: bump marked from 7.0.5 to 9.0.3 (#4554)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 15:28:38 +00:00
dependabot[bot]
0a6859a1d7 chore: bump esbuild from 0.19.3 to 0.19.4 (#4550)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-03 15:28:10 +00:00
JimmFly
69db99636b feat(core): add editor commanads (#4514)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-10-02 03:22:12 +00:00
Alex Yang
aab1a1e50a fix(electron): output check (#4547) 2023-09-30 02:02:17 -05:00
Alex Yang
19646a97af fix: twitter preview (#4545) 2023-09-30 01:19:54 -05:00
Qinghao Huang
f59a35d8d2 fix: spacing issue in getting-started template (#4540) 2023-09-30 01:02:43 -05:00
Alex Yang
c911806062 refactor: remove bookmark plugin (#4544) 2023-09-30 00:48:33 -05:00
Qinghao Huang
b440c3a820 docs: update CLA.md (#4541) 2023-09-30 00:26:50 +00:00
Alex Yang
98cabc44e4 ci: remove unstable nx.yml (#4543) 2023-09-29 18:57:31 -05:00
LongYinan
dd94ea5b45 ci: speedup ci by reduce installation packages in certain job (#4457) 2023-09-29 03:02:26 +00:00
Joooye_34
b012e615ba fix(component): content should subtract height of the header (#4507) 2023-09-28 07:04:12 +00:00
Alex Yang
603f82ffc2 refactor: using unified nanoid (#4519) 2023-09-28 02:53:04 +00:00
Alex Yang
56f75160f3 refactor: remove unused packages (#4532) 2023-09-28 01:33:42 +00:00
dependabot[bot]
a860cf8e43 chore: bump electron from 26.1.0 to 26.2.1 (#4527)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-27 10:40:30 +00:00
liuyi
504dda2092 fix(core): setting ui regression (#4525) 2023-09-27 09:56:01 +00:00
JimmFly
1df8b6edfb feat(component): add private copy link button (#4508) 2023-09-27 04:54:02 +00:00
Alex Yang
ddfa5d394d chore: bump version (#4518) 2023-09-27 02:02:54 +00:00
Alex Yang
a69820e4ca fix: type in pluginImportsFunctionMap (#4517) 2023-09-27 01:53:01 +00:00
Peng Xiao
369db3fea5 fix(component): cmdk flaky (#4512) 2023-09-27 01:37:00 +00:00
liuyi
4a03fa65d1 fix(server): wrong member count query (#4506) 2023-09-26 15:36:08 +00:00
Alex Yang
a633fb6dea fix: current page atom (#4515) 2023-09-26 14:53:01 +00:00
JimmFly
1b6cd70247 chore(core): temporarily remove set-syncing-mode (#4489) 2023-09-26 14:11:04 +00:00
Peng Xiao
29fa237dfb fix: storybook previews (#4504) 2023-09-26 05:51:39 +00:00
Alex Yang
61044d91a8 fix(core): page update date (#4502) 2023-09-26 04:09:52 +00:00
Peng Xiao
eb728f7ef2 fix: give content match a lower score (#4499) 2023-09-26 03:15:40 +00:00
JimmFly
1bdc402b7b fix: adjust 404 page style (#4491) 2023-09-26 02:51:58 +00:00
Alex Yang
127a84b4e1 refactor(plugin-cli): use @plugxjs/vite-plugin (#4501) 2023-09-26 02:49:23 +00:00
JimmFly
2e1acec3c0 fix: unexpected pop ups (#4468) 2023-09-26 02:41:45 +00:00
dependabot[bot]
672a01b385 chore: bump sinon from 15.2.0 to 16.0.0 (#4480)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 20:17:25 +00:00
Joooye_34
ad63c5a525 fix(component): background animation is different (#4495) 2023-09-25 16:50:39 +00:00
Alex Yang
3a79346ce0 test: workspace provider (#4497) 2023-09-25 16:49:23 +00:00
dependabot[bot]
bf729df7fe chore: bump vite-tsconfig-paths from 4.2.0 to 4.2.1 (#4481)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 16:05:09 +00:00
dependabot[bot]
b785840d91 chore: bump marked-gfm-heading-id from 3.0.6 to 3.1.0 (#4479)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-25 16:04:47 +00:00
Peng Xiao
e8410b948d chore(component): bump themes (#4484) 2023-09-25 12:03:28 +00:00
Peng Xiao
3f09ba92bc fix: cmdk scrollbar gutter (#4488) 2023-09-25 12:03:02 +00:00
JimmFly
35dc6d6687 fix: unexpected hover behavior of collection sidebar (#4490) 2023-09-25 10:22:59 +00:00
JimmFly
5b4ce75e13 feat: add commands (#4477) 2023-09-25 10:10:53 +00:00
Peng Xiao
dc6b66c32f fix: register command re-rendering (#4476) 2023-09-25 02:40:53 +00:00
LongYinan
5f7f5b74ca fix(core): error state for non early access user while signing in with email (#4467) 2023-09-23 00:00:09 -07:00
LongYinan
7b5157aa89 fix(server): missing dependency in sync app (#4465) 2023-09-22 21:32:45 +00:00
Alex Yang
bd0ed7f474 test: fix flaky (#4463) 2023-09-22 20:18:41 +00:00
Alex Yang
2da6702991 refactor(infra): simplify currentWorkspaceAtom (#4462) 2023-09-22 20:07:26 +00:00
Alex Yang
56d8fa5d29 docs: upload LICENSE.md 2023-09-22 14:38:19 -05:00
Alex Yang
4e5e48ce9f docs: update README.md
There are no core members actually, people is just a paid guy.
2023-09-22 14:31:15 -05:00
Peng Xiao
e0063ebc9b feat: new CMD-K (#4408) 2023-09-22 14:31:26 +00:00
Peng Xiao
27e4599c94 chore: bump components version (#4454) 2023-09-22 08:56:10 +00:00
JimmFly
edd7d00104 refactor: workspace list (#4432) 2023-09-22 15:02:31 +08:00
Alex Yang
092e2e0a3d fix(electron): missing video (#4451) 2023-09-22 05:56:43 +00:00
289 changed files with 8770 additions and 6863 deletions

1
.github/CLA.md vendored
View File

@@ -61,3 +61,4 @@ Example:
- Shishu, @shishudesu, 2023/05/19
- Kushagra Singh, @kush002, 2023/06/28
- Sarvesh Kumar, @sarvesh521 2023/08/25
- 微扰理论 Qinghao Huang, @wfnuser 2023/09/29

View File

@@ -34,7 +34,7 @@ runs:
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
shell: bash
run: |
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }}
env:
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
@@ -48,7 +48,7 @@ runs:
export CC=x86_64-unknown-linux-gnu-gcc
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc
export RUSTFLAGS="-C debuginfo=1"
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }}
chmod -R 777 node_modules/.cache
chmod -R 777 target
@@ -60,6 +60,6 @@ runs:
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
run: |
export RUSTFLAGS="-C debuginfo=1"
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} --target ${{ inputs.target }}
chmod -R 777 node_modules/.cache
chmod -R 777 target

View File

@@ -7,8 +7,7 @@ runs:
- name: 'Install @electron-forge/maker-dmg'
if: runner.os == 'macos'
shell: bash
working-directory: ./apps/electron
run: yarn add @electron-forge/maker-dmg --dev
run: yarn workspace @affine/electron add @electron-forge/maker-dmg --dev
env:
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'

View File

@@ -21,6 +21,14 @@ inputs:
description: 'set nmMode to hardlinks-local in .yarnrc.yml'
required: false
default: 'true'
build-infra:
description: 'Build infra'
required: false
default: 'true'
build-plugins:
description: 'Build plugins'
required: false
default: 'true'
runs:
using: 'composite'
@@ -42,7 +50,7 @@ runs:
if: ${{ inputs.package-install == 'true' }}
continue-on-error: true
shell: bash
run: yarn install ${{ inputs.extra-flags }}
run: yarn ${{ inputs.extra-flags }}
env:
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
@@ -52,7 +60,7 @@ runs:
- name: yarn install (try again)
if: ${{ steps.install.outcome == 'failure' }}
shell: bash
run: yarn install ${{ inputs.extra-flags }}
run: yarn ${{ inputs.extra-flags }}
env:
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
@@ -89,11 +97,11 @@ runs:
${{ runner.os }}-${{ runner.arch }}-playwright-
# If the Playwright browser binaries weren't able to be restored, we tell
# paywright to install everything for us.
# playwright to install everything for us.
- name: Install Playwright's dependencies
shell: bash
if: inputs.playwright-install == 'true' && steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps
if: inputs.playwright-install == 'true'
run: yarn playwright install --with-deps chromium
- name: Get installed Electron version
id: electron-version
@@ -114,14 +122,16 @@ runs:
- name: Install Electron binary
shell: bash
if: inputs.electron-install == 'true'
run: node apps/electron/node_modules/electron/install.js
run: node ./node_modules/electron/install.js
env:
ELECTRON_OVERRIDE_DIST_PATH: ./node_modules/.cache/electron
- name: Build Infra
shell: bash
if: inputs.build-infra == 'true'
run: yarn run build:infra
- name: Build Plugins
if: inputs.build-plugins == 'true'
shell: bash
run: yarn run build:plugins

View File

@@ -41,11 +41,11 @@ jobs:
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
run: yarn run build:plugins
with:
electron-install: false
- name: Build Core
run: yarn nx build @affine/core
- name: Upload core artifact
@@ -94,11 +94,12 @@ jobs:
}
needs: build-core
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop
playwright-install: true
hard-link-nm: false
@@ -111,8 +112,7 @@ jobs:
- name: Run unit tests
if: ${{ matrix.spec.test }}
shell: bash
run: yarn vitest
working-directory: ./apps/electron
run: yarn workspace @affine/electron vitest
- name: Download core artifact
uses: actions/download-artifact@v3
@@ -120,9 +120,6 @@ jobs:
name: core
path: apps/electron/resources/web-static
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
@@ -142,13 +139,13 @@ jobs:
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
env:
SKIP_BUNDLE: true
SKIP_WEB_BUILD: true
run: yarn workspace @affine/electron make --platform=darwin --arch=arm64
- name: Output check
if: ${{ matrix.spec.os == 'macos-latest' && matrix.spec.arch == 'arm64' }}
run: |
yarn ts-node-esm ./scripts/macos-arm64-output-check.mts
working-directory: apps/electron
yarn workspace @affine/electron ts-node-esm ./scripts/macos-arm64-output-check.mts
- name: Collect code coverage report
if: ${{ matrix.spec.test }}

View File

@@ -42,10 +42,15 @@ jobs:
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
with:
extra-flags: workspaces focus @affine/storage
electron-install: false
build-infra: false
build-plugins: false
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: 'x86_64-unknown-linux-gnu'
@@ -81,10 +86,12 @@ jobs:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- name: Initialize database
run: |
@@ -150,7 +157,7 @@ jobs:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -232,7 +239,7 @@ jobs:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
@@ -257,15 +264,13 @@ jobs:
- name: Generate prisma client
run: |
yarn exec prisma generate
yarn exec prisma db push
working-directory: apps/server
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server prisma db push
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run init-db script
run: yarn exec ts-node-esm ./scripts/init-db.ts
working-directory: apps/server
run: yarn workspace @affine/server exec ts-node-esm ./scripts/init-db.ts
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine

View File

@@ -42,7 +42,7 @@ jobs:
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -66,52 +66,18 @@ jobs:
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Run check
run: |
yarn set version $(node -e "console.log(require('./package.json').packageManager.split('@')[1])")
git diff --exit-code
build-prototype:
name: Build Prototype
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 Prototype
run: yarn nx build prototype
- name: Upload prototype artifact
uses: actions/upload-artifact@v3
with:
name: prototype
path: ./apps/prototype/dist
if-no-files-found: error
build-docs:
name: Build Docs
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
- run: yarn nx build @affine/docs
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
e2e-plugin-test:
name: E2E Plugin Test
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -142,49 +108,6 @@ jobs:
path: ./test-results
if-no-files-found: ignore
e2e-prototype-test:
name: E2E Prototype Test
runs-on: ubuntu-latest
environment: development
needs: build-prototype
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
- name: Download prototype artifact
uses: actions/download-artifact@v3
with:
name: prototype
path: ./apps/prototype/dist
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: tests/affine-prototype
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: e2etest-prototype
# name: affine
# fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-prototype
path: ./test-results
if-no-files-found: ignore
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
@@ -194,7 +117,7 @@ jobs:
shard: [1, 2, 3, 4, 5]
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -236,9 +159,9 @@ jobs:
spec:
- { package: 0.7.0-canary.18 }
- { package: 0.8.0-canary.7 }
- { package: 0.8.3 }
- { package: 0.8.4 }
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -246,12 +169,10 @@ jobs:
electron-install: false
- name: Unzip
run: yarn unzip
working-directory: ./tests/affine-legacy/${{ matrix.spec.package }}
run: yarn workspace @affine-legacy/${{ matrix.spec.package }} unzip
- name: Run playwright tests
run: yarn e2e --forbid-only
working-directory: ./tests/affine-legacy/${{ matrix.spec.package }}
run: yarn workspace @affine-legacy/${{ matrix.spec.package }} e2e --forbid-only
- name: Upload test results
if: ${{ failure() }}
@@ -266,7 +187,7 @@ jobs:
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:

View File

@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Cleanup
run: |

View File

@@ -37,7 +37,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -43,7 +43,7 @@ jobs:
environment: production
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
@@ -70,7 +70,7 @@ jobs:
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
@@ -95,7 +95,7 @@ jobs:
- build-core
- build-storage
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Download core artifact
uses: actions/download-artifact@v3
with:
@@ -180,7 +180,7 @@ jobs:
- build-docker
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Deploy to dev
uses: ./.github/actions/deploy
with:

View File

@@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout Helm chart repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
repository: toeverything/helm-charts
path: .helm-chart-repo

View File

@@ -19,7 +19,7 @@ jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Check Language Key

View File

@@ -36,7 +36,7 @@ jobs:
outputs:
version: 0.0.0-internal.${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: toeverything/set-build-version@latest
- id: version
run: echo ::set-output name=version::${{ env.BUILD_VERSION }}
@@ -47,7 +47,7 @@ jobs:
needs:
- set-build-version
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
@@ -103,7 +103,7 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
@@ -174,7 +174,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:

View File

@@ -1,54 +0,0 @@
name: NX
on:
push:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/nx.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
pull_request:
merge_group:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
- v[0-9]+.[0-9]+.x
paths-ignore:
- README.md
- .github/**
- '!.github/workflows/nx.yml'
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
jobs:
main:
name: Nx Cloud - Main Job
uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.13.0
with:
runs-on: macos-latest
main-branch-name: master
number-of-agents: 5
init-commands: |
yarn exec nx-cloud start-ci-run --stop-agents-after="build" --agent-count=5
environment-variables: |
BUILD_TYPE=canary
# parallel-commands: |
# yarn exec nx-cloud record -- yarn exec nx format:check
parallel-commands-on-agents: |
yarn exec nx affected --target=build --parallel=5
timeout: 60
agents:
name: Nx Cloud - Agents
uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.13.0
with:
runs-on: macos-latest
number-of-agents: 5
environment-variables: |
BUILD_TYPE=canary
timeout: 60

View File

@@ -17,7 +17,9 @@ jobs:
name: Check pull request title
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- run: echo "${{ github.event.pull_request.title }}" | npx commitlint -g ./.commitlintrc.json
with:
electron-install: false
- run: echo "${{ github.event.pull_request.title }}" | yarn dlx commitlint -g ./.commitlintrc.json

View File

@@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
# This is required to fetch all commits for chromatic

View File

@@ -44,7 +44,7 @@ jobs:
outputs:
RELEASE_VERSION: ${{ steps.get-canary-version.outputs.RELEASE_VERSION }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
@@ -101,7 +101,7 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
@@ -119,9 +119,6 @@ jobs:
name: core
path: apps/electron/resources/web-static
- name: Build Plugins
run: yarn run build:plugins
- name: Build Desktop Layers
run: yarn workspace @affine/electron build
@@ -172,7 +169,7 @@ jobs:
env:
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
@@ -240,7 +237,7 @@ jobs:
outputs:
FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
timeout-minutes: 10
uses: ./.github/actions/setup-node
@@ -318,7 +315,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
name: core
path: web-static
- name: Zip web-static
run: zip -r web-static.zip web-static
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:

View File

@@ -17,7 +17,7 @@ jobs:
name: Try publishing npm@latest release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Try publishing to NPM
@@ -31,7 +31,7 @@ jobs:
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Plugins
@@ -50,7 +50,7 @@ jobs:
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -72,7 +72,7 @@ jobs:
environment: development
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup Rust
@@ -97,7 +97,7 @@ jobs:
- build-core
- build-storage
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Download core artifact
uses: actions/download-artifact@v3
with:

View File

@@ -0,0 +1,15 @@
diff --git a/package.json b/package.json
index b64804360e64896c2eafde5e976dde0db480b4e3..63566d9e2725630827daefeeb211b950d329521b 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,10 @@
"types": "./index.d.ts",
"default": "./index.js"
},
+ "./core": {
+ "types": "./core/index.d.ts",
+ "default": "./core/index.js"
+ },
"./adapters": {
"types": "./adapters.d.ts"
},

View File

@@ -125,7 +125,6 @@ If you have questions, you are welcome to contact us. One of the best places to
| Official Plugin | Description | Status |
| ----------------------------------------------------- | ----------------------------------------- | ------ |
| [@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 | ✅ |
| [@affine/outline](plugins/outline) | Outline for your document | ✅ |
@@ -147,18 +146,7 @@ We would also like to give thanks to open-source projects that make AFFiNE possi
Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone.
# Contributors
## Current Core members
Team members who are currently maintaining the project:
- [JimmFly](https://github.com/JimmFly) - Jinfei Yang <yangjinfei001@gmail.com> (he/him)
- [pengx17](https://github.com/pengx17) - Peng Xiao <pengxiao@outlook.com> (he/him)
- [QiShaoXuan](https://github.com/QiSHaoXuan) - Shaoxuan Qi <qishaoxuan777@gmail.com> (he/him)
- [himself65](https://github.com/himself65) - Zeyu "Alex" Yang <himself65@outlook.com> (he/him)
## All Contributors
## Contributors
We would like to express our gratitude to all the individuals who have already contributed to AFFiNE! If you have any AFFiNE-related project, documentation, tool or template, please feel free to contribute it by submitting a pull request to our curated list on GitHub: [awesome-affine](https://github.com/toeverything/awesome-affine).

View File

@@ -266,7 +266,6 @@ export const createConfiguration: (
useDefineForClassFields: false,
},
experimental: {
keepImportAssertions: true,
plugins: [
buildFlags.coverage && [
'swc-plugin-coverage-instrument',
@@ -291,7 +290,7 @@ export const createConfiguration: (
exclude: [/node_modules/],
},
{
test: /\.(png|jpg|gif|svg|webp)$/,
test: /\.(png|jpg|gif|svg|webp|mp4)$/,
type: 'asset/resource',
},
{

View File

@@ -12,7 +12,8 @@
"./app": "./src/app.tsx",
"./router": "./src/router.ts",
"./bootstrap/setup": "./src/bootstrap/setup.ts",
"./bootstrap/register-plugins": "./src/bootstrap/register-plugins.ts"
"./bootstrap/register-plugins": "./src/bootstrap/register-plugins.ts",
"./components/pure/*": "./src/components/pure/*/index.tsx"
},
"dependencies": {
"@affine-test/fixtures": "workspace:*",
@@ -23,13 +24,13 @@
"@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/blocks": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/editor": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/global": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/block-std": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/blocks": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/editor": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/global": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/icons": "^2.1.33",
"@blocksuite/lit": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/store": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/lit": "0.0.0-20230926212737-6d4b1569-nightly",
"@blocksuite/store": "0.0.0-20230926212737-6d4b1569-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",
@@ -39,19 +40,18 @@
"@mui/material": "^5.14.7",
"@radix-ui/react-select": "^1.2.2",
"@react-hookz/web": "^23.1.0",
"@toeverything/components": "^0.0.42",
"@toeverything/components": "^0.0.43",
"async-call-rpc": "^6.3.1",
"cmdk": "^0.2.0",
"css-spring": "^4.1.0",
"cssnano": "^6.0.1",
"graphql": "^16.8.0",
"graphql": "^16.8.1",
"intl-segmenter-polyfill-rs": "^0.1.6",
"jotai": "^2.4.1",
"jotai": "^2.4.2",
"jotai-devtools": "^0.6.2",
"lit": "^2.8.0",
"lottie-web": "^5.12.2",
"mini-css-extract-plugin": "^2.7.6",
"next-auth": "^4.22.1",
"next-auth": "^4.23.1",
"next-themes": "^0.2.1",
"postcss-loader": "^7.3.3",
"react": "18.2.0",
@@ -63,17 +63,17 @@
"ses": "^0.18.8",
"swr": "2.2.0",
"valtio": "^1.11.2",
"y-protocols": "^1.0.5",
"y-protocols": "^1.0.6",
"yjs": "^13.6.8",
"zod": "^3.22.2"
},
"devDependencies": {
"@aws-sdk/client-s3": "3.400.0",
"@aws-sdk/client-s3": "3.418.0",
"@perfsee/webpack": "^1.8.4",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@sentry/webpack-plugin": "^2.7.0",
"@sentry/webpack-plugin": "^2.7.1",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.3.81",
"@swc/core": "^1.3.87",
"@types/lodash-es": "^4.17.9",
"@types/webpack-env": "^1.18.1",
"copy-webpack-plugin": "^11.0.0",

View File

@@ -20,11 +20,11 @@ import {
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { nanoid } from '@blocksuite/store';
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
import { getCurrentStore } from '@toeverything/infra/atom';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite';
import { nanoid } from 'nanoid';
import { useCallback } from 'react';
import { setPageModeAtom } from '../../atoms';

View File

@@ -9,7 +9,7 @@ import { describe, expect, test } from 'vitest';
import {
pageSettingFamily,
pageSettingsAtom,
recentPageSettingsAtom,
recentPageIdsBaseAtom,
} from '../index';
describe('page mode atom', () => {
@@ -26,20 +26,12 @@ describe('page mode atom', () => {
},
});
expect(store.get(recentPageSettingsAtom)).toEqual([
{
id: 'page0',
mode: 'page',
},
]);
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page0']);
const page1SettingAtom = pageSettingFamily('page1');
store.set(page1SettingAtom, {
mode: 'edgeless',
});
expect(store.get(recentPageSettingsAtom)).toEqual([
{ id: 'page1', mode: 'edgeless' },
{ id: 'page0', mode: 'page' },
]);
expect(store.get(recentPageIdsBaseAtom)).toEqual(['page1', 'page0']);
});
});

View File

@@ -43,10 +43,6 @@ type PageLocalSetting = {
mode: PageMode;
};
type PartialPageLocalSettingWithPageId = Partial<PageLocalSetting> & {
id: string;
};
const pageSettingsBaseAtom = atomWithStorage(
'pageSettings',
{} as Record<string, PageLocalSetting>
@@ -55,22 +51,11 @@ const pageSettingsBaseAtom = atomWithStorage(
// readonly atom by design
export const pageSettingsAtom = atom(get => get(pageSettingsBaseAtom));
const recentPageSettingsBaseAtom = atomWithStorage<string[]>(
export const recentPageIdsBaseAtom = atomWithStorage<string[]>(
'recentPageSettings',
[]
);
export const recentPageSettingsAtom = atom<PartialPageLocalSettingWithPageId[]>(
get => {
const recentPageIDs = get(recentPageSettingsBaseAtom);
const pageSettings = get(pageSettingsAtom);
return recentPageIDs.map(id => ({
...pageSettings[id],
id,
}));
}
);
const defaultPageSetting = {
mode: 'page',
} satisfies PageLocalSetting;
@@ -85,7 +70,9 @@ export const pageSettingFamily: AtomFamily<
...defaultPageSetting,
},
(get, set, patch) => {
set(recentPageSettingsBaseAtom, ids => {
// fixme: this does not work when page reload,
// since atomWithStorage is async
set(recentPageIdsBaseAtom, ids => {
// pick 3 recent page ids
return [...new Set([pageId, ...ids]).values()].slice(0, 3);
});
@@ -113,3 +100,5 @@ export const setPageModeAtom = atom(
export type PageModeOption = 'all' | 'page' | 'edgeless';
export const allPageModeSelectAtom = atom<PageModeOption>('all');
export const openWorkspaceListModalAtom = atom(false);

View File

@@ -0,0 +1,13 @@
import { atom } from 'jotai';
export type TrashModal = {
open: boolean;
pageId: string;
pageTitle: string;
};
export const trashModalAtom = atom<TrashModal>({
open: false,
pageId: '',
pageTitle: '',
});

View File

@@ -16,7 +16,7 @@ import {
} from '@toeverything/infra/__internal__/plugin';
import {
contentLayoutAtom,
currentPageAtom,
currentPageIdAtom,
currentWorkspaceAtom,
} from '@toeverything/infra/atom';
import { atom } from 'jotai';
@@ -129,7 +129,7 @@ export function createSetup(rootStore: ReturnType<typeof createStore>) {
function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
// clean up plugin windows when switching to other pages
rootStore.sub(currentPageAtom, () => {
rootStore.sub(currentPageIdAtom, () => {
rootStore.set(contentLayoutAtom, 'editor');
});
@@ -149,7 +149,7 @@ function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
'@affine/sdk/entry': {
rootStore,
currentWorkspaceAtom: currentWorkspaceAtom,
currentPageAtom: currentPageAtom,
currentPageIdAtom: currentPageIdAtom,
pushLayoutAtom: pushLayoutAtom,
deleteLayoutAtom: deleteLayoutAtom,
},
@@ -169,7 +169,10 @@ function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
Map<string, Map<string, any>>
>();
const pluginImportsFunctionMap = new Map<string, (imports: any) => void>();
const pluginImportsFunctionMap = new Map<
string,
(newUpdaters: [string, [string, ((val: any) => void)[]][]][]) => void
>();
const createImports = (pluginName: string) => {
if (pluginImportsFunctionMap.has(pluginName)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion

View File

@@ -12,7 +12,6 @@ import {
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { assertExists } from '@blocksuite/global/utils';
import { nanoid } from '@blocksuite/store';
import {
migrateLocalBlobStorage,
migrateWorkspace,
@@ -20,6 +19,7 @@ import {
} from '@toeverything/infra/blocksuite';
import { downloadBinary, overwriteBinary } from '@toeverything/y-indexeddb';
import type { createStore } from 'jotai/vanilla';
import { nanoid } from 'nanoid';
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { WorkspaceAdapters } from '../adapters/workspace';

View File

@@ -0,0 +1,78 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ImportIcon, PlusIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai';
import { openCreateWorkspaceModalAtom } from '../atoms';
import type { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
export function registerAffineCreationCommands({
store,
pageHelper,
t,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
pageHelper: ReturnType<typeof usePageHelper>;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:new-page',
category: 'affine:creation',
label: t['com.affine.cmdk.affine.new-page'],
icon: <PlusIcon />,
keyBinding: environment.isDesktop
? {
binding: '$mod+N',
skipRegister: true,
}
: undefined,
run() {
pageHelper.createPage();
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:new-edgeless-page',
category: 'affine:creation',
icon: <PlusIcon />,
label: t['com.affine.cmdk.affine.new-edgeless-page'],
run() {
pageHelper.createEdgeless();
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:new-workspace',
category: 'affine:creation',
icon: <PlusIcon />,
label: t['com.affine.cmdk.affine.new-workspace'],
run() {
store.set(openCreateWorkspaceModalAtom, 'new');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:import-workspace',
category: 'affine:creation',
icon: <ImportIcon />,
label: t['com.affine.cmdk.affine.import-workspace'],
preconditionStrategy: () => {
return environment.isDesktop;
},
run() {
store.set(openCreateWorkspaceModalAtom, 'add');
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,40 @@
import { appSidebarOpenAtom } from '@affine/component/app-sidebar';
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SidebarIcon } from '@blocksuite/icons';
import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai';
export function registerAffineLayoutCommands({
t,
store,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:toggle-left-sidebar',
category: 'affine:layout',
icon: <SidebarIcon />,
label: () => {
const open = store.get(appSidebarOpenAtom);
return t[
open
? 'com.affine.cmdk.affine.left-sidebar.collapse'
: 'com.affine.cmdk.affine.left-sidebar.expand'
]();
},
keyBinding: {
binding: '$mod+/',
},
run() {
store.set(appSidebarOpenAtom, v => !v);
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,120 @@
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { registerAffineCommand } from '@toeverything/infra/command';
import type { createStore } from 'jotai';
import {
openSettingModalAtom,
openWorkspaceListModalAtom,
type PageModeOption,
} from '../atoms';
import type { useNavigateHelper } from '../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../shared';
export function registerAffineNavigationCommands({
t,
store,
workspace,
navigationHelper,
pageListMode,
setPageListMode,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
navigationHelper: ReturnType<typeof useNavigateHelper>;
pageListMode: PageModeOption;
setPageListMode: React.Dispatch<React.SetStateAction<PageModeOption>>;
workspace: Workspace;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:goto-all-pages',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
label: () => t['com.affine.cmdk.affine.navigation.goto-all-pages'](),
run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
setPageListMode('all');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:goto-page-list',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
preconditionStrategy: () => {
return pageListMode !== 'page';
},
label: () => t['com.affine.cmdk.affine.navigation.goto-page-list'](),
run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
setPageListMode('page');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:goto-edgeless-list',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
preconditionStrategy: () => {
return pageListMode !== 'edgeless';
},
label: () => t['com.affine.cmdk.affine.navigation.goto-edgeless-list'](),
run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
setPageListMode('edgeless');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:goto-workspace',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
label: () => t['com.affine.cmdk.affine.navigation.goto-workspace'](),
run() {
store.set(openWorkspaceListModalAtom, true);
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:open-settings',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
label: () => t['com.affine.cmdk.affine.navigation.open-settings'](),
run() {
store.set(openSettingModalAtom, {
activeTab: 'appearance',
workspaceId: null,
open: true,
});
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:goto-trash',
category: 'affine:navigation',
icon: <ArrowRightBigIcon />,
label: () => t['com.affine.cmdk.affine.navigation.goto-trash'](),
run() {
navigationHelper.jumpToSubPath(workspace.id, WorkspaceSubPath.TRASH);
setPageListMode('all');
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,100 @@
import { Trans } from '@affine/i18n';
import type { useAFFiNEI18N } from '@affine/i18n/hooks';
import { SettingsIcon } from '@blocksuite/icons';
import {
PreconditionStrategy,
registerAffineCommand,
} from '@toeverything/infra/command';
import type { createStore } from 'jotai';
import type { useTheme } from 'next-themes';
import { openQuickSearchModalAtom } from '../atoms';
export function registerAffineSettingsCommands({
store,
theme,
}: {
t: ReturnType<typeof useAFFiNEI18N>;
store: ReturnType<typeof createStore>;
theme: ReturnType<typeof useTheme>;
}) {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineCommand({
id: 'affine:show-quick-search',
preconditionStrategy: PreconditionStrategy.Never,
category: 'affine:general',
keyBinding: {
binding: '$mod+K',
},
icon: <SettingsIcon />,
run() {
store.set(openQuickSearchModalAtom, true);
},
})
);
// color schemes
unsubs.push(
registerAffineCommand({
id: 'affine:change-color-scheme-to-auto',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.color-scheme.to"
values={{ colour: 'Auto' }}
>
Change Colour Scheme to <strong>colour</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => theme.theme !== 'system',
run() {
theme.setTheme('system');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:change-color-scheme-to-dark',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.color-scheme.to"
values={{ colour: 'Dark' }}
>
Change Colour Scheme to <strong>colour</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => theme.theme !== 'dark',
run() {
theme.setTheme('dark');
},
})
);
unsubs.push(
registerAffineCommand({
id: 'affine:change-color-scheme-to-light',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.color-scheme.to"
values={{ colour: 'Light' }}
>
Change Colour Scheme to <strong>colour</strong>
</Trans>
),
category: 'affine:settings',
icon: <SettingsIcon />,
preconditionStrategy: () => theme.theme !== 'light',
run() {
theme.setTheme('light');
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}

View File

@@ -0,0 +1,3 @@
export * from './affine-creation';
export * from './affine-layout';
export * from './affine-settings';

View File

@@ -3,12 +3,13 @@ import {
CountDownRender,
ModalHeader,
} from '@affine/component/auth-components';
import { getUserQuery } from '@affine/graphql';
import { type GetUserQuery, getUserQuery } from '@affine/graphql';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { GraphQLError } from 'graphql';
import { type FC, useState } from 'react';
import { useCallback } from 'react';
@@ -56,7 +57,25 @@ export const SignIn: FC<AuthPanelProps> = ({
}
setIsValidEmail(true);
const { user } = await verifyUser({ email });
// 0 for no access for internal beta
let user: GetUserQuery['user'] | null | 0 = null;
await verifyUser({ email })
.then(({ user: u }) => {
user = u;
})
.catch(err => {
const e = err?.[0];
if (e instanceof GraphQLError && e.extensions?.code === 402) {
setAuthState('noAccess');
user = 0;
} else {
throw err;
}
});
if (user === 0) {
return;
}
setAuthEmail(email);
if (user) {

View File

@@ -269,7 +269,8 @@ export const CreateWorkspaceModal = ({
const result: LoadDBFileResult = await window.apis.dialog.loadDBFile();
if (result.workspaceId && !canceled) {
setAddedId(result.workspaceId);
setStep('set-syncing-mode');
const newWorkspaceId = await addLocalWorkspace(result.workspaceId);
onCreate(newWorkspaceId);
} else if (result.error || result.canceled) {
if (result.error) {
toast(t[result.error]());
@@ -287,7 +288,7 @@ export const CreateWorkspaceModal = ({
return () => {
canceled = true;
};
}, [mode, onClose, t]);
}, [addLocalWorkspace, mode, onClose, onCreate, t]);
const onConfirmEnableCloudSyncing = useCallback(
(enableCloudSyncing: boolean) => {
@@ -332,19 +333,11 @@ export const CreateWorkspaceModal = ({
const onConfirmName = useCallback(
(name: string) => {
setWorkspaceName(name);
if (environment.isDesktop && runtimeConfig.enableSQLiteProvider) {
setStep('set-syncing-mode');
} else {
// this will be the last step for web for now
// fix me later
createLocalWorkspace(name)
.then(id => {
onCreate(id);
})
.catch(err => {
logger.error(err);
});
}
// this will be the last step for web for now
// fix me later
createLocalWorkspace(name).then(id => {
onCreate(id);
});
},
[createLocalWorkspace, onCreate]
);

View File

@@ -21,7 +21,14 @@ import { Tooltip } from '@toeverything/components/tooltip';
import clsx from 'clsx';
import { useSetAtom } from 'jotai';
import type { ReactElement } from 'react';
import { Suspense, useCallback, useMemo, useState } from 'react';
import {
Suspense,
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import type { CheckedUser } from '../../../hooks/affine/use-current-user';
@@ -96,6 +103,20 @@ export const CloudWorkspaceMembersPanel = ({
[invite, pushNotification, t]
);
const listContainerRef = useRef<HTMLDivElement | null>(null);
const [memberListHeight, setMemberListHeight] = useState<number | null>(null);
useLayoutEffect(() => {
if (
memberCount > COUNT_PER_PAGE &&
listContainerRef.current &&
memberListHeight === null
) {
const rect = listContainerRef.current.getBoundingClientRect();
setMemberListHeight(rect.height);
}
}, [listContainerRef, memberCount, memberListHeight]);
const onRevoke = useCallback<OnRevoke>(
async memberId => {
const res = await revokeMemberPermission(memberId);
@@ -129,7 +150,11 @@ export const CloudWorkspaceMembersPanel = ({
) : null}
</SettingRow>
<div className={style.membersPanel}>
<div
className={style.membersPanel}
ref={listContainerRef}
style={memberListHeight ? { height: memberListHeight } : {}}
>
<Suspense fallback={<MemberListFallback memberCount={memberCount} />}>
<MemberList
workspaceId={workspaceId}
@@ -139,11 +164,13 @@ export const CloudWorkspaceMembersPanel = ({
/>
</Suspense>
<Pagination
totalCount={memberCount}
countPerPage={COUNT_PER_PAGE}
onPageChange={onPageChange}
/>
{memberCount > COUNT_PER_PAGE && (
<Pagination
totalCount={memberCount}
countPerPage={COUNT_PER_PAGE}
onPageChange={onPageChange}
/>
)}
</div>
</>
);
@@ -186,7 +213,7 @@ const MemberList = ({
const currentUser = useCurrentUser();
return (
<>
<div className={style.memberList}>
{members.map(member => (
<MemberItem
key={member.id}
@@ -196,7 +223,7 @@ const MemberList = ({
onRevoke={onRevoke}
/>
))}
</>
</div>
);
};
@@ -225,54 +252,56 @@ const MemberItem = ({
}, [currentUser.id, isOwner, member.id, t]);
return (
<>
<div key={member.id} className={style.listItem} data-testid="member-item">
<Avatar
size={36}
url={member.avatarUrl}
name={(member.emailVerified ? member.name : member.email) as string}
/>
<div className={style.memberContainer}>
{member.emailVerified ? (
<>
<div className={style.memberName}>{member.name}</div>
<div className={style.memberEmail}>{member.email}</div>
</>
) : (
<div className={style.memberName}>{member.email}</div>
)}
</div>
<div
className={clsx(style.roleOrStatus, {
pending: !member.accepted,
})}
>
{member.accepted
? member.permission === Permission.Owner
? 'Workspace Owner'
: 'Member'
: 'Pending'}
</div>
<Menu
items={
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
{operationButtonInfo.leaveOrRevokeText}
</MenuItem>
}
>
<IconButton
disabled={!operationButtonInfo.show}
type="plain"
style={{
visibility: operationButtonInfo.show ? 'visible' : 'hidden',
flexShrink: 0,
}}
>
<MoreVerticalIcon />
</IconButton>
</Menu>
<div
key={member.id}
className={style.memberListItem}
data-testid="member-item"
>
<Avatar
size={36}
url={member.avatarUrl}
name={(member.emailVerified ? member.name : member.email) as string}
/>
<div className={style.memberContainer}>
{member.emailVerified ? (
<>
<div className={style.memberName}>{member.name}</div>
<div className={style.memberEmail}>{member.email}</div>
</>
) : (
<div className={style.memberName}>{member.email}</div>
)}
</div>
</>
<div
className={clsx(style.roleOrStatus, {
pending: !member.accepted,
})}
>
{member.accepted
? member.permission === Permission.Owner
? 'Workspace Owner'
: 'Member'
: 'Pending'}
</div>
<Menu
items={
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
{operationButtonInfo.leaveOrRevokeText}
</MenuItem>
}
>
<IconButton
disabled={!operationButtonInfo.show}
type="plain"
style={{
visibility: operationButtonInfo.show ? 'visible' : 'hidden',
flexShrink: 0,
}}
>
<MoreVerticalIcon />
</IconButton>
</Menu>
</div>
);
};

View File

@@ -93,14 +93,17 @@ export const membersFallback = style({
color: 'var(--affine-primary-color)',
});
export const membersPanel = style({
marginTop: '24px',
padding: '4px',
borderRadius: '12px',
background: 'var(--affine-background-primary-color)',
border: '1px solid var(--affine-border-color)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
});
export const listItem = style({
export const memberList = style({});
export const memberListItem = style({
padding: '0 4px 0 16px',
height: '58px',
display: 'flex',
@@ -155,7 +158,7 @@ export const memberEmail = style({
});
export const iconButton = style({});
globalStyle(`${listItem}:hover ${iconButton}`, {
globalStyle(`${memberListItem}:hover ${iconButton}`, {
opacity: 1,
pointerEvents: 'all',
});

View File

@@ -12,7 +12,7 @@ import {
useGeneralSettingList,
} from './general-setting';
import { SettingSidebar } from './setting-sidebar';
import { footerIconWrapper, settingContent } from './style.css';
import * as style from './style.css';
import { WorkspaceSetting } from './workspace-setting';
type ActiveTab = GeneralSettingKeys | 'workspace' | 'account';
@@ -84,33 +84,32 @@ export const SettingModal = ({
onAccountSettingClick={onAccountSettingClick}
/>
<div data-testid="setting-modal-content" className={settingContent}>
<div className="wrapper">
<div className="content">
{activeTab === 'workspace' && workspaceId ? (
<Suspense fallback={<WorkspaceDetailSkeleton />}>
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
</Suspense>
) : null}
{generalSettingList.find(v => v.key === activeTab) ? (
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
) : null}
{activeTab === 'account' && loginStatus === 'authenticated' ? (
<AccountSetting />
) : null}
</div>
<div className="footer">
<div className={footerIconWrapper}>
<div data-testid="setting-modal-content" className={style.wrapper}>
<div className={style.content}>
{activeTab === 'workspace' && workspaceId ? (
<Suspense fallback={<WorkspaceDetailSkeleton />}>
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
</Suspense>
) : null}
{generalSettingList.find(v => v.key === activeTab) ? (
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
) : null}
{activeTab === 'account' && loginStatus === 'authenticated' ? (
<AccountSetting />
) : null}
</div>
<div className="footer">
<a
href="https://community.affine.pro/home"
target="_blank"
rel="noreferrer"
className={style.suggestionLink}
>
<span className={style.suggestionLinkIcon}>
<ContactWithUsIcon />
</div>
<a
href="https://community.affine.pro/home"
target="_blank"
rel="noreferrer"
>
{t['com.affine.settings.suggestion']()}
</a>
</div>
</span>
{t['com.affine.settings.suggestion']()}
</a>
</div>
</div>
</Modal>

View File

@@ -49,14 +49,7 @@ export const UserInfo = ({
className={accountButton}
onClick={onAccountSettingClick}
>
<Avatar
size={28}
name={user.name}
url={user.image}
style={{
marginRight: '10px',
}}
/>
<Avatar size={28} name={user.name} url={user.image} className="avatar" />
<div className="content">
<div className="name" title={user.name}>

View File

@@ -100,6 +100,7 @@ export const accountButton = style({
cursor: 'pointer',
userSelect: 'none',
display: 'flex',
columnGap: '10px',
justifyContent: 'space-between',
alignItems: 'center',
':hover': {
@@ -107,17 +108,22 @@ export const accountButton = style({
},
});
globalStyle(`${accountButton} .avatar.not-sign`, {
globalStyle(`${accountButton} .avatar`, {
width: '28px',
height: '28px',
borderRadius: '50%',
fontSize: '22px',
fontSize: '20px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderColor: 'var(--affine-icon-secondary)',
flexShrink: 0,
});
globalStyle(`${accountButton} .avatar.not-sign`, {
color: 'var(--affine-icon-secondary)',
background: 'var(--affine-white)',
paddingBottom: '2px',
border: '1px solid var(--affine-icon-secondary)',
});
globalStyle(`${accountButton} .content`, {
flexGrow: '1',

View File

@@ -1,45 +1,38 @@
import { globalStyle, style } from '@vanilla-extract/css';
import { style } from '@vanilla-extract/css';
export const settingContent = style({
export const wrapper = style({
flexGrow: '1',
height: '100%',
padding: '40px 15px',
overflow: 'hidden',
});
globalStyle(`${settingContent} .wrapper`, {
padding: '0 15px',
height: '100%',
maxWidth: '560px',
margin: '0 auto',
overflowY: 'auto',
});
padding: '40px 15px 20px 15px',
overflow: 'hidden auto',
globalStyle(`${settingContent} .wrapper::-webkit-scrollbar`, {
display: 'none',
});
globalStyle(`${settingContent} .content`, {
minHeight: '100%',
paddingBottom: '80px',
});
globalStyle(`${settingContent} .footer`, {
cursor: 'pointer',
paddingTop: '40px',
marginTop: '-80px',
fontSize: 'var(--affine-font-sm)',
// children
display: 'flex',
minHeight: '100px',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'center',
'::-webkit-scrollbar': {
display: 'none',
},
});
globalStyle(`${settingContent} .footer a`, {
color: 'var(--affine-text-primary-color)',
lineHeight: 'normal',
export const content = style({
width: '100%',
marginBottom: '24px',
});
export const footerIconWrapper = style({
fontSize: 'var(--affine-font-base)',
color: 'var(--affine-icon-color)',
marginRight: '12px',
height: '19px',
export const suggestionLink = style({
fontSize: 'var(--affine-font-sm)',
color: 'var(--affine-text-primary-color)',
display: 'flex',
alignItems: 'center',
});
export const suggestionLinkIcon = style({
color: 'var(--affine-icon-color)',
marginRight: '12px',
display: 'flex',
});

View File

@@ -6,6 +6,7 @@ import {
import type { Page } from '@blocksuite/store';
import { useState } from 'react';
import { useExportPage } from '../../../hooks/affine/use-export-page';
import { useIsSharedPage } from '../../../hooks/affine/use-is-shared-page';
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
@@ -18,6 +19,7 @@ type SharePageModalProps = {
export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
const onTransformWorkspace = useOnTransformWorkspace();
const [open, setOpen] = useState(false);
const exportHandler = useExportPage(page);
return (
<>
<ShareMenu
@@ -26,6 +28,7 @@ export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
useIsSharedPage={useIsSharedPage}
onEnableAffineCloud={() => setOpen(true)}
togglePagePublic={async () => {}}
exportHandler={exportHandler}
/>
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
<EnableAffineCloudModal

View File

@@ -23,12 +23,15 @@ import {
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { useAtom, useSetAtom } from 'jotai';
import { useCallback, useRef, useState } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useRef } from 'react';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { pageSettingFamily, setPageModeAtom } from '../../../atoms';
import { setPageModeAtom } from '../../../atoms';
import { currentModeAtom } from '../../../atoms/mode';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useExportPage } from '../../../hooks/affine/use-export-page';
import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { toast } from '../../../utils';
@@ -43,72 +46,81 @@ type PageMenuProps = {
export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
const t = useAFFiNEI18N();
const ref = useRef(null);
const { openPage } = useNavigateHelper();
// fixme(himself65): remove these hooks ASAP
const [workspace] = useCurrentWorkspace();
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const currentPage = blockSuiteWorkspace.getPage(pageId);
assertExists(currentPage);
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
) as PageMeta;
const [setting, setSetting] = useAtom(pageSettingFamily(pageId));
const mode = setting?.mode ?? 'page';
const currentMode = useAtomValue(currentModeAtom);
const favorite = pageMeta.favorite ?? false;
const { setPageMeta, setPageTitle } = usePageMetaHelper(blockSuiteWorkspace);
const [openConfirm, setOpenConfirm] = useState(false);
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const { togglePageMode, toggleFavorite } =
useBlockSuiteMetaHelper(blockSuiteWorkspace);
const { importFile } = usePageHelper(blockSuiteWorkspace);
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace);
const handleOpenTrashModal = useCallback(() => {
setTrashModal({
open: true,
pageId,
pageTitle: pageMeta.title,
});
}, [pageId, pageMeta.title, setTrashModal]);
const handleFavorite = useCallback(() => {
setPageMeta(pageId, { favorite: !favorite });
toggleFavorite(pageId);
toast(
favorite
? t['com.affine.toastMessage.removedFavorites']()
: t['com.affine.toastMessage.addedFavorites']()
);
}, [favorite, pageId, setPageMeta, t]);
}, [favorite, pageId, t, toggleFavorite]);
const handleSwitchMode = useCallback(() => {
setSetting(setting => ({
mode: setting?.mode === 'page' ? 'edgeless' : 'page',
}));
togglePageMode(pageId);
toast(
mode === 'page'
currentMode === 'page'
? t['com.affine.toastMessage.edgelessMode']()
: t['com.affine.toastMessage.pageMode']()
);
}, [mode, setSetting, t]);
const handleOnConfirm = useCallback(() => {
removeToTrash(pageId);
toast(t['com.affine.toastMessage.movedTrash']());
setOpenConfirm(false);
}, [pageId, removeToTrash, t]);
}, [currentMode, pageId, t, togglePageMode]);
const menuItemStyle = {
padding: '4px 12px',
transition: 'all 0.3s',
};
const { openPage } = useNavigateHelper();
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const exportHandler = useExportPage(currentPage);
const setPageMode = useSetAtom(setPageModeAtom);
const duplicate = useCallback(async () => {
const currentPage = blockSuiteWorkspace.getPage(pageId);
assertExists(currentPage);
const currentPageMeta = currentPage.meta;
const newPage = createPage();
await newPage.waitForLoaded();
const update = encodeStateAsUpdate(currentPage.spaceDoc);
applyUpdate(newPage.spaceDoc, update);
setPageMeta(newPage.id, {
tags: currentPageMeta.tags,
favorite: currentPageMeta.favorite,
});
setPageMode(newPage.id, mode);
setPageMode(newPage.id, currentMode);
setPageTitle(newPage.id, `${currentPageMeta.title}(1)`);
openPage(blockSuiteWorkspace.id, newPage.id);
}, [
blockSuiteWorkspace,
blockSuiteWorkspace.id,
createPage,
mode,
currentMode,
currentPage.meta,
currentPage.spaceDoc,
openPage,
pageId,
setPageMeta,
setPageMode,
setPageTitle,
@@ -130,7 +142,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
<MenuItem
preFix={
<MenuIcon>
{mode === 'page' ? <EdgelessIcon /> : <PageIcon />}
{currentMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
</MenuIcon>
}
data-testid="editor-option-menu-edgeless"
@@ -138,7 +150,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
style={menuItemStyle}
>
{t['Convert to ']()}
{mode === 'page'
{currentMode === 'page'
? t['com.affine.pageMode.edgeless']()
: t['com.affine.pageMode.page']()}
</MenuItem>
@@ -194,13 +206,11 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
>
{t['Import']()}
</MenuItem>
<Export />
<Export exportHandler={exportHandler} />
<MenuSeparator />
<MoveToTrash
data-testid="editor-option-menu-delete"
onSelect={() => {
setOpenConfirm(true);
}}
onSelect={handleOpenTrashModal}
/>
</>
);
@@ -218,12 +228,6 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
>
<HeaderDropDownButton />
</Menu>
<MoveToTrash.ConfirmModal
open={openConfirm}
title={pageMeta.title}
onConfirm={handleOnConfirm}
onOpenChange={setOpenConfirm}
/>
</FlexWrapper>
</>
);

View File

@@ -2,11 +2,12 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { Tooltip } from '@toeverything/components/tooltip';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtom } from 'jotai';
import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { pageSettingFamily } from '../../../atoms';
import { currentModeAtom } from '../../../atoms/mode';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
import { toast } from '../../../utils';
import { StyledEditorModeSwitch, StyledKeyboardItem } from './style';
@@ -34,14 +35,17 @@ export const EditorModeSwitch = ({
blockSuiteWorkspace,
pageId,
}: EditorModeSwitchProps) => {
const [setting, setSetting] = useAtom(pageSettingFamily(pageId));
const currentMode = setting?.mode ?? 'page';
const t = useAFFiNEI18N();
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
const t = useAFFiNEI18N();
assertExists(pageMeta);
const { trash } = pageMeta;
const { togglePageMode, switchToEdgelessMode, switchToPageMode } =
useBlockSuiteMetaHelper(blockSuiteWorkspace);
const currentMode = useAtomValue(currentModeAtom);
useEffect(() => {
if (trash) {
return;
@@ -53,21 +57,33 @@ export const EditorModeSwitch = ({
: e.key === 's' && e.altKey
) {
e.preventDefault();
setSetting(setting => {
if (setting?.mode !== 'page') {
toast(t['com.affine.toastMessage.pageMode']());
return { ...setting, mode: 'page' };
} else {
toast(t['com.affine.toastMessage.edgelessMode']());
return { ...setting, mode: 'edgeless' };
}
});
togglePageMode(pageId);
toast(
currentMode === 'page'
? t['com.affine.toastMessage.edgelessMode']()
: t['com.affine.toastMessage.pageMode']()
);
}
};
document.addEventListener('keydown', keydown, { capture: true });
return () =>
document.removeEventListener('keydown', keydown, { capture: true });
}, [setSetting, t, trash]);
}, [currentMode, pageId, t, togglePageMode, trash]);
const onSwitchToPageMode = useCallback(() => {
if (currentMode === 'page') {
return;
}
switchToPageMode(pageId);
toast(t['com.affine.toastMessage.pageMode']());
}, [currentMode, pageId, switchToPageMode, t]);
const onSwitchToEdgelessMode = useCallback(() => {
if (currentMode === 'edgeless') {
return;
}
switchToEdgelessMode(pageId);
toast(t['com.affine.toastMessage.edgelessMode']());
}, [currentMode, pageId, switchToEdgelessMode, t]);
return (
<Tooltip content={<TooltipContent />}>
@@ -81,28 +97,14 @@ export const EditorModeSwitch = ({
active={currentMode === 'page'}
hide={trash && currentMode !== 'page'}
trash={trash}
onClick={() => {
setSetting(setting => {
if (setting?.mode !== 'page') {
toast(t['com.affine.toastMessage.pageMode']());
}
return { ...setting, mode: 'page' };
});
}}
onClick={onSwitchToPageMode}
/>
<EdgelessSwitchItem
data-testid="switch-edgeless-mode-button"
active={currentMode === 'edgeless'}
hide={trash && currentMode !== 'edgeless'}
trash={trash}
onClick={() => {
setSetting(setting => {
if (setting?.mode !== 'edgeless') {
toast(t['com.affine.toastMessage.edgelessMode']());
}
return { ...setting, mode: 'edgeless' };
});
}}
onClick={onSwitchToEdgelessMode}
/>
</StyledEditorModeSwitch>
</Tooltip>

View File

@@ -15,6 +15,7 @@ import { Suspense, useCallback, useMemo } from 'react';
import { allPageModeSelectAtom } from '../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper';
import { useGetPageInfoById } from '../../../hooks/use-get-page-info';
import type { BlockSuiteWorkspace } from '../../../shared';
import { toast } from '../../../utils';
@@ -134,7 +135,6 @@ export const BlockSuitePageList = ({
const pageMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
const {
toggleFavorite,
removeToTrash,
restoreFromTrash,
permanentlyDeletePage,
cancelPublicPage,
@@ -144,6 +144,8 @@ export const BlockSuitePageList = ({
usePageHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N();
const getPageInfo = useGetPageInfoById(blockSuiteWorkspace);
const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace);
const tagOptionMap = useMemo(
() =>
Object.fromEntries(
@@ -246,10 +248,13 @@ export const BlockSuitePageList = ({
onClickRestore: () => {
restoreFromTrash(pageMeta.id);
},
removeToTrash: () => {
removeToTrash(pageMeta.id);
toast(t['com.affine.toastMessage.successfullyDeleted']());
},
removeToTrash: () =>
setTrashModal({
open: true,
pageId: pageMeta.id,
pageTitle: pageMeta.title,
}),
onRestorePage: () => {
restoreFromTrash(pageMeta.id);
toast(

View File

@@ -3,7 +3,7 @@ import { WorkspaceSubPath } from '@affine/env/workspace';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { pageSettingsAtom, setPageModeAtom } from '../../../atoms';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
@@ -19,14 +19,14 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
);
const setPageMode = useSetAtom(setPageModeAtom);
const createPageAndOpen = useCallback(
(id?: string, mode?: 'page' | 'edgeless'): string => {
(id?: string, mode?: 'page' | 'edgeless') => {
const page = createPage(id);
initEmptyPage(page).catch(error => {
toast(`Failed to initialize Page: ${error.message}`);
});
setPageMode(page.id, mode || 'page');
openPage(blockSuiteWorkspace.id, page.id);
return page.id;
return page;
},
[blockSuiteWorkspace.id, createPage, openPage, setPageMode]
);
@@ -57,10 +57,17 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
};
showImportModal({ workspace: blockSuiteWorkspace, onSuccess });
}, [blockSuiteWorkspace, openPage, jumpToSubPath]);
return {
createPage: createPageAndOpen,
createEdgeless: createEdgelessAndOpen,
importFile: importFileAndOpen,
isPreferredEdgeless: isPreferredEdgeless,
};
return useMemo(() => {
return {
createPage: createPageAndOpen,
createEdgeless: createEdgelessAndOpen,
importFile: importFileAndOpen,
isPreferredEdgeless: isPreferredEdgeless,
};
}, [
createEdgelessAndOpen,
createPageAndOpen,
importFileAndOpen,
isPreferredEdgeless,
]);
};

View File

@@ -4,14 +4,8 @@ import type { SerializedBlock } from '@blocksuite/blocks';
import type { BaseBlockModel } from '@blocksuite/store';
import type { Page } from '@blocksuite/store';
import type { VEditor } from '@blocksuite/virgo';
import type { ReactElement } from 'react';
import { StrictMode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
export type BookMarkProps = {
page: Page;
};
type ShortcutMap = {
[key: string]: (e: KeyboardEvent, page: Page) => void;
};
@@ -121,7 +115,11 @@ const shouldShowBookmarkMenu = (pastedBlocks: Record<string, unknown>[]) => {
return !!firstBlock.text[0].attributes?.link;
};
const BookMarkUI = ({ page }: BookMarkProps) => {
export type BookmarkProps = {
page: Page;
};
export const Bookmark = ({ page }: BookmarkProps) => {
const [anchor, setAnchor] = useState<Range | null>(null);
const [selectedOption, setSelectedOption] = useState<string>(
menuOptions[0].id
@@ -244,15 +242,3 @@ const BookMarkUI = ({ page }: BookMarkProps) => {
</MuiClickAwayListener>
) : null;
};
type AppProps = {
page: Page;
};
export const App = (props: AppProps): ReactElement => {
return (
<StrictMode>
<BookMarkUI page={props.page} />
</StrictMode>
);
};

View File

@@ -15,7 +15,7 @@ export const pluginContainer = style({
});
export const editor = style({
height: 'calc(100% - 52px)',
height: '100%',
selectors: {
'&.full-screen': {
vars: {
@@ -25,6 +25,11 @@ export const editor = style({
},
},
});
globalStyle(`${editor} .affine-doc-viewport`, {
paddingBottom: '150px',
});
globalStyle('.is-public-page affine-page-meta-data', {
display: 'none',
});

View File

@@ -4,7 +4,7 @@ import { PageNotFoundError } from '@affine/env/constant';
import type { LayoutNode } from '@affine/sdk//entry';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
import type { Page, Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
@@ -31,6 +31,7 @@ import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { pageSettingFamily } from '../atoms';
import { fontStyleOptions, useAppSetting } from '../atoms/settings';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import { Bookmark } from './bookmark';
import * as styles from './page-detail-editor.css';
import { editorContainer, pluginContainer } from './page-detail-editor.css';
import { TrashButtonGroup } from './pure/trash-button-group';
@@ -98,13 +99,17 @@ const EditorWrapper = memo(function EditorWrapper({
setBlockHub={setBlockHub}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
page.workspace.setPageMeta(page.id, {
updatedDate: Date.now(),
});
const disposableGroup = new DisposableGroup();
disposableGroup.add(
page.slots.blockUpdated.once(() => {
page.workspace.setPageMeta(page.id, {
updatedDate: Date.now(),
});
})
);
localStorage.setItem('last_page_id', page.id);
let dispose = () => {};
if (onLoad) {
dispose = onLoad(page, editor);
disposableGroup.add(onLoad(page, editor));
}
const rootStore = getCurrentStore();
const editorItems = rootStore.get(pluginEditorAtom);
@@ -124,7 +129,7 @@ const EditorWrapper = memo(function EditorWrapper({
});
return () => {
dispose();
disposableGroup.dispose();
clearTimeout(renderTimeout);
window.setTimeout(() => {
disposes.forEach(dispose => dispose());
@@ -135,6 +140,7 @@ const EditorWrapper = memo(function EditorWrapper({
)}
/>
{meta.trash && <TrashButtonGroup />}
<Bookmark page={page} />
</>
);
});
@@ -207,6 +213,7 @@ const LayoutPanel = memo(function LayoutPanel(
return (
<PanelGroup
direction={node.direction}
style={depth === 0 ? { height: 'calc(100% - 52px)' } : undefined}
className={depth === 0 ? editorContainer : undefined}
>
<Panel
@@ -256,7 +263,11 @@ export const PageDetailEditor = (props: PageDetailEditorProps) => {
if (layout === 'editor') {
return (
<Suspense>
<PanelGroup direction="horizontal" className={editorContainer}>
<PanelGroup
style={{ height: 'calc(100% - 52px)' }}
direction="horizontal"
className={editorContainer}
>
<Panel>
<EditorWrapper {...props} />
</Panel>

View File

@@ -0,0 +1,411 @@
import { commandScore } from '@affine/cmdk';
import { useCollectionManager } from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
import type { Page, PageMeta } from '@blocksuite/store';
import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import {
getWorkspace,
waitForWorkspace,
} from '@toeverything/infra/__internal__/workspace';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
getCurrentStore,
} from '@toeverything/infra/atom';
import {
type AffineCommand,
AffineCommandRegistry,
type CommandCategory,
PreconditionStrategy,
} from '@toeverything/infra/command';
import { atom, useAtomValue } from 'jotai';
import groupBy from 'lodash/groupBy';
import { useMemo } from 'react';
import {
openQuickSearchModalAtom,
pageSettingsAtom,
recentPageIdsBaseAtom,
} from '../../../atoms';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { WorkspaceSubPath } from '../../../shared';
import { currentCollectionsAtom } from '../../../utils/user-setting';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
import type { CMDKCommand, CommandContext } from './types';
export const cmdkQueryAtom = atom('');
export const cmdkValueAtom = atom('');
// like currentWorkspaceAtom, but not throw error
const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
const currentWorkspaceId = get(currentWorkspaceIdAtom);
if (!currentWorkspaceId) {
return;
}
const currentPageId = get(currentPageIdAtom);
if (!currentPageId) {
return;
}
const workspace = getWorkspace(currentWorkspaceId);
await waitForWorkspace(workspace);
const page = workspace.getPage(currentPageId);
if (!page) {
return;
}
if (!page.loaded) {
await page.waitForLoaded();
}
return page;
});
export const commandContextAtom = atom<Promise<CommandContext>>(async get => {
const currentPage = await get(safeCurrentPageAtom);
const pageSettings = get(pageSettingsAtom);
return {
currentPage,
pageMode: currentPage ? pageSettings[currentPage.id]?.mode : undefined,
};
});
function filterCommandByContext(
command: AffineCommand,
context: CommandContext
) {
if (command.preconditionStrategy === PreconditionStrategy.Always) {
return true;
}
if (command.preconditionStrategy === PreconditionStrategy.InEdgeless) {
return context.pageMode === 'edgeless';
}
if (command.preconditionStrategy === PreconditionStrategy.InPaper) {
return context.pageMode === 'page';
}
if (command.preconditionStrategy === PreconditionStrategy.InPaperOrEdgeless) {
return !!context.currentPage;
}
if (command.preconditionStrategy === PreconditionStrategy.Never) {
return false;
}
if (typeof command.preconditionStrategy === 'function') {
return command.preconditionStrategy();
}
return true;
}
let quickSearchOpenCounter = 0;
const openCountAtom = atom(get => {
if (get(openQuickSearchModalAtom)) {
quickSearchOpenCounter++;
}
return quickSearchOpenCounter;
});
export const filteredAffineCommands = atom(async get => {
const context = await get(commandContextAtom);
// reset when modal open
get(openCountAtom);
const commands = AffineCommandRegistry.getAll();
return commands.filter(command => {
return filterCommandByContext(command, context);
});
});
const useWorkspacePages = () => {
const [currentWorkspace] = useCurrentWorkspace();
const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
return pages;
};
const useRecentPages = () => {
const pages = useWorkspacePages();
const recentPageIds = useAtomValue(recentPageIdsBaseAtom);
return useMemo(() => {
return recentPageIds
.map(pageId => {
const page = pages.find(page => page.id === pageId);
return page;
})
.filter((p): p is PageMeta => !!p);
}, [recentPageIds, pages]);
};
const valueWrapperStart = '__>>>';
const valueWrapperEnd = '<<<__';
export const pageToCommand = (
category: CommandCategory,
page: PageMeta,
store: ReturnType<typeof getCurrentStore>,
navigationHelper: ReturnType<typeof useNavigateHelper>,
t: ReturnType<typeof useAFFiNEI18N>
): CMDKCommand => {
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
const currentWorkspaceId = store.get(currentWorkspaceIdAtom);
const label = page.title || t['Untitled']();
return {
id: page.id,
label: label,
// hack: when comparing, the part between >>> and <<< will be ignored
// adding this patch so that CMDK will not complain about duplicated commands
value:
label + valueWrapperStart + page.id + '.' + category + valueWrapperEnd,
originalValue: label,
category: category,
run: () => {
if (!currentWorkspaceId) {
console.error('current workspace not found');
return;
}
navigationHelper.jumpToPage(currentWorkspaceId, page.id);
},
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
timestamp: page.updatedDate,
};
};
const contentMatchedMagicString = '__$$content_matched$$__';
export const usePageCommands = () => {
// todo: considering collections for searching pages
// const { savedCollections } = useCollectionManager(currentCollectionsAtom);
const recentPages = useRecentPages();
const pages = useWorkspacePages();
const store = getCurrentStore();
const [workspace] = useCurrentWorkspace();
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
const query = useAtomValue(cmdkQueryAtom);
const navigationHelper = useNavigateHelper();
const t = useAFFiNEI18N();
return useMemo(() => {
let results: CMDKCommand[] = [];
if (query.trim() === '') {
results = recentPages.map(page => {
return pageToCommand('affine:recent', page, store, navigationHelper, t);
});
} else {
// queried pages that has matched contents
const pageIds = Array.from(
workspace.blockSuiteWorkspace.search({ query }).values()
).map(id => {
if (id.startsWith('space:')) {
return id.slice(6);
} else {
return id;
}
});
results = pages.map(page => {
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
const category =
pageMode === 'edgeless' ? 'affine:edgeless' : 'affine:pages';
const command = pageToCommand(
category,
page,
store,
navigationHelper,
t
);
if (pageIds.includes(page.id)) {
// hack to make the page always showing in the search result
command.value += contentMatchedMagicString;
}
return command;
});
// check if the pages have exact match. if not, we should show the "create page" command
if (results.every(command => command.originalValue !== query)) {
results.push({
id: 'affine:pages:create-page',
label: (
<Trans
i18nKey="com.affine.cmdk.affine.create-new-page-as"
values={{ query }}
>
Create New Page as: <strong>query</strong>
</Trans>
),
value: 'affine::create-page' + query, // hack to make the page always showing in the search result
category: 'affine:creation',
run: async () => {
const page = pageHelper.createPage();
await page.waitForLoaded();
pageMetaHelper.setPageTitle(page.id, query);
},
icon: <PageIcon />,
});
results.push({
id: 'affine:pages:create-edgeless',
label: (
<Trans
values={{ query }}
i18nKey="com.affine.cmdk.affine.create-new-edgeless-as"
>
Create New Edgeless as: <strong>query</strong>
</Trans>
),
value: 'affine::create-edgeless' + query, // hack to make the page always showing in the search result
category: 'affine:creation',
run: async () => {
const page = pageHelper.createEdgeless();
await page.waitForLoaded();
pageMetaHelper.setPageTitle(page.id, query);
},
icon: <EdgelessIcon />,
});
}
}
return results;
}, [
pageHelper,
pageMetaHelper,
navigationHelper,
pages,
query,
recentPages,
store,
t,
workspace.blockSuiteWorkspace,
]);
};
export const collectionToCommand = (
collection: Collection,
store: ReturnType<typeof getCurrentStore>,
navigationHelper: ReturnType<typeof useNavigateHelper>,
selectCollection: ReturnType<typeof useCollectionManager>['selectCollection'],
t: ReturnType<typeof useAFFiNEI18N>
): CMDKCommand => {
const currentWorkspaceId = store.get(currentWorkspaceIdAtom);
const label = collection.name || t['Untitled']();
const category = 'affine:collections';
return {
id: collection.id,
label: label,
// hack: when comparing, the part between >>> and <<< will be ignored
// adding this patch so that CMDK will not complain about duplicated commands
value:
label +
valueWrapperStart +
collection.id +
'.' +
category +
valueWrapperEnd,
originalValue: label,
category: category,
run: () => {
if (!currentWorkspaceId) {
console.error('current workspace not found');
return;
}
navigationHelper.jumpToSubPath(currentWorkspaceId, WorkspaceSubPath.ALL);
selectCollection(collection.id);
},
icon: <ViewLayersIcon />,
};
};
export const useCollectionsCommands = () => {
// todo: considering collections for searching pages
const { savedCollections, selectCollection } = useCollectionManager(
currentCollectionsAtom
);
const store = getCurrentStore();
const query = useAtomValue(cmdkQueryAtom);
const navigationHelper = useNavigateHelper();
const t = useAFFiNEI18N();
return useMemo(() => {
let results: CMDKCommand[] = [];
if (query.trim() === '') {
return results;
} else {
results = savedCollections.map(collection => {
const command = collectionToCommand(
collection,
store,
navigationHelper,
selectCollection,
t
);
return command;
});
return results;
}
}, [query, savedCollections, store, navigationHelper, selectCollection, t]);
};
export const useCMDKCommandGroups = () => {
const pageCommands = usePageCommands();
const collectionCommands = useCollectionsCommands();
const affineCommands = useAtomValue(filteredAffineCommands);
return useMemo(() => {
const commands = [
...pageCommands,
...collectionCommands,
...affineCommands,
];
const groups = groupBy(commands, command => command.category);
return Object.entries(groups) as [CommandCategory, CMDKCommand[]][];
}, [affineCommands, collectionCommands, pageCommands]);
};
export const customCommandFilter = (value: string, search: string) => {
// strip off the part between __>>> and <<<__
let label = value.replace(
new RegExp(valueWrapperStart + '.*' + valueWrapperEnd, 'g'),
''
);
const pageContentMatched = label.includes(contentMatchedMagicString);
if (pageContentMatched) {
label = label.replace(contentMatchedMagicString, '');
}
const originalScore = commandScore(label, search);
// if the command has matched the content but not the label,
// we should give it a higher score, but not too high
if (originalScore < 0.01 && pageContentMatched) {
return 0.3;
}
return originalScore;
};
export const useCommandFilteredStatus = (
groups: [CommandCategory, CMDKCommand[]][]
) => {
// for each of the groups, show the count of commands that has matched the query
const query = useAtomValue(cmdkQueryAtom);
return useMemo(() => {
return Object.fromEntries(
groups.map(([category, commands]) => {
return [category, getCommandFilteredCount(commands, query)] as const;
})
) as Record<CommandCategory, number>;
}, [groups, query]);
};
function getCommandFilteredCount(commands: CMDKCommand[], query: string) {
return commands.filter(command => {
return command.value && customCommandFilter(command.value, query) > 0;
}).length;
}

View File

@@ -0,0 +1,2 @@
export * from './main';
export * from './modal';

View File

@@ -0,0 +1,155 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const root = style({});
export const commandsContainer = style({
height: 'calc(100% - 65px)',
padding: '8px 6px 18px 6px',
});
export const searchInput = style({
height: 66,
color: 'var(--affine-text-primary-color)',
fontSize: 'var(--affine-font-h-5)',
padding: '21px 24px',
width: '100%',
borderBottom: '1px solid var(--affine-border-color)',
flexShrink: 0,
'::placeholder': {
color: 'var(--affine-text-secondary-color)',
},
});
export const panelContainer = style({
height: '100%',
display: 'flex',
flexDirection: 'column',
});
export const itemIcon = style({
fontSize: 20,
marginRight: 16,
flexShrink: 0,
display: 'flex',
alignItems: 'center',
color: 'var(--affine-icon-secondary)',
});
export const itemLabel = style({
fontSize: 14,
lineHeight: '1.5',
color: 'var(--affine-text-primary-color)',
flex: 1,
});
export const timestamp = style({
display: 'flex',
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
});
export const keybinding = style({
display: 'flex',
fontSize: 'var(--affine-font-xs)',
columnGap: 2,
});
export const keybindingFragment = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0 4px',
borderRadius: 4,
color: 'var(--affine-text-secondary-color)',
backgroundColor: 'var(--affine-background-tertiary-color)',
width: 24,
height: 20,
});
globalStyle(`${root} [cmdk-root]`, {
height: '100%',
});
globalStyle(`${root} [cmdk-group-heading]`, {
padding: '8px',
color: 'var(--affine-text-secondary-color)',
fontSize: 'var(--affine-font-xs)',
fontWeight: 600,
lineHeight: '1.67',
});
globalStyle(`${root} [cmdk-group][hidden]`, {
display: 'none',
});
globalStyle(
`${root} [cmdk-group]:not([hidden]):first-of-type [cmdk-group-heading]`,
{
paddingTop: 16,
}
);
globalStyle(`${root} [cmdk-list]`, {
maxHeight: 400,
minHeight: 120,
overflow: 'auto',
overscrollBehavior: 'contain',
transition: '.1s ease',
transitionProperty: 'height',
height: 'min(330px, calc(var(--cmdk-list-height) + 8px))',
padding: '0 0 8px 6px',
scrollbarGutter: 'stable',
});
globalStyle(`${root} [cmdk-list]::-webkit-scrollbar`, {
width: 6,
height: 6,
});
globalStyle(`${root} [cmdk-list]::-webkit-scrollbar-thumb`, {
borderRadius: 4,
backgroundClip: 'padding-box',
});
globalStyle(`${root} [cmdk-list]:hover::-webkit-scrollbar-thumb`, {
backgroundColor: 'var(--affine-divider-color)',
});
globalStyle(`${root} [cmdk-list]:hover::-webkit-scrollbar-thumb:hover`, {
backgroundColor: 'var(--affine-icon-color)',
});
globalStyle(`${root} [cmdk-item]`, {
display: 'flex',
height: 44,
padding: '0 12px',
alignItems: 'center',
cursor: 'default',
borderRadius: 4,
userSelect: 'none',
});
globalStyle(`${root} [cmdk-item][data-selected=true]`, {
background: 'var(--affine-background-secondary-color)',
});
globalStyle(`${root} [cmdk-item][data-selected=true][data-is-danger=true]`, {
background: 'var(--affine-background-error-color)',
color: 'var(--affine-error-color)',
});
globalStyle(`${root} [cmdk-item][data-selected=true] ${itemIcon}`, {
color: 'var(--affine-icon-color)',
});
globalStyle(
`${root} [cmdk-item][data-selected=true][data-is-danger=true] ${itemIcon}`,
{
color: 'var(--affine-error-color)',
}
);
globalStyle(
`${root} [cmdk-item][data-selected=true][data-is-danger=true] ${itemLabel}`,
{
color: 'var(--affine-error-color)',
}
);

View File

@@ -0,0 +1,225 @@
import { Command } from '@affine/cmdk';
import { formatDate } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { CommandCategory } from '@toeverything/infra/command';
import clsx from 'clsx';
import { useAtom, useSetAtom } from 'jotai';
import { Suspense, useLayoutEffect, useMemo } from 'react';
import {
cmdkQueryAtom,
cmdkValueAtom,
customCommandFilter,
useCMDKCommandGroups,
} from './data';
import * as styles from './main.css';
import { CMDKModal, type CMDKModalProps } from './modal';
import type { CMDKCommand } from './types';
type NoParametersKeys<T> = {
[K in keyof T]: T[K] extends () => any ? K : never;
}[keyof T];
type i18nKey = NoParametersKeys<ReturnType<typeof useAFFiNEI18N>>;
const categoryToI18nKey: Record<CommandCategory, i18nKey> = {
'affine:recent': 'com.affine.cmdk.affine.category.affine.recent',
'affine:navigation': 'com.affine.cmdk.affine.category.affine.navigation',
'affine:creation': 'com.affine.cmdk.affine.category.affine.creation',
'affine:general': 'com.affine.cmdk.affine.category.affine.general',
'affine:layout': 'com.affine.cmdk.affine.category.affine.layout',
'affine:pages': 'com.affine.cmdk.affine.category.affine.pages',
'affine:edgeless': 'com.affine.cmdk.affine.category.affine.edgeless',
'affine:collections': 'com.affine.cmdk.affine.category.affine.collections',
'affine:settings': 'com.affine.cmdk.affine.category.affine.settings',
'affine:updates': 'com.affine.cmdk.affine.category.affine.updates',
'affine:help': 'com.affine.cmdk.affine.category.affine.help',
'editor:edgeless': 'com.affine.cmdk.affine.category.editor.edgeless',
'editor:insert-object':
'com.affine.cmdk.affine.category.editor.insert-object',
'editor:page': 'com.affine.cmdk.affine.category.editor.page',
};
const QuickSearchGroup = ({
category,
commands,
onOpenChange,
}: {
category: CommandCategory;
commands: CMDKCommand[];
onOpenChange?: (open: boolean) => void;
}) => {
const t = useAFFiNEI18N();
const i18nkey = categoryToI18nKey[category];
const setQuery = useSetAtom(cmdkQueryAtom);
return (
<Command.Group key={category} heading={t[i18nkey]()}>
{commands.map(command => {
return (
<Command.Item
key={command.id}
onSelect={() => {
command.run();
setQuery('');
onOpenChange?.(false);
}}
value={command.value}
data-is-danger={
command.id === 'editor:page-move-to-trash' ||
command.id === 'editor:edgeless-move-to-trash'
}
>
<div className={styles.itemIcon}>{command.icon}</div>
<div
data-testid="cmdk-label"
className={styles.itemLabel}
data-value={
command.originalValue ? command.originalValue : undefined
}
>
{command.label}
</div>
{command.timestamp ? (
<div className={styles.timestamp}>
{formatDate(new Date(command.timestamp))}
</div>
) : null}
{command.keyBinding ? (
<CMDKKeyBinding
keyBinding={
typeof command.keyBinding === 'string'
? command.keyBinding
: command.keyBinding.binding
}
/>
) : null}
</Command.Item>
);
})}
</Command.Group>
);
};
const QuickSearchCommands = ({
onOpenChange,
}: {
onOpenChange?: (open: boolean) => void;
}) => {
const groups = useCMDKCommandGroups();
return groups.map(([category, commands]) => {
return (
<QuickSearchGroup
key={category}
onOpenChange={onOpenChange}
category={category}
commands={commands}
/>
);
});
};
export const CMDKContainer = ({
className,
onQueryChange,
query,
children,
...rest
}: React.PropsWithChildren<{
className?: string;
query: string;
onQueryChange: (query: string) => void;
}>) => {
const t = useAFFiNEI18N();
const [value, setValue] = useAtom(cmdkValueAtom);
return (
<Command
{...rest}
data-testid="cmdk-quick-search"
filter={customCommandFilter}
className={clsx(className, styles.panelContainer)}
value={value}
onValueChange={setValue}
// Handle KeyboardEvent conflicts with blocksuite
onKeyDown={(e: React.KeyboardEvent) => {
if (
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowRight'
) {
e.stopPropagation();
}
}}
>
{/* todo: add page context here */}
<Command.Input
placeholder={t['com.affine.cmdk.placeholder']()}
autoFocus
{...rest}
value={query}
onValueChange={onQueryChange}
className={clsx(className, styles.searchInput)}
/>
<Command.List>{children}</Command.List>
</Command>
);
};
export const CMDKQuickSearchModal = (props: CMDKModalProps) => {
const [query, setQuery] = useAtom(cmdkQueryAtom);
useLayoutEffect(() => {
if (props.open) {
setQuery('');
}
}, [props.open, setQuery]);
return (
<CMDKModal {...props}>
<CMDKContainer
className={styles.root}
query={query}
onQueryChange={setQuery}
>
<Suspense fallback={<Command.Loading />}>
<QuickSearchCommands onOpenChange={props.onOpenChange} />
</Suspense>
</CMDKContainer>
</CMDKModal>
);
};
const CMDKKeyBinding = ({ keyBinding }: { keyBinding: string }) => {
const isMacOS = environment.isBrowser && environment.isMacOs;
const fragments = useMemo(() => {
return keyBinding.split('+').map(fragment => {
if (fragment === '$mod') {
return isMacOS ? '⌘' : 'Ctrl';
}
if (fragment === 'ArrowUp') {
return '↑';
}
if (fragment === 'ArrowDown') {
return '↓';
}
if (fragment === 'ArrowLeft') {
return '←';
}
if (fragment === 'ArrowRight') {
return '→';
}
return fragment;
});
}, [isMacOS, keyBinding]);
return (
<div className={styles.keybinding}>
{fragments.map((fragment, index) => {
return (
<div key={index} className={styles.keybindingFragment}>
{fragment}
</div>
);
})}
</div>
);
};

View File

@@ -0,0 +1,55 @@
import { keyframes, style } from '@vanilla-extract/css';
const contentShow = keyframes({
from: { opacity: 0, transform: 'translateY(-2%) scale(0.96)' },
to: { opacity: 1, transform: 'translateY(0) scale(1)' },
});
const contentHide = keyframes({
to: { opacity: 0, transform: 'translateY(-2%) scale(0.96)' },
from: { opacity: 1, transform: 'translateY(0) scale(1)' },
});
export const modalOverlay = style({
position: 'fixed',
inset: 0,
backgroundColor: 'transparent',
zIndex: 'var(--affine-z-index-modal)',
});
export const modalContentWrapper = style({
position: 'fixed',
inset: 0,
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'center',
zIndex: 'var(--affine-z-index-modal)',
padding: '13vh 16px 16px',
});
export const modalContent = style({
width: 640,
// height: 530,
backgroundColor: 'var(--affine-background-overlay-panel-color)',
boxShadow: 'var(--affine-cmd-shadow)',
borderRadius: '12px',
maxWidth: 'calc(100vw - 50px)',
minWidth: 480,
// minHeight: 420,
// :focus-visible will set outline
outline: 'none',
position: 'relative',
zIndex: 'var(--affine-z-index-modal)',
willChange: 'transform, opacity',
selectors: {
'&[data-state=entered], &[data-state=entering]': {
animation: `${contentShow} 120ms cubic-bezier(0.42, 0, 0.58, 1)`,
animationFillMode: 'forwards',
},
'&[data-state=exited], &[data-state=exiting]': {
animation: `${contentHide} 120ms cubic-bezier(0.42, 0, 0.58, 1)`,
animationFillMode: 'forwards',
},
},
});

View File

@@ -0,0 +1,67 @@
import * as Dialog from '@radix-ui/react-dialog';
import { useEffect, useReducer } from 'react';
import * as styles from './modal.css';
// a CMDK modal that can be used to display a CMDK command
// it has a smooth animation and can be closed by clicking outside of the modal
export interface CMDKModalProps {
open: boolean;
onOpenChange?: (open: boolean) => void;
}
type ModalAnimationState = 'entering' | 'entered' | 'exiting' | 'exited';
function reduceAnimationState(
state: ModalAnimationState,
action: 'open' | 'close' | 'finish'
) {
switch (action) {
case 'open':
return state === 'entered' || state === 'entering' ? state : 'entering';
case 'close':
return state === 'exited' || state === 'exiting' ? state : 'exiting';
case 'finish':
return state === 'entering' ? 'entered' : 'exited';
}
}
export const CMDKModal = ({
onOpenChange,
open,
children,
}: React.PropsWithChildren<CMDKModalProps>) => {
const [animationState, dispatch] = useReducer(reduceAnimationState, 'exited');
useEffect(() => {
dispatch(open ? 'open' : 'close');
const timeout = setTimeout(() => {
dispatch('finish');
}, 120);
return () => {
clearTimeout(timeout);
};
}, [open]);
return (
<Dialog.Root
modal
open={animationState !== 'exited'}
onOpenChange={onOpenChange}
>
<Dialog.Portal>
<Dialog.Overlay className={styles.modalOverlay} />
<div className={styles.modalContentWrapper}>
<Dialog.Content
className={styles.modalContent}
data-state={animationState}
>
{children}
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
);
};

View File

@@ -0,0 +1,22 @@
import type { Page } from '@blocksuite/store';
import type { CommandCategory } from '@toeverything/infra/command';
export interface CommandContext {
currentPage: Page | undefined;
pageMode: 'page' | 'edgeless' | undefined;
}
// similar to AffineCommand, but for rendering into the UI
// it unifies all possible commands into a single type so that
// we can use a single render function to render all different commands
export interface CMDKCommand {
id: string;
label: string | React.ReactNode;
icon?: React.ReactNode;
category: CommandCategory;
keyBinding?: string | { binding: string };
timestamp?: number;
value?: string; // this is used for item filtering
originalValue?: string; // some values may be transformed, this is the original value
run: (e?: Event) => void | Promise<void>;
}

View File

@@ -1,60 +0,0 @@
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteTemporarilyIcon,
FolderIcon,
SettingsIcon,
} from '@blocksuite/icons';
import { useAtom } from 'jotai';
import type { ReactElement, SVGProps } from 'react';
import { useMemo } from 'react';
import { openSettingModalAtom } from '../../../atoms';
type IconComponent = (props: SVGProps<SVGSVGElement>) => ReactElement;
interface ConfigItem {
title: string;
icon: IconComponent;
onClick: () => void;
}
interface ConfigPathItem {
title: string;
icon: IconComponent;
subPath: WorkspaceSubPath;
}
export type Config = ConfigItem | ConfigPathItem;
export const useSwitchToConfig = (workspaceId: string): Config[] => {
const t = useAFFiNEI18N();
const [, setOpenSettingModalAtom] = useAtom(openSettingModalAtom);
return useMemo(
() => [
{
title: t['com.affine.workspaceSubPath.all'](),
subPath: WorkspaceSubPath.ALL,
icon: FolderIcon,
},
{
title: t['Workspace Settings'](),
onClick: () => {
setOpenSettingModalAtom({
open: true,
activeTab: 'workspace',
workspaceId,
});
},
icon: SettingsIcon,
},
{
title: t['com.affine.workspaceSubPath.trash'](),
subPath: WorkspaceSubPath.TRASH,
icon: DeleteTemporarilyIcon,
},
],
[t, workspaceId, setOpenSettingModalAtom]
);
};

View File

@@ -1,59 +0,0 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertEquals } from '@blocksuite/global/utils';
import { PlusIcon } from '@blocksuite/icons';
import { nanoid } from '@blocksuite/store';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { Command } from 'cmdk';
import { useCallback } from 'react';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
import { StyledModalFooterContent } from './style';
export interface FooterProps {
query: string;
onClose: () => void;
blockSuiteWorkspace: BlockSuiteWorkspace;
}
export const Footer = ({
query,
onClose,
blockSuiteWorkspace,
}: FooterProps) => {
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N();
const { jumpToPage } = useNavigateHelper();
const MAX_QUERY_SHOW_LENGTH = 20;
const normalizedQuery =
query.length > MAX_QUERY_SHOW_LENGTH
? query.slice(0, MAX_QUERY_SHOW_LENGTH) + '...'
: query;
return (
<Command.Item
data-testid="quick-search-add-new-page"
onSelect={useCallback(async () => {
const id = nanoid();
const page = createPage(id);
assertEquals(page.id, id);
await initEmptyPage(page, query);
blockSuiteWorkspace.setPageMeta(page.id, {
title: query,
});
onClose();
jumpToPage(blockSuiteWorkspace.id, page.id);
}, [blockSuiteWorkspace, createPage, jumpToPage, onClose, query])}
>
<StyledModalFooterContent>
<PlusIcon />
{query ? (
<span>{t['New Keyword Page']({ query: normalizedQuery })}</span>
) : (
<span>{t['New Page']()}</span>
)}
</StyledModalFooterContent>
</Command.Item>
);
};

View File

@@ -1,164 +0,0 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Modal } from '@toeverything/components/modal';
import { Command } from 'cmdk';
import { startTransition, Suspense } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { AllWorkspace } from '../../../shared';
import { Footer } from './footer';
import { Results } from './results';
import { SearchInput } from './search-input';
import {
StyledContent,
StyledModalDivider,
StyledModalFooter,
StyledModalHeader,
StyledNotFound,
StyledShortcut,
} from './style';
export interface QuickSearchModalProps {
workspace: AllWorkspace;
open: boolean;
setOpen: (value: boolean) => void;
}
export const QuickSearchModal = ({
open,
setOpen,
workspace,
}: QuickSearchModalProps) => {
const blockSuiteWorkspace = workspace?.blockSuiteWorkspace;
const t = useAFFiNEI18N();
const inputRef = useRef<HTMLInputElement>(null);
const [query, _setQuery] = useState('');
const setQuery = useCallback((query: string) => {
startTransition(() => {
_setQuery(query);
});
}, []);
const [showCreatePage, setShowCreatePage] = useState(true);
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
// Add ‘⌘+K shortcut keys as switches
useEffect(() => {
const keydown = (e: KeyboardEvent) => {
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
const selection = window.getSelection();
// prevent search bar focus in firefox
e.preventDefault();
setQuery('');
if (selection?.toString()) {
setOpen(false);
return;
}
setOpen(!open);
}
};
document.addEventListener('keydown', keydown, { capture: true });
return () =>
document.removeEventListener('keydown', keydown, { capture: true });
}, [open, setOpen, setQuery]);
useEffect(() => {
if (open) {
// Waiting for DOM rendering
requestAnimationFrame(() => {
const inputElement = inputRef.current;
inputElement?.focus();
});
}
}, [open]);
return (
<Modal
open={open}
onOpenChange={setOpen}
width={608}
withoutCloseButton
contentOptions={{
['data-testid' as string]: 'quickSearch',
style: {
maxHeight: '80vh',
minHeight: '412px',
top: '80px',
overflow: 'hidden',
transform: 'translateX(-50%)',
padding: 0,
},
}}
>
<Command
shouldFilter={false}
//Handle KeyboardEvent conflicts with blocksuite
onKeyDown={(e: React.KeyboardEvent) => {
if (
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowRight'
) {
e.stopPropagation();
}
}}
>
<StyledModalHeader>
<SearchInput
ref={inputRef}
onValueChange={value => {
setQuery(value);
}}
onKeyDown={e => {
// Avoid triggering the cmdk onSelect event when the input method is in use
if (e.nativeEvent.isComposing) {
e.stopPropagation();
return;
}
}}
placeholder={t['Quick search placeholder']()}
/>
<StyledShortcut>
{environment.isBrowser && environment.isMacOs
? '⌘ + K'
: 'Ctrl + K'}
</StyledShortcut>
</StyledModalHeader>
<StyledModalDivider />
<Command.List>
<StyledContent>
<Suspense
fallback={
<StyledNotFound>
<span>{t['com.affine.loading']()}</span>
</StyledNotFound>
}
>
<Results
query={query}
onClose={handleClose}
workspace={workspace}
setShowCreatePage={setShowCreatePage}
/>
</Suspense>
</StyledContent>
{showCreatePage ? (
<>
<StyledModalDivider />
<StyledModalFooter>
<Footer
query={query}
onClose={handleClose}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
</StyledModalFooter>
</>
) : null}
</Command.List>
</Command>
</Modal>
);
};
export default QuickSearchModal;

View File

@@ -1,188 +0,0 @@
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { Command } from 'cmdk';
import { type Atom, atom, useAtomValue } from 'jotai';
import type { Dispatch, SetStateAction } from 'react';
import { startTransition, useEffect } from 'react';
import { recentPageSettingsAtom } from '../../../atoms';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import type { AllWorkspace } from '../../../shared';
import { useSwitchToConfig } from './config';
import { StyledListItem, StyledNotFound } from './style';
export interface ResultsProps {
workspace: AllWorkspace;
query: string;
onClose: () => void;
setShowCreatePage: Dispatch<SetStateAction<boolean>>;
}
const loadAllPageWeakMap = new WeakMap<Workspace, Atom<Promise<void>>>();
function getLoadAllPage(workspace: Workspace) {
if (loadAllPageWeakMap.has(workspace)) {
return loadAllPageWeakMap.get(workspace) as Atom<Promise<void>>;
} else {
const aAtom = atom(async () => {
// fixme: we have to load all pages here and re-index them
// there might have performance issue
await Promise.all(
[...workspace.pages.values()].map(page =>
page.waitForLoaded().then(() => {
workspace.indexer.search.refreshPageIndex(page.id, page.spaceDoc);
})
)
);
});
loadAllPageWeakMap.set(workspace, aAtom);
return aAtom;
}
}
export const Results = ({
query,
workspace,
setShowCreatePage,
onClose,
}: ResultsProps) => {
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
assertExists(blockSuiteWorkspace.id);
const list = useSwitchToConfig(workspace.id);
useAtomValue(getLoadAllPage(blockSuiteWorkspace));
const recentPageSetting = useAtomValue(recentPageSettingsAtom);
const t = useAFFiNEI18N();
const { jumpToPage, jumpToSubPath } = useNavigateHelper();
const pageIds = [...blockSuiteWorkspace.search({ query }).values()].map(
id => {
if (id.startsWith('space:')) {
return id.slice(6);
} else {
return id;
}
}
);
const resultsPageMeta = pageList.filter(
page => pageIds.indexOf(page.id) > -1 && !page.trash
);
const recentlyViewedItem = recentPageSetting.filter(recent => {
const page = pageList.find(page => recent.id === page.id);
if (!page) {
return false;
} else {
return page.trash !== true;
}
});
useEffect(() => {
startTransition(() => {
setShowCreatePage(resultsPageMeta.length === 0);
});
}, [resultsPageMeta.length, setShowCreatePage]);
if (!query) {
return (
<>
{recentlyViewedItem.length > 0 && (
<Command.Group heading={t['Recent']()}>
{recentlyViewedItem.map(recent => {
const page = pageList.find(page => recent.id === page.id);
assertExists(page);
return (
<Command.Item
key={page.id}
value={page.id}
onSelect={() => {
onClose();
jumpToPage(blockSuiteWorkspace.id, page.id);
}}
>
<StyledListItem>
{recent.mode === 'edgeless' ? (
<EdgelessIcon />
) : (
<PageIcon />
)}
<span>{page.title || UNTITLED_WORKSPACE_NAME}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
)}
<Command.Group heading={t['Jump to']()}>
{list.map(link => {
return (
<Command.Item
key={link.title}
value={link.title}
onSelect={() => {
onClose();
if ('subPath' in link) {
jumpToSubPath(blockSuiteWorkspace.id, link.subPath);
} else if ('onClick' in link) {
link.onClick();
} else {
throw new Error('Invalid link');
}
}}
>
<StyledListItem>
<link.icon />
<span>{link.title}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
</>
);
}
if (!resultsPageMeta.length) {
return (
<StyledNotFound>
<span>{t['Find 0 result']()}</span>
<img
alt="no result"
src="/imgs/no-result.svg"
width={200}
height={200}
/>
</StyledNotFound>
);
}
return (
<Command.Group
heading={t['Find results']({ number: `${resultsPageMeta.length}` })}
>
{resultsPageMeta.map(result => {
return (
<Command.Item
key={result.id}
onSelect={() => {
onClose();
assertExists(blockSuiteWorkspace.id);
jumpToPage(blockSuiteWorkspace.id, result.id);
}}
value={result.id}
>
<StyledListItem>
{result.mode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
<span>{result.title || UNTITLED_WORKSPACE_NAME}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
);
};

View File

@@ -1,33 +0,0 @@
import { SearchIcon } from '@blocksuite/icons';
import { Command } from 'cmdk';
import { forwardRef } from 'react';
import { StyledInputContent, StyledLabel } from './style';
export const SearchInput = forwardRef<
HTMLInputElement,
Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'value' | 'onChange' | 'type'
> & {
/**
* Optional controlled state for the value of the search input.
*/
value?: string;
/**
* Event handler called when the search value changes.
*/
onValueChange?: (search: string) => void;
} & React.RefAttributes<HTMLInputElement>
>((props, ref) => {
return (
<StyledInputContent>
<StyledLabel htmlFor=":r5:">
<SearchIcon />
</StyledLabel>
<Command.Input ref={ref} {...props} />
</StyledInputContent>
);
});
SearchInput.displayName = 'SearchInput';

View File

@@ -1,180 +0,0 @@
import { displayFlex, styled, textEllipsis } from '@affine/component';
export const StyledContent = styled('div')(() => {
return {
minHeight: '290px',
maxHeight: '70vh',
width: '100%',
overflow: 'auto',
marginBottom: '10px',
...displayFlex('flex-start', 'flex-start'),
flexDirection: 'column',
color: 'var(--affine-text-primary-color)',
transition: 'all 0.15s',
letterSpacing: '0.06em',
'[cmdk-group]': {
width: '100%',
},
'[cmdk-group-heading]': {
...displayFlex('start', 'center'),
margin: '0 16px',
height: '36px',
lineHeight: '22px',
fontSize: 'var(--affine-font-sm)',
color: 'var(--affine-text-secondary-color)',
},
'[cmdk-item]': {
margin: '0 4px',
},
'[aria-selected="true"]': {
transition: 'all 0.15s',
borderRadius: '4px',
color: 'var(--affine-primary-color)',
backgroundColor: 'var(--affine-hover-color)',
padding: '0 2px',
},
};
});
export const StyledJumpTo = styled('div')(() => {
return {
...displayFlex('center', 'start'),
flexDirection: 'column',
padding: '10px 10px 10px 0',
fontSize: 'var(--affine-font-base)',
strong: {
fontWeight: '500',
marginBottom: '10px',
},
};
});
export const StyledNotFound = styled('div')(() => {
return {
width: '612px',
...displayFlex('center', 'center'),
flexDirection: 'column',
padding: '0 16px',
fontSize: 'var(--affine-font-sm)',
lineHeight: '22px',
color: 'var(--affine-text-secondary-color)',
span: {
...displayFlex('flex-start', 'center'),
width: '100%',
fontWeight: '400',
height: '36px',
},
img: {
marginTop: '10px',
},
};
});
export const StyledInputContent = styled('div')(() => {
return {
...displayFlex('space-between', 'center'),
input: {
width: '492px',
height: '22px',
padding: '0 12px',
fontSize: 'var(--affine-font-base)',
...displayFlex('space-between', 'center'),
letterSpacing: '0.06em',
color: 'var(--affine-text-primary-color)',
'::placeholder': {
color: 'var(--affine-placeholder-color)',
},
},
};
});
export const StyledShortcut = styled('div')(() => {
return {
color: 'var(--affine-placeholder-color)',
fontSize: 'var(--affine-font-sm)',
whiteSpace: 'nowrap',
};
});
export const StyledLabel = styled('label')(() => {
return {
width: '20px',
height: '20px',
color: 'var(--affine-icon-color)',
fontSize: '20px',
};
});
export const StyledModalHeader = styled('div')(() => {
return {
height: '36px',
margin: '12px 16px 0px 16px',
...displayFlex('space-between', 'center'),
};
});
export const StyledModalDivider = styled('div')(() => {
return {
width: 'auto',
height: '0',
margin: '6px 16px',
borderTop: '0.5px solid var(--affine-border-color)',
};
});
export const StyledModalFooter = styled('div')(() => {
return {
fontSize: 'inherit',
lineHeight: '22px',
marginBottom: '8px',
textAlign: 'center',
color: 'var(--affine-text-primary-color)',
...displayFlex('center', 'center'),
transition: 'all .15s',
'[cmdk-item]': {
margin: '0 4px',
},
'[aria-selected="true"]': {
transition: 'all 0.15s',
borderRadius: '4px',
color: 'var(--affine-primary-color)',
backgroundColor: 'var(--affine-hover-color)',
'span,svg': {
transition: 'all 0.15s',
transform: 'scale(1.02)',
},
},
};
});
export const StyledModalFooterContent = styled('button')(() => {
return {
width: '600px',
height: '32px',
fontSize: 'var(--affine-font-base)',
lineHeight: '22px',
textAlign: 'center',
...displayFlex('center', 'center'),
color: 'inherit',
borderRadius: '4px',
transition: 'background .15s, color .15s',
'>svg': {
fontSize: '20px',
marginRight: '12px',
},
};
});
export const StyledListItem = styled('button')(() => {
return {
width: '100%',
height: '32px',
fontSize: 'inherit',
color: 'inherit',
padding: '0 12px',
borderRadius: '4px',
transition: 'all .15s',
...displayFlex('flex-start', 'center'),
span: {
...textEllipsis(1),
},
'> svg': {
fontSize: '20px',
marginRight: '12px',
},
};
});

View File

@@ -17,7 +17,7 @@ export const WorkspaceModeFilterTab = () => {
return (
<RadioButtonGroup
width={300}
defaultValue={value}
value={value}
onValueChange={handleValueChange}
>
<RadioButton value="all" style={{ textTransform: 'capitalize' }}>

View File

@@ -6,8 +6,8 @@ import type { Collection } from '@affine/env/filter';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { PlusIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { uuidv4 } from '@blocksuite/store';
import { IconButton } from '@toeverything/components/button';
import { nanoid } from 'nanoid';
import { useCallback, useState } from 'react';
import { useGetPageInfoById } from '../../../../hooks/use-get-page-info';
@@ -28,7 +28,7 @@ export const AddCollectionButton = ({
const handleClick = useCallback(() => {
showUpdateCollection(true);
setDefaultCollection({
id: uuidv4(),
id: nanoid(),
name: '',
pinned: true,
filterList: [],

View File

@@ -24,7 +24,7 @@ import React, { useCallback, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { pageSettingFamily } from '../../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import { useTrashModalHelper } from '../../../../hooks/affine/use-trash-modal-helper';
import { useNavigateHelper } from '../../../../hooks/use-navigate-helper';
import { ReferencePage } from '../components/reference-page';
import * as styles from './styles.css';
@@ -44,8 +44,15 @@ export const PageOperations = ({
inExcludeList: boolean;
addToExcludeList: (id: string) => void;
}) => {
const { removeToTrash } = useBlockSuiteMetaHelper(workspace);
const t = useAFFiNEI18N();
const { setTrashModal } = useTrashModalHelper(workspace);
const onClickDelete = useCallback(() => {
setTrashModal({
open: true,
pageId: page.id,
pageTitle: page.title,
});
}, [page.id, page.title, setTrashModal]);
const actions = useMemo<
Array<
| {
@@ -97,9 +104,7 @@ export const PageOperations = ({
</MenuIcon>
),
name: t['com.affine.trashOperation.delete'](),
click: () => {
removeToTrash(page.id);
},
click: onClickDelete,
type: 'danger',
},
],
@@ -107,10 +112,10 @@ export const PageOperations = ({
inAllowList,
t,
inExcludeList,
onClickDelete,
removeFromAllowList,
page.id,
addToExcludeList,
removeToTrash,
]
);
return (

View File

@@ -14,8 +14,9 @@ export const AddFavouriteButton = ({ workspace }: AddFavouriteButtonProps) => {
const { createPage } = usePageHelper(workspace);
const { setPageMeta } = usePageMetaHelper(workspace);
const handleAddFavorite = useCallback(async () => {
const id = createPage();
setPageMeta(id, { favorite: true });
const page = createPage();
await page.waitForLoaded();
setPageMeta(page.id, { favorite: true });
}, [createPage, setPageMeta]);
return (

View File

@@ -0,0 +1,24 @@
import { style } from '@vanilla-extract/css';
export const ItemContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '8px 14px',
gap: '14px',
cursor: 'pointer',
borderRadius: '8px',
transition: 'background-color 0.2s',
fontSize: '24px',
color: 'var(--affine-icon-secondary)',
});
export const ItemText = style({
fontSize: 'var(--affine-font-sm)',
lineHeight: '22px',
color: 'var(--affine-text-secondary-color)',
fontWeight: 400,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
});

View File

@@ -0,0 +1,44 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ImportIcon, PlusIcon } from '@blocksuite/icons';
import { MenuItem } from '@toeverything/components/menu';
import * as styles from './index.css';
export const AddWorkspace = ({
onAddWorkspace,
onNewWorkspace,
}: {
onAddWorkspace?: () => void;
onNewWorkspace?: () => void;
}) => {
const t = useAFFiNEI18N();
return (
<div>
{runtimeConfig.enableSQLiteProvider && environment.isDesktop ? (
<MenuItem
block={true}
preFix={<ImportIcon />}
onClick={onAddWorkspace}
data-testid="add-workspace"
className={styles.ItemContainer}
>
<div className={styles.ItemText}>
{t['com.affine.workspace.local.import']()}
</div>
</MenuItem>
) : null}
<MenuItem
block={true}
preFix={<PlusIcon />}
onClick={onNewWorkspace}
data-testid="new-workspace"
className={styles.ItemContainer}
>
<div className={styles.ItemText}>
{t['com.affine.workspaceList.addWorkspace.create']()}
</div>
</MenuItem>
</div>
);
};

View File

@@ -0,0 +1,57 @@
import { style } from '@vanilla-extract/css';
export const workspaceListWrapper = style({
display: 'flex',
width: '100%',
flexDirection: 'column',
});
export const signInWrapper = style({
display: 'flex',
width: '100%',
gap: '12px',
alignItems: 'center',
justifyContent: 'flex-start',
borderRadius: '8px',
});
export const iconContainer = style({
width: '28px',
padding: '2px 4px 4px',
borderRadius: '14px',
background: 'var(--affine-white)',
display: 'flex',
border: '1px solid var(--affine-icon-secondary)',
color: 'var(--affine-icon-secondary)',
alignItems: 'center',
justifyContent: 'center',
fontSize: '20px',
});
export const signInTextContainer = style({
display: 'flex',
flexDirection: 'column',
});
export const signInTextPrimary = style({
fontSize: 'var(--affine-font-sm)',
fontWeight: 600,
lineHeight: '22px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const signInTextSecondary = style({
fontSize: 'var(--affine-font-xs)',
fontWeight: 400,
lineHeight: '20px',
color: 'var(--affine-text-secondary-color)',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const menuItem = style({
borderRadius: '8px',
});

View File

@@ -1,155 +1,62 @@
import { WorkspaceList } from '@affine/component/workspace-list';
import type {
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import {
AccountIcon,
ImportIcon,
Logo1Icon,
MoreHorizontalIcon,
PlusIcon,
SignOutIcon,
} from '@blocksuite/icons';
import type { DragEndEvent } from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable';
import { IconButton } from '@toeverything/components/button';
import { Logo1Icon } from '@blocksuite/icons';
import { Divider } from '@toeverything/components/divider';
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { MenuItem } from '@toeverything/components/menu';
import { useAtomValue, useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react';
import { startTransition, useCallback, useMemo, useTransition } from 'react';
import { useCallback, useMemo } from 'react';
import {
authAtom,
openCreateWorkspaceModalAtom,
openDisableCloudAlertModalAtom,
openSettingModalAtom,
} from '../../../../atoms';
import type { AllWorkspace } from '../../../../shared';
import { signOutCloud } from '../../../../utils/cloud-utils';
import { useNavigateHelper } from '../.././../../hooks/use-navigate-helper';
import {
StyledCreateWorkspaceCardPill,
StyledCreateWorkspaceCardPillContent,
StyledCreateWorkspaceCardPillIcon,
StyledImportWorkspaceCardPill,
StyledItem,
StyledModalBody,
StyledModalContent,
StyledModalFooterContent,
StyledModalHeader,
StyledModalHeaderContent,
StyledModalHeaderLeft,
StyledModalTitle,
StyledOperationWrapper,
StyledSignInCardPill,
StyledSignInCardPillTextCotainer,
StyledSignInCardPillTextPrimary,
StyledSignInCardPillTextSecondary,
StyledWorkspaceFlavourTitle,
} from './styles';
import { AddWorkspace } from './add-workspace';
import * as styles from './index.css';
import { UserAccountItem } from './user-account';
import { AFFiNEWorkspaceList } from './workspace-list';
interface WorkspaceModalProps {
disabled?: boolean;
workspaces: RootWorkspaceMetadata[];
currentWorkspaceId: AllWorkspace['id'] | null;
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
onNewWorkspace: () => void;
onAddWorkspace: () => void;
onMoveWorkspace: (activeId: string, overId: string) => void;
}
const SignInItem = () => {
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
const setOpen = useSetAtom(authAtom);
const AccountMenu = ({
onOpenAccountSetting,
onSignOut,
}: {
onOpenAccountSetting: () => void;
onSignOut: () => void;
}) => {
const t = useAFFiNEI18N();
return (
<div>
<MenuItem
preFix={
<MenuIcon>
<AccountIcon />
</MenuIcon>
}
data-testid="editor-option-menu-import"
onClick={onOpenAccountSetting}
>
{t['com.affine.workspace.cloud.account.settings']()}
</MenuItem>
<Divider />
<MenuItem
preFix={
<MenuIcon>
<SignOutIcon />
</MenuIcon>
}
data-testid="editor-option-menu-import"
onClick={onSignOut}
>
{t['com.affine.workspace.cloud.account.logout']()}
</MenuItem>
</div>
);
};
const CloudWorkSpaceList = ({
disabled,
workspaces,
onClickWorkspace,
onClickWorkspaceSetting,
currentWorkspaceId,
onMoveWorkspace,
}: WorkspaceModalProps) => {
const t = useAFFiNEI18N();
const onClickSignIn = useCallback(async () => {
if (!runtimeConfig.enableCloud) {
setDisableCloudOpen(true);
} else {
setOpen(state => ({
...state,
openModal: true,
}));
}
}, [setOpen, setDisableCloudOpen]);
return (
<>
<StyledModalHeader>
<StyledModalHeaderLeft>
<StyledWorkspaceFlavourTitle>
{t['com.affine.workspace.cloud']()}
</StyledWorkspaceFlavourTitle>
</StyledModalHeaderLeft>
</StyledModalHeader>
<StyledModalContent>
<WorkspaceList
disabled={disabled}
items={
workspaces.filter(
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
) as (AffineCloudWorkspace | LocalWorkspace)[]
}
currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace}
onSettingClick={onClickWorkspaceSetting}
onDragEnd={useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
onMoveWorkspace(active.id as string, over?.id as string);
}
},
[onMoveWorkspace]
)}
/>
</StyledModalContent>
</>
<MenuItem
className={styles.menuItem}
onClick={onClickSignIn}
data-testid="cloud-signin-button"
>
<div className={styles.signInWrapper}>
<div className={styles.iconContainer}>
<Logo1Icon />
</div>
<div className={styles.signInTextContainer}>
<div className={styles.signInTextPrimary}>
{t['com.affine.workspace.cloud.auth']()}
</div>
<div className={styles.signInTextSecondary}>
{t['com.affine.workspace.cloud.description']()}
</div>
</div>
</div>
</MenuItem>
);
};
@@ -158,240 +65,43 @@ export const UserWithWorkspaceList = ({
}: {
onEventEnd?: () => void;
}) => {
const { data: session, status } = useSession();
const isAuthenticated = useMemo(() => status === 'authenticated', [status]);
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
const workspaces = useAtomValue(rootWorkspacesMetadataAtom, {
delay: 0,
});
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
currentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(currentPageIdAtom);
const [, startCloseTransition] = useTransition();
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
const t = useAFFiNEI18N();
const setOpen = useSetAtom(authAtom);
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
// TODO: AFFiNE Cloud support
const { data: session, status } = useSession();
const isLoggedIn = useMemo(() => status === 'authenticated', [status]);
const cloudWorkspaces = useMemo(
() =>
workspaces.filter(
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
) as (AffineCloudWorkspace | LocalWorkspace)[],
[workspaces]
);
const localWorkspaces = useMemo(
() =>
workspaces.filter(
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
) as (AffineCloudWorkspace | LocalWorkspace)[],
[workspaces]
);
const onClickWorkspaceSetting = useCallback(
(workspaceId: string) => {
setOpenSettingModalAtom({
open: true,
activeTab: 'workspace',
workspaceId,
});
onEventEnd?.();
},
[onEventEnd, setOpenSettingModalAtom]
);
const onMoveWorkspace = useCallback(
(activeId: string, overId: string) => {
const oldIndex = workspaces.findIndex(w => w.id === activeId);
const newIndex = workspaces.findIndex(w => w.id === overId);
startTransition(() => {
setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
});
},
[setWorkspaces, workspaces]
);
const onClickWorkspace = useCallback(
(workspaceId: string) => {
startCloseTransition(() => {
setCurrentWorkspaceId(workspaceId);
setCurrentPageId(null);
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL);
});
onEventEnd?.();
},
[jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId]
);
const onNewWorkspace = useCallback(() => {
setOpenCreateWorkspaceModal('new');
onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]);
const onAddWorkspace = useCallback(async () => {
setOpenCreateWorkspaceModal('add');
onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]);
const onOpenAccountSetting = useCallback(() => {
setSettingModalAtom(prev => ({
...prev,
open: true,
activeTab: 'account',
}));
onEventEnd?.();
}, [onEventEnd, setSettingModalAtom]);
const onSignOut = useCallback(async () => {
signOutCloud()
.then(() => {
jumpToIndex();
})
.catch(console.error);
onEventEnd?.();
}, [onEventEnd, jumpToIndex]);
const workspaces = useAtomValue(rootWorkspacesMetadataAtom, {
delay: 0,
});
return (
<>
{!isLoggedIn ? (
<StyledModalHeaderContent>
<StyledSignInCardPill>
<StyledItem
onClick={async () => {
if (!runtimeConfig.enableCloud) {
setDisableCloudOpen(true);
} else {
setOpen(state => ({
...state,
openModal: true,
}));
}
}}
data-testid="cloud-signin-button"
>
<StyledCreateWorkspaceCardPillContent>
<StyledCreateWorkspaceCardPillIcon>
<Logo1Icon />
</StyledCreateWorkspaceCardPillIcon>
<StyledSignInCardPillTextCotainer>
<StyledSignInCardPillTextPrimary>
{t['com.affine.workspace.cloud.auth']()}
</StyledSignInCardPillTextPrimary>
<StyledSignInCardPillTextSecondary>
{t['com.affine.workspace.cloud.description']()}
</StyledSignInCardPillTextSecondary>
</StyledSignInCardPillTextCotainer>
</StyledCreateWorkspaceCardPillContent>
</StyledItem>
</StyledSignInCardPill>
<Divider
style={{
margin: '12px 0px',
}}
/>
</StyledModalHeaderContent>
<div className={styles.workspaceListWrapper}>
{isAuthenticated ? (
<UserAccountItem
email={session?.user.email ?? 'Unknown User'}
onEventEnd={onEventEnd}
/>
) : (
<StyledModalHeaderContent>
<StyledModalHeader>
<StyledModalTitle>{session?.user.email}</StyledModalTitle>
<StyledOperationWrapper>
<Menu
items={
<AccountMenu
onOpenAccountSetting={onOpenAccountSetting}
onSignOut={onSignOut}
/>
}
contentOptions={{
side: 'right',
sideOffset: 30,
}}
>
<IconButton
data-testid="more-button"
icon={<MoreHorizontalIcon />}
type="plain"
/>
</Menu>
</StyledOperationWrapper>
</StyledModalHeader>
<Divider style={{ margin: '12px 0px' }} />
</StyledModalHeaderContent>
<SignInItem />
)}
<StyledModalBody>
{isLoggedIn && cloudWorkspaces.length !== 0 ? (
<>
<CloudWorkSpaceList
workspaces={workspaces}
onClickWorkspace={onClickWorkspace}
onClickWorkspaceSetting={onClickWorkspaceSetting}
onNewWorkspace={onNewWorkspace}
onAddWorkspace={onAddWorkspace}
currentWorkspaceId={currentWorkspaceId}
onMoveWorkspace={onMoveWorkspace}
/>
<Divider
style={{
margin: '12px 0px',
}}
/>
</>
) : null}
<StyledModalHeader>
<StyledWorkspaceFlavourTitle>
{t['com.affine.workspace.local']()}
</StyledWorkspaceFlavourTitle>
</StyledModalHeader>
<StyledModalContent>
<WorkspaceList
items={localWorkspaces}
currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace}
onSettingClick={onClickWorkspaceSetting}
onDragEnd={useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
onMoveWorkspace(active.id as string, over?.id as string);
}
},
[onMoveWorkspace]
)}
/>
</StyledModalContent>
{runtimeConfig.enableSQLiteProvider && environment.isDesktop ? (
<StyledImportWorkspaceCardPill>
<StyledItem onClick={onAddWorkspace} data-testid="add-workspace">
<StyledCreateWorkspaceCardPillContent
style={{ gap: '14px', paddingLeft: '2px' }}
>
<StyledCreateWorkspaceCardPillIcon style={{ fontSize: '24px' }}>
<ImportIcon />
</StyledCreateWorkspaceCardPillIcon>
<div>
<p>{t['com.affine.workspace.local.import']()}</p>
</div>
</StyledCreateWorkspaceCardPillContent>
</StyledItem>
</StyledImportWorkspaceCardPill>
) : null}
</StyledModalBody>
<StyledModalFooterContent>
<StyledCreateWorkspaceCardPill>
<StyledItem onClick={onNewWorkspace} data-testid="new-workspace">
<StyledCreateWorkspaceCardPillContent>
<StyledCreateWorkspaceCardPillIcon>
<PlusIcon />
</StyledCreateWorkspaceCardPillIcon>
<div>
<p>{t['New Workspace']()}</p>
</div>
</StyledCreateWorkspaceCardPillContent>
</StyledItem>
</StyledCreateWorkspaceCardPill>
</StyledModalFooterContent>
</>
<Divider size="thinner" />
<AFFiNEWorkspaceList workspaces={workspaces} onEventEnd={onEventEnd} />
{workspaces.length > 0 ? <Divider size="thinner" /> : null}
<AddWorkspace
onAddWorkspace={onAddWorkspace}
onNewWorkspace={onNewWorkspace}
/>
</div>
);
};

View File

@@ -1,273 +0,0 @@
import { displayFlex, styled, textEllipsis } from '@affine/component';
export const StyledSplitLine = styled('div')(() => {
return {
width: '1px',
height: '20px',
background: 'var(--affine-border-color)',
marginRight: '12px',
};
});
export const StyleWorkspaceInfo = styled('div')(() => {
return {
marginLeft: '15px',
width: '202px',
p: {
height: '20px',
fontSize: 'var(--affine-font-sm)',
...displayFlex('flex-start', 'center'),
},
svg: {
marginRight: '10px',
fontSize: '16px',
flexShrink: 0,
},
span: {
flexGrow: 1,
...textEllipsis(1),
},
};
});
export const StyleWorkspaceTitle = styled('div')(() => {
return {
fontSize: 'var(--affine-font-base)',
fontWeight: 600,
lineHeight: '24px',
marginBottom: '10px',
maxWidth: '200px',
...textEllipsis(1),
};
});
export const StyledCreateWorkspaceCard = styled('div')(() => {
return {
width: '310px',
height: '124px',
marginBottom: '24px',
cursor: 'pointer',
padding: '16px',
boxShadow: 'var(--affine-shadow-1)',
borderRadius: '12px',
transition: 'all .1s',
background: 'var(--affine-white-80)',
...displayFlex('flex-start', 'flex-start'),
color: 'var(--affine-text-secondary-color)',
':hover': {
background: 'var(--affine-hover-color)',
color: 'var(--affine-text-primary-color)',
'.add-icon': {
borderColor: 'var(--affine-white)',
color: 'var(--affine-primary-color)',
},
},
'@media (max-width: 720px)': {
width: '100%',
},
};
});
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
return {
borderRadius: '10px',
display: 'flex',
margin: '-8px -4px',
flexFlow: 'column',
gap: '12px',
background: 'var(--affine-background-overlay-panel-color)',
};
});
export const StyledCreateWorkspaceCardPill = styled('div')(() => {
return {
borderRadius: '8px',
display: 'flex',
width: '100%',
height: '58px',
border: `1px solid var(--affine-border-color)`,
};
});
export const StyledSignInCardPill = styled('div')(() => {
return {
borderRadius: '8px',
display: 'flex',
width: '100%',
height: '58px',
};
});
export const StyledImportWorkspaceCardPill = styled('div')(() => {
return {
borderRadius: '5px',
display: 'flex',
width: '100%',
};
});
export const StyledCreateWorkspaceCardPillContent = styled('div')(() => {
return {
display: 'flex',
gap: '12px',
alignItems: 'center',
};
});
export const StyledCreateWorkspaceCardPillIcon = styled('div')(() => {
return {
fontSize: '28px',
width: '1em',
height: '1em',
};
});
export const StyledSignInCardPillTextCotainer = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
};
});
export const StyledSignInCardPillTextSecondary = styled('div')(() => {
return {
fontSize: '12px',
color: 'var(--affine-text-secondary-color)',
};
});
export const StyledSignInCardPillTextPrimary = styled('div')(() => {
return {
fontSize: 'var(--affine-font-base)',
fontWeight: 600,
lineHeight: '24px',
maxWidth: '200px',
textAlign: 'left',
...textEllipsis(1),
};
});
export const StyledModalHeaderLeft = styled('div')(() => {
return { ...displayFlex('flex-start', 'center') };
});
export const StyledModalTitle = styled('div')(() => {
return {
fontWeight: 600,
fontSize: 'var(--affine-font-h6)',
color: 'var(--affine-text-primary-color)',
};
});
export const StyledHelperContainer = styled('div')(() => {
return {
color: 'var(--affine-icon-color)',
marginLeft: '15px',
fontWeight: 400,
fontSize: 'var(--affine-font-h6)',
...displayFlex('center', 'center'),
};
});
export const StyledModalContent = styled('div')({
...displayFlex('space-between', 'flex-start', 'flex-start'),
flexWrap: 'wrap',
flexDirection: 'column',
width: '100%',
gap: '4px',
});
export const StyledModalFooterContent = styled('div')({
...displayFlex('space-between', 'flex-start', 'flex-start'),
flexWrap: 'wrap',
flexDirection: 'column',
width: '100%',
marginTop: '12px',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
});
export const StyledModalHeaderContent = styled('div')({
...displayFlex('space-between', 'flex-start', 'flex-start'),
flexWrap: 'wrap',
flexDirection: 'column',
width: '100%',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
});
export const StyledOperationWrapper = styled('div')(() => {
return {
...displayFlex('flex-end', 'center'),
};
});
export const StyleWorkspaceAdd = styled('div')(() => {
return {
width: '58px',
height: '58px',
borderRadius: '100%',
background: 'var(--affine-background-overlay-panel-color)',
border: '1.5px dashed #f4f5fa',
transition: 'background .2s',
fontSize: '24px',
...displayFlex('center', 'center'),
borderColor: 'var(--affine-white)',
color: 'var(--affine-background-overlay-panel-color)',
};
});
export const StyledModalHeader = styled('div')(() => {
return {
width: '100%',
left: 0,
top: 0,
borderRadius: '24px 24px 0 0',
padding: '0px 14px',
...displayFlex('space-between', 'center'),
};
});
export const StyledModalBody = styled('div')(() => {
return {
display: 'inline-flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: '4px',
flex: 1,
overflowY: 'auto',
};
});
export const StyledWorkspaceFlavourTitle = styled('div')(() => {
return {
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
marginBottom: '4px',
};
});
export const StyledItem = styled('button')<{
active?: boolean;
}>(({ active = false }) => {
return {
height: 'auto',
padding: '8px 12px',
width: '100%',
borderRadius: '5px',
fontSize: 'var(--affine-font-sm)',
...displayFlex('flex-start', 'center'),
cursor: 'pointer',
position: 'relative',
backgroundColor: 'transparent',
color: 'var(--affine-text-primary-color)',
svg: {
color: 'var(--affine-icon-color)',
},
':hover': {
backgroundColor: 'var(--affine-hover-color)',
},
...(active
? {
backgroundColor: 'var(--affine-hover-color)',
}
: {}),
};
});

View File

@@ -0,0 +1,18 @@
import { style } from '@vanilla-extract/css';
export const userAccountContainer = style({
display: 'flex',
padding: '4px 0px 4px 12px',
gap: '12px',
alignItems: 'center',
justifyContent: 'space-between',
});
export const userEmail = style({
fontSize: 'var(--affine-font-sm)',
fontWeight: 400,
lineHeight: '22px',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
maxWidth: 'calc(100% - 36px)',
});

View File

@@ -0,0 +1,96 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
AccountIcon,
MoreHorizontalIcon,
SignOutIcon,
} from '@blocksuite/icons';
import { IconButton } from '@toeverything/components/button';
import { Divider } from '@toeverything/components/divider';
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { openSettingModalAtom } from '../../../../../atoms';
import { signOutCloud } from '../../../../../utils/cloud-utils';
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
import * as styles from './index.css';
const AccountMenu = ({ onEventEnd }: { onEventEnd?: () => void }) => {
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
const { jumpToIndex } = useNavigateHelper();
const onOpenAccountSetting = useCallback(() => {
setSettingModalAtom(prev => ({
...prev,
open: true,
activeTab: 'account',
}));
}, [setSettingModalAtom]);
const onSignOut = useCallback(async () => {
signOutCloud()
.then(() => {
jumpToIndex();
})
.catch(console.error);
onEventEnd?.();
}, [onEventEnd, jumpToIndex]);
const t = useAFFiNEI18N();
return (
<div>
<MenuItem
preFix={
<MenuIcon>
<AccountIcon />
</MenuIcon>
}
data-testid="editor-option-menu-import"
onClick={onOpenAccountSetting}
>
{t['com.affine.workspace.cloud.account.settings']()}
</MenuItem>
<Divider />
<MenuItem
preFix={
<MenuIcon>
<SignOutIcon />
</MenuIcon>
}
data-testid="editor-option-menu-import"
onClick={onSignOut}
>
{t['com.affine.workspace.cloud.account.logout']()}
</MenuItem>
</div>
);
};
export const UserAccountItem = ({
email,
onEventEnd,
}: {
email: string;
onEventEnd?: () => void;
}) => {
return (
<div className={styles.userAccountContainer}>
<div className={styles.userEmail}>{email}</div>
<Menu
items={<AccountMenu onEventEnd={onEventEnd} />}
contentOptions={{
side: 'right',
sideOffset: 12,
}}
>
<IconButton
data-testid="more-button"
icon={<MoreHorizontalIcon />}
type="plain"
/>
</Menu>
</div>
);
};

View File

@@ -0,0 +1,29 @@
import { style } from '@vanilla-extract/css';
export const workspaceListsWrapper = style({
display: 'flex',
width: '100%',
flexDirection: 'column',
maxHeight: 'calc(100vh - 300px)',
});
export const workspaceListWrapper = style({
display: 'flex',
width: '100%',
flexDirection: 'column',
gap: '4px',
});
export const workspaceType = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0px 12px',
fontSize: 'var(--affine-font-xs)',
lineHeight: '20px',
color: 'var(--affine-text-secondary-color)',
});
export const scrollbar = style({
transform: 'translateX(10px)',
width: '4px',
});

View File

@@ -0,0 +1,233 @@
import { ScrollableContainer } from '@affine/component';
import { WorkspaceList } from '@affine/component/workspace-list';
import type {
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { DragEndEvent } from '@dnd-kit/core';
import { arrayMove } from '@dnd-kit/sortable';
import { Divider } from '@toeverything/components/divider';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtom, useSetAtom } from 'jotai';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react';
import { startTransition, useCallback, useMemo, useTransition } from 'react';
import {
openCreateWorkspaceModalAtom,
openSettingModalAtom,
} from '../../../../../atoms';
import type { AllWorkspace } from '../../../../../shared';
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
import * as styles from './index.css';
interface WorkspaceModalProps {
disabled?: boolean;
workspaces: (AffineCloudWorkspace | LocalWorkspace)[];
currentWorkspaceId: AllWorkspace['id'] | null;
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
onNewWorkspace: () => void;
onAddWorkspace: () => void;
onDragEnd: (event: DragEndEvent) => void;
}
const CloudWorkSpaceList = ({
disabled,
workspaces,
onClickWorkspace,
onClickWorkspaceSetting,
currentWorkspaceId,
onDragEnd,
}: WorkspaceModalProps) => {
const t = useAFFiNEI18N();
if (workspaces.length === 0) {
return null;
}
return (
<div className={styles.workspaceListWrapper}>
<div className={styles.workspaceType}>
{t['com.affine.workspaceList.workspaceListType.cloud']()}
</div>
<WorkspaceList
disabled={disabled}
items={workspaces}
currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace}
onSettingClick={onClickWorkspaceSetting}
onDragEnd={onDragEnd}
useIsWorkspaceOwner={useIsWorkspaceOwner}
/>
</div>
);
};
const LocalWorkspaces = ({
disabled,
workspaces,
onClickWorkspace,
onClickWorkspaceSetting,
currentWorkspaceId,
onDragEnd,
}: WorkspaceModalProps) => {
const t = useAFFiNEI18N();
if (workspaces.length === 0) {
return null;
}
return (
<div className={styles.workspaceListWrapper}>
<div className={styles.workspaceType}>
{t['com.affine.workspaceList.workspaceListType.local']()}
</div>
<WorkspaceList
disabled={disabled}
items={workspaces}
currentWorkspaceId={currentWorkspaceId}
onClick={onClickWorkspace}
onSettingClick={onClickWorkspaceSetting}
onDragEnd={onDragEnd}
/>
</div>
);
};
export const AFFiNEWorkspaceList = ({
workspaces,
onEventEnd,
}: {
workspaces: RootWorkspaceMetadata[];
onEventEnd?: () => void;
}) => {
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
const { jumpToSubPath } = useNavigateHelper();
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
currentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(currentPageIdAtom);
const [, startCloseTransition] = useTransition();
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
// TODO: AFFiNE Cloud support
const { status } = useSession();
const isAuthenticated = useMemo(() => status === 'authenticated', [status]);
const cloudWorkspaces = useMemo(
() =>
workspaces.filter(
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
) as (AffineCloudWorkspace | LocalWorkspace)[],
[workspaces]
);
const localWorkspaces = useMemo(
() =>
workspaces.filter(
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
) as (AffineCloudWorkspace | LocalWorkspace)[],
[workspaces]
);
const onClickWorkspaceSetting = useCallback(
(workspaceId: string) => {
setOpenSettingModalAtom({
open: true,
activeTab: 'workspace',
workspaceId,
});
onEventEnd?.();
},
[onEventEnd, setOpenSettingModalAtom]
);
const onMoveWorkspace = useCallback(
(activeId: string, overId: string) => {
const oldIndex = workspaces.findIndex(w => w.id === activeId);
const newIndex = workspaces.findIndex(w => w.id === overId);
startTransition(() => {
setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
});
},
[setWorkspaces, workspaces]
);
const onClickWorkspace = useCallback(
(workspaceId: string) => {
startCloseTransition(() => {
setCurrentWorkspaceId(workspaceId);
setCurrentPageId(null);
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL);
});
onEventEnd?.();
},
[jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId]
);
const onDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event;
if (active.id !== over?.id) {
onMoveWorkspace(active.id as string, over?.id as string);
}
},
[onMoveWorkspace]
);
const onNewWorkspace = useCallback(() => {
setOpenCreateWorkspaceModal('new');
onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]);
const onAddWorkspace = useCallback(async () => {
setOpenCreateWorkspaceModal('add');
onEventEnd?.();
}, [onEventEnd, setOpenCreateWorkspaceModal]);
return (
<ScrollableContainer
className={styles.workspaceListsWrapper}
scrollBarClassName={styles.scrollbar}
>
{isAuthenticated ? (
<div>
<CloudWorkSpaceList
workspaces={cloudWorkspaces}
onClickWorkspace={onClickWorkspace}
onClickWorkspaceSetting={onClickWorkspaceSetting}
onNewWorkspace={onNewWorkspace}
onAddWorkspace={onAddWorkspace}
currentWorkspaceId={currentWorkspaceId}
onDragEnd={onDragEnd}
/>
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
<Divider size="thinner" />
) : null}
</div>
) : null}
<LocalWorkspaces
workspaces={localWorkspaces}
onClickWorkspace={onClickWorkspace}
onClickWorkspaceSetting={onClickWorkspaceSetting}
onNewWorkspace={onNewWorkspace}
onAddWorkspace={onAddWorkspace}
currentWorkspaceId={currentWorkspaceId}
onDragEnd={onDragEnd}
/>
</ScrollableContainer>
);
};

View File

@@ -30,6 +30,7 @@ import {
} from './styles';
const hoverAtom = atom(false);
// FIXME:
// 1. Remove mui style
// 2. Refactor the code to improve readability
@@ -41,6 +42,7 @@ const CloudWorkspaceStatus = () => {
</>
);
};
const SyncingWorkspaceStatus = () => {
return (
<>
@@ -49,6 +51,7 @@ const SyncingWorkspaceStatus = () => {
</>
);
};
const UnSyncWorkspaceStatus = () => {
return (
<>
@@ -82,11 +85,14 @@ const WorkspaceStatus = ({
currentWorkspace: AllWorkspace;
}) => {
const isOnline = useSystemOnline();
// todo: finish display sync status
const [forceSyncStatus, startForceSync] = useDatasourceSync(
currentWorkspace.blockSuiteWorkspace
);
const setIsHovered = useSetAtom(hoverAtom);
const content = useMemo(() => {
if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
return 'Saved locally';
@@ -103,6 +109,7 @@ const WorkspaceStatus = ({
return 'Sync with AFFiNE Cloud';
}
}, [currentWorkspace.flavour, forceSyncStatus.type, isOnline]);
const CloudWorkspaceSyncStatus = useCallback(() => {
if (forceSyncStatus.type === 'syncing') {
return SyncingWorkspaceStatus();
@@ -160,6 +167,7 @@ export const WorkspaceCard = forwardRef<
const [name] = useBlockSuiteWorkspaceName(
currentWorkspace.blockSuiteWorkspace
);
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(
currentWorkspace.blockSuiteWorkspace
);

View File

@@ -6,6 +6,7 @@ export const StyledSelectorContainer = styled('div')({
alignItems: 'center',
padding: '0 6px',
borderRadius: '8px',
outline: 'none',
color: 'var(--affine-text-primary-color)',
':hover': {
cursor: 'pointer',

View File

@@ -10,7 +10,7 @@ import {
SidebarContainer,
SidebarScrollableContainer,
} from '@affine/component/app-sidebar';
import { useCollectionManager } from '@affine/component/page-list';
import { MoveToTrash, useCollectionManager } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
DeleteTemporarilyIcon,
@@ -19,20 +19,15 @@ import {
} from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { useDroppable } from '@dnd-kit/core';
import { Popover } from '@toeverything/components/popover';
import { useAtom } from 'jotai';
import { Menu } from '@toeverything/components/menu';
import { useAtom, useAtomValue } from 'jotai';
import type { HTMLAttributes, ReactElement } from 'react';
import {
forwardRef,
Suspense,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { forwardRef, useCallback, useEffect, useMemo } from 'react';
import { openWorkspaceListModalAtom } from '../../atoms';
import { useHistoryAtom } from '../../atoms/history';
import { useAppSetting } from '../../atoms/settings';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
import type { AllWorkspace } from '../../shared';
import { currentCollectionsAtom } from '../../utils/user-setting';
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
@@ -107,13 +102,29 @@ export const RootAppSidebar = ({
const { backToAll } = useCollectionManager(currentCollectionsAtom);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const t = useAFFiNEI18N();
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useState(false);
const [openUserWorkspaceList, setOpenUserWorkspaceList] = useAtom(
openWorkspaceListModalAtom
);
const onClickNewPage = useCallback(async () => {
const page = createPage();
await page.waitForLoaded();
openPage(page.id);
}, [createPage, openPage]);
const { trashModal, setTrashModal, handleOnConfirm } =
useTrashModalHelper(blockSuiteWorkspace);
const deletePageTitle = trashModal.pageTitle;
const trashConfirmOpen = trashModal.open;
const onTrashConfirmOpenChange = useCallback(
(open: boolean) => {
setTrashModal({
...trashModal,
open,
});
},
[trashModal, setTrashModal]
);
// Listen to the "New Page" action from the menu
useEffect(() => {
if (environment.isDesktop) {
@@ -122,7 +133,7 @@ export const RootAppSidebar = ({
return;
}, [onClickNewPage]);
const [sidebarOpen, setSidebarOpen] = useAtom(appSidebarOpenAtom);
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
useEffect(() => {
if (environment.isDesktop) {
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen).catch(err => {
@@ -131,17 +142,6 @@ export const RootAppSidebar = ({
}
}, [sidebarOpen]);
useEffect(() => {
const keydown = (e: KeyboardEvent) => {
if ((e.key === '/' && e.metaKey) || (e.key === '/' && e.ctrlKey)) {
setSidebarOpen(!sidebarOpen);
}
};
document.addEventListener('keydown', keydown, { capture: true });
return () =>
document.removeEventListener('keydown', keydown, { capture: true });
}, [sidebarOpen, setSidebarOpen]);
const [history, setHistory] = useHistoryAtom();
const router = useMemo(() => {
return {
@@ -160,7 +160,7 @@ export const RootAppSidebar = ({
});
const closeUserWorkspaceList = useCallback(() => {
setOpenUserWorkspaceList(false);
}, []);
}, [setOpenUserWorkspaceList]);
return (
<>
@@ -174,28 +174,37 @@ export const RootAppSidebar = ({
)
}
>
<MoveToTrash.ConfirmModal
open={trashConfirmOpen}
onConfirm={handleOnConfirm}
onOpenChange={onTrashConfirmOpenChange}
title={deletePageTitle}
/>
<SidebarContainer>
<Popover
open={openUserWorkspaceList}
content={
<Suspense>
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
</Suspense>
<Menu
rootOptions={{
open: openUserWorkspaceList,
}}
items={
<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />
}
contentOptions={{
// hide trigger
sideOffset: -58,
onInteractOutside: closeUserWorkspaceList,
onEscapeKeyDown: closeUserWorkspaceList,
style: {
width: '300px',
},
}}
>
<WorkspaceCard
currentWorkspace={currentWorkspace}
onClick={useCallback(() => {
setOpenUserWorkspaceList(true);
}, [])}
}, [setOpenUserWorkspaceList])}
/>
</Popover>
</Menu>
<QuickSearchInput
data-testid="slider-bar-quick-search-button"
onClick={onOpenQuickSearchModal}

View File

@@ -2,8 +2,11 @@ import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { setPageModeAtom } from '../../atoms';
import { currentModeAtom } from '../../atoms/mode';
import type { BlockSuiteWorkspace } from '../../shared';
import { useReferenceLinkHelper } from './use-reference-link-helper';
@@ -14,6 +17,27 @@ export function useBlockSuiteMetaHelper(
usePageMetaHelper(blockSuiteWorkspace);
const { addReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
const setPageMode = useSetAtom(setPageModeAtom);
const currentMode = useAtomValue(currentModeAtom);
const switchToPageMode = useCallback(
(pageId: string) => {
setPageMode(pageId, 'page');
},
[setPageMode]
);
const switchToEdgelessMode = useCallback(
(pageId: string) => {
setPageMode(pageId, 'edgeless');
},
[setPageMode]
);
const togglePageMode = useCallback(
(pageId: string) => {
setPageMode(pageId, currentMode === 'edgeless' ? 'page' : 'edgeless');
},
[currentMode, setPageMode]
);
const addToFavorite = useCallback(
(pageId: string) => {
@@ -115,6 +139,10 @@ export function useBlockSuiteMetaHelper(
);
return {
switchToPageMode,
switchToEdgelessMode,
togglePageMode,
publicPage,
cancelPublicPage,

View File

@@ -0,0 +1,79 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { PageBlockModel } from '@blocksuite/blocks';
import { ContentParser } from '@blocksuite/blocks/content-parser';
import type { Page } from '@blocksuite/store';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
type ExportType = 'pdf' | 'html' | 'png' | 'markdown';
const typeToContentParserMethodMap = {
pdf: 'exportPdf',
html: 'exportHtml',
png: 'exportPng',
markdown: 'exportMarkdown',
} satisfies Record<ExportType, keyof ContentParser>;
const contentParserWeakMap = new WeakMap<Page, ContentParser>();
const getContentParser = (page: Page) => {
if (!contentParserWeakMap.has(page)) {
contentParserWeakMap.set(
page,
new ContentParser(page, {
imageProxyEndpoint: !environment.isDesktop
? runtimeConfig.imageProxyUrl
: undefined,
})
);
}
return contentParserWeakMap.get(page) as ContentParser;
};
interface ExportHandlerOptions {
page: Page;
type: ExportType;
}
async function exportHandler({ page, type }: ExportHandlerOptions) {
if (type === 'pdf' && environment.isDesktop && page.meta.mode === 'page') {
window.apis?.export.savePDFFileAs(
(page.root as PageBlockModel).title.toString()
);
} else {
const contentParser = getContentParser(page);
const method = typeToContentParserMethodMap[type];
await contentParser[method]();
}
}
export const useExportPage = (page: Page) => {
const pushNotification = useSetAtom(pushNotificationAtom);
const t = useAFFiNEI18N();
const onClickHandler = useCallback(
async (type: ExportType) => {
try {
await exportHandler({
page,
type,
});
pushNotification({
title: t['com.affine.export.success.title'](),
message: t['com.affine.export.success.message'](),
type: 'success',
});
} catch (err) {
console.error(err);
pushNotification({
title: t['com.affine.export.error.title'](),
message: t['com.affine.export.error.message'](),
type: 'error',
});
}
},
[page, pushNotification, t]
);
return onClickHandler;
};

View File

@@ -0,0 +1,208 @@
import { toast } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import {
PreconditionStrategy,
registerAffineCommand,
} from '@toeverything/infra/command';
import { useCallback, useEffect } from 'react';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
import { useExportPage } from './use-export-page';
import { useTrashModalHelper } from './use-trash-modal-helper';
export function useRegisterBlocksuiteEditorCommands(
blockSuiteWorkspace: Workspace,
pageId: string,
mode: 'page' | 'edgeless'
) {
const t = useAFFiNEI18N();
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const currentPage = blockSuiteWorkspace.getPage(pageId);
assertExists(currentPage);
const pageMeta = getPageMeta(pageId);
assertExists(pageMeta);
const favorite = pageMeta.favorite ?? false;
const trash = pageMeta.trash ?? false;
const { togglePageMode, toggleFavorite, restoreFromTrash } =
useBlockSuiteMetaHelper(blockSuiteWorkspace);
const exportHandler = useExportPage(currentPage);
const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace);
const onClickDelete = useCallback(() => {
setTrashModal({
open: true,
pageId: pageId,
pageTitle: pageMeta.title,
});
}, [pageId, pageMeta.title, setTrashModal]);
useEffect(() => {
const unsubs: Array<() => void> = [];
const preconditionStrategy = () =>
PreconditionStrategy.InPaperOrEdgeless && !trash;
//TODO: add back when edgeless presentation is ready
// this is pretty hack and easy to break. need a better way to communicate with blocksuite editor
// unsubs.push(
// registerAffineCommand({
// id: 'editor:edgeless-presentation-start',
// preconditionStrategy: () => PreconditionStrategy.InEdgeless && !trash,
// category: 'editor:edgeless',
// icon: <EdgelessIcon />,
// label: t['com.affine.cmdk.affine.editor.edgeless.presentation-start'](),
// run() {
// document
// .querySelector<HTMLElement>('edgeless-toolbar')
// ?.shadowRoot?.querySelector<HTMLElement>(
// '.edgeless-toolbar-left-part > edgeless-tool-icon-button:last-child'
// )
// ?.click();
// },
// })
// );
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-${favorite ? 'remove-from' : 'add-to'}-favourites`,
preconditionStrategy,
category: `editor:${mode}`,
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
label: favorite
? t['com.affine.favoritePageOperation.remove']()
: t['com.affine.favoritePageOperation.add'](),
run() {
toggleFavorite(pageId);
toast(
favorite
? t['com.affine.cmdk.affine.editor.remove-from-favourites']()
: t['com.affine.cmdk.affine.editor.add-to-favourites']()
);
},
})
);
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-convert-to-${
mode === 'page' ? 'edgeless' : 'page'
}`,
preconditionStrategy,
category: `editor:${mode}`,
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
label: `${t['Convert to ']()}${
mode === 'page'
? t['com.affine.pageMode.edgeless']()
: t['com.affine.pageMode.page']()
}`,
run() {
togglePageMode(pageId);
toast(
mode === 'page'
? t['com.affine.toastMessage.edgelessMode']()
: t['com.affine.toastMessage.pageMode']()
);
},
})
);
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-export-to-pdf`,
preconditionStrategy,
category: `editor:${mode}`,
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
label: t['Export to PDF'](),
run() {
exportHandler('pdf');
},
})
);
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-export-to-html`,
preconditionStrategy,
category: `editor:${mode}`,
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
label: t['Export to HTML'](),
run() {
exportHandler('html');
},
})
);
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-export-to-png`,
preconditionStrategy,
category: `editor:${mode}`,
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
label: t['Export to PNG'](),
run() {
exportHandler('png');
},
})
);
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-export-to-markdown`,
preconditionStrategy,
category: `editor:${mode}`,
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
label: t['Export to Markdown'](),
run() {
exportHandler('markdown');
},
})
);
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-move-to-trash`,
preconditionStrategy,
category: `editor:${mode}`,
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
label: t['com.affine.moveToTrash.title'](),
run() {
onClickDelete();
},
})
);
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-restore-from-trash`,
preconditionStrategy: () =>
PreconditionStrategy.InPaperOrEdgeless && trash,
category: `editor:${mode}`,
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
label: t['com.affine.cmdk.affine.editor.restore-from-trash'](),
run() {
restoreFromTrash(pageId);
},
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}, [
favorite,
mode,
onClickDelete,
exportHandler,
pageId,
pageMeta.title,
restoreFromTrash,
t,
toggleFavorite,
togglePageMode,
trash,
]);
}

View File

@@ -0,0 +1,27 @@
import { toast } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@blocksuite/store';
import { useAtom } from 'jotai';
import { useCallback } from 'react';
import { trashModalAtom } from '../../atoms/trash-modal';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
export function useTrashModalHelper(blocksuiteWorkspace: Workspace) {
const t = useAFFiNEI18N();
const [trashModal, setTrashModal] = useAtom(trashModalAtom);
const { pageId } = trashModal;
const { removeToTrash } = useBlockSuiteMetaHelper(blocksuiteWorkspace);
const handleOnConfirm = useCallback(() => {
removeToTrash(pageId);
toast(t['com.affine.toastMessage.movedTrash']());
setTrashModal({ ...trashModal, open: false });
}, [pageId, removeToTrash, setTrashModal, t, trashModal]);
return {
trashModal,
setTrashModal,
handleOnConfirm,
};
}

View File

@@ -1,5 +1,5 @@
import type { WorkspaceSubPath } from '@affine/env/workspace';
import { useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import {
type NavigateOptions,
useLocation,
@@ -103,14 +103,26 @@ export function useNavigateHelper() {
[navigate]
);
return {
jumpToPage,
jumpToPublicWorkspacePage,
jumpToSubPath,
jumpToIndex,
jumpTo404,
openPage,
jumpToExpired,
jumpToSignIn,
};
return useMemo(
() => ({
jumpToPage,
jumpToPublicWorkspacePage,
jumpToSubPath,
jumpToIndex,
jumpTo404,
openPage,
jumpToExpired,
jumpToSignIn,
}),
[
jumpTo404,
jumpToExpired,
jumpToIndex,
jumpToPage,
jumpToPublicWorkspacePage,
jumpToSignIn,
jumpToSubPath,
openPage,
]
);
}

View File

@@ -0,0 +1,60 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useAtom, useStore } from 'jotai';
import { useTheme } from 'next-themes';
import { useEffect } from 'react';
import { allPageModeSelectAtom } from '../atoms';
import {
registerAffineCreationCommands,
registerAffineLayoutCommands,
registerAffineSettingsCommands,
} from '../commands';
import { registerAffineNavigationCommands } from '../commands/affine-navigation';
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
import { useCurrentWorkspace } from './current/use-current-workspace';
import { useNavigateHelper } from './use-navigate-helper';
export function useRegisterWorkspaceCommands() {
const store = useStore();
const t = useAFFiNEI18N();
const theme = useTheme();
const [currentWorkspace] = useCurrentWorkspace();
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
const navigationHelper = useNavigateHelper();
const [pageListMode, setPageListMode] = useAtom(allPageModeSelectAtom);
useEffect(() => {
const unsubs: Array<() => void> = [];
unsubs.push(
registerAffineNavigationCommands({
store,
t,
workspace: currentWorkspace.blockSuiteWorkspace,
navigationHelper,
pageListMode,
setPageListMode,
})
);
unsubs.push(registerAffineSettingsCommands({ store, t, theme }));
unsubs.push(registerAffineLayoutCommands({ store, t }));
unsubs.push(
registerAffineCreationCommands({
store,
pageHelper: pageHelper,
t,
})
);
return () => {
unsubs.forEach(unsub => unsub());
};
}, [
store,
pageHelper,
t,
theme,
currentWorkspace.blockSuiteWorkspace,
navigationHelper,
pageListMode,
setPageListMode,
]);
}

View File

@@ -6,7 +6,6 @@ import {
getOrCreateWorkspace,
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { nanoid } from '@blocksuite/store';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import {
@@ -14,6 +13,7 @@ import {
WorkspaceVersion,
} from '@toeverything/infra/blocksuite';
import { useAtomValue, useSetAtom } from 'jotai';
import { nanoid } from 'nanoid';
import { useCallback } from 'react';
import { LocalAdapter } from '../adapters/local';

View File

@@ -17,7 +17,6 @@ import {
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/global/utils';
import { nanoid } from '@blocksuite/store';
import type { DragEndEvent } from '@dnd-kit/core';
import {
DndContext,
@@ -32,6 +31,7 @@ import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-
import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react';
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { nanoid } from 'nanoid';
import type { PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
@@ -54,6 +54,7 @@ import {
import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper';
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../hooks/use-navigate-helper';
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
import {
AllWorkspaceModals,
CurrentWorkspaceModals,
@@ -61,9 +62,9 @@ import {
import { pathGenerator } from '../shared';
import { toast } from '../utils';
const QuickSearchModal = lazy(() =>
import('../components/pure/quick-search-modal').then(module => ({
default: module.QuickSearchModal,
const CMDKQuickSearchModal = lazy(() =>
import('../components/pure/cmdk').then(module => ({
default: module.CMDKQuickSearchModal,
}))
);
@@ -79,10 +80,9 @@ export const QuickSearch = () => {
}
return (
<QuickSearchModal
workspace={currentWorkspace}
<CMDKQuickSearchModal
open={openQuickSearchModal}
setOpen={setOpenQuickSearchModalAtom}
onOpenChange={setOpenQuickSearchModalAtom}
/>
);
};
@@ -141,6 +141,10 @@ export const WorkspaceLayoutInner = ({
}: PropsWithChildren<WorkspaceLayoutProps>) => {
const [currentWorkspace] = useCurrentWorkspace();
const { openPage } = useNavigateHelper();
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
const t = useAFFiNEI18N();
useRegisterWorkspaceCommands();
useEffect(() => {
// hotfix for blockVersions
@@ -164,15 +168,13 @@ export const WorkspaceLayoutInner = ({
usePassiveWorkspaceEffect(currentWorkspace.blockSuiteWorkspace);
const helper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
const handleCreatePage = useCallback(() => {
const id = nanoid();
helper.createPage(id);
pageHelper.createPage(id);
const page = currentWorkspace.blockSuiteWorkspace.getPage(id);
assertExists(page);
return page;
}, [currentWorkspace.blockSuiteWorkspace, helper]);
}, [currentWorkspace.blockSuiteWorkspace, pageHelper]);
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
const handleOpenQuickSearchModal = useCallback(() => {
@@ -205,7 +207,6 @@ export const WorkspaceLayoutInner = ({
const { removeToTrash: moveToTrash } = useBlockSuiteMetaHelper(
currentWorkspace.blockSuiteWorkspace
);
const t = useAFFiNEI18N();
const handleDragEnd = useCallback(
(e: DragEndEvent) => {

View File

@@ -1,6 +1,7 @@
import { DebugLogger } from '@affine/debug';
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { Menu } from '@toeverything/components/menu';
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import { lazy } from 'react';
@@ -61,15 +62,29 @@ export const Component = () => {
<>
<div
style={{
width: 300,
margin: '80px auto',
borderRadius: '8px',
boxShadow: 'var(--affine-shadow-2)',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
padding: '16px 12px',
position: 'fixed',
left: '50%',
top: '50%',
}}
>
<UserWithWorkspaceList />
<Menu
rootOptions={{
open: true,
}}
items={<UserWithWorkspaceList />}
contentOptions={{
style: {
width: 300,
transform: 'translate(-50%, -50%)',
borderRadius: '8px',
boxShadow: 'var(--affine-shadow-2)',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
padding: '16px 12px',
},
}}
>
<div></div>
</Menu>
</div>
<AllWorkspaceModals />
</>

View File

@@ -24,6 +24,7 @@ import type { Map as YMap } from 'yjs';
import { getUIAdapter } from '../../adapters/workspace';
import { setPageModeAtom } from '../../atoms';
import { currentModeAtom } from '../../atoms/mode';
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { currentCollectionsAtom } from '../../utils/user-setting';
@@ -38,7 +39,7 @@ const DetailPageImpl = (): ReactElement => {
const collectionManager = useCollectionManager(currentCollectionsAtom);
const mode = useAtomValue(currentModeAtom);
const setPageMode = useSetAtom(setPageModeAtom);
useRegisterBlocksuiteEditorCommands(blockSuiteWorkspace, currentPageId, mode);
const onLoad = useCallback(
(page: Page, editor: EditorContainer) => {
try {

View File

@@ -5,6 +5,7 @@ import { currentWorkspaceAtom } from '@toeverything/infra/atom';
import { type DBSchema, openDB } from 'idb';
import { atom } from 'jotai';
import { atomWithObservable } from 'jotai/utils';
import { nanoid } from 'nanoid';
import { Observable } from 'rxjs';
import type { Map as YMap } from 'yjs';
import { Doc as YDoc } from 'yjs';
@@ -116,7 +117,7 @@ const pageCollectionBaseAtom = atomWithObservable<Collection[]>(get => {
settingMap.set(
userId,
new YDoc({
guid: `${rootDoc.guid}:settings:${userId}`,
guid: nanoid(),
})
);
}

View File

@@ -1,35 +0,0 @@
{
"name": "@affine/docs",
"version": "0.9.0-canary.13",
"type": "module",
"private": true,
"scripts": {
"dev": "waku dev",
"build": "waku build",
"build:vercel": "waku build && cp -Lr ./dist/.vercel/output ./.vercel/"
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/blocks": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/editor": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/global": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/lit": "0.0.0-20230921103931-38d8f07a-nightly",
"@blocksuite/store": "0.0.0-20230921103931-38d8f07a-nightly",
"express": "^4.18.2",
"jotai": "^2.4.1",
"react": "18.3.0-canary-7118f5dd7-20230705",
"react-dom": "18.3.0-canary-7118f5dd7-20230705",
"react-server-dom-webpack": "18.3.0-canary-7118f5dd7-20230705",
"waku": "0.14.0"
},
"devDependencies": {
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@vanilla-extract/css": "^1.13.0",
"@vanilla-extract/vite-plugin": "^3.9.0",
"autoprefixer": "^10.4.15",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2"
}
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,44 +0,0 @@
/// <reference types="vite/client" />
'use server';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { ReactElement } from 'react';
import { lazy } from 'react';
import { Sidebar } from './components/sidebar/index.js';
import { saveFile } from './server-fns.js';
const Editor = lazy(() =>
import('./components/editor.js').then(({ Editor }) => ({ default: Editor }))
);
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const AppCreator = (pathname: string) =>
function App(): ReactElement {
let path = resolve(__dirname, 'pages', 'binary');
if (!existsSync(path)) {
path = resolve(__dirname, '..', '..', 'src', 'pages', 'binary');
}
const buffer = [...readFileSync(path)];
return (
<div className="flex flex-col-reverse sm:flex-row h-screen">
<nav className="w-full sm:w-64">
<Sidebar />
</nav>
<main className="flex-1 p-6 w-full sm:w-[calc(100%-16rem)] overflow-scroll">
<Editor
workspaceId={pathname}
pageId="1"
onSave={saveFile}
binary={buffer}
/>
</main>
</div>
);
};
export default AppCreator;

View File

@@ -1,11 +0,0 @@
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import { atom } from 'jotai/vanilla';
export const workspaceAtom = atom(async () => {
const { Workspace } = await import('@blocksuite/store');
return new Workspace({
id: 'test-workspace',
})
.register(AffineSchemas)
.register(__unstableSchemas);
});

View File

@@ -1,53 +0,0 @@
'use client';
import '@blocksuite/editor/themes/affine.css';
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
import type { Page } from '@blocksuite/store';
import { useAtomValue } from 'jotai/react';
import type { ReactElement } from 'react';
import { use } from 'react';
import { applyUpdate } from 'yjs';
import { workspaceAtom } from '../atom.js';
export type EditorProps = {
workspaceId: string;
pageId: string;
binary?: number[];
onSave: (binary: any) => Promise<void>;
};
export const Editor = (props: EditorProps): ReactElement => {
const workspace = useAtomValue(workspaceAtom);
let page = workspace.getPage('page0') as Page;
if (!page) {
page = workspace.createPage({
id: 'page0',
});
}
if (props.binary && !page.root) {
use(
page.waitForLoaded().then(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
applyUpdate(page._ySpaceDoc, new Uint8Array(props.binary as number[]));
})
);
if (import.meta.env.MODE !== 'development') {
page.awarenessStore.setReadonly(page, true);
}
} else if (!page.root) {
use(
page.waitForLoaded().then(() => {
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text(''),
});
page.addBlock('affine:surface', {}, pageBlockId);
const noteBlockId = page.addBlock('affine:note', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, noteBlockId);
})
);
}
return <BlockSuiteEditor page={page} mode="page" onInit={() => {}} />;
};

View File

@@ -1,31 +0,0 @@
'use server';
import { lazy } from 'react';
import { saveFile } from '../../server-fns.js';
const SaveToLocal = lazy(() =>
import('./save-to-local.js').then(({ SaveToLocal }) => ({
default: SaveToLocal,
}))
);
export const Sidebar = () => {
return (
<div
className="h-screen text-black overflow-y-auto"
style={{
backgroundColor: '#f9f7f7',
}}
>
<a href="/">
<div className="flex items-center justify-center h-16 font-bold">
AFFiNE
</div>
</a>
{import.meta.env.MODE === 'development' && (
<SaveToLocal saveFile={saveFile} />
)}
</div>
);
};

View File

@@ -1,28 +0,0 @@
'use client';
import { assertExists } from '@blocksuite/global/utils';
import { useAtomValue } from 'jotai/react';
import { useCallback } from 'react';
import { encodeStateAsUpdate } from 'yjs';
import { workspaceAtom } from '../../atom.js';
type SaveToLocalProps = {
saveFile: (update: number[]) => void;
};
export const SaveToLocal = (props: SaveToLocalProps) => {
const workspace = useAtomValue(workspaceAtom);
const saveFile = props.saveFile;
const onSave = useCallback(() => {
const page = workspace.getPage('page0');
assertExists(page);
saveFile([...encodeStateAsUpdate(page.spaceDoc)]);
}, [saveFile, workspace]);
return (
<div>
<div className="flex items-center justify-center h-16 font-bold">
<button onClick={onSave}>Save to Local</button>
</div>
</div>
);
};

View File

@@ -1,17 +0,0 @@
import { defineRouter } from 'waku/router/server';
export default defineRouter(
async id => {
switch (id) {
case 'index': {
const { default: AppCreator } = await import('./app.js');
return AppCreator(id);
}
default:
return null;
}
},
async () => {
return ['index'];
}
);

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,36 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>AFFiNE Developer Documentation</title>
<style>
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.spinner {
width: 36px;
height: 36px;
margin: auto;
border: 2px solid #ddd;
border-top-color: #222;
border-radius: 50%;
animation: spinner 1s linear infinite;
}
#root > .spinner {
margin-top: calc(50% - 18px);
}
</style>
</head>
<body>
<!--placeholder1-->
<div id="root">
<div class="spinner"></div>
</div>
<!--/placeholder1-->
<script src="./index.tsx" defer type="module"></script>
<!--placeholder2-->
<!--/placeholder2-->
</body>
</html>

View File

@@ -1,14 +0,0 @@
import '@blocksuite/editor/themes/affine.css';
import './index.css';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Router } from 'waku/router/client';
const root = createRoot(document.getElementById('root') as HTMLElement);
root.render(
<StrictMode>
<Router />
</StrictMode>
);

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