Merge branch 'master'

Conflicts:
	.vscode/settings.json
	package.json
	packages/data-center/package.json
	pnpm-lock.yaml
This commit is contained in:
linonetwo
2023-02-09 10:13:15 +08:00
164 changed files with 3027 additions and 5273 deletions

View File

@@ -436,6 +436,33 @@
"contributions": [ "contributions": [
"code" "code"
] ]
},
{
"login": "hezhizhen",
"name": "Zhizhen He",
"avatar_url": "https://avatars.githubusercontent.com/u/7611700?v=4",
"profile": "https://t.me/littlepoint",
"contributions": [
"code"
]
},
{
"login": "AkaraChen",
"name": "AkaraChen",
"avatar_url": "https://avatars.githubusercontent.com/u/85140972?v=4",
"profile": "https://akr.moe/",
"contributions": [
"code"
]
},
{
"login": "suyanhanx",
"name": "Suyan",
"avatar_url": "https://avatars.githubusercontent.com/u/24221472?v=4",
"profile": "https://github.com/suyanhanx",
"contributions": [
"code"
]
} }
] ]
} }

View File

@@ -1,7 +0,0 @@
**/webpack.config.js
**/scripts/*.js
**/node_modules/**
.github/**
**/__tests__/**
**/tests/**

7
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,7 @@
<!--
For bug reports 🐛, please provide:
- The necessary steps to reproduce the issue.
- The browser and platform you tested, if this may be related to compatibility.
For general discussion and feature requests, please use 👉 https://community.affine.pro/
-->

0
.github/ISSUE_TEMPLATE/.gitkeep vendored Normal file
View File

View File

@@ -1,45 +0,0 @@
name: 🐛 Bug report (Alpha)
description: "Report a reproducible bug or regression for https://pathfinder.affine.pro"
title: "[bug]: "
labels: ["bug", "alpha"]
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this bug report!
- type: input
id: description
attributes:
label: Describe the bug
placeholder: A clear and concise description of what the bug is.
- type: textarea
id: reproduce
attributes:
label: To Reproduce
placeholder: "Steps to reproduce the behavior\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error"
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
placeholder: If applicable, add screenshots to help explain your problem.
- type: textarea
id: expected
attributes:
label: Expected behavior
placeholder: A clear and concise description of what you expected to happen.
- type: input
id: platform
attributes:
label: Platform
placeholder: e.g. MacOS, Windows10...
- type: input
id: browser
attributes:
label: Browser
placeholder: e.g. Chrome, Safari
- type: textarea
id: additional
attributes:
label: Additional context
placeholder: Add any other context about the problem here.

View File

@@ -1,45 +0,0 @@
name: 🐛 Bug report (Pre-Alpha)
description: "Report a reproducible bug or regression for https://livedemo.affine.pro"
title: "[bug]: "
labels: ["bug", "pre-alpha"]
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this bug report!
- type: input
id: description
attributes:
label: Describe the bug
placeholder: A clear and concise description of what the bug is.
- type: textarea
id: reproduce
attributes:
label: To Reproduce
placeholder: "Steps to reproduce the behavior\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error"
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
placeholder: If applicable, add screenshots to help explain your problem.
- type: textarea
id: expected
attributes:
label: Expected behavior
placeholder: A clear and concise description of what you expected to happen.
- type: input
id: platform
attributes:
label: Platform
placeholder: e.g. MacOS, Windows10...
- type: input
id: browser
attributes:
label: Browser
placeholder: e.g. Chrome, Safari
- type: textarea
id: additional
attributes:
label: Additional context
placeholder: Add any other context about the problem here.

View File

@@ -1,11 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: 💭 Questions and Help - Reddit
url: https://www.reddit.com/r/Affine/
about: Please ask and answer questions here.
- name: 💬 Questions and Help - Telegram
url: https://t.me/affineworkos
about: Please ask and answer questions here.
- name: 🗯 Questions and Help - Discord
url: https://discord.gg/yz6tGVsf5p
about: Please ask and answer questions here.

View File

@@ -1,31 +0,0 @@
name: ✨ Feature request
description: An idea or request for new functionality
title: "[feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this feature request!
- type: textarea
id: description
attributes:
label: 1~3 main use cases of the proposed feature
description: e.g. As a ..., I have many tasks scattered across documents, and I want to have a unified entry to view these tasks.
placeholder: e.g. As a ...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Ideas for solution
placeholder: e.g. A task view can be added to view all tasks.
- type: input
id: userType
attributes:
label: what types of users can benefit from using your proposed feature
placeholder: busy student
- type: textarea
id: additional
attributes:
label: Additional context
placeholder: Add any other context or screenshots about the feature request here.

View File

@@ -1,31 +0,0 @@
name: 🪄 Improvement request
description: An improvement to existing functionality
title: "[improvement]: "
labels: ["improvement"]
body:
- type: markdown
attributes:
value: Thanks for taking the time to fill out this improvement request!
- type: textarea
id: description
attributes:
label: 1~3 main use cases of the proposed improvement
description: e.g. As a ..., I have many tasks scattered across documents, and I want to have a unified entry to view these tasks.
placeholder: e.g. As a ...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Ideas for solution
placeholder: e.g. A task view can be added to view all tasks.
- type: input
id: userType
attributes:
label: what types of users can benefit from using your proposed improvement
placeholder: busy student
- type: textarea
id: additional
attributes:
label: Additional context
placeholder: Add any other context or screenshots about the improvement request here.

View File

@@ -1,7 +0,0 @@
---
name: I have a question
about: Feel free to ask us your questions!
title: '[Question]'
labels: ''
assignees: ''
---

View File

@@ -67,18 +67,16 @@ jobs:
lint: lint:
name: Lint and E2E Test name: Lint and E2E Test
runs-on: ubuntu-latest runs-on: self-hosted
environment: development environment: development
needs: build needs: build
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 'latest' version: 'latest'
- uses: actions/setup-node@v3
- name: Use Node.js
uses: actions/setup-node@v2
with: with:
node-version: 18.x node-version: 18.x
cache: 'pnpm' cache: 'pnpm'
@@ -96,8 +94,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }}
- name: Download artifact - name: Download artifact
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
@@ -105,20 +101,40 @@ jobs:
name: artifact name: artifact
path: packages/app/.next/ path: packages/app/.next/
- name: Lint & E2E Test - name: Lint & E2E Test & Unit Test
run: | run: |
pnpm lint --max-warnings=0 pnpm lint --max-warnings=0
PLAYWRIGHT_BROWSERS_PATH=0 npx playwright install chromium npx playwright install chromium
PLAYWRIGHT_BROWSERS_PATH=0 pnpm test pnpm run test:coverage
PLAYWRIGHT_BROWSERS_PATH=0 pnpm test:dc pnpm run test:unit
env: # env:
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} # NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }} # NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }} # NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }} # NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }} # NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }} # NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }} # NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
- name: Collect code coverage report
run: pnpm exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
- name: Upload e2e test coverage results
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/lcov.info
flags: e2etest
name: affine
fail_ci_if_error: true
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: test-results-main
path: ./test-results
if-no-files-found: ignore
build-community: build-community:
name: Build Community name: Build Community

View File

@@ -1,48 +0,0 @@
name: Unit Tests
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 'latest'
- uses: actions/setup-node@v3
with:
node-version: 16
registry-url: https://npm.pkg.github.com
scope: '@toeverything'
cache: 'pnpm'
- run: node scripts/module-resolve/ci.cjs
- name: Install dependencies
run: pnpm install --no-frozen-lockfile
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }}
- name: Install Playwright browsers
run: npx playwright install chromium
# - name: Run E2E tests
# run: pnpm run test:e2e
# env:
# NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
# NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
# NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
# NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
# NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
# NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
# NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
- name: Run Unit tests
run: pnpm run test:unit

2
.gitignore vendored
View File

@@ -4,6 +4,8 @@
*dist *dist
/tmp /tmp
/out-tsc /out-tsc
.nyc_output
.coverage
# dependencies # dependencies
node_modules node_modules

View File

@@ -19,7 +19,9 @@
"explorer.fileNesting.expand": false, "explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map", "*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map",
"package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, crowdin*, cypress.*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.config.*, webpack*, workspace.json, xo.config.*, yarn*" "package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, crowdin*, cypress.*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.config.*, webpack*, workspace.json, xo.config.*, yarn*, babel.*, .babelrc, project.json",
"Cargo.toml": "Cargo.lock",
"README.md": "LICENSE, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md"
}, },
"[rust]": { "[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer" "editor.defaultFormatter": "rust-lang.rust-analyzer"

View File

@@ -1,45 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
For answers to common questions about this code of conduct, see <https://www.contributor-covenant.org/faq>

View File

@@ -1,3 +0,0 @@
# Contributing to AFFiNE
For information related to contributing to AFFiNE, please check out the [Contributing to AFFiNE](https://docs.affine.pro/affine/developer-docs/contributions) section of the documentation at the AFFiNE site.

View File

@@ -18,13 +18,14 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
--> -->
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[all-contributors-badge]: https://img.shields.io/badge/all_contributors-45-orange.svg?style=flat-square [all-contributors-badge]: https://img.shields.io/badge/all_contributors-48-orange.svg?style=flat-square
<!-- ALL-CONTRIBUTORS-BADGE:END --> <!-- ALL-CONTRIBUTORS-BADGE:END -->
[![affine.pro](https://img.shields.io/static/v1?label=live%20demo&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=→)](https://livedemo.affine.pro) [![affine.pro](https://img.shields.io/static/v1?label=live%20demo&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=→)](https://livedemo.affine.pro)
[![stars](https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars)](https://github.com/toeverything/AFFiNE) [![stars](https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars)](https://github.com/toeverything/AFFiNE)
[![All Contributors][all-contributors-badge]](#contributors) [![All Contributors][all-contributors-badge]](#contributors)
![codecov](https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master)
[![Node](https://img.shields.io/badge/node->=16.0-success)](https://www.typescriptlang.org/) [![Node](https://img.shields.io/badge/node->=16.0-success)](https://www.typescriptlang.org/)
[![React](https://img.shields.io/badge/TypeScript-4.7-3178c6)](https://www.typescriptlang.org/) [![React](https://img.shields.io/badge/TypeScript-4.7-3178c6)](https://www.typescriptlang.org/)
[![React](https://img.shields.io/badge/React-18-61dafb)](https://reactjs.org/) [![React](https://img.shields.io/badge/React-18-61dafb)](https://reactjs.org/)
@@ -80,9 +81,9 @@ Before we tell you how to get started with AFFiNE, we'd like to shamelessly plug
[![affine.pro](https://img.shields.io/static/v1?label=Try%20it%20Online&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&message=%E2%86%92&style=for-the-badge)](https://affine.pro) No installation or registration required! Head over to our website and try it out now. [![affine.pro](https://img.shields.io/static/v1?label=Try%20it%20Online&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&message=%E2%86%92&style=for-the-badge)](https://affine.pro) No installation or registration required! Head over to our website and try it out now.
[AFFiNE Documentation](https://docs.affine.pro/affine/) - More detailed documentation on how to use and develop with AFFiNE [AFFiNE Community](https://community.affine.pro) - Our wonderful community, where you can meet and engage with the team, developers and other like-minded enthusiastic user of AFFiNE.
[Our official communities](https://docs.affine.pro/affine/community-links/official-communities) - Join our friendly communities for more support and discussions [Our official communities](https://community.affine.pro/c/start-here/) - Join our friendly communities for more support and discussions.
## Contributing ## Contributing
@@ -90,19 +91,18 @@ Calling all developers, testers, tech writers and more! Contributions of all typ
For **bug reports**, **feature requests** and other **suggestions** you can also [create a new issue](https://github.com/toeverything/AFFiNE/issues/new/choose) and choose the most appropiate template for your feedback. For **bug reports**, **feature requests** and other **suggestions** you can also [create a new issue](https://github.com/toeverything/AFFiNE/issues/new/choose) and choose the most appropiate template for your feedback.
For **translation** and **language support** you can visit our docs for the [internationalization guide](https://docs.affine.pro/affine/internationalization/welcome). For **translation** and **language support** you can visit our [i18n General Space](https://community.affine.pro/c/i18n-general).
Looking for **others ways to contribute** and wondering where to start? Check out the [AFFiNE Ambassador program](https://docs.affine.pro/affine/affine-ambassadors/welcome), we work closely with passionate members of our community and provide them with a wide-range of support and resources. Looking for **others ways to contribute** and wondering where to start? Check out the [AFFiNE Ambassador program](https://community.affine.pro/c/start-here/affine-ambassador), we work closely with passionate community members and provide them with a wide-range of support and resources.
If you have questions, join us across various [**social platforms**](https://docs.affine.pro/affine/community-links/official-communities) where our friendly community can help provide the answers. If you have questions, you are welcome to contact us. One of the best places to get more info and learn more is in the [AFFiNE Community](https://community.affine.pro) where you can engage with other like-minded individuals.
We have done a major refactoring recently, if you want to see our previous version of the code, please go to the [Pre-Alpha](https://github.com/toeverything/AFFiNE/tree/Pre-Alpha) branch to view
## Thanks ## Thanks
We would also like to give thanks to open-source projects that make AFFiNE possible: We would also like to give thanks to open-source projects that make AFFiNE possible:
- [BlockSuite](https://github.com/toeverything/BlockSuite) - AFFiNE is built with and powered by BlockSuite. - [BlockSuite](https://github.com/toeverything/BlockSuite) - 💠 BlockSuite is the open-source collaborative editor project behind AFFiNE.
- [OctoBase](https://github.com/toeverything/OctoBase) - 🐙 OctoBase is the open-source database behind AFFiNE, local-first, yet collaborative. A light-weight, scalable, data engine written in Rust.
- [Yjs](https://github.com/yjs/yjs) & [Yrs](https://github.com/y-crdt/y-crdt) -- Fundamental support of CRDTs for our implementation on state management and data sync. - [Yjs](https://github.com/yjs/yjs) & [Yrs](https://github.com/y-crdt/y-crdt) -- Fundamental support of CRDTs for our implementation on state management and data sync.
- [React](https://github.com/facebook/react) -- View layer support and web GUI framework. - [React](https://github.com/facebook/react) -- View layer support and web GUI framework.
- [Rust](https://github.com/rust-lang/rust) -- High performance language that extends the ability and availability of our real-time backend, OctoBase. - [Rust](https://github.com/rust-lang/rust) -- High performance language that extends the ability and availability of our real-time backend, OctoBase.
@@ -184,6 +184,9 @@ Thanks a lot to the community for providing such powerful and simple libraries,
<td align="center"><a href="https://github.com/MuhammedFaraz"><img src="https://avatars.githubusercontent.com/u/92734739?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Mohammed Faraz</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=MuhammedFaraz" title="Documentation">📖</a></td> <td align="center"><a href="https://github.com/MuhammedFaraz"><img src="https://avatars.githubusercontent.com/u/92734739?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Mohammed Faraz</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=MuhammedFaraz" title="Documentation">📖</a></td>
<td align="center"><a href="https://pranavsriram.dev/"><img src="https://avatars.githubusercontent.com/u/28348429?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Pranav Sriram </b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Pranav4399" title="Code">💻</a></td> <td align="center"><a href="https://pranavsriram.dev/"><img src="https://avatars.githubusercontent.com/u/28348429?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Pranav Sriram </b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Pranav4399" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/Reson-a"><img src="https://avatars.githubusercontent.com/u/20806266?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Reson-a</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Reson-a" title="Code">💻</a></td> <td align="center"><a href="https://github.com/Reson-a"><img src="https://avatars.githubusercontent.com/u/20806266?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Reson-a</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=Reson-a" title="Code">💻</a></td>
<td align="center"><a href="https://t.me/littlepoint"><img src="https://avatars.githubusercontent.com/u/7611700?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Zhizhen He</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=hezhizhen" title="Code">💻</a></td>
<td align="center"><a href="https://akr.moe/"><img src="https://avatars.githubusercontent.com/u/85140972?v=4?s=50" width="50px;" alt=""/><br /><sub><b>AkaraChen</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=AkaraChen" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/suyanhanx"><img src="https://avatars.githubusercontent.com/u/24221472?v=4?s=50" width="50px;" alt=""/><br /><sub><b>Suyan</b></sub></a><br /><a href="https://github.com/toeverything/AFFiNE/commits?author=suyanhanx" title="Code">💻</a></td>
</tr> </tr>
</table> </table>
@@ -192,10 +195,26 @@ Thanks a lot to the community for providing such powerful and simple libraries,
<!-- ALL-CONTRIBUTORS-LIST:END --> <!-- ALL-CONTRIBUTORS-LIST:END -->
## Jobs ## Self-Host
Get started with Docker and deploy your own feature-rich, restriction-free deployment of AFFiNE - check the [latest packages](https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted).
## Hiring
Some amazing companies including AFFiNE are looking for developers! Are you interested in helping build with AFFiNE and/or its partners? Check out some of the latest [jobs available](./docs/jobs/summary.md). Some amazing companies including AFFiNE are looking for developers! Are you interested in helping build with AFFiNE and/or its partners? Check out some of the latest [jobs available](./docs/jobs/summary.md).
## Upgrading
For upgrading information please see our [update page](https://affine.pro/blog?tag=Release%20Note).
## Feature Request
For feature request please see https://community.affine.pro/c/feature-requests/
## Is it awesome?
[These people](https://twitter.com/AffineOfficial/followers) seem to like it.
## License ## License
See [LICENSE](/LICENSE) for details. See [LICENSE](/LICENSE) for details.

View File

@@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo
Examples of behavior that contributes to creating a positive environment include: Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language - Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences - Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism - Gracefully accepting constructive criticism
- Focusing on what is best for the community - Focusing on what is best for the community
- Showing empathy towards other community members - Showing empathy towards other community members
Examples of unacceptable behavior by participants include: Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances - The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks - Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment - Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission - Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting - Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities ## Our Responsibilities

View File

@@ -12,8 +12,8 @@ Use the table of contents icon on the top left corner of this document to get to
Currently we have two versions of AFFiNE: Currently we have two versions of AFFiNE:
- [AFFiNE Pre-Alpha](https://livedemo.affine.pro/). This version users the branch `master`, however is no longer actively developed and will be archived in the future. - [AFFiNE Pre-Alpha](https://livedemo.affine.pro/). This version users the branch `Pre-Alpha`, it is no longer actively developed but contains some different functions and features.
- [AFFiNE Alpha](https://pathfinder.affine.pro/). This version uses the 'pathfinder' branch, this is the latest version under active development. We plan to update this to the master branch in the near future. - [AFFiNE Alpha](https://pathfinder.affine.pro/). This version uses the `master` branch, this is the latest version under active development.
To get an overview of the project, read the [README](../README.md). Here are some resources to help you get started with open source contributions: To get an overview of the project, read the [README](../README.md). Here are some resources to help you get started with open source contributions:

View File

@@ -0,0 +1,32 @@
# Bump Blocksuite
```shell
./scripts/upgrade-blocksuite.sh
```
## Understand the version number
### Stable
You can see all the stable version tags [here](https://github.com/toeverything/blocksuite/tags).
### Nightly
If it's nightly version, the version will follow `${version}-${date}-${hash}`.
For example, `0.4.0-20230203030233-b22bea7` means that
the version is based on `0.4.0`, the building date is `20230203030233`,
and the commit hash is `b22bea7`.
> For the source code, see [here](https://github.com/toeverything/set-build-version/blob/master/src/version.ts)
Using this version format, you can easily check the diff between each version.
For example, the diff from old version `0.4.0-20230201063624-4e0463b` to new version `0.4.0-20230203030233-b22bea7`
is <https://github.com/toeverything/blocksuite/compare/4e0463b...b22bea7>.
## Fix the breaking change
You can follow this file to keep updated:
https://github.com/toeverything/blocksuite/blob/master/packages/playground/src/main.ts

View File

@@ -4,6 +4,7 @@
- https://allcontributors.org/docs/en/emoji-key - https://allcontributors.org/docs/en/emoji-key
```shell ```shell
all-contributors check
all-contributors add tzhangchi code,doc all-contributors add tzhangchi code,doc
all-contributors generate all-contributors generate
``` ```

View File

@@ -26,4 +26,4 @@ You may be able to find additional help and information on our social media plat
### :earth_asia: Translations ### :earth_asia: Translations
AFFiNE is internationalized and available in multiple languages. The source content in this repository is written in English. We integrate with an external localization platform to work with the community in localizing the English content. You can find more info in our [internationalization docs](https://docs.affine.pro/affine/internationalization/welcome). AFFiNE is internationalized and available in multiple languages. The source content in this repository is written in English. We integrate with an external localization platform to work with the community in localizing the English content. You can find more info on our community page, in our [i18n General Space ](https://community.affine.pro/c/i18n-general).

View File

@@ -15,12 +15,10 @@
"start": "pnpm --filter @affine/app start", "start": "pnpm --filter @affine/app start",
"lint": "pnpm --filter @affine/app lint", "lint": "pnpm --filter @affine/app lint",
"test": "playwright test", "test": "playwright test",
"test:dc": "pnpm --filter @affine/datacenter test", "test:coverage": "cross-env COVERAGE=true pnpm test -- --forbid-only",
"test:e2e:codegen": "npx playwright codegen http://localhost:8080",
"test:unit": "playwright test --config=playwright.config.unit.ts", "test:unit": "playwright test --config=playwright.config.unit.ts",
"postinstall": "husky install", "postinstall": "husky install",
"notify": "node --experimental-modules scripts/notify.mjs", "notify": "node --experimental-modules scripts/notify.mjs",
"check:ci": "pnpm lint & pnpm test",
"update:blocksuite": "pnpm i --filter @affine/app --filter @affine/datacenter @blocksuite/blocks@nightly @blocksuite/store@nightly && pnpm i --filter @affine/app @blocksuite/editor@nightly" "update:blocksuite": "pnpm i --filter @affine/app --filter @affine/datacenter @blocksuite/blocks@nightly @blocksuite/store@nightly && pnpm i --filter @affine/app @blocksuite/editor@nightly"
}, },
"lint-staged": { "lint-staged": {
@@ -29,13 +27,13 @@
}, },
"devDependencies": { "devDependencies": {
"@changesets/cli": "^2.26.0", "@changesets/cli": "^2.26.0",
"@jest/globals": "^29.3.1",
"@playwright/test": "^1.29.1", "@playwright/test": "^1.29.1",
"@types/eslint": "^8.4.10", "@types/eslint": "^8.4.10",
"@types/node": "^18.11.17", "@types/node": "^18.11.17",
"@typescript-eslint/eslint-plugin": "^5.47.0", "@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0", "@typescript-eslint/parser": "^5.47.0",
"concurrently": "^7.6.0", "concurrently": "^7.6.0",
"babel-plugin-istanbul": "^6.1.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.30.0", "eslint": "^8.30.0",
"eslint-config-next": "12.3.1", "eslint-config-next": "12.3.1",
@@ -44,10 +42,10 @@
"fake-indexeddb": "4.0.1", "fake-indexeddb": "4.0.1",
"got": "^12.5.3", "got": "^12.5.3",
"husky": "^8.0.2", "husky": "^8.0.2",
"jest": "^29.3.1",
"lint-staged": "^13.1.0", "lint-staged": "^13.1.0",
"nyc": "^15.1.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"typescript": "^4.9.3" "typescript": "^4.9.5"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,
@@ -70,9 +68,16 @@
}, },
"reportUnusedDisableDirectives": true, "reportUnusedDisableDirectives": true,
"ignorePatterns": [ "ignorePatterns": [
"src/**/*.test.ts",
"package/**/dist/*", "package/**/dist/*",
"package/**/sync.js" "package/**/.babelrc.js",
"package/**/sync.js",
"src/**/*.test.ts",
"**/webpack.config.js",
"**/scripts/*.js",
"**/node_modules/**",
".github/**",
"**/__tests__/**",
"**/tests/**"
] ]
} }
} }

