Merge branch 'develop'

This commit is contained in:
DarkSky committed 2022-08-09 19:33:03 +08:00
commit 109977c6bb
60 files changed
+2333 -851

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
NX_LOCAL=true
NX_E2E=true
File renamed without changes.
+20 -14
View File
@@ -2,27 +2,33 @@ name: standard check
on: on:
push: push:
branches: [master] branches: [ "develop", "master" ]
pull_request: pull_request:
branches: [master] branches: [ "develop", "master" ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
main: main:
name: Nx Cloud - Main Job name: Nx Cloud - Main Job
uses: nrwl/ci/.github/workflows/nx-cloud-main.yml@v0.6 uses: ./.github/workflows/nx-cloud-main.yml
secrets: inherit secrets: inherit
with: with:
main-branch-name: master main-branch-name: develop
number-of-agents: 3 # number-of-agents: 2
parallel-commands: | parallel-commands: |
pnpm exec nx-cloud record -- pnpm exec nx format:check pnpm exec nx-cloud record -- pnpm exec nx format:check
parallel-commands-on-agents: | pnpm e2e:ci ${{ github.ref == 'refs/heads/develop' && '--record' || '' }}
pnpm exec nx affected --target=lint --parallel=3 --exclude=components-common,keck,theme pnpm exec nx affected --target=lint --parallel=2 --exclude=components-common,keck,theme
pnpm exec nx affected --target=build --parallel=3 pnpm exec nx affected --target=build --parallel=2
# parallel-commands-on-agents: |
# pnpm exec nx affected --target=lint --parallel=2 --exclude=components-common,keck,theme
# pnpm exec nx affected --target=build --parallel=2
agents: # agents:
name: Nx Cloud - Agents # name: Nx Cloud - Agents
uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.6 # uses: nrwl/ci/.github/workflows/nx-cloud-agents.yml@v0.6
secrets: inherit # with:
with: # number-of-agents: 2
number-of-agents: 3
+72
View File
@@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "develop", master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "develop" ]
schedule:
- cron: '27 1 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
+300
View File
@@ -0,0 +1,300 @@
name: Nx Cloud Main
on:
workflow_call:
secrets:
NX_CLOUD_ACCESS_TOKEN:
required: false
NX_CLOUD_AUTH_TOKEN:
required: false
NX_CYPRESS_KEY:
required: false
inputs:
number-of-agents:
required: false
type: number
environment-variables:
required: false
type: string
init-commands:
required: false
type: string
final-commands:
required: false
type: string
parallel-commands:
required: false
type: string
parallel-commands-on-agents:
required: false
type: string
node-version:
required: false
type: string
yarn-version:
required: false
type: string
npm-version:
required: false
type: string
pnpm-version:
required: false
type: string
install-commands:
required: false
type: string
main-branch-name:
required: false
type: string
default: main
runs-on:
required: false
type: string
default: ubuntu-latest
# We needed this input in order to be able to configure out integration tests for this repo, it is not documented
# so as to not cause confusion/add noise, but technically any consumer of the workflow can use it if they want to.
working-directory:
required: false
type: string
env:
NX_CLOUD_DISTRIBUTED_EXECUTION: true
NX_CLOUD_DISTRIBUTED_EXECUTION_AGENT_COUNT: ${{ inputs.number-of-agents }}
NX_BRANCH: ${{ github.event.number || github.ref_name }}
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
NX_CLOUD_AUTH_TOKEN: ${{ secrets.NX_CLOUD_AUTH_TOKEN }}
CYPRESS_RECORD_KEY: ${{ secrets.NX_CYPRESS_KEY }}
jobs:
main:
runs-on: ${{ inputs.runs-on }}
# The name of the job which will invoke this one is expected to be "Nx Cloud - Main Job", and whatever we call this will be appended
# to that one after a forward slash, so we keep this one intentionally short to produce "Nx Cloud - Main Job / Run" in the Github UI
name: Run
defaults:
run:
working-directory: ${{ inputs.working-directory || github.workspace }}
# Specify shell to help normalize across different operating systems
shell: bash
steps:
- uses: actions/checkout@v2
name: Checkout [Pull Request]
if: ${{ github.event_name == 'pull_request' }}
with:
# By default, PRs will be checked-out based on the Merge Commit, but we want the actual branch HEAD.
ref: ${{ github.event.pull_request.head.sha }}
# We need to fetch all branches and commits so that Nx affected has a base to compare against.
fetch-depth: 0
- uses: actions/checkout@v2
name: Checkout [Default Branch]
if: ${{ github.event_name != 'pull_request' }}
with:
# We need to fetch all branches and commits so that Nx affected has a base to compare against.
fetch-depth: 0
- name: Derive appropriate SHAs for base and head for `nx affected` commands
uses: nrwl/nx-set-shas@v2
with:
main-branch-name: ${{ inputs.main-branch-name }}
- name: Detect package manager
id: package_manager
shell: bash
run: |
echo "::set-output name=name::$([[ -f ./yarn.lock ]] && echo "yarn" || ([[ -f ./pnpm-lock.yaml ]] && echo "pnpm") || echo "npm")"
# Set node/npm/yarn versions using volta, with optional overrides provided by the consumer
- uses: volta-cli/action@fdf4cf319494429a105efaa71d0e5ec67f338c6e
with:
node-version: "${{ inputs.node-version }}"
npm-version: "${{ inputs.npm-version }}"
yarn-version: "${{ inputs.yarn-version }}"
# Install pnpm with exact version provided by consumer or fallback to latest
- name: Install PNPM
if: steps.package_manager.outputs.name == 'pnpm'
uses: pnpm/action-setup@v2.2.1
with:
version: ${{ inputs.pnpm-version || 'latest' }}
- name: Print node/npm/yarn versions
id: versions
run: |
node_ver=$( node --version )
yarn_ver=$( yarn --version || true )
pnpm_ver=$( pnpm --version || true )
echo "Node: ${node_ver:1}"
echo "NPM: $( npm --version )"
if [[ $yarn_ver != '' ]]; then echo "Yarn: $yarn_ver"; fi
if [[ $pnpm_ver != '' ]]; then echo "PNPM: $pnpm_ver"; fi
echo "::set-output name=node_version::${node_ver:1}"
- name: Use the node_modules cache if available [npm]
if: steps.package_manager.outputs.name == 'npm'
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-
- name: Use the node_modules cache if available [pnpm]
if: steps.package_manager.outputs.name == 'pnpm'
uses: actions/cache@v2
with:
path: ~/.pnpm-store
key: ${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-
- name: Get yarn cache directory path
if: steps.package_manager.outputs.name == 'yarn'
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Use the node_modules cache if available [yarn]
if: steps.package_manager.outputs.name == 'yarn'
uses: actions/cache@v2
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node-${{ steps.versions.outputs.node_version }}-yarn-
- name: Process environment-variables
if: ${{ inputs.environment-variables != '' }}
uses: actions/github-script@v6
env:
ENV_VARS: ${{ inputs.environment-variables }}
with:
script: |
const { appendFileSync } = require('fs');
// trim spaces and escape quotes
const cleanStr = str => str
.trim()
.replaceAll(/`/g, "\`");
// parse variable to correct type
const parseStr = str =>
str === 'true' || str === 'TRUE'
? true
: str === 'false' || str === 'FALSE'
? false
: isNaN(str)
? str
: parseFloat(str);
const varsStr = process.env.ENV_VARS || '';
const vars = varsStr
.split('\n')
.map(variable => variable.trim())
.filter(variable => variable.indexOf('=') > 0)
.map(variable => ({
name: cleanStr(variable.split('=')[0]),
value: cleanStr(variable.slice(variable.indexOf('=') + 1))
}));
for (const v of vars) {
console.log(`Appending environment variable \`${v.name}\` with value \`${v.value}\` to ${process.env.GITHUB_ENV}`);
appendFileSync(process.env.GITHUB_ENV, `${v.name}=${parseStr(v.value)}\n`);
}
- name: Run any configured install-commands
if: ${{ inputs.install-commands != '' }}
run: |
${{ inputs.install-commands }}
- name: Install dependencies
if: ${{ inputs.install-commands == '' }}
run: |
if [ "${{ steps.package_manager.outputs.name == 'yarn' }}" == "true" ]; then
echo "Running yarn install --frozen-lockfile"
yarn install --frozen-lockfile
elif [ "${{ steps.package_manager.outputs.name == 'pnpm' }}" == "true" ]; then
echo "Running pnpm install --frozen-lockfile"
pnpm install --frozen-lockfile
else
echo "Running npm ci"
npm ci
fi
# An unfortunate side-effect of the way reusable workflows work is that by the time they are pulled into the "caller"
# repo, they are effectively completely embedded in that context. This means that we cannot reference any files which
# are local to this repo which defines the workflow, and we therefore need to work around this by embedding the contents
# of the shell utilities for executing commands into the workflow directly.
- name: Create command utils
uses: actions/github-script@v6
with:
script: |
const { writeFileSync } = require('fs');
const runCommandsInParallelScript = `
# Extract the provided commands from the stringified JSON array.
IFS=$'\n' read -d '' -a userCommands < <((jq -c -r '.[]') <<<"$1")
# Invoke the provided commands in parallel and collect their exit codes.
pids=()
for userCommand in "\${userCommands[@]}"; do
eval "$userCommand" & pids+=($!)
done
# If any one of the invoked commands exited with a non-zero exit code, exit the whole thing with code 1.
for pid in \${pids[*]}; do
if ! wait $pid; then
exit 1
fi
done
# All the invoked commands must have exited with code zero.
exit 0
`;
writeFileSync('./.github/workflows/run-commands-in-parallel.sh', runCommandsInParallelScript);
- name: Prepare command utils
# We need to escape the workspace path to be consistent cross-platform: https://github.com/actions/runner/issues/1066
run: chmod +x ${GITHUB_WORKSPACE//\\//}/.github/workflows/run-commands-in-parallel.sh
- name: Initialize the Nx Cloud distributed CI run
run: npx nx-cloud start-ci-run
# The good thing about the multi-line string input for sequential commands is that we can simply forward it on as is to the bash shell and it will behave
# how we want it to in terms of quote escaping, variable assignment etc
- name: Run any configured init-commands sequentially
if: ${{ inputs.init-commands != '' }}
run: |
${{ inputs.init-commands }}
- name: Process parallel commands configuration
uses: actions/github-script@v6
id: parallel_commands_config
env:
PARALLEL_COMMANDS: ${{ inputs.parallel-commands }}
PARALLEL_COMMANDS_ON_AGENTS: ${{ inputs.parallel-commands-on-agents }}
with:
# For the ones configured for main, explicitly set NX_CLOUD_DISTRIBUTED_EXECUTION to false, taking into account commands chained with &&
# within the strings. In order to properly escape single quotes we need to do some manual replacing and escaping so that the commands
# are forwarded onto the run-commands-in-parallel.sh script appropriately.
script: |
const parallelCommandsOnMainStr = process.env.PARALLEL_COMMANDS || '';
const parallelCommandsOnAgentsStr = process.env.PARALLEL_COMMANDS_ON_AGENTS || '';
const parallelCommandsOnMain = parallelCommandsOnMainStr
.split('\n')
.map(command => command.trim())
.filter(command => command.length > 0)
.map(s => s.replace(/'/g, '%27'));
const parallelCommandsOnAgents = parallelCommandsOnAgentsStr
.split('\n')
.map(command => command.trim())
.filter(command => command.length > 0)
.map(s => s.replace(/'/g, '%27'));
const formattedArrayOfCommands = [
...parallelCommandsOnMain.map(s => s
.split(' && ')
.map(s => `NX_CLOUD_DISTRIBUTED_EXECUTION=false ${s}`)
.join(' && ')
),
...parallelCommandsOnAgents,
];
const stringifiedEncodedArrayOfCommands = JSON.stringify(formattedArrayOfCommands)
.replace(/%27/g, "'\\''");
return stringifiedEncodedArrayOfCommands
result-encoding: string
- name: Run any configured parallel commands on main and agent jobs
# We need to escape the workspace path to be consistent cross-platform: https://github.com/actions/runner/issues/1066
run: ${GITHUB_WORKSPACE//\\//}/.github/workflows/run-commands-in-parallel.sh '${{ steps.parallel_commands_config.outputs.result }}'
# The good thing about the multi-line string input for sequential commands is that we can simply forward it on as is to the bash shell and it will behave
# how we want it to in terms of quote escaping, variable assignment etc
- name: Run any configured final-commands sequentially
if: ${{ inputs.final-commands != '' }}
run: |
${{ inputs.final-commands }}
- name: Stop all running agents for this CI run
# It's important that we always run this step, otherwise in the case of any failures in preceding non-Nx steps, the agents will keep running and waste billable minutes
if: ${{ always() }}
run: npx nx-cloud stop-all-agents
+1
View File
@@ -12,6 +12,7 @@
"cssmodule", "cssmodule",
"datasource", "datasource",
"fflate", "fflate",
"fstore",
"groq", "groq",
"howpublished", "howpublished",
"immer", "immer",
+8 -8
View File
@@ -91,11 +91,11 @@ Affine is fully built with web technologies so that consistency and accessibilit
# Documentation # Documentation
AFFiNE is not yet ready for production use. To install, you may check how to build or depoly the AFFiNE in [quick-start](https://affine.gitbook.io/affine/basic-documentation/contribute-to-affine/quick-start). For the full documentation, please view it [here](https://affine.gitbook.io/affine/). AFFiNE is not yet ready for production use. To install, you may check how to build or deploy the AFFiNE in [quick-start](https://affine.gitbook.io/affine/basic-documentation/contribute-to-affine/quick-start). For the full documentation, please view it [here](https://affine.gitbook.io/affine/).
## Getting Started with development ## Getting Started with development
Please view the path Contribute-to-AFFiNE/Software-Contributions/Quick-Start in documentation. Please view the path Contribute-to-AFFiNE/Software-Contributions/Quick-Start in the documentation.
# Roadmap # Roadmap
@@ -129,25 +129,25 @@ That's why we are making AFFiNE. Some of the most important features are:
- An always good-to-read, structured docs-form page is the best for your notes, but a boundless doodle surface is better for collaboration and creativity. - An always good-to-read, structured docs-form page is the best for your notes, but a boundless doodle surface is better for collaboration and creativity.
- Atomic - Atomic
- The basic element of affine are blocks, not pages. - The basic element of affine are blocks, not pages.
- Blocks can be directly reuse and synced between pages. - Blocks can be directly reused and synced between pages.
- Pages and blocks are searched and organized on the basis of connected graphs, not tree-like paths. - Pages and blocks are searched and organized based on connected graphs, not tree-like paths.
- Dual-link and semantic search are fully supported. - Dual-link and semantic search are fully supported.
- Collaborative and privacy-first - Collaborative and privacy-first
- Data is always stored locally by default - Data is always stored locally by default
- CRDTs are applied so that peer-to-peer collaboration is possible. - CRDTs are applied so that peer-to-peer collaboration is possible.
We really appreciate the idea of Monday, airtable and notion database. They inspired what we think is right for task management. But we don't like the repeated works -- we don't want to set a todo easily with markdown but end up re-write it again in kanban or other databases. We really appreciate the idea of Monday, Airtable and Notion databases. They inspired what we think is right for task management. But we don't like the repeated works -- we don't want to set a todo easily with markdown but end up re-write it again in kanban or other databases.
With AFFiNE, every block group has infinite views, for you to keep your single source of truth. With AFFiNE, every block group has infinite views, for you to keep your single source of truth.
We would like to give special thanks to the innovators and pioneers who greatly inspired us: We would like to give special thanks to the innovators and pioneers who greatly inspired us:
- Quip & Notion -- that docs can be organized as blocks - Quip & Notion -- that docs can be organized as blocks
- Taskade & Monday -- brillant multi-demensional tables - Taskade & Monday -- brilliant multi-dimensional tables
- Height & Linear -- beautiful task management tool - Height & Linear -- beautiful task management tool
We would also like to give thanks to open-source projects that make affine possible: We would also like to give thanks to open-source projects that make affine possible:
- [Yjs](https://github.com/yjs/yjs) & [Yrs](https://github.com/y-crdt/y-crdt) -- Fundamental support of CRDTs for our implements on state management and data sync. - [Yjs](https://github.com/yjs/yjs) & [Yrs](https://github.com/y-crdt/y-crdt) -- Fundamental support of CRDTs for our implementation on state management and data sync.
- [React](https://github.com/facebook/react) -- View layer support and web GUI framework. - [React](https://github.com/facebook/react) -- View layer support and web GUI framework.
- [Rust](https://github.com/rust-lang/rust) -- High performance language that extends the ability and availability of our real-time backend, JWST. - [Rust](https://github.com/rust-lang/rust) -- High performance language that extends the ability and availability of our real-time backend, JWST.
- [Fossil](https://www2.fossil-scm.org/home/doc/trunk/www/index.wiki) -- Source code management tool made with CRDTs which inspired our design on block data structure. - [Fossil](https://www2.fossil-scm.org/home/doc/trunk/www/index.wiki) -- Source code management tool made with CRDTs which inspired our design on block data structure.
@@ -155,7 +155,7 @@ We would also like to give thanks to open-source projects that make affine possi
- [Jotai](https://github.com/pmndrs/jotai) -- Minimal state management tool for frontend. - [Jotai](https://github.com/pmndrs/jotai) -- Minimal state management tool for frontend.
- [Tldraw](https://github.com/tldraw/tldraw) -- Excellent drawing board. - [Tldraw](https://github.com/tldraw/tldraw) -- Excellent drawing board.
- [MUI](https://github.com/mui/material-ui) -- Our most used graphic UI component library. - [MUI](https://github.com/mui/material-ui) -- Our most used graphic UI component library.
- Other [dependancies](https://github.com/toeverything/AFFiNE/network/dependencies) - Other [dependencies](https://github.com/toeverything/AFFiNE/network/dependencies)
Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone. Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone.
+6 -6
View File
@@ -5,15 +5,15 @@
"author": "DarkSky <darksky2048@gmail.com>", "author": "DarkSky <darksky2048@gmail.com>",
"main": "jest.config.ts", "main": "jest.config.ts",
"dependencies": { "dependencies": {
"authing-js-sdk": "^4.23.33", "authing-js-sdk": "^4.23.35",
"firebase-admin": "^11.0.0", "firebase-admin": "^11.0.1",
"lib0": "^0.2.51", "lib0": "^0.2.52",
"lru-cache": "^7.13.0", "lru-cache": "^7.13.2",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"readable-stream": "^4.1.0", "readable-stream": "^4.1.0",
"ws": "^8.8.0", "ws": "^8.8.1",
"y-protocols": "^1.0.5", "y-protocols": "^1.0.5",
"yjs": "^13.5.39" "yjs": "^13.5.41"
}, },
"devDependencies": { "devDependencies": {
"@types/readable-stream": "^2.3.13", "@types/readable-stream": "^2.3.13",
+17
View File
@@ -0,0 +1,17 @@
{
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["src/plugins/index.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off"
}
}
]
}
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from 'cypress';
module.exports = defineConfig({
projectId: 'r1wrqr',
e2e: {
supportFile: './src/support/index.ts',
specPattern: './src/integration',
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
fileServerFolder: '.',
fixturesFolder: './src/fixtures',
video: false,
// videosFolder: '../../dist/cypress/apps/ligo-virgo-e2e/videos',
screenshotsFolder: '../../dist/cypress/apps/ligo-virgo-e2e/screenshots',
chromeWebSecurity: false,
});
+28
View File
@@ -0,0 +1,28 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/ligo-virgo-e2e/src",
"projectType": "application",
"targets": {
"e2e": {
"executor": "@nrwl/cypress:cypress",
"options": {
"cypressConfig": "apps/ligo-virgo-e2e/cypress.config.ts",
"devServerTarget": "ligo-virgo:serve"
},
"configurations": {
"production": {
"devServerTarget": "ligo-virgo:serve:production"
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/ligo-virgo-e2e/**/*.{js,ts}"]
}
}
},
"tags": [],
"implicitDependencies": ["ligo-virgo"]
}
@@ -0,0 +1,4 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
}
@@ -0,0 +1,14 @@
import { getTitle, getBoard } from '../support/app.po';
describe('ligo-virgo', () => {
beforeEach(() => cy.visit('/'));
it('basic load check', () => {
getTitle().contains('Get Started with AFFiNE');
cy.get('.block_container').contains('The Essentials');
getBoard().click();
cy.get('.tl-inner-div').contains('Graduating');
});
});
@@ -0,0 +1,3 @@
export const getTitle = () => cy.get('span[title]');
export const getDoc = () => cy.contains('Paper');
export const getBoard = () => cy.contains('Edgeless');
@@ -0,0 +1,33 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
}
}
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
+17
View File
@@ -0,0 +1,17 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"]
},
"include": ["src/**/*.ts", "src/**/*.js"]
}
+2 -2
View File
@@ -13,8 +13,8 @@
"@mui/icons-material": "^5.8.4" "@mui/icons-material": "^5.8.4"
}, },
"devDependencies": { "devDependencies": {
"firebase": "^9.8.4", "firebase": "^9.9.2",
"mini-css-extract-plugin": "^2.6.1", "mini-css-extract-plugin": "^2.6.1",
"webpack": "^5.73.0" "webpack": "^5.74.0"
} }
} }
+2 -1
View File
@@ -48,7 +48,8 @@
"configurations": { "configurations": {
"production": { "production": {
"buildTarget": "ligo-virgo:build:production", "buildTarget": "ligo-virgo:build:production",
"hmr": false "hmr": false,
"open": false
} }
} }
}, },
+1 -1
View File
@@ -4,7 +4,7 @@
<!-- local dev index.html --> <!-- local dev index.html -->
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="https://app.affine.pro/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<title>AFFiNE - All In One Workos</title> <title>AFFiNE - All In One Workos</title>
<script> <script>
window.global = window; window.global = window;
@@ -1,43 +1,73 @@
import { useState } from 'react'; import { useState } from 'react';
import { MuiDivider as Divider, styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
import type { ValueOf } from '@toeverything/utils'; import type { ValueOf } from '@toeverything/utils';
const StyledTabs = styled('div')({ const StyledTabs = styled('div')(({ theme }) => {
width: '100%',
height: '12px',
marginTop: '12px',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
});
const StyledDivider = styled(Divider, {
shouldForwardProp: (prop: string) => !['isActive'].includes(prop),
})<{ isActive?: boolean }>(({ isActive }) => {
return { return {
flex: 1, width: '100%',
backgroundColor: isActive ? '#3E6FDB' : '#ECF1FB', height: '30px',
borderWidth: '2px', marginTop: '12px',
display: 'flex',
fontSize: '12px',
fontWeight: '600',
color: theme.affine.palette.primary,
}; };
}); });
const StyledTabTitle = styled('div', {
shouldForwardProp: (prop: string) => !['isActive'].includes(prop),
})<{ isActive?: boolean; isDisabled?: boolean }>`
flex: 1;
display: flex;
align-items: center;
justify-content: center;
line-height: 18px;
padding-top: 4px;
border-top: 2px solid #ecf1fb;
position: relative;
&::after {
content: '';
width: 0;
height: 2px;
background-color: ${({ isActive, theme }) =>
isActive ? theme.affine.palette.primary : ''};
position: absolute;
left: 100%;
top: -2px;
transition: all 0.2s;
}
&.active {
&::after {
width: 100%;
left: 0;
transition-delay: 0.1s;
}
& ~ div::after {
left: 0;
}
}
`;
const TAB_TITLE = { const TAB_TITLE = {
PAGES: 'pages', PAGES: 'pages',
GALLERY: 'gallery', GALLERY: 'gallery',
TOC: 'toc', TOC: 'toc',
} as const; } as const;
const TabMap = new Map<TabKey, TabValue>([ const TabMap = new Map<TabKey, { value: TabValue; disabled?: boolean }>([
['PAGES', 'pages'], ['PAGES', { value: 'pages' }],
['GALLERY', 'gallery'], ['GALLERY', { value: 'gallery', disabled: true }],
['TOC', 'toc'], ['TOC', { value: 'toc' }],
]); ]);
type TabKey = keyof typeof TAB_TITLE; type TabKey = keyof typeof TAB_TITLE;
type TabValue = ValueOf<typeof TAB_TITLE>; type TabValue = ValueOf<typeof TAB_TITLE>;
const Tabs = () => { const Tabs = () => {
const [activeTab, setActiveTab] = useState<TabValue>(TAB_TITLE.PAGES); const [activeValue, setActiveTab] = useState<TabValue>(TAB_TITLE.PAGES);
const onClick = (v: TabValue) => { const onClick = (v: TabValue) => {
setActiveTab(v); setActiveTab(v);
@@ -45,13 +75,19 @@ const Tabs = () => {
return ( return (
<StyledTabs> <StyledTabs>
{[...TabMap.entries()].map(([k, v]) => { {[...TabMap.entries()].map(([k, { value, disabled = false }]) => {
const isActive = activeValue === value;
return ( return (
<StyledDivider <StyledTabTitle
key={v} key={value}
isActive={v === activeTab} className={isActive ? 'active' : ''}
onClick={() => onClick(v)} isActive={isActive}
/> isDisabled={disabled}
onClick={() => onClick(value)}
>
{k}
</StyledTabTitle>
); );
})} })}
</StyledTabs> </StyledTabs>
+8
View File
@@ -16,6 +16,7 @@ module.exports = function (webpackConfig) {
const config = getNxWebpackConfig(webpackConfig); const config = getNxWebpackConfig(webpackConfig);
const isProd = config.mode === 'production'; const isProd = config.mode === 'production';
const isE2E = process.env.NX_E2E;
const style9 = { const style9 = {
test: /\.(tsx|ts|js|mjs|jsx)$/, test: /\.(tsx|ts|js|mjs|jsx)$/,
@@ -147,6 +148,12 @@ module.exports = function (webpackConfig) {
} }
} }
config.module.rules.unshift({
test: /\.wasm$/,
type: 'asset/resource',
});
config.resolve.fallback = { crypto: false, fs: false, path: false };
addEmotionBabelPlugin(config); addEmotionBabelPlugin(config);
config.plugins = [ config.plugins = [
@@ -158,6 +165,7 @@ module.exports = function (webpackConfig) {
global: {}, global: {},
}), }),
isProd && isProd &&
!isE2E &&
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
title: 'AFFiNE - All In One Workos', title: 'AFFiNE - All In One Workos',
favicon: path.resolve( favicon: path.resolve(
+31 -12
View File
@@ -52,7 +52,7 @@ const Alternatives = styled(Box)<{ width: string }>(({ width }) => ({
height: '128px', height: '128px',
transform: 'translateY(-8px)', transform: 'translateY(-8px)',
overflowY: 'hidden', overflowY: 'hidden',
'@media (max-width: 768px)': { '@media (max-width: 1024px)': {
width, width,
height: '48px', height: '48px',
transform: 'translateY(0)', transform: 'translateY(0)',
@@ -64,7 +64,7 @@ const Alternatives = styled(Box)<{ width: string }>(({ width }) => ({
left: '0%', left: '0%',
top: '0%', top: '0%',
lineHeight: '96px', lineHeight: '96px',
'@media (max-width: 768px)': { '@media (max-width: 1024px)': {
lineHeight: '32px', lineHeight: '32px',
}, },
}, },
@@ -109,7 +109,7 @@ const Product = () => {
); );
const maxWidth = useMemo(() => _alternativesSize[idx], [idx]); const maxWidth = useMemo(() => _alternativesSize[idx], [idx]);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const matches = useMediaQuery('(max-width: 768px)'); const matches = useMediaQuery('(max-width: 1024px)');
useEffect(() => { useEffect(() => {
const handle = setInterval(() => { const handle = setInterval(() => {
@@ -128,7 +128,14 @@ const Product = () => {
return ( return (
<Alternatives <Alternatives
width={`${maxWidth}em`} width={`${maxWidth}em`}
sx={{ margin: 'auto', marginRight: '1em', transition: 'width .5s' }} sx={{
margin: 'auto',
marginRight: '1em',
transition: 'width .5s',
'@media (max-width: 1024px)': {
width: '8em',
},
}}
> >
<Box <Box
className={clsx( className={clsx(
@@ -144,7 +151,7 @@ const Product = () => {
color: '#06449d', color: '#06449d',
textAlign: 'right', textAlign: 'right',
overflow: 'hidden', overflow: 'hidden',
'@media (max-width: 768px)': { '@media (max-width: 1024px)': {
fontSize: '32px', fontSize: '32px',
}, },
}} }}
@@ -162,7 +169,7 @@ const Product = () => {
marginTop: '96px', marginTop: '96px',
textAlign: 'right', textAlign: 'right',
overflow: 'hidden', overflow: 'hidden',
'@media (max-width: 768px)': { '@media (max-width: 1024px)': {
marginTop: '48px', marginTop: '48px',
}, },
}} }}
@@ -173,7 +180,7 @@ const Product = () => {
sx={{ sx={{
color: '#06449d', color: '#06449d',
overflow: 'hidden', overflow: 'hidden',
'@media (max-width: 768px)': { '@media (max-width: 1024px)': {
fontSize: '32px', fontSize: '32px',
}, },
}} }}
@@ -191,7 +198,7 @@ const AffineImage = styled('img')({
}); });
const GitHub = (props: { center?: boolean; flat?: boolean }) => { const GitHub = (props: { center?: boolean; flat?: boolean }) => {
const matches = useMediaQuery('(max-width: 768px)'); const matches = useMediaQuery('(max-width: 1024px)');
return ( return (
<Button <Button
@@ -204,6 +211,10 @@ const GitHub = (props: { center?: boolean; flat?: boolean }) => {
{...{ {...{
sx: { sx: {
margin: 'auto 1em', margin: 'auto 1em',
fontSize: '24px',
'@media (max-width: 1024px)': {
fontSize: '16px',
},
...(props.flat ...(props.flat
? { ? {
padding: matches ? '0' : '0 0.5em', padding: matches ? '0' : '0 0.5em',
@@ -231,7 +242,7 @@ const GitHub = (props: { center?: boolean; flat?: boolean }) => {
}; };
export function App() { export function App() {
const matches = useMediaQuery('(max-width: 768px)'); const matches = useMediaQuery('(max-width: 1024px)');
return ( return (
<CssVarsProvider> <CssVarsProvider>
@@ -256,6 +267,10 @@ export function App() {
sx={{ sx={{
padding: matches ? '0' : '0 0.5em', padding: matches ? '0' : '0 0.5em',
':hover': { backgroundColor: 'unset' }, ':hover': { backgroundColor: 'unset' },
fontSize: '24px',
'@media (max-width: 1024px)': {
fontSize: '16px',
},
}} }}
> >
AFFiNE AFFiNE
@@ -276,6 +291,10 @@ export function App() {
sx={{ sx={{
padding: matches ? '0' : '0 0.5em', padding: matches ? '0' : '0 0.5em',
':hover': { backgroundColor: 'unset' }, ':hover': { backgroundColor: 'unset' },
fontSize: '24px',
'@media (max-width: 1024px)': {
fontSize: '16px',
},
}} }}
size="lg" size="lg"
> >
@@ -302,7 +321,7 @@ export function App() {
fontWeight={900} fontWeight={900}
sx={{ sx={{
marginRight: '0.25em', marginRight: '0.25em',
'@media (max-width: 768px)': { '@media (max-width: 1024px)': {
fontSize: '32px', fontSize: '32px',
marginRight: 0, marginRight: 0,
}, },
@@ -314,7 +333,7 @@ export function App() {
fontSize="96px" fontSize="96px"
fontWeight={900} fontWeight={900}
sx={{ sx={{
'@media (max-width: 768px)': { '@media (max-width: 1024px)': {
fontSize: '32px', fontSize: '32px',
}, },
}} }}
@@ -347,7 +366,7 @@ export function App() {
sx={{ sx={{
color: '#06449d', color: '#06449d',
margin: 'auto', margin: 'auto',
'@media (max-width: 768px)': { '@media (max-width: 1024px)': {
fontSize: '32px', fontSize: '32px',
}, },
}} }}
+1 -1
View File
@@ -6,7 +6,7 @@
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+2 -2
View File
@@ -3,10 +3,10 @@
"version": "0.0.1", "version": "0.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@authing/react-ui-components": "^3.1.23", "@authing/react-ui-components": "^3.1.39",
"@emotion/styled": "^11.9.3" "@emotion/styled": "^11.9.3"
}, },
"devDependencies": { "devDependencies": {
"authing-js-sdk": "^4.23.33" "authing-js-sdk": "^4.23.35"
} }
} }
@@ -21,9 +21,7 @@ export const ZoomBar: FC = () => {
const zoom = app.useStore(zoomSelector); const zoom = app.useStore(zoomSelector);
return ( return (
<div <ZoomBarContainer>
style={{ position: 'absolute', right: 10, bottom: 10, zIndex: 200 }}
>
<MiniMapContainer> <MiniMapContainer>
<MiniMap /> <MiniMap />
</MiniMapContainer> </MiniMapContainer>
@@ -52,10 +50,18 @@ export const ZoomBar: FC = () => {
<UnfoldMoreIcon style={{ transform: 'rotateZ(90deg)' }} /> <UnfoldMoreIcon style={{ transform: 'rotateZ(90deg)' }} />
</IconButton> </IconButton>
</div> </div>
</div> </ZoomBarContainer>
); );
}; };
const ZoomBarContainer = styled('div')({
position: 'absolute',
right: 10,
bottom: 10,
zIndex: 200,
userSelect: 'none',
});
const MiniMapContainer = styled('div')({ const MiniMapContainer = styled('div')({
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
@@ -133,6 +133,7 @@ export class EditorUtil extends TDShapeUtil<T, E> {
<HTMLContainer ref={ref} {...events}> <HTMLContainer ref={ref} {...events}>
<Container <Container
ref={containerRef} ref={containerRef}
editing={isEditing}
onPointerDown={stopPropagation} onPointerDown={stopPropagation}
onMouseEnter={activateIfEditing} onMouseEnter={activateIfEditing}
onDragEnter={activateIfEditing} onDragEnter={activateIfEditing}
@@ -248,15 +249,15 @@ export class EditorUtil extends TDShapeUtil<T, E> {
const PADDING = 16; const PADDING = 16;
// const MIN_CONTAINER_HEIGHT = 200; // const MIN_CONTAINER_HEIGHT = 200;
const Container = styled('div')({ const Container = styled('div')<{ editing: boolean }>(({ editing }) => ({
pointerEvents: 'all', pointerEvents: 'all',
position: 'relative', position: 'relative',
width: '100%', width: '100%',
}); userSelect: editing ? 'unset' : 'none',
}));
const Mask = styled('div')({ const Mask = styled('div')({
position: 'absolute', position: 'absolute',
userSelect: 'none',
top: 0, top: 0,
left: 0, left: 0,
bottom: 0, bottom: 0,
+7 -7
View File
@@ -8,21 +8,21 @@
"@codemirror/lang-css": "^6.0.0", "@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-html": "~6.1.0", "@codemirror/lang-html": "~6.1.0",
"@codemirror/lang-java": "~6.0.0", "@codemirror/lang-java": "~6.0.0",
"@codemirror/lang-javascript": "~6.0.1", "@codemirror/lang-javascript": "~6.0.2",
"@codemirror/lang-json": "~6.0.0", "@codemirror/lang-json": "~6.0.0",
"@codemirror/lang-lezer": "~6.0.0", "@codemirror/lang-lezer": "~6.0.0",
"@codemirror/lang-markdown": "~6.0.0", "@codemirror/lang-markdown": "~6.0.1",
"@codemirror/lang-php": "~6.0.0", "@codemirror/lang-php": "~6.0.0",
"@codemirror/lang-python": "~6.0.0", "@codemirror/lang-python": "~6.0.1",
"@codemirror/lang-rust": "~6.0.0", "@codemirror/lang-rust": "~6.0.0",
"@codemirror/lang-sql": "~6.0.0", "@codemirror/lang-sql": "~6.1.0",
"@codemirror/lang-xml": "~6.0.0", "@codemirror/lang-xml": "~6.0.0",
"@codemirror/language": "^6.2.0", "@codemirror/language": "^6.2.1",
"@codemirror/legacy-modes": "~6.1.0", "@codemirror/legacy-modes": "~6.1.0",
"@codemirror/next": "^0.16.0", "@codemirror/next": "^0.16.0",
"@codemirror/state": "^6.1.0", "@codemirror/state": "^6.1.1",
"@codemirror/theme-one-dark": "^6.0.0", "@codemirror/theme-one-dark": "^6.0.0",
"@codemirror/view": "^6.0.2", "@codemirror/view": "^6.2.0",
"@dnd-kit/core": "^6.0.5", "@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1", "@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0", "@dnd-kit/utilities": "^3.2.0",
@@ -86,7 +86,7 @@ export const PageView: FC<CreateView> = ({ block, editor }) => {
alwaysShowPlaceholder alwaysShowPlaceholder
ref={textRef} ref={textRef}
className={'title'} className={'title'}
supportMarkdown={true} supportMarkdown={false}
handleEnter={onTextEnter} handleEnter={onTextEnter}
placeholder={'Untitled'} placeholder={'Untitled'}
block={block} block={block}
@@ -126,7 +126,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
// block = await editor.commands.blockCommands.createNextBlock(block.id,) // block = await editor.commands.blockCommands.createNextBlock(block.id,)
const on_text_view_active = useCallback( const on_text_view_active = useCallback(
(point: CursorTypes, rang_form?: 'up' | 'down') => { (point: CursorTypes) => {
// TODO code to be optimized // TODO code to be optimized
if (textRef.current) { if (textRef.current) {
const end_selection = textRef.current.getEndSelection(); const end_selection = textRef.current.getEndSelection();
@@ -146,7 +146,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
.getElementsByClassName('text-paragraph')[0] .getElementsByClassName('text-paragraph')[0]
.getBoundingClientRect(); .getBoundingClientRect();
if (rang_form === 'up') { if (blockTop > blockDomStyle.top) {
blockTop = blockDomStyle.bottom - 5; blockTop = blockDomStyle.bottom - 5;
} else { } else {
blockTop = blockDomStyle.top + 5; blockTop = blockDomStyle.top + 5;
@@ -319,7 +319,7 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
if (nowPosition.top === startPosition.top) { if (nowPosition.top === startPosition.top) {
editor.selectionManager.activePreviousNode( editor.selectionManager.activePreviousNode(
block.id, block.id,
new Point(nowPosition.left, nowPosition.top - 20) new Point(nowPosition.left, nowPosition.top)
); );
return true; return true;
@@ -357,17 +357,14 @@ export const TextManage = forwardRef<ExtendedTextUtils, CreateTextView>(
// The specific amount of TODO needs to be determined after subsequent padding // The specific amount of TODO needs to be determined after subsequent padding
editor.selectionManager.activeNextNode( editor.selectionManager.activeNextNode(
block.id, block.id,
new Point(nowPosition.left, nowPosition.bottom + 20) new Point(nowPosition.left, nowPosition.bottom)
); );
return true; return true;
} else { } else {
if (prePosition?.bottom === endPosition.bottom) { if (prePosition?.bottom === endPosition.bottom) {
editor.selectionManager.activeNextNode( editor.selectionManager.activeNextNode(
block.id, block.id,
new Point( new Point(prePosition.left, prePosition?.bottom)
prePosition.left,
prePosition?.bottom + 20
)
); );
return true; return true;
} else { } else {
@@ -7,12 +7,6 @@ export * from './commands/types';
export { Editor as BlockEditor } from './editor'; export { Editor as BlockEditor } from './editor';
export * from './selection'; export * from './selection';
export { BlockDropPlacement, HookType, GroupDirection } from './types'; export { BlockDropPlacement, HookType, GroupDirection } from './types';
export type { export type { Plugin, PluginCreator, PluginHooks, Virgo } from './types';
BlockDomInfo,
Plugin,
PluginCreator,
PluginHooks,
Virgo,
} from './types';
export { BaseView, getTextHtml, getTextProperties } from './views/base-view'; export { BaseView, getTextHtml, getTextProperties } from './views/base-view';
export type { ChildrenView, CreateView } from './views/base-view'; export type { ChildrenView, CreateView } from './views/base-view';
@@ -1,6 +1,5 @@
import { DragEvent } from 'react';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { HooksRunner, HookType, BlockDomInfo, PluginHooks } from '../types'; import { HooksRunner, HookType, PluginHooks } from '../types';
export class Hooks implements HooksRunner, PluginHooks { export class Hooks implements HooksRunner, PluginHooks {
private _subject: Record<string, Subject<unknown>> = {}; private _subject: Record<string, Subject<unknown>> = {};
@@ -183,14 +183,6 @@ export enum HookType {
ON_ROOTNODE_SCROLL = 'onRootNodeScroll', ON_ROOTNODE_SCROLL = 'onRootNodeScroll',
} }
export interface BlockDomInfo {
blockId: string;
dom: HTMLElement;
type: BlockFlavorKeys;
rect: DOMRect;
properties: Record<string, unknown>;
}
// Editor's various callbacks, used in Editor // Editor's various callbacks, used in Editor
export interface HooksRunner { export interface HooksRunner {
init: () => void; init: () => void;
@@ -82,6 +82,11 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
const checkIfShowCommandMenu = useCallback( const checkIfShowCommandMenu = useCallback(
async (event: React.KeyboardEvent<HTMLDivElement>) => { async (event: React.KeyboardEvent<HTMLDivElement>) => {
const { type, anchorNode } = editor.selection.currentSelectInfo; const { type, anchorNode } = editor.selection.currentSelectInfo;
// console.log(await editor.getBlockById(anchorNode.id));
const activeBlock = await editor.getBlockById(anchorNode.id);
if (activeBlock.type === Protocol.Block.Type.page) {
return;
}
if (event.key === '/' && type === 'Range') { if (event.key === '/' && type === 'Range') {
if (anchorNode) { if (anchorNode) {
const text = editor.blockHelper.getBlockTextBeforeSelection( const text = editor.blockHelper.getBlockTextBeforeSelection(
@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Protocol } from '@toeverything/datasource/db-service';
import { import {
MuiClickAwayListener as ClickAwayListener, MuiClickAwayListener as ClickAwayListener,
MuiGrow as Grow, MuiGrow as Grow,
@@ -22,8 +23,14 @@ export const InlineMenuContainer = ({ editor }: InlineMenuContainerProps) => {
useEffect(() => { useEffect(() => {
// const unsubscribe = editor.selection.onSelectionChange(info => { // const unsubscribe = editor.selection.onSelectionChange(info => {
const unsubscribe = editor.selection.onSelectEnd(info => { const unsubscribe = editor.selection.onSelectEnd(async info => {
const { type, browserSelection, anchorNode } = info; const { type, browserSelection, anchorNode } = info;
if (anchorNode) {
const activeBlock = await editor.getBlockById(anchorNode.id);
if (activeBlock.type === Protocol.Block.Type.page) {
return;
}
}
if ( if (
type === 'None' || type === 'None' ||
!anchorNode || !anchorNode ||
@@ -1,4 +1,4 @@
import React, { useState } from 'react'; import { useMemo } from 'react';
import { Virgo, PluginHooks } from '@toeverything/framework/virgo'; import { Virgo, PluginHooks } from '@toeverything/framework/virgo';
import { Cascader, CascaderItemProps } from '@toeverything/components/ui'; import { Cascader, CascaderItemProps } from '@toeverything/components/ui';
import { TurnIntoMenu } from './TurnIntoMenu'; import { TurnIntoMenu } from './TurnIntoMenu';
@@ -18,42 +18,43 @@ interface LeftMenuProps {
} }
export function LeftMenu(props: LeftMenuProps) { export function LeftMenu(props: LeftMenuProps) {
const { editor, anchorEl, hooks, blockId } = props; const { editor, anchorEl, hooks, blockId, onClose } = props;
const menu: CascaderItemProps[] = [ const menu: CascaderItemProps[] = useMemo(
{ () => [
title: 'Delete', {
callback: () => { title: 'Delete',
editor.commands.blockCommands.removeBlock(blockId); callback: () => {
editor.commands.blockCommands.removeBlock(blockId);
},
shortcut: 'Del',
icon: <DeleteCashBinIcon />,
}, },
shortcut: 'Del', {
icon: <DeleteCashBinIcon />, title: 'Turn into',
}, subItems: [],
{ children: (
title: 'Turn into', <TurnIntoMenu
subItems: [], editor={editor}
children: ( hooks={hooks}
<TurnIntoMenu blockId={blockId}
editor={editor} onClose={() => {
hooks={hooks} onClose();
blockId={blockId} editor.selection.setSelectedNodesIds([]);
onClose={() => { }}
props.onClose(); />
editor.selection.setSelectedNodesIds([]); ),
}} icon: <TurnIntoIcon />,
/>
),
icon: <TurnIntoIcon />,
},
{
title: 'Divide Here As A New Group',
icon: <UngroupIcon />,
callback: () => {
editor.commands.blockCommands.splitGroupFromBlock(blockId);
}, },
}, {
].filter(v => v); title: 'Divide Here As A New Group',
icon: <UngroupIcon />,
const [menuList, setMenuList] = useState<CascaderItemProps[]>(menu); callback: () => {
editor.commands.blockCommands.splitGroupFromBlock(blockId);
},
},
],
[editor, hooks, blockId, onClose]
);
// const filterItems = ( // const filterItems = (
// value: string, // value: string,
@@ -90,7 +91,7 @@ export function LeftMenu(props: LeftMenuProps) {
<> <>
{props.children} {props.children}
<Cascader <Cascader
items={menuList} items={menu}
anchorEl={anchorEl} anchorEl={anchorEl}
placement="bottom-start" placement="bottom-start"
open={Boolean(anchorEl)} open={Boolean(anchorEl)}
@@ -6,14 +6,15 @@ import {
type DragEvent, type DragEvent,
type ReactNode, type ReactNode,
type CSSProperties, type CSSProperties,
useCallback,
} from 'react'; } from 'react';
import { import {
Virgo, Virgo,
BlockDomInfo,
PluginHooks, PluginHooks,
BlockDropPlacement, BlockDropPlacement,
LINE_GAP, LINE_GAP,
AsyncBlock,
} from '@toeverything/framework/virgo'; } from '@toeverything/framework/virgo';
import { Button } from '@toeverything/components/common'; import { Button } from '@toeverything/components/common';
import { styled } from '@toeverything/components/ui'; import { styled } from '@toeverything/components/ui';
@@ -25,6 +26,11 @@ import { MENU_WIDTH } from './menu-config';
const MENU_BUTTON_OFFSET = 4; const MENU_BUTTON_OFFSET = 4;
export interface BlockDomInfo {
block: AsyncBlock;
rect: DOMRect;
}
export type LineInfoSubject = Subject< export type LineInfoSubject = Subject<
| { | {
direction: BlockDropPlacement; direction: BlockDropPlacement;
@@ -138,11 +144,11 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
if (block == null) return; if (block == null) return;
setRootRect(editor.container.getBoundingClientRect()); setRootRect(editor.container.getBoundingClientRect());
const dragImage = await editor.blockHelper.getBlockDragImg( const dragImage = await editor.blockHelper.getBlockDragImg(
block.blockId block.block.id
); );
if (dragImage) { if (dragImage) {
event.dataTransfer.setDragImage(dragImage, -50, -10); event.dataTransfer.setDragImage(dragImage, -50, -10);
editor.dragDropManager.setDragBlockInfo(event, block.blockId); editor.dragDropManager.setDragBlockInfo(event, block.block.id);
} }
}; };
@@ -154,16 +160,18 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
const onClick = (event: MouseEvent<Element>) => { const onClick = (event: MouseEvent<Element>) => {
if (block == null) return; if (block == null) return;
const currentTarget = event.currentTarget; const currentTarget = event.currentTarget;
editor.selection.setSelectedNodesIds([block.blockId]); editor.selection.setSelectedNodesIds([block.block.id]);
setVisible(true); setVisible(true);
setAnchorEl(currentTarget); setAnchorEl(currentTarget);
}; };
const onClose = useCallback(() => setAnchorEl(undefined), [setAnchorEl]);
useEffect(() => { useEffect(() => {
const sub = blockInfo const sub = blockInfo
.pipe( .pipe(
distinctUntilChanged( distinctUntilChanged(
(prev, curr) => prev?.blockId === curr?.blockId (prev, curr) => prev?.block.id === curr?.block.id
) )
) )
.subscribe(block => { .subscribe(block => {
@@ -185,7 +193,7 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
setRootRect(editor.container.getBoundingClientRect()); setRootRect(editor.container.getBoundingClientRect());
setLine(prev => { setLine(prev => {
if ( if (
prev?.blockInfo.blockId !== blockInfo.blockId || prev?.blockInfo.block.id !== blockInfo.block.id ||
prev?.direction !== direction prev?.direction !== direction
) { ) {
return { return {
@@ -224,8 +232,8 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
anchorEl={anchorEl} anchorEl={anchorEl}
editor={props.editor} editor={props.editor}
hooks={props.hooks} hooks={props.hooks}
onClose={() => setAnchorEl(undefined)} onClose={onClose}
blockId={block.blockId} blockId={block.block.id}
> >
<Draggable onClick={onClick}> <Draggable onClick={onClick}>
<HandleChildIcon /> <HandleChildIcon />
@@ -1,12 +1,15 @@
import { BlockDomInfo, HookType } from '@toeverything/framework/virgo'; import { HookType, BlockDropPlacement } from '@toeverything/framework/virgo';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { BasePlugin } from '../../base-plugin'; import { BasePlugin } from '../../base-plugin';
import { ignoreBlockTypes } from './menu-config'; import { ignoreBlockTypes } from './menu-config';
import { LineInfoSubject, LeftMenuDraggable } from './LeftMenuDraggable'; import {
LineInfoSubject,
LeftMenuDraggable,
BlockDomInfo,
} from './LeftMenuDraggable';
import { PluginRenderRoot } from '../../utils'; import { PluginRenderRoot } from '../../utils';
import { Subject } from 'rxjs'; import { Subject, throttleTime } from 'rxjs';
import { domToRect, last, Point, throttle } from '@toeverything/utils'; import { domToRect, last, Point } from '@toeverything/utils';
import { BlockDropPlacement } from '@toeverything/framework/virgo';
const DRAG_THROTTLE_DELAY = 150; const DRAG_THROTTLE_DELAY = 150;
export class LeftMenuPlugin extends BasePlugin { export class LeftMenuPlugin extends BasePlugin {
private _mousedown?: boolean; private _mousedown?: boolean;
@@ -58,15 +61,10 @@ export class LeftMenuPlugin extends BasePlugin {
this.hooks.get(HookType.ON_ROOTNODE_DROP).subscribe(this._onDrop) this.hooks.get(HookType.ON_ROOTNODE_DROP).subscribe(this._onDrop)
); );
this.sub.add( this.sub.add(
this.hooks.get(HookType.ON_ROOTNODE_DRAG_OVER).subscribe( this.hooks
throttle( .get(HookType.ON_ROOTNODE_DRAG_OVER)
this._handleRootNodeDragover.bind(this), .pipe(throttleTime(DRAG_THROTTLE_DELAY))
DRAG_THROTTLE_DELAY, .subscribe(this._handleRootNodeDragover)
{
leading: true,
}
)
)
); );
} }
@@ -83,11 +81,8 @@ export class LeftMenuPlugin extends BasePlugin {
this._lineInfo.next({ this._lineInfo.next({
direction, direction,
blockInfo: { blockInfo: {
blockId: block.id, block,
dom: block.dom,
type: block.type,
rect: block.dom.getBoundingClientRect(), rect: block.dom.getBoundingClientRect(),
properties: block.getProperties(),
}, },
}); });
} else if (!isOuter) { } else if (!isOuter) {
@@ -118,11 +113,8 @@ export class LeftMenuPlugin extends BasePlugin {
this._lineInfo.next({ this._lineInfo.next({
direction, direction,
blockInfo: { blockInfo: {
blockId: block.id, block,
dom: block.dom,
rect: block.dom.getBoundingClientRect(), rect: block.dom.getBoundingClientRect(),
type: block.type,
properties: block.getProperties(),
}, },
}); });
}; };
@@ -171,11 +163,8 @@ export class LeftMenuPlugin extends BasePlugin {
} }
} }
this._blockInfo.next({ this._blockInfo.next({
blockId: node.id, block: node,
dom: node.dom,
rect: node.dom.getBoundingClientRect(), rect: node.dom.getBoundingClientRect(),
type: node.type,
properties: node.getProperties(),
}); });
}; };
@@ -1,5 +1,4 @@
import React, { useEffect, useState, useCallback, useRef } from 'react'; import React, { useEffect, useState, useCallback, useRef } from 'react';
import style9 from 'style9';
import { Virgo, PluginHooks, HookType } from '@toeverything/framework/virgo'; import { Virgo, PluginHooks, HookType } from '@toeverything/framework/virgo';
import { import {
@@ -1,33 +1,34 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { BasePlugin } from '../../base-plugin'; import { BasePlugin } from '../../base-plugin';
import { PluginRenderRoot } from '../../utils';
import { ReferenceMenu } from './ReferenceMenu'; import { ReferenceMenu } from './ReferenceMenu';
const PLUGIN_NAME = 'reference-menu'; const PLUGIN_NAME = 'reference-menu';
export class ReferenceMenuPlugin extends BasePlugin { export class ReferenceMenuPlugin extends BasePlugin {
private root?: Root; private _root?: PluginRenderRoot;
public static override get pluginName(): string { public static override get pluginName(): string {
return PLUGIN_NAME; return PLUGIN_NAME;
} }
protected override _onRender(): void { protected override _onRender(): void {
const container = document.createElement('div'); this._root = new PluginRenderRoot({
// TODO: remove name: PLUGIN_NAME,
container.classList.add(`id-${PLUGIN_NAME}`); render: this.editor.reactRenderRoot.render,
// this.editor.attachElement(this.menu_container); });
window.document.body.appendChild(container); this._root.mount();
this.root = createRoot(container);
this.render_reference_menu();
}
private render_reference_menu(): void { this._root?.render(
this.root?.render(
<StrictMode> <StrictMode>
<ReferenceMenu editor={this.editor} hooks={this.hooks} /> <ReferenceMenu editor={this.editor} hooks={this.hooks} />
</StrictMode> </StrictMode>
); );
} }
public override dispose() {
this._root?.unmount();
super.dispose();
}
} }
@@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import style9 from 'style9';
import { MuiClickAwayListener } from '@toeverything/components/ui'; import { MuiClickAwayListener, styled } from '@toeverything/components/ui';
import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo'; import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo';
import { Point } from '@toeverything/utils'; import { Point } from '@toeverything/utils';
@@ -120,8 +119,7 @@ export const ReferenceMenu = ({ editor, hooks, style }: ReferenceMenuProps) => {
}; };
return ( return (
<div <ReferenceMenuWrapper
className={styles('referenceMenu')}
style={{ top: position.y, left: position.x }} style={{ top: position.y, left: position.x }}
onKeyUp={handle_keyup} onKeyUp={handle_keyup}
> >
@@ -140,13 +138,11 @@ export const ReferenceMenu = ({ editor, hooks, style }: ReferenceMenuProps) => {
/> />
</div> </div>
</MuiClickAwayListener> </MuiClickAwayListener>
</div> </ReferenceMenuWrapper>
); );
}; };
const styles = style9.create({ const ReferenceMenuWrapper = styled('div')({
referenceMenu: { position: 'absolute',
position: 'absolute', zIndex: 1,
zIndex: 1,
},
}); });
@@ -0,0 +1 @@
export { ReferenceMenuPlugin } from './Plugin';
@@ -33,9 +33,9 @@ export const LayoutHeader = () => {
<IconButton onClick={toggleInfoSidebar} size="large"> <IconButton onClick={toggleInfoSidebar} size="large">
{showSettingsSidebar ? ( {showSettingsSidebar ? (
<SideBarViewIcon />
) : (
<SideBarViewCloseIcon /> <SideBarViewCloseIcon />
) : (
<SideBarViewIcon />
)} )}
</IconButton> </IconButton>
</StyledHelper> </StyledHelper>
@@ -44,10 +44,37 @@ export const LayoutHeader = () => {
<EditorBoardSwitcher /> <EditorBoardSwitcher />
</StyledContainerForEditorBoardSwitcher> </StyledContainerForEditorBoardSwitcher>
</StyledHeaderRoot> </StyledHeaderRoot>
<StyledUnstableTips>
<StyledUnstableTipsText>
AFFiNE now under active development, the version is
UNSTABLE, please DO NOT store important data in this version
</StyledUnstableTipsText>
</StyledUnstableTips>
</StyledContainerForHeaderRoot> </StyledContainerForHeaderRoot>
); );
}; };
const StyledUnstableTips = styled('div')(({ theme }) => {
return {
width: '100%',
height: '2em',
display: 'flex',
zIndex: theme.affine.zIndex.header,
backgroundColor: '#fff8c5',
borderWidth: '1px 0',
borderColor: '#e4e49588',
borderStyle: 'solid',
};
});
const StyledUnstableTipsText = styled('span')(({ theme }) => {
return {
margin: 'auto 36px',
width: '100%',
textAlign: 'center',
};
});
const StyledContainerForHeaderRoot = styled('div')(({ theme }) => { const StyledContainerForHeaderRoot = styled('div')(({ theme }) => {
return { return {
width: '100%', width: '100%',
@@ -114,7 +141,9 @@ const StyledLogoIcon = styled(LogoIcon)(({ theme }) => {
const StyledContainerForEditorBoardSwitcher = styled('div')(({ theme }) => { const StyledContainerForEditorBoardSwitcher = styled('div')(({ theme }) => {
return { return {
width: '100%',
position: 'absolute', position: 'absolute',
left: '50%', display: 'flex',
justifyContent: 'center',
}; };
}); });
@@ -100,10 +100,10 @@ export const ContainerTabs = () => {
const StyledTabsTitlesContainer = styled('div')(({ theme }) => { const StyledTabsTitlesContainer = styled('div')(({ theme }) => {
return { return {
height: '60px',
display: 'flex', display: 'flex',
alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
marginTop: 24,
marginBottom: 24,
marginLeft: theme.affine.spacing.smSpacing, marginLeft: theme.affine.spacing.smSpacing,
marginRight: theme.affine.spacing.smSpacing, marginRight: theme.affine.spacing.smSpacing,
}; };
@@ -16,7 +16,6 @@ const StyledContainerForSettingsPanel = styled('div')(({ theme }) => {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingTop: 44,
paddingBottom: 44, paddingBottom: 44,
height: '100%', height: '100%',
}; };
@@ -3,9 +3,9 @@ import React, {
type CSSProperties, type CSSProperties,
type HTMLAttributes, type HTMLAttributes,
} from 'react'; } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import cx from 'clsx'; import cx from 'clsx';
import { CloseIcon, DocumentIcon } from '@toeverything/components/common'; import { CloseIcon } from '@toeverything/components/common';
import { import {
ArrowDropDownIcon, ArrowDropDownIcon,
ArrowRightIcon, ArrowRightIcon,
@@ -15,6 +15,7 @@ import styles from './tree-item.module.scss';
import { useFlag } from '@toeverything/datasource/feature-flags'; import { useFlag } from '@toeverything/datasource/feature-flags';
import MoreActions from './MoreActions'; import MoreActions from './MoreActions';
import { useTheme } from '@toeverything/components/ui';
export type TreeItemProps = { export type TreeItemProps = {
/** The main text to display on this line */ /** The main text to display on this line */
value: string; value: string;
@@ -61,12 +62,13 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
}, },
ref ref
) => { ) => {
const { workspace_id } = useParams(); const { workspace_id, page_id } = useParams();
const navigate = useNavigate();
const BooleanPageTreeItemMoreActions = useFlag( const BooleanPageTreeItemMoreActions = useFlag(
'BooleanPageTreeItemMoreActions', 'BooleanPageTreeItemMoreActions',
true true
); );
const theme = useTheme();
return ( return (
<li <li
ref={wrapperRef} ref={wrapperRef}
@@ -103,15 +105,18 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
</Action> </Action>
<div className={styles['ItemContent']}> <div className={styles['ItemContent']}>
<span <Link
className={styles['Text']} className={styles['Text']}
{...handleProps} {...handleProps}
onClick={() => { to={`/${workspace_id}/${pageId}`}
navigate(`/${workspace_id}/${pageId}`); style={{
...(pageId === page_id && {
color: theme.affine.palette.primary,
}),
}} }}
> >
{value} {value}
</span> </Link>
{BooleanPageTreeItemMoreActions && ( {BooleanPageTreeItemMoreActions && (
<MoreActions <MoreActions
workspaceId={workspace_id} workspaceId={workspace_id}
@@ -2,7 +2,8 @@
box-sizing: border-box; box-sizing: border-box;
padding-left: var(--spacing); padding-left: var(--spacing);
list-style: none; list-style: none;
padding: 6px 0; padding-top: 6px;
padding-bottom: 6px;
font-size: 14px; font-size: 14px;
&:hover { &:hover {
background: #f5f7f8; background: #f5f7f8;
@@ -105,6 +106,9 @@
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
appearance: none;
color: unset;
text-decoration: none;
} }
.Count { .Count {
+6 -2
View File
@@ -4,8 +4,12 @@
"license": "MIT", "license": "MIT",
"author": "DarkSky <darksky2048@gmail.com>", "author": "DarkSky <darksky2048@gmail.com>",
"dependencies": { "dependencies": {
"lib0": "^0.2.51", "lib0": "^0.2.52",
"yjs": "^13.5.39", "sql.js": "^1.7.0",
"yjs": "^13.5.41",
"y-protocols": "^1.0.5" "y-protocols": "^1.0.5"
},
"devDependencies": {
"@types/sql.js": "^1.4.3"
} }
} }
+2
View File
@@ -1 +1,3 @@
export { IndexedDBProvider } from './indexeddb';
export { WebsocketProvider } from './provider'; export { WebsocketProvider } from './provider';
export { SQLiteProvider } from './sqlite';
+185
View File
@@ -0,0 +1,185 @@
import * as Y from 'yjs';
import * as idb from 'lib0/indexeddb.js';
import * as mutex from 'lib0/mutex.js';
import { Observable } from 'lib0/observable.js';
const customStoreName = 'custom';
const updatesStoreName = 'updates';
const PREFERRED_TRIM_SIZE = 500;
const fetchUpdates = async (provider: IndexedDBProvider) => {
const [updatesStore] = idb.transact(provider.db as IDBDatabase, [
updatesStoreName,
]); // , 'readonly')
const updates = await idb.getAll(
updatesStore,
idb.createIDBKeyRangeLowerBound(provider._dbref, false)
);
Y.transact(
provider.doc,
() => {
updates.forEach(val => Y.applyUpdate(provider.doc, val));
},
provider,
false
);
const lastKey = await idb.getLastKey(updatesStore);
provider._dbref = lastKey + 1;
const cnt = await idb.count(updatesStore);
provider._dbsize = cnt;
return updatesStore;
};
const storeState = (provider: IndexedDBProvider, forceStore = true) =>
fetchUpdates(provider).then(updatesStore => {
if (forceStore || provider._dbsize >= PREFERRED_TRIM_SIZE) {
idb.addAutoKey(updatesStore, Y.encodeStateAsUpdate(provider.doc))
.then(() =>
idb.del(
updatesStore,
idb.createIDBKeyRangeUpperBound(provider._dbref, true)
)
)
.then(() =>
idb.count(updatesStore).then(cnt => {
provider._dbsize = cnt;
})
);
}
});
export class IndexedDBProvider extends Observable<string> {
doc: Y.Doc;
name: string;
private _mux: mutex.mutex;
_dbref: number;
_dbsize: number;
private _destroyed: boolean;
whenSynced: Promise<IndexedDBProvider>;
db: IDBDatabase | null;
private _db: Promise<IDBDatabase>;
private synced: boolean;
private _storeTimeout: number;
private _storeTimeoutId: NodeJS.Timeout | null;
private _storeUpdate: (update: Uint8Array, origin: any) => void;
constructor(name: string, doc: Y.Doc) {
super();
this.doc = doc;
this.name = name;
this._mux = mutex.createMutex();
this._dbref = 0;
this._dbsize = 0;
this._destroyed = false;
this.db = null;
this.synced = false;
this._db = idb.openDB(name, db =>
idb.createStores(db, [
['updates', { autoIncrement: true }],
['custom'],
])
);
this.whenSynced = this._db.then(async db => {
this.db = db;
const currState = Y.encodeStateAsUpdate(doc);
const updatesStore = await fetchUpdates(this);
await idb.addAutoKey(updatesStore, currState);
if (this._destroyed) return this;
this.emit('synced', [this]);
this.synced = true;
return this;
});
// Timeout in ms untill data is merged and persisted in idb.
this._storeTimeout = 1000;
this._storeTimeoutId = null;
this._storeUpdate = (update: Uint8Array, origin: any) => {
if (this.db && origin !== this) {
const [updatesStore] = idb.transact(
/** @type {IDBDatabase} */ this.db,
[updatesStoreName]
);
idb.addAutoKey(updatesStore, update);
if (++this._dbsize >= PREFERRED_TRIM_SIZE) {
// debounce store call
if (this._storeTimeoutId !== null) {
clearTimeout(this._storeTimeoutId);
}
this._storeTimeoutId = setTimeout(() => {
storeState(this, false);
this._storeTimeoutId = null;
}, this._storeTimeout);
}
}
};
doc.on('update', this._storeUpdate);
this.destroy = this.destroy.bind(this);
doc.on('destroy', this.destroy);
}
override destroy() {
if (this._storeTimeoutId) {
clearTimeout(this._storeTimeoutId);
}
this.doc.off('update', this._storeUpdate);
this.doc.off('destroy', this.destroy);
this._destroyed = true;
return this._db.then(db => {
db.close();
});
}
/**
* Destroys this instance and removes all data from SQLite.
*
* @return {Promise<void>}
*/
async clearData(): Promise<void> {
return this.destroy().then(() => {
idb.deleteDB(this.name);
});
}
/**
* @param {String | number | ArrayBuffer | Date} key
* @return {Promise<String | number | ArrayBuffer | Date | any>}
*/
async get(
key: string | number | ArrayBuffer | Date
): Promise<string | number | ArrayBuffer | Date | any> {
return this._db.then(db => {
const [custom] = idb.transact(db, [customStoreName], 'readonly');
return idb.get(custom, key);
});
}
/**
* @param {String | number | ArrayBuffer | Date} key
* @param {String | number | ArrayBuffer | Date} value
* @return {Promise<String | number | ArrayBuffer | Date>}
*/
async set(
key: string | number | ArrayBuffer | Date,
value: string | number | ArrayBuffer | Date
): Promise<string | number | ArrayBuffer | Date> {
return this._db.then(db => {
const [custom] = idb.transact(db, [customStoreName]);
return idb.put(custom, value, key);
});
}
/**
* @param {String | number | ArrayBuffer | Date} key
* @return {Promise<undefined>}
*/
async del(key: string | number | ArrayBuffer | Date): Promise<undefined> {
return this._db.then(db => {
const [custom] = idb.transact(db, [customStoreName]);
return idb.del(custom, key);
});
}
}
+166
View File
@@ -0,0 +1,166 @@
import * as Y from 'yjs';
import sqlite, { Database, SqlJsStatic } from 'sql.js';
import { Observable } from 'lib0/observable.js';
const PREFERRED_TRIM_SIZE = 500;
const STMTS = {
create: 'CREATE TABLE updates (key INTEGER PRIMARY KEY AUTOINCREMENT, value BLOB);',
selectAll: 'SELECT * FROM updates where key >= $idx',
selectCount: 'SELECT count(*) FROM updates',
insert: 'INSERT INTO updates VALUES (null, $data);',
delete: 'DELETE FROM updates WHERE key < $idx',
drop: 'DROP TABLE updates;',
};
const countUpdates = (db: Database) => {
const [cnt] = db.exec(STMTS.selectCount);
return cnt.values[0]?.[0] as number;
};
const clearUpdates = (db: Database, idx: number) => {
db.exec(STMTS.delete, { $idx: idx });
};
const fetchUpdates = async (provider: SQLiteProvider) => {
const db = provider.db!;
const updates = db
.exec(STMTS.selectAll, { $idx: provider._dbref })
.flatMap(val => val.values as [number, Uint8Array][])
.sort(([a], [b]) => a - b);
Y.transact(
provider.doc,
() => {
updates.forEach(([, update]) =>
Y.applyUpdate(provider.doc, update)
);
},
provider,
false
);
const lastKey = Math.max(...updates.map(([idx]) => idx));
provider._dbref = lastKey + 1;
provider._dbsize = countUpdates(db);
return db;
};
const storeState = async (provider: SQLiteProvider, forceStore = true) => {
const db = await fetchUpdates(provider);
if (forceStore || provider._dbsize >= PREFERRED_TRIM_SIZE) {
db.exec(STMTS.insert, { $data: Y.encodeStateAsUpdate(provider.doc) });
clearUpdates(db, provider._dbref);
provider._dbsize = countUpdates(db);
console.log(db.export());
}
};
let _sqliteInstance: SqlJsStatic | undefined;
let _sqliteProcessing = false;
const sleep = () => new Promise(resolve => setTimeout(resolve, 500));
const initSQLiteInstance = async () => {
while (_sqliteProcessing) {
await sleep();
}
if (_sqliteInstance) return _sqliteInstance;
_sqliteProcessing = true;
_sqliteInstance = await sqlite({
locateFile: () =>
new URL('sql.js/dist/sql-wasm.wasm', import.meta.url).href,
});
_sqliteProcessing = false;
return _sqliteInstance;
};
export class SQLiteProvider extends Observable<string> {
doc: Y.Doc;
name: string;
_dbref: number;
_dbsize: number;
private _destroyed: boolean;
whenSynced: Promise<SQLiteProvider>;
db: Database | null;
private _db: Promise<Database>;
synced: boolean;
_storeTimeout: number;
_storeTimeoutId: NodeJS.Timeout | null;
_storeUpdate: (update: Uint8Array, origin: any) => void;
constructor(dbname: string, doc: Y.Doc) {
super();
this.doc = doc;
this.name = dbname;
this._dbref = 0;
this._dbsize = 0;
this._destroyed = false;
this.db = null;
this.synced = false;
this._db = initSQLiteInstance().then(db => {
const sqlite = new db.Database();
return sqlite.run(STMTS.create);
});
this.whenSynced = this._db.then(async db => {
this.db = db;
const currState = Y.encodeStateAsUpdate(doc);
await fetchUpdates(this);
db.exec(STMTS.insert, { $data: currState });
if (this._destroyed) return this;
this.emit('synced', [this]);
this.synced = true;
return this;
});
// Timeout in ms untill data is merged and persisted in idb.
this._storeTimeout = 1000;
this._storeTimeoutId = null;
this._storeUpdate = (update: Uint8Array, origin: any) => {
if (this.db && origin !== this) {
this.db.exec(STMTS.insert, { $data: update });
if (++this._dbsize >= PREFERRED_TRIM_SIZE) {
// debounce store call
if (this._storeTimeoutId !== null) {
clearTimeout(this._storeTimeoutId);
}
this._storeTimeoutId = setTimeout(() => {
storeState(this, false);
this._storeTimeoutId = null;
}, this._storeTimeout);
}
}
};
doc.on('update', this._storeUpdate);
this.destroy = this.destroy.bind(this);
doc.on('destroy', this.destroy);
}
override destroy(): Promise<void> {
if (this._storeTimeoutId) {
clearTimeout(this._storeTimeoutId);
}
this.doc.off('update', this._storeUpdate);
this.doc.off('destroy', this.destroy);
this._destroyed = true;
return this._db.then(db => {
db.close();
});
}
// Destroys this instance and removes all data from SQLite.
async clearData(): Promise<void> {
return this._db.then(db => {
db.exec(STMTS.drop);
return this.destroy();
});
}
}
+4 -5
View File
@@ -11,10 +11,9 @@
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"file-selector": "^0.6.0", "file-selector": "^0.6.0",
"flexsearch": "^0.7.21", "flexsearch": "^0.7.21",
"lib0": "^0.2.51", "lib0": "^0.2.52",
"lru-cache": "^7.12.0", "lru-cache": "^7.13.2",
"ts-debounce": "^4.0.0", "ts-debounce": "^4.0.0"
"y-indexeddb": "^9.0.8"
}, },
"dependencies": { "dependencies": {
"@types/flexsearch": "^0.7.3", "@types/flexsearch": "^0.7.3",
@@ -29,6 +28,6 @@
"sift": "^16.0.0", "sift": "^16.0.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"y-protocols": "^1.0.5", "y-protocols": "^1.0.5",
"yjs": "^13.5.39" "yjs": "^13.5.41"
} }
} }
+15 -8
View File
@@ -7,7 +7,6 @@ import { fromEvent } from 'file-selector';
import LRUCache from 'lru-cache'; import LRUCache from 'lru-cache';
import { debounce } from 'ts-debounce'; import { debounce } from 'ts-debounce';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { IndexeddbPersistence } from 'y-indexeddb';
import { Awareness } from 'y-protocols/awareness.js'; import { Awareness } from 'y-protocols/awareness.js';
import { import {
Doc, Doc,
@@ -19,7 +18,11 @@ import {
snapshot, snapshot,
} from 'yjs'; } from 'yjs';
import { WebsocketProvider } from '@toeverything/datasource/jwt-rpc'; import {
IndexedDBProvider,
SQLiteProvider,
WebsocketProvider,
} from '@toeverything/datasource/jwt-rpc';
import { import {
AsyncDatabaseAdapter, AsyncDatabaseAdapter,
@@ -46,8 +49,9 @@ const logger = getLogger('BlockDB:yjs');
type YjsProviders = { type YjsProviders = {
awareness: Awareness; awareness: Awareness;
idb: IndexeddbPersistence; idb: IndexedDBProvider;
binariesIdb: IndexeddbPersistence; binariesIdb: IndexedDBProvider;
fstore?: SQLiteProvider;
ws?: WebsocketProvider; ws?: WebsocketProvider;
backend: string; backend: string;
gatekeeper: GateKeeper; gatekeeper: GateKeeper;
@@ -117,7 +121,9 @@ async function _initYjsDatabase(
const doc = new Doc({ autoLoad: true, shouldLoad: true }); const doc = new Doc({ autoLoad: true, shouldLoad: true });
const idbp = new IndexeddbPersistence(workspace, doc).whenSynced; const idbp = new IndexedDBProvider(workspace, doc).whenSynced;
const fsp: SQLiteProvider | undefined = undefined; // new SQLiteProvider(workspace, doc).whenSynced;
const wsp = _initWebsocketProvider( const wsp = _initWebsocketProvider(
backend, backend,
workspace, workspace,
@@ -126,10 +132,10 @@ async function _initYjsDatabase(
params params
); );
const [idb, [awareness, ws]] = await Promise.all([idbp, wsp]); const [idb, [awareness, ws], fstore] = await Promise.all([idbp, wsp, fsp]);
const binaries = new Doc({ autoLoad: true, shouldLoad: true }); const binaries = new Doc({ autoLoad: true, shouldLoad: true });
const binariesIdb = await new IndexeddbPersistence( const binariesIdb = await new IndexedDBProvider(
`${workspace}_binaries`, `${workspace}_binaries`,
binaries binaries
).whenSynced; ).whenSynced;
@@ -147,6 +153,7 @@ async function _initYjsDatabase(
awareness, awareness,
idb, idb,
binariesIdb, binariesIdb,
fstore,
ws, ws,
backend, backend,
gatekeeper, gatekeeper,
@@ -374,7 +381,7 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
}; };
check(); check();
}); });
await new IndexeddbPersistence(this._provider.idb.name, doc) await new IndexedDBProvider(this._provider.idb.name, doc)
.whenSynced; .whenSynced;
applyUpdate(doc, new Uint8Array(binary)); applyUpdate(doc, new Uint8Array(binary));
await update_check; await update_check;
+24 -8
View File
@@ -19,8 +19,8 @@ declare const JWT_DEV: boolean;
const logger = getLogger('BlockDB:block'); const logger = getLogger('BlockDB:block');
const logger_debug = getLogger('debug:BlockDB:block'); const logger_debug = getLogger('debug:BlockDB:block');
const GET_BLOCK = Symbol('GET_BLOCK'); const _GET_BLOCK = Symbol('GET_BLOCK');
const SET_PARENT = Symbol('SET_PARENT'); const _SET_PARENT = Symbol('SET_PARENT');
export class AbstractBlock< export class AbstractBlock<
B extends BlockInstance<C>, B extends BlockInstance<C>,
@@ -47,6 +47,13 @@ export class AbstractBlock<
this._parentListener = new Map(); this._parentListener = new Map();
this._parent = parent; this._parent = parent;
JWT_DEV && logger_debug(`init: exists ${this._id}`); JWT_DEV && logger_debug(`init: exists ${this._id}`);
if (parent) {
parent.addChildrenListener(this._id, states => {
if (states.get(this._id) === 'delete') {
this._emitParent(parent._id, 'delete');
}
});
}
} }
public get root() { public get root() {
@@ -176,18 +183,27 @@ export class AbstractBlock<
return this.#block.creator; return this.#block.creator;
} }
[GET_BLOCK]() { [_GET_BLOCK]() {
return this.#block; return this.#block;
} }
[SET_PARENT](parent: AbstractBlock<B, C>) { private _emitParent(
this._parent = parent; parentId: string,
const states: Map<string, 'update'> = new Map([[parent.id, 'update']]); type: 'update' | 'delete' = 'update'
) {
const states: Map<string, 'update' | 'delete'> = new Map([
[parentId, type],
]);
for (const listener of this._parentListener.values()) { for (const listener of this._parentListener.values()) {
listener(states); listener(states);
} }
} }
[_SET_PARENT](parent: AbstractBlock<B, C>) {
this._parent = parent;
this._emitParent(parent.id);
}
/** /**
* Get document index tags * Get document index tags
*/ */
@@ -258,8 +274,8 @@ export class AbstractBlock<
throw new Error('insertChildren: binary not allow insert children'); throw new Error('insertChildren: binary not allow insert children');
} }
this.#block.insertChildren(block[GET_BLOCK](), position); this.#block.insertChildren(block[_GET_BLOCK](), position);
block[SET_PARENT](this); block[_SET_PARENT](this);
} }
public hasChildren(id: string): boolean { public hasChildren(id: string): boolean {
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"aws-sdk": "^2.1167.0", "aws-sdk": "^2.1189.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"util": "^0.12.4" "util": "^0.12.4"
}, },
+2 -2
View File
@@ -6,7 +6,7 @@
"jotai": "^1.7.4" "jotai": "^1.7.4"
}, },
"devDependencies": { "devDependencies": {
"authing-js-sdk": "^4.23.33", "authing-js-sdk": "^4.23.35",
"firebase": "^9.8.4" "firebase": "^9.9.2"
} }
} }
+10 -5
View File
@@ -4,12 +4,14 @@
"license": "MIT", "license": "MIT",
"author": "AFFiNE <developer@affine.pro>", "author": "AFFiNE <developer@affine.pro>",
"scripts": { "scripts": {
"start": "env-cmd -f .env.local-dev nx serve ligo-virgo", "e2e": "env-cmd -f .github/env/.env.e2e nx e2e ligo-virgo-e2e --watch",
"e2e:ci": "env-cmd -f .github/env/.env.e2e nx e2e ligo-virgo-e2e --prod",
"start": "env-cmd -f .github/env/.env.local-dev nx serve ligo-virgo",
"start:affine": "nx serve ligo-virgo", "start:affine": "nx serve ligo-virgo",
"start:keck": "nx serve keck", "start:keck": "nx serve keck",
"start:venus": "nx serve venus", "start:venus": "nx serve venus",
"build": "nx build ligo-virgo", "build": "nx build ligo-virgo",
"build:local": "env-cmd -f .env.local-dev nx build ligo-virgo", "build:local": "env-cmd -f .github/env/.env.local-dev nx build ligo-virgo",
"build:keck": "nx build keck", "build:keck": "nx build keck",
"build:venus": "nx build venus", "build:venus": "nx build venus",
"build:analytic": "cross-env BUNDLE_ANALYZER=true nx build --skip-nx-cache", "build:analytic": "cross-env BUNDLE_ANALYZER=true nx build --skip-nx-cache",
@@ -80,10 +82,11 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
}, },
"devDependencies": { "devDependencies": {
"@firebase/auth": "^0.20.4", "@firebase/auth": "^0.20.5",
"@headlessui/react": "^1.6.5", "@headlessui/react": "^1.6.5",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@nrwl/cli": "^14.4.0", "@nrwl/cli": "^14.4.0",
"@nrwl/cypress": "^14.4.0",
"@nrwl/eslint-plugin-nx": "^14.4.0", "@nrwl/eslint-plugin-nx": "^14.4.0",
"@nrwl/jest": "^14.4.0", "@nrwl/jest": "^14.4.0",
"@nrwl/js": "^14.4.0", "@nrwl/js": "^14.4.0",
@@ -117,18 +120,20 @@
"compression-webpack-plugin": "^10.0.0", "compression-webpack-plugin": "^10.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-minimizer-webpack-plugin": "^4.0.0", "css-minimizer-webpack-plugin": "^4.0.0",
"cypress": "^10.4.0",
"cz-customizable": "^5.3.0", "cz-customizable": "^5.3.0",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint-config-next": "12.2.0", "eslint-config-next": "12.2.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-cypress": "^2.10.3",
"eslint-plugin-filename-rules": "^1.2.0", "eslint-plugin-filename-rules": "^1.2.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.0", "eslint-plugin-jsx-a11y": "^6.6.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.30.1", "eslint-plugin-react": "^7.30.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"firebase": "^9.8.4", "firebase": "^9.9.2",
"fs-extra": "^10.1.0", "fs-extra": "^10.1.0",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"husky": "^8.0.1", "husky": "^8.0.1",
@@ -142,7 +147,7 @@
"ts-jest": "^28.0.5", "ts-jest": "^28.0.5",
"ts-node": "^10.8.2", "ts-node": "^10.8.2",
"typescript": "^4.7.4", "typescript": "^4.7.4",
"webpack": "^5.73.0", "webpack": "^5.74.0",
"webpack-bundle-analyzer": "^4.5.0" "webpack-bundle-analyzer": "^4.5.0"
}, },
"lint-staged": { "lint-staged": {
+1071 -596
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,7 +1,7 @@
import got from 'got'; import got from 'got';
const STAGE_HOST = 'https://nightly.affine.pro/'; const STAGE_HOST = 'https://nightly.affine.pro/';
if (process.env.CF_PAGES_BRANCH === 'master') { if (['master', 'develop'].includes(process.env.CF_PAGES_BRANCH)) {
const message = `Daily builds: New deployment of Ligo-Virgo version [${process.env.CF_PAGES_COMMIT_SHA}](${STAGE_HOST}), this [${STAGE_HOST}](${STAGE_HOST}) link always points to the latest version deployment, to access the old version, please go to Github to view the deployment record.`; const message = `Daily builds: New deployment of Ligo-Virgo version [${process.env.CF_PAGES_COMMIT_SHA}](${STAGE_HOST}), this [${STAGE_HOST}](${STAGE_HOST}) link always points to the latest version deployment, to access the old version, please go to Github to view the deployment record.`;
const url = `https://api.telegram.org/bot${process.env.BOT_TOKEN}/sendMessage`; const url = `https://api.telegram.org/bot${process.env.BOT_TOKEN}/sendMessage`;
+1
View File
@@ -28,6 +28,7 @@
"framework-virgo": "libs/framework/virgo", "framework-virgo": "libs/framework/virgo",
"keck": "apps/keck", "keck": "apps/keck",
"ligo-virgo": "apps/ligo-virgo", "ligo-virgo": "apps/ligo-virgo",
"ligo-virgo-e2e": "apps/ligo-virgo-e2e",
"utils": "libs/utils", "utils": "libs/utils",
"venus": "apps/venus" "venus": "apps/venus"
} }