From a22aeae644ce94217383777e9fad4b1f32b2a362 Mon Sep 17 00:00:00 2001 From: CJSS Date: Tue, 9 Aug 2022 14:59:41 +0800 Subject: [PATCH 01/10] Update README.md --- README.md | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 666d76fabc..7f93b10a0b 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,8 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066 # How to use -If you have experience in front-end development, please [refer to here](https://affine.gitbook.io/affine/basic-documentation/contribute-to-affine); if you want to experience our latest version, please wait a moment, we will launch a web version in the near future. -And, thanks to Lee who [made a desktop build with Tauri](https://github.com/m1911star/affine-client) for you to try out. +If you have experience in front-end development, you may wish to refer to our [documentation](https://affine.gitbook.io/affine/basic-documentation/contribute-to-affine) to learn more about deploying your own version or contributing further to development. For those intersting in trying our latest version, please bear with us as we are planning to launch a web version soon. +Also, thanks to Lee who has made a [desktop build with Tauri](https://github.com/m1911star/affine-client) for you to try out. Please notice that AFFiNE is still under Alpha stage and is not ready for production use. # Table of contents @@ -85,13 +85,13 @@ Please notice that AFFiNE is still under Alpha stage and is not ready for produc ## Create your story -We want your data always to be yours, and we don't want to make any sacrifice to your accessibility. Your data is always local-stored first, yet we support real-time collaboration on a peer-to-peer basis. We don't think "privacy-first" is a good excuse for not supporting modern web features. -Collaboration isn't only necessary for teams -- you may take and insert pics on your phone, then edit them on your desktop, and share them with your collaborators. -Affine is fully built with web technologies so that consistency and accessibility are always guaranteed on Mac, Windows and Linux. The local file system support will be available when version 0.0.1beta is released. +We want your data always to be yours, without any sacrifice to your accessibility. Your data is always stored local first, yet we support real-time collaboration on a peer-to-peer basis. We don't think "privacy-first" is a good excuse for not supporting modern web features. +And when it comes to collaboration, these features are not just necessarily for teams -- you can take and insert pictures on your phone, edit them from your desktop, and then share them with your collaborators. +Affine is fully built with web technologies to ensure consistency and accessibility on Mac, Windows and Linux. The local file system support will be available when version 0.0.1beta is released. # Documentation -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/). +AFFiNE is not yet ready for production use. For installation, you may check how to build or deploy AFFiNE from our [quick-start](https://affine.gitbook.io/affine/basic-documentation/contribute-to-affine/quick-start) guide. Alternatively, you can view our [full documentation](https://affine.gitbook.io/affine/). ## Getting Started with development @@ -107,28 +107,28 @@ Get our latest [release notes](https://github.com/toeverything/AFFiNE/wiki) from # Feature requests -Please go to [Feature request](https://github.com/toeverything/AFFiNE/issues). +Please go to [feature requests](https://github.com/toeverything/AFFiNE/issues). # FAQ -Get quick help on [Telegram](https://t.me/affineworkos) and [Discord](https://discord.gg/yz6tGVsf5p) along with other developers and contributors. +Get quick help on [Telegram](https://t.me/affineworkos) or [Discord](https://discord.gg/yz6tGVsf5p) and join our community of developers and contributors. -Latest news and technology sharing on [Twitter](https://twitter.com/AffineOfficial), [Medium](https://medium.com/@affineworkos) and [AFFiNE Blog](https://blog.affine.pro/). +Our latest news can be found on [Twitter](https://twitter.com/AffineOfficial), [Medium](https://medium.com/@affineworkos) and the [AFFiNE Blog](https://blog.affine.pro/). # The Philosophy of AFFiNE Timothy Berners-Lee once taught us about the idea of the semantic web, where all the data can be interpreted in any form while the "truth" is kept. This gives our best image of an ideal knowledge base by far, that sorting of information, planning of project and goals as well as creating of knowledge can be all together. We have witnessed waves of paradigm shift so many times. At first, everything was noted on office-like apps or DSL like LaTeX, then we found todo-list apps and WYSIWYG markdown editors better for writing and planning. Finally, here comes Notion and Miro, who take advantage of the idea of blocks to further liberate our creativity. -It is all perfect... If there are not so many waste operations and redundant information. And, we insist that privacy first should always be given by default. +It is all perfect... without waste operations and redundant information. And, we insist that privacy first should always be given by default. That's why we are making AFFiNE. Some of the most important features are: - Transformable - - Every block can be transformed equally well as a database - - e.g. you can now set up a to-do with MarkDown in text view and edit it in kanban view. - - Every doc can be turned into a whiteboard + - Every block can be transformed equally + - e.g. you can create a todo in Markdown in the text view and then later edit it in the kanban view. + - Every document can be turned into a whiteboard - 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 - - The basic element of affine are blocks, not pages. + - The basic elements of AFFiNE are blocks, not pages. - Blocks can be directly reused and synced between pages. - Pages and blocks are searched and organized based on connected graphs, not tree-like paths. - Dual-link and semantic search are fully supported. @@ -136,8 +136,7 @@ That's why we are making AFFiNE. Some of the most important features are: - Data is always stored locally by default - CRDTs are applied so that peer-to-peer collaboration is possible. -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. +We appreciate the ideas of Monday, Airtable and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a signle source of truth. We would like to give special thanks to the innovators and pioneers who greatly inspired us: From 9fa914c6d07038929a2455f4b5a200fba1948185 Mon Sep 17 00:00:00 2001 From: CJSS Date: Tue, 9 Aug 2022 15:01:38 +0800 Subject: [PATCH 02/10] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7f93b10a0b..5018d0ba4d 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ That's why we are making AFFiNE. Some of the most important features are: - Data is always stored locally by default - CRDTs are applied so that peer-to-peer collaboration is possible. -We appreciate the ideas of Monday, Airtable and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a signle source of truth. +We appreciate the ideas of Monday, Airtable, and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a signle source of truth. We would like to give special thanks to the innovators and pioneers who greatly inspired us: From 0083b7445a237f2ed87cd42e6a55aedba456304c Mon Sep 17 00:00:00 2001 From: DarkSky Date: Tue, 9 Aug 2022 18:58:18 +0800 Subject: [PATCH 03/10] feat: basic db support --- .vscode/settings.json | 1 + apps/ligo-virgo/webpack.config.js | 6 + libs/datasource/jwt-rpc/package.json | 4 + libs/datasource/jwt-rpc/src/index.ts | 2 + libs/datasource/jwt-rpc/src/indexeddb.ts | 185 +++++++++++++++++++ libs/datasource/jwt-rpc/src/sqlite.ts | 166 +++++++++++++++++ libs/datasource/jwt/package.json | 3 +- libs/datasource/jwt/src/adapter/yjs/index.ts | 23 ++- pnpm-lock.yaml | 79 ++++++-- 9 files changed, 445 insertions(+), 24 deletions(-) create mode 100644 libs/datasource/jwt-rpc/src/indexeddb.ts create mode 100644 libs/datasource/jwt-rpc/src/sqlite.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c3dd39eb0..c3d35c66ff 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "cssmodule", "datasource", "fflate", + "fstore", "groq", "howpublished", "immer", diff --git a/apps/ligo-virgo/webpack.config.js b/apps/ligo-virgo/webpack.config.js index 3903284128..9838e4dfb9 100644 --- a/apps/ligo-virgo/webpack.config.js +++ b/apps/ligo-virgo/webpack.config.js @@ -148,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); config.plugins = [ diff --git a/libs/datasource/jwt-rpc/package.json b/libs/datasource/jwt-rpc/package.json index 152a76baf9..dd6ff7a96a 100644 --- a/libs/datasource/jwt-rpc/package.json +++ b/libs/datasource/jwt-rpc/package.json @@ -5,7 +5,11 @@ "author": "DarkSky ", "dependencies": { "lib0": "^0.2.52", + "sql.js": "^1.7.0", "yjs": "^13.5.41", "y-protocols": "^1.0.5" + }, + "devDependencies": { + "@types/sql.js": "^1.4.3" } } diff --git a/libs/datasource/jwt-rpc/src/index.ts b/libs/datasource/jwt-rpc/src/index.ts index fd2c87cd68..8fda0de311 100644 --- a/libs/datasource/jwt-rpc/src/index.ts +++ b/libs/datasource/jwt-rpc/src/index.ts @@ -1 +1,3 @@ +export { IndexedDBProvider } from './indexeddb'; export { WebsocketProvider } from './provider'; +export { SQLiteProvider } from './sqlite'; diff --git a/libs/datasource/jwt-rpc/src/indexeddb.ts b/libs/datasource/jwt-rpc/src/indexeddb.ts new file mode 100644 index 0000000000..bb38fb8a88 --- /dev/null +++ b/libs/datasource/jwt-rpc/src/indexeddb.ts @@ -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 { + doc: Y.Doc; + name: string; + private _mux: mutex.mutex; + _dbref: number; + _dbsize: number; + private _destroyed: boolean; + whenSynced: Promise; + db: IDBDatabase | null; + private _db: Promise; + 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} + */ + async clearData(): Promise { + return this.destroy().then(() => { + idb.deleteDB(this.name); + }); + } + + /** + * @param {String | number | ArrayBuffer | Date} key + * @return {Promise} + */ + async get( + key: string | number | ArrayBuffer | Date + ): Promise { + 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} + */ + async set( + key: string | number | ArrayBuffer | Date, + value: string | number | ArrayBuffer | Date + ): Promise { + 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} + */ + async del(key: string | number | ArrayBuffer | Date): Promise { + return this._db.then(db => { + const [custom] = idb.transact(db, [customStoreName]); + return idb.del(custom, key); + }); + } +} diff --git a/libs/datasource/jwt-rpc/src/sqlite.ts b/libs/datasource/jwt-rpc/src/sqlite.ts new file mode 100644 index 0000000000..103078d05f --- /dev/null +++ b/libs/datasource/jwt-rpc/src/sqlite.ts @@ -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 { + doc: Y.Doc; + name: string; + _dbref: number; + _dbsize: number; + private _destroyed: boolean; + whenSynced: Promise; + db: Database | null; + private _db: Promise; + 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 { + 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 { + return this._db.then(db => { + db.exec(STMTS.drop); + return this.destroy(); + }); + } +} diff --git a/libs/datasource/jwt/package.json b/libs/datasource/jwt/package.json index 705df62855..dd8f6f6761 100644 --- a/libs/datasource/jwt/package.json +++ b/libs/datasource/jwt/package.json @@ -13,8 +13,7 @@ "flexsearch": "^0.7.21", "lib0": "^0.2.52", "lru-cache": "^7.13.2", - "ts-debounce": "^4.0.0", - "y-indexeddb": "^9.0.9" + "ts-debounce": "^4.0.0" }, "dependencies": { "@types/flexsearch": "^0.7.3", diff --git a/libs/datasource/jwt/src/adapter/yjs/index.ts b/libs/datasource/jwt/src/adapter/yjs/index.ts index eb886afae5..9f2a530ed5 100644 --- a/libs/datasource/jwt/src/adapter/yjs/index.ts +++ b/libs/datasource/jwt/src/adapter/yjs/index.ts @@ -7,7 +7,6 @@ import { fromEvent } from 'file-selector'; import LRUCache from 'lru-cache'; import { debounce } from 'ts-debounce'; import { nanoid } from 'nanoid'; -import { IndexeddbPersistence } from 'y-indexeddb'; import { Awareness } from 'y-protocols/awareness.js'; import { Doc, @@ -19,7 +18,11 @@ import { snapshot, } from 'yjs'; -import { WebsocketProvider } from '@toeverything/datasource/jwt-rpc'; +import { + IndexedDBProvider, + SQLiteProvider, + WebsocketProvider, +} from '@toeverything/datasource/jwt-rpc'; import { AsyncDatabaseAdapter, @@ -46,8 +49,9 @@ const logger = getLogger('BlockDB:yjs'); type YjsProviders = { awareness: Awareness; - idb: IndexeddbPersistence; - binariesIdb: IndexeddbPersistence; + idb: IndexedDBProvider; + binariesIdb: IndexedDBProvider; + fstore?: SQLiteProvider; ws?: WebsocketProvider; backend: string; gatekeeper: GateKeeper; @@ -117,7 +121,9 @@ async function _initYjsDatabase( 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( backend, workspace, @@ -126,10 +132,10 @@ async function _initYjsDatabase( 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 binariesIdb = await new IndexeddbPersistence( + const binariesIdb = await new IndexedDBProvider( `${workspace}_binaries`, binaries ).whenSynced; @@ -147,6 +153,7 @@ async function _initYjsDatabase( awareness, idb, binariesIdb, + fstore, ws, backend, gatekeeper, @@ -374,7 +381,7 @@ export class YjsAdapter implements AsyncDatabaseAdapter { }; check(); }); - await new IndexeddbPersistence(this._provider.idb.name, doc) + await new IndexedDBProvider(this._provider.idb.name, doc) .whenSynced; applyUpdate(doc, new Uint8Array(binary)); await update_check; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a22ee95727..a632fb99bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -194,7 +194,7 @@ importers: yjs: ^13.5.41 dependencies: authing-js-sdk: 4.23.35 - firebase-admin: 11.0.1_@firebase+app-types@0.7.0 + firebase-admin: 11.0.1 lib0: 0.2.52 lru-cache: 7.13.2 nanoid: 4.0.0 @@ -571,6 +571,9 @@ importers: dependencies: ffc-js-client-side-sdk: 1.1.5 + libs/datasource/jwst/pkg: + specifiers: {} + libs/datasource/jwt: specifiers: '@types/debug': ^4.1.7 @@ -594,7 +597,6 @@ importers: sift: ^16.0.0 ts-debounce: ^4.0.0 uuid: ^8.3.2 - y-indexeddb: ^9.0.9 y-protocols: ^1.0.5 yjs: ^13.5.41 dependencies: @@ -622,17 +624,21 @@ importers: lib0: 0.2.52 lru-cache: 7.13.2 ts-debounce: 4.0.0 - y-indexeddb: 9.0.9_yjs@13.5.41 libs/datasource/jwt-rpc: specifiers: + '@types/sql.js': ^1.4.3 lib0: ^0.2.52 + sql.js: ^1.7.0 y-protocols: ^1.0.5 yjs: ^13.5.41 dependencies: lib0: 0.2.52 + sql.js: 1.7.0 y-protocols: 1.0.5 yjs: 13.5.41 + devDependencies: + '@types/sql.js': 1.4.3 libs/datasource/remote-kv: specifiers: @@ -3288,6 +3294,15 @@ packages: - utf-8-validate dev: true + /@firebase/auth-interop-types/0.1.6_@firebase+util@1.6.3: + resolution: {integrity: sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + dependencies: + '@firebase/util': 1.6.3 + dev: false + /@firebase/auth-interop-types/0.1.6_pbfwexsq7uf6mrzcwnikj3g37m: resolution: {integrity: sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==} peerDependencies: @@ -3296,6 +3311,7 @@ packages: dependencies: '@firebase/app-types': 0.7.0 '@firebase/util': 1.6.3 + dev: true /@firebase/auth-types/0.11.0_pbfwexsq7uf6mrzcwnikj3g37m: resolution: {integrity: sha512-q7Bt6cx+ySj9elQHTsKulwk3+qDezhzRBFC9zlQ1BjgMueUOnGMcvqmU0zuKlQ4RhLSH7MNAdBV2znVaoN3Vxw==} @@ -3331,6 +3347,19 @@ packages: '@firebase/util': 1.6.3 tslib: 2.4.0 + /@firebase/database-compat/0.2.4: + resolution: {integrity: sha512-VtsGixO5mTjNMJn6PwxAJEAR70fj+3blCXIdQKel3q+eYGZAfdqxox1+tzZDnf9NWBJpaOgAHPk3JVDxEo9NFQ==} + dependencies: + '@firebase/component': 0.5.17 + '@firebase/database': 0.13.4 + '@firebase/database-types': 0.9.12 + '@firebase/logger': 0.3.3 + '@firebase/util': 1.6.3 + tslib: 2.4.0 + transitivePeerDependencies: + - '@firebase/app-types' + dev: false + /@firebase/database-compat/0.2.4_@firebase+app-types@0.7.0: resolution: {integrity: sha512-VtsGixO5mTjNMJn6PwxAJEAR70fj+3blCXIdQKel3q+eYGZAfdqxox1+tzZDnf9NWBJpaOgAHPk3JVDxEo9NFQ==} dependencies: @@ -3342,6 +3371,7 @@ packages: tslib: 2.4.0 transitivePeerDependencies: - '@firebase/app-types' + dev: true /@firebase/database-types/0.9.10: resolution: {integrity: sha512-2ji6nXRRsY+7hgU6zRhUtK0RmSjVWM71taI7Flgaw+BnopCo/lDF5HSwxp8z7LtiHlvQqeRA3Ozqx5VhlAbiKg==} @@ -3356,6 +3386,19 @@ packages: '@firebase/app-types': 0.7.0 '@firebase/util': 1.6.3 + /@firebase/database/0.13.4: + resolution: {integrity: sha512-NW7bOoiaC4sJCj6DY/m9xHoFNa0CK32YPMCh6FiMweLCDQbOZM8Ql/Kn6yyuxCb7K7ypz9eSbRlCWQJsJRQjhg==} + dependencies: + '@firebase/auth-interop-types': 0.1.6_@firebase+util@1.6.3 + '@firebase/component': 0.5.17 + '@firebase/logger': 0.3.3 + '@firebase/util': 1.6.3 + faye-websocket: 0.11.4 + tslib: 2.4.0 + transitivePeerDependencies: + - '@firebase/app-types' + dev: false + /@firebase/database/0.13.4_@firebase+app-types@0.7.0: resolution: {integrity: sha512-NW7bOoiaC4sJCj6DY/m9xHoFNa0CK32YPMCh6FiMweLCDQbOZM8Ql/Kn6yyuxCb7K7ypz9eSbRlCWQJsJRQjhg==} dependencies: @@ -3367,6 +3410,7 @@ packages: tslib: 2.4.0 transitivePeerDependencies: - '@firebase/app-types' + dev: true /@firebase/firestore-compat/0.1.23_53yvy43rwpg2c45kgeszsxtrca: resolution: {integrity: sha512-QfcuyMAavp//fQnjSfCEpnbWi7spIdKaXys1kOLu7395fLr+U6ykmto1HUMCSz8Yus9cEr/03Ujdi2SUl2GUAA==} @@ -6691,6 +6735,10 @@ packages: resolution: {integrity: sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==} dev: true + /@types/emscripten/1.39.6: + resolution: {integrity: sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==} + dev: true + /@types/eslint-scope/3.7.4: resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==} dependencies: @@ -6988,6 +7036,13 @@ packages: '@types/node': 18.0.1 dev: true + /@types/sql.js/1.4.3: + resolution: {integrity: sha512-3bz1LJIiJtKMEL8tYf7c9Nrb1lYcFeWQkE8vhWvobE29ZzizW79DtoTjqx1bR82DS2Ch2K30nOwNhuLclZ1vYg==} + dependencies: + '@types/emscripten': 1.39.6 + '@types/node': 18.0.1 + dev: true + /@types/stack-utils/2.0.1: resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} dev: true @@ -10849,12 +10904,12 @@ packages: semver-regex: 2.0.0 dev: true - /firebase-admin/11.0.1_@firebase+app-types@0.7.0: + /firebase-admin/11.0.1: resolution: {integrity: sha512-rL3wlZbi2Kb/KJgcmj1YHlD4ZhfmhfgRO2YJialxAllm0tj1IQea878hHuBLGmv4DpbW9t9nLvX9kddNR2Y65Q==} engines: {node: '>=14'} dependencies: '@fastify/busboy': 1.1.0 - '@firebase/database-compat': 0.2.4_@firebase+app-types@0.7.0 + '@firebase/database-compat': 0.2.4 '@firebase/database-types': 0.9.10 '@types/node': 18.0.1 jsonwebtoken: 8.5.1 @@ -17879,6 +17934,10 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true + /sql.js/1.7.0: + resolution: {integrity: sha512-qAfft3xkSgHqmmfNugWTp/59PsqIw8gbeao5TZmpmzQQsAJ49de3iDDKuxVixidYs6dkHNksY8m27v2dZNn2jw==} + dev: false + /sshpk/1.17.0: resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} engines: {node: '>=0.10.0'} @@ -19572,15 +19631,6 @@ packages: engines: {node: '>=0.4'} dev: true - /y-indexeddb/9.0.9_yjs@13.5.41: - resolution: {integrity: sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==} - peerDependencies: - yjs: ^13.0.0 - dependencies: - lib0: 0.2.52 - yjs: 13.5.41 - dev: true - /y-protocols/1.0.5: resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==} dependencies: @@ -19649,6 +19699,7 @@ packages: resolution: {integrity: sha512-4eSTrrs8OeI0heXKKioRY4ag7V5Bk85Z4MeniUyown3o3y0G7G4JpAZWrZWfTp7pzw2b53GkAQWKqHsHi9j9JA==} dependencies: lib0: 0.2.52 + dev: false /yn/3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} From b5d4410dca2311d01b97da84509db0bf1fcd0575 Mon Sep 17 00:00:00 2001 From: DarkSky Date: Tue, 9 Aug 2022 19:31:08 +0800 Subject: [PATCH 04/10] feat: tips bar --- .../layout/src/header/LayoutHeader.tsx | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/libs/components/layout/src/header/LayoutHeader.tsx b/libs/components/layout/src/header/LayoutHeader.tsx index 0adf0546ed..ccf6497642 100644 --- a/libs/components/layout/src/header/LayoutHeader.tsx +++ b/libs/components/layout/src/header/LayoutHeader.tsx @@ -44,10 +44,37 @@ export const LayoutHeader = () => { + + + AFFiNE now under active development, the version is + UNSTABLE, please DO NOT store important data in this version + + ); }; +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 }) => { return { width: '100%', @@ -114,7 +141,9 @@ const StyledLogoIcon = styled(LogoIcon)(({ theme }) => { const StyledContainerForEditorBoardSwitcher = styled('div')(({ theme }) => { return { + width: '100%', position: 'absolute', - left: '50%', + display: 'flex', + justifyContent: 'center', }; }); From 50270f87edf58038ed2d54d492428b43397ad504 Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:04:09 +0800 Subject: [PATCH 05/10] feat: migrate to styled --- .../page-tree/tree-item/styles.ts | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 libs/components/layout/src/workspace-sidebar/page-tree/tree-item/styles.ts diff --git a/libs/components/layout/src/workspace-sidebar/page-tree/tree-item/styles.ts b/libs/components/layout/src/workspace-sidebar/page-tree/tree-item/styles.ts new file mode 100644 index 0000000000..5c291f228a --- /dev/null +++ b/libs/components/layout/src/workspace-sidebar/page-tree/tree-item/styles.ts @@ -0,0 +1,204 @@ +import { styled } from '@toeverything/components/ui'; +import { Link } from 'react-router-dom'; + +export const TreeItemContainer = styled('div')` + box-sizing: border-box; + display: flex; + align-items: center; + color: #4c6275; +`; + +export const Wrapper = styled('li')<{ + spacing: string; + clone?: boolean; + ghost?: boolean; + indicator?: boolean; + disableSelection?: boolean; + disableInteraction?: boolean; +}>` + box-sizing: border-box; + padding-left: ${({ spacing }) => spacing}; + list-style: none; + font-size: 14px; + + ${({ clone, disableSelection }) => + (clone || disableSelection) && + `width: 100%; + .Text, + .Count { + user-select: none; + -webkit-user-select: none; + }`} + + ${({ indicator }) => + indicator && + `width: 100%; + .Text, + .Count { + user-select: none; + -webkit-user-select: none; + }`} + + ${({ disableInteraction }) => disableInteraction && `pointer-events: none;`} + + &:hover { + background: #f5f7f8; + border-radius: 5px; + } + + &.clone { + display: inline-block; + padding: 0; + margin-left: 10px; + margin-top: 5px; + pointer-events: none; + + ${TreeItemContainer} { + padding-right: 20px; + border-radius: 4px; + box-shadow: 0px 15px 15px 0 rgba(34, 33, 81, 0.1); + } + } + + &.ghost { + &.indicator { + opacity: 1; + position: relative; + z-index: 1; + margin-bottom: -1px; + + ${TreeItemContainer} { + position: relative; + padding: 0; + height: 8px; + border-color: #2389ff; + background-color: #56a1f8; + + &:before { + position: absolute; + left: -8px; + top: -4px; + display: block; + content: ''; + width: 12px; + height: 12px; + border-radius: 50%; + border: 1px solid #2389ff; + } + + > * { + /* Items are hidden using height and opacity to retain focus */ + opacity: 0; + height: 0; + } + } + } + + &:not(.indicator) { + opacity: 0.5; + } + + ${TreeItemContainer} > * { + box-shadow: none; + background-color: transparent; + } + } +`; + +export const Counter = styled('span')` + position: absolute; + top: 8px; + right: 0; + display: flex; + justify-content: center; + align-items: center; + width: 18px; + height: 18px; + border-radius: 50%; + background-color: #2389ff; + font-size: 0.9rem; + font-weight: 500; + color: #fff; +`; + +export const ActionButton = styled('button')<{ + background?: string; + fill?: string; +}>` + display: flex; + width: 12px; + padding: 0 15px; + align-items: center; + justify-content: center; + flex: 0 0 auto; + touch-action: none; + cursor: pointer; + border-radius: 5px; + border: none; + outline: none; + appearance: none; + background-color: transparent; + -webkit-tap-highlight-color: transparent; + + svg { + flex: 0 0 auto; + margin: auto; + height: 100%; + overflow: visible; + fill: #919eab; + } + + &:active { + background-color: ${({ background }) => + background ?? 'rgba(0, 0, 0, 0.05)'}; + + svg { + fill: ${({ fill }) => fill ?? '#788491'}; + } + } + + &:focus-visible { + outline: none; + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0), 0 0px 0px 2px #4c9ffe; + } +`; + +export const TreeItemMoreActions = styled('div')` + display: block; + visibility: hidden; +`; + +export const TextLink = styled(Link)<{ active?: boolean }>` + display: flex; + align-items: center; + flex-grow: 1; + height: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + cursor: pointer; + appearance: none; + text-decoration: none; + color: ${({ theme, active }) => + active ? theme.affine.palette.primary : 'unset'}; +`; + +export const TreeItemContent = styled('div')` + box-sizing: border-box; + width: 100%; + height: 32px; + position: relative; + display: flex; + align-items: center; + justify-content: space-around; + color: #4c6275; + padding-right: 0.5rem; + overflow: hidden; + + &:hover { + ${TreeItemMoreActions} { + visibility: visible; + cursor: pointer; + } + } +`; From 8d1da35b56a7fe397f23ca647212bf6c1a5f300c Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Tue, 9 Aug 2022 21:04:44 +0800 Subject: [PATCH 06/10] refactor: update component styles --- .../page-tree/tree-item/MoreActions.tsx | 28 ++-- .../page-tree/tree-item/TreeItem.tsx | 137 ++++-------------- 2 files changed, 44 insertions(+), 121 deletions(-) diff --git a/libs/components/layout/src/workspace-sidebar/page-tree/tree-item/MoreActions.tsx b/libs/components/layout/src/workspace-sidebar/page-tree/tree-item/MoreActions.tsx index f23e377f4a..1d211d9a31 100644 --- a/libs/components/layout/src/workspace-sidebar/page-tree/tree-item/MoreActions.tsx +++ b/libs/components/layout/src/workspace-sidebar/page-tree/tree-item/MoreActions.tsx @@ -1,20 +1,18 @@ -import styles from './tree-item.module.scss'; +import { AddIcon, MoreIcon } from '@toeverything/components/icons'; import { - MuiSnackbar as Snackbar, Cascader, CascaderItemProps, - MuiDivider as Divider, - MuiClickAwayListener as ClickAwayListener, IconButton, + MuiClickAwayListener as ClickAwayListener, + MuiSnackbar as Snackbar, styled, } from '@toeverything/components/ui'; -import React from 'react'; -import { NavLink, useNavigate } from 'react-router-dom'; -import { copyToClipboard } from '@toeverything/utils'; import { services, TemplateFactory } from '@toeverything/datasource/db-service'; -import { NewFromTemplatePortal } from './NewFromTemplatePortal'; import { useFlag } from '@toeverything/datasource/feature-flags'; -import { MoreIcon, AddIcon } from '@toeverything/components/icons'; +import { copyToClipboard } from '@toeverything/utils'; +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { TreeItemMoreActions } from './styles'; const MESSAGES = { COPY_LINK_SUCCESS: 'Copyed link to clipboard', @@ -47,6 +45,10 @@ function DndTreeItemMoreActions(props: ActionsProps) { set_alert_open(false); }; const handleClick = (event: React.MouseEvent) => { + if (anchorEl) { + setAnchorEl(null); + return; + } setAnchorEl(event.currentTarget); }; const handleClose = () => { @@ -246,10 +248,11 @@ function DndTreeItemMoreActions(props: ActionsProps) { return ( handleClose()}>
-
+ @@ -262,14 +265,15 @@ function DndTreeItemMoreActions(props: ActionsProps) { -
+ + + /> ( 'BooleanPageTreeItemMoreActions', true ); - const theme = useTheme(); return ( -
  • -
    - + + {childCount !== 0 && (collapsed ? ( ) : ( ))} - + -
    - + {value} - + {BooleanPageTreeItemMoreActions && ( ( {/*{!clone && onRemove && }*/} {clone && childCount && childCount > 1 ? ( - - {childCount} - + {childCount} ) : null} -
    -
    -
  • + + + ); } ); - -export interface ActionProps extends React.HTMLAttributes { - active?: { - fill: string; - background: string; - }; - // cursor?: CSSProperties['cursor']; - cursor?: 'pointer' | 'grab'; -} - -/** Customizable buttons */ -export function Action({ - active, - className, - cursor, - style, - ...props -}: ActionProps) { - return ( -