mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
Compare commits
125 Commits
v0.5.4-bet
...
v0.6.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db3f63e8f2 | ||
|
|
e562ca1011 | ||
|
|
f6adf93f90 | ||
|
|
053eba5d98 | ||
|
|
49f1ba676f | ||
|
|
48c109e149 | ||
|
|
259d7988d9 | ||
|
|
0a49258ddd | ||
|
|
fd35d3427e | ||
|
|
ef0a20b358 | ||
|
|
f01997f8ee | ||
|
|
281a068cfb | ||
|
|
fe5be0cb47 | ||
|
|
8aab1d6459 | ||
|
|
2eaaeef4a7 | ||
|
|
5fbfabb3b2 | ||
|
|
ec64260b6a | ||
|
|
2e23a4830b | ||
|
|
41a3d6f62f | ||
|
|
752bc9ca0e | ||
|
|
c08f6fdba4 | ||
|
|
b23b7e896b | ||
|
|
d68b421a4b | ||
|
|
1f510799e2 | ||
|
|
66ea97c7c9 | ||
|
|
ee300e7b60 | ||
|
|
ef2d135e9b | ||
|
|
c82fb89d57 | ||
|
|
725bf63a32 | ||
|
|
c1ca578f7d | ||
|
|
530dd5ff7f | ||
|
|
11370bc07e | ||
|
|
1c53daf1c4 | ||
|
|
b2556db33b | ||
|
|
89310c9b97 | ||
|
|
8e09af910f | ||
|
|
885aea3425 | ||
|
|
a616150f2d | ||
|
|
d80dae8a89 | ||
|
|
34ff08b92b | ||
|
|
2f7b51d7ff | ||
|
|
b7cee3185e | ||
|
|
1c5455e6ed | ||
|
|
2d303fd5d3 | ||
|
|
fbe2543c03 | ||
|
|
d6b640726e | ||
|
|
f875b37641 | ||
|
|
53c4fc6dfa | ||
|
|
6c310249d9 | ||
|
|
02e1f528bf | ||
|
|
c870104370 | ||
|
|
627d8ef787 | ||
|
|
5563823a7a | ||
|
|
d6804bb0fd | ||
|
|
1350633690 | ||
|
|
50196d8fde | ||
|
|
2e0ccb53ec | ||
|
|
1498ee405b | ||
|
|
cb863c4afa | ||
|
|
2629d39501 | ||
|
|
38305cd984 | ||
|
|
93116c24f2 | ||
|
|
017b9c8615 | ||
|
|
9ce3a96862 | ||
|
|
a0ff520ba4 | ||
|
|
a8b8986d89 | ||
|
|
8ffc096fee | ||
|
|
7e457f7b4c | ||
|
|
aedf2d339e | ||
|
|
ffd5ae52b3 | ||
|
|
3093194da8 | ||
|
|
68b4f792f0 | ||
|
|
e2c6e4f9fc | ||
|
|
9ff7dbffb7 | ||
|
|
0c561da061 | ||
|
|
06951319a6 | ||
|
|
0bfcab4067 | ||
|
|
2c4db4fa16 | ||
|
|
23b4f9ee12 | ||
|
|
e5330b1917 | ||
|
|
183611a556 | ||
|
|
7786456ba4 | ||
|
|
f4bf7e3ddf | ||
|
|
05d88215d1 | ||
|
|
b240a70e51 | ||
|
|
00fd468e9b | ||
|
|
b5a7f8b7eb | ||
|
|
f03277fd17 | ||
|
|
ee93071149 | ||
|
|
21fdced2bd | ||
|
|
10b4558947 | ||
|
|
0fbed5d9d6 | ||
|
|
8d117123d7 | ||
|
|
063ffda09d | ||
|
|
39c83bd25b | ||
|
|
4444c3d1a6 | ||
|
|
717dd93f37 | ||
|
|
c58673c55f | ||
|
|
768e55072d | ||
|
|
8c84daec2b | ||
|
|
e54a5b6128 | ||
|
|
ee1e50f391 | ||
|
|
268636c440 | ||
|
|
06fa0cdb60 | ||
|
|
73dbb39009 | ||
|
|
47848cb5da | ||
|
|
eff6a03a51 | ||
|
|
08f6a41ef4 | ||
|
|
6d1345ffe5 | ||
|
|
689f615b11 | ||
|
|
f82ea5d9c4 | ||
|
|
dc4979a80c | ||
|
|
1f48bc4301 | ||
|
|
beabd1e050 | ||
|
|
19e20a6a20 | ||
|
|
e4f13ddae4 | ||
|
|
752d0545ca | ||
|
|
08341d3d6c | ||
|
|
ef665df330 | ||
|
|
b38017cd23 | ||
|
|
0c550a2827 | ||
|
|
87ffdad862 | ||
|
|
c6e8024e16 | ||
|
|
4200b3c3e5 | ||
|
|
10976a9257 |
1
.github/CLA.md
vendored
1
.github/CLA.md
vendored
@@ -58,3 +58,4 @@ Example:
|
||||
- Howard Do, @howarddo2208, 2023/04/20
|
||||
- 三咲智子 Kevin Deng, @sxzz, 2023/04/21
|
||||
- Moeyua, @moeyua, 2023/04/22
|
||||
- Shishu, @shishudesu, 2023/05/19
|
||||
|
||||
24
.github/workflows/add-to-project.yml
vendored
24
.github/workflows/add-to-project.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: Add to GitHub projects
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issues and pull requests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@v0.4.0
|
||||
with:
|
||||
# You can target a repository in a different organization
|
||||
# to the issue
|
||||
project-url: https://github.com/orgs/toeverything/projects/10
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||
# labeled: bug, needs-triage
|
||||
# label-operator: OR
|
||||
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
@@ -35,7 +35,9 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- run: yarn lint --max-warnings=0
|
||||
- run: |
|
||||
yarn lint --max-warnings=0
|
||||
yarn circular
|
||||
|
||||
build-storybook:
|
||||
name: Build Storybook
|
||||
@@ -365,11 +367,6 @@ jobs:
|
||||
name: affine
|
||||
fail_ci_if_error: true
|
||||
|
||||
- name: Run desktop tests
|
||||
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
|
||||
run: yarn test
|
||||
working-directory: apps/electron
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -28,9 +28,9 @@ node_modules
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/settings.template.json
|
||||
!.vscode/launch.template.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
"name": "Run Dev",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
},
|
||||
{
|
||||
"command": "yarn run dev:local",
|
||||
"name": "Run Dev Locally",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
Cargo.lock
generated
21
Cargo.lock
generated
@@ -305,9 +305,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "5.1.0"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9"
|
||||
checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossbeam-channel",
|
||||
@@ -319,7 +319,7 @@ dependencies = [
|
||||
"mio",
|
||||
"serde",
|
||||
"walkdir",
|
||||
"windows-sys 0.42.0",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -652,21 +652,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.42.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
|
||||
@@ -30,6 +30,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
[](https://affine.pro/download)
|
||||
[](https://affine.pro/download)
|
||||
|
||||
[](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)
|
||||
@@ -69,7 +70,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
|
||||
Before we tell you how to get started with AFFiNE, we'd like to shamelessly plug our awesome user and developer communities across [official social platforms](https://community.affine.pro/c/start-here/)! Once you’re familiar with using the software, maybe you will share your wisdom with others and even consider joining the [AFFiNE Ambassador program](https://community.affine.pro/c/start-here/affine-ambassador) to help spread AFFiNE to the world.
|
||||
|
||||
## Getting started & Stay tunned with us.
|
||||
## Getting started & staying tuned with us.
|
||||
|
||||
⚠️ Please note that AFFiNE is still under active development and is not yet ready for production use. ⚠️
|
||||
|
||||
@@ -140,7 +141,7 @@ Thanks a lot to the community for providing such powerful and simple libraries,
|
||||
We would like to express our gratitude to all the individuals who have already contributed to AFFiNE! If you have any AFFiNE-related project, documentation, tool or template, please feel free to contribute it by submitting a pull request to our curated list on GitHub: [awesome-affine](https://github.com/toeverything/awesome-affine).
|
||||
|
||||
<a href="https://github.com/toeverything/affine/graphs/contributors">
|
||||
<img src="https://user-images.githubusercontent.com/5910926/237263745-36bb975d-83d6-4a7c-a152-d9ad020e5023.png" />
|
||||
<img src="https://user-images.githubusercontent.com/5910926/240508358-93eddded-48a0-40cd-85e4-a1d172dbe1d9.svg" />
|
||||
</a>
|
||||
|
||||
## Self-Host
|
||||
|
||||
@@ -94,7 +94,7 @@ module.exports = {
|
||||
config: {
|
||||
name: 'AFFiNE',
|
||||
setupIcon: icoPath,
|
||||
// loadingGif: './resources/icons/loading.gif',
|
||||
loadingGif: './resources/icons/affine_installing.gif',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -10,6 +10,7 @@ import { logger } from './logger';
|
||||
import { restoreOrCreateWindow } from './main-window';
|
||||
import { registerProtocol } from './protocol';
|
||||
|
||||
if (require('electron-squirrel-startup')) app.quit();
|
||||
// allow tests to overwrite app name through passing args
|
||||
if (process.argv.includes('--app-name')) {
|
||||
const appNameIndex = process.argv.indexOf('--app-name');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.5.4-beta.1",
|
||||
"version": "0.6.0-canary.7",
|
||||
"author": "affine",
|
||||
"repository": {
|
||||
"url": "https://github.com/toeverything/AFFiNE",
|
||||
|
||||
BIN
apps/electron/resources/icons/affine_installing.gif
Normal file
BIN
apps/electron/resources/icons/affine_installing.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 480 KiB |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.5.4-beta.1",
|
||||
"version": "0.6.0-canary.7",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -37,7 +37,7 @@
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/node": "^18.16.12",
|
||||
"@types/node": "^18.16.13",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"c8": "^7.13.0",
|
||||
"nodemon": "^2.0.22",
|
||||
|
||||
@@ -10,7 +10,7 @@ First, run the development server:
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:8080](http://localhost:3000) with your browser to see the result.
|
||||
Open [http://localhost:8080](http://localhost:8080) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `src/pages/workspace/[workspaceId]/all.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
{
|
||||
"name": "@affine/web",
|
||||
"private": true,
|
||||
"version": "0.5.4-beta.1",
|
||||
"version": "0.6.0-canary.7",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"export": "next export",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
@@ -19,12 +18,12 @@
|
||||
"@affine/jotai": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/blocks": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/icons": "^2.1.16",
|
||||
"@blocksuite/lit": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@blocksuite/icons": "^2.1.18",
|
||||
"@blocksuite/lit": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { AFFINE_STORAGE_KEY, config, prefixUrl } from '@affine/env';
|
||||
/**
|
||||
* This file has deprecated because we do not maintain legacy affine cloud,
|
||||
* please use new affine cloud instead.
|
||||
*/
|
||||
import { AFFINE_STORAGE_KEY, config } from '@affine/env';
|
||||
import { initPage } from '@affine/env/blocksuite';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import {
|
||||
clearLoginStorage,
|
||||
createAffineAuth,
|
||||
getLoginStorage,
|
||||
isExpired,
|
||||
parseIdToken,
|
||||
@@ -37,9 +40,9 @@ import { PageDetailEditor } from '../../components/page-detail-editor';
|
||||
import { PageLoading } from '../../components/pure/loading';
|
||||
import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh-auth-token';
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
import { affineApis, affineAuth } from '../../shared/apis';
|
||||
import { toast } from '../../utils';
|
||||
import type { WorkspaceAdapter } from '..';
|
||||
import type { WorkspaceAdapter } from '../type';
|
||||
import { QueryKey } from './fetcher';
|
||||
|
||||
const storage = createJSONStorage(() => localStorage);
|
||||
@@ -79,8 +82,6 @@ const getPersistenceAllWorkspace = () => {
|
||||
return allWorkspaces;
|
||||
};
|
||||
|
||||
export const affineAuth = createAffineAuth(prefixUrl);
|
||||
|
||||
function AuthContext({ children }: PropsWithChildren): ReactElement {
|
||||
const login = useAffineRefreshAuthToken();
|
||||
|
||||
@@ -17,12 +17,31 @@ import {
|
||||
} from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import React from 'react';
|
||||
import { lazy } from 'react';
|
||||
|
||||
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
|
||||
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
|
||||
import { PageDetailEditor } from '../../components/page-detail-editor';
|
||||
import type { WorkspaceAdapter } from '..';
|
||||
import type { WorkspaceAdapter } from '../type';
|
||||
|
||||
const WorkspaceSettingDetail = lazy(() =>
|
||||
import('../../components/affine/workspace-setting-detail').then(
|
||||
({ WorkspaceSettingDetail }) => ({
|
||||
default: WorkspaceSettingDetail,
|
||||
})
|
||||
)
|
||||
);
|
||||
const BlockSuitePageList = lazy(() =>
|
||||
import('../../components/blocksuite/block-suite-page-list').then(
|
||||
({ BlockSuitePageList }) => ({
|
||||
default: BlockSuitePageList,
|
||||
})
|
||||
)
|
||||
);
|
||||
const PageDetailEditor = lazy(() =>
|
||||
import('../../components/page-detail-editor').then(
|
||||
({ PageDetailEditor }) => ({
|
||||
default: PageDetailEditor,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const logger = new DebugLogger('use-create-first-workspace');
|
||||
|
||||
@@ -62,7 +81,7 @@ export const LocalPlugin: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
Provider: ({ children }) => {
|
||||
return <>{children}</>;
|
||||
},
|
||||
PageDetail: ({ currentWorkspace, currentPageId }) => {
|
||||
PageDetail: ({ currentWorkspace, currentPageId, onLoadEditor }) => {
|
||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(
|
||||
@@ -75,6 +94,7 @@ export const LocalPlugin: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
onInit={initPage}
|
||||
onLoad={onLoadEditor}
|
||||
workspace={currentWorkspace}
|
||||
/>
|
||||
</>
|
||||
21
apps/web/src/adapters/type.tsx
Normal file
21
apps/web/src/adapters/type.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import type {
|
||||
AppEvents,
|
||||
WorkspaceCRUD,
|
||||
WorkspaceUISchema,
|
||||
} from '@affine/workspace/type';
|
||||
import type {
|
||||
LoadPriority,
|
||||
ReleaseType,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/workspace/type';
|
||||
|
||||
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
|
||||
releaseType: ReleaseType;
|
||||
flavour: Flavour;
|
||||
// Plugin will be loaded according to the priority
|
||||
loadPriority: LoadPriority;
|
||||
Events: Partial<AppEvents>;
|
||||
// Fetch necessary data for the first render
|
||||
CRUD: WorkspaceCRUD<Flavour>;
|
||||
UI: WorkspaceUISchema<Flavour>;
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
AppEvents,
|
||||
WorkspaceCRUD,
|
||||
WorkspaceUISchema,
|
||||
} from '@affine/workspace/type';
|
||||
import type { AppEvents } from '@affine/workspace/type';
|
||||
import {
|
||||
LoadPriority,
|
||||
ReleaseType,
|
||||
@@ -11,17 +7,7 @@ import {
|
||||
|
||||
import { AffinePlugin } from './affine';
|
||||
import { LocalPlugin } from './local';
|
||||
|
||||
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
|
||||
releaseType: ReleaseType;
|
||||
flavour: Flavour;
|
||||
// Plugin will be loaded according to the priority
|
||||
loadPriority: LoadPriority;
|
||||
Events: Partial<AppEvents>;
|
||||
// Fetch necessary data for the first render
|
||||
CRUD: WorkspaceCRUD<Flavour>;
|
||||
UI: WorkspaceUISchema<Flavour>;
|
||||
}
|
||||
import type { WorkspaceAdapter } from './type';
|
||||
|
||||
const unimplemented = () => {
|
||||
throw new Error('Not implemented');
|
||||
@@ -19,7 +19,7 @@ import type { Page } from '@blocksuite/store';
|
||||
import { createStore } from 'jotai';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { WorkspaceAdapters } from '../../plugins';
|
||||
import { WorkspaceAdapters } from '../../adapters/workspace';
|
||||
import { rootCurrentWorkspaceAtom } from '../root';
|
||||
|
||||
describe('currentWorkspace atom', () => {
|
||||
|
||||
@@ -11,8 +11,8 @@ import type { Page } from '@blocksuite/store';
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
||||
import { WorkspaceAdapters } from '../plugins';
|
||||
|
||||
const logger = new DebugLogger('web:atoms');
|
||||
|
||||
@@ -96,7 +96,13 @@ export const openDisableCloudAlertModalAtom = atom(false);
|
||||
|
||||
export { workspacesAtom } from './root';
|
||||
|
||||
type View = { id: string; mode: 'page' | 'edgeless' };
|
||||
type View = {
|
||||
id: string;
|
||||
/**
|
||||
* @deprecated Use `mode` from `useWorkspacePreferredMode` instead.
|
||||
*/
|
||||
mode: 'page' | 'edgeless';
|
||||
};
|
||||
|
||||
export type WorkspaceRecentViews = Record<string, View[]>;
|
||||
|
||||
@@ -106,6 +112,9 @@ export const workspaceRecentViewsAtom = atomWithStorage<WorkspaceRecentViews>(
|
||||
);
|
||||
|
||||
export type PreferredModeRecord = Record<Page['id'], 'page' | 'edgeless'>;
|
||||
/**
|
||||
* @deprecated Use `useWorkspacePreferredMode` instead.
|
||||
*/
|
||||
export const workspacePreferredModeAtom = atomWithStorage<PreferredModeRecord>(
|
||||
'preferredMode',
|
||||
{}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
import { WorkspaceAdapters } from '../plugins';
|
||||
import type { AllWorkspace } from '../shared';
|
||||
|
||||
const logger = new DebugLogger('web:atoms:root');
|
||||
@@ -22,6 +21,7 @@ const logger = new DebugLogger('web:atoms:root');
|
||||
* Fetch all workspaces from the Plugin CRUD
|
||||
*/
|
||||
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
|
||||
const { WorkspaceAdapters } = await import('../adapters/workspace');
|
||||
const flavours: string[] = Object.values(WorkspaceAdapters).map(
|
||||
plugin => plugin.flavour
|
||||
);
|
||||
@@ -82,6 +82,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
|
||||
*/
|
||||
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
|
||||
async get => {
|
||||
const { WorkspaceAdapters } = await import('../adapters/workspace');
|
||||
const metadata = get(rootWorkspacesMetadataAtom);
|
||||
const targetId = get(rootCurrentWorkspaceIdAtom);
|
||||
if (targetId === null) {
|
||||
|
||||
@@ -36,6 +36,7 @@ const Editor: React.FC<{
|
||||
globalThis.page = page;
|
||||
// @ts-ignore
|
||||
globalThis.editor = editor;
|
||||
return () => void 0;
|
||||
}, []);
|
||||
|
||||
if (!page) {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './pinboard-menu';
|
||||
export * from './pinboard-render/';
|
||||
@@ -1,133 +0,0 @@
|
||||
import type { PureMenuProps } from '@affine/component';
|
||||
import { Input, PureMenu, TreeView } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { RemoveIcon, SearchIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useReferenceLinkHelper } from '../../../../hooks/affine/use-reference-link-helper';
|
||||
import { usePinboardData } from '../../../../hooks/use-pinboard-data';
|
||||
import { usePinboardHandler } from '../../../../hooks/use-pinboard-handler';
|
||||
import type { BlockSuiteWorkspace } from '../../../../shared';
|
||||
import { toast } from '../../../../utils';
|
||||
import { PinboardRender } from '../pinboard-render/';
|
||||
import {
|
||||
StyledMenuContent,
|
||||
StyledMenuFooter,
|
||||
StyledMenuSubTitle,
|
||||
StyledPinboard,
|
||||
StyledSearchContainer,
|
||||
} from '../styles';
|
||||
import { SearchContent } from './search-content';
|
||||
|
||||
export interface PinboardMenuProps extends PureMenuProps {
|
||||
metas: PageMeta[];
|
||||
currentMeta: PageMeta;
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
showRemovePinboard?: boolean;
|
||||
onPinboardClick?: (p: { dragId: string; dropId: string }) => void;
|
||||
}
|
||||
|
||||
export const PinboardMenu = ({
|
||||
metas: propsMetas,
|
||||
currentMeta,
|
||||
blockSuiteWorkspace,
|
||||
showRemovePinboard = false,
|
||||
onPinboardClick,
|
||||
...pureMenuProps
|
||||
}: PinboardMenuProps) => {
|
||||
const metas = useMemo(
|
||||
() => propsMetas.filter(m => m.id !== currentMeta.id),
|
||||
[currentMeta.id, propsMetas]
|
||||
);
|
||||
const t = useAFFiNEI18N();
|
||||
const [query, setQuery] = useState('');
|
||||
const isSearching = query.length > 0;
|
||||
|
||||
const searchResult = metas.filter(
|
||||
meta => !meta.trash && meta.title.includes(query)
|
||||
);
|
||||
const { removeReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
|
||||
|
||||
const { dropPin } = usePinboardHandler({
|
||||
blockSuiteWorkspace,
|
||||
metas,
|
||||
});
|
||||
|
||||
const handleClick = useCallback(
|
||||
(dropId: string) => {
|
||||
const targetTitle = metas.find(m => m.id === dropId)?.title;
|
||||
|
||||
dropPin(currentMeta.id, dropId, {
|
||||
bottomLine: false,
|
||||
topLine: false,
|
||||
internal: true,
|
||||
});
|
||||
onPinboardClick?.({ dragId: currentMeta.id, dropId });
|
||||
toast(`Moved "${currentMeta.title}" to "${targetTitle}"`);
|
||||
},
|
||||
[currentMeta.id, currentMeta.title, dropPin, metas, onPinboardClick]
|
||||
);
|
||||
|
||||
const { data } = usePinboardData({
|
||||
metas,
|
||||
pinboardRender: PinboardRender,
|
||||
blockSuiteWorkspace,
|
||||
onClick: (e, node) => {
|
||||
handleClick(node.id);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<PureMenu
|
||||
width={320}
|
||||
height={480}
|
||||
{...pureMenuProps}
|
||||
data-testid="pinboard-menu"
|
||||
>
|
||||
<StyledSearchContainer>
|
||||
<label>
|
||||
<SearchIcon />
|
||||
</label>
|
||||
<Input
|
||||
value={query}
|
||||
onChange={setQuery}
|
||||
placeholder={t['Move page to']()}
|
||||
height={32}
|
||||
noBorder={true}
|
||||
onClick={e => e.stopPropagation()}
|
||||
data-testid="pinboard-menu-search"
|
||||
/>
|
||||
</StyledSearchContainer>
|
||||
|
||||
<StyledMenuContent>
|
||||
{isSearching && (
|
||||
<SearchContent results={searchResult} onClick={handleClick} />
|
||||
)}
|
||||
{!isSearching && (
|
||||
<>
|
||||
<StyledMenuSubTitle>Suggested</StyledMenuSubTitle>
|
||||
<TreeView data={data} indent={16} enableDnd={false} />
|
||||
</>
|
||||
)}
|
||||
</StyledMenuContent>
|
||||
|
||||
{showRemovePinboard && (
|
||||
<StyledMenuFooter>
|
||||
<StyledPinboard
|
||||
data-testid={'remove-from-pinboard-button'}
|
||||
onClick={() => {
|
||||
removeReferenceLink(currentMeta.id);
|
||||
}}
|
||||
>
|
||||
<RemoveIcon />
|
||||
{t['Remove from Pivots']()}
|
||||
</StyledPinboard>
|
||||
<p>{t['RFP']()}</p>
|
||||
</StyledMenuFooter>
|
||||
)}
|
||||
</PureMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinboardMenu;
|
||||
@@ -1,63 +0,0 @@
|
||||
import { FlexWrapper } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import Image from 'next/legacy/image';
|
||||
import React from 'react';
|
||||
|
||||
import { workspacePreferredModeAtom } from '../../../../atoms';
|
||||
import { StyledMenuSubTitle, StyledPinboard } from '../styles';
|
||||
|
||||
export const SearchContent = ({
|
||||
results,
|
||||
onClick,
|
||||
}: {
|
||||
results: PageMeta[];
|
||||
onClick?: (dropId: string) => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const record = useAtomValue(workspacePreferredModeAtom);
|
||||
|
||||
if (results.length) {
|
||||
return (
|
||||
<>
|
||||
<StyledMenuSubTitle>
|
||||
{t['Find results']({ number: `${results.length}` })}
|
||||
</StyledMenuSubTitle>
|
||||
{results.map(meta => {
|
||||
return (
|
||||
<StyledPinboard
|
||||
key={meta.id}
|
||||
onClick={() => {
|
||||
onClick?.(meta.id);
|
||||
}}
|
||||
data-testid="pinboard-search-result"
|
||||
>
|
||||
{record[meta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
|
||||
{meta.title}
|
||||
</StyledPinboard>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledMenuSubTitle>{t['Find 0 result']()}</StyledMenuSubTitle>
|
||||
<FlexWrapper
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
style={{ marginTop: 20 }}
|
||||
>
|
||||
<Image
|
||||
src="/imgs/no-result.svg"
|
||||
alt="no result"
|
||||
width={150}
|
||||
height={150}
|
||||
/>
|
||||
</FlexWrapper>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
|
||||
import { StyledOperationButton } from '../styles';
|
||||
import type { OperationButtonProps } from './operation-button';
|
||||
|
||||
export const AddButton = ({
|
||||
onAdd,
|
||||
visible,
|
||||
}: Pick<OperationButtonProps, 'onAdd' | 'visible'>) => {
|
||||
return (
|
||||
<StyledOperationButton
|
||||
visible={visible}
|
||||
size="small"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onAdd();
|
||||
}}
|
||||
>
|
||||
<PlusIcon />
|
||||
</StyledOperationButton>
|
||||
);
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
|
||||
import { StyledPinboard } from '../styles';
|
||||
|
||||
export const EmptyItem = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<StyledPinboard disable={true} textWrap={true}>
|
||||
{t['Organize pages to build knowledge']()}
|
||||
</StyledPinboard>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyItem;
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Input } from '@affine/component';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
EdgelessIcon,
|
||||
LevelIcon,
|
||||
PageIcon,
|
||||
PinboardIcon,
|
||||
} 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 type { PinboardNode } from '../../../../hooks/use-pinboard-data';
|
||||
import { StyledCollapsedButton, StyledPinboard } from '../styles';
|
||||
import { AddButton } from './add-button';
|
||||
import EmptyItem from './empty-item';
|
||||
import { OperationButton } from './operation-button';
|
||||
|
||||
const getIcon = (type: 'root' | 'edgeless' | 'page') => {
|
||||
switch (type) {
|
||||
case 'root':
|
||||
return <PinboardIcon className="mode-icon" />;
|
||||
case 'edgeless':
|
||||
return <EdgelessIcon className="mode-icon" />;
|
||||
default:
|
||||
return <PageIcon className="mode-icon" />;
|
||||
}
|
||||
};
|
||||
|
||||
export const PinboardRender: PinboardNode['render'] = (
|
||||
node,
|
||||
{
|
||||
isOver,
|
||||
onAdd,
|
||||
onDelete,
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
isSelected,
|
||||
disableCollapse,
|
||||
},
|
||||
renderProps
|
||||
) => {
|
||||
const {
|
||||
onClick,
|
||||
showOperationButton = false,
|
||||
currentMeta,
|
||||
metas = [],
|
||||
blockSuiteWorkspace,
|
||||
asPath,
|
||||
} = renderProps!;
|
||||
const record = useAtomValue(workspacePreferredModeAtom);
|
||||
const { setPageTitle } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
const [showRename, setShowRename] = useState(false);
|
||||
|
||||
const active = router.query.pageId === node.id;
|
||||
const isRoot = !!currentMeta.isRootPinboard;
|
||||
return (
|
||||
<>
|
||||
<StyledPinboard
|
||||
data-testid={`pinboard-${node.id}`}
|
||||
onClick={e => {
|
||||
onClick?.(e, node);
|
||||
}}
|
||||
onMouseEnter={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
isOver={isOver || isSelected}
|
||||
active={active}
|
||||
disableCollapse={!!disableCollapse}
|
||||
>
|
||||
{!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 ? (
|
||||
<Input
|
||||
data-testid={`pinboard-input-${node.id}`}
|
||||
value={currentMeta.title || ''}
|
||||
placeholder="Untitled"
|
||||
onClick={e => e.stopPropagation()}
|
||||
height={32}
|
||||
onBlur={() => {
|
||||
setShowRename(false);
|
||||
}}
|
||||
onChange={value => {
|
||||
// FIXME: setPageTitle would make input blur, and can't input the Chinese character
|
||||
setPageTitle(node.id, value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span>{isRoot ? 'Pinboard' : currentMeta.title || 'Untitled'}</span>
|
||||
)}
|
||||
{showOperationButton && <AddButton onAdd={onAdd} visible={isHover} />}
|
||||
|
||||
{showOperationButton && (
|
||||
<OperationButton
|
||||
isRoot={isRoot}
|
||||
onAdd={onAdd}
|
||||
onDelete={onDelete}
|
||||
metas={metas}
|
||||
currentMeta={currentMeta!}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace!}
|
||||
visible={isHover}
|
||||
onMenuClose={() => setIsHover(false)}
|
||||
onRename={() => {
|
||||
setShowRename(true);
|
||||
setIsHover(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</StyledPinboard>
|
||||
|
||||
{useMemo(
|
||||
() =>
|
||||
isRoot &&
|
||||
!metas.find(m => (currentMeta.subpageIds ?? []).includes(m.id)),
|
||||
[currentMeta.subpageIds, isRoot, metas]
|
||||
) && <EmptyItem />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default PinboardRender;
|
||||
@@ -1,170 +0,0 @@
|
||||
import { MenuItem, MuiClickAwayListener, PureMenu } from '@affine/component';
|
||||
import { CopyLink, MoveToTrash } from '@affine/component/page-list';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
MoreVerticalIcon,
|
||||
MoveToIcon,
|
||||
PenIcon,
|
||||
PlusIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import type { BlockSuiteWorkspace } from '../../../../shared';
|
||||
import { toast } from '../../../../utils';
|
||||
import { PinboardMenu } from '../pinboard-menu/';
|
||||
import { StyledOperationButton } from '../styles';
|
||||
|
||||
export type OperationButtonProps = {
|
||||
isRoot: boolean;
|
||||
onAdd: () => void;
|
||||
onDelete: () => void;
|
||||
metas: PageMeta[];
|
||||
currentMeta: PageMeta;
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
visible: boolean;
|
||||
onRename?: () => void;
|
||||
onMenuClose?: () => void;
|
||||
};
|
||||
export const OperationButton = ({
|
||||
isRoot,
|
||||
onAdd,
|
||||
onDelete,
|
||||
metas,
|
||||
currentMeta,
|
||||
blockSuiteWorkspace,
|
||||
visible,
|
||||
onMenuClose,
|
||||
onRename,
|
||||
}: OperationButtonProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
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(() => parseInt(baseTheme.zIndexModal) + 1, []);
|
||||
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
|
||||
return (
|
||||
<MuiClickAwayListener
|
||||
onClickAway={() => {
|
||||
setOperationMenuOpen(false);
|
||||
setPinboardMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ display: 'flex' }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
timer.current = setTimeout(() => {
|
||||
setOperationMenuOpen(false);
|
||||
setPinboardMenuOpen(false);
|
||||
}, 150);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
clearTimeout(timer.current);
|
||||
}}
|
||||
>
|
||||
<StyledOperationButton
|
||||
data-testid="pinboard-operation-button"
|
||||
ref={ref => setAnchorEl(ref)}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setOperationMenuOpen(!operationMenuOpen);
|
||||
}}
|
||||
visible={visible}
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</StyledOperationButton>
|
||||
|
||||
<PureMenu
|
||||
data-testid="pinboard-operation-menu"
|
||||
width={256}
|
||||
anchorEl={anchorEl}
|
||||
open={operationMenuOpen}
|
||||
placement="bottom"
|
||||
zIndex={menuIndex}
|
||||
>
|
||||
<MenuItem
|
||||
data-testid="pinboard-operation-add"
|
||||
onClick={() => {
|
||||
onAdd();
|
||||
setOperationMenuOpen(false);
|
||||
onMenuClose?.();
|
||||
}}
|
||||
icon={<PlusIcon />}
|
||||
>
|
||||
{t['Add a subpage inside']()}
|
||||
</MenuItem>
|
||||
{!isRoot && (
|
||||
<MenuItem
|
||||
data-testid="pinboard-operation-move-to"
|
||||
onClick={() => {
|
||||
setOperationMenuOpen(false);
|
||||
setPinboardMenuOpen(true);
|
||||
}}
|
||||
icon={<MoveToIcon />}
|
||||
>
|
||||
{t['Move to']()}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isRoot && (
|
||||
<MenuItem
|
||||
data-testid="pinboard-operation-rename"
|
||||
onClick={() => {
|
||||
onRename?.();
|
||||
setOperationMenuOpen(false);
|
||||
onMenuClose?.();
|
||||
}}
|
||||
icon={<PenIcon />}
|
||||
>
|
||||
{t['Rename']()}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isRoot && (
|
||||
<MoveToTrash
|
||||
testId="pinboard-operation-move-to-trash"
|
||||
onItemClick={() => {
|
||||
setOperationMenuOpen(false);
|
||||
setConfirmModalOpen(true);
|
||||
onMenuClose?.();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CopyLink />
|
||||
</PureMenu>
|
||||
|
||||
<PinboardMenu
|
||||
anchorEl={anchorEl}
|
||||
open={pinboardMenuOpen}
|
||||
placement="bottom"
|
||||
zIndex={menuIndex}
|
||||
metas={metas}
|
||||
currentMeta={currentMeta}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
showRemovePinboard={true}
|
||||
/>
|
||||
<MoveToTrash.ConfirmModal
|
||||
open={confirmModalOpen}
|
||||
title={currentMeta.title}
|
||||
onConfirm={() => {
|
||||
toast(t['Moved to Trash']());
|
||||
removeToTrash(currentMeta.id);
|
||||
onDelete();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setConfirmModalOpen(false);
|
||||
}}
|
||||
confirmButtonTestId="move-to-trash-confirm"
|
||||
cancelButtonTestId="move-to-trash-cancel"
|
||||
/>
|
||||
</div>
|
||||
</MuiClickAwayListener>
|
||||
);
|
||||
};
|
||||
@@ -1,148 +0,0 @@
|
||||
import {
|
||||
displayFlex,
|
||||
IconButton,
|
||||
styled,
|
||||
textEllipsis,
|
||||
} from '@affine/component';
|
||||
|
||||
export const StyledCollapsedButton = styled('button')<{
|
||||
collapse: boolean;
|
||||
show?: boolean;
|
||||
}>(({ collapse, show = true }) => {
|
||||
return {
|
||||
width: '16px',
|
||||
height: '100%',
|
||||
...displayFlex('center', 'center'),
|
||||
fontSize: '16px',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
color: 'var(--affine-icon-color)',
|
||||
opacity: '.6',
|
||||
transition: 'opacity .15s ease-in-out',
|
||||
display: show ? 'flex' : 'none',
|
||||
svg: {
|
||||
transform: `rotate(${collapse ? '-90' : '0'}deg)`,
|
||||
},
|
||||
':hover': {
|
||||
opacity: '1',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledPinboard = styled('div')<{
|
||||
disable?: boolean;
|
||||
active?: boolean;
|
||||
isOver?: boolean;
|
||||
disableCollapse?: boolean;
|
||||
textWrap?: boolean;
|
||||
}>(
|
||||
({
|
||||
disableCollapse,
|
||||
disable = false,
|
||||
active = false,
|
||||
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
|
||||
? 'var(--affine-text-disable-color)'
|
||||
: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-text-primary-color)',
|
||||
cursor: disable ? 'not-allowed' : 'pointer',
|
||||
background: isOver ? 'rgba(118, 95, 254, 0.06)' : '',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
userSelect: 'none',
|
||||
...(textWrap
|
||||
? {
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}
|
||||
: {}),
|
||||
|
||||
span: {
|
||||
flexGrow: '1',
|
||||
textAlign: 'left',
|
||||
...textEllipsis(1),
|
||||
},
|
||||
'.path-icon': {
|
||||
fontSize: '16px',
|
||||
transform: 'translateY(-4px)',
|
||||
},
|
||||
'.mode-icon': {
|
||||
fontSize: '20px',
|
||||
marginRight: '8px',
|
||||
flexShrink: '0',
|
||||
color: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-icon-color)',
|
||||
},
|
||||
|
||||
':hover': {
|
||||
backgroundColor: disable ? '' : 'var(--affine-hover-color)',
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledOperationButton = styled(IconButton, {
|
||||
shouldForwardProp: prop => {
|
||||
return !['visible'].includes(prop as string);
|
||||
},
|
||||
})<{ visible: boolean }>(({ visible }) => {
|
||||
return {
|
||||
visibility: visible ? 'visible' : 'hidden',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSearchContainer = styled('div')(() => {
|
||||
return {
|
||||
width: 'calc(100% - 24px)',
|
||||
margin: '0 auto',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
label: {
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: '20px',
|
||||
height: '20px',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledMenuContent = styled('div')(() => {
|
||||
return {
|
||||
height: '266px',
|
||||
overflow: 'auto',
|
||||
};
|
||||
});
|
||||
export const StyledMenuSubTitle = styled('div')(() => {
|
||||
return {
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
lineHeight: '36px',
|
||||
padding: '0 12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledMenuFooter = styled('div')(() => {
|
||||
return {
|
||||
width: 'calc(100% - 24px)',
|
||||
margin: '0 auto',
|
||||
borderTop: '1px solid var(--affine-border-color)',
|
||||
padding: '6px 0',
|
||||
|
||||
p: {
|
||||
paddingLeft: '44px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: '14px',
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -6,8 +6,8 @@ import type React from 'react';
|
||||
import { Suspense, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { preload } from 'swr';
|
||||
|
||||
import { fetcher, QueryKey } from '../../../adapters/affine/fetcher';
|
||||
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
|
||||
import { fetcher, QueryKey } from '../../../plugins/affine/fetcher';
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import * as style from './index.css';
|
||||
import { CollaborationPanel } from './panel/collaboration';
|
||||
|
||||
@@ -57,7 +57,7 @@ export const StyledButtonContent = styled('div')(() => {
|
||||
|
||||
export const StyledWorkspaceName = styled('span')(() => {
|
||||
return {
|
||||
color: '#E8178A',
|
||||
fontWeight: '600',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -5,17 +5,14 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import dayjs from 'dayjs';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { workspacePreferredModeAtom } from '../../../atoms';
|
||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
import { pageListEmptyStyle } from './index.css';
|
||||
import { formatDate, usePageHelper } from './utils';
|
||||
|
||||
export type BlockSuitePageListProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
@@ -34,13 +31,6 @@ const filter = {
|
||||
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
|
||||
};
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
const formatDate = (date?: number | unknown) => {
|
||||
const dateStr =
|
||||
typeof date === 'number' ? dayjs(date).format('YYYY-MM-DD HH:mm') : '--';
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
const PageListEmpty = (props: {
|
||||
listType: BlockSuitePageListProps['listType'];
|
||||
}) => {
|
||||
@@ -80,12 +70,13 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
permanentlyDeletePage,
|
||||
cancelPublicPage,
|
||||
} = useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
const { createPage, createEdgeless, isPreferredEdgeless } =
|
||||
usePageHelper(blockSuiteWorkspace);
|
||||
const t = useAFFiNEI18N();
|
||||
const list = useMemo(
|
||||
() => pageMetas.filter(pageMeta => filter[listType](pageMeta, pageMetas)),
|
||||
[pageMetas, listType]
|
||||
);
|
||||
const record = useAtomValue(workspacePreferredModeAtom);
|
||||
if (list.length === 0) {
|
||||
return <PageListEmpty listType={listType} />;
|
||||
}
|
||||
@@ -93,8 +84,11 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
if (listType === 'trash') {
|
||||
const pageList: TrashListData[] = list.map(pageMeta => {
|
||||
return {
|
||||
icon:
|
||||
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
|
||||
icon: isPreferredEdgeless(pageMeta.id) ? (
|
||||
<EdgelessIcon />
|
||||
) : (
|
||||
<PageIcon />
|
||||
),
|
||||
pageId: pageMeta.id,
|
||||
title: pageMeta.title,
|
||||
createDate: formatDate(pageMeta.createDate),
|
||||
@@ -118,8 +112,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
|
||||
const pageList: ListData[] = list.map(pageMeta => {
|
||||
return {
|
||||
icon:
|
||||
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
|
||||
icon: isPreferredEdgeless(pageMeta.id) ? <EdgelessIcon /> : <PageIcon />,
|
||||
pageId: pageMeta.id,
|
||||
title: pageMeta.title,
|
||||
favorite: !!pageMeta.favorite,
|
||||
@@ -158,10 +151,10 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
|
||||
|
||||
return (
|
||||
<PageList
|
||||
onClickPage={onOpenPage}
|
||||
onCreateNewPage={createPage}
|
||||
onCreateNewEdgeless={createEdgeless}
|
||||
isPublicWorkspace={isPublic}
|
||||
list={pageList}
|
||||
listType={listType}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import dayjs from 'dayjs';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { useWorkspacePreferredMode } from '../../../hooks/use-recent-views';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
export const formatDate = (date?: number | unknown) => {
|
||||
const dateStr =
|
||||
typeof date === 'number' ? dayjs(date).format('MM-DD HH:mm') : '--';
|
||||
return dateStr;
|
||||
};
|
||||
|
||||
export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
|
||||
const router = useRouter();
|
||||
const { openPage } = useRouterHelper(router);
|
||||
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const { getPreferredMode, setPreferredMode } = useWorkspacePreferredMode();
|
||||
const isPreferredEdgeless = (pageId: string) => {
|
||||
return getPreferredMode(pageId) === 'edgeless';
|
||||
};
|
||||
|
||||
const createPageAndOpen = () => {
|
||||
const page = createPage();
|
||||
openPage(blockSuiteWorkspace.id, page.id);
|
||||
};
|
||||
const createEdgelessAndOpen = () => {
|
||||
const page = createPage();
|
||||
setPreferredMode(page.id, 'edgeless');
|
||||
openPage(blockSuiteWorkspace.id, page.id);
|
||||
};
|
||||
return {
|
||||
createPage: createPageAndOpen,
|
||||
createEdgeless: createEdgelessAndOpen,
|
||||
isPreferredEdgeless: isPreferredEdgeless,
|
||||
};
|
||||
};
|
||||
@@ -108,14 +108,12 @@ const PageMenu = () => {
|
||||
{mode === 'page' ? t['Edgeless']() : t['Page']()}
|
||||
</MenuItem>
|
||||
<Export />
|
||||
{!pageMeta.isRootPinboard && (
|
||||
<MoveToTrash
|
||||
testId="editor-option-menu-delete"
|
||||
onItemClick={() => {
|
||||
setOpenConfirm(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
onItemClick={() => {
|
||||
setOpenConfirm(true);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.horizontalDividerContainer}>
|
||||
<div className={styles.horizontalDivider} />
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,8 @@ import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
|
||||
import { affineAuth } from '../../../../plugins/affine';
|
||||
import type { AffineOfficialWorkspace } from '../../../../shared';
|
||||
import { affineAuth } from '../../../../shared/apis';
|
||||
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
|
||||
|
||||
const IconWrapper = styled('div')(() => {
|
||||
|
||||
@@ -16,6 +16,11 @@ export const headerContainer = style({
|
||||
WebkitAppRegion: 'drag',
|
||||
},
|
||||
},
|
||||
'@media': {
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
} as ComplexStyleRule);
|
||||
|
||||
export const header = style({
|
||||
@@ -191,10 +196,11 @@ export const windowAppControl = style({
|
||||
WebkitAppRegion: 'no-drag',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
width: '32px',
|
||||
width: '42px',
|
||||
height: '32px',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '2px',
|
||||
borderRadius: '4px',
|
||||
selectors: {
|
||||
'&[data-type="close"]:hover': {
|
||||
background: 'var(--affine-error-color)',
|
||||
|
||||
@@ -21,7 +21,7 @@ export type PageDetailEditorProps = {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
pageId: string;
|
||||
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
|
||||
onLoad?: (page: Page, editor: EditorContainer) => void;
|
||||
onLoad?: (page: Page, editor: EditorContainer) => () => void;
|
||||
header?: React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -85,7 +85,10 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
|
||||
updatedDate: Date.now(),
|
||||
});
|
||||
localStorage.setItem('last_page_id', page.id);
|
||||
onLoad?.(page, editor);
|
||||
if (onLoad) {
|
||||
return onLoad(page, editor);
|
||||
}
|
||||
return () => {};
|
||||
},
|
||||
[onLoad, setEditor]
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type React from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out';
|
||||
import { affineAuth } from '../../../plugins/affine';
|
||||
import { affineAuth } from '../../../shared/apis';
|
||||
import { toast } from '../../../utils';
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import { IconButton, Tooltip, TreeView } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
ArrowRightSmallIcon,
|
||||
CollapseIcon,
|
||||
ExpandIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { Fragment, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import type { PinboardNode } from '../../../../hooks/use-pinboard-data';
|
||||
import { usePinboardData } from '../../../../hooks/use-pinboard-data';
|
||||
import { useRouterHelper } from '../../../../hooks/use-router-helper';
|
||||
import type { BlockSuiteWorkspace } from '../../../../shared';
|
||||
import { PinboardRender } from '../../../affine/pinboard';
|
||||
import {
|
||||
StyledNavigationPathContainer,
|
||||
StyledNavPathExtendContainer,
|
||||
StyledNavPathLink,
|
||||
} from './styles';
|
||||
import { calcHowManyPathShouldBeShown, findPath } from './utils';
|
||||
|
||||
export const NavigationPath = ({
|
||||
blockSuiteWorkspace,
|
||||
pageId: propsPageId,
|
||||
onJumpToPage,
|
||||
}: {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
pageId?: string;
|
||||
onJumpToPage?: (pageId: string) => void;
|
||||
}) => {
|
||||
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
const router = useRouter();
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const [openExtend, setOpenExtend] = useState(false);
|
||||
const pageId = propsPageId ?? router.query.pageId;
|
||||
const { jumpToPage } = useRouterHelper(router);
|
||||
const pathData = useMemo(() => {
|
||||
const meta = metas.find(m => m.id === pageId);
|
||||
const path = meta ? findPath(metas, meta) : [];
|
||||
|
||||
const actualPath = calcHowManyPathShouldBeShown(path);
|
||||
return {
|
||||
hasEllipsis: path.length !== actualPath.length,
|
||||
path: actualPath,
|
||||
};
|
||||
}, [metas, pageId]);
|
||||
|
||||
if (pathData.path.length < 2) {
|
||||
// Means there is no parent page
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StyledNavigationPathContainer data-testid="navigation-path">
|
||||
{openExtend ? (
|
||||
<span>{t['Navigation Path']()}</span>
|
||||
) : (
|
||||
pathData.path.map((meta, index) => {
|
||||
const isLast = index === pathData.path.length - 1;
|
||||
const showEllipsis = pathData.hasEllipsis && index === 1;
|
||||
return (
|
||||
<Fragment key={meta.id}>
|
||||
{showEllipsis && (
|
||||
<>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setOpenExtend(true)}
|
||||
>
|
||||
<MoreHorizontalIcon />
|
||||
</IconButton>
|
||||
<ArrowRightSmallIcon className="path-arrow" />
|
||||
</>
|
||||
)}
|
||||
<StyledNavPathLink
|
||||
data-testid="navigation-path-link"
|
||||
active={isLast}
|
||||
onClick={() => {
|
||||
if (isLast) return;
|
||||
jumpToPage(blockSuiteWorkspace.id, meta.id);
|
||||
onJumpToPage?.(meta.id);
|
||||
}}
|
||||
title={meta.title}
|
||||
>
|
||||
{meta.title}
|
||||
</StyledNavPathLink>
|
||||
{!isLast && <ArrowRightSmallIcon className="path-arrow" />}
|
||||
</Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<Tooltip
|
||||
content={
|
||||
openExtend
|
||||
? t['Back to Quick Search']()
|
||||
: t['View Navigation Path']()
|
||||
}
|
||||
placement="top"
|
||||
disablePortal={true}
|
||||
>
|
||||
<IconButton
|
||||
data-testid="navigation-path-expand-btn"
|
||||
size="small"
|
||||
className="collapse-btn"
|
||||
onClick={() => {
|
||||
setOpenExtend(!openExtend);
|
||||
}}
|
||||
>
|
||||
{openExtend ? <CollapseIcon /> : <ExpandIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</StyledNavigationPathContainer>
|
||||
<NavigationPathExtendPanel
|
||||
open={openExtend}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
metas={metas}
|
||||
onJumpToPage={onJumpToPage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NavigationPathExtendPanel = ({
|
||||
open,
|
||||
metas,
|
||||
blockSuiteWorkspace,
|
||||
onJumpToPage,
|
||||
}: {
|
||||
open: boolean;
|
||||
metas: PageMeta[];
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
onJumpToPage?: (pageId: string) => void;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { jumpToPage } = useRouterHelper(router);
|
||||
|
||||
const handlePinboardClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>, node: PinboardNode) => {
|
||||
jumpToPage(blockSuiteWorkspace.id, node.id);
|
||||
onJumpToPage?.(node.id);
|
||||
},
|
||||
[blockSuiteWorkspace.id, jumpToPage, onJumpToPage]
|
||||
);
|
||||
|
||||
const { data } = usePinboardData({
|
||||
metas,
|
||||
pinboardRender: PinboardRender,
|
||||
blockSuiteWorkspace: blockSuiteWorkspace,
|
||||
onClick: handlePinboardClick,
|
||||
asPath: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledNavPathExtendContainer
|
||||
show={open}
|
||||
data-testid="navigation-path-expand-panel"
|
||||
>
|
||||
<TreeView data={data} indent={10} disableCollapse={true} />
|
||||
</StyledNavPathExtendContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
|
||||
export const StyledNavigationPathContainer = styled('div')(() => {
|
||||
return {
|
||||
height: '46px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
background: 'var(--affine-background-secondary-color)',
|
||||
padding: '0 40px 0 20px',
|
||||
position: 'relative',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
zIndex: 2,
|
||||
'.collapse-btn': {
|
||||
position: 'absolute',
|
||||
right: '12px',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
margin: 'auto',
|
||||
},
|
||||
'.path-arrow': {
|
||||
fontSize: '16px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledNavPathLink = styled('div')<{ active?: boolean }>(
|
||||
({ active }) => {
|
||||
return {
|
||||
color: active
|
||||
? 'var(--affine-text-primary-color)'
|
||||
: 'var(--affine-text-secondary-color)',
|
||||
cursor: active ? 'auto' : 'pointer',
|
||||
maxWidth: '160px',
|
||||
...textEllipsis(1),
|
||||
padding: '0 4px',
|
||||
transition: 'background .15s',
|
||||
':hover': active
|
||||
? {}
|
||||
: {
|
||||
background: 'var(--affine-hover-color)',
|
||||
borderRadius: '4px',
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export const StyledNavPathExtendContainer = styled('div')<{ show: boolean }>(
|
||||
({ show }) => {
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: show ? '0' : '-100%',
|
||||
zIndex: '1',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
background: 'var(--affine-background-secondary-color)',
|
||||
transition: 'top .15s',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
padding: '46px 12px 0 15px',
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -1,60 +0,0 @@
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
|
||||
export function findPath(metas: PageMeta[], meta: PageMeta): PageMeta[] {
|
||||
function helper(group: PageMeta[]): PageMeta[] {
|
||||
const last = group[group.length - 1];
|
||||
const parent = metas.find(m => (m.subpageIds ?? []).includes(last.id));
|
||||
if (parent) {
|
||||
return helper([...group, parent]);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
return helper([meta]).reverse();
|
||||
}
|
||||
|
||||
function getPathItemWidth(content: string) {
|
||||
// padding is 8px, arrow is 16px, and each char is 10px
|
||||
// the max width is 160px
|
||||
const charWidth = 10;
|
||||
const w = content.length * charWidth + 8 + 16;
|
||||
return w > 160 ? 160 : w;
|
||||
}
|
||||
|
||||
// XXX: this is a static way to calculate the path width, not get the real width
|
||||
export function calcHowManyPathShouldBeShown(path: PageMeta[]): PageMeta[] {
|
||||
if (path.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const first = path[0];
|
||||
const last = path[path.length - 1];
|
||||
// 20 is the ellipsis icon width
|
||||
const maxWidth = 550 - 20;
|
||||
if (first.id === last.id) {
|
||||
return [first];
|
||||
}
|
||||
|
||||
function getMiddlePath(restWidth: number, restPath: PageMeta[]): PageMeta[] {
|
||||
if (restPath.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const last = restPath[restPath.length - 1];
|
||||
const w = getPathItemWidth(last.title);
|
||||
if (restWidth - w > 80) {
|
||||
return [
|
||||
...getMiddlePath(restWidth - w, restPath.slice(0, restPath.length - 1)),
|
||||
last,
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
first,
|
||||
...getMiddlePath(
|
||||
maxWidth - getPathItemWidth(first.title),
|
||||
path.slice(1, -1)
|
||||
),
|
||||
last,
|
||||
];
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export const WorkspaceListModal = ({
|
||||
{environment.isDesktop && (
|
||||
<Menu
|
||||
placement="auto"
|
||||
trigger={['click', 'hover']}
|
||||
trigger={['click']}
|
||||
zIndex={1000}
|
||||
content={
|
||||
<StyledCreateWorkspaceCardPillContainer>
|
||||
|
||||
@@ -62,6 +62,9 @@ export const StyledCreateWorkspaceCard = styled('div')(() => {
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
},
|
||||
'@media (max-width: 720px)': {
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
|
||||
import { useAtomValue } from 'jotai';
|
||||
@@ -10,6 +11,7 @@ import { useMemo, useState } from 'react';
|
||||
import { workspacePreferredModeAtom } from '../../../../atoms';
|
||||
import type { FavoriteListProps } from '../index';
|
||||
import EmptyItem from './empty-item';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
interface FavoriteMenuItemProps {
|
||||
workspace: Workspace;
|
||||
@@ -33,16 +35,16 @@ function FavoriteMenuItem({
|
||||
return [...new Set(references.filter(ref => !parentIds.has(ref)))];
|
||||
}, [references, parentIds]);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const collapsible = referencesToShow.length > 0 && parentIds.size === 0;
|
||||
const showReferences = collapsible ? !collapsed : referencesToShow.length > 0;
|
||||
const collapsible = referencesToShow.length > 0;
|
||||
const nestedItem = parentIds.size > 0;
|
||||
const untitled = !metaMapping[pageId]?.title;
|
||||
return (
|
||||
<>
|
||||
<Collapsible.Root
|
||||
className={styles.favItemWrapper}
|
||||
data-nested={nestedItem}
|
||||
open={!collapsed}
|
||||
>
|
||||
<MenuLinkItem
|
||||
style={{
|
||||
marginLeft: nestedItem ? '12px' : undefined,
|
||||
width: nestedItem ? 'calc(100% - 12px)' : undefined,
|
||||
}}
|
||||
data-type="favorite-list-item"
|
||||
data-testid={`favorite-list-item-${pageId}`}
|
||||
active={active}
|
||||
@@ -51,21 +53,28 @@ function FavoriteMenuItem({
|
||||
collapsed={collapsible ? collapsed : undefined}
|
||||
onCollapsedChange={setCollapsed}
|
||||
>
|
||||
<span>{metaMapping[pageId]?.title || 'Untitled'}</span>
|
||||
<span className={styles.label} data-untitled={untitled}>
|
||||
{metaMapping[pageId]?.title || 'Untitled'}
|
||||
</span>
|
||||
</MenuLinkItem>
|
||||
{showReferences &&
|
||||
referencesToShow.map(ref => {
|
||||
return (
|
||||
<FavoriteMenuItem
|
||||
key={ref}
|
||||
workspace={workspace}
|
||||
pageId={ref}
|
||||
metaMapping={metaMapping}
|
||||
parentIds={new Set([...parentIds, pageId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
{collapsible && (
|
||||
<Collapsible.Content className={styles.collapsibleContent}>
|
||||
<div className={styles.collapsibleContentInner}>
|
||||
{referencesToShow.map(ref => {
|
||||
return (
|
||||
<FavoriteMenuItem
|
||||
key={ref}
|
||||
workspace={workspace}
|
||||
pageId={ref}
|
||||
metaMapping={metaMapping}
|
||||
parentIds={new Set([...parentIds, pageId])}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
)}
|
||||
</Collapsible.Root>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
export const label = style({
|
||||
selectors: {
|
||||
'&[data-untitled="true"]': {
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const favItemWrapper = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
selectors: {
|
||||
'&[data-nested="true"]': {
|
||||
marginLeft: '12px',
|
||||
width: 'calc(100% - 12px)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const slideDown = keyframes({
|
||||
'0%': {
|
||||
height: '0px',
|
||||
},
|
||||
'100%': {
|
||||
height: 'var(--radix-collapsible-content-height)',
|
||||
},
|
||||
});
|
||||
|
||||
const slideUp = keyframes({
|
||||
'0%': {
|
||||
height: 'var(--radix-collapsible-content-height)',
|
||||
},
|
||||
'100%': {
|
||||
height: '0px',
|
||||
},
|
||||
});
|
||||
|
||||
export const collapsibleContent = style({
|
||||
overflow: 'hidden',
|
||||
selectors: {
|
||||
'&[data-state="open"]': {
|
||||
animation: `${slideDown} 0.2s ease-out`,
|
||||
},
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${slideUp} 0.2s ease-out`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const collapsibleContentInner = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
});
|
||||
@@ -1,25 +1,5 @@
|
||||
import type { Page } from '@blocksuite/store';
|
||||
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
|
||||
export type FavoriteListProps = {
|
||||
currentWorkspace: AllWorkspace;
|
||||
};
|
||||
|
||||
export type WorkSpaceSliderBarProps = {
|
||||
isPublicWorkspace: boolean;
|
||||
onOpenQuickSearchModal: () => void;
|
||||
onOpenWorkspaceListModal: () => void;
|
||||
currentWorkspace: AllWorkspace | null;
|
||||
currentPageId: string | null;
|
||||
openPage: (pageId: string) => void;
|
||||
createPage: () => Page;
|
||||
currentPath: string;
|
||||
paths: {
|
||||
all: (workspaceId: string) => string;
|
||||
favorite: (workspaceId: string) => string;
|
||||
trash: (workspaceId: string) => string;
|
||||
setting: (workspaceId: string) => string;
|
||||
shared: (workspaceId: string) => string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { TreeView } from '@affine/component';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { PinboardNode } from '../../../hooks/use-pinboard-data';
|
||||
import { usePinboardData } from '../../../hooks/use-pinboard-data';
|
||||
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
import { PinboardRender } from '../../affine/pinboard';
|
||||
|
||||
export type PinboardProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
openPage: (pageId: string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const Pinboard = ({ blockSuiteWorkspace, openPage }: PinboardProps) => {
|
||||
const allMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
const handlePinboardClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>, node: PinboardNode) => {
|
||||
openPage(node.id);
|
||||
},
|
||||
[openPage]
|
||||
);
|
||||
const onAdd = useCallback(
|
||||
(id: string) => {
|
||||
openPage(id);
|
||||
},
|
||||
[openPage]
|
||||
);
|
||||
|
||||
const { data } = usePinboardData({
|
||||
metas: allMetas,
|
||||
pinboardRender: PinboardRender,
|
||||
blockSuiteWorkspace: blockSuiteWorkspace,
|
||||
onClick: handlePinboardClick,
|
||||
showOperationButton: true,
|
||||
});
|
||||
|
||||
const { addPin, deletePin, dropPin } = usePinboardHandler({
|
||||
blockSuiteWorkspace: blockSuiteWorkspace,
|
||||
metas: allMetas,
|
||||
onAdd,
|
||||
});
|
||||
|
||||
if (!data.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div data-testid="sidebar-pinboard-container">
|
||||
<TreeView
|
||||
data={data}
|
||||
onAdd={addPin}
|
||||
onDelete={deletePin}
|
||||
onDrop={dropPin}
|
||||
indent={16}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Pinboard;
|
||||
@@ -84,7 +84,7 @@ export const RootAppSidebar = ({
|
||||
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
||||
const t = useAFFiNEI18N();
|
||||
const onClickNewPage = useCallback(async () => {
|
||||
const page = await createPage();
|
||||
const page = createPage();
|
||||
openPage(page.id);
|
||||
}, [createPage, openPage]);
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ beforeAll(() => {
|
||||
`/workspace/[workspaceId/${WorkspaceSubPath.ALL}`,
|
||||
`/workspace/[workspaceId/${WorkspaceSubPath.SETTING}`,
|
||||
`/workspace/[workspaceId/${WorkspaceSubPath.TRASH}`,
|
||||
`/workspace/[workspaceId/${WorkspaceSubPath.FAVORITE}`,
|
||||
'/workspace/[workspaceId]/[pageId]',
|
||||
])
|
||||
);
|
||||
|
||||
@@ -11,14 +11,15 @@ import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { createStore, Provider } from 'jotai/index';
|
||||
import { createStore, Provider } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import routerMock from 'next-router-mock';
|
||||
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { LocalPlugin } from '../../adapters/local';
|
||||
import { workspacesAtom } from '../../atoms';
|
||||
import { LocalPlugin } from '../../plugins/local';
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { WorkspaceSubPath } from '../../shared';
|
||||
import {
|
||||
@@ -37,7 +38,6 @@ beforeAll(() => {
|
||||
`/workspace/[workspaceId/${WorkspaceSubPath.ALL}`,
|
||||
`/workspace/[workspaceId/${WorkspaceSubPath.SETTING}`,
|
||||
`/workspace/[workspaceId/${WorkspaceSubPath.TRASH}`,
|
||||
`/workspace/[workspaceId/${WorkspaceSubPath.FAVORITE}`,
|
||||
'/workspace/[workspaceId]/[pageId]',
|
||||
])
|
||||
);
|
||||
@@ -45,10 +45,11 @@ beforeAll(() => {
|
||||
|
||||
async function getJotaiContext() {
|
||||
const store = createStore();
|
||||
const ProviderWrapper: React.FC<React.PropsWithChildren> =
|
||||
function ProviderWrapper({ children }) {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
};
|
||||
const ProviderWrapper: FC<PropsWithChildren> = function ProviderWrapper({
|
||||
children,
|
||||
}) {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
};
|
||||
const workspaces = await store.get(workspacesAtom);
|
||||
expect(workspaces.length).toBe(0);
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../../plugins';
|
||||
import { WorkspaceAdapters } from '../../adapters/workspace';
|
||||
|
||||
export function useAffineLogIn() {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../../plugins';
|
||||
import { WorkspaceAdapters } from '../../adapters/workspace';
|
||||
|
||||
export function useAffineLogOut() {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@affine/workspace/affine/login';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { affineAuth } from '../../plugins/affine';
|
||||
import { affineAuth } from '../../shared/apis';
|
||||
|
||||
const logger = new DebugLogger('auth-token');
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Member } from '@affine/workspace/affine/api';
|
||||
import { useCallback } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
export function useMembers(workspaceId: string) {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { currentEditorAtom } from '../../atoms';
|
||||
|
||||
export function useReferenceLinkEffect(props?: {
|
||||
pageLinkClicked?: (params: { pageId: string }) => void;
|
||||
subpageLinked?: (params: { pageId: string }) => void;
|
||||
subpageUnlinked?: (params: { pageId: string }) => void;
|
||||
}) {
|
||||
const { pageLinkClicked, subpageLinked, subpageUnlinked } = props ?? {};
|
||||
const editor = useAtomValue(currentEditorAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const linkClickedDisposable = editor.slots.pageLinkClicked.on(
|
||||
({ pageId }) => {
|
||||
pageLinkClicked?.({ pageId });
|
||||
}
|
||||
);
|
||||
|
||||
// const subpageLinkedDisposable = editor.slots.subpageLinked.on(
|
||||
// ({ pageId }) => {
|
||||
// subpageLinked?.({ pageId });
|
||||
// }
|
||||
// );
|
||||
// const subpageUnlinkedDisposable = editor.slots.subpageUnlinked.on(
|
||||
// ({ pageId }) => {
|
||||
// subpageUnlinked?.({ pageId });
|
||||
// }
|
||||
// );
|
||||
|
||||
return () => {
|
||||
linkClickedDisposable.dispose();
|
||||
// subpageLinkedDisposable.dispose();
|
||||
// subpageUnlinkedDisposable.dispose();
|
||||
};
|
||||
}, [editor, pageLinkClicked, subpageLinked, subpageUnlinked]);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import type { AffineLegacyCloudWorkspace } from '@affine/workspace/type';
|
||||
import { useCallback } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
import { affineApis } from '../../shared/apis';
|
||||
|
||||
export function useToggleWorkspacePublish(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
|
||||
export interface QueryEmailMember {
|
||||
id: string;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { affineAuth } from '../../plugins/affine';
|
||||
import { affineAuth } from '../../shared/apis';
|
||||
import { useTransformWorkspace } from '../use-transform-workspace';
|
||||
|
||||
export function useOnTransformWorkspace() {
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import type { Node } from '@affine/component';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { BlockSuiteWorkspace } from '../shared';
|
||||
|
||||
export type RenderProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
onClick?: (e: MouseEvent<HTMLDivElement>, node: PinboardNode) => void;
|
||||
showOperationButton?: boolean;
|
||||
// If true, the node will be rendered with path icon at start
|
||||
asPath?: boolean;
|
||||
};
|
||||
|
||||
export type NodeRenderProps = RenderProps & {
|
||||
metas: PageMeta[];
|
||||
currentMeta: PageMeta;
|
||||
};
|
||||
|
||||
export type PinboardNode = Node<NodeRenderProps>;
|
||||
|
||||
function flattenToTree(
|
||||
metas: PageMeta[],
|
||||
pinboardRender: PinboardNode['render'],
|
||||
renderProps: RenderProps
|
||||
): PinboardNode[] {
|
||||
const rootMeta = metas.find(meta => meta.isRootPinboard);
|
||||
const helper = (internalMetas: PageMeta[]): PinboardNode[] => {
|
||||
return internalMetas.reduce<PinboardNode[]>(
|
||||
(returnedMetas, internalMeta) => {
|
||||
const { subpageIds = [] } = internalMeta;
|
||||
const childrenMetas = subpageIds
|
||||
.map(id => metas.find(m => m.id === id)!)
|
||||
.filter(m => m);
|
||||
// @ts-ignore
|
||||
const returnedMeta: PinboardNode = {
|
||||
...internalMeta,
|
||||
children: helper(childrenMetas),
|
||||
render: (node, props) =>
|
||||
pinboardRender(node, props, {
|
||||
...renderProps,
|
||||
currentMeta: internalMeta,
|
||||
metas,
|
||||
}),
|
||||
};
|
||||
returnedMetas.push(returnedMeta);
|
||||
return returnedMetas;
|
||||
},
|
||||
[]
|
||||
);
|
||||
};
|
||||
// Unreachable code, we have removed the root pinboard
|
||||
// @ts-expect-error
|
||||
return helper(rootMeta ? [{ ...rootMeta, renderTopLine: false }] : []);
|
||||
}
|
||||
|
||||
export function usePinboardData({
|
||||
metas,
|
||||
pinboardRender,
|
||||
blockSuiteWorkspace,
|
||||
onClick,
|
||||
showOperationButton,
|
||||
asPath,
|
||||
}: {
|
||||
metas: PageMeta[];
|
||||
pinboardRender: PinboardNode['render'];
|
||||
} & RenderProps) {
|
||||
const data = useMemo(
|
||||
() =>
|
||||
flattenToTree(metas, pinboardRender, {
|
||||
blockSuiteWorkspace,
|
||||
onClick,
|
||||
showOperationButton,
|
||||
asPath,
|
||||
}),
|
||||
[
|
||||
asPath,
|
||||
blockSuiteWorkspace,
|
||||
metas,
|
||||
onClick,
|
||||
pinboardRender,
|
||||
showOperationButton,
|
||||
]
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export default usePinboardData;
|
||||
@@ -1,138 +0,0 @@
|
||||
import type { TreeViewProps } from '@affine/component';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import type { BlockSuiteWorkspace } from '../shared';
|
||||
import { useBlockSuiteMetaHelper } from './affine/use-block-suite-meta-helper';
|
||||
import { useReferenceLinkHelper } from './affine/use-reference-link-helper';
|
||||
import type { NodeRenderProps } from './use-pinboard-data';
|
||||
|
||||
const logger = new DebugLogger('pinboard');
|
||||
|
||||
function findRootIds(metas: PageMeta[], id: string): string[] {
|
||||
const parentMeta = metas.find(m => m.subpageIds?.includes(id));
|
||||
if (!parentMeta) {
|
||||
return [id];
|
||||
}
|
||||
return [parentMeta.id, ...findRootIds(metas, parentMeta.id)];
|
||||
}
|
||||
export function usePinboardHandler({
|
||||
blockSuiteWorkspace,
|
||||
metas: propsMetas,
|
||||
onAdd,
|
||||
onDelete,
|
||||
onDrop,
|
||||
}: {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
metas?: PageMeta[];
|
||||
onAdd?: (addedId: string, parentId: string) => void;
|
||||
onDelete?: TreeViewProps<NodeRenderProps>['onDelete'];
|
||||
onDrop?: TreeViewProps<NodeRenderProps>['onDrop'];
|
||||
}) {
|
||||
const metas = useMemo(
|
||||
() => propsMetas || blockSuiteWorkspace.meta.pageMetas || [],
|
||||
[blockSuiteWorkspace.meta.pageMetas, propsMetas]
|
||||
);
|
||||
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
|
||||
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const { removeToTrash: removeToTrashHelper } =
|
||||
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
const { addReferenceLink, removeReferenceLink } =
|
||||
useReferenceLinkHelper(blockSuiteWorkspace);
|
||||
|
||||
const addPin = useCallback(
|
||||
(parentId: string) => {
|
||||
const id = nanoid();
|
||||
createPage(id);
|
||||
onAdd?.(id, parentId);
|
||||
addReferenceLink(parentId, id);
|
||||
},
|
||||
[addReferenceLink, createPage, onAdd]
|
||||
);
|
||||
|
||||
const deletePin = useCallback(
|
||||
(deleteId: string) => {
|
||||
removeToTrashHelper(deleteId);
|
||||
onDelete?.(deleteId);
|
||||
},
|
||||
[removeToTrashHelper, onDelete]
|
||||
);
|
||||
|
||||
const dropPin = useCallback(
|
||||
(
|
||||
dragId: string,
|
||||
dropId: string,
|
||||
position: {
|
||||
topLine: boolean;
|
||||
bottomLine: boolean;
|
||||
internal: boolean;
|
||||
}
|
||||
) => {
|
||||
if (dragId === dropId) {
|
||||
return;
|
||||
}
|
||||
const dropRootIds = findRootIds(metas, dropId);
|
||||
if (dropRootIds.includes(dragId)) {
|
||||
return;
|
||||
}
|
||||
logger.info('handleDrop', {
|
||||
dragId,
|
||||
dropId,
|
||||
position,
|
||||
metas,
|
||||
});
|
||||
|
||||
const { topLine, bottomLine } = position;
|
||||
|
||||
const dragParentMeta = metas.find(meta =>
|
||||
meta.subpageIds?.includes(dragId)
|
||||
);
|
||||
if (bottomLine || topLine) {
|
||||
const insertOffset = bottomLine ? 1 : 0;
|
||||
|
||||
const dropParentMeta = metas.find(m => m.subpageIds?.includes(dropId));
|
||||
if (dropParentMeta?.id === dragParentMeta?.id) {
|
||||
// same parent, resort node
|
||||
const newSubpageIds = [...(dragParentMeta?.subpageIds ?? [])];
|
||||
const deleteIndex = newSubpageIds.findIndex(id => id === dragId);
|
||||
newSubpageIds.splice(deleteIndex, 1);
|
||||
const insertIndex =
|
||||
newSubpageIds.findIndex(id => id === dropId) + insertOffset;
|
||||
newSubpageIds.splice(insertIndex, 0, dragId);
|
||||
dragParentMeta &&
|
||||
setPageMeta(dragParentMeta.id, {
|
||||
subpageIds: newSubpageIds,
|
||||
});
|
||||
return onDrop?.(dragId, dropId, position);
|
||||
}
|
||||
// Old parent will delete drag node, new parent will be added
|
||||
removeReferenceLink(dragId);
|
||||
dropParentMeta && addReferenceLink(dropParentMeta.id, dragId);
|
||||
return onDrop?.(dragId, dropId, position);
|
||||
}
|
||||
|
||||
// drop into the node
|
||||
if (dragParentMeta && dragParentMeta.id === dropId) {
|
||||
return;
|
||||
}
|
||||
if (dragParentMeta) {
|
||||
removeReferenceLink(dragId);
|
||||
}
|
||||
const dropMeta = metas.find(meta => meta.id === dropId)!;
|
||||
addReferenceLink(dropMeta.id, dragId);
|
||||
},
|
||||
[addReferenceLink, metas, onDrop, removeReferenceLink, setPageMeta]
|
||||
);
|
||||
|
||||
return {
|
||||
dropPin,
|
||||
addPin,
|
||||
deletePin,
|
||||
};
|
||||
}
|
||||
|
||||
export default usePinboardHandler;
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { NextRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@@ -11,6 +11,19 @@ import {
|
||||
} from '../atoms';
|
||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
||||
|
||||
export const useWorkspacePreferredMode = () => {
|
||||
const [record, setPreferred] = useAtom(workspacePreferredModeAtom);
|
||||
return {
|
||||
getPreferredMode: (pageId: Page['id']) => record[pageId] ?? 'page',
|
||||
setPreferredMode: (pageId: Page['id'], mode: 'page' | 'edgeless') => {
|
||||
setPreferred(record => ({
|
||||
...record,
|
||||
[pageId]: mode,
|
||||
}));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function useRecentlyViewed() {
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const workspaceId = workspace?.id || null;
|
||||
@@ -30,15 +43,19 @@ export function useSyncRecentViewsWithRouter(
|
||||
const meta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
meta => meta.id === pageId
|
||||
);
|
||||
const currentMode = useAtomValue(workspacePreferredModeAtom)[
|
||||
pageId as string
|
||||
];
|
||||
const { getPreferredMode } = useWorkspacePreferredMode();
|
||||
|
||||
const currentMode =
|
||||
typeof pageId === 'string' ? getPreferredMode(pageId) : 'page';
|
||||
useEffect(() => {
|
||||
if (!workspaceId) return;
|
||||
if (pageId && meta) {
|
||||
set(workspaceId, {
|
||||
id: pageId as string,
|
||||
mode: currentMode ?? 'page',
|
||||
/**
|
||||
* @deprecated No necessary update `mode` at here, use `mode` from {@link useWorkspacePreferredMode} directly.
|
||||
*/
|
||||
mode: currentMode,
|
||||
});
|
||||
}
|
||||
}, [pageId, meta, workspaceId, set, currentMode]);
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { WorkspaceRegistry } from '@affine/workspace/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../plugins';
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
|
||||
/**
|
||||
* Transform workspace from one flavour to another
|
||||
|
||||
@@ -8,9 +8,9 @@ import { nanoid } from '@blocksuite/store';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { LocalPlugin } from '../adapters/local';
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import { workspacesAtom } from '../atoms';
|
||||
import { WorkspaceAdapters } from '../plugins';
|
||||
import { LocalPlugin } from '../plugins/local';
|
||||
import type { AllWorkspace } from '../shared';
|
||||
|
||||
export function useWorkspaces(): AllWorkspace[] {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useRouter } from 'next/router';
|
||||
import type { FC, PropsWithChildren, ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
|
||||
import { useTrackRouterHistoryEffect } from '../atoms/history';
|
||||
import {
|
||||
@@ -38,7 +39,6 @@ import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
import { useRouterHelper } from '../hooks/use-router-helper';
|
||||
import { useRouterTitle } from '../hooks/use-router-title';
|
||||
import { useWorkspaces } from '../hooks/use-workspaces';
|
||||
import { WorkspaceAdapters } from '../plugins';
|
||||
import { ModalProvider } from '../providers/modal-provider';
|
||||
import { pathGenerator, publicPathGenerator } from '../shared';
|
||||
|
||||
@@ -243,12 +243,15 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
|
||||
(meta && WorkspaceAdapters[meta.flavour].UI.Provider) ?? DefaultProvider;
|
||||
return (
|
||||
<>
|
||||
{/* fixme(himself65): don't re-render whole modals */}
|
||||
<AllWorkspaceContext>
|
||||
<CurrentWorkspaceContext>
|
||||
<ModalProvider key={currentWorkspaceId} />
|
||||
</CurrentWorkspaceContext>
|
||||
</AllWorkspaceContext>
|
||||
{/* load all workspaces is costly, do not block the whole UI */}
|
||||
<Suspense fallback={null}>
|
||||
<AllWorkspaceContext>
|
||||
<CurrentWorkspaceContext>
|
||||
{/* fixme(himself65): don't re-render whole modals */}
|
||||
<ModalProvider key={currentWorkspaceId} />
|
||||
</CurrentWorkspaceContext>
|
||||
</AllWorkspaceContext>
|
||||
</Suspense>
|
||||
<CurrentWorkspaceContext>
|
||||
<Suspense fallback={<WorkspaceFallback />}>
|
||||
<Provider>
|
||||
@@ -284,11 +287,6 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
|
||||
void jumpToPage(currentWorkspace.id, pageId);
|
||||
}
|
||||
}
|
||||
|
||||
// fixme: pinboard has been removed,
|
||||
// the related code should be removed in the future.
|
||||
// no matter the workspace is empty, ensure the root pinboard exists
|
||||
// ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
|
||||
//#endregion
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -11,9 +11,9 @@ import { useRouter } from 'next/router';
|
||||
import { Suspense } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../adapters/affine/fetcher';
|
||||
import { PageLoading } from '../../components/pure/loading';
|
||||
import { RouteLogic, useRouterHelper } from '../../hooks/use-router-helper';
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import type { NextPageWithLayout } from '../../shared';
|
||||
import { WorkspaceSubPath } from '../../shared';
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useAtom, useAtomValue } from 'jotai';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense, useCallback, useEffect } from 'react';
|
||||
import { Suspense, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
publicPageBlockSuiteAtom,
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
import { PageDetailEditor } from '../../../components/page-detail-editor';
|
||||
import { WorkspaceAvatar } from '../../../components/pure/footer';
|
||||
import { PageLoading } from '../../../components/pure/loading';
|
||||
import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link-effect';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import {
|
||||
PublicQuickSearch,
|
||||
@@ -64,14 +63,6 @@ const PublicWorkspaceDetailPageInner = (): ReactElement => {
|
||||
}
|
||||
const router = useRouter();
|
||||
const { openPage } = useRouterHelper(router);
|
||||
useReferenceLinkEffect({
|
||||
pageLinkClicked: useCallback(
|
||||
({ pageId }: { pageId: string }) => {
|
||||
return openPage(blockSuiteWorkspace.id, pageId);
|
||||
},
|
||||
[blockSuiteWorkspace.id, openPage]
|
||||
),
|
||||
});
|
||||
const t = useAFFiNEI18N();
|
||||
const [name] = useBlockSuiteWorkspaceName(blockSuiteWorkspace);
|
||||
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(blockSuiteWorkspace);
|
||||
@@ -86,6 +77,12 @@ const PublicWorkspaceDetailPageInner = (): ReactElement => {
|
||||
onLoad={(_, editor) => {
|
||||
const { page } = editor;
|
||||
page.awarenessStore.setReadonly(page, true);
|
||||
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
|
||||
return openPage(blockSuiteWorkspace.id, pageId);
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
}}
|
||||
onInit={initPage}
|
||||
header={
|
||||
|
||||
@@ -4,25 +4,21 @@ import { config } from '@affine/env';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useRouter } from 'next/router';
|
||||
import type React from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../../../adapters/workspace';
|
||||
import { rootCurrentWorkspaceAtom } from '../../../atoms/root';
|
||||
import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link-effect';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
|
||||
import { useSyncRecentViewsWithRouter } from '../../../hooks/use-recent-views';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import { WorkspaceAdapters } from '../../../plugins';
|
||||
import type { BlockSuiteWorkspace, NextPageWithLayout } from '../../../shared';
|
||||
|
||||
function setEditorFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
|
||||
@@ -42,41 +38,19 @@ const WorkspaceDetail: React.FC = () => {
|
||||
assertExists(currentWorkspace);
|
||||
assertExists(currentPageId);
|
||||
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
|
||||
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const { deletePin } = usePinboardHandler({
|
||||
blockSuiteWorkspace,
|
||||
metas: useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace),
|
||||
});
|
||||
|
||||
useSyncRecentViewsWithRouter(router, blockSuiteWorkspace);
|
||||
|
||||
useReferenceLinkEffect({
|
||||
pageLinkClicked: useCallback(
|
||||
({ pageId }: { pageId: string }) => {
|
||||
assertExists(currentWorkspace);
|
||||
return openPage(currentWorkspace.id, pageId);
|
||||
},
|
||||
[currentWorkspace, openPage]
|
||||
),
|
||||
subpageUnlinked: useCallback(
|
||||
({ pageId }: { pageId: string }) => {
|
||||
deletePin(pageId);
|
||||
},
|
||||
[deletePin]
|
||||
),
|
||||
subpageLinked: useCallback(
|
||||
({ pageId }: { pageId: string }) => {
|
||||
const meta = currentPageId && getPageMeta(currentPageId);
|
||||
if (!meta || meta.subpageIds?.includes(pageId)) {
|
||||
return;
|
||||
}
|
||||
setPageMeta(currentPageId, {
|
||||
subpageIds: [...(meta.subpageIds ?? []), pageId],
|
||||
});
|
||||
},
|
||||
[currentPageId, getPageMeta, setPageMeta]
|
||||
),
|
||||
});
|
||||
const onLoad = useCallback(
|
||||
(page: Page, editor: EditorContainer) => {
|
||||
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
|
||||
return openPage(blockSuiteWorkspace.id, pageId);
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
},
|
||||
[blockSuiteWorkspace.id, openPage]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
@@ -90,6 +64,7 @@ const WorkspaceDetail: React.FC = () => {
|
||||
<PageDetail
|
||||
currentWorkspace={currentWorkspace}
|
||||
currentPageId={currentPageId}
|
||||
onLoadEditor={onLoad}
|
||||
/>
|
||||
);
|
||||
} else if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
@@ -99,6 +74,7 @@ const WorkspaceDetail: React.FC = () => {
|
||||
<PageDetail
|
||||
currentWorkspace={currentWorkspace}
|
||||
currentPageId={currentPageId}
|
||||
onLoadEditor={onLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../../../adapters/workspace';
|
||||
import { PageLoading } from '../../../components/pure/loading';
|
||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useRouterHelper } from '../../../hooks/use-router-helper';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import { WorkspaceAdapters } from '../../../plugins';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
|
||||
const AllPage: NextPageWithLayout = () => {
|
||||
|
||||
@@ -16,13 +16,13 @@ import type { NextRouter } from 'next/router';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../../../adapters/workspace';
|
||||
import { PageLoading } from '../../../components/pure/loading';
|
||||
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
|
||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
||||
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
||||
import { useAppHelper } from '../../../hooks/use-workspaces';
|
||||
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
|
||||
import { WorkspaceAdapters } from '../../../plugins';
|
||||
import type { NextPageWithLayout } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { memo } from 'react';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
import { fetcher } from '../plugins/affine/fetcher';
|
||||
import { fetcher } from '../adapters/affine/fetcher';
|
||||
|
||||
const config: SWRConfiguration = {
|
||||
suspense: true,
|
||||
|
||||
@@ -4,18 +4,16 @@ import { memo, useRef } from 'react';
|
||||
|
||||
const themes = ['dark', 'light'];
|
||||
|
||||
// a workaround to sync theme to electron
|
||||
let firstRender = true;
|
||||
|
||||
const DesktopThemeSync = memo(function DesktopThemeSync() {
|
||||
const { theme } = useTheme();
|
||||
const lastThemeRef = useRef(theme);
|
||||
if (lastThemeRef.current !== theme || firstRender) {
|
||||
const onceRef = useRef(false);
|
||||
if (lastThemeRef.current !== theme || !onceRef.current) {
|
||||
if (environment.isDesktop && theme) {
|
||||
window.apis?.ui.handleThemeChange(theme as 'dark' | 'light' | 'system');
|
||||
}
|
||||
lastThemeRef.current = theme;
|
||||
firstRender = false;
|
||||
onceRef.current = true;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -6,9 +6,14 @@ import {
|
||||
} from '@affine/workspace/affine/api';
|
||||
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
|
||||
import type { LoginResponse } from '@affine/workspace/affine/login';
|
||||
import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login';
|
||||
import {
|
||||
createAffineAuth,
|
||||
parseIdToken,
|
||||
setLoginStorage,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import { rootStore } from '@affine/workspace/atom';
|
||||
|
||||
export const affineAuth = createAffineAuth(prefixUrl);
|
||||
const affineApis = {} as ReturnType<typeof createUserApis> &
|
||||
ReturnType<typeof createWorkspaceApis>;
|
||||
Object.assign(affineApis, createUserApis(prefixUrl));
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
import type { ToastOptions } from '@affine/component';
|
||||
import { toast as basicToast } from '@affine/component';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
|
||||
const logger = new DebugLogger('toast');
|
||||
|
||||
export const toast = (message: string, options?: ToastOptions) => {
|
||||
const mainContainer = document.querySelector(
|
||||
'.main-container'
|
||||
) as HTMLElement;
|
||||
logger.debug(`toast with message: "${message}"`, options);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine-toast:emit', { detail: message })
|
||||
);
|
||||
return basicToast(message, {
|
||||
portal: mainContainer || document.body,
|
||||
...options,
|
||||
|
||||
@@ -31,13 +31,13 @@ nvm use 18
|
||||
|
||||
## Setup Environment
|
||||
|
||||
This setup requires modern yarn (currently `3.5.0`), run this if your yarn version is `1.x`
|
||||
This setup requires modern yarn (currently `3.x`), run this if your yarn version is `1.x`
|
||||
|
||||
Reference: [Yarn installation doc](https://yarnpkg.com/getting-started/install)
|
||||
|
||||
```sh
|
||||
corepack enable
|
||||
corepack prepare yarn@3.5.0 --activate
|
||||
corepack prepare yarn@stable --activate
|
||||
```
|
||||
|
||||
```sh
|
||||
@@ -52,7 +52,7 @@ yarn install
|
||||
```shell
|
||||
# Run OctoBase container in background
|
||||
docker pull ghcr.io/toeverything/cloud-self-hosted:nightly-latest
|
||||
docker run --env=SIGN_KEY=test123 --env=RUST_LOG=debug --env=JWST_DEV=1 --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin --workdir=/app -p 3000:3000 --runtime=runc -d ghcr.io/toeverything/cloud-self-hosted:nightly-latest
|
||||
docker run --env=SIGN_KEY=test123 --env=RUST_LOG=debug --env=JWST_DEV=1 --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin --workdir=/app -p 127.0.0.1:3000:3000 --runtime=runc -d ghcr.io/toeverything/cloud-self-hosted:nightly-latest
|
||||
```
|
||||
|
||||
```shell
|
||||
|
||||
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "AFFiNE",
|
||||
"version": "0.5.4-beta.1",
|
||||
"version": "0.6.0-canary.7",
|
||||
"private": true,
|
||||
"author": "toeverything",
|
||||
"license": "MPL-2.0",
|
||||
@@ -13,12 +13,13 @@
|
||||
"scripts": {
|
||||
"dev": "dev-web",
|
||||
"dev:ac": "API_SERVER_PROFILE=ac yarn workspace @affine/web dev",
|
||||
"dev:local": "API_SERVER_PROFILE=local yarn workspace @affine/web dev",
|
||||
"dev:local": "PORT=8080 API_SERVER_PROFILE=local yarn workspace @affine/web dev",
|
||||
"dev:app": "yarn workspace @affine/electron dev:app",
|
||||
"build": "yarn workspace @affine/web build",
|
||||
"build:client": "yarn workspace @affine/client-app build:app",
|
||||
"build:storybook": "yarn workspace @affine/component build-storybook",
|
||||
"bump:nightly": "./scripts/bump-blocksuite.sh",
|
||||
"circular": "madge --circular --ts-config ./tsconfig.json ./apps/web/src/pages/**/*.tsx",
|
||||
"export": "yarn workspace @affine/web export",
|
||||
"start": "yarn workspace @affine/web start",
|
||||
"start:storybook": "yarn exec serve packages/component/storybook-static -l 6006",
|
||||
@@ -54,7 +55,7 @@
|
||||
"@taplo/cli": "^0.5.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/eslint": "^8.37.0",
|
||||
"@types/node": "^18.16.12",
|
||||
"@types/node": "^18.16.13",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.6",
|
||||
"@typescript-eslint/parser": "^5.59.6",
|
||||
"@vanilla-extract/vite-plugin": "^3.8.1",
|
||||
@@ -75,6 +76,7 @@
|
||||
"happy-dom": "^9.18.3",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"madge": "^6.0.0",
|
||||
"msw": "^1.2.1",
|
||||
"nanoid": "^4.0.2",
|
||||
"nyc": "^15.1.0",
|
||||
@@ -83,7 +85,7 @@
|
||||
"react-dom": "18.3.0-canary-16d053d59-20230506",
|
||||
"serve": "^14.2.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.7",
|
||||
"vite": "^4.3.8",
|
||||
"vite-plugin-istanbul": "^4.0.1",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.31.1",
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^16.0.3"
|
||||
},
|
||||
"version": "0.5.4-beta.1"
|
||||
"version": "0.6.0-canary.7"
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"@mui/material": "^5.13.1",
|
||||
"@popperjs/core": "^2.11.7",
|
||||
"@radix-ui/react-avatar": "^1.0.2",
|
||||
"@radix-ui/react-collapsible": "^1.0.2",
|
||||
"@toeverything/hooks": "workspace:*",
|
||||
"@toeverything/theme": "^0.5.8",
|
||||
"@vanilla-extract/dynamic": "^2.0.3",
|
||||
@@ -50,12 +51,12 @@
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/blocks": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/icons": "^2.1.16",
|
||||
"@blocksuite/lit": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230518051344-45970a96-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@blocksuite/global": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@blocksuite/icons": "^2.1.18",
|
||||
"@blocksuite/lit": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230519102837-01acd96b-nightly",
|
||||
"@storybook/addon-actions": "^7.0.12",
|
||||
"@storybook/addon-coverage": "^0.0.8",
|
||||
"@storybook/addon-essentials": "^7.0.12",
|
||||
@@ -80,9 +81,9 @@
|
||||
"storybook": "^7.0.12",
|
||||
"storybook-dark-mode": "^3.0.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.7",
|
||||
"vite": "^4.3.8",
|
||||
"wait-on": "^7.0.1",
|
||||
"yjs": "^13.6.1"
|
||||
},
|
||||
"version": "0.5.4-beta.1"
|
||||
"version": "0.6.0-canary.7"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { AffineLogoSimSBlue1_1Icon, CloseIcon } from '@blocksuite/icons';
|
||||
|
||||
import {
|
||||
@@ -18,16 +19,18 @@ export const DownloadTips = ({ onClose }: { onClose: () => void }) => {
|
||||
<div className={downloadTipStyle}>
|
||||
<AffineLogoSimSBlue1_1Icon className={downloadTipIconStyle} />
|
||||
<div className={downloadMessageStyle}>
|
||||
Enjoying the demo?
|
||||
<a
|
||||
className={linkStyle}
|
||||
href="https://github.com/toeverything/AFFiNE/releases"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Download the AFFiNE Client
|
||||
</a>
|
||||
for the full experience.
|
||||
<Trans i18nKey="com.affine.banner.content">
|
||||
Enjoying the demo?
|
||||
<a
|
||||
className={linkStyle}
|
||||
href="https://affine.pro/download"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Download the AFFiNE Client
|
||||
</a>
|
||||
for the full experience.
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -12,7 +12,6 @@ interface AddPageButtonProps {
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
// Although it is called an input, it is actually a button.
|
||||
export function AddPageButton({
|
||||
onClick,
|
||||
className,
|
||||
|
||||
@@ -62,11 +62,17 @@ export const closeIcon = style({
|
||||
cursor: 'pointer',
|
||||
transition: '0.1s',
|
||||
borderRadius: '50%',
|
||||
transform: 'scale(0.6)',
|
||||
zIndex: 1,
|
||||
opacity: 0,
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)',
|
||||
},
|
||||
[`${root}:hover &`]: {
|
||||
opacity: 1,
|
||||
transform: 'scale(1)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -153,28 +159,32 @@ export const particles = style({
|
||||
backgroundRepeat: 'no-repeat, repeat',
|
||||
backgroundPosition: 'center, center top 100%',
|
||||
backgroundSize: '100%, 130%',
|
||||
WebkitMaskImage:
|
||||
'linear-gradient(to top, transparent, black, black, transparent)',
|
||||
maskImage: 'linear-gradient(to top, transparent, black, black, transparent)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
export const particlesBefore = style({
|
||||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
|
||||
backgroundRepeat: 'no-repeat, repeat, repeat',
|
||||
backgroundPosition: 'center, center top 100%, center center',
|
||||
backgroundSize: '100% 120%, 150%, 120%',
|
||||
filter: 'blur(1px)',
|
||||
willChange: 'filter',
|
||||
pointerEvents: 'none',
|
||||
display: 'none',
|
||||
selectors: {
|
||||
[`${root}:hover &`]: {
|
||||
display: 'block',
|
||||
},
|
||||
'&:before': {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
|
||||
backgroundRepeat: 'no-repeat, repeat, repeat',
|
||||
backgroundPosition: 'center, center top 100%, center center',
|
||||
backgroundSize: '100% 120%, 150%, 120%',
|
||||
filter: 'blur(1px)',
|
||||
willChange: 'filter',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const halo = style({
|
||||
|
||||
@@ -85,6 +85,7 @@ export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
|
||||
}
|
||||
} else if (currentChangelogUnread) {
|
||||
window.open(config.changelogUrl, '_blank');
|
||||
onDismissCurrentChangelog();
|
||||
} else {
|
||||
throw new Unreachable();
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Skeleton } from '@mui/material';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
||||
import { AppSidebar } from './index';
|
||||
|
||||
export const AppSidebarFallback = (): ReactElement | null => {
|
||||
return (
|
||||
<AppSidebar>
|
||||
<div className={fallbackStyle}>
|
||||
<div className={fallbackHeaderStyle}>
|
||||
<Skeleton variant="circular" width={40} height={40} />
|
||||
<Skeleton variant="rectangular" width={150} height={40} />
|
||||
</div>
|
||||
</div>
|
||||
</AppSidebar>
|
||||
);
|
||||
};
|
||||
@@ -14,6 +14,7 @@ export const navWrapperStyle = style({
|
||||
minWidth: navWidthVar,
|
||||
height: '100%',
|
||||
zIndex: 2,
|
||||
paddingBottom: '8px',
|
||||
backgroundColor: 'transparent',
|
||||
'@media': {
|
||||
[`(max-width: ${floatingMaxWidth}px)`]: {
|
||||
@@ -26,6 +27,10 @@ export const navWrapperStyle = style({
|
||||
},
|
||||
},
|
||||
},
|
||||
print: {
|
||||
display: 'none',
|
||||
zIndex: -1,
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
'&[data-open="false"]': {
|
||||
@@ -97,5 +102,8 @@ export const sidebarFloatMaskStyle = style({
|
||||
},
|
||||
},
|
||||
},
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import { Skeleton } from '@mui/material';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
||||
import {
|
||||
floatingMaxWidth,
|
||||
navBodyStyle,
|
||||
@@ -114,10 +116,22 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
|
||||
);
|
||||
}
|
||||
|
||||
export const AppSidebarFallback = (): ReactElement | null => {
|
||||
return (
|
||||
<AppSidebar>
|
||||
<div className={fallbackStyle}>
|
||||
<div className={fallbackHeaderStyle}>
|
||||
<Skeleton variant="circular" width={40} height={40} />
|
||||
<Skeleton variant="rectangular" width={150} height={40} />
|
||||
</div>
|
||||
</div>
|
||||
</AppSidebar>
|
||||
);
|
||||
};
|
||||
|
||||
export * from './add-page-button';
|
||||
export * from './app-updater-button';
|
||||
export * from './category-divider';
|
||||
export { AppSidebarFallback } from './fallback';
|
||||
export * from './menu-item';
|
||||
export * from './quick-search-input';
|
||||
export * from './sidebar-containers';
|
||||
|
||||
@@ -69,6 +69,7 @@ export const iconsContainer = style({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
width: '28px',
|
||||
flexShrink: 0,
|
||||
selectors: {
|
||||
'&[data-collapsible="true"]': {
|
||||
width: '40px',
|
||||
|
||||
@@ -39,28 +39,29 @@ export function MenuItem({
|
||||
data-disabled={disabled}
|
||||
data-collapsible={collapsible}
|
||||
>
|
||||
<div className={styles.iconsContainer} data-collapsible={collapsible}>
|
||||
{collapsible && (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // for links
|
||||
onCollapsedChange?.(!collapsed);
|
||||
}}
|
||||
data-testid="fav-collapsed-button"
|
||||
className={styles.collapsedIconContainer}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={collapsed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{icon &&
|
||||
React.cloneElement(icon, {
|
||||
{icon && (
|
||||
<div className={styles.iconsContainer} data-collapsible={collapsible}>
|
||||
{collapsible && (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // for links
|
||||
onCollapsedChange?.(!collapsed);
|
||||
}}
|
||||
data-testid="fav-collapsed-button"
|
||||
className={styles.collapsedIconContainer}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={collapsed}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{React.cloneElement(icon, {
|
||||
className: clsx([styles.icon, icon.props.className]),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const baseContainer = style({
|
||||
padding: '12px 16px',
|
||||
padding: '4px 16px',
|
||||
display: 'flex',
|
||||
flexFlow: 'column nowrap',
|
||||
rowGap: '4px',
|
||||
@@ -13,17 +13,27 @@ export const scrollableContainerRoot = style({
|
||||
vars: {
|
||||
'--scrollbar-width': '10px',
|
||||
},
|
||||
transition: 'all .3s .2s',
|
||||
borderTop: '1px solid transparent',
|
||||
});
|
||||
|
||||
export const scrollTopBorder = style({
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '16px',
|
||||
right: '16px',
|
||||
height: '1px',
|
||||
transition: 'opacity .3s .2s',
|
||||
opacity: 0,
|
||||
background: 'var(--affine-black-10)',
|
||||
selectors: {
|
||||
'&[data-has-scroll-top="true"]': {
|
||||
boxShadow: 'inset 0 8px 8px -8px var(--affine-black-10)',
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const scrollableViewport = style({
|
||||
height: '100%',
|
||||
marginTop: '4px',
|
||||
});
|
||||
|
||||
globalStyle(`${scrollableViewport} > div`, {
|
||||
|
||||
@@ -39,10 +39,11 @@ function useHasScrollTop() {
|
||||
export function SidebarScrollableContainer({ children }: PropsWithChildren) {
|
||||
const [hasScrollTop, ref] = useHasScrollTop();
|
||||
return (
|
||||
<ScrollArea.Root
|
||||
data-has-scroll-top={hasScrollTop}
|
||||
className={styles.scrollableContainerRoot}
|
||||
>
|
||||
<ScrollArea.Root className={styles.scrollableContainerRoot}>
|
||||
<div
|
||||
data-has-scroll-top={hasScrollTop}
|
||||
className={styles.scrollTopBorder}
|
||||
/>
|
||||
<ScrollArea.Viewport
|
||||
className={clsx([styles.scrollableViewport])}
|
||||
ref={ref}
|
||||
|
||||
@@ -22,15 +22,11 @@ export type SidebarHeaderProps = {
|
||||
export const SidebarHeader = (props: SidebarHeaderProps) => {
|
||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||
const environment = getEnvironment();
|
||||
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
|
||||
return (
|
||||
<div
|
||||
className={navHeaderStyle}
|
||||
data-is-macos-electron={isMacosDesktop}
|
||||
data-open={open}
|
||||
>
|
||||
{isMacosDesktop && (
|
||||
<div className={navHeaderStyle} data-open={open}>
|
||||
{environment.isDesktop && (
|
||||
<>
|
||||
{environment.isMacOs && <div style={{ flex: 1 }} />}
|
||||
<IconButton
|
||||
size="middle"
|
||||
data-testid="app-sidebar-arrow-button-back"
|
||||
@@ -57,6 +53,8 @@ export const SidebarHeader = (props: SidebarHeaderProps) => {
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</IconButton>
|
||||
|
||||
{!environment.isMacOs && <div style={{ flex: 1 }} />}
|
||||
</>
|
||||
)}
|
||||
<IconButton
|
||||
|
||||
@@ -16,7 +16,7 @@ export const spotlight = style({
|
||||
background: `radial-gradient(${spotlightSize} circle at ${spotlightX} ${spotlightY}, var(--affine-text-primary-color), transparent)`,
|
||||
inset: '0px',
|
||||
pointerEvents: 'none',
|
||||
willChange: 'background',
|
||||
willChange: 'background, opacity',
|
||||
opacity: spotlightOpacity,
|
||||
zIndex: 1,
|
||||
transition: 'all 0.2s',
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { type PropsWithChildren } from 'react';
|
||||
|
||||
import { Spotlight } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Components/AppSidebar/Spotlight',
|
||||
component: Spotlight,
|
||||
} satisfies Meta;
|
||||
|
||||
const Container = ({ children }: PropsWithChildren) => (
|
||||
<main
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '320px',
|
||||
height: '320px',
|
||||
border: '1px solid #ccc',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Spotlight />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -12,10 +12,10 @@ function useMouseOffset() {
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.parentElement) {
|
||||
const el = ref.current.parentElement;
|
||||
const bound = el.getBoundingClientRect();
|
||||
|
||||
// debounce?
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
const bound = el.getBoundingClientRect();
|
||||
setOffset({ x: e.clientX - bound.x, y: e.clientY - bound.y });
|
||||
setOutside(false);
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ export type EditorProps = {
|
||||
page: Page;
|
||||
mode: 'page' | 'edgeless';
|
||||
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
|
||||
onLoad?: (page: Page, editor: EditorContainer) => void;
|
||||
onLoad?: (page: Page, editor: EditorContainer) => () => void;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
|
||||
if (page.root === null) {
|
||||
props.onInit(page, editor);
|
||||
}
|
||||
props.onLoad?.(page, editor);
|
||||
return props.onLoad?.(page, editor);
|
||||
}
|
||||
}, [props.page, props.onInit, props.onLoad, editor, props, page]);
|
||||
|
||||
|
||||
25
packages/component/src/components/card/block-card/index.tsx
Normal file
25
packages/component/src/components/card/block-card/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const BlockCard = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
left?: ReactNode;
|
||||
title: string;
|
||||
desc?: string;
|
||||
right?: ReactNode;
|
||||
} & HTMLAttributes<HTMLDivElement>
|
||||
>(({ left, title, desc, right, ...props }, ref) => {
|
||||
return (
|
||||
<div ref={ref} className={styles.blockCard} {...props}>
|
||||
{left && <div className={styles.blockCardAround}>{left}</div>}
|
||||
<div className={styles.blockCardContent}>
|
||||
<div>{title}</div>
|
||||
<div className={styles.blockCardDesc}>{desc}</div>
|
||||
</div>
|
||||
{right && <div className={styles.blockCardAround}>{right}</div>}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
BlockCard.displayName = 'BlockCard';
|
||||
@@ -0,0 +1,36 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const blockCard = style({
|
||||
display: 'flex',
|
||||
gap: '12px',
|
||||
padding: '8px 12px',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
backgroundColor: 'var(--affine-background-primary-color)',
|
||||
borderRadius: '4px',
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'start',
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
boxShadow: 'var(--affine-shadow-1)',
|
||||
},
|
||||
// TODO active styles
|
||||
},
|
||||
});
|
||||
|
||||
export const blockCardAround = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const blockCardContent = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export const blockCardDesc = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-
|
||||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAvatar } from '../workspace-avatar';
|
||||
import { WorkspaceAvatar } from '../../workspace-avatar';
|
||||
import {
|
||||
StyledCard,
|
||||
StyledSettingLink,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user