diff --git a/.eslintignore b/.eslintignore index ea887a3d50..52d14c3637 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ node_modules dist .next +out diff --git a/.eslintrc.js b/.eslintrc.js index 630cee3d94..0b9f30dfeb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { 'no-empty': 'off', 'no-func-assign': 'off', 'no-cond-assign': 'off', + 'react/prop-types': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-empty-function': 'off', diff --git a/.github/deployment/Dockerfile b/.github/deployment/Dockerfile index 59639d6c49..7b7a868b90 100644 --- a/.github/deployment/Dockerfile +++ b/.github/deployment/Dockerfile @@ -10,4 +10,4 @@ COPY --from=relocate /app . EXPOSE 80 ENV API_SERVER=$API_SERVER -CMD ["caddy", "run"] \ No newline at end of file +CMD ["caddy", "run"] diff --git a/.gitignore b/.gitignore index 7b54b96e70..655db92dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -58,7 +58,8 @@ module-resolve.cjs # Cache .eslintcache +next-env.d.ts # generated assets apps/desktop/public/affine-out -apps/desktop/public/preload \ No newline at end of file +apps/desktop/public/preload diff --git a/apps/web/.env.local.template b/apps/web/.env.local.template index fadb3c8bbd..0371eb4412 100644 --- a/apps/web/.env.local.template +++ b/apps/web/.env.local.template @@ -9,3 +9,6 @@ NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID= LOCAL_BLOCK_SUITE= # see next.config.js NODE_API_SERVER= +# save workspace to idb +ENABLE_IDB_PROVIDER=1 +PREFETCH_WORKSPACE=1 diff --git a/apps/web/CHANGELOG.md b/apps/web/CHANGELOG.md deleted file mode 100644 index 236fe7333f..0000000000 --- a/apps/web/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @affine/app - -## 1.0.0 - -### Major Changes - -- cc72448: add changeset config diff --git a/apps/web/README.md b/apps/web/README.md deleted file mode 100644 index c87e0421d2..0000000000 --- a/apps/web/README.md +++ /dev/null @@ -1,34 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 643717e814..cd3411f244 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,17 +1,14 @@ -import { getGitVersion, getCommitHash } from './scripts/gitInfo.mjs'; -import fs from 'node:fs'; import path from 'node:path'; -import { printer } from './scripts/printer.mjs'; import debugLocal from 'next-debug-local'; +import preset from './preset.config.mjs'; +import { createRequire } from 'node:module'; +import { getCommitHash, getGitVersion } from './scripts/gitInfo.mjs'; -const dependencies = JSON.parse(fs.readFileSync('./package.json', 'utf8'))[ - 'dependencies' -]; +const require = createRequire(import.meta.url); + +console.info('Runtime Preset', preset); const enableDebugLocal = path.isAbsolute(process.env.LOCAL_BLOCK_SUITE ?? ''); -const EDITOR_VERSION = enableDebugLocal - ? 'local-version' - : dependencies['@blocksuite/editor']; const profileTarget = { ac: '100.85.73.88:12001', @@ -20,6 +17,7 @@ const profileTarget = { stage: '', pro: 'http://pathfinder.affine.pro', local: '127.0.0.1:3000', + rem: 'stage.affine.pro', }; const getRedirectConfig = profile => { @@ -41,23 +39,39 @@ const getRedirectConfig = profile => { /** @type {import('next').NextConfig} */ const nextConfig = { productionBrowserSourceMaps: true, - reactStrictMode: true, - swcMinify: false, - publicRuntimeConfig: { - NODE_ENV: process.env.NODE_ENV, - PROJECT_NAME: process.env.npm_package_name, - BUILD_DATE: new Date().toISOString(), - CI: process.env.CI || null, - VERSION: getGitVersion(), - COMMIT_HASH: getCommitHash(), - EDITOR_VERSION, + compiler: { + removeConsole: { + exclude: ['error', 'log', 'warn', 'info'], + }, + emotion: { + sourceMap: true, + }, }, + images: { + unoptimized: true, + }, + experimental: { + swcPlugins: [ + ['@swc-jotai/debug-label', {}], + // ['@swc-jotai/react-refresh', {}], + ], + }, + reactStrictMode: true, transpilePackages: [ '@affine/component', - '@affine/i18n', '@affine/datacenter', - '@toeverything/pathfinder-logger', + '@affine/i18n', ], + publicRuntimeConfig: { + PROJECT_NAME: process.env.npm_package_name, + BUILD_DATE: new Date().toISOString(), + gitVersion: getGitVersion(), + hash: getCommitHash(), + serverAPI: + profileTarget[process.env.NODE_API_SERVER || 'dev'] ?? profileTarget.dev, + editorVersion: require('./package.json').dependencies['@blocksuite/editor'], + ...preset, + }, webpack: config => { config.experiments = { ...config.experiments, topLevelAwait: true }; config.module.rules.push({ @@ -67,20 +81,14 @@ const nextConfig = { return config; }, - images: { - unoptimized: true, - }, rewrites: async () => { const [profile, target, desc] = getRedirectConfig( process.env.NODE_API_SERVER ); - printer.info(`API request proxy to [${desc} Server]: ` + target); + console.info(`API request proxy to [${desc} Server]: ` + target); return profile; }, basePath: process.env.NEXT_BASE_PATH, - experimental: { - forceSwcTransforms: true, - }, }; const baseDir = process.env.LOCAL_BLOCK_SUITE ?? '/'; @@ -111,9 +119,9 @@ const withDebugLocal = debugLocal( const detectFirebaseConfig = () => { if (!process.env.NEXT_PUBLIC_FIREBASE_API_KEY) { - printer.warn('NEXT_PUBLIC_FIREBASE_API_KEY not found, please check it'); + console.warn('NEXT_PUBLIC_FIREBASE_API_KEY not found, please check it'); } else { - printer.info('NEXT_PUBLIC_FIREBASE_API_KEY found'); + console.info('NEXT_PUBLIC_FIREBASE_API_KEY found'); } }; detectFirebaseConfig(); diff --git a/apps/web/package.json b/apps/web/package.json index ccce01408d..8d616de485 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@affine/app", - "version": "0.3.1", + "private": true, "scripts": { "dev": "next dev -p 8080", "build": "next build", @@ -11,47 +11,44 @@ "dependencies": { "@affine/component": "workspace:*", "@affine/datacenter": "workspace:*", - "@affine/debug": "workspace:*", "@affine/i18n": "workspace:*", "@blocksuite/blocks": "0.4.1-20230220214107-0a354de", "@blocksuite/editor": "0.4.1-20230220214107-0a354de", - "@blocksuite/global": "0.4.1-20230220214107-0a354de", - "@blocksuite/icons": "2.0.17", + "@blocksuite/icons": "^2.0.17", + "@blocksuite/react": "0.4.1-20230220214107-0a354de", "@blocksuite/store": "0.4.1-20230220214107-0a354de", + "@emotion/cache": "^11.10.5", "@emotion/css": "^11.10.6", "@emotion/react": "^11.10.6", - "@emotion/server": "^11.10.0", - "@emotion/styled": "^11.10.6", - "@fontsource/poppins": "^4.5.10", - "@fontsource/space-mono": "^4.5.12", - "@mui/base": "5.0.0-alpha.118", - "@mui/icons-material": "^5.11.9", - "@mui/material": "^5.11.9", - "@toeverything/pathfinder-logger": "workspace:@affine/logger@*", + "@mui/material": "^5.11.10", "cmdk": "^0.1.22", "css-spring": "^4.1.0", "dayjs": "^1.11.7", + "jotai": "^2.0.2", + "jotai-devtools": "^0.2.0", "lit": "^2.6.1", - "next": "13.1.0", - "next-debug-local": "^0.1.5", - "prettier": "^2.8.4", - "quill": "^1.3.7", - "quill-cursors": "^4.0.0", - "react": "18.2.0", - "react-dom": "18.2.0", - "yjs": "^13.5.46", - "zustand": "^4.3.3" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-helmet-async": "^1.3.0", + "swr": "^2.0.4", + "y-indexeddb": "^9.0.9", + "yjs": "^13.5.47", + "zod": "^3.20.6" }, "devDependencies": { - "@types/node": "18.14.0", - "@types/react": "18.0.28", - "@types/react-dom": "18.0.11", - "@types/wicg-file-system-access": "^2020.9.5", - "chalk": "^5.2.0", - "eslint": "^8.34.0", - "eslint-config-next": "13.1.6", + "@redux-devtools/extension": "^3.2.5", + "@swc-jotai/debug-label": "^0.0.6", + "@swc-jotai/react-refresh": "^0.0.4", + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "@types/webpack-env": "^1.18.0", + "dotenv": "^16.0.3", + "eslint-config-next": "^13.2.2", + "next": "^13.2.2", + "next-debug-local": "^0.1.5", + "next-router-mock": "^0.9.2", "raw-loader": "^4.0.2", - "typescript": "^4.9.5", - "webpack": "^5.75.0" + "redux": "^4.2.1", + "typescript": "^4.9.5" } } diff --git a/apps/web/preset.config.mjs b/apps/web/preset.config.mjs new file mode 100644 index 0000000000..baac73bbc2 --- /dev/null +++ b/apps/web/preset.config.mjs @@ -0,0 +1,6 @@ +import 'dotenv/config'; + +export default { + enableIndexedDBProvider: Boolean(process.env.ENABLE_IDB_PROVIDER ?? '1'), + prefetchWorkspace: Boolean(process.env.PREFETCH_WORKSPACE ?? '1'), +}; diff --git a/apps/web/scripts/__tests__/printer.spec.ts b/apps/web/scripts/__tests__/printer.spec.ts deleted file mode 100644 index 4a7716d4c5..0000000000 --- a/apps/web/scripts/__tests__/printer.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import { printer } from './../printer'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const chalk = require('chalk'); -test.describe('printer', () => { - test('test debug', () => { - expect(printer.debug('test debug')).toBe( - chalk.green`debug` + chalk.white(' - test debug') - ); - }); - - test('test info', () => { - expect(printer.info('test info')).toBe( - chalk.rgb(19, 167, 205)`info` + chalk.white(' - test info') - ); - }); - test('test warn', () => { - expect(printer.warn('test warn')).toBe( - chalk.yellow`warn` + chalk.white(' - test warn') - ); - }); -}); diff --git a/apps/web/scripts/printer.mjs b/apps/web/scripts/printer.mjs deleted file mode 100644 index b180187ad3..0000000000 --- a/apps/web/scripts/printer.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import chalk from 'chalk'; -export const printer = { - debug: msg => { - const result = chalk.green`debug` + chalk.white(' - ' + msg); - console.log(result); - return result; - }, - info: msg => { - const result = chalk.rgb(19, 167, 205)`info` + chalk.white(' - ' + msg); - console.log(result); - return result; - }, - warn: msg => { - const result = chalk.yellow`warn` + chalk.white(' - ' + msg); - console.log(result); - return result; - }, -}; diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts new file mode 100644 index 0000000000..d62ec0baf0 --- /dev/null +++ b/apps/web/src/atoms/index.ts @@ -0,0 +1,36 @@ +import { atom } from 'jotai'; +import { createStore } from 'jotai/index'; +import { atomWithStorage } from 'jotai/utils'; +import { unstable_batchedUpdates } from 'react-dom'; + +// workspace necessary atoms +export const currentWorkspaceIdAtom = atomWithStorage( + 'affine-current-workspace-id', + null +); +export const currentPageIdAtom = atomWithStorage( + 'affine-current-page-id', + null +); +// If the workspace is locked, it means that the user maybe updating the workspace +// from local to remote or vice versa +export const workspaceLockAtom = atom(false); +export async function lockMutex(fn: () => Promise) { + if (jotaiStore.get(workspaceLockAtom)) { + throw new Error('Workspace is locked'); + } + unstable_batchedUpdates(() => { + jotaiStore.set(workspaceLockAtom, true); + }); + await fn(); + unstable_batchedUpdates(() => { + jotaiStore.set(workspaceLockAtom, false); + }); +} + +// modal atoms +export const openWorkspacesModalAtom = atom(false); +export const openCreateWorkspaceModalAtom = atom(false); +export const openQuickSearchModalAtom = atom(false); + +export const jotaiStore = createStore(); diff --git a/apps/web/src/atoms/public-workspace/index.ts b/apps/web/src/atoms/public-workspace/index.ts new file mode 100644 index 0000000000..bf13b09882 --- /dev/null +++ b/apps/web/src/atoms/public-workspace/index.ts @@ -0,0 +1,48 @@ +import { atom } from 'jotai/index'; + +import { + BlockSuiteWorkspace, + LocalWorkspace, + RemWorkspaceFlavour, +} from '../../shared'; +import { apis } from '../../shared/apis'; +import { createEmptyBlockSuiteWorkspace } from '../../utils'; + +export const publicWorkspaceIdAtom = atom(null); +export const publicBlockSuiteAtom = atom>( + async get => { + const workspaceId = get(publicWorkspaceIdAtom); + if (!workspaceId) { + throw new Error('No workspace id'); + } + const binary = await apis.downloadWorkspace(workspaceId, true); + const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(workspaceId); + BlockSuiteWorkspace.Y.applyUpdate( + blockSuiteWorkspace.doc, + new Uint8Array(binary) + ); + blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', false); + blockSuiteWorkspace.awarenessStore.setFlag('enable_set_remote_flag', false); + blockSuiteWorkspace.awarenessStore.setFlag('enable_database', false); + blockSuiteWorkspace.awarenessStore.setFlag( + 'enable_edgeless_toolbar', + false + ); + blockSuiteWorkspace.awarenessStore.setFlag('enable_slash_menu', false); + blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', false); + return new Promise(resolve => { + setTimeout(() => { + const workspace: LocalWorkspace = { + id: workspaceId, + blockSuiteWorkspace, + flavour: RemWorkspaceFlavour.LOCAL, + syncBinary: () => Promise.resolve(workspace), + providers: [], + }; + dataCenter.workspaces.push(workspace); + dataCenter.callbacks.forEach(cb => cb()); + resolve(blockSuiteWorkspace); + }, 0); + }); + } +); diff --git a/apps/web/src/blocksuite/__tests__/index.spec.ts b/apps/web/src/blocksuite/__tests__/index.spec.ts new file mode 100644 index 0000000000..3c22366dbd --- /dev/null +++ b/apps/web/src/blocksuite/__tests__/index.spec.ts @@ -0,0 +1,32 @@ +/** + * @vitest-environment happy-dom + */ +import 'fake-indexeddb/auto'; + +import { beforeEach, describe, expect, test } from 'vitest'; + +import { BlockSuiteWorkspace } from '../../shared'; +import { createAffineProviders, createLocalProviders } from '..'; + +let blockSuiteWorkspace: BlockSuiteWorkspace; + +beforeEach(() => { + blockSuiteWorkspace = new BlockSuiteWorkspace({ + room: 'test', + }); +}); + +describe('blocksuite providers', () => { + test('should be valid provider', () => { + [createLocalProviders, createAffineProviders].forEach(createProviders => { + createProviders(blockSuiteWorkspace).forEach(provider => { + expect(provider).toBeTypeOf('object'); + expect(provider).toHaveProperty('flavour'); + expect(provider).toHaveProperty('connect'); + expect(provider.connect).toBeTypeOf('function'); + expect(provider).toHaveProperty('disconnect'); + expect(provider.disconnect).toBeTypeOf('function'); + }); + }); + }); +}); diff --git a/apps/web/src/blocksuite/index.ts b/apps/web/src/blocksuite/index.ts new file mode 100644 index 0000000000..5d3c60019b --- /dev/null +++ b/apps/web/src/blocksuite/index.ts @@ -0,0 +1,26 @@ +import { BlockSuiteWorkspace, Provider } from '../shared'; +import { config } from '../shared/env'; +import { createIndexedDBProvider, createWebSocketProvider } from './providers'; + +export const createAffineProviders = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): Provider[] => { + return ( + [ + createWebSocketProvider(blockSuiteWorkspace), + config.enableIndexedDBProvider && + createIndexedDBProvider(blockSuiteWorkspace), + ] as any[] + ).filter(v => Boolean(v)); +}; + +export const createLocalProviders = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): Provider[] => { + return ( + [ + config.enableIndexedDBProvider && + createIndexedDBProvider(blockSuiteWorkspace), + ] as any[] + ).filter(v => Boolean(v)); +}; diff --git a/apps/web/src/blocksuite/providers/index.ts b/apps/web/src/blocksuite/providers/index.ts new file mode 100644 index 0000000000..b8df35b25e --- /dev/null +++ b/apps/web/src/blocksuite/providers/index.ts @@ -0,0 +1,71 @@ +import { WebsocketProvider } from '@affine/datacenter'; +import { assertExists } from '@blocksuite/store'; +import { IndexeddbPersistence } from 'y-indexeddb'; + +import { + AffineWebSocketProvider, + BlockSuiteWorkspace, + LocalIndexedDBProvider, +} from '../../shared'; +import { apis } from '../../shared/apis'; + +export const createWebSocketProvider = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): AffineWebSocketProvider => { + let webSocketProvider: WebsocketProvider | null = null; + return { + flavour: 'affine-websocket', + cleanup: () => { + assertExists(webSocketProvider); + webSocketProvider?.destroy(); + }, + connect: () => { + const wsUrl = `${ + window.location.protocol === 'https:' ? 'wss' : 'ws' + }://${window.location.host}/api/sync/`; + webSocketProvider = new WebsocketProvider( + wsUrl, + blockSuiteWorkspace.room as string, + blockSuiteWorkspace.doc, + { + params: { token: apis.auth.refresh }, + // @ts-expect-error ignore the type + awareness: blockSuiteWorkspace.awarenessStore.awareness, + } + ); + console.log('connect', webSocketProvider.roomname); + webSocketProvider.connect(); + }, + disconnect: () => { + assertExists(webSocketProvider); + console.log('disconnect', webSocketProvider.roomname); + webSocketProvider?.disconnect(); + }, + }; +}; + +export const createIndexedDBProvider = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): LocalIndexedDBProvider => { + let indexdbProvider: IndexeddbPersistence | null = null; + return { + flavour: 'local-indexeddb', + cleanup: () => { + assertExists(indexdbProvider); + indexdbProvider.clearData(); + }, + connect: () => { + console.info('connect indexeddb provider', blockSuiteWorkspace.room); + indexdbProvider = new IndexeddbPersistence( + blockSuiteWorkspace.room as string, + blockSuiteWorkspace.doc + ); + }, + disconnect: () => { + assertExists(indexdbProvider); + console.info('disconnect indexeddb provider', blockSuiteWorkspace.room); + indexdbProvider.destroy(); + indexdbProvider = null; + }, + }; +}; diff --git a/apps/web/src/components/404/index.tsx b/apps/web/src/components/404/index.tsx deleted file mode 100644 index c4b0aa31e4..0000000000 --- a/apps/web/src/components/404/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Button } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import Image from 'next/image'; -import { useRouter } from 'next/router'; - -import ErrorImg from '../../../public/imgs/invite-error.svg'; -import { StyledContainer } from './styles'; - -export const NotfoundPage = () => { - const { t } = useTranslation(); - const router = useRouter(); - return ( - - 404 - -

{t('404 - Page Not Found')}

- -
- ); -}; - -export default NotfoundPage; diff --git a/apps/web/src/components/404/styles.ts b/apps/web/src/components/404/styles.ts deleted file mode 100644 index c69fe45e74..0000000000 --- a/apps/web/src/components/404/styles.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { displayFlex, styled } from '@affine/component'; - -export const StyledContainer = styled.div(() => { - return { - ...displayFlex('center', 'center'), - flexDirection: 'column', - height: '100vh', - - img: { - width: '360px', - height: '270px', - }, - p: { - fontSize: '22px', - fontWeight: 600, - margin: '24px 0', - }, - }; -}); diff --git a/apps/web/src/components/__tests__/ProviderComposer.spec.tsx b/apps/web/src/components/__tests__/ProviderComposer.spec.tsx new file mode 100644 index 0000000000..9fc3942185 --- /dev/null +++ b/apps/web/src/components/__tests__/ProviderComposer.spec.tsx @@ -0,0 +1,30 @@ +/** + * @vitest-environment happy-dom + */ +import { render } from '@testing-library/react'; +import React, { createContext, useContext } from 'react'; +import { expect, test } from 'vitest'; + +import { ProviderComposer } from '../provider-composer'; + +test('ProviderComposer', async () => { + const Context = createContext('null'); + const Provider: React.FC = ({ children }) => { + return {children}; + }; + const ConsumerComponent = () => { + const value = useContext(Context); + return <>{value}; + }; + const Component = () => { + return ( + ]}> + + + ); + }; + + const result = render(); + await result.findByText('test1'); + expect(result.asFragment()).toMatchSnapshot(); +}); diff --git a/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx b/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx new file mode 100644 index 0000000000..a6d3127a21 --- /dev/null +++ b/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx @@ -0,0 +1,89 @@ +/** + * @vitest-environment happy-dom + */ +import 'fake-indexeddb/auto'; + +import { assertExists } from '@blocksuite/store'; +import { render, renderHook } from '@testing-library/react'; +import { useRouter } from 'next/router'; +import { useCallback, useState } from 'react'; +import { describe, expect, test, vi } from 'vitest'; +import createFetchMock from 'vitest-fetch-mock'; + +import { useCurrentPageId } from '../../hooks/current/use-current-page-id'; +import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace'; +import { useBlockSuiteWorkspaceHelper } from '../../hooks/use-blocksuite-workspace-helper'; +import { useWorkspacesHelper } from '../../hooks/use-workspaces'; +import { ThemeProvider } from '../../providers/ThemeProvider'; +import { pathGenerator } from '../../shared'; +import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar'; + +const fetchMocker = createFetchMock(vi); + +// fetchMocker.enableMocks(); + +describe('WorkSpaceSliderBar', () => { + test('basic', async () => { + // fetchMocker.mock + + const onOpenWorkspaceListModalFn = vi.fn(); + const onOpenQuickSearchModalFn = vi.fn(); + const mutationHook = renderHook(() => useWorkspacesHelper()); + const id = mutationHook.result.current.createRemLocalWorkspace('test0'); + mutationHook.result.current.createWorkspacePage(id, 'test1'); + const currentWorkspaceHook = renderHook(() => useCurrentWorkspace()); + let i = 0; + const Component = () => { + const [show, setShow] = useState(false); + const [currentWorkspace] = useCurrentWorkspace(); + const [currentPageId] = useCurrentPageId(); + assertExists(currentWorkspace); + const helper = useBlockSuiteWorkspaceHelper( + currentWorkspace.blockSuiteWorkspace + ); + return ( + {}, [])} + createPage={() => { + i++; + return helper.createPage('page-test-' + i); + }} + show={show} + setShow={setShow} + currentPath={useRouter().asPath} + paths={pathGenerator} + isPublicWorkspace={false} + /> + ); + }; + const App = () => { + return ( + + + + ); + }; + currentWorkspaceHook.result.current[1](id); + const app = render(); + const card = await app.findByTestId('current-workspace'); + expect(onOpenWorkspaceListModalFn).toBeCalledTimes(0); + card.click(); + expect(onOpenWorkspaceListModalFn).toBeCalledTimes(1); + const newPageButton = await app.findByTestId('new-page-button'); + newPageButton.click(); + expect( + currentWorkspaceHook.result.current[0]?.blockSuiteWorkspace.meta + .pageMetas[1].id + ).toBe('page-test-1'); + expect(onOpenQuickSearchModalFn).toBeCalledTimes(0); + const quickSearchButton = await app.findByTestId( + 'slider-bar-quick-search-button' + ); + quickSearchButton.click(); + expect(onOpenQuickSearchModalFn).toBeCalledTimes(1); + }); +}); diff --git a/apps/web/src/components/__tests__/__snapshots__/ProviderComposer.spec.tsx.snap b/apps/web/src/components/__tests__/__snapshots__/ProviderComposer.spec.tsx.snap new file mode 100644 index 0000000000..52836a18e3 --- /dev/null +++ b/apps/web/src/components/__tests__/__snapshots__/ProviderComposer.spec.tsx.snap @@ -0,0 +1,7 @@ +// Vitest Snapshot v1 + +exports[`ProviderComposer 1`] = ` + + test1 + +`; diff --git a/apps/web/src/components/affine/README.md b/apps/web/src/components/affine/README.md new file mode 100644 index 0000000000..82c58f9852 --- /dev/null +++ b/apps/web/src/components/affine/README.md @@ -0,0 +1,19 @@ +# Affine Official Workspace Component + +This component need specific configuration to work properly. + +## Configuration + +### SWR + +Each component use SWR to fetch data from the API. You need to provide a configuration to SWR to make it work. + +```tsx +const Wrapper = () => { + return ( + + + + ); +}; +``` diff --git a/apps/web/src/components/affine/affine-error-eoundary.tsx b/apps/web/src/components/affine/affine-error-eoundary.tsx new file mode 100644 index 0000000000..bd5958aa40 --- /dev/null +++ b/apps/web/src/components/affine/affine-error-eoundary.tsx @@ -0,0 +1,129 @@ +import { RequestError } from '@affine/datacenter'; +import { NextRouter } from 'next/router'; +import React, { Component, ErrorInfo } from 'react'; + +import { BlockSuiteWorkspace } from '../../shared'; + +export type AffineErrorBoundaryProps = React.PropsWithChildren<{ + router: NextRouter; +}>; + +export class PageNotFoundError extends TypeError { + readonly workspace: BlockSuiteWorkspace; + readonly pageId: string; + + constructor(workspace: BlockSuiteWorkspace, pageId: string) { + super(); + this.workspace = workspace; + this.pageId = pageId; + } +} + +export class WorkspaceNotFoundError extends TypeError { + readonly workspaceId: string; + + constructor(workspaceId: string) { + super(); + this.workspaceId = workspaceId; + } +} + +export class QueryParamError extends TypeError { + readonly targetKey: string; + readonly query: unknown; + + constructor(targetKey: string, query: unknown) { + super(); + this.targetKey = targetKey; + this.query = query; + } +} + +export class Unreachable extends Error { + constructor(message?: string) { + super(message); + } +} + +type AffineError = + | QueryParamError + | Unreachable + | WorkspaceNotFoundError + | PageNotFoundError + | RequestError + | Error; + +interface AffineErrorBoundaryState { + error: AffineError | null; +} + +export class AffineErrorBoundary extends Component< + AffineErrorBoundaryProps, + AffineErrorBoundaryState +> { + public state: AffineErrorBoundaryState = { + error: null, + }; + + public static getDerivedStateFromError( + error: AffineError + ): AffineErrorBoundaryState { + return { error }; + } + + public componentDidCatch(error: AffineError, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + } + + public render() { + if (this.state.error) { + const error = this.state.error; + if (error instanceof PageNotFoundError) { + return ( + <> +