28
packages/app/.babelrc.js Normal file
View File

@@ -0,0 +1,28 @@
const plugins = [];
if (process.env.NODE_ENV === 'development' || process.env.COVERAGE === 'true') {
console.log(
'Detected development environment. Instrumenting code for coverage.'
);
plugins.push('istanbul');
}
plugins.push([
'@emotion',
{
// See https://emotion.sh/docs/@emotion/babel-plugin
importMap: {
'@/styles': {
styled: {
canonicalImport: ['@emotion/styled', 'default'],
styledBaseImport: ['@/styles', 'styled'],
},
},
},
},
]);
module.exports = {
presets: ['next/babel'],
plugins,
};

View File

@@ -68,6 +68,9 @@ const nextConfig = {
return profile; return profile;
}, },
basePath: process.env.BASE_PATH, basePath: process.env.BASE_PATH,
experimental: {
forceSwcTransforms: true,
},
}; };
const baseDir = process.env.LOCAL_BLOCK_SUITE ?? '/'; const baseDir = process.env.LOCAL_BLOCK_SUITE ?? '/';
@@ -98,6 +101,7 @@ const withDebugLocal = require('next-debug-local')(
const withPWA = require('next-pwa')({ const withPWA = require('next-pwa')({
dest: 'public', dest: 'public',
scope: '/_next',
disable: process.env.NODE_ENV !== 'production', disable: process.env.NODE_ENV !== 'production',
}); });

View File

@@ -11,10 +11,10 @@
"dependencies": { "dependencies": {
"@affine/datacenter": "workspace:*", "@affine/datacenter": "workspace:*",
"@affine/i18n": "workspace:*", "@affine/i18n": "workspace:*",
"@blocksuite/blocks": "0.4.0-20230111171650-bc63456", "@blocksuite/blocks": "0.4.0-alpha.2",
"@blocksuite/editor": "0.4.0-20230111171650-bc63456", "@blocksuite/editor": "0.4.0-alpha.2",
"@blocksuite/icons": "^2.0.2", "@blocksuite/icons": "^2.0.2",
"@blocksuite/store": "0.4.0-20230111171650-bc63456", "@blocksuite/store": "0.4.0-alpha.2",
"@emotion/css": "^11.10.0", "@emotion/css": "^11.10.0",
"@emotion/react": "^11.10.4", "@emotion/react": "^11.10.4",
"@emotion/server": "^11.10.0", "@emotion/server": "^11.10.0",
@@ -28,7 +28,7 @@
"cmdk": "^0.1.20", "cmdk": "^0.1.20",
"css-spring": "^4.1.0", "css-spring": "^4.1.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"lit": "^2.3.1", "lit": "^2.6.1",
"next": "13.1.0", "next": "13.1.0",
"next-debug-local": "^0.1.5", "next-debug-local": "^0.1.5",
"prettier": "^2.7.1", "prettier": "^2.7.1",
@@ -36,9 +36,10 @@
"quill-cursors": "^4.0.0", "quill-cursors": "^4.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"yjs": "^13.5.44" "yjs": "^13.5.45"
}, },
"devDependencies": { "devDependencies": {
"@emotion/babel-plugin": "^11.10.5",
"@types/node": "18.7.18", "@types/node": "18.7.18",
"@types/react": "18.0.20", "@types/react": "18.0.20",
"@types/react-dom": "18.0.6", "@types/react-dom": "18.0.6",
@@ -50,7 +51,7 @@
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"next-pwa": "^5.6.0", "next-pwa": "^5.6.0",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"typescript": "4.8.3" "typescript": "^4.9.5"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@@ -144,12 +144,12 @@ button,
select, select,
keygen, keygen,
legend { legend {
color: var(--affine-text-color);
background-color: unset; background-color: unset;
outline: 0; outline: 0;
border: 0; border: 0;
font-size: 18px; font-size: var(--affine-font-base);
line-height: 1.5; line-height: var(--affine-line-height);
color: var(--affine-text-color);
font-family: var(--affine-font-family); font-family: var(--affine-font-family);
} }
body { body {
@@ -173,7 +173,7 @@ input {
} }
input:-webkit-autofill { input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset; -webkit-box-shadow: 0 0 0 1000px white inset;
} }
input[type='number'] { input[type='number'] {

View File

@@ -7,7 +7,7 @@ export const NotfoundPage = () => {
const router = useRouter(); const router = useRouter();
return ( return (
<PageContainer> <PageContainer>
<NotFoundTitle> <NotFoundTitle data-testid="notFound">
{t('404 - Page Not Found')} {t('404 - Page Not Found')}
<p> <p>
<Button <Button
@@ -15,7 +15,7 @@ export const NotfoundPage = () => {
router.push('/workspace'); router.push('/workspace');
}} }}
> >
Back Home {t('Back Home')}
</Button> </Button>
</p> </p>
</NotFoundTitle> </NotFoundTitle>

View File

@@ -91,8 +91,6 @@ export const ContactModal = ({
<span>Alpha</span> <span>Alpha</span>
</StyledModalHeaderLeft> </StyledModalHeaderLeft>
<ModalCloseButton <ModalCloseButton
top={6}
right={6}
onClick={() => { onClick={() => {
onClose(); onClose();
}} }}

View File

@@ -53,13 +53,12 @@ export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => {
</Header> </Header>
<Content> <Content>
<ContentTitle>{t('New Workspace')}</ContentTitle> <ContentTitle>{t('New Workspace')}</ContentTitle>
<p> <p>{t('Workspace description')}</p>
Workspace is your virtual space to capture, create and plan as
just one person or together as a team.
</p>
<Input <Input
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
placeholder={'Set a Workspace name'} placeholder={t('Set a Workspace name')}
maxLength={15}
minLength={0}
onChange={value => { onChange={value => {
setWorkspaceName(value); setWorkspaceName(value);
}} }}

View File

@@ -29,8 +29,6 @@ export const DeleteModal = ({
<ModalWrapper width={620} height={334}> <ModalWrapper width={620} height={334}>
<Header> <Header>
<ModalCloseButton <ModalCloseButton
top={6}
right={6}
onClick={() => { onClick={() => {
onClose(); onClose();
}} }}

View File

@@ -14,8 +14,8 @@ import {
UndoIcon, UndoIcon,
RedoIcon, RedoIcon,
} from './Icons'; } from './Icons';
import { MuiSlide } from '@/ui/mui';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import Slide from '@mui/material/Slide';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import useHistoryUpdated from '@/hooks/use-history-update'; import useHistoryUpdated from '@/hooks/use-history-update';
@@ -127,7 +127,7 @@ export const EdgelessToolbar = () => {
const { mode } = useCurrentPageMeta() || {}; const { mode } = useCurrentPageMeta() || {};
return ( return (
<Slide <MuiSlide
direction="right" direction="right"
in={mode === 'edgeless'} in={mode === 'edgeless'}
mountOnEnter mountOnEnter
@@ -155,7 +155,7 @@ export const EdgelessToolbar = () => {
</StyledToolbarWrapper> </StyledToolbarWrapper>
<UndoRedo /> <UndoRedo />
</StyledEdgelessToolbar> </StyledEdgelessToolbar>
</Slide> </MuiSlide>
); );
}; };

View File

@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react';
import type { Page, Workspace } from '@blocksuite/store'; import type { Page, Workspace } from '@blocksuite/store';
import '@blocksuite/blocks'; import '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor'; import { EditorContainer } from '@blocksuite/editor';
import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhill.md'; import exampleMarkdown from '@/templates/Welcome-to-AFFiNE-Alpha-Downhills.md';
import { styled } from '@/styles'; import { styled } from '@/styles';
const StyledEditorContainer = styled('div')(() => { const StyledEditorContainer = styled('div')(() => {
@@ -39,22 +39,21 @@ export const Editor = ({ page, workspace, setEditor }: Props) => {
const title = metaTitle const title = metaTitle
? metaTitle ? metaTitle
: isFirstPage : isFirstPage
? 'Welcome to AFFiNE Alpha "Downhill"' ? 'Welcome to AFFiNE Alpha "Downhills"'
: ''; : '';
workspace?.setPageMeta(page.id, { title }); workspace?.setPageMeta(page.id, { title });
const pageBlockId = page.addBlockByFlavour('affine:page', { title });
const pageId = page.addBlock({ page.addBlockByFlavour('affine:surface', {}, null);
flavour: 'affine:page', // Add frame block inside page block
title, const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId);
}); // Add paragraph block inside frame block
page.addBlock({ flavour: 'affine:surface' });
const frameId = page.addBlock({ flavour: 'affine:frame' }, pageId);
page.addBlock({ flavour: 'affine:frame' }, pageId);
// If this is a first page in workspace, init an introduction markdown // If this is a first page in workspace, init an introduction markdown
if (isFirstPage) { if (isFirstPage) {
editor.clipboard.importMarkdown(exampleMarkdown, `${frameId}`); editor.clipboard.importMarkdown(exampleMarkdown, frameId);
workspace.setPageMeta(page.id, { title }); workspace.setPageMeta(page.id, { title });
page.resetHistory(); page.resetHistory();
} else {
page.addBlockByFlavour('affine:paragraph', {}, frameId);
} }
page.resetHistory(); page.resetHistory();
} }

View File

@@ -0,0 +1,111 @@
import { styled } from '@/styles';
import { Modal, ModalWrapper } from '@/ui/modal';
import { Button, IconButton } from '@/ui/button';
import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider';
import { useState } from 'react';
import router from 'next/router';
import { toast } from '@/ui/toast';
import { CloseIcon } from '@blocksuite/icons';
interface EnableWorkspaceModalProps {
open: boolean;
onClose: () => void;
}
export const EnableWorkspaceModal = ({
open,
onClose,
}: EnableWorkspaceModalProps) => {
const { t } = useTranslation();
const { user, dataCenter, login, currentWorkspace } = useAppState();
const [loading, setLoading] = useState(false);
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}>
<Header>
<IconButton
onClick={() => {
onClose();
}}
>
<CloseIcon />
</IconButton>
</Header>
<Content>
<ContentTitle>{t('Enable AFFiNE Cloud')}?</ContentTitle>
<StyleTips>{t('Enable AFFiNE Cloud Description')}</StyleTips>
{/* <StyleTips>{t('Retain local cached data')}</StyleTips> */}
<div>
<StyleButton
shape="round"
type="primary"
loading={loading}
onClick={async () => {
setLoading(true);
if (!user) {
await login();
}
if (currentWorkspace) {
const workspace = await dataCenter.enableWorkspaceCloud(
currentWorkspace
);
workspace &&
router.push(`/workspace/${workspace.id}/setting`);
toast(t('Enabled success'));
}
}}
>
{user ? t('Enable') : t('Sign in and Enable')}
</StyleButton>
<StyleButton
shape="round"
onClick={() => {
onClose();
}}
>
{t('Not now')}
</StyleButton>
</div>
</Content>
</ModalWrapper>
</Modal>
);
};
const Header = styled('div')({
height: '44px',
display: 'flex',
flexDirection: 'row-reverse',
paddingRight: '10px',
paddingTop: '10px',
});
const Content = styled('div')({
textAlign: 'center',
});
const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
});
const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
width: '400px',
margin: 'auto',
marginBottom: '32px',
marginTop: '12px',
};
});
const StyleButton = styled(Button)(() => {
return {
width: '284px',
display: 'block',
margin: 'auto',
marginTop: '16px',
};
});

View File

@@ -1,24 +1,28 @@
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { useState } from 'react'; import { useState } from 'react';
import { EnableWorkspaceModal } from './EnableWorkspaceModal';
export const EnableWorkspaceButton = () => { export const EnableWorkspaceButton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { enableWorkspace } = useWorkspaceHelper(); const [enableModalOpen, setEnableModalOpen] = useState(false);
const [loading, setLoading] = useState(false);
return ( return (
<Button <>
type="primary" <Button
shape="circle" type="light"
loading={loading} shape="circle"
onClick={async () => { onClick={async () => {
setLoading(true); setEnableModalOpen(true);
await enableWorkspace(); }}
setLoading(false); >
}} {t('Enable AFFiNE Cloud')}
> </Button>
{t('Enable AFFiNE Cloud')} <EnableWorkspaceModal
</Button> open={enableModalOpen}
onClose={() => {
setEnableModalOpen(false);
}}
></EnableWorkspaceModal>
</>
); );
}; };

View File

@@ -6,7 +6,7 @@ import {
StyledBrowserWarning, StyledBrowserWarning,
StyledCloseButton, StyledCloseButton,
} from './styles'; } from './styles';
import CloseIcon from '@mui/icons-material/Close'; import { CloseIcon } from '@blocksuite/icons';
import { useWarningMessage, shouldShowWarning } from './utils'; import { useWarningMessage, shouldShowWarning } from './utils';
import EditorOptionMenu from './header-right-items/EditorOptionMenu'; import EditorOptionMenu from './header-right-items/EditorOptionMenu';
import TrashButtonGroup from './header-right-items/TrashButtonGroup'; import TrashButtonGroup from './header-right-items/TrashButtonGroup';

View File

@@ -1,5 +1,6 @@
import { PropsWithChildren, ReactNode } from 'react'; import { PropsWithChildren, ReactNode } from 'react';
import Header from './Header'; import Header from './Header';
import QuickSearchButton from './QuickSearchButton';
import { StyledPageListTittleWrapper } from './styles'; import { StyledPageListTittleWrapper } from './styles';
// import QuickSearchButton from './QuickSearchButton'; // import QuickSearchButton from './QuickSearchButton';
@@ -12,7 +13,7 @@ export const PageListHeader = ({ icon, children }: PageListHeaderProps) => {
<StyledPageListTittleWrapper> <StyledPageListTittleWrapper>
{icon} {icon}
{children} {children}
{/* <QuickSearchButton style={{ marginLeft: '5px' }} /> */} <QuickSearchButton />
</StyledPageListTittleWrapper> </StyledPageListTittleWrapper>
</Header> </Header>
); );

View File

@@ -1,15 +1,41 @@
import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons'; import { CloudUnsyncedIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/GlobalModalProvider'; import { useModal } from '@/providers/GlobalModalProvider';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { IconButton } from '@/ui/button'; import { IconButton } from '@/ui/button';
// Temporary solution to use this component, since the @blocksuite/icons has not been published yet
const DefaultSyncIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 13.6493C3 16.6044 5.41766 19 8.4 19L16.5 19C18.9853 19 21 16.9839 21 14.4969C21 12.6503 19.8893 10.9449 18.3 10.25C18.1317 7.32251 15.684 5 12.6893 5C10.3514 5 8.34694 6.48637 7.5 8.5C4.8 8.9375 3 11.2001 3 13.6493Z"
stroke="#888A9E"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.4571 9L9 16H10.4392L12.0021 11.1586L13.5657 16H15L12.5425 9H11.4571Z"
fill="#888A9E"
/>
</svg>
);
};
export const SyncUser = () => { export const SyncUser = () => {
const { triggerLoginModal } = useModal(); const { triggerLoginModal } = useModal();
const appState = useAppState(); const appState = useAppState();
return appState.user ? ( return appState.user ? (
<IconButton iconSize="middle" disabled> <IconButton iconSize="middle" disabled>
<CloudInsyncIcon /> <DefaultSyncIcon />
</IconButton> </IconButton>
) : ( ) : (
<IconButton <IconButton

View File

@@ -103,7 +103,7 @@ export const StyledSearchArrowWrapper = styled.div(() => {
export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => { export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => {
return { return {
fontSize: theme.font.sm, fontSize: theme.font.base,
color: theme.colors.textColor, color: theme.colors.textColor,
'>svg': { '>svg': {
fontSize: '20px', fontSize: '20px',

View File

@@ -6,7 +6,7 @@ import {
StyledTransformIcon, StyledTransformIcon,
} from './style'; } from './style';
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons'; import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
import Grow from '@mui/material/Grow'; import { MuiGrow } from '@/ui/mui';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { useModal } from '@/providers/GlobalModalProvider'; import { useModal } from '@/providers/GlobalModalProvider';
@@ -35,7 +35,7 @@ export const HelpIsland = ({
setShowContent(false); setShowContent(false);
}} }}
> >
<Grow in={showContent}> <MuiGrow in={showContent}>
<StyledIslandWrapper> <StyledIslandWrapper>
{showList.includes('contact') && ( {showList.includes('contact') && (
<Tooltip content={t('Contact Us')} placement="left-end"> <Tooltip content={t('Contact Us')} placement="left-end">
@@ -66,7 +66,7 @@ export const HelpIsland = ({
</Tooltip> </Tooltip>
)} )}
</StyledIslandWrapper> </StyledIslandWrapper>
</Grow> </MuiGrow>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<StyledIconWrapper <StyledIconWrapper

View File

@@ -1,7 +1,7 @@
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal'; import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { StyledButtonWrapper, StyledTitle } from './styles'; import { StyledButtonWrapper, StyledTitle } from './styles';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { Wrapper, Content } from '@/ui/layout'; import { Content, FlexWrapper } from '@/ui/layout';
import Loading from '@/components/loading'; import Loading from '@/components/loading';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
@@ -116,7 +116,7 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
)} )}
{status === 'importing' && ( {status === 'importing' && (
<Wrapper <FlexWrapper
wrap={true} wrap={true}
justifyContent="center" justifyContent="center"
style={{ marginTop: 22, paddingBottom: '32px' }} style={{ marginTop: 22, paddingBottom: '32px' }}
@@ -126,7 +126,7 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
OOOOPS! Sorry forgot to remind you that we are working on the OOOOPS! Sorry forgot to remind you that we are working on the
import function import function
</Content> </Content>
</Wrapper> </FlexWrapper>
)} )}
</ModalWrapper> </ModalWrapper>
</Modal> </Modal>

View File

@@ -0,0 +1,28 @@
export const GoogleIcon = () => {
return (
<svg
width="25"
height="24"
viewBox="0 0 25 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.327 16.3285 15.1115 18 12.5 18C9.1865 18 6.5 15.3135 6.5 12C6.5 8.6865 9.1865 6 12.5 6C14.0295 6 15.421 6.577 16.4805 7.5195L19.309 4.691C17.523 3.0265 15.134 2 12.5 2C6.9775 2 2.5 6.4775 2.5 12C2.5 17.5225 6.9775 22 12.5 22C18.0225 22 22.5 17.5225 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z"
fill="#FFC107"
/>
<path
d="M3.65234 7.3455L6.93784 9.755C7.82684 7.554 9.97984 6 12.4993 6C14.0288 6 15.4203 6.577 16.4798 7.5195L19.3083 4.691C17.5223 3.0265 15.1333 2 12.4993 2C8.65834 2 5.32734 4.1685 3.65234 7.3455Z"
fill="#FF3D00"
/>
<path
d="M12.5002 22.0003C15.0832 22.0003 17.4302 21.0118 19.2047 19.4043L16.1097 16.7853C15.0719 17.5745 13.8039 18.0014 12.5002 18.0003C9.89916 18.0003 7.69066 16.3418 6.85866 14.0273L3.59766 16.5398C5.25266 19.7783 8.61366 22.0003 12.5002 22.0003Z"
fill="#4CAF50"
/>
<path
d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.7571 15.1082 17.0467 16.0766 16.108 16.7855L16.1095 16.7845L19.2045 19.4035C18.9855 19.6025 22.5 17 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z"
fill="#1976D2"
/>
</svg>
);
};

View File

@@ -1,40 +0,0 @@
import { CloudUnsyncedIcon } from '@blocksuite/icons';
import { styled } from '@/styles';
import GoogleSvg from './google.svg';
export const GoogleIcon = () => {
return (
<GoogleIconWrapper>
<picture>
<img src={GoogleSvg.src} alt="Google" />
</picture>
</GoogleIconWrapper>
);
};
const GoogleIconWrapper = styled('div')(({ theme }) => ({
background: theme.colors.pageBackground,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}));
export const StayLogOutIcon = () => {
return (
<StayLogOutWrapper>
<CloudUnsyncedIcon />
</StayLogOutWrapper>
);
};
const StayLogOutWrapper = styled('div')(({ theme }) => {
return {
width: '48px',
height: '48px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '24px',
background: theme.colors.hoverBackground,
};
});

View File

@@ -1,99 +0,0 @@
import { styled } from '@/styles';
import { Button } from '@/ui/button';
import { GoogleIcon, StayLogOutIcon } from './Icons';
import { useTranslation } from '@affine/i18n';
export const GoogleLoginButton = () => {
const { t } = useTranslation();
return (
<StyledGoogleButton>
<ButtonWrapper>
<IconWrapper>
<GoogleIcon />
</IconWrapper>
<TextWrapper>{t('Continue with Google')}</TextWrapper>
</ButtonWrapper>
</StyledGoogleButton>
);
};
export const StayLogOutButton = () => {
const { t } = useTranslation();
return (
<StyledStayLogOutButton>
<ButtonWrapper>
<IconWrapper>
<StayLogOutIcon />
</IconWrapper>
<TextWrapper>
<Title>{t('Stay logged out')}</Title>
<Description>{t('All changes are saved locally')}</Description>
</TextWrapper>
</ButtonWrapper>
</StyledStayLogOutButton>
);
};
const StyledGoogleButton = styled('div')(({ theme }) => {
return {
width: '284px',
height: '40px',
marginTop: '30px',
fontSize: '16px',
cursor: 'pointer',
borderRadius: '40px',
border: `1px solid ${theme.colors.iconColor}`,
overflow: 'hidden',
':hover': {
border: `1px solid ${theme.colors.primaryColor}`,
},
};
});
const StyledStayLogOutButton = styled(Button)(() => {
return {
width: '361px',
height: '56px',
padding: '4px',
':hover': {
borderColor: '#6880FF',
},
};
});
const ButtonWrapper = styled('div')({
display: 'flex',
flexDirection: 'row',
width: '100%',
});
const IconWrapper = styled('div')({
flex: '0 48px',
borderRadius: '5px',
overflow: 'hidden',
marginRight: '12px',
marginTop: '8px',
});
const TextWrapper = styled('div')({
flex: 1,
textAlign: 'left',
height: '40px',
lineHeight: '40px',
});
const Title = styled('h1')(() => {
return {
fontSize: '18px',
lineHeight: '26px',
fontWeight: 500,
};
});
const Description = styled('p')(() => {
return {
fontSize: '16px',
lineHeight: '22px',
fontWeight: 400,
};
});

View File

@@ -1,6 +0,0 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.327 16.3285 15.1115 18 12.5 18C9.1865 18 6.5 15.3135 6.5 12C6.5 8.6865 9.1865 6 12.5 6C14.0295 6 15.421 6.577 16.4805 7.5195L19.309 4.691C17.523 3.0265 15.134 2 12.5 2C6.9775 2 2.5 6.4775 2.5 12C2.5 17.5225 6.9775 22 12.5 22C18.0225 22 22.5 17.5225 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#FFC107"/>
<path d="M3.65234 7.3455L6.93784 9.755C7.82684 7.554 9.97984 6 12.4993 6C14.0288 6 15.4203 6.577 16.4798 7.5195L19.3083 4.691C17.5223 3.0265 15.1333 2 12.4993 2C8.65834 2 5.32734 4.1685 3.65234 7.3455Z" fill="#FF3D00"/>
<path d="M12.5002 22.0003C15.0832 22.0003 17.4302 21.0118 19.2047 19.4043L16.1097 16.7853C15.0719 17.5745 13.8039 18.0014 12.5002 18.0003C9.89916 18.0003 7.69066 16.3418 6.85866 14.0273L3.59766 16.5398C5.25266 19.7783 8.61366 22.0003 12.5002 22.0003Z" fill="#4CAF50"/>
<path d="M22.3055 10.0415H21.5V10H12.5V14H18.1515C17.7571 15.1082 17.0467 16.0766 16.108 16.7855L16.1095 16.7845L19.2045 19.4035C18.9855 19.6025 22.5 17 22.5 12C22.5 11.3295 22.431 10.675 22.3055 10.0415Z" fill="#1976D2"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,7 +1,9 @@
import { styled } from '@/styles'; import { positionAbsolute, styled } from '@/styles';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal'; import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { GoogleLoginButton } from './LoginOptionButton'; import { Button } from '@/ui/button';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { useTranslation } from '@affine/i18n';
import { GoogleIcon } from './GoogleIcon';
interface LoginModalProps { interface LoginModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@@ -9,38 +11,44 @@ interface LoginModalProps {
export const LoginModal = ({ open, onClose }: LoginModalProps) => { export const LoginModal = ({ open, onClose }: LoginModalProps) => {
const { login } = useAppState(); const { login } = useAppState();
const { t } = useTranslation();
return ( return (
<Modal open={open} onClose={onClose} data-testid="login-modal"> <Modal open={open} onClose={onClose} data-testid="login-modal">
<ModalWrapper width={560} height={292}> <ModalWrapper width={560} height={292} style={{ paddingTop: '44px' }}>
<Header> <ModalCloseButton
<ModalCloseButton onClick={() => {
top={6} onClose();
right={6} }}
onClick={() => { />
onClose();
}}
/>
</Header>
<Content> <Content>
<ContentTitle>{'Sign in'}</ContentTitle> <ContentTitle>{t('Sign in')}</ContentTitle>
<SignDes>Set up an AFFINE account to sync data</SignDes> <SignDes>{t('Set up an AFFiNE account to sync data')}</SignDes>
<span <StyledLoginButton
shape="round"
onClick={async () => { onClick={async () => {
await login(); await login();
onClose(); onClose();
}} }}
> >
<GoogleLoginButton /> <GoogleIcon />
</span> {t('Continue with Google')}
</StyledLoginButton>
</Content> </Content>
</ModalWrapper> </ModalWrapper>
</Modal> </Modal>
); );
}; };
const Header = styled('div')({ const StyledLoginButton = styled(Button)(() => {
position: 'relative', return {
height: '44px', width: '284px',
marginTop: '30px',
position: 'relative',
svg: {
...positionAbsolute({ left: '18px', top: '0', bottom: '0' }),
margin: 'auto',
},
};
}); });
const Content = styled('div')({ const Content = styled('div')({

View File

@@ -8,7 +8,7 @@ export const Check = () => {
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<g clip-path="url(#clip0_9266_16831)"> <g clipPath="url(#clip0_9266_16831)">
<path <path
fillRule="evenodd" fillRule="evenodd"
clipRule="evenodd" clipRule="evenodd"

View File

@@ -3,28 +3,34 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { Check, UnCheck } from './icon'; import { Check, UnCheck } from './icon';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider';
interface LoginModalProps { interface LoginModalProps {
open: boolean; open: boolean;
onClose: (wait: boolean) => void; onClose: (wait: boolean) => void;
} }
export const LogoutModal = ({ open, onClose }: LoginModalProps) => { export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
const [localCache, setLocalCache] = useState(false); const [localCache, setLocalCache] = useState(true);
const { blobDataSynced } = useAppState();
const { t } = useTranslation();
return ( return (
<Modal open={open} onClose={onClose} data-testid="logout-modal"> <Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}> <ModalWrapper width={560} height={292}>
<Header> <Header>
<ModalCloseButton <ModalCloseButton
top={6}
right={6}
onClick={() => { onClick={() => {
onClose(true); onClose(true);
}} }}
/> />
</Header> </Header>
<Content> <Content>
<ContentTitle>{'Sign out?'}</ContentTitle> <ContentTitle>{t('Sign out')}?</ContentTitle>
<SignDes>Set up an AFFINE account to sync data</SignDes> <SignDes>
{blobDataSynced
? t('Set up an AFFiNE account to sync data')
: 'All data has been stored in the cloud'}
</SignDes>
<StyleTips> <StyleTips>
{localCache ? ( {localCache ? (
<StyleCheck <StyleCheck
@@ -43,28 +49,51 @@ export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
<UnCheck></UnCheck> <UnCheck></UnCheck>
</StyleCheck> </StyleCheck>
)} )}
Retain local cached data {t('Retain local cached data')}
</StyleTips> </StyleTips>
<div> {blobDataSynced ? (
<Button <div>
style={{ marginRight: '16px' }} <Button
shape="round" type="danger"
onClick={() => { shape="round"
onClose(true); style={{ marginRight: '16px' }}
}} onClick={async () => {
> onClose(false);
Wait for Sync }}
</Button> >
<Button {t('Force Sign Out')}
type="danger" </Button>
shape="round" <Button
onClick={() => { shape="round"
onClose(false); onClick={() => {
}} onClose(true);
> }}
Force Sign Out >
</Button> {t('Wait for Sync')}
</div> </Button>
</div>
) : (
<div>
<Button
type="primary"
style={{ marginRight: '16px' }}
shape="round"
onClick={() => {
onClose(true);
}}
>
{t('Cancel')}
</Button>
<Button
shape="round"
onClick={() => {
onClose(false);
}}
>
{t('Sign out')}
</Button>
</div>
)}
</Content> </Content>
</ModalWrapper> </ModalWrapper>
</Modal> </Modal>

View File

@@ -14,7 +14,6 @@ export const PageListEmpty = (props: { listType?: string }) => {
{listType === 'all' && <p>{t('emptyAllPages')}</p>} {listType === 'all' && <p>{t('emptyAllPages')}</p>}
{listType === 'favorite' && <p>{t('emptyFavourite')}</p>} {listType === 'favorite' && <p>{t('emptyFavourite')}</p>}
{listType === 'trash' && <p>{t('emptyTrash')}</p>} {listType === 'trash' && <p>{t('emptyTrash')}</p>}
<p>{t('still designed')}</p>
</div> </div>
); );
}; };

View File

@@ -1,7 +1,7 @@
import { useConfirm } from '@/providers/ConfirmProvider'; import { useConfirm } from '@/providers/ConfirmProvider';
import { PageMeta } from '@/providers/app-state-provider'; import { PageMeta } from '@/providers/app-state-provider';
import { Menu, MenuItem } from '@/ui/menu'; import { Menu, MenuItem } from '@/ui/menu';
import { Wrapper } from '@/ui/layout'; import { FlexWrapper } from '@/ui/layout';
import { IconButton } from '@/ui/button'; import { IconButton } from '@/ui/button';
import { import {
MoreVerticalIcon, MoreVerticalIcon,
@@ -63,13 +63,13 @@ export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
</> </>
); );
return ( return (
<Wrapper alignItems="center" justifyContent="center"> <FlexWrapper alignItems="center" justifyContent="center">
<Menu content={OperationMenu} placement="bottom-end" disablePortal={true}> <Menu content={OperationMenu} placement="bottom-end" disablePortal={true}>
<IconButton darker={true}> <IconButton darker={true}>
<MoreVerticalIcon /> <MoreVerticalIcon />
</IconButton> </IconButton>
</Menu> </Menu>
</Wrapper> </FlexWrapper>
); );
}; };
@@ -80,7 +80,7 @@ export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { confirm } = useConfirm(); const { confirm } = useConfirm();
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Wrapper> <FlexWrapper>
<IconButton <IconButton
darker={true} darker={true}
style={{ marginRight: '12px' }} style={{ marginRight: '12px' }}
@@ -108,6 +108,6 @@ export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
> >
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</Wrapper> </FlexWrapper>
); );
}; };

View File

@@ -1,26 +1,23 @@
import React from 'react'; import React from 'react';
import { AddIcon } from '@blocksuite/icons'; import { AddIcon } from '@blocksuite/icons';
import { StyledModalFooterContent } from './style'; import { StyledModalFooterContent } from './style';
import { useModal } from '@/providers/GlobalModalProvider';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
export const Footer = (props: { query: string }) => { export const Footer = (props: { query: string; onClose: () => void }) => {
const { triggerQuickSearchModal } = useModal();
const { openPage, createPage } = usePageHelper(); const { openPage, createPage } = usePageHelper();
const { t } = useTranslation(); const { t } = useTranslation();
const query = props.query; const { query, onClose } = props;
return ( return (
<Command.Item <Command.Item
data-testid="quickSearch-addNewPage" data-testid="quickSearch-addNewPage"
onSelect={async () => { onSelect={async () => {
onClose();
const pageId = await createPage({ title: query }); const pageId = await createPage({ title: query });
if (pageId) { if (pageId) {
openPage(pageId); openPage(pageId);
} }
triggerQuickSearchModal();
}} }}
> >
<StyledModalFooterContent> <StyledModalFooterContent>

View File

@@ -8,32 +8,41 @@ import React, {
import { SearchIcon } from '@blocksuite/icons'; import { SearchIcon } from '@blocksuite/icons';
import { StyledInputContent, StyledLabel } from './style'; import { StyledInputContent, StyledLabel } from './style';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { useAppState } from '@/providers/app-state-provider';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
export const Input = (props: { export const Input = (props: {
open: boolean;
query: string; query: string;
setQuery: Dispatch<SetStateAction<string>>; setQuery: Dispatch<SetStateAction<string>>;
setLoading: Dispatch<SetStateAction<boolean>>; setLoading: Dispatch<SetStateAction<boolean>>;
isPublic: boolean;
publishWorkspaceName: string | undefined;
}) => { }) => {
const { open, query, setQuery, setLoading, isPublic, publishWorkspaceName } =
props;
const [isComposition, setIsComposition] = useState(false); const [isComposition, setIsComposition] = useState(false);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const { currentWorkspace } = useAppState();
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => { useEffect(() => {
inputRef.current?.addEventListener( if (open) {
'blur', const inputElement = inputRef.current;
() => { return inputElement?.focus();
inputRef.current?.focus(); }
}, }, [open]);
true
);
return inputRef.current?.focus();
}, [inputRef]);
useEffect(() => { useEffect(() => {
return setInputValue(props.query); const inputElement = inputRef.current;
// eslint-disable-next-line react-hooks/exhaustive-deps 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 ( return (
<StyledInputContent> <StyledInputContent>
<StyledLabel htmlFor=":r5:"> <StyledLabel htmlFor=":r5:">
@@ -46,18 +55,18 @@ export const Input = (props: {
setIsComposition(true); setIsComposition(true);
}} }}
onCompositionEnd={e => { onCompositionEnd={e => {
props.setQuery(e.data); setQuery(e.data);
setIsComposition(false); setIsComposition(false);
if (!props.query) { if (!query) {
props.setLoading(true); setLoading(true);
} }
}} }}
onValueChange={str => { onValueChange={str => {
setInputValue(str); setInputValue(str);
if (!isComposition) { if (!isComposition) {
props.setQuery(str); setQuery(str);
if (!props.query) { if (!query) {
props.setLoading(true); setLoading(true);
} }
} }
}} }}
@@ -78,9 +87,9 @@ export const Input = (props: {
} }
}} }}
placeholder={ placeholder={
currentWorkspace?.isPublish isPublic
? t('Quick search placeholder2', { ? t('Quick search placeholder2', {
workspace: currentWorkspace?.blocksuiteWorkspace?.meta.name, workspace: publishWorkspaceName,
}) })
: t('Quick search placeholder') : t('Quick search placeholder')
} }

View File

@@ -0,0 +1,95 @@
import { Command } from 'cmdk';
import { StyledListItem, StyledNotFound } from './style';
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useAppState, PageMeta } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { NoResultSVG } from './NoResultSVG';
import { useTranslation } from '@affine/i18n';
import usePageHelper from '@/hooks/use-page-helper';
import { Workspace } from '@blocksuite/store';
export const PublishedResults = (props: {
query: string;
loading: boolean;
setLoading: Dispatch<SetStateAction<boolean>>;
setPublishWorkspaceName: Dispatch<SetStateAction<string>>;
onClose: () => void;
}) => {
const [workspace, setWorkspace] = useState<Workspace>();
const { query, loading, setLoading, onClose, setPublishWorkspaceName } =
props;
const { search } = usePageHelper();
const [results, setResults] = useState(new Map<string, string | undefined>());
const { dataCenter } = useAppState();
const router = useRouter();
const [pageList, setPageList] = useState<PageMeta[]>([]);
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(search(query, workspace));
setLoading(false);
//Save the Map<BlockId, PageId> obtained from the search as state
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query, setResults, setLoading]);
const pageIds = [...results.values()];
const resultsPageMeta = pageList.filter(
page => pageIds.indexOf(page.id) > -1 && !page.trash
);
return loading ? null : (
<>
{query ? (
resultsPageMeta.length ? (
<Command.Group
heading={t('Find results', { number: resultsPageMeta.length })}
>
{resultsPageMeta.map(result => {
return (
<Command.Item
key={result.id}
onSelect={() => {
router.push(
`/public-workspace/${router.query.workspaceId}/${result.id}`
);
onClose();
}}
value={result.id}
>
<StyledListItem>
{result.mode === 'edgeless' ? (
<EdgelessIcon />
) : (
<PaperIcon />
)}
<span>{result.title}</span>
</StyledListItem>
</Command.Item>
);
})}
</Command.Group>
) : (
<StyledNotFound>
<span>{t('Find 0 result')}</span>
<NoResultSVG />
</StyledNotFound>
)
) : (
<></>
)}
</>
);
};

View File

@@ -1,6 +1,5 @@
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { StyledListItem, StyledNotFound } from './style'; import { StyledListItem, StyledNotFound } from './style';
import { useModal } from '@/providers/GlobalModalProvider';
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons'; import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
@@ -12,14 +11,11 @@ import usePageHelper from '@/hooks/use-page-helper';
export const Results = (props: { export const Results = (props: {
query: string; query: string;
loading: boolean; loading: boolean;
onClose: () => void;
setLoading: Dispatch<SetStateAction<boolean>>; setLoading: Dispatch<SetStateAction<boolean>>;
setShowCreatePage: Dispatch<SetStateAction<boolean>>; setShowCreatePage: Dispatch<SetStateAction<boolean>>;
}) => { }) => {
const query = props.query; const { query, loading, setLoading, setShowCreatePage, onClose } = props;
const loading = props.loading;
const setLoading = props.setLoading;
const setShowCreatePage = props.setShowCreatePage;
const { triggerQuickSearchModal } = useModal();
const { openPage } = usePageHelper(); const { openPage } = usePageHelper();
const router = useRouter(); const router = useRouter();
const { currentWorkspace, pageList } = useAppState(); const { currentWorkspace, pageList } = useAppState();
@@ -55,8 +51,8 @@ export const Results = (props: {
<Command.Item <Command.Item
key={result.id} key={result.id}
onSelect={() => { onSelect={() => {
onClose();
openPage(result.id); openPage(result.id);
triggerQuickSearchModal();
}} }}
value={result.id} value={result.id}
> >
@@ -86,8 +82,8 @@ export const Results = (props: {
key={link.title} key={link.title}
value={link.title} value={link.title}
onSelect={() => { onSelect={() => {
onClose();
router.push(link.href); router.push(link.href);
triggerQuickSearchModal();
}} }}
> >
<StyledListItem> <StyledListItem>

View File

@@ -1,5 +1,10 @@
import { FC, SVGProps } from 'react'; import { FC, SVGProps } from 'react';
import { AllPagesIcon, FavouritesIcon, TrashIcon } from '@blocksuite/icons'; import {
AllPagesIcon,
FavouritesIcon,
TrashIcon,
SettingsIcon,
} from '@blocksuite/icons';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
export const useSwitchToConfig = ( export const useSwitchToConfig = (
@@ -23,6 +28,13 @@ export const useSwitchToConfig = (
: '', : '',
icon: FavouritesIcon, icon: FavouritesIcon,
}, },
{
title: t('Settings'),
href: currentWorkspaceId
? `/workspace/${currentWorkspaceId}/setting`
: '',
icon: SettingsIcon,
},
{ {
title: t('Trash'), title: t('Trash'),
href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/trash` : '', href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/trash` : '',

View File

@@ -13,7 +13,8 @@ import { Command } from 'cmdk';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useModal } from '@/providers/GlobalModalProvider'; import { useModal } from '@/providers/GlobalModalProvider';
import { getUaHelper } from '@/utils'; import { getUaHelper } from '@/utils';
import { useAppState } from '@/providers/app-state-provider'; import { useRouter } from 'next/router';
import { PublishedResults } from './PublishedResults';
type TransitionsModalProps = { type TransitionsModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@@ -22,18 +23,30 @@ const isMac = () => {
return getUaHelper().isMacOs; return getUaHelper().isMacOs;
}; };
export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => { export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
const { currentWorkspace } = useAppState(); const router = useRouter();
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isPublic, setIsPublic] = useState(false);
const [publishWorkspaceName, setPublishWorkspaceName] = useState('');
const [showCreatePage, setShowCreatePage] = useState(true); const [showCreatePage, setShowCreatePage] = useState(true);
const { triggerQuickSearchModal } = useModal(); const { triggerQuickSearchModal } = useModal();
const isPublicAndNoQuery = () => {
return isPublic && query.length === 0;
};
const handleClose = () => {
setQuery('');
onClose();
};
// Add ‘⌘+K’ shortcut keys as switches // Add ‘⌘+K’ shortcut keys as switches
useEffect(() => { useEffect(() => {
if (router.pathname.startsWith('/404')) {
triggerQuickSearchModal(false);
return;
}
const down = (e: KeyboardEvent) => { const down = (e: KeyboardEvent) => {
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) { if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
const selection = window.getSelection(); const selection = window.getSelection();
setQuery('');
if (selection?.toString()) { if (selection?.toString()) {
triggerQuickSearchModal(false); triggerQuickSearchModal(false);
return; return;
@@ -43,18 +56,23 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
} }
} }
}; };
if (!open) {
setQuery('');
}
document.addEventListener('keydown', down, { capture: true }); document.addEventListener('keydown', down, { capture: true });
return () => return () =>
document.removeEventListener('keydown', down, { capture: true }); document.removeEventListener('keydown', down, { capture: true });
}, [open, triggerQuickSearchModal]); }, [open, router.pathname, triggerQuickSearchModal]);
useEffect(() => {
if (router.pathname.startsWith('/public-workspace')) {
return setIsPublic(true);
} else {
return setIsPublic(false);
}
}, [router]);
return ( return (
<Modal <Modal
open={open} open={open}
onClose={onClose} onClose={handleClose}
wrapperPosition={['top', 'center']} wrapperPosition={['top', 'center']}
data-testid="quickSearch" data-testid="quickSearch"
> >
@@ -62,7 +80,7 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
width={620} width={620}
style={{ style={{
maxHeight: '80vh', maxHeight: '80vh',
minHeight: '350px', minHeight: isPublicAndNoQuery() ? '72px' : '350px',
top: '12vh', top: '12vh',
}} }}
> >
@@ -81,28 +99,51 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
}} }}
> >
<StyledModalHeader> <StyledModalHeader>
<Input query={query} setQuery={setQuery} setLoading={setLoading} /> <Input
open={open}
query={query}
setQuery={setQuery}
setLoading={setLoading}
isPublic={isPublic}
publishWorkspaceName={publishWorkspaceName}
/>
<StyledShortcut>{isMac() ? '⌘ + K' : 'Ctrl + K'}</StyledShortcut> <StyledShortcut>{isMac() ? '⌘ + K' : 'Ctrl + K'}</StyledShortcut>
</StyledModalHeader> </StyledModalHeader>
<StyledModalDivider /> <StyledModalDivider
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
/>
<Command.List> <Command.List>
<StyledContent> <StyledContent
<Results style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
query={query} >
loading={loading} {!isPublic ? (
setLoading={setLoading} <Results
setShowCreatePage={setShowCreatePage} query={query}
/> loading={loading}
setLoading={setLoading}
onClose={handleClose}
setShowCreatePage={setShowCreatePage}
/>
) : (
<PublishedResults
query={query}
loading={loading}
setLoading={setLoading}
onClose={handleClose}
setPublishWorkspaceName={setPublishWorkspaceName}
data-testid="publishedSearchResults"
/>
)}
</StyledContent> </StyledContent>
{currentWorkspace?.published ? ( {!isPublic ? (
<></> showCreatePage ? (
) : showCreatePage ? ( <>
<> <StyledModalDivider />
<StyledModalDivider /> <StyledModalFooter>
<StyledModalFooter> <Footer query={query} onClose={handleClose} />
<Footer query={query} /> </StyledModalFooter>
</StyledModalFooter> </>
</> ) : null
) : null} ) : null}
</Command.List> </Command.List>
</Command> </Command>

View File

@@ -9,6 +9,7 @@ export const StyledContent = styled('div')(({ theme }) => {
marginBottom: '10px', marginBottom: '10px',
...displayFlex('center', 'flex-start'), ...displayFlex('center', 'flex-start'),
color: theme.colors.popoverColor, color: theme.colors.popoverColor,
transition: 'all 0.15s',
letterSpacing: '0.06em', letterSpacing: '0.06em',
'[cmdk-group-heading]': { '[cmdk-group-heading]': {
margin: '5px 16px', margin: '5px 16px',
@@ -16,7 +17,6 @@ export const StyledContent = styled('div')(({ theme }) => {
fontWeight: '500', fontWeight: '500',
}, },
'[aria-selected="true"]': { '[aria-selected="true"]': {
transition: 'background .15s, color .15s',
borderRadius: '5px', borderRadius: '5px',
color: theme.colors.primaryColor, color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground, backgroundColor: theme.colors.hoverBackground,
@@ -103,6 +103,7 @@ export const StyledModalDivider = styled('div')(({ theme }) => {
margin: '6px 16px 6.5px 16px', margin: '6px 16px 6.5px 16px',
position: 'relative', position: 'relative',
borderTop: `0.5px solid ${theme.colors.placeHolderColor}`, borderTop: `0.5px solid ${theme.colors.placeHolderColor}`,
transition: 'all 0.15s',
}; };
}); });

View File

@@ -13,7 +13,7 @@ import {
useWindowsKeyboardShortcuts, useWindowsKeyboardShortcuts,
useWinMarkdownShortcuts, useWinMarkdownShortcuts,
} from '@/components/shortcuts-modal/config'; } from '@/components/shortcuts-modal/config';
import Slide from '@mui/material/Slide'; import { MuiSlide } from '@/ui/mui';
import { ModalCloseButton } from '@/ui/modal'; import { ModalCloseButton } from '@/ui/modal';
import { getUaHelper } from '@/utils'; import { getUaHelper } from '@/utils';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
@@ -40,7 +40,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
: windowsKeyboardShortcuts; : windowsKeyboardShortcuts;
return createPortal( return createPortal(
<Slide direction="left" in={open} mountOnEnter unmountOnExit> <MuiSlide direction="left" in={open} mountOnEnter unmountOnExit>
<StyledShortcutsModal data-testid="shortcuts-modal"> <StyledShortcutsModal data-testid="shortcuts-modal">
<> <>
<StyledModalHeader> <StyledModalHeader>
@@ -81,7 +81,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
})} })}
</> </>
</StyledShortcutsModal> </StyledShortcutsModal>
</Slide>, </MuiSlide>,
document.body document.body
); );
}; };

View File

@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import type { WorkspaceUnit } from '@affine/datacenter'; import type { WorkspaceUnit } from '@affine/datacenter';
import { WorkspaceAvatar as Avatar } from './Avatar'; import { WorkspaceAvatar as Avatar } from './Avatar';
const useAvatar = (workspaceUnit?: WorkspaceUnit) => { const useAvatar = (workspaceUnit: WorkspaceUnit | null) => {
const [avatarUrl, setAvatarUrl] = useState(''); const [avatarUrl, setAvatarUrl] = useState('');
const avatarId = const avatarId =
workspaceUnit?.avatar || workspaceUnit?.blocksuiteWorkspace?.meta.avatar; workspaceUnit?.avatar || workspaceUnit?.blocksuiteWorkspace?.meta.avatar;
@@ -23,7 +23,7 @@ const useAvatar = (workspaceUnit?: WorkspaceUnit) => {
export const WorkspaceUnitAvatar = ({ export const WorkspaceUnitAvatar = ({
size = 20, size = 20,
name, name,
workspaceUnit, workspaceUnit = null,
style, style,
}: { }: {
size?: number; size?: number;
@@ -31,7 +31,7 @@ export const WorkspaceUnitAvatar = ({
workspaceUnit?: WorkspaceUnit | null; workspaceUnit?: WorkspaceUnit | null;
style?: React.CSSProperties; style?: React.CSSProperties;
}) => { }) => {
const avatarUrl = useAvatar(workspaceUnit || undefined); const avatarUrl = useAvatar(workspaceUnit);
return ( return (
<Avatar <Avatar
size={size} size={size}

View File

@@ -0,0 +1,64 @@
import { CloudInsyncIcon, LogOutIcon } from '@blocksuite/icons';
import { FlexWrapper } from '@/ui/layout';
import { WorkspaceAvatar } from '@/components/workspace-avatar';
import { IconButton } from '@/ui/button';
import { useAppState } from '@/providers/app-state-provider';
import { StyledFooter, StyleUserInfo, StyledSignInButton } from './styles';
import { useTranslation } from '@affine/i18n';
import { Tooltip } from '@/ui/tooltip';
export const Footer = ({
onLogin,
onLogout,
}: {
onLogin: () => void;
onLogout: () => void;
}) => {
const { user } = useAppState();
const { t } = useTranslation();
return (
<StyledFooter>
{user && (
<>
<FlexWrapper>
<WorkspaceAvatar
size={40}
name={user.name}
avatar={user.avatar}
></WorkspaceAvatar>
<StyleUserInfo>
<p>{user.name}</p>
<p>{user.email}</p>
</StyleUserInfo>
</FlexWrapper>
<Tooltip content={t('Sign out')} disablePortal={true}>
<IconButton
onClick={() => {
onLogout();
}}
>
<LogOutIcon />
</IconButton>
</Tooltip>
</>
)}
{!user && (
<StyledSignInButton
noBorder
bold
icon={
<div className="circle">
<CloudInsyncIcon fontSize={16} />
</div>
}
onClick={async () => {
onLogin();
}}
>
{t('Sign in')}
</StyledSignInButton>
)}
</StyledFooter>
);
};

View File

@@ -0,0 +1,62 @@
import { LOCALES } from '@affine/i18n';
import { styled } from '@/styles';
import { useTranslation } from '@affine/i18n';
import { ArrowDownIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button';
import { Menu, MenuItem } from '@/ui/menu';
const LanguageMenuContent = () => {
const { i18n } = useTranslation();
const changeLanguage = (event: string) => {
i18n.changeLanguage(event);
};
return (
<>
{LOCALES.map(option => {
return (
<ListItem
key={option.name}
title={option.name}
onClick={() => {
changeLanguage(option.tag);
}}
>
{option.originalName}
</ListItem>
);
})}
</>
);
};
export const LanguageMenu = () => {
const { i18n } = useTranslation();
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
return (
<Menu
content={<LanguageMenuContent />}
placement="bottom"
trigger="click"
disablePortal={true}
>
<Button
icon={<ArrowDownIcon />}
iconPosition="end"
noBorder={true}
style={{ textTransform: 'capitalize' }}
data-testid="language-menu-button"
>
{currentLanguage?.originalName}
</Button>
</Menu>
);
};
const ListItem = styled(MenuItem)(({ theme }) => ({
height: '38px',
color: theme.colors.popoverColor,
fontSize: theme.font.sm,
textTransform: 'capitalize',
padding: '0 24px',
}));

View File

@@ -0,0 +1,83 @@
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import {
CloudIcon,
LocalIcon,
OfflineIcon,
PublishedIcon,
} from '@/components/workspace-modal/icons';
import { UsersIcon } from '@blocksuite/icons';
import { WorkspaceUnit } from '@affine/datacenter';
import { useAppState } from '@/providers/app-state-provider';
import { StyleWorkspaceInfo, StyleWorkspaceTitle, StyledCard } from './styles';
import { useTranslation } from '@affine/i18n';
import { FlexWrapper } from '@/ui/layout';
const WorkspaceType = ({ workspaceData }: { workspaceData: WorkspaceUnit }) => {
const { user } = useAppState();
const { t } = useTranslation();
const isOwner = user?.id === workspaceData.owner?.id;
if (workspaceData.provider === 'local') {
return (
<p>
<LocalIcon />
{t('Local Workspace')}
</p>
);
}
return isOwner ? (
<p>
<CloudIcon />
{t('Cloud Workspace')}
</p>
) : (
<p>
<UsersIcon fontSize={20} color={'#FF646B'} />
{t('Joined Workspace')}
</p>
);
};
export const WorkspaceCard = ({
workspaceData,
onClick,
}: {
workspaceData: WorkspaceUnit;
onClick: (data: WorkspaceUnit) => void;
}) => {
const { currentWorkspace } = useAppState();
const { t } = useTranslation();
return (
<StyledCard
data-testid="workspace-card"
onClick={() => {
onClick(workspaceData);
}}
active={workspaceData.id === currentWorkspace?.id}
>
<FlexWrapper>
<WorkspaceUnitAvatar size={58} workspaceUnit={workspaceData} />
</FlexWrapper>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>
{workspaceData.name || 'AFFiNE'}
</StyleWorkspaceTitle>
<WorkspaceType workspaceData={workspaceData} />
{workspaceData.provider === 'local' && (
<p>
<OfflineIcon />
{t('Available Offline')}
</p>
)}
{workspaceData.published && (
<p>
<PublishedIcon />
{t('Published to Web')}
</p>
)}
</StyleWorkspaceInfo>
</StyledCard>
);
};

View File

@@ -70,7 +70,7 @@ export const CloudIcon = () => {
<path <path
d="M2.5 11.3744C2.5 13.837 4.51472 15.8333 7 15.8333L13.75 15.8333C15.8211 15.8333 17.5 14.1532 17.5 12.0807C17.5 10.5419 16.5744 9.12069 15.25 8.54163C15.1098 6.10205 13.07 4.16663 10.5744 4.16663C8.62616 4.16663 6.95578 5.40527 6.25 7.08329C4 7.44788 2.5 9.33336 2.5 11.3744Z" d="M2.5 11.3744C2.5 13.837 4.51472 15.8333 7 15.8333L13.75 15.8333C15.8211 15.8333 17.5 14.1532 17.5 12.0807C17.5 10.5419 16.5744 9.12069 15.25 8.54163C15.1098 6.10205 13.07 4.16663 10.5744 4.16663C8.62616 4.16663 6.95578 5.40527 6.25 7.08329C4 7.44788 2.5 9.33336 2.5 11.3744Z"
stroke="#60A5FA" stroke="#60A5FA"
stroke-width="1.25" strokeWidth="1.25"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />

View File

@@ -1,28 +1,34 @@
import { styled } from '@/styles'; import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Modal, ModalWrapper } from '@/ui/modal'; import { FlexWrapper } from '@/ui/layout';
import { Button, IconButton } from '@/ui/button';
import { useState } from 'react'; import { useState } from 'react';
import { CreateWorkspaceModal } from '../create-workspace'; import { CreateWorkspaceModal } from '../create-workspace';
import {
UsersIcon, import { Tooltip } from '@/ui/tooltip';
AddIcon,
LogOutIcon, import { AddIcon, HelpCenterIcon } from '@blocksuite/icons';
CloudInsyncIcon,
PublishIcon,
CloseIcon,
} from '@blocksuite/icons';
import {
WorkspaceAvatar,
WorkspaceUnitAvatar,
} from '@/components/workspace-avatar';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { LanguageMenu } from './languageMenu'; import { LanguageMenu } from './SelectLanguageMenu';
import { CloudIcon, LineIcon, LocalIcon, OfflineIcon } from './icons';
import { LoginModal } from '../login-modal'; import { LoginModal } from '../login-modal';
import { LogoutModal } from '../logout-modal'; import { LogoutModal } from '../logout-modal';
import {
StyledCard,
StyledSplitLine,
StyleWorkspaceInfo,
StyleWorkspaceTitle,
StyledModalHeaderLeft,
StyledModalTitle,
StyledHelperContainer,
StyledModalContent,
StyledOperationWrapper,
StyleWorkspaceAdd,
StyledModalHeader,
} from './styles';
import { WorkspaceCard } from './WorkspaceCard';
import { Footer } from './Footer';
interface WorkspaceModalProps { interface WorkspaceModalProps {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@@ -30,337 +36,117 @@ interface WorkspaceModalProps {
export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => { export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => {
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false); const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const { workspaceList, currentWorkspace, user, logout, isOwner } = const { logout, dataCenter } = useAppState();
useAppState();
const router = useRouter(); const router = useRouter();
const { t } = useTranslation(); const { t } = useTranslation();
const [loginOpen, setLoginOpen] = useState(false); const [loginOpen, setLoginOpen] = useState(false);
const [logoutOpen, setLogoutOpen] = useState(false); const [logoutOpen, setLogoutOpen] = useState(false);
return ( return (
<div> <>
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<ModalWrapper <ModalWrapper
width={720} width={720}
height={690}
style={{ style={{
padding: '24px 40px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
}} }}
> >
<Header> <StyledModalHeader>
<ContentTitle>{t('My Workspaces')}</ContentTitle> <StyledModalHeaderLeft>
<HeaderOption> <StyledModalTitle>{t('My Workspaces')}</StyledModalTitle>
<LanguageMenu /> <Tooltip
<div content={t('Workspace description')}
style={{ placement="top-start"
display: 'inline-block', disablePortal={true}
border: 'none',
margin: '2px 16px',
height: '24px',
position: 'relative',
top: '4px',
}}
> >
<LineIcon></LineIcon> <StyledHelperContainer>
</div> <HelpCenterIcon />
</StyledHelperContainer>
</Tooltip>
</StyledModalHeaderLeft>
<Button <StyledOperationWrapper>
style={{ border: 'none', padding: 0 }} <LanguageMenu />
<StyledSplitLine />
<ModalCloseButton
onClick={() => { onClick={() => {
onClose(); onClose();
}} }}
> absolute={false}
<CloseIcon></CloseIcon> />
</Button> </StyledOperationWrapper>
</HeaderOption> </StyledModalHeader>
</Header>
<Content>
<WorkspaceList>
{workspaceList.map((item, index) => {
return (
<WorkspaceItem
onClick={() => {
router.replace(`/workspace/${item.id}`);
onClose();
}}
active={item.id === currentWorkspace?.id}
key={index}
>
<div>
<WorkspaceUnitAvatar size={58} workspaceUnit={item} />
</div>
<StyleWorkspaceInfo> <StyledModalContent>
<StyleWorkspaceTitle> {dataCenter.workspaces.map((item, index) => {
{item.name || 'AFFiNE'} return (
</StyleWorkspaceTitle> <WorkspaceCard
{isOwner ? ( workspaceData={item}
item.provider === 'local' ? ( onClick={workspaceData => {
<p> router.replace(`/workspace/${workspaceData.id}`);
<LocalIcon /> onClose();
Local Workspace }}
</p> key={index}
) : ( ></WorkspaceCard>
<p> );
<CloudIcon /> })}
Cloud Workspace <StyledCard
</p> onClick={() => {
) setCreateWorkspaceOpen(true);
) : ( }}
<p> >
<UsersIcon fontSize={20} color={'#FF646B'} /> <FlexWrapper>
Joined Workspace <StyleWorkspaceAdd className="add-icon">
</p> <AddIcon fontSize={18} />
)} </StyleWorkspaceAdd>
{item.provider === 'local' && ( </FlexWrapper>
<p>
<OfflineIcon />
All data can be accessed offline
</p>
)}
{item.published && (
<p>
<PublishIcon fontSize={16} /> Published to Web
</p>
)}
</StyleWorkspaceInfo>
</WorkspaceItem>
);
})}
<WorkspaceItem
onClick={() => {
setCreateWorkspaceOpen(true);
}}
>
<div>
<StyleWorkspaceAdd className="add-icon">
<AddIcon fontSize={18} />
</StyleWorkspaceAdd>
</div>
<StyleWorkspaceInfo> <StyleWorkspaceInfo>
<StyleWorkspaceTitle>New workspace</StyleWorkspaceTitle> <StyleWorkspaceTitle>{t('New Workspace')}</StyleWorkspaceTitle>
<p>Crete or import</p> <p>{t('Create Or Import')}</p>
</StyleWorkspaceInfo> </StyleWorkspaceInfo>
</WorkspaceItem> </StyledCard>
</WorkspaceList> </StyledModalContent>
{/* <p style={{ fontSize: '14px', color: '#ccc', margin: '12px 0' }}>
{t('Tips')} <Footer
{t('Workspace description')} onLogin={() => {
</p> */} setLoginOpen(true);
</Content>
<LoginModal
open={loginOpen}
onClose={() => {
setLoginOpen(false);
}} }}
></LoginModal> onLogout={() => {
<Footer> setLogoutOpen(true);
{!user ? (
<StyleSignIn
onClick={async () => {
setLoginOpen(true);
}}
>
<span>
<CloudInsyncIcon fontSize={16} />
</span>
Sign in to sync with AFFINE Cloud
</StyleSignIn>
) : (
<div style={{ display: 'flex' }}>
<div style={{ paddingTop: '20px' }}>
<WorkspaceAvatar
size={40}
name={user.name}
avatar={user.avatar}
></WorkspaceAvatar>
</div>
<StyleUserInfo style={{}}>
<p>{user.name}</p>
<p>{user.email}</p>
</StyleUserInfo>
<div style={{ paddingTop: '25px' }}>
<IconButton
onClick={() => {
setLogoutOpen(true);
// confirm({
// title: 'Sign out?',
// content: `All data has been stored in the cloud. `,
// confirmText: 'Sign out',
// cancelText: 'Cancel',
// }).then(async confirm => {
// // if (confirm) {
// // if (user) {
// // await logout();
// // router.replace(`/workspace`);
// // toast('Enabled success');
// // }
// // }
// });
}}
>
<LogOutIcon></LogOutIcon>
</IconButton>
</div>
</div>
)}
</Footer>
<CreateWorkspaceModal
open={createWorkspaceOpen}
onClose={() => {
setCreateWorkspaceOpen(false);
}} }}
></CreateWorkspaceModal> />
<LogoutModal
open={logoutOpen}
onClose={async wait => {
if (!wait) {
await logout();
router.replace(`/workspace`);
}
setLogoutOpen(false);
}}
></LogoutModal>
</ModalWrapper> </ModalWrapper>
</Modal> </Modal>
</div>
<LoginModal
open={loginOpen}
onClose={() => {
setLoginOpen(false);
}}
/>
<LogoutModal
open={logoutOpen}
onClose={async wait => {
if (!wait) {
await logout();
if (dataCenter.workspaces.length === 0) {
router.push(`/workspace`);
} else {
router.push(`/workspace/${dataCenter.workspaces[0].id}`);
}
}
setLogoutOpen(false);
}}
/>
<CreateWorkspaceModal
open={createWorkspaceOpen}
onClose={() => {
setCreateWorkspaceOpen(false);
}}
/>
</>
); );
}; };
const Header = styled('div')({
display: 'flex',
});
const Content = styled('div')({
flexDirection: 'column',
alignItems: 'center',
gap: '16px',
flex: 1,
});
const HeaderOption = styled.div(() => {
return {
marginLeft: '16px',
};
});
const ContentTitle = styled('div')({
fontSize: '20px',
lineHeight: '24px',
fontWeight: 600,
textAlign: 'left',
flex: 1,
});
const WorkspaceList = styled('div')({
maxHeight: '500px',
overflow: 'auto',
display: 'grid',
gridRowGap: '24px',
gridColumnGap: '24px',
fontSize: '16px',
marginTop: '36px',
gridTemplateColumns: 'repeat(2, 1fr)',
});
export const WorkspaceItem = styled.div<{
active?: boolean;
}>(({ theme, active }) => {
const borderColor = active ? theme.colors.primaryColor : 'transparent';
return {
cursor: 'pointer',
padding: '16px',
height: '124px',
boxShadow: theme.shadow.modal,
display: 'flex',
borderRadius: '12px',
border: `1px solid ${borderColor}`,
':hover': {
background: theme.colors.hoverBackground,
'.add-icon': {
border: `1.5px dashed ${theme.colors.primaryColor}`,
svg: {
fill: theme.colors.primaryColor,
},
},
},
};
});
const StyleWorkspaceInfo = styled.div(({ theme }) => {
return {
marginLeft: '16px',
p: {
fontSize: theme.font.xs,
lineHeight: '16px',
},
svg: {
verticalAlign: 'text-bottom',
marginRight: '8px',
},
};
});
const StyleWorkspaceTitle = styled.div(({ theme }) => {
return {
fontSize: theme.font.base,
fontWeight: 600,
lineHeight: '24px',
marginBottom: '8px',
};
});
const StyleWorkspaceAdd = styled.div(() => {
return {
width: '58px',
height: '58px',
borderRadius: '100%',
textAlign: 'center',
background: '#f4f5fa',
border: '1.5px dashed #f4f5fa',
lineHeight: '58px',
marginTop: '2px',
};
});
const Footer = styled('div')({
paddingTop: '16px',
});
const StyleUserInfo = styled.div(({ theme }) => {
return {
textAlign: 'left',
marginLeft: '16px',
marginTop: '16px',
flex: 1,
p: {
lineHeight: '24px',
color: theme.colors.iconColor,
},
'p:nth-child(1)': {
color: theme.colors.textColor,
fontWeight: 600,
},
};
});
const StyleSignIn = styled.div(({ theme }) => {
return {
cursor: 'pointer',
fontSize: '16px',
fontWeight: 700,
color: theme.colors.iconColor,
span: {
display: 'inline-block',
width: '40px',
height: '40px',
borderRadius: '40px',
backgroundColor: theme.colors.innerHoverBackground,
textAlign: 'center',
lineHeight: '44px',
marginRight: '16px',
svg: {
fill: theme.colors.primaryColor,
},
},
};
});

View File

@@ -1,93 +0,0 @@
import { LOCALES } from '@affine/i18n';
import { TooltipProps } from '@mui/material';
import { styled } from '@/styles';
import { Tooltip } from '@mui/material';
import { useState } from 'react';
import { useTranslation } from '@affine/i18n';
import { ArrowDownIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button';
export const LanguageMenu = () => {
const { i18n } = useTranslation();
const changeLanguage = (event: string) => {
i18n.changeLanguage(event);
};
const [show, setShow] = useState(false);
const currentLanguage = LOCALES.find(item => item.tag === i18n.language);
const [languageName, setLanguageName] = useState(
currentLanguage?.originalName
);
return (
<StyledTooltip
title={
<>
{LOCALES.map(option => {
return (
<ListItem
style={{ border: 'none' }}
key={option.name}
title={option.name}
onClick={() => {
changeLanguage(option.tag);
setShow(false);
setLanguageName(option.originalName);
}}
>
{option.originalName}
</ListItem>
);
})}
</>
}
open={show}
>
<StyledTitleButton
style={{ border: 'none', padding: '0px' }}
onClick={() => {
setShow(!show);
}}
>
<StyledContainer>
<StyledText>{languageName}</StyledText>
<ArrowDownIcon fontSize={18} />
</StyledContainer>
</StyledTitleButton>
</StyledTooltip>
);
};
const StyledContainer = styled('div')(() => ({
display: 'flex',
alignItems: 'center',
}));
const StyledText = styled('span')(({ theme }) => ({
marginRight: '4px',
marginLeft: '16px',
fontSize: theme.font.sm,
fontWeight: '500',
textTransform: 'capitalize',
}));
const StyledTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
zIndex: theme.zIndex.modal,
'& .MuiTooltip-tooltip': {
backgroundColor: theme.colors.popoverBackground,
boxShadow: theme.shadow.modal,
color: theme.colors.popoverColor,
},
}));
const ListItem = styled(Button)(({ theme }) => ({
display: 'block',
width: '100%',
color: theme.colors.popoverColor,
fontSize: theme.font.sm,
textTransform: 'capitalize',
}));
const StyledTitleButton = styled(Button)(({ theme }) => ({
color: theme.colors.popoverColor,
fontSize: theme.font.sm,
}));

View File

@@ -1,38 +1,159 @@
import { displayFlex, styled } from '@/styles'; import { displayFlex, displayInlineFlex, styled, textEllipsis } from '@/styles';
import { Button } from '@/ui/button';
export const StyledTitle = styled.div(() => { export const StyledSplitLine = styled.div(({ theme }) => {
return { return {
...displayFlex('center', 'center'), width: '1px',
fontSize: '20px', height: '20px',
fontWeight: 500, background: theme.colors.iconColor,
marginTop: '60px', marginRight: '24px',
lineHeight: 1,
}; };
}); });
export const StyledContent = styled.div(() => { export const StyleWorkspaceInfo = styled.div(({ theme }) => {
return { return {
padding: '0 40px', marginLeft: '15px',
marginTop: '32px', p: {
fontSize: '18px', height: '20px',
lineHeight: '25px', fontSize: theme.font.xs,
'p:not(last-of-type)': { ...displayFlex('flex-start', 'center'),
marginBottom: '10px', },
svg: {
marginRight: '10px',
}, },
}; };
}); });
export const StyledButton = styled.div(({ theme }) => { export const StyleWorkspaceTitle = styled.div(({ theme }) => {
return { return {
width: '146px', fontSize: theme.font.base,
height: '42px', fontWeight: 600,
background: theme.colors.primaryColor, lineHeight: '24px',
color: '#FFFFFF', marginBottom: '10px',
fontSize: '18px', maxWidth: '200px',
fontWeight: 500, ...textEllipsis(1),
borderRadius: '21px', };
margin: '52px auto 0', });
export const StyledCard = styled.div<{
active?: boolean;
}>(({ theme, active }) => {
const borderColor = active ? theme.colors.primaryColor : 'transparent';
return {
width: '310px',
height: '124px',
cursor: 'pointer', 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',
':hover': {
background: theme.colors.hoverBackground,
'.add-icon': {
border: `1.5px dashed ${theme.colors.primaryColor}`,
svg: {
fill: theme.colors.primaryColor,
},
},
},
};
});
export const StyledFooter = styled('div')({
height: '84px',
padding: '0 40px',
...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:nth-child(1)': {
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,
...displayFlex('center', 'center'), ...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%',
textAlign: 'center',
background: '#f4f5fa',
border: '1.5px dashed #f4f5fa',
lineHeight: '58px',
marginTop: '2px',
};
});
export const StyledModalHeader = styled('div')(({ theme }) => {
return {
width: '100%',
height: '72px',
position: 'absolute',
left: 0,
top: 0,
background: theme.colors.pageBackground,
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'),
},
};
});

View File

@@ -1,24 +1,16 @@
import { styled } from '@/styles';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { Trans } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
export const ExportPageTitleContainer = styled('div')(() => { import { Wrapper } from '@/ui/layout';
return { import { Button } from '@/ui/button';
display: 'flex',
fontWeight: '500',
flex: 1,
};
});
export const ExportPage = ({ workspace }: { workspace: WorkspaceUnit }) => { export const ExportPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { t } = useTranslation();
console.log(workspace);
return ( return (
<ExportPageTitleContainer> <>
<Trans i18nKey="Export Workspace"> <Wrapper marginBottom="32px"> {t('Export Description')}</Wrapper>
Export Workspace <Button type="light" shape="circle" disabled>
<code style={{ margin: '0 10px' }}> {t('Export AFFINE backup file')}
{{ workspace: workspace.name }} </Button>
</code> </>
Is Comming
</Trans>
</ExportPageTitleContainer>
); );
}; };

View File

@@ -1,20 +1,12 @@
import {
StyledCopyButtonContainer,
StyledPublishContent,
StyledPublishCopyContainer,
StyledPublishExplanation,
StyledSettingH2,
StyledStopPublishContainer,
} from './style';
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import Input from '@/ui/input'; import Input from '@/ui/input';
import { toast } from '@/ui/toast'; import { toast } from '@/ui/toast';
// import { useAppState } from '@/providers/app-state-provider3';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '../enable-workspace'; import { EnableWorkspaceButton } from '../enable-workspace';
import { Wrapper, Content, FlexWrapper } from '@/ui/layout';
export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => { export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const shareUrl = window.location.host + '/public-workspace/' + workspace.id; const shareUrl = window.location.host + '/public-workspace/' + workspace.id;
const { publishWorkspace } = useWorkspaceHelper(); const { publishWorkspace } = useWorkspaceHelper();
@@ -23,95 +15,75 @@ export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const togglePublic = async (flag: boolean) => { const togglePublic = async (flag: boolean) => {
try { try {
await publishWorkspace(workspace.id.toString(), flag); await publishWorkspace(workspace.id.toString(), flag);
setLoaded(false);
} catch (e) { } catch (e) {
toast('Failed to publish workspace'); toast(t('Failed to publish workspace'));
} }
}; };
const copyUrl = () => { const copyUrl = () => {
navigator.clipboard.writeText(shareUrl); navigator.clipboard.writeText(shareUrl);
toast('Copied url to clipboard'); toast(t('Copied link to clipboard'));
}; };
if (workspace.provider === 'affine') {
if (workspace.published) {
return (
<>
<Wrapper marginBottom="32px">{t('Published Description')}</Wrapper>
<Wrapper marginBottom="12px">
<Content weight="500">{t('Share with link')}</Content>
</Wrapper>
<FlexWrapper>
<Input width={582} value={shareUrl} disabled={true}></Input>
<Button
onClick={copyUrl}
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t('Copy Link')}
</Button>
</FlexWrapper>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(false);
}}
loading={false}
type="danger"
shape="circle"
style={{ marginTop: '38px' }}
>
{t('Stop publishing')}
</Button>
</>
);
}
return (
<>
<Wrapper marginBottom="32px">{t('Publishing Description')}</Wrapper>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(true);
}}
loading={loaded}
type="light"
shape="circle"
>
{t('Publish to web')}
</Button>
</>
);
}
return ( return (
<> <>
{workspace.provider === 'affine' ? ( <Wrapper marginBottom="32px">{t('Publishing')}</Wrapper>
<div <EnableWorkspaceButton />
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
>
<StyledPublishContent>
{workspace.published ? (
<>
<StyledPublishExplanation>
The current workspace has been published to the web, everyone
can view the contents of this workspace through the link.
</StyledPublishExplanation>
<StyledPublishCopyContainer>
<StyledSettingH2 marginBottom={16}>
{t('Share with link')}
</StyledSettingH2>
<Input width={500} value={shareUrl} disabled={true}></Input>
<StyledCopyButtonContainer>
<Button onClick={copyUrl} type="primary" shape="circle">
{t('Copy Link')}
</Button>
</StyledCopyButtonContainer>
</StyledPublishCopyContainer>
</>
) : (
<StyledPublishExplanation>
{t('Publishing Description')}
<div style={{ marginTop: '64px' }}>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(true);
setLoaded(false);
}}
loading={loaded}
type="primary"
shape="circle"
>
{t('Publish to web')}
</Button>
</div>
</StyledPublishExplanation>
)}
</StyledPublishContent>
{workspace.published ? (
<StyledStopPublishContainer>
<Button
onClick={async () => {
setLoaded(true);
await togglePublic(false);
setLoaded(true);
}}
loading={false}
type="danger"
shape="circle"
>
{t('Stop publishing')}
</Button>
</StyledStopPublishContainer>
) : (
<></>
)}
</div>
) : (
<StyledPublishContent>
<>
<StyledPublishExplanation>
Publishing to web requires AFFiNE Cloud service.
</StyledPublishExplanation>
<div style={{ marginTop: '72px' }}>
<EnableWorkspaceButton></EnableWorkspaceButton>
</div>
</>
</StyledPublishContent>
)}
</> </>
); );
}; };

View File

@@ -1,84 +1,106 @@
import { import {
StyleAsync,
StyledPublishContent,
StyledPublishExplanation,
StyledWorkspaceName, StyledWorkspaceName,
StyledWorkspaceType, StyledEmail,
// StyledDownloadCard,
// StyledDownloadCardDes,
} from './style'; } from './style';
import { DownloadIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button';
import { Menu, MenuItem } from '@/ui/menu';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { Trans, useTranslation } from '@affine/i18n'; import { useTranslation, Trans } from '@affine/i18n';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { EnableWorkspaceButton } from '../enable-workspace'; import { EnableWorkspaceButton } from '../enable-workspace';
import { useAppState } from '@/providers/app-state-provider';
import { FlexWrapper, Content, Wrapper } from '@/ui/layout';
// // FIXME: Temporary solution, since the @blocksuite/icons is broken
// const ActiveIcon = () => {
// return (
// <svg
// width="24"
// height="24"
// viewBox="0 0 24 24"
// fill="none"
// xmlns="http://www.w3.org/2000/svg"
// >
// <path
// fillRule="evenodd"
// clipRule="evenodd"
// d="M2.25 12C2.25 6.61522 6.61522 2.25 12 2.25C17.3848 2.25 21.75 6.61522 21.75 12C21.75 17.3848 17.3848 21.75 12 21.75C6.61522 21.75 2.25 17.3848 2.25 12Z"
// fill="#6880FF"
// />
// <path
// fillRule="evenodd"
// clipRule="evenodd"
// d="M16.5068 8.44714C16.8121 8.72703 16.8328 9.20146 16.5529 9.5068L11.0529 15.5068C10.9146 15.6576 10.7208 15.7454 10.5163 15.7498C10.3118 15.7543 10.1143 15.675 9.96967 15.5303L7.46967 13.0303C7.17678 12.7374 7.17678 12.2626 7.46967 11.9697C7.76256 11.6768 8.23744 11.6768 8.53033 11.9697L10.4764 13.9158L15.4471 8.49321C15.727 8.18787 16.2015 8.16724 16.5068 8.44714Z"
// fill="white"
// />
// </svg>
// );
// };
export const SyncPage = ({ workspace }: { workspace: WorkspaceUnit }) => { export const SyncPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { user } = useAppState();
if (workspace.provider === 'local') {
return (
<>
<FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
<WorkspaceUnitAvatar
size={32}
name={workspace.name}
workspaceUnit={workspace}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{workspace.name}&nbsp;</StyledWorkspaceName>
<Content weight={500}>{t('is a Local Workspace')}</Content>
</FlexWrapper>
<p>{t('Local Workspace Description')}</p>
<Wrapper marginTop="32px">
<EnableWorkspaceButton />
</Wrapper>
</>
);
}
return ( return (
<div> <>
<StyledPublishContent> <FlexWrapper alignItems="center" style={{ marginBottom: '12px' }}>
{workspace.provider === 'local' ? ( <WorkspaceUnitAvatar
<> size={32}
<StyledPublishExplanation> name={workspace.name}
<WorkspaceUnitAvatar workspaceUnit={workspace}
size={32} style={{ marginRight: '12px' }}
name={workspace.name} />
workspaceUnit={workspace} <StyledWorkspaceName>{workspace.name}&nbsp;</StyledWorkspaceName>
style={{ marginRight: '12px' }} <Content weight={500}>{t('is a Cloud Workspace')}</Content>
/> </FlexWrapper>
<StyledWorkspaceName>{workspace.name};</StyledWorkspaceName> <Trans i18nKey="Cloud Workspace Description">
<StyledWorkspaceType>is a Local Workspace.</StyledWorkspaceType> All data will be synchronised and saved to the AFFiNE account
</StyledPublishExplanation> <StyledEmail>
<StyledWorkspaceType> {{
All data is stored on the current device. You can enable AFFiNE email: '{' + user?.email + '}.',
Cloud for this workspace to keep data in sync with the cloud. }}
</StyledWorkspaceType> </StyledEmail>
<StyleAsync> </Trans>
<EnableWorkspaceButton></EnableWorkspaceButton>
</StyleAsync> {/*<Wrapper marginBottom="12px" marginTop="32px">*/}
</> {/* <Content weight="500">{t('Data sync mode')}</Content>*/}
) : ( {/*</Wrapper>*/}
<> {/*<FlexWrapper>*/}
<StyledPublishExplanation> {/* <StyledDownloadCard>*/}
<Trans i18nKey="Sync Description2"> {/* <ActiveIcon />*/}
<code>{{ workspaceName: workspace.name ?? 'Affine' }}</code> {/* <Wrapper>{t('Download all data')}</Wrapper>*/}
is Cloud Workspace. All data will be synchronised and saved to {/* <StyledDownloadCardDes>*/}
the AFFiNE {/* {t('It takes up more space on your device.')}*/}
</Trans> {/* </StyledDownloadCardDes>*/}
</StyledPublishExplanation> {/* </StyledDownloadCard>*/}
<StyleAsync>
<Menu {/* <StyledDownloadCard active>*/}
content={ {/* <ActiveIcon />*/}
<> {/* <Wrapper>{t('Download core data')}</Wrapper>*/}
<MenuItem {/* <StyledDownloadCardDes>*/}
onClick={() => { {/* {t('It takes up little space on your device.')}*/}
// deleteMember(workspace.id, 0); {/* </StyledDownloadCardDes>*/}
}} {/* </StyledDownloadCard>*/}
icon={<DownloadIcon />} {/*</FlexWrapper>*/}
> </>
{t('Download data to device', { CoreOrAll: 'core' })}
</MenuItem>
<MenuItem
onClick={() => {
// deleteMember(workspace.id, 0);
}}
icon={<DownloadIcon />}
>
{t('Download data to device', { CoreOrAll: 'all' })}
</MenuItem>
</>
}
placement="bottom-end"
disablePortal={true}
>
<Button type="primary">
{t('Download data to device', { CoreOrAll: 'all' })}
</Button>
</Menu>
</StyleAsync>
</>
)}
</StyledPublishContent>
</div>
); );
}; };

View File

@@ -1,23 +1,13 @@
import { import { StyledInput, StyledProviderInfo, StyledAvatar } from './style';
StyledDeleteButtonContainer, import { StyledSettingKey, StyledRow } from '../style';
// StyledSettingAvatar, import { FlexWrapper, Content } from '@/ui/layout';
StyledSettingAvatarContent,
StyledSettingInputContainer,
StyledDoneButtonContainer,
StyledInput,
StyledProviderInfo,
StyleGeneral,
StyledAvatar,
} from './style';
import { StyledSettingH2 } from '../style';
import { useState } from 'react'; import { useState } from 'react';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDelete } from './delete'; import { WorkspaceDelete } from './delete';
import { WorkspaceLeave } from './leave'; import { WorkspaceLeave } from './leave';
import { DoneIcon, UsersIcon } from '@blocksuite/icons'; import { UsersIcon } from '@blocksuite/icons';
// import { Upload } from '@/components/file-upload';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
@@ -33,9 +23,6 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { updateWorkspace } = useWorkspaceHelper(); const { updateWorkspace } = useWorkspaceHelper();
const { t } = useTranslation(); const { t } = useTranslation();
const handleChangeWorkSpaceName = (newName: string) => {
setWorkspaceName(newName);
};
const handleUpdateWorkspaceName = () => { const handleUpdateWorkspaceName = () => {
currentWorkspace && currentWorkspace &&
updateWorkspace({ name: workspaceName }, currentWorkspace); updateWorkspace({ name: workspaceName }, currentWorkspace);
@@ -47,83 +34,105 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
(await updateWorkspace({ avatarBlob: blob }, currentWorkspace)); (await updateWorkspace({ avatarBlob: blob }, currentWorkspace));
}; };
return workspace ? ( if (!workspace) {
<StyleGeneral> return null;
<div style={{ flex: 1, overflow: 'auto' }}> }
<StyledSettingH2>Workspace Avatar</StyledSettingH2>
<StyledSettingAvatarContent> return (
<StyledAvatar> <>
<Upload <StyledRow>
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg" <StyledSettingKey>{t('Workspace Avatar')}</StyledSettingKey>
fileChange={fileChange} <StyledAvatar>
> <Upload
<> accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
<div className="camera-icon"> fileChange={fileChange}
<CameraIcon></CameraIcon> >
</div> <>
<WorkspaceUnitAvatar <div className="camera-icon">
size={60} <CameraIcon></CameraIcon>
name={workspace.name} </div>
workspaceUnit={workspace} <WorkspaceUnitAvatar
/> size={72}
</> name={workspace.name}
</Upload> workspaceUnit={workspace}
</StyledAvatar> />
{/* TODO: Wait for image sync to complete */} </>
{/* <Upload </Upload>
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg" </StyledAvatar>
fileChange={fileChange} </StyledRow>
>
<Button loading={uploading}>{t('Upload')}</Button> <StyledRow>
</Upload> */} <StyledSettingKey>{t('Workspace Name')}</StyledSettingKey>
{/* TODO: add upload logic */} <FlexWrapper>
</StyledSettingAvatarContent>
<StyledSettingH2 marginTop={20}>{t('Workspace Name')}</StyledSettingH2>
<StyledSettingInputContainer>
<StyledInput <StyledInput
width={284} width={284}
height={32} height={38}
value={workspaceName} value={workspaceName}
placeholder={t('Workspace Name')} placeholder={t('Workspace Name')}
maxLength={14} maxLength={15}
minLength={1} minLength={0}
disabled={!isOwner} disabled={!isOwner}
onChange={handleChangeWorkSpaceName} onChange={newName => {
setWorkspaceName(newName);
}}
></StyledInput> ></StyledInput>
{isOwner ? ( {isOwner && (
<StyledDoneButtonContainer <>
onClick={() => { <Button
handleUpdateWorkspaceName(); type="default"
}} shape="circle"
> style={{ marginLeft: '24px' }}
<DoneIcon /> onClick={() => {
</StyledDoneButtonContainer> setWorkspaceName(workspace.name);
) : null} }}
</StyledSettingInputContainer> >
<StyledSettingH2 marginTop={20}>{t('Workspace Type')}</StyledSettingH2> {t('Cancel')}
<StyledSettingInputContainer> </Button>
<Button
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
onClick={() => {
handleUpdateWorkspaceName();
}}
>
{t('Confirm')}
</Button>
</>
)}
</FlexWrapper>
</StyledRow>
<StyledRow>
<StyledSettingKey>{t('Workspace Type')}</StyledSettingKey>
<FlexWrapper>
{isOwner ? ( {isOwner ? (
currentWorkspace?.provider === 'local' ? ( currentWorkspace?.provider === 'local' ? (
<StyledProviderInfo> <FlexWrapper alignItems="center">
<LocalIcon /> <LocalIcon />
Local Workspace <Content style={{ marginLeft: '15px' }}>
</StyledProviderInfo> {t('Local Workspace')}
</Content>
</FlexWrapper>
) : ( ) : (
<StyledProviderInfo> <FlexWrapper alignItems="center">
<CloudIcon /> <CloudIcon />
All data can be accessed offline <Content style={{ marginLeft: '15px' }}>
</StyledProviderInfo> {t('Available Offline')}
</Content>
</FlexWrapper>
) )
) : ( ) : (
<StyledProviderInfo> <StyledProviderInfo>
<UsersIcon fontSize={20} color={'#FF646B'} /> <UsersIcon fontSize={20} color={'#FF646B'} />
Joined Workspace {t('Joined Workspace')}
</StyledProviderInfo> </StyledProviderInfo>
)} )}
</StyledSettingInputContainer> </FlexWrapper>
</div> </StyledRow>
<StyledDeleteButtonContainer> <StyledRow>
<StyledSettingKey> {t('Delete Workspace')}</StyledSettingKey>
{isOwner ? ( {isOwner ? (
<> <>
<Button <Button
@@ -163,7 +172,7 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
/> />
</> </>
)} )}
</StyledDeleteButtonContainer> </StyledRow>
</StyleGeneral> </>
) : null; );
}; };

View File

@@ -1,3 +1,4 @@
import { useRouter } from 'next/router';
import Modal from '@/ui/modal'; import Modal from '@/ui/modal';
import Input from '@/ui/input'; import Input from '@/ui/input';
import { import {
@@ -11,7 +12,6 @@ import {
import { useState } from 'react'; import { useState } from 'react';
import { ModalCloseButton } from '@/ui/modal'; import { ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { useRouter } from 'next/router';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import { Trans, useTranslation } from '@affine/i18n'; import { Trans, useTranslation } from '@affine/i18n';

View File

@@ -1,40 +1,6 @@
import { displayFlex, styled } from '@/styles'; import { styled } from '@/styles';
import MuiAvatar from '@mui/material/Avatar';
import IconButton from '@/ui/button/IconButton';
import Input from '@/ui/input'; import Input from '@/ui/input';
export const StyledSettingInputContainer = styled('div')(() => {
return {
marginTop: '12px',
width: '100%',
...displayFlex('flex-start', 'center'),
};
});
export const StyledDeleteButtonContainer = styled('div')(() => {
return {
textAlign: 'center',
};
});
export const StyleGeneral = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
height: '100%',
};
});
export const StyledDoneButtonContainer = styled(IconButton)(({ theme }) => {
return {
border: `1px solid ${theme.colors.borderColor}`,
borderRadius: '10px',
height: '32px',
overflow: 'hidden',
marginLeft: '20px',
':hover': {
borderColor: theme.colors.primaryColor,
},
};
});
export const StyledInput = styled(Input)(({ theme }) => { export const StyledInput = styled(Input)(({ theme }) => {
return { return {
border: `1px solid ${theme.colors.borderColor}`, border: `1px solid ${theme.colors.borderColor}`,
@@ -43,24 +9,9 @@ export const StyledInput = styled(Input)(({ theme }) => {
}; };
}); });
export const StyledSettingAvatarContent = styled('div')(() => {
return {
marginTop: '12px',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
height: '72px',
};
});
export const StyledSettingAvatar = styled(MuiAvatar)(() => {
return { height: '72px', width: '72px', marginRight: '24px' };
});
export const StyledProviderInfo = styled('p')(({ theme }) => { export const StyledProviderInfo = styled('p')(({ theme }) => {
return { return {
color: theme.colors.iconColor, color: theme.colors.iconColor,
fontSize: theme.font.sm,
svg: { svg: {
verticalAlign: 'sub', verticalAlign: 'sub',
marginRight: '10px', marginRight: '10px',
@@ -71,7 +22,6 @@ export const StyledProviderInfo = styled('p')(({ theme }) => {
export const StyledAvatar = styled('div')(() => { export const StyledAvatar = styled('div')(() => {
return { return {
position: 'relative', position: 'relative',
marginRight: '20px',
cursor: 'pointer', cursor: 'pointer',
':hover': { ':hover': {
'.camera-icon': { '.camera-icon': {

View File

@@ -4,7 +4,7 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import Input from '@/ui/input'; import Input from '@/ui/input';
import { useState } from 'react'; import { useState } from 'react';
import { Avatar } from '@mui/material'; import { MuiAvatar } from '@/ui/mui';
import useMembers from '@/hooks/use-members'; import useMembers from '@/hooks/use-members';
import { User } from '@affine/datacenter'; import { User } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
@@ -76,10 +76,9 @@ export const InviteMemberModal = ({
<ModalWrapper width={460} height={236}> <ModalWrapper width={460} height={236}>
<Header> <Header>
<ModalCloseButton <ModalCloseButton
top={6}
right={6}
onClick={() => { onClick={() => {
onClose(); onClose();
setEmail('');
}} }}
/> />
</Header> </Header>
@@ -102,7 +101,7 @@ export const InviteMemberModal = ({
) : ( ) : (
<Member> <Member>
{userData?.avatar ? ( {userData?.avatar ? (
<Avatar src={userData?.avatar}></Avatar> <MuiAvatar src={userData?.avatar}></MuiAvatar>
) : ( ) : (
<MemberIcon> <MemberIcon>
<EmailIcon></EmailIcon> <EmailIcon></EmailIcon>
@@ -125,6 +124,7 @@ export const InviteMemberModal = ({
style={{ width: '364px', height: '38px', borderRadius: '40px' }} style={{ width: '364px', height: '38px', borderRadius: '40px' }}
onClick={async () => { onClick={async () => {
await inviteMember(email); await inviteMember(email);
setEmail('');
onInviteSuccess(); onInviteSuccess();
}} }}
> >

View File

@@ -10,10 +10,9 @@ import {
StyledMemberRoleContainer, StyledMemberRoleContainer,
StyledMemberTitleContainer, StyledMemberTitleContainer,
StyledMoreVerticalButton, StyledMoreVerticalButton,
StyledPublishExplanation,
StyledMemberWarp,
StyledMemberContainer, StyledMemberContainer,
} from './style'; } from './style';
import { Wrapper } from '@/ui/layout';
import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons'; import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons';
import { useState } from 'react'; import { useState } from 'react';
import { Button, IconButton } from '@/ui/button'; import { Button, IconButton } from '@/ui/button';
@@ -25,14 +24,13 @@ import { useConfirm } from '@/providers/ConfirmProvider';
import { toast } from '@/ui/toast'; import { toast } from '@/ui/toast';
import useMembers from '@/hooks/use-members'; import useMembers from '@/hooks/use-members';
import Loading from '@/components/loading'; import Loading from '@/components/loading';
import { Wrapper } from '@/ui/layout'; import { FlexWrapper } from '@/ui/layout';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '@/components/enable-workspace'; import { EnableWorkspaceButton } from '@/components/enable-workspace';
export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => { export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false); const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const { members, removeMember, loaded } = useMembers(); const { members, removeMember, loaded } = useMembers();
const { t } = useTranslation(); const { t } = useTranslation();
const { confirm } = useConfirm(); const { confirm } = useConfirm();
@@ -41,9 +39,9 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
<StyledMemberContainer> <StyledMemberContainer>
<StyledMemberListContainer> <StyledMemberListContainer>
{!loaded && ( {!loaded && (
<Wrapper justifyContent="center"> <FlexWrapper justifyContent="center">
<Loading size={25} /> <Loading size={25} />
</Wrapper> </FlexWrapper>
)} )}
{loaded && members.length === 0 && ( {loaded && members.length === 0 && (
<Empty width={648} sx={{ marginTop: '60px' }} height={300} /> <Empty width={648} sx={{ marginTop: '60px' }} height={300} />
@@ -89,42 +87,50 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
<StyledMemberRoleContainer> <StyledMemberRoleContainer>
{member.accepted {member.accepted
? member.type !== 99 ? member.type !== 99
? 'Member' ? t('Member')
: 'Workspace Owner' : t('Owner')
: 'Pending'} : t('Pending')}
</StyledMemberRoleContainer> </StyledMemberRoleContainer>
<StyledMoreVerticalButton> <StyledMoreVerticalButton>
<Menu {member.type === 99 ? (
content={ <></>
<> ) : (
<MenuItem <Menu
onClick={async () => { content={
const confirmRemove = await confirm({ <>
title: 'Delete Member?', <MenuItem
content: `will delete member`, onClick={async () => {
confirmText: 'Delete', const confirmRemove = await confirm({
confirmType: 'danger', title: t('Delete Member?'),
}); content: t('will delete member'),
confirmText: t('Delete'),
confirmType: 'danger',
});
if (!confirmRemove) { if (!confirmRemove) {
return; return;
} }
await removeMember(member.id); await removeMember(member.id);
toast(`${user.name} has been removed`); toast(
}} t('Member has been removed', {
icon={<TrashIcon />} name: user.name,
> })
Delete );
</MenuItem> }}
</> icon={<TrashIcon />}
} >
placement="bottom-end" {t('Delete')}
disablePortal={true} </MenuItem>
> </>
<IconButton> }
<MoreVerticalIcon /> placement="bottom-end"
</IconButton> disablePortal={true}
</Menu> >
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
)}
</StyledMoreVerticalButton> </StyledMoreVerticalButton>
</StyledMemberListItem> </StyledMemberListItem>
); );
@@ -159,11 +165,14 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
} }
return ( return (
<StyledMemberWarp> <Wrapper
{t('Collaboration Description')} style={{
<StyledPublishExplanation> fontWeight: '500',
<EnableWorkspaceButton></EnableWorkspaceButton> fontSize: '18px',
</StyledPublishExplanation> }}
</StyledMemberWarp> >
<Wrapper marginBottom="32px">{t('Collaboration Description')}</Wrapper>
<EnableWorkspaceButton />
</Wrapper>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { styled } from '@/styles'; import { styled } from '@/styles';
import MuiAvatar from '@mui/material/Avatar'; import { MuiAvatar } from '@/ui/mui';
export const StyledMemberTitleContainer = styled('li')(() => { export const StyledMemberTitleContainer = styled('li')(() => {
return { return {
@@ -97,24 +97,3 @@ export const StyledMoreVerticalButton = styled('button')(() => {
paddingRight: '48px', paddingRight: '48px',
}; };
}); });
export const StyledPublishExplanation = styled('div')(() => {
return {
paddingRight: '48px',
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
flex: 1,
marginTop: '64px',
};
});
export const StyledMemberWarp = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
padding: '0 0 48px 0',
fontWeight: '500',
fontSize: '18px',
};
});

View File

@@ -1,5 +1,5 @@
import { styled } from '@/styles'; import { styled } from '@/styles';
import { FlexWrapper } from '@/ui/layout';
export const StyledSettingContainer = styled('div')(() => { export const StyledSettingContainer = styled('div')(() => {
return { return {
display: 'flex', display: 'flex',
@@ -13,41 +13,19 @@ export const StyledSettingContainer = styled('div')(() => {
export const StyledSettingSidebar = styled('div')(() => { export const StyledSettingSidebar = styled('div')(() => {
{ {
return { return {
// height: '48px', marginTop: '48px',
marginTop: '50px',
}; };
} }
}); });
export const StyledSettingContent = styled('div')(() => { export const StyledSettingContent = styled('div')(() => {
return { return {
overflow: 'hidden', overflow: 'auto',
flex: 1, flex: 1,
paddingTop: '48px', paddingTop: '48px',
}; };
}); });
export const StyledSetting = styled('div')(({ theme }) => {
{
return {
width: '236px',
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
};
}
});
export const StyledSettingSidebarHeader = styled('div')(() => {
{
return {
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'left',
marginTop: '37px',
};
}
});
export const StyledSettingTabContainer = styled('ul')(() => { export const StyledSettingTabContainer = styled('ul')(() => {
{ {
return { return {
@@ -65,8 +43,8 @@ export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
height: '34px', height: '34px',
color: isActive ? theme.colors.primaryColor : theme.colors.textColor, color: isActive ? theme.colors.primaryColor : theme.colors.textColor,
fontWeight: '500', fontWeight: '500',
fontSize: theme.font.base, fontSize: theme.font.h6,
lineHeight: theme.font.lineHeightBase, lineHeight: theme.font.lineHeight,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.15s ease', transition: 'all 0.15s ease',
borderBottom: `2px solid ${ borderBottom: `2px solid ${
@@ -78,79 +56,60 @@ export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
} }
); );
export const StyledSettingTagIconContainer = styled('div')(() => { export const StyledSettingKey = styled.div(({ theme }) => {
return { return {
display: 'flex', width: '140px',
alignItems: 'center', textAlign: 'right',
};
});
export const StyledSettingH2 = styled('h2')<{
marginTop?: number;
marginBottom?: number;
}>(({ marginTop, marginBottom, theme }) => {
return {
fontWeight: '500',
fontSize: theme.font.base, fontSize: theme.font.base,
lineHeight: theme.font.lineHeightBase, fontWeight: 500,
marginTop: marginTop ? `${marginTop}px` : '0px', marginRight: '56px',
marginBottom: marginBottom ? `${marginBottom}px` : '0px', flexShrink: 0,
};
});
export const StyledRow = styled(FlexWrapper)(() => {
return {
marginBottom: '32px',
}; };
}); });
export const StyledPublishExplanation = styled('div')(() => { export const StyledWorkspaceName = styled('span')(({ theme }) => {
return {
paddingRight: '48px',
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
marginBottom: '22px',
};
});
export const StyledWorkspaceName = styled('span')(() => {
return { return {
fontWeight: '400', fontWeight: '400',
fontSize: '18px', fontSize: theme.font.h6,
lineHeight: '26px',
};
});
export const StyledWorkspaceType = styled('span')(() => {
return {
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
}; };
}); });
export const StyledPublishCopyContainer = styled('div')(() => { export const StyledEmail = styled('span')(() => {
return { return {
marginTop: '12px', color: '#E8178A',
marginBottom: '20px',
paddingTop: '20px',
flex: 1,
};
});
export const StyledStopPublishContainer = styled('div')(() => {
return {
textAlign: 'center',
}; };
}); });
export const StyledCopyButtonContainer = styled('div')(() => { // export const StyledDownloadCard = styled.div<{ active?: boolean }>(
return { // ({ theme, active }) => {
marginTop: '64px', // return {
}; // width: '240px',
}); // height: '86px',
// border: '1px solid',
export const StyledPublishContent = styled('div')(() => { // borderColor: active
return { // ? theme.colors.primaryColor
display: 'flex', // : theme.colors.borderColor,
flexDirection: 'column', // borderRadius: '10px',
}; // padding: '8px 12px',
}); // position: 'relative',
// ':not(:last-of-type)': {
export const StyleAsync = styled('div')(() => { // marginRight: '24px',
return { // },
marginTop: '64px', // svg: {
}; // display: active ? 'block' : 'none',
}); // ...positionAbsolute({ top: '-12px', right: '-12px' }),
// },
// };
// }
// );
// export const StyledDownloadCardDes = styled.div(({ theme }) => {
// return {
// fontSize: theme.font.sm,
// color: theme.colors.iconColor,
// };
// });

View File

@@ -1,4 +1,4 @@
import MuiAvatar from '@mui/material/Avatar'; import { MuiAvatar } from '@/ui/mui';
import { styled } from '@/styles'; import { styled } from '@/styles';
export const WorkspaceItemWrapper = styled('div')(({ theme }) => ({ export const WorkspaceItemWrapper = styled('div')(({ theme }) => ({

View File

@@ -32,13 +32,13 @@ export const WorkspaceSelector = () => {
> >
<WorkspaceUnitAvatar <WorkspaceUnitAvatar
size={28} size={28}
name={currentWorkspace?.name ?? 'AFFiNE'} name={currentWorkspace?.name ?? 'AFFiNE Test'}
workspaceUnit={currentWorkspace} workspaceUnit={currentWorkspace}
/> />
</div> </div>
</Avatar> </Avatar>
<WorkspaceName data-testid="workspace-name"> <WorkspaceName data-testid="workspace-name">
{currentWorkspace?.name ?? 'AFFiNE'} {currentWorkspace?.name ?? 'AFFiNE Test'}
</WorkspaceName> </WorkspaceName>
</SelectorWrapper> </SelectorWrapper>
<WorkspaceModal <WorkspaceModal

View File

@@ -1,4 +1,4 @@
import MuiAvatar from '@mui/material/Avatar'; import { MuiAvatar } from '@/ui/mui';
import { styled } from '@/styles'; import { styled } from '@/styles';
import { StyledPopperContainer } from '@/ui/shared/Container'; import { StyledPopperContainer } from '@/ui/shared/Container';

View File

@@ -11,18 +11,17 @@ import {
StyledSubListItem, StyledSubListItem,
} from './style'; } from './style';
import { Arrow } from './icons'; import { Arrow } from './icons';
import Collapse from '@mui/material/Collapse';
import { import {
ArrowDownIcon, ArrowDownIcon,
SearchIcon, SearchIcon,
AllPagesIcon, AllPagesIcon,
FavouritesIcon, FavouritesIcon,
ImportIcon,
TrashIcon, TrashIcon,
AddIcon, AddIcon,
SettingsIcon, SettingsIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import Link from 'next/link'; import Link from 'next/link';
import { MuiCollapse } from '@/ui/mui';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import { useModal } from '@/providers/GlobalModalProvider'; import { useModal } from '@/providers/GlobalModalProvider';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
@@ -39,7 +38,7 @@ const FavoriteList = ({ showList }: { showList: boolean }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const favoriteList = pageList.filter(p => p.favorite && !p.trash); const favoriteList = pageList.filter(p => p.favorite && !p.trash);
return ( return (
<Collapse in={showList}> <MuiCollapse in={showList}>
{favoriteList.map((pageMeta, index) => { {favoriteList.map((pageMeta, index) => {
const active = router.query.pageId === pageMeta.id; const active = router.query.pageId === pageMeta.id;
return ( return (
@@ -61,11 +60,11 @@ const FavoriteList = ({ showList }: { showList: boolean }) => {
{favoriteList.length === 0 && ( {favoriteList.length === 0 && (
<StyledSubListItem disable={true}>{t('No item')}</StyledSubListItem> <StyledSubListItem disable={true}>{t('No item')}</StyledSubListItem>
)} )}
</Collapse> </MuiCollapse>
); );
}; };
export const WorkSpaceSliderBar = () => { export const WorkSpaceSliderBar = () => {
const { triggerQuickSearchModal, triggerImportModal } = useModal(); const { triggerQuickSearchModal } = useModal();
const [showSubFavorite, setShowSubFavorite] = useState(true); const [showSubFavorite, setShowSubFavorite] = useState(true);
const { currentWorkspace } = useAppState(); const { currentWorkspace } = useAppState();
const { openPage, createPage } = usePageHelper(); const { openPage, createPage } = usePageHelper();
@@ -161,14 +160,14 @@ export const WorkSpaceSliderBar = () => {
setShowWorkspaceSetting(false); setShowWorkspaceSetting(false);
}} }}
/> */} /> */}
{/* TODO: will finish the feature next version */}
<StyledListItem {/* <StyledListItem
onClick={() => { onClick={() => {
triggerImportModal(); triggerImportModal();
}} }}
> >
<ImportIcon /> {t('Import')} <ImportIcon /> {t('Import')}
</StyledListItem> </StyledListItem> */}
<Link href={{ pathname: paths.trash }}> <Link href={{ pathname: paths.trash }}>
<StyledListItem active={router.asPath === paths.trash}> <StyledListItem active={router.asPath === paths.trash}>

View File

@@ -4,9 +4,9 @@ import Link from 'next/link';
export const StyledSliderBar = styled.div<{ show: boolean }>( export const StyledSliderBar = styled.div<{ show: boolean }>(
({ theme, show }) => { ({ theme, show }) => {
return { return {
width: show ? '320px' : '0', width: show ? '256px' : '0',
height: '100vh', height: '100vh',
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC', background: theme.mode === 'dark' ? '#272727' : '#F9F9FB',
boxShadow: theme.shadow.modal, boxShadow: theme.shadow.modal,
transition: 'width .15s, padding .15s', transition: 'width .15s, padding .15s',
position: 'relative', position: 'relative',
@@ -58,7 +58,6 @@ export const StyledListItem = styled.div<{
width: '296px', width: '296px',
height: '32px', height: '32px',
marginTop: '12px', marginTop: '12px',
fontSize: theme.font.sm,
color: active ? theme.colors.primaryColor : theme.colors.popoverColor, color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
paddingLeft: '12px', paddingLeft: '12px',
borderRadius: '5px', borderRadius: '5px',
@@ -122,9 +121,8 @@ export const StyledSubListItem = styled.button<{
width: '296px', width: '296px',
height: '32px', height: '32px',
marginTop: '4px', marginTop: '4px',
fontSize: theme.font.sm,
color: disable color: disable
? theme.colors.iconColor ? theme.colors.disableColor
: active : active
? theme.colors.primaryColor ? theme.colors.primaryColor
: theme.colors.popoverColor, : theme.colors.popoverColor,

View File

@@ -38,7 +38,7 @@ export const useEnsureWorkspace = () => {
// } // }
const workspaceId = const workspaceId =
(router.query.workspaceId as string) || workspaceList[0]?.id; (router.query.workspaceId as string) || workspaceList[0]?.id;
loadWorkspace(workspaceId).finally(() => { loadWorkspace.current(workspaceId).finally(() => {
setWorkspaceLoaded(true); setWorkspaceLoaded(true);
setActiveWorkspaceId(activeWorkspaceId); setActiveWorkspaceId(activeWorkspaceId);
}); });

View File

@@ -1,4 +1,4 @@
import { uuidv4 } from '@blocksuite/store'; import { uuidv4, Workspace } from '@blocksuite/store';
import { QueryContent } from '@blocksuite/store/dist/workspace/search'; import { QueryContent } from '@blocksuite/store/dist/workspace/search';
import { PageMeta, useAppState } from '@/providers/app-state-provider'; import { PageMeta, useAppState } from '@/providers/app-state-provider';
import { EditorContainer } from '@blocksuite/editor'; import { EditorContainer } from '@blocksuite/editor';
@@ -20,7 +20,10 @@ export type EditorHandlers = {
toggleDeletePage: (pageId: string) => Promise<boolean>; toggleDeletePage: (pageId: string) => Promise<boolean>;
toggleFavoritePage: (pageId: string) => Promise<boolean>; toggleFavoritePage: (pageId: string) => Promise<boolean>;
permanentlyDeletePage: (pageId: string) => void; permanentlyDeletePage: (pageId: string) => void;
search: (query: QueryContent) => Map<string, string | undefined>; search: (
query: QueryContent,
workspace?: Workspace
) => Map<string, string | undefined>;
// changeEditorMode: (pageId: string) => void; // changeEditorMode: (pageId: string) => void;
changePageMode: ( changePageMode: (
pageId: string, pageId: string,
@@ -83,9 +86,16 @@ export const usePageHelper = (): EditorHandlers => {
}); });
return trash; return trash;
}, },
search: (query: QueryContent) => { search: (query: QueryContent, workspace?: Workspace) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (workspace) {
return currentWorkspace!.blocksuiteWorkspace!.search(query); return workspace.search(query);
}
if (currentWorkspace) {
if (currentWorkspace.blocksuiteWorkspace) {
return currentWorkspace.blocksuiteWorkspace.search(query);
}
}
return new Map();
}, },
changePageMode: async (pageId, mode) => { changePageMode: async (pageId, mode) => {
const pageMeta = getPageMeta(currentWorkspace, pageId); const pageMeta = getPageMeta(currentWorkspace, pageId);

View File

@@ -1,12 +1,8 @@
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { useConfirm } from '@/providers/ConfirmProvider';
import { toast } from '@/ui/toast';
import { WorkspaceUnit } from '@affine/datacenter'; import { WorkspaceUnit } from '@affine/datacenter';
import router from 'next/router';
export const useWorkspaceHelper = () => { export const useWorkspaceHelper = () => {
const { confirm } = useConfirm(); const { dataCenter, currentWorkspace } = useAppState();
const { dataCenter, currentWorkspace, user, login } = useAppState();
const createWorkspace = async (name: string) => { const createWorkspace = async (name: string) => {
const workspaceInfo = await dataCenter.createWorkspace({ const workspaceInfo = await dataCenter.createWorkspace({
name: name, name: name,
@@ -31,33 +27,11 @@ export const useWorkspaceHelper = () => {
await dataCenter.updateWorkspaceMeta({ name }, workspace); await dataCenter.updateWorkspaceMeta({ name }, workspace);
} }
if (avatarBlob) { if (avatarBlob) {
// const blobId = await dataCenter.setBlob(workspace, avatarBlob); const blobId = await dataCenter.setBlob(workspace, avatarBlob);
// await dataCenter.updateWorkspaceMeta({ avatar: blobId }, workspace); await dataCenter.updateWorkspaceMeta({ avatar: blobId }, workspace);
} }
}; };
const enableWorkspace = async () => {
confirm({
title: 'Enable AFFiNE Cloud?',
content: `If enabled, the data in this workspace will be backed up and synchronized via AFFiNE Cloud.`,
confirmText: user ? 'Enable' : 'Sign in and Enable',
cancelText: 'Skip',
confirmType: 'primary',
buttonDirection: 'column',
}).then(async confirm => {
if (confirm && currentWorkspace) {
if (!user) {
await login();
}
const workspace = await dataCenter.enableWorkspaceCloud(
currentWorkspace
);
workspace && router.push(`/workspace/${workspace.id}/setting`);
toast('Enabled success');
}
});
};
const deleteWorkSpace = async () => { const deleteWorkSpace = async () => {
currentWorkspace && (await dataCenter.deleteWorkspace(currentWorkspace.id)); currentWorkspace && (await dataCenter.deleteWorkspace(currentWorkspace.id));
}; };
@@ -77,7 +51,6 @@ export const useWorkspaceHelper = () => {
createWorkspace, createWorkspace,
publishWorkspace, publishWorkspace,
updateWorkspace, updateWorkspace,
enableWorkspace,
deleteWorkSpace, deleteWorkSpace,
leaveWorkSpace, leaveWorkSpace,
acceptInvite, acceptInvite,

View File

@@ -1,10 +0,0 @@
export const AlreadyJoined = () => {
return (
<div>
<p>
You are already a member of <code>Workspace name</code>
</p>
<button>Open Workspace</button>
</div>
);
};

View File

@@ -1,11 +0,0 @@
export const Confirm = () => {
return (
<div>
<p>Inviter name</p>
<p>
invite you to join in <code>Workspace name</code>
</p>
<button>Join</button>
</div>
);
};

View File

@@ -1,8 +0,0 @@
export const LinkExpired = () => {
return (
<div>
<h1>The current invitation link has expired.</h1>
<a href={location.origin}>Back to home</a>
</div>
);
};

View File

@@ -1,21 +0,0 @@
import { useRouter } from 'next/router';
import { AlreadyJoined } from './AlreadyJoined';
import { Confirm } from './Confirm';
import { LinkExpired } from './LinkExpired';
export const ConfirmInvitation = () => {
const router = useRouter();
// Temporary code. The code should be returned by request.
const { code } = router.query;
const Component = {
'-1': LinkExpired,
0: Confirm,
1: AlreadyJoined,
}[code as string];
return (
<div>
<h1>Confirm Invitation</h1>
{Component ? <Component /> : null}
</div>
);
};

View File

@@ -67,15 +67,20 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => {
}; };
const AppDefender = ({ children }: PropsWithChildren) => { const AppDefender = ({ children }: PropsWithChildren) => {
const { synced } = useAppState();
const router = useRouter(); const router = useRouter();
const { synced } = useAppState();
useEffect(() => { useEffect(() => {
if (router.pathname === '/') { if (router.asPath === '/') {
router.replace('/workspace'); router.replace('/workspace');
} }
}, [router]); }, [router]);
// if you visit /404, you will see the children directly
if (router.route === '/404') {
return <div>{children}</div>;
}
return <div>{synced ? children : <PageLoading />}</div>; return <div>{synced ? children : <PageLoading />}</div>;
}; };

View File

@@ -36,7 +36,6 @@ const Affine = () => {
<div>hi</div> <div>hi</div>
</Modal> </Modal>
<Loading /> <Loading />
<Button <Button
icon={<FavouritedIcon />} icon={<FavouritedIcon />}
onClick={() => { onClick={() => {
@@ -48,13 +47,15 @@ const Affine = () => {
<Button icon={<FavouritedIcon />} type={'primary'}> <Button icon={<FavouritedIcon />} type={'primary'}>
click me! click me!
</Button> </Button>
<Button icon={<FavouritedIcon />} type={'light'}>
click me!
</Button>
<Button icon={<FavouritedIcon />} type={'warning'}> <Button icon={<FavouritedIcon />} type={'warning'}>
click me! click me!
</Button> </Button>
<Button icon={<FavouritedIcon />} type={'danger'}> <Button icon={<FavouritedIcon />} type={'danger'}>
click me! click me!
</Button> </Button>
<Button icon={<FavouritedIcon />}></Button> <Button icon={<FavouritedIcon />}></Button>
<Button icon={<FavouritedIcon />} shape="round"></Button> <Button icon={<FavouritedIcon />} shape="round"></Button>
<Button loading={true}></Button> <Button loading={true}></Button>

View File

@@ -1,8 +0,0 @@
import type { NextPage } from 'next';
import { ConfirmInvitation } from '../pages-content/ConfirmInvitationPage';
const ConfirmInvitationPage: NextPage = () => {
return <ConfirmInvitation />;
};
export default ConfirmInvitationPage;

View File

@@ -1,9 +1,9 @@
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { styled } from '@/styles'; import { styled } from '@/styles';
import { Empty } from '@/ui/empty'; import { Empty } from '@/ui/empty';
// import { Avatar } from '@mui/material';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { PageLoading } from '@/components/loading';
// const User = ({ name, avatar }: { name: string; avatar?: string }) => { // const User = ({ name, avatar }: { name: string; avatar?: string }) => {
// return ( // return (
@@ -19,19 +19,29 @@ import { useEffect, useState } from 'react';
// }; // };
export default function DevPage() { export default function DevPage() {
const [loading, setLoading] = useState(true);
const router = useRouter(); const router = useRouter();
const [successInvited, setSuccessInvited] = useState<boolean>(false); const [successInvited, setSuccessInvited] = useState<boolean>(false);
const { acceptInvite } = useWorkspaceHelper(); const { acceptInvite } = useWorkspaceHelper();
useEffect(() => { useEffect(() => {
router.query.invite_code && if (router.query.invite_code) {
acceptInvite(router.query.invite_code as string).then(data => { acceptInvite(router.query.invite_code as string)
if (data && data.accepted) { .then(data => {
setSuccessInvited(true); if (data && data.accepted) {
} setSuccessInvited(true);
}); }
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
}, [router, acceptInvite]); }, [router, acceptInvite]);
return ( return loading ? (
<PageLoading />
) : (
<Invited> <Invited>
<div> <div>
<Empty width={310} height={310}></Empty> <Empty width={310} height={310}></Empty>

View File

@@ -1,11 +1,17 @@
import { ReactElement, useEffect, useState } from 'react'; import { ReactElement, useEffect, useState } from 'react';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import type { NextPageWithLayout } from '../..//_app'; import type { NextPageWithLayout } from '../..//_app';
import { styled } from '@/styles'; import { displayFlex, styled } from '@/styles';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { Page as PageStore, Workspace } from '@blocksuite/store'; import { Page as PageStore, Workspace } from '@blocksuite/store';
import { PageLoading } from '@/components/loading'; import { PageLoading } from '@/components/loading';
import { Breadcrumbs } from '@/ui/breadcrumbs';
import { IconButton } from '@/ui/button';
import NextLink from 'next/link';
import { PaperIcon, SearchIcon } from '@blocksuite/icons';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { useModal } from '@/providers/GlobalModalProvider';
const DynamicBlocksuite = dynamic(() => import('@/components/editor'), { const DynamicBlocksuite = dynamic(() => import('@/components/editor'), {
ssr: false, ssr: false,
@@ -16,12 +22,16 @@ const Page: NextPageWithLayout = () => {
const { dataCenter } = useAppState(); const { dataCenter } = useAppState();
const router = useRouter(); const router = useRouter();
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const [workspaceName, setWorkspaceName] = useState('');
const [pageTitle, setPageTitle] = useState('');
const { triggerQuickSearchModal } = useModal();
useEffect(() => { useEffect(() => {
dataCenter dataCenter
.loadPublicWorkspace(router.query.workspaceId as string) .loadPublicWorkspace(router.query.workspaceId as string)
.then(data => { .then(data => {
if (data && data.blocksuiteWorkspace) { setWorkspaceName(data.blocksuiteWorkspace?.meta.name as string);
if (data.blocksuiteWorkspace) {
setWorkspace(data.blocksuiteWorkspace); setWorkspace(data.blocksuiteWorkspace);
if ( if (
router.query.pageId && router.query.pageId &&
@@ -32,6 +42,7 @@ const Page: NextPageWithLayout = () => {
const page = data.blocksuiteWorkspace.getPage( const page = data.blocksuiteWorkspace.getPage(
router.query.pageId as string router.query.pageId as string
); );
page && setPageTitle(page.meta.title);
page && setPage(page); page && setPage(page);
} else { } else {
router.push('/404'); router.push('/404');
@@ -46,6 +57,30 @@ const Page: NextPageWithLayout = () => {
<> <>
{!loaded && <PageLoading />} {!loaded && <PageLoading />}
<PageContainer> <PageContainer>
<NavContainer>
<Breadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${router.query.workspaceId}`}
>
<WorkspaceUnitAvatar size={24} name={workspaceName} />
<span>{workspaceName}</span>
</StyledBreadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${router.query.workspaceId}/${router.query.pageId}`}
>
<PaperIcon fontSize={24} />
<span>{pageTitle ? pageTitle : 'Untitled'}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
<SearchButton
onClick={() => {
triggerQuickSearchModal();
}}
>
<SearchIcon />
</SearchButton>
</NavContainer>
{workspace && page && ( {workspace && page && (
<DynamicBlocksuite <DynamicBlocksuite
page={page} page={page}
@@ -69,9 +104,43 @@ export default Page;
export const PageContainer = styled.div(({ theme }) => { export const PageContainer = styled.div(({ theme }) => {
return { return {
height: 'calc(100vh)', height: '100vh',
padding: '78px 72px',
overflowY: 'auto', overflowY: 'auto',
backgroundColor: theme.colors.pageBackground, backgroundColor: theme.colors.pageBackground,
}; };
}); });
export const NavContainer = styled.div(({ theme }) => {
return {
width: '100vw',
padding: '0 12px',
height: '60px',
...displayFlex('start', 'center'),
backgroundColor: theme.colors.pageBackground,
};
});
export const StyledBreadcrumbs = styled(NextLink)(({ theme }) => {
return {
flex: 1,
...displayFlex('center', 'center'),
paddingLeft: '12px',
span: {
padding: '0 12px',
fontSize: theme.font.base,
lineHeight: theme.font.lineHeight,
},
':hover': { color: theme.colors.primaryColor },
transition: 'all .15s',
':visited': {
color: theme.colors.popoverColor,
':hover': { color: theme.colors.primaryColor },
},
};
});
export const SearchButton = styled(IconButton)(({ theme }) => {
return {
color: theme.colors.iconColor,
fontSize: '24px',
marginLeft: 'auto',
padding: '0 24px',
};
});

View File

@@ -2,16 +2,28 @@ import { PageList } from '@/components/page-list';
import { ReactElement, useEffect, useState } from 'react'; import { ReactElement, useEffect, useState } from 'react';
import { PageMeta, useAppState } from '@/providers/app-state-provider'; import { PageMeta, useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { PageContainer } from './[pageId]'; import {
PageContainer,
NavContainer,
StyledBreadcrumbs,
SearchButton,
} from './[pageId]';
import { Breadcrumbs } from '@/ui/breadcrumbs';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { SearchIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/GlobalModalProvider';
const All = () => { const All = () => {
const { dataCenter } = useAppState(); const { dataCenter } = useAppState();
const router = useRouter(); const router = useRouter();
const [pageList, setPageList] = useState<PageMeta[]>([]); const [pageList, setPageList] = useState<PageMeta[]>([]);
const [workspaceName, setWorkspaceName] = useState('');
const { triggerQuickSearchModal } = useModal();
useEffect(() => { useEffect(() => {
dataCenter dataCenter
.loadPublicWorkspace(router.query.workspaceId as string) .loadPublicWorkspace(router.query.workspaceId as string)
.then(data => { .then(data => {
setPageList(data.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]); setPageList(data.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]);
setWorkspaceName(data.blocksuiteWorkspace?.meta.name as string);
}) })
.catch(() => { .catch(() => {
router.push('/404'); router.push('/404');
@@ -20,6 +32,23 @@ const All = () => {
return ( return (
<PageContainer> <PageContainer>
<NavContainer>
<Breadcrumbs>
<StyledBreadcrumbs
href={`/public-workspace/${router.query.workspaceId}`}
>
<WorkspaceUnitAvatar size={24} name={workspaceName} />
<span>{workspaceName}</span>
</StyledBreadcrumbs>
</Breadcrumbs>
<SearchButton
onClick={() => {
triggerQuickSearchModal();
}}
>
<SearchIcon />
</SearchButton>
</NavContainer>
<PageList <PageList
pageList={pageList.filter(p => !p.trash)} pageList={pageList.filter(p => !p.trash)}
showFavoriteTag={false} showFavoriteTag={false}

View File

@@ -1,14 +1,8 @@
debug-menu {
display: none !important;
}
.affine-block-children-container.edgeless {
background-color: #fff;
}
.affine-default-page-block-title-container { .affine-default-page-block-title-container {
margin-top: 78px; margin-top: 78px;
margin-bottom: 40px; margin-bottom: 40px;
} }
.affine-editor-container {
background: transparent !important; .affine-default-page-block-container {
width: 686px !important;
} }

View File

@@ -4,9 +4,11 @@ import { PageListHeader } from '@/components/header';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import WorkspaceLayout from '@/components/workspace-layout'; import WorkspaceLayout from '@/components/workspace-layout';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider'; import { PageMeta, useAppState } from '@/providers/app-state-provider';
const All = () => { const All = () => {
const { pageList } = useAppState(); const { currentWorkspace } = useAppState();
const pageList = (currentWorkspace?.blocksuiteWorkspace?.meta.pageMetas ||
[]) as PageMeta[];
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>

View File

@@ -10,8 +10,8 @@ import {
GeneralPage, GeneralPage,
MembersPage, MembersPage,
PublishPage, PublishPage,
ExportPage,
SyncPage, SyncPage,
ExportPage,
} from '@/components/workspace-setting'; } from '@/components/workspace-setting';
import { SettingsIcon } from '@blocksuite/icons'; import { SettingsIcon } from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
@@ -20,41 +20,37 @@ import { WorkspaceUnit } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n'; import { useTranslation } from '@affine/i18n';
import { PageListHeader } from '@/components/header'; import { PageListHeader } from '@/components/header';
type TabNames = 'General' | 'Sync' | 'Collaboration' | 'Publish' | 'Export'; const useTabMap = () => {
const tabMap: {
name: TabNames;
panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = [
{
name: 'General',
panelRender: workspace => <GeneralPage workspace={workspace} />,
},
{
name: 'Sync',
panelRender: workspace => <SyncPage workspace={workspace} />,
},
{
name: 'Collaboration',
panelRender: workspace => <MembersPage workspace={workspace} />,
},
{
name: 'Publish',
panelRender: workspace => <PublishPage workspace={workspace} />,
},
{
name: 'Export',
panelRender: workspace => <ExportPage workspace={workspace} />,
},
];
const WorkspaceSetting = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentWorkspace, isOwner } = useAppState(); const { isOwner } = useAppState();
const tabMap: {
const [activeTab, setActiveTab] = useState<TabNames>(tabMap[0].name); name: string;
const handleTabChange = (tab: TabNames) => { panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = [
{
name: t('General'),
panelRender: workspace => <GeneralPage workspace={workspace} />,
},
{
name: t('Sync'),
panelRender: workspace => <SyncPage workspace={workspace} />,
},
{
name: t('Collaboration'),
panelRender: workspace => <MembersPage workspace={workspace} />,
},
{
name: t('Publish'),
panelRender: workspace => <PublishPage workspace={workspace} />,
},
// TODO: next version will finish this feature
{
name: t('Export'),
panelRender: workspace => <ExportPage workspace={workspace} />,
},
];
const [activeTab, setActiveTab] = useState<string>(tabMap[0].name);
const handleTabChange = (tab: string) => {
setActiveTab(tab); setActiveTab(tab);
}; };
@@ -62,7 +58,7 @@ const WorkspaceSetting = () => {
tab => tab.name === activeTab tab => tab.name === activeTab
)?.panelRender; )?.panelRender;
let tableArr: { let tableArr: {
name: TabNames; name: string;
panelRender: (workspace: WorkspaceUnit) => ReactNode; panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = tabMap; }[] = tabMap;
if (!isOwner) { if (!isOwner) {
@@ -73,6 +69,14 @@ const WorkspaceSetting = () => {
}, },
]; ];
} }
return { activeTabPanelRender, tableArr, handleTabChange, activeTab };
};
const WorkspaceSetting = () => {
const { t } = useTranslation();
const { currentWorkspace } = useAppState();
const { activeTabPanelRender, tableArr, handleTabChange, activeTab } =
useTabMap();
return ( return (
<> <>
<StyledSettingContainer> <StyledSettingContainer>

View File

@@ -10,16 +10,21 @@ import {
import { createDefaultWorkspace } from './utils'; import { createDefaultWorkspace } from './utils';
import { User } from '@affine/datacenter'; import { User } from '@affine/datacenter';
export interface Disposable {
dispose(): void;
}
type AppStateContextProps = PropsWithChildren<Record<string, unknown>>; type AppStateContextProps = PropsWithChildren<Record<string, unknown>>;
export const AppState = createContext<AppStateContext>({} as AppStateContext); export const AppState = createContext<AppStateContext>({} as AppStateContext);
export const useAppState = () => useContext(AppState); export const useAppState = () => useContext(AppState);
export const AppStateProvider = ({ export const AppStateProvider = ({
children, children,
}: PropsWithChildren<AppStateContextProps>) => { }: PropsWithChildren<AppStateContextProps>) => {
const [appState, setAppState] = useState<AppStateValue>({} as AppStateValue); const [appState, setAppState] = useState<AppStateValue>({} as AppStateValue);
const [blobState, setBlobState] = useState(false);
const [userInfo, setUser] = useState<User | null>({} as User);
useEffect(() => { useEffect(() => {
const initState = async () => { const initState = async () => {
const dataCenter = await getDataCenter(); const dataCenter = await getDataCenter();
@@ -27,10 +32,9 @@ export const AppStateProvider = ({
if (dataCenter.workspaces.length === 0) { if (dataCenter.workspaces.length === 0) {
await createDefaultWorkspace(dataCenter); await createDefaultWorkspace(dataCenter);
} }
setUser((await dataCenter.getUserInfo()) || null);
setAppState({ setAppState({
dataCenter, dataCenter,
user: (await dataCenter.getUserInfo()) || null,
workspaceList: dataCenter.workspaces, workspaceList: dataCenter.workspaces,
currentWorkspace: null, currentWorkspace: null,
pageList: [], pageList: [],
@@ -90,15 +94,17 @@ export const AppStateProvider = ({
}); });
}; };
const loadWorkspace = useRef<AppStateFunction['loadWorkspace']>(); const loadWorkspace: AppStateFunction['loadWorkspace'] =
useRef() as AppStateFunction['loadWorkspace'];
loadWorkspace.current = async (workspaceId: string) => { loadWorkspace.current = async (workspaceId: string) => {
const { dataCenter, workspaceList, currentWorkspace, user } = appState; const { dataCenter, workspaceList, currentWorkspace } = appState;
if (!workspaceList.find(v => v.id.toString() === workspaceId)) { if (!workspaceList.find(v => v.id.toString() === workspaceId)) {
return null; return null;
} }
if (workspaceId === currentWorkspace?.id) { if (workspaceId === currentWorkspace?.id) {
return currentWorkspace; return currentWorkspace;
} }
const workspace = (await dataCenter.loadWorkspace(workspaceId)) ?? null; const workspace = (await dataCenter.loadWorkspace(workspaceId)) ?? null;
let isOwner; let isOwner;
if (workspace?.provider === 'local') { if (workspace?.provider === 'local') {
@@ -106,8 +112,9 @@ export const AppStateProvider = ({
isOwner = true; isOwner = true;
} else { } else {
// We must ensure workspace.owner exists, then ensure id same. // We must ensure workspace.owner exists, then ensure id same.
isOwner = workspace?.owner && user?.id === workspace.owner.id; isOwner = workspace?.owner && userInfo?.id === workspace.owner.id;
} }
const pageList = const pageList =
(workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? []; (workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? [];
setAppState({ setAppState({
@@ -122,6 +129,26 @@ export const AppStateProvider = ({
return workspace; return workspace;
}; };
useEffect(() => {
let syncChangeDisposable: Disposable | undefined;
const currentWorkspace = appState.currentWorkspace;
if (!currentWorkspace) {
return;
}
const getBlobStorage = async () => {
const blobStorage = await currentWorkspace?.blocksuiteWorkspace?.blobs;
syncChangeDisposable = blobStorage?.signals.onBlobSyncStateChange.on(
() => {
setBlobState(blobStorage?.uploading);
}
);
};
getBlobStorage();
return () => {
syncChangeDisposable?.dispose();
};
}, [appState.currentWorkspace]);
const setEditor: AppStateFunction['setEditor'] = const setEditor: AppStateFunction['setEditor'] =
useRef() as AppStateFunction['setEditor']; useRef() as AppStateFunction['setEditor'];
setEditor.current = editor => { setEditor.current = editor => {
@@ -139,20 +166,13 @@ export const AppStateProvider = ({
if (!user) { if (!user) {
throw new Error('User info not found'); throw new Error('User info not found');
} }
setAppState({ setUser(user);
...appState,
user,
});
return user; return user;
}; };
const logout = async () => { const logout = async () => {
const { dataCenter } = appState; const { dataCenter } = appState;
await dataCenter.logout(); await dataCenter.logout();
setAppState({ setUser(null);
...appState,
user: null,
});
}; };
return ( return (
@@ -161,9 +181,11 @@ export const AppStateProvider = ({
...appState, ...appState,
setEditor, setEditor,
loadPage: loadPage.current, loadPage: loadPage.current,
loadWorkspace: loadWorkspace.current, loadWorkspace: loadWorkspace,
login, login,
logout, logout,
blobDataSynced: blobState,
user: userInfo,
}} }}
> >
{children} {children}

View File

@@ -16,7 +16,7 @@ export interface PageMeta extends StorePageMeta {
export type AppStateValue = { export type AppStateValue = {
dataCenter: DataCenter; dataCenter: DataCenter;
user: User | null; user?: User | null;
workspaceList: WorkspaceUnit[]; workspaceList: WorkspaceUnit[];
currentWorkspace: WorkspaceUnit | null; currentWorkspace: WorkspaceUnit | null;
pageList: PageMeta[]; pageList: PageMeta[];
@@ -24,12 +24,15 @@ export type AppStateValue = {
editor?: EditorContainer | null; editor?: EditorContainer | null;
synced: boolean; synced: boolean;
isOwner?: boolean; isOwner?: boolean;
blobDataSynced?: boolean;
}; };
export type AppStateFunction = { export type AppStateFunction = {
setEditor: MutableRefObject<(page: EditorContainer) => void>; setEditor: MutableRefObject<(page: EditorContainer) => void>;
loadWorkspace: (workspaceId: string) => Promise<WorkspaceUnit | null>; loadWorkspace: MutableRefObject<
(workspaceId: string) => Promise<WorkspaceUnit | null>
>;
loadPage: (pageId: string) => void; loadPage: (pageId: string) => void;
login: () => Promise<User>; login: () => Promise<User>;

View File

@@ -1,6 +1,6 @@
import { DataCenter } from '@affine/datacenter'; import { DataCenter } from '@affine/datacenter';
const DEFAULT_WORKSPACE_NAME = 'affine'; const DEFAULT_WORKSPACE_NAME = 'AFFiNE Test';
export const createDefaultWorkspace = async (dataCenter: DataCenter) => { export const createDefaultWorkspace = async (dataCenter: DataCenter) => {
return dataCenter.createWorkspace({ return dataCenter.createWorkspace({

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