Compare commits

...

72 Commits

Author SHA1 Message Date
Peng Xiao
9ffe45102b fix: macos build 2023-04-19 00:43:51 +08:00
Peng Xiao
6448b6a515 fix: release app workflow (#2017) 2023-04-19 00:21:44 +08:00
Peng Xiao
ba462fb79b fix: artifacts in release (#2016) 2023-04-18 22:20:34 +08:00
Peng Xiao
f36d415c3d build: optimize release app workflow (#2011) 2023-04-18 17:50:29 +08:00
Himself65
f6fb049ff2 feat: support disable legacy cloud (#2006) 2023-04-18 02:23:00 -05:00
JimmFly
94063352f5 chore: disable slider bar link item drag (#2010) 2023-04-18 02:16:38 -05:00
Himself65
c895c18deb ci: collect server coverage report (#2002) 2023-04-18 01:01:14 -05:00
JimmFly
346484ed44 chore: add translation (#2001) 2023-04-18 00:34:21 -05:00
Himself65
18223c22ef test(server): migrate to node internal test (#2000) 2023-04-18 00:07:03 -05:00
himself65
ea9861bfa0 ci: update labeler.yml 2023-04-17 23:13:10 -05:00
Himself65
7be96a2e41 build: remove unused config (#1990) 2023-04-17 23:11:46 -05:00
LongYinan
91c3040db7 feat(server): init nestjs server (#1997)
Co-authored-by: himself65 <himself65@outlook.com>
2023-04-17 22:24:44 -05:00
himself65
a92d0fff4a docs: update badge in README.md 2023-04-17 21:06:29 -05:00
Jordy Delgado
64e5d65eb3 docs: sign CLA (#1995) 2023-04-17 21:03:15 -05:00
Peng Xiao
11de3a681f build: add canary build (#1986)
Co-authored-by: Himself65 <himself65@outlook.com>
Co-authored-by: Horus <lhlxtl@gmail.com>
2023-04-17 11:32:10 -05:00
hehe
54a30bbf20 chore: remove absolete module-resolve (#1991) 2023-04-17 15:02:22 +00:00
usedtobe
6c77006bcc docs: fix typo (#1984) 2023-04-17 08:34:50 -05:00
Qi
143a55a6e8 fix: error style of sidebar (#1981) 2023-04-17 06:52:04 +00:00
Qi
19894aad5a feat: modify empty text & style of favorite & pinboard (#1977)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-17 13:41:07 +08:00
JimmFly
f534e4a6dd chore: update change log link (#1973)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-16 21:48:29 -05:00
Himself65
3d70a36dd3 refactor: remove null type in hooks (#1955) 2023-04-16 21:36:32 -05:00
Himself65
9c517907eb fix: first binary on y-indexeddb (#1972) 2023-04-16 21:33:54 -05:00
Himself65
4cb6b8fdc8 chore: bump version (#1970) 2023-04-16 20:36:59 -05:00
Horus
134e1e8668 feat: support release windows installer with squirrel (#1965)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-04-16 19:28:29 -05:00
Himself65
c76bbeab67 ci: add sentry in desktop release (#1914) 2023-04-16 21:22:48 +00:00
himself65
ec50d721ea chore: release 0.5.3 2023-04-16 16:04:21 -05:00
Himself65
7bbe67af43 refactor: workspace loading logic (#1966) 2023-04-16 16:02:41 -05:00
Himself65
caa292e097 test: mark public single page as fail (#1967) 2023-04-16 09:45:50 -05:00
HeJiachen-PM
73b8b805c6 Rewrite section 2.3 2023-04-16 15:19:22 +08:00
HeJiachen-PM
084d4e043a Add summery to subsections in section 2 2023-04-16 15:09:08 +08:00
HeJiachen-PM
69a9c34f11 Rewrite the third section 2023-04-16 04:37:35 +08:00
Himself65
d742cab1d5 fix: hydration error (#1961) 2023-04-15 13:10:24 -05:00
Horus
8b3c1fb363 fix: force to use powershell on windows to fix zx script crash (#1962) 2023-04-15 12:24:57 -05:00
Horus
ec445207d6 fix: fix windows build client error and release cannot open (#1959) 2023-04-16 00:00:47 +08:00
HeJiachen-PM
49281e68a6 Rewrite the second section 2023-04-15 15:31:56 +08:00
HeJiachen-PM
a918d6e14c Proofreading introduction 2023-04-15 15:27:09 +08:00
Himself65
7cf7187893 docs: add behind-the-code.md (#1957) 2023-04-15 00:19:13 -05:00
Himself65
2383165470 refactor: remove NoSsr on top level (#1951) 2023-04-14 17:07:41 -05:00
Himself65
43a96fe8e3 fix: move suspense to the correct place (#1954) 2023-04-14 15:44:23 -05:00
Himself65
b771a2504b test: fix flaky (#1953) 2023-04-14 15:03:16 -05:00
himself65
8d2fefb5f8 ci: fix labeler.yml 2023-04-14 14:14:58 -05:00
himself65
c71e5f1c96 fix(cli): run dev server at 8080 2023-04-14 11:06:22 -05:00
Skye Sun
5b96fb0db3 docs: update CLA.md (#1950) 2023-04-14 08:02:21 -05:00
Peng Xiao
46cd0c5c9a fix: share url (#1948) 2023-04-14 08:01:31 -05:00
Qi
261a41f8da feat: add history back & forward for desktop app (#1926) 2023-04-14 09:19:52 +00:00
Himself65
bd387f6551 fix: theme color (#1944) 2023-04-14 02:13:14 -05:00
JimmFly
5335118e93 chore: add translation (#1946) 2023-04-14 15:02:43 +08:00
Himself65
70313eb5ee chore: bump version (#1943) 2023-04-14 01:57:54 -05:00
himself65
ccd2b79d20 docs: update logo in README.md 2023-04-14 00:38:35 -05:00
Himself65
5ca94db5d2 fix: effect deps (#1940) 2023-04-14 00:24:44 -05:00
Himself65
d58f9db289 docs: update BUG-REPORT.yml (#1941) 2023-04-13 22:27:01 -05:00
Chi Zhang
93e78c315c Update jobs.md 2023-04-14 10:27:45 +08:00
himself65
3954f309aa chore: fix packages version 2023-04-13 18:33:21 -05:00
himself65
f902d0c324 ci: fix cache in build-master.yml 2023-04-13 18:22:20 -05:00
Himself65
e79fb1ae3a build: add log when coverage (#1933) 2023-04-13 18:20:41 -05:00
Himself65
08d67b316c docs: update README.md (#1931) 2023-04-13 17:54:20 -05:00
himself65
d12c00d5cb ci: fix coverage report 2023-04-13 17:53:34 -05:00
himself65
68bb538dd1 ci: remove version tag in release 2023-04-13 16:39:50 -05:00
himself65
b394764b1c ci: fix upload-artifact path 2023-04-13 16:33:12 -05:00
Himself65
01a686dc28 feat: enable share menu (#1883)
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
2023-04-13 16:22:49 -05:00
Simon He
32b206a137 chore: add lint cache (#1917) 2023-04-13 20:30:18 +00:00
Peng Xiao
42756045bb fix: failed to load blobs in electron (#1927) 2023-04-13 15:14:46 +00:00
Peng Xiao
934e242116 fix: electron sourcemap issues (#1919) 2023-04-13 08:37:50 -05:00
Qi
6571ec2df6 fix: pinboard operation menu disappear inexplicably when hover to menu from button, fixed #1898 (#1922) 2023-04-13 07:58:22 -05:00
Qi
7d64815aca feat: add navigation path in quick search (#1920) 2023-04-13 16:31:28 +08:00
Himself65
f20a151e57 fix(y-indexeddb): migration in firefox (#1904) 2023-04-12 22:42:17 -05:00
Himself65
6180a4c3cb fix: wrap React.lazy with Suspense (#1915) 2023-04-12 22:33:31 -05:00
Himself65
2bcda973d3 build: support sourcemap in sentry (#1910) 2023-04-12 21:26:06 -05:00
Himself65
1162bffb30 build: support sentry replay (#1908) 2023-04-12 21:18:41 -05:00
Himself65
2a2d682211 fix: cannot update a component while rendering a different component (#1907) 2023-04-12 16:46:29 -05:00
Sirocco
8f53043100 fix: improve UX of dropdown (#1905)
Removed the logic of onMouseLeave. The logic of clicking to open and clicking to close is clearer.

Fixes: #1898
2023-04-12 15:35:41 -05:00
Himself65
6d5b101bb3 fix: use startTransition (#1903) 2023-04-12 12:06:22 -05:00
267 changed files with 11370 additions and 22378 deletions

View File

@@ -6,6 +6,7 @@
"always",
[
"electron",
"server",
"web",
"docs",
"component",

View File

@@ -4,3 +4,4 @@ dist
out
storybook-static
affine-out
_next

View File

@@ -1,8 +1,11 @@
module.exports = {
/**
* @type {import('eslint').Linter.Config}
*/
const config = {
root: true,
settings: {
react: {
version: '18',
version: 'detect',
},
next: {
rootDir: 'apps/web',
@@ -10,6 +13,7 @@ module.exports = {
},
extends: [
'eslint:recommended',
'plugin:react-hooks/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',
@@ -64,4 +68,14 @@ module.exports = {
},
],
},
overrides: [
{
files: 'apps/server/**/*.ts',
rules: {
'@typescript-eslint/consistent-type-imports': 0,
},
},
],
};
module.exports = config;

2
.github/CLA.md vendored
View File

@@ -53,3 +53,5 @@ Example:
- Aditya Sharma, @adityash1, 2023/03/21
- Fangdun Tsai, @fundon, 2023/03/21
- Zhilin Liu, @lzlme, 2023/04/09
- Skye Sun, @skyesun, 2023/04/14
- Jordy Delgado, @Jdelgad8, 2023/04/17

View File

@@ -23,6 +23,11 @@ body:
options:
- app.affine.pro
- stage.affine.pro
- dev.affine.live
- affine-preview.vercel.app
- macOS x64
- macOS ARM 64
- Windows x64
validations:
required: true
- type: dropdown

View File

@@ -9,10 +9,6 @@ inputs:
description: 'Run the install step.'
required: false
default: 'true'
electron-workspace-install:
description: 'Run the install step for the electron workspace.'
required: false
default: 'false'
playwright-install:
description: 'Run the install step for Playwright.'
required: false
@@ -33,10 +29,6 @@ runs:
scope: '@toeverything'
cache: 'yarn'
- name: CI Module Resolve
shell: bash
run: node scripts/module-resolve/ci.cjs
- name: Expose yarn config as "$GITHUB_OUTPUT"
id: yarn-config
shell: bash
@@ -86,17 +78,6 @@ runs:
YARN_INSTALL_STATE_PATH: .yarn/ci-cache/install-state.gz
HUSKY: '0'
- name: yarn install (electron)
if: ${{ inputs.electron-workspace-install == 'true' }}
shell: bash
run: yarn install ${{ inputs.extra-flags }}
working-directory: apps/electron
env:
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
YARN_ENABLE_GLOBAL_CACHE: 'false'
YARN_INSTALL_STATE_PATH: ../../.yarn/ci-cache/install-state.gz
HUSKY: '0'
- name: Get installed Playwright version
id: playwright-version
if: ${{ inputs.playwright-install == 'true' }}

4
.github/labeler.yml vendored
View File

@@ -3,7 +3,7 @@ docs:
- '**/README.md'
- 'packages/templates/**/*'
tests:
test:
- 'tests/**/*'
- '**/tests/**/*'
- '**/__tests__/**/*'
@@ -40,3 +40,5 @@ package:y-indexeddb: 'packages/y-indexeddb/**/*'
app:web: 'apps/web/**/*'
app:electron: 'apps/electron/**/*'
app:server: 'apps/server/**/*'

View File

@@ -123,6 +123,45 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-frontend-dev:
name: Build @affine/web dev
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Cache Next.js
uses: actions/cache@v3
with:
path: |
${{ github.workspace }}/apps/web/.next/cache
key: ${{ runner.os }}-nextjs-dev-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-dev-${{ hashFiles('**/yarn.lock') }}-
- name: Build
run: yarn build
env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
API_SERVER_PROFILE: local
ENABLE_DEBUG_PAGE: true
COVERAGE: true
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: next-js-dev
path: ./apps/web/.next
if-no-files-found: error
storybook-test:
name: Storybook Test
runs-on: ubuntu-latest
@@ -152,6 +191,26 @@ jobs:
name: affine
fail_ci_if_error: true
server-test:
name: Server Test
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Run server tests
run: yarn test:coverage
working-directory: apps/server
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./apps/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: true
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
@@ -160,7 +219,7 @@ jobs:
matrix:
shard: [1, 2, 3, 4]
environment: development
needs: [build-frontend, build-storybook]
needs: [build-frontend-dev, build-storybook]
services:
octobase:
image: ghcr.io/toeverything/cloud-self-hosted:nightly-latest
@@ -183,7 +242,7 @@ jobs:
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: next-js
name: next-js-dev
path: ./apps/web/.next
- name: Download storybook artifact
@@ -192,6 +251,10 @@ jobs:
name: storybook
path: ./packages/component/storybook-static
- name: Wait for Octobase Ready
run: |
node ./scripts/wait-3000-healthz.mjs
- name: Run playwright tests
run: yarn test --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
@@ -213,7 +276,7 @@ jobs:
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: test-results-e2e
name: test-results-e2e-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore

View File

@@ -26,8 +26,6 @@ jobs:
- uses: actions/checkout@v3
- name: Install All Dependencies
uses: ./.github/actions/setup-node
with:
electron-workspace-install: true
build-storybook:
name: Build Storybook
@@ -73,6 +71,9 @@ jobs:
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
API_SERVER_PROFILE: local
ENABLE_DEBUG_PAGE: true
COVERAGE: true
- name: Upload artifact
uses: actions/upload-artifact@v3
@@ -81,6 +82,26 @@ jobs:
path: ./apps/web/.next
if-no-files-found: error
server-test:
name: Server Test
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Run server tests
run: yarn test:coverage
working-directory: apps/server
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./apps/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: true
storybook-test:
name: Storybook Test
runs-on: ubuntu-latest
@@ -147,6 +168,10 @@ jobs:
name: storybook
path: ./packages/component/storybook-static
- name: Wait for Octobase Ready
run: |
node ./scripts/wait-3000-healthz.mjs
- name: Run playwright tests
run: yarn test --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
@@ -168,7 +193,7 @@ jobs:
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: test-results-e2e
name: test-results-e2e-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore

View File

@@ -17,6 +17,11 @@ on:
type: boolean
required: true
default: true
is-canary:
description: 'Canary Release? The app will be named as "AFFiNE Canary"'
type: boolean
required: true
default: true
permissions:
actions: write
@@ -29,137 +34,141 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true
jobs:
make-macos:
environment: production
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }}
AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }}
API_SERVER_PROFILE: prod
env:
BUILD_TYPE: ${{ github.event.inputs.is-canary == 'true' && 'canary' || 'stable' }}
runs-on: macos-latest
strategy:
matrix:
arch: [x64, arm64]
jobs:
before-make:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: generate-assets
working-directory: apps/electron
run: yarn generate-assets
env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }}
AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
- name: Upload Artifact (web-static)
uses: actions/upload-artifact@v3
with:
electron-workspace-install: true
name: before-make-web-static
path: apps/electron/resources/web-static
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: add arm64 target
if: matrix.arch == 'arm64'
run: rustup target add aarch64-apple-darwin
- name: Rust cache
uses: swatinem/rust-cache@v2
- name: Upload Artifact (electron dist)
uses: actions/upload-artifact@v3
with:
key: ${{ matrix.arch }}
workspaces: './packages/octobase-node -> target'
name: before-make-electron-dist
path: apps/electron/dist
make-distribution:
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
spec:
- { os: macos-latest, platform: macos, arch: x64 }
- { os: macos-latest, platform: macos, arch: arm64 }
- { os: ubuntu-latest, platform: linux, arch: x64 }
- { os: windows-latest, platform: windows, arch: x64 }
runs-on: ${{ matrix.spec.os }}
needs: before-make
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
SKIP_GENERATE_ASSETS: 1
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- uses: actions/download-artifact@v3
with:
name: before-make-web-static
path: apps/electron/resources/web-static
- uses: actions/download-artifact@v3
with:
name: before-make-electron-dist
path: apps/electron/dist
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'macos' }}
uses: apple-actions/import-codesign-certs@v2
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: make build
run: yarn make-macos-${{ matrix.arch }}
- name: make
run: yarn make-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
working-directory: apps/electron
- name: Save artifacts
- name: Save artifacts (mac)
if: ${{ matrix.spec.platform == 'macos' }}
run: |
mkdir -p builds
mv apps/electron/out/make/AFFiNE.dmg ./builds/affine-darwin-${{ matrix.arch }}-${{ github.event.inputs.version }}.dmg
mv apps/electron/out/*/make/*.dmg ./builds/affine-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
- name: Save artifacts (windows)
if: ${{ matrix.spec.platform == 'windows' }}
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.zip
mv apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.exe
mv apps/electron/out/*/make/squirrel.windows/x64/*.msi ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.msi
mv apps/electron/out/*/make/squirrel.windows/x64/*.nupkg ./builds/affine-${{ env.BUILD_TYPE }}-windows-x64.nupkg
- name: Save artifacts (linux)
if: ${{ matrix.spec.platform == 'linux' }}
run: |
mkdir -p builds
mv apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ env.BUILD_TYPE }}-linux-x64.zip
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-darwin-${{ matrix.arch }}-builds
path: builds
make-windows:
runs-on: windows-latest
environment: production
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }}
AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }}
API_SERVER_PROFILE: prod
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-workspace-install: true
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: './packages/octobase-node -> target'
- name: make build
run: yarn make-windows-x64
working-directory: apps/electron
- name: Save windows artifacts
run: |
mkdir -p builds
mv apps/electron/out/make/zip/win32/x64/AFFiNE-win32-x64-0.0.0.zip ./builds/affine-windows-x64-${{ github.event.inputs.version }}.zip
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: affine-windows-x64-builds
name: affine-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}-builds
path: builds
release:
needs: [make-macos, make-windows]
needs: make-distribution
runs-on: ubuntu-latest
steps:
- name: Download MacOS x64 Artifacts
uses: actions/download-artifact@v3
with:
name: affine-darwin-x64-builds
path: ./
- name: Download MacOS arm64 Artifacts
steps:
- name: Download Artifacts (macos-x64)
uses: actions/download-artifact@v3
with:
name: affine-darwin-arm64-builds
name: affine-macos-x64-builds
path: ./
- name: Download Windows Artifacts
- name: Download Artifacts (macos-arm64)
uses: actions/download-artifact@v3
with:
name: affine-macos-arm64-builds
path: ./
- name: Download Artifacts (windows-x64)
uses: actions/download-artifact@v3
with:
name: affine-windows-x64-builds
path: ./
- name: Download Artifacts (linux-x64)
uses: actions/download-artifact@v3
with:
name: affine-linux-x64-builds
path: ./
- name: Create Release Draft
uses: softprops/action-gh-release@v1

4
.gitignore vendored
View File

@@ -5,7 +5,7 @@
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.yarn/versions
# compiled output
*dist
@@ -58,8 +58,6 @@ Thumbs.db
out/
storybook-static
module-resolve.js
module-resolve.cjs
/test-results/
/playwright-report/
/playwright/.cache/

View File

@@ -3,7 +3,6 @@
# check lockfile is up to date
yarn install
cd ./apps/eletron && yarn install
# lint staged files
yarn exec lint-staged

View File

@@ -1,6 +1,6 @@
{
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"arrowParens": "avoid"
"singleQuote": true,
"trailingComma": "es5",
"tabWidth": 2,
"arrowParens": "avoid"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,5 +11,9 @@ npmPublishRegistry: 'https://registry.npmjs.org'
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: '@yarnpkg/plugin-interactive-tools'
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: '@yarnpkg/plugin-version'
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: '@yarnpkg/plugin-workspace-tools'
yarnPath: .yarn/releases/yarn-3.5.0.cjs

View File

@@ -24,7 +24,11 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![affine-app-logo]](https://app.affine.pro)
[![AFFiNE Web](<https://img.shields.io/badge/-Try%20It%20Online%20%E2%86%92-rgb(84,56,255)?style=flat-square&logoColor=white&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAADAAAAAwAEwd99eAAABjElEQVRYhe1W0U3DMBB9RfyTDeoNyAYNG2QDOgJsECYgGxA26AZ4hIxgJqCZ4PjIGV+tUxK7raqiPsmKdXe5e3fOs7IiIlwSdxetfiNw7QRKAD0Ax/ssrI5QgQOw5v03AJOTJHcCL1x84LVmWzJyJlBg7P4BwCvb3pmIAbBPykZEqaulEU7YHNva1HypxUsKqIS9EvbynASs0n3ss+ciUIsuO8VvhL9emjdFBa3YO8XvALwpsZNYSqBB0PwUWgRZNksSL5GhlN0ngGd+dkpsD6AG8IGlslxwTh2fa09EBc3Dir32rRysuQlUAL54/wTAcpePPAXHPsOTGXhSEv69rAlYpZOt6DSO29J4D/TRRLJk6AvtaZSY9PkCFYVLqI9i/NF5YkkECgrXa6P4fVEn4iolrhNxRQqBZu7FqMNdZiMqAUPj2KdGZyicu1dHzlGqBHxn2sdTR53bmeJ+ebJd7LtXhGH4uQEwd0ttAPzMxGi5/6BdxTuMej41Bs59gGP+CU+Cq/4tvxH4HwR+Ab3Uqr/VGbqEAAAAAElFTkSuQmCC>)](https://app.affine.pro)
[![AFFiNE macOS M1/M2 Chip](https://img.shields.io/badge/-macOS_M_Chip%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://github.com/toeverything/AFFiNE/releases/latest)
[![AFFiNE macOS x64](https://img.shields.io/badge/-macOS_x86%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://github.com/toeverything/AFFiNE/releases/latest)
[![AFFiNE Window x64](https://img.shields.io/badge/-Windows%20%E2%86%92-blue?style=flat-square&logo=windows&logoColor=white)](https://github.com/toeverything/AFFiNE/releases/latest)
[![stars-icon]](https://github.com/toeverything/AFFiNE)
[![All Contributors][all-contributors-badge]](#contributors)
[![codecov]](https://codecov.io/gh/toeverything/AFFiNE)
@@ -36,6 +40,8 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
</div>
---
<div align="center">
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAMAAAAPkIrYAAAAP1BMVEU8b9w8b9w+b947cNw7b9w6b908b909b9w8b9w7b9w8b9w7cN08b9w7b908b9w7b9w8b907cNw8b9w8b91HcEx3NJCJAAAAFXRSTlP/3QWSgA+lHPlu6Di4XtIrxk/xRADGudUoAAAB9UlEQVR42tWYwbKjIBREG0GJKkRj/v9bZ1ZvRC99rzib11tTB9qqnKoW3/+X38vy7ifzQ1b/wk/8Q1bCv3y6Z6wFh2x2llIRGB6xRhzz6p+wVhRJD1gRZZYHrADYSyqsjFPGZtYbuFESesUysZXlcMnYyJpxTW5keQh5N7G6CUJCE2uHFNfEGiBmbmB1H4jxDawNcqbuPmtAJTtj6RZ0lpIwiR5jNmgfNtHHwLXPWfFYcS2NMdxkjac/dNaNCJPo3yf9pFuseHbDrBsRFguGs8te8Q4rXzTjVSPCIHp3FePKWbzi30xE+4zlBMmoJaGLfpLUmAmLiN4Xyibahy76WZRQMLJ2WX27on2oFvQVac8yi4p+J2forA0V8W1c++AVS1f1H6p9KKLHxk9RWKmsyB+VLC76gV65DLjokdg5KmsEMXsiDwXWSmTc9ezSoKJHoi9zUVihbMHfQOSsXB7Mrz1S1huKPde69sEsiKgNt8hYTjiWlAyENeu7IFe1D15RSEBN+yCiXw17K1RZm/w7UtJVWYN8f1ZyLlkVb2bT4vIVVrINH1dqX2YttkHmIWsfVWs646wcRFYis6fIVGpfYq1kjpGSW8kSRD+xYSmXRM0Ang9eSZioVdy/5pWaLqzIRyIpuVxYozvGf1m67I7pf/s3UXv+AP61NI2Y+BbSAAAAAElFTkSuQmCC" height=25></a>
&nbsp;
@@ -55,7 +61,6 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
<em>See docs, canvas and tables are hyper merged with AFFiNE - just like the word affine (əˈɪn | a-fine).</em>
</div>
<br />
</div>
![img_v2_37a7cc04-ab3f-4405-ae9a-f84ceb4c948g](https://user-images.githubusercontent.com/79301703/230892907-5fd5c0c5-1665-4d75-8a35-744e0afc36a5.gif)
@@ -260,11 +265,10 @@ See [LICENSE] for details.
[jobs available]: ./docs/jobs.md
[latest packages]: https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted
[contributor license agreement]: https://github.com/toeverything/affine/edit/master/.github/CLA.md
[affine-app-logo]: https://img.shields.io/static/v1?label=Try%20Online&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=%E2%86%92
[rust-version-icon]: https://img.shields.io/badge/Rust-1.70.0-dea584
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
[codecov]: https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.15.0-success
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.0-success
[typescript-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/typescript
[react-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/react?color=rgb%2897%2C%20218%2C%20251%29
[blocksuite-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/AFFiNE/@blocksuite/store?color=6880ff&filename=apps%2Fweb%2Fpackage.json&label=blocksuite

View File

@@ -1,5 +1,5 @@
cacheFolder: '../../.yarn/cache'
# deferredVersionFolder: '../../.yarn/versions'
deferredVersionFolder: '../../.yarn/versions'
globalFolder: '../../.yarn/global'
installStatePath: '../../.yarn/install-state.gz'
patchFolder: '../../.yarn/patches'

View File

@@ -1,21 +1,17 @@
# AFFiNE Electron App
# ⚠️ NOTE ⚠️
Due to PNPM related issues, this project is currently using **yarn 3**.
See https://github.com/electron/forge/issues/2633
## Development
```
# in project root, start web app at :8080
To run AFFiNE Desktop Client Application locally, run the following commands:
```sh
# in repo root
yarn install
yarn dev
# build octobase-node
yarn workspace @affine/octobase-node build
# in /apps/electron, start electron app
yarn dev
# in apps/electron
yarn generate-assets
yarn dev # or yarn prod for production build
```
## Credits

View File

@@ -1,7 +1,30 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const {
utils: { fromBuildIdentifier },
} = require('@electron-forge/core');
const isCanary = process.env.BUILD_TYPE === 'canary';
const productName = isCanary ? 'AFFiNE-Canary' : 'AFFiNE';
const icoPath = isCanary
? './resources/icons/icon_canary.ico'
: './resources/icons/icon.ico';
const icnsPath = isCanary
? './resources/icons/icon_canary.icns'
: './resources/icons/icon.icns';
/**
* @type {import('@electron-forge/shared-types').ForgeConfig}
*/
module.exports = {
buildIdentifier: isCanary ? 'canary' : 'stable',
packagerConfig: {
name: 'AFFiNE',
icon: './resources/icons/icon.icns',
name: productName,
appBundleId: fromBuildIdentifier({
canary: 'pro.affine.canary',
stable: 'pro.affine.app',
}),
icon: icnsPath,
osxSign: {
identity: 'Developer ID Application: TOEVERYTHING PTE. LTD.',
'hardened-runtime': true,
@@ -20,7 +43,7 @@ module.exports = {
name: '@electron-forge/maker-dmg',
config: {
format: 'ULFO',
icon: './resources/icons/icon.icns',
icon: icnsPath,
name: 'AFFiNE',
},
},
@@ -28,21 +51,46 @@ module.exports = {
name: '@electron-forge/maker-zip',
config: {
name: 'affine',
iconUrl: './resources/icons/icon.ico',
setupIcon: './resources/icons/icon.ico',
iconUrl: icoPath,
setupIcon: icoPath,
platforms: ['darwin', 'linux', 'win32'],
},
},
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'AFFiNE',
setupIcon: icoPath,
// loadingGif: './resources/icons/loading.gif',
},
},
],
hooks: {
readPackageJson: async (_, packageJson) => {
// we want different package name for canary build
// so stable and canary will not share the same app data
packageJson.productName = productName;
},
generateAssets: async (_, platform, arch) => {
if (process.env.SKIP_GENERATE_ASSETS) {
return;
}
const { $ } = await import('zx');
// TODO: right now we do not need the following
// it is for octobase-node, but we dont use it for now.
if (platform === 'darwin' && arch === 'arm64') {
// In GitHub Actions runner, MacOS is always x64
// we need to manually set TARGET to aarch64-apple-darwin
process.env.TARGET = 'aarch64-apple-darwin';
}
if (platform === 'win32') {
$.shell = 'powershell.exe';
$.prefix = '';
}
// run yarn generate-assets
await $`yarn generate-assets`;
},

View File

@@ -1,16 +1,10 @@
import type { RequestInit } from 'undici';
import { fetch, ProxyAgent } from 'undici';
const redirectUri = 'https://affine.pro/client/auth-callback';
export const oauthEndpoint = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${process.env.AFFINE_GOOGLE_CLIENT_ID}&redirect_uri=${redirectUri}&response_type=code&scope=openid https://www.googleapis.com/auth/userinfo.email profile&access_type=offline&customParameters={"prompt":"select_account"}`;
const tokenEndpoint = 'https://oauth2.googleapis.com/token';
export const exchangeToken = async (code: string) => {
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
const proxyAgent = httpProxy ? new ProxyAgent(httpProxy) : undefined;
export const getExchangeTokenParams = (code: string) => {
const postData = {
code,
client_id: process.env.AFFINE_GOOGLE_CLIENT_ID || '',
@@ -18,15 +12,12 @@ export const exchangeToken = async (code: string) => {
redirect_uri: redirectUri,
grant_type: 'authorization_code',
};
const requestOptions: RequestInit = {
const requestInit: RequestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(postData).toString(),
dispatcher: proxyAgent,
};
return fetch(tokenEndpoint, requestOptions).then(response => {
return response.json();
});
return { requestInit, url: tokenEndpoint };
};

View File

@@ -1,13 +1,13 @@
import * as os from 'node:os';
import path from 'node:path';
import { Storage } from '@affine/octobase-node';
import { app, shell } from 'electron';
import { BrowserWindow, ipcMain, nativeTheme } from 'electron';
import fs from 'fs-extra';
import { parse } from 'url';
import { exchangeToken, oauthEndpoint } from './google-auth';
import { isMacOS } from '../../../utils';
import { getExchangeTokenParams, oauthEndpoint } from './google-auth';
const AFFINE_ROOT = path.join(os.homedir(), '.affine');
@@ -15,16 +15,7 @@ fs.ensureDirSync(AFFINE_ROOT);
const logger = console;
// todo: rethink this
export const appState = {
storage: new Storage(path.join(AFFINE_ROOT, 'test.db')),
};
export const registerHandlers = () => {
ipcMain.handle('octo:workspace-sync', async (_, id) => {
return appState.storage.sync(id, '');
});
ipcMain.handle('ui:theme-change', async (_, theme) => {
nativeTheme.themeSource = theme;
logger.info('theme change', theme);
@@ -32,31 +23,35 @@ export const registerHandlers = () => {
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
// todo
const windows = BrowserWindow.getAllWindows();
windows.forEach(w => {
// hide window buttons when sidebar is not visible
w.setWindowButtonVisibility(visible);
});
logger.info('sidebar visibility change', visible);
// detect if os is macos
if (isMacOS()) {
const windows = BrowserWindow.getAllWindows();
windows.forEach(w => {
// hide window buttons when sidebar is not visible
w.setWindowButtonVisibility(visible);
});
logger.info('sidebar visibility change', visible);
}
});
ipcMain.handle('ui:google-sign-in', async () => {
ipcMain.handle('ui:get-google-oauth-code', async () => {
logger.info('starting google sign in ...');
shell.openExternal(oauthEndpoint);
return new Promise<string>((resolve, reject) => {
return new Promise((resolve, reject) => {
const handleOpenUrl = async (_: any, url: string) => {
const mainWindow = BrowserWindow.getAllWindows().find(
w => !w.isDestroyed()
);
const urlObj = parse(url.replace('??', '?'), true);
if (!mainWindow || !url.startsWith('affine://')) return;
const token = (await exchangeToken(urlObj.query['code'] as string)) as {
id_token: string;
};
if (!mainWindow || !url.startsWith('affine://auth-callback')) return;
const code = urlObj.query['code'] as string;
if (!code) return;
logger.info('google sign in code received from callback', code);
app.removeListener('open-url', handleOpenUrl);
resolve(token.id_token);
logger.info('google sign in successful', token);
resolve(getExchangeTokenParams(code));
};
app.on('open-url', handleOpenUrl);
@@ -64,7 +59,7 @@ export const registerHandlers = () => {
setTimeout(() => {
reject(new Error('Timed out'));
app.removeListener('open-url', handleOpenUrl);
}, 60000);
}, 30000);
});
});

View File

@@ -7,6 +7,7 @@ import { registerHandlers } from './app-state';
import { restoreOrCreateWindow } from './main-window';
import { registerProtocol } from './protocol';
if (require('electron-squirrel-startup')) app.exit();
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('affine', process.execPath, [

View File

@@ -14,12 +14,12 @@ async function createWindow() {
const browserWindow = new BrowserWindow({
titleBarStyle: isMacOS() ? 'hiddenInset' : 'default',
trafficLightPosition: { x: 20, y: 18 },
trafficLightPosition: { x: 24, y: 18 },
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
minWidth: 640,
transparent: true,
transparent: isMacOS(),
visualEffectState: 'active',
vibrancy: 'under-window',
height: mainWindowState.height,

View File

@@ -1,23 +1,50 @@
import { protocol, session } from 'electron';
import { join } from 'path';
protocol.registerSchemesAsPrivileged([
{
scheme: 'assets',
privileges: {
secure: false,
corsEnabled: true,
supportFetchAPI: true,
standard: true,
bypassCSP: true,
},
},
]);
function toAbsolutePath(url: string) {
let realpath = decodeURIComponent(url);
const webStaticDir = join(__dirname, '../../../resources/web-static');
if (url.startsWith('./')) {
// if is a file type, load the file in resources
if (url.split('/').at(-1)?.includes('.')) {
realpath = join(webStaticDir, decodeURIComponent(url));
} else {
// else, fallback to load the index.html instead
realpath = join(webStaticDir, 'index.html');
}
}
return realpath;
}
export function registerProtocol() {
if (process.env.NODE_ENV === 'production') {
protocol.interceptFileProtocol('file', (request, callback) => {
const url = request.url.replace(/^file:\/\//, '');
const webStaticDir = join(__dirname, '../../../resources/web-static');
if (url.startsWith('./')) {
// if is a file type, load the file in resources
if (url.split('/').at(-1)?.includes('.')) {
const realpath = join(webStaticDir, decodeURIComponent(url));
callback(realpath);
} else {
// else, fallback to load the index.html instead
const realpath = join(webStaticDir, 'index.html');
console.log(realpath, 'realpath', url, 'url');
callback(realpath);
}
}
const realpath = toAbsolutePath(url);
// console.log('realpath', realpath, 'for', url);
callback(realpath);
return true;
});
protocol.registerFileProtocol('assets', (request, callback) => {
const url = request.url.replace(/^assets:\/\//, '');
const realpath = toAbsolutePath(url);
// console.log('realpath', realpath, 'for', url);
callback(realpath);
return true;
});
}

View File

@@ -7,6 +7,6 @@ interface Window {
*
* @see https://github.com/cawa-93/dts-for-context-bridge
*/
readonly apis: { workspaceSync: (id: string) => Promise<any>; onThemeChange: (theme: string) => Promise<any>; onSidebarVisibilityChange: (visible: boolean) => Promise<any>; googleSignIn: () => Promise<string>; updateEnv: (env: string, value: string) => void; };
readonly apis: { workspaceSync: (id: string) => Promise<any>; onThemeChange: (theme: string) => Promise<any>; onSidebarVisibilityChange: (visible: boolean) => Promise<any>; getGoogleOauthCode: () => Promise<{ requestInit: RequestInit; url: string; }>; updateEnv: (env: string, value: string) => void; };
readonly appInfo: { electron: boolean; isMacOS: boolean; };
}

View File

@@ -23,6 +23,7 @@ import { isMacOS } from '../../utils';
*/
contextBridge.exposeInMainWorld('apis', {
workspaceSync: (id: string) => ipcRenderer.invoke('octo:workspace-sync', id),
// ui
onThemeChange: (theme: string) =>
ipcRenderer.invoke('ui:theme-change', theme),
@@ -31,9 +32,11 @@ contextBridge.exposeInMainWorld('apis', {
ipcRenderer.invoke('ui:sidebar-visibility-change', visible),
/**
* Try sign in using Google and return a Google IDToken
* Try sign in using Google and return a request object to exchange the code for a token
* Not exchange in Node side because it is easier to do it in the renderer with VPN
*/
googleSignIn: (): Promise<string> => ipcRenderer.invoke('ui:google-sign-in'),
getGoogleOauthCode: (): Promise<{ requestInit: RequestInit; url: string }> =>
ipcRenderer.invoke('ui:get-google-oauth-code'),
/**
* Secret backdoor to update environment variables in main process

View File

@@ -1,32 +1,26 @@
{
"name": "@affine/electron",
"productName": "AFFiNE",
"private": true,
"version": "0.0.0",
"version": "0.5.3",
"author": "affine",
"description": "AFFiNE App",
"homepage": "https://github.com/toeverything/AFFiNE",
"workspaces": [
"../../packages/*",
"../../tests/fixtures"
],
"scripts": {
"dev": "cross-env NODE_ENV=development node scripts/dev.mjs",
"prod": "cross-env NODE_ENV=production node scripts/dev.mjs",
"generate-assets": "zx scripts/generate-assets.mjs",
"package": "electron-forge package",
"make": "electron-forge make",
"make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
"make-macos-x64": "electron-forge make --platform=darwin --arch=x64",
"make-windows-x64": "electron-forge make --platform=win32 --arch=x64",
"build:octobase-node": "yarn workspace @affine/octobase-node build",
"postinstall": "ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs"
"make-linux-x64": "electron-forge make --platform=linux --arch=x64"
},
"config": {
"forge": "./forge.config.js"
},
"main": "./dist/layers/main/index.js",
"devDependencies": {
"@affine/octobase-node": "workspace:*",
"@electron-forge/cli": "^6.1.1",
"@electron-forge/core": "^6.1.1",
"@electron-forge/core-utils": "^6.1.1",
@@ -35,17 +29,18 @@
"@electron-forge/maker-squirrel": "^6.1.1",
"@electron-forge/maker-zip": "^6.1.1",
"@electron-forge/shared-types": "^6.1.1",
"@electron/rebuild": "^3.2.10",
"@electron/rebuild": "^3.2.12",
"@electron/remote": "2.0.9",
"dts-for-context-bridge": "^0.7.1",
"electron": "24.0.0",
"esbuild": "^0.17.16",
"electron": "24.1.2",
"electron-squirrel-startup": "1.0.0",
"esbuild": "^0.17.17",
"zx": "^7.2.1"
},
"dependencies": {
"cross-env": "7.0.3",
"electron-window-state": "^5.0.3",
"firebase": "^9.18.0",
"firebase": "^9.19.1",
"fs-extra": "^11.1.1",
"undici": "^5.21.2"
},

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -1,13 +1,5 @@
import fs from 'node:fs';
import path from 'node:path';
import * as url from 'node:url';
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
const { node } = JSON.parse(
fs.readFileSync(
path.join(__dirname, '../electron-vendors.autogen.json'),
'utf-8'
)
);
const NODE_MAJOR_VERSION = 18;
const nativeNodeModulesPlugin = {
name: 'native-node-modules',
@@ -35,7 +27,7 @@ export default () => {
entryPoints: ['layers/main/src/index.ts'],
outdir: 'dist/layers/main',
bundle: true,
target: `node${node}`,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron'],
plugins: [nativeNodeModulesPlugin],
@@ -45,7 +37,7 @@ export default () => {
entryPoints: ['layers/preload/src/index.ts'],
outdir: 'dist/layers/preload',
bundle: true,
target: `node${node}`,
target: `node${NODE_MAJOR_VERSION}`,
platform: 'node',
external: ['electron'],
define: define,

View File

@@ -29,28 +29,38 @@ console.log('build with following dir', {
await cleanup();
echo('Clean up done');
if (process.platform === 'win32') {
$.shell = 'powershell.exe';
$.prefix = '';
}
// step 1: build web (nextjs) dist
process.env.ENABLE_LEGACY_PROVIDER = 'false';
cd(repoRootDir);
await $`yarn add`;
await $`yarn build`;
await $`yarn export`;
// step 1.5: amend sourceMappingURL to allow debugging in devtools
await glob('**/*.{js,css}', { cwd: affineWebOutDir }).then(files => {
return files.map(async file => {
const dir = path.dirname(file);
const fullpath = path.join(affineWebOutDir, file);
let content = await fs.readFile(fullpath, 'utf-8');
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
content = content.replace(/# sourceMappingURL=(.*)\.map/g, (_, p1) => {
return `# sourceMappingURL=assets://./${dir}/${p1}.map`;
});
await fs.writeFile(fullpath, content);
});
});
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
// step 2: build electron resources
await buildLayers();
echo('Build layers done');
// step 3: build octobase-node
let buildOctobaseNode = 'yarn workspace @affine/octobase-node build';
if (process.env.TARGET) {
buildOctobaseNode += ` --target=${process.env.TARGET}`;
}
await $([buildOctobaseNode]);
// step 4: copy octobase-node to electron dist
await fs.ensureDir('./apps/electron/dist/layers/main/');
await $`cp ./packages/octobase-node/octobase.*.node ./apps/electron/dist/layers/main/`;
/// --------
/// --------
/// --------

View File

@@ -1,17 +0,0 @@
/**
* This script should be run in electron context
* @example
* ELECTRON_RUN_AS_NODE=1 electron scripts/update-electron-vendors.mjs
*/
import { writeFileSync } from 'fs';
const electronRelease = process.versions;
const node = electronRelease.node.split('.')[0];
const chrome = electronRelease.v8.split('.').splice(0, 2).join('');
writeFileSync(
'./electron-vendors.autogen.json',
JSON.stringify({ chrome, node })
);

File diff suppressed because it is too large Load Diff

1
apps/server/.env.example Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL="postgresql://affine@localhost:5432/affine"

2
apps/server/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
src/schema.gql

71
apps/server/package.json Normal file
View File

@@ -0,0 +1,71 @@
{
"name": "@affine/server",
"private": true,
"version": "0.5.3",
"description": "Affine Node.js server",
"type": "module",
"scripts": {
"dev": "nodemon ./src/index.ts",
"test": "NODE_ENV=test node --loader ts-node/esm.mjs --es-module-specifier-resolution node --test ./src/tests/*",
"test:coverage": "NODE_ENV=test c8 node --loader ts-node/esm.mjs --es-module-specifier-resolution node --experimental-test-coverage ./src/tests/*"
},
"dependencies": {
"@apollo/server": "^4.6.0",
"@nestjs/apollo": "^11.0.5",
"@nestjs/common": "^9.4.0",
"@nestjs/core": "^9.4.0",
"@nestjs/graphql": "^11.0.5",
"@nestjs/platform-express": "^9.4.0",
"@prisma/client": "^4.12.0",
"dotenv": "^16.0.3",
"graphql": "^16.6.0",
"graphql-type-json": "^0.3.2",
"lodash-es": "^4.17.21",
"prisma": "^4.12.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0"
},
"devDependencies": {
"@nestjs/testing": "^9.4.0",
"@types/lodash-es": "^4.14.194",
"@types/node": "^18.15.11",
"c8": "^7.13.0",
"nodemon": "^2.0.22",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"vitest": "^0.30.1"
},
"nodemonConfig": {
"exec": "node",
"script": "./src/index.ts",
"nodeArgs": [
"--loader",
"ts-node/esm.mjs",
"--es-module-specifier-resolution",
"node"
],
"ignore": [
"**/__tests__/**",
"**/dist/**"
],
"env": {
"TS_NODE_PROJECT": "./tsconfig.json",
"NODE_ENV": "development",
"DEBUG": "affine:*",
"FORCE_COLOR": true,
"DEBUG_COLORS": true
},
"delay": 1000
},
"c8": {
"reporter": [
"text",
"lcov"
],
"report-dir": ".coverage",
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}
}

52
apps/server/schema.prisma Normal file
View File

@@ -0,0 +1,52 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model google_users {
id String @id @db.VarChar
user_id String @db.VarChar
google_id String @unique @db.VarChar
users users @relation(fields: [user_id], references: [id], onDelete: Cascade)
}
model permissions {
id String @id @db.VarChar
workspace_id String @db.VarChar
user_id String? @db.VarChar
user_email String?
type Int @db.SmallInt
accepted Boolean @default(false)
created_at DateTime? @default(now()) @db.Timestamptz(6)
users users? @relation(fields: [user_id], references: [id], onDelete: Cascade)
workspaces workspaces @relation(fields: [workspace_id], references: [id], onDelete: Cascade)
}
model seaql_migrations {
version String @id @db.VarChar
applied_at BigInt
}
model users {
id String @id @db.VarChar
name String @db.VarChar
email String @unique @db.VarChar
avatar_url String? @db.VarChar
token_nonce Int? @default(0) @db.SmallInt
password String? @db.VarChar
created_at DateTime? @default(now()) @db.Timestamptz(6)
google_users google_users[]
permissions permissions[]
}
model workspaces {
id String @id @db.VarChar
public Boolean
type Int @db.SmallInt
created_at DateTime? @default(now()) @db.Timestamptz(6)
permissions permissions[]
}

16
apps/server/src/app.ts Normal file
View File

@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from './config';
import { GqlModule } from './graphql.module';
import { BusinessModules } from './modules';
import { PrismaModule } from './prisma';
@Module({
imports: [
PrismaModule,
GqlModule,
ConfigModule.forRoot(),
...BusinessModules,
],
})
export class AppModule {}

View File

@@ -0,0 +1,202 @@
import type { ApolloDriverConfig } from '@nestjs/apollo';
import type { LeafPaths } from '../utils/types';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace globalThis {
// eslint-disable-next-line no-var
var AFFiNE: AFFiNEConfig;
}
}
export const enum ExternalAccount {
github = 'github',
google = 'google',
firebase = 'firebase',
}
type EnvConfigType = 'string' | 'int' | 'float' | 'boolean';
type ConfigPaths = LeafPaths<
Omit<
AFFiNEConfig,
| 'ENV_MAP'
| 'version'
| 'baseUrl'
| 'origin'
| 'prod'
| 'dev'
| 'test'
| 'deploy'
>,
'',
'....'
>;
/**
* parse number value from environment variables
*/
function int(value: string) {
const n = parseInt(value);
return Number.isNaN(n) ? undefined : n;
}
function float(value: string) {
const n = parseFloat(value);
return Number.isNaN(n) ? undefined : n;
}
function boolean(value: string) {
return value === '1' || value.toLowerCase() === 'true';
}
export function parseEnvValue(value: string | undefined, type?: EnvConfigType) {
if (typeof value === 'undefined') {
return;
}
return type === 'int'
? int(value)
: type === 'float'
? float(value)
: type === 'boolean'
? boolean(value)
: value;
}
/**
* All Configurations that would control AFFiNE server behaviors
*
*/
export interface AFFiNEConfig {
ENV_MAP: Record<string, ConfigPaths | [ConfigPaths, EnvConfigType?]>;
/**
* System version
*/
readonly version: string;
/**
* alias to `process.env.NODE_ENV`
*
* @default 'production'
* @env NODE_ENV
*/
readonly env: string;
/**
* fast environment judge
*/
get prod(): boolean;
get dev(): boolean;
get test(): boolean;
get deploy(): boolean;
/**
* Whether the server is hosted on a ssl enabled domain
*/
https: boolean;
/**
* where the server get deployed.
*
* @default 'localhost'
* @env AFFINE_SERVER_HOST
*/
host: string;
/**
* which port the server will listen on
*
* @default 3000
* @env AFFINE_SERVER_PORT
*/
port: number;
/**
* subpath where the server get deployed if there is.
*
* @default '' // empty string
* @env AFFINE_SERVER_SUB_PATH
*/
path: string;
/**
* Readonly property `baseUrl` is the full url of the server consists of `https://HOST:PORT/PATH`.
*
* if `host` is not `localhost` then the port will be ignored
*/
get baseUrl(): string;
/**
* Readonly property `origin` is domain origin in the form of `https://HOST:PORT` without subpath.
*
* if `host` is not `localhost` then the port will be ignored
*/
get origin(): string;
/**
* the apollo driver config
*/
graphql: ApolloDriverConfig;
/**
* object storage Config
*
* all artifacts and logs will be stored on instance disk,
* and can not shared between instances if not configured
*/
objectStorage: {
/**
* whether use remote object storage
*/
enable: boolean;
/**
* used to store all uploaded builds and analysis reports
*
* the concrete type definition is not given here because different storage providers introduce
* significant differences in configuration
*
* @example
* {
* provider: 'aws',
* region: 'eu-west-1',
* aws_access_key_id: '',
* aws_secret_access_key: '',
* // other aws storage config...
* }
*/
config: Record<string, string>;
};
/**
* authentication config
*/
auth: {
/**
* whether allow user to signup with email directly
*/
enableSignup: boolean;
/**
* whether allow user to signup by oauth providers
*/
enableOauth: boolean;
/**
* all available oauth providers
*/
oauthProviders: Partial<
Record<
ExternalAccount,
{
clientId: string;
clientSecret: string;
/**
* uri to start oauth flow
*/
authorizationUri?: string;
/**
* uri to authenticate `access_token` when user is redirected back from oauth provider with `code`
*/
accessTokenUri?: string;
/**
* uri to get user info with authenticated `access_token`
*/
userInfoUri?: string;
args?: Record<string, any>;
}
>
>;
};
}

View File

@@ -0,0 +1,51 @@
import pkg from '../../package.json' assert { type: 'json' };
import type { AFFiNEConfig } from './def';
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
version: pkg.version,
ENV_MAP: {},
env: process.env.NODE_ENV ?? 'development',
get prod() {
return this.env === 'production';
},
get dev() {
return this.env === 'development';
},
get test() {
return this.env === 'test';
},
get deploy() {
return !this.dev && !this.test;
},
https: false,
host: 'localhost',
port: 3000,
path: '',
get origin() {
return this.dev
? 'http://localhost:8080'
: `${this.https ? 'https' : 'http'}://${this.host}${
this.host === 'localhost' ? `:${this.port}` : ''
}`;
},
get baseUrl() {
return `${this.origin}${this.path}`;
},
graphql: {
buildSchemaOptions: {
numberScalarMode: 'integer',
},
introspection: true,
playground: true,
debug: true,
},
auth: {
enableSignup: true,
enableOauth: false,
oauthProviders: {},
},
objectStorage: {
enable: false,
config: {},
},
});

View File

@@ -0,0 +1,15 @@
import { set } from 'lodash-es';
import { parseEnvValue } from './def';
for (const env in AFFiNE.ENV_MAP) {
const config = AFFiNE.ENV_MAP[env];
const [path, value] =
typeof config === 'string'
? [config, process.env[env]]
: [config[0], parseEnvValue(process.env[env], config[1])];
if (typeof value !== 'undefined') {
set(globalThis.AFFiNE, path, process.env[env]);
}
}

View File

@@ -0,0 +1,69 @@
import type { DynamicModule, FactoryProvider } from '@nestjs/common';
import { merge } from 'lodash-es';
import type { DeepPartial } from '../utils/types';
import type { AFFiNEConfig } from './def';
type ConstructorOf<T> = {
new (): T;
};
function ApplyType<T>(): ConstructorOf<T> {
// @ts-expect-error used to fake the type of config
return class Inner implements T {
constructor() {}
};
}
/**
* usage:
* ```
* import { Config } from '@affine/server'
*
* class TestConfig {
* constructor(private readonly config: Config) {}
* test() {
* return this.config.env
* }
* }
* ```
*/
export class Config extends ApplyType<AFFiNEConfig>() {}
function createConfigProvider(
override?: DeepPartial<Config>
): FactoryProvider<Config> {
return {
provide: Config,
useFactory: () => {
const wrapper = new Config();
const config = merge({}, AFFiNE, override);
const proxy: Config = new Proxy(wrapper, {
get: (_target, property: keyof Config) => {
const desc = Object.getOwnPropertyDescriptor(AFFiNE, property);
if (desc?.get) {
return desc.get.call(proxy);
}
return config[property];
},
});
return proxy;
},
};
}
export class ConfigModule {
static forRoot = (override?: DeepPartial<Config>): DynamicModule => {
const provider = createConfigProvider(override);
return {
global: true,
module: ConfigModule,
providers: [provider],
exports: [provider],
};
};
}
export { AFFiNEConfig } from './def';

View File

@@ -0,0 +1,30 @@
import type { ApolloDriverConfig } from '@nestjs/apollo';
import { ApolloDriver } from '@nestjs/apollo';
import { Global, Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { fileURLToPath } from 'url';
import { Config } from './config';
@Global()
@Module({
imports: [
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
useFactory: (config: Config) => {
return {
...config.graphql,
path: `${config.path}/graphql`,
autoSchemaFile: join(
fileURLToPath(import.meta.url),
'..',
'schema.gql'
),
};
},
inject: [Config],
}),
],
})
export class GqlModule {}

20
apps/server/src/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import './prelude';
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app';
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
cors: {
origin:
process.env.AFFINE_ENV === 'preview'
? ['https://affine-preview.vercel.app']
: ['http://localhost:8080'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: '*',
},
bodyParser: true,
});
await app.listen(process.env.PORT ?? 3010);

View File

@@ -0,0 +1,3 @@
import { WorkspaceModule } from './workspaces';
export const BusinessModules = [WorkspaceModule];

View File

@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { WorkspaceResolver } from './resolver';
@Module({
providers: [WorkspaceResolver],
})
export class WorkspaceModule {}

View File

@@ -0,0 +1,56 @@
import {
Args,
Field,
ObjectType,
Query,
registerEnumType,
Resolver,
} from '@nestjs/graphql';
import type { workspaces } from '@prisma/client';
import { PrismaService } from '../../prisma/service';
export enum WorkspaceType {
Private = 0,
Normal = 1,
}
registerEnumType(WorkspaceType, {
name: 'WorkspaceType',
});
@ObjectType()
export class Workspace implements workspaces {
@Field()
id!: string;
@Field({ description: 'is Public workspace' })
public!: boolean;
@Field(() => WorkspaceType, { description: 'Workspace type' })
type!: WorkspaceType;
@Field({ description: 'Workspace created date' })
created_at!: Date;
}
@Resolver(() => Workspace)
export class WorkspaceResolver {
constructor(private readonly prisma: PrismaService) {}
// debug only query should be removed
@Query(() => [Workspace], {
name: 'workspaces',
description: 'Get all workspaces',
})
async workspaces() {
return this.prisma.workspaces.findMany();
}
@Query(() => Workspace, {
name: 'workspace',
description: 'Get workspace by id',
})
async workspace(@Args('id') id: string) {
return this.prisma.workspaces.findUnique({
where: { id },
});
}
}

View File

@@ -0,0 +1,6 @@
import 'reflect-metadata';
import 'dotenv/config';
import { getDefaultAFFiNEConfig } from './config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();

View File

@@ -0,0 +1,10 @@
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './service';
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,16 @@
import type { INestApplication, OnModuleInit } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}

View File

@@ -0,0 +1,35 @@
import { equal, ok } from 'node:assert';
import { beforeEach, test } from 'node:test';
import { Test } from '@nestjs/testing';
import { Config, ConfigModule } from '../config';
import { getDefaultAFFiNEConfig } from '../config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
let config: Config;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [ConfigModule.forRoot()],
}).compile();
config = module.get(Config);
});
test('should be able to get config', t => {
ok(typeof config.host === 'string');
equal(config.env, 'test');
});
test('should be able to override config', async t => {
const module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
host: 'testing',
}),
],
}).compile();
const config = module.get(Config);
ok(config.host, 'testing');
});

View File

@@ -0,0 +1,42 @@
export type DeepPartial<T> = T extends Array<infer U>
? DeepPartial<U>[]
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends object
? {
[K in keyof T]?: DeepPartial<T[K]>;
}
: T;
type Join<Prefix, Suffixes> = Prefix extends string | number
? Suffixes extends string | number
? Prefix extends ''
? Suffixes
: `${Prefix}.${Suffixes}`
: never
: never;
export type PrimitiveType =
| string
| number
| boolean
| symbol
| null
| undefined;
export type LeafPaths<
T,
Path extends string = '',
MaxDepth extends string = '...',
Depth extends string = ''
> = Depth extends MaxDepth
? never
: T extends Record<string | number, any>
? {
[K in keyof T]-?: K extends string | number
? T[K] extends PrimitiveType
? K
: Join<K, LeafPaths<T[K], Path, MaxDepth, `${Depth}.`>>
: never;
}[keyof T]
: never;

22
apps/server/tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"isolatedModules": false,
"resolveJsonModule": true,
"types": ["node"],
"outDir": "dist",
"noEmit": false
},
"include": ["src", "package.json"],
"exclude": ["dist", "node_modules"],
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}

View File

@@ -17,7 +17,10 @@ EXPOSE_INTERNAL=1
ENABLE_DEBUG_PAGE=
ENABLE_SUBPAGE=
ENABLE_CHANGELOG=1
ENABLE_LEGACY_PROVIDER=true
# Sentry
SENTRY_AUTH_TOKEN=
SENTRY_ORG=
SENTRY_PROJECT=
NEXT_PUBLIC_SENTRY_DSN=

View File

@@ -20,10 +20,7 @@ For more information on Next.js, take a look at the [Next.js Documentation](http
`preset.config.mjs` contains the build presets for the application. The presets are used to configure the build process for different environments. The presets are:
- `enableIndexedDBProvider`: Enables the IndexedDB provider for the application. This is used to store data in the browser.
- `enableBroadCastChannelProvider`: Enables the Broadcast Channel provider for the application. This is used to communicate between local browser tabs.
- `prefetchWorkspace`: **deprecated**
- `exposeInternal`: Exposes internal variables into `globalThis` for debugging purposes.
- `enableDebugPage`: Enables the debug page for the application. This is used for debugging purposes.
## BlockSuite Integration

View File

@@ -4,6 +4,7 @@ import path from 'node:path';
import { PerfseePlugin } from '@perfsee/webpack';
import { withSentryConfig } from '@sentry/nextjs';
import SentryWebpackPlugin from '@sentry/webpack-plugin';
import debugLocal from 'next-debug-local';
import preset from './preset.config.mjs';
@@ -21,6 +22,10 @@ if (enableDebugLocal) {
console.info('Debugging local blocksuite');
}
if (process.env.COVERAGE === 'true') {
console.info('Enable coverage report');
}
const profileTarget = {
ac: '100.85.73.88:12001',
dev: '100.84.105.99:11001',
@@ -73,6 +78,7 @@ const nextConfig = {
},
reactStrictMode: true,
transpilePackages: [
'jotai-devtools',
'@affine/component',
'@affine/i18n',
'@affine/debug',
@@ -113,6 +119,19 @@ const nextConfig = {
config.plugins = [perfsee];
}
}
if (
process.env.SENTRY_AUTH_TOKEN &&
process.env.SENTRY_ORG &&
process.env.SENTRY_PROJECT
) {
config.plugins.push(
new SentryWebpackPlugin({
include: '.next',
ignore: ['node_modules', 'cypress', 'test'],
urlPrefix: '~/_next',
})
);
}
return config;
},
@@ -124,6 +143,7 @@ const nextConfig = {
return profile;
},
basePath: process.env.NEXT_BASE_PATH,
assetPrefix: process.env.NEXT_ASSET_PREFIX,
pageExtensions: [...(preset.enableDebugPage ? ['tsx', 'dev.tsx'] : ['tsx'])],
};

View File

@@ -2,10 +2,10 @@
"name": "@affine/web",
"private": true,
"scripts": {
"dev": "node src/server.mjs",
"dev": "next dev",
"build": "next build",
"export": "next export",
"start": "NODE_ENV=production node src/server.mjs",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
@@ -17,24 +17,26 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230412041719-76e5b5b9-nightly",
"@blocksuite/editor": "0.0.0-20230412041719-76e5b5b9-nightly",
"@blocksuite/global": "0.0.0-20230412041719-76e5b5b9-nightly",
"@blocksuite/icons": "^2.1.9",
"@blocksuite/store": "0.0.0-20230412041719-76e5b5b9-nightly",
"@blocksuite/blocks": "0.0.0-20230416194015-c6ae6f0f-nightly",
"@blocksuite/editor": "0.0.0-20230416194015-c6ae6f0f-nightly",
"@blocksuite/global": "0.0.0-20230416194015-c6ae6f0f-nightly",
"@blocksuite/icons": "^2.1.10",
"@blocksuite/store": "0.0.0-20230416194015-c6ae6f0f-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.10.7",
"@emotion/react": "^11.10.6",
"@emotion/server": "^11.10.0",
"@emotion/styled": "^11.10.6",
"@mui/material": "^5.11.16",
"@sentry/nextjs": "^7.47.0",
"@mui/material": "^5.12.0",
"@react-hookz/web": "^23.0.0",
"@sentry/nextjs": "^7.48.0",
"@toeverything/hooks": "workspace:*",
"cmdk": "^0.2.0",
"css-spring": "^4.1.0",
"dayjs": "^1.11.7",
"jotai": "^2.0.4",
"jotai-devtools": "^0.4.0",
"lit": "^2.7.2",
"lottie-web": "^5.11.0",
"next-themes": "^0.2.1",
@@ -50,13 +52,14 @@
"@perfsee/webpack": "^1.5.0",
"@redux-devtools/extension": "^3.2.5",
"@rich-data/viewer": "^2.15.6",
"@sentry/webpack-plugin": "^1.20.0",
"@swc-jotai/debug-label": "^0.0.9",
"@swc-jotai/react-refresh": "^0.0.7",
"@types/react": "=18.0.31",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/webpack-env": "^1.18.0",
"@vanilla-extract/css": "^1.11.0",
"@vanilla-extract/next-plugin": "^2.1.1",
"@vanilla-extract/next-plugin": "^2.1.2",
"dotenv": "^16.0.3",
"eslint": "^8.38.0",
"eslint-config-next": "^13.3.0",
@@ -67,6 +70,6 @@
"redux": "^4.2.1",
"swc-plugin-coverage-instrument": "=0.0.14",
"typescript": "^5.0.4",
"webpack": "^5.78.0"
"webpack": "^5.79.0"
}
}

View File

@@ -1,16 +1,14 @@
import 'dotenv/config';
const config = {
enableIndexedDBProvider: Boolean(process.env.ENABLE_IDB_PROVIDER ?? '1'),
enableLegacyCloud: process.env.ENABLE_LEGACY_PROVIDER
? process.env.ENABLE_LEGACY_PROVIDER === 'true'
: true,
enableBroadCastChannelProvider: Boolean(
process.env.ENABLE_BC_PROVIDER ?? '1'
),
prefetchWorkspace: Boolean(process.env.PREFETCH_WORKSPACE ?? '1'),
exposeInternal: Boolean(process.env.EXPOSE_INTERNAL ?? '1'),
enableDebugPage: Boolean(
process.env.ENABLE_DEBUG_PAGE ?? process.env.NODE_ENV === 'development'
),
enableSubpage: Boolean(process.env.ENABLE_SUBPAGE),
enableChangeLog: Boolean(process.env.ENABLE_CHANGELOG),
};
export default config;

View File

@@ -5,4 +5,7 @@ const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [new Sentry.Replay()],
});

View File

@@ -1,40 +1,68 @@
import { DebugLogger } from '@affine/debug';
import { atomWithSyncStorage } from '@affine/jotai';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
rootCurrentEditorAtom,
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms');
// workspace necessary atoms
export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null);
export const currentEditorAtom = atom<Readonly<EditorContainer> | null>(null);
/**
* @deprecated Use `rootCurrentWorkspaceIdAtom` directly instead.
*/
export const currentWorkspaceIdAtom = rootCurrentWorkspaceIdAtom;
// todo(himself65): move this to the workspace package
rootWorkspacesMetadataAtom.onMount = setAtom => {
function createFirst(): RootWorkspaceMetadata[] {
const Plugins = Object.values(WorkspacePlugins).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
} satisfies RootWorkspaceMetadata)
);
}).filter((ids): ids is RootWorkspaceMetadata => !!ids);
}
setAtom(metadata => {
if (metadata.length === 0) {
const newMetadata = createFirst();
logger.info('create first workspace', newMetadata);
return newMetadata;
}
return metadata;
});
};
/**
* @deprecated Use `rootCurrentPageIdAtom` directly instead.
*/
export const currentPageIdAtom = rootCurrentPageIdAtom;
/**
* @deprecated Use `rootCurrentEditorAtom` directly instead.
*/
export const currentEditorAtom = rootCurrentEditorAtom;
// modal atoms
export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom(false);
export const openQuickSearchModalAtom = atom(false);
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(jotaiWorkspacesAtom).filter(workspace =>
flavours.includes(workspace.flavour)
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
export { workspacesAtom } from './root';
type View = { id: string; mode: 'page' | 'edgeless' };

View File

@@ -9,13 +9,17 @@ import { affineApis } from '../../shared/apis';
function createPublicWorkspace(
workspaceId: string,
binary: ArrayBuffer
binary: ArrayBuffer,
singlePage = false
): AffinePublicWorkspace {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspaceId,
(k: string) =>
// fixme: token could be expired
({ api: `api/workspace`, token: getLoginStorage()?.token }[k])
({ api: `api/workspace`, token: getLoginStorage()?.token }[k]),
{
cachePrefix: WorkspaceFlavour.PUBLIC + (singlePage ? '-single-page' : ''),
}
);
BlockSuiteWorkspace.Y.applyUpdate(
blockSuiteWorkspace.doc,
@@ -49,7 +53,7 @@ export const publicPageBlockSuiteAtom = atom<Promise<AffinePublicWorkspace>>(
workspaceId,
pageId
);
return createPublicWorkspace(workspaceId, binary);
return createPublicWorkspace(workspaceId, binary, true);
}
);
export const publicWorkspaceAtom = atom<Promise<AffinePublicWorkspace>>(
@@ -59,6 +63,6 @@ export const publicWorkspaceAtom = atom<Promise<AffinePublicWorkspace>>(
throw new Error('No workspace id');
}
const binary = await affineApis.downloadWorkspace(workspaceId, true);
return createPublicWorkspace(workspaceId, binary);
return createPublicWorkspace(workspaceId, binary, false);
}
);

View File

@@ -0,0 +1,87 @@
//#region async atoms that to load the real workspace data
import { DebugLogger } from '@affine/debug';
import { config } from '@affine/env';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms:root');
/**
* Fetch all workspaces from the Plugin CRUD
*/
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom)
.filter(
workspace => flavours.includes(workspace.flavour)
// TODO: remove this when we remove the legacy cloud
)
.filter(workspace =>
!config.enableLegacyCloud
? workspace.flavour !== WorkspaceFlavour.AFFINE
: true
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
logger.info('workspaces', workspaces);
workspaces.forEach(workspace => {
if (workspace === null) {
console.warn(
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
);
}
});
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
/**
* This will throw an error if the workspace is not found,
* should not be used on the root component,
* use `rootCurrentWorkspaceIdAtom` instead
*/
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
async get => {
const metadata = get(rootWorkspacesMetadataAtom);
const targetId = get(rootCurrentWorkspaceIdAtom);
if (targetId === null) {
throw new Error(
'current workspace id is null. this should not happen. If you see this error, please report it to the developer.'
);
}
const targetWorkspace = metadata.find(meta => meta.id === targetId);
if (!targetWorkspace) {
throw new Error(`cannot find the workspace with id ${targetId}.`);
}
const workspace = await WorkspacePlugins[targetWorkspace.flavour].CRUD.get(
targetWorkspace.id
);
if (!workspace) {
throw new Error(
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
);
}
return workspace;
}
);
// Do not add `rootCurrentWorkspacePageAtom`, this is not needed.
// It can be derived from `rootCurrentWorkspaceAtom` and `rootCurrentPageIdAtom`
//#endregion

View File

@@ -19,8 +19,7 @@ export const createAffineProviders = (
createAffineWebSocketProvider(blockSuiteWorkspace),
config.enableBroadCastChannelProvider &&
createBroadCastChannelProvider(blockSuiteWorkspace),
config.enableIndexedDBProvider &&
createIndexedDBProvider(blockSuiteWorkspace),
createIndexedDBProvider(blockSuiteWorkspace),
] as any[]
).filter(v => Boolean(v));
};

View File

@@ -1,4 +1,3 @@
'use client';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
@@ -11,7 +10,9 @@ import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
'test',
_ => undefined,
Generator.AutoIncrement
{
idGenerator: Generator.AutoIncrement,
}
);
const page = blockSuiteWorkspace.createPage('page0');

View File

@@ -3,6 +3,7 @@
*/
import 'fake-indexeddb/auto';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import type { PageMeta } from '@blocksuite/store';
import matchers from '@testing-library/jest-dom/matchers';
import type { RenderResult } from '@testing-library/react';
@@ -12,11 +13,9 @@ import type { FC, PropsWithChildren } from 'react';
import { beforeEach, describe, expect, test } from 'vitest';
import { workspacesAtom } from '../../atoms';
import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../../hooks/current/use-current-workspace';
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useAppHelper } from '../../hooks/use-workspaces';
import { ThemeProvider } from '../../providers/ThemeProvider';
import type { BlockSuiteWorkspace } from '../../shared';
import type { PinboardProps } from '../pure/workspace-slider-bar/Pinboard';
@@ -42,24 +41,26 @@ const initPinBoard = async () => {
// - pinboard2
// - noPinboardPage
const mutationHook = renderHook(() => useWorkspacesHelper(), {
const mutationHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
const rootPageIds = ['hasPinboardPage', 'noPinboardPage'];
const pinboardPageIds = ['pinboard1', 'pinboard2'];
const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(workspacesAtom);
mutationHook.rerender();
await store.get(currentWorkspaceAtom);
store.set(rootCurrentWorkspaceIdAtom, id);
await store.get(workspacesAtom);
await store.get(rootCurrentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
currentWorkspaceHook.result.current[1](id);
const currentWorkspace = await store.get(currentWorkspaceAtom);
const currentWorkspace = await store.get(rootCurrentWorkspaceAtom);
const blockSuiteWorkspace =
currentWorkspace?.blockSuiteWorkspace as BlockSuiteWorkspace;
mutationHook.rerender();
// create root pinboard
mutationHook.result.current.createWorkspacePage(id, 'rootPinboard');
blockSuiteWorkspace.meta.setPageMeta('rootPinboard', {
@@ -73,7 +74,7 @@ const initPinBoard = async () => {
subpageIds: rootPageId === rootPageIds[0] ? pinboardPageIds : [],
});
});
// create children to firs parent
// create children to first parent
pinboardPageIds.forEach(pinboardId => {
mutationHook.result.current.createWorkspacePage(id, pinboardId);
blockSuiteWorkspace.meta.setPageMeta(pinboardId, {

View File

@@ -3,23 +3,27 @@
*/
import 'fake-indexeddb/auto';
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
} from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { render, renderHook } from '@testing-library/react';
import { createStore, getDefaultStore, Provider } from 'jotai';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { createStore, getDefaultStore, Provider, useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import type React from 'react';
import { useCallback } from 'react';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { workspacesAtom } from '../../atoms';
import { useCurrentPageId } from '../../hooks/current/use-current-page-id';
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../../hooks/current/use-current-workspace';
import { useBlockSuiteWorkspaceHelper } from '../../hooks/use-blocksuite-workspace-helper';
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
import { useAppHelper } from '../../hooks/use-workspaces';
import { ThemeProvider } from '../../providers/ThemeProvider';
import { pathGenerator } from '../../shared';
import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar';
@@ -45,21 +49,22 @@ describe('WorkSpaceSliderBar', () => {
const onOpenWorkspaceListModalFn = vi.fn();
const onOpenQuickSearchModalFn = vi.fn();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
const mutationHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(workspacesAtom);
mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'test1');
await store.get(currentWorkspaceAtom);
store.set(rootCurrentWorkspaceIdAtom, id);
await store.get(rootCurrentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
let i = 0;
const Component = () => {
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId();
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
assertExists(currentWorkspace);
const helper = useBlockSuiteWorkspaceHelper(
currentWorkspace.blockSuiteWorkspace

View File

@@ -122,6 +122,7 @@ export class AffineErrorBoundary extends Component<
return (
<>
<h1>Sorry.. there was an error</h1>
{error.message ?? error.toString()}
</>
);
}

View File

@@ -0,0 +1,58 @@
import { MenuItem, styled } from '@affine/component';
import type { PublicLinkDisableProps } from '@affine/component/share-menu';
import { PublicLinkDisableModal } from '@affine/component/share-menu';
import { useTranslation } from '@affine/i18n';
import { ShareIcon } from '@blocksuite/icons';
import type { CommonMenuItemProps } from './types';
const StyledMenuItem = styled(MenuItem)(({ theme }) => {
return {
div: {
color: theme.palette.error.main,
svg: {
color: theme.palette.error.main,
},
},
':hover': {
div: {
color: theme.palette.error.main,
svg: {
color: theme.palette.error.main,
},
},
},
};
});
export const DisablePublicSharing = ({
onSelect,
onItemClick,
testId,
}: CommonMenuItemProps) => {
const { t } = useTranslation();
return (
<>
<StyledMenuItem
data-testid={testId}
onClick={() => {
onItemClick?.();
onSelect?.();
}}
style={{ color: 'red' }}
icon={<ShareIcon />}
>
{t('Disable Public Sharing')}
</StyledMenuItem>
</>
);
};
const DisablePublicSharingModal = ({
page,
open,
onClose,
}: PublicLinkDisableProps) => {
return <PublicLinkDisableModal page={page} open={open} onClose={onClose} />;
};
DisablePublicSharing.DisablePublicSharingModal = DisablePublicSharingModal;

View File

@@ -1,4 +1,5 @@
export * from './CopyLink';
export * from './DisablePublicSharing';
export * from './Export';
export * from './MoveTo';
export * from './MoveToTrash';

View File

@@ -3,9 +3,9 @@ import { Input, PureMenu, TreeView } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { RemoveIcon, SearchIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import React, { useCallback, useMemo, useState } from 'react';
import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import { usePinboardData } from '../../../../hooks/use-pinboard-data';
import { usePinboardHandler } from '../../../../hooks/use-pinboard-handler';
import type { BlockSuiteWorkspace } from '../../../../shared';

View File

@@ -5,8 +5,8 @@ import { StyledPinboard } from '../styles';
export const EmptyItem = () => {
const { t } = useTranslation();
return (
<StyledPinboard disable={true} style={{ paddingLeft: '32px' }}>
{t('No item')}
<StyledPinboard disable={true} textWrap={true}>
{t('Organize pages to build knowledge')}
</StyledPinboard>
);
};

View File

@@ -8,9 +8,9 @@ import {
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useTheme } from '@mui/material';
import { useMemo, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import { useMetaHelper } from '../../../../hooks/affine/use-meta-helper';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { CopyLink, MoveToTrash } from '../../operation-menu-items';
@@ -44,12 +44,13 @@ export const OperationButton = ({
} = useTheme();
const { t } = useTranslation();
const timer = useRef<ReturnType<typeof setTimeout>>();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [operationMenuOpen, setOperationMenuOpen] = useState(false);
const [pinboardMenuOpen, setPinboardMenuOpen] = useState(false);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const menuIndex = useMemo(() => modalIndex + 1, [modalIndex]);
const { removeToTrash } = useMetaHelper(blockSuiteWorkspace);
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
return (
<MuiClickAwayListener
@@ -63,8 +64,13 @@ export const OperationButton = ({
e.stopPropagation();
}}
onMouseLeave={() => {
setOperationMenuOpen(false);
setPinboardMenuOpen(false);
timer.current = setTimeout(() => {
setOperationMenuOpen(false);
setPinboardMenuOpen(false);
}, 150);
}}
onMouseEnter={() => {
clearTimeout(timer.current);
}}
>
<StyledOperationButton

View File

@@ -2,15 +2,16 @@ import { Input } from '@affine/component';
import {
ArrowDownSmallIcon,
EdgelessIcon,
LevelIcon,
PageIcon,
PivotsIcon,
} from '@blocksuite/icons';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import { useMemo, useState } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { usePageMetaHelper } from '../../../../hooks/use-page-meta';
import type { PinboardNode } from '../../../../hooks/use-pinboard-data';
import { StyledCollapsedButton, StyledPinboard } from '../styles';
import EmptyItem from './EmptyItem';
@@ -19,17 +20,25 @@ import { OperationButton } from './OperationButton';
const getIcon = (type: 'root' | 'edgeless' | 'page') => {
switch (type) {
case 'root':
return <PivotsIcon />;
return <PivotsIcon className="mode-icon" />;
case 'edgeless':
return <EdgelessIcon />;
return <EdgelessIcon className="mode-icon" />;
default:
return <PageIcon />;
return <PageIcon className="mode-icon" />;
}
};
export const PinboardRender: PinboardNode['render'] = (
node,
{ isOver, onAdd, onDelete, collapsed, setCollapsed, isSelected },
{
isOver,
onAdd,
onDelete,
collapsed,
setCollapsed,
isSelected,
disableCollapse,
},
renderProps
) => {
const {
@@ -38,6 +47,7 @@ export const PinboardRender: PinboardNode['render'] = (
currentMeta,
metas = [],
blockSuiteWorkspace,
asPath,
} = renderProps!;
const record = useAtomValue(workspacePreferredModeAtom);
const { setPageTitle } = usePageMetaHelper(blockSuiteWorkspace);
@@ -60,17 +70,22 @@ export const PinboardRender: PinboardNode['render'] = (
onMouseLeave={() => setIsHover(false)}
isOver={isOver || isSelected}
active={active}
disableCollapse={!!disableCollapse}
>
<StyledCollapsedButton
collapse={collapsed}
show={!!node.children?.length}
onClick={e => {
e.stopPropagation();
setCollapsed(node.id, !collapsed);
}}
>
<ArrowDownSmallIcon />
</StyledCollapsedButton>
{!disableCollapse && (
<StyledCollapsedButton
collapse={collapsed}
show={!!node.children?.length}
onClick={e => {
e.stopPropagation();
setCollapsed(node.id, !collapsed);
}}
>
<ArrowDownSmallIcon />
</StyledCollapsedButton>
)}
{asPath && !isRoot ? <LevelIcon className="path-icon" /> : null}
{getIcon(isRoot ? 'root' : record[node.id])}
{showRename ? (

View File

@@ -12,7 +12,8 @@ export const StyledCollapsedButton = styled('button')<{
}>(({ collapse, show = true, theme }) => {
return {
width: '16px',
height: '16px',
height: '100%',
...displayFlex('center', 'center'),
fontSize: '16px',
position: 'absolute',
left: '0',
@@ -21,9 +22,13 @@ export const StyledCollapsedButton = styled('button')<{
margin: 'auto',
color: theme.colors.iconColor,
opacity: '.6',
transition: 'opacity .15s ease-in-out',
display: show ? 'flex' : 'none',
svg: {
transform: `rotate(${collapse ? '0' : '-90'}deg)`,
transform: `rotate(${collapse ? '-90' : '0'}deg)`,
},
':hover': {
opacity: '1',
},
};
});
@@ -32,40 +37,63 @@ export const StyledPinboard = styled('div')<{
disable?: boolean;
active?: boolean;
isOver?: boolean;
}>(({ disable = false, active = false, theme, isOver }) => {
return {
width: '100%',
height: '32px',
borderRadius: '8px',
...displayFlex('flex-start', 'center'),
padding: '0 2px 0 16px',
position: 'relative',
color: disable
? theme.colors.disableColor
: active
? theme.colors.primaryColor
: theme.colors.textColor,
cursor: disable ? 'not-allowed' : 'pointer',
background: isOver ? alpha(theme.colors.primaryColor, 0.06) : '',
fontSize: theme.font.base,
userSelect: 'none',
span: {
flexGrow: '1',
textAlign: 'left',
...textEllipsis(1),
},
'> svg': {
fontSize: '20px',
marginRight: '8px',
flexShrink: '0',
color: active ? theme.colors.primaryColor : theme.colors.iconColor,
},
disableCollapse?: boolean;
textWrap?: boolean;
}>(
({
disableCollapse,
disable = false,
active = false,
theme,
isOver,
textWrap = false,
}) => {
return {
width: '100%',
lineHeight: '1.5',
minHeight: '32px',
borderRadius: '8px',
...displayFlex('flex-start', 'center'),
padding: disableCollapse ? '0 5px' : '0 2px 0 16px',
position: 'relative',
color: disable
? theme.colors.disableColor
: active
? theme.colors.primaryColor
: theme.colors.textColor,
cursor: disable ? 'not-allowed' : 'pointer',
background: isOver ? alpha(theme.colors.primaryColor, 0.06) : '',
fontSize: theme.font.base,
userSelect: 'none',
...(textWrap
? {
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
}
: {}),
':hover': {
backgroundColor: disable ? '' : theme.colors.hoverBackground,
},
};
});
span: {
flexGrow: '1',
textAlign: 'left',
...textEllipsis(1),
},
'.path-icon': {
fontSize: '16px',
transform: 'translateY(-4px)',
},
'.mode-icon': {
fontSize: '20px',
marginRight: '8px',
flexShrink: '0',
color: active ? theme.colors.primaryColor : theme.colors.iconColor,
},
':hover': {
backgroundColor: disable ? '' : theme.colors.hoverBackground,
},
};
}
);
export const StyledOperationButton = styled(IconButton, {
shouldForwardProp: prop => {

View File

@@ -13,12 +13,14 @@ import { StyledSidebarSwitch } from './style';
type SidebarSwitchProps = {
visible?: boolean;
tooltipContent?: string;
testid?: string;
};
// fixme: the following code is not correct, SSR will fail because hydrate will not match the client side render
// in `StyledSidebarSwitch` component
export const SidebarSwitch = ({
visible = true,
tooltipContent,
testid = '',
...props
}: SidebarSwitchProps) => {
useUpdateTipsOnVersionChange();
const [open, setOpen] = useSidebarStatus();
@@ -38,9 +40,9 @@ export const SidebarSwitch = ({
visible={tooltipVisible}
>
<StyledSidebarSwitch
{...props}
visible={visible}
disabled={!visible}
data-testid={testid}
onClick={useCallback(() => {
setOpen(!open);
setTooltipVisible(false);

View File

@@ -19,7 +19,11 @@ export const TransformWorkspaceToAffineModal: React.FC<
const user = useCurrentUser();
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<Modal
open={open}
onClose={onClose}
data-testid="enable-affine-cloud-modal"
>
<ModalWrapper width={560} height={292}>
<Header>
<IconButton

View File

@@ -1,4 +1,5 @@
import { Button, IconButton, Menu, MenuItem, Wrapper } from '@affine/component';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { PermissionType } from '@affine/workspace/affine/api';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
@@ -171,16 +172,18 @@ const LocalCollaborationPanel: React.FC<
return (
<>
<Wrapper marginBottom="42px">{t('Collaboration Description')}</Wrapper>
<Button
data-testid="local-workspace-enable-cloud-button"
type="light"
shape="circle"
onClick={() => {
setOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
{config.enableLegacyCloud && (
<Button
data-testid="local-workspace-enable-cloud-button"
type="light"
shape="circle"
onClick={() => {
setOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
)}
<TransformWorkspaceToAffineModal
open={open}
onClose={() => {

View File

@@ -1,7 +1,7 @@
import { Button, Input, Modal, ModalCloseButton } from '@affine/component';
import { Trans, useTranslation } from '@affine/i18n';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-blocksuite-workspace-name';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useCallback, useState } from 'react';
import type { AffineOfficialWorkspace } from '../../../../../../shared';

View File

@@ -2,8 +2,8 @@ import { Button, FlexWrapper, MuiFade } from '@affine/component';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { useTranslation } from '@affine/i18n';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-blocksuite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-blocksuite-workspace-name';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import type React from 'react';
import { useState } from 'react';

View File

@@ -5,6 +5,7 @@ import {
Input,
Wrapper,
} from '@affine/component';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
@@ -89,8 +90,8 @@ const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
<Wrapper marginBottom="42px">{t('Publishing Description')}</Wrapper>
<Button
data-testid="publish-to-web-button"
onClick={() => {
publishWorkspace(true);
onClick={async () => {
await publishWorkspace(true);
}}
type="light"
shape="circle"
@@ -120,16 +121,18 @@ const PublishPanelLocal: React.FC<PublishPanelLocalProps> = ({
>
{t('Publishing')}
</Box>
<Button
data-testid="publish-enable-affine-cloud-button"
type="light"
shape="circle"
onClick={() => {
setOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
{config.enableLegacyCloud && (
<Button
data-testid="publish-enable-affine-cloud-button"
type="light"
shape="circle"
onClick={() => {
setOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
)}
<EnableAffineCloudModal
open={open}
onClose={() => {

View File

@@ -1,8 +1,8 @@
import { Content, FlexWrapper, styled } from '@affine/component';
import { Trans, useTranslation } from '@affine/i18n';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-blocksuite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-blocksuite-workspace-name';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import type React from 'react';
import { useCurrentUser } from '../../../../../hooks/current/use-current-user';

View File

@@ -16,12 +16,17 @@ import {
ResetIcon,
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import type React from 'react';
import { useState } from 'react';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { MoveTo, MoveToTrash } from '../../../affine/operation-menu-items';
import {
DisablePublicSharing,
MoveTo,
MoveToTrash,
} from '../../../affine/operation-menu-items';
export type OperationCellProps = {
pageMeta: PageMeta;
@@ -40,12 +45,24 @@ export const OperationCell: React.FC<OperationCellProps> = ({
onToggleFavoritePage,
onToggleTrashPage,
}) => {
const { id, favorite } = pageMeta;
const { id, favorite, isPublic } = pageMeta;
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [openDisableShared, setOpenDisableShared] = useState(false);
const page = blockSuiteWorkspace.getPage(id);
assertExists(page);
const OperationMenu = (
<>
{isPublic && (
<DisablePublicSharing
testId="disable-public-sharing"
onItemClick={() => {
setOpenDisableShared(true);
}}
/>
)}
<MenuItem
onClick={() => {
onToggleFavoritePage(id);
@@ -111,6 +128,13 @@ export const OperationCell: React.FC<OperationCellProps> = ({
setOpen(false);
}}
/>
<DisablePublicSharing.DisablePublicSharingModal
page={page}
open={openDisableShared}
onClose={() => {
setOpenDisableShared(false);
}}
/>
</>
);
};

View File

@@ -17,16 +17,16 @@ import {
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useMediaQuery, useTheme } from '@mui/material';
import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import type React from 'react';
import { useMemo } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { useMetaHelper } from '../../../../hooks/affine/use-meta-helper';
import {
usePageMeta,
usePageMetaHelper,
} from '../../../../hooks/use-page-meta';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import DateCell from './DateCell';
@@ -81,7 +81,7 @@ const FavoriteTag: React.FC<FavoriteTagProps> = ({
type PageListProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
isPublic?: boolean;
listType?: 'all' | 'trash' | 'favorite';
listType?: 'all' | 'trash' | 'favorite' | 'shared';
onClickPage: (pageId: string, newTab?: boolean) => void;
};
@@ -92,6 +92,7 @@ const filter = {
return !parentMeta?.trash && pageMeta.trash;
},
favorite: (pageMeta: PageMeta) => pageMeta.favorite && !pageMeta.trash,
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
};
export const PageList: React.FC<PageListProps> = ({
@@ -100,14 +101,15 @@ export const PageList: React.FC<PageListProps> = ({
listType,
onClickPage,
}) => {
const pageList = usePageMeta(blockSuiteWorkspace);
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
const helper = usePageMetaHelper(blockSuiteWorkspace);
const { removeToTrash, restoreFromTrash } =
useMetaHelper(blockSuiteWorkspace);
useBlockSuiteMetaHelper(blockSuiteWorkspace);
const { t } = useTranslation();
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.up('sm'));
const isTrash = listType === 'trash';
const isShared = listType === 'shared';
const record = useAtomValue(workspacePreferredModeAtom);
const list = useMemo(
() =>
@@ -130,7 +132,11 @@ export const PageList: React.FC<PageListProps> = ({
<TableCell proportion={0.5}>{t('Title')}</TableCell>
<TableCell proportion={0.2}>{t('Created')}</TableCell>
<TableCell proportion={0.2}>
{isTrash ? t('Moved to Trash') : t('Updated')}
{isTrash
? t('Moved to Trash')
: isShared
? 'Shared'
: t('Updated')}
</TableCell>
<TableCell proportion={0.1}></TableCell>
</>

View File

@@ -1,9 +1,9 @@
import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue, useSetAtom } from 'jotai';
import type { CSSProperties } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { usePageMeta } from '../../../../hooks/use-page-meta';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { StyledEditorModeSwitch } from './style';
@@ -24,7 +24,7 @@ export const EditorModeSwitch = ({
const currentMode =
useAtomValue(workspacePreferredModeAtom)[pageId] ?? 'page';
const setMode = useSetAtom(workspacePreferredModeAtom);
const pageMeta = usePageMeta(blockSuiteWorkspace).find(
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
assertExists(pageMeta);

View File

@@ -0,0 +1,39 @@
import { displayFlex, styled, TextButton } from '@affine/component';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { useRouterHelper } from '../../../../hooks/use-router-helper';
export const EditPage = () => {
const router = useRouter();
const pageId = router.query.pageId as string;
const workspaceId = router.query.workspaceId as string;
const { jumpToPage } = useRouterHelper(router);
const onClickPage = useCallback(() => {
if (workspaceId && pageId) {
jumpToPage(workspaceId, pageId);
}
}, [jumpToPage, pageId, workspaceId]);
return (
<div>
<StyledEditPageButton onClick={() => onClickPage()}>
Edit Page
</StyledEditPageButton>
</div>
);
};
export default EditPage;
const StyledEditPageButton = styled(
TextButton,
{}
)(({ theme }) => {
return {
border: `1px solid ${theme.colors.primaryColor}`,
color: theme.colors.primaryColor,
width: '100%',
borderRadius: '8px',
whiteSpace: 'nowrap',
padding: '0 16px',
...displayFlex('center', 'center'),
};
});

View File

@@ -10,17 +10,17 @@ import {
} from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useTheme } from '@mui/material';
import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtom } from 'jotai';
import { useState } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { useMetaHelper } from '../../../../hooks/affine/use-meta-helper';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import {
usePageMeta,
usePageMetaHelper,
} from '../../../../hooks/use-page-meta';
import { toast } from '../../../../utils';
import {
Export,
@@ -38,17 +38,17 @@ export const EditorOptionMenu = () => {
assertExists(workspace);
assertExists(pageId);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const pageMeta = usePageMeta(blockSuiteWorkspace).find(
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
const allMetas = usePageMeta(blockSuiteWorkspace);
const allMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
const [record, set] = useAtom(workspacePreferredModeAtom);
const mode = record[pageId] ?? 'page';
assertExists(pageMeta);
const { favorite } = pageMeta;
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const [openConfirm, setOpenConfirm] = useState(false);
const { removeToTrash } = useMetaHelper(blockSuiteWorkspace);
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const EditMenu = (
<>
<MenuItem

View File

@@ -0,0 +1,116 @@
import { ShareMenu } from '@affine/component/share-menu';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { Page } from '@blocksuite/store';
import { assertEquals } from '@blocksuite/store';
import { useRouter } from 'next/router';
import type React from 'react';
import { useCallback, useState } from 'react';
import { useToggleWorkspacePublish } from '../../../../hooks/affine/use-toggle-workspace-publish';
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
import { useRouterHelper } from '../../../../hooks/use-router-helper';
import { WorkspaceSubPath } from '../../../../shared';
import { Unreachable } from '../../../affine/affine-error-eoundary';
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
import type { BaseHeaderProps } from '../header';
const AffineHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
// todo: these hooks should be moved to the top level
const togglePublish = useToggleWorkspacePublish(
props.workspace as AffineWorkspace
);
const helper = useRouterHelper(useRouter());
return (
<ShareMenu
workspace={props.workspace as AffineWorkspace}
currentPage={props.currentPage as Page}
onEnableAffineCloud={useCallback(async () => {
throw new Unreachable(
'Affine workspace should not enable affine cloud again'
);
}, [])}
onOpenWorkspaceSettings={useCallback(
async workspace => {
return helper.jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
},
[helper]
)}
togglePagePublic={useCallback(async (page, isPublic) => {
page.workspace.setPageMeta(page.id, { isPublic });
}, [])}
toggleWorkspacePublish={useCallback(
async (workspace, publish) => {
assertEquals(workspace.flavour, WorkspaceFlavour.AFFINE);
assertEquals(workspace.id, props.workspace.id);
await togglePublish(publish);
},
[props.workspace.id, togglePublish]
)}
/>
);
};
const LocalHeaderShareMenu: React.FC<BaseHeaderProps> = props => {
// todo: these hooks should be moved to the top level
const onTransformWorkspace = useOnTransformWorkspace();
const helper = useRouterHelper(useRouter());
const [open, setOpen] = useState(false);
return (
<>
<ShareMenu
workspace={props.workspace as LocalWorkspace}
currentPage={props.currentPage as Page}
onEnableAffineCloud={useCallback(
async workspace => {
assertEquals(workspace.flavour, WorkspaceFlavour.LOCAL);
assertEquals(workspace.id, props.workspace.id);
setOpen(true);
},
[props.workspace.id]
)}
onOpenWorkspaceSettings={useCallback(
async workspace => {
await helper.jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
},
[helper]
)}
togglePagePublic={useCallback(async (page, isPublic) => {
// local workspace should not have public page
throw new Error('unreachable');
}, [])}
toggleWorkspacePublish={useCallback(
async (workspace, publish) => {
assertEquals(workspace.flavour, WorkspaceFlavour.LOCAL);
assertEquals(workspace.id, props.workspace.id);
await helper.jumpToSubPath(workspace.id, WorkspaceSubPath.SETTING);
},
[helper, props.workspace.id]
)}
/>
<TransformWorkspaceToAffineModal
open={open}
onClose={() => {
setOpen(false);
}}
onConform={() => {
onTransformWorkspace(
WorkspaceFlavour.LOCAL,
WorkspaceFlavour.AFFINE,
props.workspace as LocalWorkspace
);
setOpen(false);
}}
/>
</>
);
};
export const HeaderShareMenu: React.FC<BaseHeaderProps> = props => {
if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
return <AffineHeaderShareMenu {...props} />;
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <LocalHeaderShareMenu {...props} />;
}
throw new Error('unreachable');
};

View File

@@ -1,4 +1,5 @@
import { displayFlex, IconButton, styled, Tooltip } from '@affine/component';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import {
getLoginStorage,
@@ -80,6 +81,10 @@ export const SyncUser = () => {
const { t } = useTranslation();
const transformWorkspace = useTransformWorkspace();
if (!config.enableLegacyCloud) {
return null;
}
if (status === 'offline') {
return (
<Tooltip

View File

@@ -1,13 +1,13 @@
import { Button, Confirm } from '@affine/component';
import { useTranslation } from '@affine/i18n';
import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useMetaHelper } from '../../../../hooks/affine/use-meta-helper';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { usePageMeta } from '../../../../hooks/use-page-meta';
export const TrashButtonGroup = () => {
// fixme(himself65): remove these hooks ASAP
@@ -16,13 +16,13 @@ export const TrashButtonGroup = () => {
assertExists(workspace);
assertExists(pageId);
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const pageMeta = usePageMeta(blockSuiteWorkspace).find(
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
assertExists(pageMeta);
const { t } = useTranslation();
const router = useRouter();
const { restoreFromTrash } = useMetaHelper(blockSuiteWorkspace);
const { restoreFromTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const [open, setOpen] = useState(false);

View File

@@ -0,0 +1,101 @@
import { Menu, MenuItem } from '@affine/component';
import { AffineIcon, SignOutIcon } from '@blocksuite/icons';
import type { CSSProperties } from 'react';
import { forwardRef } from 'react';
import { useCurrentUser } from '../../../../hooks/current/use-current-user';
const EditMenu = (
<MenuItem data-testid="editor-option-menu-favorite" icon={<SignOutIcon />}>
Sign Out
</MenuItem>
);
export const UserAvatar = () => {
const user = useCurrentUser();
return (
<Menu
width={276}
content={EditMenu}
placement="bottom-end"
disablePortal={true}
trigger="click"
>
{user ? (
<WorkspaceAvatar
size={24}
name={user.name}
avatar={user.avatar_url}
></WorkspaceAvatar>
) : (
<WorkspaceAvatar size={24}></WorkspaceAvatar>
)}
</Menu>
);
};
interface WorkspaceAvatarProps {
size: number;
name?: string;
avatar?: string;
style?: CSSProperties;
}
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
function WorkspaceAvatar(props, ref) {
const size = props.size || 20;
const sizeStr = size + 'px';
return (
<>
{props.avatar ? (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
<picture>
<img
style={{ width: sizeStr, height: sizeStr }}
src={props.avatar}
alt=""
referrerPolicy="no-referrer"
/>
</picture>
</div>
) : (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
border: '1px solid #fff',
color: '#fff',
fontSize: Math.ceil(0.5 * size) + 'px',
borderRadius: '50%',
textAlign: 'center',
lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
{props.name ? (
props.name.substring(0, 1)
) : (
<AffineIcon fontSize={24} color={'#5438FF'} />
)}
</div>
)}
</>
);
}
);
export default UserAvatar;

View File

@@ -1,20 +1,29 @@
import { useTranslation } from '@affine/i18n';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { CloseIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import type React from 'react';
import { forwardRef, useEffect, useMemo, useState } from 'react';
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
import {
forwardRef,
lazy,
Suspense,
useEffect,
useMemo,
useState,
} from 'react';
import {
useSidebarFloating,
useSidebarStatus,
} from '../../../hooks/use-sidebar-status';
import type { AffineOfficialWorkspace } from '../../../shared';
import { SidebarSwitch } from '../../affine/sidebar-switch';
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
import EditPage from './header-right-items/EditPage';
import { HeaderShareMenu } from './header-right-items/ShareMenu';
import SyncUser from './header-right-items/SyncUser';
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
import UserAvatar from './header-right-items/UserAvatar';
import {
StyledBrowserWarning,
StyledCloseButton,
@@ -24,6 +33,12 @@ import {
} from './styles';
import { OSWarningMessage, shouldShowWarning } from './utils';
const SidebarSwitch = lazy(() =>
import('../../affine/sidebar-switch').then(module => ({
default: module.SidebarSwitch,
}))
);
const BrowserWarning = ({
show,
onClose,
@@ -56,10 +71,12 @@ export const enum HeaderRightItemName {
ThemeModeSwitch = 'themeModeSwitch',
SyncUser = 'syncUser',
ShareMenu = 'shareMenu',
EditPage = 'editPage',
UserAvatar = 'userAvatar',
}
type HeaderItem = {
Component: React.FC<BaseHeaderProps>;
Component: FC<BaseHeaderProps>;
// todo: public workspace should be one of the flavour
availableWhen: (
workspace: AffineOfficialWorkspace,
@@ -70,7 +87,6 @@ type HeaderItem = {
}
) => boolean;
};
const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
[HeaderRightItemName.TrashButtonGroup]: {
Component: TrashButtonGroup,
@@ -90,18 +106,30 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
return currentPage?.meta.trash !== true;
},
},
[HeaderRightItemName.ShareMenu]: {
Component: HeaderShareMenu,
availableWhen: (workspace, currentPage, { isPublic, isPreview }) => {
return workspace.flavour !== WorkspaceFlavour.PUBLIC && !!currentPage;
},
},
[HeaderRightItemName.EditPage]: {
Component: EditPage,
availableWhen: (workspace, currentPage, { isPublic, isPreview }) => {
return isPublic;
},
},
[HeaderRightItemName.UserAvatar]: {
Component: UserAvatar,
availableWhen: (workspace, currentPage, { isPublic, isPreview }) => {
return isPublic;
},
},
[HeaderRightItemName.EditorOptionMenu]: {
Component: EditorOptionMenu,
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
return !!currentPage && !isPublic && !isPreview;
},
},
[HeaderRightItemName.ShareMenu]: {
Component: () => null,
availableWhen: (_, currentPage, { isPublic, isPreview }) => {
return false;
},
},
};
export type HeaderProps = BaseHeaderProps;
@@ -136,11 +164,13 @@ export const Header = forwardRef<
data-testid="editor-header-items"
data-tauri-drag-region
>
<SidebarSwitch
visible={!open}
tooltipContent={t('Expand sidebar')}
testid="sliderBar-arrowButton-expand"
/>
<Suspense>
<SidebarSwitch
visible={!open}
tooltipContent={t('Expand sidebar')}
data-testid="sliderBar-arrowButton-expand"
/>
</Suspense>
{props.children}
<StyledHeaderRightSide>

View File

@@ -3,13 +3,13 @@ import { QuickSearchTips } from '@affine/component';
import { getEnvironment } from '@affine/env';
import { ArrowDownSmallIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue, useSetAtom } from 'jotai';
import type { HTMLAttributes, PropsWithChildren } from 'react';
import { forwardRef, useCallback, useRef } from 'react';
import { currentEditorAtom, openQuickSearchModalAtom } from '../../../atoms';
import { useGuideHidden } from '../../../hooks/use-is-first-load';
import { usePageMeta } from '../../../hooks/use-page-meta';
import { useElementResizeEffect } from '../../../hooks/use-workspaces';
import { QuickSearchButton } from '../../pure/quick-search-button';
import { EditorModeSwitch } from './editor-mode-switch';
@@ -34,7 +34,7 @@ export const WorkspaceHeader = forwardRef<
const { workspace, currentPage, children, isPublic } = props;
// fixme(himself65): remove this atom and move it to props
const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom);
const pageMeta = usePageMeta(workspace.blockSuiteWorkspace).find(
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
meta => meta.id === currentPage?.id
);
assertExists(pageMeta);

View File

@@ -1,16 +1,17 @@
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-blocksuite-workspace-page-title';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-block-suite-workspace-page-title';
import { useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head';
import type React from 'react';
import { lazy, useCallback } from 'react';
import { startTransition, useCallback } from 'react';
import { currentEditorAtom, workspacePreferredModeAtom } from '../atoms';
import { usePageMeta } from '../hooks/use-page-meta';
import type { AffineOfficialWorkspace } from '../shared';
import { PageNotFoundError } from './affine/affine-error-eoundary';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import { WorkspaceHeader } from './blocksuite/workspace-header';
export type PageDetailEditorProps = {
@@ -23,12 +24,6 @@ export type PageDetailEditorProps = {
header?: React.ReactNode;
};
const Editor = lazy(() =>
import('./blocksuite/block-suite-editor').then(module => ({
default: module.BlockSuiteEditor,
}))
);
export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
workspace,
pageId,
@@ -44,7 +39,7 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
throw new PageNotFoundError(blockSuiteWorkspace, pageId);
}
const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, pageId);
const meta = usePageMeta(blockSuiteWorkspace).find(
const meta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
const currentMode =
@@ -68,19 +63,23 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
style={{
height: 'calc(100% - 52px)',
}}
key={pageId}
key={`${workspace.flavour}-${workspace.id}-${[pageId]}`}
mode={isPublic ? 'page' : currentMode}
page={page}
onInit={useCallback(
(page: Page, editor: Readonly<EditorContainer>) => {
setEditor(editor);
startTransition(() => {
setEditor(editor);
});
onInit(page, editor);
},
[onInit, setEditor]
)}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
setEditor(editor);
startTransition(() => {
setEditor(editor);
});
onLoad?.(page, editor);
},
[onLoad, setEditor]

View File

@@ -1,11 +1,13 @@
import { FlexWrapper } from '@affine/component';
import { IconButton } from '@affine/component';
import { Tooltip } from '@affine/component';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons';
import type { CSSProperties } from 'react';
import type React from 'react';
import { forwardRef } from 'react';
import { stringToColour } from '../../../utils';
import { StyledFooter, StyledSignInButton, StyleUserInfo } from './styles';
@@ -19,6 +21,10 @@ export type FooterProps = {
export const Footer: React.FC<FooterProps> = ({ user, onLogin, onLogout }) => {
const { t } = useTranslation();
if (!config.enableLegacyCloud) {
return null;
}
return (
<StyledFooter data-testid="workspace-list-modal-footer">
{user && (
@@ -74,54 +80,58 @@ interface WorkspaceAvatarProps {
style?: CSSProperties;
}
export const WorkspaceAvatar: React.FC<WorkspaceAvatarProps> = props => {
const size = props.size || 20;
const sizeStr = size + 'px';
export const WorkspaceAvatar = forwardRef<HTMLDivElement, WorkspaceAvatarProps>(
function WorkspaceAvatar(props, ref) {
const size = props.size || 20;
const sizeStr = size + 'px';
return (
<>
{props.avatar ? (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
}}
>
<picture>
<img
style={{ width: sizeStr, height: sizeStr }}
src={props.avatar}
alt=""
referrerPolicy="no-referrer"
/>
</picture>
</div>
) : (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
border: '1px solid #fff',
color: '#fff',
fontSize: Math.ceil(0.5 * size) + 'px',
background: stringToColour(props.name || 'AFFiNE'),
borderRadius: '50%',
textAlign: 'center',
lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
}}
>
{(props.name || 'AFFiNE').substring(0, 1)}
</div>
)}
</>
);
};
return (
<>
{props.avatar ? (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
color: '#fff',
borderRadius: '50%',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
<picture>
<img
style={{ width: sizeStr, height: sizeStr }}
src={props.avatar}
alt=""
referrerPolicy="no-referrer"
/>
</picture>
</div>
) : (
<div
style={{
...props.style,
width: sizeStr,
height: sizeStr,
border: '1px solid #fff',
color: '#fff',
fontSize: Math.ceil(0.5 * size) + 'px',
background: stringToColour(props.name || 'AFFiNE'),
borderRadius: '50%',
textAlign: 'center',
lineHeight: size + 'px',
display: 'inline-block',
verticalAlign: 'middle',
}}
ref={ref}
>
{(props.name || 'AFFiNE').substring(0, 1)}
</div>
)}
</>
);
}
);

View File

@@ -1,8 +1,7 @@
import { MuiFade, Tooltip } from '@affine/component';
import { config } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { CloseIcon, NewIcon } from '@blocksuite/icons';
import { lazy, useState } from 'react';
import { lazy, Suspense, useState } from 'react';
import { ShortcutsModal } from '../shortcuts-modal';
import { ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
@@ -21,9 +20,7 @@ const ContactModal = lazy(() =>
export type IslandItemNames = 'whatNew' | 'contact' | 'shortcuts';
export const HelpIsland = ({
showList = config.enableChangeLog
? ['whatNew', 'contact', 'shortcuts']
: ['contact', 'shortcuts'],
showList = ['whatNew', 'contact', 'shortcuts'],
}: {
showList?: IslandItemNames[];
}) => {
@@ -61,11 +58,14 @@ export const HelpIsland = ({
style={{ height: spread ? `${showList.length * 44}px` : 0 }}
>
{showList.includes('whatNew') && (
<Tooltip content={t("Discover what's new")} placement="left-end">
<Tooltip content={t("Discover what's new!")} placement="left-end">
<StyledIconWrapper
data-testid="right-bottom-change-log-icon"
onClick={() => {
window.open('https://affine.pro', '_blank');
window.open(
'https://github.com/toeverything/AFFiNE/releases',
'_blank'
);
}}
>
<NewIcon />
@@ -113,11 +113,13 @@ export const HelpIsland = ({
</StyledTriggerWrapper>
</MuiFade>
</StyledIsland>
<ContactModal
open={open}
onClose={() => setOpen(false)}
logoSrc="/imgs/affine-text-logo.png"
/>
<Suspense>
<ContactModal
open={open}
onClose={() => setOpen(false)}
logoSrc="/imgs/affine-text-logo.png"
/>
</Suspense>
<ShortcutsModal
open={openShortCut}
onClose={() => setOpenShortCut(false)}

View File

@@ -1,12 +1,14 @@
import { initPage } from '@affine/env/blocksuite';
import { useTranslation } from '@affine/i18n';
import type { PageBlockModel } from '@blocksuite/blocks';
import { PlusIcon } from '@blocksuite/icons';
import { assertEquals, nanoid } from '@blocksuite/store';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { Command } from 'cmdk';
import type { NextRouter } from 'next/router';
import type React from 'react';
import { useCallback } from 'react';
import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
import { StyledModalFooterContent } from './style';
@@ -35,25 +37,25 @@ export const Footer: React.FC<FooterProps> = ({
return (
<Command.Item
data-testid="quick-search-add-new-page"
onSelect={async () => {
onClose();
onSelect={useCallback(() => {
const id = nanoid();
const page = await createPage(id);
const page = createPage(id);
assertEquals(page.id, id);
await jumpToPage(blockSuiteWorkspace.id, page.id);
if (!query) {
return;
initPage(page);
const block = page.getBlockByFlavour(
'affine:page'
)[0] as PageBlockModel;
if (block) {
block.title.insert(query, 0);
} else {
console.warn('No page block found');
}
const newPage = blockSuiteWorkspace.getPage(page.id);
if (newPage) {
const block = newPage.getBlockByFlavour(
'affine:page'
)[0] as PageBlockModel;
if (block) {
block.title.insert(query, 0);
}
}
}}
blockSuiteWorkspace.setPageMeta(page.id, {
title: query,
});
onClose();
void jumpToPage(blockSuiteWorkspace.id, page.id);
}, [blockSuiteWorkspace, createPage, jumpToPage, onClose, query])}
>
<StyledModalFooterContent>
<PlusIcon />

View File

@@ -1,12 +1,12 @@
import { useTranslation } from '@affine/i18n';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { Command } from 'cmdk';
import Image from 'next/legacy/image';
import { useRouter } from 'next/router';
import type { FC } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { usePageMeta } from '../../../hooks/use-page-meta';
import type { BlockSuiteWorkspace } from '../../../shared';
import { StyledListItem, StyledNotFound } from './style';
@@ -26,7 +26,7 @@ export const PublishedResults: FC<PublishedResultsProps> = ({
}) => {
const [results, setResults] = useState(new Map<string, string | undefined>());
const router = useRouter();
const pageList = usePageMeta(blockSuiteWorkspace);
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
// useEffect(() => {
// dataCenter
// .loadPublicWorkspace(router.query.workspaceId as string)

View File

@@ -2,14 +2,14 @@ import { UNTITLED_WORKSPACE_NAME } from '@affine/env';
import { useTranslation } from '@affine/i18n';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import { assertExists } 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 Image from 'next/legacy/image';
import type { NextRouter } from 'next/router';
import type { Dispatch, FC, SetStateAction } from 'react';
import { useEffect } from 'react';
import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper';
import { usePageMeta } from '../../../hooks/use-page-meta';
import { useRecentlyViewed } from '../../../hooks/use-recent-views';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
@@ -31,7 +31,7 @@ export const Results: FC<ResultsProps> = ({
onClose,
}) => {
useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const pageList = usePageMeta(blockSuiteWorkspace);
const pageList = useBlockSuitePageMeta(blockSuiteWorkspace);
assertExists(blockSuiteWorkspace.id);
const List = useSwitchToConfig(blockSuiteWorkspace.id);

View File

@@ -15,6 +15,7 @@ import {
import type { BlockSuiteWorkspace } from '../../../shared';
import { Footer } from './Footer';
import { NavigationPath } from './navigation-path';
import { PublishedResults } from './PublishedResults';
import { Results } from './Results';
import { SearchInput } from './SearchInput';
@@ -106,8 +107,15 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
maxHeight: '80vh',
minHeight: isPublicAndNoQuery() ? '72px' : '412px',
top: '80px',
overflow: 'hidden',
}}
>
<NavigationPath
blockSuiteWorkspace={blockSuiteWorkspace}
onJumpToPage={() => {
setOpen(false);
}}
/>
<Command
shouldFilter={false}
//Handle KeyboardEvent conflicts with blocksuite

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