Sorry.. there was an error

+ <> + Page error + + Cannot find page {error.pageId} in workspace{' '} + {error.workspace.meta.name} + + + + + ); + } else if (error instanceof RequestError) { + return ( + <> +

Sorry.. there was an error

+ {error.message} + + ); + } + return ( + <> +

Sorry.. there was an error

+ + ); + } + + return this.props.children; + } +} diff --git a/apps/web/src/components/affine/enable-affine-cloud-modal/index.tsx b/apps/web/src/components/affine/enable-affine-cloud-modal/index.tsx new file mode 100644 index 0000000000..6e2dd0330e --- /dev/null +++ b/apps/web/src/components/affine/enable-affine-cloud-modal/index.tsx @@ -0,0 +1,64 @@ +import { IconButton, Modal, ModalWrapper } from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import { CloseIcon } from '@blocksuite/icons'; +import React from 'react'; + +import { useCurrentUser } from '../../../hooks/current/use-current-user'; +import { AffineRemoteWorkspace } from '../../../shared'; +import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style'; + +interface EnableAffineCloudModalProps { + workspace: AffineRemoteWorkspace; + open: boolean; + onConfirm: () => void; + onClose: () => void; +} + +export const EnableAffineCloudModal: React.FC = ({ + onConfirm, + open, + onClose, +}) => { + const { t } = useTranslation(); + const user = useCurrentUser(); + + return ( + + +
+ { + onClose(); + }} + > + + +
+ + {t('Enable AFFiNE Cloud')}? + {t('Enable AFFiNE Cloud Description')} + {/* {t('Retain cached cloud data')} */} +
+ { + onConfirm(); + }} + > + {user ? t('Enable') : t('Sign in and Enable')} + + { + onClose(); + }} + > + {t('Not now')} + +
+
+
+
+ ); +}; diff --git a/apps/web/src/components/enable-workspace-modal/style.ts b/apps/web/src/components/affine/enable-affine-cloud-modal/style.ts similarity index 100% rename from apps/web/src/components/enable-workspace-modal/style.ts rename to apps/web/src/components/affine/enable-affine-cloud-modal/style.ts diff --git a/apps/web/src/components/enable-workspace-modal/index.tsx b/apps/web/src/components/affine/transform-workspace-to-affine-modal/index.tsx similarity index 51% rename from apps/web/src/components/enable-workspace-modal/index.tsx rename to apps/web/src/components/affine/transform-workspace-to-affine-modal/index.tsx index 7d9c696ae1..c7c38c31e9 100644 --- a/apps/web/src/components/enable-workspace-modal/index.tsx +++ b/apps/web/src/components/affine/transform-workspace-to-affine-modal/index.tsx @@ -1,31 +1,22 @@ -import { IconButton, Modal, ModalWrapper, toast } from '@affine/component'; +import { IconButton, Modal, ModalWrapper } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { CloseIcon } from '@blocksuite/icons'; -import { useRouter } from 'next/router'; -import { useCallback, useState } from 'react'; - -import { useGlobalState } from '@/store/app'; +import React from 'react'; +import { useCurrentUser } from '../../../hooks/current/use-current-user'; import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style'; -interface EnableWorkspaceModalProps { +export type TransformWorkspaceToAffineModalProps = { open: boolean; onClose: () => void; -} + onConform: () => void; +}; -export const EnableWorkspaceModal = ({ - open, - onClose, -}: EnableWorkspaceModalProps) => { +export const TransformWorkspaceToAffineModal: React.FC< + TransformWorkspaceToAffineModalProps +> = ({ open, onClose, onConform }) => { const { t } = useTranslation(); - const login = useGlobalState(store => store.login); - const user = useGlobalState(store => store.user); - const dataCenter = useGlobalState(store => store.dataCenter); - const currentWorkspace = useGlobalState( - useCallback(store => store.currentDataCenterWorkspace, []) - ); - const [loading, setLoading] = useState(false); - const router = useRouter(); + const user = useCurrentUser(); return ( @@ -47,22 +38,22 @@ export const EnableWorkspaceModal = ({ { - setLoading(true); - if (user || (await login())) { - if (currentWorkspace) { - const workspace = await dataCenter.enableWorkspaceCloud( - currentWorkspace - ); - toast(t('Enabled success')); - - if (workspace) { - router.push(`/workspace/${workspace.id}/setting`); - } - } - } - setLoading(false); + onConform(); + // setLoading(true); + // if (user || (await login())) { + // if (currentWorkspace) { + // const workspace = await dataCenter.enableWorkspaceCloud( + // currentWorkspace + // ); + // toast(t('Enabled success')); + // + // if (workspace) { + // router.push(`/workspace/${workspace.id}/setting`); + // } + // } + // } + // setLoading(false); }} > {user ? t('Enable') : t('Sign in and Enable')} diff --git a/apps/web/src/components/affine/transform-workspace-to-affine-modal/style.ts b/apps/web/src/components/affine/transform-workspace-to-affine-modal/style.ts new file mode 100644 index 0000000000..d49dd1a77d --- /dev/null +++ b/apps/web/src/components/affine/transform-workspace-to-affine-modal/style.ts @@ -0,0 +1,40 @@ +import { Button, styled } from '@affine/component'; + +export const Header = styled('div')({ + height: '44px', + display: 'flex', + flexDirection: 'row-reverse', + paddingRight: '10px', + paddingTop: '10px', + flexShrink: 0, +}); + +export const Content = styled('div')({ + textAlign: 'center', +}); + +export const ContentTitle = styled('h1')({ + fontSize: '20px', + lineHeight: '28px', + fontWeight: 600, + textAlign: 'center', +}); + +export const StyleTips = styled('div')(() => { + return { + userSelect: 'none', + width: '400px', + margin: 'auto', + marginBottom: '32px', + marginTop: '12px', + }; +}); + +export const StyleButton = styled(Button)(() => { + return { + width: '284px', + display: 'block', + margin: 'auto', + marginTop: '16px', + }; +}); diff --git a/apps/web/src/components/affine/workspace-setting-detail/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/index.tsx new file mode 100644 index 0000000000..1010183e79 --- /dev/null +++ b/apps/web/src/components/affine/workspace-setting-detail/index.tsx @@ -0,0 +1,159 @@ +import { useTranslation } from '@affine/i18n'; +import React, { + MouseEvent, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; +import { preload } from 'swr'; + +import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner'; +import { fetcher, QueryKey } from '../../../plugins/affine/fetcher'; +import { + AffineOfficialWorkspace, + SettingPanel, + settingPanel, +} from '../../../shared'; +import { CollaborationPanel } from './panel/collaboration'; +import { ExportPanel } from './panel/export'; +import { GeneralPanel } from './panel/general'; +import { PublishPanel } from './panel/publish'; +import { + StyledIndicator, + StyledSettingContainer, + StyledSettingContent, + StyledTabButtonWrapper, + WorkspaceSettingTagItem, +} from './style'; + +export type WorkspaceSettingDetailProps = { + workspace: AffineOfficialWorkspace; + currentTab: SettingPanel; + onChangeTab: (tab: SettingPanel) => void; + onDeleteWorkspace: () => void; + onTransferWorkspace: (targetWorkspaceId: string) => void; +}; + +export type PanelProps = WorkspaceSettingDetailProps; + +const panelMap = { + [settingPanel.General]: { + name: 'General', + ui: GeneralPanel, + }, + [settingPanel.Collaboration]: { + name: 'Collaboration', + ui: CollaborationPanel, + }, + [settingPanel.Publish]: { + name: 'Publish', + ui: PublishPanel, + }, + [settingPanel.Export]: { + name: 'Export', + ui: ExportPanel, + }, +} satisfies { + [Key in SettingPanel]: { + name: string; + ui: React.FC; + }; +}; + +function assertInstanceOf( + obj: T, + type: new (...args: any[]) => U +): asserts obj is U { + if (!(obj instanceof type)) { + throw new Error('Object is not instance of type'); + } +} + +export const WorkspaceSettingDetail: React.FC< + WorkspaceSettingDetailProps +> = props => { + const { + workspace, + currentTab, + onChangeTab, + // onDeleteWorkspace, + // onTransferWorkspace, + } = props; + const isAffine = workspace.flavour === 'affine'; + const isOwner = useIsWorkspaceOwner(workspace); + if (!(workspace.flavour === 'affine' || workspace.flavour === 'local')) { + throw new Error('Unsupported workspace flavour'); + } + if (!(currentTab in panelMap)) { + throw new Error('Invalid activeTab: ' + currentTab); + } + const { t } = useTranslation(); + const workspaceId = workspace.id; + useEffect(() => { + if (isAffine && isOwner) { + preload([QueryKey.getMembers, workspaceId], fetcher); + } + }, [isAffine, isOwner, workspaceId]); + const containerRef = useRef(null); + const indicatorRef = useRef(null); + const startTransaction = useCallback(() => { + if (indicatorRef.current && containerRef.current) { + const indicator = indicatorRef.current; + const activeTabElement = containerRef.current.querySelector( + `[data-tab-key="${currentTab}"]` + ); + assertInstanceOf(activeTabElement, HTMLElement); + requestAnimationFrame(() => { + indicator.style.left = `${activeTabElement.offsetLeft}px`; + indicator.style.width = `${activeTabElement.offsetWidth}px`; + }); + } + }, [currentTab]); + const handleTabClick = useCallback( + (event: MouseEvent) => { + assertInstanceOf(event.target, HTMLElement); + const key = event.target.getAttribute('data-tab-key'); + if (!key || !(key in panelMap)) { + throw new Error('data-tab-key is invalid: ' + key); + } + onChangeTab(key as SettingPanel); + startTransaction(); + }, + [onChangeTab, startTransaction] + ); + const Component = useMemo(() => panelMap[currentTab].ui, [currentTab]); + return ( + + + {Object.entries(panelMap).map(([key, value]) => { + if (!isAffine && key === 'Sync') { + return null; + } + return ( + + {t(value.name)} + + ); + })} + { + indicatorRef.current = ref; + startTransaction(); + }} + /> + + + + + + ); +}; diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx new file mode 100644 index 0000000000..ec0035a8cc --- /dev/null +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx @@ -0,0 +1,227 @@ +import { + Button, + IconButton, + Menu, + MenuItem, + toast, + Wrapper, +} from '@affine/component'; +import { PermissionType } from '@affine/datacenter'; +import { useTranslation } from '@affine/i18n'; +import { + DeleteTemporarilyIcon, + EmailIcon, + MoreVerticalIcon, +} from '@blocksuite/icons'; +import React, { useCallback, useState } from 'react'; + +import { lockMutex } from '../../../../../atoms'; +import { useMembers } from '../../../../../hooks/affine/use-members'; +import { transformWorkspace } from '../../../../../plugins'; +import { + AffineRemoteWorkspace, + LocalWorkspace, + RemWorkspaceFlavour, +} from '../../../../../shared'; +import { Unreachable } from '../../../affine-error-eoundary'; +import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal'; +import { PanelProps } from '../../index'; +import { InviteMemberModal } from './invite-member-modal'; +import { + StyledMemberAvatar, + StyledMemberButtonContainer, + StyledMemberContainer, + StyledMemberEmail, + StyledMemberInfo, + StyledMemberListContainer, + StyledMemberListItem, + StyledMemberName, + StyledMemberNameContainer, + StyledMemberRoleContainer, + StyledMemberTitleContainer, + StyledMoreVerticalButton, + StyledMoreVerticalDiv, +} from './style'; + +const AffineRemoteCollaborationPanel: React.FC< + Omit & { + workspace: AffineRemoteWorkspace; + } +> = ({ workspace }) => { + const [isInviteModalShow, setIsInviteModalShow] = useState(false); + const { t } = useTranslation(); + const { members, removeMember } = useMembers(workspace.id); + return ( + <> + +
    + + + {t('Users')} ({members.length}) + + + {t('Access level')} + +
    +
    +
+ + + {members.length > 0 && ( + <> + {members + .sort((b, a) => a.type - b.type) + .map((member, index) => { + const user = { + avatar_url: '', + id: '', + name: '', + ...member.user, + }; + return ( + + + + + + + + {user.name} + + {member.user.email} + + + + + {member.accepted + ? member.type !== PermissionType.Owner + ? t('Member') + : t('Owner') + : t('Pending')} + + {member.type === PermissionType.Owner ? ( + + ) : ( + + + { + // FIXME: remove ignore + + // @ts-ignore + await removeMember(member.id); + toast( + t('Member has been removed', { + name: user.name, + }) + ); + }} + icon={} + > + {t('Remove from workspace')} + + + } + placement="bottom-end" + disablePortal={true} + trigger="click" + > + + + + + + )} + + ); + })} + + )} + + + + +
+ { + setIsInviteModalShow(false); + }, [])} + onInviteSuccess={useCallback(() => { + setIsInviteModalShow(false); + }, [])} + workspaceId={workspace.id} + open={isInviteModalShow} + /> + + ); +}; + +const LocalCollaborationPanel: React.FC< + Omit & { + workspace: LocalWorkspace; + } +> = ({ workspace, onTransferWorkspace }) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + return ( + <> + {t('Collaboration Description')} + + { + setOpen(false); + }} + onConform={() => { + // todo(himself65): move this function out of affine component + lockMutex(async () => { + const id = await transformWorkspace( + RemWorkspaceFlavour.LOCAL, + RemWorkspaceFlavour.AFFINE, + workspace + ); + onTransferWorkspace(id); + setOpen(false); + }); + }} + /> + + ); +}; + +export const CollaborationPanel: React.FC = props => { + switch (props.workspace.flavour) { + case RemWorkspaceFlavour.AFFINE: { + const workspace = props.workspace as AffineRemoteWorkspace; + return ( + + ); + } + case RemWorkspaceFlavour.LOCAL: { + const workspace = props.workspace as LocalWorkspace; + return ; + } + } + throw new Unreachable(); +}; diff --git a/apps/web/src/components/workspace-setting/member/InviteMemberModal.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx similarity index 61% rename from apps/web/src/components/workspace-setting/member/InviteMemberModal.tsx rename to apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx index 7d9e1aaab5..fddb323852 100644 --- a/apps/web/src/components/workspace-setting/member/InviteMemberModal.tsx +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx @@ -3,12 +3,13 @@ import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; import { Button } from '@affine/component'; import { Input } from '@affine/component'; import { MuiAvatar } from '@affine/component'; -import { User } from '@affine/datacenter'; import { useTranslation } from '@affine/i18n'; import { EmailIcon } from '@blocksuite/icons'; -import { useState } from 'react'; +import React, { Suspense, useCallback, useState } from 'react'; + +import { useMembers } from '../../../../../../hooks/affine/use-members'; +import { useUsersByEmail } from '../../../../../../hooks/affine/use-users-by-email'; -import useMembers from '@/hooks/use-members'; interface LoginModalProps { open: boolean; onClose: () => void; @@ -16,60 +17,47 @@ interface LoginModalProps { onInviteSuccess: () => void; } -export const debounce = any>( - fn: T, - time?: number, - immediate?: boolean -): ((...args: any) => any) => { - let timeoutId: null | number; - let defaultImmediate = immediate || false; - const delay = time || 300; +const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/; - return (...args: any) => { - if (defaultImmediate) { - fn.apply(this, args); - defaultImmediate = false; - return; - } - if (timeoutId) { - clearTimeout(timeoutId); - } - - // @ts-ignore - timeoutId = setTimeout(() => { - fn.apply(this, args); - timeoutId = null; - }, delay); - }; +const Result: React.FC<{ + workspaceId: string; + queryEmail: string; +}> = ({ workspaceId, queryEmail }) => { + const users = useUsersByEmail(workspaceId, queryEmail); + const firstUser = users?.at(0) ?? null; + if (!firstUser || !firstUser.email) { + return null; + } + return ( + + + {firstUser.avatar_url ? ( + + ) : ( + + + + )} + {firstUser.email} + {/*
invited
*/} +
+
+ ); }; -const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/; export const InviteMemberModal = ({ open, onClose, onInviteSuccess, + workspaceId, }: LoginModalProps) => { + const { inviteMember } = useMembers(workspaceId); const [email, setEmail] = useState(''); - const [showMember, setShowMember] = useState(false); - const [showTip, setShowTip] = useState(false); - const [userData, setUserData] = useState(null); - const { inviteMember, getUserByEmail } = useMembers(); + const [showMemberPreview, setShowMemberPreview] = useState(false); const { t } = useTranslation(); - const inputChange = (value: string) => { - setShowMember(true); - if (gmailReg.test(value)) { - setEmail(value); - setShowTip(false); - getUserByEmail(value).then(data => { - if (data?.name) { - setUserData(data); - setShowTip(false); - } - }); - } else { - setShowTip(true); - } - }; + const inputChange = useCallback((value: string) => { + setEmail(value); + }, []); return (
@@ -89,36 +77,24 @@ export const InviteMemberModal = ({ width={360} value={email} onChange={inputChange} - onBlur={() => { - setShowMember(false); - }} + onFocus={useCallback(() => { + setShowMemberPreview(true); + }, [])} + onBlur={useCallback(() => { + setShowMemberPreview(false); + }, [])} placeholder={t('Invite placeholder')} > - {showMember ? ( - - {showTip ? ( - {t('Non-Gmail')} - ) : ( - - {userData?.avatar ? ( - - ) : ( - - - - )} - {email} - {/*
invited
*/} -
- )} -
- ) : ( - <> + {showMemberPreview && gmailReg.test(email) && ( + + + )}
- { setShowDelete(false); diff --git a/apps/web/src/components/workspace-setting/general/leave/Leave.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/general/leave/index.tsx similarity index 80% rename from apps/web/src/components/workspace-setting/general/leave/Leave.tsx rename to apps/web/src/components/affine/workspace-setting-detail/panel/general/leave/index.tsx index 3389fc794c..61e9f8dd32 100644 --- a/apps/web/src/components/workspace-setting/general/leave/Leave.tsx +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/general/leave/index.tsx @@ -3,16 +3,12 @@ import { ModalCloseButton } from '@affine/component'; import { Button } from '@affine/component'; import { useTranslation } from '@affine/i18n'; -import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; - import { StyledButtonContent, StyledModalHeader, StyledModalWrapper, StyledTextContent, } from './style'; -// import { getDataCenter } from '@affine/datacenter'; -// import { useAppState } from '@/providers/app-state-provider'; interface WorkspaceDeleteProps { open: boolean; @@ -20,10 +16,10 @@ interface WorkspaceDeleteProps { } export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => { - const { leaveWorkSpace } = useWorkspaceHelper(); + // const { leaveWorkSpace } = useWorkspaceHelper(); const { t } = useTranslation(); const handleLeave = async () => { - await leaveWorkSpace(); + // await leaveWorkSpace(); onClose(); }; @@ -52,5 +48,3 @@ export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => { ); }; - -export default WorkspaceLeave; diff --git a/apps/web/src/components/workspace-setting/general/leave/style.ts b/apps/web/src/components/affine/workspace-setting-detail/panel/general/leave/style.ts similarity index 100% rename from apps/web/src/components/workspace-setting/general/leave/style.ts rename to apps/web/src/components/affine/workspace-setting-detail/panel/general/leave/style.ts diff --git a/apps/web/src/components/workspace-setting/general/style.ts b/apps/web/src/components/affine/workspace-setting-detail/panel/general/style.ts similarity index 99% rename from apps/web/src/components/workspace-setting/general/style.ts rename to apps/web/src/components/affine/workspace-setting-detail/panel/general/style.ts index 47e41de79b..4886505b09 100644 --- a/apps/web/src/components/workspace-setting/general/style.ts +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/general/style.ts @@ -8,6 +8,7 @@ export const StyledInput = styled(Input)(({ theme }) => { fontSize: theme.font.sm, }; }); + export const StyledWorkspaceInfo = styled.div(({ theme }) => { return { ...displayFlex('flex-start', 'center'), @@ -18,6 +19,7 @@ export const StyledWorkspaceInfo = styled.div(({ theme }) => { }, }; }); + export const StyledAvatar = styled('div')( ({ disabled }: { disabled: boolean }) => { return { diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/publish/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/publish/index.tsx new file mode 100644 index 0000000000..4a80a60ee9 --- /dev/null +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/publish/index.tsx @@ -0,0 +1,147 @@ +import { + Button, + Content, + FlexWrapper, + Input, + toast, + Wrapper, +} from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import React, { useCallback, useEffect, useState } from 'react'; + +import { lockMutex } from '../../../../../atoms'; +import { useToggleWorkspacePublish } from '../../../../../hooks/affine/use-toggle-workspace-publish'; +import { + AffineOfficialWorkspace, + AffineRemoteWorkspace, + LocalWorkspace, + RemWorkspaceFlavour, +} from '../../../../../shared'; +import { Unreachable } from '../../../affine-error-eoundary'; +import { EnableAffineCloudModal } from '../../../enable-affine-cloud-modal'; + +export type PublishPanelProps = { + workspace: AffineOfficialWorkspace; +}; + +export type PublishPanelAffineProps = { + workspace: AffineRemoteWorkspace; +}; + +const PublishPanelAffine: React.FC = ({ + workspace, +}) => { + const [origin, setOrigin] = useState(''); + useEffect(() => { + setOrigin( + typeof window !== 'undefined' && window.location.origin + ? window.location.origin + : '' + ); + }, []); + const shareUrl = origin + '/public-workspace/' + workspace.id; + const { t } = useTranslation(); + const publishWorkspace = useToggleWorkspacePublish(workspace); + const copyUrl = useCallback(() => { + navigator.clipboard.writeText(shareUrl); + toast(t('Copied link to clipboard')); + }, [shareUrl, t]); + + const [open, setOpen] = useState(false); + + if (workspace.public) { + return ( + <> + {t('Published Description')} + + + {t('Share with link')} + + + + + + + + ); + } + return ( + <> + {t('Publishing Description')} + + { + setOpen(false); + }} + onConfirm={() => { + lockMutex(async () => { + return publishWorkspace(true); + }).then(() => { + setOpen(false); + }); + }} + /> + + ); +}; + +export type PublishPanelLocalProps = { + workspace: LocalWorkspace; +}; + +const PublishPanelLocal: React.FC = ({ workspace }) => { + const { t } = useTranslation(); + return ( + <> + {t('Publishing')} + + + ); +}; + +export const PublishPanel: React.FC = ({ workspace }) => { + if (workspace.flavour === RemWorkspaceFlavour.AFFINE) { + return ; + } else if (workspace.flavour === RemWorkspaceFlavour.LOCAL) { + return ; + } + throw new Unreachable(); +}; diff --git a/apps/web/src/components/workspace-setting/style.ts b/apps/web/src/components/affine/workspace-setting-detail/style.ts similarity index 86% rename from apps/web/src/components/workspace-setting/style.ts rename to apps/web/src/components/affine/workspace-setting-detail/style.ts index bc0b49a863..c58c40af2e 100644 --- a/apps/web/src/components/workspace-setting/style.ts +++ b/apps/web/src/components/affine/workspace-setting-detail/style.ts @@ -65,6 +65,24 @@ export const StyledWorkspaceName = styled('span')(({ theme }) => { }; }); +export const StyledIndicator = styled.div(({ theme }) => { + return { + height: '2px', + background: theme.colors.primaryColor, + position: 'absolute', + left: '0', + bottom: '0', + transition: 'left .3s, width .3s', + }; +}); + +export const StyledTabButtonWrapper = styled.div(() => { + return { + display: 'flex', + position: 'relative', + }; +}); + // export const StyledDownloadCard = styled.div<{ active?: boolean }>( // ({ theme, active }) => { // return { diff --git a/apps/web/src/components/blocksuite/block-suite-editor/index.tsx b/apps/web/src/components/blocksuite/block-suite-editor/index.tsx new file mode 100644 index 0000000000..c1c5735417 --- /dev/null +++ b/apps/web/src/components/blocksuite/block-suite-editor/index.tsx @@ -0,0 +1,104 @@ +import { BlockHub } from '@blocksuite/blocks'; +import { EditorContainer } from '@blocksuite/editor'; +import type { Page } from '@blocksuite/store'; +import { assertExists } from '@blocksuite/store'; +import { useEffect, useRef } from 'react'; + +import { BlockSuiteWorkspace } from '../../../shared'; + +export type EditorProps = { + blockSuiteWorkspace: BlockSuiteWorkspace; + page: Page; + mode: 'page' | 'edgeless'; + onInit?: (page: Page, editor: Readonly) => void; + onLoad?: (page: Page, editor: EditorContainer) => void; +}; + +import markdown from '../../../templates/Welcome-to-AFFiNE-Alpha-Downhills.md'; + +const exampleTitle = markdown + .split('\n') + .splice(0, 1) + .join('') + .replaceAll('#', '') + .trim(); +const exampleText = markdown.split('\n').slice(1).join('\n'); + +const kFirstPage = 'affine-first-page'; + +export const BlockSuiteEditor = (props: EditorProps) => { + const page = props.page; + const editorRef = useRef(null); + const blockHubRef = useRef(null); + if (editorRef.current === null) { + editorRef.current = new EditorContainer(); + // fixme(himself65): remove `globalThis.editor` + // @ts-expect-error + globalThis.editor = editorRef.current; + } + const ref = useRef(null); + useEffect(() => { + if (editorRef.current) { + editorRef.current.mode = props.mode; + } + }, [props.mode]); + + useEffect(() => { + const editor = editorRef.current; + if (!editor || !ref.current || !page) { + return; + } + + editor.page = page; + if (page.root === null) { + if (props.onInit) { + props.onInit(page, editor); + } else { + console.debug('Initializing page with default content'); + // Add page block and surface block at root level + const title = + localStorage.getItem(kFirstPage) === null ? exampleTitle : undefined; + const pageBlockId = page.addBlockByFlavour('affine:page', { + title, + }); + page.addBlockByFlavour('affine:surface', {}, null); + const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId); + page.addBlockByFlavour('affine:paragraph', {}, frameId); + if (localStorage.getItem(kFirstPage) === null) { + // fixme(himself65): remove + editor.clipboard.importMarkdown(exampleText, frameId); + props.blockSuiteWorkspace.setPageMeta(page.id, { title }); + localStorage.setItem(kFirstPage, 'true'); + } + page.resetHistory(); + } + } + props.onLoad?.(page, editor); + return; + }, [page, props]); + + useEffect(() => { + const editor = editorRef.current; + const container = ref.current; + + if (!editor || !container || !page) { + return; + } + + editor.createBlockHub().then(blockHub => { + if (blockHubRef.current) { + blockHubRef.current.remove(); + } + blockHubRef.current = blockHub; + const toolWrapper = document.querySelector('#toolWrapper'); + assertExists(toolWrapper); + toolWrapper.appendChild(blockHub); + }); + container.appendChild(editor); + return () => { + blockHubRef.current?.remove(); + container.removeChild(editor); + }; + }, [page]); + return
; +}; diff --git a/apps/web/src/components/blocksuite/block-suite-page-list/index.tsx b/apps/web/src/components/blocksuite/block-suite-page-list/index.tsx new file mode 100644 index 0000000000..5754450e15 --- /dev/null +++ b/apps/web/src/components/blocksuite/block-suite-page-list/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { BlockSuiteWorkspace } from '../../../shared'; +import PageList from './page-list'; + +export type BlockSuitePageListProps = { + blockSuiteWorkspace: BlockSuiteWorkspace; + onOpenPage: (pageId: string, newTab?: boolean) => void; +}; + +export const BlockSuitePageList: React.FC = ({ + blockSuiteWorkspace, + onOpenPage, +}) => { + return ( + + ); +}; + +export const BlockSuitePublicPageList: React.FC = ({ + blockSuiteWorkspace, + onOpenPage, +}) => { + return ( + + ); +}; diff --git a/apps/web/src/components/page-list/DateCell.tsx b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/DateCell.tsx similarity index 91% rename from apps/web/src/components/page-list/DateCell.tsx rename to apps/web/src/components/blocksuite/block-suite-page-list/page-list/DateCell.tsx index 13c80c2c5b..55f90e9449 100644 --- a/apps/web/src/components/page-list/DateCell.tsx +++ b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/DateCell.tsx @@ -1,10 +1,9 @@ import { TableCell, TableCellProps } from '@affine/component'; +import { PageMeta } from '@blocksuite/store'; import dayjs from 'dayjs'; import localizedFormat from 'dayjs/plugin/localizedFormat'; import React from 'react'; -import { PageMeta } from '@/providers/app-state-provider'; - dayjs.extend(localizedFormat); export const DateCell = ({ diff --git a/apps/web/src/components/page-list/Empty.tsx b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/Empty.tsx similarity index 100% rename from apps/web/src/components/page-list/Empty.tsx rename to apps/web/src/components/blocksuite/block-suite-page-list/page-list/Empty.tsx diff --git a/apps/web/src/components/blocksuite/block-suite-page-list/page-list/OperationCell.tsx b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/OperationCell.tsx new file mode 100644 index 0000000000..ff1f0c1a70 --- /dev/null +++ b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/OperationCell.tsx @@ -0,0 +1,171 @@ +import { + Confirm, + FlexWrapper, + IconButton, + Menu, + MenuItem, + Tooltip, +} from '@affine/component'; +import { toast } from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import { + DeletePermanentlyIcon, + DeleteTemporarilyIcon, + FavoritedIcon, + FavoriteIcon, + MoreVerticalIcon, + OpenInNewIcon, + ResetIcon, +} from '@blocksuite/icons'; +import { PageMeta } from '@blocksuite/store'; +import React, { useState } from 'react'; + +export type OperationCellProps = { + pageMeta: PageMeta; + onOpenPageInNewTab: (pageId: string) => void; + onToggleFavoritePage: (pageId: string) => void; + onToggleTrashPage: (pageId: string) => void; +}; +export const OperationCell: React.FC = ({ + pageMeta, + onOpenPageInNewTab, + onToggleFavoritePage, + onToggleTrashPage, +}) => { + const { id, favorite } = pageMeta; + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const OperationMenu = ( + <> + { + onToggleFavoritePage(id); + toast( + favorite ? t('Removed from Favorites') : t('Added to Favorites') + ); + }} + icon={favorite ? : } + > + {favorite ? t('Remove from favorites') : t('Add to Favorites')} + + { + onOpenPageInNewTab(id); + }} + icon={} + > + {t('Open in new tab')} + + { + setOpen(true); + }} + icon={} + > + {t('Delete')} + + + ); + return ( + <> + + + + + + + + { + onToggleTrashPage(id); + toast(t('Deleted')); + setOpen(false); + }} + onClose={() => { + setOpen(false); + }} + onCancel={() => { + setOpen(false); + }} + /> + + ); +}; + +export type TrashOperationCellProps = { + pageMeta: PageMeta; + onPermanentlyDeletePage: (pageId: string) => void; + onRestorePage: (pageId: string) => void; + onOpenPage: (pageId: string) => void; +}; + +export const TrashOperationCell: React.FC = ({ + pageMeta, + onPermanentlyDeletePage, + onRestorePage, + onOpenPage, +}) => { + const { id, title } = pageMeta; + // const { openPage, getPageMeta } = usePageHelper(); + // const { toggleDeletePage, permanentlyDeletePage } = usePageHelper(); + // const confirm = useConfirm(store => store.confirm); + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + return ( + + + { + onRestorePage(id); + toast(t('restored', { title: title || 'Untitled' })); + onOpenPage(id); + }} + > + + + + + { + setOpen(true); + }} + > + + + + { + onPermanentlyDeletePage(id); + toast(t('Permanently deleted')); + setOpen(false); + }} + onClose={() => { + setOpen(false); + }} + onCancel={() => { + setOpen(false); + }} + /> + + ); +}; diff --git a/apps/web/src/components/blocksuite/block-suite-page-list/page-list/index.tsx b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/index.tsx new file mode 100644 index 0000000000..4331cf4a7b --- /dev/null +++ b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/index.tsx @@ -0,0 +1,216 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from '@affine/component'; +import { Content, IconButton, toast, Tooltip } from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import { + EdgelessIcon, + FavoritedIcon, + FavoriteIcon, + PaperIcon, +} from '@blocksuite/icons'; +import { PageMeta } from '@blocksuite/store'; +import { useMediaQuery, useTheme as useMuiTheme } from '@mui/material'; +import React, { useMemo } from 'react'; + +import { + usePageMeta, + usePageMetaHelper, +} from '../../../../hooks/use-page-meta'; +import { useTheme } from '../../../../providers/ThemeProvider'; +import { BlockSuiteWorkspace } from '../../../../shared'; +import DateCell from './DateCell'; +import Empty from './Empty'; +import { OperationCell, TrashOperationCell } from './OperationCell'; +import { + StyledTableContainer, + StyledTableRow, + StyledTitleLink, + StyledTitleWrapper, +} from './styles'; +const FavoriteTag = ({ + pageMeta: { favorite, id }, +}: { + pageMeta: PageMeta; +}) => { + const { theme } = useTheme(); + const { t } = useTranslation(); + return ( + + { + e.stopPropagation(); + // toggleFavoritePage(id); + toast( + favorite ? t('Removed from Favorites') : t('Added to Favorites') + ); + }} + style={{ + color: favorite ? theme.colors.primaryColor : theme.colors.iconColor, + }} + className={favorite ? '' : 'favorite-button'} + > + {favorite ? ( + + ) : ( + + )} + + + ); +}; + +type PageListProps = { + blockSuiteWorkspace: BlockSuiteWorkspace; + isPublic?: boolean; + listType?: 'all' | 'trash' | 'favorite'; + onClickPage: (pageId: string, newTab?: boolean) => void; +}; + +const filter = { + all: (pageMeta: PageMeta) => !pageMeta.trash, + trash: (pageMeta: PageMeta) => pageMeta.trash, + favorite: (pageMeta: PageMeta) => pageMeta.favorite, +}; + +export const PageList: React.FC = ({ + blockSuiteWorkspace, + isPublic = false, + listType, + onClickPage, +}) => { + const pageList = usePageMeta(blockSuiteWorkspace); + const helper = usePageMetaHelper(blockSuiteWorkspace); + const { t } = useTranslation(); + const theme = useMuiTheme(); + const matches = useMediaQuery(theme.breakpoints.up('sm')); + const isTrash = listType === 'trash'; + const list = useMemo( + () => pageList.filter(filter[listType ?? 'all']), + [pageList, listType] + ); + if (list.length === 0) { + return ; + } + + return ( + + + + + {matches && ( + <> + {t('Title')} + {t('Created')} + + {isTrash ? t('Moved to Trash') : t('Updated')} + + + + )} + + + + {list.map((pageMeta, index) => { + return ( + + { + onClickPage(pageMeta.id); + }} + > + + + {pageMeta.mode === 'edgeless' ? ( + + ) : ( + + )} + + {pageMeta.title || t('Untitled')} + + + {!isTrash && } + + + {matches && ( + <> + { + onClickPage(pageMeta.id); + }} + /> + { + onClickPage(pageMeta.id); + }} + /> + {!isPublic && ( + + {isTrash ? ( + { + blockSuiteWorkspace.removePage(pageId); + }} + onRestorePage={() => { + helper.setPageMeta(pageMeta.id, { + trash: false, + }); + }} + onOpenPage={pageId => { + onClickPage(pageId, false); + }} + /> + ) : ( + { + onClickPage(pageId, true); + }} + onToggleFavoritePage={(pageId: string) => { + helper.setPageMeta(pageId, { + favorite: !pageMeta.favorite, + }); + }} + onToggleTrashPage={() => { + helper.setPageMeta(pageMeta.id, { + trash: !pageMeta.trash, + }); + }} + /> + )} + + )} + + )} + + ); + })} + +
+
+ ); +}; + +export default PageList; diff --git a/apps/web/src/components/page-list/styles.ts b/apps/web/src/components/blocksuite/block-suite-page-list/page-list/styles.ts similarity index 100% rename from apps/web/src/components/page-list/styles.ts rename to apps/web/src/components/blocksuite/block-suite-page-list/page-list/styles.ts diff --git a/apps/web/src/components/editor-mode-switch/Icons.tsx b/apps/web/src/components/blocksuite/header/editor-mode-switch/Icons.tsx similarity index 100% rename from apps/web/src/components/editor-mode-switch/Icons.tsx rename to apps/web/src/components/blocksuite/header/editor-mode-switch/Icons.tsx diff --git a/apps/web/src/components/editor-mode-switch/index.tsx b/apps/web/src/components/blocksuite/header/editor-mode-switch/index.tsx similarity index 75% rename from apps/web/src/components/editor-mode-switch/index.tsx rename to apps/web/src/components/blocksuite/header/editor-mode-switch/index.tsx index 4a2c1751fa..7ef97804bc 100644 --- a/apps/web/src/components/editor-mode-switch/index.tsx +++ b/apps/web/src/components/blocksuite/header/editor-mode-switch/index.tsx @@ -1,10 +1,14 @@ import { useTranslation } from '@affine/i18n'; -import React, { cloneElement, useEffect, useState } from 'react'; - -import useCurrentPageMeta from '@/hooks/use-current-page-meta'; -import { usePageHelper } from '@/hooks/use-page-helper'; -import { useTheme } from '@/providers/ThemeProvider'; +import { assertExists } from '@blocksuite/store'; +import React, { cloneElement, CSSProperties, useEffect, useState } from 'react'; +import { + usePageMeta, + usePageMetaHelper, +} from '../../../../hooks/use-page-meta'; +// todo(himself65): remove `useTheme` hook +import { useTheme } from '../../../../providers/ThemeProvider'; +import { BlockSuiteWorkspace } from '../../../../shared'; import { EdgelessIcon, PaperIcon } from './Icons'; import { StyledAnimateRadioContainer, @@ -13,11 +17,7 @@ import { StyledMiddleLine, StyledRadioItem, } from './style'; -import type { - AnimateRadioItemProps, - AnimateRadioProps, - RadioItemStatus, -} from './type'; +import type { AnimateRadioItemProps, RadioItemStatus } from './type'; const PaperItem = ({ active }: { active?: boolean }) => { const { theme: { @@ -64,13 +64,27 @@ const AnimateRadioItem = ({ ); }; -export const EditorModeSwitch = ({ +export type EditorModeSwitchProps = { + // todo(himself65): combine these two properties + blockSuiteWorkspace: BlockSuiteWorkspace; + pageId: string; + isHover: boolean; + style: CSSProperties; +}; + +export const EditorModeSwitch: React.FC = ({ isHover, style = {}, -}: AnimateRadioProps) => { + blockSuiteWorkspace, + pageId, +}) => { const { mode: themeMode } = useTheme(); - const { changePageMode } = usePageHelper(); - const { trash, mode = 'page', id = '' } = useCurrentPageMeta() || {}; + const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); + const pageMeta = usePageMeta(blockSuiteWorkspace).find( + meta => meta.id === pageId + ); + assertExists(pageMeta); + const { trash, mode = 'page' } = pageMeta; const modifyRadioItemStatus = (): RadioItemStatus => { return { @@ -113,7 +127,7 @@ export const EditorModeSwitch = ({ active={mode === 'page'} status={radioItemStatus.left} onClick={() => { - changePageMode(id, 'page'); + setPageMeta(pageId, { mode: 'page' }); }} onMouseEnter={() => { setRadioItemStatus({ @@ -134,7 +148,7 @@ export const EditorModeSwitch = ({ active={mode === 'edgeless'} status={radioItemStatus.right} onClick={() => { - changePageMode(id, 'edgeless'); + setPageMeta(pageId, { mode: 'edgeless' }); }} onMouseEnter={() => { setRadioItemStatus({ diff --git a/apps/web/src/components/editor-mode-switch/style.ts b/apps/web/src/components/blocksuite/header/editor-mode-switch/style.ts similarity index 100% rename from apps/web/src/components/editor-mode-switch/style.ts rename to apps/web/src/components/blocksuite/header/editor-mode-switch/style.ts diff --git a/apps/web/src/components/editor-mode-switch/type.ts b/apps/web/src/components/blocksuite/header/editor-mode-switch/type.ts similarity index 68% rename from apps/web/src/components/editor-mode-switch/type.ts rename to apps/web/src/components/blocksuite/header/editor-mode-switch/type.ts index d9bf0b5245..0909c6b1ba 100644 --- a/apps/web/src/components/editor-mode-switch/type.ts +++ b/apps/web/src/components/blocksuite/header/editor-mode-switch/type.ts @@ -1,4 +1,4 @@ -import { CSSProperties, DOMAttributes, ReactElement } from 'react'; +import { DOMAttributes, ReactElement } from 'react'; export type ItemStatus = 'normal' | 'stretch' | 'shrink' | 'hidden'; @@ -6,10 +6,6 @@ export type RadioItemStatus = { left: ItemStatus; right: ItemStatus; }; -export type AnimateRadioProps = { - isHover: boolean; - style: CSSProperties; -}; export type AnimateRadioItemProps = { active: boolean; status: ItemStatus; diff --git a/apps/web/src/components/header/header-right-items/EditorOptionMenu.tsx b/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx similarity index 59% rename from apps/web/src/components/header/header-right-items/EditorOptionMenu.tsx rename to apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx index 07534f6c96..9cc5ac4e44 100644 --- a/apps/web/src/components/header/header-right-items/EditorOptionMenu.tsx +++ b/apps/web/src/components/blocksuite/header/header-right-items/EditorOptionMenu.tsx @@ -1,42 +1,49 @@ +// fixme(himself65): refactor this file import { Menu, MenuItem } from '@affine/component'; import { IconButton } from '@affine/component'; import { toast } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { DeleteTemporarilyIcon, - EdgelessIcon, ExportIcon, ExportToHtmlIcon, ExportToMarkdownIcon, FavoritedIcon, FavoriteIcon, MoreVerticalIcon, - PaperIcon, } from '@blocksuite/icons'; +import { assertExists } from '@blocksuite/store'; + +import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id'; +import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace'; +import { + usePageMeta, + usePageMetaHelper, +} from '../../../../hooks/use-page-meta'; +import { EdgelessIcon, PaperIcon } from '../editor-mode-switch/Icons'; -import useCurrentPageMeta from '@/hooks/use-current-page-meta'; -import { usePageHelper } from '@/hooks/use-page-helper'; -import { useConfirm } from '@/providers/ConfirmProvider'; -import { useGlobalState } from '@/store/app'; const PopoverContent = () => { - const editor = useGlobalState(store => store.editor); - const { toggleFavoritePage, toggleDeletePage } = usePageHelper(); - const { changePageMode } = usePageHelper(); - const confirm = useConfirm(store => store.confirm); const { t } = useTranslation(); - const { - mode = 'page', - id = '', - favorite = false, - title = '', - } = useCurrentPageMeta() || {}; + // fixme(himself65): remove these hooks ASAP + const [workspace] = useCurrentWorkspace(); + const [pageId] = useCurrentPageId(); + assertExists(workspace); + assertExists(pageId); + const blockSuiteWorkspace = workspace.blockSuiteWorkspace; + const pageMeta = usePageMeta(blockSuiteWorkspace).find( + meta => meta.id === pageId + ); + assertExists(pageMeta); + const { mode = 'page', favorite, trash } = pageMeta; + const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); + // return ( <> { - toggleFavoritePage(id); + setPageMeta(pageId, { favorite: !favorite }); toast( favorite ? t('Removed from Favorites') : t('Added to Favorites') ); @@ -49,7 +56,9 @@ const PopoverContent = () => { icon={mode === 'page' ? : } data-testid="editor-option-menu-edgeless" onClick={() => { - changePageMode(id, mode === 'page' ? 'edgeless' : 'page'); + setPageMeta(pageId, { + mode: mode === 'page' ? 'edgeless' : 'page', + }); }} > {t('Convert to ')} @@ -61,7 +70,8 @@ const PopoverContent = () => { <> { - editor && editor.contentParser.onExportHtml(); + // @ts-expect-error + globalThis.editor.contentParser.onExportHtml(); }} icon={} > @@ -69,7 +79,8 @@ const PopoverContent = () => { { - editor && editor.contentParser.onExportMarkdown(); + // @ts-expect-error + globalThis.editor.contentParser.onExportMarkdown(); }} icon={} > @@ -85,17 +96,9 @@ const PopoverContent = () => { { - confirm({ - title: t('Delete page?'), - content: t('will be moved to Trash', { - title: title || 'Untitled', - }), - confirmText: t('Delete'), - confirmType: 'danger', - }).then(confirm => { - confirm && toggleDeletePage(id); - confirm && toast(t('Moved to Trash')); - }); + // fixme(himself65): regression that don't have conform dialog + setPageMeta(pageId, { trash: !trash }); + toast(t('Moved to Trash')); }} icon={} > @@ -108,11 +111,9 @@ const PopoverContent = () => { export const EditorOptionMenu = () => { return ( } placement="bottom-end" trigger="click"> - + ); }; - -export default EditorOptionMenu; diff --git a/apps/web/src/components/header/header-right-items/SyncUser.tsx b/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx similarity index 58% rename from apps/web/src/components/header/header-right-items/SyncUser.tsx rename to apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx index 0302349f85..cd76e90442 100644 --- a/apps/web/src/components/header/header-right-items/SyncUser.tsx +++ b/apps/web/src/components/blocksuite/header/header-right-items/SyncUser.tsx @@ -1,11 +1,20 @@ import { displayFlex, IconButton, styled, Tooltip } from '@affine/component'; -import { WorkspaceUnit } from '@affine/datacenter'; import { useTranslation } from '@affine/i18n'; -import { CloudWorkspaceIcon, LocalWorkspaceIcon } from '@blocksuite/icons'; -import { useCallback, useEffect, useState } from 'react'; +import { CloudWorkspaceIcon } from '@blocksuite/icons'; +import { assertEquals, assertExists } from '@blocksuite/store'; +import { useRouter } from 'next/router'; +import React, { useEffect, useState } from 'react'; -import { useGlobalState } from '@/store/app'; -import { useModal } from '@/store/globalModal'; +import { lockMutex } from '../../../../atoms'; +import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace'; +import { transformWorkspace } from '../../../../plugins'; +import { + AffineOfficialWorkspace, + LocalWorkspace, + RemWorkspaceFlavour, +} from '../../../../shared'; +import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal'; +import { LocalWorkspaceIcon } from '../../../pure/icons'; const NoNetWorkIcon = () => { return ( @@ -34,28 +43,34 @@ const IconWrapper = styled.div(() => { }; }); -const getStatus = (workspace: WorkspaceUnit | null) => { +const getStatus = (workspace: AffineOfficialWorkspace) => { if (!navigator.onLine) { return 'offline'; } - if (workspace?.provider === 'local') { + if (workspace.flavour === 'local') { return 'local'; } return 'cloud'; }; + export const SyncUser = () => { - const currentWorkspace = useGlobalState( - useCallback(store => store.currentDataCenterWorkspace, []) - ); - const { triggerEnableWorkspaceModal } = useModal(); + //#region fixme(himself65): remove these hooks ASAP + const [workspace] = useCurrentWorkspace(); + assertExists(workspace); + const router = useRouter(); const [status, setStatus] = useState<'offline' | 'local' | 'cloud'>( - getStatus(currentWorkspace) + getStatus(workspace) ); + const [prevWorkspace, setPrevWorkspace] = useState(workspace); + if (prevWorkspace !== workspace) { + setPrevWorkspace(workspace); + setStatus(getStatus(workspace)); + } useEffect(() => { const online = () => { - setStatus(getStatus(currentWorkspace)); + setStatus(getStatus(workspace)); }; const offline = () => { @@ -67,7 +82,10 @@ export const SyncUser = () => { window.removeEventListener('online', online); window.removeEventListener('offline', offline); }; - }, [currentWorkspace]); + }, [workspace]); + //#endregion + + const [open, setOpen] = useState(false); const { t } = useTranslation(); @@ -86,19 +104,50 @@ export const SyncUser = () => { if (status === 'local') { return ( - - { - triggerEnableWorkspaceModal(); - }} - style={{ marginRight: '12px' }} + <> + - - - + { + setOpen(true); + }} + style={{ marginRight: '12px' }} + > + + + + { + setOpen(false); + }} + onConform={() => { + // todo(himself65): move this function out of affine component + lockMutex(async () => { + assertEquals(workspace.flavour, RemWorkspaceFlavour.LOCAL); + const id = await transformWorkspace( + RemWorkspaceFlavour.LOCAL, + RemWorkspaceFlavour.AFFINE, + workspace as LocalWorkspace + ); + // fixme(himself65): refactor this + router + .replace({ + pathname: `/workspace/[workspaceId]/all`, + query: { + workspaceId: id, + }, + }) + .then(() => { + router.reload(); + }); + setOpen(false); + }); + }} + /> + ); } diff --git a/apps/web/src/components/blocksuite/header/header-right-items/TrashButtonGroup.tsx b/apps/web/src/components/blocksuite/header/header-right-items/TrashButtonGroup.tsx new file mode 100644 index 0000000000..052edf1f0f --- /dev/null +++ b/apps/web/src/components/blocksuite/header/header-right-items/TrashButtonGroup.tsx @@ -0,0 +1,83 @@ +import { Button, Confirm } from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import { assertExists } from '@blocksuite/store'; +import { useRouter } from 'next/router'; +import { useState } from 'react'; + +import { useCurrentPageId } from '../../../../hooks/current/use-current-page-id'; +import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace'; +import { + usePageMeta, + usePageMetaHelper, +} from '../../../../hooks/use-page-meta'; + +export const TrashButtonGroup = () => { + // fixme(himself65): remove these hooks ASAP + const [workspace] = useCurrentWorkspace(); + const [pageId] = useCurrentPageId(); + assertExists(workspace); + assertExists(pageId); + const blockSuiteWorkspace = workspace.blockSuiteWorkspace; + const pageMeta = usePageMeta(blockSuiteWorkspace).find( + meta => meta.id === pageId + ); + assertExists(pageMeta); + const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace); + const router = useRouter(); + // + const [open, setOpen] = useState(false); + + const { t } = useTranslation(); + return ( + <> + + + { + // fixme(himself65): remove these hooks ASAP + router + .push({ + pathname: '/workspace/[workspaceId]/all', + query: { + workspaceId: workspace.id, + }, + }) + .then(() => { + blockSuiteWorkspace.removePage(pageId); + }); + }} + onCancel={() => { + setOpen(false); + }} + onClose={() => { + setOpen(false); + }} + /> + + ); +}; + +export default TrashButtonGroup; diff --git a/apps/web/src/components/header/header-right-items/theme-mode-switch/Icons.tsx b/apps/web/src/components/blocksuite/header/header-right-items/theme-mode-switch/Icons.tsx similarity index 100% rename from apps/web/src/components/header/header-right-items/theme-mode-switch/Icons.tsx rename to apps/web/src/components/blocksuite/header/header-right-items/theme-mode-switch/Icons.tsx diff --git a/apps/web/src/components/header/header-right-items/theme-mode-switch/index.tsx b/apps/web/src/components/blocksuite/header/header-right-items/theme-mode-switch/index.tsx similarity index 94% rename from apps/web/src/components/header/header-right-items/theme-mode-switch/index.tsx rename to apps/web/src/components/blocksuite/header/header-right-items/theme-mode-switch/index.tsx index d2a197a808..edd9a155a3 100644 --- a/apps/web/src/components/header/header-right-items/theme-mode-switch/index.tsx +++ b/apps/web/src/components/blocksuite/header/header-right-items/theme-mode-switch/index.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; -import { useTheme } from '@/providers/ThemeProvider'; - +import { useTheme } from '../../../../../providers/ThemeProvider'; import { MoonIcon, SunIcon } from './Icons'; import { StyledSwitchItem, StyledThemeModeSwitch } from './style'; export const ThemeModeSwitch = () => { diff --git a/apps/web/src/components/header/header-right-items/theme-mode-switch/style.ts b/apps/web/src/components/blocksuite/header/header-right-items/theme-mode-switch/style.ts similarity index 100% rename from apps/web/src/components/header/header-right-items/theme-mode-switch/style.ts rename to apps/web/src/components/blocksuite/header/header-right-items/theme-mode-switch/style.ts diff --git a/apps/web/src/components/header/Header.tsx b/apps/web/src/components/blocksuite/header/header.tsx similarity index 58% rename from apps/web/src/components/header/Header.tsx rename to apps/web/src/components/blocksuite/header/header.tsx index 1190d937ab..f83fad9680 100644 --- a/apps/web/src/components/header/Header.tsx +++ b/apps/web/src/components/blocksuite/header/header.tsx @@ -1,7 +1,7 @@ import { CloseIcon } from '@blocksuite/icons'; -import React, { PropsWithChildren, ReactNode, useState } from 'react'; +import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'; -import EditorOptionMenu from './header-right-items/EditorOptionMenu'; +import { EditorOptionMenu } from './header-right-items/EditorOptionMenu'; import SyncUser from './header-right-items/SyncUser'; import ThemeModeSwitch from './header-right-items/theme-mode-switch'; import TrashButtonGroup from './header-right-items/TrashButtonGroup'; @@ -12,7 +12,7 @@ import { StyledHeaderContainer, StyledHeaderRightSide, } from './styles'; -import { shouldShowWarning, useWarningMessage } from './utils'; +import { OSWarningMessage, shouldShowWarning } from './utils'; const BrowserWarning = ({ show, @@ -23,7 +23,7 @@ const BrowserWarning = ({ }) => { return ( - {useWarningMessage()} + @@ -37,18 +37,25 @@ type HeaderRightItemNames = | 'themeModeSwitch' | 'syncUser'; -const HeaderRightItems: Record = { - editorOptionMenu: , - trashButtonGroup: , - themeModeSwitch: , - syncUser: , +const HeaderRightItems: Record = { + editorOptionMenu: EditorOptionMenu, + trashButtonGroup: TrashButtonGroup, + themeModeSwitch: ThemeModeSwitch, + syncUser: SyncUser, }; -export const Header = ({ +export type HeaderProps = PropsWithChildren<{ + rightItems?: HeaderRightItemNames[]; +}>; + +export const Header: React.FC = ({ rightItems = ['syncUser', 'themeModeSwitch'], children, -}: PropsWithChildren<{ rightItems?: HeaderRightItemNames[] }>) => { - const [showWarning, setShowWarning] = useState(shouldShowWarning()); +}) => { + const [showWarning, setShowWarning] = useState(false); + useEffect(() => { + setShowWarning(shouldShowWarning()); + }, []); return ( {children} - {rightItems.map(itemName => { - return HeaderRightItems[itemName]; - })} + {useMemo( + () => + rightItems.map(itemName => { + const Item = HeaderRightItems[itemName]; + return ; + }), + [rightItems] + )} diff --git a/apps/web/src/components/blocksuite/header/index.tsx b/apps/web/src/components/blocksuite/header/index.tsx new file mode 100644 index 0000000000..eb9877a60b --- /dev/null +++ b/apps/web/src/components/blocksuite/header/index.tsx @@ -0,0 +1,90 @@ +import { Content } from '@affine/component'; +import { assertExists } from '@blocksuite/store'; +import { useSetAtom } from 'jotai'; +import React, { useState } from 'react'; + +import { openQuickSearchModalAtom } from '../../../atoms'; +import { usePageMeta } from '../../../hooks/use-page-meta'; +import { BlockSuiteWorkspace } from '../../../shared'; +import { PageNotFoundError } from '../../affine/affine-error-eoundary'; +import { EditorModeSwitch } from './editor-mode-switch'; +import Header from './header'; +import { QuickSearchButton } from './quick-search-button'; +import { + StyledSearchArrowWrapper, + StyledSwitchWrapper, + StyledTitle, + StyledTitleWrapper, +} from './styles'; + +export type BlockSuiteEditorHeaderProps = React.PropsWithChildren<{ + blockSuiteWorkspace: BlockSuiteWorkspace; + pageId: string; +}>; + +export const BlockSuiteEditorHeader: React.FC = ({ + blockSuiteWorkspace, + pageId, + children, +}) => { + const page = blockSuiteWorkspace.getPage(pageId); + // fixme(himself65): remove this atom and move it to props + const setOpenQuickSearch = useSetAtom(openQuickSearchModalAtom); + if (!page) { + throw new PageNotFoundError(blockSuiteWorkspace, pageId); + } + const pageMeta = usePageMeta(blockSuiteWorkspace).find( + meta => meta.id === pageId + ); + assertExists(pageMeta); + const title = pageMeta.title; + const [isHover, setIsHover] = useState(false); + const { trash: isTrash } = pageMeta; + return ( +
+ {children} + {title && ( + { + if (isTrash) return; + + setIsHover(true); + }} + onMouseLeave={() => { + if (isTrash) return; + + setIsHover(false); + }} + > + + + + + {title} + + { + setOpenQuickSearch(true); + }} + /> + + + + )} +
+ ); +}; diff --git a/apps/web/src/components/header/QuickSearchButton.tsx b/apps/web/src/components/blocksuite/header/quick-search-button/index.tsx similarity index 83% rename from apps/web/src/components/header/QuickSearchButton.tsx rename to apps/web/src/components/blocksuite/header/quick-search-button/index.tsx index 80aa353122..bad22d00af 100644 --- a/apps/web/src/components/header/QuickSearchButton.tsx +++ b/apps/web/src/components/blocksuite/header/quick-search-button/index.tsx @@ -3,8 +3,6 @@ import { styled } from '@affine/component'; import { ArrowDownSmallIcon } from '@blocksuite/icons'; import React from 'react'; -import { useModal } from '@/store/globalModal'; - const StyledIconButtonWithAnimate = styled(IconButton)(({ theme }) => { return { svg: { @@ -20,23 +18,21 @@ const StyledIconButtonWithAnimate = styled(IconButton)(({ theme }) => { }, }; }); + +// fixme(himself65): need to refactor export const QuickSearchButton = ({ onClick, ...props }: Omit) => { - const { triggerQuickSearchModal } = useModal(); return ( { onClick?.(e); - triggerQuickSearchModal(); }} > ); }; - -export default QuickSearchButton; diff --git a/apps/web/src/components/header/styles.ts b/apps/web/src/components/blocksuite/header/styles.ts similarity index 97% rename from apps/web/src/components/header/styles.ts rename to apps/web/src/components/blocksuite/header/styles.ts index c8bce44054..bde4615da9 100644 --- a/apps/web/src/components/header/styles.ts +++ b/apps/web/src/components/blocksuite/header/styles.ts @@ -8,12 +8,12 @@ export const StyledHeaderContainer = styled.div<{ hasWarning: boolean }>( }; } ); -export const StyledHeader = styled.div<{ hasWarning: boolean }>(() => { +export const StyledHeader = styled.div<{ hasWarning: boolean }>(({ theme }) => { return { height: '60px', width: '100%', ...displayFlex('flex-end', 'center'), - background: 'var(--affine-page-background)', + background: theme.colors.pageBackground, transition: 'background-color 0.5s', zIndex: 99, }; diff --git a/apps/web/src/components/header/utils.tsx b/apps/web/src/components/blocksuite/header/utils.tsx similarity index 65% rename from apps/web/src/components/header/utils.tsx rename to apps/web/src/components/blocksuite/header/utils.tsx index ebb54c3958..97b98f068f 100644 --- a/apps/web/src/components/header/utils.tsx +++ b/apps/web/src/components/blocksuite/header/utils.tsx @@ -1,6 +1,8 @@ import { Trans, useTranslation } from '@affine/i18n'; +import React, { useEffect, useState } from 'react'; + +import { getIsMobile } from '../../../utils/get-is-mobile'; -import getIsMobile from '@/utils/get-is-mobile'; // Inspire by https://stackoverflow.com/a/4900484/8415727 const getChromeVersion = () => { const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); @@ -21,9 +23,15 @@ export const shouldShowWarning = () => { ); }; -export const useWarningMessage = () => { +export const OSWarningMessage: React.FC = () => { const { t } = useTranslation(); - if (!getIsChrome()) { + const [notChrome, setNotChrome] = useState(false); + const [notGoodVersion, setNotGoodVersion] = useState(false); + useEffect(() => { + setNotChrome(getIsChrome()); + setNotGoodVersion(getChromeVersion() < minimumChromeVersion); + }, []); + if (notChrome) { return ( @@ -32,9 +40,8 @@ export const useWarningMessage = () => { ); - } - if (getChromeVersion() < minimumChromeVersion) { + } else if (notGoodVersion) { return {t('upgradeBrowser')}; } - return ''; + return null; }; diff --git a/apps/web/src/components/delete-workspace/index.tsx b/apps/web/src/components/delete-workspace/index.tsx deleted file mode 100644 index 586354913d..0000000000 --- a/apps/web/src/components/delete-workspace/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { styled } from '@affine/component'; -import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; -import { Button } from '@affine/component'; -import { Input } from '@affine/component'; -import { useState } from 'react'; - -interface LoginModalProps { - open: boolean; - onClose: () => void; - workSpaceName: string; -} - -export const DeleteModal = ({ - open, - onClose, - workSpaceName, -}: LoginModalProps) => { - const [canDelete, setCanDelete] = useState(true); - const InputChange = (value: string) => { - if (value === workSpaceName) { - setCanDelete(false); - } else { - setCanDelete(true); - } - }; - return ( -
- - -
- { - onClose(); - }} - /> -
- - Delete Workspace -
- This action cannot be undone. This will permanently delete{' '} - {workSpaceName} workspace name along with all its content. -
- - -
-
- - -
-
-
-
- ); -}; - -const Header = styled('div')({ - position: 'relative', - height: '44px', -}); - -const Content = styled('div')({ - display: 'flex', - padding: '0 48px', - flexDirection: 'column', - alignItems: 'center', - gap: '16px', -}); - -const ContentTitle = styled('h1')({ - fontSize: '20px', - lineHeight: '28px', - fontWeight: 600, - textAlign: 'center', - paddingBottom: '16px', -}); - -const Footer = styled('div')({ - height: '70px', - paddingLeft: '24px', - marginTop: '32px', - textAlign: 'center', -}); diff --git a/apps/web/src/components/edgeless-toolbar/Icons.tsx b/apps/web/src/components/edgeless-toolbar/Icons.tsx deleted file mode 100644 index aa04207aad..0000000000 --- a/apps/web/src/components/edgeless-toolbar/Icons.tsx +++ /dev/null @@ -1,151 +0,0 @@ -export const SelectIcon = () => { - return ( - - - - - - - - - - - ); -}; - -export const TextIcon = () => { - return ( - - - - ); -}; - -export const ShapeIcon = () => { - return ( - - - - ); -}; - -export const PenIcon = () => { - return ( - - - - ); -}; - -export const StickerIcon = () => { - return ( - - - - ); -}; - -export const ConnectorIcon = () => { - return ( - - - - ); -}; - -export const UndoIcon = () => { - return ( - - - - ); -}; - -export const RedoIcon = () => { - return ( - - - - ); -}; diff --git a/apps/web/src/components/edgeless-toolbar/index.tsx b/apps/web/src/components/edgeless-toolbar/index.tsx deleted file mode 100644 index 3c1b7fca8b..0000000000 --- a/apps/web/src/components/edgeless-toolbar/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { MuiSlide } from '@affine/component'; -import { Tooltip } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { useEffect, useState } from 'react'; - -import useCurrentPageMeta from '@/hooks/use-current-page-meta'; -import useHistoryUpdated from '@/hooks/use-history-update'; -import { useGlobalState } from '@/store/app'; - -import { - ConnectorIcon, - PenIcon, - RedoIcon, - SelectIcon, - ShapeIcon, - StickerIcon, - TextIcon, - UndoIcon, -} from './Icons'; -import { - StyledEdgelessToolbar, - StyledToolbarItem, - StyledToolbarWrapper, -} from './style'; - -const useToolbarList1 = () => { - const { t } = useTranslation(); - return [ - { - flavor: 'select', - icon: , - toolTip: t('Select'), - disable: false, - callback: () => { - window.dispatchEvent( - new CustomEvent('affine.switch-mouse-mode', { - detail: { - type: 'default', - }, - }) - ); - }, - }, - { - flavor: 'text', - icon: , - toolTip: t('Text'), - disable: true, - }, - { - flavor: 'shape', - icon: , - toolTip: t('Shape'), - disable: false, - callback: () => { - window.dispatchEvent( - new CustomEvent('affine.switch-mouse-mode', { - detail: { - type: 'shape', - color: 'black', - shape: 'rectangle', - }, - }) - ); - }, - }, - { - flavor: 'sticky', - icon: , - toolTip: t('Sticky'), - disable: true, - }, - { - flavor: 'pen', - icon: , - toolTip: t('Pen'), - disable: true, - }, - - { - flavor: 'connector', - icon: , - toolTip: t('Connector'), - disable: true, - }, - ]; -}; - -const UndoRedo = () => { - const [canUndo, setCanUndo] = useState(false); - const [canRedo, setCanRedo] = useState(false); - const currentPage = useGlobalState(store => store.currentPage); - const onHistoryUpdated = useHistoryUpdated(); - const { t } = useTranslation(); - useEffect(() => { - onHistoryUpdated(page => { - setCanUndo(page.canUndo); - setCanRedo(page.canRedo); - }); - }, [onHistoryUpdated]); - - return ( - - - { - currentPage?.undo(); - }} - > - - - - - { - currentPage?.redo(); - }} - > - - - - - ); -}; - -export const EdgelessToolbar = () => { - const { mode } = useCurrentPageMeta() || {}; - - return ( - - - - {useToolbarList1().map( - ({ icon, toolTip, flavor, disable, callback }, index) => { - return ( - - { - console.log('click toolbar button:', flavor); - callback?.(); - }} - > - {icon} - - - ); - } - )} - - - - - ); -}; - -export default EdgelessToolbar; diff --git a/apps/web/src/components/edgeless-toolbar/reply.svg b/apps/web/src/components/edgeless-toolbar/reply.svg deleted file mode 100644 index 7b9f8f2a2e..0000000000 --- a/apps/web/src/components/edgeless-toolbar/reply.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/web/src/components/edgeless-toolbar/style.ts b/apps/web/src/components/edgeless-toolbar/style.ts deleted file mode 100644 index 920ded54ff..0000000000 --- a/apps/web/src/components/edgeless-toolbar/style.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { displayFlex, styled } from '@affine/component'; - -export const StyledEdgelessToolbar = styled.div(({ theme }) => ({ - height: '320px', - position: 'absolute', - left: '12px', - top: 0, - bottom: 0, - margin: 'auto', - zIndex: theme.zIndex.modal - 1, -})); - -export const StyledToolbarWrapper = styled.div(({ theme }) => ({ - width: '44px', - borderRadius: '10px', - boxShadow: theme.shadow.modal, - padding: '4px', - background: theme.colors.popoverBackground, - transition: 'background .5s', - marginBottom: '12px', -})); - -export const StyledToolbarItem = styled.div<{ - disable?: boolean; -}>(({ theme, disable = false }) => ({ - width: '36px', - height: '36px', - ...displayFlex('center', 'center'), - color: disable ? theme.colors.disableColor : theme.colors.iconColor, - cursor: disable ? 'not-allowed' : 'pointer', - svg: { - width: '36px', - height: '36px', - }, - ':hover': disable - ? {} - : { - color: theme.colors.primaryColor, - background: theme.colors.hoverBackground, - }, -})); diff --git a/apps/web/src/components/editor/index.tsx b/apps/web/src/components/editor/index.tsx deleted file mode 100644 index 25968ee52a..0000000000 --- a/apps/web/src/components/editor/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import '@blocksuite/blocks'; - -import { styled } from '@affine/component'; -import { EditorContainer } from '@blocksuite/editor'; -import type { Page, Workspace } from '@blocksuite/store'; -import { useEffect, useRef } from 'react'; - -const StyledEditorContainer = styled('div')(() => { - return { - position: 'relative', - height: 'calc(100% - 60px)', - padding: '0 32px', - }; -}); - -type Props = { - page: Page; - workspace: Workspace; - setEditor: (editor: EditorContainer) => void; - templateMarkdown?: string; - templateTitle?: string; -}; - -export const Editor = ({ - page, - workspace, - setEditor, - templateMarkdown, - templateTitle = '', -}: Props) => { - const editorContainer = useRef(null); - useEffect(() => { - const ret = () => { - const node = editorContainer.current; - while (node?.firstChild) { - node.removeChild(node.firstChild); - } - }; - - const editor = new EditorContainer(); - editor.page = page; - editor.mode = page.meta.mode as typeof editor.mode; - - editorContainer.current?.appendChild(editor); - if (page.isEmpty) { - // Can not use useCurrentPageMeta to get new title, cause meta title will trigger rerender, but the second time can not remove title - const { title: metaTitle } = page.meta; - const title = metaTitle ? metaTitle : templateTitle; - workspace?.setPageMeta(page.id, { title }); - const pageBlockId = page.addBlockByFlavour('affine:page', { title }); - page.addBlockByFlavour('affine:surface', {}, null); - // Add frame block inside page block - const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId); - // Add paragraph block inside frame block - // If this is a first page in workspace, init an introduction markdown - if (templateMarkdown) { - editor.clipboard.importMarkdown(templateMarkdown, frameId); - workspace.setPageMeta(page.id, { title }); - } else { - page.addBlockByFlavour('affine:paragraph', {}, frameId); - } - page.resetHistory(); - } - - setEditor(editor); - return ret; - }, [workspace, page, setEditor, templateTitle, templateMarkdown]); - - return ; -}; - -export default Editor; diff --git a/apps/web/src/components/header/EditorHeader.tsx b/apps/web/src/components/header/EditorHeader.tsx deleted file mode 100644 index 9134132795..0000000000 --- a/apps/web/src/components/header/EditorHeader.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Content } from '@affine/component'; -import React, { useEffect, useState } from 'react'; - -import EditorModeSwitch from '@/components/editor-mode-switch'; -import useCurrentPageMeta from '@/hooks/use-current-page-meta'; -import usePropsUpdated from '@/hooks/use-props-updated'; -import { useGlobalState } from '@/store/app'; - -import Header from './Header'; -import QuickSearchButton from './QuickSearchButton'; -import { - StyledSearchArrowWrapper, - StyledSwitchWrapper, - StyledTitle, - StyledTitleWrapper, -} from './styles'; - -export const EditorHeader = () => { - const [title, setTitle] = useState(''); - const [isHover, setIsHover] = useState(false); - const editor = useGlobalState(store => store.editor); - const { trash: isTrash = false } = useCurrentPageMeta() || {}; - const onPropsUpdated = usePropsUpdated(); - - useEffect(() => { - onPropsUpdated(editor => { - setTitle(editor.pageBlockModel?.title || 'Untitled'); - }); - }, [onPropsUpdated]); - - useEffect(() => { - setTimeout(() => { - // If first time in, need to wait for editor to be inserted into DOM - setTitle(editor?.pageBlockModel?.title || 'Untitled'); - }, 300); - }, [editor]); - - return ( -
- {title && ( - { - if (isTrash) return; - - setIsHover(true); - }} - onMouseLeave={() => { - if (isTrash) return; - - setIsHover(false); - }} - > - - - - - {title} - - - - - - )} -
- ); -}; - -export default EditorHeader; diff --git a/apps/web/src/components/header/PageListHeader.tsx b/apps/web/src/components/header/PageListHeader.tsx deleted file mode 100644 index c863c01ca3..0000000000 --- a/apps/web/src/components/header/PageListHeader.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { PropsWithChildren, ReactNode } from 'react'; - -import Header from './Header'; -import QuickSearchButton from './QuickSearchButton'; -import { StyledPageListTittleWrapper } from './styles'; -// import QuickSearchButton from './QuickSearchButton'; - -export type PageListHeaderProps = PropsWithChildren<{ - icon?: ReactNode; -}>; -export const PageListHeader = ({ icon, children }: PageListHeaderProps) => { - return ( -
- - {icon} - {children} - - -
- ); -}; - -export default PageListHeader; diff --git a/apps/web/src/components/header/header-right-items/TrashButtonGroup.tsx b/apps/web/src/components/header/header-right-items/TrashButtonGroup.tsx deleted file mode 100644 index 05eb00287a..0000000000 --- a/apps/web/src/components/header/header-right-items/TrashButtonGroup.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Button } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { useRouter } from 'next/router'; -import { useCallback } from 'react'; - -import useCurrentPageMeta from '@/hooks/use-current-page-meta'; -import { usePageHelper } from '@/hooks/use-page-helper'; -import { useConfirm } from '@/providers/ConfirmProvider'; -import { useGlobalState } from '@/store/app'; - -export const TrashButtonGroup = () => { - const { permanentlyDeletePage } = usePageHelper(); - const currentWorkspace = useGlobalState( - useCallback(store => store.currentDataCenterWorkspace, []) - ); - const { toggleDeletePage } = usePageHelper(); - const confirm = useConfirm(store => store.confirm); - const router = useRouter(); - const { id = '' } = useCurrentPageMeta() || {}; - const { t } = useTranslation(); - return ( - <> - - - - ); -}; - -export default TrashButtonGroup; diff --git a/apps/web/src/components/header/index.tsx b/apps/web/src/components/header/index.tsx deleted file mode 100644 index ceb0e0d347..0000000000 --- a/apps/web/src/components/header/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export * from './EditorHeader'; -export * from './Header'; -export * from './PageListHeader'; diff --git a/apps/web/src/components/help-island/index.tsx b/apps/web/src/components/help-island/index.tsx deleted file mode 100644 index 6b497577a1..0000000000 --- a/apps/web/src/components/help-island/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Tooltip } from '@affine/component'; -import { MuiFade } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { CloseIcon } from '@blocksuite/icons'; -import { useEffect, useState } from 'react'; - -import { useGlobalState } from '@/store/app'; -import { useModal } from '@/store/globalModal'; - -import { ContactIcon, HelpIcon, KeyboardIcon } from './Icons'; -import { - StyledAnimateWrapper, - StyledIconWrapper, - StyledIsland, - StyledTriggerWrapper, -} from './style'; -export type IslandItemNames = 'contact' | 'shortcuts'; -export const HelpIsland = ({ - showList = ['contact', 'shortcuts'], -}: { - showList?: IslandItemNames[]; -}) => { - const [spread, setShowSpread] = useState(false); - const { triggerShortcutsModal, triggerContactModal } = useModal(); - const blockHub = useGlobalState(store => store.blockHub); - const { t } = useTranslation(); - - useEffect(() => { - blockHub?.blockHubStatusUpdated.on(status => { - if (status) { - setShowSpread(false); - } - }); - return () => { - blockHub?.blockHubStatusUpdated.dispose(); - }; - }, [blockHub]); - - useEffect(() => { - spread && blockHub?.toggleMenu(false); - }, [blockHub, spread]); - return ( - { - setShowSpread(!spread); - }} - > - - {showList.includes('contact') && ( - - { - setShowSpread(false); - triggerContactModal(); - }} - > - - - - )} - {showList.includes('shortcuts') && ( - - { - setShowSpread(false); - triggerShortcutsModal(); - }} - > - - - - )} - - - - - - - - - - - - - - - - ); -}; - -export default HelpIsland; diff --git a/apps/web/src/components/import/index.tsx b/apps/web/src/components/import/index.tsx deleted file mode 100644 index e77236a2b0..0000000000 --- a/apps/web/src/components/import/index.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; -import { Button } from '@affine/component'; -import { Content, FlexWrapper } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { useCallback, useEffect, useState } from 'react'; - -import Loading from '@/components/loading'; -import { usePageHelper } from '@/hooks/use-page-helper'; -import { useGlobalState } from '@/store/app'; - -import { StyledButtonWrapper, StyledTitle } from './styles'; -// import { Tooltip } from '@affine/component'; -type ImportModalProps = { - open: boolean; - onClose: () => void; -}; -type Template = { - name: string; - source: string; -}; -export const ImportModal = ({ open, onClose }: ImportModalProps) => { - const [status, setStatus] = useState<'unImported' | 'importing'>('importing'); - const { openPage, createPage } = usePageHelper(); - const currentWorkspace = useGlobalState( - useCallback(store => store.currentDataCenterWorkspace, []) - ); - const { t } = useTranslation(); - const _applyTemplate = function (pageId: string, template: Template) { - const page = currentWorkspace?.blocksuiteWorkspace?.getPage(pageId); - - const title = template.name; - if (page) { - currentWorkspace?.blocksuiteWorkspace?.setPageMeta(page.id, { title }); - if (page.root === null) { - setTimeout(() => { - try { - const editor = document.querySelector('editor-container'); - if (editor) { - page.addBlock({ flavour: 'affine:surface' }, null); - const frameId = page.addBlock( - { flavour: 'affine:frame' }, - pageId - ); - // TODO blocksuite should offer a method to import markdown from store - editor.clipboard.importMarkdown(template.source, `${frameId}`); - page.resetHistory(); - editor.requestUpdate(); - } - } catch (e) { - console.error(e); - } - }, 300); - } - } - }; - const _handleAppleTemplate = async function (template: Template) { - const pageId = await createPage(); - if (pageId) { - openPage(pageId); - _applyTemplate(pageId, template); - } - }; - const _handleAppleTemplateFromFilePicker = async () => { - if (!window.showOpenFilePicker) { - return; - } - const arrFileHandle = await window.showOpenFilePicker({ - types: [ - { - accept: { - 'text/markdown': ['.md'], - 'text/html': ['.html', '.htm'], - 'text/plain': ['.text'], - }, - }, - ], - multiple: false, - }); - for (const fileHandle of arrFileHandle) { - const file = await fileHandle.getFile(); - const text = await file.text(); - _handleAppleTemplate({ - name: file.name, - source: text, - }); - } - onClose && onClose(); - }; - useEffect(() => { - if (status === 'importing') { - setTimeout(() => { - setStatus('unImported'); - }, 1500); - } - }, [status]); - - return ( - - - - {t('Import')} - - {status === 'unImported' && ( - - - - - )} - - {status === 'importing' && ( - - - - OOOOPS! Sorry forgot to remind you that we are working on the - import function - - - )} - - - ); -}; - -export default ImportModal; diff --git a/apps/web/src/components/import/styles.ts b/apps/web/src/components/import/styles.ts deleted file mode 100644 index 1665d28991..0000000000 --- a/apps/web/src/components/import/styles.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { styled } from '@affine/component'; - -export const StyledTitle = styled.div(({ theme }) => { - return { - fontSize: theme.font.h6, - fontWeight: 600, - textAlign: 'center', - marginTop: '45px', - color: theme.colors.popoverColor, - }; -}); - -export const StyledButtonWrapper = styled.div(() => { - return { - width: '280px', - margin: '24px auto 0', - button: { - display: 'block', - width: '100%', - ':not(:last-child)': { - marginBottom: '16px', - }, - }, - }; -}); diff --git a/apps/web/src/components/login-modal/GoogleIcon.tsx b/apps/web/src/components/login-modal/GoogleIcon.tsx deleted file mode 100644 index f0454955cb..0000000000 --- a/apps/web/src/components/login-modal/GoogleIcon.tsx +++ /dev/null @@ -1,28 +0,0 @@ -export const GoogleIcon = () => { - return ( - - - - - - - ); -}; diff --git a/apps/web/src/components/login-modal/index.tsx b/apps/web/src/components/login-modal/index.tsx deleted file mode 100644 index 0fbfd7b11b..0000000000 --- a/apps/web/src/components/login-modal/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { positionAbsolute, styled } from '@affine/component'; -import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; -import { Button } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; - -import { useGlobalState } from '@/store/app'; - -import { GoogleIcon } from './GoogleIcon'; -interface LoginModalProps { - open: boolean; - onClose: () => void; -} - -export const LoginModal = ({ open, onClose }: LoginModalProps) => { - const login = useGlobalState(store => store.login); - const { t } = useTranslation(); - return ( - - - { - onClose(); - }} - /> - - {t('Sign in')} - {t('Set up an AFFiNE account to sync data')} - { - await login(); - onClose(); - }} - > - - {t('Continue with Google')} - - - - - ); -}; - -const StyledLoginButton = styled(Button)(() => { - return { - width: '284px', - marginTop: '30px', - position: 'relative', - svg: { - ...positionAbsolute({ left: '18px', top: '0', bottom: '0' }), - margin: 'auto', - }, - }; -}); - -const Content = styled('div')({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '16px', -}); - -const ContentTitle = styled('h1')({ - fontSize: '20px', - lineHeight: '28px', - fontWeight: 600, - textAlign: 'center', - paddingBottom: '16px', -}); - -const SignDes = styled('div')(({ theme }) => { - return { - fontWeight: 400, - color: theme.colors.textColor, - fontSize: '16px', - }; -}); diff --git a/apps/web/src/components/logout-modal/icon.tsx b/apps/web/src/components/logout-modal/icon.tsx deleted file mode 100644 index afca63f57f..0000000000 --- a/apps/web/src/components/logout-modal/icon.tsx +++ /dev/null @@ -1,48 +0,0 @@ -export const Check = () => { - return ( - - - - - - - - - - - - - ); -}; - -export const UnCheck = () => { - return ( - - - - - - ); -}; diff --git a/apps/web/src/components/logout-modal/index.tsx b/apps/web/src/components/logout-modal/index.tsx deleted file mode 100644 index 4028df3c1a..0000000000 --- a/apps/web/src/components/logout-modal/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { styled } from '@affine/component'; -import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; -import { Button } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { useState } from 'react'; - -import { useAppState } from '@/providers/app-state-provider'; - -import { Check, UnCheck } from './icon'; -interface LoginModalProps { - open: boolean; - onClose: (wait: boolean) => void; -} - -export const LogoutModal = ({ open, onClose }: LoginModalProps) => { - const [localCache, setLocalCache] = useState(true); - const { blobDataSynced } = useAppState(); - const { t } = useTranslation(); - - return ( - - -
- { - onClose(true); - }} - /> -
- - {t('Sign out')}? - - {blobDataSynced - ? t('Sign out description') - : t('All data has been stored in the cloud')} - - - {localCache ? ( - { - setLocalCache(false); - }} - > - - - ) : ( - { - setLocalCache(true); - }} - > - - - )} - {t('Retain cached cloud data')} - - {blobDataSynced ? ( -
- - -
- ) : ( -
- - -
- )} -
-
-
- ); -}; - -const Header = styled('div')({ - position: 'relative', - height: '44px', -}); - -const Content = styled('div')({ - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - gap: '16px', -}); - -const ContentTitle = styled('h1')({ - fontSize: '20px', - lineHeight: '28px', - fontWeight: 600, - textAlign: 'center', - paddingBottom: '16px', -}); - -const SignDes = styled('div')(({ theme }) => { - return { - fontWeight: 400, - color: theme.colors.textColor, - fontSize: '16px', - }; -}); - -const StyleCheck = styled('span')(() => { - return { - display: 'inline-block', - cursor: 'pointer', - - svg: { - verticalAlign: 'sub', - marginRight: '8px', - }, - }; -}); - -const StyleTips = styled('span')(() => { - return { - userSelect: 'none', - }; -}); diff --git a/apps/web/src/components/message-center-handler/index.tsx b/apps/web/src/components/message-center-handler/index.tsx deleted file mode 100644 index f747d9ebf0..0000000000 --- a/apps/web/src/components/message-center-handler/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { toast } from '@affine/component'; -import { DataCenter, MessageCenter } from '@affine/datacenter'; -import { AffineProvider } from '@affine/datacenter'; -import { DebugLogger } from '@affine/debug'; -import { useRouter } from 'next/router'; -import { ReactNode, useCallback, useEffect } from 'react'; - -import { useGlobalState } from '@/store/app'; - -const logger = new DebugLogger('messageCenter'); - -const clearAuth = (dataCenter: DataCenter, providerName: string) => { - const affineProvider = dataCenter.providers.find(p => p.id === providerName); - if (affineProvider && affineProvider instanceof AffineProvider) { - affineProvider.apis.auth.clear(); - } else { - logger.error('cannot find affine provider, please fix this ASAP'); - } -}; -export function MessageCenterHandler({ children }: { children?: ReactNode }) { - const router = useRouter(); - const dataCenter = useGlobalState(useCallback(store => store.dataCenter, [])); - useEffect(() => { - const instance = MessageCenter.getInstance(); - if (instance) { - return instance.onMessage(async message => { - if (message.code === MessageCenter.messageCode.noPermission) { - // todo: translate message - // todo: more specific message for accessing different resources - // todo: error toast style - toast('You have no permission to access this workspace'); - // todo(himself65): remove dynamic lookup - clearAuth(dataCenter, 'affine'); - // the status of the app right now is unknown, and it won't help if we let - // the app continue and let the user auth the app. - // that's why so we need to reload the page for now. - // - // fix: a better option is to keep loading the app, and prompt the user to login - // or perhaps displaying page 401? - await router.push('/'); - router.reload(); - } - - if (message.code === MessageCenter.messageCode.refreshTokenError) { - toast('Session expired, please log in again'); - clearAuth(dataCenter, 'affine'); - await router.push('/'); - router.reload(); - } - }); - } - }, [dataCenter, router]); - - return <>{children}; -} diff --git a/apps/web/src/components/mobile-modal/bg.png b/apps/web/src/components/mobile-modal/bg.png deleted file mode 100644 index ab9c6725a6..0000000000 Binary files a/apps/web/src/components/mobile-modal/bg.png and /dev/null differ diff --git a/apps/web/src/components/mobile-modal/index.tsx b/apps/web/src/components/mobile-modal/index.tsx deleted file mode 100644 index 9a7a83277d..0000000000 --- a/apps/web/src/components/mobile-modal/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import React, { useState } from 'react'; - -import getIsMobile from '@/utils/get-is-mobile'; - -import bg from './bg.png'; -import { StyledButton, StyledContent, StyledTitle } from './styles'; -export const MobileModal = () => { - const [showModal, setShowModal] = useState(getIsMobile()); - const { t } = useTranslation(); - return ( - { - setShowModal(false); - }} - > - - { - setShowModal(false); - }} - /> - - {t('Ooops!')} - -

{t('mobile device')}

-

{t('mobile device description')}

-
- { - setShowModal(false); - }} - > - {t('Got it')} - -
-
- ); -}; - -export default MobileModal; diff --git a/apps/web/src/components/mobile-modal/styles.ts b/apps/web/src/components/mobile-modal/styles.ts deleted file mode 100644 index 771a6b585c..0000000000 --- a/apps/web/src/components/mobile-modal/styles.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { displayFlex, styled } from '@affine/component'; - -export const StyledTitle = styled.div(() => { - return { - ...displayFlex('center', 'center'), - fontSize: '20px', - fontWeight: 500, - marginTop: '60px', - lineHeight: 1, - }; -}); - -export const StyledContent = styled.div(() => { - return { - padding: '0 40px', - marginTop: '32px', - fontSize: '18px', - lineHeight: '25px', - 'p:not(last-of-type)': { - marginBottom: '10px', - }, - }; -}); - -export const StyledButton = styled.div(({ theme }) => { - return { - width: '146px', - height: '42px', - background: theme.colors.primaryColor, - color: '#FFFFFF', - fontSize: '18px', - fontWeight: 500, - borderRadius: '21px', - margin: '52px auto 0', - cursor: 'pointer', - ...displayFlex('center', 'center'), - }; -}); diff --git a/apps/web/src/components/page-detail-editor.tsx b/apps/web/src/components/page-detail-editor.tsx new file mode 100644 index 0000000000..5bcb8c7bd8 --- /dev/null +++ b/apps/web/src/components/page-detail-editor.tsx @@ -0,0 +1,65 @@ +import type { EditorContainer } from '@blocksuite/editor'; +import { assertExists, Page } from '@blocksuite/store'; +import dynamic from 'next/dynamic'; +import React from 'react'; +import { Helmet } from 'react-helmet-async'; + +import { useBlockSuiteWorkspacePageTitle } from '../hooks/use-blocksuite-workspace-page-title'; +import { usePageMeta } from '../hooks/use-page-meta'; +import { BlockSuiteWorkspace } from '../shared'; +import { PageNotFoundError } from './affine/affine-error-eoundary'; +import { BlockSuiteEditorHeader } from './blocksuite/header'; + +export type PageDetailEditorProps = { + blockSuiteWorkspace: BlockSuiteWorkspace; + pageId: string; + onInit?: (page: Page, editor: Readonly) => void; + onLoad?: (page: Page, editor: EditorContainer) => void; + header?: React.ReactNode; +}; + +const Editor = dynamic( + async () => + (await import('./blocksuite/block-suite-editor')).BlockSuiteEditor, + { + ssr: false, + } +); + +export const PageDetailEditor: React.FC = ({ + blockSuiteWorkspace, + pageId, + onInit, + onLoad, + header, +}) => { + const page = blockSuiteWorkspace.getPage(pageId); + if (!page) { + throw new PageNotFoundError(blockSuiteWorkspace, pageId); + } + const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, pageId); + const meta = usePageMeta(blockSuiteWorkspace).find( + meta => meta.id === pageId + ); + assertExists(meta); + return ( + <> + + {title} + + + {header} + + + + ); +}; diff --git a/apps/web/src/components/page-list/OperationCell.tsx b/apps/web/src/components/page-list/OperationCell.tsx deleted file mode 100644 index 82f476bcef..0000000000 --- a/apps/web/src/components/page-list/OperationCell.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { - FlexWrapper, - IconButton, - Menu, - MenuItem, - Tooltip, -} from '@affine/component'; -import { toast } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { - DeletePermanentlyIcon, - DeleteTemporarilyIcon, - FavoritedIcon, - FavoriteIcon, - MoreVerticalIcon, - OpenInNewIcon, - ResetIcon, -} from '@blocksuite/icons'; - -import { usePageHelper } from '@/hooks/use-page-helper'; -import { PageMeta } from '@/providers/app-state-provider'; -import { useConfirm } from '@/providers/ConfirmProvider'; - -export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => { - const { id, favorite } = pageMeta; - const { openPage } = usePageHelper(); - const { toggleFavoritePage, toggleDeletePage } = usePageHelper(); - const confirm = useConfirm(store => store.confirm); - const { t } = useTranslation(); - const OperationMenu = ( - <> - { - toggleFavoritePage(id); - toast( - favorite ? t('Removed from Favorites') : t('Added to Favorites') - ); - }} - icon={favorite ? : } - > - {favorite ? t('Remove from favorites') : t('Add to Favorites')} - - { - openPage(id, {}, true); - }} - icon={} - > - {t('Open in new tab')} - - { - confirm({ - title: t('Delete page?'), - content: t('will be moved to Trash', { - title: pageMeta.title || 'Untitled', - }), - confirmText: t('Delete'), - confirmType: 'danger', - }).then(confirm => { - confirm && toggleDeletePage(id); - confirm && toast(t('Moved to Trash')); - }); - }} - icon={} - > - {t('Delete')} - - - ); - return ( - - - - - - - - ); -}; - -export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => { - const { id } = pageMeta; - const { openPage, getPageMeta } = usePageHelper(); - const { toggleDeletePage, permanentlyDeletePage } = usePageHelper(); - const confirm = useConfirm(store => store.confirm); - const { t } = useTranslation(); - return ( - - - { - toggleDeletePage(id); - toast( - t('restored', { title: getPageMeta(id)?.title || 'Untitled' }) - ); - openPage(id); - }} - > - - - - - { - confirm({ - title: t('Delete permanently?'), - content: t("Once deleted, you can't undo this action."), - confirmText: t('Delete'), - confirmType: 'danger', - }).then(confirm => { - confirm && permanentlyDeletePage(id); - toast(t('Permanently deleted')); - }); - }} - > - - - - - ); -}; diff --git a/apps/web/src/components/page-list/index.tsx b/apps/web/src/components/page-list/index.tsx deleted file mode 100644 index eeb30c505e..0000000000 --- a/apps/web/src/components/page-list/index.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { - Table, - TableBody, - TableCell, - TableHead, - TableRow, -} from '@affine/component'; -import { Content, IconButton, toast, Tooltip } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { - EdgelessIcon, - FavoritedIcon, - FavoriteIcon, - PaperIcon, -} from '@blocksuite/icons'; -import { useRouter } from 'next/router'; -import React, { useCallback } from 'react'; - -import DateCell from '@/components/page-list/DateCell'; -import { usePageHelper } from '@/hooks/use-page-helper'; -import { PageMeta } from '@/providers/app-state-provider'; -import { useTheme } from '@/providers/ThemeProvider'; -import { useGlobalState } from '@/store/app'; - -import Empty from './Empty'; -import { OperationCell, TrashOperationCell } from './OperationCell'; -import { - StyledTableContainer, - StyledTableRow, - StyledTitleLink, - StyledTitleWrapper, -} from './styles'; -const FavoriteTag = ({ - pageMeta: { favorite, id }, -}: { - pageMeta: PageMeta; -}) => { - const { toggleFavoritePage } = usePageHelper(); - const { theme } = useTheme(); - const { t } = useTranslation(); - return ( - - { - e.stopPropagation(); - toggleFavoritePage(id); - toast( - favorite ? t('Removed from Favorites') : t('Added to Favorites') - ); - }} - style={{ - color: favorite ? theme.colors.primaryColor : theme.colors.iconColor, - }} - className={favorite ? '' : 'favorite-button'} - > - {favorite ? ( - - ) : ( - - )} - - - ); -}; - -export const PageList = ({ - pageList, - showFavoriteTag = false, - isTrash = false, - isPublic = false, - listType, -}: { - pageList: PageMeta[]; - showFavoriteTag?: boolean; - isTrash?: boolean; - isPublic?: boolean; - listType?: 'all' | 'trash' | 'favorite'; -}) => { - const router = useRouter(); - const currentWorkspace = useGlobalState( - useCallback(store => store.currentDataCenterWorkspace, []) - ); - const { t } = useTranslation(); - if (pageList.length === 0) { - return ; - } - - return ( - - - - - {t('Title')} - {t('Created')} - - {isTrash ? t('Moved to Trash') : t('Updated')} - - - - - - {pageList.map((pageMeta, index) => { - // On click event must be set on the table cell, since the last operation cell is not clickable, and if set on the row, the menu will have bug on close. - const onClick = () => { - if (isPublic) { - router.push( - `/public-workspace/${router.query.workspaceId}/${pageMeta.id}` - ); - } else { - router.push( - `/workspace/${currentWorkspace?.id}/${pageMeta.id}` - ); - } - }; - return ( - - - - - {pageMeta.mode === 'edgeless' ? ( - - ) : ( - - )} - - {pageMeta.title || t('Untitled')} - - - {showFavoriteTag && } - - - - - {!isPublic && ( - - {isTrash ? ( - - ) : ( - - )} - - )} - - ); - })} - -
-
- ); -}; - -export default PageList; diff --git a/apps/web/src/components/provider-composer.ts b/apps/web/src/components/provider-composer.tsx similarity index 90% rename from apps/web/src/components/provider-composer.ts rename to apps/web/src/components/provider-composer.tsx index 188d0b76fb..935b89f602 100644 --- a/apps/web/src/components/provider-composer.ts +++ b/apps/web/src/components/provider-composer.tsx @@ -12,5 +12,3 @@ export const ProviderComposer: FC< }), children ); - -export default ProviderComposer; diff --git a/apps/web/src/components/contact-modal/Icons.tsx b/apps/web/src/components/pure/contact-modal/Icons.tsx similarity index 100% rename from apps/web/src/components/contact-modal/Icons.tsx rename to apps/web/src/components/pure/contact-modal/Icons.tsx diff --git a/apps/web/src/components/contact-modal/affine-text-logo.png b/apps/web/src/components/pure/contact-modal/affine-text-logo.png similarity index 100% rename from apps/web/src/components/contact-modal/affine-text-logo.png rename to apps/web/src/components/pure/contact-modal/affine-text-logo.png diff --git a/apps/web/src/components/contact-modal/index.tsx b/apps/web/src/components/pure/contact-modal/index.tsx similarity index 100% rename from apps/web/src/components/contact-modal/index.tsx rename to apps/web/src/components/pure/contact-modal/index.tsx diff --git a/apps/web/src/components/contact-modal/style.ts b/apps/web/src/components/pure/contact-modal/style.ts similarity index 100% rename from apps/web/src/components/contact-modal/style.ts rename to apps/web/src/components/pure/contact-modal/style.ts diff --git a/apps/web/src/components/create-workspace/index.tsx b/apps/web/src/components/pure/create-workspace-modal/index.tsx similarity index 70% rename from apps/web/src/components/create-workspace/index.tsx rename to apps/web/src/components/pure/create-workspace-modal/index.tsx index e20ef4954e..361a3c5368 100644 --- a/apps/web/src/components/create-workspace/index.tsx +++ b/apps/web/src/components/pure/create-workspace-modal/index.tsx @@ -2,42 +2,34 @@ import { styled } from '@affine/component'; import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; import { Button } from '@affine/component'; import { Input } from '@affine/component'; -import { toast } from '@affine/component'; import { useTranslation } from '@affine/i18n'; -import { useRouter } from 'next/router'; -import { useRef, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { KeyboardEvent } from 'react'; -import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; - interface ModalProps { open: boolean; onClose: () => void; + onCreate: (name: string) => void; } -export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => { +export const CreateWorkspaceModal = ({ + open, + onClose, + onCreate, +}: ModalProps) => { const [workspaceName, setWorkspaceName] = useState(''); - const [loading, setLoading] = useState(false); - const { createWorkspace } = useWorkspaceHelper(); const isComposition = useRef(false); - const router = useRouter(); - const handleCreateWorkspace = async () => { - setLoading(true); - const workspace = await createWorkspace(workspaceName); - - if (workspace && workspace.id) { - setLoading(false); - router.replace(`/workspace/${workspace.id}`); - onClose(); - } else { - toast('create error'); - } - }; - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Enter' && workspaceName && !isComposition.current) { - handleCreateWorkspace(); - } - }; + const handleCreateWorkspace = useCallback(() => { + onCreate(workspaceName); + }, [onCreate, workspaceName]); + const handleKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'Enter' && workspaceName && !isComposition.current) { + handleCreateWorkspace(); + } + }, + [handleCreateWorkspace, workspaceName] + ); const { t } = useTranslation(); return (
@@ -78,7 +70,6 @@ export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => { marginTop: '16px', opacity: !workspaceName ? 0.5 : 1, }} - loading={loading} type="primary" onClick={() => { handleCreateWorkspace(); @@ -121,10 +112,3 @@ const ContentTitle = styled('div')(() => { paddingBottom: '16px', }; }); - -// const Footer = styled('div')({ -// height: '70px', -// paddingLeft: '24px', -// marginTop: '32px', -// textAlign: 'center', -// }); diff --git a/apps/web/src/components/file-upload/index.tsx b/apps/web/src/components/pure/file-upload/index.tsx similarity index 80% rename from apps/web/src/components/file-upload/index.tsx rename to apps/web/src/components/pure/file-upload/index.tsx index d79c9faa81..e195b7d32c 100644 --- a/apps/web/src/components/file-upload/index.tsx +++ b/apps/web/src/components/pure/file-upload/index.tsx @@ -1,15 +1,19 @@ import { Button } from '@affine/component'; import { styled } from '@affine/component'; import { useTranslation } from '@affine/i18n'; -import { ChangeEvent, FC, ReactElement, useRef } from 'react'; -interface Props { +import React, { ChangeEvent, useRef } from 'react'; + +export type UploadProps = React.PropsWithChildren<{ uploadType?: string; - children?: ReactElement; accept?: string; fileChange: (file: File) => void; -} -export const Upload: FC = props => { - const { fileChange, accept } = props; +}>; + +export const Upload: React.FC = ({ + fileChange, + accept, + children, +}) => { const { t } = useTranslation(); const input_ref = useRef(null); const _chooseFile = () => { @@ -30,7 +34,7 @@ export const Upload: FC = props => { }; return ( - {props.children ?? } + {children ?? } void; + onLogout: () => void; +}; + +export const Footer: React.FC = ({ user, onLogin, onLogout }) => { + const { t } = useTranslation(); + + return ( + + {user && ( + <> + + + +

{user.name}

+

{user.email}

+
+
+ + { + onLogout(); + }} + > + + + + + )} + + {!user && ( + + +
+ } + onClick={async () => { + onLogin(); + }} + > + {t('Sign in')} + + )} + + ); +}; + +interface WorkspaceAvatarProps { + size: number; + name: string; + avatar: string; + style?: CSSProperties; +} + +export const WorkspaceAvatar: React.FC = props => { + const size = props.size || 20; + const sizeStr = size + 'px'; + + return ( + <> + {props.avatar ? ( +
+ + + +
+ ) : ( +
+ {(props.name || 'AFFiNE').substring(0, 1)} +
+ )} + + ); +}; diff --git a/apps/web/src/components/workspace-modal/styles.ts b/apps/web/src/components/pure/footer/styles.ts similarity index 97% rename from apps/web/src/components/workspace-modal/styles.ts rename to apps/web/src/components/pure/footer/styles.ts index e9620b4cc5..c49b55b807 100644 --- a/apps/web/src/components/workspace-modal/styles.ts +++ b/apps/web/src/components/pure/footer/styles.ts @@ -90,7 +90,7 @@ export const StyleUserInfo = styled.div(({ theme }) => { lineHeight: '24px', color: theme.colors.iconColor, }, - 'p:first-child': { + 'p:first-of-type': { color: theme.colors.textColor, fontWeight: 600, }, @@ -166,8 +166,6 @@ export const StyledSignInButton = styled(Button)(({ theme }) => { backgroundColor: theme.colors.innerHoverBackground, flexShrink: 0, marginRight: '16px', - fontSize: '24px', - color: theme.colors.primaryColor, ...displayInlineFlex('center', 'center'), }, }; diff --git a/apps/web/src/components/help-island/Icons.tsx b/apps/web/src/components/pure/help-island/Icons.tsx similarity index 100% rename from apps/web/src/components/help-island/Icons.tsx rename to apps/web/src/components/pure/help-island/Icons.tsx diff --git a/apps/web/src/components/pure/help-island/index.tsx b/apps/web/src/components/pure/help-island/index.tsx new file mode 100644 index 0000000000..31fa9c04ad --- /dev/null +++ b/apps/web/src/components/pure/help-island/index.tsx @@ -0,0 +1,103 @@ +import { Tooltip } from '@affine/component'; +import { MuiFade } from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import { CloseIcon } from '@blocksuite/icons'; +import { useState } from 'react'; + +import ContactModal from '../contact-modal'; +import { ShortcutsModal } from '../shortcuts-modal'; +import { ContactIcon, HelpIcon, KeyboardIcon } from './Icons'; +import { + StyledAnimateWrapper, + StyledIconWrapper, + StyledIsland, + StyledTriggerWrapper, +} from './style'; +export type IslandItemNames = 'contact' | 'shortcuts'; +export const HelpIsland = ({ + showList = ['contact', 'shortcuts'], +}: { + showList?: IslandItemNames[]; +}) => { + const [spread, setShowSpread] = useState(false); + // const { triggerShortcutsModal, triggerContactModal } = useModal(); + // const blockHub = useGlobalState(store => store.blockHub); + const { t } = useTranslation(); + // + // useEffect(() => { + // blockHub?.blockHubStatusUpdated.on(status => { + // if (status) { + // setShowSpread(false); + // } + // }); + // return () => { + // blockHub?.blockHubStatusUpdated.dispose(); + // }; + // }, [blockHub]); + // + // useEffect(() => { + // spread && blockHub?.toggleMenu(false); + // }, [blockHub, spread]); + const [open, setOpen] = useState(false); + const [openShortCut, setOpenShortCut] = useState(false); + return ( + <> + { + setShowSpread(!spread); + }} + > + + {showList.includes('contact') && ( + + { + setShowSpread(false); + setOpen(true); + }} + > + + + + )} + {showList.includes('shortcuts') && ( + + { + setShowSpread(false); + setOpenShortCut(true); + }} + > + + + + )} + + + + + + + + + + + + + + + + setOpen(false)} /> + setOpenShortCut(false)} + /> + + ); +}; diff --git a/apps/web/src/components/help-island/style.ts b/apps/web/src/components/pure/help-island/style.ts similarity index 100% rename from apps/web/src/components/help-island/style.ts rename to apps/web/src/components/pure/help-island/style.ts diff --git a/apps/web/src/components/icons/index.tsx b/apps/web/src/components/pure/icons/index.tsx similarity index 100% rename from apps/web/src/components/icons/index.tsx rename to apps/web/src/components/pure/icons/index.tsx diff --git a/apps/web/src/components/loading/Loading.tsx b/apps/web/src/components/pure/loading/Loading.tsx similarity index 100% rename from apps/web/src/components/loading/Loading.tsx rename to apps/web/src/components/pure/loading/Loading.tsx diff --git a/apps/web/src/components/loading/PageLoading.tsx b/apps/web/src/components/pure/loading/PageLoading.tsx similarity index 100% rename from apps/web/src/components/loading/PageLoading.tsx rename to apps/web/src/components/pure/loading/PageLoading.tsx diff --git a/apps/web/src/components/loading/index.tsx b/apps/web/src/components/pure/loading/index.tsx similarity index 100% rename from apps/web/src/components/loading/index.tsx rename to apps/web/src/components/pure/loading/index.tsx diff --git a/apps/web/src/components/loading/styled.ts b/apps/web/src/components/pure/loading/styled.ts similarity index 100% rename from apps/web/src/components/loading/styled.ts rename to apps/web/src/components/pure/loading/styled.ts diff --git a/apps/web/src/components/pure/quick-search-modal/Footer.tsx b/apps/web/src/components/pure/quick-search-modal/Footer.tsx new file mode 100644 index 0000000000..eb3e03b99d --- /dev/null +++ b/apps/web/src/components/pure/quick-search-modal/Footer.tsx @@ -0,0 +1,55 @@ +import { useTranslation } from '@affine/i18n'; +import { PlusIcon } from '@blocksuite/icons'; +import { assertEquals, uuidv4 } from '@blocksuite/store'; +import { Command } from 'cmdk'; +import { NextRouter } from 'next/router'; +import React from 'react'; + +import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper'; +import { BlockSuiteWorkspace } from '../../../shared'; +import { StyledModalFooterContent } from './style'; + +export type FooterProps = { + query: string; + onClose: () => void; + blockSuiteWorkspace: BlockSuiteWorkspace; + router: NextRouter; +}; + +export const Footer: React.FC = ({ + query, + onClose, + blockSuiteWorkspace, + router, +}) => { + const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); + const { t } = useTranslation(); + + return ( + { + onClose(); + const id = uuidv4(); + const pageId = await createPage(id, query); + assertEquals(pageId, id); + router.push({ + pathname: '/workspace/[workspaceId]/[pageId]', + query: { + workspaceId: blockSuiteWorkspace.room, + pageId, + }, + }); + }} + > + + + {query ? ( + {t('New Keyword Page', { query: query })} + ) : ( + {t('New Page')} + )} + + + ); +}; diff --git a/apps/web/src/components/pure/quick-search-modal/Input.tsx b/apps/web/src/components/pure/quick-search-modal/Input.tsx new file mode 100644 index 0000000000..0e80359baf --- /dev/null +++ b/apps/web/src/components/pure/quick-search-modal/Input.tsx @@ -0,0 +1,86 @@ +import { useTranslation } from '@affine/i18n'; +import { SearchIcon } from '@blocksuite/icons'; +import { Command } from 'cmdk'; +import React, { useEffect, useRef, useState } from 'react'; + +import { StyledInputContent, StyledLabel } from './style'; +export const Input = (props: { + open: boolean; + query: string; + setQuery: (query: string) => void; + isPublic: boolean; + publishWorkspaceName: string | undefined; +}) => { + const { open, query, setQuery, isPublic, publishWorkspaceName } = props; + const [isComposition, setIsComposition] = useState(false); + const [inputValue, setInputValue] = useState(''); + const inputRef = useRef(null); + const { t } = useTranslation(); + useEffect(() => { + if (open) { + const inputElement = inputRef.current; + return inputElement?.focus(); + } + }, [open]); + useEffect(() => { + const inputElement = inputRef.current; + if (!open) { + return; + } + const handleFocus = () => { + inputElement?.focus(); + }; + inputElement?.addEventListener('blur', handleFocus, true); + return () => inputElement?.removeEventListener('blur', handleFocus, true); + }, [inputRef, open]); + useEffect(() => { + setInputValue(query); + }, [query]); + return ( + + + + + { + setIsComposition(true); + }} + onCompositionEnd={e => { + setQuery(e.data); + setIsComposition(false); + }} + onValueChange={str => { + setInputValue(str); + if (!isComposition) { + setQuery(str); + } + }} + onKeyDown={(e: React.KeyboardEvent) => { + if (e.key === 'a' && e.metaKey) { + e.stopPropagation(); + inputRef.current?.select(); + return; + } + if (isComposition) { + if ( + e.key === 'ArrowDown' || + e.key === 'ArrowUp' || + e.key === 'Enter' + ) { + e.stopPropagation(); + } + } + }} + placeholder={ + isPublic + ? t('Quick search placeholder2', { + workspace: publishWorkspaceName, + }) + : t('Quick search placeholder') + } + /> + + ); +}; diff --git a/apps/web/src/components/quick-search/NoResultSVG.tsx b/apps/web/src/components/pure/quick-search-modal/NoResultSVG.tsx similarity index 99% rename from apps/web/src/components/quick-search/NoResultSVG.tsx rename to apps/web/src/components/pure/quick-search-modal/NoResultSVG.tsx index aa546549a6..b25bb01ad6 100644 --- a/apps/web/src/components/quick-search/NoResultSVG.tsx +++ b/apps/web/src/components/pure/quick-search-modal/NoResultSVG.tsx @@ -1,4 +1,6 @@ -export const NoResultSVG = () => { +import { memo } from 'react'; + +export const NoResultSVG = memo(function NoResultSVG() { return ( { ); -}; +}); diff --git a/apps/web/src/components/pure/quick-search-modal/PublishedResults.tsx b/apps/web/src/components/pure/quick-search-modal/PublishedResults.tsx new file mode 100644 index 0000000000..518e0e1ca4 --- /dev/null +++ b/apps/web/src/components/pure/quick-search-modal/PublishedResults.tsx @@ -0,0 +1,97 @@ +import { useTranslation } from '@affine/i18n'; +import { EdgelessIcon, PaperIcon } from '@blocksuite/icons'; +import { Command } from 'cmdk'; +import { useRouter } from 'next/router'; +import React, { useEffect, useMemo, useState } from 'react'; + +import { usePageMeta } from '../../../hooks/use-page-meta'; +import { BlockSuiteWorkspace } from '../../../shared'; +import { NoResultSVG } from './NoResultSVG'; +import { StyledListItem, StyledNotFound } from './style'; + +export type PublishedResultsProps = { + query: string; + loading: boolean; + setPublishWorkspaceName: (name: string) => void; + onClose: () => void; + blockSuiteWorkspace: BlockSuiteWorkspace; +}; + +export const PublishedResults: React.FC = ({ + query, + loading, + onClose, + setPublishWorkspaceName, + blockSuiteWorkspace, +}) => { + const [results, setResults] = useState(new Map()); + const router = useRouter(); + const pageList = usePageMeta(blockSuiteWorkspace); + // useEffect(() => { + // dataCenter + // .loadPublicWorkspace(router.query.workspaceId as string) + // .then(data => { + // setPageList(data.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]); + // if (data.blocksuiteWorkspace) { + // setWorkspace(data.blocksuiteWorkspace); + // setPublishWorkspaceName(data.blocksuiteWorkspace.meta.name); + // } + // }) + // .catch(() => { + // router.push('/404'); + // }); + // }, [router, dataCenter, setPublishWorkspaceName]); + const { t } = useTranslation(); + useEffect(() => { + setResults(blockSuiteWorkspace.search(query)); + //Save the Map obtained from the search as state + }, [blockSuiteWorkspace, query, setResults]); + const pageIds = useMemo(() => [...results.values()], [results]); + const resultsPageMeta = useMemo( + () => pageList.filter(page => pageIds.indexOf(page.id) > -1 && !page.trash), + [pageIds, pageList] + ); + + return loading ? null : ( + <> + {query ? ( + resultsPageMeta.length ? ( + + {resultsPageMeta.map(result => { + return ( + { + router.push( + `/public-workspace/${router.query.workspaceId}/${result.id}` + ); + onClose(); + }} + value={result.id} + > + + {result.mode === 'edgeless' ? ( + + ) : ( + + )} + {result.title} + + + ); + })} + + ) : ( + + {t('Find 0 result')} + + + ) + ) : ( + <> + )} + + ); +}; diff --git a/apps/web/src/components/pure/quick-search-modal/Results.tsx b/apps/web/src/components/pure/quick-search-modal/Results.tsx new file mode 100644 index 0000000000..9c08072592 --- /dev/null +++ b/apps/web/src/components/pure/quick-search-modal/Results.tsx @@ -0,0 +1,118 @@ +import { useTranslation } from '@affine/i18n'; +import { EdgelessIcon, PaperIcon } from '@blocksuite/icons'; +import { assertExists } from '@blocksuite/store'; +import { Command } from 'cmdk'; +import { NextRouter } from 'next/router'; +import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; + +import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper'; +import { usePageMeta } from '../../../hooks/use-page-meta'; +import { BlockSuiteWorkspace } from '../../../shared'; +import { useSwitchToConfig } from './config'; +import { NoResultSVG } from './NoResultSVG'; +import { StyledListItem, StyledNotFound } from './style'; + +export type ResultsProps = { + blockSuiteWorkspace: BlockSuiteWorkspace; + query: string; + loading: boolean; + onClose: () => void; + setShowCreatePage: Dispatch>; + router: NextRouter; +}; +export const Results: React.FC = ({ + query, + loading, + blockSuiteWorkspace, + setShowCreatePage, + router, + onClose, +}) => { + useBlockSuiteWorkspaceHelper(blockSuiteWorkspace); + const pageList = usePageMeta(blockSuiteWorkspace); + assertExists(blockSuiteWorkspace.room); + const List = useSwitchToConfig(blockSuiteWorkspace.room); + const [results, setResults] = useState(new Map()); + const { t } = useTranslation(); + useEffect(() => { + setResults(blockSuiteWorkspace.search(query)); + //Save the Map obtained from the search as state + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [query, setResults]); + const pageIds = [...results.values()]; + + const resultsPageMeta = pageList.filter( + page => pageIds.indexOf(page.id) > -1 && !page.trash + ); + + useEffect(() => { + setShowCreatePage(!resultsPageMeta.length); + //Determine whether to display the ‘+ New page’ + }, [resultsPageMeta.length, setShowCreatePage]); + return loading ? null : ( + <> + {query ? ( + resultsPageMeta.length ? ( + + {resultsPageMeta.map(result => { + return ( + { + onClose(); + assertExists(blockSuiteWorkspace.room); + // fixme: refactor to `useRouterHelper` + router.push({ + pathname: '/workspace/[workspaceId]/[pageId]', + query: { + workspaceId: blockSuiteWorkspace.room, + pageId: result.id, + }, + }); + }} + value={result.id} + > + + {result.mode === 'edgeless' ? ( + + ) : ( + + )} + {result.title} + + + ); + })} + + ) : ( + + {t('Find 0 result')} + + + ) + ) : ( + + {List.map(link => { + return ( + { + onClose(); + router.push(link.href); + }} + > + + + {link.title} + + + ); + })} + + )} + + ); +}; diff --git a/apps/web/src/components/pure/quick-search-modal/config.ts b/apps/web/src/components/pure/quick-search-modal/config.ts new file mode 100644 index 0000000000..e7aafee937 --- /dev/null +++ b/apps/web/src/components/pure/quick-search-modal/config.ts @@ -0,0 +1,44 @@ +import { useTranslation } from '@affine/i18n'; +import { + DeleteTemporarilyIcon, + FavoriteIcon, + FolderIcon, + SettingsIcon, +} from '@blocksuite/icons'; +import { FC, SVGProps, useMemo } from 'react'; + +import { pathGenerator } from '../../../shared'; +export const useSwitchToConfig = ( + workspaceId: string +): { + title: string; + href: string; + icon: FC>; +}[] => { + const { t } = useTranslation(); + return useMemo( + () => [ + { + title: t('All pages'), + href: pathGenerator.all(workspaceId), + icon: FolderIcon, + }, + { + title: t('Favorites'), + href: pathGenerator.favorite(workspaceId), + icon: FavoriteIcon, + }, + { + title: t('Workspace Settings'), + href: pathGenerator.setting(workspaceId), + icon: SettingsIcon, + }, + { + title: t('Trash'), + href: pathGenerator.trash(workspaceId), + icon: DeleteTemporarilyIcon, + }, + ], + [workspaceId, t] + ); +}; diff --git a/apps/web/src/components/quick-search/index.tsx b/apps/web/src/components/pure/quick-search-modal/index.tsx similarity index 52% rename from apps/web/src/components/quick-search/index.tsx rename to apps/web/src/components/pure/quick-search-modal/index.tsx index 252bc98ead..1d40bf8548 100644 --- a/apps/web/src/components/quick-search/index.tsx +++ b/apps/web/src/components/pure/quick-search-modal/index.tsx @@ -1,16 +1,20 @@ import { Modal, ModalWrapper } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; import { Command } from 'cmdk'; -import { useRouter } from 'next/router'; -import { useEffect, useRef, useState } from 'react'; - -import { useModal } from '@/store/globalModal'; -import { getUaHelper } from '@/utils'; +import { NextRouter } from 'next/router'; +import React, { + useCallback, + useEffect, + useMemo, + useState, + useTransition, +} from 'react'; +import { BlockSuiteWorkspace } from '../../../shared'; +import { getUaHelper } from '../../../utils/useragent'; import { Footer } from './Footer'; +import { Input } from './Input'; import { PublishedResults } from './PublishedResults'; import { Results } from './Results'; -import { SearchInput } from './SearchInput'; import { StyledContent, StyledModalDivider, @@ -19,78 +23,69 @@ import { StyledShortcut, } from './style'; -type TransitionsModalProps = { - open: boolean; - onClose: () => void; -}; - const isMac = () => { return getUaHelper().isMacOs; }; -// fixme(himself65): support ssr -export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => { - const router = useRouter(); - const { t } = useTranslation(); - const inputRef = useRef(null); - const [query, setQuery] = useState(''); - const [isPublic, setIsPublic] = useState(false); +export type QuickSearchModalProps = { + blockSuiteWorkspace: BlockSuiteWorkspace; + enableShortCut: boolean; + open: boolean; + setOpen: (value: boolean) => void; + router: NextRouter; +}; + +export const QuickSearchModal: React.FC = ({ + open, + setOpen, + router, + enableShortCut, + blockSuiteWorkspace, +}) => { + const [loading, startTransition] = useTransition(); + const [query, _setQuery] = useState(''); + const setQuery = useCallback((query: string) => { + startTransition(() => { + _setQuery(query); + }); + }, []); + const isPublicWorkspace = useMemo( + () => router.pathname.startsWith('/public-workspace'), + [router] + ); const [publishWorkspaceName, setPublishWorkspaceName] = useState(''); const [showCreatePage, setShowCreatePage] = useState(true); - const { triggerQuickSearchModal } = useModal(); - const isPublicAndNoQuery = () => { - return isPublic && query.length === 0; - }; - const handleClose = () => { - onClose(); - }; + const isPublicAndNoQuery = useCallback(() => { + return isPublicWorkspace && query.length === 0; + }, [isPublicWorkspace, query.length]); + const handleClose = useCallback(() => { + setQuery(''); + setOpen(false); + }, [setOpen, setQuery]); // Add ‘⌘+K’ shortcut keys as switches useEffect(() => { - if (router.pathname.startsWith('/404')) { + if (!enableShortCut) { return; } - const down = (e: KeyboardEvent) => { + const keydown = (e: KeyboardEvent) => { if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) { const selection = window.getSelection(); // prevent search bar focus in firefox e.preventDefault(); setQuery(''); if (selection?.toString()) { - triggerQuickSearchModal(false); + setOpen(false); return; } if (selection?.isCollapsed) { - triggerQuickSearchModal(!open); + setOpen(!open); } } }; - document.addEventListener('keydown', down, { capture: true }); + document.addEventListener('keydown', keydown, { capture: true }); return () => - document.removeEventListener('keydown', down, { capture: true }); - }, [open, router, triggerQuickSearchModal]); - - useEffect(() => { - if (router.pathname.startsWith('/public-workspace')) { - return setIsPublic(true); - } else { - return setIsPublic(false); - } - }, [router]); - useEffect(() => { - if (router.pathname.startsWith('/404')) { - onClose(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useEffect(() => { - if (open) { - // Waiting for DOM rendering - requestAnimationFrame(() => { - const inputElement = inputRef.current; - inputElement?.focus(); - }); - } - }, [open]); + document.removeEventListener('keydown', keydown, { capture: true }); + }, [enableShortCut, open, router, setOpen, setQuery]); return ( { }} > - { - setQuery(value); - }} - onKeyDown={e => { - // Avoid triggering the cmdk onSelect event when the input method is in use - if (e.nativeEvent.isComposing) { - e.stopPropagation(); - return; - } - }} - placeholder={ - isPublic - ? t('Quick search placeholder2', { - workspace: publishWorkspaceName, - }) - : t('Quick search placeholder') - } + {isMac() ? '⌘ + K' : 'Ctrl + K'} @@ -151,27 +133,37 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => { - {!isPublic ? ( + {!isPublicWorkspace ? ( ) : ( )} - {!isPublic ? ( + {!isPublicWorkspace ? ( showCreatePage ? ( <> -
+
) : null @@ -183,4 +175,4 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => { ); }; -export default QuickSearch; +export default QuickSearchModal; diff --git a/apps/web/src/components/quick-search/style.ts b/apps/web/src/components/pure/quick-search-modal/style.ts similarity index 100% rename from apps/web/src/components/quick-search/style.ts rename to apps/web/src/components/pure/quick-search-modal/style.ts diff --git a/apps/web/src/components/shortcuts-modal/Icons.tsx b/apps/web/src/components/pure/shortcuts-modal/Icons.tsx similarity index 100% rename from apps/web/src/components/shortcuts-modal/Icons.tsx rename to apps/web/src/components/pure/shortcuts-modal/Icons.tsx diff --git a/apps/web/src/components/shortcuts-modal/config.ts b/apps/web/src/components/pure/shortcuts-modal/config.ts similarity index 100% rename from apps/web/src/components/shortcuts-modal/config.ts rename to apps/web/src/components/pure/shortcuts-modal/config.ts diff --git a/apps/web/src/components/shortcuts-modal/index.tsx b/apps/web/src/components/pure/shortcuts-modal/index.tsx similarity index 86% rename from apps/web/src/components/shortcuts-modal/index.tsx rename to apps/web/src/components/pure/shortcuts-modal/index.tsx index 87fa4c118b..8d89ba0eec 100644 --- a/apps/web/src/components/shortcuts-modal/index.tsx +++ b/apps/web/src/components/pure/shortcuts-modal/index.tsx @@ -4,16 +4,15 @@ import { MuiSlide, } from '@affine/component'; import { useTranslation } from '@affine/i18n'; -import { createPortal } from 'react-dom'; +import { useEffect, useState } from 'react'; +import { getUaHelper } from '../../../utils/useragent'; import { useMacKeyboardShortcuts, useMacMarkdownShortcuts, useWindowsKeyboardShortcuts, useWinMarkdownShortcuts, -} from '@/components/shortcuts-modal/config'; -import { getUaHelper } from '@/utils'; - +} from './config'; import { KeyboardIcon } from './Icons'; import { StyledListItem, @@ -27,25 +26,27 @@ type ModalProps = { onClose: () => void; }; -const isMac = () => { +const checkIsMac = () => { return getUaHelper().isMacOs; }; -// fixme(himself65): support ssr export const ShortcutsModal = ({ open, onClose }: ModalProps) => { const { t } = useTranslation(); const macMarkdownShortcuts = useMacMarkdownShortcuts(); const winMarkdownShortcuts = useWinMarkdownShortcuts(); const macKeyboardShortcuts = useMacKeyboardShortcuts(); const windowsKeyboardShortcuts = useWindowsKeyboardShortcuts(); - const markdownShortcuts = isMac() - ? macMarkdownShortcuts - : winMarkdownShortcuts; - const keyboardShortcuts = isMac() + const [isMac, setIsMac] = useState(false); + const markdownShortcuts = isMac ? macMarkdownShortcuts : winMarkdownShortcuts; + const keyboardShortcuts = isMac ? macKeyboardShortcuts : windowsKeyboardShortcuts; - return createPortal( + useEffect(() => { + setIsMac(checkIsMac()); + }, []); + + return ( {
- , - document.body + ); }; - -export default ShortcutsModal; diff --git a/apps/web/src/components/shortcuts-modal/style.ts b/apps/web/src/components/pure/shortcuts-modal/style.ts similarity index 100% rename from apps/web/src/components/shortcuts-modal/style.ts rename to apps/web/src/components/pure/shortcuts-modal/style.ts diff --git a/apps/web/src/components/pure/workspace-avatar/index.tsx b/apps/web/src/components/pure/workspace-avatar/index.tsx new file mode 100644 index 0000000000..6650eb91ed --- /dev/null +++ b/apps/web/src/components/pure/workspace-avatar/index.tsx @@ -0,0 +1,114 @@ +import React from 'react'; + +import { useWorkspaceBlobImage } from '../../../hooks/use-workspace-blob'; +import { BlockSuiteWorkspace, RemWorkspace } from '../../../shared'; +import { stringToColour } from '../../../utils'; + +interface AvatarProps { + size: number; + name: string; + avatar_url: string; + style?: React.CSSProperties; +} + +export const Avatar: React.FC = React.memo( + function Avatar(props) { + const size = props.size || 20; + const sizeStr = size + 'px'; + + return ( + <> + {props.avatar_url ? ( +
+ + + +
+ ) : ( +
+ {(props.name || 'AFFiNE').substring(0, 1)} +
+ )} + + ); + } +); + +export type WorkspaceUnitAvatarProps = { + size?: number; + workspace: RemWorkspace | null; + style?: React.CSSProperties; +}; + +export type BlockSuiteWorkspaceAvatar = Omit< + WorkspaceUnitAvatarProps, + 'workspace' +> & { + workspace: BlockSuiteWorkspace; +}; + +export const BlockSuiteWorkspaceAvatar: React.FC = ({ + size = 20, + workspace, + style, +}) => { + const avatarURL = useWorkspaceBlobImage(workspace.meta.avatar, workspace); + return ( + + ); +}; + +export const WorkspaceAvatar: React.FC = ({ + size = 20, + workspace, + style, + ...props +}) => { + if (workspace && 'blockSuiteWorkspace' in workspace) { + return ( + + ); + } + return ; +}; diff --git a/apps/web/src/components/pure/workspace-card/index.tsx b/apps/web/src/components/pure/workspace-card/index.tsx new file mode 100644 index 0000000000..f4c81b7f5f --- /dev/null +++ b/apps/web/src/components/pure/workspace-card/index.tsx @@ -0,0 +1,101 @@ +import { PermissionType } from '@affine/datacenter'; +import { useTranslation } from '@affine/i18n'; +import React, { useCallback } from 'react'; + +import { RemWorkspace, RemWorkspaceFlavour } from '../../../shared'; +import { + CloudWorkspaceIcon, + JoinedWorkspaceIcon, + LocalDataIcon, + LocalWorkspaceIcon, + PublishIcon, +} from '../icons'; +import { WorkspaceAvatar } from '../workspace-avatar'; +import { StyledCard, StyleWorkspaceInfo, StyleWorkspaceTitle } from './styles'; + +export type WorkspaceTypeProps = { + workspace: RemWorkspace; +}; + +const WorkspaceType: React.FC = ({ workspace }) => { + const { t } = useTranslation(); + let isOwner = true; + if (workspace.flavour === RemWorkspaceFlavour.AFFINE) { + isOwner = workspace.permission === PermissionType.Owner; + } else if (workspace.flavour === RemWorkspaceFlavour.LOCAL) { + isOwner = true; + } + + if (workspace.flavour === RemWorkspaceFlavour.LOCAL) { + return ( +

+ + {t('Local Workspace')} +

+ ); + } + + return isOwner ? ( +

+ + {t('Cloud Workspace')} +

+ ) : ( +

+ + {t('Joined Workspace')} +

+ ); +}; + +export type WorkspaceCardProps = { + currentWorkspaceId: string | null; + workspace: RemWorkspace; + onClick: (workspace: RemWorkspace) => void; +}; + +export const WorkspaceCard: React.FC = ({ + workspace, + onClick, + currentWorkspaceId, +}) => { + const { t } = useTranslation(); + let name = 'UNKNOWN'; + if (workspace.flavour === RemWorkspaceFlavour.LOCAL) { + name = workspace.blockSuiteWorkspace.meta.name; + } else if (workspace.flavour === RemWorkspaceFlavour.AFFINE) { + if (workspace.firstBinarySynced) { + name = workspace.blockSuiteWorkspace.meta.name; + } + } + + return ( + { + onClick(workspace); + }, [onClick, workspace])} + active={workspace.id === currentWorkspaceId} + > + + + + {name} + + {workspace.flavour === RemWorkspaceFlavour.LOCAL && ( +

+ + {t('Available Offline')} +

+ )} + {workspace.flavour === RemWorkspaceFlavour.AFFINE && + workspace.public && ( +

+ + {t('Published to Web')} +

+ )} +
+
+ ); +}; diff --git a/apps/web/src/components/pure/workspace-card/styles.ts b/apps/web/src/components/pure/workspace-card/styles.ts new file mode 100644 index 0000000000..c49b55b807 --- /dev/null +++ b/apps/web/src/components/pure/workspace-card/styles.ts @@ -0,0 +1,172 @@ +import { + displayFlex, + displayInlineFlex, + styled, + textEllipsis, +} from '@affine/component'; +import { Button } from '@affine/component'; + +export const StyledSplitLine = styled.div(({ theme }) => { + return { + width: '1px', + height: '20px', + background: theme.colors.borderColor, + marginRight: '24px', + }; +}); + +export const StyleWorkspaceInfo = styled.div(({ theme }) => { + return { + marginLeft: '15px', + width: '202px', + p: { + color: theme.colors.popoverColor, + height: '20px', + fontSize: theme.font.sm, + ...displayFlex('flex-start', 'center'), + }, + svg: { + marginRight: '10px', + fontSize: '16px', + flexShrink: 0, + }, + span: { + flexGrow: 1, + ...textEllipsis(1), + }, + }; +}); + +export const StyleWorkspaceTitle = styled.div(({ theme }) => { + return { + fontSize: theme.font.base, + fontWeight: 600, + lineHeight: '24px', + marginBottom: '10px', + maxWidth: '200px', + ...textEllipsis(1), + }; +}); + +export const StyledCard = styled.div<{ + active?: boolean; +}>(({ theme, active }) => { + const borderColor = active ? theme.colors.primaryColor : 'transparent'; + return { + width: '310px', + height: '124px', + cursor: 'pointer', + padding: '16px', + boxShadow: '0px 0px 8px rgba(0, 0, 0, 0.1)', + borderRadius: '12px', + border: `1px solid ${borderColor}`, + ...displayFlex('flex-start', 'flex-start'), + marginBottom: '24px', + transition: 'background .2s', + background: theme.mode === 'light' ? '#FFF' : '#2C2C2C', + ':hover': { + background: theme.colors.cardHoverBackground, + '.add-icon': { + borderColor: theme.colors.primaryColor, + color: theme.colors.primaryColor, + }, + }, + }; +}); + +export const StyledFooter = styled('div')({ + height: '84px', + padding: '0 40px', + flexShrink: 0, + ...displayFlex('space-between', 'center'), +}); + +export const StyleUserInfo = styled.div(({ theme }) => { + return { + textAlign: 'left', + marginLeft: '16px', + flex: 1, + p: { + lineHeight: '24px', + color: theme.colors.iconColor, + }, + 'p:first-of-type': { + color: theme.colors.textColor, + fontWeight: 600, + }, + }; +}); + +export const StyledModalHeaderLeft = styled.div(() => { + return { ...displayFlex('flex-start', 'center') }; +}); +export const StyledModalTitle = styled.div(({ theme }) => { + return { + fontWeight: 600, + fontSize: theme.font.h6, + }; +}); + +export const StyledHelperContainer = styled.div(({ theme }) => { + return { + color: theme.colors.iconColor, + marginLeft: '15px', + fontWeight: 400, + fontSize: theme.font.h6, + ...displayFlex('center', 'center'), + }; +}); + +export const StyledModalContent = styled('div')({ + height: '534px', + padding: '8px 40px', + marginTop: '72px', + overflow: 'auto', + ...displayFlex('space-between', 'flex-start', 'flex-start'), + flexWrap: 'wrap', +}); +export const StyledOperationWrapper = styled.div(() => { + return { + ...displayFlex('flex-end', 'center'), + }; +}); + +export const StyleWorkspaceAdd = styled.div(() => { + return { + width: '58px', + height: '58px', + borderRadius: '100%', + background: '#f4f5fa', + border: '1.5px dashed #f4f5fa', + transition: 'background .2s', + ...displayFlex('center', 'center'), + }; +}); +export const StyledModalHeader = styled('div')(() => { + return { + width: '100%', + height: '72px', + position: 'absolute', + left: 0, + top: 0, + borderRadius: '24px 24px 0 0', + padding: '0 40px', + ...displayFlex('space-between', 'center'), + }; +}); + +export const StyledSignInButton = styled(Button)(({ theme }) => { + return { + fontWeight: 700, + paddingLeft: 0, + '.circle': { + width: '40px', + height: '40px', + borderRadius: '20px', + backgroundColor: theme.colors.innerHoverBackground, + flexShrink: 0, + marginRight: '16px', + ...displayInlineFlex('center', 'center'), + }, + }; +}); diff --git a/apps/web/src/components/pure/workspace-list-modal/index.tsx b/apps/web/src/components/pure/workspace-list-modal/index.tsx new file mode 100644 index 0000000000..66aabd083f --- /dev/null +++ b/apps/web/src/components/pure/workspace-list-modal/index.tsx @@ -0,0 +1,118 @@ +import { + Modal, + ModalCloseButton, + ModalWrapper, + Tooltip, +} from '@affine/component'; +import { AccessTokenMessage } from '@affine/datacenter'; +import { useTranslation } from '@affine/i18n'; +import { HelpIcon, PlusIcon } from '@blocksuite/icons'; + +import { RemWorkspace } from '../../../shared'; +import { Footer } from '../footer'; +import { WorkspaceCard } from '../workspace-card'; +import { LanguageMenu } from './language-menu'; +import { + StyledCard, + StyledHelperContainer, + StyledModalContent, + StyledModalHeader, + StyledModalHeaderLeft, + StyledModalTitle, + StyledOperationWrapper, + StyledSplitLine, + StyleWorkspaceAdd, + StyleWorkspaceInfo, + StyleWorkspaceTitle, +} from './styles'; + +interface WorkspaceModalProps { + user: AccessTokenMessage | null; + workspaces: RemWorkspace[]; + currentWorkspaceId: RemWorkspace['id'] | null; + open: boolean; + onClose: () => void; + onClickWorkspace: (workspace: RemWorkspace) => void; + onClickLogin: () => void; + onClickLogout: () => void; + onCreateWorkspace: () => void; +} + +export const WorkspaceListModal = ({ + open, + onClose, + workspaces, + user, + onClickLogin, + onClickLogout, + onClickWorkspace, + onCreateWorkspace, + currentWorkspaceId, +}: WorkspaceModalProps) => { + const { t } = useTranslation(); + + return ( + + + + + {t('My Workspaces')} + + + + + + + + + + + { + onClose(); + }} + absolute={false} + /> + + + + + {workspaces.map(workspace => { + return ( + + ); + })} + + + + + + + {t('New Workspace')} +

{t('Create Or Import')}

+
+
+
+ +
+ + + ); +}; diff --git a/apps/web/src/components/workspace-modal/SelectLanguageMenu.tsx b/apps/web/src/components/pure/workspace-list-modal/language-menu.tsx similarity index 79% rename from apps/web/src/components/workspace-modal/SelectLanguageMenu.tsx rename to apps/web/src/components/pure/workspace-list-modal/language-menu.tsx index 2b6a5e7bb9..7d4c9aa74e 100644 --- a/apps/web/src/components/workspace-modal/SelectLanguageMenu.tsx +++ b/apps/web/src/components/pure/workspace-list-modal/language-menu.tsx @@ -1,15 +1,17 @@ -import { styled } from '@affine/component'; -import { Button } from '@affine/component'; -import { Menu, MenuItem } from '@affine/component'; +import { Button, Menu, MenuItem, styled } from '@affine/component'; import { LOCALES } from '@affine/i18n'; import { useTranslation } from '@affine/i18n'; import { ArrowDownSmallIcon } from '@blocksuite/icons'; +import React, { useCallback } from 'react'; -const LanguageMenuContent = () => { +const LanguageMenuContent: React.FC = () => { const { i18n } = useTranslation(); - const changeLanguage = (event: string) => { - i18n.changeLanguage(event); - }; + const changeLanguage = useCallback( + (event: string) => { + i18n.changeLanguage(event); + }, + [i18n] + ); return ( <> {LOCALES.map(option => { @@ -28,7 +30,7 @@ const LanguageMenuContent = () => { ); }; -export const LanguageMenu = () => { +export const LanguageMenu: React.FC = () => { const { i18n } = useTranslation(); const currentLanguage = LOCALES.find(item => item.tag === i18n.language); diff --git a/apps/web/src/components/pure/workspace-list-modal/styles.ts b/apps/web/src/components/pure/workspace-list-modal/styles.ts new file mode 100644 index 0000000000..c49b55b807 --- /dev/null +++ b/apps/web/src/components/pure/workspace-list-modal/styles.ts @@ -0,0 +1,172 @@ +import { + displayFlex, + displayInlineFlex, + styled, + textEllipsis, +} from '@affine/component'; +import { Button } from '@affine/component'; + +export const StyledSplitLine = styled.div(({ theme }) => { + return { + width: '1px', + height: '20px', + background: theme.colors.borderColor, + marginRight: '24px', + }; +}); + +export const StyleWorkspaceInfo = styled.div(({ theme }) => { + return { + marginLeft: '15px', + width: '202px', + p: { + color: theme.colors.popoverColor, + height: '20px', + fontSize: theme.font.sm, + ...displayFlex('flex-start', 'center'), + }, + svg: { + marginRight: '10px', + fontSize: '16px', + flexShrink: 0, + }, + span: { + flexGrow: 1, + ...textEllipsis(1), + }, + }; +}); + +export const StyleWorkspaceTitle = styled.div(({ theme }) => { + return { + fontSize: theme.font.base, + fontWeight: 600, + lineHeight: '24px', + marginBottom: '10px', + maxWidth: '200px', + ...textEllipsis(1), + }; +}); + +export const StyledCard = styled.div<{ + active?: boolean; +}>(({ theme, active }) => { + const borderColor = active ? theme.colors.primaryColor : 'transparent'; + return { + width: '310px', + height: '124px', + cursor: 'pointer', + padding: '16px', + boxShadow: '0px 0px 8px rgba(0, 0, 0, 0.1)', + borderRadius: '12px', + border: `1px solid ${borderColor}`, + ...displayFlex('flex-start', 'flex-start'), + marginBottom: '24px', + transition: 'background .2s', + background: theme.mode === 'light' ? '#FFF' : '#2C2C2C', + ':hover': { + background: theme.colors.cardHoverBackground, + '.add-icon': { + borderColor: theme.colors.primaryColor, + color: theme.colors.primaryColor, + }, + }, + }; +}); + +export const StyledFooter = styled('div')({ + height: '84px', + padding: '0 40px', + flexShrink: 0, + ...displayFlex('space-between', 'center'), +}); + +export const StyleUserInfo = styled.div(({ theme }) => { + return { + textAlign: 'left', + marginLeft: '16px', + flex: 1, + p: { + lineHeight: '24px', + color: theme.colors.iconColor, + }, + 'p:first-of-type': { + color: theme.colors.textColor, + fontWeight: 600, + }, + }; +}); + +export const StyledModalHeaderLeft = styled.div(() => { + return { ...displayFlex('flex-start', 'center') }; +}); +export const StyledModalTitle = styled.div(({ theme }) => { + return { + fontWeight: 600, + fontSize: theme.font.h6, + }; +}); + +export const StyledHelperContainer = styled.div(({ theme }) => { + return { + color: theme.colors.iconColor, + marginLeft: '15px', + fontWeight: 400, + fontSize: theme.font.h6, + ...displayFlex('center', 'center'), + }; +}); + +export const StyledModalContent = styled('div')({ + height: '534px', + padding: '8px 40px', + marginTop: '72px', + overflow: 'auto', + ...displayFlex('space-between', 'flex-start', 'flex-start'), + flexWrap: 'wrap', +}); +export const StyledOperationWrapper = styled.div(() => { + return { + ...displayFlex('flex-end', 'center'), + }; +}); + +export const StyleWorkspaceAdd = styled.div(() => { + return { + width: '58px', + height: '58px', + borderRadius: '100%', + background: '#f4f5fa', + border: '1.5px dashed #f4f5fa', + transition: 'background .2s', + ...displayFlex('center', 'center'), + }; +}); +export const StyledModalHeader = styled('div')(() => { + return { + width: '100%', + height: '72px', + position: 'absolute', + left: 0, + top: 0, + borderRadius: '24px 24px 0 0', + padding: '0 40px', + ...displayFlex('space-between', 'center'), + }; +}); + +export const StyledSignInButton = styled(Button)(({ theme }) => { + return { + fontWeight: 700, + paddingLeft: 0, + '.circle': { + width: '40px', + height: '40px', + borderRadius: '20px', + backgroundColor: theme.colors.innerHoverBackground, + flexShrink: 0, + marginRight: '16px', + ...displayInlineFlex('center', 'center'), + }, + }; +}); diff --git a/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx b/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx new file mode 100644 index 0000000000..8812e00076 --- /dev/null +++ b/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx @@ -0,0 +1,41 @@ +import React from 'react'; + +import { RemWorkspace } from '../../../../shared'; +import { WorkspaceAvatar } from '../../workspace-avatar'; +import { SelectorWrapper, WorkspaceName } from './styles'; + +export type WorkspaceSelectorProps = { + currentWorkspace: RemWorkspace | null; + onClick: () => void; +}; + +export const WorkspaceSelector: React.FC = ({ + currentWorkspace, + onClick, +}) => { + let name = 'Untitled Workspace'; + if (currentWorkspace) { + if (currentWorkspace.flavour === 'affine') { + if (currentWorkspace.firstBinarySynced) { + name = currentWorkspace.blockSuiteWorkspace.meta.name; + } + } else if (currentWorkspace.flavour === 'local') { + name = currentWorkspace.blockSuiteWorkspace.meta.name; + } + } + return ( + <> + + + {name} + + + ); +}; diff --git a/apps/web/src/components/workspace-slider-bar/WorkspaceSelector/index.ts b/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/index.ts similarity index 100% rename from apps/web/src/components/workspace-slider-bar/WorkspaceSelector/index.ts rename to apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/index.ts diff --git a/apps/web/src/components/workspace-slider-bar/WorkspaceSelector/styles.ts b/apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/styles.ts similarity index 100% rename from apps/web/src/components/workspace-slider-bar/WorkspaceSelector/styles.ts rename to apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/styles.ts diff --git a/apps/web/src/components/workspace-slider-bar/icons.tsx b/apps/web/src/components/pure/workspace-slider-bar/icons.tsx similarity index 100% rename from apps/web/src/components/workspace-slider-bar/icons.tsx rename to apps/web/src/components/pure/workspace-slider-bar/icons.tsx diff --git a/apps/web/src/components/workspace-slider-bar/icons/Icons.tsx b/apps/web/src/components/pure/workspace-slider-bar/icons/Icons.tsx similarity index 100% rename from apps/web/src/components/workspace-slider-bar/icons/Icons.tsx rename to apps/web/src/components/pure/workspace-slider-bar/icons/Icons.tsx diff --git a/apps/web/src/components/pure/workspace-slider-bar/index.tsx b/apps/web/src/components/pure/workspace-slider-bar/index.tsx new file mode 100644 index 0000000000..e91fe2984b --- /dev/null +++ b/apps/web/src/components/pure/workspace-slider-bar/index.tsx @@ -0,0 +1,273 @@ +import { MuiCollapse } from '@affine/component'; +import { Tooltip } from '@affine/component'; +import { IconButton } from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import { + ArrowDownSmallIcon, + DeleteTemporarilyIcon, + FavoriteIcon, + FolderIcon, + PlusIcon, + SearchIcon, + SettingsIcon, +} from '@blocksuite/icons'; +import { PageMeta } from '@blocksuite/store'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import React, { useCallback, useMemo, useState } from 'react'; + +import { usePageMeta } from '../../../hooks/use-page-meta'; +import { RemWorkspace } from '../../../shared'; +import { Arrow } from './icons'; +import { + StyledArrowButton, + StyledLink, + StyledListItem, + StyledNewPageButton, + StyledSliderBar, + StyledSliderBarWrapper, + StyledSubListItem, +} from './style'; +import { WorkspaceSelector } from './WorkspaceSelector'; + +export type FavoriteListProps = { + currentPageId: string | null; + openPage: (pageId: string) => void; + showList: boolean; + pageMeta: PageMeta[]; +}; + +const FavoriteList: React.FC = ({ + pageMeta, + openPage, + showList, +}) => { + const router = useRouter(); + const { t } = useTranslation(); + const favoriteList = useMemo( + () => pageMeta.filter(p => p.favorite && !p.trash), + [pageMeta] + ); + return ( + + {favoriteList.map((pageMeta, index) => { + const active = router.query.pageId === pageMeta.id; + return ( + { + if (active) { + return; + } + openPage(pageMeta.id); + }} + > + {pageMeta.title || 'Untitled'} + + ); + })} + {favoriteList.length === 0 && ( + {t('No item')} + )} + + ); +}; + +export type WorkSpaceSliderBarProps = { + isPublicWorkspace: boolean; + onOpenQuickSearchModal: () => void; + onOpenWorkspaceListModal: () => void; + currentWorkspace: RemWorkspace | null; + currentPageId: string | null; + openPage: (pageId: string) => void; + createPage: () => Promise; + show: boolean; + setShow: (show: boolean) => void; + currentPath: string; + paths: { + all: (workspaceId: string) => string; + favorite: (workspaceId: string) => string; + trash: (workspaceId: string) => string; + setting: (workspaceId: string) => string; + }; +}; + +export const WorkSpaceSliderBar: React.FC = ({ + isPublicWorkspace, + currentWorkspace, + currentPageId, + openPage, + createPage, + show, + setShow, + currentPath, + paths, + onOpenQuickSearchModal, + onOpenWorkspaceListModal, +}) => { + const currentWorkspaceId = currentWorkspace?.id || null; + const [showSubFavorite, setShowSubFavorite] = useState(true); + const [showTip, setShowTip] = useState(false); + const { t } = useTranslation(); + const pageMeta = usePageMeta(currentWorkspace?.blockSuiteWorkspace ?? null); + const onClickNewPage = useCallback(async () => { + const pageId = await createPage(); + if (pageId) { + openPage(pageId); + } + }, [createPage, openPage]); + return ( + <> + + + { + setShow(!show); + setShowTip(false); + }, [setShow, show])} + onMouseEnter={useCallback(() => { + setShowTip(true); + }, [])} + onMouseLeave={useCallback(() => { + setShowTip(false); + }, [])} + > + + + + + + + + { + onOpenQuickSearchModal(); + }, [onOpenQuickSearchModal])} + > + + {t('Quick search')} + + + + + {t('All pages')} + + + + + + {t('Favorites')} + + { + setShowSubFavorite(!showSubFavorite); + }, [showSubFavorite])} + > + + + + + + + + {t('Workspace Settings')} + + + + {/* { + setShowWorkspaceSetting(false); + }} + /> */} + {/* TODO: will finish the feature next version */} + {/* { + triggerImportModal(); + }} + > + {t('Import')} + */} + + + + {t('Trash')} + + + + {t('New Page')} + + + + + ); +}; + +export default WorkSpaceSliderBar; diff --git a/apps/web/src/components/workspace-slider-bar/style.ts b/apps/web/src/components/pure/workspace-slider-bar/style.ts similarity index 84% rename from apps/web/src/components/workspace-slider-bar/style.ts rename to apps/web/src/components/pure/workspace-slider-bar/style.ts index 966769b8f1..1cf29b15c8 100644 --- a/apps/web/src/components/workspace-slider-bar/style.ts +++ b/apps/web/src/components/pure/workspace-slider-bar/style.ts @@ -1,24 +1,22 @@ import { displayFlex, styled, textEllipsis } from '@affine/component'; import Link from 'next/link'; -import { CSSProperties } from 'react'; -export const StyledSliderBar = styled.div<{ - show: boolean; - width: CSSProperties['width']; -}>(({ theme, show, width }) => { - return { - width: show ? width : '0', - height: '100vh', - minHeight: '450px', - background: theme.colors.hubBackground, - boxShadow: theme.shadow.modal, - transition: 'width .15s, padding .15s', - position: 'relative', - zIndex: theme.zIndex.modal, - padding: show ? '24px 12px' : '24px 0', - flexShrink: 0, - }; -}); +export const StyledSliderBar = styled.div<{ show: boolean }>( + ({ theme, show }) => { + return { + width: show ? '256px' : '0', + height: '100vh', + minHeight: '450px', + background: theme.colors.hubBackground, + boxShadow: theme.shadow.modal, + transition: 'width .15s, padding .15s', + position: 'relative', + zIndex: theme.zIndex.modal, + padding: show ? '24px 12px' : '24px 0', + flexShrink: 0, + }; + } +); export const StyledSliderBarWrapper = styled.div(() => { return { height: '100%', diff --git a/apps/web/src/components/pure/workspace-title/index.tsx b/apps/web/src/components/pure/workspace-title/index.tsx new file mode 100644 index 0000000000..8d0a0456f0 --- /dev/null +++ b/apps/web/src/components/pure/workspace-title/index.tsx @@ -0,0 +1,24 @@ +import React, { ReactNode } from 'react'; + +import Header from '../../blocksuite/header/header'; +import { StyledPageListTittleWrapper } from '../../blocksuite/header/styles'; + +export type WorkspaceTitleProps = React.PropsWithChildren<{ + icon?: ReactNode; +}>; + +export const WorkspaceTitle: React.FC = ({ + icon, + children, +}) => { + return ( +
+ + {icon} + {children} + {/* fixme(himself65): todo *;/} + {/**/} + +
+ ); +}; diff --git a/apps/web/src/components/quick-search/Footer.tsx b/apps/web/src/components/quick-search/Footer.tsx deleted file mode 100644 index 7904f19674..0000000000 --- a/apps/web/src/components/quick-search/Footer.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useTranslation } from '@affine/i18n'; -import { PlusIcon } from '@blocksuite/icons'; -import { Command } from 'cmdk'; -import React from 'react'; - -import { usePageHelper } from '@/hooks/use-page-helper'; - -import { StyledModalFooterContent } from './style'; - -const MAX_QUERY_SHOW_LENGTH = 20; - -export const Footer = ({ - query, - onClose, -}: { - query: string; - onClose: () => void; -}) => { - const { openPage, createPage } = usePageHelper(); - const { t } = useTranslation(); - const normalizedQuery = - query.length > MAX_QUERY_SHOW_LENGTH - ? query.slice(0, MAX_QUERY_SHOW_LENGTH) + '...' - : query; - - return ( - { - onClose(); - const pageId = await createPage({ title: query }); - if (pageId) { - openPage(pageId); - } - }} - > - - - {query ? ( - {t('New Keyword Page', { query: normalizedQuery })} - ) : ( - {t('New Page')} - )} - - - ); -}; diff --git a/apps/web/src/components/quick-search/PublishedResults.tsx b/apps/web/src/components/quick-search/PublishedResults.tsx deleted file mode 100644 index 63ae937128..0000000000 --- a/apps/web/src/components/quick-search/PublishedResults.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { useTranslation } from '@affine/i18n'; -import { EdgelessIcon, PaperIcon } from '@blocksuite/icons'; -import { Workspace } from '@blocksuite/store'; -import { Command } from 'cmdk'; -import { useRouter } from 'next/router'; -import { Dispatch, SetStateAction, useEffect, useState } from 'react'; - -import usePageHelper from '@/hooks/use-page-helper'; -import { PageMeta } from '@/providers/app-state-provider'; -import { useGlobalState } from '@/store/app'; - -import { NoResultSVG } from './NoResultSVG'; -import { StyledListItem, StyledNotFound } from './style'; - -export const PublishedResults = ({ - query, - onClose, - setPublishWorkspaceName, -}: { - query: string; - setPublishWorkspaceName: Dispatch>; - onClose: () => void; -}) => { - const [workspace, setWorkspace] = useState(); - const { search } = usePageHelper(); - const dataCenter = useGlobalState(store => store.dataCenter); - const router = useRouter(); - const [pageList, setPageList] = useState([]); - useEffect(() => { - dataCenter - .loadPublicWorkspace(router.query.workspaceId as string) - .then(data => { - setPageList(data.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]); - if (data.blocksuiteWorkspace) { - setWorkspace(data.blocksuiteWorkspace); - setPublishWorkspaceName(data.blocksuiteWorkspace.meta.name); - } - }) - .catch(() => { - router.push('/404'); - }); - }, [router, dataCenter, setPublishWorkspaceName]); - const { t } = useTranslation(); - - if (!query) { - return <>; - } - - const results = search(query, workspace); - const pageIds = [...results.values()]; - const resultsPageMeta = pageList.filter( - page => pageIds.indexOf(page.id) > -1 && !page.trash - ); - - if (!resultsPageMeta.length) { - return ( - - {t('Find 0 result')} - - - ); - } - - return ( - - {resultsPageMeta.map(result => { - return ( - { - router.push( - `/public-workspace/${router.query.workspaceId}/${result.id}` - ); - onClose(); - }} - value={result.id} - > - - {result.mode === 'edgeless' ? : } - {result.title} - - - ); - })} - - ); -}; diff --git a/apps/web/src/components/quick-search/Results.tsx b/apps/web/src/components/quick-search/Results.tsx deleted file mode 100644 index cb86e81fe9..0000000000 --- a/apps/web/src/components/quick-search/Results.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useTranslation } from '@affine/i18n'; -import { EdgelessIcon, PaperIcon } from '@blocksuite/icons'; -import { Command } from 'cmdk'; -import { useRouter } from 'next/router'; -import { Dispatch, SetStateAction, useCallback, useEffect } from 'react'; - -import usePageHelper from '@/hooks/use-page-helper'; -import { useGlobalState } from '@/store/app'; - -import { useSwitchToConfig } from './config'; -import { NoResultSVG } from './NoResultSVG'; -import { StyledListItem, StyledNotFound } from './style'; - -export const Results = ({ - query, - setShowCreatePage, - onClose, -}: { - query: string; - onClose: () => void; - setShowCreatePage: Dispatch>; -}) => { - const { openPage } = usePageHelper(); - const router = useRouter(); - const currentWorkspace = useGlobalState( - useCallback(store => store.currentDataCenterWorkspace, []) - ); - const pageList = useGlobalState( - useCallback(store => store.dataCenterPageList, []) - ); - const { search } = usePageHelper(); - const List = useSwitchToConfig(currentWorkspace?.id); - const { t } = useTranslation(); - - const results = search(query); - const pageIds = [...results.values()]; - const resultsPageMeta = pageList.filter( - page => pageIds.indexOf(page.id) > -1 && !page.trash - ); - - // TODO lift this state up and remove this effect! - useEffect(() => { - setShowCreatePage(!resultsPageMeta.length); - //Determine whether to display the ‘+ New page’ - }, [resultsPageMeta, setShowCreatePage]); - - if (!query) { - return ( - - {List.map(link => { - return ( - { - onClose(); - router.push(link.href); - }} - > - - - {link.title} - - - ); - })} - - ); - } - - if (!resultsPageMeta.length) { - return ( - - {t('Find 0 result')} - - - ); - } - - return ( - - {resultsPageMeta.map(result => { - return ( - { - onClose(); - openPage(result.id); - }} - value={result.id} - > - - {result.mode === 'edgeless' ? : } - {result.title} - - - ); - })} - - ); -}; diff --git a/apps/web/src/components/quick-search/SearchInput.tsx b/apps/web/src/components/quick-search/SearchInput.tsx deleted file mode 100644 index d36ee7305a..0000000000 --- a/apps/web/src/components/quick-search/SearchInput.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { SearchIcon } from '@blocksuite/icons'; -import { Command } from 'cmdk'; -import { forwardRef } from 'react'; - -import { StyledInputContent, StyledLabel } from './style'; - -export const SearchInput = forwardRef< - HTMLInputElement, - Omit< - React.InputHTMLAttributes, - 'value' | 'onChange' | 'type' - > & { - /** - * Optional controlled state for the value of the search input. - */ - value?: string; - /** - * Event handler called when the search value changes. - */ - onValueChange?: (search: string) => void; - } & React.RefAttributes ->((props, ref) => { - return ( - - - - - - - ); -}); - -SearchInput.displayName = 'SearchInput'; diff --git a/apps/web/src/components/quick-search/config.ts b/apps/web/src/components/quick-search/config.ts deleted file mode 100644 index a8281aa228..0000000000 --- a/apps/web/src/components/quick-search/config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useTranslation } from '@affine/i18n'; -import { - DeleteTemporarilyIcon, - FavoriteIcon, - FolderIcon, - SettingsIcon, -} from '@blocksuite/icons'; -import { FC, SVGProps } from 'react'; - -export const useSwitchToConfig = ( - currentWorkspaceId?: string -): { - title: string; - href: string; - icon: FC>; -}[] => { - const { t } = useTranslation(); - return [ - { - title: t('All pages'), - href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/all` : '', - icon: FolderIcon, - }, - { - title: t('Favorites'), - href: currentWorkspaceId - ? `/workspace/${currentWorkspaceId}/favorite` - : '', - icon: FavoriteIcon, - }, - { - title: t('Workspace Settings'), - href: currentWorkspaceId - ? `/workspace/${currentWorkspaceId}/setting` - : '', - icon: SettingsIcon, - }, - { - title: t('Trash'), - href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/trash` : '', - icon: DeleteTemporarilyIcon, - }, - ]; -}; diff --git a/apps/web/src/components/simple-counter/index.ts b/apps/web/src/components/simple-counter/index.ts deleted file mode 100644 index 89cf2e0567..0000000000 --- a/apps/web/src/components/simple-counter/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { css, html, LitElement } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import * as React from 'react'; - -export const tagName = 'simple-counter'; - -// Adapt React in order to be able to use custom tags properly -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace JSX { - interface IntrinsicElements { - [tagName]: PersonInfoProps; - } - } -} - -interface PersonInfoProps - extends React.DetailedHTMLProps< - React.HTMLAttributes, - HTMLElement - > { - name?: string; -} -// ===================== Adapt end ==================== - -@customElement(tagName) -export class Counter extends LitElement { - static styles = css` - .counter-container { - display: flex; - color: var(--affine-text-color); - } - button { - margin: 0 5px; - } - `; - - @property() - name?: string = ''; - - @state() - count = 0; - // Render the UI as a function of component state - render() { - return html`
-
${this.name}
- -
${this.count}
- -
`; - } - - private _increment() { - this.count++; - } - private _subtract() { - this.count--; - } -} diff --git a/apps/web/src/components/workspace-avatar/Avatar.tsx b/apps/web/src/components/workspace-avatar/Avatar.tsx deleted file mode 100644 index a83865761d..0000000000 --- a/apps/web/src/components/workspace-avatar/Avatar.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { stringToColour } from '@/utils'; -interface IWorkspaceAvatar { - size: number; - name: string; - avatar: string; - style?: React.CSSProperties; -} - -export const WorkspaceAvatar = (props: IWorkspaceAvatar) => { - const size = props.size || 20; - const sizeStr = size + 'px'; - - return ( - <> - {props.avatar ? ( -
- - - -
- ) : ( -
- {(props.name || 'AFFiNE').substring(0, 1)} -
- )} - - ); -}; diff --git a/apps/web/src/components/workspace-avatar/WorkspaceUnitAvatar.tsx b/apps/web/src/components/workspace-avatar/WorkspaceUnitAvatar.tsx deleted file mode 100644 index 91586606e7..0000000000 --- a/apps/web/src/components/workspace-avatar/WorkspaceUnitAvatar.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { WorkspaceUnit } from '@affine/datacenter'; -import { useEffect, useState } from 'react'; - -import { WorkspaceAvatar as Avatar } from './Avatar'; - -const useAvatar = (workspaceUnit: WorkspaceUnit | null) => { - const [avatarUrl, setAvatarUrl] = useState(''); - const avatarId = - workspaceUnit?.avatar || workspaceUnit?.blocksuiteWorkspace?.meta.avatar; - const blobs = workspaceUnit?.blocksuiteWorkspace?.blobs; - useEffect(() => { - if (avatarId && blobs) { - blobs.then(blobs => { - blobs?.get(avatarId).then(url => setAvatarUrl(url || '')); - }); - } else { - setAvatarUrl(''); - } - }, [avatarId, blobs]); - - return avatarUrl; -}; - -export const WorkspaceUnitAvatar = ({ - size = 20, - name, - workspaceUnit = null, - style, -}: { - size?: number; - name?: string; - workspaceUnit?: WorkspaceUnit | null; - style?: React.CSSProperties; -}) => { - const avatarUrl = useAvatar(workspaceUnit); - return ( - - ); -}; diff --git a/apps/web/src/components/workspace-avatar/index.ts b/apps/web/src/components/workspace-avatar/index.ts deleted file mode 100644 index 8d6dfe17ba..0000000000 --- a/apps/web/src/components/workspace-avatar/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { WorkspaceAvatar } from './Avatar'; -export { WorkspaceUnitAvatar } from './WorkspaceUnitAvatar'; diff --git a/apps/web/src/components/workspace-layout/index.tsx b/apps/web/src/components/workspace-layout/index.tsx deleted file mode 100644 index 839113afbb..0000000000 --- a/apps/web/src/components/workspace-layout/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useRouter } from 'next/router'; -import { PropsWithChildren } from 'react'; - -import HelpIsland from '@/components/help-island'; -import { WorkSpaceSliderBar } from '@/components/workspace-slider-bar'; -import useEnsureWorkspace from '@/hooks/use-ensure-workspace'; - -import { PageLoading } from '../loading'; -import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles'; - -export const WorkspaceDefender = ({ children }: PropsWithChildren) => { - const { workspaceLoaded } = useEnsureWorkspace(); - return <>{workspaceLoaded ? children : }; -}; - -export const WorkspaceLayout = ({ children }: PropsWithChildren) => { - const router = useRouter(); - - return ( - - - - {children} - -
- {/* Slot for block hub */} -
- -
-
-
- ); -}; - -export const Layout = ({ children }: PropsWithChildren) => { - return ( - - {children} - - ); -}; -export default Layout; diff --git a/apps/web/src/components/workspace-modal/Footer.tsx b/apps/web/src/components/workspace-modal/Footer.tsx deleted file mode 100644 index fbc49ac3f8..0000000000 --- a/apps/web/src/components/workspace-modal/Footer.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { FlexWrapper } from '@affine/component'; -import { IconButton } from '@affine/component'; -import { Tooltip } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { CloudWorkspaceIcon, SignOutIcon } from '@blocksuite/icons'; - -import { WorkspaceAvatar } from '@/components/workspace-avatar'; -import { useGlobalState } from '@/store/app'; - -import { StyledFooter, StyledSignInButton, StyleUserInfo } from './styles'; -export const Footer = ({ - onLogin, - onLogout, -}: { - onLogin: () => void; - onLogout: () => void; -}) => { - const user = useGlobalState(store => store.user); - const { t } = useTranslation(); - - return ( - - {user && ( - <> - - - -

{user.name}

-

{user.email}

-
-
- - { - onLogout(); - }} - > - - - - - )} - - {!user && ( - - -
- } - onClick={async () => { - onLogin(); - }} - > - {t('Sign in')} - - )} - - ); -}; diff --git a/apps/web/src/components/workspace-modal/WorkspaceCard.tsx b/apps/web/src/components/workspace-modal/WorkspaceCard.tsx deleted file mode 100644 index 6de4352e30..0000000000 --- a/apps/web/src/components/workspace-modal/WorkspaceCard.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { WorkspaceUnit } from '@affine/datacenter'; -import { useTranslation } from '@affine/i18n'; -import { useCallback } from 'react'; - -import { - CloudWorkspaceIcon, - JoinedWorkspaceIcon, - LocalDataIcon, - LocalWorkspaceIcon, - PublishIcon, -} from '@/components/icons'; -import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; -import { useGlobalState } from '@/store/app'; - -import { StyledCard, StyleWorkspaceInfo, StyleWorkspaceTitle } from './styles'; - -const WorkspaceType = ({ workspaceData }: { workspaceData: WorkspaceUnit }) => { - const user = useGlobalState(store => store.user); - const { t } = useTranslation(); - const isOwner = user?.id === workspaceData.owner?.id; - - if (workspaceData.provider === 'local') { - return ( -

- - {t('Local Workspace')} -

- ); - } - - return isOwner ? ( -

- - {t('Cloud Workspace')} -

- ) : ( -

- - {t('Joined Workspace')} -

- ); -}; - -export const WorkspaceCard = ({ - workspaceData, - onClick, -}: { - workspaceData: WorkspaceUnit; - onClick: (data: WorkspaceUnit) => void; -}) => { - const currentWorkspace = useGlobalState( - useCallback(store => store.currentDataCenterWorkspace, []) - ); - const { t } = useTranslation(); - return ( - { - onClick(workspaceData); - }} - active={workspaceData.id === currentWorkspace?.id} - > - - - - - {workspaceData.name || 'AFFiNE'} - - - {workspaceData.provider === 'local' && ( -

- - {t('Available Offline')} -

- )} - {workspaceData.published && ( -

- - {t('Published to Web')} -

- )} -
-
- ); -}; diff --git a/apps/web/src/components/workspace-modal/index.tsx b/apps/web/src/components/workspace-modal/index.tsx deleted file mode 100644 index d857810ab9..0000000000 --- a/apps/web/src/components/workspace-modal/index.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; -import { Tooltip } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { HelpIcon, PlusIcon } from '@blocksuite/icons'; -import { useRouter } from 'next/router'; -import { useState } from 'react'; - -import { useGlobalState } from '@/store/app'; - -import { CreateWorkspaceModal } from '../create-workspace'; -import { LoginModal } from '../login-modal'; -import { LogoutModal } from '../logout-modal'; -import { Footer } from './Footer'; -import { LanguageMenu } from './SelectLanguageMenu'; -import { - StyledCard, - StyledHelperContainer, - StyledModalContent, - StyledModalHeader, - StyledModalHeaderLeft, - StyledModalTitle, - StyledOperationWrapper, - StyledSplitLine, - StyleWorkspaceAdd, - StyleWorkspaceInfo, - StyleWorkspaceTitle, -} from './styles'; -import { WorkspaceCard } from './WorkspaceCard'; -interface WorkspaceModalProps { - open: boolean; - onClose: () => void; -} - -export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => { - const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false); - const logout = useGlobalState(store => store.logout); - const dataCenter = useGlobalState(store => store.dataCenter); - const router = useRouter(); - const { t } = useTranslation(); - const [loginOpen, setLoginOpen] = useState(false); - const [logoutOpen, setLogoutOpen] = useState(false); - - return ( - <> - - - - - {t('My Workspaces')} - - - - - - - - - - - { - onClose(); - }} - absolute={false} - /> - - - - - {dataCenter.workspaces.map((item, index) => { - return ( - { - router.push(`/workspace/${workspaceData.id}/all`); - onClose(); - }} - key={index} - > - ); - })} - { - setCreateWorkspaceOpen(true); - }} - > - - - - - - {t('New Workspace')} -

{t('Create Or Import')}

-
-
-
- -