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
+27
View File
@@ -436,6 +436,33 @@
"contributions": [
"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"
]
}
]
}
-7
View File
@@ -1,7 +0,0 @@
**/webpack.config.js
**/scripts/*.js
**/node_modules/**
.github/**
**/__tests__/**
**/tests/**
+7
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/
-->
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.
@@ -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.
-11
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.
@@ -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.
@@ -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.
-7
View File
@@ -1,7 +0,0 @@
---
name: I have a question
about: Feel free to ask us your questions!
title: '[Question]'
labels: ''
assignees: ''
---
+35 -19
View File
@@ -67,18 +67,16 @@ jobs:
lint:
name: Lint and E2E Test
runs-on: ubuntu-latest
runs-on: self-hosted
environment: development
needs: build
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 'latest'
- name: Use Node.js
uses: actions/setup-node@v2
- uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'pnpm'
@@ -96,8 +94,6 @@ jobs:
- name: Install dependencies
run: pnpm install
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }}
- name: Download artifact
uses: actions/download-artifact@v3
@@ -105,20 +101,40 @@ jobs:
name: artifact
path: packages/app/.next/
- name: Lint & E2E Test
- name: Lint & E2E Test & Unit Test
run: |
pnpm lint --max-warnings=0
PLAYWRIGHT_BROWSERS_PATH=0 npx playwright install chromium
PLAYWRIGHT_BROWSERS_PATH=0 pnpm test
PLAYWRIGHT_BROWSERS_PATH=0 pnpm test:dc
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 }}
npx playwright install chromium
pnpm run test:coverage
pnpm run test:unit
# 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: 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:
name: Build Community
-48
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
View File
@@ -4,6 +4,8 @@
*dist
/tmp
/out-tsc
.nyc_output
.coverage
# dependencies
node_modules
+3 -1
View File
@@ -19,7 +19,9 @@
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*.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]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
-45
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>
-3
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.
+29 -10
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]: 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 -->
[![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)
[![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/)
[![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/)
@@ -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 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
@@ -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 **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.
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
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.
## Thanks
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.
- [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.
@@ -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://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://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>
</table>
@@ -192,10 +195,26 @@ Thanks a lot to the community for providing such powerful and simple libraries,
<!-- 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).
## 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
See [LICENSE](/LICENSE) for details.
+10 -10
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:
- 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
- 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
- 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
+2 -2
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:
- [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 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 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 `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:
+32
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
+1
View File
@@ -4,6 +4,7 @@
- https://allcontributors.org/docs/en/emoji-key
```shell
all-contributors check
all-contributors add tzhangchi code,doc
all-contributors generate
```
+1 -1
View File
@@ -26,4 +26,4 @@ You may be able to find additional help and information on our social media plat
### :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).
+13 -8
View File
@@ -15,12 +15,10 @@
"start": "pnpm --filter @affine/app start",
"lint": "pnpm --filter @affine/app lint",
"test": "playwright test",
"test:dc": "pnpm --filter @affine/datacenter test",
"test:e2e:codegen": "npx playwright codegen http://localhost:8080",
"test:coverage": "cross-env COVERAGE=true pnpm test -- --forbid-only",
"test:unit": "playwright test --config=playwright.config.unit.ts",
"postinstall": "husky install",
"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"
},
"lint-staged": {
@@ -29,13 +27,13 @@
},
"devDependencies": {
"@changesets/cli": "^2.26.0",
"@jest/globals": "^29.3.1",
"@playwright/test": "^1.29.1",
"@types/eslint": "^8.4.10",
"@types/node": "^18.11.17",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"concurrently": "^7.6.0",
"babel-plugin-istanbul": "^6.1.1",
"cross-env": "^7.0.3",
"eslint": "^8.30.0",
"eslint-config-next": "12.3.1",
@@ -44,10 +42,10 @@
"fake-indexeddb": "4.0.1",
"got": "^12.5.3",
"husky": "^8.0.2",
"jest": "^29.3.1",
"lint-staged": "^13.1.0",
"nyc": "^15.1.0",
"prettier": "^2.7.1",
"typescript": "^4.9.3"
"typescript": "^4.9.5"
},
"eslintConfig": {
"root": true,
@@ -70,9 +68,16 @@
},
"reportUnusedDisableDirectives": true,
"ignorePatterns": [
"src/**/*.test.ts",
"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
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,
};
+4
View File
@@ -68,6 +68,9 @@ const nextConfig = {
return profile;
},
basePath: process.env.BASE_PATH,
experimental: {
forceSwcTransforms: true,
},
};
const baseDir = process.env.LOCAL_BLOCK_SUITE ?? '/';
@@ -98,6 +101,7 @@ const withDebugLocal = require('next-debug-local')(
const withPWA = require('next-pwa')({
dest: 'public',
scope: '/_next',
disable: process.env.NODE_ENV !== 'production',
});
+7 -6
View File
@@ -11,10 +11,10 @@
"dependencies": {
"@affine/datacenter": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/blocks": "0.4.0-20230111171650-bc63456",
"@blocksuite/editor": "0.4.0-20230111171650-bc63456",
"@blocksuite/blocks": "0.4.0-alpha.2",
"@blocksuite/editor": "0.4.0-alpha.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/react": "^11.10.4",
"@emotion/server": "^11.10.0",
@@ -28,7 +28,7 @@
"cmdk": "^0.1.20",
"css-spring": "^4.1.0",
"dayjs": "^1.11.7",
"lit": "^2.3.1",
"lit": "^2.6.1",
"next": "13.1.0",
"next-debug-local": "^0.1.5",
"prettier": "^2.7.1",
@@ -36,9 +36,10 @@
"quill-cursors": "^4.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"yjs": "^13.5.44"
"yjs": "^13.5.45"
},
"devDependencies": {
"@emotion/babel-plugin": "^11.10.5",
"@types/node": "18.7.18",
"@types/react": "18.0.20",
"@types/react-dom": "18.0.6",
@@ -50,7 +51,7 @@
"eslint-plugin-prettier": "^4.2.1",
"next-pwa": "^5.6.0",
"raw-loader": "^4.0.2",
"typescript": "4.8.3"
"typescript": "^4.9.5"
},
"eslintConfig": {
"extends": [
+4 -4
View File
@@ -144,12 +144,12 @@ button,
select,
keygen,
legend {
color: var(--affine-text-color);
background-color: unset;
outline: 0;
border: 0;
font-size: 18px;
line-height: 1.5;
font-size: var(--affine-font-base);
line-height: var(--affine-line-height);
color: var(--affine-text-color);
font-family: var(--affine-font-family);
}
body {
@@ -173,7 +173,7 @@ input {
}
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset;
-webkit-box-shadow: 0 0 0 1000px white inset;
}
input[type='number'] {
+2 -2
View File
@@ -7,7 +7,7 @@ export const NotfoundPage = () => {
const router = useRouter();
return (
<PageContainer>
<NotFoundTitle>
<NotFoundTitle data-testid="notFound">
{t('404 - Page Not Found')}
<p>
<Button
@@ -15,7 +15,7 @@ export const NotfoundPage = () => {
router.push('/workspace');
}}
>
Back Home
{t('Back Home')}
</Button>
</p>
</NotFoundTitle>
@@ -91,8 +91,6 @@ export const ContactModal = ({
<span>Alpha</span>
</StyledModalHeaderLeft>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
@@ -53,13 +53,12 @@ export const CreateWorkspaceModal = ({ open, onClose }: ModalProps) => {
</Header>
<Content>
<ContentTitle>{t('New Workspace')}</ContentTitle>
<p>
Workspace is your virtual space to capture, create and plan as
just one person or together as a team.
</p>
<p>{t('Workspace description')}</p>
<Input
onKeyDown={handleKeyDown}
placeholder={'Set a Workspace name'}
placeholder={t('Set a Workspace name')}
maxLength={15}
minLength={0}
onChange={value => {
setWorkspaceName(value);
}}
@@ -29,8 +29,6 @@ export const DeleteModal = ({
<ModalWrapper width={620} height={334}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
@@ -14,8 +14,8 @@ import {
UndoIcon,
RedoIcon,
} from './Icons';
import { MuiSlide } from '@/ui/mui';
import { Tooltip } from '@/ui/tooltip';
import Slide from '@mui/material/Slide';
import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { useAppState } from '@/providers/app-state-provider';
import useHistoryUpdated from '@/hooks/use-history-update';
@@ -127,7 +127,7 @@ export const EdgelessToolbar = () => {
const { mode } = useCurrentPageMeta() || {};
return (
<Slide
<MuiSlide
direction="right"
in={mode === 'edgeless'}
mountOnEnter
@@ -155,7 +155,7 @@ export const EdgelessToolbar = () => {
</StyledToolbarWrapper>
<UndoRedo />
</StyledEdgelessToolbar>
</Slide>
</MuiSlide>
);
};
+10 -11
View File
@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react';
import type { Page, Workspace } from '@blocksuite/store';
import '@blocksuite/blocks';
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';
const StyledEditorContainer = styled('div')(() => {
@@ -39,22 +39,21 @@ export const Editor = ({ page, workspace, setEditor }: Props) => {
const title = metaTitle
? metaTitle
: isFirstPage
? 'Welcome to AFFiNE Alpha "Downhill"'
? 'Welcome to AFFiNE Alpha "Downhills"'
: '';
workspace?.setPageMeta(page.id, { title });
const pageId = page.addBlock({
flavour: 'affine:page',
title,
});
page.addBlock({ flavour: 'affine:surface' });
const frameId = page.addBlock({ flavour: 'affine:frame' }, pageId);
page.addBlock({ flavour: 'affine:frame' }, pageId);
const pageBlockId = page.addBlockByFlavour('affine:page', { title });
page.addBlockByFlavour('affine:surface', {}, null);
// Add frame block inside page block
const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId);
// Add paragraph block inside frame block
// If this is a first page in workspace, init an introduction markdown
if (isFirstPage) {
editor.clipboard.importMarkdown(exampleMarkdown, `${frameId}`);
editor.clipboard.importMarkdown(exampleMarkdown, frameId);
workspace.setPageMeta(page.id, { title });
page.resetHistory();
} else {
page.addBlockByFlavour('affine:paragraph', {}, frameId);
}
page.resetHistory();
}
@@ -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',
};
});
@@ -1,24 +1,28 @@
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { Button } from '@/ui/button';
import { useTranslation } from '@affine/i18n';
import { useState } from 'react';
import { EnableWorkspaceModal } from './EnableWorkspaceModal';
export const EnableWorkspaceButton = () => {
const { t } = useTranslation();
const { enableWorkspace } = useWorkspaceHelper();
const [loading, setLoading] = useState(false);
const [enableModalOpen, setEnableModalOpen] = useState(false);
return (
<Button
type="primary"
shape="circle"
loading={loading}
onClick={async () => {
setLoading(true);
await enableWorkspace();
setLoading(false);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
<>
<Button
type="light"
shape="circle"
onClick={async () => {
setEnableModalOpen(true);
}}
>
{t('Enable AFFiNE Cloud')}
</Button>
<EnableWorkspaceModal
open={enableModalOpen}
onClose={() => {
setEnableModalOpen(false);
}}
></EnableWorkspaceModal>
</>
);
};
@@ -6,7 +6,7 @@ import {
StyledBrowserWarning,
StyledCloseButton,
} from './styles';
import CloseIcon from '@mui/icons-material/Close';
import { CloseIcon } from '@blocksuite/icons';
import { useWarningMessage, shouldShowWarning } from './utils';
import EditorOptionMenu from './header-right-items/EditorOptionMenu';
import TrashButtonGroup from './header-right-items/TrashButtonGroup';
@@ -1,5 +1,6 @@
import { PropsWithChildren, ReactNode } from 'react';
import Header from './Header';
import QuickSearchButton from './QuickSearchButton';
import { StyledPageListTittleWrapper } from './styles';
// import QuickSearchButton from './QuickSearchButton';
@@ -12,7 +13,7 @@ export const PageListHeader = ({ icon, children }: PageListHeaderProps) => {
<StyledPageListTittleWrapper>
{icon}
{children}
{/* <QuickSearchButton style={{ marginLeft: '5px' }} /> */}
<QuickSearchButton />
</StyledPageListTittleWrapper>
</Header>
);
@@ -1,15 +1,41 @@
import { CloudUnsyncedIcon, CloudInsyncIcon } from '@blocksuite/icons';
import { CloudUnsyncedIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/GlobalModalProvider';
import { useAppState } from '@/providers/app-state-provider';
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 = () => {
const { triggerLoginModal } = useModal();
const appState = useAppState();
return appState.user ? (
<IconButton iconSize="middle" disabled>
<CloudInsyncIcon />
<DefaultSyncIcon />
</IconButton>
) : (
<IconButton
+1 -1
View File
@@ -103,7 +103,7 @@ export const StyledSearchArrowWrapper = styled.div(() => {
export const StyledPageListTittleWrapper = styled(StyledTitle)(({ theme }) => {
return {
fontSize: theme.font.sm,
fontSize: theme.font.base,
color: theme.colors.textColor,
'>svg': {
fontSize: '20px',
@@ -6,7 +6,7 @@ import {
StyledTransformIcon,
} from './style';
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
import Grow from '@mui/material/Grow';
import { MuiGrow } from '@/ui/mui';
import { Tooltip } from '@/ui/tooltip';
import { useTranslation } from '@affine/i18n';
import { useModal } from '@/providers/GlobalModalProvider';
@@ -35,7 +35,7 @@ export const HelpIsland = ({
setShowContent(false);
}}
>
<Grow in={showContent}>
<MuiGrow in={showContent}>
<StyledIslandWrapper>
{showList.includes('contact') && (
<Tooltip content={t('Contact Us')} placement="left-end">
@@ -66,7 +66,7 @@ export const HelpIsland = ({
</Tooltip>
)}
</StyledIslandWrapper>
</Grow>
</MuiGrow>
<div style={{ position: 'relative' }}>
<StyledIconWrapper
+3 -3
View File
@@ -1,7 +1,7 @@
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { StyledButtonWrapper, StyledTitle } from './styles';
import { Button } from '@/ui/button';
import { Wrapper, Content } from '@/ui/layout';
import { Content, FlexWrapper } from '@/ui/layout';
import Loading from '@/components/loading';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useAppState } from '@/providers/app-state-provider';
@@ -116,7 +116,7 @@ export const ImportModal = ({ open, onClose }: ImportModalProps) => {
)}
{status === 'importing' && (
<Wrapper
<FlexWrapper
wrap={true}
justifyContent="center"
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
import function
</Content>
</Wrapper>
</FlexWrapper>
)}
</ModalWrapper>
</Modal>
@@ -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>
);
};
@@ -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,
};
});
@@ -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,
};
});
@@ -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

@@ -1,7 +1,9 @@
import { styled } from '@/styles';
import { positionAbsolute, styled } from '@/styles';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { GoogleLoginButton } from './LoginOptionButton';
import { Button } from '@/ui/button';
import { useAppState } from '@/providers/app-state-provider';
import { useTranslation } from '@affine/i18n';
import { GoogleIcon } from './GoogleIcon';
interface LoginModalProps {
open: boolean;
onClose: () => void;
@@ -9,38 +11,44 @@ interface LoginModalProps {
export const LoginModal = ({ open, onClose }: LoginModalProps) => {
const { login } = useAppState();
const { t } = useTranslation();
return (
<Modal open={open} onClose={onClose} data-testid="login-modal">
<ModalWrapper width={560} height={292}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
/>
</Header>
<ModalWrapper width={560} height={292} style={{ paddingTop: '44px' }}>
<ModalCloseButton
onClick={() => {
onClose();
}}
/>
<Content>
<ContentTitle>{'Sign in'}</ContentTitle>
<SignDes>Set up an AFFINE account to sync data</SignDes>
<span
<ContentTitle>{t('Sign in')}</ContentTitle>
<SignDes>{t('Set up an AFFiNE account to sync data')}</SignDes>
<StyledLoginButton
shape="round"
onClick={async () => {
await login();
onClose();
}}
>
<GoogleLoginButton />
</span>
<GoogleIcon />
{t('Continue with Google')}
</StyledLoginButton>
</Content>
</ModalWrapper>
</Modal>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
const StyledLoginButton = styled(Button)(() => {
return {
width: '284px',
marginTop: '30px',
position: 'relative',
svg: {
...positionAbsolute({ left: '18px', top: '0', bottom: '0' }),
margin: 'auto',
},
};
});
const Content = styled('div')({
@@ -8,7 +8,7 @@ export const Check = () => {
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_9266_16831)">
<g clipPath="url(#clip0_9266_16831)">
<path
fillRule="evenodd"
clipRule="evenodd"
@@ -3,28 +3,34 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import { Check, UnCheck } from './icon';
import { useState } from 'react';
import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider';
interface LoginModalProps {
open: boolean;
onClose: (wait: boolean) => void;
}
export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
const [localCache, setLocalCache] = useState(false);
const [localCache, setLocalCache] = useState(true);
const { blobDataSynced } = useAppState();
const { t } = useTranslation();
return (
<Modal open={open} onClose={onClose} data-testid="logout-modal">
<ModalWrapper width={560} height={292}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose(true);
}}
/>
</Header>
<Content>
<ContentTitle>{'Sign out?'}</ContentTitle>
<SignDes>Set up an AFFINE account to sync data</SignDes>
<ContentTitle>{t('Sign out')}?</ContentTitle>
<SignDes>
{blobDataSynced
? t('Set up an AFFiNE account to sync data')
: 'All data has been stored in the cloud'}
</SignDes>
<StyleTips>
{localCache ? (
<StyleCheck
@@ -43,28 +49,51 @@ export const LogoutModal = ({ open, onClose }: LoginModalProps) => {
<UnCheck></UnCheck>
</StyleCheck>
)}
Retain local cached data
{t('Retain local cached data')}
</StyleTips>
<div>
<Button
style={{ marginRight: '16px' }}
shape="round"
onClick={() => {
onClose(true);
}}
>
Wait for Sync
</Button>
<Button
type="danger"
shape="round"
onClick={() => {
onClose(false);
}}
>
Force Sign Out
</Button>
</div>
{blobDataSynced ? (
<div>
<Button
type="danger"
shape="round"
style={{ marginRight: '16px' }}
onClick={async () => {
onClose(false);
}}
>
{t('Force Sign Out')}
</Button>
<Button
shape="round"
onClick={() => {
onClose(true);
}}
>
{t('Wait for Sync')}
</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>
</ModalWrapper>
</Modal>
@@ -14,7 +14,6 @@ export const PageListEmpty = (props: { listType?: string }) => {
{listType === 'all' && <p>{t('emptyAllPages')}</p>}
{listType === 'favorite' && <p>{t('emptyFavourite')}</p>}
{listType === 'trash' && <p>{t('emptyTrash')}</p>}
<p>{t('still designed')}</p>
</div>
);
};
@@ -1,7 +1,7 @@
import { useConfirm } from '@/providers/ConfirmProvider';
import { PageMeta } from '@/providers/app-state-provider';
import { Menu, MenuItem } from '@/ui/menu';
import { Wrapper } from '@/ui/layout';
import { FlexWrapper } from '@/ui/layout';
import { IconButton } from '@/ui/button';
import {
MoreVerticalIcon,
@@ -63,13 +63,13 @@ export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
</>
);
return (
<Wrapper alignItems="center" justifyContent="center">
<FlexWrapper alignItems="center" justifyContent="center">
<Menu content={OperationMenu} placement="bottom-end" disablePortal={true}>
<IconButton darker={true}>
<MoreVerticalIcon />
</IconButton>
</Menu>
</Wrapper>
</FlexWrapper>
);
};
@@ -80,7 +80,7 @@ export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { confirm } = useConfirm();
const { t } = useTranslation();
return (
<Wrapper>
<FlexWrapper>
<IconButton
darker={true}
style={{ marginRight: '12px' }}
@@ -108,6 +108,6 @@ export const TrashOperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
>
<DeleteIcon />
</IconButton>
</Wrapper>
</FlexWrapper>
);
};
@@ -1,26 +1,23 @@
import React from 'react';
import { AddIcon } from '@blocksuite/icons';
import { StyledModalFooterContent } from './style';
import { useModal } from '@/providers/GlobalModalProvider';
import { Command } from 'cmdk';
import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from '@affine/i18n';
export const Footer = (props: { query: string }) => {
const { triggerQuickSearchModal } = useModal();
export const Footer = (props: { query: string; onClose: () => void }) => {
const { openPage, createPage } = usePageHelper();
const { t } = useTranslation();
const query = props.query;
const { query, onClose } = props;
return (
<Command.Item
data-testid="quickSearch-addNewPage"
onSelect={async () => {
onClose();
const pageId = await createPage({ title: query });
if (pageId) {
openPage(pageId);
}
triggerQuickSearchModal();
}}
>
<StyledModalFooterContent>
@@ -8,32 +8,41 @@ import React, {
import { SearchIcon } from '@blocksuite/icons';
import { StyledInputContent, StyledLabel } from './style';
import { Command } from 'cmdk';
import { useAppState } from '@/providers/app-state-provider';
import { useTranslation } from '@affine/i18n';
export const Input = (props: {
open: boolean;
query: string;
setQuery: Dispatch<SetStateAction<string>>;
setLoading: Dispatch<SetStateAction<boolean>>;
isPublic: boolean;
publishWorkspaceName: string | undefined;
}) => {
const { open, query, setQuery, setLoading, isPublic, publishWorkspaceName } =
props;
const [isComposition, setIsComposition] = useState(false);
const [inputValue, setInputValue] = useState('');
const inputRef = useRef<HTMLInputElement>(null);
const { currentWorkspace } = useAppState();
const { t } = useTranslation();
useEffect(() => {
inputRef.current?.addEventListener(
'blur',
() => {
inputRef.current?.focus();
},
true
);
return inputRef.current?.focus();
}, [inputRef]);
if (open) {
const inputElement = inputRef.current;
return inputElement?.focus();
}
}, [open]);
useEffect(() => {
return setInputValue(props.query);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const inputElement = inputRef.current;
if (!open) {
return;
}
const handleFocus = () => {
inputElement?.focus();
};
inputElement?.addEventListener('blur', handleFocus, true);
return () => inputElement?.removeEventListener('blur', handleFocus, true);
}, [inputRef, open]);
useEffect(() => {
setInputValue(query);
}, [query]);
return (
<StyledInputContent>
<StyledLabel htmlFor=":r5:">
@@ -46,18 +55,18 @@ export const Input = (props: {
setIsComposition(true);
}}
onCompositionEnd={e => {
props.setQuery(e.data);
setQuery(e.data);
setIsComposition(false);
if (!props.query) {
props.setLoading(true);
if (!query) {
setLoading(true);
}
}}
onValueChange={str => {
setInputValue(str);
if (!isComposition) {
props.setQuery(str);
if (!props.query) {
props.setLoading(true);
setQuery(str);
if (!query) {
setLoading(true);
}
}
}}
@@ -78,9 +87,9 @@ export const Input = (props: {
}
}}
placeholder={
currentWorkspace?.isPublish
isPublic
? t('Quick search placeholder2', {
workspace: currentWorkspace?.blocksuiteWorkspace?.meta.name,
workspace: publishWorkspaceName,
})
: t('Quick search placeholder')
}
@@ -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>
)
) : (
<></>
)}
</>
);
};
@@ -1,6 +1,5 @@
import { Command } from 'cmdk';
import { StyledListItem, StyledNotFound } from './style';
import { useModal } from '@/providers/GlobalModalProvider';
import { PaperIcon, EdgelessIcon } from '@blocksuite/icons';
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { useAppState } from '@/providers/app-state-provider';
@@ -12,14 +11,11 @@ import usePageHelper from '@/hooks/use-page-helper';
export const Results = (props: {
query: string;
loading: boolean;
onClose: () => void;
setLoading: Dispatch<SetStateAction<boolean>>;
setShowCreatePage: Dispatch<SetStateAction<boolean>>;
}) => {
const query = props.query;
const loading = props.loading;
const setLoading = props.setLoading;
const setShowCreatePage = props.setShowCreatePage;
const { triggerQuickSearchModal } = useModal();
const { query, loading, setLoading, setShowCreatePage, onClose } = props;
const { openPage } = usePageHelper();
const router = useRouter();
const { currentWorkspace, pageList } = useAppState();
@@ -55,8 +51,8 @@ export const Results = (props: {
<Command.Item
key={result.id}
onSelect={() => {
onClose();
openPage(result.id);
triggerQuickSearchModal();
}}
value={result.id}
>
@@ -86,8 +82,8 @@ export const Results = (props: {
key={link.title}
value={link.title}
onSelect={() => {
onClose();
router.push(link.href);
triggerQuickSearchModal();
}}
>
<StyledListItem>
@@ -1,5 +1,10 @@
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';
export const useSwitchToConfig = (
@@ -23,6 +28,13 @@ export const useSwitchToConfig = (
: '',
icon: FavouritesIcon,
},
{
title: t('Settings'),
href: currentWorkspaceId
? `/workspace/${currentWorkspaceId}/setting`
: '',
icon: SettingsIcon,
},
{
title: t('Trash'),
href: currentWorkspaceId ? `/workspace/${currentWorkspaceId}/trash` : '',
@@ -13,7 +13,8 @@ import { Command } from 'cmdk';
import { useEffect, useState } from 'react';
import { useModal } from '@/providers/GlobalModalProvider';
import { getUaHelper } from '@/utils';
import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
import { PublishedResults } from './PublishedResults';
type TransitionsModalProps = {
open: boolean;
onClose: () => void;
@@ -22,18 +23,30 @@ const isMac = () => {
return getUaHelper().isMacOs;
};
export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
const { currentWorkspace } = useAppState();
const router = useRouter();
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(true);
const [isPublic, setIsPublic] = useState(false);
const [publishWorkspaceName, setPublishWorkspaceName] = useState('');
const [showCreatePage, setShowCreatePage] = useState(true);
const { triggerQuickSearchModal } = useModal();
const isPublicAndNoQuery = () => {
return isPublic && query.length === 0;
};
const handleClose = () => {
setQuery('');
onClose();
};
// Add ‘⌘+K shortcut keys as switches
useEffect(() => {
if (router.pathname.startsWith('/404')) {
triggerQuickSearchModal(false);
return;
}
const down = (e: KeyboardEvent) => {
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
const selection = window.getSelection();
setQuery('');
if (selection?.toString()) {
triggerQuickSearchModal(false);
return;
@@ -43,18 +56,23 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
}
}
};
if (!open) {
setQuery('');
}
document.addEventListener('keydown', down, { capture: true });
return () =>
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 (
<Modal
open={open}
onClose={onClose}
onClose={handleClose}
wrapperPosition={['top', 'center']}
data-testid="quickSearch"
>
@@ -62,7 +80,7 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
width={620}
style={{
maxHeight: '80vh',
minHeight: '350px',
minHeight: isPublicAndNoQuery() ? '72px' : '350px',
top: '12vh',
}}
>
@@ -81,28 +99,51 @@ export const QuickSearch = ({ open, onClose }: TransitionsModalProps) => {
}}
>
<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>
</StyledModalHeader>
<StyledModalDivider />
<StyledModalDivider
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
/>
<Command.List>
<StyledContent>
<Results
query={query}
loading={loading}
setLoading={setLoading}
setShowCreatePage={setShowCreatePage}
/>
<StyledContent
style={{ display: isPublicAndNoQuery() ? 'none' : '' }}
>
{!isPublic ? (
<Results
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>
{currentWorkspace?.published ? (
<></>
) : showCreatePage ? (
<>
<StyledModalDivider />
<StyledModalFooter>
<Footer query={query} />
</StyledModalFooter>
</>
{!isPublic ? (
showCreatePage ? (
<>
<StyledModalDivider />
<StyledModalFooter>
<Footer query={query} onClose={handleClose} />
</StyledModalFooter>
</>
) : null
) : null}
</Command.List>
</Command>
@@ -9,6 +9,7 @@ export const StyledContent = styled('div')(({ theme }) => {
marginBottom: '10px',
...displayFlex('center', 'flex-start'),
color: theme.colors.popoverColor,
transition: 'all 0.15s',
letterSpacing: '0.06em',
'[cmdk-group-heading]': {
margin: '5px 16px',
@@ -16,7 +17,6 @@ export const StyledContent = styled('div')(({ theme }) => {
fontWeight: '500',
},
'[aria-selected="true"]': {
transition: 'background .15s, color .15s',
borderRadius: '5px',
color: theme.colors.primaryColor,
backgroundColor: theme.colors.hoverBackground,
@@ -103,6 +103,7 @@ export const StyledModalDivider = styled('div')(({ theme }) => {
margin: '6px 16px 6.5px 16px',
position: 'relative',
borderTop: `0.5px solid ${theme.colors.placeHolderColor}`,
transition: 'all 0.15s',
};
});
@@ -13,7 +13,7 @@ import {
useWindowsKeyboardShortcuts,
useWinMarkdownShortcuts,
} from '@/components/shortcuts-modal/config';
import Slide from '@mui/material/Slide';
import { MuiSlide } from '@/ui/mui';
import { ModalCloseButton } from '@/ui/modal';
import { getUaHelper } from '@/utils';
import { useTranslation } from '@affine/i18n';
@@ -40,7 +40,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
: windowsKeyboardShortcuts;
return createPortal(
<Slide direction="left" in={open} mountOnEnter unmountOnExit>
<MuiSlide direction="left" in={open} mountOnEnter unmountOnExit>
<StyledShortcutsModal data-testid="shortcuts-modal">
<>
<StyledModalHeader>
@@ -81,7 +81,7 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
})}
</>
</StyledShortcutsModal>
</Slide>,
</MuiSlide>,
document.body
);
};
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import type { WorkspaceUnit } from '@affine/datacenter';
import { WorkspaceAvatar as Avatar } from './Avatar';
const useAvatar = (workspaceUnit?: WorkspaceUnit) => {
const useAvatar = (workspaceUnit: WorkspaceUnit | null) => {
const [avatarUrl, setAvatarUrl] = useState('');
const avatarId =
workspaceUnit?.avatar || workspaceUnit?.blocksuiteWorkspace?.meta.avatar;
@@ -23,7 +23,7 @@ const useAvatar = (workspaceUnit?: WorkspaceUnit) => {
export const WorkspaceUnitAvatar = ({
size = 20,
name,
workspaceUnit,
workspaceUnit = null,
style,
}: {
size?: number;
@@ -31,7 +31,7 @@ export const WorkspaceUnitAvatar = ({
workspaceUnit?: WorkspaceUnit | null;
style?: React.CSSProperties;
}) => {
const avatarUrl = useAvatar(workspaceUnit || undefined);
const avatarUrl = useAvatar(workspaceUnit);
return (
<Avatar
size={size}
@@ -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>
);
};
@@ -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',
}));
@@ -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>
);
};
@@ -70,7 +70,7 @@ export const CloudIcon = () => {
<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"
stroke="#60A5FA"
stroke-width="1.25"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
@@ -1,28 +1,34 @@
import { styled } from '@/styles';
import { Modal, ModalWrapper } from '@/ui/modal';
import { Button, IconButton } from '@/ui/button';
import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { FlexWrapper } from '@/ui/layout';
import { useState } from 'react';
import { CreateWorkspaceModal } from '../create-workspace';
import {
UsersIcon,
AddIcon,
LogOutIcon,
CloudInsyncIcon,
PublishIcon,
CloseIcon,
} from '@blocksuite/icons';
import {
WorkspaceAvatar,
WorkspaceUnitAvatar,
} from '@/components/workspace-avatar';
import { Tooltip } from '@/ui/tooltip';
import { AddIcon, HelpCenterIcon } from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router';
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 { 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 {
open: boolean;
onClose: () => void;
@@ -30,337 +36,117 @@ interface WorkspaceModalProps {
export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => {
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const { workspaceList, currentWorkspace, user, logout, isOwner } =
useAppState();
const { logout, dataCenter } = useAppState();
const router = useRouter();
const { t } = useTranslation();
const [loginOpen, setLoginOpen] = useState(false);
const [logoutOpen, setLogoutOpen] = useState(false);
return (
<div>
<>
<Modal open={open} onClose={onClose}>
<ModalWrapper
width={720}
height={690}
style={{
padding: '24px 40px',
display: 'flex',
flexDirection: 'column',
}}
>
<Header>
<ContentTitle>{t('My Workspaces')}</ContentTitle>
<HeaderOption>
<LanguageMenu />
<div
style={{
display: 'inline-block',
border: 'none',
margin: '2px 16px',
height: '24px',
position: 'relative',
top: '4px',
}}
<StyledModalHeader>
<StyledModalHeaderLeft>
<StyledModalTitle>{t('My Workspaces')}</StyledModalTitle>
<Tooltip
content={t('Workspace description')}
placement="top-start"
disablePortal={true}
>
<LineIcon></LineIcon>
</div>
<StyledHelperContainer>
<HelpCenterIcon />
</StyledHelperContainer>
</Tooltip>
</StyledModalHeaderLeft>
<Button
style={{ border: 'none', padding: 0 }}
<StyledOperationWrapper>
<LanguageMenu />
<StyledSplitLine />
<ModalCloseButton
onClick={() => {
onClose();
}}
>
<CloseIcon></CloseIcon>
</Button>
</HeaderOption>
</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>
absolute={false}
/>
</StyledOperationWrapper>
</StyledModalHeader>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>
{item.name || 'AFFiNE'}
</StyleWorkspaceTitle>
{isOwner ? (
item.provider === 'local' ? (
<p>
<LocalIcon />
Local Workspace
</p>
) : (
<p>
<CloudIcon />
Cloud Workspace
</p>
)
) : (
<p>
<UsersIcon fontSize={20} color={'#FF646B'} />
Joined Workspace
</p>
)}
{item.provider === 'local' && (
<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>
<StyledModalContent>
{dataCenter.workspaces.map((item, index) => {
return (
<WorkspaceCard
workspaceData={item}
onClick={workspaceData => {
router.replace(`/workspace/${workspaceData.id}`);
onClose();
}}
key={index}
></WorkspaceCard>
);
})}
<StyledCard
onClick={() => {
setCreateWorkspaceOpen(true);
}}
>
<FlexWrapper>
<StyleWorkspaceAdd className="add-icon">
<AddIcon fontSize={18} />
</StyleWorkspaceAdd>
</FlexWrapper>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>New workspace</StyleWorkspaceTitle>
<p>Crete or import</p>
</StyleWorkspaceInfo>
</WorkspaceItem>
</WorkspaceList>
{/* <p style={{ fontSize: '14px', color: '#ccc', margin: '12px 0' }}>
{t('Tips')}
{t('Workspace description')}
</p> */}
</Content>
<LoginModal
open={loginOpen}
onClose={() => {
setLoginOpen(false);
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>{t('New Workspace')}</StyleWorkspaceTitle>
<p>{t('Create Or Import')}</p>
</StyleWorkspaceInfo>
</StyledCard>
</StyledModalContent>
<Footer
onLogin={() => {
setLoginOpen(true);
}}
></LoginModal>
<Footer>
{!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);
onLogout={() => {
setLogoutOpen(true);
}}
></CreateWorkspaceModal>
<LogoutModal
open={logoutOpen}
onClose={async wait => {
if (!wait) {
await logout();
router.replace(`/workspace`);
}
setLogoutOpen(false);
}}
></LogoutModal>
/>
</ModalWrapper>
</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,
},
},
};
});
@@ -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,
}));
@@ -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 {
...displayFlex('center', 'center'),
fontSize: '20px',
fontWeight: 500,
marginTop: '60px',
lineHeight: 1,
width: '1px',
height: '20px',
background: theme.colors.iconColor,
marginRight: '24px',
};
});
export const StyledContent = styled.div(() => {
export const StyleWorkspaceInfo = styled.div(({ theme }) => {
return {
padding: '0 40px',
marginTop: '32px',
fontSize: '18px',
lineHeight: '25px',
'p:not(last-of-type)': {
marginBottom: '10px',
marginLeft: '15px',
p: {
height: '20px',
fontSize: theme.font.xs,
...displayFlex('flex-start', 'center'),
},
svg: {
marginRight: '10px',
},
};
});
export const StyledButton = styled.div(({ theme }) => {
export const StyleWorkspaceTitle = styled.div(({ theme }) => {
return {
width: '146px',
height: '42px',
background: theme.colors.primaryColor,
color: '#FFFFFF',
fontSize: '18px',
fontWeight: 500,
borderRadius: '21px',
margin: '52px auto 0',
fontSize: theme.font.base,
fontWeight: 600,
lineHeight: '24px',
marginBottom: '10px',
maxWidth: '200px',
...textEllipsis(1),
};
});
export const StyledCard = styled.div<{
active?: boolean;
}>(({ theme, active }) => {
const borderColor = active ? theme.colors.primaryColor : 'transparent';
return {
width: '310px',
height: '124px',
cursor: 'pointer',
padding: '16px',
boxShadow: '0px 0px 8px rgba(0, 0, 0, 0.1)',
borderRadius: '12px',
border: `1px solid ${borderColor}`,
...displayFlex('flex-start', 'flex-start'),
marginBottom: '24px',
':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'),
};
});
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'),
},
};
});
@@ -1,24 +1,16 @@
import { styled } from '@/styles';
import { WorkspaceUnit } from '@affine/datacenter';
import { Trans } from '@affine/i18n';
export const ExportPageTitleContainer = styled('div')(() => {
return {
display: 'flex',
fontWeight: '500',
flex: 1,
};
});
import { useTranslation } from '@affine/i18n';
import { Wrapper } from '@/ui/layout';
import { Button } from '@/ui/button';
export const ExportPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { t } = useTranslation();
console.log(workspace);
return (
<ExportPageTitleContainer>
<Trans i18nKey="Export Workspace">
Export Workspace
<code style={{ margin: '0 10px' }}>
{{ workspace: workspace.name }}
</code>
Is Comming
</Trans>
</ExportPageTitleContainer>
<>
<Wrapper marginBottom="32px"> {t('Export Description')}</Wrapper>
<Button type="light" shape="circle" disabled>
{t('Export AFFINE backup file')}
</Button>
</>
);
};
@@ -1,20 +1,12 @@
import {
StyledCopyButtonContainer,
StyledPublishContent,
StyledPublishCopyContainer,
StyledPublishExplanation,
StyledSettingH2,
StyledStopPublishContainer,
} from './style';
import { useState } from 'react';
import { Button } from '@/ui/button';
import Input from '@/ui/input';
import { toast } from '@/ui/toast';
// import { useAppState } from '@/providers/app-state-provider3';
import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '../enable-workspace';
import { Wrapper, Content, FlexWrapper } from '@/ui/layout';
export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const shareUrl = window.location.host + '/public-workspace/' + workspace.id;
const { publishWorkspace } = useWorkspaceHelper();
@@ -23,95 +15,75 @@ export const PublishPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const togglePublic = async (flag: boolean) => {
try {
await publishWorkspace(workspace.id.toString(), flag);
setLoaded(false);
} catch (e) {
toast('Failed to publish workspace');
toast(t('Failed to publish workspace'));
}
};
const copyUrl = () => {
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 (
<>
{workspace.provider === 'affine' ? (
<div
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>
)}
<Wrapper marginBottom="32px">{t('Publishing')}</Wrapper>
<EnableWorkspaceButton />
</>
);
};
@@ -1,84 +1,106 @@
import {
StyleAsync,
StyledPublishContent,
StyledPublishExplanation,
StyledWorkspaceName,
StyledWorkspaceType,
StyledEmail,
// StyledDownloadCard,
// StyledDownloadCardDes,
} from './style';
import { DownloadIcon } from '@blocksuite/icons';
import { Button } from '@/ui/button';
import { Menu, MenuItem } from '@/ui/menu';
import { WorkspaceUnit } from '@affine/datacenter';
import { Trans, useTranslation } from '@affine/i18n';
import { useTranslation, Trans } from '@affine/i18n';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
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 }) => {
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 (
<div>
<StyledPublishContent>
{workspace.provider === 'local' ? (
<>
<StyledPublishExplanation>
<WorkspaceUnitAvatar
size={32}
name={workspace.name}
workspaceUnit={workspace}
style={{ marginRight: '12px' }}
/>
<StyledWorkspaceName>{workspace.name};</StyledWorkspaceName>
<StyledWorkspaceType>is a Local Workspace.</StyledWorkspaceType>
</StyledPublishExplanation>
<StyledWorkspaceType>
All data is stored on the current device. You can enable AFFiNE
Cloud for this workspace to keep data in sync with the cloud.
</StyledWorkspaceType>
<StyleAsync>
<EnableWorkspaceButton></EnableWorkspaceButton>
</StyleAsync>
</>
) : (
<>
<StyledPublishExplanation>
<Trans i18nKey="Sync Description2">
<code>{{ workspaceName: workspace.name ?? 'Affine' }}</code>
is Cloud Workspace. All data will be synchronised and saved to
the AFFiNE
</Trans>
</StyledPublishExplanation>
<StyleAsync>
<Menu
content={
<>
<MenuItem
onClick={() => {
// deleteMember(workspace.id, 0);
}}
icon={<DownloadIcon />}
>
{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>
<>
<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 Cloud Workspace')}</Content>
</FlexWrapper>
<Trans i18nKey="Cloud Workspace Description">
All data will be synchronised and saved to the AFFiNE account
<StyledEmail>
{{
email: '{' + user?.email + '}.',
}}
</StyledEmail>
</Trans>
{/*<Wrapper marginBottom="12px" marginTop="32px">*/}
{/* <Content weight="500">{t('Data sync mode')}</Content>*/}
{/*</Wrapper>*/}
{/*<FlexWrapper>*/}
{/* <StyledDownloadCard>*/}
{/* <ActiveIcon />*/}
{/* <Wrapper>{t('Download all data')}</Wrapper>*/}
{/* <StyledDownloadCardDes>*/}
{/* {t('It takes up more space on your device.')}*/}
{/* </StyledDownloadCardDes>*/}
{/* </StyledDownloadCard>*/}
{/* <StyledDownloadCard active>*/}
{/* <ActiveIcon />*/}
{/* <Wrapper>{t('Download core data')}</Wrapper>*/}
{/* <StyledDownloadCardDes>*/}
{/* {t('It takes up little space on your device.')}*/}
{/* </StyledDownloadCardDes>*/}
{/* </StyledDownloadCard>*/}
{/*</FlexWrapper>*/}
</>
);
};
@@ -1,23 +1,13 @@
import {
StyledDeleteButtonContainer,
// StyledSettingAvatar,
StyledSettingAvatarContent,
StyledSettingInputContainer,
StyledDoneButtonContainer,
StyledInput,
StyledProviderInfo,
StyleGeneral,
StyledAvatar,
} from './style';
import { StyledSettingH2 } from '../style';
import { StyledInput, StyledProviderInfo, StyledAvatar } from './style';
import { StyledSettingKey, StyledRow } from '../style';
import { FlexWrapper, Content } from '@/ui/layout';
import { useState } from 'react';
import { Button } from '@/ui/button';
import { useAppState } from '@/providers/app-state-provider';
import { WorkspaceDelete } from './delete';
import { WorkspaceLeave } from './leave';
import { DoneIcon, UsersIcon } from '@blocksuite/icons';
// import { Upload } from '@/components/file-upload';
import { UsersIcon } from '@blocksuite/icons';
import { WorkspaceUnitAvatar } from '@/components/workspace-avatar';
import { WorkspaceUnit } from '@affine/datacenter';
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
@@ -33,9 +23,6 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const { updateWorkspace } = useWorkspaceHelper();
const { t } = useTranslation();
const handleChangeWorkSpaceName = (newName: string) => {
setWorkspaceName(newName);
};
const handleUpdateWorkspaceName = () => {
currentWorkspace &&
updateWorkspace({ name: workspaceName }, currentWorkspace);
@@ -47,83 +34,105 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
(await updateWorkspace({ avatarBlob: blob }, currentWorkspace));
};
return workspace ? (
<StyleGeneral>
<div style={{ flex: 1, overflow: 'auto' }}>
<StyledSettingH2>Workspace Avatar</StyledSettingH2>
<StyledSettingAvatarContent>
<StyledAvatar>
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange}
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
</div>
<WorkspaceUnitAvatar
size={60}
name={workspace.name}
workspaceUnit={workspace}
/>
</>
</Upload>
</StyledAvatar>
{/* TODO: Wait for image sync to complete */}
{/* <Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange}
>
<Button loading={uploading}>{t('Upload')}</Button>
</Upload> */}
{/* TODO: add upload logic */}
</StyledSettingAvatarContent>
<StyledSettingH2 marginTop={20}>{t('Workspace Name')}</StyledSettingH2>
<StyledSettingInputContainer>
if (!workspace) {
return null;
}
return (
<>
<StyledRow>
<StyledSettingKey>{t('Workspace Avatar')}</StyledSettingKey>
<StyledAvatar>
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={fileChange}
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
</div>
<WorkspaceUnitAvatar
size={72}
name={workspace.name}
workspaceUnit={workspace}
/>
</>
</Upload>
</StyledAvatar>
</StyledRow>
<StyledRow>
<StyledSettingKey>{t('Workspace Name')}</StyledSettingKey>
<FlexWrapper>
<StyledInput
width={284}
height={32}
height={38}
value={workspaceName}
placeholder={t('Workspace Name')}
maxLength={14}
minLength={1}
maxLength={15}
minLength={0}
disabled={!isOwner}
onChange={handleChangeWorkSpaceName}
onChange={newName => {
setWorkspaceName(newName);
}}
></StyledInput>
{isOwner ? (
<StyledDoneButtonContainer
onClick={() => {
handleUpdateWorkspaceName();
}}
>
<DoneIcon />
</StyledDoneButtonContainer>
) : null}
</StyledSettingInputContainer>
<StyledSettingH2 marginTop={20}>{t('Workspace Type')}</StyledSettingH2>
<StyledSettingInputContainer>
{isOwner && (
<>
<Button
type="default"
shape="circle"
style={{ marginLeft: '24px' }}
onClick={() => {
setWorkspaceName(workspace.name);
}}
>
{t('Cancel')}
</Button>
<Button
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
onClick={() => {
handleUpdateWorkspaceName();
}}
>
{t('Confirm')}
</Button>
</>
)}
</FlexWrapper>
</StyledRow>
<StyledRow>
<StyledSettingKey>{t('Workspace Type')}</StyledSettingKey>
<FlexWrapper>
{isOwner ? (
currentWorkspace?.provider === 'local' ? (
<StyledProviderInfo>
<FlexWrapper alignItems="center">
<LocalIcon />
Local Workspace
</StyledProviderInfo>
<Content style={{ marginLeft: '15px' }}>
{t('Local Workspace')}
</Content>
</FlexWrapper>
) : (
<StyledProviderInfo>
<FlexWrapper alignItems="center">
<CloudIcon />
All data can be accessed offline
</StyledProviderInfo>
<Content style={{ marginLeft: '15px' }}>
{t('Available Offline')}
</Content>
</FlexWrapper>
)
) : (
<StyledProviderInfo>
<UsersIcon fontSize={20} color={'#FF646B'} />
Joined Workspace
{t('Joined Workspace')}
</StyledProviderInfo>
)}
</StyledSettingInputContainer>
</div>
</FlexWrapper>
</StyledRow>
<StyledDeleteButtonContainer>
<StyledRow>
<StyledSettingKey> {t('Delete Workspace')}</StyledSettingKey>
{isOwner ? (
<>
<Button
@@ -163,7 +172,7 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
/>
</>
)}
</StyledDeleteButtonContainer>
</StyleGeneral>
) : null;
</StyledRow>
</>
);
};
@@ -1,3 +1,4 @@
import { useRouter } from 'next/router';
import Modal from '@/ui/modal';
import Input from '@/ui/input';
import {
@@ -11,7 +12,6 @@ import {
import { useState } from 'react';
import { ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import { useRouter } from 'next/router';
import { WorkspaceUnit } from '@affine/datacenter';
import { Trans, useTranslation } from '@affine/i18n';
@@ -1,40 +1,6 @@
import { displayFlex, styled } from '@/styles';
import MuiAvatar from '@mui/material/Avatar';
import IconButton from '@/ui/button/IconButton';
import { styled } from '@/styles';
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 }) => {
return {
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 }) => {
return {
color: theme.colors.iconColor,
fontSize: theme.font.sm,
svg: {
verticalAlign: 'sub',
marginRight: '10px',
@@ -71,7 +22,6 @@ export const StyledProviderInfo = styled('p')(({ theme }) => {
export const StyledAvatar = styled('div')(() => {
return {
position: 'relative',
marginRight: '20px',
cursor: 'pointer',
':hover': {
'.camera-icon': {
@@ -4,7 +4,7 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
import { Button } from '@/ui/button';
import Input from '@/ui/input';
import { useState } from 'react';
import { Avatar } from '@mui/material';
import { MuiAvatar } from '@/ui/mui';
import useMembers from '@/hooks/use-members';
import { User } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
@@ -76,10 +76,9 @@ export const InviteMemberModal = ({
<ModalWrapper width={460} height={236}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
setEmail('');
}}
/>
</Header>
@@ -102,7 +101,7 @@ export const InviteMemberModal = ({
) : (
<Member>
{userData?.avatar ? (
<Avatar src={userData?.avatar}></Avatar>
<MuiAvatar src={userData?.avatar}></MuiAvatar>
) : (
<MemberIcon>
<EmailIcon></EmailIcon>
@@ -125,6 +124,7 @@ export const InviteMemberModal = ({
style={{ width: '364px', height: '38px', borderRadius: '40px' }}
onClick={async () => {
await inviteMember(email);
setEmail('');
onInviteSuccess();
}}
>
@@ -10,10 +10,9 @@ import {
StyledMemberRoleContainer,
StyledMemberTitleContainer,
StyledMoreVerticalButton,
StyledPublishExplanation,
StyledMemberWarp,
StyledMemberContainer,
} from './style';
import { Wrapper } from '@/ui/layout';
import { MoreVerticalIcon, EmailIcon, TrashIcon } from '@blocksuite/icons';
import { useState } from 'react';
import { Button, IconButton } from '@/ui/button';
@@ -25,14 +24,13 @@ import { useConfirm } from '@/providers/ConfirmProvider';
import { toast } from '@/ui/toast';
import useMembers from '@/hooks/use-members';
import Loading from '@/components/loading';
import { Wrapper } from '@/ui/layout';
import { FlexWrapper } from '@/ui/layout';
import { useTranslation } from '@affine/i18n';
import { EnableWorkspaceButton } from '@/components/enable-workspace';
export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
const { members, removeMember, loaded } = useMembers();
const { t } = useTranslation();
const { confirm } = useConfirm();
@@ -41,9 +39,9 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
<StyledMemberContainer>
<StyledMemberListContainer>
{!loaded && (
<Wrapper justifyContent="center">
<FlexWrapper justifyContent="center">
<Loading size={25} />
</Wrapper>
</FlexWrapper>
)}
{loaded && members.length === 0 && (
<Empty width={648} sx={{ marginTop: '60px' }} height={300} />
@@ -89,42 +87,50 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
<StyledMemberRoleContainer>
{member.accepted
? member.type !== 99
? 'Member'
: 'Workspace Owner'
: 'Pending'}
? t('Member')
: t('Owner')
: t('Pending')}
</StyledMemberRoleContainer>
<StyledMoreVerticalButton>
<Menu
content={
<>
<MenuItem
onClick={async () => {
const confirmRemove = await confirm({
title: 'Delete Member?',
content: `will delete member`,
confirmText: 'Delete',
confirmType: 'danger',
});
{member.type === 99 ? (
<></>
) : (
<Menu
content={
<>
<MenuItem
onClick={async () => {
const confirmRemove = await confirm({
title: t('Delete Member?'),
content: t('will delete member'),
confirmText: t('Delete'),
confirmType: 'danger',
});
if (!confirmRemove) {
return;
}
await removeMember(member.id);
toast(`${user.name} has been removed`);
}}
icon={<TrashIcon />}
>
Delete
</MenuItem>
</>
}
placement="bottom-end"
disablePortal={true}
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
if (!confirmRemove) {
return;
}
await removeMember(member.id);
toast(
t('Member has been removed', {
name: user.name,
})
);
}}
icon={<TrashIcon />}
>
{t('Delete')}
</MenuItem>
</>
}
placement="bottom-end"
disablePortal={true}
>
<IconButton>
<MoreVerticalIcon />
</IconButton>
</Menu>
)}
</StyledMoreVerticalButton>
</StyledMemberListItem>
);
@@ -159,11 +165,14 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => {
}
return (
<StyledMemberWarp>
{t('Collaboration Description')}
<StyledPublishExplanation>
<EnableWorkspaceButton></EnableWorkspaceButton>
</StyledPublishExplanation>
</StyledMemberWarp>
<Wrapper
style={{
fontWeight: '500',
fontSize: '18px',
}}
>
<Wrapper marginBottom="32px">{t('Collaboration Description')}</Wrapper>
<EnableWorkspaceButton />
</Wrapper>
);
};
@@ -1,5 +1,5 @@
import { styled } from '@/styles';
import MuiAvatar from '@mui/material/Avatar';
import { MuiAvatar } from '@/ui/mui';
export const StyledMemberTitleContainer = styled('li')(() => {
return {
@@ -97,24 +97,3 @@ export const StyledMoreVerticalButton = styled('button')(() => {
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',
};
});
@@ -1,5 +1,5 @@
import { styled } from '@/styles';
import { FlexWrapper } from '@/ui/layout';
export const StyledSettingContainer = styled('div')(() => {
return {
display: 'flex',
@@ -13,41 +13,19 @@ export const StyledSettingContainer = styled('div')(() => {
export const StyledSettingSidebar = styled('div')(() => {
{
return {
// height: '48px',
marginTop: '50px',
marginTop: '48px',
};
}
});
export const StyledSettingContent = styled('div')(() => {
return {
overflow: 'hidden',
overflow: 'auto',
flex: 1,
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')(() => {
{
return {
@@ -65,8 +43,8 @@ export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
height: '34px',
color: isActive ? theme.colors.primaryColor : theme.colors.textColor,
fontWeight: '500',
fontSize: theme.font.base,
lineHeight: theme.font.lineHeightBase,
fontSize: theme.font.h6,
lineHeight: theme.font.lineHeight,
cursor: 'pointer',
transition: 'all 0.15s ease',
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 {
display: 'flex',
alignItems: 'center',
};
});
export const StyledSettingH2 = styled('h2')<{
marginTop?: number;
marginBottom?: number;
}>(({ marginTop, marginBottom, theme }) => {
return {
fontWeight: '500',
width: '140px',
textAlign: 'right',
fontSize: theme.font.base,
lineHeight: theme.font.lineHeightBase,
marginTop: marginTop ? `${marginTop}px` : '0px',
marginBottom: marginBottom ? `${marginBottom}px` : '0px',
fontWeight: 500,
marginRight: '56px',
flexShrink: 0,
};
});
export const StyledRow = styled(FlexWrapper)(() => {
return {
marginBottom: '32px',
};
});
export const StyledPublishExplanation = styled('div')(() => {
return {
paddingRight: '48px',
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
marginBottom: '22px',
};
});
export const StyledWorkspaceName = styled('span')(() => {
export const StyledWorkspaceName = styled('span')(({ theme }) => {
return {
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
};
});
export const StyledWorkspaceType = styled('span')(() => {
return {
fontWeight: '500',
fontSize: '18px',
lineHeight: '26px',
fontSize: theme.font.h6,
};
});
export const StyledPublishCopyContainer = styled('div')(() => {
export const StyledEmail = styled('span')(() => {
return {
marginTop: '12px',
marginBottom: '20px',
paddingTop: '20px',
flex: 1,
};
});
export const StyledStopPublishContainer = styled('div')(() => {
return {
textAlign: 'center',
color: '#E8178A',
};
});
export const StyledCopyButtonContainer = styled('div')(() => {
return {
marginTop: '64px',
};
});
export const StyledPublishContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
};
});
export const StyleAsync = styled('div')(() => {
return {
marginTop: '64px',
};
});
// export const StyledDownloadCard = styled.div<{ active?: boolean }>(
// ({ theme, active }) => {
// return {
// width: '240px',
// height: '86px',
// border: '1px solid',
// borderColor: active
// ? theme.colors.primaryColor
// : theme.colors.borderColor,
// borderRadius: '10px',
// padding: '8px 12px',
// position: 'relative',
// ':not(:last-of-type)': {
// marginRight: '24px',
// },
// 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,
// };
// });
@@ -1,4 +1,4 @@
import MuiAvatar from '@mui/material/Avatar';
import { MuiAvatar } from '@/ui/mui';
import { styled } from '@/styles';
export const WorkspaceItemWrapper = styled('div')(({ theme }) => ({
@@ -32,13 +32,13 @@ export const WorkspaceSelector = () => {
>
<WorkspaceUnitAvatar
size={28}
name={currentWorkspace?.name ?? 'AFFiNE'}
name={currentWorkspace?.name ?? 'AFFiNE Test'}
workspaceUnit={currentWorkspace}
/>
</div>
</Avatar>
<WorkspaceName data-testid="workspace-name">
{currentWorkspace?.name ?? 'AFFiNE'}
{currentWorkspace?.name ?? 'AFFiNE Test'}
</WorkspaceName>
</SelectorWrapper>
<WorkspaceModal
@@ -1,4 +1,4 @@
import MuiAvatar from '@mui/material/Avatar';
import { MuiAvatar } from '@/ui/mui';
import { styled } from '@/styles';
import { StyledPopperContainer } from '@/ui/shared/Container';
@@ -11,18 +11,17 @@ import {
StyledSubListItem,
} from './style';
import { Arrow } from './icons';
import Collapse from '@mui/material/Collapse';
import {
ArrowDownIcon,
SearchIcon,
AllPagesIcon,
FavouritesIcon,
ImportIcon,
TrashIcon,
AddIcon,
SettingsIcon,
} from '@blocksuite/icons';
import Link from 'next/link';
import { MuiCollapse } from '@/ui/mui';
import { Tooltip } from '@/ui/tooltip';
import { useModal } from '@/providers/GlobalModalProvider';
import { useAppState } from '@/providers/app-state-provider';
@@ -39,7 +38,7 @@ const FavoriteList = ({ showList }: { showList: boolean }) => {
const { t } = useTranslation();
const favoriteList = pageList.filter(p => p.favorite && !p.trash);
return (
<Collapse in={showList}>
<MuiCollapse in={showList}>
{favoriteList.map((pageMeta, index) => {
const active = router.query.pageId === pageMeta.id;
return (
@@ -61,11 +60,11 @@ const FavoriteList = ({ showList }: { showList: boolean }) => {
{favoriteList.length === 0 && (
<StyledSubListItem disable={true}>{t('No item')}</StyledSubListItem>
)}
</Collapse>
</MuiCollapse>
);
};
export const WorkSpaceSliderBar = () => {
const { triggerQuickSearchModal, triggerImportModal } = useModal();
const { triggerQuickSearchModal } = useModal();
const [showSubFavorite, setShowSubFavorite] = useState(true);
const { currentWorkspace } = useAppState();
const { openPage, createPage } = usePageHelper();
@@ -161,14 +160,14 @@ export const WorkSpaceSliderBar = () => {
setShowWorkspaceSetting(false);
}}
/> */}
<StyledListItem
{/* TODO: will finish the feature next version */}
{/* <StyledListItem
onClick={() => {
triggerImportModal();
}}
>
<ImportIcon /> {t('Import')}
</StyledListItem>
</StyledListItem> */}
<Link href={{ pathname: paths.trash }}>
<StyledListItem active={router.asPath === paths.trash}>
@@ -4,9 +4,9 @@ import Link from 'next/link';
export const StyledSliderBar = styled.div<{ show: boolean }>(
({ theme, show }) => {
return {
width: show ? '320px' : '0',
width: show ? '256px' : '0',
height: '100vh',
background: theme.mode === 'dark' ? '#272727' : '#FBFBFC',
background: theme.mode === 'dark' ? '#272727' : '#F9F9FB',
boxShadow: theme.shadow.modal,
transition: 'width .15s, padding .15s',
position: 'relative',
@@ -58,7 +58,6 @@ export const StyledListItem = styled.div<{
width: '296px',
height: '32px',
marginTop: '12px',
fontSize: theme.font.sm,
color: active ? theme.colors.primaryColor : theme.colors.popoverColor,
paddingLeft: '12px',
borderRadius: '5px',
@@ -122,9 +121,8 @@ export const StyledSubListItem = styled.button<{
width: '296px',
height: '32px',
marginTop: '4px',
fontSize: theme.font.sm,
color: disable
? theme.colors.iconColor
? theme.colors.disableColor
: active
? theme.colors.primaryColor
: theme.colors.popoverColor,
@@ -38,7 +38,7 @@ export const useEnsureWorkspace = () => {
// }
const workspaceId =
(router.query.workspaceId as string) || workspaceList[0]?.id;
loadWorkspace(workspaceId).finally(() => {
loadWorkspace.current(workspaceId).finally(() => {
setWorkspaceLoaded(true);
setActiveWorkspaceId(activeWorkspaceId);
});
+15 -5
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 { PageMeta, useAppState } from '@/providers/app-state-provider';
import { EditorContainer } from '@blocksuite/editor';
@@ -20,7 +20,10 @@ export type EditorHandlers = {
toggleDeletePage: (pageId: string) => Promise<boolean>;
toggleFavoritePage: (pageId: string) => Promise<boolean>;
permanentlyDeletePage: (pageId: string) => void;
search: (query: QueryContent) => Map<string, string | undefined>;
search: (
query: QueryContent,
workspace?: Workspace
) => Map<string, string | undefined>;
// changeEditorMode: (pageId: string) => void;
changePageMode: (
pageId: string,
@@ -83,9 +86,16 @@ export const usePageHelper = (): EditorHandlers => {
});
return trash;
},
search: (query: QueryContent) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return currentWorkspace!.blocksuiteWorkspace!.search(query);
search: (query: QueryContent, workspace?: Workspace) => {
if (workspace) {
return workspace.search(query);
}
if (currentWorkspace) {
if (currentWorkspace.blocksuiteWorkspace) {
return currentWorkspace.blocksuiteWorkspace.search(query);
}
}
return new Map();
},
changePageMode: async (pageId, mode) => {
const pageMeta = getPageMeta(currentWorkspace, pageId);
+3 -30
View File
@@ -1,12 +1,8 @@
import { useAppState } from '@/providers/app-state-provider';
import { useConfirm } from '@/providers/ConfirmProvider';
import { toast } from '@/ui/toast';
import { WorkspaceUnit } from '@affine/datacenter';
import router from 'next/router';
export const useWorkspaceHelper = () => {
const { confirm } = useConfirm();
const { dataCenter, currentWorkspace, user, login } = useAppState();
const { dataCenter, currentWorkspace } = useAppState();
const createWorkspace = async (name: string) => {
const workspaceInfo = await dataCenter.createWorkspace({
name: name,
@@ -31,33 +27,11 @@ export const useWorkspaceHelper = () => {
await dataCenter.updateWorkspaceMeta({ name }, workspace);
}
if (avatarBlob) {
// const blobId = await dataCenter.setBlob(workspace, avatarBlob);
// await dataCenter.updateWorkspaceMeta({ avatar: blobId }, workspace);
const blobId = await dataCenter.setBlob(workspace, avatarBlob);
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 () => {
currentWorkspace && (await dataCenter.deleteWorkspace(currentWorkspace.id));
};
@@ -77,7 +51,6 @@ export const useWorkspaceHelper = () => {
createWorkspace,
publishWorkspace,
updateWorkspace,
enableWorkspace,
deleteWorkSpace,
leaveWorkSpace,
acceptInvite,
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
@@ -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>
);
};
+7 -2
View File
@@ -67,15 +67,20 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => {
};
const AppDefender = ({ children }: PropsWithChildren) => {
const { synced } = useAppState();
const router = useRouter();
const { synced } = useAppState();
useEffect(() => {
if (router.pathname === '/') {
if (router.asPath === '/') {
router.replace('/workspace');
}
}, [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>;
};
+3 -2
View File
@@ -36,7 +36,6 @@ const Affine = () => {
<div>hi</div>
</Modal>
<Loading />
<Button
icon={<FavouritedIcon />}
onClick={() => {
@@ -48,13 +47,15 @@ const Affine = () => {
<Button icon={<FavouritedIcon />} type={'primary'}>
click me!
</Button>
<Button icon={<FavouritedIcon />} type={'light'}>
click me!
</Button>
<Button icon={<FavouritedIcon />} type={'warning'}>
click me!
</Button>
<Button icon={<FavouritedIcon />} type={'danger'}>
click me!
</Button>
<Button icon={<FavouritedIcon />}></Button>
<Button icon={<FavouritedIcon />} shape="round"></Button>
<Button loading={true}></Button>
@@ -1,8 +0,0 @@
import type { NextPage } from 'next';
import { ConfirmInvitation } from '../pages-content/ConfirmInvitationPage';
const ConfirmInvitationPage: NextPage = () => {
return <ConfirmInvitation />;
};
export default ConfirmInvitationPage;
@@ -1,9 +1,9 @@
import { useWorkspaceHelper } from '@/hooks/use-workspace-helper';
import { styled } from '@/styles';
import { Empty } from '@/ui/empty';
// import { Avatar } from '@mui/material';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { PageLoading } from '@/components/loading';
// const User = ({ name, avatar }: { name: string; avatar?: string }) => {
// return (
@@ -19,19 +19,29 @@ import { useEffect, useState } from 'react';
// };
export default function DevPage() {
const [loading, setLoading] = useState(true);
const router = useRouter();
const [successInvited, setSuccessInvited] = useState<boolean>(false);
const { acceptInvite } = useWorkspaceHelper();
useEffect(() => {
router.query.invite_code &&
acceptInvite(router.query.invite_code as string).then(data => {
if (data && data.accepted) {
setSuccessInvited(true);
}
});
if (router.query.invite_code) {
acceptInvite(router.query.invite_code as string)
.then(data => {
if (data && data.accepted) {
setSuccessInvited(true);
}
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
}, [router, acceptInvite]);
return (
return loading ? (
<PageLoading />
) : (
<Invited>
<div>
<Empty width={310} height={310}></Empty>
@@ -1,11 +1,17 @@
import { ReactElement, useEffect, useState } from 'react';
import { useAppState } from '@/providers/app-state-provider';
import type { NextPageWithLayout } from '../..//_app';
import { styled } from '@/styles';
import { displayFlex, styled } from '@/styles';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import { Page as PageStore, Workspace } from '@blocksuite/store';
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'), {
ssr: false,
@@ -16,12 +22,16 @@ const Page: NextPageWithLayout = () => {
const { dataCenter } = useAppState();
const router = useRouter();
const [loaded, setLoaded] = useState(false);
const [workspaceName, setWorkspaceName] = useState('');
const [pageTitle, setPageTitle] = useState('');
const { triggerQuickSearchModal } = useModal();
useEffect(() => {
dataCenter
.loadPublicWorkspace(router.query.workspaceId as string)
.then(data => {
if (data && data.blocksuiteWorkspace) {
setWorkspaceName(data.blocksuiteWorkspace?.meta.name as string);
if (data.blocksuiteWorkspace) {
setWorkspace(data.blocksuiteWorkspace);
if (
router.query.pageId &&
@@ -32,6 +42,7 @@ const Page: NextPageWithLayout = () => {
const page = data.blocksuiteWorkspace.getPage(
router.query.pageId as string
);
page && setPageTitle(page.meta.title);
page && setPage(page);
} else {
router.push('/404');
@@ -46,6 +57,30 @@ const Page: NextPageWithLayout = () => {
<>
{!loaded && <PageLoading />}
<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 && (
<DynamicBlocksuite
page={page}
@@ -69,9 +104,43 @@ export default Page;
export const PageContainer = styled.div(({ theme }) => {
return {
height: 'calc(100vh)',
padding: '78px 72px',
height: '100vh',
overflowY: 'auto',
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',
};
});
@@ -2,16 +2,28 @@ import { PageList } from '@/components/page-list';
import { ReactElement, useEffect, useState } from 'react';
import { PageMeta, useAppState } from '@/providers/app-state-provider';
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 { dataCenter } = useAppState();
const router = useRouter();
const [pageList, setPageList] = useState<PageMeta[]>([]);
const [workspaceName, setWorkspaceName] = useState('');
const { triggerQuickSearchModal } = useModal();
useEffect(() => {
dataCenter
.loadPublicWorkspace(router.query.workspaceId as string)
.then(data => {
setPageList(data.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]);
setWorkspaceName(data.blocksuiteWorkspace?.meta.name as string);
})
.catch(() => {
router.push('/404');
@@ -20,6 +32,23 @@ const All = () => {
return (
<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.filter(p => !p.trash)}
showFavoriteTag={false}
+3 -9
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 {
margin-top: 78px;
margin-bottom: 40px;
}
.affine-editor-container {
background: transparent !important;
.affine-default-page-block-container {
width: 686px !important;
}
@@ -4,9 +4,11 @@ import { PageListHeader } from '@/components/header';
import { ReactElement } from 'react';
import WorkspaceLayout from '@/components/workspace-layout';
import { useTranslation } from '@affine/i18n';
import { useAppState } from '@/providers/app-state-provider';
import { PageMeta, useAppState } from '@/providers/app-state-provider';
const All = () => {
const { pageList } = useAppState();
const { currentWorkspace } = useAppState();
const pageList = (currentWorkspace?.blocksuiteWorkspace?.meta.pageMetas ||
[]) as PageMeta[];
const { t } = useTranslation();
return (
<>
@@ -10,8 +10,8 @@ import {
GeneralPage,
MembersPage,
PublishPage,
ExportPage,
SyncPage,
ExportPage,
} from '@/components/workspace-setting';
import { SettingsIcon } from '@blocksuite/icons';
import { useAppState } from '@/providers/app-state-provider';
@@ -20,41 +20,37 @@ import { WorkspaceUnit } from '@affine/datacenter';
import { useTranslation } from '@affine/i18n';
import { PageListHeader } from '@/components/header';
type TabNames = 'General' | 'Sync' | 'Collaboration' | 'Publish' | 'Export';
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 useTabMap = () => {
const { t } = useTranslation();
const { currentWorkspace, isOwner } = useAppState();
const [activeTab, setActiveTab] = useState<TabNames>(tabMap[0].name);
const handleTabChange = (tab: TabNames) => {
const { isOwner } = useAppState();
const tabMap: {
name: string;
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);
};
@@ -62,7 +58,7 @@ const WorkspaceSetting = () => {
tab => tab.name === activeTab
)?.panelRender;
let tableArr: {
name: TabNames;
name: string;
panelRender: (workspace: WorkspaceUnit) => ReactNode;
}[] = tabMap;
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 (
<>
<StyledSettingContainer>
@@ -10,16 +10,21 @@ import {
import { createDefaultWorkspace } from './utils';
import { User } from '@affine/datacenter';
export interface Disposable {
dispose(): void;
}
type AppStateContextProps = PropsWithChildren<Record<string, unknown>>;
export const AppState = createContext<AppStateContext>({} as AppStateContext);
export const useAppState = () => useContext(AppState);
export const AppStateProvider = ({
children,
}: PropsWithChildren<AppStateContextProps>) => {
const [appState, setAppState] = useState<AppStateValue>({} as AppStateValue);
const [blobState, setBlobState] = useState(false);
const [userInfo, setUser] = useState<User | null>({} as User);
useEffect(() => {
const initState = async () => {
const dataCenter = await getDataCenter();
@@ -27,10 +32,9 @@ export const AppStateProvider = ({
if (dataCenter.workspaces.length === 0) {
await createDefaultWorkspace(dataCenter);
}
setUser((await dataCenter.getUserInfo()) || null);
setAppState({
dataCenter,
user: (await dataCenter.getUserInfo()) || null,
workspaceList: dataCenter.workspaces,
currentWorkspace: null,
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) => {
const { dataCenter, workspaceList, currentWorkspace, user } = appState;
const { dataCenter, workspaceList, currentWorkspace } = appState;
if (!workspaceList.find(v => v.id.toString() === workspaceId)) {
return null;
}
if (workspaceId === currentWorkspace?.id) {
return currentWorkspace;
}
const workspace = (await dataCenter.loadWorkspace(workspaceId)) ?? null;
let isOwner;
if (workspace?.provider === 'local') {
@@ -106,8 +112,9 @@ export const AppStateProvider = ({
isOwner = true;
} else {
// 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 =
(workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? [];
setAppState({
@@ -122,6 +129,26 @@ export const AppStateProvider = ({
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'] =
useRef() as AppStateFunction['setEditor'];
setEditor.current = editor => {
@@ -139,20 +166,13 @@ export const AppStateProvider = ({
if (!user) {
throw new Error('User info not found');
}
setAppState({
...appState,
user,
});
setUser(user);
return user;
};
const logout = async () => {
const { dataCenter } = appState;
await dataCenter.logout();
setAppState({
...appState,
user: null,
});
setUser(null);
};
return (
@@ -161,9 +181,11 @@ export const AppStateProvider = ({
...appState,
setEditor,
loadPage: loadPage.current,
loadWorkspace: loadWorkspace.current,
loadWorkspace: loadWorkspace,
login,
logout,
blobDataSynced: blobState,
user: userInfo,
}}
>
{children}
@@ -16,7 +16,7 @@ export interface PageMeta extends StorePageMeta {
export type AppStateValue = {
dataCenter: DataCenter;
user: User | null;
user?: User | null;
workspaceList: WorkspaceUnit[];
currentWorkspace: WorkspaceUnit | null;
pageList: PageMeta[];
@@ -24,12 +24,15 @@ export type AppStateValue = {
editor?: EditorContainer | null;
synced: boolean;
isOwner?: boolean;
blobDataSynced?: boolean;
};
export type AppStateFunction = {
setEditor: MutableRefObject<(page: EditorContainer) => void>;
loadWorkspace: (workspaceId: string) => Promise<WorkspaceUnit | null>;
loadWorkspace: MutableRefObject<
(workspaceId: string) => Promise<WorkspaceUnit | null>
>;
loadPage: (pageId: string) => void;
login: () => Promise<User>;
@@ -1,6 +1,6 @@
import { DataCenter } from '@affine/datacenter';
const DEFAULT_WORKSPACE_NAME = 'affine';
const DEFAULT_WORKSPACE_NAME = 'AFFiNE Test';
export const createDefaultWorkspace = async (dataCenter: DataCenter) => {
return dataCenter.createWorkspace({

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