From edfd1ab1166ec0fc67b1c74bcbc79aaa79220b5e Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Thu, 16 May 2024 08:09:02 +0200 Subject: [PATCH 01/30] chore: bump version 0.5.8 (#5341) * chore: bump version 0.5.8 * chore: update release date Co-authored-by: Lucas.Xu * chore: reverse order --------- Co-authored-by: Lucas.Xu --- CHANGELOG.md | 17 +++++++++++++++++ frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 550cd2bbce..e612d832f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,21 @@ # Release Notes +## Version 0.5.8 - 05/20/2024 +### New Features +- Improvement to the Callout block to insert new lines +- New settings page "Manage data" replaced the "Files" page +- New settings page "Workspace" replaced the "Appearance" and "Language" pages +- A custom implementation of a title bar for Windows users +- Added support for selecting Cards in kanban and performing grouped keyboard shortcuts +- Added support for default system font family +- Support for scaling the application up/down using a keyboard shortcut (CMD/CTRL + PLUS/MINUS) + +### Bug Fixes +- Resolved and refined the UI on Mobile +- Resolved issue with text editing in database +- Improved appearance of empty text cells in kanban/calendar +- Resolved an issue where a page's more actions (delete, duplicate) did not work properly +- Resolved and inconsistency in padding on get started screen on Desktop + ## Version 0.5.7 - 05/10/2024 ### Bug Fixes - Resolved page opening issue on Android. diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index d5fad27238..5a962f3cbf 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.5.7" +APPFLOWY_VERSION = "0.5.8" FLUTTER_DESKTOP_FEATURES = "dart" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index fa2e0f5c9f..8ad4bd55be 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.5.7 +version: 0.5.8 environment: flutter: ">=3.19.0" From a6467c1b16b4812c0612ee7c62267cb576ce9813 Mon Sep 17 00:00:00 2001 From: Stefan Weiberg <2744377+suntorytimed@users.noreply.github.com> Date: Thu, 16 May 2024 11:31:02 +0200 Subject: [PATCH 02/30] =?UTF-8?q?chore:=20update=20German=20translations?= =?UTF-8?q?=20with=20Fink=20=F0=9F=90=A6=20(#5335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/resources/translations/ar-SA.json | 2 +- frontend/resources/translations/ca-ES.json | 2 +- frontend/resources/translations/ckb-KU.json | 2 +- frontend/resources/translations/cs-CZ.json | 2 +- frontend/resources/translations/de-DE.json | 48 ++++++++++++++++++++- frontend/resources/translations/en.json | 2 +- frontend/resources/translations/es-VE.json | 2 +- frontend/resources/translations/eu-ES.json | 2 +- frontend/resources/translations/fa.json | 2 +- frontend/resources/translations/fr-CA.json | 2 +- frontend/resources/translations/fr-FR.json | 2 +- frontend/resources/translations/hu-HU.json | 2 +- frontend/resources/translations/id-ID.json | 2 +- frontend/resources/translations/it-IT.json | 2 +- frontend/resources/translations/ja-JP.json | 2 +- frontend/resources/translations/ko-KR.json | 2 +- frontend/resources/translations/pl-PL.json | 2 +- frontend/resources/translations/pt-BR.json | 2 +- frontend/resources/translations/pt-PT.json | 2 +- frontend/resources/translations/ru-RU.json | 2 +- frontend/resources/translations/sv-SE.json | 2 +- frontend/resources/translations/tr-TR.json | 2 +- frontend/resources/translations/vi-VN.json | 2 +- frontend/resources/translations/vi.json | 2 +- frontend/resources/translations/zh-CN.json | 2 +- frontend/resources/translations/zh-TW.json | 2 +- 26 files changed, 72 insertions(+), 26 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 409cdfdc28..84d0f64985 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -1169,4 +1169,4 @@ "addField": "إضافة حقل", "userIcon": "رمز المستخدم" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index 2e98070d46..dccb18dcf8 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -812,4 +812,4 @@ "deleteContentTitle": "Esteu segur que voleu suprimir {pageType}?", "deleteContentCaption": "si suprimiu aquest {pageType}, podeu restaurar-lo des de la paperera." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index 1260ecf330..28535be2c8 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -946,4 +946,4 @@ "frequentlyUsed": "زۆرجار بەکارت هێناوە" } } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index b6e1282376..a5887f16b9 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -1094,4 +1094,4 @@ "font": "Písmo", "actions": "Příkazy" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index d8b12aea2e..f24d6bd2a0 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -425,6 +425,52 @@ "deleteWorkspace": "Arbeitsbereich löschen" } }, + "manageDataPage": { + "menuLabel": "Daten verwalten", + "title": "Daten verwalten", + "description": "Verwalte den lokalen Datenspeicher oder importiere deine vorhandenen Daten in AppFlowy. Du kannst deine Daten mit End-to-End-Verschlüsselung absichern.", + "dataStorage": { + "title": "Speicherort", + "tooltip": "Das Verzeichnis, in dem deine Dateien gespeichert sind", + "actions": { + "change": "Pfad ändern", + "open": "Ordner öffnen", + "openTooltip": "Aktuellen Speicherort des Datenordners öffnen", + "copy": "Pfad kopieren", + "copiedHint": "Link kopiert!" + }, + "resetDialog": { + "title": "Bist du sicher?", + "description": "Durch das Zurücksetzen des Pfads auf das Standardverzeichnis werden deine Daten nicht gelöscht. Wenn du deine aktuellen Daten erneut importieren möchtest, solltest du zuerst den Pfad deines aktuellen Speicherorts kopieren." + } + }, + "importData": { + "title": "Daten importieren", + "tooltip": "Daten aus AppFlowy-Backups/Datenordnern importieren", + "description": "Daten aus einem externen AppFlowy-Datenordner kopieren und in den aktuellen AppFlowy-Datenordner importieren", + "action": "Ordner durchsuchen" + }, + "encryption": { + "title": "Verschlüsselung", + "tooltip": "Verwalte, wie deine Daten gespeichert und verschlüsselt werden", + "descriptionNoEncryption": "Durch das Einschalten der Verschlüsselung werden alle Daten verschlüsselt. Dieser Vorgang kann nicht rückgängig gemacht werden.", + "descriptionEncrypted": "Deine Daten sind verschlüsselt.", + "action": "Daten verschlüsseln", + "dialog": { + "title": "Alle deine Daten verschlüsseln?", + "description": "Durch die Verschlüsselung all deiner Daten bleiben diese sicher und geschützt. Diese Aktion kann NICHT rückgängig gemacht werden. Möchtest du wirklich fortfahren?" + } + }, + "cache": { + "title": "Cache leeren", + "description": "Wenn Bilder nicht geladen werden oder Schriftarten nicht richtig angezeigt werden, versuche den Cache zu leeren. Deine Benutzerdaten werden dadurch nicht gelöscht.", + "dialog": { + "title": "Bist du sicher?", + "description": "Durch das Leeren des Caches werden Bilder und Schriftarten beim Laden erneut heruntergeladen. Deine Daten werden durch diese Aktion weder entfernt noch geändert.", + "successHint": "Cache geleert!" + } + } + }, "common": { "reset": "Zurücksetzen" }, @@ -1630,4 +1676,4 @@ "betaTooltip": "Wir unterstützen derzeit nur die Suche nach Seiten", "fromTrashHint": "Aus dem Mülleimer" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 88ebedb546..96c7314dbf 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -420,7 +420,7 @@ "manageDataPage": { "menuLabel": "Manage data", "title": "Manage data", - "description": "Manage data local storage or Import your existing data into Appflowy. You can secure your data with end to end encryption.", + "description": "Manage data local storage or Import your existing data into AppFlowy. You can secure your data with end to end encryption.", "dataStorage": { "title": "File storage location", "tooltip": "The location where your files are stored", diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 35f5541197..80af6a0756 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -1543,4 +1543,4 @@ "betaTooltip": "Actualmente solo admitimos la búsqueda de páginas.", "fromTrashHint": "De la papelera" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index 496a262596..d3c25fa3aa 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -601,4 +601,4 @@ "deleteContentTitle": "Ziur {pageType} ezabatu nahi duzula?", "deleteContentCaption": "{pageType} hau ezabatzen baduzu, zaborrontzitik leheneratu dezakezu." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 56e97333d7..0bb112d168 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -674,4 +674,4 @@ "frequentlyUsed": "استفاده‌شده" } } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index a6799575f1..958762094e 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -1262,4 +1262,4 @@ "userIcon": "Icône utilisateur" }, "noLogFiles": "Il n'y a pas de log" -} \ No newline at end of file +} diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index dc4e2348fb..db1a933b28 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -1468,4 +1468,4 @@ "loadingTooltip": "Nous recherchons des résultats...", "betaTooltip": "Nous ne prenons actuellement en charge que la recherche de pages" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index bfecc2756f..3a0d464d5a 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -599,4 +599,4 @@ "deleteContentTitle": "Biztosan törli a következőt: {pageType}?", "deleteContentCaption": "ha törli ezt a {pageType} oldalt, visszaállíthatja a kukából." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index f1f5c6a1f1..4aa1e71038 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -1022,4 +1022,4 @@ "noFavorite": "Tidak ada halaman favorit", "noFavoriteHintText": "Geser halaman ke kiri untuk menambahkannya ke favorit Anda" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index 4093c824ff..47e90eaa87 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -1262,4 +1262,4 @@ "userIcon": "Icona utente" }, "noLogFiles": "Non ci sono file di log" -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index 704552b55d..4388f9c3a9 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -686,4 +686,4 @@ "deleteContentTitle": "{pageType} を削除してもよろしいですか?", "deleteContentCaption": "この {pageType} を削除しても、ゴミ箱から復元できます。" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 03174e2ed2..81f7be0185 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -598,4 +598,4 @@ "deleteContentTitle": "{pageType}을(를) 삭제하시겠습니까?", "deleteContentCaption": "이 {pageType}을(를) 삭제하면 휴지통에서 복원할 수 있습니다." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 43d17c717e..5b11fec3c9 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -1077,4 +1077,4 @@ "language": "Język", "font": "Czcionka" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 046eee783e..1341f735b7 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -1219,4 +1219,4 @@ "addField": "Adicionar campo", "userIcon": "Ícone do usuário" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index ab4bd7d438..8a9745650a 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -857,4 +857,4 @@ "noResult": "Nenhum resultado", "caseSensitive": "Maiúsculas e minúsculas" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 72e7a7424e..c138c87bd1 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -1311,4 +1311,4 @@ "userIcon": "Пользовательская иконка" }, "noLogFiles": "Нет файлов журналов" -} \ No newline at end of file +} diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index f35966d390..1d125b1402 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -668,4 +668,4 @@ "deleteContentTitle": "Är du säker på att du vill ta bort {pageType}?", "deleteContentCaption": "om du tar bort denna {pageType} kan du återställa den från papperskorgen." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index acfacef891..8f82fedb62 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -1477,4 +1477,4 @@ "betaTooltip": "Şu anda yalnızca sayfaları aramayı destekliyoruz", "fromTrashHint": "Çöp kutusundan" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 9b89f50dbd..09b73db901 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -868,4 +868,4 @@ "font": "Phông chữ", "date": "Ngày" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/vi.json b/frontend/resources/translations/vi.json index 4d1716447a..b921c1844e 100644 --- a/frontend/resources/translations/vi.json +++ b/frontend/resources/translations/vi.json @@ -6,4 +6,4 @@ "failedToLoad": "Không tải được chế độ xem bảng" } } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index a1f9d3807b..8d342913c2 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -1357,4 +1357,4 @@ "addField": "添加字段", "userIcon": "用户图标" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index a7f81d0205..3e623c2b0e 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -1479,4 +1479,4 @@ "betaLabel": "BETA", "betaTooltip": "目前我們只支援搜尋頁面" } -} \ No newline at end of file +} From 759998cacbcfbe59c18e62af7a24070200cb3bc4 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Fri, 17 May 2024 10:22:05 +0800 Subject: [PATCH 03/30] chore: enable ai summary (#5351) --- .../widgets/field/field_type_list.dart | 2 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 24 ++++++------- frontend/appflowy_web/wasm-libs/Cargo.lock | 36 +++++++++++++------ 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart index bcb1867086..02bc0700a4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart @@ -20,7 +20,7 @@ const List _supportedFieldTypes = [ FieldType.LastEditedTime, FieldType.CreatedTime, FieldType.Relation, - // FieldType.Summary, + FieldType.Summary, ]; class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 3eea9e18ae..c4bf194b87 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -860,7 +860,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "async-trait", @@ -884,7 +884,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "async-trait", @@ -914,7 +914,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "collab", @@ -933,7 +933,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "bytes", @@ -948,7 +948,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "chrono", @@ -986,7 +986,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "async-stream", @@ -1067,7 +1067,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "collab", @@ -1296,7 +1296,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -4729,7 +4729,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", "heck 0.4.1", - "itertools 0.10.5", + "itertools 0.11.0", "log", "multimap", "once_cell", @@ -4750,7 +4750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.47", @@ -7968,9 +7968,9 @@ dependencies = [ [[package]] name = "yrs" -version = "0.18.7" +version = "0.18.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58fbc807677598fedfab76f99f6e1aa5c644411255002b5438ea0ab14672398" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" dependencies = [ "arc-swap", "atomic_refcell", diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index a7d441c1fc..8024a9ba1f 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -638,7 +638,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "async-trait", @@ -662,7 +662,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "collab", @@ -681,7 +681,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "bytes", @@ -696,7 +696,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "chrono", @@ -734,7 +734,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "async-stream", @@ -814,7 +814,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" dependencies = [ "anyhow", "collab", @@ -966,7 +966,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -2790,7 +2790,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -2810,6 +2810,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -2877,6 +2878,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -4909,9 +4923,9 @@ dependencies = [ [[package]] name = "yrs" -version = "0.18.7" +version = "0.18.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58fbc807677598fedfab76f99f6e1aa5c644411255002b5438ea0ab14672398" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" dependencies = [ "arc-swap", "atomic_refcell", @@ -4995,4 +5009,4 @@ dependencies = [ [[patch.unused]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" From c1e7e7215444111838c80dd55a11d36c77c85568 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Fri, 17 May 2024 18:23:29 +0800 Subject: [PATCH 04/30] feat: support web grid preview (#5353) --- .../style-dictionary/tokens/base.json | 2 +- .../cypress/fixtures/user_workspace.json | 61 ++ .../cypress/support/commands.ts | 1 + frontend/appflowy_web_app/index.html | 4 +- frontend/appflowy_web_app/package.json | 10 +- frontend/appflowy_web_app/pnpm-lock.yaml | 84 ++- .../src/application/collab.type.ts | 269 +++++++- .../src/application/database-yjs/const.ts | 2 + .../src/application/database-yjs/context.ts | 127 ++++ .../application/database-yjs/database.type.ts | 51 ++ .../fields/checkbox/checkbox.type.ts | 10 + .../database-yjs/fields/checkbox/index.ts | 1 + .../fields/checklist/checklist.type.ts | 10 + .../database-yjs/fields/checklist/index.ts | 2 + .../database-yjs/fields/checklist/parse.ts | 22 + .../database-yjs/fields/date/date.type.ts | 32 + .../database-yjs/fields/date/index.ts | 2 + .../database-yjs/fields/date/utils.ts | 29 + .../application/database-yjs/fields/index.ts | 8 + .../fields/number/__tests__/format.test.ts | 628 ++++++++++++++++++ .../database-yjs/fields/number/format.ts | 229 +++++++ .../database-yjs/fields/number/index.ts | 3 + .../database-yjs/fields/number/number.type.ts | 56 ++ .../database-yjs/fields/number/parse.ts | 11 + .../database-yjs/fields/relation/index.ts | 2 + .../database-yjs/fields/relation/parse.ts | 9 + .../fields/relation/relation.type.ts | 9 + .../fields/select-option/index.ts | 2 + .../fields/select-option/parse.ts | 28 + .../select-option/select_option.type.ts | 38 ++ .../database-yjs/fields/text/index.ts | 1 + .../database-yjs/fields/text/text.type.ts | 17 + .../database-yjs/fields/type_option.ts | 8 + .../src/application/database-yjs/filter.ts | 223 +++++++ .../src/application/database-yjs/index.ts | 8 + .../src/application/database-yjs/selector.ts | 227 +++++++ .../src/application/database-yjs/sort.ts | 79 +++ .../src/application/document.type.ts | 176 ----- .../services/js-services/database.service.ts | 170 +++++ .../services/js-services/db/index.ts | 46 +- .../services/js-services/db/tables/users.ts | 10 - .../services/js-services/document.service.ts | 31 +- .../services/js-services/folder.service.ts | 32 +- .../application/services/js-services/index.ts | 5 + .../services/js-services/storage/auth.ts | 10 +- .../services/js-services/storage/collab.ts | 101 +++ .../services/js-services/storage/document.ts | 21 - .../services/js-services/storage/folder.ts | 21 - .../services/js-services/storage/index.ts | 6 +- .../services/js-services/storage/user.ts | 36 +- .../services/js-services/user.service.ts | 18 +- .../services/js-services/wasm/client_api.ts | 44 +- .../src/application/services/services.type.ts | 19 + .../tauri-services/database.service.ts | 29 + .../tauri-services/document.service.ts | 2 +- .../services/tauri-services/index.ts | 5 + .../application/slate-yjs/plugins/withYjs.ts | 14 +- .../slate-yjs/utils/applySlateOpts.ts | 6 +- .../utils/translateYjsEvent/textEvent.ts | 2 +- .../src/application/user.type.ts | 7 + frontend/appflowy_web_app/src/assets/add.svg | 3 - .../src/assets/align-center.svg | 5 - .../src/assets/align-left.svg | 5 - .../src/assets/align-right.svg | 5 - .../src/assets/arrow-left.svg | 3 - .../src/assets/arrow-right.svg | 3 - .../appflowy_web_app/src/assets/board.svg | 16 - frontend/appflowy_web_app/src/assets/bold.svg | 3 - .../src/assets/clock_alarm.svg | 6 - .../appflowy_web_app/src/assets/close.svg | 4 - frontend/appflowy_web_app/src/assets/copy.svg | 4 - .../appflowy_web_app/src/assets/dark-logo.svg | 73 -- .../src/assets/database/checkbox-check.svg | 4 - .../src/assets/database/checkbox-uncheck.svg | 3 - .../src/assets/database/field-type-attach.svg | 3 - .../assets/database/field-type-checkbox.svg | 4 - .../assets/database/field-type-checklist.svg | 4 - .../src/assets/database/field-type-date.svg | 6 - .../database/field-type-last-edited-time.svg | 4 - .../database/field-type-multi-select.svg | 8 - .../src/assets/database/field-type-number.svg | 3 - .../src/assets/database/field-type-person.svg | 4 - .../assets/database/field-type-relation.svg | 8 - .../database/field-type-single-select.svg | 4 - .../src/assets/database/field-type-text.svg | 4 - .../src/assets/database/field-type-url.svg | 3 - frontend/appflowy_web_app/src/assets/date.svg | 6 - .../appflowy_web_app/src/assets/delete.svg | 6 - .../appflowy_web_app/src/assets/details.svg | 5 - .../appflowy_web_app/src/assets/document.svg | 14 - frontend/appflowy_web_app/src/assets/drag.svg | 8 - .../appflowy_web_app/src/assets/dropdown.svg | 6 - frontend/appflowy_web_app/src/assets/edit.svg | 9 - .../appflowy_web_app/src/assets/eye_close.svg | 9 - .../appflowy_web_app/src/assets/eye_open.svg | 16 - frontend/appflowy_web_app/src/assets/grid.svg | 6 - frontend/appflowy_web_app/src/assets/h1.svg | 4 - frontend/appflowy_web_app/src/assets/h2.svg | 4 - frontend/appflowy_web_app/src/assets/h3.svg | 4 - .../appflowy_web_app/src/assets/hide-menu.svg | 6 - frontend/appflowy_web_app/src/assets/hide.svg | 4 - .../appflowy_web_app/src/assets/image.svg | 5 - .../src/assets/images/default_cover.jpg | Bin 281498 -> 0 bytes .../src/assets/inline-code.svg | 4 - .../appflowy_web_app/src/assets/italic.svg | 3 - frontend/appflowy_web_app/src/assets/left.svg | 5 - .../src/assets/light-logo.svg | 51 -- frontend/appflowy_web_app/src/assets/link.svg | 4 - .../src/assets/list-dropdown.svg | 4 - frontend/appflowy_web_app/src/assets/list.svg | 8 - .../appflowy_web_app/src/assets/mention.svg | 3 - frontend/appflowy_web_app/src/assets/more.svg | 3 - .../appflowy_web_app/src/assets/numbers.svg | 3 - frontend/appflowy_web_app/src/assets/open.svg | 6 - .../appflowy_web_app/src/assets/quote.svg | 4 - .../appflowy_web_app/src/assets/react.svg | 1 - .../appflowy_web_app/src/assets/right.svg | 5 - .../appflowy_web_app/src/assets/search.svg | 4 - .../src/assets/select-check.svg | 3 - .../appflowy_web_app/src/assets/settings.svg | 4 - .../src/assets/settings/account.svg | 3 - .../src/assets/settings/check_circle.svg | 8 - .../src/assets/settings/dark.png | Bin 16280 -> 0 bytes .../src/assets/settings/light.png | Bin 13240 -> 0 bytes .../src/assets/settings/workplace.svg | 10 - .../appflowy_web_app/src/assets/show-menu.svg | 6 - frontend/appflowy_web_app/src/assets/sort.svg | 4 - .../src/assets/strikethrough.svg | 4 - frontend/appflowy_web_app/src/assets/text.svg | 4 - .../appflowy_web_app/src/assets/todo-list.svg | 4 - .../appflowy_web_app/src/assets/underline.svg | 4 - frontend/appflowy_web_app/src/assets/up.svg | 3 - .../_shared/not-found/RecordNotFound.tsx | 2 +- .../components/_shared/page/usePageInfo.tsx | 8 +- .../components/_shared/popover/Popover.tsx | 19 + .../src/components/_shared/popover/index.ts | 1 + .../progress/LinearProgressWithLabel.tsx | 47 ++ .../_shared/scroller/AFScroller.tsx | 98 +-- .../src/components/_shared/tag/Tag.tsx | 29 + .../src/components/_shared/tag/index.ts | 1 + .../src/components/app/App.tsx | 2 +- .../src/components/app/AppTheme.tsx | 9 + .../src/components/auth/Welcome.cy.tsx | 3 +- .../src/components/auth/auth.hooks.ts | 1 + .../src/components/database/Database.tsx | 148 +++++ .../components/database/DatabaseContext.tsx | 10 + .../src/components/database/DatabaseTitle.tsx | 19 + .../src/components/database/board/Board.tsx | 7 + .../src/components/database/board/index.ts | 1 + .../components/database/calendar/Calendar.tsx | 7 + .../src/components/database/calendar/index.ts | 1 + .../calculation-cell/CalculationCell.tsx | 40 ++ .../components/calculation-cell/cell.type.ts | 8 + .../components/calculation-cell/index.ts | 1 + .../database/components/cell/Cell.hooks.ts | 47 ++ .../database/components/cell/Cell.tsx | 62 ++ .../database/components/cell/CheckboxCell.tsx | 14 + .../components/cell/ChecklistCell.tsx | 21 + .../database/components/cell/DateTimeCell.tsx | 35 + .../database/components/cell/NumberCell.tsx | 27 + .../database/components/cell/RelationCell.tsx | 84 +++ .../components/cell/RowCreateModifiedTime.tsx | 43 ++ .../components/cell/SelectionCell.tsx | 32 + .../database/components/cell/TextCell.tsx | 12 + .../database/components/cell/UrlCell.tsx | 37 ++ .../database/components/cell/cell.const.ts | 25 + .../database/components/cell/cell.parse.ts | 46 ++ .../database/components/cell/cell.type.ts | 90 +++ .../database/components/cell/index.ts | 1 + .../components/conditions/DatabaseActions.tsx | 35 + .../conditions/DatabaseConditions.tsx | 33 + .../database/components/conditions/context.ts | 12 + .../database/components/conditions/index.ts | 2 + .../components/field/FieldDisplay.tsx | 20 + .../components/field/FieldTypeIcon.tsx | 33 + .../database/components/field/index.ts | 2 + .../field/select-option/SelectOptionList.tsx | 30 + .../components/field/select-option/index.ts | 1 + .../database/components/filters/Filter.tsx | 57 ++ .../database/components/filters/Filters.tsx | 32 + .../filter-menu/CheckboxFilterMenu.tsx | 33 + .../filter-menu/ChecklistFilterMenu.tsx | 33 + .../filters/filter-menu/FieldMenuTitle.tsx | 23 + .../filters/filter-menu/FilterMenu.tsx | 39 ++ .../MultiSelectOptionFilterMenu.tsx | 56 ++ .../filters/filter-menu/NumberFilterMenu.tsx | 74 +++ .../SingleSelectOptionFilterMenu.tsx | 48 ++ .../filters/filter-menu/TextFilterMenu.tsx | 74 +++ .../components/filters/filter-menu/index.ts | 1 + .../database/components/filters/index.ts | 1 + .../overview/DateFilterContentOverview.tsx | 51 ++ .../overview/FilterContentOverview.tsx | 59 ++ .../overview/NumberFilterContentOverview.tsx | 38 ++ .../overview/SelectFilterContentOverview.tsx | 42 ++ .../overview/TextFilterContentOverview.tsx | 33 + .../components/filters/overview/index.ts | 1 + .../database/components/filters/package.json | 14 + .../components/grid-cell/GridCell.tsx | 64 ++ .../database/components/grid-cell/index.ts | 1 + .../components/grid-column/GridColumn.tsx | 35 + .../database/components/grid-column/index.ts | 2 + .../grid-column/useRenderColumns.tsx | 73 ++ .../components/grid-header/GridHeader.tsx | 73 ++ .../database/components/grid-header/index.ts | 1 + .../grid-row/GridCalculateRowCell.tsx | 41 ++ .../components/grid-row/GridRowCell.tsx | 28 + .../database/components/grid-row/index.ts | 3 + .../components/grid-row/useRenderRows.tsx | 44 ++ .../components/grid-table/GridTable.tsx | 177 +++++ .../database/components/grid-table/index.ts | 1 + .../database/components/sorts/Sort.tsx | 20 + .../components/sorts/SortCondition.tsx | 30 + .../database/components/sorts/SortList.tsx | 17 + .../database/components/sorts/Sorts.tsx | 43 ++ .../database/components/sorts/index.ts | 1 + .../database/components/tabs/DatabaseTabs.tsx | 97 +++ .../database/components/tabs/TextButton.tsx | 18 + .../database/components/tabs/ViewTabs.tsx | 52 ++ .../database/components/tabs/index.ts | 2 + .../src/components/database/grid/Grid.tsx | 45 ++ .../src/components/database/grid/index.ts | 1 + .../src/components/database/index.ts | 3 + .../components/editor/CollaborativeEditor.tsx | 11 +- .../src/components/editor/command/index.ts | 5 +- .../components/blocks/image/ImageEmpty.tsx | 2 +- .../blocks/todo_list/CheckboxIcon.tsx | 4 +- .../blocks/toggle_list/ToggleIcon.tsx | 2 +- .../components/leaf/mention/MentionDate.tsx | 6 +- .../src/components/error/ErrorModal.tsx | 12 +- .../src/components/layout/layout.scss | 11 +- .../src/pages/DatabasePage.tsx | 10 + .../src/pages/ProductPage.tsx | 24 +- .../src/styles/variables/dark.variables.css | 4 +- .../src/styles/variables/light.variables.css | 10 +- frontend/appflowy_web_app/src/utils/time.ts | 8 +- frontend/appflowy_web_app/src/utils/url.ts | 8 +- .../style-dictionary/tailwind/box-shadow.cjs | 2 +- .../style-dictionary/tailwind/colors.cjs | 2 +- .../style-dictionary/tokens/base.json | 2 +- frontend/appflowy_web_app/tsconfig.json | 5 +- frontend/appflowy_web_app/vite.config.ts | 7 +- 241 files changed, 5570 insertions(+), 937 deletions(-) create mode 100644 frontend/appflowy_web_app/cypress/fixtures/user_workspace.json create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/const.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/context.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/database.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/filter.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/index.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/selector.ts create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/sort.ts delete mode 100644 frontend/appflowy_web_app/src/application/document.type.ts create mode 100644 frontend/appflowy_web_app/src/application/services/js-services/database.service.ts delete mode 100644 frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts create mode 100644 frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts delete mode 100644 frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts delete mode 100644 frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts create mode 100644 frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts delete mode 100644 frontend/appflowy_web_app/src/assets/add.svg delete mode 100644 frontend/appflowy_web_app/src/assets/align-center.svg delete mode 100644 frontend/appflowy_web_app/src/assets/align-left.svg delete mode 100644 frontend/appflowy_web_app/src/assets/align-right.svg delete mode 100644 frontend/appflowy_web_app/src/assets/arrow-left.svg delete mode 100644 frontend/appflowy_web_app/src/assets/arrow-right.svg delete mode 100644 frontend/appflowy_web_app/src/assets/board.svg delete mode 100644 frontend/appflowy_web_app/src/assets/bold.svg delete mode 100644 frontend/appflowy_web_app/src/assets/clock_alarm.svg delete mode 100644 frontend/appflowy_web_app/src/assets/close.svg delete mode 100644 frontend/appflowy_web_app/src/assets/copy.svg delete mode 100644 frontend/appflowy_web_app/src/assets/dark-logo.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/checkbox-check.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-attach.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-date.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-number.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-person.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-relation.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-text.svg delete mode 100644 frontend/appflowy_web_app/src/assets/database/field-type-url.svg delete mode 100644 frontend/appflowy_web_app/src/assets/date.svg delete mode 100644 frontend/appflowy_web_app/src/assets/delete.svg delete mode 100644 frontend/appflowy_web_app/src/assets/details.svg delete mode 100644 frontend/appflowy_web_app/src/assets/document.svg delete mode 100644 frontend/appflowy_web_app/src/assets/drag.svg delete mode 100644 frontend/appflowy_web_app/src/assets/dropdown.svg delete mode 100644 frontend/appflowy_web_app/src/assets/edit.svg delete mode 100644 frontend/appflowy_web_app/src/assets/eye_close.svg delete mode 100644 frontend/appflowy_web_app/src/assets/eye_open.svg delete mode 100644 frontend/appflowy_web_app/src/assets/grid.svg delete mode 100644 frontend/appflowy_web_app/src/assets/h1.svg delete mode 100644 frontend/appflowy_web_app/src/assets/h2.svg delete mode 100644 frontend/appflowy_web_app/src/assets/h3.svg delete mode 100644 frontend/appflowy_web_app/src/assets/hide-menu.svg delete mode 100644 frontend/appflowy_web_app/src/assets/hide.svg delete mode 100644 frontend/appflowy_web_app/src/assets/image.svg delete mode 100644 frontend/appflowy_web_app/src/assets/images/default_cover.jpg delete mode 100644 frontend/appflowy_web_app/src/assets/inline-code.svg delete mode 100644 frontend/appflowy_web_app/src/assets/italic.svg delete mode 100644 frontend/appflowy_web_app/src/assets/left.svg delete mode 100644 frontend/appflowy_web_app/src/assets/light-logo.svg delete mode 100644 frontend/appflowy_web_app/src/assets/link.svg delete mode 100644 frontend/appflowy_web_app/src/assets/list-dropdown.svg delete mode 100644 frontend/appflowy_web_app/src/assets/list.svg delete mode 100644 frontend/appflowy_web_app/src/assets/mention.svg delete mode 100644 frontend/appflowy_web_app/src/assets/more.svg delete mode 100644 frontend/appflowy_web_app/src/assets/numbers.svg delete mode 100644 frontend/appflowy_web_app/src/assets/open.svg delete mode 100644 frontend/appflowy_web_app/src/assets/quote.svg delete mode 100644 frontend/appflowy_web_app/src/assets/react.svg delete mode 100644 frontend/appflowy_web_app/src/assets/right.svg delete mode 100644 frontend/appflowy_web_app/src/assets/search.svg delete mode 100644 frontend/appflowy_web_app/src/assets/select-check.svg delete mode 100644 frontend/appflowy_web_app/src/assets/settings.svg delete mode 100644 frontend/appflowy_web_app/src/assets/settings/account.svg delete mode 100644 frontend/appflowy_web_app/src/assets/settings/check_circle.svg delete mode 100644 frontend/appflowy_web_app/src/assets/settings/dark.png delete mode 100644 frontend/appflowy_web_app/src/assets/settings/light.png delete mode 100644 frontend/appflowy_web_app/src/assets/settings/workplace.svg delete mode 100644 frontend/appflowy_web_app/src/assets/show-menu.svg delete mode 100644 frontend/appflowy_web_app/src/assets/sort.svg delete mode 100644 frontend/appflowy_web_app/src/assets/strikethrough.svg delete mode 100644 frontend/appflowy_web_app/src/assets/text.svg delete mode 100644 frontend/appflowy_web_app/src/assets/todo-list.svg delete mode 100644 frontend/appflowy_web_app/src/assets/underline.svg delete mode 100644 frontend/appflowy_web_app/src/assets/up.svg create mode 100644 frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx create mode 100644 frontend/appflowy_web_app/src/components/_shared/popover/index.ts create mode 100644 frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx create mode 100644 frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx create mode 100644 frontend/appflowy_web_app/src/components/_shared/tag/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/Database.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/board/Board.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/board/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/calendar/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/conditions/context.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/conditions/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/filters/package.json create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/sorts/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/tabs/ViewTabs.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/tabs/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/grid/Grid.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/grid/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/index.ts create mode 100644 frontend/appflowy_web_app/src/pages/DatabasePage.tsx diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/base.json b/frontend/appflowy_tauri/style-dictionary/tokens/base.json index 4e31b0523d..fb58a867b1 100644 --- a/frontend/appflowy_tauri/style-dictionary/tokens/base.json +++ b/frontend/appflowy_tauri/style-dictionary/tokens/base.json @@ -7,7 +7,7 @@ "type": "color" }, "100": { - "value": "#edeef2", + "value": "#dadbdd", "type": "color" }, "200": { diff --git a/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json b/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json new file mode 100644 index 0000000000..6961f6f1c4 --- /dev/null +++ b/frontend/appflowy_web_app/cypress/fixtures/user_workspace.json @@ -0,0 +1,61 @@ +{ + "data": { + "user_profile": { + "uid": 304120109071339520, + "uuid": "cbff060a-196d-415a-aa80-759c01886466", + "email": "lu@appflowy.io", + "password": "", + "name": "Kilu", + "metadata": { + "icon_url": "🇽🇰" + }, + "encryption_sign": null, + "latest_workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "updated_at": 1715847453 + }, + "visiting_workspace": { + "workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "database_storage_id": "375874be-7a4f-4b7c-8b89-1dc9a39838f4", + "owner_uid": 304120109071339520, + "owner_name": "Kilu", + "workspace_type": 0, + "workspace_name": "Kilu Works", + "created_at": "2024-03-13T07:23:10.275174Z", + "icon": "😆" + }, + "workspaces": [ + { + "workspace_id": "81570fa8-8be9-4b2d-9f1c-1ef4f34079a8", + "database_storage_id": "6c1f1a2c-e8d5-4bc2-917f-495bce862abb", + "owner_uid": 311828434584080384, + "owner_name": "Zack Zi Xiang Fu", + "workspace_type": 0, + "workspace_name": "My Workspace", + "created_at": "2024-04-03T13:53:18.295918Z", + "icon": "" + }, + { + "workspace_id": "fcb503f9-9287-4de4-8de0-ea191e680968", + "database_storage_id": "ae1b82a5-2b93-45c7-901a-f9357c544534", + "owner_uid": 276169796100296704, + "owner_name": "Annie Anqi Wang", + "workspace_type": 0, + "workspace_name": "AppFlowy Test", + "created_at": "2023-12-27T04:18:36.372013Z", + "icon": "" + }, + { + "workspace_id": "9eebea03-3ed5-4298-86b2-a7f77856d48b", + "database_storage_id": "375874be-7a4f-4b7c-8b89-1dc9a39838f4", + "owner_uid": 304120109071339520, + "owner_name": "Kilu", + "workspace_type": 0, + "workspace_name": "Kilu Works", + "created_at": "2024-03-13T07:23:10.275174Z", + "icon": "😆" + } + ] + }, + "code": 0, + "message": "Operation completed successfully." +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/cypress/support/commands.ts b/frontend/appflowy_web_app/cypress/support/commands.ts index 6146bd1c01..b275a842c5 100644 --- a/frontend/appflowy_web_app/cypress/support/commands.ts +++ b/frontend/appflowy_web_app/cypress/support/commands.ts @@ -37,6 +37,7 @@ Cypress.Commands.add('mockAPI', () => { cy.intercept('POST', '/gotrue/token?grant_type=refresh_token', json).as('refreshToken'); }); cy.intercept('GET', '/api/user/profile', { fixture: 'user' }).as('getUserProfile'); + cy.intercept('GET', '/api/user/workspace', { fixture: 'user_workspace' }).as('getUserWorkspace'); }); // Example use: diff --git a/frontend/appflowy_web_app/index.html b/frontend/appflowy_web_app/index.html index 3548e9b85d..5480f37859 100644 --- a/frontend/appflowy_web_app/index.html +++ b/frontend/appflowy_web_app/index.html @@ -3,7 +3,9 @@ - + AppFlowy diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 1acc7d6e82..2dafe5e66d 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -22,12 +22,13 @@ "test:unit": "jest" }, "dependencies": { - "@appflowyinc/client-api-wasm": "0.0.2-alpha.2", + "@appflowyinc/client-api-wasm": "0.0.3", "@atlaskit/primitives": "^5.5.3", "@emoji-mart/data": "^1.1.2", "@emoji-mart/react": "^1.1.1", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", + "@jest/globals": "^29.7.0", "@mui/icons-material": "^5.11.11", "@mui/material": "6.0.0-alpha.2", "@mui/x-date-pickers-pro": "^6.18.2", @@ -35,9 +36,10 @@ "@slate-yjs/core": "^1.0.2", "@tauri-apps/api": "^1.5.3", "@types/react-swipeable-views": "^0.13.4", + "async-retry": "^1.3.3", "axios": "^1.6.8", "dayjs": "^1.11.9", - "dexie": "^4.0.1", + "decimal.js": "^10.4.3", "emoji-mart": "^5.5.2", "emoji-regex": "^10.2.1", "events": "^3.3.0", @@ -51,6 +53,7 @@ "katex": "^0.16.7", "lodash-es": "^4.17.21", "nanoid": "^4.0.0", + "numeral": "^2.0.6", "prismjs": "^1.29.0", "protoc-gen-ts": "0.8.7", "quill": "^1.3.7", @@ -66,6 +69,7 @@ "react-hot-toast": "^2.4.1", "react-i18next": "^14.1.0", "react-katex": "^3.0.1", + "react-measure": "^2.5.2", "react-redux": "^8.0.5", "react-router-dom": "^6.22.3", "react-swipeable-views": "^0.14.0", @@ -98,6 +102,7 @@ "@types/katex": "^0.16.0", "@types/lodash-es": "^4.17.11", "@types/node": "^20.11.30", + "@types/numeral": "^2.0.5", "@types/prismjs": "^1.26.0", "@types/quill": "^2.0.10", "@types/react": "^18.2.66", @@ -107,6 +112,7 @@ "@types/react-datepicker": "^4.19.3", "@types/react-dom": "^18.2.22", "@types/react-katex": "^3.0.0", + "@types/react-measure": "^2.0.12", "@types/react-transition-group": "^4.4.6", "@types/react-window": "^1.8.8", "@types/utf8": "^3.0.1", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index b9fe83de2f..770298d3b9 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: dependencies: '@appflowyinc/client-api-wasm': - specifier: 0.0.2-alpha.2 - version: 0.0.2-alpha.2 + specifier: 0.0.3 + version: 0.0.3 '@atlaskit/primitives': specifier: ^5.5.3 version: 5.5.3(@types/react@18.2.66)(react@18.2.0) @@ -23,6 +23,9 @@ dependencies: '@emotion/styled': specifier: ^11.10.6 version: 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@jest/globals': + specifier: ^29.7.0 + version: 29.7.0 '@mui/icons-material': specifier: ^5.11.11 version: 5.11.11(@mui/material@6.0.0-alpha.2)(@types/react@18.2.66)(react@18.2.0) @@ -44,15 +47,18 @@ dependencies: '@types/react-swipeable-views': specifier: ^0.13.4 version: 0.13.4 + async-retry: + specifier: ^1.3.3 + version: 1.3.3 axios: specifier: ^1.6.8 version: 1.6.8 dayjs: specifier: ^1.11.9 version: 1.11.9 - dexie: - specifier: ^4.0.1 - version: 4.0.1 + decimal.js: + specifier: ^10.4.3 + version: 10.4.3 emoji-mart: specifier: ^5.5.2 version: 5.5.2 @@ -92,6 +98,9 @@ dependencies: nanoid: specifier: ^4.0.0 version: 4.0.0 + numeral: + specifier: ^2.0.6 + version: 2.0.6 prismjs: specifier: ^1.29.0 version: 1.29.0 @@ -137,6 +146,9 @@ dependencies: react-katex: specifier: ^3.0.1 version: 3.0.1(prop-types@15.8.1)(react@18.2.0) + react-measure: + specifier: ^2.5.2 + version: 2.5.2(react-dom@18.2.0)(react@18.2.0) react-redux: specifier: ^8.0.5 version: 8.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) @@ -229,6 +241,9 @@ devDependencies: '@types/node': specifier: ^20.11.30 version: 20.11.30 + '@types/numeral': + specifier: ^2.0.5 + version: 2.0.5 '@types/prismjs': specifier: ^1.26.0 version: 1.26.0 @@ -256,6 +271,9 @@ devDependencies: '@types/react-katex': specifier: ^3.0.0 version: 3.0.0 + '@types/react-measure': + specifier: ^2.0.12 + version: 2.0.12 '@types/react-transition-group': specifier: ^4.4.6 version: 4.4.6 @@ -376,8 +394,8 @@ packages: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - /@appflowyinc/client-api-wasm@0.0.2-alpha.2: - resolution: {integrity: sha512-BcRK06zHHJdaGNYohYxGaR2xPfQ1RwU48jMzdMZDf2HXVLU2WWQ6cYfuM4lrsK+O3QEfJdeEL2fntnQDaaeQng==} + /@appflowyinc/client-api-wasm@0.0.3: + resolution: {integrity: sha512-ARjLhiDZ8MiZ9egWDbAX9VAdXXS30av+InCPLrS/iqCMYrhuuU9rxS9jQeNEB7jucFrj158gBRusimFN7P/lyw==} dev: false /@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0): @@ -2677,6 +2695,10 @@ packages: dependencies: undici-types: 5.26.5 + /@types/numeral@2.0.5: + resolution: {integrity: sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw==} + dev: true + /@types/parse-json@4.0.2: resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} dev: false @@ -2737,6 +2759,12 @@ packages: '@types/react': 18.2.66 dev: true + /@types/react-measure@2.0.12: + resolution: {integrity: sha512-Y6V11CH6bU7RhqrIdENPwEUZlPXhfXNGylMNnGwq5TAEs2wDoBA3kSVVM/EQ8u72sz5r9ja+7W8M8PIVcS841Q==} + dependencies: + '@types/react': 18.2.66 + dev: true + /@types/react-redux@7.1.33: resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==} dependencies: @@ -3234,6 +3262,12 @@ packages: engines: {node: '>=8'} dev: true + /async-retry@1.3.3: + resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + dependencies: + retry: 0.13.1 + dev: false + /async@3.2.5: resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} dev: true @@ -4015,7 +4049,6 @@ packages: /decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} - dev: true /dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} @@ -4101,10 +4134,6 @@ packages: minimist: 1.2.8 dev: true - /dexie@4.0.1: - resolution: {integrity: sha512-wSNn+TcCh+DuE2pdg058K3MhxA4g+IiZlW7yGz4cMd/t3z2rJXZcV3HDxZljbrICU2Iq0qY4UHnbolTMK/+bcA==} - dev: false - /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -4875,6 +4904,10 @@ packages: has-symbols: 1.0.3 hasown: 2.0.2 + /get-node-dimensions@1.2.1: + resolution: {integrity: sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==} + dev: false + /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} @@ -6384,6 +6417,10 @@ packages: boolbase: 1.0.0 dev: true + /numeral@2.0.6: + resolution: {integrity: sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA==} + dev: false + /nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} dev: true @@ -7138,6 +7175,20 @@ packages: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false + /react-measure@2.5.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-M+rpbTLWJ3FD6FXvYV6YEGvQ5tMayQ3fGrZhRPHrE9bVlBYfDCLuDcgNttYfk8IqfOI03jz6cbpqMRTUclQnaA==} + peerDependencies: + react: '>0.13.0' + react-dom: '>0.13.0' + dependencies: + '@babel/runtime': 7.24.4 + get-node-dimensions: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + resize-observer-polyfill: 1.5.1 + dev: false + /react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} peerDependencies: @@ -7452,6 +7503,10 @@ packages: resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} dev: false + /resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + dev: false + /resolve-cwd@3.0.0: resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} engines: {node: '>=8'} @@ -7495,6 +7550,11 @@ packages: signal-exit: 3.0.7 dev: true + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index 0df2729749..9a2bcfe186 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -1,4 +1,4 @@ -import Y from 'yjs'; +import * as Y from 'yjs'; export type BlockId = string; @@ -8,6 +8,10 @@ export type ChildrenId = string; export type ViewId = string; +export type RowId = string; + +export type CellId = string; + export enum BlockType { Paragraph = 'paragraph', Page = 'page', @@ -192,6 +196,51 @@ export enum YjsFolderKey { type = 'ty', value = 'value', layout = 'layout', + bid = 'bid', +} + +export enum YjsDatabaseKey { + views = 'views', + id = 'id', + metas = 'metas', + fields = 'fields', + is_primary = 'is_primary', + last_modified = 'last_modified', + created_at = 'created_at', + name = 'name', + type = 'ty', + type_option = 'type_option', + content = 'content', + data = 'data', + iid = 'iid', + database_id = 'database_id', + field_orders = 'field_orders', + field_settings = 'field_settings', + visibility = 'visibility', + wrap = 'wrap', + width = 'width', + filters = 'filters', + groups = 'groups', + layout = 'layout', + layout_settings = 'layout_settings', + modified_at = 'modified_at', + row_orders = 'row_orders', + sorts = 'sorts', + height = 'height', + cells = 'cells', + field_type = 'field_type', + end_timestamp = 'end_timestamp', + include_time = 'include_time', + is_range = 'is_range', + reminder_id = 'reminder_id', + time_format = 'time_format', + date_format = 'date_format', + calculations = 'calculations', + field_id = 'field_id', + calculation_value = 'calculation_value', + condition = 'condition', + format = 'format', + filter_type = 'filter_type', } export interface YDoc extends Y.Doc { @@ -199,11 +248,54 @@ export interface YDoc extends Y.Doc { getMap(key: YjsEditorKey.data_section): YSharedRoot | any; } +export interface YDatabaseRow extends Y.Map { + get(key: YjsDatabaseKey.id): RowId; + + get(key: YjsDatabaseKey.height): string; + + get(key: YjsDatabaseKey.visibility): boolean; + + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.last_modified): LastModified; + + get(key: YjsDatabaseKey.cells): YDatabaseCells; +} + +export interface YDatabaseCells extends Y.Map { + get(key: FieldId): YDatabaseCell; +} + +export type EndTimestamp = string; +export type ReminderId = string; + +export interface YDatabaseCell extends Y.Map { + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.last_modified): LastModified; + + get(key: YjsDatabaseKey.field_type): string; + + get(key: YjsDatabaseKey.data): object | string | boolean | number; + + get(key: YjsDatabaseKey.end_timestamp): EndTimestamp; + + get(key: YjsDatabaseKey.include_time): boolean; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.is_range): boolean; + + get(key: YjsDatabaseKey.reminder_id): ReminderId; +} + export interface YSharedRoot extends Y.Map { get(key: YjsEditorKey.document): YDocument; - // eslint-disable-next-line @typescript-eslint/unified-signatures get(key: YjsEditorKey.folder): YFolder; + + get(key: YjsEditorKey.database): YDatabase; + + get(key: YjsEditorKey.database_row): YDatabaseRow; } export interface YFolder extends Y.Map { @@ -226,6 +318,9 @@ export interface YViews extends Y.Map { export interface YView extends Y.Map { get(key: YjsFolderKey.id): ViewId; + get(key: YjsFolderKey.bid): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures get(key: YjsFolderKey.name): string; // eslint-disable-next-line @typescript-eslint/unified-signatures @@ -271,6 +366,166 @@ export interface YTextMap extends Y.Map { get(key: ExternalId): Y.Text; } +export interface YDatabase extends Y.Map { + get(key: YjsDatabaseKey.views): YDatabaseViews; + + get(key: YjsDatabaseKey.metas): YDatabaseMetas; + + get(key: YjsDatabaseKey.fields): YDatabaseFields; + + get(key: YjsDatabaseKey.id): string; +} + +export interface YDatabaseViews extends Y.Map { + get(key: ViewId): YDatabaseView; +} + +export type DatabaseId = string; +export type CreatedAt = string; +export type LastModified = string; +export type ModifiedAt = string; +export type FieldId = string; + +export enum DatabaseViewLayout { + Grid = 0, + Board = 1, + Calendar = 2, +} + +export interface YDatabaseView extends Y.Map { + get(key: YjsDatabaseKey.database_id): DatabaseId; + + get(key: YjsDatabaseKey.name): string; + + get(key: YjsDatabaseKey.created_at): CreatedAt; + + get(key: YjsDatabaseKey.modified_at): ModifiedAt; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.layout): string; + + get(key: YjsDatabaseKey.layout_settings): YDatabaseLayoutSettings; + + get(key: YjsDatabaseKey.filters): YDatabaseFilters; + + get(key: YjsDatabaseKey.groups): YDatabaseGroups; + + get(key: YjsDatabaseKey.sorts): YDatabaseSorts; + + get(key: YjsDatabaseKey.field_settings): YDatabaseFieldSettings; + + get(key: YjsDatabaseKey.field_orders): YDatabaseFieldOrders; + + get(key: YjsDatabaseKey.row_orders): YDatabaseRowOrders; + + get(key: YjsDatabaseKey.calculations): YDatabaseCalculations; +} + +export type YDatabaseFieldOrders = Y.Array; // [ { id: FieldId } ] + +export type YDatabaseRowOrders = Y.Array; // [ { id: RowId, height: number } ] + +export type YDatabaseGroups = Y.Array; + +export type YDatabaseFilters = Y.Array; + +export type YDatabaseSorts = Y.Array; + +export type YDatabaseLayoutSettings = Y.Map; + +export type YDatabaseCalculations = Y.Array; + +export type SortId = string; + +export interface YDatabaseRowOrder extends Y.Map { + get(key: YjsDatabaseKey.id): SortId; + + get(key: YjsDatabaseKey.height): number; +} + +export interface YDatabaseSort extends Y.Map { + get(key: YjsDatabaseKey.id): SortId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.condition): string; +} + +export type FilterId = string; + +export interface YDatabaseFilter extends Y.Map { + get(key: YjsDatabaseKey.id): FilterId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.type | YjsDatabaseKey.condition | YjsDatabaseKey.content | YjsDatabaseKey.filter_type): string; +} + +export interface YDatabaseCalculation extends Y.Map { + get(key: YjsDatabaseKey.field_id): FieldId; + + get(key: YjsDatabaseKey.id | YjsDatabaseKey.type | YjsDatabaseKey.calculation_value): string; +} + +export interface YDatabaseFieldSettings extends Y.Map { + get(key: FieldId): YDatabaseFieldSetting; +} + +export interface YDatabaseFieldSetting extends Y.Map { + get(key: YjsDatabaseKey.visibility): string; + + get(key: YjsDatabaseKey.wrap): boolean; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.width): string; +} + +export interface YDatabaseMetas extends Y.Map { + get(key: YjsDatabaseKey.iid): string; +} + +export interface YDatabaseFields extends Y.Map { + get(key: FieldId): YDatabaseField; +} + +export interface YDatabaseField extends Y.Map { + get(key: YjsDatabaseKey.name): string; + + get(key: YjsDatabaseKey.id): FieldId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.type): string; + + get(key: YjsDatabaseKey.type_option): YDatabaseFieldTypeOption; + + get(key: YjsDatabaseKey.is_primary): boolean; + + get(key: YjsDatabaseKey.last_modified): LastModified; +} + +export interface YDatabaseFieldTypeOption extends Y.Map { + // key is the field type + get(key: string): YMapFieldTypeOption; +} + +export interface YMapFieldTypeOption extends Y.Map { + get(key: YjsDatabaseKey.content): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.data): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.time_format): string; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.date_format): string; + + get(key: YjsDatabaseKey.database_id): DatabaseId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.format): string; +} + export enum CollabType { Document = 0, Database = 1, @@ -282,8 +537,12 @@ export enum CollabType { } export enum CollabOrigin { + // from local changes and never sync to remote. used for read-only mode Local = 'local', + // from remote changes and never sync to remote. Remote = 'remote', + // from local changes and sync to remote. used for collaborative mode + LocalSync = 'local_sync', } export const layoutMap = { @@ -292,3 +551,9 @@ export const layoutMap = { [ViewLayout.Board]: 'board', [ViewLayout.Calendar]: 'calendar', }; + +export const databaseLayoutMap = { + [DatabaseViewLayout.Grid]: 'grid', + [DatabaseViewLayout.Board]: 'board', + [DatabaseViewLayout.Calendar]: 'calendar', +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/const.ts b/frontend/appflowy_web_app/src/application/database-yjs/const.ts new file mode 100644 index 0000000000..b082acc6a4 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/const.ts @@ -0,0 +1,2 @@ +export const DEFAULT_ROW_HEIGHT = 37; +export const MIN_COLUMN_WIDTH = 100; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts new file mode 100644 index 0000000000..8717aa0ffe --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -0,0 +1,127 @@ +import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { filterBy } from '@/application/database-yjs/filter'; +import { Row } from '@/application/database-yjs/selector'; +import { sortBy } from '@/application/database-yjs/sort'; +import { createContext, useContext, useEffect, useState } from 'react'; +import * as Y from 'yjs'; +import debounce from 'lodash-es/debounce'; + +export interface DatabaseContextState { + readOnly: boolean; + doc: YDoc; + viewId: string; + rowDocMap: Y.Map; +} + +export const DatabaseContext = createContext(null); + +export const useDatabase = () => { + const database = useContext(DatabaseContext) + ?.doc?.getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.database) as YDatabase; + + return database; +}; + +export const useRowMeta = (rowId: string) => { + const rows = useContext(DatabaseContext)?.rowDocMap; + const rowMetaDoc = rows?.get(rowId); + const rowMeta = rowMetaDoc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + return rowMeta; +}; + +export const useViewId = () => { + const context = useContext(DatabaseContext); + + return context?.viewId; +}; + +export const useReadOnly = () => { + const context = useContext(DatabaseContext); + + return context?.readOnly; +}; + +export const useDatabaseView = () => { + const database = useDatabase(); + const viewId = useViewId(); + + return viewId ? database.get(YjsDatabaseKey.views)?.get(viewId) : undefined; +}; + +export function useDatabaseFields() { + const database = useDatabase(); + + return database.get(YjsDatabaseKey.fields); +} + +export interface GridRowsState { + rowOrders: Row[]; +} + +export const GridRowsContext = createContext(null); + +export function useGridRowsContext() { + return useContext(GridRowsContext); +} + +export function useGridRows() { + return useGridRowsContext()?.rowOrders; +} + +export function useGridRowOrders() { + const rows = useContext(DatabaseContext)?.rowDocMap; + const [rowOrders, setRowOrders] = useState(); + const view = useDatabaseView(); + const sorts = view?.get(YjsDatabaseKey.sorts); + const fields = useDatabaseFields(); + const filters = view?.get(YjsDatabaseKey.filters); + + useEffect(() => { + const onConditionsChange = () => { + const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON(); + + if (!originalRowOrders || !rows) return; + + console.log('sort or filter changed'); + if (sorts?.length === 0 && filters?.length === 0) { + setRowOrders(originalRowOrders); + return; + } + + let rowOrders: Row[] | undefined; + + if (sorts?.length) { + rowOrders = sortBy(originalRowOrders, sorts, fields, rows); + } + + if (filters?.length) { + rowOrders = filterBy(rowOrders ?? originalRowOrders, filters, fields, rows); + } + + if (rowOrders) { + setRowOrders(rowOrders); + } else { + setRowOrders(originalRowOrders); + } + }; + + const debounceConditionsChange = debounce(onConditionsChange, 200); + + onConditionsChange(); + sorts?.observeDeep(debounceConditionsChange); + filters?.observeDeep(debounceConditionsChange); + fields?.observeDeep(debounceConditionsChange); + rows?.observeDeep(debounceConditionsChange); + + return () => { + sorts?.unobserveDeep(debounceConditionsChange); + filters?.unobserveDeep(debounceConditionsChange); + fields?.unobserveDeep(debounceConditionsChange); + rows?.observeDeep(debounceConditionsChange); + }; + }, [fields, rows, sorts, filters, view]); + + return rowOrders; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts new file mode 100644 index 0000000000..f5d4aeac61 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts @@ -0,0 +1,51 @@ +import { FieldId } from '@/application/collab.type'; + +export enum FieldVisibility { + AlwaysShown = 0, + HideWhenEmpty = 1, + AlwaysHidden = 2, +} + +export enum FieldType { + RichText = 0, + Number = 1, + DateTime = 2, + SingleSelect = 3, + MultiSelect = 4, + Checkbox = 5, + URL = 6, + Checklist = 7, + LastEditedTime = 8, + CreatedTime = 9, + Relation = 10, +} + +export enum CalculationType { + Average = 0, + Max = 1, + Median = 2, + Min = 3, + Sum = 4, + Count = 5, + CountEmpty = 6, + CountNonEmpty = 7, +} + +export enum SortCondition { + Ascending = 0, + Descending = 1, +} + +export enum FilterType { + Data = 0, + And = 1, + Or = 2, +} + +export interface Filter { + fieldId: FieldId; + filterType: FilterType; + condition: number; + id: string; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts new file mode 100644 index 0000000000..b9da4341f6 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/checkbox.type.ts @@ -0,0 +1,10 @@ +import { Filter } from '@/application/database-yjs'; + +export enum CheckboxFilterCondition { + IsChecked = 0, + IsUnChecked = 1, +} + +export interface CheckboxFilter extends Filter { + condition: CheckboxFilterCondition; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts new file mode 100644 index 0000000000..9ccd409dc8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checkbox/index.ts @@ -0,0 +1 @@ +export * from './checkbox.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts new file mode 100644 index 0000000000..2b504ded8a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/checklist.type.ts @@ -0,0 +1,10 @@ +import { Filter } from '@/application/database-yjs'; + +export enum ChecklistFilterCondition { + IsComplete = 0, + IsIncomplete = 1, +} + +export interface ChecklistFilter extends Filter { + condition: ChecklistFilterCondition; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts new file mode 100644 index 0000000000..15d37f912b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/index.ts @@ -0,0 +1,2 @@ +export * from './checklist.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts new file mode 100644 index 0000000000..6dd14c71e0 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts @@ -0,0 +1,22 @@ +import { SelectOption } from '../select-option'; + +export interface ChecklistCellData { + selectedOptionIds?: string[]; + options?: SelectOption[]; + percentage: number; +} + +export function parseChecklistData(data: string): ChecklistCellData | null { + try { + const { options, selected_option_ids } = JSON.parse(data); + const percentage = (selected_option_ids.length / options.length) * 100; + + return { + percentage, + options, + selectedOptionIds: selected_option_ids, + }; + } catch (e) { + return null; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts new file mode 100644 index 0000000000..0db15f21eb --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/date.type.ts @@ -0,0 +1,32 @@ +import { Filter } from '@/application/database-yjs'; + +export enum TimeFormat { + TwelveHour = 0, + TwentyFourHour = 1, +} + +export enum DateFormat { + Local = 0, + US = 1, + ISO = 2, + Friendly = 3, + DayMonthYear = 4, +} + +export enum DateFilterCondition { + DateIs = 0, + DateBefore = 1, + DateAfter = 2, + DateOnOrBefore = 3, + DateOnOrAfter = 4, + DateWithIn = 5, + DateIsEmpty = 6, + DateIsNotEmpty = 7, +} + +export interface DateFilter extends Filter { + condition: DateFilterCondition; + start?: number; + end?: number; + timestamp?: number; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts new file mode 100644 index 0000000000..106279c949 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/index.ts @@ -0,0 +1,2 @@ +export * from './date.type'; +export * from './utils'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts new file mode 100644 index 0000000000..985402768b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/date/utils.ts @@ -0,0 +1,29 @@ +import { TimeFormat, DateFormat } from '@/application/database-yjs'; + +export function getTimeFormat(timeFormat?: TimeFormat) { + switch (timeFormat) { + case TimeFormat.TwelveHour: + return 'h:mm A'; + case TimeFormat.TwentyFourHour: + return 'HH:mm'; + default: + return 'HH:mm'; + } +} + +export function getDateFormat(dateFormat?: DateFormat) { + switch (dateFormat) { + case DateFormat.Friendly: + return 'MMM DD, YYYY'; + case DateFormat.ISO: + return 'YYYY-MM-DD'; + case DateFormat.US: + return 'YYYY/MM/DD'; + case DateFormat.Local: + return 'MM/DD/YYYY'; + case DateFormat.DayMonthYear: + return 'DD/MM/YYYY'; + default: + return 'YYYY-MM-DD'; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts new file mode 100644 index 0000000000..5505f0e4ed --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/index.ts @@ -0,0 +1,8 @@ +export * from './type_option'; +export * from './date'; +export * from './number'; +export * from './select-option'; +export * from './text'; +export * from './checkbox'; +export * from './checklist'; +export * from './relation'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts new file mode 100644 index 0000000000..e165752348 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/__tests__/format.test.ts @@ -0,0 +1,628 @@ +import { currencyFormaterMap } from '../format'; +import { NumberFormat } from '../number.type'; +import { expect } from '@jest/globals'; + +const testCases = [0, 1, 0.5, 0.5666, 1000, 10000, 1000000, 10000000, 1000000.0]; +describe('currencyFormaterMap', () => { + test('should return the correct formatter for Num', () => { + const formater = currencyFormaterMap[NumberFormat.Num]; + const result = ['0', '1', '0.5', '0.5666', '1,000', '10,000', '1,000,000', '10,000,000', '1,000,000']; + testCases.forEach((testCase) => { + expect(formater(testCase)).toBe(result[testCases.indexOf(testCase)]); + }); + }); + + test('should return the correct formatter for Percent', () => { + const formater = currencyFormaterMap[NumberFormat.Percent]; + const result = ['0%', '1%', '0.5%', '0.57%', '1,000%', '10,000%', '1,000,000%', '10,000,000%', '1,000,000%']; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for USD', () => { + const formater = currencyFormaterMap[NumberFormat.USD]; + const result = ['$0', '$1', '$0.5', '$0.57', '$1,000', '$10,000', '$1,000,000', '$10,000,000', '$1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for CanadianDollar', () => { + const formater = currencyFormaterMap[NumberFormat.CanadianDollar]; + const result = [ + 'CA$0', + 'CA$1', + 'CA$0.5', + 'CA$0.57', + 'CA$1,000', + 'CA$10,000', + 'CA$1,000,000', + 'CA$10,000,000', + 'CA$1,000,000', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for EUR', () => { + const formater = currencyFormaterMap[NumberFormat.EUR]; + + const result = ['€0', '€1', '€0.5', '€0.57', '€1,000', '€10,000', '€1,000,000', '€10,000,000', '€1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Pound', () => { + const formater = currencyFormaterMap[NumberFormat.Pound]; + + const result = ['£0', '£1', '£0.5', '£0.57', '£1,000', '£10,000', '£1,000,000', '£10,000,000', '£1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Yen', () => { + const formater = currencyFormaterMap[NumberFormat.Yen]; + + const result = [ + '¥0', + '¥1', + '¥0.5', + '¥0.57', + '¥1,000', + '¥10,000', + '¥1,000,000', + '¥10,000,000', + '¥1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Ruble', () => { + const formater = currencyFormaterMap[NumberFormat.Ruble]; + + const result = [ + '0 RUB', + '1 RUB', + '0,5 RUB', + '0,57 RUB', + '1 000 RUB', + '10 000 RUB', + '1 000 000 RUB', + '10 000 000 RUB', + '1 000 000 RUB', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rupee', () => { + const formater = currencyFormaterMap[NumberFormat.Rupee]; + + const result = ['₹0', '₹1', '₹0.5', '₹0.57', '₹1,000', '₹10,000', '₹10,00,000', '₹1,00,00,000', '₹10,00,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Won', () => { + const formater = currencyFormaterMap[NumberFormat.Won]; + + const result = ['₩0', '₩1', '₩0.5', '₩0.57', '₩1,000', '₩10,000', '₩1,000,000', '₩10,000,000', '₩1,000,000']; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Yuan', () => { + const formater = currencyFormaterMap[NumberFormat.Yuan]; + + const result = [ + 'CN¥0', + 'CN¥1', + 'CN¥0.5', + 'CN¥0.57', + 'CN¥1,000', + 'CN¥10,000', + 'CN¥1,000,000', + 'CN¥10,000,000', + 'CN¥1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Real', () => { + const formater = currencyFormaterMap[NumberFormat.Real]; + + const result = [ + 'R$ 0', + 'R$ 1', + 'R$ 0,5', + 'R$ 0,57', + 'R$ 1.000', + 'R$ 10.000', + 'R$ 1.000.000', + 'R$ 10.000.000', + 'R$ 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Lira', () => { + const formater = currencyFormaterMap[NumberFormat.Lira]; + + const result = [ + 'TRY 0', + 'TRY 1', + 'TRY 0,5', + 'TRY 0,57', + 'TRY 1.000', + 'TRY 10.000', + 'TRY 1.000.000', + 'TRY 10.000.000', + 'TRY 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rupiah', () => { + const formater = currencyFormaterMap[NumberFormat.Rupiah]; + + const result = [ + 'IDR 0', + 'IDR 1', + 'IDR 0,5', + 'IDR 0,57', + 'IDR 1.000', + 'IDR 10.000', + 'IDR 1.000.000', + 'IDR 10.000.000', + 'IDR 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Franc', () => { + const formater = currencyFormaterMap[NumberFormat.Franc]; + + const result = [ + 'CHF 0', + 'CHF 1', + 'CHF 0.5', + 'CHF 0.57', + `CHF 1’000`, + `CHF 10’000`, + `CHF 1’000’000`, + `CHF 10’000’000`, + `CHF 1’000’000`, + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for HongKongDollar', () => { + const formater = currencyFormaterMap[NumberFormat.HongKongDollar]; + + const result = [ + 'HK$0', + 'HK$1', + 'HK$0.5', + 'HK$0.57', + 'HK$1,000', + 'HK$10,000', + 'HK$1,000,000', + 'HK$10,000,000', + 'HK$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for NewZealandDollar', () => { + const formater = currencyFormaterMap[NumberFormat.NewZealandDollar]; + + const result = [ + 'NZ$0', + 'NZ$1', + 'NZ$0.5', + 'NZ$0.57', + 'NZ$1,000', + 'NZ$10,000', + 'NZ$1,000,000', + 'NZ$10,000,000', + 'NZ$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Krona', () => { + const formater = currencyFormaterMap[NumberFormat.Krona]; + + const result = [ + '0 SEK', + '1 SEK', + '0,5 SEK', + '0,57 SEK', + '1 000 SEK', + '10 000 SEK', + '1 000 000 SEK', + '10 000 000 SEK', + '1 000 000 SEK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for NorwegianKrone', () => { + const formater = currencyFormaterMap[NumberFormat.NorwegianKrone]; + + const result = [ + 'NOK 0', + 'NOK 1', + 'NOK 0,5', + 'NOK 0,57', + 'NOK 1 000', + 'NOK 10 000', + 'NOK 1 000 000', + 'NOK 10 000 000', + 'NOK 1 000 000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for MexicanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.MexicanPeso]; + + const result = [ + 'MX$0', + 'MX$1', + 'MX$0.5', + 'MX$0.57', + 'MX$1,000', + 'MX$10,000', + 'MX$1,000,000', + 'MX$10,000,000', + 'MX$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Rand', () => { + const formater = currencyFormaterMap[NumberFormat.Rand]; + + const result = [ + 'ZAR 0', + 'ZAR 1', + 'ZAR 0,5', + 'ZAR 0,57', + 'ZAR 1 000', + 'ZAR 10 000', + 'ZAR 1 000 000', + 'ZAR 10 000 000', + 'ZAR 1 000 000', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for NewTaiwanDollar', () => { + const formater = currencyFormaterMap[NumberFormat.NewTaiwanDollar]; + + const result = [ + 'NT$0', + 'NT$1', + 'NT$0.5', + 'NT$0.57', + 'NT$1,000', + 'NT$10,000', + 'NT$1,000,000', + 'NT$10,000,000', + 'NT$1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for DanishKrone', () => { + const formater = currencyFormaterMap[NumberFormat.DanishKrone]; + + const result = [ + '0 DKK', + '1 DKK', + '0,5 DKK', + '0,57 DKK', + '1.000 DKK', + '10.000 DKK', + '1.000.000 DKK', + '10.000.000 DKK', + '1.000.000 DKK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Baht', () => { + const formater = currencyFormaterMap[NumberFormat.Baht]; + + const result = [ + 'THB 0', + 'THB 1', + 'THB 0.5', + 'THB 0.57', + 'THB 1,000', + 'THB 10,000', + 'THB 1,000,000', + 'THB 10,000,000', + 'THB 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Forint', () => { + const formater = currencyFormaterMap[NumberFormat.Forint]; + + const result = [ + '0 HUF', + '1 HUF', + '0,5 HUF', + '0,57 HUF', + '1 000 HUF', + '10 000 HUF', + '1 000 000 HUF', + '10 000 000 HUF', + '1 000 000 HUF', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Koruna', () => { + const formater = currencyFormaterMap[NumberFormat.Koruna]; + + const result = [ + '0 CZK', + '1 CZK', + '0,5 CZK', + '0,57 CZK', + '1 000 CZK', + '10 000 CZK', + '1 000 000 CZK', + '10 000 000 CZK', + '1 000 000 CZK', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Shekel', () => { + const formater = currencyFormaterMap[NumberFormat.Shekel]; + + const result = [ + '‏0 ‏₪', + '‏1 ‏₪', + '‏0.5 ‏₪', + '‏0.57 ‏₪', + '‏1,000 ‏₪', + '‏10,000 ‏₪', + '‏1,000,000 ‏₪', + '‏10,000,000 ‏₪', + '‏1,000,000 ‏₪', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for ChileanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.ChileanPeso]; + + const result = [ + 'CLP 0', + 'CLP 1', + 'CLP 0,5', + 'CLP 0,57', + 'CLP 1.000', + 'CLP 10.000', + 'CLP 1.000.000', + 'CLP 10.000.000', + 'CLP 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for PhilippinePeso', () => { + const formater = currencyFormaterMap[NumberFormat.PhilippinePeso]; + + const result = ['₱0', '₱1', '₱0.5', '₱0.57', '₱1,000', '₱10,000', '₱1,000,000', '₱10,000,000', '₱1,000,000']; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Dirham', () => { + const formater = currencyFormaterMap[NumberFormat.Dirham]; + + const result = [ + '‏0 AED', + '‏1 AED', + '‏0.5 AED', + '‏0.57 AED', + '‏1,000 AED', + '‏10,000 AED', + '‏1,000,000 AED', + '‏10,000,000 AED', + '‏1,000,000 AED', + ]; + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for ColombianPeso', () => { + const formater = currencyFormaterMap[NumberFormat.ColombianPeso]; + + const result = [ + 'COP 0', + 'COP 1', + 'COP 0,5', + 'COP 0,57', + 'COP 1.000', + 'COP 10.000', + 'COP 1.000.000', + 'COP 10.000.000', + 'COP 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + test('should return the correct formatter for Riyal', () => { + const formater = currencyFormaterMap[NumberFormat.Riyal]; + + const result = [ + 'SAR 0', + 'SAR 1', + 'SAR 0.5', + 'SAR 0.57', + 'SAR 1,000', + 'SAR 10,000', + 'SAR 1,000,000', + 'SAR 10,000,000', + 'SAR 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Ringgit', () => { + const formater = currencyFormaterMap[NumberFormat.Ringgit]; + + const result = [ + 'RM 0', + 'RM 1', + 'RM 0.5', + 'RM 0.57', + 'RM 1,000', + 'RM 10,000', + 'RM 1,000,000', + 'RM 10,000,000', + 'RM 1,000,000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for Leu', () => { + const formater = currencyFormaterMap[NumberFormat.Leu]; + + const result = [ + '0 RON', + '1 RON', + '0,5 RON', + '0,57 RON', + '1.000 RON', + '10.000 RON', + '1.000.000 RON', + '10.000.000 RON', + '1.000.000 RON', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for ArgentinePeso', () => { + const formater = currencyFormaterMap[NumberFormat.ArgentinePeso]; + + const result = [ + 'ARS 0', + 'ARS 1', + 'ARS 0,5', + 'ARS 0,57', + 'ARS 1.000', + 'ARS 10.000', + 'ARS 1.000.000', + 'ARS 10.000.000', + 'ARS 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); + + test('should return the correct formatter for UruguayanPeso', () => { + const formater = currencyFormaterMap[NumberFormat.UruguayanPeso]; + + const result = [ + 'UYU 0', + 'UYU 1', + 'UYU 0,5', + 'UYU 0,57', + 'UYU 1.000', + 'UYU 10.000', + 'UYU 1.000.000', + 'UYU 10.000.000', + 'UYU 1.000.000', + ]; + + testCases.forEach((testCase, index) => { + expect(formater(testCase)).toBe(result[index]); + }); + }); +}); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts new file mode 100644 index 0000000000..589f6ac3ec --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/format.ts @@ -0,0 +1,229 @@ +import { NumberFormat } from './number.type'; + +const commonProps = { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + style: 'currency', + currencyDisplay: 'symbol', + useGrouping: true, +}; + +export const currencyFormaterMap: Record string> = { + [NumberFormat.Num]: (n: number) => + new Intl.NumberFormat('en-US', { + style: 'decimal', + minimumFractionDigits: 0, + maximumFractionDigits: 20, + }).format(n), + [NumberFormat.Percent]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + style: 'decimal', + }).format(n) + '%', + [NumberFormat.USD]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + currency: 'USD', + }).format(n), + [NumberFormat.CanadianDollar]: (n: number) => + new Intl.NumberFormat('en-CA', { + ...commonProps, + currency: 'CAD', + }) + .format(n) + .replace('$', 'CA$'), + [NumberFormat.EUR]: (n: number) => + new Intl.NumberFormat('en-IE', { + ...commonProps, + currency: 'EUR', + }).format(n), + [NumberFormat.Pound]: (n: number) => + new Intl.NumberFormat('en-GB', { + ...commonProps, + currency: 'GBP', + }).format(n), + [NumberFormat.Yen]: (n: number) => + new Intl.NumberFormat('ja-JP', { + ...commonProps, + currency: 'JPY', + }).format(n), + [NumberFormat.Ruble]: (n: number) => + new Intl.NumberFormat('ru-RU', { + ...commonProps, + currency: 'RUB', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Rupee]: (n: number) => + new Intl.NumberFormat('hi-IN', { + ...commonProps, + currency: 'INR', + }).format(n), + [NumberFormat.Won]: (n: number) => + new Intl.NumberFormat('ko-KR', { + ...commonProps, + currency: 'KRW', + }).format(n), + [NumberFormat.Yuan]: (n: number) => + new Intl.NumberFormat('zh-CN', { + ...commonProps, + currency: 'CNY', + }) + .format(n) + .replace('¥', 'CN¥'), + [NumberFormat.Real]: (n: number) => + new Intl.NumberFormat('pt-BR', { + ...commonProps, + currency: 'BRL', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Lira]: (n: number) => + new Intl.NumberFormat('tr-TR', { + ...commonProps, + currency: 'TRY', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Rupiah]: (n: number) => + new Intl.NumberFormat('id-ID', { + ...commonProps, + currency: 'IDR', + currencyDisplay: 'code', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.Franc]: (n: number) => + new Intl.NumberFormat('de-CH', { + ...commonProps, + currency: 'CHF', + }) + .format(n) + .replaceAll(' ', ' '), + [NumberFormat.HongKongDollar]: (n: number) => + new Intl.NumberFormat('zh-HK', { + ...commonProps, + currency: 'HKD', + }).format(n), + [NumberFormat.NewZealandDollar]: (n: number) => + new Intl.NumberFormat('en-NZ', { + ...commonProps, + currency: 'NZD', + }) + .format(n) + .replace('$', 'NZ$'), + [NumberFormat.Krona]: (n: number) => + new Intl.NumberFormat('sv-SE', { + ...commonProps, + currency: 'SEK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.NorwegianKrone]: (n: number) => + new Intl.NumberFormat('nb-NO', { + ...commonProps, + currency: 'NOK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.MexicanPeso]: (n: number) => + new Intl.NumberFormat('es-MX', { + ...commonProps, + currency: 'MXN', + }) + .format(n) + .replace('$', 'MX$'), + [NumberFormat.Rand]: (n: number) => + new Intl.NumberFormat('en-ZA', { + ...commonProps, + currency: 'ZAR', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.NewTaiwanDollar]: (n: number) => + new Intl.NumberFormat('zh-TW', { + ...commonProps, + currency: 'TWD', + }) + .format(n) + .replace('$', 'NT$'), + [NumberFormat.DanishKrone]: (n: number) => + new Intl.NumberFormat('da-DK', { + ...commonProps, + currency: 'DKK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Baht]: (n: number) => + new Intl.NumberFormat('th-TH', { + ...commonProps, + currency: 'THB', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Forint]: (n: number) => + new Intl.NumberFormat('hu-HU', { + ...commonProps, + currency: 'HUF', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Koruna]: (n: number) => + new Intl.NumberFormat('cs-CZ', { + ...commonProps, + currency: 'CZK', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Shekel]: (n: number) => + new Intl.NumberFormat('he-IL', { + ...commonProps, + currency: 'ILS', + }).format(n), + [NumberFormat.ChileanPeso]: (n: number) => + new Intl.NumberFormat('es-CL', { + ...commonProps, + currency: 'CLP', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.PhilippinePeso]: (n: number) => + new Intl.NumberFormat('fil-PH', { + ...commonProps, + currency: 'PHP', + }).format(n), + [NumberFormat.Dirham]: (n: number) => + new Intl.NumberFormat('ar-AE', { + ...commonProps, + currency: 'AED', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.ColombianPeso]: (n: number) => + new Intl.NumberFormat('es-CO', { + ...commonProps, + currency: 'COP', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Riyal]: (n: number) => + new Intl.NumberFormat('en-US', { + ...commonProps, + currency: 'SAR', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.Ringgit]: (n: number) => + new Intl.NumberFormat('ms-MY', { + ...commonProps, + currency: 'MYR', + }).format(n), + [NumberFormat.Leu]: (n: number) => + new Intl.NumberFormat('ro-RO', { + ...commonProps, + currency: 'RON', + }).format(n), + [NumberFormat.ArgentinePeso]: (n: number) => + new Intl.NumberFormat('es-AR', { + ...commonProps, + currency: 'ARS', + currencyDisplay: 'code', + }).format(n), + [NumberFormat.UruguayanPeso]: (n: number) => + new Intl.NumberFormat('es-UY', { + ...commonProps, + currency: 'UYU', + currencyDisplay: 'code', + }).format(n), +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts new file mode 100644 index 0000000000..27ca7cd8d8 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/index.ts @@ -0,0 +1,3 @@ +export * from './format'; +export * from './number.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts new file mode 100644 index 0000000000..9140531325 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/number.type.ts @@ -0,0 +1,56 @@ +import { Filter } from '@/application/database-yjs'; + +export enum NumberFormat { + Num = 0, + USD = 1, + CanadianDollar = 2, + EUR = 4, + Pound = 5, + Yen = 6, + Ruble = 7, + Rupee = 8, + Won = 9, + Yuan = 10, + Real = 11, + Lira = 12, + Rupiah = 13, + Franc = 14, + HongKongDollar = 15, + NewZealandDollar = 16, + Krona = 17, + NorwegianKrone = 18, + MexicanPeso = 19, + Rand = 20, + NewTaiwanDollar = 21, + DanishKrone = 22, + Baht = 23, + Forint = 24, + Koruna = 25, + Shekel = 26, + ChileanPeso = 27, + PhilippinePeso = 28, + Dirham = 29, + ColombianPeso = 30, + Riyal = 31, + Ringgit = 32, + Leu = 33, + ArgentinePeso = 34, + UruguayanPeso = 35, + Percent = 36, +} + +export enum NumberFilterCondition { + Equal = 0, + NotEqual = 1, + GreaterThan = 2, + LessThan = 3, + GreaterThanOrEqualTo = 4, + LessThanOrEqualTo = 5, + NumberIsEmpty = 6, + NumberIsNotEmpty = 7, +} + +export interface NumberFilter extends Filter { + condition: NumberFilterCondition; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts new file mode 100644 index 0000000000..9abac198b4 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/number/parse.ts @@ -0,0 +1,11 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { getTypeOptions } from '../type_option'; +import { NumberFormat } from './number.type'; + +export function parseNumberTypeOptions(field: YDatabaseField) { + const numberTypeOption = getTypeOptions(field)?.toJSON(); + + return { + format: parseInt(numberTypeOption.format) as NumberFormat, + }; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts new file mode 100644 index 0000000000..4b94064b52 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/index.ts @@ -0,0 +1,2 @@ +export * from './parse'; +export * from './relation.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts new file mode 100644 index 0000000000..c5820576cd --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/parse.ts @@ -0,0 +1,9 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { RelationTypeOption } from './relation.type'; +import { getTypeOptions } from '../type_option'; + +export function parseRelationTypeOption(field: YDatabaseField) { + const relationTypeOption = getTypeOptions(field)?.toJSON(); + + return relationTypeOption as RelationTypeOption; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts new file mode 100644 index 0000000000..31021afc38 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/relation/relation.type.ts @@ -0,0 +1,9 @@ +import { Filter } from '@/application/database-yjs'; + +export interface RelationTypeOption { + database_id: string; +} + +export interface RelationFilter extends Filter { + condition: number; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts new file mode 100644 index 0000000000..a569b2ca47 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/index.ts @@ -0,0 +1,2 @@ +export * from './select_option.type'; +export * from './parse'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts new file mode 100644 index 0000000000..7840278a34 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/parse.ts @@ -0,0 +1,28 @@ +import { YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { getTypeOptions } from '../type_option'; +import { SelectTypeOption } from './select_option.type'; + +export function parseSelectOptionTypeOptions(field: YDatabaseField) { + const content = getTypeOptions(field)?.get(YjsDatabaseKey.content); + + if (!content) return null; + + try { + return JSON.parse(content) as SelectTypeOption; + } catch (e) { + return null; + } +} + +export function parseSelectOptionCellData(field: YDatabaseField, data: string) { + const typeOption = parseSelectOptionTypeOptions(field); + const selectedIds = typeof data === 'string' ? data.split(',') : []; + + return selectedIds + .map((id) => { + const option = typeOption?.options?.find((option) => option.id === id); + + return option?.name ?? ''; + }) + .join(', '); +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts new file mode 100644 index 0000000000..343941d588 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/select-option/select_option.type.ts @@ -0,0 +1,38 @@ +import { Filter } from '@/application/database-yjs'; + +export enum SelectOptionColor { + Purple = 'Purple', + Pink = 'Pink', + LightPink = 'LightPink', + Orange = 'Orange', + Yellow = 'Yellow', + Lime = 'Lime', + Green = 'Green', + Aqua = 'Aqua', + Blue = 'Blue', +} + +export enum SelectOptionFilterCondition { + OptionIs = 0, + OptionIsNot = 1, + OptionContains = 2, + OptionDoesNotContain = 3, + OptionIsEmpty = 4, + OptionIsNotEmpty = 5, +} + +export interface SelectOptionFilter extends Filter { + condition: SelectOptionFilterCondition; + optionIds: string[]; +} + +export interface SelectOption { + id: string; + name: string; + color: SelectOptionColor; +} + +export interface SelectTypeOption { + disable_color: boolean; + options: SelectOption[]; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts new file mode 100644 index 0000000000..7d0a52cd9d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/index.ts @@ -0,0 +1 @@ +export * from './text.type'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts new file mode 100644 index 0000000000..c2f230c738 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/text/text.type.ts @@ -0,0 +1,17 @@ +import { Filter } from '@/application/database-yjs'; + +export enum TextFilterCondition { + TextIs = 0, + TextIsNot = 1, + TextContains = 2, + TextDoesNotContain = 3, + TextStartsWith = 4, + TextEndsWith = 5, + TextIsEmpty = 6, + TextIsNotEmpty = 7, +} + +export interface TextFilter extends Filter { + condition: TextFilterCondition; + content: string; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts new file mode 100644 index 0000000000..bf9c80706f --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/type_option.ts @@ -0,0 +1,8 @@ +import { YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs'; + +export function getTypeOptions(field: YDatabaseField) { + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/filter.ts b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts new file mode 100644 index 0000000000..73a8663371 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts @@ -0,0 +1,223 @@ +import { + YDatabaseFields, + YDatabaseFilter, + YDatabaseFilters, + YDatabaseRow, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { + CheckboxFilter, + CheckboxFilterCondition, + ChecklistFilter, + ChecklistFilterCondition, + DateFilter, + NumberFilter, + NumberFilterCondition, + parseChecklistData, + SelectOptionFilter, + SelectOptionFilterCondition, + TextFilter, + TextFilterCondition, +} from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import Decimal from 'decimal.js'; +import * as Y from 'yjs'; +import { every, filter, some } from 'lodash-es'; + +export function parseFilter(fieldType: FieldType, filter: YDatabaseFilter) { + const fieldId = filter.get(YjsDatabaseKey.field_id); + const filterType = Number(filter.get(YjsDatabaseKey.filter_type)); + const id = filter.get(YjsDatabaseKey.id); + const content = filter.get(YjsDatabaseKey.content); + const condition = Number(filter.get(YjsDatabaseKey.condition)); + + const value = { + fieldId, + filterType, + condition, + id, + content, + }; + + switch (fieldType) { + case FieldType.URL: + case FieldType.RichText: + return value as TextFilter; + case FieldType.Number: + return value as NumberFilter; + case FieldType.Checklist: + return value as ChecklistFilter; + case FieldType.Checkbox: + return value as CheckboxFilter; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + // eslint-disable-next-line no-case-declarations + const options = content.split(','); + + return { + ...value, + optionIds: options, + } as SelectOptionFilter; + case FieldType.DateTime: + case FieldType.CreatedTime: + case FieldType.LastEditedTime: + return value as DateFilter; + } + + return value; +} + +function createPredicate(conditions: ((row: Row) => boolean)[]) { + return function (item: Row) { + return every(conditions, (condition) => condition(item)); + }; +} + +export function filterBy(rows: Row[], filters: YDatabaseFilters, fields: YDatabaseFields, rowMetas: Y.Map) { + const filterArray = filters.toArray(); + const conditions = filterArray.map((filter) => { + return (row: { id: string }) => { + const fieldId = filter.get(YjsDatabaseKey.field_id); + const field = fields.get(fieldId); + const fieldType = Number(field.get(YjsDatabaseKey.type)); + const rowId = row.id; + const rowMeta = rowMetas.get(rowId); + + if (!rowMeta) return false; + const filterValue = parseFilter(fieldType, filter); + const meta = rowMeta.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + if (!meta) return false; + + const cells = meta.get(YjsDatabaseKey.cells); + const cell = cells.get(fieldId); + + if (!cell) return false; + const { condition, content } = filterValue; + + switch (fieldType) { + case FieldType.URL: + case FieldType.RichText: + return textFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Number: + return numberFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Checkbox: + return checkboxFilterCheck(cell.get(YjsDatabaseKey.data) as string, condition); + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return selectOptionFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + case FieldType.Checklist: + return checklistFilterCheck(cell.get(YjsDatabaseKey.data) as string, content, condition); + default: + return true; + } + }; + }); + const predicate = createPredicate(conditions); + + return filter(rows, predicate); +} + +export function textFilterCheck(data: string, content: string, condition: TextFilterCondition) { + switch (condition) { + case TextFilterCondition.TextContains: + return data.includes(content); + case TextFilterCondition.TextDoesNotContain: + return !data.includes(content); + case TextFilterCondition.TextIs: + return data === content; + case TextFilterCondition.TextIsNot: + return data !== content; + case TextFilterCondition.TextIsEmpty: + return data === ''; + case TextFilterCondition.TextIsNotEmpty: + return data !== ''; + default: + return false; + } +} + +export function numberFilterCheck(data: string, content: string, condition: number) { + const decimal = new Decimal(data).toNumber(); + const filterDecimal = new Decimal(content).toNumber(); + + switch (condition) { + case NumberFilterCondition.Equal: + return decimal === filterDecimal; + case NumberFilterCondition.NotEqual: + return decimal !== filterDecimal; + case NumberFilterCondition.GreaterThan: + return decimal > filterDecimal; + case NumberFilterCondition.GreaterThanOrEqualTo: + return decimal >= filterDecimal; + case NumberFilterCondition.LessThan: + return decimal < filterDecimal; + case NumberFilterCondition.LessThanOrEqualTo: + return decimal <= filterDecimal; + case NumberFilterCondition.NumberIsEmpty: + return data === ''; + case NumberFilterCondition.NumberIsNotEmpty: + return data !== ''; + default: + return false; + } +} + +export function checkboxFilterCheck(data: string, condition: number) { + switch (condition) { + case CheckboxFilterCondition.IsChecked: + return data === 'Yes'; + case CheckboxFilterCondition.IsUnChecked: + return data !== 'Yes'; + default: + return false; + } +} + +export function checklistFilterCheck(data: string, content: string, condition: number) { + const percentage = parseChecklistData(data)?.percentage ?? 0; + + if (condition === ChecklistFilterCondition.IsComplete) { + return percentage === 100; + } + + return percentage !== 100; +} + +export function selectOptionFilterCheck(data: string, content: string, condition: number) { + const selectedOptionIds = data.split(','); + const filterOptionIds = content.split(','); + + switch (condition) { + // Ensure all filterOptionIds are included in selectedOptionIds + case SelectOptionFilterCondition.OptionIs: + return every(filterOptionIds, (option) => selectedOptionIds.includes(option)); + + // Ensure none of the filterOptionIds are included in selectedOptionIds + case SelectOptionFilterCondition.OptionIsNot: + return every(filterOptionIds, (option) => !selectedOptionIds.includes(option)); + + // Ensure at least one of the filterOptionIds is included in selectedOptionIds + case SelectOptionFilterCondition.OptionContains: + return some(filterOptionIds, (option) => selectedOptionIds.includes(option)); + + // Ensure at least one of the filterOptionIds is not included in selectedOptionIds + case SelectOptionFilterCondition.OptionDoesNotContain: + return some(filterOptionIds, (option) => !selectedOptionIds.includes(option)); + + // Ensure selectedOptionIds is empty + case SelectOptionFilterCondition.OptionIsEmpty: + return selectedOptionIds.length === 0; + + // Ensure selectedOptionIds is not empty + case SelectOptionFilterCondition.OptionIsNotEmpty: + return selectedOptionIds.length !== 0; + + // Default case, if no conditions match + default: + return false; + } +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/index.ts new file mode 100644 index 0000000000..708ae080d2 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/index.ts @@ -0,0 +1,8 @@ +export * from './context'; +export * from './fields'; +export * from './context'; +export * from './selector'; +export * from './database.type'; +export * from './const'; +export * from './filter'; +export * from './sort'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts new file mode 100644 index 0000000000..c3222fdf65 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -0,0 +1,227 @@ +import { FieldId, SortId, YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; +import { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; +import { useDatabase, useGridRows, useViewId } from '@/application/database-yjs/context'; +import { parseFilter } from '@/application/database-yjs/filter'; +import { FieldType, FieldVisibility, Filter, SortCondition } from './database.type'; +import { useEffect, useMemo, useState } from 'react'; + +export interface Column { + fieldId: string; + width: number; + visibility: FieldVisibility; + wrap?: boolean; +} + +export interface Row { + id: string; + height: number; +} + +const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; + +export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibility[] = defaultVisible) { + const database = useDatabase(); + const [columns, setColumns] = useState([]); + + useEffect(() => { + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const fields = database?.get(YjsDatabaseKey.fields); + const fieldsOrder = view?.get(YjsDatabaseKey.field_orders); + const fieldSettings = view?.get(YjsDatabaseKey.field_settings); + const getColumns = () => { + if (!fields || !fieldsOrder || !fieldSettings) return []; + const fieldIds = fieldsOrder.toJSON().map((item) => item.id) as string[]; + + return fieldIds + .map((fieldId) => { + const setting = fieldSettings.get(fieldId); + + return { + fieldId, + width: parseInt(setting?.get(YjsDatabaseKey.width)) || MIN_COLUMN_WIDTH, + visibility: parseInt(setting?.get(YjsDatabaseKey.visibility)) as FieldVisibility, + wrap: setting?.get(YjsDatabaseKey.wrap), + }; + }) + .filter((column) => visibilitys.includes(column.visibility)); + }; + + const observerEvent = () => setColumns(getColumns()); + + setColumns(getColumns()); + + fieldsOrder?.observe(observerEvent); + fieldSettings?.observe(observerEvent); + + return () => { + fieldsOrder?.unobserve(observerEvent); + fieldSettings?.unobserve(observerEvent); + }; + }, [database, viewId, visibilitys]); + + return columns; +} + +export function useGridRowsSelector() { + const rowOrders = useGridRows(); + + return useMemo(() => rowOrders ?? [], [rowOrders]); +} + +export function useFieldSelector(fieldId: string) { + const database = useDatabase(); + const [field, setField] = useState(null); + const [clock, setClock] = useState(0); + + useEffect(() => { + if (!database) return; + + const field = database.get(YjsDatabaseKey.fields)?.get(fieldId); + + setField(field || null); + const observerEvent = () => setClock((prev) => prev + 1); + + field.observe(observerEvent); + + return () => { + field.unobserve(observerEvent); + }; + }, [database, fieldId]); + + return { + field, + clock, + }; +} + +export function useFiltersSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [filters, setFilters] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const filterOrders = view?.get(YjsDatabaseKey.filters); + + if (!filterOrders) return; + + const getFilters = () => { + return filterOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setFilters(getFilters()); + + setFilters(getFilters()); + + filterOrders.observe(observerEvent); + + return () => { + filterOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return filters; +} + +export function useFilterSelector(filterId: string) { + const database = useDatabase(); + const viewId = useViewId(); + const fields = database?.get(YjsDatabaseKey.fields); + const [filterValue, setFilterValue] = useState(null); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const filter = view + ?.get(YjsDatabaseKey.filters) + .toArray() + .find((filter) => filter.get(YjsDatabaseKey.id) === filterId); + const field = fields?.get(filter?.get(YjsDatabaseKey.field_id) as FieldId); + + const observerEvent = () => { + if (!filter || !field) return; + const fieldType = Number(field.get(YjsDatabaseKey.type)) as FieldType; + + setFilterValue(parseFilter(fieldType, filter)); + }; + + observerEvent(); + field?.observe(observerEvent); + filter?.observe(observerEvent); + return () => { + field?.unobserve(observerEvent); + filter?.unobserve(observerEvent); + }; + }, [fields, viewId, filterId, database]); + return filterValue; +} + +export function useSortsSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [sorts, setSorts] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const sortOrders = view?.get(YjsDatabaseKey.sorts); + + if (!sortOrders) return; + + const getSorts = () => { + return sortOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setSorts(getSorts()); + + setSorts(getSorts()); + + sortOrders.observe(observerEvent); + + return () => { + sortOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return sorts; +} + +export interface Sort { + fieldId: FieldId; + condition: SortCondition; + id: SortId; +} + +export function useSortSelector(sortId: SortId) { + const database = useDatabase(); + const viewId = useViewId(); + const [sortValue, setSortValue] = useState(null); + const views = database?.get(YjsDatabaseKey.views); + + useEffect(() => { + if (!viewId) return; + const view = views?.get(viewId); + const sort = view + ?.get(YjsDatabaseKey.sorts) + .toArray() + .find((sort) => sort.get(YjsDatabaseKey.id) === sortId); + + const observerEvent = () => { + setSortValue({ + fieldId: sort?.get(YjsDatabaseKey.field_id) as FieldId, + condition: Number(sort?.get(YjsDatabaseKey.condition)), + id: sort?.get(YjsDatabaseKey.id) as SortId, + }); + }; + + observerEvent(); + sort?.observe(observerEvent); + + return () => { + sort?.unobserve(observerEvent); + }; + }, [viewId, sortId, views]); + + return sortValue; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/sort.ts b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts new file mode 100644 index 0000000000..355d4b4ad9 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts @@ -0,0 +1,79 @@ +import { + YDatabaseField, + YDatabaseFields, + YDatabaseRow, + YDatabaseSorts, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { FieldType, SortCondition } from '@/application/database-yjs/database.type'; +import { parseChecklistData, parseSelectOptionCellData } from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import orderBy from 'lodash-es/orderBy'; +import * as Y from 'yjs'; + +export function sortBy(rows: Row[], sorts: YDatabaseSorts, fields: YDatabaseFields, rowMetas: Y.Map) { + const sortArray = sorts.toArray(); + const iteratees = sortArray.map((sort) => { + return (row: { id: string }) => { + const fieldId = sort.get(YjsDatabaseKey.field_id); + const field = fields.get(fieldId); + const fieldType = Number(field.get(YjsDatabaseKey.type)); + + const rowId = row.id; + const rowMeta = rowMetas.get(rowId); + + const defaultData = parseCellDataForSort(field, ''); + + if (!rowMeta) return defaultData; + const meta = rowMeta.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + if (!meta) return defaultData; + if (fieldType === FieldType.LastEditedTime) { + return meta.get(YjsDatabaseKey.last_modified); + } + + if (fieldType === FieldType.CreatedTime) { + return meta.get(YjsDatabaseKey.created_at); + } + + const cells = meta.get(YjsDatabaseKey.cells); + const cell = cells.get(fieldId); + + if (!cell) return defaultData; + + return parseCellDataForSort(field, cell.get(YjsDatabaseKey.data) ?? ''); + }; + }); + const orders = sortArray.map((sort) => { + const condition = Number(sort.get(YjsDatabaseKey.condition)); + + if (condition === SortCondition.Descending) return 'desc'; + return 'asc'; + }); + + return orderBy(rows, iteratees, orders); +} + +export function parseCellDataForSort(field: YDatabaseField, data: string | boolean | number | object) { + const fieldType = Number(field.get(YjsDatabaseKey.type)); + + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + case FieldType.Number: + return data; + case FieldType.Checkbox: + return data === 'Yes'; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return parseSelectOptionCellData(field, typeof data === 'string' ? data : ''); + case FieldType.Checklist: + return parseChecklistData(typeof data === 'string' ? data : '')?.percentage ?? 0; + case FieldType.DateTime: + return Number(data); + case FieldType.Relation: + return ''; + } +} diff --git a/frontend/appflowy_web_app/src/application/document.type.ts b/frontend/appflowy_web_app/src/application/document.type.ts deleted file mode 100644 index da559c5bde..0000000000 --- a/frontend/appflowy_web_app/src/application/document.type.ts +++ /dev/null @@ -1,176 +0,0 @@ -import Y from 'yjs'; - -export type BlockId = string; - -export type ExternalId = string; - -export type ChildrenId = string; - -export enum BlockType { - Paragraph = 'paragraph', - Page = 'page', - HeadingBlock = 'heading', - TodoListBlock = 'todo_list', - BulletedListBlock = 'bulleted_list', - NumberedListBlock = 'numbered_list', - ToggleListBlock = 'toggle_list', - CodeBlock = 'code', - EquationBlock = 'math_equation', - QuoteBlock = 'quote', - CalloutBlock = 'callout', - DividerBlock = 'divider', - ImageBlock = 'image', - GridBlock = 'grid', - OutlineBlock = 'outline', - TableBlock = 'table', - TableCell = 'table/cell', -} - -export enum InlineBlockType { - Formula = 'formula', - Mention = 'mention', -} - -export enum AlignType { - Left = 'left', - Center = 'center', - Right = 'right', -} - -export interface BlockData { - bg_color?: string; - font_color?: string; - align?: AlignType; -} - -export interface HeadingBlockData extends BlockData { - level: number; -} - -export interface NumberedListBlockData extends BlockData { - number: number; -} - -export interface TodoListBlockData extends BlockData { - checked: boolean; -} - -export interface ToggleListBlockData extends BlockData { - collapsed: boolean; -} - -export interface CodeBlockData extends BlockData { - language: string; -} - -export interface CalloutBlockData extends BlockData { - icon: string; -} - -export interface MathEquationBlockData extends BlockData { - formula?: string; -} - -export enum ImageType { - Local = 0, - Internal = 1, - External = 2, -} - -export interface ImageBlockData extends BlockData { - url?: string; - width?: number; - align?: AlignType; - image_type?: ImageType; - height?: number; -} - -export interface OutlineBlockData extends BlockData { - depth?: number; -} - -export interface TableBlockData extends BlockData { - colDefaultWidth: number; - colMinimumWidth: number; - colsHeight: number; - colsLen: number; - rowDefaultHeight: number; - rowsLen: number; -} - -export interface TableCellBlockData extends BlockData { - colPosition: number; - height: number; - rowPosition: number; - width: number; -} - -export enum MentionType { - PageRef = 'page', - Date = 'date', -} - -export interface Mention { - // inline page ref id - page_id?: string; - // reminder date ref id - date?: string; - - type: MentionType; -} - -export enum YjsEditorKey { - data_section = 'data', - document = 'document', - database = 'database', - workspace_database = 'databases', - folder = 'folder', - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - database_row = 'data', - user_awareness = 'user_awareness', - blocks = 'blocks', - page_id = 'page_id', - meta = 'meta', - children_map = 'children_map', - text_map = 'text_map', - text = 'text', - delta = 'delta', - - block_id = 'id', - block_type = 'ty', - // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values - block_data = 'data', - block_parent = 'parent', - block_children = 'children', - block_external_id = 'external_id', - block_external_type = 'external_type', -} - -export interface YDoc extends Y.Doc { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - get(key: YjsEditorKey.data_section | string): YSharedRoot | any; -} - -export interface YSharedRoot extends Y.Map { - get(key: YjsEditorKey.document): YDocument; -} - -export interface YDocument extends Y.Map { - get(key: YjsEditorKey.blocks | YjsEditorKey.page_id | YjsEditorKey.meta): YBlocks | YMeta | string; -} - -export interface YBlocks extends Y.Map { - get(key: BlockId): Y.Map; -} - -export interface YMeta extends Y.Map { - get(key: YjsEditorKey.children_map | YjsEditorKey.text_map): YChildrenMap | YTextMap; -} - -export interface YChildrenMap extends Y.Map { - get(key: ChildrenId): Y.Array; -} - -export interface YTextMap extends Y.Map { - get(key: ExternalId): Y.Text; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts new file mode 100644 index 0000000000..a1bfcdbf21 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts @@ -0,0 +1,170 @@ +import { CollabOrigin, CollabType, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { + batchCollabs, + getCollabStorage, + getCollabStorageWithAPICall, + getUserWorkspace, +} from '@/application/services/js-services/storage'; +import { DatabaseService } from '@/application/services/services.type'; +import * as Y from 'yjs'; + +export class JSDatabaseService implements DatabaseService { + private loadedDatabaseId: Set = new Set(); + + constructor() { + // + } + + async getDatabase( + workspaceId: string, + databaseId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + const rootRowsDoc = new Y.Doc(); + const rowsFolder = rootRowsDoc.getMap(); + const isLoaded = this.loadedDatabaseId.has(databaseId); + let databaseDoc: YDoc | undefined = undefined; + + if (isLoaded) { + databaseDoc = (await getCollabStorage(databaseId, CollabType.Database)).doc; + } else { + databaseDoc = await getCollabStorageWithAPICall(workspaceId, databaseId, CollabType.Database); + } + + const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; + const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString(); + const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); + const rowIds = rowOrders.toJSON() as { + id: string; + }[]; + + if (!rowIds) { + throw new Error('Database rows not found'); + } + + if (isLoaded) { + for (const row of rowIds) { + const { doc } = await getCollabStorage(row.id, CollabType.DatabaseRow); + + rowsFolder.set(row.id, doc); + } + } else { + const rows = await this.loadDatabaseRows( + workspaceId, + rowIds.map((item) => item.id) + ); + + rows.forEach((row, id) => { + rowsFolder.set(id, row); + }); + } + + this.loadedDatabaseId.add(databaseId); + + return { + databaseDoc, + rows: rowsFolder as Y.Map, + }; + } + + async openDatabase( + workspaceId: string, + viewId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + const userWorkspace = await getUserWorkspace(); + + if (!userWorkspace) { + throw new Error('User workspace not found'); + } + + const workspaceDatabaseId = userWorkspace.workspaces.find( + (workspace) => workspace.id === workspaceId + )?.workspaceDatabaseId; + + if (!workspaceDatabaseId) { + throw new Error('Workspace database not found'); + } + + const workspaceDatabase = await getCollabStorageWithAPICall( + workspaceId, + workspaceDatabaseId, + CollabType.WorkspaceDatabase + ); + + const databases = workspaceDatabase + .getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.workspace_database) + .toJSON() as { + views: string[]; + database_id: string; + }[]; + + const databaseMeta = databases.find((item) => { + return item.views.some((databaseViewId: string) => databaseViewId === viewId); + }); + + if (!databaseMeta) { + throw new Error('Database not found'); + } + + const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id); + const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; + const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); + + // Update rows if new rows are added + rowOrders?.observe((event) => { + if (event.changes.added.size > 0) { + const rowIds = rowOrders.toJSON() as { + id: string; + }[]; + + console.log('Update rows', rowIds); + void this.loadDatabaseRows( + workspaceId, + rowIds.map((item) => item.id) + ).then((newRows) => { + newRows.forEach((row, id) => { + rows.set(id, row); + }); + }); + } + }); + const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); + } + }; + + databaseDoc.on('update', handleUpdate); + + return { + databaseDoc, + rows, + }; + } + + async loadDatabaseRows(workspaceId: string, rowIds: string[]) { + const rows = new Map(); + + try { + await batchCollabs( + workspaceId, + rowIds.map((id) => ({ + object_id: id, + collab_type: CollabType.DatabaseRow, + })), + (id, rowDoc) => rows.set(id, rowDoc) + ); + } catch (e) { + console.error(e); + } + + return rows; + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts index ebe8870c15..bf5f0c7aa1 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/db/index.ts @@ -1,41 +1,8 @@ import { YDoc } from '@/application/collab.type'; -import { getAuthInfo } from '@/application/services/js-services/storage'; -import * as Y from 'yjs'; -import { IndexeddbPersistence } from 'y-indexeddb'; import { databasePrefix } from '@/application/constants'; -import BaseDexie from 'dexie'; -import { usersSchema, UsersTable } from './tables/users'; - -const version = 1; - -type DexieTables = UsersTable; -export type Dexie = BaseDexie & T; - -let db: Dexie | undefined; - -export function getDB() { - const authInfo = getAuthInfo(); - - if (!db && authInfo?.uuid) { - return openDB(authInfo?.uuid); - } - - return db; -} - -export function openDB(uuid: string) { - const dbName = `${databasePrefix}_${uuid}`; - - if (db && db.name === dbName) { - return db; - } - - db = new BaseDexie(dbName) as Dexie; - const schema = Object.assign({}, usersSchema); - - db.version(version).stores(schema); - return db; -} +import { getAuthInfo } from '@/application/services/js-services/storage'; +import { IndexeddbPersistence } from 'y-indexeddb'; +import * as Y from 'yjs'; /** * Open the collaboration database, and return a function to close it @@ -66,3 +33,10 @@ export async function deleteCollabDB(docName: string) { await provider.destroy(); } + +export function getDBName(id: string, type: string) { + const { uuid } = getAuthInfo() || {}; + + if (!uuid) throw new Error('No user found'); + return `${uuid}_${type}_${id}`; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts b/frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts deleted file mode 100644 index 1da8f20b0c..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/db/tables/users.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Table } from 'dexie'; -import { UserProfile } from '@/application/user.type'; - -export type UsersTable = { - users: Table; -}; - -export const usersSchema = { - users: 'uuid, uid, email, name, workspaceId, iconUrl', -}; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts index 1af92df8a0..e93809449d 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts @@ -1,41 +1,20 @@ import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; -import { getDocumentStorage } from '@/application/services/js-services/storage/document'; +import { getCollabStorageWithAPICall } from '@/application/services/js-services/storage'; import { DocumentService } from '@/application/services/services.type'; -import { APIService } from 'src/application/services/js-services/wasm'; -import { applyDocument } from 'src/application/ydoc/apply'; export class JSDocumentService implements DocumentService { constructor() { // } - fetchDocument(workspaceId: string, docId: string) { - return APIService.getCollab(workspaceId, docId, CollabType.Document); - } - async openDocument(workspaceId: string, docId: string): Promise { - const { doc, localExist } = await getDocumentStorage(docId); - const asyncApply = async () => { - const res = await this.fetchDocument(workspaceId, docId); - - applyDocument(doc, res.state); - }; - - // If the document exists locally, apply the state asynchronously, - // otherwise, apply the state synchronously - if (localExist) { - void asyncApply(); - } else { - await asyncApply(); - } + const doc = await getCollabStorageWithAPICall(workspaceId, docId, CollabType.Document); const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { - if (origin === CollabOrigin.Remote) { - return; + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); } - - // Send the update to the server - console.log('update', update); }; doc.on('update', handleUpdate); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts index 796cd078d6..c475cfa935 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/folder.service.ts @@ -1,41 +1,19 @@ import { CollabOrigin, CollabType, YDoc } from '@/application/collab.type'; -import { getFolderStorage } from '@/application/services/js-services/storage/folder'; +import { getCollabStorageWithAPICall } from '@/application/services/js-services/storage'; import { FolderService } from '@/application/services/services.type'; -import { APIService } from 'src/application/services/js-services/wasm'; -import { applyDocument } from 'src/application/ydoc/apply'; export class JSFolderService implements FolderService { constructor() { // } - fetchFolder(workspaceId: string) { - return APIService.getCollab(workspaceId, workspaceId, CollabType.Folder); - } - async openWorkspace(workspaceId: string): Promise { - const { doc, localExist } = await getFolderStorage(workspaceId); - const asyncApply = async () => { - const res = await this.fetchFolder(workspaceId); - - applyDocument(doc, res.state); - }; - - // If the document exists locally, apply the state asynchronously, - // otherwise, apply the state synchronously - if (localExist) { - void asyncApply(); - } else { - await asyncApply(); - } - + const doc = await getCollabStorageWithAPICall(workspaceId, workspaceId, CollabType.Folder); const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { - if (origin === CollabOrigin.Remote) { - return; + if (origin === CollabOrigin.LocalSync) { + // Send the update to the server + console.log('update', update); } - - // Send the update to the server - console.log('update', update); }; doc.on('update', handleUpdate); diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts index 3410c8d27e..d31b7f117a 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -1,7 +1,9 @@ +import { JSDatabaseService } from '@/application/services/js-services/database.service'; import { AFService, AFServiceConfig, AuthService, + DatabaseService, DocumentService, FolderService, UserService, @@ -22,6 +24,8 @@ export class AFClientService implements AFService { folderService: FolderService; + databaseService: DatabaseService; + private deviceId: string = nanoid(8); private clientId: string = 'web'; @@ -45,5 +49,6 @@ export class AFClientService implements AFService { this.userService = new JSUserService(); this.documentService = new JSDocumentService(); this.folderService = new JSFolderService(); + this.databaseService = new JSDatabaseService(); } } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts index bb19f590bc..dd8d3d1d99 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/auth.ts @@ -1,11 +1,3 @@ -import { getAuthInfo } from '@/application/services/js-services/storage/token'; -import { openDB } from '@/application/services/js-services/db'; - export async function signInSuccess() { - const authInfo = getAuthInfo(); - - if (authInfo) { - // Open the database - openDB(authInfo.uuid); - } + // Do nothing } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts new file mode 100644 index 0000000000..27ce771d74 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts @@ -0,0 +1,101 @@ +import { CollabType, YDoc, YjsEditorKey } from '@/application/collab.type'; +import { getDBName, openCollabDB } from '@/application/services/js-services/db'; +import { APIService } from '@/application/services/js-services/wasm'; +import { applyDocument } from '@/application/ydoc/apply'; + +export function fetchCollab(workspaceId: string, id: string, type: CollabType) { + return APIService.getCollab(workspaceId, id, type); +} + +export function batchFetchCollab(workspaceId: string, params: { object_id: string; collab_type: CollabType }[]) { + return APIService.batchGetCollab(workspaceId, params); +} + +function collabTypeToDBType(type: CollabType) { + switch (type) { + case CollabType.Folder: + return 'folder'; + case CollabType.Document: + return 'document'; + case CollabType.Database: + return 'database'; + case CollabType.WorkspaceDatabase: + return 'databases'; + case CollabType.DatabaseRow: + return 'database_row'; + case CollabType.UserAwareness: + return 'user_awareness'; + default: + return ''; + } +} + +export async function getCollabStorage(id: string, type: CollabType) { + const name = getDBName(id, collabTypeToDBType(type)); + + const doc = await openCollabDB(name); + const localExist = doc.share.has(YjsEditorKey.data_section); + + return { + doc, + localExist, + }; +} + +export async function getCollabStorageWithAPICall(workspaceId: string, id: string, type: CollabType) { + const { doc, localExist } = await getCollabStorage(id, type); + const asyncApply = async () => { + const res = await fetchCollab(workspaceId, id, type); + + applyDocument(doc, res.state); + }; + + // If the document exists locally, apply the state asynchronously, + // otherwise, apply the state synchronously + if (localExist) { + void asyncApply(); + } else { + await asyncApply(); + } + + return doc; +} + +export async function batchCollabs( + workspaceId: string, + params: { + object_id: string; + collab_type: CollabType; + }[], + rowCallback?: (id: string, doc: YDoc) => void +) { + console.log('Fetching collab data:', params); + // Create or get Y.Doc from local storage + for (const item of params) { + const { object_id, collab_type } = item; + + const { doc } = await getCollabStorage(object_id, collab_type); + + if (rowCallback) { + rowCallback(object_id, doc); + } + } + + // Async fetch collab data and apply to Y.Doc + void (async () => { + const res = await batchFetchCollab(workspaceId, params); + + for (const id of Object.keys(res)) { + const type = params.find((param) => param.object_id === id)?.collab_type; + const data = res[id]; + + if (type === undefined || !data) { + continue; + } + + const { doc } = await getCollabStorage(id, type); + + applyDocument(doc, data); + } + })(); +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts deleted file mode 100644 index 0c1278d216..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/document.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { YjsEditorKey } from '@/application/collab.type'; -import { openCollabDB } from '@/application/services/js-services/db'; -import { getAuthInfo } from '@/application/services/js-services/storage/token'; - -export async function getDocumentStorage(docId: string) { - const docName = getDocName(docId); - const doc = await openCollabDB(docName); - const localExist = doc.share.has(YjsEditorKey.data_section); - - return { - doc, - localExist, - }; -} - -export function getDocName(docId: string) { - const { uuid } = getAuthInfo() || {}; - - if (!uuid) throw new Error('No user found'); - return `${uuid}_document_${docId}`; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts deleted file mode 100644 index 8d70df8d0a..0000000000 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/folder.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { YjsEditorKey } from '@/application/collab.type'; -import { openCollabDB } from '@/application/services/js-services/db'; -import { getAuthInfo } from '@/application/services/js-services/storage/token'; - -export async function getFolderStorage(workspaceId: string) { - const docName = getDocName(workspaceId); - const doc = await openCollabDB(docName); - const localExist = doc.share.has(YjsEditorKey.data_section); - - return { - doc, - localExist, - }; -} - -export function getDocName(workspaceId: string) { - const { uuid } = getAuthInfo() || {}; - - if (!uuid) throw new Error('No user found'); - return `${uuid}_folder_${workspaceId}`; -} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts index d983c71b07..f0b9cab2d6 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/index.ts @@ -1,2 +1,4 @@ -export * from './token'; -export * from './user'; \ No newline at end of file +export * from './token'; +export * from './user'; +export * from './collab'; +export * from './auth'; diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts index 0194bb8e0f..db9626ae8e 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/user.ts @@ -1,18 +1,36 @@ -import { UserProfile } from '@/application/user.type'; -import { getDB } from '@/application/services/js-services/db'; -import { getAuthInfo } from '@/application/services/js-services/storage/token'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; -const primaryKeyName = 'uid'; +const userKey = 'user'; +const workspaceKey = 'workspace'; export async function getSignInUser(): Promise { - const db = getDB(); - const authInfo = getAuthInfo(); + const userStr = localStorage.getItem(userKey); - return db?.users.get(authInfo?.uuid); + try { + return userStr ? JSON.parse(userStr) : undefined; + } catch (e) { + return undefined; + } } export async function setSignInUser(profile: UserProfile) { - const db = getDB(); + const userStr = JSON.stringify(profile); - return db?.users.put(profile, primaryKeyName); + localStorage.setItem(userKey, userStr); +} + +export async function getUserWorkspace(): Promise { + const str = localStorage.getItem(workspaceKey); + + try { + return str ? JSON.parse(str) : undefined; + } catch (e) { + return undefined; + } +} + +export async function setUserWorkspace(workspace: UserWorkspace) { + const str = JSON.stringify(workspace); + + localStorage.setItem(workspaceKey, str); } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts index 88e8ba996a..c4853f850d 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts @@ -1,7 +1,14 @@ import { UserService } from '@/application/services/services.type'; -import { UserProfile } from '@/application/user.type'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; import { APIService } from 'src/application/services/js-services/wasm'; -import { getAuthInfo, getSignInUser, invalidToken, setSignInUser } from '@/application/services/js-services/storage'; +import { + getAuthInfo, + getSignInUser, + getUserWorkspace, + invalidToken, + setSignInUser, + setUserWorkspace, +} from '@/application/services/js-services/storage'; import { asyncDataDecorator } from '@/application/services/js-services/decorator'; async function getUser() { @@ -22,10 +29,17 @@ export class JSUserService implements UserService { return Promise.reject('Not authenticated'); } + await this.getUserWorkspace(); + return null!; } async checkUser(): Promise { return (await getSignInUser()) !== undefined; } + + @asyncDataDecorator(getUserWorkspace, setUserWorkspace, APIService.getUserWorkspace) + async getUserWorkspace(): Promise { + return null!; + } } diff --git a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts index 48a76d1837..f3fecb1215 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/wasm/client_api.ts @@ -1,6 +1,6 @@ import { CollabType } from '@/application/collab.type'; import { ClientAPI } from '@appflowyinc/client-api-wasm'; -import { UserProfile } from '@/application/user.type'; +import { UserProfile, UserWorkspace } from '@/application/user.type'; import { AFCloudConfig } from '@/application/services/services.type'; import { invalidToken, readTokenStr, writeToken } from '@/application/services/js-services/storage'; @@ -77,3 +77,45 @@ export async function getCollab(workspaceId: string, object_id: string, collabTy state, }; } + +export async function batchGetCollab( + workspaceId: string, + params: { + object_id: string; + collab_type: CollabType; + }[] +) { + const res = (await client.batch_get_collab( + workspaceId, + params.map((param) => ({ + object_id: param.object_id, + collab_type: Number(param.collab_type) as 0 | 1 | 2 | 3 | 4 | 5, + })) + )) as unknown as Map; + + const result: Record = {}; + + res.forEach((value, key) => { + result[key] = new Uint8Array(value.doc_state); + }); + return result; +} + +export async function getUserWorkspace(): Promise { + const res = await client.get_user_workspace(); + + return { + visitingWorkspaceId: res.visiting_workspace_id, + workspaces: res.workspaces.map((workspace) => ({ + id: workspace.workspace_id, + name: workspace.workspace_name, + icon: workspace.icon, + owner: { + id: Number(workspace.owner_uid), + name: workspace.owner_name, + }, + type: workspace.workspace_type, + workspaceDatabaseId: workspace.database_storage_id, + })), + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts index d7d3ad069c..7e170b683b 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -1,5 +1,6 @@ import { YDoc } from '@/application/collab.type'; import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/user.type'; +import * as Y from 'yjs'; export interface AFService { getDeviceID: () => string; @@ -8,6 +9,7 @@ export interface AFService { userService: UserService; documentService: DocumentService; folderService: FolderService; + databaseService: DatabaseService; } export interface AFServiceConfig { @@ -32,6 +34,23 @@ export interface DocumentService { openDocument: (workspaceId: string, docId: string) => Promise; } +export interface DatabaseService { + openDatabase: ( + workspaceId: string, + viewId: string + ) => Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }>; + getDatabase: ( + workspaceId: string, + databaseId: string + ) => Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }>; +} + export interface UserService { getUserProfile: () => Promise; checkUser: () => Promise; diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts new file mode 100644 index 0000000000..8644914ca7 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/database.service.ts @@ -0,0 +1,29 @@ +import { YDoc } from '@/application/collab.type'; +import { DatabaseService } from '@/application/services/services.type'; +import * as Y from 'yjs'; + +export class TauriDatabaseService implements DatabaseService { + constructor() { + // + } + + async openDatabase( + _workspaceId: string, + _viewId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + return Promise.reject('Not implemented'); + } + + async getDatabase( + _workspaceId: string, + _databaseId: string + ): Promise<{ + databaseDoc: YDoc; + rows: Y.Map; + }> { + return Promise.reject('Not implemented'); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts index 8bcede6523..9ae2987350 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts @@ -1,5 +1,5 @@ import { DocumentService } from '@/application/services/services.type'; -import Y from 'yjs'; +import * as Y from 'yjs'; export class TauriDocumentService implements DocumentService { async openDocument(_id: string): Promise { diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts index 0f162ba36f..8908c002ee 100644 --- a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts @@ -2,11 +2,13 @@ import { AFService, AFServiceConfig, AuthService, + DatabaseService, DocumentService, FolderService, UserService, } from '@/application/services/services.type'; import { TauriAuthService } from '@/application/services/tauri-services/auth.service'; +import { TauriDatabaseService } from '@/application/services/tauri-services/database.service'; import { TauriFolderService } from '@/application/services/tauri-services/folder.service'; import { TauriUserService } from '@/application/services/tauri-services/user.service'; import { TauriDocumentService } from '@/application/services/tauri-services/document.service'; @@ -21,6 +23,8 @@ export class AFClientService implements AFService { folderService: FolderService; + databaseService: DatabaseService; + private deviceId: string = nanoid(8); private clientId: string = 'web'; @@ -41,5 +45,6 @@ export class AFClientService implements AFService { this.userService = new TauriUserService(); this.documentService = new TauriDocumentService(); this.folderService = new TauriFolderService(); + this.databaseService = new TauriDatabaseService(); } } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts index 1484813ab1..efa2044622 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts @@ -54,7 +54,11 @@ export const YjsEditor = { }, }; -export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor { +export function withYjs( + editor: T, + doc: Y.Doc, + localOrigin: CollabOrigin = CollabOrigin.Local +): T & YjsEditor { const e = editor as T & YjsEditor; const { apply, onChange } = e; @@ -73,11 +77,9 @@ export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor }; const handleYEvents = (events: Array>, transaction: Transaction) => { - if (transaction.origin === CollabOrigin.Local) { - return; + if (transaction.origin === CollabOrigin.Remote) { + YjsEditor.applyRemoteEvents(e, events, transaction); } - - YjsEditor.applyRemoteEvents(e, events, transaction); }; e.connect = () => { @@ -123,7 +125,7 @@ export function withYjs(editor: T, doc: Y.Doc): T & YjsEditor changes.forEach((change) => { applySlateOp(doc, { children: change.slateContent }, change.op); }); - }, CollabOrigin.Local); + }, localOrigin); }; e.apply = (op) => { diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts index edb14cfa0a..5a2fd6670c 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/applySlateOpts.ts @@ -1,6 +1,6 @@ import { Operation, Node } from 'slate'; -import Y from 'yjs'; +import * as Y from 'yjs'; -export function applySlateOp (ydoc: Y.Doc, slateRoot: Node, op: Operation) { +export function applySlateOp(ydoc: Y.Doc, slateRoot: Node, op: Operation) { console.log('applySlateOp', op); -} \ No newline at end of file +} diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts index 3dce8a3d59..dfe5c029e9 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts @@ -1,4 +1,4 @@ -import { YSharedRoot } from '@/application/document.type'; +import { YSharedRoot } from '@/application/collab.type'; import * as Y from 'yjs'; import { Editor, Operation } from 'slate'; diff --git a/frontend/appflowy_web_app/src/application/user.type.ts b/frontend/appflowy_web_app/src/application/user.type.ts index be64d574b4..e2c3bcdb43 100644 --- a/frontend/appflowy_web_app/src/application/user.type.ts +++ b/frontend/appflowy_web_app/src/application/user.type.ts @@ -18,6 +18,11 @@ export interface UserProfile { workspaceId?: string; } +export interface UserWorkspace { + visitingWorkspaceId: string; + workspaces: Workspace[]; +} + export interface Workspace { id: string; name: string; @@ -26,6 +31,8 @@ export interface Workspace { id: number; name: string; }; + type: number; + workspaceDatabaseId: string; } export interface SignUpWithEmailPasswordParams { diff --git a/frontend/appflowy_web_app/src/assets/add.svg b/frontend/appflowy_web_app/src/assets/add.svg deleted file mode 100644 index 049be05cec..0000000000 --- a/frontend/appflowy_web_app/src/assets/add.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/align-center.svg b/frontend/appflowy_web_app/src/assets/align-center.svg deleted file mode 100644 index f4f4999514..0000000000 --- a/frontend/appflowy_web_app/src/assets/align-center.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/align-left.svg b/frontend/appflowy_web_app/src/assets/align-left.svg deleted file mode 100644 index 23957285c7..0000000000 --- a/frontend/appflowy_web_app/src/assets/align-left.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/align-right.svg b/frontend/appflowy_web_app/src/assets/align-right.svg deleted file mode 100644 index bca2d14fc7..0000000000 --- a/frontend/appflowy_web_app/src/assets/align-right.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/arrow-left.svg b/frontend/appflowy_web_app/src/assets/arrow-left.svg deleted file mode 100644 index e4ab9068be..0000000000 --- a/frontend/appflowy_web_app/src/assets/arrow-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/arrow-right.svg b/frontend/appflowy_web_app/src/assets/arrow-right.svg deleted file mode 100644 index dc40ae52a6..0000000000 --- a/frontend/appflowy_web_app/src/assets/arrow-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/board.svg b/frontend/appflowy_web_app/src/assets/board.svg deleted file mode 100644 index 0bb0e3fabe..0000000000 --- a/frontend/appflowy_web_app/src/assets/board.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/bold.svg b/frontend/appflowy_web_app/src/assets/bold.svg deleted file mode 100644 index 878b6329b3..0000000000 --- a/frontend/appflowy_web_app/src/assets/bold.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/clock_alarm.svg b/frontend/appflowy_web_app/src/assets/clock_alarm.svg deleted file mode 100644 index 33a5585ceb..0000000000 --- a/frontend/appflowy_web_app/src/assets/clock_alarm.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/close.svg b/frontend/appflowy_web_app/src/assets/close.svg deleted file mode 100644 index b519b419c0..0000000000 --- a/frontend/appflowy_web_app/src/assets/close.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/copy.svg b/frontend/appflowy_web_app/src/assets/copy.svg deleted file mode 100644 index e21e6cb082..0000000000 --- a/frontend/appflowy_web_app/src/assets/copy.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/dark-logo.svg b/frontend/appflowy_web_app/src/assets/dark-logo.svg deleted file mode 100644 index 80d8c4132e..0000000000 --- a/frontend/appflowy_web_app/src/assets/dark-logo.svg +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg b/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg deleted file mode 100644 index d2fc54c4b7..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg b/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg deleted file mode 100644 index 3b3e17dd31..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg b/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg deleted file mode 100644 index f00f5c7aa2..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg b/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg deleted file mode 100644 index 37f52c47ed..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg b/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg deleted file mode 100644 index 3a88d236a1..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-date.svg b/frontend/appflowy_web_app/src/assets/database/field-type-date.svg deleted file mode 100644 index 78243f1e75..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-date.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg b/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg deleted file mode 100644 index 634af3e361..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg b/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg deleted file mode 100644 index 97a2e9c434..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-number.svg b/frontend/appflowy_web_app/src/assets/database/field-type-number.svg deleted file mode 100644 index 9d8b98d10d..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-number.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-person.svg b/frontend/appflowy_web_app/src/assets/database/field-type-person.svg deleted file mode 100644 index 2fc04be065..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-person.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg b/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg deleted file mode 100644 index f82a41d226..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg b/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg deleted file mode 100644 index 8ccbc9a2e3..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-text.svg b/frontend/appflowy_web_app/src/assets/database/field-type-text.svg deleted file mode 100644 index 7befa5080f..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-text.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-url.svg b/frontend/appflowy_web_app/src/assets/database/field-type-url.svg deleted file mode 100644 index f00f5c7aa2..0000000000 --- a/frontend/appflowy_web_app/src/assets/database/field-type-url.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/date.svg b/frontend/appflowy_web_app/src/assets/date.svg deleted file mode 100644 index 78243f1e75..0000000000 --- a/frontend/appflowy_web_app/src/assets/date.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/delete.svg b/frontend/appflowy_web_app/src/assets/delete.svg deleted file mode 100644 index 9e51636798..0000000000 --- a/frontend/appflowy_web_app/src/assets/delete.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/details.svg b/frontend/appflowy_web_app/src/assets/details.svg deleted file mode 100644 index 22c6830916..0000000000 --- a/frontend/appflowy_web_app/src/assets/details.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/document.svg b/frontend/appflowy_web_app/src/assets/document.svg deleted file mode 100644 index b00e1cfb38..0000000000 --- a/frontend/appflowy_web_app/src/assets/document.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/drag.svg b/frontend/appflowy_web_app/src/assets/drag.svg deleted file mode 100644 index 627c959f9f..0000000000 --- a/frontend/appflowy_web_app/src/assets/drag.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/dropdown.svg b/frontend/appflowy_web_app/src/assets/dropdown.svg deleted file mode 100644 index 95e4964b53..0000000000 --- a/frontend/appflowy_web_app/src/assets/dropdown.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/edit.svg b/frontend/appflowy_web_app/src/assets/edit.svg deleted file mode 100644 index ae93287114..0000000000 --- a/frontend/appflowy_web_app/src/assets/edit.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/eye_close.svg b/frontend/appflowy_web_app/src/assets/eye_close.svg deleted file mode 100644 index 116c715ca8..0000000000 --- a/frontend/appflowy_web_app/src/assets/eye_close.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/eye_open.svg b/frontend/appflowy_web_app/src/assets/eye_open.svg deleted file mode 100644 index fa3017c04d..0000000000 --- a/frontend/appflowy_web_app/src/assets/eye_open.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/grid.svg b/frontend/appflowy_web_app/src/assets/grid.svg deleted file mode 100644 index c397af8130..0000000000 --- a/frontend/appflowy_web_app/src/assets/grid.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/h1.svg b/frontend/appflowy_web_app/src/assets/h1.svg deleted file mode 100644 index b33bd52135..0000000000 --- a/frontend/appflowy_web_app/src/assets/h1.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/h2.svg b/frontend/appflowy_web_app/src/assets/h2.svg deleted file mode 100644 index 7449c57391..0000000000 --- a/frontend/appflowy_web_app/src/assets/h2.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/h3.svg b/frontend/appflowy_web_app/src/assets/h3.svg deleted file mode 100644 index 0976945974..0000000000 --- a/frontend/appflowy_web_app/src/assets/h3.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/hide-menu.svg b/frontend/appflowy_web_app/src/assets/hide-menu.svg deleted file mode 100644 index ce88af8ea7..0000000000 --- a/frontend/appflowy_web_app/src/assets/hide-menu.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/hide.svg b/frontend/appflowy_web_app/src/assets/hide.svg deleted file mode 100644 index 22001ef65d..0000000000 --- a/frontend/appflowy_web_app/src/assets/hide.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/image.svg b/frontend/appflowy_web_app/src/assets/image.svg deleted file mode 100644 index 0739605066..0000000000 --- a/frontend/appflowy_web_app/src/assets/image.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/images/default_cover.jpg b/frontend/appflowy_web_app/src/assets/images/default_cover.jpg deleted file mode 100644 index aeaa6a0f29b2dd9639999e5ad43f42c39420caad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 281498 zcmeFY2UL^ow1x%12p-E^0B29V=(p0Kcr7BejAwuXKX-fU* zLMYNnLazx@0)#O6pP6-L?p-tIo^|J(bI-T#q^_*I>wTZS-{;xSZ~yjhpZ_{vq`3js z25Hk=xOjo)7xhVVK11`6=EDEx;~yva-#o>I3+LZySgu`s#$b8r0t?MWmJ63yE}VDL z0BC3~(EitR)BLx`g^QOi(_XoH?K&Mj^?=$NG#4*ix^(gKC0g3cm#IewQ~#&A%tCwf z_PqyJSPg8h-tlCUefu%*nt*2I4|eDXK~T=t>)mxa4o)s^9wA{7Q896Oh5L$1$|?^Z zX=&>`27wKYj7?0RnZfK{*uQjebaM9g@%8hE2fPjokBE$lejk&RoRXTB{^@f@KB53w zi27PoTvc6DTUX!E_^rL8v#Y!3uim~<%-HzEd7yYU4ODvaZZ{NFe^MS!t8&B3dvTv`k zX@1PB{Bd1C4oYCR^%|k$5R}IX5&vP@KUwy#Gwj{}E6e`bu>Wq?G!4V03)Gu;iG>D8 zL($wkA)J2ya`6ovjVR6k`TE`T3DP>c63-vfSIvy-0_FyD8WdWgF*%{D0> z4)n{ECE_vCVY{-SNZSyZw#@B&ZDPPXYm$Wv@+_!Yyd6gzM?jE|pZcsDWvQ#7LDYmDFUqp!LoS_T)mCaq7UvTr%vmi44s9a(=0{;uT!p{)7S@T zofW`NUr z-!&t)#1(i@yKy%jxLm$J@w3a_)t$}5eC7ww!^<}`-znuqinjN#8E6-O?2h>woo7T# zV@m3W)xOelZiv%@rEX3Zsf=$jL}dx!y;(Gve=M!a>~+d!HjEebwPfL zZHgURifs}|up~+MQPPM|{-fGY?SIp?2^8}z;wW1$?k+@)eB)2a^6fTd=)zZH}I%0pH zBPTnkrqV0BwaX24_Pv+&u>EIIdyr0Skq}BlLjAAbbbASeRGq6OW@NeU&(3{HA4%hnPiwdTE^!6VsMWF7IuhSNNnR;OIZ2Z@z1~Dmj2Tn5Q)$WCgp2Tw+ zx53a(K*g2p#eQJ;AmN;*-7iO}mE`gT6&b=q3d1vSQtr63ZH8PPDJ_w2k!9p}XVzUj zy#+qf!npcmyL7=qqkU&g+f-7%A%A0GX0?2UH-?0aaq6`?Ak$oRxJ^b>b%dFsn6lWZ z4!g3lc4qjk#sI_K+X<+*ah^MF6 z6QM9}lakMAF3*uXiRU!@CzD{siD@<_^lOi8E2u-<`C%46 zO4ngSVFmCelhK7g-5Fh8mzGuuC1m2%@AN>U3?^J96=V|d;@opV{M;{gSsW*FC$Ql~ z;=aqAT*XpeV7XMK{@iIzsZCK(SGQEp_u=&w1yKXDK(sy7YF=It3k4y!rae8A-v!N% z=xyMpt6W1o&uM<>DeO)eb}rPLZnKn9I!{b85tKp2c_~aLcDpbPrJql-*||e#4Z9jI z69mYcHT%_@4_AFk+_DmBA(W-xs^Rea#&&q=No@s&XBbj*SS9-MMxTelppckds&b%% zvY1hIks`<_$vvaQ@l*k7>N=?LaybB~Bb6Yo#HZ$2+6)dmOwiA;W@}_y_8(NxPt}{> zF>P|&CJR|L&W4(V65JQM%1Hvd9m8*cHTLP-n}y_y^`seZywk4hf%`P`4-S6C$_dIf z(Bd4&FobZ_fwuI1NMy|4^lKHh=Ih6~a(3KjfupVJ#>uKHj4wyKyBBoGr*U`5Aam|_ z0goyZ>52sLVQ6uP64FF(s40B$_!iK9YuVsbNog4C+89(9s;gKO-O)^6z7-(#N^O?U z#k;rBXgMW#k{@CQ+UqI`l4^Bd(Z#b*b~NqA=CbXE+%R)iac_4Sj;1JmBUz(dXopH| z2s`-B>4N#|RpSidjbpAs@+U7ik-gsDe8>cEYu`xYG+w4kY1vM3wzNA2BujgU#(2+Z zS8RoLU*Zj)8*44^A>N>&HqT%N*8Dzu8&+JaOKF^=|<6Bv_ z3l_+4=E5&DWZ=}EG8<+Z%o&( zPDIdzT3c89cUzNg52aIwtU-)=M-f~DUdEjjONTI#^+%fx5St;_tH#c0oPtdc!@ku| zE)qndgXnQhbF2oF+chiW`Vj{Jdgp14Qoy?)sRH>l4S|F~0ir_qLMGm0)>K*?WfD%* zmwA?P6D4Zw`Mu{#)-6ZlLL`^6*XiT2xcrT=w2HQ!>0?PPzU|eg*~%s-<#mNP{QZi4 zrbY`qlnyL4^BDy49)fjKHh~{tXMddh{KDMW>{><{N}>!;`0+4Oos7M!q7j!7#F62p zgPRFu5r8p*gP~`xX=~jDRtj5_{#W|;Xj#mT3#RTI%jd2jEuLZ zdnHil4G+Tvkxju$=W3nsOmm?YN4*EF?sf-c6VwQN^;M8K-ie+h{XxDRJz`ERIyz@ ziBHS;2(Di*SE`NlXSy$wT=e+J+p20_d-Q^*#4%jS&d$oMDOV_0QLQn58QI7_ecWXx zr794`%m+rxI4<`)ggE#tKrCI4oLrfs(c`L#UxBvS=7+&Q+g$Xgc1_RF@@W(2G!DAV zF(K@^xSfpWW76A5!kad&=3Jx7(dco7TXfwo>)VJ|KBFWjr*q!BetiZrx77E(RnyE} z;F;WC`LTbwHy-{vmQkYM3|V8~b!2zHEKkgdyJiMtvC>`gsNz#ZL{LK%*s6D}<<#lr zKGb88d^?Y%|GgY5 z(M8B3TuH}0#17JH|2zm~e>QJUO08M0{Ozkp4@%nU=`kU-Xpgti+DP?sP5c))E(S^y8dnY6qwFO+$HH z@&QpMe#`Fpi+Ykf_OvDPkhe->sP*o+i43FJPR7c2c-l^TFo*tcx%IqLJvD(7X~V$= znTQvO2Wb`eFYp%@y(lzLlvnWy{G;i+&;w)Bs4THE`W)bTjze% zdNU_B&Am%HO_6;#btx%m!J>%yFwTO5`2A^W<5(-8)JT^Z@cQZDURc37O;zYr&!IVq z_f&**lR2vGQZs!aN%yga(R4Q|v5u76^*ZkH(+!FQk^L`MUGp~A;pO0b1A7#8NOq&I z_RrLB*CbNR$i62JNlp77@I14wxLr;>DP7KLG|e=9VIvFJ@rfrHYUaSWA1^iQ9c&PX%C6mGrGXCHJd*90p;iNF)Mni0`z0nc7k&+}sub3!M49 z_do;Y8GbY7gszQEz2a}2W>dtd8QY@FMUe<9M3NHedaB7G^WK^^h!rPMG@)o9jn3O> z6(Kw!s)fHwgZq>-I>vQ@#4b;4^l3mB4&I(}D0K%t+}9dt@T)c1@ZgLgn>!(I7yOZ{pknOJcX~h0C>3dv8lcrw3s^d*;i+(8?i#DS2eaGD5(-dNcQz=B zZkL}QEcOxIuJUZl#g2!T^(@`~la_{!=IMq1^Y#A+7r4XC+gpW+J<&di*GO_5CzCN` z!}9qqlw-&(!p`S_5|`I4t*DT*jC|_WYCcmG&`Si#B^M+Sr)(b2QCOr~CO?1*4$*Gkzc`bJw*bSl!|M4lI#tK#M{6X2dHK z33gZ-fHAUTG&@LJfW^uasU1Nlj}T8qn-qy+kl-wPmE;tgYmM^ZKJYojM>9d~7e2n? zYa8IIstQ`O$G217Jh;q~!Kx>m;e;-%)l<(1He`2Js5mIb64{EMc0MgqQCKqxFET;E zQnJKH(O_pe6f_Gj3So^&5zKm#tjAdR;%_~9|3e0U^=DhP&Q>Ea_pp|fiVBh;Nnv$M zL!UJ`3Qrt-P6`aAhs$XvS&LhFUJ=X*>W!VU&UC-ia;zAcPnx@l!{0I##%~rMzj(aH63Cd@Ye_;LNOzlP7QhWscRH%lCi%01McU+H$ikuG8ST(73)SB3C7gWlW55li#H1)%_^o7|+M^*N zDc6;Okzt0OXN>fcoydm6Yr~(|tC`-!7>m%FBr@9Cjz1GmS)Q{Z+mk>#AHkdaW7q4eqD>rpj*m;0rJ zs2-SUj1>E$D4j7>!h&(V$%P~_kr<_RNZ)33trQR}HgrtSXHrATXuau)$X-6ENOqf_ zPqFygH?1sMx6AHODqdSY0+7g|49(vlK9ubC^#6I<r zLX)*Fv3iPHKWiqMYTlWnKa)3GG>;%7`%Nxq4u%+?%~rmidq0-pPPW8aWAS^=*QcQ= zs}g${)HReC#Kf)FOh^p$wHs>jtJ#u)ozt2{1MKv5Ygr7m&DCYus0tKJcKup7a{7$Y z3*-mdTE|TI=5Q$(Z;t?Bega*7*{3NnCfBG-jPdo5KJN-XR- z@Pae#xHUV+?pFSw{iW%tZi5v6xcmdCb&OLXMvpvW+4JBqeM^VRSCP-Ywk_sa-%xu% z*gdo(2(`udGGARYASHIcrZkp0wQ>>?!)ws!$g0s9*yMqk-5cvo|B-%|{Iv-d>rIsp zwPWR_TfDj>wVB(%{z31!D|*`Ol?T}hCP68A*T6_{5_oSp+<2SaK}=O!F~&W^KtGW; zv3+?TZ^)|1uI$x*FE(XmqHM(SPHnQEu2x_JipuY`Y-(1w`VZLSsXT_m9 z;$iB%UGtnRjhNj6%#PbD$%9~xz+fFvQ`=~*o}9_Z!7Wv@kpoNF!(GU@d^XivRgD?b zD`LFm*JC^Igt1CEIHr!jIiP3^tovCQFNhaJa%&r;*mF~TCO_&pAMyHG#@%vFXQX%x zAxY0t7&B6U(UZr33NxJ7_EgptfLcJ?VUm#PcI7z@Ee#>G69}LTEHFq08Dp6v(-Ds* z4(9y3a&8gg(-64AQ4Gxb8r)4Ugfe{U(y5Cawh!>Xd_mIZuUb96jmbm6^tAtUkOd}M zwmGMiM`>dx8@!ouBSLx16E)bsE%H~cq;fK-#lBzK=-A#J$eZ!Jf)%CDYS8U#DsRUH zcGV;|?=9=6DSorG%ucSZ|DGd2*lEHt-`JdIC2A*!et~DUp9~dY7g!u&r8Q}<3uDXH z(lP7bT9b>+2G9yfO6FH{3$uB1Fj|oFMlb7E`#UXg2>WfilqD3Tz)lGwO-vC?L5S8O zpnCEX>)FH<2d*J0uq@ah$sZb*iqS82nv>%xaX^!-7ZW>kxk%7+n(mCNQPaOQD>=37 zUC__me3>gE72Br#y!z@n7wkpXx&=Ei1!;AKfnSjk%Eh){4Zjva{4(p(T(wWV!M?yR@X9(=g7Zp;Ami6;H3OyT1i4egxE)cP9S&ZF1JwEI_cY zm3cF56<85Ep!o<3WMbj3_LMve6^_e!pT= zv;96)i*D5BSWdr_wcURmGFuGU?7g4tv=-uI`t9|DKLG`8ENzQP*7dpViBrrE&+5s( z1XwWd;{AFQ+}}FzH}2NO)vgoH@MasSIDR%_;(Ik#_fX-se3HLat`X)=UCzqn_Mw15 zRl|hOne6C_L?mI&lfxi-0F?^u60^mNwa+FStwK}hz)y=O1yQuzmLd*)WA?%&0b|tB zSy)sUZCW*3Al%Hnz#dIm0;*n(+CwK(Q+sv8gT+V8k20pGR}NR^hlb_@o0=MNb+xs1 zF`ykcH;0e5|47zTmooy8z6qiTlOzzg0b3!a#qQ%E&UlWty-!vaWYpE3_j_BWlTbHJNp|FMF;O0d%z|2Yf3?26Q9*<K)z8l#gs%Q=)zUGd z+D4@n8JYu#2%OA2jhkd?!XTA`ZoNa+CTb)u?$VO1pWN>aC?&;OwM6M-B%!#&Ky!N0 zf`-pLn+9WLTl<6e#~s?nc3*8tl}X$7uvjH^Y8C(1-qaAqkyT<=8{Vi@he}nR6NWk zh1z4AfbUiO@`4u^Zj!t^VfwMnGH8pnK}Ur-=2JVJjAz>l+c%tfg3_m5xBT|hYY%Fd z!{(w;$I~uWXXc3(*Q{PQEMk|~(@)cVrkiGx)LMjc$m@MKQ1SU)W`~iN z&$MA&K2~Tj#LR4$+iVKG?eH>)ms+y?w@=#uUluC#$*kpgk?nDOcrCSBF&x&);^^Pc zoEReG{Q6Zo&;0}z+YC$Qj)`5Vd{SoD(G6YJ2ME}l4r91sM{w!9(}dSd?JyCDky!7+ zy8k>4cZeRAz>*N1vkOTo673ZgM!|e= z_`$QDkb%&uo*$u8oYMBdN+=tqh?JN3THmLzodw`t8gxt$?06BArQ3SpbXL8_o)7^k$mXkk&?*tj2JRu$`b^_T+F=g(b$9)}N9BTunu?^l4 zveaLqOBVN1`bwPx+{_=&ttMv&h1kGkdtQ=eJMNWQbDN(0jw1PsW3w*HZlM^yj+&%8 zkJeNweJe7X>@unHb=Ea^;_<e)b< znL0vb&olC2FlrHPwJu$)ioJKph zB2Wyi6V;l`y&0a$*P~28i=hn2H_=Ip@KbIW%OoybZsTTJrcq=$>UVk~drBproaSam zHs3_GCA3_vl_ew|yX>wVdhg(z=1-m9g#Xmu`PUUME*!i}$U1lNRD`H@j!h)tq>zKvGgak48NGY}hsfKArw}%t!6dH`u^P`1QpNZA z`Z$WNW|^o-g(SKYvUe;jKotZ;#RXh3rxe4xJ`maxvsj{$lF~M5#K=om!-qY&l)KsHc?vysF2weGi(3uP z1X>@2MODXH2~BvS6O8a&rHKjitfYVfl6kv^7=aSukBtxP7FK^Cuh3-Zz>zj#FgvTO z{z35`Y?yU+o!G@5H&GGhsg%T#BwLB?XqX+Ulj9BR#NCeVc zYU#=#1r9QP_fxYMrh?VTf+s43#E6_5@FMKdg%t{?=TV$j2a%TP3Nm9fO+Os#v;+_8 zzt?5A_jqi85MZTBZ!U|S)!?N6h}&ysv;Sn5CO~V*5|t$yRBvq+n))vXqJQ07{CBOj zU7Zaj=Sm1p@HOiuHB#4ANI5x#9Sq^5R$>BwWUIv#Kt9=Qt|t7T+5 z36|dIF|0_t<RWO(WDr1j zT0XOh=OnX*9Y!~w<+aK=QMi68=!Vj*QK&gZc+f4E0C()}RXA(M(aA(`E`%e z2{m+H?xJ#;D6$9j(}Ju?ioi2R@N*L!5`)xTT)U)^`abQ+O854DHI!~7ISQO z*m72^Awroxr>Sx1+@1Eu(>wO!7h+UuBPd84Y?E##Gn3R+)xTyB7w2M7RIKdi`r25tstop`x(CU& ztlS6ni-~E;PXbAo2y>*-GwU+yWKNChiHHP%`XJDh43HHx*{xS7P%!E7aa~hF*kLT? zdJ#1@{CU;k8KO)A*&2P8F+F^0+Qlz3Q6BW5d;|ER zDrSCYa(lWi&)@sdG0F9hrX(xBgR{2d~v035wK#kh~WN8j5B80 z#B_A?QF7h7lS9#8mBM(v?_mUSisL6V4AFzz0wzX=iPfpkri!d)`=kn%c{g@nCRdUyt)B(j{0V3{ zr{Qi}Yz?hr?q2u>0H1}afw1Q^k#(>O1cyisUU)xlJ{PsAN2Kp=r6Kqq!W>W3=UOI8 z6{Bhf4Gn5XYEEU?HxFNX&$^`bdLEj)@&xubt{gv+xn|#BF>lr^VmUlLR<6si46p;& zTDdt1=c7csdcS#i)!ZsuO498_e87t0=T&6CE)R=;D3}e-5{Dc)AqPgi+*sV_{F+h| zJ7P|%XXNGM%?*C~y2@CNO6T;LjQH|Ond`Ph3xFKHu20^xGPBgKLU#|ldow`4e7r^f z2UAJo6??8B+3-}nXhW}mTx!NK%0|8*3-&F^scapsa0IArBRrKM$#8C)Q`ZjTS(X~$ zRDrBZ@NeI}40nJ`s43&zqhFuXynW?M@@;$HdWTS3ItH^yuW-&7JWR#T(H_v3lODUv zONsY^geG6rOu5$Ts>_bl6j7{2u##=U-D5pZ(a~swZlAUG>ah50MlKmc_n=wFrgyS( zzb|E3}6a2h;%FMh>Qa7R&`nRg-cT7ih=| z1nc(4wdistIPkmY#*tVc&YJaM;ml_1or&7gki(;=O{v@anY!vCka}}fDe2+u3lk@M zD=w?VJ$~LFOY}F^j7zP`L{cG0H)BUbFOP7W%SjV@eUKf#=}1~5?0?J8e&Y$*|O;M+`Hw>>pnkk-_#$4AWNs`XV!C9I5E42 z@dSqvL(v`&&WjWM@Q$9@{!7=NS1Ehj?m;ef;BN$_(U->4~d6D^(L=v2BLaq>2JM<$cY@=$di`LhF2~+EU-jCjN*xS36jDiFc1^Ru1Ljyhb z<~LrKAw2zyou|W(R1#1D{-NHUo+9K-&!cEQCtNz`YI87-^XAJSvu-|6d=5}~*xR|U zOoG!2d>3_1lLZy^W3i&fMxQ=?xq(ONRg={Bk&Z<#vNZ42D1>J2L7H{?VpYj4hAY;Q z$nYQ7FGVAHAlQwB?YfziRb}cXE+S@Bq$oC)f)WEA8538Z|4;6sVdm4y`r&L}I+k(& z$k5ZnOsdxLR_Qvnl0Ae@fhRZsmPnzM&>^ZgEfS4Ox7Li40(Dcnfh4|CKxcn?XGTzv zwn)~Y7=IV}Nl6@#R$-C>%_W06U6Q6>xq#eoX?tSW!700Xz-x4qZ1@u}fdftK&(gjxFGvX21SUwWTFc z#+wR~cJ%~8?<-Uxw1JG`E~6~gNs94wEF{O#M3U2wkjLSbc4>mSP!O;b26~mW|8qpJ z4)j`rvGDE}+Ar5R0x&yz;-O6f<$HJXFesUr8zGgkG)1xsz0G6J-(m>Z@5;6}O2ib5nF7I>izrFB&CC`LOG5cb}C ze^#S*P%WRC5-mR=+XeTC<%uxQO`D>mR(MtQ(!*=W!|uY&7RYIZHO_9 z+tz_D1{V4=chcdCG;3vWhFNN}maWh!^5<3Kj!6U=rf_>zv=)%k%DHDB^H=A8!g?#^ zUfUwpnpdZsGG#?eh^Rp>5;(uNF`Camdi}7Jl-y-?Z&KPX4u8|J3hrC|L#l!Q;^dhj zr8DPdj^4f*o=Kz}q+QMp|B-9bK4w1N=OxXD4MH&cw_r0Ls>RoV2c4(LXuO}42`-DD zLwfUS@=rrcv}q>}t-D*WCP#2<`}Ldnch-acw;Q@e;U__@dEvty6z(`+dXaUv%!|a` z2Zb|X-bIk>_80Q2BK;C2n~+mHNjqN%FX4YJ-c`ynuU$6`8B|A_SS^bl%opV6WJuKK zJ8T1WLa$ebD8Vt<-AE<)IGIx%<^C*eq*zAqx!_|@B_U3Eq0#xJFP%hkBmoHRTlADA&!FpYbT?%T;BX^|of7dqY6DJR2LNIFeYsU=3KiV$e`|KBp zw|7>!+x)PTiOriYvwiwmoLWl2y8g6{97GX}R3SYc5E0cCH(X>Ed^{L{`@Hqq;q_&Lsd%^ zIJE)@yHWcyQh1(5+wpx##exW<3 zNqe=aGDu2P9QaK>r;+PfHCypb?0WgR)jh)&ZC5QhEZbv}Bf_1~V|P!kD7wp*_$($> z!n7P#uwh)J4DoWU3pF%A=rC%h0hqzrJZw(wsXX#|T!u&pi$R2nM~p$7z(4iUN(fjr zcRc;Si0&^czl@<>kgx2_o&Pv~6(gywU$IhrBU`8c8rZ$YmZ~?uxQ;0@v8chSByC@7 z1U8DGKF50E)HV2`mADA9`go@5vA1lYJA1n_AoqjI3O49w1d(@7-x>!4>&#akFSl5*K z7UM;QN!iP!oOnrT{zSr8Oc=$Uje4rLs{KT9lb%2SCkXJL%1Crf%6u$;3DjO_M7ftZ z-~Tn`wSyoDpKahVGHC`Byp)&mcdRqn2*FAIc?2kLdO>l@QmERx#a#{BX_F8H^Ly{xH^xR}gxQ9&yd2F!^0t(I}j zH$Kl_oIwG%ZnYZ5EV~|e(+{kbmP@%!`Zg2$6CN8zS)9`ZFx?8S7aa^pbu`Z3Hp$AH zs@-V=Npz7j)trAhll+~>Q2Ny_bD`LzX8XNY)n;4~|EVPZp*h23c6wY)kkwf$&?%Q~ zW>&o`@-vYMkELWRhdn$5Mx6LYbyN!#jvJjy%}qwSx#N5;>m62mz;sdz+)tg)a_0H< zJ7n^5->9itg*)1LGhNH;ZXgwmTbIJa61s`)ks_b_teKN9x?DGl)*IjF)s^if)e18fGBb9LLNA*h+7NC2#R}Aa0^;}o zv3cRbe%sbmIZ31gHqjP}(|e71II%0gG)e|~ew}G@B1jHk7Ayw+`s#i@=BJmBclcIq zc8Bxsalz1+AQZbzC8YRZ!CyoC6m`ko!$Ay2L>{F&pT8mz}*DyauAK9*(|gPk5P##3$fh2@%+ zG`Mqij^V{ISwslihEd|1S@T(#o>1)=tGR{dvT%LFdu)ctE$HI{WB9DU$x|IqYY?}# zCo~+P)N3Uh3yrnJnCb-}$j1Y+TOFd^IIn@pOL6^jeC%dVyX}u7Ia=v?!^@cy5=7|V zN2u(J_iP)YA*_i;(as5STQYCzUx&8mULYk?>wU-`w0!*p?0zg_R$nS(lVf6%ps<`V zTj18~{&gP5`yLBI*2g~nmLxwLSGpIF3Wo38?%nH4F_E|$(P_}LqU5i5Z4z-^Jh@}2 zzzC^3ZG%?JC#GOvaMm|Vw9|8yPj7+fv}G!rOGh>=7IRwZvGC21yO04%lNaR`uI?-@wOMk z?@hNni=13t0dL*hksoxfCFz=*67r{39(aa&2j|0YI@o81pd(m;2eO?cV0V8yYeeaH zvo1Y$3t)AEv3UI64@F4kcX9BBv4|mKYKze8L$E&Osq>4rn$Wc_lV@?c8vU*5{QNdZ z@!E=j{%%`1C5KzvxKg2Q#?vLXc!gbv+o0Xx$7*wcp7*GWgjY(@!`@a=sy;!!Rf%Ns z9ge_S9!0gPk$5(Qn=rMx7p-Qbt37QC-g=(BRahcg?>_Hk3*jCo>j*|XONo1jXdfc; z;9fDxlr8ysWErGEq>+5urcino6-$u=Gouvexpnq;op!v{BY+~3^(mOlj4KJmhO!aV z9xej;u*E&sKP}DrhIRG}ci@pPsm<{-`yMIci^_NKDz!?PqS!v4vHX+`OAsZ*MMbz8 zI|ts?CSLhiv;r{XgNeeK1+-_Qy%i0-iU z@GZ6>!e5gtw8WIg^P-W^2xl)n6i-TVw_JF#o`Ps$hEwhuhhFy5tX3LUE(b8UH*h4{ zGa3=Z%Y%yhY}(xWC0t3$FsUU*hYIP8J;xAC^F#A)4A-3kgFbdXF(;8ke1BU^SFl)lAl$hP-!l>j2; zT}D$x(w3`YQyKL6`{bbMz}gOvngA>D)K@qc=Jb$pY<;BH&>Y{922v&9(Tn2e6;KH4 zAfM$ptmt&)yKqUVlWl9ik*R|9mUee{ziw4E#%D&=QWb(Jb=~qg)99Y()s^6tcnqDN z7#fd0F?|gmIX}-e%4PE?LEgxiN~y?j4ZM`O+l8wnkxc|{CG^-z;O{le+P`kd zvXbL`j~4%xWg@`}Gc1knw3UGPx=f;}8X*&}A}BN^Gj!}}xLCXVdN-A=>%I|w2XReP zO#5D(Z4&im5CalS0(TX!ivIW6b@`<+?WYKXUdcacUMIoXMqQ_bXQqVwC=Wrs1<9^k zNlhW|x$3R{sHpElWPZL%=y>;ewFE_<2+bT;XY@Dd_f&Yblq!0Z+vP4Q<-RO!ZHUQB zvE4N56AuT#(xLS!oGqbZUME@0lhM0{e?{3BhgK;5rIYuu8_4!Z<6y&}kFbAGGzhbv zKeF44v!4XkKOxwE^MQqUZyERh^N z{Y?9Aq9c<1NJ3yd{5C|b2Fs-t{7FkIxK~ie=;{9oR{js@VcAccDekX6uaW}vrHyNU zgzYd2rzAtcpfwB#wUHACjeC)0&o0`+NA0Kab6Oib1+rK@2yeK!ac?eR>doN?2F3EB zlAhB=(@bm0Jm_BG9Je3r2Q}1`8&f+^Ynl@USd(ow=8eqrG)9h!>M2)CLboT9D263f zE$$*NJvdNUQ`o++?@9)#IdLY+DDFzJyl_e8Rf> zGD?6Nb9F{B%N}kD!4}Wf{fdpE8k^rUGq@ZuUnbulP_rQhc39I_{biIRkwBV^YH;%| zD3_F`mmR3s_g3o;*@q$0?8`Yn;RFk^L~Wcw&WSRr14T>6+V&a7P7g1<>$H7EnO!$b zDKfO4N1G;PnVNwF$e05u?Nie+_XAhRius(bQOmb6U;5q|pCkXt>XyN|fx+%|ha5AZ zj}@{zdZF|4moA0(-&Inpm+8Q?KbIv-11`O5AF-(M`1M4H>uHD0x{ejh#}N~uB(cs_ z5PI3DNToU(mgHRorB-lTmX+(pK*xQ0^(%e86aUL>=6{%W|B9LU|LGwY*4h}G9}sW$ zcV-B`B^;TIeVGW-Dp{YFPA_<%qMm?6yKtsW2gf(pakjq96iioy{-8<2-8A$#!%6Ge z?t!^?1Q_3>G37|G#+K_`k2HCT79ZcxPUZ0^$#P(FPS(X6G&KRii{%9iF#zr6CKbFi z15pOJ7#AB=HH(Sx=QdR$6{1 z#?sf-wmCtUSK{>AyM^Wfn}cQ$p`ZCX_XMXDR^yKz(~_#K`x5us@pH-}$k^fx4-_<2 zRMViO=oAhukMKf*Yq|Pa6#d$uCrp&d9KcBs$vBG$z~I-A-<9IN1=(bIOwz*Ru_3(N z=|uGq-XFGXUvZvhAx@dPD&N)6r`iD_in7=;l@9E0zG2CMVr+vo6B7J!k73~=nGyeW zsg(pIUhLknjC@I0h`xnyedC?_o8wxo1bqDCD6rPaP=L^VoO~32yakWC+n<5TUatM4 zysz@vi)`hftZXrL!=TjsN%E=w!&3nFJ_|vjCrB#jXw7#&>@V*i^E^p=eMV}f-IO`! z`Sau1FPo#^ZIsew@*qBFgN{&VLZNI#pmss^bc*SO^Mlhz8yq>eFl1&z^5c&@{q3*y zS}4UMI|dp>p7amh_PRk+s*fhW#l2CDq)b}4P7M;e#Bjwc+~YoHg|an8BQ3d7T>Tj! zjmmE*kYn4MqsER??FMREvKke{aB{~ZL^$Na52}ape?>R8$DU~{eo1=3t+%D$@{y<=+Tq($r<;KzF!sr2i~A~9 z)Mi;u*0&oQc9?>9<|k6N{aq|`li|ok({L*V`*KO5>Kk}q>>ou&h{ztihjV0Lx^{`t z%e}JO-}~uv?7h+@`C9Wn*^JGpxaH>xLeDOb0vGcZm^Gx<>onNUDqCsb<~a4Zdpgdo zN`%xb6MoT(cwWz(`M!<4iJj4aC1LZaAVpjoQ~Sdw73=YQZ4Tw*1d5cs@&V5?z>NmO zOIgr3JH3~1I!|Ogl5Ka8&RAR%sSGhq1wAK{9&ZZqFv%_fhqo4}Ia>r)+CBkSp0|)V zYk(b6sfB$i@&Iz+#qkYx0I0#T(LJs$X;${|uAg~&+G%u&ECkez35dLdxm-Nh`kB;h z2sKVuM{UzyK55$uK7Ft*0E5T(ki{3?%MM>DdZpPk^kHDauT`R^>do=f9(f8M@m6*3 zO5>y@o$V?Xb(Q!+D4K8@D;jKH7P|Fh1sV@`oK3OcD#90GqMF#no)bT~e?D_Unpb!JRPxN&0!;suEOAC5ANynDGE zI806NANAeTmmOztZ8ZDR*83w7$fOr^4O2O#)=tZSdENeMJnT;~y9+g7G9P0Ocw?1!v}R8f4#v_! z!s(K$N9B}6+Oj;ZhJj6{yql^@#(sB=i%q|v41}be1V^iqOpcNc+%zf~6hPysL4|TT z(DZDtP-%-f%qb6>I^h=jr!~R7jm(NVG@_;fQDkq@3^s8()O^xiN`os%{imAl;%)?0 zd#6UZIi}_*U0*uC4m*mLJ}|#_PE#j1(#-V1kudkmN%6JMZ)XjuksWF2>AoS{&@R5A zM`b|vl`u!)#T$?or}nZtKI2*-P4TDiVvhof=S{ryL^0Gp1%<`c;{*wrJk4j0T`ILg z+~X+$W6HK!b`B`{1E-q(@S3J?WNNqEyH6CVs)u^X7gQvOK8!ufq0qvyt+#_DNiKL^ zM!622{ptV0-FrYax%GR(c#aK3kS0|)N)JtX5y@)-3`l@L=tV$EKtOsZk|WaVp@d#U z2%#grgAj^z=?YR3dQU(gv^dYX_s)Fp%y(z*_ulW!y|ZSq7HjdaR-Te)@BQDu_Ad*q z_kTH)67u6F%8ckzQi6y~Has1{jWBG^wEtck75uE+6h@pI+mt&>PoJN8w6kpV9cq=d zSee>n0aykS(a+o)1P5TRLyUS%dSr9irXpvPozM;6+MkGZ!Mk6)CPxyFyc{0^?hN4> zShQW@q*^|#u*pPY+Q7cEcI!^>Ap`&p=z&XB3&iCRb#lJ#9sN#5pFzmY-6lD4=XaMj z{tT#*=*WH$)-m0F^PCsBAMcp5jRmIK>!#XG{#$Xk98b$zZSu^VbT(BO-%)O5eBBk5 zy%9R$yn_N0c6o=~U)j-xUd>V%EmDP}O=`^H7TDS(^UJ%XxojPMk9zWsiibte?I|N3 zuUss{abvj0G?^R4u^wScMJm3kVL#m-`m6KK7M?Y!wL`rYuWv)>9i*Hn#iNQYlsu{$oJI+iUmmZuICC?3LjK2HOrIBvbEkq8Jgdt%&P>7VAw$!Y31Ba@n zIR{V3W@z0*-9jRtipq+lDJxoG0w7bpQ3cuaKdEpk{NrLWpFNYG6n&?-b#ZBDu6#RG zzDiF|tz&6$*FK3Xp*eyd{6Sy<#Ynbm;96p(dxV`;R*M zjdC0k1FGiyhsNUs#-d!#cs|1eW8XOFTsxdTQwFY>6}6ZHW&%vhksQZ6l>}6j#cx1# zoGa0T;My&wPO#01Ept9+S){*1mg2c3o=8lD6V2)@Up9n0`UOVecbv4F7na0aO($<_ z-*Eh6b~BwnsXcQ^AxAN{`&R!3evw$g@`Rp)F44l>G*Xt%9pd} z)rqp2M=ESMFDOQ+No6LBUo-w-0;^&=sn>es0;zBp_8(9iS%$T%seBar^%P(S|K;%p z^AJ^A>5qnp_T6!}ECU>WQiW&|D+t_Z5^Sm0JB4x+kP}vL^x=O@wQ(W1sf|92mzAzH zP1Meb{B7K`j$48_*ArV!&eNN^8bi2V4ptZ-=O}iOl=u4^kfIV(lFYVY`spQM7|opI zmf~|Q44Z~~%I+%0(Jf9!Y~RRi?S5J2z%?aZ{WoPME-BTHo<+EfI-`T;RLgAU2gEz@>=xXhu79-7dZ+e|bu32kbM(tf+# zoG}0G`9Z##(4+Vy{l11CsN`Bgl%sI|@>o-(tEvxXKJqkDD}TmpNMOR%siwxzPh`g% zWz0h~)h6@0Zr?nkp#W<%4Mibv1TaoQTuB1QJu39Xp(rHqRB{OGfnc(~<*3V^EQ-;= z&rH(PxC4cGn(i7gtw%I7m}d)n(%4YfG>%Dxjqa8Gq*BJa2n-#(nq~;@8GZM@NWbtR zi(L(@C$NriXC>qkKJQi)L*{nHWkh~*iV!{A}Me_@lBlu>_e^TM=>-Gw; z?G!+@bM5>lu^dIy9xPt|KE)(Xmvu&L=XiD{QiQ?oqoYo9#mwDE0t6eWnw70+7k!av zjZ=2&{k?0l3zR)7kT-zfw=LUa9j$%&<778BK+F-Xe=>ILjNo_P!#c}cXeWt`Ds}#K z(FDI9?@_3M`SNmh%f3?T;v6SWwD>$NUgY@-&~A=2|Mqq+}|`vSiSQ^r~X9( z7Uv`q-j|kPe=fi(`6h^7`ic2t&32N{Q8HyrSt1C@weTtKmxE%X9JT`*#tsr7LOK+l z&cz4*%A|eVAj*Pl9M9plULOMKnk{VhaAT*P-0k)|^l+4MJT`P}cvE+zdWKF>R?%aT zh-Qke)`<6mx{a1z=Bxcl^}a}4)lYfX^6MN1Qx;NI8xvF8ibkVbB;~L|a7)k^+}2zFI%RQdPTpVYer^;qS{LV)Oq0LDv3FsP)pL--4(w z-+FZIGQ;QJVjo@0%^!0d(f@(i{*taGGlnRUIn;VMZXO$n>oSx(vuV1z%FwfWdHrGP z;d`(IgB2~z!VR}i=<`v~|}TvuA&*kF$>*5af3)~8z5W!3VgX2()WeZopT zEkDmTelwXjwdh?n?XRLISj2{ne!(YE*dFS~4 zp=F^t{0<6jaY#T=n;dsw4)eS1#QT3l0{O2*2?`xG`6uu)Uca{8zBuyM1dfj~Ba@b% z?Rus1CMehc9>@u?s@T*W%%zMnw2)#Jf&%-`lrvIAy^J)oqQfpSFCr~&{G=++c?JcM z-br>Ar>Vco6o!SuOk}q|(K0RDD`&wkBXkI4N`JpX_r>7P*F?#;-|J=>NKX7H2 zPM-iX+aMWHum#A+YZszbL>OktJc~RbUOeoZe&}(Kk*QPtZJ&=0d){M$$xPbg&Mi*} z%!$LnvvUr~3Ds=J!WUs3(dn@w;gx2O;%cOZZ-1H^bTY_73t0nx@YlO}j#GJRY%L}M zqpyoXv4WUU%Im;!dd36Mu#VEG4hplEB$wOL0*mc;_T-fYqkiPMfof>aixT@=oobmE zc}1zKM%mo>6TKtZWZoND)*EUdtY`{gG2v7ZbhKn|6Gfg80t>kx#IccM&-Cl-5(gHr z8=wJ1F5pLo_$=7hSW#dI*i@RK(R*=zJU_@W4-B@&`@2(Sz zu5j;}>4UCS_6+R6^>Ji9lzF35GB{wEdaW*9BnpA4Z+JUpWFMp+>G1L~J71(xe~U}> zB||mZ{9mri;x!~Q&Foo(`ecRrgw-Is={{O2MeBj}Z+n-jTI^UQ2P9Kln(u#_#zuD7 z5EUZ7KDVQ1Qq%BsV>2`0_T0j6jiu{hO_K01M!;8(kb3h(cS6>w(d#DcbPv{^`BbpA zts!}L*HwURhj36X=#%_}OH!RN#rrOUp@OcS1{!oPRMhid@ZOK)8%e*eH>yE zjei#XD{r2N2$g;iC|7Yj$sP&|UYy8Pfb{%RNOVG7fd-d%Y2lZvPtHzSV^kaTD|YDAD+ zJ63rSUiiHThJjM^*5JH>hMqntxc>m|5z|B%FWAFmvioaI?Ek*yor=fN>)2tG;@L{#{z04!>dJ7uDH zfPkWR%&GF}8v0u$WqMSrCGbk)gdN;yaKzq$!g?EW@8o!VljGXkwXFQqx^mmZog0T> zb7EM`WMzZKId$e_`cjUu zaW(>N#fBw5+aiJTc{uqVmJ46CDV0ZW19s;Ud=7oAc2 zT6Xi01>$1Z z2nCBr?T8Q!BMz=~W{cSds4q&~!EcUttQxCxy>NqlCX$b-m1?-(XkQEKvaQS$k~P3G zEuTMx<~$TCuC*0*4-EMCaK76AUpZg#|I0YvOP}{ROXI77A()rP;^}o4jZO+G&HttJ z`ftBK8v2bPEh}vknfZ&#me#80ybp{;s)qHZhP4Kaa09*#rG?o7pLakqUy$l4{E zr#J0YgQ}&gA5=eRzU^aeoR+$hmbzRm`Df9XnSPc3zSxW->nXDNHtTfqW6H3(3faVL z=#|7PL5F#N&dM9u8PQeb2Vduc1=<+qSL+(W)ZGt&T#Lc-#A;L7H|e{zOpuw42M$5TxoUy%VVB}0KP<-1m|P#xsysh z^bAD4q-yTuSQt(#bZK8xbg0dk4^1p-Q@3|6{H1u;8p`rFIQu86OTPg@S5Xdd{Zd|vvfHEC)RIffUr?7VZRe1^P%_lscKU@|c>2?+ z*7*x0)+C&*7E!iJEJ)-vm#as;N5xy#)E|Y}{-jd3xHG7HhZv`TTT(_6lrJOf*puln zqN-q;Dl;zyz^Ta5=s23fx@x2pL)?Yy7NxfZR)!(}GbXpMxV{8>slz>zax zp^_mAYWIWhvamg7(2$Gooai+d- z&;7_5Ne$tE_&d7)as&USKJV{dFQzBTQ~RPHpN&3iWrxy#iH*iKeFWixt^>(IEY^4! z$j!f?on=yXv_y6^Z2|+Wxuf(kdhs|KO>ruA$^#-T5Lww71UBsK&E2p3>(z93Jt$M! zyQ!y%E7`u#+byC7#!5a}HzvZ2Toq1Dmn@<(`Zh;0dX^6`eyJ{wHOHREMLfh=a@43t znM`t(lF5GDt$$z#`M+x`1`eVpi!AJjf)U_ZdxC6Fe4q<}!u(Ouv2f-3O6JTE>$}}O z)^3v)-OsCY!xc-W6}KSHZ=9Gl{c~s6-=@2{a*g~+*;MmeEBPx$VE zQF+B}{F90rsVUz-bzARrNIzpGaGDjgD;;U}!$zI3qD6TzeM%eX_m|Bt|H%9ESJ5>8 z_2Ms`&Mh&vS52QY<#keSswttmMrNw@Rqjpme*d_6yX|_y4UumjY|$33RY1=dX`av; z|7Q7TwDKzAQ}+iz;HcOY>T=Y zdzjd%r{To!yop%$vCKO>eH`b^KdDUqd;E+ZMz$r@6Q(;_)z9?sadGF6#f@(R-%SxI z+cT;M&R^-U@8`va!-_-Lgqo6bJS&?@!&yd+{Cy=D?Y}=Ton3A$mzK!z$rPG&rH9Wf zw0BMb6m7k#NzBu%^lC^>3NmdmCwN}UcHvvL;OlEQNg>w(xQpPSeAy;r(Hc8EWt{V3 zC3JWnE9j+Y+ZcKqeph%rBS}zU;0F^hjRl30=?Og<_ITG>&PCRcK!J@fJz`6w_TbXo z@c>h7CtnP1_S^S)6Kz-J*l9*{ZTtc=*-lXHR(U3V!#T#VAp+?)>x4CsRkKc z`it43JyU}X+V@NGFgk^3JI-rSx#77`nh<4ip6~|Kh~iE+FHCAjXG(P|cw6&Kj`HR8 zcgKawpHw^481=FiXYe_wJt9HdsrVCo**JN|uDX^4uZ`ATe!@IP_UTIA-FJ0%OE6M( zlDOIMAc_}6;UU>xi9n2;(JLk8THJQlOn{=5J%v^GRW*#BzvT2mCS#aF!^Ukkcz?Vp z3+oR|Sy2BzRRE!h2XN;uJ>pmvE!5C5oq0u-AcO-*S~CNs8Wz7vhcSXUXV>Ff%dzV0 zW2GY@Z4#70@SVUb&zA_=xKV$`P_hbs!67CO+mX|y4 zCGln37PEbqSXF-1VWy2^M9s3RiO{Rc$*fBlHt@~!r=Jf3hC1OO;=EMpz6A)^!=%3n#nRakJ63BQ^a_`*MR(#W+1NEa zFS0?7`CKiGZAKUC{|C&hf3+y`A6o&yfHZ9}1x3`_yA}97hf!eq+=jaXzxQqS6Kmbx zQU#Mq-i#T!-r%Z86e=u`sm<%#MUk(|fEVEo^!syV=?{I(RVOCd74Lh!DDHt@+Ogi6 zKb|aV7)#t35gk!}z4V+PNkq{T1ZlPCI0h_ha!q3u60cEvpzN)jdW-@RCCWDdUP?9u z_)ZEelkHjV|D?(z`+o8ww{veVZ?WC|0_ksfCbCKXqCAlp8!VR(wD_X9TFfC+gvK#p^PD1;v7jrR>U8yagLSg5v$Wb*(2%pPz z?5ls8zFEp$c1p(w8_+g)-mavc-lMJNd9Sed)?_TtprsM>&M^FXg*4y zJezyu*UHtt4<9Xv%k5mQ!6OKkwaJuWdHv~Y&}!5psR8_$@G~b^L^#AbN=5_f&>l$>p>Do}eT_cag8v4<|HJFw)!nqR5t};U zdTpn11h^&KuS?ri-v!*Hmw7^NxuUGPc0;dZetsLsZ%nb=6Z0?dMQ&FmgARpf&~Hx4 zb^$`4!yMI8yNkgzH~WXXd@z3h2ZYnp>g%z=wJSzG22^nxCo@TF8ZIpNmiR|0i0$$9 zsjz-{9xQlH>Er86AMJXz^ik|+!MH%7^ndOEQvsO&pk&I#XkZ^t6;Iblc)E9?%mNQN zsJrdXx$yq7ewzxIHu1Boz&Md@3JWP)ykV;rK4LEdM9fhzo?<(AU-{ z0dgll0m25PHj3`E(GDe#_d%lxzCiC##hD?s-ew;b?xf%$)PqA}!;( zL<;Ium)xQLsp1#qCnR=wi1MshpX17R38=SA`)zyoSU9d;ZD_ z@VI$w%E`;2TxN$;Q?j3^e%%s7@(H)eb_Y3RK1e8h}W7)QQ_9!st#t<)gl0^ zu1-@H*TdTxkumLz7g=re`o!w>72HXW+v9vBuKU%~b9I8UnmEWf_~W-BkW=XE)ad6} zJqCkYpYH|_@vAckP2lVlhaHMxA%5uZZMP~0Ms~macizT7*DU@Z-yQqV%_)R)xico# z-#?rWX&;0%gD`&?U^l*(EX-n|$bcT5kow~jhg(vv&@f|?AhqygHy0@&8I4^{V^}H* z$je2Plszx`k{g+8dLuGVlPR=otmp|tSOt`&&@k78F3Uu=NFRnZK%2NHf%GzMsXL)k zP_ew-oOcUET`&OrzPi6YGu7@)vhSGJscs@JbfvCLVc#g8cQw}4k|a{oPiyM-Wm_P{ z=37e6EtRf+Cu($1AGTYdLRW36L;VI_IAUnR#~ zQ(fApA*+D;*H~e(yrL!arNiiHQA+V9lqEBdkMPkb+7tow9y9@nTe8iWMgSY(d5^Ln zVO6iI?GkB~yY#+qtah&vt>w`jT0cE_A<6}JEfhe4b74t%695YS{t%jkfzp&g8Vdv8 z{BMPVx&JSLf`MdT!alOhS!}hbF{|tK?5rx^!Qk2lRc4K`XKPg-65a^QB{U=WoBj9? zu8vJMi*G}A)@svDoeFL18iLd!{e72^cA@(G7K+yK@TGXRTc0=CpZ)f`gO*IXkGB3) zQN?cy@Zx&~*Ph^AuOq8_l_u*$&zbOX49MOt!w5IA?6-JlRv3w{*5^(>qq%vXGX^0y z?K6%ktATwtQ`V8fU3S;HYc!f_55p+wdX&JJ-p=sEq8CA)f=7M}h(GOznJ*-10Q zjKtN(Nu-pOtG{=s>mP9|b<)Pnn2Vz^p9**6asyt}MCe8h=0c^jO!OP!8c7?CeUE88 zq7|2qqJ7g>6E|3VmY-ve<*Lz{Ow3fLWjic9h3KMQ{Lhw%D$?xC3e+$ zW2I|0MX{*{{cO|=QCt<3gi!3(Pcqn#mKY>7)s#l81*I3Pu>h?P2ODQhYdPmC9vH1` zJr4R^DQe+Y^0XGQE^#SK9XRDjqCxu`j{*ma`>nuTI^mt2VAXWf>4Av^>K)!OFU~F1 zShdEQ>gIIhWf$9+$8yqeghqamTA927wT5RBVN*>4p+SA1suGi*272xOS}!US<{p&b z9_cU7IVU(;<9?X?=x?mzGCPmQIGr+@IR~8lzn*U??ur$JayG=OXE=+77b*K*c8$nF zDw7~Gvyg(R7=aA!k&osN^2h5=cQ*A3ekrc5`XKV4`N~rrfTO^}^Mcp5eTb(L`M2qT&Aq6FLiG-nRaN;UfE~QZ9VJLbwg#&W-=Xr>ay_qS}wv@UqT`LM!&T!D2}+VkPAzUK6QM7FJo{NV8#Z5_)QLqtU&H@>(SPYEZd>q-<^0h_L!dmu@}{yt3Ws5^OmgK^8wg44Dzjb8xysoUy1ELM z@s)4mn+zJU!J9J)Xld#giwoXzm~p8t{E8BObH5G!o#}@fF;Q}9moJN}H{w8uu%AQM z@L)zmp(4eh}GS zZr~B_@Iklamiws|o*4c&f))iIaijD3dx8TRS;n}t3$i592M=To(1&v0B!h{~Poi-1 z*NAfQx8w7S?lPKbhWYM!X+gKFRdUOml!B*C%BzbsJ~%(oYiVl!<7(2|pH#OgAaY($ zn+)<@w~8P+EMsDr+#GeFl$Pxjo$xvjN{ZnUuuQ?9V7YF?(Thb^``aa#pwQ$S5}kPB z#B7Yr!cjJGyvnTo26|-zPRmV~fYG>N{LnN@BVly7%(zj%M0ZOmAt`$eJH6KX9@sDb z%9o&^(ey^eHI!OQIcU%dG2n3yIqhHNyo-+poU zBXerrk%!Eo2(e4{k<#MrUn_ZV8~iByckf@RJgE#T{{+#k*g8e>VVaa1yYm%Ad*fJW z5|Fm|C$0{WFqd~N@oUS6+lKuA%Q0Ym<+>}%e{k1@?s9xiBk0{|n6=^zZq+Q4i;<2k z`l0f3MUWW$!<$^xv93G6BVHD zRB`&*D29|1@MZcv5vWR+SET_guM+bzOe^S}d<&`zq^d2nCS@7}Bj`#5&fXWw!=gRA zqO(HLAo)kM*ENK!ARRNm(uU1gy_w#f92A(IE=IIhCfyB=ObDpXHPH#E1;m-mB^TCU zf&bi%_Fr}O3&cf4+2TcZyB%ug+EM%DPQ2`FmXu}a0?kmDYV=I>!3>Un&@rbqz2~62 z-j7*ci>Z@jN;Wn8-k;N=Js%(!jMRLmP|-z0)MQbT>f2ZG3UsW>QfSPtMT1gLQ;mj) z#qWP2NZ9U4?AvTqgjf6sKGg(T?S(5s1@iYr<{9uZ>o9S)$L~yWDg7i=vJc6 z!liSg@mJ?!XEp)95G%Uol-Rmk0_BP5Bqi4z04M0f6eWsttE~k`o2<3@h6ql8Z1eqbEwE2 zGKP%}5o&u@q)8q9&Sj5r;=}7Nr?1}d1;*8WJkcIITR%&m=(`J3K#?N@IrMTP`nL`q zXBM`ove`(QiA44q1XY-nahR#%kl~NH;y*7b!PAHfJv`rBmwG>?6@VEi+)gGgiCh`H zBxn}nRyI>6oyw$W5L@x z2zdi3{Pv`k2NzsPTwGSt*GtsJ(q9SL^hYMYM9$U<{rW$Kj{i^I@V|mJcv!u7t?SJV zyldC>b4`Mnod1QA`MsZ18VKimk^Xa1roFZCUYrUA3H-j^rVtw;CN-e>b6ew#5oPB@ zGD$EzZyV3g?jh_r1FOB5b~hzsUf!Kvb5&i>dHD_Wvq97aL1Qy?#STf z%*M>p@(I2Ari|L7=5gvq=Dr0DL7u>6BrB zNwADw;Al}jdl~>H8Pw+om=VFD=kmD$YDDpfJaSk!_oWP+NnJ^aqmoRz0k&{`+8t@r zd-V*OXw)z!tLXB6vPBS|D;I-8h)$n^Ka;#wV@B>b7`-Ub3;sB1uQ*zw_&Cc2*&~@E zW5d5AY+_cdTO9+Lfow?JcsSt3j^>cWO$4FY`(aqQC63HKev$lX8PVD(eEnOBFfs?y_C;B`kU!dFqHcyh_#BLf1ZWX7-+=tf zK;vB$nZ_>#08Uo=W42_=$=t@YEXeNl|&qR)a zX~@>CHZ_zy@M3Td(L~}KAZY100|&U1ADsOyK#m;ZQGu)`i1l{U(~-yV0cLCy3(zG;y(jBECvt}^?S;3=k;}%2 z^K_(ZN497dZ8iCX$e<%dkSWiJJ6Y#SszKeCC8JCgf>b&1tUJUB%oF;sT&09EB^d+^ zZF~;5g=SiTfT)QdGwyz^7HgwGlX4BTqSgdj>2)wxY;}o+RenvN{QTP*yfOCzFa>o0 zhVf=_<-)ejpNiz^pH#LIQI@|1SQ08CPH6&OoXLgYcYehk2VNnc2l8*4wB=dxbh+NL z*gW!45*WB+H~mS4zoB7Nb}i~tAp1D~7=B9P_MJ&(w|AGgZ&D{a6Ap{NaY8HUm08l0o${%zR@eX zjNzGw%ys+s?!zrV@E9;(x^SQXf(W8H>gxeM=bRzO1nAzWN0jJ3Uf>iBq&|m`rLjnh zJHI}jRAD5~8j|{^r*H&lJ%Mw`@u3E8pqX@?q#hyD`(=5O)nfc*J#lBO!_5kXrT9eS zmHXS7p2%UN3O)jMEEZ6#N9?^Mgu6xXZ+ZPc!m5KCPki}(OR($#1-!i76T`f*F-VYj ztJ1L982m-+8zdus$3(d5f3zp z^{u-R zQU*%QH1jnx6mcHjaok0hIUV}!PY`hUVp!ptu!wOPXG_H-<&Bq$c!kc6N-ogAN||hP zWN}f3vTvVZ?FqaNtl=?u*I*vz0Li5okB&Z>`{s^yN)I>t_cvVK`y9Ml`6Hc7m<>m` zb5yFaMz})AoutI3lam6*0;siB~0}c4}t7732JBt`-aokwqXSlL%k<7Du5mM4c%j7OJtow^-WcP`_$% zdqY5l=P(4w=ObOarpJ(<$!?+n4EZw~>BEg~0ugz4Xtv-J=Ne^Z0iB^)a7aq45$Uc0z!X}P5DD39R8&6Z|HsKI`x{93jM64PNp zL-z0H{&V+_$P^!iHduW z@WVZ`gfnZE&8$NF!`6}{nng*c^+OHNX*^+ux#*Dw#<=w^R4Tcyk`lyIDY5NCJU9{{ z^W>fj5H(bbwqVXvDtD2qP7nN@O?y=&8nl1RL3cjHyo}TQNb5Or6XD(`e*}%X(!!|a zg%Zz2`TwM1;nCE_uyS6AwwMi_LkNBP1N+4R-z+b`4iSa>o!DQ0-Az4v<9D1I(A>f| zR<_&b;JhbO7=^H#EP;oJ%F$%sxDrg6!i%$hA|~sqi*@qDN@~r#C3_@GqPrpfhy1o) zP#zp^-1!Uy5m=E|9NC(o*r;Rzg%It-C+2n4b6iG^4eVF+DZA3+#|gC2?wO+W5ba)!sAao$&&kBIlI3ta(BlX9otwO(mv!;_C-7r-}f7~s9IS0V9uzLLm!eG z5+}ySx6mzLxW9EwJF#1?!~nb^dVXp?2S5m)vQTOnDo+IcKb?`s<>c0ns^K{b=@8Lkm8bH zQeGKb=9Z28D+a`W)sy{?1nMu+AlhnBoWXDO)68S8FBd+z?sG(A^7Rt86H)QMT9`=8 zH`QAnW5ZE$Wh^u!jJn@8Z0$t9|1qRWnF_-$rKx9@h>1a2TAj8oi=c69|Iy zQiLFZ4(aa)|CJJ3mSt;X3)@zJ;ch?T0?unwg=Ryt4=E>_1*~Uw|lov`Bv<7sGi!wLI+W z3FKe0F7CO%c%H`@9Lp=1XFFFwA|7f4! z)I2cInBUOU+|&?X(9qb}kd|${D~O&qE&z1XFYSEn?D&9`06NZ_VIBB&tNTep_}8f> zG9SO1{j=2MtKY2{kc<%EF&xiRi&5yw0s+08^nz`!tE%#$m^5IS7{z|Q%w?eYYoi7% zu>~Xipk4sj;Lo~j2i5caE06TwF(CePKV^JW##yt=WH=sw!7*lfVPihAE#nO8xr`8O zov3|KSX%0QH|Ey|18!c*Iz5q?wC|vxXDoG`^rq|)IQ;z|D~1e_6`J&qlbGC#Oj@6f zVW7h1jZk+$>|8hV>_EC@O$`4%L!Lmp#;SQK>7}t9SaHa0a*f0DzORdMNJ&hu^_G4{ z^j%G0!ZE0Y^lL~4b0$LM0`pKEPixDh@rG|Hmngj!%q_9z()hTLR{`)Ph}iTild#Cz z>*712; z*%bG9Za_^+^fR$BJ^Jrr+Gi0ElPSxUg>7P=2be`+skgm=hXpUFDNT}(to$z0kwju# zElDBP{nEz79*2Xq2ozi0pCEphz%@L7D#@6TLc6p+ET%AwN}41aQ%e3<&@E$!@=-O; z%SongT5hcOVQUwovUG>CIA5fLZFFh8CUay3S9t$Az}daHsnD;JWKXTZ6r7Z+P!BY< z^@+~ow`W)Vvw!X6Q~`Wlr(35iq+1sgfwM;F=Z52^u{0ARU&^jQ%Qz;Ah4o4_$`mzN zgn=e4ctLi-+oQ*Z9SdZkiX9$exq{3ao37iy!gkJGS%&EiQ{m%l8X7=so4bhZC_m?m zvs6!>SE~>4(rmSd*2h>!Bi#QMdiw$#)?&Uy8!L$;NWLC%|A7X$_z=;jbAnz9PdL4z zv^xHUi!ZErL1?rjwnu}j>(^HZStchv+Bk9?4YCc+AB7|FgfU?ft@2V;9~-?=lL$3e zTxJRpRaGE}Xo_^c{1z*aUp(Q9Q96Jx?Jf0yiO{$iG+fyh|3>dfkDOM>av(cGmw{|y z@G5*>CQ?&vYz)EYRGoa@^Ytl3I(5qHrA?q-DOa>f6jQ$ZNyJxku>@WjcHE2TbXONE zGJw>rfb_F(B&Ny33V5%j44WM5<$z z$P*e1M(Fk|AxIKaS?rx`$^zP5ndYIn+rVs6iPzvlw=@%c&Y7Mp$%lC^5k}sfPKTHCD_d$dr{#OtLcD4~928PuaxF0Cj@3QG9F3egKq}DX2_YPMY45 zIz0cx(TXJFs8a?t$7+;XDS(Alr7y1bNq7|l5f3Fv}KwoV<@@a(5gQl_{ zWVNZe`Q5iWDX=!Uht#US{-tn#M#1q=s+<)EKk8?bC|_eJVqdH3Tm;PAEs~42ohLp= zp;i#lW2u5HTmHQl_Y+n!?~BYQ9L4Iw-CzbSCqmzfY4k1BeD`a~Eh#4x8PIeJzfhw}g)Pgx)_Mxz$k2}yXb{{rk`T?cU|3__ z+^_w#owZ79-AyUB)y=m_NZ!;m`}(!xD*B`EZzg$*@Am2p5?as2%{4->ut`T(@7B-l zyWP9f2M_%)OdB74;am@~29qT&8J5lODBBi0C>iuA=JpH24}KkgSp+tOE8+1LAGGft zS4mOFwXIWzFR}D2QqlIT)ZH3V28fb^_KJqIP8MDvC`c!vCXe59`mkRA&?IXLU9Rq1 zcO#+F?1$d5O~%D{Viots7a)(ryyW2PV4J=K485l5U5^G^kSNF6wR7j?V3BO@LkBD@ zbjMXGuzl#tm}y1r`N;bUnf-q=3gI6GYN@FhpHaC(7B6jTajw_CNOT!6IMQ?vo+-$= z8Zgk{u2d|aj@zufW<`!%4`pJ~!{>pG#Z`|-(1C*H%5+i{9brZGQ<~p;ff)T9(E0Pm z04?h@26g4$%9+aCbL0BMpHu~bl-??KgL{@-MY#T5*jYk7<++bupCoK5e?!l_d`+TB z1bAr$rEN*7I8;$xDA<>MMd2sa??%=2g{bl5D9&o%`_LX?kq-b@ zY~z-3HQv(s)3p%r%F+OLd@sjOs-J4F6C%j*bv%X%sriRug z=mr0z>SkW(o7q~ng@tgmcVnO^La^zHTzO8qkGz;N#E0RAXNd>T<4@ODxEBkrf4vhK z)JyL6J*+C0)*5w5Y7+SVo}xW(QB>28&NkrX$4zK`Nh%!q;5?kMk7%>q%pao|cun~n zO^@!?2P|Sj7K5xM9=_gRDdM!mCrn8JOwh%h^v$@OF1!=oe!xVo z9Vc5V=(>`eD=!h8AI=p}m}@4Il8M|O6<30+R0 zDh?KexnZGFS&)9f{>Cai><;LkZJ57f1jeFa&UFcfSI4yKU)DOWCzu<*6S5j!9eU^9(%|4c(`bvL zO!ARW^#acF=;4_(CGy64_m9AVN@YmOIF4LG$bZdc)0>#3*vpxSFR}6^+XQVX#Run# za|~3%9alYl#WW+N&5n{p2p^L^dTB4Am=vln>ssi!u6W@3ULU@)X#=u!N zqKLUj;}l<{<8nVE9V}9EaT~1o_fO<6A9Vj=n}G{9VDIM`aDTi7Lc$u5n+e^K3!hL_ zEwZSkRvX!f9O2WAnAHq(W=Qp>Je7F$QeV+HSHZ?5?B)7eq}%BIvEOg z%cFrlPW}<4VTcB2HO#vTcJnYoGnYE%k?tT;}I(R=e-yX)o zl?!uASyzh!gc6KIFi=&DcjnYdd;-&)L&xvNlr534A)+rnk_>V5zFi5DWV5N4{qHEjCwHsloji+iWRk@K#9rpKCxZPGJkJ$vyDgi)B<@a&4wJ9v~IOVz=r8N z=y&KxuLvuviCMX^wbAu1Il(f`Udu?wGE}JQq$it#VjWXDe^LQM&DJu`@)v;y*YF&A z*$gob>N|Sf>H0S2#As3kjIBV4l5&*sfWD?C{vvza5C}Y-?S$BJy{Zgn%JZ*_=hUm@lEm|YVyA@_uf%W?d`TOZVL)3(mPSW`m+v^|J>NO^?(yySeEYli z{sCiT4N9`sZ$0bz%{iYr^9|4w(t#RZYF2lYjk|vl>`?GzFX(IT-_{ZR88Y_2xe@!Ai1$J7)iE>lbDy$*n}`LS_q(P0^)v%R0gs^UgBRYiJj#1U_sKkp zyY(fp^HyvAg8YXR&+-8ubrc@7_n|OC@caonjEjDsd>4+EEtsV{Z#JdR z@%qrmqR(S@fg^ahy;o=$rZmL-_*)mxZ(pe)h466P^m!ue(YzSB4hUnmRsEbl z`Id?s^anW8&YFF;kmqamkL<5$q#(^BgyhM`Y#vpiIKA&0YkZ}|0{tGB753Fu44Nn!W}{wB8pq%F^YTS7*b@@P`{y*|d6 zS|HsP=rs=^&h=Jw#n=Cy=dza5@0L@h+GdZK=!%toOE)m+RhYY5L0Ob%*TD++wj=_2 zM60(+8&;3muRKYVqOjdgQz!79r^e%(isz+G6$0wYn!7Q)*ftsi zrnar1ewMElqaD~;4k~8jWW##MXB1c3h?NdowqrX{$bRgw-Gi7kioS$3a$amt*wHv> zq7o=d0hu;!JFq9yKdH_ji(f0g^cVJhe{Y_;&M@JW1|r>dQymp>N35e0-j~0vtlkGM z-PX>92(=r}GM1c-Wsq|}x9)u@_LLA*P7ie2KI!Eq?p?kI3J=goQ9*X1FCrsE)Dv&f z__>aZq=*2bxb}-Z+coA#z(RTcKUEosmydi|FZim_T`*7AQ!{RM3-8+-`|V}xBjK*P zS@(9eph3?#o^h;x`6nsBS8r>)I=aR`oc$UN?J2Sr>ks$Y>F4pS`CXakqxJTUA6MzF zKi~OQ2Cf>nRV=fsO54>YPYgWWaV33mW`XMtX4c0X77rc011#MdQb%mb>5`MmpZ}w1 zQR5ZKEYk;gP;J5|fJOe4-W33padlU2kjaQs;@~o4F-aBY&~V@qjkwKsI~(776d68~ug!&&}s>Y?*cf%pQU@QFgrdY-ah}u+JZg9<{ z;Jaf>A*%%PW7nj+rG`aDKW$`IlGg^au#QRR43g3RCzYkk&ci>iegE+}|CMqKsz+8O zkm)<|&@Fz{Y8z9CqMJ%X2PhQVtbmPwZtnp*LrPefk(p1ra`4)r2y?44+)wrMA32ao z|7p(YI0LMn`+(%{EhCj4`&K`x_*>tq*K7X)KKI4f>uU4OPfM*ra8#;U*F)JdPhLh# zZ_ov2CI(aQz--yGMyY~Pva#Z3s|!ya=dg$6rcI(>b}TDTFv@h8_d{ly)d<>h_#ipi zkn5%9e4p1lG!#_+M82q;eTLv!PO->Dp;cTuyJhR=@D%%uJBKIvclrStXy?mrkt&y}C$WO%D&I zGY^n|goseSLQp+1Y|RcsV9y0`sRYJAXYo7x8@=Z{7Px7f50E17nm$TH_zOeB!1DW@ zjOHmOUFM~heO`yLbms_KTKN!RT=V;aVoR|Q7Y*eKnSzOZ2XMQ29II&C{AgifgV@8C z@qIqP)T-x`YN^6#MFrYqjpUW?l4@S&hagS^6T=&k#qq|hiIimqc9b%ms2JW z)U#JflM6f`X08Ab21&o_;2`1~FOk6M!!LHSgTo9Xxs=5gaqps+c9P?aq1d* zO{AWc$b6Pr^ZmUE7B-q{`@#c;THOVPPD*Ug)~E$C})Z7L;`Pf%`9_u4u&4YUPBd-5_IO!&)xNY-gpD9)>;X_u-s| zi{}%}4Ld|r&rM)^H_p2NLd(S{3t{I=X|7tGDhTUz{9aP9Y- zZQs5bFyG)vUSc23bWk#2Fbc!*p#XNkLsv#$saGK#hISwprhCYZ8VuaxaQBgeM__QTkgx_1I(*+Pt^AAWPVY{HC&fXg~R{d?>1Smu{rH2j+cfNWCZc;%@pSTtKv| zb5tls%qZ79{@#q~@Ng(ERo`u@(OA?aE$^gQ{8+xwzcV(@zowa*g7Q_yT=7EVop@p6 zN{trf;Yy70Y(L~st=F&w?FsY&?)KkNqJG{RvF!v3`o zZo`2$Yo1{-l}^bBqweBq&H*2E#V`CgsgBdFFT(J(#F#@!UVx}{Vb68u9oYI~4Ng$!L!?Ongm`X{gvnit#I#*N+`&PyXFt{U9K>8PZVw8Nd*A5ee$p}8liyop2TM=9 zZZT}$Nx3oMCBXxk`BuW4LiEkK0FS()y+Kxu`9!p{lpoT4Z)=t*ZHz8otol)|E1y`S zxBjMCQ>OvNm!z&wm^?^>-^zAIuBXK|t7Gh_dY12 z=$9RA5)f^pr;gjqp^ijD4&SU9_&|0?Z3&bX+%uz%J4A)3Qqoha8Yk-+UfWSGxj3WY zES9_0-~N) zHbz&_{sYW~=wtfVJ{Rs_*zH)sxXf)KyjU71yB>80U~fXF*hlmUn^EqkedDxW&36B; zgHj)ZU&(-$k52$BU!dxi_urxW_-nV^e+AUkPJu_yW1xFf3*key>#4)o+FTMMC}M=k zKbETq25m$Th9X0}&pJ7w#zXLm^uI!@`SD}j@ZNk{&}^}91w8-v(Z_z6rcr!AYj8%E z#sqf4QnY7CHl3a4{DS*vmg9OyxQYqZz~33omx(F>wmDCp)M9%T1~2zWp;K|K)kX|i zD;(Zok2BHy!hF*s!dfyOQ(KN%0IJ=t+cuxw#(WiQ3P}cS_))w`<2F_VlJaCCKU}*6`4dY~B&cy{Jmb31L5AYOl!d+YI*m z=Nj;GMBww|EgIkaeltIjkqjfh^$w{FUAI0T@8V0se66B#>8>*0JF=Z3jxkDoESbfF z47EO)CXOSCBRbtauqiL+r2FKbRF#g~w{qrt+C3Z=B+#W(YsclOF;2PIX&X0{@b5A+ zQo$?Ps-nTc_p$}jb>iy#KQCIn?e9(-367KFuvmhN`vAou}^S_fcJTkNBbm>Y83? z#owi4tkv>u@R^9Sm_Tz>HyJ^+n_zX5y@j^QC9h%^{7}z$jumq7Z+j+CIUm{xdS0X( zIo*{cVL$^3jmqI@79~FYlyeb5Kkvl`jDdVb6Sd+>Nr?b>KC3p0kj*2ZjYqS4?lsjU z{Z61{p-JeDeqX%sRkYZsTZ1(0Sbncbf6;&EEkJk!vu~Q0#qY~M*0-ul2`qUM%WDu@ zqMPXyC^)37;xA4Y&?lO%_m)S%C2P1}WF%iiwp_X0BuxyM&uUac2js|Q#GcM5n7gf# zJxNLPAbNx_DQW=-!jO!ZoyGt`PB4596)A6aaNZVs8!t)io;J%pfvf*$s@d2OV?0`6 zL>sQjc(?Sr>6PbuW1@HHHR5k3d1eKfY#4i3elUHJ$!QA`OPRd$4Q(V38!f$JZ$w|r zxq1Y+(ar?P`S=&RY;SMM|D?KbNEuOYFyD0XKe2-|;u|^>XyT9*MnBRW!jMh%gv37d zr7xV$Y8)(GtXV=zoz;6L+YZ8p2z;&2V!fLfd@E2OeMGdy_(TxWXk9=ePsdg%8#vcs zKLqV7-%6WsZ^vjwm$Qw<#Bwj0r)Dk)TXt%@?#er~cdw8hUzJ@?LKz{Nv#|wMx;b^x ziFHr2@`d`WdV)pk9SAh7pAKLAp&rsok8E$vsq?}BnXGo@CrC?#87VLiO%|18B|8xk z+A~^I%|1>dAcWoa_bS=NzCQkVk_6+uuL!*gSdaiJ5I1vvZ$@j-M}+8Q zk(4dnZogZ!xJ_pTZ9_*CM;ieu>v5Y$iG`DLxJ$YszRNLoyRia^%LKO-Qjq0~Pb#LM zZF?>MBwguZh16HGhB1joDWg{P?3Pdh%RkW+#4#4j~DNlHIT}?H*9*v26Wn zAMgL6epIsSaw7lk>+_iLCL0X@cvS?Ssbv_eA{`e@@tpMD%F6>98N1!+jQY%!W6z}8 z+Px<8(Nv`(-O8F;s}IF~9)W(4{F6<@knM^gDsS~T?WKR5;P1obR)^-3@}gHjx;2(J zqbl~r4c0|PIZadX_uj=-NT8!{@Qw5^u+H$keE#M>icQNnsm`8NSZz4^V~~5E`Bx)1 z;ogrzh0{PxAkycg5&&u0fs%MG0gVp0w za#iE2IvV~$2KiwruEy>e>CzOYBdAQ=x+aQmIbGL|$2P})m+OAj*L&}EWj|6D%W|62&B-j46}LveBb{@mep2;7>mfaD;eaBfQ)|S5*n^p4Tg$%sRlmTtnvJ<%DKT z*%#}UjWI8dV5Rj@K$3$g);y@L;)}G>OM6$-E|WHXHsxMTJA-MyI&;rnTh!Cid;g#~ zDv|BnAwWfG7j+~l7LjW4m0=-Lq&o{@cEYuz;M=$sT4O@}k8nJaT07Uq?O38TcRlSm zH*s*ize~YfMWE89SpQ(I#;w|oBjnY=0TQbwN#_H2h^f_bz=PhneeiN#x-=-yv^KV4 znCZgIZ5(PG6J3uVLp|+ZS+Z;q8=V$1%3>d0@{~zTOm-Q72_%jy)$>iHnR))0iyz-Z zSN!+3;{Tz3ICFs9WIqAHOAjVbbDJdzIohmE$gnM2ur^U0lX|cvX2(pB$nm*2_Jc*^ z9Bt|PHlLMXzoun8}1Y2g??_h1dc50R! zMF>bYbprD6E&gXMy_5JMflrTHMR6dNtMv??zaXQ4M+;1eW_Sq}La=;BN=NOwi-QVpq z7G!@2;WMW>vsM~g)yhgARZqpM)s^B!293NDfvEvMp*yMZ9AU!ts~TAxCOFVm84ul+ zG@KNPi<`}!@WJXqu11_(GHevh%vR9UA}OreXXI(*soyX<1U4hqjef4~4o!79qEe~l zd2YfY_{XKnEVO~YW3GXOQ852eYm`L!lmyyC#8gz@7)7nz?^-#~l4xrU<1clgFWwln z^^6#+VN#zsBtNJapE|_$`iOECdeX+S;Ll`LW#0dIj&z8Y$dySxM zs=`$-iY@oxupTWWr(qK*E_p~AeP6YJ7TItAuLRS7ZJz$ZFGyOtv6|7fQg`p*J$fFi*W(CTD+9HJhNNbw`?uvToY13%elwNIh=Ru@cgQle zqs>-6!S#M`r(wrSs2|_H#{`EyU-}$k4v1C21@j7!GuG}&pRP^E%1*)M%Wo>G~h}ri1w^C^#S)MhB4A*tXq`o!ZY*xE#@_ZBLe3HqN>KPKfU0%Po~jQ}9)h z!Z8%{Y(OMVREM0T3Um5^eeaVigq}bZS6b^7t0!9~sQ*G-Cn_ia;tD$K@Dcu*>!JDg&Do&e@ysD3Zj$FC+|848xV%KH z-s#UYUo4f1pR?iMtdYmc@nNK<^!J@*Xf647kA!1^+i8y9(YY24R9 zc;WFb_3I@iE5L)0Y2{^7s;E@AZl)vQ$jdLZ6CmlbrjLJv{mAK z4YY@tPoCq#|G&8e{PSA<+tUYmdyb&pMiLlk=-QYOJg6XT=h7xbVg*oQIhV-P<*<`2 z@YAwy%!UikJZFE%GS7wXscy@NU+r}0!Go?@^!f&AMWLQ0b*bY)TiVA6bHX9*+fz;i z=b-IX#Q_2n4sQf@D&pY>>z-sj1Iho@L z?Nk1Fba>SOT02%Ll5QyVYsoAm%^VGPW?qwPZF?CzUh>e@q}kP|)*@3=Sr>hBi|0OM zKjGbUL3&(`hauOEf{Ro%-~u%ZD(~M~JE+*G9-f(|xYu&j#BP+0-~)1FXfHk%iMe;uEC{IhSGYeL#{u0j`yScyS5r4Nz>k z*@EeFbIdCnbo5WEnV~7wwgW!7<(72*y%ixRwE#q$-Q zG)AJDVbatImzIdUbfUiC<*V#qE$PA^$ul&*;i`pMmhG0aLj-`5j~PSI5+u7I3|wnv zLqea|=SglU_Xd5se9ZinoVb(?{MN82U8Y?dXHMqW2gzLCN(fU3O#k+v+6vG_Gm<*# zFp^kB=&&Cw5AgU>#L3qouQbcMI|jgd)na&oJ*b|q$K%&MUW^K@he1|)kIR|6GxA~1 z%61p$Y4>I&e9rl{N@DD*MiLGJ?)-9igXoIDohgkgD{BohcSA^OvA}G`|TQhN4T;R0(ZIO%z zR^FCn(Z&_q>|QO+g0_Ep4cjKzC%xtPT|1kw?^Wj5SSTxz0UMMPX>%|wwVuYIha$W9 zoqkNBj8w$s)Mbd0?LZ#zqka}+{L?dp6?S`j0~5ly@8fekE@|C}??WPkRYP={OU(R3 zs^k6AB;Cq%yPs6e+sDkgKqsz=;3eMCt0g3Cg%dbCpv-bFlQw%7Kv%w5ug!{#K1!2@ zJoT{m2jAp8h=yfs*>Mu-!oD~PMNn-zLMOaiC6awgBM&L4H;}OxE{K zP(kpLa`?Ww(GN~6LpM(@p(EzHh_{^|=4`RPTB9h|KboF2y_N@V((7!!gkNnUpT(F| zk*3h{uUrfaOHiMeHj8x)BNU|3{Sra1QEx|sOUT8_K1Rn*&xAta$JdsSdI?m-sh-QY zb&W8xHK99;;~Pyf%MByS*B--MYx+)Rcl>yex_P_V7bQVfW*!|UgSr(O=ryv#X-dc~ z#Km-6>rDiB;&GiTpFe~JW*GOhL$;g$-lI1g$58C!O|_8`?So)_o3*AGuS@*NPpIRN z@_3#L_-xkXS+8WgFfI>kaZI~maY?kO8)}t1WRRxno|?`3ZQ^MyUl(1X;Q_q2T*#V@}m_8)kWLXm_yyjFsJ(dt^6)i>AUfp}D| z`z5ZR_s=Wg$S#$T@^|i70DF1uA(zTgk>St9dJq&+?sQ;ltv^~>4zE56p_A>@yA+>S z`ZX!G(K^}21r*_($>nlMv>x7TtH=uPMGr-FeC;*_TqwqMpTMRXSPI@JJF%eHCDzJ0 z4c6*5LPmhMMBU>xO^u(ZCw3mhl0u7sVdb<+t8cK$p#A;$*9F!Lm#8Uqtd zy=#HG`cwQhI$#Bk2!>==`?V53{IsF|$A*tj0LA&UX)mXmdjyqDMODaI@*QGiXPoXc z>vn8cy5`b&b#0TnOI>qAV}lsCQLx^sgomT_=X0+<4+p+hKr}q~!6{M)Gmnn5*ya0C z4?{1GLMDwwjKfWm7yPm&)+*C$rTn*oEr@E>MefI^5*yBpjg7c$$DI@tkeJ(R zO(vhbJLj(TaIp}yxvpt*N#}`c(4*NpE@?@d>G(l2eoT+I*rBwffjok8=Dd5orR;}Z zn^8akmppynW)JANfP(Uqs%EA86oPArP~-2vvK=j_MVNd+wnig8rU4Yfxf5^8Fx}nC zO}re!MZ7+$VF4*!ebli`nV2@vzdNgtkgt%l=&a0Ro?#GY6T%|D{$M2YXniz|7x6vO z4}BHx>z1!t@V(p<290D*ov$82gH4%@OzL7A(bRo2M~*n5U9DrRd2LI1AQZ3fZV5cx z%G_pe5HFySjh%@NjP5nGj z!hU?7piZGlI|&RK1l9HcO@c+1%+61TN{sUdx0O@-A;D!soTA5c9q%3$d9S<#lTNO}d)#;E<;_e56!1&F1yg289&!k`5HF(HIyP<0H% zxSS&8ovOePHf|RbErF6tQAsQ)EY>YhHWyo>7A<-|9cwk8ZTY-7I$i+|Z@z2eh#rv3 z%=fhXARMf$t6-KC^Hk?!JxiPV#y8cDKW-_Ska~!MRs@fRKm64hXKdkC6P$XEqgX74 zKXsygA+dX6<3aqmJnct!^9X?m(U@W;w%jX?_F;lx?=fD}i8#3ug*o}9<_0&#CH*#F<2fCg=4ue~;<~3fU`{2H&E1G7GPJYhXT9@MsN=1eHYO31JjLraAh3rg! zoGcswxb=&7l$o6Opbq3Y^0m`^byom+)+WK`MW^-6?|qf1gIsh?bPMXvf%%}<(uZQ@ zkyqeItqFuj2`rV>*y`IXyy7R7dFqAKVDkIYr-4Y{#fMM0(7*DF=c~MP?P`3I$4c~4 zWng`B`O3hTK$2T}+#Zxh$J@T<%hju88}dm_y zT-~iCAs2v%I{a&>i7*%%A(!g=|0I;o1^>d^*2#UreSMi z(OD|5y)&Ikk{!!N_qE5M?-j{Rq6aOft@X(B8=!h9=43nS2?9J?e?_%D-Omn)mkF)A za?V}+(ec(PIub$uj`Z}dN_W$-bEdzCmGvdbA9c?|JeLX}Aw{00BIOz#f+d`{0;ZSSmq_=2OYayqX~{S6O7~4J4>4I%)-{Wp zN!hHJ-D)8E2SxrXo!S4Q3qRvLIJj%rTsOLRqL*=Ua`HxO^YP>P)b*X6opsX}*pU(J z`M?SZPXWJ(i#N1WVYUu&mfy|}MPux}m23>9cyEb8Cm< z==+advO9LW7~Qd`EDWZ6|6>BF`!sh>FV=jdt;c3a_Vey#eaIye3%r$s(0SG<>%P30 znXN$?Z#vQ6M%==v@-jeydv9>-JX$mC#yzpJRDee3YSV{SvYzJPRxb90?TDC^jz$ku z_pxP;z>1YjU?*ZX(>*2F?&-bWQM_dCU!r9ciys=!x?|EXIx54xEA_=&m~*;<*8kgE zu121Zcj%)jOr7>3c&8r~7J}O*?lX3JN2tYC4{^vy6`bQ+uN7@-ddR7zWWcSF>2&A} zWE@re3F^kGxD%Mv)+f8v)kxp1K4p^)2~c+UK2N2VKo^0END!AVB^6SH)fJ~}tRyBs zsx{Dt<%dP`3$0U>KPnFZVA{bJv*Sm*t>KE$g8v@>`RCBhKfykQGXMU5Fs~x^2HWZP zf>guR#3%imSC`mun%;PsW9DtXmSn&O;F4wAwzkW9*~$Arz^?8}RCD5iJZQQw5df8f zI0V((nD6O&^u7_q5c$#>$UwON=%+^|9y*w( zS-B+Lk&`6VVy8FQj}MYrgW`-E`tpR_9iJX9^oNjrz+Zk+%{feBKMF4~ zATj04OBWlFiz7=7l;4IlQZ*IJ-G~YP2LC|t!~q1aZR~SK7f13G?aklmTpg$uPS?QQ zpa-8;3RTQKzzwul7DN)77k^R-?NqkN4x^GE7EgW9Ewyz6&h47iV$2@wt;`U3#cxzk zc>nbjaqqioy2G8z&Aw(#)o!I>8#s~!ZAuSq zR4&({O8d~!)9$M>f!VjbC!khGA(rMDQkgt&%KE^=2jb;UdcS3aSLBPCl}X5H!k0-n(|+;mVP(XMwolyPV8Z@-`UV*cVn3Y9EH;+OE3V(Ge(7ILKvKKoBSrtcbF34PuEycU`X@ z*~^lc5EWPJ7`c)4$_m@bB>h6q<9ExqQGyhC-_1iMeC)O&-r&X;AIsKfInEZ}7k*9Y zb3b-=c)P3={ifMdkzq5mv!R%xq*)8qc7dyZ@MiD{gO?(z&Pum z%>);TV`3JZ1lY>DZ~_o_!!3?h8`8ho*`Fo-0rWwGln$w1A|q~Nd}U_~dl}^-!nbYA z-$P+yaI|lv`3jNNcMhJ`?!(4pZMoJI-JH!w$f}rj(pR&!)~b5%4!x|S`jcvsZnO0G zk?FA}TwVPtd_gW=6w`K5%7Uu_^b+&vS)l`l%;<`f;Snt=)xk?!kZEIOc=)>J=!G3% zON|vW%|2YWX{0FHMDx(Jt1GlJpGJK8WTVDP4W#3W=ZV-M`Y@Z;P1xAS%80nUWNxbm zG4b3Dq)zC1@zK4%TZsE7*jz|WNLSVE6Sa_mwmQMB=cU$p?oVJw2w9TlN@;bDX;b!q zbK|u#`=#neqUl`ZqjU?GxP=D7?1##wk|_5>%e46Qm^oG9a)bES>FkJ7GWNd z8=J+YS}qdLX`O{&J;(Rszyt2K#zp^MFStLQj`u=Icq1^lLCLE2r`oDZOf9$J|_c00CyBc=j zTt_bk zEJx_vNM~oTKt*AdAEheL0rrexGQWazi!Ya*6+U;?KqlD{#j+q&FVrVqtfey}H&Ki( zaW~n&Yl_ls5lMD84tJB!RWXH`hPfG)>K53D7#ADBy1g^4V{v>7LhsXIb-z_q zK}G;irEKZX4jhc#Ju07scEBu8P%$={nVEG&o8fK^kxN3O{wo>^6j}qFpH$c2ohP?i z<7UOiOEc`@k18V&mLhGI=&o5#ll;ydILa2^(kgo6fSKe=Z|1)wl0x*WX_kPQ_HD?7 ziMk;P_ajlYhF7Ffc&pA4B=7ep7uxyq*NL}d_xV|TpN1=2x7HjIvSL9pA;L@r{QcX z5m4E(nPq`wreRL8T#iQNJV z&t@&7eW&y9*Yu^| zSyyawE~u)5u?*3XA}w3eU2ce7(^^72UTCkl-uadC75ByYZ1hf}por-Xb}Tvy_mKpmw4sr%_80Mk-(3QB(M^bOnJw)Ch zOgY{J?M<8O`jvDuU&lVR8MbmmzMEpb<_I=S!TAod5AMX@?DeTmt_p0`eEV=0QOU}t zKbl$97y-4W59`vBjm;GVq!uo$tj_5&?q?_etpE5|2g?86XY=3i-_?zW3I*55`XA5K^){H~6>K8}$0 z@%71!(0#)U^Sl)|88Y0rsH9D4(8)e5hLD|IvBb1#pRXYGK57MQ9DS_l~>myiW$c=8c3T0!IYPzP$M3FP)=O`Tb1x z-HL~?2{;*v?UQ+iIh4ocTR}PJMG|etY}*K;)x_)#e<#u%F#c?&?d2A;I;xB~N&fxKv6s5lMuZ{gD9{Sh6lQ|lf%!s#u`umt;Aon``am@zni)h+ zH!ug9G{Y2ykHrJ84-$kSgpN`kwfHkY>jtXm3k+e%A%9$Xwyr2iPQw8@2ZQ54JDRMV z>Vr7{VnOv*#RtH#`@Q_B#!Z$yT9m!`L8JoTq>cSWQ4Q(nEw-Y{M!u&S*9yxoYSGr+ z%xhgtm_4f?>DRNfdsSq2ElcE4fjQ9ns3o-k4sIVev1emVQc0nJtw;ws7Rt_ zN!O=;{8e=!K*y@y&{DbSMBwo_+a6S=Lp;HsrlNBZN)6BZXGjp8yYZ7jgpTjcE)R&B17!s3b&in8N^LL7tB?V&+(N-M!v$`!mFSxFB!WXHmmM_IXFRCd zu+vAtNf<)^n!v+7P;5^5RveKn_>w;|Y=SnjiUi~pJ*;kQ)i-Kbrtte@!p#Nzyi*tk zYn4p$|A?Nja0@W3Oh~Cqs5Ua}1pJR)CR@ebGc3y|-y8xT8V~sT$~CYmGW6cUbnSov zwbGztDh&0iF0o%Mc8D+j%a2XVs7Lz9Z*d%Q-g_`0QLp%Gu12QN*IX@nlvW&v;e%h< zdP2Y!)H~MxqK1f6v|+RkACg;P@jE~o2z)R2)ms51`Sf^0siSGpUtGa|?P5{i1XsN7 zj^j&|De%cld;0W-DZL2`U?3s#htB`g8Yhfbc+fX7_Qu6Rz4VVxXInJ#hLewm&07>lFiHoEYtaGD#{R3vc`TUoOWb zT+-n!N_j{Ki3~_h#Im^FBr+dHd5II!Lur0eCH(3zvdj0F^qwVsk-(CN$wP!%6?x~Q|5HFK!)bW-rlw|7IDpmA`OHiN=GF#F&KjppI94Y+Mmkq3ahvtEV7*^%b($ z>P`&_SLCoka#;5PS4_e6e}FAVpo8VpH*yKslLH4*8o_8^{Ly5+t=I;VuHOA!uAk(~ zR*{FxsM*rz&MxLJI;ph|0|wq>IxNo$o7#LQc;W)hnkOW`cns^F=&V#bM>7MWym zj4hX|_``ZT3iz6I8DnD8w9;iZhX1!@8vfttwfBpVzvCaLh*STAaweoljiou>LmihE z{uq#71&a5-@uy>1e=uup`CU1Xc_AAg+{Q$T4z?5@XOU!t4blIeZGwCcuYZ~U+6f_w z`*9&JMJ(+s1oT!3np&Y&=cbW4GqWJvSE$M^q*Ra}m$%^9Xnuq|xg~?>$AqX7ch{mY zN-D{|bd2y)&JF}4GvEY5)z>AENMILm1?eK26iX)m388S-G5fq;mb5;bGMGW2Pe<{`QIJx84tts<`8yRDk5^zO$|2hsLs7X2qsUFcg$d;<4II z#pq_ykz#6hrTW~XhA6eGB-@1x=BEjGm)`(|#KT+z6j-h{wl(X1r=+R9iob}Hatp{I z!-@I3o4%NVd0fY%>x)Y?)v4%Jt${I4{;(Ft+&?dK#83 zG0Ff{P|cVQ7G8eA*9u49a3M@I&M#c;7|!n(SUh#1#V=E<Em;?S}f=7&0ajo?P>(EiICUjMKrS1EZ6i-*eU&ObtVjMIW>|! z)7+r0$&0(_4)v%V{R_EVTb@pop{jJa2N}Y|;(UoR9C$TnA=pg3#ov&Ix4*7gbv|1n zrt%ZmRF5t$-PiJh-x5cOO}t%SGK$T?fdf&qQp{nQrPx&2mz0*Jx0bS(d#q}XPC1nT zpY3tsl8r2;VI+EWa`?GF6nS6~u6J^WTPysr_GQz`7}M2L=y_|Y`Ai#=K>ie!(M(~) z9b&?PDQTZ@(>;O|McB2h8GiSA9BhfwlApUc#Fla++M#j{39GF-(bEAHrJuBYPvF`&X8*tL<%9L8sKRa@^jUL$L z;;{^ktiw1|GZFS2hII=PeKStv*1Gpa^w)q0s0&ClM68BbEFulH7wxX?69fu<3R(`LTq zDm6LB)a^Esp}kKK7k*1-s`^GFjk>VlRfbms8;ImhHPtT}6{K|z34$VM1cRYDIU)Ri zuMYH27TuDw2D?S+*;jq{t{=aUx_xW}jJE2ao7~op3k8ysjC6HYftTyqvEvOB`=W?5 z-cE*O+2@iJH6Ou6{D<|2QuXqFN1!0DB!b-vMc3Q3k6>Cc!*2lDGD74Wi5#ktGOoeV z?;7imo9W2;z*ZuD^2+`Ea*^5S0^|XZFtJ0OWqxx*tF6Ed7`pR{{7H4){Z8dt;)1Lk z-%qN`!&T&|v+u}uA68QFxH_guxQ=>z>aGUP zcX}%x zIMzy3Lo#c$Wzftjhe$womQb@su}ErB3iW-NX1{}X_>Y4R66P0Chc{!h1#9ehqw#k( z>ggePn+A%qEq$L49X?A4u_Q>+l)F^Xj^E1)bfwe2{lL72WE$FN%25jq@@ z;k(cwqWmaV9_>Q8@-GvP7+-SyXGfDsqBLU~=>}VfjK4as3iO)W6BkCY-w&esKuJnZ zL5CC1HVWB28C2T`4l|dIG%+F6HZrl7711Jb{+R%^b@4+FK^FC~DM!t}YbSJSDFE88 zgQc#bGB*(#MiY$CbV?n z643M|i0%cLI?rY!ed$Cf@oD_cYAYDlzZ#A`v6zrtsvoKIP*GrXQz>6UC95}QxogjzsEku=M=plE5KWpHz=KLVZ$(o=gr#c`5e|RJm=n+^JKR-l$l__RIH}#Gsz{ zSAEE|h0YAWURi#dO()P#3@o%aHSBn}+E)+zfi*J>SDNu1FID6)DMkmS2(pP=Qs8s- zhg)|Ra;9k)=HgnFNNY>=Np?*vJWPbiX}*?fg7)*eUIEBu(w+OS6WL;|Jj!Zp*3Hch zQVloMZnRD`-xkkg4QlC|JZ(N@qa@he*n<*W!pJvq7e~A92y`}kF}VDs0!j%h<~#I_ zM)EF2pr=E&)X=2ok#Hy7AD!l1_>~k#I2g@`jxLYrm)cQ(3D0@ylWCZV-$gk1_@jCw zI(Hj8sY45~ojBM>em5UaQ+~E~(@K^4@4O;4YR;8&LplGAEBT+V>wkWADg}@`_bCGe z$}sZ1$S{H)(WRUIbCE|yPejEgo>1{xGQIqAe=)rK!>HxO zlGl22>MMs^62#@o=TOaTiQ=RMwc>6bels)2S{79qLh;HpiP5@gYv-`$2&&xTjM z?dVuw%D*h(71fuC5;j5WR<5^R^=^je#1UxeAy}s)IXmAtgYUJ+W%E^jPS5yC7E0_- zQL2i@bTsP|28NV{4T}2E_NRB}sj6wt@V%nqN`z3+J%K4CdaS0FAKcR~$nfkH1tv>8 zhb*37_P)dUY%yBStK^;WaWrW?(Wux+4`yIw7q6c`@78Nju2YUOdSaKDntb_@>8?b3G5^cpv#kQ?R)YNUO2=GIs#!ii$H24DoonV}tK!qv?a;nVphpx7v3{6j$ajBW)=2gwibd?i*2sO#!n+k0rUhmU7AkIWR3OscD_yaZP9 zPNj%%QIE?KFe*R~4C(z_VSztkyVbSS=hJg8%WX3tVB@V4r0Pcv1kfkRD^Sw5#h4dU z%tiU38asi|l|eh9cOD9})0RZAuWCv>gzhe1Y(e1@~=46(0ZQQ#Et*?RPLOdpA6(gKU)eGBE z$M4`jzz%rTEu&d1L^33&pj5mt93`C~^pk2zh5vm{;Uz`Yd*bKfcT{B?Rx)+6%Q5UFlcpkbUu-L}eR39TPJU~@If5B^y4)T6=ogtHxTxn}M{v*0}FvP^?PAt>~N z@7WZ*wA?9Lz&=A)2D8#wW_lM5+Iqar_{P~YM~oMU8&@($U+kveStrts{mFU$3pX~1 zsis}b_YU8yfy+^1MqOK6)q0%N@%I)r*!i^1ydH}%JCpq444Pe6#wZ35aRDmVv3$NC z>Fy?@C4kX`DauHKS$GUBx1eMo-tC>}lWeUohgF*= z0c7oY+S7to1`JD*KmKK_07?JRiCi{2DTQnHy5axe?!BX$+`6@I+*VLTdX*@MbdcW3 zqi#Tg5PFx0lu$%OTBzBUNC%}#%|-~J7pc-B5J0;0ra*#-^n?-uB=NiVDdYX#G0yWo z=bZPPGrsZt!x+g3aA)1?UTdy7ulc(?9`dy76}7E)5Va7=XYVc5rdT$Q5ih9KsB`%J25^IpCn@+=mJ9-_6K>3NI# zJ3L;KL)EPcHeI?hX|VEfqAzQy8R1CVyprs+$o=Mog{Bc*rn#F2p*Gt&@hX?D1ZLhb z#YiFl^6KdV7O+6ZM3@UTYAM%-;y_J`XbFx9zu_!=f-%**{TlyZiin?4G)3tpjETEue$|$1ekJm8suG03(FQy zo~&xpDvTP1t26l75<(5>nY9^9>dIl0muDqEs*scKauyxL9)xP-!7ovSJL4rsJ0I>k zDU5hZPMAlrLAbsrsZ!Ja34zgAux7l>_9F5o3B7fC$u8Y)Qw4NxU6to}lTyRW(I@W2vN9~7K zT!lF*(mKcIRrhp>hPapHPoN6cxFtu*P#<#X6jmXP@I-*9>#s^odsn`?WVn#|xi48+ zvAIEt;Foacv)v-66s>ah7{dM4>Y?T)Aqn>wWJU<}Wmn#HQeH*ipH%FfDQ&e%+xCHP zAg6z3V2oUUA|p>|Y<^X}68<^k>33IuSTd1>XgwSH&M}CW{Uv@Z=wJC4lw7HrSIaAi zGW1U<8yUpr=Wc--X>!o%qo&;Gy^B7Z;|j1ZR0kBE+G{wIsVp<`vTnSEZMk;O+Qi`b z3685D{!TpnA3gZ|<8xYM3$la#{H6%<$<8Ht8rSvkOOb5o(};4aLc4U^`|#_~xW3>& z`v^tU!az8b70rxh#l{z)wC!+RY{X#F`ugWbQU*h08%iU^7`o}}eSr3c&e4I63%lcb z?8reR3_0h2B7Fj02-A(U=o@CV9q=RJdjLjY4|vzt#nV4b4i<*aPFXtq7*Y-*In!m#{L3g=19r3by-u%X9%*iY6-n5q~}MYUK5J$A|k2&P75L zS&N#mpP|6RM1XDiZe}6pb*DFUr?4N6yBW0y-gi?rgaGAk-72Mlf^MaR=X4~~!`2mm zbV?oN>)vQBdr)jKM}>QAQ8q!0bs% zxGz8QNd%OYo^VhW<{9C=t~V3LIJl^f+>09vu#HjZS#5OuOK}moRTQW#>TqH z#r3a@g(;_zj9JEBGVLo3_wC2n>MAdd3lNsOou@bBJYJr#>tXMnC?)ZD#*mJbl>)*Q zeF6H@A9DSkih^#2K5!)`v|HQ4e_T%n=sCONpYGTs*PK-dGrR zn$KGpTNsP%ottMkmFf;z;@U=rhISnHpDk8I7_c48)8TJVOuYRavK(_LCy^s5=qi<+ zijh~+(^^UE^Nb5jXp;U274AEyg7aNNy4R9FQHS?C*tV3l6=lbl3Y{*0$PVXk~0|G zB~%>QlEx>HMH2>dg!}(uV#yCIYKd@;6!^>KtQ6VFq5{WRw)@;iz)oaf^+d7x2O&iS z>qkow3%v7m2eyMsSosMH%=UYud?Uq=^0i?+W6s}x&cTNbQ?R9zAn)Vrvq{(r@$vQS z|G4ENI~3+%B<$;u1)BDyanv$|fc5!uCG`#RaTV3pQ$SD@kEZX5X<3y_~ zOxU+%3-q{VSQSIFG6&)yWakPc6fKs8e1i0{I7T-fXhq zYt*h7tN?8f_|zd8!)3sOB?6^h--#Bb0=;Lon?MhUC7LA?_+Opo$48?g^r~fges2A2 zE06Xm(K|SHQBp!B;0JV}E|e2tFB!>aL?}*|7na>0qjt+7)quX$3fzXJPSi9H>>yTm zBqjSqZQY4jwuM_6sq>@cFvbtpi|QVyX@#eYp7@_U(H+}B!k}JB$(_JB0$!3~$kG!?4sH4vTYcDu{%|6+ zcwSz7$Cvg%W8^yoXP{d*O(Kt;!{wtn;>HObWouac>HC&G_kO;hfVRxAv5cwX_0DHjlmXtMO`S$%E0vc&T4f{Q z;cDAgmAB_9!AJKqPJA~e+uIJ3oCm<3TRWR{%(VFA4r1JCkLTuYpU;-ELL8vWs9Q)JG8yGyXWbjA$X8f`bf8g zvA2oY48rL&!%Vzk>Bs)|$Djrr$xoc7S<-sbdFYSFZ{{i&pGI5hcQt2ZSruItgrN|O@(;dfxpKBoK6>{%S+ zC{cCCy>*Ac`q4l?tZ5WW#RW%r+VA1wfY@p-rzvTRtvpPtYc|3gJ-2uAmDWCfn3KM; z_zlhY(rBE2Ep7t4`;+M4P^vcFjTH@0A9 zzj*+z7W8-n_w23&Ni{S0G+*pj>cSLJk<{7ui%A#UfL>G6kmEfTC*^5KJ^~(Q#9p^o zQ)opKVa+l(@)F^`<)b4X&AMMqQ?xEtYpM5$NDlmH7RtT`y$^pLeqF{|q>+Poy)4b~ zoXyZxk@9q~Ac)jSg*-DEYPC|U0<^QnGAaFgknX~_aSFQF@h_ME2x2<*sfu*-N`kun z6>Gq0DMWs9d(ozZjsklA?(U*9R;Ar>F>eb!ZT_UU@T#@v(pH z7t^2BhGxJDLqZ>xuq-$HRG>P!Vb>yQ=T*wy9(OHuisKVO ztnC7FzIF5$)0n-+O8OGKUHBqR`wQ?=OfWo7nt0VXbN~>Ewz06$nZ#Uo2Sw*Nk_uM< zh^q#lk@%;!e06~GScs#niue)3$Wzk#$GsBZ!cUQD+5JzyiQ6IPztBAyMNrXB9c(m= zLRZBJ*w73OV&z)q9nkr2kg6*35(Q%w(5yH*JDW)USiRNypTC$W6Iu+8?4+ZED8@-B zY?kX6)9OC3PVmK@uw>AFF_i!UaXgqH*#sE2*&=ru3rAGw4n0T)PH63$ zi^uFLb#kM*ELRH~#mW&_BJ>dGsx4{!^Q37svAWh4<~M zWo?k8)*p-`r6p-yuX?wI!o|g#eWjN=0cj267H+8lMl!tz@ULMpLno><>8ZTsrPYpa zk?UYCujYt}zfeS17~-BiKDx(5oid@|c43m`>qGrfIIB*2NvUo{#q2;Zwjm}2c3^dK zvmK>GGU%dG$wWCT0bNN%L5efBYxap68+4Laq?FA=JZ#PXogAqt-JqnVD z@zA(CGe`qfQEt&5{~JT_?>LeF{g1G%Cox!eNM{q!oyfWK#J8_hu|)akNMX`%UlmvK zYe?|d%Vf^3iy2~PhkBDl!!WkvgQ^Xv1(=jd6vGecvm>yq;3P&BBaD?U|M9s=PzYp^vW1E~AI+`wWzH-6Y;$Oh53m%M+9{ z_qGK*sjS6%nLQ>}E2LhF!-f1a(TV3#{kZY}Qf9XYq z%~MNLfnImw#mt`o1gxD|v2Ts8t+Tma)%hlYURrlpwhlG{#E#F6mfGg=I_a>Rc7-lF zoxA1wq6uwg8e&oKM-g{Qs`9$sxfPf@rDw;&3U?&s#v*Xd87V#>>G&b+%1y3?3w(}*`Jnb zW>nwqX$s>s`7)XxZhfLM#P~?4u-?@F0H)o0_@+2k;|BBqO;Mqi#VjE)6}qR(VsjLI z`P9|Gg!LH%Ng3_EZGn#IWuYWN6~4Z(a9uWukq?_nIr$)}UBy{(^TO-$ec^8X1&IXl zBvX?HXnYq@6kJ#Q2rhILA#2kjIhAj2-9qmVyE#pkg&pJ}#r`LnsUvHtRq)M^K-9WM zT6{Im5CbY&NN-jf1_!Gr(*H8Xdr4kon1sh4jT=4!Vna61J-g4>yu*?Ny+yAKr^w)1 zUA8)e*&mWF9@7V!&@aohZ)MVW-vR#fg!E zrEpU<7$5%LpqyIAF}E|*jH!#2xG07I)qW55vBCEH#7+~Oq8G6svEXIxnoP0AsLG1 z0c-w{wkL+AT%{0M&|7N9G8V-+>w1gG#?genm~7S!!DyGsU|8O59%@w?ifd}TxyWUMW|I6`l!s>^4D&zNbIyq}>m;$43Yvw$rapViF01R#t*^>qIRlM{QUvSgO%5r6?SdI zNu@sQEehj1+jq{2QKr8O-R%@I>P(vgMJtB8I{=NOx%PEOr20ABNxNc73970#2xDz5 zR%$@$eI2QSuvzAD+LLFcE{YOk1J+940mLqjpR0Tq+Kv5%Qur^a`u;KH7n?4B7Q&GN z;Yb@@<=%^0zy6N`!2j=yKHyQfr2=6-Y6fPO3Z`^Wc?qC6fg2vO89lW^)WNStT(b_VZF6uL8G7Qf;1Q?m^jvLYLZxT?G8-TPN(_HM&=1y3;*`5GvM1!^ z+?koWrZ3O>0`JaeZ{#3N!*vUk7StR%GfuIg7d(uBQVgg^Zhunq zR_1sbuq&9y&-K|V& znyqwuvufg1=t}&)X6_&FH;NMD_qRmJ3}2q6&?*d%EE$<#2(lp8V+C!5Jf-mQ@Dib@ z*r*$QY0Wf`rGn2A#kXAZrOT?Sv%ZFJJx#J%U6Z_zfW-Bcjb+0RF7i-{<_F}RVnkU@!7{*lrD22=T=*@rQ?K#49#;Q%JaZ^U+9 z|2*$hq9`iv7Zb8eEbf!?5g?3-()RfHtRX+z(rfm?xFK+s_d#gv*OJcn+7gPNo{tXE z3qNG%jbtDGPDRFf4ohX1nB*B|g|QsPdxqJ2A^1!=DTVb%I|DOHwDvcRzucwK;vSPRBAt5si0Aktm@}m-@tCOpdr0Jz6bm zfB4p}O2MY^a_| z-2_(=WAIvb&I-kuUpMMl6d<>n0ZKQ}1%_ZaGf9MtR0VJdS=QPG>vKm%nkIcKh>GjS zTQtAg-&J(Ntn+~DoxB7e&@94+x>(&Q1rut!**K^vJyo1dd)?c&vNEnvpW_j<6Ha}l zG>AqJPn#Z8wvXhWWhD$tUIE%G^c_p(f#CZbw{btj9Qm{H+;{aSpuVTJLV)Kk>uYS( z`?)&o1?h<@m9ZkJ+T76e(S93yrG}~)-Matkfbc(G_WAok=0EzI&tBjq*6*EOH<46x zU35b>8PBRx3~@~dpO}~~|M4*5Y`D`gIisUo zo^*(RAWGD>_La1&ti$n9y}G)TzRv$`B<4|d3rV)sW~@v2VT1wH2#943>nwgTsiPZq z{@4?ekIB1)$f-UMPhHKqm`S>N-%@hVyz7UyG@ft!8YT*F6%P)y;D5a=FnKFredLh* zX&G||v`I}zIz1=V?Se`LSrTfVCW%@Xq0Ew2HCaCAi4* z`)V*kA6+V-ozK+Q-DT1zagoaP_B-P)C?UkI)(fRFI%#vHZw;&ZO!n_nd5z(&E0d<> zYsmvCA<>UjXkFX?H8yNO@^PtfiYGK6UBlwH% zN7uF5O$?3ZB=2U)e{b5oQqs|C$X__t@8@sR*b5OJV{=%?UZ|fsv71G&M!*h#L$dvy zuFL=MBKh}!LHO|G7VR2(xvCcyg=BqkFJQ>UF?J9Cq33ggFTt1_WJ4B;xsjQu@qMg( z+vo8>+3WVE`lNw3O$qJT7fq3lQ2dcT6u#-lS3J>UsGM_4-IdJOlih3)AUVvnofQZ_ z3l9T#M5eUMn*oM4ICkxO{zbLg1)^my34mq0MlXA}-2@@^>59fU)+oRMI0lkVGMqiLdIPd#^-rHPHgB^5rmp_2AQ=_=!o>b}7DE z!85nI-1o3fCKWQhnu8zdM}t&FoE*6I&cFQ_YO*m?UF^tJF*GoNjmm5MPs zl}7MdR**|mC2rWHX`1kbvPX`yJ6Zl5H^JP)(v4|nsPY8dA2?QJ5ECMlFVEXnw3RGe zS!|8GLe2Ye!Nz6^4j;@G&$`ZvYN?Hgy8oiT+b;d95)#wO-l>5+L)tzs@=5=)WxhZY z%{)nle3WqS_M@6K`5k-zSQMWj&Ls*Zz+7*v_KQghu&SQKx9cLZ(bSiCtFk-kif#8n zmtFTtM(#!-Tv(lI`xPBE6H4}+e@+J09IW+#oCY_R{dVKwF@8=1F#E_`&WP+z##M4& zR58N4JL_g9QTDXetNC>S{VF)?LBd4HrFK`&0&FH%?1v6TRMtX3BH~7($LMqtC}YXr z@oG3uvD2A6BXj?%Ce_hO!aCntYtHgVjxSGY!Hv}X8`yJ&DNd`G{p7EZA_FtXAeL1- zG#h;Ofm`&zUU&gN>{L>xm0@?YZM-(YOuta5WU6Q$YoEHRUndUne@P3b7Xgxy=}^Rk zHvB{@Gw>p}O>;Zdxx`7iIj!`y>~&I5ZJ4Bw30HJkWmLsiq$iPoD6yvW>(|*ez(GwC z5wjnfs{&J7lJiO(3zUBPL!?T@wyaS+?a>D0z&y_QAvsW|ss-#4TcI)5B~MZM*?V$d zf|v*EK;El@qwxKvolD`S15ds9AVb~y$;+3r&l(z>H@D(^RXdo$b|5`jaPSp->dtF< z74-MOgM{tOkU*@Lb#Q17HgfR2bjaSW~GX8OVqokfM;Y{6o3J{ z%&FX8Gow8MxIOOIATIUF<41dz{F>}ZRuaUg?^>1DP4R>ci{xCX2``h}dnyu!MVZzg zwEofFE^&%%B*W3GtY~=Y%je{78I#cgu9|AZ_r%|?C;$GC|LQ%rhy3@h3HYTJhyk7` zdL{qlRAaJMRBxCivoTqRw4xRT!_Dk(ktRuII}pv zUfsBH!L~fWXfxfJvmkKXE`kz^8Da=~3Hmsj3U+8 z(LU9I?12jZj%MxWrUlfu!2?S4V9Lm=9AA%BjR%n|!%}4tVe)=<;ic-ky?K@rtx6`= zxrW4Nn_3@gd!+lJN3&G#le_J4tw00n@?hCYctOSsmBQj4LGu z@{bpcyzX#*rmHwC`O(-!NKFo6>HGsQY5^~00+t8?->$d9NlM+39P~e1nKh05v5Sbi zX4Uo6E448^1_9$})7H@gnD?uO9r4)B`@%o#{A% zg;f4lFYyRtBB?sbYhNVXV&83WF|NY8ATiZNQUX*!^*b?Yl^(1`kl=}qlHE9iZLNGw z^~aIt<34OgN?*gomuXnMl5$rVAcgc;@y~I?ZFC5jAzIc>6I!e7uqSNm@^XjY`r>iYh>YdE; z^}*_p2D{jo@oqzR1)o>I*v$LQo|zSYeUh3S$`?2o_J37v`foUO{r!OA?}ses_YDC@ z2}a~lH6n$G#8=JL;+tJ<1fZQdx2P8hUmHz7k2d0wd^6kAfkw=#A;!UgaWCXJaKh*2 z_eSzp&T_pm4WQO_8SDd33Pgl;wVyuLbk#sX_|Y+lgC0j;BQ|Rm@8?Z0-hmW1YJWIm z)VX$B1}hXP>=z}o@ifp&u{d92yyRr(kFWCdlH5nN{rgA7E2GCnJjPl;K}={St2(Gr z=aOJR`68{SV~*l|oa+vE4-(6aad(-h6-tii(@+6I+8BvK0EBlVy-$|o2sGy@f#EAG z;7T@f1uS{`uGjVaXU65!KT0J158pq#+6RZ}CDRdu7Xj;W$omhKM;_K0URI!Q=LY>0 zSGuM%c1KGYKMnKBV8Y>7OwSL)>XBRWOW;}2C0IhFTKM0Z?z#{LM1BAC@YFV47nUbS z>|4l3g^L%)*aa`FH~J|-MP$&k$5suPvf0&-XL|4+|rAy3+~>zO7o&-marQ2 z=;}_bOxAN=c`FkzZ<#2q1Nz(8^>jnmhEZ*;f_dAYSt@lY#>$#Ls1GALc*f&hGyK6gMxDRvz}(tRq0U_a5&l<6V@($66)L zF*cz1Dk)_TGfIFbkQG|hXjju2Bxg1_gFRz=rUK#K#TKy;{93)EzxiDvRaD>KI8X9?L|}E z`CI~Li3uxV?~+J{RL{x*22}1mKMVbe^5P7(Br6vWL(Jj}*NA)3^=tUD4k_4K;2fXw8LJ{v(RPlFGNV{3QnWeuBTJe3l=&7WxMw5Eg zWigXB%?aS9<%jTurIF=qP{WGhmI}5)mKyo$YnuKre7+O9+kjo)X~o`RO5O_5)OUi7 z65BnvhTfawcLA9-z*g9+`fBg_I|GVKbl`RQcG54V`Tk+&kr&VYdc$+%`wnz3OY8Wk z_WS?qBv1Qmb6<;`a1x?|Pj0Lu&pbZ3mrba{Tba$%>%v}5I5=zM0 zKXGv5&PC?LW$i_oUbh_Cx6TiKUxf*xj)&!AYB;-kOb6D!&)i#nIj1UPkEJSv%>#Tmc6tQf0PM>2Xkw?np;Hu}Yq?DB-I0jHklrJC2{I8Ss zfBh`}=V=do!KkezIvNRQn`5JNPA(;=RBdKP3Wm$_o-+J6DW2Eoo_j6eT$nVD`y2Hc ze;$80zJc_kSy(ovl|d#Higt#By7R6PMY&@F#8O;yD6Jd)mPdu}r*V)XO@w-1kBw;u zu}V-Hw+mlui>}&#eryl{ii|qa3r*>>_A$aIaXDS>lTo#~^IFx?W4QI^{$H-DL=?9EC3(6a7R*KAvV@fL?GqiztvF0^e5Z+2=yjgrw7f948_B^ z>a~2;pj01>Y+mfxZ&#?;ZuSSWg=^?!7p%vv_0Hd~y%V3PJNd)<_vq<;-_b7Aa;Bdu zI7WeMqMD|IvaC)tf^KQ|t9a`Bt%03iyq3?jJJFcVH?xNTN=mt3OkUu)Ky`=CN+D8} z48GYeDJGs*PX1+IQ1LyM-k4NRe<0sgY`n&PSplW~Eiya*9c__=jZX zFwK3pz+fJ%i4De{YrQ96`4`h&1_!m5aG3B_{Pi;E^4>askAqEmo*J{9J%77lLaqOR z|KJC6M0|@NG~Gm@IQNp3oTJmZcIorhi6Yo+n_&?4@0Bh8x1b|btt%;Rk&|&{9XAhS z)%GWX&n?#WiuF=d3zSuDeu%zXo(w3t)NEb#8~P|{Ql@fi1d$MWq>IdTpk^zogSoxUfx$=hxIw--a`h^R`JLStqSB|DMmM0{&|yse6<1!f5ch>gHB-Kmq}6GTg;*-iqxvR z<1zt@Jf3~m@Bobxx z)&;;YjrNY339Cx@54Z=SoX+a$HT7HdF~$2@A5)Gr?LzsP`6f& zYeY#V$gMLZe(gjrs)MLNwYBUqIu(VokL6NN5nt8~G5%-*;+RFwY*0+-17DTU(l=Z-!29D(5sJSFOIwtg>f>Gf4=ggv~fL>c=3P%6Kde(4l zB_hgnEZ{1e!T8ucR8K*fmvzuWIHkc)yRk8-Vb5>5hI+q|O~zWa&cpPjnZFlc?nHIr zyQ7%%{h-c;XES?NwHi+>ez>Mdjg8G!ugl4LvbDv{hm};D6`$P9gj4?f3#rfOLG!90 zpD)%$b!`CZGT;QQ9LK6<8zH<7?Br9A17$DT8r~QSq@TwNU!73#IqfuBMT>jqf*Y%s zE?&abPAF$DepsJ-QsClM&HozU#T)QGuMpV3u_Ek>;pOioz9OjbcS6?Y%#5-8(miHA z&NY?_scGRVdgj?a55##28{-RZV0c=T>paVfvX9nVOkQsI&Yq_5(M!O-psU`n%YYCa zhiY8p0K82T06;V`Cb6C{auI>-ve(WK8J=3TC#%KPN3h+lMVq$Bp35l8KAa4+enH+p z5q$Sp{(Hk-t<3zXRCCnh$m^yk!(9u<+7gZaX)mBH+TTms&L|^Y&2dPzsxPkB`RT2M z`&L#aiWxEyvj8@@WhI`gaI-P!!`RjX7@naJE=ix#^m=2!wf_D`Cvnes;&GL%c*BHp1q{;GKBkf@w>@Gt=35o8G2l=VT0N7+ z*UwdB>s3>6r;cmQGGR-iP1O@3tGq1HC#i4PV`!+{SYV$gk}7SM1wg(-@>RlAevWx! z3yg1Oc@~ZvWKI2u`iA~sK;tx^e5E&Q%P;Q;0EB|y4w7Obxv1WnGl&A1um}l`Ql6X! zK^arfOF#Xo<9H`wVR5oN#rt-c-+M%WSMbyo*qVHCH=JHyKyT?A^dBsfEYVK~^9pcF zmyJh>*(ZPhu|i3)%m>4vS!*WYCH2y31JX~4don!dED5odCBsEENisi#QsCNiF z@KVE}>Yj+vnbB5rspdC2_xhFM;edUBm_)zax><6wXN7?0HXHkl=S5{~d z+H$I!B_I7Kj0w4vj&97@x=j-70oCC?jm#e6g*B-fOZC%{--t#Jf;$D=7>(j6f;XDu z_EObFZ>MYHtrp912?AVcPp*+F6H5F1A}-CT<}@~chZ|Q3@}pPyEi3mEMh_ENj*8B+I`Mf?qdV)4e0cfmg^^KtW^u3N>1)!m z4+=_-PMrD{xK14|qQFZT=*Hj#U;@iJP-aQJn@2<-bM|C3U?>dMLjE$&Nd!yOhT`fub$8pk+gml}K``4iWGiwDCOuXq5;%fyyKYB1iqT{R-O zlUFmX3e^e|OLgKRyH_p6fv-3sR`p4}(IE5j;oORKr>hM$w-(o?Z4~G2*Fzs4MykjF zINXYNM$(x0S2fkCOKZ<8%>pGC=PVLz&O)rN_aEi+HwNBF@k@5RGIUEt!Mx)xJO3k* z72eBsg)0~Lx{bT4VFF?$-s>kDy1spJ!|(lX z!`pZ%kr#t_CT8V^$)D+)Z0$%dHmca-)+`Xa$&kQ3=sAO>a{y+R(3`aLxGop;pm{p{ zdc_cCBP$*4SLQi)K3>`PGUBDp6pgcDVQnhIhUYjqcAqX1oIbS&z2=p!Hl@0te_FTS zUt7;n?mR!s!B4E@?K_1U`TP?70W0eI+>Z*!o?#_rEvWXsFE3^;`K7@PSe;a)ER36q zht*<(p1n-9gmcYJv2N!LsgbURoev2!8dqWUHmey^&NkdRkcF%ZN()N6F6srT2v_ykmkz&vd$>o&5>{AFvQ93$*+?&Z1q)B&dJvtw0|<|nB~A0Ol;s6^By zn+(q>N={;})R_GpA_Tk+8xq7T>eMx7zWiUDpxE{=&|iY#-|0Y2)>Zz+;)QLWM-5~M zH3%#euIC!xE7J}pe}P@`R?Yam?^>Sk<=eqEa*0Y`I?PN`A=@XJk344y;|= z&m}wHb96;8kwT2GXy&Kda23q}s@g00V^#kqb^GQZk zX}S^>At7+A-M4iC)VF#R3RQQQF+H8q?G1;+y*Z)&!NLAoH&iK8q=F&sJx#q!_r~t^ z$KG4Khyu_ewvTx(vcmWidTa_%ozA!FydEjomRpMydJsr6k9GKlD?$&}hq;PJG0c`_ z=4Jrq%url0)%hIXm8<7|fWM)xtLizb%`QycIGn5YQMJF}=q&=Ku5U63|#si$Yen z>h-l(q22bSx=_?Zam?#3vhO8pwD!cUcgLw-5y28=_L1XP-z-l)ZUm}C5diBtLR(df zjgrIPn{)dUb*I9O4{&I+(_4PldN+r}H&Z4(QCQfPC!%1Di7i~VM|ZAhngn3!PZ>R6 zlGV{#z<%hhqoWbUTUM92 zY(O^dD#S9_YkuqY1JefSk~nwVn9|P!IyTGgt5z%--c=eoRexD|N#>lq0)Ayi-7+V& zsS35G-w^m_GXD;`v3uFWd`LYgUfdYzt2vBe0Z(O~kI#|pe#iT&%mnKKOYGuw}prU()B}uC< z!_|s<#@2POlbsri5W#n=V?;xo9KJc&G}l(wCy2xv@~<}O*)9j)kWvs{z5KM7gPiD# zA~4tqn|rbTo8imQ)5%YNSR{=og}WIJa}l5y(^uYwTPv|^SeBUNIP1r+NDi|b_8V#| zsF$l=jwkcA0%qPUUV8x5(*(tLBl}69RR675L$zSB_rCd54UPeIA zRyb3CF_C2iTP_6FC7F*gxDY|xDjrjeJ;(HmMmvHATs;~-o2{Y=qE^q{=9gKuqX%&t zNuw_*T8Kqr5Lj-67Mcb-+jO>ERk6}Sa!HAeLupWXg}+spR|4z6`HCPpQHV8ZkdLX# zT$#+(BK4?flp^1f4dU&D?SSt5>kWbL!oFCUmq2Cn#R5k(hwdQia&w$f^hrG=rPves zJ$N~)7Zd|zQsHWJ7mRd3cgL2%@bS79 zzV{7{N&Y?|D|w#CHDo9FLaQ!G>5mU!R&52sS9#W_>9_T|F23yAv%)_jTlvViduO|p zethJwy{J&y)}W=(JyGJ+dgOvI!|JDje}pfp4Jv(&2p>_#LpkH{kiDF`c#%4kl&N)s zl=5_oNe)N6P0)O#F$^R8@V|RY@cfyjiXqCv?~xb@p0bRZ%Jm=t)ijsilv5Vg?Q6Js5^NG24q+7ft zS6CZt;myr0WSbcAy0`3SoJ7aP?!9%1v7zkn)NldTJE(B&PZQ>tlqJvQJ+*URy_QRR z3MKn(4htlQ0YaXXkJv!``w4S1BicuLUZ$!WaU@z-H$nkXcGf&m3!f;_L&`Wkj_*{c z$RM@{ekkZ;(nX;tj}w8@E^(9GBDpYzR0XmHJ>aK*eFdt?Yhq1QkEz54|kJKPi_ zjWD>;oAXQ2+dg=K-h^b|Xw8u`T75*z=lc)WKWS>~@-}?Q&$`*0;V%RPtTNtoHWHx0 zo7`c0hV;CA-{~1Pk62fWoMT}-mwVv&(MMY#71m{e)(b)oCXJ8acc?Kq!zbz`uko5k zYNv;jqB7-5dbp_WfrMOjh-~N6if;9s<9dM_TwRyR>k57GnNhtP+ckFu4MlULMABWT zm%B=In7%LBtnUHq)bKO2*ah<-qaB(Sx+kTC#mv7%y_4PMS&E^2HjQWNXJRslWl{57 zy6yExN-N1@w<<~T1)-{F=Lqm;i#XBqEBtcxdD=yvuv)~!vJrclS? zIrm~yejq8kaku2Q+&(m=&pi%#dxhB|?cv|t+;04KSMrSOZ%Qvtov|s-hIw2A)OuR;pSu2>lzL~$?g^D2p^aub%8c#M8sz?xJb$M*`83RPeyvn0Lj zL0$=GC4N=oqlV>9^^?D(352A>%qA*Q{LaBlO3Sp%bOx5qa};I-DwNyS5#=)9$$Xbg zIJx<3lJZ4X1&KYAB5zCL-L)EYGd$z)R)Tu*7&uf{LVl9!N1ZNsg0e9HQCG zm+;f_nUy5aHT@n2^gWNWb&V88S{a;zR0#8u#m}N%?Nu`O5s%f&qqA6Wq_9-NX9wjJMxyH^r+3_yX zNd9Z)!^S_n#y#K2{LzqkD^VFXh6wA9R3tF$!E>NFDtAE#oCWcDSGBT)=(rq@*VRx&k*mfHMJnC8FQqJgbJYzsj3dUZp)16-Br-$(WGam z_BM()YUSBVi8Ugp`K5!l&&MCXm~z|*=q;9G`I0G8Z_@5a;ZBsTq{(}NoN|Iq58q=} z4T~x(XO}E@zae_LB%u7d%qzMqGsP`r1qoODL89IZ0cgwAn;G5GSNW*ZOVGG*(UUs} z7NUk+-O0`cFICm{Yw1HGiH|ml+Vg`8y`G7DOt}8#fNo&c?9pK?$A8nv@4GvL6&_Fx zv<*ATCw=mBw^5NCE6bR|K)I#7q>M}D>hJ3WRgNVJa$DEy*X8pY{*BJtg`2Y?1ZinF zaxYgg`vs5gCEYrZB=}!q<8_;%S>HC|*Q}Q)Hd1^Ido{HC*~HXJ+(G-ZA`N#@AOWge zVG>Vf+grp(@{z?~9>n3;sI`Ba3RkL}(MUWrn68^261S*-I@kiQ!M(otZrAEXdi=Q# zse3as$H%B3gg>ce%6B=!3A3;deR4SzaMD{TKaWoJ(v zp8}>4Kuman3J0VkFFYd0{35?BveKjpivY38lPdmz>b;CSLro8>&>iU(K2N?}LZFB4 zLF=YA-#4`YkY67n8wni8-KP31EuKR>BK~4Btp;p$XgVbu$6J`R z;i?ku=BYrf+^}vZkxAcrQBUmM8(GmW=1uAw8xtfx?|ShIpI^6>GO1Frn=Z3{Z{}a7 z-pN|rd$0RWjz~cWIB46rdf?e{$P>Z!{Z+{YpitO%V?{%vyDt4L|Av6vRUAo z4nVVePKD+)SgbwUupeq$Nj-kBRf%;gG2%ZFo8ZZ4_{F4y7t*;KE%60w-F9E+zKWqq zuC2b3b@6+E9i5Ue@lj;l+(b^t!t*Gk$O@{Ag>aTgnPVg&oU!I(hLU>S^9$5zQx>6eg4_1a_2uQR$!@DSR5#kxp8B~G5+PJ}D(aJ|f10+oYwC~joE44!3d8<6 zIj@w|RO`3KdLD15Uv`dnIF~qkYN(qVOBxYVQURmV{VDU552La?(MO?erwi#omHfp6 z;J*4tsv$dWwF8DIR7nEp6^xA`OI7L>mT9BL5%$T}6_p5}z}*SHq9S|x4r!{O#PNYg z-_vjd!aQBROMvMkX-_UGyit`^79Cwa_QM0(5C1BO8mUoMuu@Q|p7gY^6yIECE=_gO zn20w|`*;&$9)xZ!-(1@A#}Dp8_;bAivGy3rPL`aHmabT1h`FI;xT1;qVZHqZuM2sA z=!=)%_xaG zIh?^>w_@X;9Jh#-;H|sSxL-`wt28086GKK9xaJk$f_nZ%BsXBi7!WDsD~RkwQ&XzE z1cqK?Nk72C8=BLx^#K#DT37=LR6r#vj;2>mjk=p2VR1Kk%3-Q{;&K_b!`t_K?~hi& z=R>tBnO1mf?8s(&?H%^G1f&b!c#_OYh4lCg(3q{&1vR6`7jH`iba|$sS;LQmcY;TCk*sh}&n!r&vWwSmeCVA@qOW5##Sr8CaV9 zd~34yY*L_C*+hcpC`9{1$+^4~YU&MQPJ#Qkl>v#O3M%uzTq)o&1zsY^&q0?PX`Jih z6!vLdU_{3NmeMC(E&%A96i?B7U3;^QSap_q_vEJ5y-k|vN=E@A3@1tGOU3v zCyVj_hr0I;YI6PCeRWxoqDb#xq4(a5WC;Qi1f)Y~3y?0o1&lb%q(lfELQ!f8 zNS7`}AVEMtB%y|uwV!q7oSD7PdEc|&z4yHP{LViPBQP+-^W68hT-WESW%f32DxlFi z^={Yz(h;JU7jCeqg*C~sm3dCdvDVedJwaL^m3B;in?~FIAn<6{hUNw~u94iIa(Z$e zr6faBri3h5p|ru0TQaJLZ(%lCZGNqP4gRPC3V}LlljY(aO%OIw#e4=_F5J2LOl#8D4Wle&xpl_NriTz znC@*78>d;skwRj`G`vC^@-@MBG;Ldz5*DHrH&`!^!=NDzs~KXwo?@Evi@(9V)gM9< zzVCiVMbOR6@NbO4ba5xCQ6Pg4FqGa+<&t6tEW(>_Trq1x@nKvffoUa>fvkWP$rcQV z4sc3S`{UEPNxtVqrkX>R2h#i8GI!x!k0EnZM`_h|+h!5B#=R{BmtoxX!&Jv&k2#LH zvdHoAuv($k_6{rN=ZT3GX)rZM}U{Auva?mo!+%pNx*bW)j6M8O~ zT~l+i;T1d(%decdbOm@ z$kB?W0Wwx^4^hJ5j2QW(e1Cy08D0awHF}OHk5aOlFb`icQEBA(ewLAW>r_LD={6kn zr3`25nC7yOzi%PU*=~YfIe_yV?k4?xp$+O6%ZHwBnDXiBc(>}YmKA!s`(<3E+$#Om zFNIgXB&aeKJ*LrOeeT-Y;X=M0zeo3-GF8WsbjB19jI8}zi_d${yX3_i|NOVLFaKFm zr2n@O;{U?uZbV`IcP2v-qoP6xTt@01E{-%D)>hi)*Gvt6llndE?+e?=^a7)7nn8g5 zA&vouDgp!%T*`exu3qZnd;a)S8dv-M z<#c98sCDjRCsuN^D`F+t&(Dsl;1j$Wi` zZmX`Iv%gk+E1zW3xn$b@oL`RoE2-q&MgI%DMnklESyj}q{tCJ!*8G%`THANw2^e<05$3ALS-;9@C;9*@ z8JBz3fT&APg$coZR%!av?2MiMqqA8z@rWaocHRfw%Ldul#!N>z%{9JZ6%+no`gfQSrOagl(;xa*ZNh-^sakFPVyq``fJ9ru9x;ye$Qsd9W zcm|nnw}4zl?&kcFhCWoI`6W-Hr&kXk`f{|}jbyyYD*)y4@?q;xqD^agctDEDY?iDZ zue#goS;{hw@=!=+S}8;bPIHJ1h%O!m7b@Hw-yk{3_M^X}z^fYC6kv0Fr4}d=W6mBV zc_+szoH_v#)N&Zd zq2Y^b6yY*SLlNxbi4^}PULk$9K3hkeT7ibUUjL^yL-VA5c?OdLB9g)0*uF@TIqB}O zY0;WLRg&DPq}r!Y_A0s-wk!(^H?p6q#B|<|E@#>EhJQ}st~J>AF}ZKjvWc9N`z@R2Gn&y$ZCAI+*}d$89>2dsh~IxwfB4@>)FP4*J&F+=9#j#^ zOeGvjwn|v)F1iR(d$CgBVA8G&AV*iAi@NWRcVo3SXU?!DT5>elvgFzWh$&{rYEgJ0cI~#ItdSf$O2QFSX8pr zK(-`Row7pKGNFu-efySJ4vgf4$%xmr!Swe$tnO``NNPW8x-Q{Ih|MgX+c@#_PgVG} z=y;qnv)*n{Fiix|{z@aLJuz3#flLpK;qNRiB_n#x^B1w)tRI(Zg-(jTMbJ(%u}^-0zY4m3uVYav=u8mQd!{~cs7%{#gV%X%g4(3d7px-PL+P+`OWRQPX3*c zN}AF$KFW(=vL!M5WhXy0{sGw`zG+zgx4T(WR<6FG$n?R562W8@-Pn_J@)`RuP`J|l z2!>_9-nETQRoZ*pcRi*;RAL~D-_!&@)ACG%T?sXK+BAENOe(zX2p1*QrnYP2KN@4;_^bFW!B-BOm&@Y19gEpG$4fNx#f-}?rx_#JxPvPCnw(kF z=Hx6x^ctxT_ap}b3Pt3G^9yVe3pIci?iLa`r%Fg^?-kf>)8%tcVg_ND)uYgo1z%u3xs9cwg~mIymg1qmu!*>lxL{97 z)k?hv^}TK+Vc<`^uVj?F>v6>V_XQv8TS+#`%?*%eU8!gKBpRldC%vKFlbcQ?$blTM z#=_=`A4-2zszOC<#a$-c1Us2MT_J6*M@;w3KK0%=Juf+SW2*Z6vh$5CR{7D|hU+Q=52MR!=*Y!^Y*hJKvZ)ZT53T1W+-CbI;tV9wXiA4UB zuKKnli|d0EU?=opTS-%~&|~U;>LT8KQyU>`$WjJ4hW;7Gn#4c?6Qc>LEnJ%ZX(oU; zZt}ypM|o}=741l7l?xIj)}enA0fVI37Jk|7+pb9V5(^WF2XamFU9vx#AvM(&ZM2vYyPtcQW zNx%owS1em(A#QAT(lMdnud;5l7F7{I4%oGCFqbm~)*5k>U>3%H@48MV118NAG%Iw*nS;nzZcfZ0fCBdL@p;MQBDKGWe;Y1F{9L_@ z#P}vK2DC_sWm<159!rdVW-;aU!0)Gc3kcENd;KgC-M^5Xa!@BF-$Cpl2{})=B z{|`Ga>N~{p;%L;^HGtY@G)G4rSI$9T?vvawE!EtOy$v~V@0D+9nt@Cb9N3=S8hf=e zlb(QTprPt&KE;a{kxScIdWde^reRA7aNTIr3`Z6HIlrQ-QyI+Yhh?V8R52(E(h#E3 zl)b+*)_2$3|K0k{$w;s3bZV~LAy7l)sgC&lnNM}qHudLHmXr$!=>LwY=s&iu`max( zi`#(Ia6PG=XW8NPbnZ^__T@Q~jhW`=mfm5Y?5#V{v!B!VwuRz(XP7w&v729%noMl9 z4wuvwg%uoT5SlVpM(DvV^XPIBKIwh`4|%S0Z`?=tejOcN-BFUi&_*PQGm2dvJ7@3KT$0lH@N zE?^wj?6e{)iJKH(QAhhxI&HO~LE`5|vu3&L4^AIY275XIAb4mk?9L>%th2zyD-`a{ zjXm`>d-ePANgevFbD^A2Q&v`3TP#m}1b$tgx#^wAL@3^Y4j7;=DXf66HEcMxhSz9k zy37{(N;Z@mP@Bm&Zq#U4jR;wX+$msixi-^nIVg{ojNqV6!695;crnFAZ`tdJt*@V=lbtCw*Sg?OD13zurX(eZ9X7Ol2Cp-Z*T~m*@w;O#?p~!45;UOl-m=S76oU zO=yo`iM+69(^C;&8gyCd9e_#i*!+gWK6Kfr_%M**%lFiSTsf%PMVs%}u4>7W17-?j z*x|~Z8rZv1WZFG5@YAgLCElei__5)O)NW$HN0+bEA*6j45A988_ROG75S+~@kpH8trwHw`W5qw3h%0WF)K zWq;bEah>3EMvUQY0$|buzl0T~$m{tF(EI>dr@7|qz3l}|5|V==Ju5$_dkHJi_yNwx z&pMJ^lwRS@%Hn!kUs!Z_Dgu^%jZnbhJ)=U1xHYgV888@_6k(czS#4cc&&cB8f<_ZG zORBXY=qn?WcHzba5RSvT=+!GU`$gMYWK%*d&Z=F6CX&^YW6ohN_Y_JUXd7=&y`|cmXUWct7a9-lg`tJO44Q7cnJgw39j~un(lT^gRa0elxmO(RXhI_WV?*+!x=q zxdx8oXA!B5!I*UWRo%^VnGJEy(M%Cmsy9h2J}=0EL_23&;6Q!sKdG1YgGo!c8<#6^ zQ>~pwm!gIus(Ja1&&%S5I-2F#yo{J~`w4PDmBVjZPSDdpR1q_E$g;=-PussQ+_5}< zjd*h9!i!MAzhFXf1BUMGBFPyWCxvB)mUYvJh8nbka_==ay{6z(Rt`($WXXnOTMPJuM-Ear6(Whf7__ z{Hswi#2Ta{+%;WB9oCf^S}~CwB4-rZMJ9D+Q{US2!lL>zRXuI*Oy85~e&NcwfIQV_ z%sQ2H@oaL_SIVB=a{TDzY?ujt+Vvb{$cAs1;yQQz?4S&rHyuL*1Bq-4((gjWYDVK} zH2tuIA(QTfriRwGv`2T?$qHINf5_gMWBD;3^#l9*?VnT{vhQnE(}XjlmES_k&(c6D zJ;yujZeg|8fB0j^dio>>VKDRfh^>cuW6z!p_R$d?S93rDf_vOj%u69{JhnzJ?ziO zml})YqF$q@@UCCy&dbRXdb33B@%=m-@9RPFx47BSuHF&z)qGYxe5IXXv8&Vxk3c2r zhnA~Sn%6AWeu|uI6I;fSKWl*%lcsGEMuV5o<Pq2W_*mIu8iwQ-B4pG@9aBy^LxBxe-Sri7a6;x+ZmXe5Pg9>Y3gmTub zl63s`Ai;%!8N{r+o9{FS**u6Ir)-vtG;z4EWs%*GxTfxDl%xjNC=n3l-Z9!*e$V)n zavj)D(ozJ7I_b5Pk*~Jyr1`~e>4-1BBk9jU54?l5o6OSm4B8l(WOapb68DTQ`$|4S zo|OD_BO+ouZ&D7hwjRW0Yu-?LM_IMz8>&ZHpfy{iJZISfu23edBofjAe_Ush{8+J& z88CFQ7b=&l(C9RaT!-X$iIGcUD0xY$Gz$HyMyx+w(Ov`z5O_L^))fX(vcb8G;~`Iw zdKQLC%+;#KC~>(Gs2|eA#HOezE(Oj)zC@(+ilB+^$Oyk*omEnK9&4cL1x*U&2te=5 z`rMA(PO@mUvCcz%b85@6c|D|g?N*pZ*T(t^O?06WWnMoKejD@Yoxu(E!G@~6cyNOp z7c+6_B=(*HZdA!SM1?+TsGM{hA|W%#@0d&QYm1fGghYRo6lSD>cD-E|pz#mA?Dw!C zMHV%fSm+gxUk+HRF$Mg7Pk}s0%!HLu)mM}aLr1xA)?Ky(xDd28-2VHpSun~XS_4r_ zj5_@Kp0Ty_+L;^W-tuU4I5HM)S-vy0@=p?7^O)Y^z) z2NjE7;Z~a*W4euK)6{Ks6T8QIKzDl``K+elU45N06eA*%9afa>&v@PWIN-IGREk}W zDFyT0w^OOCW~A){Vjg zs-_-(z)P2%lcZ{jT5!O|9&8l%z>fVNi^G}rv#bq&;>F4)0Y^ung+K@Nq-BGrWrc)x=pP-p z%`V`c<2)DdOlTYZ>MTQgF4J!dqo!Vd`i`F?N{m!49o4V8eo^<55nnGVUL?vIL~xKV zP8@N|aomH>9(wCDfs|dMkd{12NXC_+JWMKEUBo>n)j_7@F2+$%pL6B9$?(U~ty%Uv z!K+$=Vb8okPwhZT5Gzr_rsM6G7)WkKY_;fEi3sUC;tkCq*9;5_&fRrVzg9xCX_tdo zsW`Dr%o~K<{Jk!+yQ;tEHtIGp!D#fkLS@%D38BV2gIk(^e&}%?Ee&EM>mL?O*EB6% z+v=1+lTzM#m$`>(h)&O&O+KkwywyTlP3sSPUJBFr(q9XU+*}l^c~*7H98l^SG26MT z3s+EIQU@LA8o?|YnbueD6kT$JVdE$Yw~73>N6Bp;u%MC{FHkL83}I*y4RTS>fxEid z+%IIgl>V19ms4>2FJX)QxE*htP5%1=Fa$j~Z|rggwChoa7}615V3tuY>r{7) z%I&epPKhByjjnA*ToGVzH}$CUYzdF3=@gieR7_=BNM)o%Y-0a(V4@(k$KhHP+Lxh^ zv*NT#hcTd~gYP}Z7S0)~>DaaFI;J?oYF_yY?k>yd@jzQrKxDAzo|zxu^?(?Lu2(2f zX72Q?$Xe-{^y)7L^Hz+no8Qk+b7~9wuwXvtnX?inv@En1IO0MNWCgW_c>V-9zl7If*~H&jcf4hz9@H}%S`S% z6q<@{^YSMkUbg@&Yg6Ht8x>wP3!^BnkWWVIaO*egZG|q;IG4S3EG8d#-!ru~gGlGtgp zzeOTqAUj3r1wD>mi5%NOKrkYs&lIXDB{=S`IJg8`e|sucoxKcYx9Gtykn*dK>5~DE zQSOqVembjba5HnxM9A35710sa3QXyFDZ!AbK;LH0P!3|++`Q&7Ff+bB+4E0wm~@zq zoJnT&uwYt1i++dqTEIcEuJQ5b{`m6?r<;XBrg(iB*v-UjojX`6La!A~J6|hV zwLOiSG|3R*)X(x^ev(-`D4d&AJv}ngcL1Oet^kPiNPq*|qX2%k^KEC@2;(rT|EIxo zA|eLv@E#VY*(N}9AP^h3%=pS11V+umq6h;Ho9a5rPQX(as`5#wqc0YIDVXScao~hi zz3Nk*z5B`K;bY)7`=R#R`kfP*EBaW}45@1H!c~ddWSz)oc&&aS7&>J{B9&_S4M=yFH%#Xao(&C{_c%dMfcwZPrtj9i@!pq2@13gu4kj&5E1J8v&M^m6 zh?-_Mr1@qA=r65V`s+p&E&J#EQWAB%{~MBGF1puS)MRmFx3@dCRpIw+z&Mv zUMwoa3M81=rLyR)AaY>VO+XSjKEj_`OQt!QKSu)n>bK68?aqd++9M4gw^wFi1r7w2 zxR%C4huyv0^?roQTG4!w?P1X`>eAeEH*>`2J%`Q3(Y&JNh=GOnBCj0kL)3oYmgYtUxLO zwKp35ml159>M9BETdc}Eo6{k7x;n820fPo?D{d5!Jy^8YQC8`GmT=Xr_s8Egg1O&h zLJYkK<{rIf^J5x5*3WBnuX@DQCUgS{_sD)|sndt!wy?B(7tuhe3}XI0FSp;#FOGl# z+qH-Vf%?$xzSBo%V&tM<&%J?%>N5WgivJ6MBli053mm51C!;TKQ{q0_Z!vB1^Nth8 z+KoTJ0`VL)`}1 z4?j=MMqIS~#!ve9wBP@Nvkrk8dqK1FtghvJZpJus4Ln)ouwe?^tfc&H%)y3o$II*9 zdXlZSu>BCsSdoXe%7+Uob{1G!jAlH{7U*&f!7N_|$sfqU1Nxif&EVT1X%(UO2&6j-)InxBbe;~bbk z&Sagnulh96K{;n~gbl%DIt1nc@yWQSW!7$*y;##ID;c z;ogX9E!&j?ciW16+q+x#$8Y_xW8)-O~k1pur&-MByD0) zJ;*t4k{EFt2`QcIn)=Ps%OB*6@gMm;B^m?Z&xZyR0hOX?j}SAMy%XLoX? zOtg|1S$=--QVp*1#w=D3XH`Xc7HB@YX(Q0z3fyc|?v*l-O+tU;yoT)(jgI``j=m(8 zei*`ut1dGPx61j2+LhKl&))Yv@rI%HpBFG>n#27Gn;GVY^dn@nM^z_>k13&Qb{s$R zWnJ2JHP%cn%+4~*=mJW~9{jq92mCZOx$6(tPi4~s-{ozg#Fbyu6Sx4p zMbAAzVrZBVR#P$gF&nVD`L{EkI8pdk(7eJdajk_Z5Adz8Cz1_miJvm$0h!L00%Sj6 z)Wdo?kh15HX00s2VQspaCBhh!-;sM<*rful_-9x3ADb6nv3Tkd#;Doc=_f7F5a(sKu9NGIxjJJHu3Xo8Rv+6^a>Zo-#=e4qeQeHYMfyQc6ndagzPzpGT@i_`$9|{!JW(tBzdMZQqp7^n~KX425r-b0?gJCKL)R~U$V%1 z{OakIrv7OE4^$xOU4op=xI0GuN}cJh=BHOR?bUsY1}g7$UKg1VaE+` z;3P!j(a%}p8EQ=?8cgK(H_)F%;ud)!aW~9iugCq%Ou61pV5bS~zB`%Xl!P6zl6Nzi z$|di{g2fFTbb(1fMWkr^p$TLB65|1QHi{^OGKP4qE?G&rVtFUmgPm;V61u0N?1l z?Wi|{S@vra;Bu||NUSs^WUItsv_`475j_f2(K+`eQZAf^oEp{T4*ui&U0aW%Le|MD z$>8JakkXj(UOUYlt;xra2q7LCcRqKuJ{3}IEqoZ+okU(=FvppjSY5Q4KSq3dhpm zt$o0JZ^rO}hPlz?$%ZrGux#=4I2C1YZ0qBz{0fk;GAq-9z`Y4vCp%dsN-43Hk@6Wk zw-j?QFFYSwLW$A`=#BTB+cLtY)uWY^c5=i#f4Qfm;C6kgal5$l8_7DG-{vx0U^yIRVA8J@&dzRYJv;H}{?|c{lGr*i#5=D4nZA1&Z>=n2yNh>li;?`(!!79ae zR1oiKU1TH7Y0g&Y{gf{SLm8j*Mggsa2% zGYK;9kBi>0yQw-^Zjz(XxuN6jwa0FkLr!87em#ER9Y5rjI!D$pK7;y=$kv|*{od*o z@Au-YoY@i)Hy@wg+l)X{t0P8a%0y&N8jD<)v}KscjY-Z~ZcG?Ls{%(dN;H$dbysKuf}E55SM1>i2&Eg>%01h0Efqwhi@Gy{<*(H;-lmn zH=^l@keW4R|Hb*r27qhE?027+gJm_RykaW9&V0wsh&AI*X;rQ{BJOj;deDzw=My9_ z2f|HdTm}na3#{SoC)vyRMIjjsIW$O%)sN=|Old8YL?G2Z92s{D4Fk%I<|eZYwC;7A z#z2ftHGK?pF+UO>J%yDg6SI^h1Ox8s-jUs`NxCF)WBH}_3p!6dRkYxNf#QVS1XO{d z#S(*Cu4k*u8M+S_mS9vfsg1>rRRBBT(x6QBNzYz3u@MKf|9?_3T|6H} z^axTpYB5XSM3ys{yKhZ_5Z<+Li|I#6IzB?=&U$~1SAO4IrER(oHJDSM-?VxtTlv`x z(O0{zL}sES?vIc?tHno}?Ru0Nd~e7%t*w33if$NCY^>N z4xyu+c>vtd4j!n4fY6P4L9yqW8r9IxU8jm-UQ>>-PGqVcl{BsXWfR&5xS49afnP3CR_LOzj;=uUG z8qwhOE}Le3Nt+qyIF!+?-3=ecc+IcVGI4~G{(2A87`Mxtcj<~LSFW2{r*t8sbhqRC z1*a_{*(z7jV{hQ5Np4_4lhZ!`QV2%*A3Jv~?SxHo^-HqB49{U`mMuc_BoK}-Ub&aFEdnf!OiGOuN)^A!w5 zY9DBwg0I+(msx~~Fgh+B+8cBAYE0>}y@O;*>bS+npNaH=tZfn-2U>R`3NPI#gv;TIm{WVcv#}}kI=b3Gcke-9@<4$XAU2bi~pxJ(f@MY3e9Kg(9$(G$_9Cqu!PkH z95>ZlbqlZ*_ZA5##qyEvRJ)#R#vixeHRatyd5OR!JUQ6kx&DAN!)lb+MVVxhFDo{ND)1Ewreb(w8y#&`Hi<_1=@}}RZjw1{x zg#n@UY_7Yprq~QS;fZP;u4V=L_+AW0z8@#t&IX#FdqM+%*vR3G@kLFpjf7!ltGFoY zO>1q^{GaG%?S%1r=XZ2{S%+Em1m8-Of13E&FK7?dL;lPSxnmODG7al0!m1I^bQl@N z2PqNx0na3FWq4G!hN+~aCHRO{nP%~+?jrTC>wY)52W;V~BM#S46p#I&{?^ocWyWMg z;Zqq=|Z ztQDBf{$)T?h0~rFffxxLn)6@C7L6`Ie;t|_<-2!#rv1^I#bIah)+gEA+L0Td*!r@H z1-*rA4q=sJSsw4C%*t|{Gu%WcjkG0&`BmX;X~v!Wrj~)} zh+XIXX>tqoZtXZrifYlt_`@@q^c9Y4q^Q)pNXs~iPjFq3f0GN1h&orNqvW7Q@wVXl zwYBk>iqZ&!VFL(^#(x@q-gM{tN#L+5{>8%X!K1xTC(ghADxw~tj+GJaSXQ&d%v9g~F{n7rUN=aXpO4Rp%i1y?laVgy zeY~Wwb2j+;?AG!P;fyV(U3Dwor{7mo`d82M>{`f>pr82VEkh$~Gn}L6a2FcYbr2e} zWKdg*h++NtSA!22PiEJPgD6rEDL~6wtrq0bJ5az4hzcI9=P=w?t%1VeEvh|^D;&OV zzp>eb9WJ076BT9zbn%QD(!FDZ&e1Z;1a_!F?6UVB`uxUr<}U4mGimZUB=X`v0U+GN z)Fe5dz%Q+3z3RJ;jnC-P4Nt_JWrLlAz2~}7O4BEZAdlsI`19 zoOA}XaV@9;v_o%RUYZWE)toT#Xx)`Y6eIrJKngDAm=8H83H# zZn--T4{yWozi#~Lx?B|5d(H>a#Ht*GzFiYj@vU4_D4p9VzegXfHy=D)vUvpluq!k7 zeBWc(nQ>bc_6n=u5m+@J^V5kyYqJP}gY(?v(+oHCXUnJZl5G6#gI}Cpj+hR)(sCx) z+Lq+T{EgDp&GD+6KCS)%vAm79`9j)d*W1k!SA^T?7Ozr@wYw<29aR?NCAmM)Ea>Kh zq5DX?e3p<93rnz0H}$L02SYWTMe!=ZRj-LYzfe9xK2JbHrk>v*w&QLH0qURNZ{pFE zBnEaWn#&aiavP=;9u3JkLLe)O1VuIduf4KfSHd1d+bnxQRFLk*hGR7bL(IdR@t<2lXjhVTpgoK=6di_K( zEe03(=N#e-?A^OuTwJw9fEhSPFTklTCtvaRjP#utsQjnz0h7^oGv?lmO78I6qmLo;-6%n{tQ0hb$i5ip{=Py-F_yUx0v63&%C_xkxCHq`7Xup#Qt1b zBYD?KrY!CyrBH?dQYJ&t z?Q!54Z`hw*py-1!^QU>VAUC1|s~3BxVB2`j_2H6v_n8li``_%tWcBYS>Ux#U=}hQE z!ghSWJO8;2C=sNmdM2xT!^*?~D|*olOPovlzU!tVpah<(Bp}sIGMq;Zrep7vsVB&- zC5a9IO5^Ir-*R@aBsy_Zne3WxtS zSC&}*Yvz+S`7_}t)j4I9c=S?o@z?v;uoCP?@^@UwvHS1BAZWfUt~KNlT7vR!${$&0 z{$OwP)=M_Zsp757nTwnP2d+B`1y4d7AS}rAG8G=JMy$ z%&ChZ=9gD<7;@L~P9OiOYt45R|4h)$yAZZKO4q*p{VkF@gml3qEJN;7G zE}yJn`q-1uxNYX|u=zi1HpZ&-Up32?pbuotfM;6B^3=S4;L4^d~*V zQM7e)Z_>BRD;H+U(tot|F}cD(*>Su1gP@T>?oO=CKmh!zgu)Pc>evPB9M%0-g(cp} zb5}DE$apY>*Jo6_H@DU2Eb!aKR3{I~!Jb{EXJqtvjbD)%#}M2#H$eyWZ0mGp#Y5w3 z`NW#>jyd`*`fd5&&#yy9wv0NP5z=b|CyQSCpF_nt1rPU110SAR+dO-X^tzP9q~n{u zb_RauwEOH-e5m2^(nVwAJ{Rjw8n&!8EAbJgj}=ZN)d4eqE6+i$L^n9qjeqSD?fm#}QCHD*WL}KSzQpb#vuY6-9)CrhdOOX`d(+y<{=3 zIlU9KHEchsGv%{i0D6U}nD;me+?Wl0$!9_(M-{ z`2vk#diSL?&k`=+@xLnu0Gp!kj}??K;zM@tHNn|&GQXt zM^Rd$ z4VuRH8irOxM?OyCo*viO&!2Ol=L3+wN)VX_UO7qOmcucR%st} z4+FBqQ}PWNqg$PUOM@d4V+N!-x8F-!vtAUTeN{S+O2&O11iUFtvKjdwDxExgS^xTx zQJ$w;kh6AdXk?iAY)Nsj;giyqE~8G3b26|Gn66b^5H%?jt`?oF7R*alRZ$b%=5Usc zEJgiMRT^so^o5bvK#`1>`5)B&VVof(YA7E~5aGh;^#W^{GA`}eUVsYxPuCbuTOl}M zpqT6NUGcyGSlxK$GM763$vxq&zS^1XTVv}!SH&&0&}mR!uUrtuo*gK=lv8~`6XPwM z5f%{fTkTy}$Qe92wG{8?%drd#y@aPMvJxK)(5?k9a!3*37P`5Plu%i`j5l1FMjaIs zYm->hS!=)V?&N7^k7@aQ+O~V#)RFgj7Sw7RjN;{0J8D0r@i@uUzLX?DbcJ0SiL#5HL@C<*^5VhXYBew<%F$@;oPqLB45$nE5-hXDVzyu^XU@_)iM1LiZuh_3hwlK45XjQRc{vC7%u*j5} zl`bydRM%ek6oGPlUhXjeMx;?oL-OCQp%-HozSThl5ejkv}4 z!_tL@TM=qK5V>9#wKcmW0Fo<+WK-2;dD%Ax)P|5J8E$MA8N4MzLO~M>-76%uLiQ{b zsa}Et1CY(dYfrady-M^FbmGB`5q3Ny*+Ub6^%&rUI zH|2W^N0MLm9!D6YmFdm+Di6!HLR%%<#F;W($(cW?o6^YcJkXFua5@kb_qc^8&m{-c zCp&TRCEl6T3inaks|>+7dQQc_jXEd6frb&M#!FhlozM0$z}+(Sg5D-oKh-u}XUg

$Nf|E_>X+vFuBd^*P6lt`?4B2MmcLsP(2HFT5;~=7ZGekb&yCr4#Y2zApRq) zK!XqGG_eYUsx6REQmEG^M|RhoTr5rk7WNzga=QZ857KbJ?Am_CspmUexo_64ia+P5 z(=ax3qNCy5-auM1x-HW|HC%TO02N)CIB)NLFZuTc$EBFufzG^AJ`soW_h@eo%ct$r zU+1^%xhQ`%q9mDZem3%e*bVBE1{YPDP#t zuB7sud-1&{*nVmVs+dl;U8`1}%im}swS}iO*#aI*9MQz&d}nGSP&3GD0x^t?gR2n* zIJQ0+#hi(}-_j5zD|aCPVWK@B>38;v>a9=H6=8R3D*E$lN1pU`7c*nl(-WIJ0^md&PX_ zL-eG7O_P_fO9F5yvQEizYJ4XR8G$Y02FeFl>I^9^riJIWoR|90(L^2l3VPsz z+?)n`Hnv9op5In}8+JIM4YK+0mUmcu(w}w3A_f{<0Xaj987hel3?m0s)wE~J3jnlo zn{0()mDz?N+ifW%t&V?4NJxf_;iv)s`fX?y>%|RzT8au8L9|aK?l8kuDdyz6eN;Zu zmB`IbfIGsn?8`zyX_E}INZ)OR%ZS^in&Uga;3|KStQJRFa)%BrwyzgVMrvm5$vaxI z#1@PD)xIv)2`-Z2G)EccR}O3hOeFot|2}7t;Ikd76IiX2U(B89F_Ee3>7T#CWw24o zni~*a&32@lH@)@>m{*6(Bu6lwK`CCP6jOUjBIz?x=4B9=-y^XaCICc;M#A+8b|Ssd zd%CPB6wK6kCTNfz%;b^GN_y0C@^B`rTS^W2uEnauJ%_o8J~Fx`jiAjIMY;>ypi87Bq6prX zNsGW3v-k%Yi7R4Xm@y%|qS&_7AL#Fs>LPK^g83je&s~DN;>Tw{M9aV9 z(qdSJcJPAu9&3A=iIt~UKug6Qs5AuR0T3aHO>IcVhHhc?Bh#4iwF*&;%5!NC#hMe( zz)vHIRRF1L@(fJm6EXQ3yv;>a|f7Ism{6ZJ?Yx#l{8H9%#{iAltDVAl2uN z0srkjG*=vRAzb%NtTu6l@R2eCpn)C<(BzqVvqb=_4$O5Tf?`o!B{cU$c!7XyMIk&M}s=mGl4pmQ1MrBl{&a1(`gI;^jrF}gHy@8Ls>Wi97 zw(qB+k=xb7zuH!|rM#BX&Ze7N(A5=d2W{`4TsfD3OeuXg#A=Tz&YwG5P5DagpUdb0 zIlD^NjZC`QGyiI;ud8cH@j?v_R(!wR(%h1uhs>N-eOvHSK}?4!AV0&I6^I4`UMA0Z zpMF8K_s{6L1OQGc6arK_KJ(#%p9^$xGO-TK5lob z@z|6BFBW>rLDORjX#v`=)U4u92wLaDqd%=BdKsC%uKvGdGyj4G|Ns400)nGrtG%fg zB1Q$81h(U==5e`A+=@##BG{|P@p&EgVO8zN#j(rLpBvlB21~KqZm`Sg+FtKrui$OW zYEOXdsEpf~d?9?{Eutfvy=s^XmwxR`u<{2_+dx4s10%%?_>%h6!R_zR0iOMD9Q zBsNk7vYU|V8RcCfH8nmmK0Y$iYtIu=ZL4iX~_bQ^4!NJN}Iskra4kz%e4gHQ=Vwgi`+Iya1uxJt_L@+wYOZaGxGg%+lHFA6B z+?Kg9RY42b3kgC&L~8x()TRP8X=u(f4Pl-p#5L;I(NdbTKbI~vKD%L*eA2=PD>~?; zBlxWnx|5kg3M)8#whL*asj?Tgzo1=4JQpc?hHA|KA_R*&PI%*#c zm(tEm1mKyC*)RwX_%GbOXH=7G+a|2rf^=!pL8bTJL)hvD1f&x>L_oSommp1%UX?0c z#1MMuy$A^)U3xDGO`0au03mzsJ!{si`SCp8_pbTYd^7LxzZ2ZYR|zMWL4U!g`73tC~YbxkY@Tjm{~A_oqPwCT!&alE_8y zD1EjWZ>zPo{D;^R@f?;axQ7Gv#XW--A)ofI9%n@f$l-N@!23FttFXvJs%P+^snWEy zj>fXl>2(dc^y$XzY4zWXv%Bpo>%WsY1im*1^sdq!W}8fzcs0M09wQaG=;RBn2|JWM z|M~?8nx?m|%?)KsH9BAIRm16p&HJL=A5`O-VnFQP5@P-s&wK9d@1G}*FTgUO^sXOp zW*bpGiie-~mnN7AqGR8n)OAb}j%nD#vXqr7fQyZtoVJy%sq4cOO+Ni$IIdzd2q+eV zg)TL~X+#-ech){71s1y({um>BJMs;t7-nUb9S4KBb1Tu3>1WP3PLxm&R%$Htne!%Wcr?t2meA{LzJK@cB z&^y)N^r1+pxZ}uYP8ZI?are>2-A6!2#v*r~@XvG(IP8EUPj=E$c5+(w*mXB~Pf?G_ zjv{TFEp1mrCi1D49z#TmcN*V4Al*iG{U7RFUiimH|8`)0{pTuqJ1z?!E zP3A>W5u?;j_G|H9e_w+YCU`3Mi9HDR_V&KRd!sopt54-pMI4PS61S|0=DXc=dZN1E z;nsQks;I6@lwI@Kd0eF%Nl4}b!@@AXL?}vbSefg$piegqI4>=f*P7cqBnl-^mm4!Dzre)}a__0sXfE;H+Znar+_ zOb({5y7Z--vee?7H_Ju_Tl%goXKDslpYtV+@a{Vg zdQGIu>Nk5r*^LpHCMqM;VL#_+sh()p=^U4oZo^fE7f&8ZUbIn;6!hBphfiJ47_?)IyaJxdwvjR#=y zu%wG9vzSs=pXqY$@*mRz2g1cI=@*mHs=A%Mtp&4ck5+GUC$2vIbqhmuqTLIYLcx$H z+X+DNTE)5nk~57GX5lc3C|!sO$Q={@UZ!9~BYPw?9am82x3QYoDG98`ELOgw1SOfH zTb*;s@KjhUVdugD1orzYl21Z>Wcp&`k6SYl6x573Lus5iuRSk0<)5}X?8*eqrQP# zEhh?sr5Z|K5CLr7&Z)DtVZ8qtI62pk!}bF(eE8J^hf6tvhKZJ#+*nc=rH34omql2u zW}>X?$!lRY=}Xz~RZAMY3Exfbr>HLW19pX`3&AXUn>N zo!7T^&RP}xbb#Xa!{FHeQz7gig>T8RZ=8DyxCCMw5`{y|^mDX3Cd6-Lf4hi`ThVn@ zSgfGmdTN(~0S}x8Bvy%?Ei5hF&+={PDhEfA)Np6xZGD#D``-FA4vNVy$XhC|*{zM}lDkFa?zYc(l76X=PHe0dlQQcl5eG1io@{uhxV~vYLl!DejV$S zj<@Ph#M&@x7wAlWo_OgOb8GhzS7ERiUt1os;m$#EbEG)S3cLBZxWJ;bs~el{{9njo zXfjdh_#-|8!Hp}RaexIp?8c4iqgdo}jJkh}9z|x@%ZR=>O4yCM!Iacv*f$!OGgM*( zPwMk=418sauv+jnLV&4YAz3i@?vEJaqIA)x^*ta2aiXqAyB?>g5Qm5=;^3ly#hHlc zC73eVI}80N8i_+=*`meZ63oa=hLaDUY3so3p%W`CIJ54Z_8TD>_sbEVg)y7DIv9O# zy{@@C!^#rxLOWpe9dJGQpv$o}PDN)Gyt2&7|3o64_M&D@VDb#LO5Pc7UU2IunWipx zGX9W=Oz>&1DyB)&2h(98y^6z&>^e;7>Am&p`Fwg4wT67?C(KXBth&B6FIj{RleT$5 zxt&@Xz%2fGg|@cB!o}sf@m!RYsaPq1=FP-5*(bUx8VKRwBR3k{>4~%!b5tt}ai9kB zkIDDlXS4H!fOwFmK7W#N!8GPiPKEcN$N3C+&nTj2I{8;xb-VC~J~riTo88=LGnNe;?T zu*^m$x*AtE!=H7ww${6B zQ}U84S)LEVc^q6a0s_8SA~UzFpB@$FyR0q#QSnfdG=JfLB}E|HQOzWTrFu!+**@2$ zVMK3bRjDashs6yeN8qs|lAoTb3`!4Lm^$Z_Y*wFgEpl2mN~iV#W1e%#4^i}T<)J)l zYVR$8GIK8JLEm;uA2dGSLx(aFwdNDoJlL$RPifkqp3tkq9M#4$tQAM%NI{T%yYyp#M5s=?8QrpifzX{wEs;B6&DMkJMH$uT|&dhTrV zwzQnlq=Dhmt(JTPMCKr*(CoTthB#sxuqN;;vx9RyDW%<}zWA!8TZZ zhJFXGPuZt+VP)`$XH|{mfnG;<5@I=YMBmCgeNyMn6AF|U3O2)Q#)G_d&|?g+BQzVU zDh+p0Uoa%kn^QECRuO{8G#>Gut}#GqHz>2X*o*YZF`%&y(M2b@I^{8bYrBfb#b%v{ znN(hVPf(M6Cc^?ih?Qug`9hpw2EV!G-LJbP+Wvfc)ct2Bd0Lmk+KlI4i+n#J$Mg#U z1+xNB5q3oqpWrmqqD@KnI^RQc)W?rilI2yn37QqN+V1oViShkDuU0hW?XB3IYNPiv zsn~R~GUjz>FB-7UlfXExQ}N>AI8NpUX4L!ry0sa(SJxef&bsJoxBR`CxXf!E zgHwo9!Qv;xr+${Aw|$Rc{*j4_SFV$N_B`?BCuMZrQCz82fNU;3EDdq;)_|-sn1K+D z2$sN;3a}k!IysPAn-Uaqr_^p!qe|u$ zhXxr_BC6en3DR_)8Q)HsjAst_UXsh$lC52Ge5psts!_~0V=nqvwSPxh&$=y_oNxtb zxlE`hF+i_FSWY$JTy~BCWa{W^5$e{hLW2I9sPbD@(I2m@hw)C)-<(1cbIKPRTiTKq z++*wvW~10)rh)Sg$-{wyyUeyUlnvvah4-)zSORV=uv%B!viz?j5Av@_iH~E)=%AR zyxUWFPtkYA>UJZzJST&hmG)-xL1WaynDoK-IwrTL~ zUfhZrr5ic+Hg2W>6PvAG&BfJ}te>>9ET>~(>*z`~jKiu${n^@vJd*j`9O>9(PbfUSa{3EI-=C)it4i9&Fq5^y8vgM@P8_K#YdY+eV zvz}0arSsKeQTnG{46v=_KX98bU-Ib%_Nf$*rC^G&=XgCXk~mx;Kvez@ddOZ-duj=e zV4;`O``DeJGb`8OfL@qEV%6x}GB37cK#$7Y*j8F9-I|P{ZrPnRWy>RBrMubbCqo0? zfng=KW2(!WQ-`2gX8Hlys?q&^(s${EB9ZUXxpfb}AMW2XOxb98O({5VnA=9JLzcOj zWos1$e?N4%f15tS?g?dhx1HcP%Fb)bj>1IHC{oxv;bF?(*T5pCm9dp(bTQ@1(|gmU zmK|=jzU8J37D`j)Mn12M^Rxzya=$L7<#;rXb#<+!7c>?`gsm0)eXVh<=l6nhe;2JM zm=C7X(-@{+P2^-&+j_TX{64h&)XV z<*E<6}VEK3k_Ali3A)wpK$auj^d3dJl&(tlN=;Ba+7b zYh{?yDU)phat%FVVvn|UUBjwq<+zH9_UKI5)39zYwKaGcU@K7ERp0au%ui$?xMQI= zU;>z~p1rW>*V(gZN={0BAYEDIJyzEj9iWgsQV=bNq;t)F{SK~E-hhnq>pJMr{v}^v zF)hB`5c0xR*_oDN1C|hf!!k;^U3eN9~#C|2?+@Hwy4=C&;WQ;(0@*@yG-alm?)$xp6ZAItVV_cvLorh{uIOCndKe>IixdzjfU-{%9^zQoRv?B(XxB|ho=~b(*$G%1cKDgtn!Tdc z7&$7dKc7BXHdOG4za=xfV>atbI;`6zY^@DY4-y%JPK^kF11n&!%8qC5d2`Z)7Nf+P zC4$uPk5{`tS`n1@P;V4sWZlDPV1MY-?AbnOo5?X(CLV-fMVg8CJ^X^69^;EvwJKIL zi-HxXyfS2Xheg(<6kFx){~V1JH1PL?=ozHuy@GXXW`4A!T(H&8$pSFU?15U4hD}^) zOr>pZ(f+0$9egaXR9hk&A!}ds%I0CqZ-JmH$Oy4zcBMd%@%m*vaMu7z(D1KBKH&O1 z*qzXZMJ7V2hz{!oH-l|){E3*rlhvr1P}ZsT5Dfd@*Qj^TEIZ;{7o6k#k(90%$(HbNP9jMc@nirna|+$QGwouNX#fwy_UJ(yCA`vlyK+7iMj()zK;NG2|RD_wEQ zJ6G(Nnls;TZH2^Nu$C1IGnpc7MmGS5FdUrbNLMNqQEArj(3U@giY!-300J6wioUyi7#DO=yA~KU?z`BScZoJiLW0W%9D&j zSx>K32j#d^<{TmTiz?)D+}WZ_KW+wzR^zp* zbM^Jaw5%L97vRf1Oen654Xhj^m&%RMIbV}m6@ zkAY6ZYGuss*41g&z1)k&_|vh2cy1ohP?^)bsl;{d+Crmty8*)^A7A5giZUjpkl)V6 zsVN@(6PXI4m4gIdQQn$4VS>5Isc%WqkU*$ak2hcvf)s5DbabCrSum};bRbdY08{n9 z21Wk*Ek-z%;@XR|W$Fv>ES;iGSk#&tm-RXI@pFh^e3~f5#rT z(b5pS7Rv~4=G@QD#6l`yafgsbo$q#BP>L6dxzUudh&ZHPrM0fh)W(->VcDq%gSn21 z#;Li#e|P>>$-);T@4b=b__8+A+_ap0v{D^x`ARRB(P6M+lSM%&1nDm#Sn1)5`_)-- z8vd`xCI5a@@c$##?SK3ys5!ZxP6aTh?gcR3i5p5GgLHZnO{J)ae%!B3F8p?FO-d}xkB0-{{t8gv2 z17;&$ze*&E1aV|^(FV&nuXA&2Iqhuhf(54@wl&peY{^08DdWrcx|m=o?n;gecC*J> z!-{b;+E-n@3F=&c&fW|)O#QUkQBgQB$!(`$$co)m?tz}MmhWTVa%0YfbS3Sj<_b_t z$bygBP}uxYnp3C*EOq_&Bzs)7zYwS;a+M<{LN zHw?@y>gT$U(H5kZVS(9q(UP6ub*UJ>J`qVVmp$b*_XYxZQ8T&z zDAY)^-kqs5b;}br!7y$rrwa)7(8awna^+oA^aYKH4**h3Ob{|?|!LZK^oz{U7t*` z%=eX~JR96~m-u8_EWCCJ7QO}Q|GKy&GO)eV$=2XEz7IFMyP`wARmpL$vP{R5`z$Ov zm|Oq7VgT*CY=r0-(@R?qTXJx)^~@*Oh{A{_+3o7%uLWXS8pRJZXxcw_Z5-`=2Mjl#0<5SjTdiz4qI=i+3po1!Zewdw( zmmCr@E0~cxb3S})aLautvc4gIvnIyg`5M0E?Kv~)=B63G(#6QX-1uwf(v;Oc^0!(_ z+c`VtXS@0v6Ek9auKmK!y{nzD#hSKruD^yyhxc`4YMca0os@VlQ+%N=DRG9{1H38w zDYa1E&`6e&UJc>#I72NzEenYh_Gfngb{z8G6{KV%YHVFE3c|TL@3$57f*Beo+6}#U zU5K6-zozkEgG{P6)1bwY=Rr2G`ikkhw^5H7a&zsj0(@8$h_f$kS4VCTh=brS z;P=cdv6B(hEf<=qyrXD8v7^tfPp2z#*e89Kd0Y_Qr6a*kBu8Uhana27HADps14y$- ztN>IA<2J=3?rMm7?mmeGdz?MY_HVcNZaOBx2Fud?b(qTa7}7^_d$L-4pYJ#53tt9fF3-?M~_=7FxJ^XTmWQgnd=%2Ie z$Z*BhNxTXQ0AYl>_1X-vZ%k(VQHl!<&H~Id{Da$xgQU8cfU@dO?KV_>6I+#$2oZSC z?3Yj~#_K21C?uh=;3mipPyeSKK;RJ|@`SRcZdn-Jb*^W=cPL~QJF|YNcU3!BOU*SM zwg1^OVMGmMruJ3`Fi_#b#%+8_1sC?sYI?Z*;HigX!=Maqd5H%<*z_8$;5x z%UTH0<^i!fxAK}htLU)$0GSNqV}AS@6ZxfrSyZic&EUw2(k!djioT+ShW^=y{=vNc zw7?d)pt_R8I7r}2IU&C1gWIlBLU}N5&RXLEI7eBF*t#DU0QvhGyXFPk{~2=i|GxiS z|9{^i|HVtL$6}%MF19qdUv1b`Mz@S?fheNPS2wm_9TiZeq%oZd*>Vd{f6Beg>z|&u zyHnMHdz~N1f00Qy-rHcM@$FUG(nndVWZL@%U&hFQXmnE@VTwV|Y#p+;%(G$7XlQ9~ zPxyPYWY_OJ$u^AhX@|2Q_{QRT0e7;O8Zyk5EWzURiPjnqJ2YL4iZp28mZ@(pt0S!Q zQs21b{2)<=dFn5mp`ne=2RE>4u?n6+4r?~NLzv`9FmtO2H<5=#R^6IavE9}fo6iT3 zFt-B%Nj%-KQ(KD4-t@%%j-03C$DaU4vi|abQQKP=9^0$cuE(!u>;_lYkmJ*cWa5r@zxYo0za#SMzdez( zz3zFbN zaMd7xE^;z<`%qhv#9yAb-eQ;G)kq&$#b2rZ#tZE;l|(o8x+5iuhnk2k=MRfhbZ_oK%x=dG5bPUR2ZiC{ ze5tJlso=i|2Y7uW!Yn=rFo9(_kr52L2a7s9?OAYTGo7p#q?jWFY14EEh-U(ME~nSS zH8>WtK=tA#!L*Fd>CLGzGS<6w>3BkGy3k`IA3v{}&@uAIzh+G&f55d*%{IG)JCgU1 zf3%AY1vBB|-v&z!F$vHxVD`Bvxnqm;WQ#s+H%!$mJ;`oXAIh*O4vZU?B3Aix+1nfJo7K$-v581=V|$qaodQ!4{BYGpeXPs34)%PgS#RjBCOecZs6yUX`XX~;9)<}xV39gSckNt+414WHqxlg(lA;ytlwxC z2o@I-An|(19c#|~Kgq+`;HZ2y49P(q2YD(I<8qCJ7s+(gjy*9#LCmS)uPn6kkM>f^8448ZE93!Zc>m0sHr-e$li2zt<+qXUDrq^f^6zW!WYFbk`i>GiY^D|@H8t#XLkTeXdsj2d#qfTJ ze`3o{kb^QYDItVXlfzMFFS;MDE4qb{KArlEnV#HA$jpNa3GDdq>T-~f$``$Cuc&u_ z9%*O=X!~Yo!q<1beR_H6+oemjc>PPdkTqgGfqhpU=keg*T?I(zCjdC#g$ZlrcuDsz6IosYTfw{aNP0x3o=lr!OpRQ>sxB008tH;FfS=8xS?LZYwXXX#sV_AwRQ_aUWXU}vmmod*4mXA~r?HK;UgcY>ksy66FkrZ)3kY)? z4jfQtaGClQmxai_JRh_BoSpu4>-RbQ|L7WOpS^A@JR-wURj1czmE8ASxS_7}J%Ia< zAO(ay3Qh)kI5Z>IY3MPbV&8gEw|Y5LQt1Pa3&K9rOr3+c*{FYqp?elWXtUf`$ zh1~tu1dbc)8Bbqm*?k0@gQz(^w(-WGig-$?Dpc-1V$3$u)#A@jl+AXp)7S8f>b3VW zk5g>tdf3Up6kWcCk9HGBYe3yDQi2CGi4 zNN|i`mwk0`qj_Ipkgmy496)w>@9%)9OjR zbXOxphH1Gec?jpFb};8v9+497Y(+3DC1i~KiFe9!g)c9;cXHRKJ{y17IwNLux~Mn( zlKbF|Zsfjc>LPTxk8M<~&elq48Z?t37^K)A31G7omvkRdvOV55RD8-z0;cK8X*IIb zZ?tRSmWf)Um7vGP7&*NR0Zv+H)6_1RvEp!`I~R|wEW1m`k%~Hyr{_KFA)st3p<`2#G(d3O zq7nV3Eyoj&fZmEhuqqjr-=2cAwD>%B6Dn0rDW`Z--vC|&4E3Ct6I((R`1%QU1-O6& z4l*=r6d7$emc$NF2!xwKZR`o~h`>?jbo-k0>XTZcNU8AjuGf|?T3lApv|Mq*z}4#N ztAD%-fWlG+ON~R>G~dZuJw-fv3pHwOh|q8r?tPIVCMQ?4QRV}GG_ z8MnQ&`NY8H&bTxIk;5IoJTt+C1D}b=iZ`qE);7!nQy3g#&d=JPN`V9Hou%gvtpm0bnI)P=bpa7-o-r=LWhzx~fqwsIGD;WXt; z0gO8-bnN)H%ASt;ixGLE`iU^}P;KpNRc*?}uuMoOhZ_u{Cq; zsL3V4f)2)G*$kXJaeghwiI(#YDLNL}tWVSh`GpRCiG9sp*j;%h zsdZ4C!SCot+reoX{1i8q)Xsx#54Z`7M>UVKU|)7Jx|(L1XIq`64=19 zQU65rwd2e$D-oe8hbcM1N?RrRK-o*Hw793m8V^2y(!6XUzhVu3y~^15Gf7G13TUwG>v2II z7^Pub+>LyXvWdKx-#$~C$}422Z&u|Y7RG|YLcFQrd3p8va=p_d=y*op471DEa{QQ1 z$9T`_{&m|V!!)NXl6&ThKz?Y#NL}tKd*qN$J5u5wOtOv&#SXMz{IujzMXbPV6Kz&X zVd83>fKVDx=bs-*=(F=7iVJWip$&VUkcHL`fhMCILIH}kK9LSNE9O{rSz zrm!nN@njB74wZt@8S_QQwKFAP%<)XZCq6Hq;m^Sy;eA8o`qaj((NQWq!@iC{H5hUGwyaX`ropYNg zvKTMj-+N47eExC+{wtR$R5IkV0a?&mSQM8sA?YHVAoFK|1)H_^K$u`lO>I+?3^^4Y zd!EMb1X**zVCrB~l4e$T-9BBp;#ENpchPEst;+Q14gu0_J}R6{V^9QDLicm0ZYqj0 z^lG$M!(1JXmik4zr~4hil~%iRY)re@T>$01WO~db+>a%o&|vB{!8OO;z-5loE#q8C zlND~(cqDXQ*{wnS$@QpS(?|;N1Q5ge^l_SX5m$hchD1U~ZQG=$c>U$2>;CVGS5Ecq z^(~2o%?+)M9}DwWV7X*TE{+;We?4cl(f)7;IVkfIA@kk$*%0q&HA{QB;sAcGy-sdO z>R8y$dp^|R9nzRGq1!=n+Tov4##$YCRK`nLM)RI((`yxnI599s_G7%}_U}DRk|0Yc z)M1L~wcFLGq|)H(P@D!pI&%ezw6e)GXY$_WL1|qT>B_+EKY<5oSM3hz;SQL#0spHI z6SU|_VXN-peXn@ZLP^I8fIzE$i8{1|Jf4`q7sq;3y{fc}g38l#d-S$so&{h299TzE z5o|JHWBIUvoaZ2g5y|sV1!^<{dK2QPFAj(x5ouen{UX{=kkP~8I@!`V6zWFuu=Kt> z`&~SX8C&kBVU8N^TW^7Yq9d>n8fQxugZq@k+cSkWD9zN!5T`ItcvvcGdoN15n2Uu& zPk@pQ!Wj(aW-zT(bA~9cBs%Jkis0V=e*9LUT@I5N)~5`-BT=T<+U=ra$J^7{bF);0 zjg>FM0s*T$_#Dev-^47Vep6>SMet$g&|8q|x;|rbx!76(fSz-riH13YW#2Z zLJFhG7__~VdF$Lu8ZxqIIJpVh!JCjPPfL4i#Il{i3=exBQ)|mKSfjLh*GX2o9eJnl z01vtPnv$+1XpGQHXE8zAHfS}l`3VABZ_&iv3^)+UBY`H}n0fI7Wc5|QXtxH%^gY}{ zY&*%_^(@frUeoJTYfGDN!${VQg+N38I2VqJA46VRD@*y*W9= zxjCxMv*$z|sZ4L&gP#PyhGsc_%6(;#^?X+^&rO2vZJ)i6ajyY-ZBIL2f-b@GBp0sxOQ|*_Rsh+LfzoGn~TWWVeMOLZ;#AZ1vmX zwp}1Fz_1!8%Hfi+3h_j>Fl+%0%<|Is$MK}}SRhA+SC-{6<&s!r<;bwYo1#BI$C~Md z?_}sNJB*x*oP}P$kcJ&#qi;IfKEw$`TlC-c3VkAKB9#T)k!Df({4h4XMrBrUo8*3s zFZe+|4S7EuRXo|J!Mqo^kq}2^k+nLY_b1Rs)(tb$I4^0wA392S^Ytf2*ti#Bk{;+| zkfFfvIMQ0&hHV6)XMQo~C>VN58Ys>8hDLC|0qP^N4GAK%%D1k@sDeu#*VcYi*n5(M zTHUKJfY~Bp8}F<&-7Y^q?;>DaTZhYxMg~i`BNdVzaHEKFoqIJ{TDlQTOtAQeUd)j5 zj6k%()Pwg|_^TA|zY0p63-D4af}1AQu+q)r9hgYUR$5?CYJw%4~E)#3_1j@KH zb0*5Fr?%X9?^7$RVo?qeRTveelhhx^1FT3NB&Je=)<<+y&f5SfhfXOxU{Zvj^nMJa zheGt=ZvJ+{Es=s+Wj+W^s!ic|CXHmgKlJ#1m<%AW-2RsQc9prywJVNvm%xqV`PSNm z4vWiIX>X~;bw&8Oii%Y=kc;|Vc32rU7Z^h>`+NB2#f&bgIUf}QV8q*=eH zSaM_)926f>k*&&h+bIrSwCM*nw`K0yRU(7V>H1ZDJLP&oY*?Mb{v|d!j*E;C=zo=i z<-Tj-me2S@OkgHsfpUkaib=;ib$uRNm!PxQnU%kYDl8=Ei8&ih9=tkU5A?XX(D;!W zC`6OTq${c_fyTuVq-r^oeb>8GzUXz@_C7{Vo4Pn{*Qd9BAK*2i;)bggl|R5^(HiO6 zark)SRKt!m_yDVqdi_M&xp7_H_#$R8$2~_RCthVJ@IXRLAY);7W=rUPOJhs8MqE%( z44+8`D<+*y$^N71(+yKigKzJ40V4?6Nv-0%NqwgRlM9&^mE8t?lUXCS=eiyqzCpg8 z&d#2yz$_GRWx24Z&U>byAoIQEk*DWP?ZXk!L~B2zVYwD>LP?zYTVq%LML^oR63f@- z`Q^_At;YR~%8ry@W6fc106>U1KwUBs7`~}7sJV6lR0bemi!zuNFN^{F{;2lO7MdbX z7F`-jUph~F5EZa4L?JBo>+SAPfwqatR}CU+G=x@82J_a&n(2F#O5%RHuc%c9zd2`_ zx|%jn-ImUOSb6Ks2g6db!R+{4)vx!20S$Tz1Aj2@gvFztXXI4Pa*F8(slO*I$r%^q zEN^c8z>p@tUAgZ<&7)^LhxQLtd@YIjlbQ2Je_!)0$R-`M)PwILt$$HmJ4U`=B?}cG z{orN*apOGK#=;xAVhR}AL(0M&U^?9^T<-*?<$^XEn!8$roiZqOR5(?1>pG_WL(B|X z>L-pp9`;?DPJ41u#--9kg0PpDskOy{_{P{JMPIwQO?KGx=jzXolZz9}|y%h4q)Tj2aI&$H+drn)+pHF^DOJb*-zm zxb(^d;Jz;=XO@^fZNn{L_Sx@0gW~I-nH6`X@;HD9S_2)vx68$SYDYZJY2);wN#}!N zowl%Rs+I5olk|JE$4bG%tzvm2XPKf7sfVnEQMegIa;bDg>5jWAH{_0|kZ9`tY27{Vo8ZO18)0>6SRJOj zLU1`KvYe?x^wN_kcOkgo^pK<+5N*Mu{V^bkze7pP=fcDGZhD6F>sJKx1}HJ{s*qc6 zFqC8MNbM$wV-%xyx2Aq~4YnS4xG>UrtJ@SYWX3<}o#w_zBiY-d$5NrLAcR|C9hCGt z8l2ir12PT;?%pziToW#P`$yT^YybREnVp1d0p~K`i;P&lS{=aJ6YP_Q z5YqEW+g4K2^LYiF=f8!t^?n%vvDS+oZ5^7%y#MI8W${e?1*Lv|q_?6#PMCO`fA+D483R!Ehu=KOQl#!BIdbhDoaEW{Qb<|E1SAOGHr zOniRlEZ}$T!b8G+Q1oUT=qvz4qqHEse|@0>Rw06Q!K{y%x~YC?$n8RLuDb6qO&8@b z#G^1gBbHy168k7E#s;MT%X!%Ts6sucClYs@pQ(RVYwLT-^`Qx09c|S>wUe)xx`E%bs7`bb*X>-hWFVV z9yzWa!liFGn5eD0ImRnryb_TyrkX#2eGwzzDZLQxy2=S7VBNVL^ z*OP=XD>RsWQGC`a!?x0E^>oZ<S6tot3e?}df6!B&Zy2g zJ6^m6AbQg6k@!0PN;*O-uasBWe03JgRBkvvJEMq*LE~<(74@_iELtO$ zzC9b~q1+OMO66_ZCe^sK^p(opGb#6&v&aAXu>^8>{vpV^!Wc9f@ndMU1(m)piotZ3 zV3>&MZ1$F8><>b`(ZmTIWj|xA$dVR<5ISbt9P3e}Amx_HCeqVm7N->EgmV``J&_q{ z?vk$eSz#D*$L4N)nVs1)s2rSnk&^?DK*Z&mFoWF3sIceRMgUFv?`y75PG`k@GThnE z&2Bya=Ea>`eJy(G9CECXh*_G=mVE2I$8bwuIF%|06(f~JO;LxQGIs+M%*u%_tp?>r znctxF?SYlShWNj)C4@eNMQMaM^Z}Fx&?8usS(cbyp+D~r*VA7ZbBmpfK2C6-YDx9{ z;vB>LTkaqw zx2NZuyQyH@LsG(fz84Rj#N-7TOnj}zV(6| z%$aL7_>Epnsp&Z2zc$SiaxUCA{owCwuLfSWagUSk5%#dZ!&_2@iBjvo9$>Uu>-X$^ zh+I;$-|JY7Qq%6aqvn2MYe5rITiMC8IHn~g?fjAMQQdWS%?cVX;JjEn6Lk7_0qeq?Biz>b&AiX$AZQvLwIx%m!*jvP=~!fU?~@AdcFF3 z(POzoKXF}85=(nmu_=bI8duY`0MhIY>{C-6ttm_70Euj|COw4Z{#9yMjYCAE3=*~n zfGa{TiYvi&yy(tkgnga6!`A;|9Qa?jef-amb8Z}~k;v7`HNOC++mC>e#^QkW!x1bk z^UnQ@IjZb46qL=ytJp{SMW*3jlm)rfkJ(?-;#Lm?hMcpn?z9K>OvdMyz6h4dk}Vo> zd9f7KlhtcsIXcLQlgY`PGxTlUY`x;{Q7%i4A&SWF=gW799aIM27LS7}81lBR^q<*5 z@#wI^NB>jIl>d*!mjA$?U*B0J4;9!gAP+VN85Wx_y4iau?`ciRUY2s*YO8L|SlBE| z+N3va%o4At6wrz=rd?)Pkk?}C`b9=p7zijF!yPHpIzCQg}QnVv;963fx z<>EKNGpG)Dai~y%wK8!lS|2x0b{!lt0QnFPcM@Ea;nQ?cRB4uj@cNN;Ka&*;?$gr- zTSg(f9iu{BvM4a;CW+Mx4fDO8h<1`LO+q`F@4tSS>(Z{Lz@Sh2*cTp^u-i|qp%~21 zmL^!%V0r3ODx8|9B{S@Pj=Q1~ys zJ*n_cD(vxaGeH1R7vW4Q=Xk9H025$e=&i(Ie1jx}OXa%GT)R9sQ*^IJq?Wx}KCJ8K zZxj*QU!I6pT5))99(NupAT#J<09RZW**q>Wcwd;9{&cGQaR-Okote18m;$x>Qf;_^ zVp*8f>^Uy%$*pUiFeut=g&uz|9_u0XJm`wjsA1~BXiG+{J!N6A_739D?|njlN_F-p z{aIlt1k9!R| zU|HItaWu$TcTiaF0bmPt@%Od9g@$LTl~uk;2$I?vgcMw)0UD z=`pEKFgxM(awnGDd)F~IKgsbt5SNW6&9aic6q6Ke*e`>5w<{B-yH*UrKwhr1MvveF zXVl~)MHQNm(NPo`>^C%u$|>%mCM#`BDwUI@D`WHY1C0@EvMv(t2f-9w@w5v)Z&feX zko_ajwGHLv#Lj4EEVYb93VU&5{=xCif#+Icp;b)PdSmO+gOq#Zs`Vc@K7D~XHGu>2 zu1b$}FI>IV#-WtGEgF>X4?E>BBoSo9Fd}nZMtPn~Mtro|k1sMxfi}zFo0IJ#C6rQd zqIXRM!#5%d=Bi>~dY?IyA%a)CM`Ro%Ldw$|M4VYFQY8oN%&2M@y?*o?qSwlGA?XyK z+|qGN>8{gyCfy?{uQqYf}?oBEZqO!XSC6(FOt z@hvhdg9Q>{NEVPfq%N(+^XoOq22YzLQ^26O7a#m5mMJR9zfXzejAl&~WfIW56^+cY zFS7@izlSSLl~VE3GbFK7l|w3_b4Kq0gxFcR?n*z}N0gar!$OnRab_9Il~sxb7v_xAWg z@>{|$>hE5k%FbyFd**+081~x>&yM&!%h??-F7YDbJ_2gjV&x<zl#@=7B0N`@yJ)-6m=^m_evNzi?ukj1s%Eo+o=j;_;JG3y)zL)fB*sMT_C^&1f=(pAiX6t1BA>oeD_}KtbM&}tq*6dv(G+X z`~Y4e8RL1zGw%O=|8C^iL_aXw-&)6o+Tg%+>y|WUCA`-&U!3RbXPKj-dN#7O-Kabm zBNJu9-7UW0?pa52GDr4mrokrAQSU3GfKD-s2%~TU@7vFRUt>^$GWIA(iqgMLc&g3I z4(!`SRB2L8ZDPgSW&!R>-R;Vnfo|r->lC0seymBJ7S+dbViy5wO6fhJ4AL8y% zCmdJt4#8|_Q(q{Bat`=CZDj;Y!8}_MHKe{GOu4hcMUkjXM-PWu&`zK|5S%X5&bw{g zHJ**O=q$jxL}rCWEUMY?bpZ=_xW(bILb*nh zoL0Hm$5R%f&4~P4ZhLb<(TLzrj~|lX5h*S;0h96^^^rd9d*r zf0k2j*G1d=Rmytwiu1(u;sJqfJgl#S5#K0qHv6=ZJ(@hZCU%jhXu_q~)$FAgDEoB^ zSRaHGgs{_`JXKp=P4MyqiSFq#?Fj4oZFm$H;U(jHqCa;YF4)}yNoSv7Z&TanET5Xr z5xIRS&%f&+z&V%?04;mvOD;-P-LC{dBA0!EP0R! z?I!cCUIYu?xF1;EEOf_-NK?x{=neYV_55?2fY_ZIcL;&*hV8Fmo@yM=qUzeMjaq$mpOHWh#8f(+Tdd}sp4Zzz-%xV4zQIu5X{}dm5Rc1ek%0>N)z$-(?7Px){TbZ-JEOly3KWK|KjqW5H1%` zpvZt8!}@DosIFH1Ffa{pa+6RErOLOD$mb|V+!A8f_N~V%wf4?!3yd3wlu3r{(%UZAxcYM_{Z3&reki%l?7yfUJQ@skqBM!nHBZw$9`Q>OUrIHJXUqWUhRJ$09C`?x$3O_%5!XUb z{?X&T+Frn+v6{V7+nEU5xnZ;bU?Pm*_;?zwk0x$@;n<80T(ap=?4TlqMW3`w8g`~Y z>c{0SyVeS5DeG*giFNit*f-9~`4_XWVk><|?-EjCNVk&hF{civDYFfo@I(aYTR5Mw zIg(OO|Kp(koZ+}f_7q?3S&+d{_t(!L<@Eb(G2k*iJ?V%ieIPxbI4{4YDqZ&(c?5U+ z&{Ahl7%LHJO&E<+HD2>(utjV}w>jd(J$hBHzFXj}bpk74Q6|k(^){LN;*PQNQ$iiB z`DTrcu}b$W=zBP{%Zql(CK5ma+Yhao3ORhhFUP{o9I6hMlC9gJrVHVYx3%dl;`!Km zy9V9KHi6UqcYwemMi3xS)PYeUJ^SD*?J2L$?hfr*t5jR|W z257i2u));AE_-=+#!k5v^^&wEPQ5^X5qW#)mv&Y1(Y{3krNml`{2g(Q>p06wF-h#S zI`}@8OF}21G9eOFC1{ESnb|98Qza-5=-H0Qgia%h*vkASVr@s0_mzlQFST^UF=iO; zx?M#ID&JJIH@Pn+;KPceqWmd!47SVr^Xnugbd}n-Kk23lhyoNOW+;x1#L$XM!azBR zP;5aISrTT1Ym5l!q$N&#xtNKTAl(ZR?~e+kuGH^QS&qY@)Pyj!B@j2i9qan~-ujV< za;qH$6$HkcxqVR~s9{A7qpG13Ep;MdhE|n-s-|n0X4z$Ck!@T?Y3dgxlN@jD4q)nR zP{{R-W;@mKG$p-?(vj4~LV0HSLw*GN!=|MNqVoI`cIIP8=>4usJL2Wp-c_l(FnO|o zp)P@|Gljvg!{(BiBIj5RV$;D=-#RA z*f}A6OEnslL{aLh6p@M{cl(>kj~$zAzcLeZ7a=d^O=WrJV)OKd)}1bUkyW3`>bgY) zhwkM#5y04w7%XT7>&}U%rMDIcgFgk{x)$~j*kM_TWbF!1$@IvresEKP0T;@{YN;*YLjN zzGSG8+o7M|-`9e&`;XV4FG8@t^V)=V1B?cW#Lnl3^C?*GeSQ4l<1xU7%Nsc}|1GDR z%;%AL62?Fb>s9BPBJtt8ST=UP#nhwUgp%vGY$fYwfCjhM1Ijg-ovh8B5y}I?=s$&%r$2fML*^Z?KZkj#F z4yMIkCAbMh7uUj_S?LoHx2MXuS|VjWBTVF+7`LEA;g#N@uo!3^qz2o5$FlzenQpcR zKl6Ki<8^VzjC8bpLM|5=V* zxY$cJNp8V3+fP^+rgLPpWCxMqSGmjdI92%zmG30oyzx?EPcJFZo57!Caqk$1d^e0` z?miJRG20%U8|6!&*)S^?8BEuB@KqyNco3v<6TFW6QTp$h#IIjcUj7O^LDhKsQ3$vb zkx(@3z50rN1*V847t&16sLNOw3siOD*fM+78Y6V}odhjGTA4_5nu1qxCNxuEVo zq|A3M^<`zUU#F>`ud+*>iZ4{{l7~6e_`*L&Qn(p^8c&ziOIh#ss{j)(PY$mNrB;=l zJ7{6_xyBgZI=iQI?7n2EHDF}y8jsIYwRqSw78Y?Qyo>-ZRnn&`9;MD1C~RH%pW=e+&qs!oPHu7qJ1I z>U?aP^gugDD&W$v2HH6Qc6Bl^0J5;=U6p@*N-*Dbs4*|c`6^}C|BOHH7}mc&-8Gm* zS=U=9nuDfLCMJ1$DXD-_aL{0&!NBYV;Fq~!H!T&x=~{wIUcHt1>WOH(vxsxf!pPl2xu0dv5M$7(dXtX`{VZaIt$opQ60a|LYDFrPc{Q1?6K4Ph1Lz9jkf*apSiRg@*Ge8TC1!+p$o%!mQXuy4 zYdZKWkw5s~!}ja*?1Eku}W)CxN#OS zNr!<(->wANE^I~1wOdt4aLB)_JTMW!8$}(6BZCXG0!Ht4e{uzyIF2!$ga{b;qtDl{ zz-)pObRRw?B%oxUQC+odI4HqrZ1s}CCneQs&)3&>%nN+Y#joR=#Mie~GUDXgMZrEOrjg!&*4^3$ zrFreknti_tjeK%CTvkHrLh9-_rB`_x9x?sIhyBN2CR&ozUEX0U`lScghC!)CoEvOU zWj1mr`P$C^M6jN!(kEC2vEe%ujMAB8_sY$o?lOI25iWHFc!XNYn|h6Jv7I=8r=s`& zkDfib!tV27f2cl!1KQDa*xr{5kYv&3yV+1#TyDL6n=v7ZHp7<(UPEuh z{D;qHq$z#3tyF3eN2cEHt@z;SH&MO_h;oDDRZfCAL?=8=1FOLS!xEqjqY_;fn*&Or zfz_E1odbuOnP>fL0bXWFb;wgsu|wfDHC`{Hj9qy~QX2Z?FOutj(_ra8|BU{(U1z#! zNxTiBiPwITsmceKd43ShR!YRgjQ&@P}%RCk5@}9yIvoqaa zR?)FXU@L?e)}&5Q(@5bqL{p5Y|;y+#XTjnlKk1c{;QAPh*D=Xby zUOM3H@UzQHiHU#VXImFg(G|kq((n|)rfr&AbX!xxG&eimkqtFylJXfaNO?6R>m5G& zp>f6x=rjJDf@2*rq2K2Dz5N?jOz|$=OD0Q6vnwKM`ORxN%5O~rnyftAM0~koyqg7P zCiWkmZ6Xa{MR>OIqbV*Mi_38SDt@2O;&8~f+3p?2#$`u_%5!(_BH8UO{z_D_uxUgY%Koto~n-6_6x=f zRHRqTRpT{bsy}Z5&W$hEe6HPidu{dlcxUmM2BvQjP*oPZ8V(j-H!MswoO&II|MHK0 zoMy=ZD)h7+zM9D)_1a&Ao^FcVkvn7YD;Kea{_ktZSSazC-R-h$S#B-RQ2k>E8X61E&|i3@*_`bXgr{DT7s7m%lr=B!pBxpP-%a}Q zcIDI&>iD5(PinHP-p%jWh~)R9xC6X?qO4kLaX4 z6==*|;XB~wU2foVt{4a5#}aSwfA1blgYXUCGs!oNNZw4i-P^}pEB4fMJds=f$K8U< z5drO7vocf6V8$8v+vrwp<`p0D$Aa@ojgkg6QHMSfLn!gm3j?E)P!`M{&y^fS%7lmJ z9gmfbjif4dZA1q1o|@~W5@5OIiL&R_ll9_{tR}td-&mjRnoP;-59K{Fp7cg0@0?v+ z9Q|CZt<72aRtGPIKCWlGvFGb@=VjIic8+>#-|vv5hcd?kK*bo$R2h^$x>DJ!64LJ0 zlIA{DrtlLSBx2~F;+LA^wN!A@neNp5${^!8KykbgF(gxgridImT5P?cna_Ib?U(0U zJ#^t+SgA=zDkZoAM}$g%t1;sC1k#W`{Qjq85ep&LfA?4ne392Av@h(}FzjpcmTqM zD`G2XsQlq5g&un5m>X!)V?M2dznuZ-TfcV}b@CF@ z85y}0=^cRw+o{oq0(k+OD_5MW`4C>ILQYgyv)wj>im>(8Zgn_S3Y*JC6C&2jegD=W(oD#nlEJ z`kaa8ll0?2A?8yB`|#f9(sd%ZYS^M0Tu_qM8WiBm=4l{ej0W0-l93jvn})ux^<>-* z%!EMx2~TwUe1H^?kPEvOl3i%5B*mzioK!%g>gz;M#3Fk-V^o}1_70;Vx9~}cV!v>e zekQm?om_=ZJ)B3be|YxrefgJonX+3tBraG?Hygkh`+cqI`d8(S_AV|*4C!8ipG8Wq z&va1YKn;WY5#-dCX=$=JJIEy2t@GrN@2h>z_hv&6!^NyboA-TvBWD*QJbBLOk`XV@ zhS>v;wWMeN^oaX!_&opX|K^=FEWm$ffEy6F)Y!DEsWl zEYZ#4W;*O1=B|6oF4xuN&OGiph)~>&!WVy4Zh$TvBNb@UuRgw?ii=M zAc8VqvsKK?DU8dv?-BOk5jYwaTM2u< z21#d#;1XblvM2O`6LbOox0_x-naVgqyWVEHQajactPH5Iur9dU!3#^mX2b~M0fMGGa6ZxdF=E|a7oJj4%? zU&cp-SADV?=r~hx<&J*vP2qmMc2xzpGzXMD9+sV{i}Pvv%9Sp9D;xt1;0bc>*wyW2 zplZGE5su}Nest68%HZdlkOqc&8av@{ea~xBnFPSpaR{okGVOANMS{~F$^r>8FEf>xooyds<{BvY2dbsZ^pd9iX}a5_i0&qpkbo^9yeYS1QK@8tFaWv% zwduWr=Q_OJ{`I-7M|oJZLcuN#E4o%wzv!gl6xB14rkDKeed?x!WrwAvOz!tdha5Ma zG{U3RjD%1Io#IiPFT&PpSKYySd21%cZlOPrN-TMH@v3Be5LRWPc;|M+KH?c!3sa)} zOX&GY)CC{0{=~Vc$8z+pV^8KDI<~!?t5Xr{Il-K@>gsH1ZMKW^i+|m}o~12ImxR=; z@X$8yHvi&a&#_|KhvVUCL7xA8Eeay(lN2-Ybqjy|wbbD=+C^2HQNDY;2V543QZ_}# z_gq04gEiIO;msIPB{46TTys z$hARgh$7oX{^Un4FTE-bOS0FCV)DAX@4d6;^Ob`H*Z>E0kwY8M>afL2QGl2lyPL%ylTaB<|!rm8+Spkm<#? zqu0sziLKobwzh_XyKVjSvBPgF@Uvn_S{>$?M4s@n?kb5Ej6-#RoBf(f?A@2OC~M== zQWMl(`B88~W05f={Mi_dZAJS+H zIVx=x1mnwQdZx;>i-jaRfh=jK+Tams6NJ%WP2b4WV*`Sr(z(pG>i2WqO;eC2K#L>% zbKSOCILIVBP34dHHQ|bu^NBI4YiCy;XK-TDpCt7toO+=Pl9of?Lw|ekUVE^7E3FC% zBkp}DM*iBcd*d9>&iDQ-q}4p-%~BmD(?09z3d@_@Z9%J}H>>kiU96n7c$g~R4#~zl z>qic|&*1iQ_(pixozAB{(ylbwIAAKBwp}CbG=d)SUym+eNgi2FL={W;3;fC-PU=c{ zmcYJK81=2#e${!lc#AuU3RMhDNAWK;el1tkrlyWPJ7`1c%ubu_0EgCYoPbdZZFD6l z)f{>k{r%A?{?9w3Um!OKbeJRtYobk$vCTl~Vy{}yP_S}iZCzc%vxz0UuVrF!G)^C- znPi_H>2mpE_P&63mZ^!dePlJ?13T=>HT!Dw+OMI!tcj@TVQKFhX--)$f1%Hpl=)io ztSF|^h+1nTP1L@+slveebZe|I3T0C4059Mml<0g>{*^D9_#W7+UCoL#|=E=A-R`%}5f<8fzNAiS7>Zbd{C&ii^0cN{M3@E4NDI1NFFM zIlKI^=HbjP;hTy>;i~`WdoInE3#7+mk?~*Fq|&O1q+co6^{Ti>EW=RKJz(_E7xmwK zfWy@}=V;R_%lar}9O_-Kz3<7rsD6M7Q#M|)g%cUCjQ(_9I#XoLEJchxapK4=N7hAH z`y5}8o?Slwlg8aGHxzBJ2(3M;{k?{iU+}ws`Ao@jD68A$xXAk`AF@mjB0+3B z@COmd44@S#z&Bv`d6DX_%z&Fk?-)uiLa!}RXKm4bt>8LdgAvtya&@b*ExM9~1UqzCUR_Tz6r=CH8gxVeR`;#z1b~ z4J9|70F*1MafI+z15vvN$Z`+o)}o_UGS%FredAvJr2-Td)}5U?4P{kV)Iq`cz8ud2 zub_{5T4d(CI}`|Y41HsZ4;A7ND+_McT2sX}5G!H|hC=~y-Sy-$uXAf=@9Tv)y4WQt zQ*}2+=E8E62F{mn)4OP|!yaWBe39~~~#e&Ma|%exFCe)ydj+DVC4W&e|;*MV0<#z}4x zdw>xLa6jr(=W+JGUWr)fse!%b=6NG&HW7##R_O2Jh>ESV6Wl(iU%9hg(qk6ezp?FW zVQQn0ZbUhK(T!4}LY1U*1Y!e0&&ozNz5^p*-3haG0artC0x8o4#*w-CHoiQ>T-nsr z2hwNG7t$@WvC%%E+ax@I(l!KMasMzsiK+lRN*Q}8mqKvotA-9$nBrLgk@}OR2lOt< zbv}N2xMIh6tAl4u&Wok{jj0`hHTpD|ui#Qcdkk>UzLbDadn{@0K=};E>#lUn+dfw} zW54=yNr1yq3BAL82@+=Kj>F1>2HJPS1KJQmPOp-uO?+d$fMj2QkC{+3TCl8pXvuaw zwRoScH_+>co4Ade;7lob1&TL<&hwEeI|*?{MA^$nRz!<6vl)`K#~SZ#V$T;2fKC~$ zO1Z{MXA800PFdQr&PNr&f-Dm^pH6v7%I%x#&B)Pmf@?pDA98fnwHxV9N}eCZZ{Eto z=8N9k5$-NRn54OuS|H?7lXD$K`o`l-;tQ3vcF@D@S}t$DN}VUL+T2-p9q9Hc1rG04 z4|IhD%H5Fcx@zVJ+0Z-SidTSXq&~D8iq{HTfRd?r@nQX8TyEX#EKqBJI9}zi zXEfKPYx==U#PBGtwsxG#?=$gL=ek9Ep*xv%CJ;add@yPn2S|T+cIFc!s@%ZQ42Msr zPT~gYuJnysezz3pP1|pqp7@hrtfuJ0Fj}r z+s>eC*KjF-(y-8YIZC|WxDN!HZxV=#t44_hJaway^HJ~ME)}yxNAnbHe|%M&c^O=Z z04YN*?N}Q{!9%=*{&Y>RS##o;h;?`0U64?Lk zQ~5)=+_HI-S72)c%~wIjpVK-8Jhqa|gm0YOAX5U!@|#mvfcJbA0r(x^=J=Jf;J7Xo z7t(L}+#fK&zSW7T3^K$2ct?_SDFq%spRCtRxDTejck3Y6qICP@qMVxGC zH4nDtE$3{Pbk;VNHz@cD4LcO)hZYRF(?EBD;k_?7CpLGxOPdlYB+?&{w(w;2tE`xU zjUlr%*WtW=`dtLG$N(Z(4&ZYNqwJc+$H6cAN5kqV_QnVvXX*g;j{>mSU_Z@oX)usE z-4zs6PD}E_cu>VlHai#E1vqXx%dz?=>2i~DZ~Pxl)h*c=Hc3C!m_sv&4%z6>R*Bp1 zeQ~TlPiII}q!dzN#w~JXh^53MDEo{<^cdrd1j<{+>YXp7X0t6@mxQ3RilgM6 zfy%}J-(?``s8)j@4TAi!A?M%=^b5=TwCcmoa*q^7hasnD%KMIosa z7KPzB>qO~t{eA8ESRMBb-N|~J^LveLo%s)-HEt@Q2$Y@W_KdAnqy}yJ^IgXpKi|TK zLK2q8hJZwV@3xNqpns}E)i@XcKIhmHE6l~XsWP+o%LB%}(;x-|rG3(Nw#DPth{^?B zh4wWv6_AO)Id@guxtblupeh91Tv2gdiburb9#bN+t3#6Li{9b6kN1xD^ps4@tFX4V z+qGtmI34mbecT#iKDiet{!4GN_#>m{h7goT>3Y@E9MvzIsUUEdN||vs(jpHgZ2rL` zTfB6nnCk_4a7__kw>K6EbgLbM{K}#>ZIjn?L0a0rUUqicCUFLylldcOHF)L_3fO-n zuKj0-!vEdhV?D_eHJ%BylYKlcF3>qupQkyhH@^4$+Gt4W4y*LMm9-{YdDhF9&arG= z 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

1rhuBp5382z!v%`%^a0uDfTQJghm9_c3~Z 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 - - - diff --git a/frontend/appflowy_web_app/src/assets/italic.svg b/frontend/appflowy_web_app/src/assets/italic.svg deleted file mode 100644 index b295c230f0..0000000000 --- a/frontend/appflowy_web_app/src/assets/italic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/left.svg b/frontend/appflowy_web_app/src/assets/left.svg deleted file mode 100644 index 0f771a3858..0000000000 --- a/frontend/appflowy_web_app/src/assets/left.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/light-logo.svg b/frontend/appflowy_web_app/src/assets/light-logo.svg deleted file mode 100644 index f5cd761ba7..0000000000 --- a/frontend/appflowy_web_app/src/assets/light-logo.svg +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/link.svg b/frontend/appflowy_web_app/src/assets/link.svg deleted file mode 100644 index 5fbcc8d787..0000000000 --- a/frontend/appflowy_web_app/src/assets/link.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/list-dropdown.svg b/frontend/appflowy_web_app/src/assets/list-dropdown.svg deleted file mode 100644 index 4a8424c5f8..0000000000 --- a/frontend/appflowy_web_app/src/assets/list-dropdown.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/list.svg b/frontend/appflowy_web_app/src/assets/list.svg deleted file mode 100644 index 97a2e9c434..0000000000 --- a/frontend/appflowy_web_app/src/assets/list.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/mention.svg b/frontend/appflowy_web_app/src/assets/mention.svg deleted file mode 100644 index b98318132c..0000000000 --- a/frontend/appflowy_web_app/src/assets/mention.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/more.svg b/frontend/appflowy_web_app/src/assets/more.svg deleted file mode 100644 index b191e64a10..0000000000 --- a/frontend/appflowy_web_app/src/assets/more.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/numbers.svg b/frontend/appflowy_web_app/src/assets/numbers.svg deleted file mode 100644 index 9d8b98d10d..0000000000 --- a/frontend/appflowy_web_app/src/assets/numbers.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/open.svg b/frontend/appflowy_web_app/src/assets/open.svg deleted file mode 100644 index b443c8b993..0000000000 --- a/frontend/appflowy_web_app/src/assets/open.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/quote.svg b/frontend/appflowy_web_app/src/assets/quote.svg deleted file mode 100644 index 57839231ff..0000000000 --- a/frontend/appflowy_web_app/src/assets/quote.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/react.svg b/frontend/appflowy_web_app/src/assets/react.svg deleted file mode 100644 index 6c87de9bb3..0000000000 --- a/frontend/appflowy_web_app/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/right.svg b/frontend/appflowy_web_app/src/assets/right.svg deleted file mode 100644 index 7d738f4e69..0000000000 --- a/frontend/appflowy_web_app/src/assets/right.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/appflowy_web_app/src/assets/search.svg b/frontend/appflowy_web_app/src/assets/search.svg deleted file mode 100644 index a8a92df509..0000000000 --- a/frontend/appflowy_web_app/src/assets/search.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/select-check.svg b/frontend/appflowy_web_app/src/assets/select-check.svg deleted file mode 100644 index 05caec861a..0000000000 --- a/frontend/appflowy_web_app/src/assets/select-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/settings.svg b/frontend/appflowy_web_app/src/assets/settings.svg deleted file mode 100644 index 92140a3c23..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/settings/account.svg b/frontend/appflowy_web_app/src/assets/settings/account.svg deleted file mode 100644 index fddfca7575..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings/account.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/assets/settings/check_circle.svg b/frontend/appflowy_web_app/src/assets/settings/check_circle.svg deleted file mode 100644 index c6fa56067b..0000000000 --- a/frontend/appflowy_web_app/src/assets/settings/check_circle.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/settings/dark.png b/frontend/appflowy_web_app/src/assets/settings/dark.png deleted file mode 100644 index 15a2db5eb8d0b0bfb2fb3e22821eb56f3a8f709e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16280 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 - - - - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/show-menu.svg b/frontend/appflowy_web_app/src/assets/show-menu.svg deleted file mode 100644 index 8baf55bffd..0000000000 --- a/frontend/appflowy_web_app/src/assets/show-menu.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontend/appflowy_web_app/src/assets/sort.svg b/frontend/appflowy_web_app/src/assets/sort.svg deleted file mode 100644 index e3b6a49a56..0000000000 --- a/frontend/appflowy_web_app/src/assets/sort.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/strikethrough.svg b/frontend/appflowy_web_app/src/assets/strikethrough.svg deleted file mode 100644 index c118422a15..0000000000 --- a/frontend/appflowy_web_app/src/assets/strikethrough.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/text.svg b/frontend/appflowy_web_app/src/assets/text.svg deleted file mode 100644 index 7befa5080f..0000000000 --- a/frontend/appflowy_web_app/src/assets/text.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/todo-list.svg b/frontend/appflowy_web_app/src/assets/todo-list.svg deleted file mode 100644 index 37f52c47ed..0000000000 --- a/frontend/appflowy_web_app/src/assets/todo-list.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/underline.svg b/frontend/appflowy_web_app/src/assets/underline.svg deleted file mode 100644 index f5d53f0ec2..0000000000 --- a/frontend/appflowy_web_app/src/assets/underline.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/appflowy_web_app/src/assets/up.svg b/frontend/appflowy_web_app/src/assets/up.svg deleted file mode 100644 index bd8f3067d3..0000000000 --- a/frontend/appflowy_web_app/src/assets/up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx b/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx index 00441e5281..9216a92c69 100644 --- a/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/not-found/RecordNotFound.tsx @@ -10,7 +10,7 @@ export function RecordNotFound({ open, workspaceId }: { workspaceId: string; ope Oops.. something went wrong - Sorry, the document you are looking for does not exist. + Sorry, the page you are looking for does not exist. diff --git a/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx b/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx index 4fec272b79..be0dc61dc7 100644 --- a/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/page/usePageInfo.tsx @@ -1,10 +1,10 @@ import { ViewLayout, YjsFolderKey, YView } from '@/application/collab.type'; import { useViewSelector } from '@/application/folder-yjs'; import React, { useMemo } from 'react'; -import { ReactComponent as DocumentSvg } from '@/assets/document.svg'; -import { ReactComponent as GridSvg } from '@/assets/grid.svg'; -import { ReactComponent as BoardSvg } from '@/assets/board.svg'; -import { ReactComponent as CalendarSvg } from '@/assets/date.svg'; +import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; +import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; +import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; import { useTranslation } from 'react-i18next'; export function usePageInfo(id: string) { diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx b/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx new file mode 100644 index 0000000000..f91ac8284e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/Popover.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Popover as PopoverComponent, PopoverProps as PopoverComponentProps } from '@mui/material'; + +const defaultProps: Partial = { + keepMounted: false, + disableRestoreFocus: true, + anchorOrigin: { + vertical: 'bottom', + horizontal: 'left', + }, +}; + +export function Popover({ children, ...props }: PopoverComponentProps) { + return ( + + {children} + + ); +} diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/index.ts b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts new file mode 100644 index 0000000000..8f473de4b9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts @@ -0,0 +1 @@ +export * from './Popover'; diff --git a/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx b/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx new file mode 100644 index 0000000000..f12cfe4c01 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/progress/LinearProgressWithLabel.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from 'react'; + +function LinearProgressWithLabel({ + value, + count, + selectedCount, +}: { + value: number; + count: number; + selectedCount: number; +}) { + const result = useMemo(() => `${Math.round(value * 100)}%`, [value]); + + const options = useMemo(() => { + return Array.from({ length: count }, (_, i) => ({ + id: i, + checked: i < selectedCount, + })); + }, [count, selectedCount]); + + const isSplit = count < 6; + + return ( +
+
+ {options.map((option) => ( + + ))} +
+
{result}
+
+ ); +} + +export default LinearProgressWithLabel; diff --git a/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx index 0527b6cc26..9d07c8b908 100644 --- a/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/scroller/AFScroller.tsx @@ -7,49 +7,67 @@ export interface AFScrollerProps { overflowYHidden?: boolean; className?: string; style?: React.CSSProperties; + onScroll?: (e: React.UIEvent) => void; } -export const AFScroller = ({ style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps) => { - return ( -
} - renderThumbVertical={(props) =>
} - {...(overflowXHidden && { - renderTrackHorizontal: (props) => ( + +export const AFScroller = React.forwardRef( + ({ onScroll, style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps, ref) => { + return ( + { + if (!el) return; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const scrollEl = el.container?.firstChild as HTMLElement; + + if (!scrollEl) return; + if (typeof ref === 'function') { + ref(scrollEl); + } else if (ref) { + ref.current = scrollEl; + } + }} + renderThumbHorizontal={(props) =>
} + renderThumbVertical={(props) =>
} + {...(overflowXHidden && { + renderTrackHorizontal: (props) => ( +
+ ), + })} + {...(overflowYHidden && { + renderTrackVertical: (props) => ( +
+ ), + })} + style={style} + renderView={(props) => (
- ), - })} - {...(overflowYHidden && { - renderTrackVertical: (props) => ( -
- ), - })} - style={style} - renderView={(props) => ( -
- )} - > - {children} - - ); -}; + )} + > + {children} + + ); + } +); diff --git a/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx b/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx new file mode 100644 index 0000000000..fbd9ac486d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/tag/Tag.tsx @@ -0,0 +1,29 @@ +import { FC, useMemo } from 'react'; + +export interface TagProps { + color?: string; + label?: string; + size?: 'small' | 'medium'; +} + +export const Tag: FC = ({ color, size = 'small', label }) => { + const className = useMemo(() => { + const classList = ['rounded-md', 'font-medium', 'text-xs', 'leading-[18px]']; + + if (color) classList.push(`text-text-title`); + if (size === 'small') classList.push('text-xs', 'px-2', 'py-[2px]'); + if (size === 'medium') classList.push('text-sm', 'px-3', 'py-1'); + return classList.join(' '); + }, [color, size]); + + return ( +
+ {label} +
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/_shared/tag/index.ts b/frontend/appflowy_web_app/src/components/_shared/tag/index.ts new file mode 100644 index 0000000000..9790fcbf11 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/tag/index.ts @@ -0,0 +1 @@ +export * from './Tag'; diff --git a/frontend/appflowy_web_app/src/components/app/App.tsx b/frontend/appflowy_web_app/src/components/app/App.tsx index 1504c99f07..b2ee81eb20 100644 --- a/frontend/appflowy_web_app/src/components/app/App.tsx +++ b/frontend/appflowy_web_app/src/components/app/App.tsx @@ -10,7 +10,7 @@ const AppMain = withAppWrapper(() => { }> } /> - } /> + } /> } /> diff --git a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx index 2d00bec2a3..179b371125 100644 --- a/frontend/appflowy_web_app/src/components/app/AppTheme.tsx +++ b/frontend/appflowy_web_app/src/components/app/AppTheme.tsx @@ -72,6 +72,7 @@ function AppTheme({ children }: { children: React.ReactNode }) { styleOverrides: { root: { backgroundImage: 'none', + boxShadow: 'var(--shadow)', }, }, }, @@ -100,6 +101,14 @@ function AppTheme({ children }: { children: React.ReactNode }) { }, }, MuiInputBase: { + defaultProps: { + sx: { + '&.Mui-disabled, .Mui-disabled': { + color: 'var(--text-caption)', + WebkitTextFillColor: 'var(--text-caption) !important', + }, + }, + }, styleOverrides: { input: { backgroundColor: 'transparent !important', diff --git a/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx b/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx index f0f83d366a..768cf3587b 100644 --- a/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx +++ b/frontend/appflowy_web_app/src/components/auth/Welcome.cy.tsx @@ -9,7 +9,7 @@ describe('', () => { it('renders', () => { const AppWrapper = withAppWrapper(Welcome); - + cy.mount(); }); @@ -29,6 +29,7 @@ describe('', () => { cy.wait('@loginSuccess'); cy.wait('@verifyToken'); cy.wait('@getUserProfile'); + cy.wait('@getUserWorkspace'); cy.get('@dialog').should('not.exist'); }); }); diff --git a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts index cb972283bf..affe339c81 100644 --- a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts +++ b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts @@ -56,6 +56,7 @@ export const useAuth = () => { throw new Error('Failed to check user'); } + console.log('userProfile', userProfile); await setUser(userProfile); return userProfile; diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx new file mode 100644 index 0000000000..9e54b68ad0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -0,0 +1,148 @@ +import { DatabaseViewLayout, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import RecordNotFound from '@/components/_shared/not-found/RecordNotFound'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { Board } from '@/components/database/board'; +import { Calendar } from '@/components/database/calendar'; +import { DatabaseConditionsContext } from '@/components/database/components/conditions/context'; +import { Grid } from '@/components/database/grid'; +import { DatabaseTabs, TabPanel } from '@/components/database/components/tabs'; +import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import DatabaseTitle from '@/components/database/DatabaseTitle'; +import { Log } from '@/utils/log'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import SwipeableViews from 'react-swipeable-views'; +import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions'; +import * as Y from 'yjs'; + +export const Database = memo(() => { + const { objectId, workspaceId } = useId() || {}; + const [search, setSearch] = useSearchParams(); + const viewId = search.get('v'); + + const [doc, setDoc] = useState(null); + const [rows, setRows] = useState | null>(null); // Map(false); + const databaseService = useContext(AFConfigContext)?.service?.databaseService; + + const handleOpenDocument = useCallback(async () => { + if (!databaseService || !workspaceId || !objectId) return; + + try { + setDoc(null); + const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId); + + console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON()); + setDoc(databaseDoc); + setRows(rows); + } catch (e) { + Log.error(e); + setNotFound(true); + } + }, [databaseService, workspaceId, objectId]); + + useEffect(() => { + setNotFound(false); + void handleOpenDocument(); + }, [handleOpenDocument]); + + const database = useMemo(() => doc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase, [doc]); + + const views = useMemo(() => database?.get(YjsDatabaseKey.views), [database]); + + const handleChangeView = useCallback( + (viewId: string) => { + setSearch({ v: viewId }); + }, + [setSearch] + ); + + const viewIds = useMemo(() => (views ? Array.from(views.keys()) : []), [views]); + + const value = useMemo(() => { + return Math.max( + 0, + viewIds.findIndex((id) => id === (viewId ?? objectId)) + ); + }, [viewId, viewIds, objectId]); + + const getDatabaseViewComponent = useCallback((layout: DatabaseViewLayout) => { + switch (layout) { + case DatabaseViewLayout.Grid: + return Grid; + case DatabaseViewLayout.Board: + return Board; + case DatabaseViewLayout.Calendar: + return Calendar; + } + }, []); + + const [conditionsExpanded, setConditionsExpanded] = useState(false); + const toggleExpanded = useCallback(() => { + setConditionsExpanded((prev) => !prev); + }, []); + + console.log('viewId', viewId, 'objectId', doc, objectId, database); + if (!objectId) return null; + + if (!doc) { + return ; + } + + if (!rows) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+ + + + + + + {viewIds.map((viewId, index) => { + const layout = Number(views.get(viewId)?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + const Component = getDatabaseViewComponent(layout); + + return ( + + + + ); + })} + + +
+
+ ); +}); + +export default Database; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx new file mode 100644 index 0000000000..8adc87d4e6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseContext.tsx @@ -0,0 +1,10 @@ +import { DatabaseContext, DatabaseContextState } from '@/application/database-yjs'; + +export const DatabaseContextProvider = ({ + children, + ...props +}: DatabaseContextState & { + children: React.ReactNode; +}) => { + return {children}; +}; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx new file mode 100644 index 0000000000..baf314130e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx @@ -0,0 +1,19 @@ +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import React from 'react'; + +function DatabaseTitle({ viewId }: { viewId: string }) { + const { name, icon } = usePageInfo(viewId); + + return ( +
+
+
+
{icon}
+
{name}
+
+
+
+ ); +} + +export default DatabaseTitle; diff --git a/frontend/appflowy_web_app/src/components/database/board/Board.tsx b/frontend/appflowy_web_app/src/components/database/board/Board.tsx new file mode 100644 index 0000000000..eabc9c2631 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/board/Board.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export function Board() { + return
Board
; +} + +export default Board; diff --git a/frontend/appflowy_web_app/src/components/database/board/index.ts b/frontend/appflowy_web_app/src/components/database/board/index.ts new file mode 100644 index 0000000000..9294d869ce --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/board/index.ts @@ -0,0 +1 @@ +export * from './Board'; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx new file mode 100644 index 0000000000..c21e37b362 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +export function Calendar() { + return
Calendar
; +} + +export default Calendar; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/index.ts b/frontend/appflowy_web_app/src/components/database/calendar/index.ts new file mode 100644 index 0000000000..a723380592 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/index.ts @@ -0,0 +1 @@ +export * from './Calendar'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx new file mode 100644 index 0000000000..eeefee18bb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx @@ -0,0 +1,40 @@ +import { CalculationType } from '@/application/database-yjs/database.type'; +import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function CalculationCell({ cell }: { cell?: CalulationCell }) { + const { t } = useTranslation(); + + const prefix = useMemo(() => { + if (!cell) return ''; + + switch (cell.type) { + case CalculationType.Average: + return t('grid.calculationTypeLabel.average'); + case CalculationType.Max: + return t('grid.calculationTypeLabel.max'); + case CalculationType.Count: + return t('grid.calculationTypeLabel.count'); + case CalculationType.Min: + return t('grid.calculationTypeLabel.min'); + case CalculationType.Sum: + return t('grid.calculationTypeLabel.sum'); + case CalculationType.CountEmpty: + return t('grid.calculationTypeLabel.countEmptyShort'); + case CalculationType.CountNonEmpty: + return t('grid.calculationTypeLabel.countNonEmptyShort'); + default: + return ''; + } + }, [cell, t]); + + return ( +
+ {prefix} + {cell?.value ?? ''} +
+ ); +} + +export default CalculationCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts new file mode 100644 index 0000000000..ef44e2e745 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts @@ -0,0 +1,8 @@ +import { CalculationType } from '@/application/database-yjs/database.type'; + +export interface CalulationCell { + value: string; + fieldId: string; + id: string; + type: CalculationType; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts new file mode 100644 index 0000000000..9bf73af548 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts @@ -0,0 +1 @@ +export * from './CalculationCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts new file mode 100644 index 0000000000..1012dd4543 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts @@ -0,0 +1,47 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { DateFormat, TimeFormat, getDateFormat, getTimeFormat } from '@/application/database-yjs'; +import { renderDate } from '@/utils/time'; +import { useCallback, useMemo } from 'react'; + +export function useCellTypeOption(fieldId: string) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + return useMemo(() => { + return field?.get(YjsDatabaseKey.type_option)?.get(String(fieldType)); + }, [fieldType, field]); +} + +export function useDateTypeCellDispatcher(fieldId: string) { + const typeOption = useCellTypeOption(fieldId); + const typeOptionValue = useMemo(() => { + if (!typeOption) return null; + return { + timeFormat: parseInt(typeOption.get(YjsDatabaseKey.time_format)) as TimeFormat, + dateFormat: parseInt(typeOption.get(YjsDatabaseKey.date_format)) as DateFormat, + }; + }, [typeOption]); + + const getDateTimeStr = useCallback( + (timeStamp: string, includeTime?: boolean) => { + if (!typeOptionValue) return null; + const timeFormat = getTimeFormat(typeOptionValue.timeFormat); + const dateFormat = getDateFormat(typeOptionValue.dateFormat); + const format = [dateFormat]; + + if (includeTime) { + format.push(timeFormat); + } + + return renderDate(timeStamp, format.join(' '), true); + }, + [typeOptionValue] + ); + + return { + getDateTimeStr, + typeOptionValue, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx new file mode 100644 index 0000000000..ee3cde673b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx @@ -0,0 +1,62 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import RowCreateModifiedTime from '@/components/database/components/cell/RowCreateModifiedTime'; +import React, { FC, useMemo } from 'react'; +import RichTextCell from '@/components/database/components/cell/TextCell'; +import UrlCell from '@/components/database/components/cell/UrlCell'; +import NumberCell from '@/components/database/components/cell/NumberCell'; +import CheckboxCell from '@/components/database/components/cell/CheckboxCell'; +import SelectCell from '@/components/database/components/cell/SelectionCell'; +import DateTimeCell from '@/components/database/components/cell/DateTimeCell'; +import ChecklistCell from '@/components/database/components/cell/ChecklistCell'; +import { Cell as CellValue } from '@/components/database/components/cell/cell.type'; +import RelationCell from '@/components/database/components/cell/RelationCell'; + +export interface CellProps { + rowId: string; + fieldId: FieldId; + cell?: CellValue; +} + +export function Cell({ cell, rowId, fieldId }: CellProps) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const Component = useMemo(() => { + switch (fieldType) { + case FieldType.RichText: + return RichTextCell; + case FieldType.URL: + return UrlCell; + case FieldType.Number: + return NumberCell; + case FieldType.Checkbox: + return CheckboxCell; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectCell; + case FieldType.DateTime: + return DateTimeCell; + case FieldType.Checklist: + return ChecklistCell; + case FieldType.Relation: + return RelationCell; + default: + return RichTextCell; + } + }, [fieldType]) as FC<{ cell?: CellValue; rowId: string; fieldId: FieldId }>; + + if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { + const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; + + return ; + } + + if (cell?.fieldType !== fieldType) { + return null; + } + + return ; +} + +export default Cell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx new file mode 100644 index 0000000000..558c424f62 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx @@ -0,0 +1,14 @@ +import { FieldId } from '@/application/collab.type'; +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; +import { CheckboxCell } from '@/components/database/components/cell/cell.type'; + +export default function ({ cell }: { cell?: CheckboxCell; rowId: string; fieldId: FieldId }) { + const checked = cell?.data; + + return ( +
+ {checked ? : } +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx new file mode 100644 index 0000000000..32d97d758f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx @@ -0,0 +1,21 @@ +import { FieldId } from '@/application/collab.type'; +import { parseChecklistData } from '@/application/database-yjs'; +import { ChecklistCell } from '@/components/database/components/cell/cell.type'; +import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel'; +import React, { useMemo } from 'react'; + +export default function ({ cell }: { cell?: ChecklistCell; rowId: string; fieldId: FieldId }) { + const data = useMemo(() => { + return parseChecklistData(cell?.data ?? ''); + }, [cell?.data]); + + const options = data?.options; + const selectedOptions = data?.selectedOptionIds; + + if (!data || !options || !selectedOptions) return null; + return ( +
+ +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx new file mode 100644 index 0000000000..490a2bd95e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx @@ -0,0 +1,35 @@ +import { FieldId } from '@/application/collab.type'; +import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; +import { DateTimeCell } from '@/components/database/components/cell/cell.type'; +import React, { useMemo } from 'react'; +import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg'; + +export default function ({ cell, fieldId }: { cell?: DateTimeCell; rowId: string; fieldId: FieldId }) { + const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); + + const startDateTime = useMemo(() => { + return getDateTimeStr(cell?.data || '', cell?.includeTime); + }, [cell, getDateTimeStr]); + + const endDateTime = useMemo(() => { + if (!cell) return null; + const { endTimestamp, isRange } = cell; + + if (!isRange) return null; + + return getDateTimeStr(endTimestamp || '', cell?.includeTime); + }, [cell, getDateTimeStr]); + + const dateStr = useMemo(() => { + return [startDateTime, endDateTime].filter(Boolean).join(' -> '); + }, [startDateTime, endDateTime]); + + const hasReminder = !!cell?.reminderId; + + return ( +
+ {hasReminder && } + {dateStr} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx new file mode 100644 index 0000000000..851e14a34e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx @@ -0,0 +1,27 @@ +import { FieldId } from '@/application/collab.type'; +import { currencyFormaterMap, NumberFormat, useFieldSelector, parseNumberTypeOptions } from '@/application/database-yjs'; +import { UrlCell } from '@/components/database/components/cell/cell.type'; +import React, { useMemo } from 'react'; +import Decimal from 'decimal.js'; + +export default function ({ cell, fieldId }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) { + const { field } = useFieldSelector(fieldId); + + const format = useMemo(() => (field ? parseNumberTypeOptions(field).format : NumberFormat.Num), [field]); + + const className = useMemo(() => { + const classList = ['select-text', 'cursor-text']; + + return classList.join(' '); + }, []); + + const value = useMemo(() => { + if (!cell) return ''; + const numberFormater = currencyFormaterMap[format]; + + if (!numberFormater) return cell.data; + return numberFormater(new Decimal(cell.data).toNumber()); + }, [cell, format]); + + return
{value}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx new file mode 100644 index 0000000000..56c1e8d27b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx @@ -0,0 +1,84 @@ +import { + FieldId, + YDatabaseField, + YDatabaseFields, + YDatabaseRow, + YDoc, + YjsDatabaseKey, + YjsEditorKey, +} from '@/application/collab.type'; +import { useFieldSelector, parseRelationTypeOption } from '@/application/database-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; +import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import * as Y from 'yjs'; + +export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: string; rowId: string }) { + const { field } = useFieldSelector(fieldId); + const workspaceId = useId()?.workspaceId; + const rowIds = useMemo(() => (cell?.data.toJSON() as RelationCellData) ?? [], [cell?.data]); + const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined; + const databaseService = useContext(AFConfigContext)?.service?.databaseService; + const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState(undefined); + const [rows, setRows] = useState | null>(); + + useEffect(() => { + if (!workspaceId || !databaseId) return; + void databaseService?.getDatabase(workspaceId, databaseId).then(({ databaseDoc: doc, rows }) => { + const fields = doc + .getMap(YjsEditorKey.data_section) + .get(YjsEditorKey.database) + .get(YjsDatabaseKey.fields) as YDatabaseFields; + + fields.forEach((field, fieldId) => { + if ((field as YDatabaseField).get(YjsDatabaseKey.is_primary)) { + setDatabasePrimaryFieldId(fieldId); + } + }); + + setRows(rows); + }); + }, [workspaceId, databaseId, databaseService]); + + return ( +
+ {rowIds.map((rowId) => { + const rowDoc = rows?.get(rowId); + + return ( +
+ {rowDoc && databasePrimaryFieldId && ( + + )} +
+ ); + })} +
+ ); +} + +function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) { + const [text, setText] = useState(null); + + useEffect(() => { + const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + const cells = row.get(YjsDatabaseKey.cells); + const primaryCell = cells.get(fieldId); + + if (!primaryCell) return; + const observeHandler = () => { + setText(parseYDatabaseCellToCell(primaryCell).data as string); + }; + + observeHandler(); + + primaryCell.observe(observeHandler); + return () => { + primaryCell.unobserve(observeHandler); + }; + }, [rowDoc, fieldId]); + + return
{text}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx new file mode 100644 index 0000000000..d685b53cf9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx @@ -0,0 +1,43 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useRowMeta } from '@/application/database-yjs'; +import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; +import React, { useEffect, useMemo, useState } from 'react'; + +function RowCreateModifiedTime({ + rowId, + fieldId, + attrName, +}: { + rowId: string; + fieldId: string; + attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at; +}) { + const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); + const rowMeta = useRowMeta(rowId); + const [value, setValue] = useState(null); + + useEffect(() => { + if (!rowMeta) return; + const observeHandler = () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + setValue(rowMeta.get(attrName)); + }; + + observeHandler(); + + rowMeta.observe(observeHandler); + return () => { + rowMeta.unobserve(observeHandler); + }; + }, [rowMeta, attrName]); + + const time = useMemo(() => { + if (!value) return null; + return getDateTimeStr(value, false); + }, [value, getDateTimeStr]); + + return
{time}
; +} + +export default RowCreateModifiedTime; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx new file mode 100644 index 0000000000..a915d31a9b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx @@ -0,0 +1,32 @@ +import { FieldId } from '@/application/collab.type'; +import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs'; +import { Tag } from '@/components/_shared/tag'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import { SelectCell } from '@/components/database/components/cell/cell.type'; +import React, { useCallback, useMemo } from 'react'; + +export default function ({ cell, fieldId }: { cell?: SelectCell; rowId: string; fieldId: FieldId }) { + const selectOptionIds = useMemo(() => cell?.data.split(','), [cell]); + const { field } = useFieldSelector(fieldId); + const typeOption = useMemo(() => { + if (!field) return null; + return parseSelectOptionTypeOptions(field); + }, [field]); + + const renderSelectedOptions = useCallback( + (selected: string[]) => + selected.map((id) => { + const option = typeOption?.options?.find((option) => option.id === id); + + if (!option) return null; + return ; + }), + [typeOption] + ); + + return ( +
+ {selectOptionIds ? renderSelectedOptions(selectOptionIds) : null} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx new file mode 100644 index 0000000000..f9c8749258 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx @@ -0,0 +1,12 @@ +import { FieldId } from '@/application/collab.type'; +import { useReadOnly } from '@/application/database-yjs'; +import { TextCell } from '@/components/database/components/cell/cell.type'; +import React from 'react'; + +function TextCellComponent({ cell }: { cell?: TextCell; rowId: string; fieldId: FieldId }) { + const readOnly = useReadOnly(); + + return
{cell?.data}
; +} + +export default TextCellComponent; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx new file mode 100644 index 0000000000..e2d3d2c87f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx @@ -0,0 +1,37 @@ +import { FieldId } from '@/application/collab.type'; +import { useReadOnly } from '@/application/database-yjs'; +import { UrlCell } from '@/components/database/components/cell/cell.type'; +import { openUrl, processUrl } from '@/utils/url'; +import React, { useMemo } from 'react'; + +export default function ({ cell }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) { + const readOnly = useReadOnly(); + + const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]); + + const className = useMemo(() => { + const classList = ['select-text']; + + if (isUrl) { + classList.push('text-content-blue-400', 'underline', 'cursor-pointer'); + } else { + classList.push('cursor-text'); + } + + return classList.join(' '); + }, [isUrl]); + + return ( +
{ + if (!isUrl || !cell) return; + if (readOnly) { + void openUrl(cell.data, '_blank'); + } + }} + className={className} + > + {cell?.data} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts new file mode 100644 index 0000000000..d9e3564096 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.const.ts @@ -0,0 +1,25 @@ +import { SelectOptionColor } from '@/application/database-yjs'; + +export const SelectOptionColorMap = { + [SelectOptionColor.Purple]: '--tint-purple', + [SelectOptionColor.Pink]: '--tint-pink', + [SelectOptionColor.LightPink]: '--tint-red', + [SelectOptionColor.Orange]: '--tint-orange', + [SelectOptionColor.Yellow]: '--tint-yellow', + [SelectOptionColor.Lime]: '--tint-lime', + [SelectOptionColor.Green]: '--tint-green', + [SelectOptionColor.Aqua]: '--tint-aqua', + [SelectOptionColor.Blue]: '--tint-blue', +}; + +export const SelectOptionColorTextMap = { + [SelectOptionColor.Purple]: 'purpleColor', + [SelectOptionColor.Pink]: 'pinkColor', + [SelectOptionColor.LightPink]: 'lightPinkColor', + [SelectOptionColor.Orange]: 'orangeColor', + [SelectOptionColor.Yellow]: 'yellowColor', + [SelectOptionColor.Lime]: 'limeColor', + [SelectOptionColor.Green]: 'greenColor', + [SelectOptionColor.Aqua]: 'aquaColor', + [SelectOptionColor.Blue]: 'blueColor', +} as const; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts new file mode 100644 index 0000000000..4124381c06 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.parse.ts @@ -0,0 +1,46 @@ +import { YDatabaseCell, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { Cell, CheckboxCell, DateTimeCell } from './cell.type'; + +export function parseYDatabaseCommonCellToCell(cell: YDatabaseCell): Cell { + return { + createdAt: Number(cell.get(YjsDatabaseKey.created_at)), + lastModified: Number(cell.get(YjsDatabaseKey.last_modified)), + fieldType: parseInt(cell.get(YjsDatabaseKey.field_type)) as FieldType, + data: cell.get(YjsDatabaseKey.data), + }; +} + +export function parseYDatabaseCellToCell(cell: YDatabaseCell): Cell { + const fieldType = parseInt(cell.get(YjsDatabaseKey.field_type)); + + if (fieldType === FieldType.DateTime) { + return parseYDatabaseDateTimeCellToCell(cell); + } + + if (fieldType === FieldType.Checkbox) { + return parseYDatabaseCheckboxCellToCell(cell); + } + + return parseYDatabaseCommonCellToCell(cell); +} + +export function parseYDatabaseDateTimeCellToCell(cell: YDatabaseCell): DateTimeCell { + return { + ...parseYDatabaseCommonCellToCell(cell), + data: cell.get(YjsDatabaseKey.data) as string, + fieldType: FieldType.DateTime, + endTimestamp: cell.get(YjsDatabaseKey.end_timestamp), + includeTime: cell.get(YjsDatabaseKey.include_time), + isRange: cell.get(YjsDatabaseKey.is_range), + reminderId: cell.get(YjsDatabaseKey.reminder_id), + }; +} + +export function parseYDatabaseCheckboxCellToCell(cell: YDatabaseCell): CheckboxCell { + return { + ...parseYDatabaseCommonCellToCell(cell), + data: cell.get(YjsDatabaseKey.data) === 'Yes', + fieldType: FieldType.Checkbox, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts new file mode 100644 index 0000000000..185cca9409 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts @@ -0,0 +1,90 @@ +import { RowId } from '@/application/collab.type'; +import { DateFormat, SelectOption, TimeFormat } from '@/application/database-yjs'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { YArray } from 'yjs/dist/src/types/YArray'; + +export interface Cell { + createdAt: number; + lastModified: number; + fieldType: FieldType; + data: unknown; +} + +export interface TextCell extends Cell { + fieldType: FieldType.RichText; + data: string; +} + +export interface NumberCell extends Cell { + fieldType: FieldType.Number; + data: string; +} + +export interface CheckboxCell extends Cell { + fieldType: FieldType.Checkbox; + data: boolean; +} + +export interface UrlCell extends Cell { + fieldType: FieldType.URL; + data: string; +} + +export type SelectionId = string; + +export interface SelectCell extends Cell { + fieldType: FieldType.SingleSelect | FieldType.MultiSelect; + data: SelectionId; +} + +export interface DataTimeTypeOption { + timeFormat: TimeFormat; + dateFormat: DateFormat; +} + +export interface DateTimeCell extends Cell { + fieldType: FieldType.DateTime; + data: string; + endTimestamp?: string; + includeTime?: boolean; + isRange?: boolean; + reminderId?: string; +} + +export interface TimeStampCell extends Cell { + fieldType: FieldType.LastEditedTime | FieldType.CreatedTime; + data: TimestampCellData; +} + +export interface DateTimeCellData { + date?: string; + time?: string; + timestamp?: number; + includeTime?: boolean; + endDate?: string; + endTime?: string; + endTimestamp?: number; + isRange?: boolean; +} + +export interface TimestampCellData { + dataTime?: string; + timestamp?: number; +} + +export interface ChecklistCell extends Cell { + fieldType: FieldType.Checklist; + data: string; +} + +export interface RelationCell extends Cell { + fieldType: FieldType.Relation; + data: YArray; +} + +export type RelationCellData = RowId[]; + +export interface ChecklistCellData { + selected_option_ids?: string[]; + options?: SelectOption[]; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/index.ts new file mode 100644 index 0000000000..2440976340 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/index.ts @@ -0,0 +1 @@ +export * from './Cell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx new file mode 100644 index 0000000000..6b4a836597 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseActions.tsx @@ -0,0 +1,35 @@ +import { useFiltersSelector, useSortsSelector } from '@/application/database-yjs'; +import { useConditionsContext } from '@/components/database/components/conditions/context'; +import { TextButton } from '@/components/database/components/tabs/TextButton'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export function DatabaseActions() { + const { t } = useTranslation(); + const sorts = useSortsSelector(); + const filter = useFiltersSelector(); + const conditionsContext = useConditionsContext(); + + return ( +
+ { + conditionsContext?.toggleExpanded(); + }} + color={filter.length > 0 ? 'primary' : 'inherit'} + > + {t('grid.settings.filter')} + + { + conditionsContext?.toggleExpanded(); + }} + color={sorts.length > 0 ? 'primary' : 'inherit'} + > + {t('grid.settings.sort')} + +
+ ); +} + +export default DatabaseActions; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx new file mode 100644 index 0000000000..fc36c470d6 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx @@ -0,0 +1,33 @@ +import { useFiltersSelector, useSortsSelector } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import { useConditionsContext } from '@/components/database/components/conditions/context'; +import React from 'react'; +import Filters from 'src/components/database/components/filters/Filters'; +import Sorts from 'src/components/database/components/sorts/Sorts'; + +export function DatabaseConditions() { + const conditionsContext = useConditionsContext(); + const expanded = conditionsContext?.expanded ?? false; + const sorts = useSortsSelector(); + const filters = useFiltersSelector(); + + return ( +
+ + + {sorts.length > 0 && filters.length > 0 &&
} + + +
+ ); +} + +export default DatabaseConditions; diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts b/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts new file mode 100644 index 0000000000..aadb5007af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/context.ts @@ -0,0 +1,12 @@ +import { createContext, useContext } from 'react'; + +interface DatabaseConditionsContextType { + expanded: boolean; + toggleExpanded: () => void; +} + +export function useConditionsContext() { + return useContext(DatabaseConditionsContext); +} + +export const DatabaseConditionsContext = createContext(undefined); diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts b/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts new file mode 100644 index 0000000000..7b30286c5c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseActions'; +export * from './DatabaseConditions'; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx b/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx new file mode 100644 index 0000000000..3ff135e8f7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/FieldDisplay.tsx @@ -0,0 +1,20 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, useFieldSelector } from '@/application/database-yjs'; +import { FieldTypeIcon } from '@/components/database/components/field/FieldTypeIcon'; +import React from 'react'; + +export function FieldDisplay({ fieldId }: { fieldId: FieldId }) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + if (!field) return null; + + return ( +
+ + {field?.get(YjsDatabaseKey.name)} +
+ ); +} + +export default FieldDisplay; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx new file mode 100644 index 0000000000..3749e21afd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/FieldTypeIcon.tsx @@ -0,0 +1,33 @@ +import { FieldType } from '@/application/database-yjs/database.type'; +import { FC, memo } from 'react'; +import { ReactComponent as TextSvg } from '$icons/16x/text.svg'; +import { ReactComponent as NumberSvg } from '$icons/16x/number.svg'; +import { ReactComponent as DateSvg } from '$icons/16x/date.svg'; +import { ReactComponent as SingleSelectSvg } from '$icons/16x/single_select.svg'; +import { ReactComponent as MultiSelectSvg } from '$icons/16x/multiselect.svg'; +import { ReactComponent as ChecklistSvg } from '$icons/16x/checklist.svg'; +import { ReactComponent as CheckboxSvg } from '$icons/16x/checkbox.svg'; +import { ReactComponent as URLSvg } from '$icons/16x/url.svg'; +import { ReactComponent as LastEditedTimeSvg } from '$icons/16x/last_modified.svg'; +import { ReactComponent as CreatedSvg } from '$icons/16x/created_at.svg'; +import { ReactComponent as RelationSvg } from '$icons/16x/relation.svg'; + +export const FieldTypeSvgMap: Record>> = { + [FieldType.RichText]: TextSvg, + [FieldType.Number]: NumberSvg, + [FieldType.DateTime]: DateSvg, + [FieldType.SingleSelect]: SingleSelectSvg, + [FieldType.MultiSelect]: MultiSelectSvg, + [FieldType.Checkbox]: CheckboxSvg, + [FieldType.URL]: URLSvg, + [FieldType.Checklist]: ChecklistSvg, + [FieldType.LastEditedTime]: LastEditedTimeSvg, + [FieldType.CreatedTime]: CreatedSvg, + [FieldType.Relation]: RelationSvg, +}; + +export const FieldTypeIcon: FC<{ type: FieldType; className?: string }> = memo(({ type, ...props }) => { + const Svg = FieldTypeSvgMap[type]; + + return ; +}); diff --git a/frontend/appflowy_web_app/src/components/database/components/field/index.ts b/frontend/appflowy_web_app/src/components/database/components/field/index.ts new file mode 100644 index 0000000000..85ff96da07 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/index.ts @@ -0,0 +1,2 @@ +export * from './FieldTypeIcon'; +export * from './FieldDisplay'; diff --git a/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx b/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx new file mode 100644 index 0000000000..353ef5d349 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/select-option/SelectOptionList.tsx @@ -0,0 +1,30 @@ +import { parseSelectOptionTypeOptions, SelectOption, useFieldSelector } from '@/application/database-yjs'; +import { Tag } from '@/components/_shared/tag'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import React, { useCallback, useMemo } from 'react'; +import { ReactComponent as CheckIcon } from '$icons/16x/check.svg'; + +export function SelectOptionList({ fieldId, selectedIds }: { fieldId: string; selectedIds: string[] }) { + const { field } = useFieldSelector(fieldId); + const typeOption = useMemo(() => { + if (!field) return null; + return parseSelectOptionTypeOptions(field); + }, [field]); + + const renderOption = useCallback( + (option: SelectOption) => { + const isSelected = selectedIds.includes(option.id); + + return ( +
+ + {isSelected && } +
+ ); + }, + [selectedIds] + ); + + if (!field || !typeOption) return null; + return
{typeOption.options.map(renderOption)}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts b/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts new file mode 100644 index 0000000000..20465070b4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/field/select-option/index.ts @@ -0,0 +1 @@ +export * from './SelectOptionList'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx new file mode 100644 index 0000000000..3fe0c4daf3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/Filter.tsx @@ -0,0 +1,57 @@ +import { useFilterSelector } from '@/application/database-yjs'; +import { Popover } from '@/components/_shared/popover'; +import { FilterContentOverview } from './overview'; +import React, { useState } from 'react'; +import { FieldDisplay } from '@/components/database/components/field'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; +import { FilterMenu } from './filter-menu'; + +function Filter({ filterId }: { filterId: string }) { + const filter = useFilterSelector(filterId); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + if (!filter) return null; + + return ( + <> +
{ + setAnchorEl(e.currentTarget); + }} + className={ + 'flex cursor-pointer flex-nowrap items-center gap-1 rounded-full border border-line-divider py-1 px-2 hover:border-fill-default hover:text-fill-default hover:shadow-sm' + } + > +
+ +
+ +
+ +
+ +
+ {open && ( + { + setAnchorEl(null); + }} + slotProps={{ + paper: { + style: { + maxHeight: '260px', + }, + }, + }} + > + + + )} + + ); +} + +export default Filter; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx new file mode 100644 index 0000000000..41f54f8cac --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/Filters.tsx @@ -0,0 +1,32 @@ +import { useFiltersSelector, useReadOnly } from '@/application/database-yjs'; +import Filter from '@/components/database/components/filters/Filter'; +import Button from '@mui/material/Button'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as AddFilterSvg } from '$icons/16x/add.svg'; + +export function Filters() { + const filters = useFiltersSelector(); + const { t } = useTranslation(); + const readOnly = useReadOnly(); + + return ( + <> + {filters.map((filterId) => ( + + ))} + + + ); +} + +export default Filters; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx new file mode 100644 index 0000000000..851e811499 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/CheckboxFilterMenu.tsx @@ -0,0 +1,33 @@ +import { CheckboxFilter, CheckboxFilterCondition } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function CheckboxFilterMenu({ filter }: { filter: CheckboxFilter }) { + const { t } = useTranslation(); + + const conditions = useMemo( + () => [ + { + value: CheckboxFilterCondition.IsChecked, + text: t('grid.checkboxFilter.isChecked'), + }, + { + value: CheckboxFilterCondition.IsUnChecked, + text: t('grid.checkboxFilter.isUnchecked'), + }, + ], + [t] + ); + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + return ( +
+ +
+ ); +} + +export default CheckboxFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx new file mode 100644 index 0000000000..5d6398b242 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/ChecklistFilterMenu.tsx @@ -0,0 +1,33 @@ +import { ChecklistFilter, ChecklistFilterCondition } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function ChecklistFilterMenu({ filter }: { filter: ChecklistFilter }) { + const { t } = useTranslation(); + + const conditions = useMemo( + () => [ + { + value: ChecklistFilterCondition.IsComplete, + text: t('grid.checklistFilter.isComplete'), + }, + { + value: ChecklistFilterCondition.IsIncomplete, + text: t('grid.checklistFilter.isIncomplted'), + }, + ], + [t] + ); + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + return ( +
+ +
+ ); +} + +export default ChecklistFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx new file mode 100644 index 0000000000..e5784b44f5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FieldMenuTitle.tsx @@ -0,0 +1,23 @@ +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; +import { FieldDisplay } from '@/components/database/components/field'; +import React from 'react'; + +function FieldMenuTitle({ fieldId, selectedConditionText }: { fieldId: string; selectedConditionText: string }) { + return ( +
+
+ +
+
+
+
+ {selectedConditionText} +
+ +
+
+
+ ); +} + +export default FieldMenuTitle; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx new file mode 100644 index 0000000000..720dac3d3d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/FilterMenu.tsx @@ -0,0 +1,39 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, Filter, SelectOptionFilter, useFieldSelector } from '@/application/database-yjs'; +import CheckboxFilterMenu from './CheckboxFilterMenu'; +import ChecklistFilterMenu from './ChecklistFilterMenu'; +import MultiSelectOptionFilterMenu from './MultiSelectOptionFilterMenu'; +import NumberFilterMenu from './NumberFilterMenu'; +import SingleSelectOptionFilterMenu from './SingleSelectOptionFilterMenu'; +import TextFilterMenu from './TextFilterMenu'; +import React, { useMemo } from 'react'; + +export function FilterMenu({ filter }: { filter: Filter }) { + const { field } = useFieldSelector(filter?.fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + const menu = useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.Checkbox: + return ; + case FieldType.Checklist: + return ; + case FieldType.Number: + return ; + case FieldType.MultiSelect: + return ; + case FieldType.SingleSelect: + return ; + default: + return null; + } + }, [field, fieldType, filter]); + + return menu; +} + +export default FilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx new file mode 100644 index 0000000000..68def09bb8 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/MultiSelectOptionFilterMenu.tsx @@ -0,0 +1,56 @@ +import { SelectOptionFilter, SelectOptionFilterCondition } from '@/application/database-yjs'; +import { SelectOptionList } from '@/components/database/components/field/select-option'; +import FieldMenuTitle from './FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function MultiSelectOptionFilterMenu({ filter }: { filter: SelectOptionFilter }) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + return [ + { + value: SelectOptionFilterCondition.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterCondition.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterCondition.OptionContains, + text: t('grid.selectOptionFilter.contains'), + }, + { + value: SelectOptionFilterCondition.OptionDoesNotContain, + text: t('grid.selectOptionFilter.doesNotContain'), + }, + { + value: SelectOptionFilterCondition.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterCondition.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displaySelectOptionList = useMemo(() => { + return ![SelectOptionFilterCondition.OptionIsEmpty, SelectOptionFilterCondition.OptionIsNotEmpty].includes( + filter.condition + ); + }, [filter.condition]); + + return ( +
+ + {displaySelectOptionList && } +
+ ); +} + +export default MultiSelectOptionFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx new file mode 100644 index 0000000000..fdd8963ef2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/NumberFilterMenu.tsx @@ -0,0 +1,74 @@ +import { NumberFilter, NumberFilterCondition, useReadOnly } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import { TextField } from '@mui/material'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterMenu({ filter }: { filter: NumberFilter }) { + const { t } = useTranslation(); + const readOnly = useReadOnly(); + const conditions = useMemo(() => { + return [ + { + value: NumberFilterCondition.Equal, + text: t('grid.numberFilter.equal'), + }, + { + value: NumberFilterCondition.NotEqual, + text: t('grid.numberFilter.notEqual'), + }, + { + value: NumberFilterCondition.GreaterThan, + text: t('grid.numberFilter.greaterThan'), + }, + { + value: NumberFilterCondition.LessThan, + text: t('grid.numberFilter.lessThan'), + }, + { + value: NumberFilterCondition.GreaterThanOrEqualTo, + text: t('grid.numberFilter.greaterThanOrEqualTo'), + }, + { + value: NumberFilterCondition.LessThanOrEqualTo, + text: t('grid.numberFilter.lessThanOrEqualTo'), + }, + { + value: NumberFilterCondition.NumberIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: NumberFilterCondition.NumberIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displayTextField = useMemo(() => { + return ![NumberFilterCondition.NumberIsEmpty, NumberFilterCondition.NumberIsNotEmpty].includes(filter.condition); + }, [filter.condition]); + + return ( +
+ + {displayTextField && ( + + )} +
+ ); +} + +export default NumberFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx new file mode 100644 index 0000000000..217ad8d1ae --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/SingleSelectOptionFilterMenu.tsx @@ -0,0 +1,48 @@ +import { SelectOptionFilter, SelectOptionFilterCondition } from '@/application/database-yjs'; +import { SelectOptionList } from '@/components/database/components/field/select-option'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function SingleSelectOptionFilterMenu({ filter }: { filter: SelectOptionFilter }) { + const { t } = useTranslation(); + const conditions = useMemo(() => { + return [ + { + value: SelectOptionFilterCondition.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterCondition.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterCondition.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterCondition.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displaySelectOptionList = useMemo(() => { + return ![SelectOptionFilterCondition.OptionIsEmpty, SelectOptionFilterCondition.OptionIsNotEmpty].includes( + filter.condition + ); + }, [filter.condition]); + + return ( +
+ + {displaySelectOptionList && } +
+ ); +} + +export default SingleSelectOptionFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx new file mode 100644 index 0000000000..f3ca7690af --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/TextFilterMenu.tsx @@ -0,0 +1,74 @@ +import { TextFilter, TextFilterCondition, useReadOnly } from '@/application/database-yjs'; +import FieldMenuTitle from '@/components/database/components/filters/filter-menu/FieldMenuTitle'; +import { TextField } from '@mui/material'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function TextFilterMenu({ filter }: { filter: TextFilter }) { + const { t } = useTranslation(); + const readOnly = useReadOnly(); + const conditions = useMemo(() => { + return [ + { + value: TextFilterCondition.TextContains, + text: t('grid.textFilter.contains'), + }, + { + value: TextFilterCondition.TextDoesNotContain, + text: t('grid.textFilter.doesNotContain'), + }, + { + value: TextFilterCondition.TextStartsWith, + text: t('grid.textFilter.startWith'), + }, + { + value: TextFilterCondition.TextEndsWith, + text: t('grid.textFilter.endsWith'), + }, + { + value: TextFilterCondition.TextIs, + text: t('grid.textFilter.is'), + }, + { + value: TextFilterCondition.TextIsNot, + text: t('grid.textFilter.isNot'), + }, + { + value: TextFilterCondition.TextIsEmpty, + text: t('grid.textFilter.isEmpty'), + }, + { + value: TextFilterCondition.TextIsNotEmpty, + text: t('grid.textFilter.isNotEmpty'), + }, + ]; + }, [t]); + + const selectedCondition = useMemo(() => { + return conditions.find((c) => c.value === filter.condition); + }, [filter.condition, conditions]); + + const displayTextField = useMemo(() => { + return ![TextFilterCondition.TextIsEmpty, TextFilterCondition.TextIsNotEmpty].includes(filter.condition); + }, [filter.condition]); + + return ( +
+ + {displayTextField && ( + + )} +
+ ); +} + +export default TextFilterMenu; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts new file mode 100644 index 0000000000..fc54ea0f3a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/filter-menu/index.ts @@ -0,0 +1 @@ +export * from './FilterMenu'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/index.ts new file mode 100644 index 0000000000..c7b59bcd2f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/index.ts @@ -0,0 +1 @@ +export * from './Filters'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx new file mode 100644 index 0000000000..d3a30e1844 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/DateFilterContentOverview.tsx @@ -0,0 +1,51 @@ +import { DateFilter, DateFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import dayjs from 'dayjs'; + +function DateFilterContentOverview({ filter }: { filter: DateFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.timestamp) return ''; + + let startStr = ''; + let endStr = ''; + + if (filter.start) { + const end = filter.end ?? filter.start; + const moreThanOneYear = dayjs.unix(end).diff(dayjs.unix(filter.start), 'year') > 1; + const format = moreThanOneYear ? 'MMM D, YYYY' : 'MMM D'; + + startStr = dayjs.unix(filter.start).format(format); + endStr = dayjs.unix(end).format(format); + } + + const timestamp = dayjs.unix(filter.timestamp).format('MMM D'); + + switch (filter.condition) { + case DateFilterCondition.DateIs: + return `: ${timestamp}`; + case DateFilterCondition.DateBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.before')} ${timestamp}`; + case DateFilterCondition.DateAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.after')} ${timestamp}`; + case DateFilterCondition.DateOnOrBefore: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrBefore')} ${timestamp}`; + case DateFilterCondition.DateOnOrAfter: + return `: ${t('grid.dateFilter.choicechipPrefix.onOrAfter')} ${timestamp}`; + case DateFilterCondition.DateWithIn: + return `: ${startStr} - ${endStr}`; + case DateFilterCondition.DateIsEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isEmpty')}`; + case DateFilterCondition.DateIsNotEmpty: + return `: ${t('grid.dateFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [filter, t]); + + return <>{value}; +} + +export default DateFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx new file mode 100644 index 0000000000..9f6d1ea188 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/FilterContentOverview.tsx @@ -0,0 +1,59 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { + CheckboxFilterCondition, + ChecklistFilterCondition, + FieldType, + Filter, + SelectOptionFilter, + useFieldSelector, +} from '@/application/database-yjs'; +import DateFilterContentOverview from '@/components/database/components/filters/overview/DateFilterContentOverview'; +import NumberFilterContentOverview from '@/components/database/components/filters/overview/NumberFilterContentOverview'; +import SelectFilterContentOverview from '@/components/database/components/filters/overview/SelectFilterContentOverview'; +import TextFilterContentOverview from '@/components/database/components/filters/overview/TextFilterContentOverview'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function FilterContentOverview({ filter }: { filter: Filter }) { + const { field } = useFieldSelector(filter?.fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const { t } = useTranslation(); + + return useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.RichText: + case FieldType.URL: + return ; + case FieldType.Number: + return ; + case FieldType.DateTime: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return ; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return ; + case FieldType.Checkbox: + return ( + <> + : {t('grid.checkboxFilter.choicechipPrefix.is')}{' '} + {filter.condition === CheckboxFilterCondition.IsChecked + ? t('grid.checkboxFilter.isChecked') + : t('grid.checkboxFilter.isUnchecked')} + + ); + case FieldType.Checklist: + return ( + <> + :{' '} + {filter.condition === ChecklistFilterCondition.IsComplete + ? t('grid.checklistFilter.isComplete') + : t('grid.checklistFilter.isIncomplted')} + + ); + default: + return null; + } + }, [field, fieldType, filter, t]); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx new file mode 100644 index 0000000000..64864541e7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/NumberFilterContentOverview.tsx @@ -0,0 +1,38 @@ +import { NumberFilter, NumberFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NumberFilterContentOverview({ filter }: { filter: NumberFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.content) { + return ''; + } + + const content = parseInt(filter.content); + + switch (filter.condition) { + case NumberFilterCondition.Equal: + return `= ${content}`; + case NumberFilterCondition.NotEqual: + return `!= ${content}`; + case NumberFilterCondition.GreaterThan: + return `> ${content}`; + case NumberFilterCondition.GreaterThanOrEqualTo: + return `>= ${content}`; + case NumberFilterCondition.LessThan: + return `< ${content}`; + case NumberFilterCondition.LessThanOrEqualTo: + return `<= ${content}`; + case NumberFilterCondition.NumberIsEmpty: + return t('grid.textFilter.isEmpty'); + case NumberFilterCondition.NumberIsNotEmpty: + return t('grid.textFilter.isNotEmpty'); + } + }, [filter.condition, filter.content, t]); + + return <>{value}; +} + +export default NumberFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx new file mode 100644 index 0000000000..64e8ddc00c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/SelectFilterContentOverview.tsx @@ -0,0 +1,42 @@ +import { YDatabaseField } from '@/application/collab.type'; +import { + parseSelectOptionTypeOptions, + SelectOptionFilter, + SelectOptionFilterCondition, +} from '@/application/database-yjs'; +import React, { useMemo } from 'react'; + +import { useTranslation } from 'react-i18next'; + +function SelectFilterContentOverview({ filter, field }: { filter: SelectOptionFilter; field: YDatabaseField }) { + const typeOption = parseSelectOptionTypeOptions(field); + const { t } = useTranslation(); + const value = useMemo(() => { + if (!filter.optionIds?.length) return ''; + + const options = filter.optionIds + .map((optionId) => { + const option = typeOption?.options?.find((option) => option.id === optionId); + + return option?.name; + }) + .join(', '); + + switch (filter.condition) { + case SelectOptionFilterCondition.OptionIs: + return `: ${options}`; + case SelectOptionFilterCondition.OptionIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${options}`; + case SelectOptionFilterCondition.OptionIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case SelectOptionFilterCondition.OptionIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [filter.condition, filter.optionIds, t, typeOption?.options]); + + return <>{value}; +} + +export default SelectFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx b/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx new file mode 100644 index 0000000000..fc03b39c96 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/TextFilterContentOverview.tsx @@ -0,0 +1,33 @@ +import { TextFilter, TextFilterCondition } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function TextFilterContentOverview({ filter }: { filter: TextFilter }) { + const { t } = useTranslation(); + + const value = useMemo(() => { + if (!filter.content) return ''; + switch (filter.condition) { + case TextFilterCondition.TextContains: + case TextFilterCondition.TextIs: + return `: ${filter.content}`; + case TextFilterCondition.TextDoesNotContain: + case TextFilterCondition.TextIsNot: + return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${filter.content}`; + case TextFilterCondition.TextStartsWith: + return `: ${t('grid.textFilter.choicechipPrefix.startWith')} ${filter.content}`; + case TextFilterCondition.TextEndsWith: + return `: ${t('grid.textFilter.choicechipPrefix.endWith')} ${filter.content}`; + case TextFilterCondition.TextIsEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; + case TextFilterCondition.TextIsNotEmpty: + return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; + default: + return ''; + } + }, [t, filter]); + + return <>{value}; +} + +export default TextFilterContentOverview; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts b/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts new file mode 100644 index 0000000000..47e041409e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/overview/index.ts @@ -0,0 +1 @@ +export * from './FilterContentOverview'; diff --git a/frontend/appflowy_web_app/src/components/database/components/filters/package.json b/frontend/appflowy_web_app/src/components/database/components/filters/package.json new file mode 100644 index 0000000000..e56f3198c9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/filters/package.json @@ -0,0 +1,14 @@ +{ + "name": "filters", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/qinluhe/AppFlowy.git" + }, + "private": true +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx new file mode 100644 index 0000000000..b9a5017b38 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx @@ -0,0 +1,64 @@ +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { useRowMeta } from '@/application/database-yjs'; +import { useFieldSelector } from '@/application/database-yjs/selector'; +import { Cell } from '@/components/database/components/cell'; +import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; +import React, { useEffect, useState } from 'react'; + +export interface GridCellProps { + rowId: string; + fieldId: FieldId; + columnIndex: number; + rowIndex: number; + onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void; +} + +export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) { + const ref = React.useRef(null); + const field = useFieldSelector(fieldId); + const row = useRowMeta(rowId); + const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId); + const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined)); + + useEffect(() => { + if (!cell) return; + setCellValue(parseYDatabaseCellToCell(cell)); + const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell)); + + cell.observe(observerEvent); + + return () => { + cell.unobserve(observerEvent); + }; + }, [cell]); + + useEffect(() => { + const el = ref.current; + + if (!el) return; + + const observer = new ResizeObserver(() => { + if (onResize) { + onResize(rowIndex, columnIndex, { + width: el.offsetWidth, + height: el.offsetHeight, + }); + } + }); + + observer.observe(el); + + return () => { + observer.disconnect(); + }; + }, [columnIndex, onResize, rowIndex]); + + if (!field) return null; + return ( +
+ +
+ ); +} + +export default GridCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts new file mode 100644 index 0000000000..2b6d663ef5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts @@ -0,0 +1 @@ +export * from './GridCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx new file mode 100644 index 0000000000..88c6fae84e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx @@ -0,0 +1,35 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { Column, useFieldSelector } from '@/application/database-yjs/selector'; +import { FieldTypeIcon } from '@/components/database/components/field'; +import React, { useMemo } from 'react'; + +export function GridColumn({ column, index }: { column: Column; index: number }) { + const { field } = useFieldSelector(column.fieldId); + const name = field?.get(YjsDatabaseKey.name); + const type = useMemo(() => { + const type = field?.get(YjsDatabaseKey.type); + + if (!type) return FieldType.RichText; + + return parseInt(type) as FieldType; + }, [field]); + + return ( +
+
+ +
+
{name}
+
+ ); +} + +export default GridColumn; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts new file mode 100644 index 0000000000..6de83c7026 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts @@ -0,0 +1,2 @@ +export * from './GridColumn'; +export * from './useRenderColumns'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx new file mode 100644 index 0000000000..c0041b5c5e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx @@ -0,0 +1,73 @@ +import { FieldId } from '@/application/collab.type'; +import { FieldVisibility } from '@/application/database-yjs/database.type'; +import { useGridColumnsSelector } from '@/application/database-yjs/selector'; +import { useCallback, useMemo } from 'react'; + +export enum GridColumnType { + Action, + Field, + NewProperty, +} + +const defaultVisibilitys = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; + +export type RenderColumn = { + type: GridColumnType; + visibility?: FieldVisibility; + fieldId?: FieldId; + width: number; + wrap?: boolean; +}; + +export function useRenderColumns(viewId: string) { + const columns = useGridColumnsSelector(viewId, defaultVisibilitys); + + console.log('columns', columns); + const renderColumns = useMemo(() => { + const fields = columns.map((column) => ({ + ...column, + type: GridColumnType.Field, + })); + + return [ + { + type: GridColumnType.Action, + width: 96, + }, + ...fields, + { + type: GridColumnType.NewProperty, + width: 150, + }, + { + type: GridColumnType.Action, + width: 96, + }, + ].filter(Boolean) as RenderColumn[]; + }, [columns]); + + const columnWidth = useCallback( + (index: number, containerWidth: number) => { + const { type, width } = renderColumns[index]; + + if (type === GridColumnType.NewProperty) { + const totalWidth = renderColumns.reduce((acc, column) => acc + column.width, 0); + const remainingWidth = containerWidth - totalWidth; + + return remainingWidth > 0 ? remainingWidth + width : width; + } + + if (type === GridColumnType.Action && containerWidth < 800) { + return 16; + } + + return width; + }, + [renderColumns] + ); + + return { + columns: renderColumns, + columnWidth, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx new file mode 100644 index 0000000000..64d0c39117 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx @@ -0,0 +1,73 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import { GridChildComponentProps, VariableSizeGrid } from 'react-window'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridColumnType, RenderColumn, GridColumn } from '../grid-column'; + +export interface GridHeaderProps { + onScrollLeft: (left: number) => void; + columnWidth: (index: number, totalWidth: number) => number; + columns: RenderColumn[]; + scrollLeft?: number; +} + +export const GridHeader = ({ scrollLeft, onScrollLeft, columnWidth, columns }: GridHeaderProps) => { + const ref = useRef(null); + const Cell = useCallback(({ columnIndex, style, data }: GridChildComponentProps) => { + const column = data[columnIndex]; + + // Placeholder for Action toolbar + if (!column || column.type === GridColumnType.Action) return
; + + if (column.type === GridColumnType.Field) { + return ( +
+ +
+ ); + } + + return
; + }, []); + + useEffect(() => { + if (ref.current) { + ref.current.scrollTo({ scrollLeft }); + } + }, [scrollLeft]); + + useEffect(() => { + if (ref.current) { + ref.current?.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + } + }, [columns]); + + return ( +
+ + {({ height, width }: { height: number; width: number }) => { + return ( + 36} + rowCount={1} + columnCount={columns.length} + columnWidth={(index) => columnWidth(index, width)} + ref={ref} + onScroll={(props) => { + onScrollLeft(props.scrollLeft); + }} + itemData={columns} + style={{ overscrollBehavior: 'none' }} + > + {Cell} + + ); + }} + +
+ ); +}; + +export default GridHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts new file mode 100644 index 0000000000..44d8082bd7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts @@ -0,0 +1 @@ +export * from './GridHeader'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx new file mode 100644 index 0000000000..650ed3bfbe --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx @@ -0,0 +1,41 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useDatabaseView } from '@/application/database-yjs'; +import { CalculationType } from '@/application/database-yjs/database.type'; +import { CalculationCell } from '@/components/database/components/calculation-cell'; +import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type'; +import React, { useEffect, useState } from 'react'; + +export interface GridCalculateRowCellProps { + fieldId: string; +} + +export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) { + const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations); + const [calculation, setCalculation] = useState(); + + useEffect(() => { + if (!calculations) return; + const observerHandle = () => { + calculations.forEach((calculation) => { + if (calculation.get(YjsDatabaseKey.field_id) === fieldId) { + setCalculation({ + id: calculation.get(YjsDatabaseKey.id), + fieldId: calculation.get(YjsDatabaseKey.field_id), + value: calculation.get(YjsDatabaseKey.calculation_value), + type: Number(calculation.get(YjsDatabaseKey.type)) as CalculationType, + }); + } + }); + }; + + observerHandle(); + calculations.observeDeep(observerHandle); + + return () => { + calculations.unobserveDeep(observerHandle); + }; + }, [calculations, fieldId]); + return ; +} + +export default GridCalculateRowCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx new file mode 100644 index 0000000000..ef4be68406 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx @@ -0,0 +1,28 @@ +import { GridColumnType } from '@/components/database/components/grid-column'; +import React from 'react'; +import GridCell from 'src/components/database/components/grid-cell/GridCell'; + +export interface GridRowCellProps { + rowId: string; + fieldId?: string; + type: GridColumnType; + columnIndex: number; + rowIndex: number; + onResize?: (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => void; +} + +export function GridRowCell({ onResize, rowIndex, columnIndex, rowId, fieldId, type }: GridRowCellProps) { + if (type === GridColumnType.Field && fieldId) { + return ( + + ); + } + + if (type === GridColumnType.Action) { + return null; + } + + return null; +} + +export default GridRowCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts new file mode 100644 index 0000000000..365c3f467e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts @@ -0,0 +1,3 @@ +export * from './GridCalculateRowCell'; +export * from './GridRowCell'; +export * from './useRenderRows'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx new file mode 100644 index 0000000000..e5038cafff --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx @@ -0,0 +1,44 @@ +import { useReadOnly } from '@/application/database-yjs'; +import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const'; +import { useGridRowsSelector } from '@/application/database-yjs/selector'; +import { useMemo } from 'react'; + +export enum RenderRowType { + Row = 'row', + NewRow = 'new-row', + CalculateRow = 'calculate-row', +} + +export type RenderRow = { + type: RenderRowType; + rowId?: string; + height?: number; +}; + +export function useRenderRows() { + const rows = useGridRowsSelector(); + const readOnly = useReadOnly(); + + const renderRows = useMemo(() => { + return [ + ...rows.map((row) => ({ + type: RenderRowType.Row, + rowId: row.id, + height: row.height, + })), + + !readOnly && { + type: RenderRowType.NewRow, + height: DEFAULT_ROW_HEIGHT, + }, + { + type: RenderRowType.CalculateRow, + height: DEFAULT_ROW_HEIGHT, + }, + ].filter(Boolean) as RenderRow[]; + }, [readOnly, rows]); + + return { + rows: renderRows, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx new file mode 100644 index 0000000000..dd3ed13bfe --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx @@ -0,0 +1,177 @@ +import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const'; +import { AFScroller } from '@/components/_shared/scroller'; +import { GridColumnType, RenderColumn } from '@/components/database/components/grid-column'; +import { + GridCalculateRowCell, + GridRowCell, + RenderRowType, + useRenderRows, +} from '@/components/database/components/grid-row'; +import React, { useCallback, useEffect, useRef } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { GridChildComponentProps, VariableSizeGrid } from 'react-window'; + +export interface GridTableProps { + onScrollLeft: (left: number) => void; + columnWidth: (index: number, totalWidth: number) => number; + + columns: RenderColumn[]; + scrollLeft?: number; + viewId: string; +} + +export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: GridTableProps) => { + const ref = useRef(null); + const { rows } = useRenderRows(); + const rowHeights = useRef<{ [key: string]: number }>({}); + + useEffect(() => { + if (ref.current) { + console.log(ref.current, scrollLeft); + ref.current.scrollTo({ scrollLeft }); + } + }, [scrollLeft]); + + useEffect(() => { + if (ref.current) { + ref.current.resetAfterIndices({ columnIndex: 0, rowIndex: 0 }); + } + }, [columns]); + + const rowHeight = useCallback( + (index: number) => { + const row = rows[index]; + + if (!row || !row.rowId) return DEFAULT_ROW_HEIGHT; + + return rowHeights.current[row.rowId] || DEFAULT_ROW_HEIGHT; + }, + [rows] + ); + + const setRowHeight = useCallback( + (index: number, height: number) => { + const row = rows[index]; + const rowId = row.rowId; + + if (!row || !rowId) return; + const oldHeight = rowHeights.current[rowId]; + + rowHeights.current[rowId] = Math.max(oldHeight || DEFAULT_ROW_HEIGHT, height); + if (oldHeight !== height) { + ref.current?.resetAfterRowIndex(index, true); + } + }, + [rows] + ); + + const onResize = useCallback( + (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => { + setRowHeight(rowIndex, size.height); + }, + [setRowHeight] + ); + + const getItemKey = useCallback( + ({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => { + const row = rows[rowIndex]; + const column = columns[columnIndex]; + const fieldId = column.fieldId; + + if (row.type === RenderRowType.Row) { + if (fieldId) { + return `${row.rowId}:${fieldId}`; + } + + return `${rowIndex}:${columnIndex}`; + } + + if (fieldId) { + return `${row.type}:${fieldId}`; + } + + return `${rowIndex}:${columnIndex}`; + }, + [columns, rows] + ); + const Cell = useCallback( + ({ columnIndex, rowIndex, style, data }: GridChildComponentProps) => { + const row = data.rows[rowIndex]; + const column = data.columns[columnIndex] as RenderColumn; + + const classList = ['flex', 'items-center', 'overflow-hidden']; + + if (column.wrap) { + classList.push('whitespace-pre-wrap', 'break-words'); + } else { + classList.push('whitespace-nowrap'); + } + + if (column.type === GridColumnType.Field) { + classList.push('border-b', 'border-l', 'border-line-divider', 'px-2'); + } + + if (column.type === GridColumnType.NewProperty) { + classList.push('border-b', 'border-line-divider', 'px-2'); + } + + if (row.type === RenderRowType.Row) { + return ( +
+ +
+ ); + } + + if (row.type === RenderRowType.CalculateRow && column.fieldId) { + return ( +
+ +
+ ); + } + + return
; + }, + [onResize] + ); + + return ( + + {({ height, width }: { height: number; width: number }) => ( + onScrollLeft(scrollLeft)} + rowCount={rows.length} + columnCount={columns.length} + columnWidth={(index) => columnWidth(index, width)} + rowHeight={rowHeight} + overscanRowCount={5} + overscanColumnCount={5} + style={{ + overscrollBehavior: 'none', + }} + itemKey={getItemKey} + itemData={{ columns, rows }} + outerElementType={AFScroller} + > + {Cell} + + )} + + ); +}; + +export default GridTable; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts new file mode 100644 index 0000000000..49518fa391 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts @@ -0,0 +1 @@ +export * from './GridTable'; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx new file mode 100644 index 0000000000..37575224ac --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/Sort.tsx @@ -0,0 +1,20 @@ +import { useSortSelector } from '@/application/database-yjs'; +import SortCondition from '@/components/database/components/sorts/SortCondition'; +import React from 'react'; +import { FieldDisplay } from 'src/components/database/components/field'; + +function Sort({ sortId }: { sortId: string }) { + const sort = useSortSelector(sortId); + + if (!sort) return null; + return ( +
+
+ +
+ +
+ ); +} + +export default Sort; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx new file mode 100644 index 0000000000..78457da1ca --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/SortCondition.tsx @@ -0,0 +1,30 @@ +import { Sort } from '@/application/database-yjs'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; + +function SortCondition({ sort }: { sort: Sort }) { + const condition = sort.condition; + const { t } = useTranslation(); + const conditionText = useMemo(() => { + switch (condition) { + case 0: + return t('grid.sort.ascending'); + case 1: + return t('grid.sort.descending'); + } + }, [condition, t]); + + return ( +
+ {conditionText} + +
+ ); +} + +export default SortCondition; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx new file mode 100644 index 0000000000..a657b4a0b9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/SortList.tsx @@ -0,0 +1,17 @@ +import { useSortsSelector } from '@/application/database-yjs'; +import Sort from '@/components/database/components/sorts/Sort'; +import React from 'react'; + +function SortList() { + const sorts = useSortsSelector(); + + return ( +
+ {sorts.map((sortId) => ( + + ))} +
+ ); +} + +export default SortList; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx b/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx new file mode 100644 index 0000000000..a00aeea20c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/Sorts.tsx @@ -0,0 +1,43 @@ +import { useSortsSelector } from '@/application/database-yjs'; +import { Popover } from '@/components/_shared/popover'; +import SortList from '@/components/database/components/sorts/SortList'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as SortSvg } from '$icons/16x/sort_ascending.svg'; +import { ReactComponent as ArrowDownSvg } from '$icons/16x/arrow_down.svg'; + +export function Sorts() { + const { t } = useTranslation(); + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const sorts = useSortsSelector(); + + if (sorts.length === 0) return null; + return ( + <> +
{ + setAnchorEl(e.currentTarget); + }} + className='flex cursor-pointer items-center gap-1 rounded-full border border-line-divider px-2 py-1 text-xs hover:border-fill-default hover:text-fill-default hover:shadow-sm' + > + + {t('grid.settings.sort')} + +
+ {open && ( + { + setAnchorEl(null); + }} + > + + + )} + + ); +} + +export default Sorts; diff --git a/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts b/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts new file mode 100644 index 0000000000..467acd9081 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/sorts/index.ts @@ -0,0 +1 @@ +export * from './Sorts'; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx new file mode 100644 index 0000000000..65a1b238bb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -0,0 +1,97 @@ +import { ViewLayout, YjsFolderKey, YView } from '@/application/collab.type'; +import { useFolderContext } from '@/application/folder-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { DatabaseActions } from '@/components/database/components/conditions'; +import { forwardRef, FunctionComponent, SVGProps, useCallback, useEffect, useMemo } from 'react'; +import { ViewTabs, ViewTab } from './ViewTabs'; +import { useTranslation } from 'react-i18next'; + +import { ReactComponent as GridSvg } from '$icons/16x/grid.svg'; +import { ReactComponent as BoardSvg } from '$icons/16x/board.svg'; +import { ReactComponent as CalendarSvg } from '$icons/16x/date.svg'; +import { ReactComponent as DocumentSvg } from '$icons/16x/document.svg'; + +export interface DatabaseTabBarProps { + viewIds: string[]; + selectedViewId?: string; + setSelectedViewId?: (viewId: string) => void; +} + +const DatabaseIcons: { + [key in ViewLayout]: FunctionComponent & { title?: string | undefined }>; +} = { + [ViewLayout.Document]: DocumentSvg, + [ViewLayout.Grid]: GridSvg, + [ViewLayout.Board]: BoardSvg, + [ViewLayout.Calendar]: CalendarSvg, +}; + +export const DatabaseTabs = forwardRef( + ({ viewIds, selectedViewId, setSelectedViewId }, ref) => { + const objectId = useId().objectId; + const { t } = useTranslation(); + const folder = useFolderContext(); + const handleChange = (_: React.SyntheticEvent, newValue: string) => { + setSelectedViewId?.(newValue); + }; + + useEffect(() => { + if (selectedViewId === undefined) { + setSelectedViewId?.(objectId); + } + }, [selectedViewId, setSelectedViewId, objectId]); + const isSelected = useMemo(() => viewIds.some((viewId) => viewId === selectedViewId), [viewIds, selectedViewId]); + + const getFolderView = useCallback( + (viewId: string) => { + if (!folder) return null; + return folder.get(YjsFolderKey.views)?.get(viewId) as YView | null; + }, + [folder] + ); + + if (viewIds.length === 0) return null; + return ( +
+
+ + {viewIds.map((viewId, index) => { + const view = getFolderView(viewId); + + if (!view) return null; + const layout = Number(view.get(YjsFolderKey.layout)) as ViewLayout; + const Icon = DatabaseIcons[layout]; + const name = view.get(YjsFolderKey.name); + + return ( + } + iconPosition='start' + color='inherit' + label={name || t('grid.title.placeholder')} + value={viewId} + /> + ); + })} + +
+ +
+ ); + } +); diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx new file mode 100644 index 0000000000..7bbf91cf65 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/TextButton.tsx @@ -0,0 +1,18 @@ +import { Button, ButtonProps, styled } from '@mui/material'; + +export const TextButton = styled((props: ButtonProps) => ( +
diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss index 4133489130..bac3baae69 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -35,6 +35,7 @@ .appflowy-scroll-container { &::-webkit-scrollbar { width: 0; + height: 0; } } @@ -44,20 +45,14 @@ opacity: 60%; } -.workspaces { - ::-webkit-scrollbar { - width: 0px; - } -} - - -.MuiPopover-root, .MuiPaper-root { +.workspaces, .database-conditions, .grid-scroll-table { ::-webkit-scrollbar { width: 0; height: 0; } } + .view-icon { @apply flex w-fit cursor-pointer rounded-lg py-2 text-6xl; font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols; diff --git a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx new file mode 100644 index 0000000000..e1fbfb8067 --- /dev/null +++ b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx @@ -0,0 +1,10 @@ +import { Database } from '@/components/database'; +import React from 'react'; + +function DatabasePage () { + return ( + + ); +} + +export default DatabasePage; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/pages/ProductPage.tsx b/frontend/appflowy_web_app/src/pages/ProductPage.tsx index 8080e339ef..f7b5615a77 100644 --- a/frontend/appflowy_web_app/src/pages/ProductPage.tsx +++ b/frontend/appflowy_web_app/src/pages/ProductPage.tsx @@ -1,34 +1,44 @@ import { CollabType } from '@/application/collab.type'; import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; +import DatabasePage from '@/pages/DatabasePage'; import React, { useMemo } from 'react'; import { useParams } from 'react-router-dom'; import DocumentPage from '@/pages/DocumentPage'; enum URL_COLLAB_TYPE { DOCUMENT = 'document', - DATABASE = 'database', + GRID = 'grid', + BOARD = 'board', + CALENDAR = 'calendar', } const collabTypeMap: Record = { [URL_COLLAB_TYPE.DOCUMENT]: CollabType.Document, - [URL_COLLAB_TYPE.DATABASE]: CollabType.Database, + [URL_COLLAB_TYPE.GRID]: CollabType.WorkspaceDatabase, + [URL_COLLAB_TYPE.BOARD]: CollabType.WorkspaceDatabase, + [URL_COLLAB_TYPE.CALENDAR]: CollabType.WorkspaceDatabase, }; function ProductPage() { - const { workspaceId, collabType, objectId } = useParams(); + const { workspaceId, type, objectId } = useParams(); const PageComponent = useMemo(() => { - switch (collabType) { + switch (type) { case URL_COLLAB_TYPE.DOCUMENT: return DocumentPage; + case URL_COLLAB_TYPE.GRID: + case URL_COLLAB_TYPE.BOARD: + case URL_COLLAB_TYPE.CALENDAR: + return DatabasePage; default: return null; } - }, [collabType]); + }, [type]); - if (!workspaceId || !collabType || !objectId) return null; + console.log(workspaceId, type, objectId); + if (!workspaceId || !type || !objectId) return null; return ( - + {PageComponent && } ); diff --git a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css index b82d97e5be..6753969ca0 100644 --- a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css @@ -1,12 +1,12 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ :root[data-dark-mode=true] { --base-light-neutral-50: #f9fafd; - --base-light-neutral-100: #edeef2; + --base-light-neutral-100: #e5e5e5; --base-light-neutral-200: #e2e4eb; --base-light-neutral-300: #f2f2f2; --base-light-neutral-400: #e0e0e0; diff --git a/frontend/appflowy_web_app/src/styles/variables/light.variables.css b/frontend/appflowy_web_app/src/styles/variables/light.variables.css index 0477655f66..b1494114bd 100644 --- a/frontend/appflowy_web_app/src/styles/variables/light.variables.css +++ b/frontend/appflowy_web_app/src/styles/variables/light.variables.css @@ -1,12 +1,12 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ :root { --base-light-neutral-50: #f9fafd; - --base-light-neutral-100: #edeef2; + --base-light-neutral-100: #e5e5e5; --base-light-neutral-200: #e2e4eb; --base-light-neutral-300: #f2f2f2; --base-light-neutral-400: #e0e0e0; @@ -83,7 +83,7 @@ --icon-disabled: #e0e0e0; --icon-on-toolbar: #ffffff; --line-border: #bdbdbd; - --line-divider: #edeef2; + --line-divider: #e5e5e5; --line-on-toolbar: #4f4f4f; --fill-toolbar: #333333; --fill-default: #00bcf0; @@ -91,7 +91,7 @@ --fill-pressed: #009fd1; --fill-active: #e0f8ff; --fill-list-hover: #e0f8ff; - --fill-list-active: #edeef2; + --fill-list-active: #f9fafd; --content-blue-400: #00bcf0; --content-blue-300: #52d1f4; --content-blue-600: #009fd1; @@ -120,5 +120,5 @@ --tint-yellow: #fff2cd; --shadow: 0px 0px 10px 0px rgba(0,0,0,0.1); --scrollbar-thumb: #bdbdbd; - --scrollbar-track: #edeef2; + --scrollbar-track: #e5e5e5; } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/utils/time.ts b/frontend/appflowy_web_app/src/utils/time.ts index 3b6920fb34..792b72ee61 100644 --- a/frontend/appflowy_web_app/src/utils/time.ts +++ b/frontend/appflowy_web_app/src/utils/time.ts @@ -1,10 +1,6 @@ import dayjs from 'dayjs'; -export enum DateFormat { - Date = 'MMM D, YYYY', - DateTime = 'MMM D, YYYY h:mm A', -} - -export function renderDate(date: string, format: DateFormat = DateFormat.Date): string { +export function renderDate(date: string, format: string, isUnix?: boolean): string { + if (isUnix) return dayjs.unix(Number(date)).format(format); return dayjs(date).format(format); } diff --git a/frontend/appflowy_web_app/src/utils/url.ts b/frontend/appflowy_web_app/src/utils/url.ts index 8d67f3583f..a10cf9ca85 100644 --- a/frontend/appflowy_web_app/src/utils/url.ts +++ b/frontend/appflowy_web_app/src/utils/url.ts @@ -1,12 +1,14 @@ import { getPlatform } from '@/utils/platform'; -import validator from 'validator'; +import isURL from 'validator/lib/isURL'; +import isIP from 'validator/lib/isIP'; +import isFQDN from 'validator/lib/isFQDN'; export const downloadPage = 'https://appflowy.io/download'; export const openAppFlowySchema = 'appflowy-flutter://'; export function isValidUrl(input: string) { - return validator.isURL(input, { require_protocol: true, require_host: false }); + return isURL(input, { require_protocol: true, require_host: false }); } // Process the URL to make sure it's a valid URL @@ -20,7 +22,7 @@ export function processUrl(input: string) { const domain = input.split('/')[0]; - if (validator.isIP(domain) || validator.isFQDN(domain)) { + if (isIP(domain) || isFQDN(domain)) { processedUrl = `https://${input}`; if (isValidUrl(processedUrl)) { return processedUrl; diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs b/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs index 00647333e2..9de67fc1be 100644 --- a/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs +++ b/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs b/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs index 798741f06c..63e679a90a 100644 --- a/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs +++ b/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated on Thu, 09 May 2024 03:26:45 GMT * Generated from $pnpm css:variables */ diff --git a/frontend/appflowy_web_app/style-dictionary/tokens/base.json b/frontend/appflowy_web_app/style-dictionary/tokens/base.json index 4e31b0523d..f92d39267f 100644 --- a/frontend/appflowy_web_app/style-dictionary/tokens/base.json +++ b/frontend/appflowy_web_app/style-dictionary/tokens/base.json @@ -7,7 +7,7 @@ "type": "color" }, "100": { - "value": "#edeef2", + "value": "#e5e5e5", "type": "color" }, "200": { diff --git a/frontend/appflowy_web_app/tsconfig.json b/frontend/appflowy_web_app/tsconfig.json index de30c24901..05dcd8d587 100644 --- a/frontend/appflowy_web_app/tsconfig.json +++ b/frontend/appflowy_web_app/tsconfig.json @@ -27,7 +27,7 @@ "node", "jest" ], - "baseUrl": "./", + "baseUrl": ".", "paths": { "@/*": [ "src/*" @@ -37,6 +37,9 @@ ], "$client-services": [ "src/application/services/js-services" + ], + "$icons/*": [ + "../resources/flowy-flowy_icons/*" ] } }, diff --git a/frontend/appflowy_web_app/vite.config.ts b/frontend/appflowy_web_app/vite.config.ts index b2621799ed..0e4cfebb4b 100644 --- a/frontend/appflowy_web_app/vite.config.ts +++ b/frontend/appflowy_web_app/vite.config.ts @@ -5,7 +5,9 @@ import wasm from 'vite-plugin-wasm'; import { visualizer } from 'rollup-plugin-visualizer'; import usePluginImport from 'vite-plugin-importer'; import { totalBundleSize } from 'vite-plugin-total-bundle-size'; +import path from 'path'; +const resourcesPath = path.resolve(__dirname, '../resources'); const isDev = process.env.NODE_ENV === 'development'; // https://vitejs.dev/config/ export default defineConfig({ @@ -104,8 +106,8 @@ export default defineConfig({ id.includes('/react-is@') || id.includes('/yjs@') || id.includes('/y-indexeddb@') || - id.includes('/dexie@') || - id.includes('/redux') + id.includes('/redux') || + id.includes('/react-custom-scrollbars') ) { return 'common'; } @@ -124,6 +126,7 @@ export default defineConfig({ ? `${__dirname}/src/application/services/tauri-services` : `${__dirname}/src/application/services/js-services`, }, + { find: '$icons', replacement: `${resourcesPath}/flowy_icons/` }, ], }, From 3c6ce4ea4d3f0b43908c2dea5289a26c7eb42ed0 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 17 May 2024 23:15:37 +0800 Subject: [PATCH 05/30] fix: workspace icon displays incorrectly on Linux (#5358) --- .../menu/sidebar/workspace/_sidebar_workspace_icon.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index 100b8d6099..f3f78ee99b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -1,12 +1,12 @@ import 'dart:math'; -import 'package:flutter/material.dart'; - +import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class WorkspaceIcon extends StatefulWidget { const WorkspaceIcon({ @@ -35,8 +35,8 @@ class _WorkspaceIconState extends State { ? Container( width: widget.iconSize, alignment: Alignment.center, - child: FlowyText( - widget.workspace.icon, + child: EmojiText( + emoji: widget.workspace.icon, fontSize: widget.iconSize, ), ) From bfc6cd721e0d67e5c80e3378b94f5c7ac3917396 Mon Sep 17 00:00:00 2001 From: JB Date: Sat, 18 May 2024 15:56:26 +0200 Subject: [PATCH 06/30] chore: update fr-FR.json (#5356) Fix typo --- frontend/resources/translations/fr-FR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index db1a933b28..68254d7a4d 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -170,7 +170,7 @@ "shortcuts": "Raccourcis", "whatsNew": "Nouveautés", "help": "Aide et Support", - "markdown": "Réduction", + "markdown": "Rédaction", "debug": { "name": "Informations de Débogage", "success": "Informations de débogage copiées dans le presse-papiers !", From 7e83d598f844b45e6e9997cb4595e3e37a221559 Mon Sep 17 00:00:00 2001 From: atellier2 Date: Sat, 18 May 2024 15:57:56 +0200 Subject: [PATCH 07/30] =?UTF-8?q?chore:=20update=20translations=20with=20F?= =?UTF-8?q?ink=20=F0=9F=90=A6=20(#5322)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/resources/translations/fr-FR.json | 105 +++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 68254d7a4d..6a85f8cbc9 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -42,6 +42,7 @@ "emailHint": "Courriel", "passwordHint": "Mot de passe", "dontHaveAnAccount": "Vous n'avez pas encore de compte ?", + "createAccount": "Créer un compte", "repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe", "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", "syncPromptMessage": "La synchronisation des données peut prendre un certain temps. Merci de ne pas fermer pas cette page.", @@ -51,6 +52,7 @@ "pleaseInputYourEmail": "Veuillez entrer votre adresse e-mail", "magicLinkSent": "Lien magique envoyé à votre email, veuillez vérifier votre boîte de réception", "invalidEmail": "S'il vous plaît, mettez une adresse email valide", + "logIn": "Connexion", "LogInWithGoogle": "Se connecter avec Google", "LogInWithGithub": "Se connecter avec Github", "LogInWithDiscord": "Se connecter avec Discord", @@ -93,6 +95,7 @@ "buttonText": "Partager", "workInProgress": "Bientôt disponible", "markdown": "Markdown", + "html": "HTML", "clipboard": "Copier dans le presse-papier", "csv": "CSV", "copyLink": "Copier le lien" @@ -310,6 +313,108 @@ }, "settings": { "title": "Paramètres", + "accountPage": { + "menuLabel": "Mon compte", + "title": "Mon compte", + "email": { + "title": "Email", + "actions": { + "change": "Modifier l'email" + } + }, + "login": { + "loginLabel": "Connexion", + "logoutLabel": "Déconnexion" + } + }, + "workspacePage": { + "menuLabel": "Espace de travail", + "title": "Espace de travail", + "description": "Personnalisez l'apparence, le thème, la police, la disposition du texte, le format de la date/heure et la langue de votre espace de travail.", + "workspaceName": { + "title": "Nom de l'espace de travail", + "savedMessage": "Nom de l'espace de travail enregistré" + }, + "workspaceIcon": { + "title": "Icône de l'espace de travail", + "description": "Personnalisez l'apparence, le thème, la police, la disposition du texte, la date, l'heure et la langue de votre espace de travail." + }, + "appearance": { + "title": "Apparence", + "description": "Personnalisez l'apparence, le thème, la police, la disposition du texte, la date, l'heure et la langue de votre espace de travail.", + "options": { + "system": "Auto", + "light": "Clair", + "dark": "Foncé" + } + }, + "theme": { + "title": "Thème", + "description": "Sélectionnez un thème prédéfini ou téléchargez votre propre thème personnalisé." + }, + "workspaceFont": { + "title": "Police de caractère de l'espace de travail" + }, + "textDirection": { + "title": "Sens du texte", + "leftToRight": "De gauche à droite", + "rightToLeft": "De droite à gauche", + "auto": "Auto" + }, + "layoutDirection": { + "leftToRight": "De gauche à droite", + "rightToLeft": "De droite à gauche" + }, + "dateTime": { + "title": "Date et heure", + "24HourTime": "Heure sur 24 heures", + "dateFormat": { + "label": "Format de date", + "local": "Locale", + "us": "US", + "iso": "ISO", + "dmy": "J/M/A" + } + }, + "language": { + "title": "Langue" + }, + "deleteWorkspacePrompt": { + "title": "Supprimer l'espace de travail", + "content": "Êtes-vous sûr de vouloir supprimer cet espace de travail ? Cette action ne peut pas être annulée." + }, + "leaveWorkspacePrompt": { + "title": "Quitter l'espace de travail", + "content": "Êtes-vous sûr de vouloir quitter cet espace de travail ? Vous allez perdre l’accès à toutes les pages et données qu’il contient." + }, + "manageWorkspace": { + "title": "Gérer l'espace de travail", + "leaveWorkspace": "Quitter l'espace de travail", + "deleteWorkspace": "Supprimer l'espace de travail" + } + }, + "manageDataPage": { + "menuLabel": "Gérer les données", + "title": "Gérer les données", + "dataStorage": { + "actions": { + "change": "Changer de chemin", + "open": "Ouvrir le répertoire", + "copy": "Copier le chemin", + "copiedHint": "Lien copié !" + }, + "resetDialog": { + "title": "Êtes-vous sûr ?" + } + }, + "importData": { + "title": "Importer des données", + "action": "Parcourir le dossier" + }, + "encryption": { + "title": "Chiffrement" + } + }, "menu": { "appearance": "Apparence", "language": "Langue", From bd62a161bef02c5f0dcecfa9aa43f1663b78e893 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 19 May 2024 21:35:14 +0800 Subject: [PATCH 08/30] chore: do not use the cell of given summary field (#5362) * chore: do not use the cell of given summary field * chore: update docs --- frontend/rust-lib/flowy-database2/src/manager.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 21858ad201..77d9a477bc 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -424,15 +424,19 @@ impl DatabaseManager { if let Some(row) = database.get_row(&view_id, &row_id) { let fields = database.get_fields(&view_id, None); for field in fields { - if let Some(cell) = row.cells.get(&field.id) { - summary_row_content.insert(field.name.clone(), stringify_cell(cell, &field)); + // When summarizing a row, skip the content in the "AI summary" cell; it does not need to + // be summarized. + if field.id != field_id { + if let Some(cell) = row.cells.get(&field.id) { + summary_row_content.insert(field.name.clone(), stringify_cell(cell, &field)); + } } } } // Call the cloud service to summarize the row. trace!( - "[AI]: summarize row:{}, content:{:?}", + "[AI]:summarize row:{}, content:{:?}", row_id, summary_row_content ); From f1b035e362d04b076dfa9e24aa86db86882b18bc Mon Sep 17 00:00:00 2001 From: Vladislav Poturai <42515136+Dreykp@users.noreply.github.com> Date: Mon, 20 May 2024 06:28:13 +0300 Subject: [PATCH 09/30] chore: support Ukrainian language #5350 (#5369) --- project.inlang/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/project.inlang/settings.json b/project.inlang/settings.json index c49b392792..2d09236b50 100644 --- a/project.inlang/settings.json +++ b/project.inlang/settings.json @@ -24,6 +24,7 @@ "ru-RU", "sv-SE", "tr-TR", + "uk-UA", "vi", "vi-VN", "zh-CN", From 0c0bd54f527346e79db6961d8e3527e15295a82b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 20 May 2024 17:18:39 +0800 Subject: [PATCH 10/30] fix: heading node validate failed (#5370) --- .../document_data_pb_extension.dart | 33 ++++++++---- .../presentation/editor_configuration.dart | 5 +- .../error/error_block_component_builder.dart | 54 +++++++++++++++---- frontend/resources/translations/en.json | 3 +- 4 files changed, 69 insertions(+), 26 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart index e60782605a..da99886014 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart' @@ -10,7 +11,13 @@ import 'package:appflowy_editor/appflowy_editor.dart' Delta, ParagraphBlockKeys, NodeIterator, - NodeExternalValues; + NodeExternalValues, + HeadingBlockKeys, + QuoteBlockKeys, + NumberedListBlockKeys, + BulletedListBlockKeys, + blockComponentDelta; +import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:collection/collection.dart'; import 'package:nanoid/nanoid.dart'; @@ -144,21 +151,25 @@ extension BlockToNode on BlockPB { final deltaString = meta.textMap[externalId]; if (deltaString != null) { final delta = jsonDecode(deltaString); - map['delta'] = delta; - // map.putIfAbsent( - // 'delta', - // () => delta, - // ); + map[blockComponentDelta] = delta; } } } + Attributes adapterCallback(Attributes map) => map + ..putIfAbsent( + blockComponentDelta, + () => Delta().toJson(), + ); + final adapter = { - ParagraphBlockKeys.type: (Attributes map) => map - ..putIfAbsent( - 'delta', - () => Delta().toJson(), - ), + ParagraphBlockKeys.type: adapterCallback, + HeadingBlockKeys.type: adapterCallback, + CodeBlockKeys.type: adapterCallback, + QuoteBlockKeys.type: adapterCallback, + NumberedListBlockKeys.type: adapterCallback, + BulletedListBlockKeys.type: adapterCallback, + ToggleListBlockKeys.type: adapterCallback, }; return adapter[ty]?.call(map) ?? map; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index 8f18cb1d58..c8222c5e03 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; @@ -13,6 +10,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:easy_localization/easy_localization.dart' hide TextDirection; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; Map getEditorBuilderMap({ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index 3fdf332eb3..d16c035115 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -74,17 +74,9 @@ class _ErrorBlockComponentWidgetState extends State ClipboardServiceData(plainText: jsonEncode(node.toJson())), ); }, - text: SizedBox( - height: 52, - child: Row( - children: [ - const HSpace(4), - FlowyText( - LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), - ), - ], - ), - ), + text: PlatformExtension.isDesktopOrWeb + ? _buildDesktopErrorBlock(context) + : _buildMobileErrorBlock(context), ), ); @@ -111,4 +103,44 @@ class _ErrorBlockComponentWidgetState extends State return child; } + + Widget _buildDesktopErrorBlock(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + const HSpace(4), + FlowyText.regular( + LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), + ), + const HSpace(4), + FlowyText.regular( + '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', + color: Theme.of(context).hintColor, + ), + ], + ), + ); + } + + Widget _buildMobileErrorBlock(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FlowyText.regular( + LocaleKeys.document_errorBlock_theBlockIsNotSupported.tr(), + ), + const VSpace(6), + FlowyText.regular( + '(${LocaleKeys.document_errorBlock_clickToCopyTheBlockContent.tr()})', + color: Theme.of(context).hintColor, + fontSize: 12.0, + ), + ], + ), + ); + } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 96c7314dbf..ff43722720 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1204,7 +1204,8 @@ "resetToDefaultFont": "Reset to default" }, "errorBlock": { - "theBlockIsNotSupported": "The current version does not support this block.", + "theBlockIsNotSupported": "Unable to parse the block content", + "clickToCopyTheBlockContent": "Click to copy the block content", "blockContentHasBeenCopied": "The block content has been copied." }, "mobilePageSelector": { From 9b7ee4b978c0b6f5c55413694efedf6294ff0f80 Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari Date: Mon, 20 May 2024 12:49:58 +0330 Subject: [PATCH 11/30] fix: changing field type from text to checkbox causes exception #5360 (#5366) --- .../desktop/board/board_field_test.dart | 37 +++++++++++++++++++ .../shared/database_test_op.dart | 19 +++++++--- .../database/card/mobile_card_content.dart | 8 ++-- .../plugins/database/widgets/card/card.dart | 9 ++--- .../database/widgets/card/card_bloc.dart | 34 ++++++++++++++--- 5 files changed, 87 insertions(+), 20 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart new file mode 100644 index 0000000000..3fe48b5f6f --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart @@ -0,0 +1,37 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/database_test_op.dart'; +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('board field test', () { + testWidgets('change field type whithin card #5360', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Board); + const name = 'Card 1'; + final card1 = find.text(name); + await tester.tapButton(card1); + + const fieldName = "test change field"; + await tester.createField( + FieldType.RichText, + fieldName, + layout: ViewLayoutPB.Board, + ); + await tester.tapButton(card1); + await tester.changeFieldTypeOfFieldWithName( + fieldName, + FieldType.Checkbox, + layout: ViewLayoutPB.Board, + ); + await tester.hoverOnWidget(find.text('Card 2')); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart index 8697b832c0..8277b4db97 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -661,10 +661,13 @@ extension AppFlowyDatabaseTest on WidgetTester { Future changeFieldTypeOfFieldWithName( String name, - FieldType type, - ) async { + FieldType type, { + ViewLayoutPB layout = ViewLayoutPB.Grid, + }) async { await tapGridFieldWithName(name); - await tapEditFieldButton(); + if (layout == ViewLayoutPB.Grid) { + await tapEditFieldButton(); + } await tapSwitchFieldTypeButton(); await selectFieldType(type); @@ -881,8 +884,14 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButtonWithName(LocaleKeys.grid_row_delete.tr()); } - Future createField(FieldType fieldType, String name) async { - await scrollToRight(find.byType(GridPage)); + Future createField( + FieldType fieldType, + String name, { + ViewLayoutPB layout = ViewLayoutPB.Grid, + }) async { + if (layout == ViewLayoutPB.Grid) { + await scrollToRight(find.byType(GridPage)); + } await tapNewPropertyButton(); await renameField(name); await tapSwitchFieldTypeButton(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart index 057d3937cb..48d8b2f097 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart @@ -1,5 +1,5 @@ -import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/widgets/card/card.dart'; +import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; @@ -16,7 +16,7 @@ class MobileCardContent extends StatelessWidget { final RowMetaPB rowMeta; final CardCellBuilder cellBuilder; - final List cells; + final List cells; final RowCardStyleConfiguration styleConfiguration; @override @@ -26,9 +26,9 @@ class MobileCardContent extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: cells.map( - (cellContext) { + (cellMeta) { return cellBuilder.build( - cellContext: cellContext, + cellContext: cellMeta.cellContext(), styleMap: mobileBoardCardCellStyleMap(context), hasNotes: !rowMeta.isDocumentEmpty, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart index b2daf625f6..d5c73a1179 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card.dart @@ -1,6 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/mobile/presentation/database/card/card.dart'; -import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/row/action.dart'; @@ -186,7 +185,7 @@ class _CardContent extends StatelessWidget { final RowMetaPB rowMeta; final CardCellBuilder cellBuilder; - final List cells; + final List cells; final RowCardStyleConfiguration styleConfiguration; @override @@ -210,9 +209,9 @@ class _CardContent extends StatelessWidget { List _makeCells( BuildContext context, RowMetaPB rowMeta, - List cells, + List cells, ) { - return cells.mapIndexed((int index, CellContext cellContext) { + return cells.mapIndexed((int index, CellMeta cellMeta) { EditableCardNotifier? cellNotifier; if (index == 0) { @@ -225,7 +224,7 @@ class _CardContent extends StatelessWidget { } return cellBuilder.build( - cellContext: cellContext, + cellContext: cellMeta.cellContext(), cellNotifier: cellNotifier, styleMap: styleConfiguration.cellStyleMap, hasNotes: !rowMeta.isDocumentEmpty, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart index 3cb47d9f43..5bd4d6f505 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:appflowy/plugins/database/application/row/row_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter/foundation.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; @@ -7,7 +9,6 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/application/row/row_cache.dart'; import 'package:appflowy/plugins/database/domain/row_listener.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -104,7 +105,7 @@ class CardBloc extends Bloc { } } -List _makeCells( +List _makeCells( FieldController fieldController, String? groupFieldId, List cellContexts, @@ -116,7 +117,15 @@ List _makeCells( !(fieldInfo.visibility?.isVisibleState() ?? false) || (groupFieldId != null && cellContext.fieldId == groupFieldId); }); - return cellContexts.toList(); + return cellContexts + .map( + (cellCtx) => CellMeta( + fieldId: cellCtx.fieldId, + rowId: cellCtx.rowId, + fieldType: fieldController.getField(cellCtx.fieldId)!.fieldType, + ), + ) + .toList(); } @freezed @@ -124,17 +133,30 @@ class CardEvent with _$CardEvent { const factory CardEvent.initial() = _InitialRow; const factory CardEvent.setIsEditing(bool isEditing) = _IsEditing; const factory CardEvent.didReceiveCells( - List cells, + List cells, ChangedReason reason, ) = _DidReceiveCells; const factory CardEvent.didUpdateRowMeta(RowMetaPB rowMeta) = _DidUpdateRowMeta; } +@freezed +class CellMeta with _$CellMeta { + const CellMeta._(); + + const factory CellMeta({ + required String fieldId, + required RowId rowId, + required FieldType fieldType, + }) = _DatabaseCellMeta; + + CellContext cellContext() => CellContext(fieldId: fieldId, rowId: rowId); +} + @freezed class CardState with _$CardState { const factory CardState({ - required List cells, + required List cells, required RowMetaPB rowMeta, required bool isEditing, ChangedReason? changeReason, @@ -142,7 +164,7 @@ class CardState with _$CardState { factory CardState.initial( RowMetaPB rowMeta, - List cells, + List cells, bool isEditing, ) => CardState( From e6d166df3bcbc35bf3c778a426bf0380f7a10fe7 Mon Sep 17 00:00:00 2001 From: Stefan Weiberg <2744377+suntorytimed@users.noreply.github.com> Date: Mon, 20 May 2024 11:20:14 +0200 Subject: [PATCH 12/30] chore: update translations to consistently use appName instead of AppFlowy (#5354) --- frontend/resources/translations/ar-SA.json | 18 +++---- frontend/resources/translations/ca-ES.json | 16 +++--- frontend/resources/translations/ckb-KU.json | 22 ++++---- frontend/resources/translations/cs-CZ.json | 16 +++--- frontend/resources/translations/de-DE.json | 60 ++++++++++----------- frontend/resources/translations/en.json | 36 ++++++------- frontend/resources/translations/es-VE.json | 26 ++++----- frontend/resources/translations/eu-ES.json | 12 ++--- frontend/resources/translations/fa.json | 12 ++--- frontend/resources/translations/fr-CA.json | 28 +++++----- frontend/resources/translations/fr-FR.json | 28 +++++----- frontend/resources/translations/hu-HU.json | 12 ++--- frontend/resources/translations/id-ID.json | 14 ++--- frontend/resources/translations/it-IT.json | 28 +++++----- frontend/resources/translations/ja-JP.json | 16 +++--- frontend/resources/translations/ko-KR.json | 12 ++--- frontend/resources/translations/pl-PL.json | 14 ++--- frontend/resources/translations/pt-BR.json | 26 ++++----- frontend/resources/translations/pt-PT.json | 14 ++--- frontend/resources/translations/ru-RU.json | 24 ++++----- frontend/resources/translations/sv-SE.json | 22 ++++---- frontend/resources/translations/tr-TR.json | 28 +++++----- frontend/resources/translations/vi-VN.json | 22 ++++---- frontend/resources/translations/zh-CN.json | 28 +++++----- frontend/resources/translations/zh-TW.json | 32 +++++------ 25 files changed, 283 insertions(+), 283 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 84d0f64985..077ca23e03 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -56,7 +56,7 @@ "resetWorkspacePrompt": "ستؤدي إعادة تعيين مساحة العمل إلى حذف جميع الصفحات والبيانات الموجودة بداخلها. هل أنت متأكد أنك تريد إعادة تعيين مساحة العمل؟ وبدلاً من ذلك، يمكنك الاتصال بفريق الدعم لاستعادة مساحة العمل", "hint": "مساحة العمل", "notFoundError": "مساحة العمل غير موجودة", - "failedToLoad": "هناك خطأ ما! فشل تحميل مساحة العمل. حاول إغلاق أي مثيل مفتوح لـ AppFlowy وحاول مرة أخرى.", + "failedToLoad": "هناك خطأ ما! فشل تحميل مساحة العمل. حاول إغلاق أي مثيل مفتوح لـ @:appName وحاول مرة أخرى.", "errorActions": { "reportIssue": "بلغ عن خطأ", "reachOut": "تواصل مع ديسكورد" @@ -279,7 +279,7 @@ "cloudSupabaseUrl": "رابط Supabase", "cloudSupabaseAnonKey": "مفتاح Supabase الخفي", "cloudSupabaseAnonKeyCanNotBeEmpty": "لا يمكن أن يكون المفتاح المجهول فارغًا إذا لم يكن عنوان URL الخاص بـ Supabase فارغًا", - "cloudAppFlowy": "سحابة AppFlowy", + "cloudAppFlowy": "سحابة @:appName", "clickToCopy": "انقر للنسخ", "selfHostStart": "إذا لم يكن لديك خادم، يرجى الرجوع إلى", "selfHostContent": "مستند", @@ -298,7 +298,7 @@ "historicalUserList": "سجل تسجيل دخول المستخدم", "historicalUserListTooltip": "تعرض هذه القائمة حساباتك المجهولة. يمكنك النقر على الحساب لعرض تفاصيله. يتم إنشاء الحسابات المجهولة بالنقر فوق الزر \"البدء\".", "openHistoricalUser": "انقر لفتح الحساب الخفي", - "customPathPrompt": "قد يؤدي تخزين مجلد بيانات AppFlowy في مجلد متزامن على السحابة مثل Google Drive إلى مخاطر. إذا تم الوصول إلى قاعدة البيانات الموجودة في هذا المجلد أو تعديلها من مواقع متعددة في نفس الوقت، فقد يؤدي ذلك إلى حدوث تعارضات في المزامنة وتلف محتمل للبيانات", + "customPathPrompt": "قد يؤدي تخزين مجلد بيانات @:appName في مجلد متزامن على السحابة مثل Google Drive إلى مخاطر. إذا تم الوصول إلى قاعدة البيانات الموجودة في هذا المجلد أو تعديلها من مواقع متعددة في نفس الوقت، فقد يؤدي ذلك إلى حدوث تعارضات في المزامنة وتلف محتمل للبيانات", "supabaseSetting": "إعداد Supabase", "cloudSetting": "إعداد السحابة" }, @@ -337,7 +337,7 @@ "themeUpload": { "button": "رفع", "uploadTheme": "تحميل الموضوع", - "description": "قم بتحميل قالب AppFlowy الخاص بك باستخدام الزر أدناه.", + "description": "قم بتحميل قالب @:appName الخاص بك باستخدام الزر أدناه.", "loading": "يرجى الانتظار بينما نقوم بالتحقق من صحة السمة الخاصة بك وتحميلها ...", "uploadSuccess": "تم تحميل موضوعك بنجاح", "deletionFailure": "فشل حذف الموضوع. حاول حذفه يدويًا.", @@ -370,7 +370,7 @@ "defaultLocation": "أين يتم تخزين بياناتك الآن", "exportData": "قم بتصدير بياناتك", "doubleTapToCopy": "انقر نقرًا مزدوجًا لنسخ المسار", - "restoreLocation": "استعادة المسار الافتراضي AppFlowy", + "restoreLocation": "استعادة المسار الافتراضي @:appName", "customizeLocation": "افتح مجلدًا آخر", "restartApp": "يرجى إعادة تشغيل التطبيق لتصبح التغييرات سارية المفعول.", "exportDatabase": "تصدير قاعدة البيانات", @@ -382,10 +382,10 @@ "defineWhereYourDataIsStored": "حدد مكان تخزين بياناتك", "open": "يفتح", "openFolder": "افتح مجلدًا موجودًا", - "openFolderDesc": "اقرأها واكتبها في مجلد AppFlowy الموجود لديك", + "openFolderDesc": "اقرأها واكتبها في مجلد @:appName الموجود لديك", "folderHintText": "إسم الملف", "location": "إنشاء مجلد جديد", - "locationDesc": "اختر اسمًا لمجلد بيانات AppFlowy", + "locationDesc": "اختر اسمًا لمجلد بيانات @:appName", "browser": "تصفح", "create": "يخلق", "set": "تعيين", @@ -396,7 +396,7 @@ "change": "يتغير", "openLocationTooltips": "افتح دليل بيانات آخر", "openCurrentDataFolder": "افتح دليل البيانات الحالي", - "recoverLocationTooltips": "إعادة التعيين إلى دليل البيانات الافتراضي لـ AppFlowy", + "recoverLocationTooltips": "إعادة التعيين إلى دليل البيانات الافتراضي لـ @:appName", "exportFileSuccess": "تم تصدير الملف بنجاح!", "exportFileFail": "فشل تصدير الملف!", "export": "يصدّر" @@ -904,7 +904,7 @@ "quickJumpYear": "انتقل إلى" }, "errorDialog": { - "title": "خطأ AppFlowy", + "title": "خطأ @:appName", "howToFixFallback": "نأسف للإزعاج! قم بإرسال مشكلة على صفحة GitHub الخاصة بنا والتي تصف الخطأ الخاص بك.", "github": "عرض على جيثب" }, diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index dccb18dcf8..a9a86fb5fb 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -270,7 +270,7 @@ "invalidCloudURLScheme": "Esquema no vàlid", "cloudServerType": "Servidor al núvol", "cloudLocal": "Local", - "cloudAppFlowy": "AppFlowy Cloud Beta", + "cloudAppFlowy": "@:appName Cloud Beta", "clickToCopy": "Feu clic per copiar", "selfHostContent": "document", "cloudURLHint": "Introduïu l'URL base del vostre servidor", @@ -278,7 +278,7 @@ "inputEncryptPrompt": "Introduïu el vostre secret de xifratge per a", "clickToCopySecret": "Feu clic per copiar el secret", "inputTextFieldHint": "El teu secret", - "importSuccess": "S'ha importat correctament la carpeta de dades d'AppFlowy", + "importSuccess": "S'ha importat correctament la carpeta de dades d'@:appName", "supabaseSetting": "Configuració Supabase" }, "notifications": { @@ -319,7 +319,7 @@ "themeUpload": { "button": "Carrega", "uploadTheme": "Carrega el tema", - "description": "Carregueu el vostre propi tema AppFlowy amb el botó següent.", + "description": "Carregueu el vostre propi tema @:appName amb el botó següent.", "loading": "Si us plau, espereu mentre validem i carreguem el vostre tema...", "uploadSuccess": "El teu tema s'ha penjat correctament", "deletionFailure": "No s'ha pogut suprimir el tema. Intenta esborrar-lo manualment.", @@ -347,7 +347,7 @@ "defaultLocation": "Llegir fitxers i ubicació d'emmagatzematge de dades", "exportData": "Exporteu les vostres dades", "doubleTapToCopy": "Fes doble toc per copiar el camí", - "restoreLocation": "Restaura al camí predeterminat d'AppFlowy", + "restoreLocation": "Restaura al camí predeterminat d'@:appName", "customizeLocation": "Obriu una altra carpeta", "restartApp": "Si us plau, reinicieu l'aplicació perquè els canvis tinguin efecte.", "exportDatabase": "Exportar la base de dades", @@ -359,10 +359,10 @@ "defineWhereYourDataIsStored": "Definiu on s'emmagatzemen les vostres dades", "open": "Obert", "openFolder": "Obre una carpeta existent", - "openFolderDesc": "Llegiu-lo i escriviu-lo a la vostra carpeta AppFlowy existent", + "openFolderDesc": "Llegiu-lo i escriviu-lo a la vostra carpeta @:appName existent", "folderHintText": "nom de la carpeta", "location": "Creació d'una carpeta nova", - "locationDesc": "Trieu un nom per a la vostra carpeta de dades d'AppFlowy", + "locationDesc": "Trieu un nom per a la vostra carpeta de dades d'@:appName", "browser": "Navega", "create": "Crear", "set": "Conjunt", @@ -373,7 +373,7 @@ "change": "Canviar", "openLocationTooltips": "Obriu un altre directori de dades", "openCurrentDataFolder": "Obre el directori de dades actual", - "recoverLocationTooltips": "Restableix al directori de dades predeterminat d'AppFlowy", + "recoverLocationTooltips": "Restableix al directori de dades predeterminat d'@:appName", "exportFileSuccess": "Exporta el fitxer correctament!", "exportFileFail": "Ha fallat l'exportació del fitxer!", "export": "Exporta" @@ -791,7 +791,7 @@ "referencedCalendarPrefix": "Vista de" }, "errorDialog": { - "title": "Error d'AppFlowy", + "title": "Error d'@:appName", "howToFixFallback": "Lamentem les molèsties! Envieu un problema a la nostra pàgina de GitHub que descrigui el vostre error.", "github": "Veure a GitHub" }, diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index 28535be2c8..1bbd3ccd25 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -64,7 +64,7 @@ "resetWorkspacePrompt": "ڕێستکردنی شوێنی کارەکە هەموو لاپەڕە و داتاکانی ناوی دەسڕێتەوە. ئایا دڵنیای کە دەتەوێت شوێنی کارەکە ڕێست بکەیتەوە؟ یان دەتوانیت پەیوەندی بە تیمی پشتگیرییەوە بکەیت بۆ گەڕاندنەوەی شوێنی کارەکە", "hint": "شوێنی کارکردن", "notFoundError": "هیچ شوێنێکی کار نەدۆزراوە", - "failedToLoad": "هەندێ شت بە هەڵە ڕۆیشت! شکستی هێنا لە بارکردنی شوێنی کارکردن. هەوڵبدە هەر نموونەیەکی کراوەی AppFlowy دابخەیت و دووبارە هەوڵبدەرەوە.", + "failedToLoad": "هەندێ شت بە هەڵە ڕۆیشت! شکستی هێنا لە بارکردنی شوێنی کارکردن. هەوڵبدە هەر نموونەیەکی کراوەی @:appName دابخەیت و دووبارە هەوڵبدەرەوە.", "errorActions": { "reportIssue": "ڕاپۆرت کردنی کێشەیەک", "reportIssueOnGithub": "ڕاپۆرت کردنی کێشەیەک لەسەر گیتهابەوە ", @@ -358,12 +358,12 @@ "historicalUserList": "مێژووی چوونەژوورەوەی بەکارهێنەر", "historicalUserListTooltip": "ئەم لیستە ئەکاونتە بێناوەکانت پیشان دەدات. دەتوانیت کلیک لەسەر ئەکاونتێک بکەیت بۆ بینینی وردەکارییەکانی. ئەکاونتی بێناو بە کلیک کردن لەسەر دوگمەی دەستپێکردن دروست دەکرێت", "openHistoricalUser": "بۆ کردنەوەی ئەکاونتی بێناو کلیک بکە", - "customPathPrompt": "هەڵگرتنی فۆڵدەری داتاکانی AppFlowy لە فۆڵدەرێکی هاوکاتی کڵاود وەک گووگڵ درایڤ دەتوانێت مەترسی دروست بکات. ئەگەر بنکەدراوەی ناو ئەم فۆڵدەرە لە یەک کاتدا لە چەندین شوێنەوە دەستی پێ بگات یان دەستکاری بکرێت، لەوانەیە ببێتە هۆی ناکۆکی هاوکاتکردن و ئەگەری تێکچوونی داتاکان", - "importAppFlowyData": "هێنانی داتا لە فۆڵدەری دەرەکی AppFlowy", + "customPathPrompt": "هەڵگرتنی فۆڵدەری داتاکانی @:appName لە فۆڵدەرێکی هاوکاتی کڵاود وەک گووگڵ درایڤ دەتوانێت مەترسی دروست بکات. ئەگەر بنکەدراوەی ناو ئەم فۆڵدەرە لە یەک کاتدا لە چەندین شوێنەوە دەستی پێ بگات یان دەستکاری بکرێت، لەوانەیە ببێتە هۆی ناکۆکی هاوکاتکردن و ئەگەری تێکچوونی داتاکان", + "importAppFlowyData": "هێنانی داتا لە فۆڵدەری دەرەکی @:appName", "importingAppFlowyDataTip": "هێنانی داتا لە قۆناغی جێبەجێکردندایە. تکایە ئەپەکە دامەخە", - "importAppFlowyDataDescription": "داتا لە فۆڵدەری داتای دەرەکی AppFlowy کۆپی بکە و هاوردە بکە بۆ ناو فۆڵدەری داتاکانی AppFlowy ی ئێستا", - "importSuccess": "بە سەرکەوتوویی فۆڵدەری داتاکانی AppFlowy هاوردە کرد", - "importFailed": "هاوردەکردنی فۆڵدەری داتاکانی AppFlowy شکستی هێنا", + "importAppFlowyDataDescription": "داتا لە فۆڵدەری داتای دەرەکی @:appName کۆپی بکە و هاوردە بکە بۆ ناو فۆڵدەری داتاکانی @:appName ی ئێستا", + "importSuccess": "بە سەرکەوتوویی فۆڵدەری داتاکانی @:appName هاوردە کرد", + "importFailed": "هاوردەکردنی فۆڵدەری داتاکانی @:appName شکستی هێنا", "importGuide": "بۆ زانیاری زیاتر، تکایە بەڵگەنامەی ئاماژەپێکراو بپشکنە" }, "notifications": { @@ -413,7 +413,7 @@ "themeUpload": { "button": "بارکردن", "uploadTheme": "بارکردنی تێم", - "description": "بە بەکارهێنانی دوگمەی خوارەوە تێمی AppFlowy ـەکەت باربکە.", + "description": "بە بەکارهێنانی دوگمەی خوارەوە تێمی @:appName ـەکەت باربکە.", "loading": "تکایە چاوەڕوان بن تا ئێمە تێمی قاڵبەکەت پشتڕاست دەکەینەوە و بار دەکەین...", "uploadSuccess": "تێمی قاڵبەکەت بە سەرکەوتوویی بارکرا", "deletionFailure": "تێمەکە نەسڕدرایەوە. هەوڵبدە بە دەستی لابەریت.", @@ -444,7 +444,7 @@ "defaultLocation": "خوێندنەوەی پەڕگەکان و شوێنی هەڵگرتنی داتاکان", "exportData": "دەرچوون لە داتاکانتەوە بەدەست بهێنە", "doubleTapToCopy": "بۆ کۆپیکردن دووجار کلیک بکە", - "restoreLocation": "گەڕاندنەوە بۆ ڕێڕەوی پێشوەختەی AppFlowy", + "restoreLocation": "گەڕاندنەوە بۆ ڕێڕەوی پێشوەختەی @:appName", "customizeLocation": "فۆڵدەرێکی دیکە بکەرەوە", "restartApp": "تکایە ئەپەکە دابخە و بیکەرەوە بۆ ئەوەی گۆڕانکارییەکان جێبەجێ بکرێن.", "exportDatabase": "هەناردە کردنی بنکەدراوە", @@ -456,10 +456,10 @@ "defineWhereYourDataIsStored": "پێناسە بکە کە داتاکانت لە کوێ هەڵدەگیرێن", "open": "کردنەوە", "openFolder": "فۆڵدەرێکی هەبوو بکەرەوە", - "openFolderDesc": "خوێندن و نووسین بۆ فۆڵدەری AppFlowy ی ئێستات", + "openFolderDesc": "خوێندن و نووسین بۆ فۆڵدەری @:appName ی ئێستات", "folderHintText": "ناوی فۆڵدەر", "location": "دروستکردنی فۆڵدەرێکی نوێ", - "locationDesc": "ناوێک بۆ فۆڵدەری داتاکانی AppFlowy هەڵبژێرە", + "locationDesc": "ناوێک بۆ فۆڵدەری داتاکانی @:appName هەڵبژێرە", "browser": "وێبگەڕ", "create": "دروستکردن", "set": "دانان", @@ -893,7 +893,7 @@ "referencedCalendarPrefix": "دیمەنی..." }, "errorDialog": { - "title": "هەڵەی⛔️ AppFlowy", + "title": "هەڵەی⛔️ @:appName", "howToFixFallback": "ببورن بۆ کێشەکە🥺️! پرسەکە و وەسفەکەی لە لاپەڕەی GitHub ـمان بنێرن.", "github": "بینین لە GitHub" }, diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index a5887f16b9..76453591a5 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -55,7 +55,7 @@ "resetWorkspacePrompt": "Obnovením pracovního prostoru smažete všechny stránky a data v nich. Opravdu chcete obnovit pracovní prostor? Pro obnovení pracovního prostoru můžete kontaktovat podporu.", "hint": "pracovní plocha", "notFoundError": "Pracovní prostor nenalezen", - "failedToLoad": "Něco se pokazilo! Nepodařilo se načíst pracovní prostor. Zkuste zavřít a znovu otevřít AppFlowy a zkuste to znovu.", + "failedToLoad": "Něco se pokazilo! Nepodařilo se načíst pracovní prostor. Zkuste zavřít a znovu otevřít @:appName a zkuste to znovu.", "errorActions": { "reportIssue": "Nahlásit problém", "reachOut": "Ozvat se na Discordu" @@ -265,7 +265,7 @@ "enableSync": "Zapnout synchronizaci", "enableEncrypt": "Šifrovat data", "cloudURL": "URL adresa serveru", - "cloudAppFlowy": "AppFlowy Cloud Beta", + "cloudAppFlowy": "@:appName Cloud Beta", "enableEncryptPrompt": "Zapněte šifrování a zabezpečte svá ", "inputEncryptPrompt": "Vložte prosím Váš šifrovací klíč k", "clickToCopySecret": "Kliknutím zkopírujete šifrovací klíč", @@ -273,7 +273,7 @@ "historicalUserList": "Historie přihlášení uživatele", "historicalUserListTooltip": "V tomto seznamu vidíte anonymní účty. Kliknutím na účet zobrazíte jeho detaily. Anonymní účty vznikají kliknutím na tlačítko \"Začínáme\"", "openHistoricalUser": "Kliknutím založíte anonymní účet", - "customPathPrompt": "Uložením složky s daty AppFlowy ve složce synchronizovanéí jako např. Google Drive může nést rizika. Pokud se databáze v složce navštíví nebo změní ", + "customPathPrompt": "Uložením složky s daty @:appName ve složce synchronizovanéí jako např. Google Drive může nést rizika. Pokud se databáze v složce navštíví nebo změní ", "cloudSetting": "Nastavení cloudu" }, "notifications": { @@ -311,7 +311,7 @@ "themeUpload": { "button": "Nahrát", "uploadTheme": "Nahrát motiv vzhledu", - "description": "Nahrajte vlastní motiv vzhledu pro AppFlowy stisknutím tlačítka níže.", + "description": "Nahrajte vlastní motiv vzhledu pro @:appName stisknutím tlačítka níže.", "loading": "Prosím počkejte dokud nedokončíme kontrolu a nahrávání vašeho motivu vzhledu...", "uploadSuccess": "Váš motiv vzhledu byl úspěšně nahrán", "deletionFailure": "Nepodařilo se smazat motiv vzhledu. Zkuste ho smazat ručně.", @@ -342,7 +342,7 @@ "defaultLocation": "Umístění pro čtení a ukládání dat", "exportData": "Exportovat data", "doubleTapToCopy": "Dvojitým klepnutím zkopírujete cestu", - "restoreLocation": "Obnovit výchozí AppFlowy cestu", + "restoreLocation": "Obnovit výchozí @:appName cestu", "customizeLocation": "OtevřítProsím tre další složku", "restartApp": "Aby se projevily změny, restartujte prosím aplikaci.", "exportDatabase": "Exportovat databázi", @@ -354,10 +354,10 @@ "defineWhereYourDataIsStored": "Vyberte kde jsou ukládána Vaše data", "open": "Otevřít", "openFolder": "Otevřít existující složku", - "openFolderDesc": "Číst a zapisovat do existující AppFlowy složky", + "openFolderDesc": "Číst a zapisovat do existující @:appName složky", "folderHintText": "název složky", "location": "Vytváření nové složky", - "locationDesc": "Vyberte název pro složku, kam bude AppFlowy ukládat Vaše data", + "locationDesc": "Vyberte název pro složku, kam bude @:appName ukládat Vaše data", "browser": "Procházet", "create": "Vytvořit", "set": "Nastavit", @@ -836,7 +836,7 @@ "referencedCalendarPrefix": "Pohled na" }, "errorDialog": { - "title": "Chyba AppFlowy", + "title": "Chyba @:appName", "howToFixFallback": "Omlouváme se za nepříjemnost! Pošlete hlášení na náš GitHub, kde popíšete chybu na kterou jste narazili.", "github": "Zobrazit na GitHubu" }, diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index f24d6bd2a0..11c8da8e62 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -76,7 +76,7 @@ "resetWorkspacePrompt": "Das Zurücksetzen des Workspace löscht alle enthaltenen Seiten und Daten. Bist du sicher, dass du den Arbeitsbereich zurücksetzen möchstest? ", "hint": "Workspace", "notFoundError": "Workspace nicht gefunden", - "failedToLoad": "Etwas ist schief gelaufen! Der Workspace konnte nicht geladen werden. Versuche, alle AppFlowy-Instanzen zu schließen & versuche es erneut.", + "failedToLoad": "Etwas ist schief gelaufen! Der Workspace konnte nicht geladen werden. Versuche, alle @:appName-Instanzen zu schließen & versuche es erneut.", "errorActions": { "reportIssue": "Problem melden", "reportIssueOnGithub": "Melde ein Problem auf Github", @@ -246,7 +246,7 @@ "addAPageToWorkspace": "Eine Seite zum Arbeitsbereich hinzufügen", "recent": "Zuletzt", "public": "Öffentlich", - "clickToHidePublic": "Hier klicken, um den öffentlichen Raum auszublenden.\nVon dir hier erstellte Seiten sind für jedes Mitglied sichtbar.", + "clickToHidePublic": "Hier klicken, um den öffentlichen Bereich auszublenden.\nHier erstellte Seiten sind für jedes Mitglied sichtbar.", "addAPageToPublic": "Eine Seite zum öffentlichen Bereich hinzufügen." }, "notifications": { @@ -329,10 +329,10 @@ "accountPage": { "menuLabel": "Mein Konto", "title": "Mein Konto", - "description": "Passe dein Profil an, verwalte deine Sicherheitseinstellungen und AI-API-Schlüssel oder melde dich bei deinem Konto an.", + "description": "Passe dein Profil an, verwalte deine Sicherheitseinstellungen und KI API-Schlüssel oder melde dich bei deinem Konto an.", "general": { "title": "Kontoname und Profilbild", - "changeProfilePicture": "Ändern" + "changeProfilePicture": "Profilbild ändern" }, "email": { "title": "E-Mail", @@ -404,7 +404,7 @@ "local": "Lokal", "us": "US", "iso": "ISO", - "friendly": "leserlich", + "friendly": "Leserlich", "dmy": "T/M/J" } }, @@ -428,7 +428,7 @@ "manageDataPage": { "menuLabel": "Daten verwalten", "title": "Daten verwalten", - "description": "Verwalte den lokalen Datenspeicher oder importiere deine vorhandenen Daten in AppFlowy. Du kannst deine Daten mit End-to-End-Verschlüsselung absichern.", + "description": "Verwalte den lokalen Datenspeicher oder importiere deine vorhandenen Daten in @:appName. Du kannst deine Daten mit End-to-End-Verschlüsselung absichern.", "dataStorage": { "title": "Speicherort", "tooltip": "Das Verzeichnis, in dem deine Dateien gespeichert sind", @@ -446,8 +446,8 @@ }, "importData": { "title": "Daten importieren", - "tooltip": "Daten aus AppFlowy-Backups/Datenordnern importieren", - "description": "Daten aus einem externen AppFlowy-Datenordner kopieren und in den aktuellen AppFlowy-Datenordner importieren", + "tooltip": "Daten aus @:appName-Backups/Datenordnern importieren", + "description": "Daten aus einem externen @:appName-Datenordner kopieren und in den aktuellen @:appName-Datenordner importieren", "action": "Ordner durchsuchen" }, "encryption": { @@ -498,18 +498,18 @@ "cloudSupabaseUrlCanNotBeEmpty": "Die Supabase-URL darf nicht leer sein", "cloudSupabaseAnonKey": "Supabase anonymer Schlüssel", "cloudSupabaseAnonKeyCanNotBeEmpty": "Der anonyme Schlüssel darf nicht leer sein", - "cloudAppFlowy": "AppFlowy Cloud [BETA]", - "cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud Self-hosted", "appFlowyCloudUrlCanNotBeEmpty": "Die Cloud-URL darf nicht leer sein", "clickToCopy": "Klicken, um zu kopieren", - "selfHostStart": "Falls du keinen Server hast, nehme lieber folgende", + "selfHostStart": "Falls du keinen Server hast, konsultiere bitte", "selfHostContent": "Dokument", - "selfHostEnd": "für Hilfe, um einen einen eigenen Server aufzusetzen", + "selfHostEnd": "um einen einen eigenen Server aufzusetzen", "cloudURLHint": "Eingabe der Basis- URL Ihres Servers", "cloudWSURL": "Websocket URL", "cloudWSURLHint": "Eingbe der Websocket Adresse Ihres Servers", "restartApp": "Neustart", - "restartAppTip": "Programm neustarten, um die Änderungen zu übernehmen. Bitte bachten, dass der aktuelle Account eventuell ausgeloggt wird.", + "restartAppTip": "Programm neustarten, um die Änderungen zu übernehmen. Bitte beachten, dass der aktuelle Account eventuell ausgeloggt wird.", "changeServerTip": "Nach dem Wechsel des Servers muss auf die Schaltfläche „Neustart“ geklickt werden, damit die Änderungen wirksam werden", "enableEncryptPrompt": "Verschlüsselung aktivieren, um deine Daten mit dem Secret Key zu verschlüsseln. Verwahre den Schlüssel sicher! \nEinmal aktiviert kann es nicht mehr rückgängig gemacht werden.\nFalls der Schlüssel verloren geht sind die Daten unwiderbringlich verloren.\nKlicken, um zu kopieren.", "inputEncryptPrompt": "Bitte den Encryption Secret Code eingeben", @@ -520,12 +520,12 @@ "historicalUserList": "Anmeldeverlauf", "historicalUserListTooltip": "Diese Liste zeigt deine anonymen Accounts. Du kannst einen Account anklicken, um mehr Informationen zu sehen.\nAnonyme Accounts werden über den 'Erste Schritte' Button erstellt.", "openHistoricalUser": "Klicken, um einen anonymen Account zu öffnen", - "customPathPrompt": "Den AppFlowy Daten-Ordner in einem mit der Cloud synchronisierten Ordner (z.B. Google Drive) zu speichern, könnte Risiken bergen. Falls die Datenbank innerhalb dieses Ordners gleichzeitig von mehreren Orten zugegriffen oder verändert wird könnte es zu Synchronisationskonflikten und potentiellen Daten-Beschädigung führen", - "importAppFlowyData": "Daten von einem externen AppFlowy Ordner importieren.", + "customPathPrompt": "Den @:appName Daten-Ordner in einem mit der Cloud synchronisierten Ordner (z.B. Google Drive) zu speichern, könnte Risiken bergen. Falls die Datenbank innerhalb dieses Ordners gleichzeitig von mehreren Orten zugegriffen oder verändert wird könnte dies zu Synchronisationskonflikten und potentiellen Daten-Beschädigungen führen", + "importAppFlowyData": "Daten von einem externen @:appName Ordner importieren.", "importingAppFlowyDataTip": "Der Datenimport läuft. Bitte die App nicht schließen oder in den Hintergrund setzten", - "importAppFlowyDataDescription": "Daten von einem externen AppFlowy Ordner kopieren und in den aktuellen AppFlowy Datenordner importieren.", - "importSuccess": "Der AppFlowy Dateienordner wurde erfolgreich importiert", - "importFailed": "Der AppFlowy Dateienordner-Import ist fehlgeschlagen", + "importAppFlowyDataDescription": "Daten von einem externen @:appName Ordner kopieren und in den aktuellen @:appName Datenordner importieren.", + "importSuccess": "Der @:appName Dateienordner wurde erfolgreich importiert", + "importFailed": "Der @:appName Dateienordner-Import ist fehlgeschlagen", "importGuide": "Für weitere Details, bitte das verlinkte Dokument prüfen" }, "notifications": { @@ -577,13 +577,13 @@ "themeUpload": { "button": "Hochladen", "uploadTheme": "Theme hochladen", - "description": "Lade eigenes AppFlowy-Theme über die Schaltfläche unten hoch.", + "description": "Lade eigenes @:appName-Theme über die untere Schaltfläche hoch.", "loading": "Bitte warte einen Moment . . .\nWir validieren gerade dein Theme und laden es hoch.", "uploadSuccess": "Das Theme wurde erfolgreich hochgeladen", "deletionFailure": "Das Theme konnte nicht gelöscht werden. Versuche, es manuell zu löschen.", "filePickerDialogTitle": "Wähle eine .flowy_plugin-Datei", "urlUploadFailure": "URL konnte nicht geöffnet werden: {}", - "failure": "Das hochgeladene Theme hatte ein ungültiges Format." + "failure": "Das hochgeladene Theme hat ein ungültiges Format." }, "theme": "Theme", "builtInsLabel": "Integrierte Theme", @@ -593,7 +593,7 @@ "local": "Lokal", "us": "US", "iso": "ISO", - "friendly": "Freundlich", + "friendly": "Leserlich", "dmy": "TT/MM/JJJJ" }, "timeFormat": { @@ -602,7 +602,7 @@ "twentyFourHour": "24 Stunden" }, "showNamingDialogWhenCreatingPage": "Zeige Bennenungsfenster, wenn eine neue Seite erstellt wird", - "enableRTLToolbarItems": "Aktivieren Sie RTL-Symbolleiste", + "enableRTLToolbarItems": "RTL-Symbolleistenelemente aktivieren", "members": { "title": "Mitglieder-Einstellungen", "inviteMembers": "Mitglieder einladen", @@ -636,10 +636,10 @@ }, "files": { "copy": "Kopieren", - "defaultLocation": "Dateien und Speicherort", + "defaultLocation": "@:appName Datenverzeichnis", "exportData": "Daten exportieren", "doubleTapToCopy": "Zweimal tippen, um den Pfad zu kopieren", - "restoreLocation": "AppFlowy-Standardpfad wiederherstellen", + "restoreLocation": "@:appName-Standardpfad wiederherstellen", "customizeLocation": "Einen anderen Ordner öffnen", "restartApp": "Bitte starten Sie die App neu, damit die Änderungen wirksam werden.", "exportDatabase": "Datenbank exportieren", @@ -651,10 +651,10 @@ "defineWhereYourDataIsStored": "Wo sind die Daten gespeichert?", "open": "Offen", "openFolder": "Einen vorhandenen Ordner öffnen", - "openFolderDesc": "Öffnen und speichern im vorhandenen AppFlowy-Ordner", + "openFolderDesc": "Öffnen und speichern im vorhandenen @:appName-Ordner", "folderHintText": "Ordnernamen", "location": "Ein neuen Ordner erstellen", - "locationDesc": "Einen Namen für den AppFlowy-Datenordner festlegen", + "locationDesc": "Einen Namen für den @:appName-Datenordner festlegen", "browser": "Durchsuchen", "create": "Erstellen", "set": "Festlegen", @@ -665,7 +665,7 @@ "change": "Ändern", "openLocationTooltips": "Win anderes Datenverzeichnis öffnen", "openCurrentDataFolder": "Aktuelles Datenverzeichnis öffnen", - "recoverLocationTooltips": "Zurücksetzen auf das Standarddatenverzeichnis von AppFlowy", + "recoverLocationTooltips": "Zurücksetzen auf das Standarddatenverzeichnis von @:appName", "exportFileSuccess": "Datei erfolgreich exportiert!", "exportFileFail": "Datei-Export fehlgeschlagen!", "export": "Export", @@ -1294,7 +1294,7 @@ "duplicateEvent": "Doppeltes Ereignis" }, "errorDialog": { - "title": "AppFlowy-Fehler", + "title": "@:appName-Fehler", "howToFixFallback": "Wir entschuldigen uns für die Unannehmlichkeiten! Reiche auf unserer GitHub-Seite ein Problem ein, das Ihren Fehler beschreibt.", "github": "Auf GitHub ansehen" }, @@ -1535,7 +1535,7 @@ "opacity": "Transparenz", "resetToDefaultColor": "Auf Standardfarben zurücksetzen", "ltr": "LTR (Links nach rechts)", - "rtl": "RTL (Rechts nach lins)", + "rtl": "RTL (rechts nach links)", "auto": "Auto", "cut": "Ausschneiden", "copy": "Kopieren", @@ -1662,7 +1662,7 @@ "none": "Keines", "photoPermissionDescription": "Erlaube den Zugriff auf die Fotobibliothek zum Hochladen von Bildern.", "openSettings": "Einstellungen öffnen", - "photoPermissionTitle": "AppFlowy möchte auf deine Fotobibliothek zugreifen", + "photoPermissionTitle": "@:appName möchte auf deine Fotobibliothek zugreifen", "doNotAllow": "Nicht zulassen", "image": "Bild" }, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ff43722720..1b286e2498 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -73,7 +73,7 @@ "resetWorkspacePrompt": "Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace", "hint": "workspace", "notFoundError": "Workspace not found", - "failedToLoad": "Something went wrong! Failed to load the workspace. Try to close any open instance of AppFlowy and try again.", + "failedToLoad": "Something went wrong! Failed to load the workspace. Try to close any open instance of @:appName and try again.", "errorActions": { "reportIssue": "Report an issue", "reportIssueOnGithub": "Report an issue on Github", @@ -420,7 +420,7 @@ "manageDataPage": { "menuLabel": "Manage data", "title": "Manage data", - "description": "Manage data local storage or Import your existing data into AppFlowy. You can secure your data with end to end encryption.", + "description": "Manage data local storage or Import your existing data into @:appName. You can secure your data with end to end encryption.", "dataStorage": { "title": "File storage location", "tooltip": "The location where your files are stored", @@ -438,8 +438,8 @@ }, "importData": { "title": "Import data", - "tooltip": "Import data from AppFlowy backups/data folders", - "description": "Copy data from an external AppFlowy data folder and import it into the current AppFlowy data folder", + "tooltip": "Import data from @:appName backups/data folders", + "description": "Copy data from an external @:appName data folder and import it into the current @:appName data folder", "action": "Browse folder" }, "encryption": { @@ -490,8 +490,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "The supabase url can't be empty", "cloudSupabaseAnonKey": "Supabase anon key", "cloudSupabaseAnonKeyCanNotBeEmpty": "The anon key can't be empty", - "cloudAppFlowy": "AppFlowy Cloud Beta", - "cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud Self-hosted", "appFlowyCloudUrlCanNotBeEmpty": "The cloud url can't be empty", "clickToCopy": "Click to copy", "selfHostStart": "If you don't have a server, please refer to the", @@ -512,12 +512,12 @@ "historicalUserList": "User login history", "historicalUserListTooltip": "This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button", "openHistoricalUser": "Click to open the anonymous account", - "customPathPrompt": "Storing the AppFlowy data folder in a cloud-synced folder such as Google Drive can pose risks. If the database within this folder is accessed or modified from multiple locations at the same time, it may result in synchronization conflicts and potential data corruption", - "importAppFlowyData": "Import Data from External AppFlowy Folder", + "customPathPrompt": "Storing the @:appName data folder in a cloud-synced folder such as Google Drive can pose risks. If the database within this folder is accessed or modified from multiple locations at the same time, it may result in synchronization conflicts and potential data corruption", + "importAppFlowyData": "Import Data from External @:appName Folder", "importingAppFlowyDataTip": "Data import is in progress. Please do not close the app", - "importAppFlowyDataDescription": "Copy data from an external AppFlowy data folder and import it into the current AppFlowy data folder", - "importSuccess": "Successfully imported the AppFlowy data folder", - "importFailed": "Importing the AppFlowy data folder failed", + "importAppFlowyDataDescription": "Copy data from an external @:appName data folder and import it into the current AppFlowy data folder", + "importSuccess": "Successfully imported the @:appName data folder", + "importFailed": "Importing the @:appName data folder failed", "importGuide": "For further details, please check the referenced document" }, "notifications": { @@ -569,7 +569,7 @@ "themeUpload": { "button": "Upload", "uploadTheme": "Upload theme", - "description": "Upload your own AppFlowy theme using the button below.", + "description": "Upload your own @:appName theme using the button below.", "loading": "Please wait while we validate and upload your theme...", "uploadSuccess": "Your theme was uploaded successfully", "deletionFailure": "Failed to delete the theme. Try to delete it manually.", @@ -630,7 +630,7 @@ "defaultLocation": "Read files and data storage location", "exportData": "Export your data", "doubleTapToCopy": "Double tap to copy the path", - "restoreLocation": "Restore to AppFlowy default path", + "restoreLocation": "Restore to @:appName default path", "customizeLocation": "Open another folder", "restartApp": "Please restart app for the changes to take effect.", "exportDatabase": "Export database", @@ -642,10 +642,10 @@ "defineWhereYourDataIsStored": "Define where your data is stored", "open": "Open", "openFolder": "Open an existing folder", - "openFolderDesc": "Read and write it to your existing AppFlowy folder", + "openFolderDesc": "Read and write it to your existing @:appName folder", "folderHintText": "folder name", "location": "Creating a new folder", - "locationDesc": "Pick a name for your AppFlowy data folder", + "locationDesc": "Pick a name for your @:appName data folder", "browser": "Browse", "create": "Create", "set": "Set", @@ -656,7 +656,7 @@ "change": "Change", "openLocationTooltips": "Open another data directory", "openCurrentDataFolder": "Open current data directory", - "recoverLocationTooltips": "Reset to AppFlowy's default data directory", + "recoverLocationTooltips": "Reset to @:appName's default data directory", "exportFileSuccess": "Export file successfully!", "exportFileFail": "Export file failed!", "export": "Export", @@ -1289,7 +1289,7 @@ "duplicateEvent": "Duplicate event" }, "errorDialog": { - "title": "AppFlowy Error", + "title": "@:appName Error", "howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.", "github": "View on GitHub" }, @@ -1657,7 +1657,7 @@ "none": "None", "photoPermissionDescription": "Allow access to the photo library for uploading images.", "openSettings": "Open Settings", - "photoPermissionTitle": "AppFlowy Would Like to Access Your Photo Library", + "photoPermissionTitle": "@:appName would like to access your photo library", "doNotAllow": "Don't Allow", "image": "Image" }, diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 80af6a0756..1393cbc7e1 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -78,7 +78,7 @@ "resetWorkspacePrompt": "Al restablecer el espacio de trabajo se eliminarán todas las páginas y datos que contiene. ¿Está seguro de que desea restablecer el espacio de trabajo? Alternativamente, puede comunicarse con el equipo de soporte para restaurar el espacio de trabajo.", "hint": "Espacio de trabajo", "notFoundError": "Espacio de trabajo no encontrado", - "failedToLoad": "¡Algo salió mal! No se pudo cargar el espacio de trabajo. Intente cerrar cualquier instancia abierta de AppFlowy y vuelva a intentarlo.", + "failedToLoad": "¡Algo salió mal! No se pudo cargar el espacio de trabajo. Intente cerrar cualquier instancia abierta de @:appName y vuelva a intentarlo.", "errorActions": { "reportIssue": "Reportar un problema", "reportIssueOnGithub": "Informar un problema en Github", @@ -375,8 +375,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "La URL de supabase no puede estar vacía.", "cloudSupabaseAnonKey": "Supabase clave anon", "cloudSupabaseAnonKeyCanNotBeEmpty": "La clave anon no puede estar vacía si la URL de supabase no está vacía", - "cloudAppFlowy": "Nube AppFlowy", - "cloudAppFlowySelfHost": "AppFlowy Cloud autohospedado", + "cloudAppFlowy": "Nube @:appName", + "cloudAppFlowySelfHost": "@:appName Cloud autohospedado", "appFlowyCloudUrlCanNotBeEmpty": "La URL de la nube no puede estar vacía", "clickToCopy": "Haga clic para copiar", "selfHostStart": "Si no tiene un servidor, consulte la", @@ -397,12 +397,12 @@ "historicalUserList": "Historial de inicio de sesión del usuario", "historicalUserListTooltip": "Esta lista muestra tus cuentas anónimas. Puedes hacer clic en una cuenta para ver sus detalles. Las cuentas anónimas se crean haciendo clic en el botón \"Comenzar\".", "openHistoricalUser": "Haga clic para abrir la cuenta anónima", - "customPathPrompt": "Almacenar la carpeta de datos de AppFlowy en una carpeta sincronizada en la nube, como Google Drive, puede presentar riesgos. Si se accede a la base de datos dentro de esta carpeta o se modifica desde varias ubicaciones al mismo tiempo, se pueden producir conflictos de sincronización y posibles daños en los datos", - "importAppFlowyData": "Importar datos desde una carpeta externa de AppFlowy", + "customPathPrompt": "Almacenar la carpeta de datos de @:appName en una carpeta sincronizada en la nube, como Google Drive, puede presentar riesgos. Si se accede a la base de datos dentro de esta carpeta o se modifica desde varias ubicaciones al mismo tiempo, se pueden producir conflictos de sincronización y posibles daños en los datos", + "importAppFlowyData": "Importar datos desde una carpeta externa de @:appName", "importingAppFlowyDataTip": "La importación de datos está en curso. Por favor no cierres la aplicación.", - "importAppFlowyDataDescription": "Copia los datos de una carpeta de datos externa de AppFlowy e impórtalos a la carpeta de datos actual de AppFlowy", - "importSuccess": "Importó exitosamente la carpeta de datos de AppFlowy", - "importFailed": "Error al importar la carpeta de datos de AppFlowy", + "importAppFlowyDataDescription": "Copia los datos de una carpeta de datos externa de @:appName e impórtalos a la carpeta de datos actual de @:appName", + "importSuccess": "Importó exitosamente la carpeta de datos de @:appName", + "importFailed": "Error al importar la carpeta de datos de @:appName", "importGuide": "Para obtener más detalles, consulte el documento de referencia.", "supabaseSetting": "Ajuste de base superior" }, @@ -455,7 +455,7 @@ "themeUpload": { "button": "Subir", "uploadTheme": "Subir tema", - "description": "Cargue su propio tema AppFlowy usando el botón de abajo.", + "description": "Cargue su propio tema @:appName usando el botón de abajo.", "loading": "Espere mientras validamos y cargamos su tema...", "uploadSuccess": "Su tema se ha subido con éxito", "deletionFailure": "No se pudo eliminar el tema. Intenta eliminarlo manualmente.", @@ -517,7 +517,7 @@ "defaultLocation": "Leer archivos y ubicación de almacenamiento de datos", "exportData": "Exporta tus datos", "doubleTapToCopy": "Toca dos veces para copiar la ruta", - "restoreLocation": "Restaurar a la ruta predeterminada de AppFlowy", + "restoreLocation": "Restaurar a la ruta predeterminada de @:appName", "customizeLocation": "Abrir otra carpeta", "restartApp": "Reinicie la aplicación para que los cambios surtan efecto.", "exportDatabase": "Exportar base de datos", @@ -529,10 +529,10 @@ "defineWhereYourDataIsStored": "Defina dónde se almacenan sus datos", "open": "Abierto", "openFolder": "Abrir una carpeta existente", - "openFolderDesc": "Léalo y escríbalo en su carpeta AppFlowy existente", + "openFolderDesc": "Léalo y escríbalo en su carpeta @:appName existente", "folderHintText": "nombre de la carpeta", "location": "Creando una nueva carpeta", - "locationDesc": "Elija un nombre para su carpeta de datos de AppFlowy", + "locationDesc": "Elija un nombre para su carpeta de datos de @:appName", "browser": "Navegar", "create": "Crear", "set": "Colocar", @@ -543,7 +543,7 @@ "change": "Cambiar", "openLocationTooltips": "Abrir otro directorio de datos", "openCurrentDataFolder": "Abrir el directorio de datos actual", - "recoverLocationTooltips": "Restablecer al directorio de datos predeterminado de AppFlowy", + "recoverLocationTooltips": "Restablecer al directorio de datos predeterminado de @:appName", "exportFileSuccess": "¡Exportar archivo con éxito!", "exportFileFail": "¡Error en la exportación del archivo!", "export": "Exportar", diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index d3c25fa3aa..a070a59c54 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -222,7 +222,7 @@ }, "themeUpload": { "button": "Kargatu", - "description": "Kargatu zure AppFlowy gaia beheko botoia erabiliz.", + "description": "Kargatu zure @:appName gaia beheko botoia erabiliz.", "loading": "Mesedez, itxaron zure gaia balioztatzen eta kargatzen dugun bitartean...", "uploadSuccess": "Zure gaia behar bezala kargatu da", "deletionFailure": "Ezin izan da gaia ezabatu. Saiatu eskuz ezabatzen.", @@ -239,7 +239,7 @@ "defaultLocation": "Non gordetzen diren zure datuak", "exportData": "Esportatu zure datuak", "doubleTapToCopy": "Sakatu birritan bidea kopiatzeko", - "restoreLocation": "Berrezarri AppFlowy-ren biden lehenetsira", + "restoreLocation": "Berrezarri @:appName-ren biden lehenetsira", "customizeLocation": "Beste karpeta bat ireki", "restartApp": "Mesedez, berrabiarazi aplikazioa aldaketak indarrean egon daitezen.", "exportDatabase": "Datubasea exportatu", @@ -251,10 +251,10 @@ "defineWhereYourDataIsStored": "Zure datuak non gordetzen diren zehaztu", "open": "Oreki", "openFolder": "Ireki karpeta bat", - "openFolderDesc": "Irakurri eta idatzi zure AppFlowy karpetan...", + "openFolderDesc": "Irakurri eta idatzi zure @:appName karpetan...", "folderHintText": "karpetaren izena", "location": "Karpeta berria sortzen", - "locationDesc": "Aukeratu izen bat AppFlowy datuen karpetarako", + "locationDesc": "Aukeratu izen bat @:appName datuen karpetarako", "browser": "Bilatu", "create": "Sortu", "set": "Ezarri", @@ -265,7 +265,7 @@ "change": "Aldatu", "openLocationTooltips": "Ireki beste datu-direktorio bat", "openCurrentDataFolder": "Ireki uneko datuen direktorioa", - "recoverLocationTooltips": "Berrezarri AppFlowyren datu-direktorio lehenetsira", + "recoverLocationTooltips": "Berrezarri @:appNameren datu-direktorio lehenetsira", "exportFileSuccess": "Esportatu fitxategia behar bezala!", "exportFileFail": "Ezin izan da esportatu fitxategia!", "export": "Esportatu" @@ -580,7 +580,7 @@ "referencedCalendarPrefix": "-ren ikuspegia" }, "errorDialog": { - "title": "AppFlowy errorea", + "title": "@:appName errorea", "howToFixFallback": "Sentitzen dugu eragozpenak! Bidali zure errorea deskribatzen duen arazo bat gure GitHub orrian.", "github": "Ikusi GitHub-en" }, diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 0bb112d168..1f9b3526de 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -245,7 +245,7 @@ }, "themeUpload": { "button": "بارگذاری", - "description": "تم قالب AppFlowy خود را با استفاده از دکمه زیر آپلود کنید.", + "description": "تم قالب @:appName خود را با استفاده از دکمه زیر آپلود کنید.", "loading": "لطفاً منتظر بمانید تا تم قالب شما را اعتبارسنجی و آپلود کنیم...", "uploadSuccess": "تم قالب شما با موفقیت آپلود شد", "deletionFailure": "تم حذف نشد. سعی کنید آن را به صورت دستی حذف کنید.", @@ -262,7 +262,7 @@ "defaultLocation": "خواندن فایل‌ها و مکان ذخیره داده‌ها", "exportData": "از داده‌های خود خروجی بگیرید", "doubleTapToCopy": "برای کپی کردن دوبار کلیک کنید", - "restoreLocation": "بازیابی به مسیر پیش فرض AppFlowy", + "restoreLocation": "بازیابی به مسیر پیش فرض @:appName", "customizeLocation": "پوشه دیگری باز کنید", "restartApp": "لطفاً برنامه را مجدداً راه اندازی کنید تا تغییرات اعمال شوند.", "exportDatabase": "از پایگاه داده‌ها خروجی بگیرید", @@ -274,10 +274,10 @@ "defineWhereYourDataIsStored": "محل ذخیره داده های خود را مشخص کنید", "open": "باز کردن", "openFolder": "باز کردن یک پوشه موجود", - "openFolderDesc": "خواندن و نوشتن آن در یک پوشه AppFlowy موجود", + "openFolderDesc": "خواندن و نوشتن آن در یک پوشه @:appName موجود", "folderHintText": "نام پوشه", "location": "ایجاد یک پوشه جدید", - "locationDesc": "یک نام برای پوشه داده AppFlowy خود انتخاب کنید", + "locationDesc": "یک نام برای پوشه داده @:appName خود انتخاب کنید", "browser": "مرورگر", "create": "ایجاد کردن", "set": "تنظیم کردن", @@ -288,7 +288,7 @@ "change": "تغییر", "openLocationTooltips": "باز کردن یک فهرست پوشه دیگر", "openCurrentDataFolder": "باز کردن فهرست پوشه فعلی", - "recoverLocationTooltips": "بازنشانی به فهرست داده های پیش فرض AppFlowy", + "recoverLocationTooltips": "بازنشانی به فهرست داده های پیش فرض @:appName", "exportFileSuccess": "خروجی گرفتن از فایل با موفقیت انجام شد.", "exportFileFail": "خروجی گرفتن از فایل انجام نشد!", "export": "خروجی گرفتن" @@ -621,7 +621,7 @@ "referencedCalendarPrefix": "نمای" }, "errorDialog": { - "title": "خطای AppFlowy", + "title": "خطای @:appName", "howToFixFallback": "بابت مشکل پیش آمده متأسفیم! مشکل و شرح آن را در صفحه GitHub ما ارسال کنید.", "github": "مشاهده در GitHub" }, diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 958762094e..9fd37affce 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -58,7 +58,7 @@ "resetWorkspacePrompt": "La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail", "hint": "Espace de travail", "notFoundError": "Espace de travail introuvable", - "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'AppFlowy et réessayez.", + "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'@:appName et réessayez.", "errorActions": { "reportIssue": "Signaler un problème", "reportIssueOnGithub": "Signaler un bug sur Github", @@ -283,8 +283,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "L'URL Supabase ne peut pas être vide", "cloudSupabaseAnonKey": "Clé anonyme Supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de Supabase n'est pas vide", - "cloudAppFlowy": "AppFlowy Cloud Bêta", - "cloudAppFlowySelfHost": "AppFlowy Cloud auto-hébergé", + "cloudAppFlowy": "@:appName Cloud Bêta", + "cloudAppFlowySelfHost": "@:appName Cloud auto-hébergé", "appFlowyCloudUrlCanNotBeEmpty": "L'URL cloud ne peut pas être vide", "clickToCopy": "Cliquez pour copier", "selfHostStart": "Si vous n'avez pas de serveur, veuillez vous référer au", @@ -305,12 +305,12 @@ "historicalUserList": "Historique de connexion d'utilisateurs", "historicalUserListTooltip": "Cette liste affiche vos comptes anonymes. Vous pouvez cliquer sur un compte pour afficher ses détails. Les comptes anonymes sont créés en cliquant sur le bouton « Commencer »", "openHistoricalUser": "Cliquez pour ouvrir le compte anonyme", - "customPathPrompt": "Le stockage du dossier de données AppFlowy dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.", - "importAppFlowyData": "Importer des données à partir du dossier AppFlowy externe", + "customPathPrompt": "Le stockage du dossier de données @:appName dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.", + "importAppFlowyData": "Importer des données à partir du dossier @:appName externe", "importingAppFlowyDataTip": "L'importation des données est en cours. Veuillez ne pas fermer l'application", - "importAppFlowyDataDescription": "Copiez les données d'un dossier de données AppFlowy externe et importez-les dans le dossier de données AppFlowy actuel", - "importSuccess": "Importation réussie du dossier de données AppFlowy", - "importFailed": "L'importation du dossier de données AppFlowy a échoué", + "importAppFlowyDataDescription": "Copiez les données d'un dossier de données @:appName externe et importez-les dans le dossier de données @:appName actuel", + "importSuccess": "Importation réussie du dossier de données @:appName", + "importFailed": "L'importation du dossier de données @:appName a échoué", "importGuide": "Pour plus de détails, veuillez consulter le document référencé", "supabaseSetting": "Paramètre Supabase" }, @@ -361,7 +361,7 @@ "themeUpload": { "button": "Téléverser", "uploadTheme": "Téléverser le thème", - "description": "Téléversez votre propre thème AppFlowy en utilisant le bouton ci-dessous.", + "description": "Téléversez votre propre thème @:appName en utilisant le bouton ci-dessous.", "loading": "Veuillez patienter pendant que nous validons et téléchargeons votre thème...", "uploadSuccess": "Votre thème a été téléversé avec succès", "deletionFailure": "Échec de la suppression du thème. Essayez de le supprimer manuellement.", @@ -392,7 +392,7 @@ "defaultLocation": "Lire les fichiers et l'emplacement de stockage des données", "exportData": "Exportez vos données", "doubleTapToCopy": "Appuyez deux fois pour copier le chemin", - "restoreLocation": "Restaurer le chemin par défaut d'AppFlowy", + "restoreLocation": "Restaurer le chemin par défaut d'@:appName", "customizeLocation": "Ouvrir un autre dossier", "restartApp": "Veuillez redémarrer l'application pour que les modifications prennent effet.", "exportDatabase": "Exporter la base de données", @@ -404,10 +404,10 @@ "defineWhereYourDataIsStored": "Définissez où vos données sont stockées", "open": "Ouvrir", "openFolder": "Ouvrir un dossier existant", - "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier AppFlowy existant", + "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier @:appName existant", "folderHintText": "Nom de dossier", "location": "Création d'un nouveau dossier", - "locationDesc": "Choisissez un nom pour votre dossier de données AppFlowy", + "locationDesc": "Choisissez un nom pour votre dossier de données @:appName", "browser": "Parcourir", "create": "Créer", "set": "Définir", @@ -418,7 +418,7 @@ "change": "Changer", "openLocationTooltips": "Ouvrir un autre répertoire de données", "openCurrentDataFolder": "Ouvrir le répertoire de données actuel", - "recoverLocationTooltips": "Réinitialiser au répertoire de données par défaut d'AppFlowy", + "recoverLocationTooltips": "Réinitialiser au répertoire de données par défaut d'@:appName", "exportFileSuccess": "Exporter le fichier avec succès !", "exportFileFail": "Échec de l'export du fichier !", "export": "Exporter" @@ -963,7 +963,7 @@ "quickJumpYear": "Sauter à" }, "errorDialog": { - "title": "Erreur AppFlowy", + "title": "Erreur @:appName", "howToFixFallback": "Nous sommes désolés pour le désagrément ! Soumettez un problème sur notre page GitHub qui décrit votre erreur.", "github": "Afficher sur GitHub" }, diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 6a85f8cbc9..c24ce58aa7 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -66,7 +66,7 @@ "resetWorkspacePrompt": "La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail", "hint": "Espace de travail", "notFoundError": "Espace de travail introuvable", - "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'AppFlowy et réessayez.", + "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'@:appName et réessayez.", "errorActions": { "reportIssue": "Signaler un problème", "reportIssueOnGithub": "Signaler un bug sur Github", @@ -439,8 +439,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "L'URL Supabase ne peut pas être vide", "cloudSupabaseAnonKey": "Clé anonyme Supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de Supabase n'est pas vide", - "cloudAppFlowy": "AppFlowy Cloud Bêta", - "cloudAppFlowySelfHost": "AppFlowy Cloud auto-hébergé", + "cloudAppFlowy": "@:appName Cloud Bêta", + "cloudAppFlowySelfHost": "@:appName Cloud auto-hébergé", "appFlowyCloudUrlCanNotBeEmpty": "L'URL cloud ne peut pas être vide", "clickToCopy": "Cliquez pour copier", "selfHostStart": "Si vous n'avez pas de serveur, veuillez vous référer au", @@ -461,12 +461,12 @@ "historicalUserList": "Historique de connexion d'utilisateurs", "historicalUserListTooltip": "Cette liste affiche vos comptes anonymes. Vous pouvez cliquer sur un compte pour afficher ses détails. Les comptes anonymes sont créés en cliquant sur le bouton « Commencer »", "openHistoricalUser": "Cliquez pour ouvrir le compte anonyme", - "customPathPrompt": "Le stockage du dossier de données AppFlowy dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.", - "importAppFlowyData": "Importer des données à partir du dossier AppFlowy externe", + "customPathPrompt": "Le stockage du dossier de données @:appName dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.", + "importAppFlowyData": "Importer des données à partir du dossier @:appName externe", "importingAppFlowyDataTip": "L'importation des données est en cours. Veuillez ne pas fermer l'application", - "importAppFlowyDataDescription": "Copiez les données d'un dossier de données AppFlowy externe et importez-les dans le dossier de données AppFlowy actuel", - "importSuccess": "Importation réussie du dossier de données AppFlowy", - "importFailed": "L'importation du dossier de données AppFlowy a échoué", + "importAppFlowyDataDescription": "Copiez les données d'un dossier de données @:appName externe et importez-les dans le dossier de données @:appName actuel", + "importSuccess": "Importation réussie du dossier de données @:appName", + "importFailed": "L'importation du dossier de données @:appName a échoué", "importGuide": "Pour plus de détails, veuillez consulter le document référencé", "supabaseSetting": "Paramètre Supabase" }, @@ -518,7 +518,7 @@ "themeUpload": { "button": "Téléverser", "uploadTheme": "Téléverser le thème", - "description": "Téléversez votre propre thème AppFlowy en utilisant le bouton ci-dessous.", + "description": "Téléversez votre propre thème @:appName en utilisant le bouton ci-dessous.", "loading": "Veuillez patienter pendant que nous validons et téléchargeons votre thème...", "uploadSuccess": "Votre thème a été téléversé avec succès", "deletionFailure": "Échec de la suppression du thème. Essayez de le supprimer manuellement.", @@ -578,7 +578,7 @@ "defaultLocation": "Lire les fichiers et l'emplacement de stockage des données", "exportData": "Exportez vos données", "doubleTapToCopy": "Appuyez deux fois pour copier le chemin", - "restoreLocation": "Restaurer le chemin par défaut d'AppFlowy", + "restoreLocation": "Restaurer le chemin par défaut d'@:appName", "customizeLocation": "Ouvrir un autre dossier", "restartApp": "Veuillez redémarrer l'application pour que les modifications prennent effet.", "exportDatabase": "Exporter la base de données", @@ -590,10 +590,10 @@ "defineWhereYourDataIsStored": "Définissez où vos données sont stockées", "open": "Ouvrir", "openFolder": "Ouvrir un dossier existant", - "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier AppFlowy existant", + "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier @:appName existant", "folderHintText": "Nom de dossier", "location": "Création d'un nouveau dossier", - "locationDesc": "Choisissez un nom pour votre dossier de données AppFlowy", + "locationDesc": "Choisissez un nom pour votre dossier de données @:appName", "browser": "Parcourir", "create": "Créer", "set": "Définir", @@ -604,7 +604,7 @@ "change": "Changer", "openLocationTooltips": "Ouvrir un autre répertoire de données", "openCurrentDataFolder": "Ouvrir le répertoire de données actuel", - "recoverLocationTooltips": "Réinitialiser au répertoire de données par défaut d'AppFlowy", + "recoverLocationTooltips": "Réinitialiser au répertoire de données par défaut d'@:appName", "exportFileSuccess": "Exporter le fichier avec succès !", "exportFileFail": "Échec de l'export du fichier !", "export": "Exporter", @@ -1218,7 +1218,7 @@ "duplicateEvent": "Événement en double" }, "errorDialog": { - "title": "Erreur AppFlowy", + "title": "Erreur @:appName", "howToFixFallback": "Nous sommes désolés pour le désagrément ! Soumettez un problème sur notre page GitHub qui décrit votre erreur.", "github": "Afficher sur GitHub" }, diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index 3a0d464d5a..7c25d96d14 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -226,7 +226,7 @@ }, "themeUpload": { "button": "Feltöltés", - "description": "Töltse fel saját AppFlowy témáját az alábbi gomb segítségével.", + "description": "Töltse fel saját @:appName témáját az alábbi gomb segítségével.", "loading": "Kérjük, várjon, amíg ellenőrizzük és feltöltjük a témát...", "uploadSuccess": "A témát sikeresen feltöltötte", "deletionFailure": "Nem sikerült törölni a témát. Próbálja meg manuálisan törölni.", @@ -243,7 +243,7 @@ "defaultLocation": "Fájlok és adattárolási hely olvasása", "exportData": "Exportálja adatait", "doubleTapToCopy": "Koppintson duplán az útvonal másolásához", - "restoreLocation": "Visszaállítás az AppFlowy alapértelmezett elérési útjára", + "restoreLocation": "Visszaállítás az @:appName alapértelmezett elérési útjára", "customizeLocation": "Nyisson meg egy másik mappát", "restartApp": "Kérjük, indítsa újra az alkalmazást, hogy a változtatások életbe lépjenek.", "exportDatabase": "Adatbázis exportálása", @@ -255,10 +255,10 @@ "defineWhereYourDataIsStored": "Határozza meg, hol tárolják adatait", "open": "Nyisd ki", "openFolder": "Nyisson meg egy meglévő mappát", - "openFolderDesc": "Olvassa el és írja be a meglévő AppFlowy mappájába", + "openFolderDesc": "Olvassa el és írja be a meglévő @:appName mappájába", "folderHintText": "mappa neve", "location": "Új mappa létrehozása", - "locationDesc": "Válasszon nevet az AppFlowy adatmappájának", + "locationDesc": "Válasszon nevet az @:appName adatmappájának", "browser": "Tallózás", "create": "Teremt", "set": "Készlet", @@ -269,7 +269,7 @@ "change": "változás", "openLocationTooltips": "Nyisson meg egy másik adatkönyvtárat", "openCurrentDataFolder": "Nyissa meg az aktuális adatkönyvtárat", - "recoverLocationTooltips": "Állítsa vissza az AppFlowy alapértelmezett adatkönyvtárát", + "recoverLocationTooltips": "Állítsa vissza az @:appName alapértelmezett adatkönyvtárát", "exportFileSuccess": "A fájl exportálása sikeres volt!", "exportFileFail": "A fájl exportálása nem sikerült!", "export": "Export" @@ -578,7 +578,7 @@ "referencedCalendarPrefix": "Nézet" }, "errorDialog": { - "title": "AppFlowy hiba", + "title": "@:appName hiba", "howToFixFallback": "Elnézést kérünk a kellemetlenségért! Nyújtsa be a problémát a GitHub-oldalunkon, amely leírja a hibát.", "github": "Megtekintés a GitHubon" }, diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index 4aa1e71038..90c1835f57 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -58,7 +58,7 @@ "resetWorkspacePrompt": "Mengatur ulang area kerja akan menghapus semua halaman dan data di dalamnya. Apakah anda yakin ingin Mengatur ulang area kerja? Selain itu, anda bisa menghubungi tim dukungan untuk mengembalikan area kerja", "hint": "Area kerja", "notFoundError": "Area kerja tidak ditemukan", - "failedToLoad": "Ada yang tidak beres! Gagal memuat area kerja. Coba tutup AppFlowy yang terbuka dan coba lagi.", + "failedToLoad": "Ada yang tidak beres! Gagal memuat area kerja. Coba tutup @:appName yang terbuka dan coba lagi.", "errorActions": { "reportIssue": "Melaporkan isu", "reachOut": "Hubungi di Discord" @@ -309,7 +309,7 @@ "themeUpload": { "button": "Mengunggah", "uploadTheme": "Unggah tema", - "description": "Unggah tema AppFlowy Anda sendiri menggunakan tombol di bawah ini.", + "description": "Unggah tema @:appName Anda sendiri menggunakan tombol di bawah ini.", "loading": "Harap tunggu sementara kami memvalidasi dan mengunggah tema Anda...", "uploadSuccess": "Tema Anda berhasil diunggah", "deletionFailure": "Gagal menghapus tema. Cobalah untuk menghapusnya secara manual.", @@ -340,7 +340,7 @@ "defaultLocation": "Baca file dan lokasi penyimpanan data", "exportData": "Ekspor data Anda", "doubleTapToCopy": "Ketuk dua kali untuk menyalin jalur", - "restoreLocation": "Pulihkan ke jalur default AppFlowy", + "restoreLocation": "Pulihkan ke jalur default @:appName", "customizeLocation": "Buka folder lain", "restartApp": "Harap mulai ulang aplikasi agar perubahan diterapkan.", "exportDatabase": "Ekspor basis data", @@ -352,10 +352,10 @@ "defineWhereYourDataIsStored": "Tentukan di mana data Anda disimpan", "open": "Membuka", "openFolder": "Buka folder yang ada", - "openFolderDesc": "Baca dan tulis ke folder AppFlowy Anda yang sudah ada", + "openFolderDesc": "Baca dan tulis ke folder @:appName Anda yang sudah ada", "folderHintText": "nama folder", "location": "Membuat folder baru", - "locationDesc": "Pilih nama untuk folder data AppFlowy Anda", + "locationDesc": "Pilih nama untuk folder data @:appName Anda", "browser": "Jelajahi", "create": "Membuat", "set": "Mengatur", @@ -366,7 +366,7 @@ "change": "Mengubah", "openLocationTooltips": "Buka direktori data lain", "openCurrentDataFolder": "Buka direktori data saat ini", - "recoverLocationTooltips": "Setel ulang ke direktori data default AppFlowy", + "recoverLocationTooltips": "Setel ulang ke direktori data default @:appName", "exportFileSuccess": "Ekspor file berhasil!", "exportFileFail": "File ekspor gagal!", "export": "Ekspor" @@ -788,7 +788,7 @@ "referencedCalendarPrefix": "Pemandangan dari" }, "errorDialog": { - "title": "Kesalahan AppFlowy", + "title": "Kesalahan @:appName", "howToFixFallback": "Kami mohon maaf atas ketidaknyamanan ini! Kirimkan masalah di halaman GitHub kami yang menjelaskan kesalahan Anda.", "github": "Lihat di GitHub" }, diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index 47e90eaa87..afed354749 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -56,7 +56,7 @@ "resetWorkspacePrompt": "Il ripristino dello spazio di lavoro eliminerà tutte le pagine e i dati al suo interno. Sei sicuro di voler ripristinare lo spazio di lavoro? In alternativa, puoi contattare il team di supporto per ristabilire lo spazio di lavoro", "hint": "spazio di lavoro", "notFoundError": "Spazio di lavoro non trovato", - "failedToLoad": "Qualcosa è andato storto! Impossibile caricare lo spazio di lavoro. Prova a chiudere qualsiasi istanza aperta di AppFlowy e riprova.", + "failedToLoad": "Qualcosa è andato storto! Impossibile caricare lo spazio di lavoro. Prova a chiudere qualsiasi istanza aperta di @:appName e riprova.", "errorActions": { "reportIssue": "Segnala un problema", "reportIssueOnGithub": "Segnalate un problema su Github", @@ -283,8 +283,8 @@ "cloudSupabase": "Supabase", "cloudSupabaseUrl": "URL di Supabase", "cloudSupabaseUrlCanNotBeEmpty": "L'url di supabase non può essere vuoto", - "cloudAppFlowy": "AppFlowy Cloud", - "cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted (autogestito)", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud Self-hosted (autogestito)", "appFlowyCloudUrlCanNotBeEmpty": "L'url del cloud non può essere vuoto", "clickToCopy": "Fare clic per copiare", "selfHostStart": "Se non disponi di un server, fai riferimento a", @@ -305,12 +305,12 @@ "historicalUserList": "Cronologia di accesso dell'utente", "historicalUserListTooltip": "Questo elenco mostra i tuoi account anonimi. È possibile fare clic su un account per visualizzarne i dettagli. Gli account anonimi vengono creati facendo clic sul pulsante \"Inizia\".", "openHistoricalUser": "Fare clic per aprire l'account anonimo", - "customPathPrompt": "L'archiviazione della cartella dati di AppFlowy in una cartella sincronizzata sul cloud come Google Drive può comportare rischi. Se si accede o si modifica il database all'interno di questa cartella da più posizioni contemporaneamente, potrebbero verificarsi conflitti di sincronizzazione e potenziale danneggiamento dei dati", - "importAppFlowyData": "Importa dati dalla cartella AppFlowy esterna", + "customPathPrompt": "L'archiviazione della cartella dati di @:appName in una cartella sincronizzata sul cloud come Google Drive può comportare rischi. Se si accede o si modifica il database all'interno di questa cartella da più posizioni contemporaneamente, potrebbero verificarsi conflitti di sincronizzazione e potenziale danneggiamento dei dati", + "importAppFlowyData": "Importa dati dalla cartella @:appName esterna", "importingAppFlowyDataTip": "L'importazione dei dati è in corso. Non chiudere l'applicazione", - "importAppFlowyDataDescription": "Copia i dati da una cartella dati AppFlowy esterna e importali nella cartella dati AppFlowy corrente", - "importSuccess": "Importazione della cartella dati AppFlowy riuscita", - "importFailed": "L'importazione della cartella dati di AppFlowy non è riuscita", + "importAppFlowyDataDescription": "Copia i dati da una cartella dati @:appName esterna e importali nella cartella dati @:appName corrente", + "importSuccess": "Importazione della cartella dati @:appName riuscita", + "importFailed": "L'importazione della cartella dati di @:appName non è riuscita", "importGuide": "Per ulteriori dettagli si prega di consultare il documento di riferimento", "supabaseSetting": "Impostazione Supabase" }, @@ -360,7 +360,7 @@ "themeUpload": { "button": "Caricamento", "uploadTheme": "Carica tema", - "description": "Carica il tuo tema AppFlowy utilizzando il pulsante in basso.", + "description": "Carica il tuo tema @:appName utilizzando il pulsante in basso.", "loading": "Attendi mentre convalidiamo e carichiamo il tuo tema...", "uploadSuccess": "Il tuo tema è stato caricato correttamente", "deletionFailure": "Impossibile eliminare il tema. Prova a eliminarlo manualmente.", @@ -391,7 +391,7 @@ "defaultLocation": "Leggi i file e la posizione di archiviazione dei dati", "exportData": "Esporta i tuoi dati", "doubleTapToCopy": "Tocca due volte per copiare il percorso", - "restoreLocation": "Ripristina nel percorso predefinito di AppFlowy", + "restoreLocation": "Ripristina nel percorso predefinito di @:appName", "customizeLocation": "Apri un'altra cartella", "restartApp": "Riavvia l'app per rendere effettive le modifiche.", "exportDatabase": "Esporta banca dati", @@ -403,10 +403,10 @@ "defineWhereYourDataIsStored": "Definisci dove sono archiviati i tuoi dati", "open": "Aprire", "openFolder": "Apri una cartella esistente", - "openFolderDesc": "Leggilo e scrivilo nella tua cartella AppFlowy esistente", + "openFolderDesc": "Leggilo e scrivilo nella tua cartella @:appName esistente", "folderHintText": "nome della cartella", "location": "Creazione di una nuova cartella", - "locationDesc": "Scegli un nome per la cartella dei dati di AppFlowy", + "locationDesc": "Scegli un nome per la cartella dei dati di @:appName", "browser": "Navigare", "create": "Creare", "set": "Impostato", @@ -417,7 +417,7 @@ "change": "Modifica", "openLocationTooltips": "Apri un'altra directory di dati", "openCurrentDataFolder": "Apre la directory dei dati corrente", - "recoverLocationTooltips": "Ripristina la directory dei dati predefinita di AppFlowy", + "recoverLocationTooltips": "Ripristina la directory dei dati predefinita di @:appName", "exportFileSuccess": "Esporta file con successo!", "exportFileFail": "File di esportazione non riuscito!", "export": "Esportare" @@ -969,7 +969,7 @@ "quickJumpYear": "Salta a" }, "errorDialog": { - "title": "Errore AppFlow", + "title": "Errore @:appName", "howToFixFallback": "Ci scusiamo per l'inconveniente! Invia un problema sulla nostra pagina GitHub che descriva il tuo errore.", "github": "Visualizza su GitHub" }, diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index 4388f9c3a9..5dc6c63285 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -255,8 +255,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "Supabase URLは空白にはできません", "cloudSupabaseAnonKey": "Supabase anon key", "cloudSupabaseAnonKeyCanNotBeEmpty": "Supabase anon keyは空白にはできません", - "cloudAppFlowy": "AppFlowy Cloud Beta", - "cloudAppFlowySelfHost": "AppFlowy Cloud セルフホスト", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud セルフホスト", "appFlowyCloudUrlCanNotBeEmpty": "クラウドURLは空白にはできません", "clickToCopy": "クリックしてコピー", "selfHostStart": "サーバーが準備できていない場合、", @@ -284,7 +284,7 @@ "themeUpload": { "button": "アップロード", "uploadTheme": "テーマをアップロード", - "description": "下のボタンを使用して、独自のAppFlowyテーマをアップロードします。", + "description": "下のボタンを使用して、独自の@:appNameテーマをアップロードします。", "loading": "テーマを検証してアップロードするまでお待ちください...", "uploadSuccess": "テーマは正常にアップロードされました", "deletionFailure": "テーマの削除に失敗しました。手動で削除してみてください。", @@ -309,7 +309,7 @@ "defaultLocation": "ファイルの読み取りとデータの保存場所", "exportData": "データをエクスポートする", "doubleTapToCopy": "ダブルタップしてパスをコピーします", - "restoreLocation": "AppFlowyのデフォルトパスに戻す", + "restoreLocation": "@:appNameのデフォルトパスに戻す", "customizeLocation": "別のフォルダーを開く", "restartApp": "変更を有効にするには、アプリを再起動してください。", "exportDatabase": "データベースのエクスポート", @@ -321,10 +321,10 @@ "defineWhereYourDataIsStored": "データの保存場所を定義する", "open": "開ける", "openFolder": "既存のフォルダーを開く", - "openFolderDesc": "既存のAppFlowyフォルダに読み書きします", + "openFolderDesc": "既存の@:appNameフォルダに読み書きします", "folderHintText": "フォルダ名", "location": "新しいフォルダーの作成", - "locationDesc": "AppFlowyデータフォルダーの名前を選択します", + "locationDesc": "@:appNameデータフォルダーの名前を選択します", "browser": "ブラウズ", "create": "作成", "set": "設定", @@ -335,7 +335,7 @@ "change": "変化", "openLocationTooltips": "別のデータ ディレクトリを開く", "openCurrentDataFolder": "現在のデータディレクトリを開く", - "recoverLocationTooltips": "AppFlowyのデフォルトのデータディレクトリにリセットします", + "recoverLocationTooltips": "@:appNameのデフォルトのデータディレクトリにリセットします", "exportFileSuccess": "ファイルのエクスポートに成功しました。", "exportFileFail": "ファイルのエクスポートに失敗しました!", "export": "書き出す" @@ -665,7 +665,7 @@ "referencedCalendarPrefix": "のビュー" }, "errorDialog": { - "title": "AppFlowyエラー", + "title": "@:appNameエラー", "howToFixFallback": "ご不便をおかけして申し訳ございません。エラーを説明した issue を GitHub ページに送信してください。", "github": "GitHub で見る" }, diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 81f7be0185..ecfc4dfe5b 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -222,7 +222,7 @@ }, "themeUpload": { "button": "업로드", - "description": "아래 버튼을 사용하여 나만의 AppFlowy 테마를 업로드하세요.", + "description": "아래 버튼을 사용하여 나만의 @:appName 테마를 업로드하세요.", "loading": "테마를 확인하고 업로드하는 동안 잠시 기다려 주십시오...", "uploadSuccess": "테마가 성공적으로 업로드되었습니다.", "deletionFailure": "테마를 삭제하지 못했습니다. 수동으로 삭제해 보십시오.", @@ -241,7 +241,7 @@ "defaultLocation": "파일 및 데이터 저장 위치 읽기", "exportData": "데이터 내보내기", "doubleTapToCopy": "경로를 복사하려면 두 번 탭하세요.", - "restoreLocation": "AppFlowy 기본 경로로 복원", + "restoreLocation": "@:appName 기본 경로로 복원", "customizeLocation": "다른 폴더 열기", "restartApp": "변경 사항을 적용하려면 앱을 다시 시작하십시오.", "exportDatabase": "데이터베이스 내보내기", @@ -253,10 +253,10 @@ "defineWhereYourDataIsStored": "데이터가 저장되는 위치 정의", "open": "열려 있는", "openFolder": "기존 폴더 열기", - "openFolderDesc": "기존 AppFlowy 폴더에서 읽고 쓰기", + "openFolderDesc": "기존 @:appName 폴더에서 읽고 쓰기", "folderHintText": "폴더 이름", "location": "새 폴더 만들기", - "locationDesc": "AppFlowy 데이터 폴더의 이름을 선택하세요", + "locationDesc": "@:appName 데이터 폴더의 이름을 선택하세요", "browser": "검색", "create": "만들다", "set": "세트", @@ -267,7 +267,7 @@ "change": "변화", "openLocationTooltips": "다른 데이터 디렉토리 열기", "openCurrentDataFolder": "현재 데이터 디렉토리 열기", - "recoverLocationTooltips": "AppFlowy의 기본 데이터 디렉터리로 재설정", + "recoverLocationTooltips": "@:appName의 기본 데이터 디렉터리로 재설정", "exportFileSuccess": "파일을 성공적으로 내보냈습니다!", "exportFileFail": "파일 내보내기 실패!", "export": "내보내다" @@ -577,7 +577,7 @@ "referencedCalendarPrefix": "관점" }, "errorDialog": { - "title": "AppFlowy 오류", + "title": "@:appName 오류", "howToFixFallback": "불편을 끼쳐드려 죄송합니다! 오류를 설명하는 문제를 GitHub 페이지에 제출하세요.", "github": "GitHub에서 보기" }, diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 5b11fec3c9..5ee2bc91e5 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -56,7 +56,7 @@ "resetWorkspacePrompt": "Zresetowanie przestrzeni roboczej spowoduje usunięcie wszystkich znajdujących się w niej stron i danych. Czy na pewno chcesz zresetować przestrzeń roboczą? Alternatywnie możesz skontaktować się z zespołem pomocy technicznej, aby przywrócić przestrzeń roboczą", "hint": "przestrzeń robocza", "notFoundError": "Przestrzeni nie znaleziono", - "failedToLoad": "Coś poszło nie tak! Wczytywanie przestrzeni roboczej nie powiodło się. Spróbuj wyłączyć wszystkie otwarte instancje AppFlowy i spróbuj ponownie.", + "failedToLoad": "Coś poszło nie tak! Wczytywanie przestrzeni roboczej nie powiodło się. Spróbuj wyłączyć wszystkie otwarte instancje @:appName i spróbuj ponownie.", "errorActions": { "reportIssue": "Zgłoś problem", "reachOut": "Skontaktuj się na Discord" @@ -310,7 +310,7 @@ }, "themeUpload": { "button": "Prześlij", - "description": "Prześlij własny motyw AppFlowy za pomocą przycisku poniżej.", + "description": "Prześlij własny motyw @:appName za pomocą przycisku poniżej.", "loading": "Poczekaj, aż zweryfikujemy i prześlemy Twój motyw...", "uploadSuccess": "Twój motyw został przesłany pomyślnie", "deletionFailure": "Nie udało się usunąć motywu. Spróbuj usunąć go ręcznie.", @@ -341,7 +341,7 @@ "defaultLocation": "Ścieżka katalogu z plikami", "exportData": "Eksportuj swoje dane", "doubleTapToCopy": "Kliknij dwukrotnie, aby skopiować ścieżkę", - "restoreLocation": "Przywróć domyślną ścieżkę AppFlowy", + "restoreLocation": "Przywróć domyślną ścieżkę @:appName", "customizeLocation": "Otwórz inny folder", "restartApp": "Uruchom ponownie aplikację, aby zmiany zaczęły obowiązywać.", "exportDatabase": "Eksportuj bazę danych", @@ -353,10 +353,10 @@ "defineWhereYourDataIsStored": "Zdefiniuj miejsce przechowywania Twoich danych", "open": "Otwórz", "openFolder": "Otwórz istniejący folder", - "openFolderDesc": "Przeczytaj i zapisz go w istniejącym folderze AppFlowy", + "openFolderDesc": "Przeczytaj i zapisz go w istniejącym folderze @:appName", "folderHintText": "Nazwa folderu", "location": "Tworzenie nowego folderu", - "locationDesc": "Wybierz nazwę folderu danych AppFlowy", + "locationDesc": "Wybierz nazwę folderu danych @:appName", "browser": "Przeglądaj", "create": "Stwórz", "set": "Ustaw", @@ -367,7 +367,7 @@ "change": "Zmień", "openLocationTooltips": "Otwórz inny katalog danych", "openCurrentDataFolder": "Otwórz bieżący katalog danych", - "recoverLocationTooltips": "Zresetuj do domyślnego katalogu danych AppFlowy", + "recoverLocationTooltips": "Zresetuj do domyślnego katalogu danych @:appName", "exportFileSuccess": "Eksportowanie pliku zakończono pomyślnie!", "exportFileFail": "Eksport pliku nie powiódł się!", "export": "Eksport" @@ -821,7 +821,7 @@ "referencedCalendarPrefix": "Widok" }, "errorDialog": { - "title": "Błąd AppFlowy", + "title": "Błąd @:appName", "howToFixFallback": "Przepraszamy za niedogodności! Zgłoś problem na naszej stronie GitHub, który opisuje Twój błąd.", "github": "Zobacz na GitHubie" }, diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 1341f735b7..de9dca7320 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -58,7 +58,7 @@ "resetWorkspacePrompt": "A redefinição do espaço de trabalho excluirá todas as páginas e dados contidos nele. Tem certeza de que deseja redefinir o espaço de trabalho? Alternativamente, você pode entrar em contato com a equipe de suporte para restaurar o espaço de trabalho", "hint": "Espaço de trabalho", "notFoundError": "Espaço de trabalho não encontrado", - "failedToLoad": "Algo deu errado! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do AppFlowy e tente novamente.", + "failedToLoad": "Algo deu errado! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do @:appName e tente novamente.", "errorActions": { "reportIssue": "Reporte um problema", "reachOut": "Entre em contato no Discord" @@ -280,7 +280,7 @@ "cloudSupabaseUrl": "URL da Supabase", "cloudSupabaseAnonKey": "Chave anônima Supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "A chave anon não pode estar vazia se o URL da supabase não estiver vazio", - "cloudAppFlowy": "AppFlowy Cloud", + "cloudAppFlowy": "@:appName Cloud Beta", "clickToCopy": "Clique para copiar", "selfHostStart": "Se você não possui um servidor, consulte o", "selfHostContent": "documento", @@ -299,11 +299,11 @@ "historicalUserList": "Histórico de login do usuário", "historicalUserListTooltip": "Esta lista exibe suas contas anônimas. Você pode clicar em uma conta para ver seus detalhes. Contas anônimas são criadas clicando no botão ‘Começar’", "openHistoricalUser": "Clique para abrir a conta anônima", - "customPathPrompt": "Armazenar a pasta de dados do AppFlowy em uma pasta sincronizada na nuvem, como o Google Drive, pode representar riscos. Se o banco de dados nesta pasta for acessado ou modificado de vários locais ao mesmo tempo, isso poderá resultar em conflitos de sincronização e possível corrupção de dados", - "importAppFlowyData": "Importar dados da pasta AppFlowy externa", - "importAppFlowyDataDescription": "Copie dados de uma pasta de dados externa do AppFlowy e importe-os para a pasta de dados atual do AppFlowy", - "importSuccess": "Importou com sucesso a pasta de dados do AppFlowy", - "importFailed": "Falha ao importar a pasta de dados do AppFlowy", + "customPathPrompt": "Armazenar a pasta de dados do @:appName em uma pasta sincronizada na nuvem, como o Google Drive, pode representar riscos. Se o banco de dados nesta pasta for acessado ou modificado de vários locais ao mesmo tempo, isso poderá resultar em conflitos de sincronização e possível corrupção de dados", + "importAppFlowyData": "Importar dados da pasta @:appName externa", + "importAppFlowyDataDescription": "Copie dados de uma pasta de dados externa do @:appName e importe-os para a pasta de dados atual do @:appName", + "importSuccess": "Importou com sucesso a pasta de dados do @:appName", + "importFailed": "Falha ao importar a pasta de dados do @:appName", "importGuide": "Para mais detalhes, consulte o documento referenciado", "supabaseSetting": "Configuração de Supabase" }, @@ -354,7 +354,7 @@ "themeUpload": { "button": "Carregar", "uploadTheme": "Carregar tema", - "description": "Carregue seu próprio tema AppFlowy usando o botão abaixo.", + "description": "Carregue seu próprio tema @:appName usando o botão abaixo.", "loading": "Aguarde enquanto validamos e carregamos seu tema...", "uploadSuccess": "Seu tema foi carregado com sucesso", "deletionFailure": "Falha ao excluir o tema. Tente excluí-lo manualmente.", @@ -385,7 +385,7 @@ "defaultLocation": "Onde os seus dados ficam armazenados", "exportData": "Exporte seus dados", "doubleTapToCopy": "Clique duas vezes para copiar o caminho", - "restoreLocation": "Restaurar para o caminho padrão do AppFlowy", + "restoreLocation": "Restaurar para o caminho padrão do @:appName", "customizeLocation": "Abrir outra pasta", "restartApp": "Reinicie o aplicativo para que as alterações entrem em vigor.", "exportDatabase": "Exportar banco de dados", @@ -397,10 +397,10 @@ "defineWhereYourDataIsStored": "Defina onde seus dados são armazenados", "open": "Abrir", "openFolder": "Abra uma pasta existente", - "openFolderDesc": "Gravar na pasta AppFlowy existente ...", + "openFolderDesc": "Gravar na pasta @:appName existente ...", "folderHintText": "nome da pasta", "location": "Criando nova pasta", - "locationDesc": "Escolha um nome para sua pasta de dados do AppFlowy", + "locationDesc": "Escolha um nome para sua pasta de dados do @:appName", "browser": "Navegar", "create": "Criar", "set": "Definir", @@ -411,7 +411,7 @@ "change": "Mudar", "openLocationTooltips": "Abra outro diretório de dados", "openCurrentDataFolder": "Abra o diretório de dados atual", - "recoverLocationTooltips": "Redefinir para o diretório de dados padrão do AppFlowy", + "recoverLocationTooltips": "Redefinir para o diretório de dados padrão do @:appName", "exportFileSuccess": "Exportar arquivo com sucesso!", "exportFileFail": "Falha na exportação do arquivo!", "export": "Exportar" @@ -945,7 +945,7 @@ "quickJumpYear": "Ir para" }, "errorDialog": { - "title": "Erro do AppFlowy", + "title": "Erro do @:appName", "howToFixFallback": "Lamentamos o inconveniente! Envie um problema em nossa página do GitHub que descreva seu erro.", "github": "Ver no GitHub" }, diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index 8a9745650a..699be162e8 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -56,7 +56,7 @@ "resetWorkspacePrompt": "Reinciar do espaço de trabalho excluirá todas as páginas e dados contidos. Tem certeza de que deseja reiniciar o espaço de trabalho? Alternativamente, podes entrar em contato com a equipe de suporte para restaurar o espaço de trabalho", "hint": "ambiente de trabalho", "notFoundError": "Ambiente de trabalho não encontrada", - "failedToLoad": "Algo correu mal! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do AppFlowy e tente novamente.", + "failedToLoad": "Algo correu mal! Falha ao carregar o espaço de trabalho. Tente fechar qualquer instância aberta do @:appName e tente novamente.", "errorActions": { "reportIssue": "Relatar problema", "reachOut": "Entre em contacto no Discord" @@ -293,7 +293,7 @@ "themeUpload": { "button": "Carregar", "uploadTheme": "Carregar tema", - "description": "Carregue seu próprio tema AppFlowy usando o botão abaixo.", + "description": "Carregue seu próprio tema @:appName usando o botão abaixo.", "loading": "Aguarde enquanto validamos e carregamos seu tema...", "uploadSuccess": "Seu tema foi carregado com sucesso", "deletionFailure": "Falha ao excluir o tema. Tente excluí-lo manualmente.", @@ -326,7 +326,7 @@ "defaultLocation": "Leia arquivos e local de armazenamento de dados", "exportData": "Exporte seus dados", "doubleTapToCopy": "Toque duas vezes para copiar o caminho", - "restoreLocation": "Restaurar para o caminho padrão do AppFlowy", + "restoreLocation": "Restaurar para o caminho padrão do @:appName", "customizeLocation": "Abra outra pasta", "restartApp": "Reinicie o aplicativo para que as alterações entrem em vigor.", "exportDatabase": "Exportar banco de dados", @@ -338,10 +338,10 @@ "defineWhereYourDataIsStored": "Defina onde seus dados são armazenados", "open": "Abrir", "openFolder": "Abra uma pasta existente", - "openFolderDesc": "Leia e grave-o em sua pasta AppFlowy existente", + "openFolderDesc": "Leia e grave-o em sua pasta @:appName existente", "folderHintText": "nome da pasta", "location": "Criando uma nova pasta", - "locationDesc": "Escolha um nome para sua pasta de dados do AppFlowy", + "locationDesc": "Escolha um nome para sua pasta de dados do @:appName", "browser": "Navegar", "create": "Criar", "set": "Definir", @@ -352,7 +352,7 @@ "change": "Mudar", "openLocationTooltips": "Abra outro diretório de dados", "openCurrentDataFolder": "Abra o diretório de dados atual", - "recoverLocationTooltips": "Redefinir para o diretório de dados padrão do AppFlowy", + "recoverLocationTooltips": "Redefinir para o diretório de dados padrão do @:appName", "exportFileSuccess": "Exportar arquivo com sucesso!", "exportFileFail": "Falha na exportação do arquivo!", "export": "Exportar" @@ -754,7 +754,7 @@ "referencedCalendarPrefix": "Vista de" }, "errorDialog": { - "title": "Erro do AppFlowy", + "title": "Erro do @:appName", "howToFixFallback": "Lamentamos o inconveniente! Envie um problema em nossa página do GitHub que descreva seu erro.", "github": "Ver no GitHub" }, diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index c138c87bd1..8034301d0e 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -58,7 +58,7 @@ "resetWorkspacePrompt": "Сброс рабочего пространства приведет к удалению всех страниц и данных внутри него. Вы уверены, что хотите сбросить рабочее пространство? В качестве альтернативы вы можете обратиться в службу поддержки для восстановления рабочего пространства", "hint": "рабочее пространство", "notFoundError": "Рабочее пространство не найдено", - "failedToLoad": "Что-то пошло не так! Не удалось загрузить рабочее пространство. Попробуйте закрыть все открытые экземпляры AppFlowy и повторите попытку.", + "failedToLoad": "Что-то пошло не так! Не удалось загрузить рабочее пространство. Попробуйте закрыть все открытые экземпляры @:appName и повторите попытку.", "errorActions": { "reportIssue": "Сообщить о проблеме", "reportIssueOnGithub": "Сообщить о проблеме на Github", @@ -292,8 +292,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "URL-адрес Supabase не может быть пустым.", "cloudSupabaseAnonKey": "Анонимный ключ Supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "Анонимный ключ не может быть пустым, если URL Supabase не пуст", - "cloudAppFlowy": "AppFlowy Cloud", - "cloudAppFlowySelfHost": "AppFlowy Cloud на своём сервере", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud на своём сервере", "appFlowyCloudUrlCanNotBeEmpty": "URL облака не может быть пустым.", "clickToCopy": "Нажмите, чтобы скопировать", "selfHostStart": "Если у вас нет сервера, пожалуйста, обратитесь к", @@ -314,12 +314,12 @@ "historicalUserList": "История входа пользователя", "historicalUserListTooltip": "В этом списке отображаются ваши анонимные аккаунты. Вы можете нажать на аккаунт, чтобы посмотреть данные. Анонимный аккаунт создаётся нажатием кнопки «Начать».", "openHistoricalUser": "Нажмите, чтобы открыть анонимный аккаунт", - "customPathPrompt": "Хранение папки данных AppFlowy в папке с облачной синхронизацией, например на Google Диске, может представлять риск. Если база данных в этой папке будет доступна или изменена с нескольких мест одновременно, это может привести к конфликтам синхронизации и потенциальному повреждению данных", - "importAppFlowyData": "Импортировать данные из внешней папки AppFlowy", + "customPathPrompt": "Хранение папки данных @:appName в папке с облачной синхронизацией, например на Google Диске, может представлять риск. Если база данных в этой папке будет доступна или изменена с нескольких мест одновременно, это может привести к конфликтам синхронизации и потенциальному повреждению данных", + "importAppFlowyData": "Импортировать данные из внешней папки @:appName", "importingAppFlowyDataTip": "Выполняется импорт данных. Пожалуйста, не закрывайте приложение", - "importAppFlowyDataDescription": "Скопируйте данные из внешней папки данных AppFlowy и импортируйте их в текущую папку данных AppFlowy.", - "importSuccess": "Папка данных AppFlowy успешно импортирована", - "importFailed": "Не удалось импортировать папку данных AppFlowy", + "importAppFlowyDataDescription": "Скопируйте данные из внешней папки данных @:appName и импортируйте их в текущую папку данных @:appName.", + "importSuccess": "Папка данных @:appName успешно импортирована", + "importFailed": "Не удалось импортировать папку данных @:appName", "importGuide": "Для получения более подробной информации, пожалуйста, проверьте указанный документ.", "supabaseSetting": "Настройка Supabase" }, @@ -371,7 +371,7 @@ "themeUpload": { "button": "Загрузить", "uploadTheme": "Загрузить тему", - "description": "Загрузите собственную тему AppFlowy, используя кнопку ниже.", + "description": "Загрузите собственную тему @:appName, используя кнопку ниже.", "loading": "Подождите, пока мы проверим и загрузим вашу тему...", "uploadSuccess": "Ваша тема была успешно загружена", "deletionFailure": "Не удалось удалить тему. Попробуйте удалить её вручную.", @@ -403,7 +403,7 @@ "defaultLocation": "Путь до хранилища", "exportData": "Экспорт данных", "doubleTapToCopy": "Нажмите дважды, чтобы скопировать путь", - "restoreLocation": "Восстановить путь AppFlowy по умолчанию", + "restoreLocation": "Восстановить путь @:appName по умолчанию", "customizeLocation": "Выбрать другую папку", "restartApp": "Пожалуйста, перезапустите приложение, чтобы изменения вступили в силу.", "exportDatabase": "Экспорт базы данных", @@ -415,10 +415,10 @@ "defineWhereYourDataIsStored": "Указать хранилище данных", "open": "Открыть", "openFolder": "Открыть существующую папку", - "openFolderDesc": "Чтение и запись в существующую папку AppFlowy ...", + "openFolderDesc": "Чтение и запись в существующую папку @:appName ...", "folderHintText": "имя папки", "location": "Создание новой папки", - "locationDesc": "Выбрать имя папки данных AppFlowy", + "locationDesc": "Выбрать имя папки данных @:appName", "browser": "Обзор", "create": "Создать", "set": "Установить", diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index 1d125b1402..2231ec75fb 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -272,12 +272,12 @@ "historicalUserList": "Användarinloggningshistorik", "historicalUserListTooltip": "Den här listan visar dina anonyma konton. Du kan klicka på ett konto för att se dess detaljer. Anonyma konton skapas genom att klicka på knappen \"Kom igång\".", "openHistoricalUser": "Klicka för att öppna det anonyma kontot", - "customPathPrompt": "Att lagra AppFlowy-datamappen i en molnsynkroniserad mapp som Google Drive kan innebära risker. Om databasen i den här mappen nås eller ändras från flera platser samtidigt, kan det resultera i synkroniseringskonflikter och potentiell datakorruption", - "importAppFlowyData": "Importera data från extern AppFlowy-mapp", + "customPathPrompt": "Att lagra @:appName-datamappen i en molnsynkroniserad mapp som Google Drive kan innebära risker. Om databasen i den här mappen nås eller ändras från flera platser samtidigt, kan det resultera i synkroniseringskonflikter och potentiell datakorruption", + "importAppFlowyData": "Importera data från extern @:appName-mapp", "importingAppFlowyDataTip": "Dataimport pågår. Stäng inte appen", - "importAppFlowyDataDescription": "Kopiera data från en extern AppFlowy-datamapp och importera den till den aktuella AppFlowy-datamappen", - "importSuccess": "AppFlowy-datamappen har importerats", - "importFailed": "Det gick inte att importera AppFlowy-datamappen", + "importAppFlowyDataDescription": "Kopiera data från en extern @:appName-datamapp och importera den till den aktuella @:appName-datamappen", + "importSuccess": "@:appName-datamappen har importerats", + "importFailed": "Det gick inte att importera @:appName-datamappen", "importGuide": "För ytterligare information, snälla se det refererade dokumentet", "supabaseSetting": "Supabase-inställning" }, @@ -294,7 +294,7 @@ }, "themeUpload": { "button": "Ladda upp", - "description": "Ladda upp ditt eget AppFlowy-tema med knappen nedan.", + "description": "Ladda upp ditt eget @:appName-tema med knappen nedan.", "loading": "Vänta medan vi validerar och laddar upp ditt tema...", "uploadSuccess": "Ditt tema laddades upp", "deletionFailure": "Det gick inte att ta bort temat. Försök att radera det manuellt.", @@ -311,7 +311,7 @@ "defaultLocation": "Läs filer och datalagringsplats", "exportData": "Exportera dina data", "doubleTapToCopy": "Dubbeltryck för att kopiera sökvägen", - "restoreLocation": "Återställ till AppFlowy standardsökväg", + "restoreLocation": "Återställ till @:appName standardsökväg", "customizeLocation": "Öppna en annan mapp", "restartApp": "Starta om appen för att ändringarna ska träda i kraft.", "exportDatabase": "Exportera databas", @@ -323,10 +323,10 @@ "defineWhereYourDataIsStored": "Definiera var din data lagras", "open": "Öppen", "openFolder": "Öppna en befintlig mapp", - "openFolderDesc": "Läs och skriv det till din befintliga AppFlowy-mapp", + "openFolderDesc": "Läs och skriv det till din befintliga @:appName-mapp", "folderHintText": "mappnamn", "location": "Skapar en ny mapp", - "locationDesc": "Välj ett namn för din AppFlowy-datamapp", + "locationDesc": "Välj ett namn för din @:appName-datamapp", "browser": "Bläddra", "create": "Skapa", "set": "Uppsättning", @@ -337,7 +337,7 @@ "change": "Förändra", "openLocationTooltips": "Öppna en annan datakatalog", "openCurrentDataFolder": "Öppna aktuell datakatalog", - "recoverLocationTooltips": "Återställ till AppFlowys standarddatakatalog", + "recoverLocationTooltips": "Återställ till @:appNames standarddatakatalog", "exportFileSuccess": "Exporterade filen framgångsrikt!", "exportFileFail": "Export av fil misslyckades!", "export": "Exportera" @@ -647,7 +647,7 @@ "referencedCalendarPrefix": "Utsikt över" }, "errorDialog": { - "title": "AppFlowy-fel", + "title": "@:appName-fel", "howToFixFallback": "Vi ber om ursäkt för besväret! Skapa en felrapport på vår GitHub-sida som beskriver ditt fel.", "github": "Visa på GitHub" }, diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 8f82fedb62..8648129b27 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -63,7 +63,7 @@ "resetWorkspacePrompt": "Çalışma alanını sıfırlamak, içindeki tüm sayfaları ve verileri silecektir. Çalışma alanını sıfırlamak istediğinizden emin misiniz? Alternatif olarak, çalışma alanını geri yüklemek için destek ekibiyle iletişime geçebilirsiniz", "hint": "çalışma alanı", "notFoundError": "Çalışma alanı bulunamadı", - "failedToLoad": "Bir şeyler yanlış gitti! Çalışma alanı yüklenemedi. Açık olan tüm AppFlowy örneklerini kapatıp tekrar deneyin.", + "failedToLoad": "Bir şeyler yanlış gitti! Çalışma alanı yüklenemedi. Açık olan tüm @:appName örneklerini kapatıp tekrar deneyin.", "errorActions": { "reportIssue": "Sorun bildir", "reportIssueOnGithub": "GitHub'da sorun bildir", @@ -330,8 +330,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "Supabase url'si boş olamaz", "cloudSupabaseAnonKey": "Supabase anonim anahtarı", "cloudSupabaseAnonKeyCanNotBeEmpty": "Anonim anahtar boş olamaz", - "cloudAppFlowy": "AppFlowy Bulutu Beta", - "cloudAppFlowySelfHost": "AppFlowy Bulutu Kendi Sunucunuzda", + "cloudAppFlowy": "@:appName Bulutu Beta", + "cloudAppFlowySelfHost": "@:appName Bulutu Kendi Sunucunuzda", "appFlowyCloudUrlCanNotBeEmpty": "Bulut url'si boş olamaz", "clickToCopy": "Kopyalamak için tıklayın", "selfHostStart": "Bir sunucunuz yoksa, lütfen", @@ -352,12 +352,12 @@ "historicalUserList": "Kullanıcı giriş geçmişi", "historicalUserListTooltip": "Bu liste anonim hesaplarınızı görüntüler. Ayrıntılarını görüntülemek için bir hesaba tıklayabilirsiniz. Anonim hesaplar 'Başlayın' düğmesine tıklanarak oluşturulur", "openHistoricalUser": "Anonim hesabı açmak için tıklayın", - "customPathPrompt": "AppFlowy veri klasörünü Google Drive gibi bulutla senkronize edilmiş bir klasörde saklamak risk oluşturabilir. Bu klasördeki veritabanına aynı anda birden fazla konumdan erişilir veya değiştirilirse, senkronizasyon çakışmaları ve olası veri bozulmasıyla sonuçlanabilir", - "importAppFlowyData": "Harici AppFlowy Klasöründen Veri Al", + "customPathPrompt": "@:appName veri klasörünü Google Drive gibi bulutla senkronize edilmiş bir klasörde saklamak risk oluşturabilir. Bu klasördeki veritabanına aynı anda birden fazla konumdan erişilir veya değiştirilirse, senkronizasyon çakışmaları ve olası veri bozulmasıyla sonuçlanabilir", + "importAppFlowyData": "Harici @:appName Klasöründen Veri Al", "importingAppFlowyDataTip": "Veri aktarımı devam ediyor. Lütfen uygulamayı kapatmayın", - "importAppFlowyDataDescription": "Harici bir AppFlowy veri klasöründen veri kopyalayın ve geçerli AppFlowy veri klasörüne aktarın", - "importSuccess": "AppFlowy veri klasörü başarıyla alındı", - "importFailed": "AppFlowy veri klasörü alınamadı", + "importAppFlowyDataDescription": "Harici bir @:appName veri klasöründen veri kopyalayın ve geçerli @:appName veri klasörüne aktarın", + "importSuccess": "@:appName veri klasörü başarıyla alındı", + "importFailed": "@:appName veri klasörü alınamadı", "importGuide": "Daha fazla bilgi için lütfen referans belgeyi kontrol edin" }, "notifications": { @@ -408,7 +408,7 @@ "themeUpload": { "button": "Yükle", "uploadTheme": "Temayı yükle", - "description": "Aşağıdaki düğmeyi kullanarak kendi AppFlowy temanızı yükleyin.", + "description": "Aşağıdaki düğmeyi kullanarak kendi @:appName temanızı yükleyin.", "loading": "Lütfen temanızı doğrularken ve yüklerken bekleyin...", "uploadSuccess": "Temanız başarıyla yüklendi", "deletionFailure": "Temayı silinemedi. Manuel olarak silmeyi deneyin.", @@ -467,7 +467,7 @@ "defaultLocation": "Dosyaları ve verileri okuma konumu", "exportData": "Verilerinizi dışa aktarın", "doubleTapToCopy": "Yolu kopyalamak için iki kez dokunun", - "restoreLocation": "AppFlowy varsayılan yoluna geri yükle", + "restoreLocation": "@:appName varsayılan yoluna geri yükle", "customizeLocation": "Başka bir klasör aç", "restartApp": "Değişikliklerin etkili olması için lütfen uygulamayı yeniden başlatın.", "exportDatabase": "Veritabanını dışa aktar", @@ -479,10 +479,10 @@ "defineWhereYourDataIsStored": "Verilerinizin nerede saklandığını tanımlayın", "open": "Aç", "openFolder": "Mevcut bir klasörü aç", - "openFolderDesc": "Mevcut AppFlowy klasörünüze okuyun ve yazın", + "openFolderDesc": "Mevcut @:appName klasörünüze okuyun ve yazın", "folderHintText": "klasör adı", "location": "Yeni bir klasör oluşturuluyor", - "locationDesc": "AppFlowy veri klasörünüz için bir ad seçin", + "locationDesc": "@:appName veri klasörünüz için bir ad seçin", "browser": "Gözat", "create": "Oluştur", "set": "Ayarla", @@ -493,7 +493,7 @@ "change": "Değiştir", "openLocationTooltips": "Başka bir veri dizini aç", "openCurrentDataFolder": "Geçerli veri dizinini aç", - "recoverLocationTooltips": "AppFlowy'nin varsayılan veri dizinine sıfırla", + "recoverLocationTooltips": "@:appName'nin varsayılan veri dizinine sıfırla", "exportFileSuccess": "Dosya başarıyla dışa aktarıldı!", "exportFileFail": "Dosya dışa aktarılamadı!", "export": "Dışa Aktar", @@ -1116,7 +1116,7 @@ "duplicateEvent": "Etkinliği kopyala" }, "errorDialog": { - "title": "AppFlowy Hatası", + "title": "@:appName Hatası", "howToFixFallback": "Verdiğimiz rahatsızlıktan dolayı özür dileriz! Lütfen GitHub sayfamızda hatanızı açıklayan bir sorun bildirin.", "github": "GitHub'da Görüntüle" }, diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 09b73db901..8be445a75d 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -60,7 +60,7 @@ "resetWorkspacePrompt": "Đặt lại không gian làm việc sẽ xóa tất cả các trang và dữ liệu trong đó. Bạn có chắc chắn muốn đặt lại không gian làm việc? Ngoài ra, bạn có thể liên hệ với nhóm hỗ trợ để khôi phục không gian làm việc", "hint": "không gian làm việc", "notFoundError": "Không tìm thấy không gian làm việc", - "failedToLoad": "Đã xảy ra lỗi! Không tải được không gian làm việc. Hãy thử đóng mọi phiên bản đang mở của AppFlowy và thử lại.", + "failedToLoad": "Đã xảy ra lỗi! Không tải được không gian làm việc. Hãy thử đóng mọi phiên bản đang mở của @:appName và thử lại.", "errorActions": { "reportIssue": "Báo cáo một vấn đề", "reportIssueOnGithub": "Báo cáo sự cố trên Github", @@ -314,7 +314,7 @@ "cloudSupabaseUrl": "Supabase URL", "cloudSupabaseAnonKey": "Supabase anon key", "cloudSupabaseAnonKeyCanNotBeEmpty": "Anon key không được để trống nếu url supabase không trống", - "cloudAppFlowy": "AppFlowy Cloud Beta", + "cloudAppFlowy": "@:appName Cloud Beta", "clickToCopy": "Bấm để sao chép", "selfHostStart": "Nếu bạn không có máy chủ, vui lòng tham khảo", "selfHostContent": "tài liệu", @@ -332,11 +332,11 @@ "inputTextFieldHint": "Bí mật của bạn", "historicalUserList": "Lịch sử đăng nhập", "openHistoricalUser": "Ấn để mở tài khoản ẩn danh", - "importAppFlowyData": "Nhập dữ liệu từ thư mục AppFlowy bên ngoài", + "importAppFlowyData": "Nhập dữ liệu từ thư mục @:appName bên ngoài", "importingAppFlowyDataTip": "Quá trình nhập dữ liệu đang diễn ra. Vui lòng không đóng ứng dụng", - "importAppFlowyDataDescription": "Sao chép dữ liệu từ thư mục dữ liệu AppFlowy bên ngoài và nhập dữ liệu đó vào thư mục dữ liệu AppFlowy hiện tại", - "importSuccess": "Đã nhập thành công thư mục dữ liệu AppFlowy", - "importFailed": "Nhập thư mục dữ liệu AppFlowy không thành công", + "importAppFlowyDataDescription": "Sao chép dữ liệu từ thư mục dữ liệu @:appName bên ngoài và nhập dữ liệu đó vào thư mục dữ liệu @:appName hiện tại", + "importSuccess": "Đã nhập thành công thư mục dữ liệu @:appName", + "importFailed": "Nhập thư mục dữ liệu @:appName không thành công", "importGuide": "Để biết thêm chi tiết, vui lòng kiểm tra tài liệu được tham chiếu" }, "notifications": { @@ -430,7 +430,7 @@ "defaultLocation": "Đọc tập tin và vị trí lưu trữ dữ liệu", "exportData": "Xuất dữ liệu của bạn", "doubleTapToCopy": "Nhấn đúp để sao chép đường dẫn", - "restoreLocation": "Khôi phục về đường dẫn mặc định của AppFlowy", + "restoreLocation": "Khôi phục về đường dẫn mặc định của @:appName", "customizeLocation": "Mở thư mục khác", "restartApp": "Vui lòng khởi động lại ứng dụng để những thay đổi có hiệu lực.", "exportDatabase": "Xuất cơ sở dữ liệu", @@ -442,10 +442,10 @@ "defineWhereYourDataIsStored": "Xác định nơi dữ liệu của bạn được lưu trữ", "open": "Mở", "openFolder": "Mở một thư mục hiện có", - "openFolderDesc": "Đọc và ghi nó vào thư mục AppFlowy hiện có của bạn", + "openFolderDesc": "Đọc và ghi nó vào thư mục @:appName hiện có của bạn", "folderHintText": "tên thư mục", "location": "Tạo một thư mục mới", - "locationDesc": "Chọn tên cho thư mục dữ liệu AppFlowy của bạn", + "locationDesc": "Chọn tên cho thư mục dữ liệu @:appName của bạn", "browser": "Duyệt", "create": "Tạo", "folderPath": "Đường dẫn lưu trữ thư mục của bạn", @@ -455,7 +455,7 @@ "change": "Thay đổi", "openLocationTooltips": "Mở thư mục dữ liệu khác", "openCurrentDataFolder": "Mở thư mục dữ liệu hiện tại", - "recoverLocationTooltips": "Đặt lại về thư mục dữ liệu mặc định của AppFlowy", + "recoverLocationTooltips": "Đặt lại về thư mục dữ liệu mặc định của @:appName", "exportFileSuccess": "Xuất tập tin thành công!", "exportFileFail": "Xuất tập tin thất bại!", "export": "Xuất" @@ -765,7 +765,7 @@ } }, "errorDialog": { - "title": "Lỗi của AppFlowy", + "title": "Lỗi của @:appName", "howToFixFallback": "Chúng tôi xin lỗi vì sự cố này! Vui lòng mở sự cố trên GitHub để báo lỗi.", "github": "Xem trên GitHub" }, diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index 8d342913c2..ed9cf5e0bd 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -64,7 +64,7 @@ "resetWorkspacePrompt": "重置工作区将删除其中的所有页面和数据。您确定要重置工作区吗?您也可以联系技术支持团队来恢复工作区", "hint": "工作区", "notFoundError": "找不到工作区", - "failedToLoad": "出了些问题!我们无法加载工作区。请尝试关闭所有打开的 AppFlowy 实例,然后重试。", + "failedToLoad": "出了些问题!我们无法加载工作区。请尝试关闭所有打开的 @:appName 实例,然后重试。", "errorActions": { "reportIssue": "上报问题", "reportIssueOnGithub": "在 Github 上报告问题", @@ -333,8 +333,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "supabase url 不能为空", "cloudSupabaseAnonKey": "Supabase Anon key", "cloudSupabaseAnonKeyCanNotBeEmpty": "如果 Supabase url 不为空,则 Anon key 不能为空", - "cloudAppFlowy": "AppFlowy Cloud", - "cloudAppFlowySelfHost": "AppFlowy Cloud 自托管", + "cloudAppFlowy": "@:appName Cloud Beta", + "cloudAppFlowySelfHost": "@:appName Cloud 自托管", "appFlowyCloudUrlCanNotBeEmpty": "云地址不能为空", "clickToCopy": "点击复制", "selfHostStart": "如果您没有服务器,请参阅", @@ -355,12 +355,12 @@ "historicalUserList": "用户登录历史记录", "historicalUserListTooltip": "此列表显示您的匿名帐户。您可以单击某个帐户来查看其详细信息。单击“开始”按钮即可创建匿名帐户", "openHistoricalUser": "点击开设匿名账户", - "customPathPrompt": "将 AppFlowy 数据文件夹存储在云同步文件夹(例如 Google Drive)中可能会带来风险。如果同时从多个位置访问或修改此文件夹中的数据库,可能会导致同步冲突和潜在的数据损坏", - "importAppFlowyData": "从外部 AppFlowy 文件夹导入数据", + "customPathPrompt": "将 @:appName 数据文件夹存储在云同步文件夹(例如 Google Drive)中可能会带来风险。如果同时从多个位置访问或修改此文件夹中的数据库,可能会导致同步冲突和潜在的数据损坏", + "importAppFlowyData": "从外部 @:appName 文件夹导入数据", "importingAppFlowyDataTip": "数据导入正在进行中。请不要关闭应用程序", - "importAppFlowyDataDescription": "从外部 AppFlowy 数据文件夹复制数据并将其导入到当前 AppFlowy 数据文件夹中", - "importSuccess": "成功导入AppFlowy数据文件夹", - "importFailed": "导入 AppFlowy 数据文件夹失败", + "importAppFlowyDataDescription": "从外部 @:appName 数据文件夹复制数据并将其导入到当前 @:appName 数据文件夹中", + "importSuccess": "成功导入@:appName数据文件夹", + "importFailed": "导入 @:appName 数据文件夹失败", "importGuide": "有关详细信息,请参阅参考文档", "supabaseSetting": "Supabase 设置", "cloudSetting": "云设置" @@ -413,7 +413,7 @@ "themeUpload": { "button": "上传", "uploadTheme": "上传主题", - "description": "使用下面的按钮上传您自己的 AppFlowy 主题。", + "description": "使用下面的按钮上传您自己的 @:appName 主题。", "loading": "我们正在验证并上传您的主题,请稍候...", "uploadSuccess": "您的主题已上传成功", "deletionFailure": "删除主题失败,请尝试手动删除。", @@ -463,7 +463,7 @@ "defaultLocation": "读取文件和数据存储位置", "exportData": "导出您的数据", "doubleTapToCopy": "双击复制路径", - "restoreLocation": "恢复为 AppFlowy 默认路径", + "restoreLocation": "恢复为 @:appName 默认路径", "customizeLocation": "打开另一个文件夹", "restartApp": "请重启 App 使设置生效", "exportDatabase": "导出数据库", @@ -475,10 +475,10 @@ "defineWhereYourDataIsStored": "定义数据存储位置", "open": "打开", "openFolder": "打开现有文件夹", - "openFolderDesc": "读取并将其写入您现有的 AppFlowy 文件夹", + "openFolderDesc": "读取并将其写入您现有的 @:appName 文件夹", "folderHintText": "文件夹名", "location": "正在新建文件夹", - "locationDesc": "为您的 AppFlowy 数据文件夹选择一个名称", + "locationDesc": "为您的 @:appName 数据文件夹选择一个名称", "browser": "浏览", "create": "新建", "set": "设置", @@ -489,7 +489,7 @@ "change": "更改", "openLocationTooltips": "打开另一个数据目录", "openCurrentDataFolder": "打开当前数据目录", - "recoverLocationTooltips": "恢复为 AppFlowy 默认数据目录", + "recoverLocationTooltips": "恢复为 @:appName 默认数据目录", "exportFileSuccess": "导出成功!", "exportFileFail": "导出失败!", "export": "导出", @@ -1093,7 +1093,7 @@ "quickJumpYear": "跳转到" }, "errorDialog": { - "title": "AppFlowy 错误", + "title": "@:appName 错误", "howToFixFallback": "对于给您带来的不便, 我们深表歉意! 请在我们的 GitHub 页面上提交 issue 并描述您遇到的错误。", "github": "在 GitHub 查看" }, diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index 3e623c2b0e..c6f0520f05 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -75,7 +75,7 @@ "resetWorkspacePrompt": "重設工作區將刪除其中所有頁面和資料。你確定要重設工作區嗎?或者,你可以聯絡支援團隊來恢復工作區。", "hint": "工作區", "notFoundError": "找不到工作區", - "failedToLoad": "出了些問題!無法載入工作區。請嘗試關閉 AppFlowy 的任何開啟執行個體,然後再試一次。", + "failedToLoad": "出了些問題!無法載入工作區。請嘗試關閉 @:appName 的任何開啟執行個體,然後再試一次。", "errorActions": { "reportIssue": "回報問題", "reportIssueOnGithub": "在 Github 提交 issue", @@ -368,8 +368,8 @@ "cloudSupabaseUrlCanNotBeEmpty": "Supabase 網址不能為空", "cloudSupabaseAnonKey": "Supabase 匿名金鑰", "cloudSupabaseAnonKeyCanNotBeEmpty": "如果 Supabase 網址不為空,則匿名金鑰不得為空", - "cloudAppFlowy": "AppFlowy 雲端測試版 (Beta)", - "cloudAppFlowySelfHost": "自架 AppFlowy 雲端伺服器", + "cloudAppFlowy": "@:appName 雲端測試版 (Beta)", + "cloudAppFlowySelfHost": "自架 @:appName 雲端伺服器", "appFlowyCloudUrlCanNotBeEmpty": "雲端網址不能為空", "clickToCopy": "點選以複製", "selfHostStart": "若您尚未設定伺服器,請參閱", @@ -390,12 +390,12 @@ "historicalUserList": "使用者登入歷史", "historicalUserListTooltip": "此列表顯示您的匿名帳號。您可以點選帳號以檢視其詳細資訊。透過點選「開始使用」按鈕來建立匿名帳號", "openHistoricalUser": "點選以開啟匿名帳號", - "customPathPrompt": "將 AppFlowy 資料資料夾儲存在如 Google 雲端硬碟等雲端同步資料夾中可能會帶來風險。如果該資料庫在多處同時被存取或修改,可能會導致同步衝突和潛在的資料損壞", - "importAppFlowyData": "從外部 AppFlowy 資料夾匯入資料", + "customPathPrompt": "將 @:appName 資料資料夾儲存在如 Google 雲端硬碟等雲端同步資料夾中可能會帶來風險。如果該資料庫在多處同時被存取或修改,可能會導致同步衝突和潛在的資料損壞", + "importAppFlowyData": "從外部 @:appName 資料夾匯入資料", "importingAppFlowyDataTip": "資料正在匯入中。請勿關閉應用程式", - "importAppFlowyDataDescription": "從外部 AppFlowy 資料夾複製資料並匯入到目前的 AppFlowy 資料夾", - "importSuccess": "成功匯入 AppFlowy 資料夾", - "importFailed": "匯入 AppFlowy 資料夾失敗", + "importAppFlowyDataDescription": "從外部 @:appName 資料夾複製資料並匯入到目前的 @:appName 資料夾", + "importSuccess": "成功匯入 @:appName 資料夾", + "importFailed": "匯入 @:appName 資料夾失敗", "importGuide": "欲瞭解更多詳細資訊,請查閱參考文件", "supabaseSetting": "supabase 設定" }, @@ -448,7 +448,7 @@ "themeUpload": { "button": "上傳", "uploadTheme": "上傳主題", - "description": "使用下方的按鈕上傳您自己的 AppFlowy 主題。", + "description": "使用下方的按鈕上傳您自己的 @:appName 主題。", "loading": "我們正在驗證並上傳您的主題,請稍候...", "uploadSuccess": "您的主題已成功上傳", "deletionFailure": "刪除主題失敗。請嘗試手動刪除。", @@ -499,10 +499,10 @@ }, "files": { "copy": "複製", - "defaultLocation": "AppFlowy 資料儲存位置", + "defaultLocation": "@:appName 資料儲存位置", "exportData": "匯出您的資料", "doubleTapToCopy": "點選兩下以複製路徑", - "restoreLocation": "恢復為 AppFlowy 預設路徑", + "restoreLocation": "恢復為 @:appName 預設路徑", "customizeLocation": "開啟其他資料夾", "restartApp": "請重新啟動應用程式以使變更生效。", "exportDatabase": "匯出資料庫", @@ -514,10 +514,10 @@ "defineWhereYourDataIsStored": "定義您的資料儲存位置", "open": "開啟", "openFolder": "開啟一個已經存在的資料夾", - "openFolderDesc": "讀取並寫入到現有的 AppFlowy 資料夾", + "openFolderDesc": "讀取並寫入到現有的 @:appName 資料夾", "folderHintText": "資料夾名稱", "location": "建立新資料夾", - "locationDesc": "為您的 AppFlowy 資料夾選擇一個名稱", + "locationDesc": "為您的 @:appName 資料夾選擇一個名稱", "browser": "瀏覽", "create": "建立", "set": "設定", @@ -528,7 +528,7 @@ "change": "更改", "openLocationTooltips": "開啟另一個資料目錄", "openCurrentDataFolder": "開啟目前資料目錄", - "recoverLocationTooltips": "重設為 AppFlowy 的預設資料目錄", + "recoverLocationTooltips": "重設為 @:appName 的預設資料目錄", "exportFileSuccess": "匯出檔案成功!", "exportFileFail": "匯出檔案失敗!", "export": "匯出", @@ -1105,7 +1105,7 @@ "quickJumpYear": "跳到" }, "errorDialog": { - "title": "AppFlowy 錯誤", + "title": "@:appName 錯誤", "howToFixFallback": "對於給您帶來的不便,我們深表歉意!在我們的 GitHub 頁面上提交描述您的錯誤的問題。", "github": "在 GitHub 上檢視" }, @@ -1469,7 +1469,7 @@ "none": "無", "photoPermissionDescription": "允許存取圖片庫以上傳圖片", "openSettings": "打開設定", - "photoPermissionTitle": "AppFlowy 希望存取您的圖片庫", + "photoPermissionTitle": "@:appName 希望存取您的圖片庫", "doNotAllow": "不允許" }, "commandPalette": { From d303d5affd37ec64de8cc69458af1b32de68c2f6 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 20 May 2024 21:51:40 +0800 Subject: [PATCH 13/30] fix: use full update states instead of patch update (#5371) --- .../document/application/document_bloc.dart | 9 +++++++++ frontend/appflowy_tauri/src-tauri/Cargo.lock | 14 +++++++------- frontend/appflowy_tauri/src-tauri/Cargo.toml | 14 +++++++------- frontend/appflowy_web/wasm-libs/Cargo.toml | 14 +++++++------- frontend/appflowy_web_app/src-tauri/Cargo.lock | 18 +++++++++--------- frontend/appflowy_web_app/src-tauri/Cargo.toml | 14 +++++++------- frontend/rust-lib/Cargo.lock | 14 +++++++------- frontend/rust-lib/Cargo.toml | 14 +++++++------- 8 files changed, 60 insertions(+), 51 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 038405c957..667554baff 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -76,6 +76,8 @@ class DocumentBloc extends Bloc { StreamSubscription? _transactionSubscription; + bool isClosing = false; + final _updateSelectionDebounce = Debounce(); final _syncThrottle = Throttler(duration: const Duration(milliseconds: 500)); @@ -92,6 +94,10 @@ class DocumentBloc extends Bloc { @override Future close() async { + isClosing = true; + _updateSelectionDebounce.dispose(); + _syncThrottle.dispose(); + await _documentService.syncAwarenessStates(documentId: documentId); await _documentListener.stop(); await _syncStateListener.stop(); await _viewListener?.stop(); @@ -354,6 +360,9 @@ class DocumentBloc extends Bloc { } Future _onSelectionUpdate() async { + if (isClosing) { + return; + } final user = state.userProfilePB; final deviceId = ApplicationInfo.deviceId; if (!FeatureFlag.syncDocument.isOn || user == null) { diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index c4bf194b87..2a1c42eaa1 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -860,7 +860,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -884,7 +884,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -914,7 +914,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -933,7 +933,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "bytes", @@ -948,7 +948,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "chrono", @@ -986,7 +986,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-stream", @@ -1067,7 +1067,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index f323062ffc..9206c93d27 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -104,10 +104,10 @@ default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] [patch.crates-io] -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } \ No newline at end of file +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } \ No newline at end of file diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 1b6cb52c67..e6637b4d31 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -70,10 +70,10 @@ opt-level = 3 codegen-units = 1 [patch.crates-io] -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } \ No newline at end of file +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock index 2fcd8de4d4..2bb44433ed 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.lock +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -843,7 +843,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -867,7 +867,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -897,7 +897,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -916,7 +916,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "bytes", @@ -931,7 +931,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "chrono", @@ -969,7 +969,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-stream", @@ -1050,7 +1050,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=870cd70#870cd70e31fa30bc6f94595ca040a91c685dfb4e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -8272,9 +8272,9 @@ dependencies = [ [[package]] name = "yrs" -version = "0.18.7" +version = "0.18.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58fbc807677598fedfab76f99f6e1aa5c644411255002b5438ea0ab14672398" +checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f" dependencies = [ "arc-swap", "atomic_refcell", diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 63bc2269e3..45d822b46e 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -103,10 +103,10 @@ default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] [patch.crates-io] -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } \ No newline at end of file +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } \ No newline at end of file diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index a33996614f..0560f222de 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -739,7 +739,7 @@ dependencies = [ [[package]] name = "collab" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -763,7 +763,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-trait", @@ -793,7 +793,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "bytes", @@ -827,7 +827,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "chrono", @@ -865,7 +865,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "async-stream", @@ -946,7 +946,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.2.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index b650644ec5..bb728e820b 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -129,10 +129,10 @@ rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec1 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } -collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2c430e0" } \ No newline at end of file +collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } +collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" } \ No newline at end of file From c67e266174868ecfd5e13717487c4baf26810690 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 21 May 2024 09:21:50 +0800 Subject: [PATCH 14/30] fix: range error raise if heading level >= 6 (#5373) --- .../plugins/document/presentation/editor_configuration.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index c8222c5e03..39cd608d0e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -92,8 +92,9 @@ Map getEditorBuilderMap({ final factor = pageStyle.fontLayout.factor; final headingPaddings = pageStyle.lineHeightLayout.headingPaddings .map((e) => e * factor); - final level = node.attributes[HeadingBlockKeys.level] ?? 6; - return EdgeInsets.only(top: headingPaddings.elementAt(level)); + int level = node.attributes[HeadingBlockKeys.level] ?? 6; + level = level.clamp(1, 6); + return EdgeInsets.only(top: headingPaddings.elementAt(level - 1)); } return const EdgeInsets.only(top: 12.0, bottom: 4.0); From 24b3d6986046f61491c7c62b47a59dc973727822 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 21 May 2024 10:47:31 +0800 Subject: [PATCH 15/30] fix: infinite loop when getting ancestor for orphan view (#5375) --- .../src/folder_event.rs | 30 +++++++++++++++++++ .../tests/folder/local_test/folder_test.rs | 16 ++++++++++ frontend/rust-lib/flowy-folder/src/manager.rs | 4 +++ 3 files changed, 50 insertions(+) diff --git a/frontend/rust-lib/event-integration-test/src/folder_event.rs b/frontend/rust-lib/event-integration-test/src/folder_event.rs index 280e91e008..194d15a54c 100644 --- a/frontend/rust-lib/event-integration-test/src/folder_event.rs +++ b/frontend/rust-lib/event-integration-test/src/folder_event.rs @@ -139,6 +139,24 @@ impl EventIntegrationTest { } } + /// Create orphan views in the folder. + /// Orphan view: the parent_view_id equal to the view_id + /// Normally, the orphan view will be created in nested database + pub async fn create_orphan_view(&self, name: &str, view_id: &str, layout: ViewLayoutPB) { + let payload = CreateOrphanViewPayloadPB { + name: name.to_string(), + desc: "".to_string(), + layout, + view_id: view_id.to_string(), + initial_data: vec![], + }; + EventBuilder::new(self.clone()) + .event(FolderEvent::CreateOrphanView) + .payload(payload) + .async_send() + .await; + } + pub fn get_folder_data(&self) -> FolderData { let mutex_folder = self.appflowy_core.folder_manager.get_mutex_folder().clone(); let folder_lock_guard = mutex_folder.read(); @@ -240,6 +258,18 @@ impl EventIntegrationTest { .await .parse::() } + + pub async fn get_view_ancestors(&self, view_id: &str) -> Vec { + EventBuilder::new(self.clone()) + .event(FolderEvent::GetViewAncestors) + .payload(ViewIdPB { + value: view_id.to_string(), + }) + .async_send() + .await + .parse::() + .items + } } pub struct ViewTest { diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs index 7523f5ab0f..d0a4a28429 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/folder_test.rs @@ -1,6 +1,8 @@ use collab_folder::ViewLayout; +use event_integration_test::EventIntegrationTest; use flowy_folder::entities::icon::{ViewIconPB, ViewIconTypePB}; +use flowy_folder::entities::ViewLayoutPB; use crate::folder::local_test::script::FolderScript::*; use crate::folder::local_test::script::FolderTest; @@ -331,3 +333,17 @@ async fn move_view_event_test() { assert_eq!(after_view_ids[0], view_ids[1]); assert_eq!(after_view_ids[1], view_ids[0]); } + +#[tokio::test] +async fn create_orphan_child_view_and_get_its_ancestors_test() { + let test = EventIntegrationTest::new_anon().await; + let name = "Orphan View"; + let view_id = "20240521"; + test + .create_orphan_view(name, view_id, ViewLayoutPB::Grid) + .await; + let ancestors = test.get_view_ancestors(view_id).await; + assert_eq!(ancestors.len(), 1); + assert_eq!(ancestors[0].name, "Orphan View"); + assert_eq!(ancestors[0].id, view_id); +} diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 2433a181a5..35d2ffc72b 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -530,6 +530,10 @@ impl FolderManager { while let Some(view) = self.with_folder(|| None, |folder| folder.views.get_view(&parent_view_id)) { + // If the view is already in the ancestors list, then break the loop + if ancestors.iter().any(|v: &ViewPB| v.id == view.id) { + break; + } ancestors.push(view_pb_without_child_views(view.as_ref().clone())); parent_view_id = view.parent_view_id.clone(); } From e85dbe724c9c8eb74a65c6aff59e96be23739948 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Tue, 21 May 2024 14:41:47 +0800 Subject: [PATCH 16/30] chore: add limitation when importing csv file (#5381) --- frontend/rust-lib/flowy-database2/src/manager.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 77d9a477bc..283ad989c1 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -347,6 +347,12 @@ impl DatabaseManager { }) .await .map_err(internal_error)??; + + // Currently, we only support importing up to 500 rows. We can support more rows in the future. + if !cfg!(debug_assertions) && params.rows.len() > 500 { + return Err(FlowyError::internal().with_context("The number of rows exceeds the limit")); + } + let result = ImportResult { database_id: params.database_id.clone(), view_id: params.inline_view_id.clone(), From aa0739325345b817288ccbd43f941f207c75fb49 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Tue, 21 May 2024 15:07:15 +0800 Subject: [PATCH 17/30] fix: unable to remove the remote selection when the user close the page on mobile (#5376) * fix: unable to remove the remote selection when the user close the page on mobile * fix: unable to apply settings font to editor * chore: remove transparent background color --- .../document/application/document_bloc.dart | 28 ++----------------- .../application/document_collab_adapter.dart | 7 ++--- .../page_style/_page_style_cover_image.dart | 2 -- .../page_style/_page_style_icon.dart | 1 - .../page_style/_page_style_layout.dart | 1 - .../document/presentation/editor_style.dart | 6 +++- 6 files changed, 11 insertions(+), 34 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart index 667554baff..121582e1f3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_bloc.dart @@ -20,7 +20,6 @@ import 'package:appflowy/util/debounce.dart'; import 'package:appflowy/util/throttle.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -78,13 +77,13 @@ class DocumentBloc extends Bloc { bool isClosing = false; - final _updateSelectionDebounce = Debounce(); - final _syncThrottle = Throttler(duration: const Duration(milliseconds: 500)); + static const _syncDuration = Duration(milliseconds: 250); + final _updateSelectionDebounce = Debounce(duration: _syncDuration); + final _syncThrottle = Throttler(duration: _syncDuration); // The conflict handle logic is not fully implemented yet // use the syncTimer to force to reload the document state when the conflict happens. Timer? _syncTimer; - bool _shouldSync = false; bool get isLocalMode { final userProfilePB = state.userProfilePB; @@ -116,7 +115,6 @@ class DocumentBloc extends Bloc { ) async { await event.when( initial: () async { - _resetSyncTimer(); final result = await _fetchDocumentState(); _onViewChanged(); _onDocumentChanged(); @@ -211,19 +209,6 @@ class DocumentBloc extends Bloc { ); } - void _resetSyncTimer() { - _syncTimer?.cancel(); - _syncTimer = null; - _syncTimer = Timer.periodic(const Duration(seconds: 10), (_) { - if (!_shouldSync) { - return; - } - Log.debug('auto sync document'); - // unawaited(_documentCollabAdapter.forceReload()); - _shouldSync = false; - }); - } - /// Fetch document Future> _fetchDocumentState() async { final result = await _documentService.openDocument(documentId: documentId); @@ -263,10 +248,6 @@ class DocumentBloc extends Bloc { // ignore: invalid_use_of_visible_for_testing_member emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty)); } - - // reset the sync timer - _shouldSync = true; - _resetSyncTimer(); }, ); @@ -328,8 +309,6 @@ class DocumentBloc extends Bloc { } unawaited(_documentCollabAdapter.syncV3(docEvent: docEvent)); - - _resetSyncTimer(); } Future _onAwarenessStatesUpdate( @@ -353,7 +332,6 @@ class DocumentBloc extends Bloc { } void _throttleSyncDoc(DocEventPB docEvent) { - _shouldSync = true; _syncThrottle.call(() { _onDocumentStateUpdate(docEvent); }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart index 43292817d1..8d8841cc3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_collab_adapter.dart @@ -183,7 +183,7 @@ class DocumentCollabAdapter { for (final state in values) { // the following code is only for version 1 if (state.version != 1 || state.metadata.isEmpty) { - return; + continue; } final uid = state.user.uid.toString(); final did = state.user.deviceId; @@ -244,9 +244,8 @@ class DocumentCollabAdapter { ); remoteSelections.add(remoteSelection); } - if (remoteSelections.isNotEmpty) { - editorState.remoteSelections.value = remoteSelections; - } + + editorState.remoteSelections.value = remoteSelections; } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 3055c7c440..214fb7456f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -188,7 +188,6 @@ class PageStyleCoverImage extends StatelessWidget { ); }, title: LocaleKeys.pageStyle_presets.tr(), - barrierColor: Colors.transparent, backgroundColor: Theme.of(context).colorScheme.background, builder: (_) { return BlocProvider.value( @@ -267,7 +266,6 @@ class PageStyleCoverImage extends StatelessWidget { showHeader: true, showRemoveButton: true, title: LocaleKeys.pageStyle_unsplash.tr(), - barrierColor: Colors.transparent, backgroundColor: Theme.of(context).colorScheme.background, onRemove: () { pageStyleBloc.add( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart index ae975048d6..a3ccc974f7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart @@ -77,7 +77,6 @@ class _PageStyleIconState extends State { showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_pageIcon.tr(), - barrierColor: Colors.transparent, backgroundColor: Theme.of(context).colorScheme.background, isScrollControlled: true, enableDraggableScrollable: true, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart index 7b8d5cfb92..054f544c8f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart @@ -208,7 +208,6 @@ class _FontButton extends StatelessWidget { showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_font.tr(), - barrierColor: Colors.transparent, backgroundColor: Theme.of(context).colorScheme.background, isScrollControlled: true, enableDraggableScrollable: true, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 6d81e8eb8f..6196388bd1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -42,9 +42,13 @@ class EditorStyleCustomizer { EditorStyle desktop() { final theme = Theme.of(context); + final appearanceFont = context.read().state.font; final appearance = context.read().state; final fontSize = appearance.fontSize; - final fontFamily = appearance.fontFamily; + String fontFamily = appearance.fontFamily; + if (fontFamily.isEmpty && appearanceFont.isNotEmpty) { + fontFamily = appearanceFont; + } return EditorStyle.desktop( padding: padding, From b7bc847107b686c82b628c443e1482889b889ae8 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Tue, 21 May 2024 17:26:00 +0800 Subject: [PATCH 18/30] feat: support board preview on web (#5384) --- frontend/appflowy_web_app/package.json | 1 + frontend/appflowy_web_app/pnpm-lock.yaml | 15 + .../src/application/collab.type.ts | 37 ++- .../src/application/database-yjs/context.ts | 73 +---- .../src/application/database-yjs/group.ts | 60 ++++ .../src/application/database-yjs/selector.ts | 273 +++++++++++++++++- .../src/application/folder-yjs/selector.ts | 9 +- .../src/components/database/Database.tsx | 79 +---- .../src/components/database/DatabaseViews.tsx | 86 ++++++ .../src/components/database/board/Board.tsx | 29 +- .../components/database/calendar/Calendar.tsx | 11 +- .../database/calendar/calendar.scss | 2 + .../database/components/board/card/Card.tsx | 50 ++++ .../components/board/card/CardField.tsx | 48 +++ .../database/components/board/card/index.ts | 1 + .../components/board/column/Column.tsx | 130 +++++++++ .../components/board/column/ListItem.tsx | 74 +++++ .../database/components/board/column/index.ts | 1 + .../board/column/useRenderColumn.ts | 31 ++ .../database/components/board/group/Group.tsx | 71 +++++ .../database/components/board/group/index.ts | 1 + .../database/components/board/index.ts | 1 + .../components/calculation-cell/cell.type.ts | 8 - .../database/components/cell/Cell.hooks.ts | 2 +- .../database/components/cell/Cell.tsx | 45 ++- .../database/components/cell/CheckboxCell.tsx | 14 - .../database/components/cell/TextCell.tsx | 12 - .../database/components/cell/cell.type.ts | 26 +- .../components/cell/checkbox/CheckboxCell.tsx | 13 + .../components/cell/checkbox/index.ts | 1 + .../cell/{ => checklist}/ChecklistCell.tsx | 7 +- .../components/cell/checklist/index.ts | 1 + .../RowCreateModifiedTime.tsx | 7 +- .../components/cell/created-modified/index.ts | 1 + .../cell/{ => date}/DateTimeCell.tsx | 8 +- .../database/components/cell/date/index.ts | 1 + .../cell/{ => number}/NumberCell.tsx | 12 +- .../database/components/cell/number/index.ts | 1 + .../components/cell/relation/RelationCell.tsx | 7 + .../RelationItems.tsx} | 44 +-- .../cell/relation/RelationPrimaryValue.tsx | 27 ++ .../components/cell/relation/index.ts | 1 + .../SelectOptionCell.tsx} | 11 +- .../components/cell/select-option/index.ts | 1 + .../components/cell/text/TextCell.tsx | 14 + .../database/components/cell/text/index.ts | 1 + .../components/cell/{ => url}/UrlCell.tsx | 8 +- .../database/components/cell/url/index.ts | 1 + .../database/components/cell/useMeasure.ts | 53 ++++ .../database/components/grid-column/index.ts | 2 - .../CalculationCell.tsx | 16 +- .../grid-calculation-cell}/index.ts | 0 .../{ => grid}/grid-cell/GridCell.tsx | 28 +- .../components/{ => grid}/grid-cell/index.ts | 0 .../{ => grid}/grid-column/GridColumn.tsx | 0 .../components/grid/grid-column/index.ts | 2 + .../grid-column/useRenderFields.tsx} | 18 +- .../{ => grid}/grid-header/GridHeader.tsx | 0 .../{ => grid}/grid-header/index.ts | 0 .../grid-row/GridCalculateRowCell.tsx | 5 +- .../{ => grid}/grid-row/GridRowCell.tsx | 4 +- .../components/{ => grid}/grid-row/index.ts | 0 .../{ => grid}/grid-row/useRenderRows.tsx | 7 +- .../{ => grid}/grid-table/GridTable.tsx | 9 +- .../components/{ => grid}/grid-table/index.ts | 0 .../database/components/grid/index.ts | 3 + .../database/components/tabs/DatabaseTabs.tsx | 5 +- .../src/components/database/grid/Grid.tsx | 21 +- .../src/components/layout/layout.scss | 2 +- frontend/resources/translations/en.json | 4 +- 70 files changed, 1179 insertions(+), 357 deletions(-) create mode 100644 frontend/appflowy_web_app/src/application/database-yjs/group.ts create mode 100644 frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/calendar/calendar.scss create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/card/CardField.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/card/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/group/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/board/index.ts delete mode 100644 frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts delete mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx delete mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => checklist}/ChecklistCell.tsx (68%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => created-modified}/RowCreateModifiedTime.tsx (88%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => date}/DateTimeCell.tsx (75%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => number}/NumberCell.tsx (69%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx rename frontend/appflowy_web_app/src/components/database/components/cell/{RelationCell.tsx => relation/RelationItems.tsx} (58%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{SelectionCell.tsx => select-option/SelectOptionCell.tsx} (68%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts rename frontend/appflowy_web_app/src/components/database/components/cell/{ => url}/UrlCell.tsx (77%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts delete mode 100644 frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts rename frontend/appflowy_web_app/src/components/database/components/{calculation-cell => grid/grid-calculation-cell}/CalculationCell.tsx (83%) rename frontend/appflowy_web_app/src/components/database/components/{calculation-cell => grid/grid-calculation-cell}/index.ts (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-cell/GridCell.tsx (57%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-cell/index.ts (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-column/GridColumn.tsx (100%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts rename frontend/appflowy_web_app/src/components/database/components/{grid-column/useRenderColumns.tsx => grid/grid-column/useRenderFields.tsx} (75%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-header/GridHeader.tsx (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-header/index.ts (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-row/GridCalculateRowCell.tsx (83%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-row/GridRowCell.tsx (81%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-row/index.ts (100%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-row/useRenderRows.tsx (76%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-table/GridTable.tsx (95%) rename frontend/appflowy_web_app/src/components/database/components/{ => grid}/grid-table/index.ts (100%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/grid/index.ts diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json index 2dafe5e66d..6eeb31ee09 100644 --- a/frontend/appflowy_web_app/package.json +++ b/frontend/appflowy_web_app/package.json @@ -107,6 +107,7 @@ "@types/quill": "^2.0.10", "@types/react": "^18.2.66", "@types/react-beautiful-dnd": "^13.1.3", + "@types/react-big-calendar": "^1.8.9", "@types/react-color": "^3.0.6", "@types/react-custom-scrollbars": "^4.0.13", "@types/react-datepicker": "^4.19.3", diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index 770298d3b9..4cc4e224c2 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -256,6 +256,9 @@ devDependencies: '@types/react-beautiful-dnd': specifier: ^13.1.3 version: 13.1.3 + '@types/react-big-calendar': + specifier: ^1.8.9 + version: 1.8.9 '@types/react-color': specifier: ^3.0.6 version: 3.0.6 @@ -2618,6 +2621,10 @@ packages: dependencies: '@babel/types': 7.24.0 + /@types/date-arithmetic@4.1.4: + resolution: {integrity: sha512-p9eZ2X9B80iKiTW4ukVj8B4K6q9/+xFtQ5MGYA5HWToY9nL4EkhV9+6ftT2VHpVMEZb5Tv00Iel516bVdO+yRw==} + dev: true + /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -2723,6 +2730,14 @@ packages: '@types/react': 18.2.66 dev: true + /@types/react-big-calendar@1.8.9: + resolution: {integrity: sha512-HIHLUxR3PzWHrFdZ00VnCMvDjAh5uzlL0vMC2b7tL3bKaAJsqq9T8h+x0GVeDbZfMfHAd1cs5tZBhVvourNJXQ==} + dependencies: + '@types/date-arithmetic': 4.1.4 + '@types/prop-types': 15.7.12 + '@types/react': 18.2.66 + dev: true + /@types/react-color@3.0.6: resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==} dependencies: diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index 9a2bcfe186..ac6be1f3f8 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -241,6 +241,9 @@ export enum YjsDatabaseKey { condition = 'condition', format = 'format', filter_type = 'filter_type', + visible = 'visible', + hide_ungrouped_column = 'hide_ungrouped_column', + collapse_hidden_groups = 'collapse_hidden_groups', } export interface YDoc extends Y.Doc { @@ -425,18 +428,48 @@ export type YDatabaseFieldOrders = Y.Array; // [ { id: FieldId } ] export type YDatabaseRowOrders = Y.Array; // [ { id: RowId, height: number } ] -export type YDatabaseGroups = Y.Array; +export type YDatabaseGroups = Y.Array; export type YDatabaseFilters = Y.Array; export type YDatabaseSorts = Y.Array; -export type YDatabaseLayoutSettings = Y.Map; +export type YDatabaseLayoutSettings = Y.Map; export type YDatabaseCalculations = Y.Array; export type SortId = string; +export type GroupId = string; + +export interface YDatabaseLayoutSetting extends Y.Map { + // DatabaseViewLayout.Board + get(key: '2'): YDatabaseBoardLayoutSetting; +} + +export interface YDatabaseBoardLayoutSetting extends Y.Map { + get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean; +} + +export interface YDatabaseGroup extends Y.Map { + get(key: YjsDatabaseKey.id): GroupId; + + get(key: YjsDatabaseKey.field_id): FieldId; + + // eslint-disable-next-line @typescript-eslint/unified-signatures + get(key: YjsDatabaseKey.content): string; + + get(key: YjsDatabaseKey.groups): YDatabaseGroupColumns; +} + +export type YDatabaseGroupColumns = Y.Array; + +export interface YDatabaseGroupColumn extends Y.Map { + get(key: YjsDatabaseKey.id): string; + + get(key: YjsDatabaseKey.visible): boolean; +} + export interface YDatabaseRowOrder extends Y.Map { get(key: YjsDatabaseKey.id): SortId; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts index 8717aa0ffe..73feb8d0d7 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/context.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -1,10 +1,7 @@ import { YDatabase, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; -import { filterBy } from '@/application/database-yjs/filter'; import { Row } from '@/application/database-yjs/selector'; -import { sortBy } from '@/application/database-yjs/sort'; -import { createContext, useContext, useEffect, useState } from 'react'; +import { createContext, useContext } from 'react'; import * as Y from 'yjs'; -import debounce from 'lodash-es/debounce'; export interface DatabaseContextState { readOnly: boolean; @@ -56,72 +53,16 @@ export function useDatabaseFields() { return database.get(YjsDatabaseKey.fields); } -export interface GridRowsState { +export interface RowsState { rowOrders: Row[]; } -export const GridRowsContext = createContext(null); +export const RowsContext = createContext(null); -export function useGridRowsContext() { - return useContext(GridRowsContext); +export function useRowsContext() { + return useContext(RowsContext); } -export function useGridRows() { - return useGridRowsContext()?.rowOrders; -} - -export function useGridRowOrders() { - const rows = useContext(DatabaseContext)?.rowDocMap; - const [rowOrders, setRowOrders] = useState(); - const view = useDatabaseView(); - const sorts = view?.get(YjsDatabaseKey.sorts); - const fields = useDatabaseFields(); - const filters = view?.get(YjsDatabaseKey.filters); - - useEffect(() => { - const onConditionsChange = () => { - const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON(); - - if (!originalRowOrders || !rows) return; - - console.log('sort or filter changed'); - if (sorts?.length === 0 && filters?.length === 0) { - setRowOrders(originalRowOrders); - return; - } - - let rowOrders: Row[] | undefined; - - if (sorts?.length) { - rowOrders = sortBy(originalRowOrders, sorts, fields, rows); - } - - if (filters?.length) { - rowOrders = filterBy(rowOrders ?? originalRowOrders, filters, fields, rows); - } - - if (rowOrders) { - setRowOrders(rowOrders); - } else { - setRowOrders(originalRowOrders); - } - }; - - const debounceConditionsChange = debounce(onConditionsChange, 200); - - onConditionsChange(); - sorts?.observeDeep(debounceConditionsChange); - filters?.observeDeep(debounceConditionsChange); - fields?.observeDeep(debounceConditionsChange); - rows?.observeDeep(debounceConditionsChange); - - return () => { - sorts?.unobserveDeep(debounceConditionsChange); - filters?.unobserveDeep(debounceConditionsChange); - fields?.unobserveDeep(debounceConditionsChange); - rows?.observeDeep(debounceConditionsChange); - }; - }, [fields, rows, sorts, filters, view]); - - return rowOrders; +export function useRows() { + return useRowsContext()?.rowOrders; } diff --git a/frontend/appflowy_web_app/src/application/database-yjs/group.ts b/frontend/appflowy_web_app/src/application/database-yjs/group.ts new file mode 100644 index 0000000000..ddefab9a26 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/database-yjs/group.ts @@ -0,0 +1,60 @@ +import { YDatabaseField, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { FieldType } from '@/application/database-yjs/database.type'; +import { parseSelectOptionTypeOptions } from '@/application/database-yjs/fields'; +import { Row } from '@/application/database-yjs/selector'; +import * as Y from 'yjs'; + +export function groupByField(rows: Row[], rowMetas: Y.Map, field: YDatabaseField) { + const fieldType = Number(field.get(YjsDatabaseKey.type)); + const isSelectOptionField = [FieldType.SingleSelect, FieldType.MultiSelect].includes(fieldType); + + if (!isSelectOptionField) return; + return groupBySelectOption(rows, rowMetas, field); +} + +function getCellData(rowId: string, fieldId: string, rowMetas: Y.Map) { + const rowMeta = rowMetas.get(rowId); + const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + return meta?.get(YjsDatabaseKey.cells)?.get(fieldId)?.get(YjsDatabaseKey.data); +} + +export function groupBySelectOption(rows: Row[], rowMetas: Y.Map, field: YDatabaseField) { + const fieldId = field.get(YjsDatabaseKey.id); + const result = new Map(); + const typeOption = parseSelectOptionTypeOptions(field); + + if (!typeOption) { + return; + } + + if (typeOption.options.length === 0) { + result.set(fieldId, rows); + return result; + } + + rows.forEach((row) => { + const cellData = getCellData(row.id, fieldId, rowMetas); + + const selectedIds = (cellData as string)?.split(',') ?? []; + + if (selectedIds.length === 0) { + const group = result.get(fieldId) ?? []; + + group.push(row); + result.set(fieldId, group); + return; + } + + selectedIds.forEach((id) => { + const option = typeOption.options.find((option) => option.id === id); + const groupName = option?.id ?? fieldId; + const group = result.get(groupName) ?? []; + + group.push(row); + result.set(groupName, group); + }); + }); + + return result; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts index c3222fdf65..f115ff5eb4 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -1,9 +1,22 @@ import { FieldId, SortId, YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; import { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; -import { useDatabase, useGridRows, useViewId } from '@/application/database-yjs/context'; -import { parseFilter } from '@/application/database-yjs/filter'; +import { + DatabaseContext, + useDatabase, + useDatabaseFields, + useDatabaseView, + useRowMeta, + useRows, + useViewId, +} from '@/application/database-yjs/context'; +import { filterBy, parseFilter } from '@/application/database-yjs/filter'; +import { groupByField } from '@/application/database-yjs/group'; +import { sortBy } from '@/application/database-yjs/sort'; +import { useViewsIdSelector } from '@/application/folder-yjs'; +import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; +import debounce from 'lodash-es/debounce'; +import { useContext, useEffect, useMemo, useState } from 'react'; import { FieldType, FieldVisibility, Filter, SortCondition } from './database.type'; -import { useEffect, useMemo, useState } from 'react'; export interface Column { fieldId: string; @@ -19,11 +32,43 @@ export interface Row { const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; -export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibility[] = defaultVisible) { +export function useDatabaseViewsSelector() { + const database = useDatabase(); + const { viewsId: visibleViewsId } = useViewsIdSelector(); + const views = database?.get(YjsDatabaseKey.views); + const [viewIds, setViewIds] = useState([]); + const childViews = useMemo(() => { + return viewIds.map((viewId) => views?.get(viewId)); + }, [viewIds, views]); + + useEffect(() => { + if (!views) return; + + const observerEvent = () => { + setViewIds(Array.from(views.keys()).filter((id) => visibleViewsId.includes(id))); + }; + + observerEvent(); + views.observe(observerEvent); + + return () => { + views.unobserve(observerEvent); + }; + }, [visibleViewsId, views]); + + return { + childViews, + viewIds, + }; +} + +export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisible) { + const viewId = useViewId(); const database = useDatabase(); const [columns, setColumns] = useState([]); useEffect(() => { + if (!viewId) return; const view = database?.get(YjsDatabaseKey.views)?.get(viewId); const fields = database?.get(YjsDatabaseKey.fields); const fieldsOrder = view?.get(YjsDatabaseKey.field_orders); @@ -39,11 +84,15 @@ export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibil return { fieldId, width: parseInt(setting?.get(YjsDatabaseKey.width)) || MIN_COLUMN_WIDTH, - visibility: parseInt(setting?.get(YjsDatabaseKey.visibility)) as FieldVisibility, + visibility: Number( + setting?.get(YjsDatabaseKey.visibility) || FieldVisibility.AlwaysShown + ) as FieldVisibility, wrap: setting?.get(YjsDatabaseKey.wrap), }; }) - .filter((column) => visibilitys.includes(column.visibility)); + .filter((column) => { + return visibilitys.includes(column.visibility); + }); }; const observerEvent = () => setColumns(getColumns()); @@ -62,8 +111,8 @@ export function useGridColumnsSelector(viewId: string, visibilitys: FieldVisibil return columns; } -export function useGridRowsSelector() { - const rowOrders = useGridRows(); +export function useRowsSelector() { + const rowOrders = useRows(); return useMemo(() => rowOrders ?? [], [rowOrders]); } @@ -81,10 +130,10 @@ export function useFieldSelector(fieldId: string) { setField(field || null); const observerEvent = () => setClock((prev) => prev + 1); - field.observe(observerEvent); + field?.observe(observerEvent); return () => { - field.unobserve(observerEvent); + field?.unobserve(observerEvent); }; }, [database, fieldId]); @@ -225,3 +274,207 @@ export function useSortSelector(sortId: SortId) { return sortValue; } + +export function useGroupsSelector() { + const database = useDatabase(); + const viewId = useViewId(); + const [groups, setGroups] = useState([]); + + useEffect(() => { + if (!viewId) return; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const groupOrders = view?.get(YjsDatabaseKey.groups); + + if (!groupOrders) return; + + const getGroups = () => { + return groupOrders.toJSON().map((item) => item.id); + }; + + const observerEvent = () => setGroups(getGroups()); + + setGroups(getGroups()); + + groupOrders.observe(observerEvent); + + return () => { + groupOrders.unobserve(observerEvent); + }; + }, [database, viewId]); + + return groups; +} + +export interface GroupColumn { + id: string; + visible: boolean; +} + +export function useGroup(groupId: string) { + const database = useDatabase(); + const viewId = useViewId() as string; + const view = database?.get(YjsDatabaseKey.views)?.get(viewId); + const group = view + ?.get(YjsDatabaseKey.groups) + ?.toArray() + .find((group) => group.get(YjsDatabaseKey.id) === groupId); + const groupColumns = group?.get(YjsDatabaseKey.groups); + const [fieldId, setFieldId] = useState(null); + const [columns, setColumns] = useState([]); + + useEffect(() => { + if (!viewId) return; + + const observerEvent = () => { + setFieldId(group?.get(YjsDatabaseKey.field_id) as string); + }; + + observerEvent(); + group?.observe(observerEvent); + + const observerColumns = () => { + if (!groupColumns) return; + setColumns(groupColumns.toJSON()); + }; + + observerColumns(); + groupColumns?.observe(observerColumns); + + return () => { + group?.unobserve(observerEvent); + groupColumns?.unobserve(observerColumns); + }; + }, [database, viewId, groupId, group, groupColumns]); + + return { + columns, + fieldId, + }; +} + +export function useRowsByGroup(groupId: string) { + const { columns, fieldId } = useGroup(groupId); + const rows = useContext(DatabaseContext)?.rowDocMap; + const rowOrders = useRowOrdersSelector(); + const fields = useDatabaseFields(); + const [notFound, setNotFound] = useState(false); + const [groupResult, setGroupResult] = useState>(new Map()); + + useEffect(() => { + if (!fieldId || !rowOrders || !rows) return; + + const onConditionsChange = () => { + const newResult = new Map(); + + const field = fields.get(fieldId); + + if (!field) { + setNotFound(true); + setGroupResult(newResult); + return; + } + + const groupResult = groupByField(rowOrders, rows, field); + + if (!groupResult) { + setGroupResult(newResult); + return; + } + + setGroupResult(groupResult); + }; + + onConditionsChange(); + + const debounceConditionsChange = debounce(onConditionsChange, 200); + + fields.observeDeep(debounceConditionsChange); + return () => { + fields.unobserveDeep(debounceConditionsChange); + }; + }, [fieldId, fields, rowOrders, rows]); + + const visibleColumns = columns.filter((column) => column.visible); + + return { + fieldId, + groupResult, + columns: visibleColumns, + notFound, + }; +} + +export function useRowOrdersSelector() { + const rows = useContext(DatabaseContext)?.rowDocMap; + const [rowOrders, setRowOrders] = useState(); + const view = useDatabaseView(); + const sorts = view?.get(YjsDatabaseKey.sorts); + const fields = useDatabaseFields(); + const filters = view?.get(YjsDatabaseKey.filters); + + useEffect(() => { + const onConditionsChange = () => { + const originalRowOrders = view?.get(YjsDatabaseKey.row_orders).toJSON(); + + if (!originalRowOrders || !rows) return; + + if (sorts?.length === 0 && filters?.length === 0) { + setRowOrders(originalRowOrders); + return; + } + + let rowOrders: Row[] | undefined; + + if (sorts?.length) { + rowOrders = sortBy(originalRowOrders, sorts, fields, rows); + } + + if (filters?.length) { + rowOrders = filterBy(rowOrders ?? originalRowOrders, filters, fields, rows); + } + + if (rowOrders) { + setRowOrders(rowOrders); + } else { + setRowOrders(originalRowOrders); + } + }; + + const debounceConditionsChange = debounce(onConditionsChange, 200); + + onConditionsChange(); + sorts?.observeDeep(debounceConditionsChange); + filters?.observeDeep(debounceConditionsChange); + fields?.observeDeep(debounceConditionsChange); + rows?.observeDeep(debounceConditionsChange); + + return () => { + sorts?.unobserveDeep(debounceConditionsChange); + filters?.unobserveDeep(debounceConditionsChange); + fields?.unobserveDeep(debounceConditionsChange); + rows?.observeDeep(debounceConditionsChange); + }; + }, [fields, rows, sorts, filters, view]); + + return rowOrders; +} + +export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) { + const row = useRowMeta(rowId); + const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId); + const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined)); + + useEffect(() => { + if (!cell) return; + setCellValue(parseYDatabaseCellToCell(cell)); + const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell)); + + cell.observe(observerEvent); + + return () => { + cell.unobserve(observerEvent); + }; + }, [cell]); + + return cellValue; +} diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts index 295315874b..648e27c9d3 100644 --- a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts @@ -12,11 +12,14 @@ export function useViewsIdSelector() { const views = folder.get(YjsFolderKey.views); const trash = folder.get(YjsFolderKey.section)?.get(YjsFolderKey.trash); const meta = folder.get(YjsFolderKey.meta); + const trashUid = Array.from(trash?.keys())[0]; + const userTrash = trash?.get(trashUid); - console.log('folder', folder.toJSON()); const collectIds = () => { + const trashIds = userTrash?.toJSON()?.map((item) => item.id) || []; + return Array.from(views.keys()).filter( - (id) => !trash?.has(id) && id !== meta?.get(YjsFolderKey.current_workspace) + (id) => !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace) ); }; @@ -24,9 +27,11 @@ export function useViewsIdSelector() { const observerEvent = () => setViewsId(collectIds()); folder.observe(observerEvent); + userTrash.observe(observerEvent); return () => { folder.unobserve(observerEvent); + userTrash.unobserve(observerEvent); }; }, [folder]); diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx index 9e54b68ad0..fd09fadd41 100644 --- a/frontend/appflowy_web_app/src/components/database/Database.tsx +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -1,27 +1,20 @@ -import { DatabaseViewLayout, YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { YDoc, YjsEditorKey } from '@/application/collab.type'; import { useId } from '@/components/_shared/context-provider/IdProvider'; import RecordNotFound from '@/components/_shared/not-found/RecordNotFound'; import { AFConfigContext } from '@/components/app/AppConfig'; -import { Board } from '@/components/database/board'; -import { Calendar } from '@/components/database/calendar'; -import { DatabaseConditionsContext } from '@/components/database/components/conditions/context'; -import { Grid } from '@/components/database/grid'; -import { DatabaseTabs, TabPanel } from '@/components/database/components/tabs'; +import DatabaseViews from '@/components/database/DatabaseViews'; import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; import DatabaseTitle from '@/components/database/DatabaseTitle'; import { Log } from '@/utils/log'; import CircularProgress from '@mui/material/CircularProgress'; -import React, { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useContext, useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; -import SwipeableViews from 'react-swipeable-views'; -import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions'; import * as Y from 'yjs'; export const Database = memo(() => { const { objectId, workspaceId } = useId() || {}; const [search, setSearch] = useSearchParams(); const viewId = search.get('v'); - const [doc, setDoc] = useState(null); const [rows, setRows] = useState | null>(null); // Map(false); @@ -48,10 +41,6 @@ export const Database = memo(() => { void handleOpenDocument(); }, [handleOpenDocument]); - const database = useMemo(() => doc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase, [doc]); - - const views = useMemo(() => database?.get(YjsDatabaseKey.views), [database]); - const handleChangeView = useCallback( (viewId: string) => { setSearch({ v: viewId }); @@ -59,32 +48,6 @@ export const Database = memo(() => { [setSearch] ); - const viewIds = useMemo(() => (views ? Array.from(views.keys()) : []), [views]); - - const value = useMemo(() => { - return Math.max( - 0, - viewIds.findIndex((id) => id === (viewId ?? objectId)) - ); - }, [viewId, viewIds, objectId]); - - const getDatabaseViewComponent = useCallback((layout: DatabaseViewLayout) => { - switch (layout) { - case DatabaseViewLayout.Grid: - return Grid; - case DatabaseViewLayout.Board: - return Board; - case DatabaseViewLayout.Calendar: - return Calendar; - } - }, []); - - const [conditionsExpanded, setConditionsExpanded] = useState(false); - const toggleExpanded = useCallback(() => { - setConditionsExpanded((prev) => !prev); - }, []); - - console.log('viewId', viewId, 'objectId', doc, objectId, database); if (!objectId) return null; if (!doc) { @@ -104,41 +67,7 @@ export const Database = memo(() => {
- - - - - - {viewIds.map((viewId, index) => { - const layout = Number(views.get(viewId)?.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; - const Component = getDatabaseViewComponent(layout); - - return ( - - - - ); - })} - +
diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx new file mode 100644 index 0000000000..5d055780b9 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx @@ -0,0 +1,86 @@ +import { DatabaseViewLayout, YjsDatabaseKey } from '@/application/collab.type'; +import { useDatabaseViewsSelector } from '@/application/database-yjs'; +import { Board } from '@/components/database/board'; +import { Calendar } from '@/components/database/calendar'; +import { DatabaseConditionsContext } from '@/components/database/components/conditions/context'; +import { DatabaseTabs, TabPanel } from '@/components/database/components/tabs'; +import { Grid } from '@/components/database/grid'; +import React, { useCallback, useMemo, useState } from 'react'; +import SwipeableViews from 'react-swipeable-views'; +import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions'; + +function DatabaseViews({ + onChangeView, + currentViewId, +}: { + onChangeView: (viewId: string) => void; + currentViewId: string; +}) { + const { childViews, viewIds } = useDatabaseViewsSelector(); + + const value = useMemo(() => { + return Math.max( + 0, + viewIds.findIndex((id) => id === currentViewId) + ); + }, [currentViewId, viewIds]); + + const getDatabaseViewComponent = useCallback((layout: DatabaseViewLayout) => { + switch (layout) { + case DatabaseViewLayout.Grid: + return Grid; + case DatabaseViewLayout.Board: + return Board; + case DatabaseViewLayout.Calendar: + return Calendar; + } + }, []); + + const [conditionsExpanded, setConditionsExpanded] = useState(false); + const toggleExpanded = useCallback(() => { + setConditionsExpanded((prev) => !prev); + }, []); + + return ( + <> + + + + + + {childViews.map((view, index) => { + const layout = Number(view.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + const Component = getDatabaseViewComponent(layout); + const viewId = viewIds[index]; + + return ( + + + + ); + })} + + + ); +} + +export default DatabaseViews; diff --git a/frontend/appflowy_web_app/src/components/database/board/Board.tsx b/frontend/appflowy_web_app/src/components/database/board/Board.tsx index eabc9c2631..27c43bf8b3 100644 --- a/frontend/appflowy_web_app/src/components/database/board/Board.tsx +++ b/frontend/appflowy_web_app/src/components/database/board/Board.tsx @@ -1,7 +1,34 @@ +import { useDatabase, useGroupsSelector } from '@/application/database-yjs'; +import { Group } from '@/components/database/components/board'; +import { CircularProgress } from '@mui/material'; import React from 'react'; +import { DragDropContext } from 'react-beautiful-dnd'; export function Board() { - return
Board
; + const database = useDatabase(); + const groups = useGroupsSelector(); + + if (!database) { + return ( +
+ +
+ ); + } + + return ( + { + // + }} + > +
+ {groups.map((groupId) => ( + + ))} +
+
+ ); } export default Board; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx index c21e37b362..4face8913f 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -1,7 +1,16 @@ import React from 'react'; +import { Calendar as BigCalendar, dayjsLocalizer } from 'react-big-calendar'; +import dayjs from 'dayjs'; +import './calendar.scss'; + +const localizer = dayjsLocalizer(dayjs); export function Calendar() { - return
Calendar
; + return ( +
+ +
+ ); } export default Calendar; diff --git a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss new file mode 100644 index 0000000000..3a3aebd3db --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss @@ -0,0 +1,2 @@ +@import 'react-big-calendar/lib/sass/styles'; +@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx new file mode 100644 index 0000000000..7dbe829662 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx @@ -0,0 +1,50 @@ +import { useFieldsSelector } from '@/application/database-yjs'; +import CardField from '@/components/database/components/board/card/CardField'; +import React, { useEffect, useMemo } from 'react'; + +export interface CardProps { + groupFieldId: string; + rowId: string; + onResize?: (height: number) => void; + isDragging?: boolean; +} + +export function Card({ groupFieldId, rowId, onResize, isDragging }: CardProps) { + const fields = useFieldsSelector(); + const showFields = useMemo(() => fields.filter((field) => field.fieldId !== groupFieldId), [fields, groupFieldId]); + + const ref = React.useRef(null); + + useEffect(() => { + if (isDragging) return; + const el = ref.current; + + if (!el) return; + + const observer = new ResizeObserver(() => { + onResize?.(el.offsetHeight); + }); + + observer.observe(el); + + return () => { + observer.disconnect(); + }; + }, [onResize, isDragging]); + + return ( +
+ {showFields.map((field, index) => { + return ; + })} +
+ ); +} + +export default Card; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/CardField.tsx b/frontend/appflowy_web_app/src/components/database/components/board/card/CardField.tsx new file mode 100644 index 0000000000..585a3d2ce0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/CardField.tsx @@ -0,0 +1,48 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useCellSelector, useFieldSelector } from '@/application/database-yjs'; +import Cell from '@/components/database/components/cell/Cell'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function CardField({ rowId, fieldId, index }: { rowId: string; fieldId: string; index: number }) { + const { t } = useTranslation(); + const { field } = useFieldSelector(fieldId); + const cell = useCellSelector({ + rowId, + fieldId, + }); + + const isPrimary = field?.get(YjsDatabaseKey.is_primary); + const style = useMemo(() => { + const styleProperties = { + fontSize: '12px', + }; + + if (isPrimary) { + Object.assign(styleProperties, { + fontSize: '14px', + fontWeight: 500, + }); + } + + if (index !== 0) { + Object.assign(styleProperties, { + marginTop: '8px', + }); + } + + return styleProperties; + }, [index, isPrimary]); + + if (isPrimary && !cell?.data) { + return ( +
+ {t('grid.row.titlePlaceholder')} +
+ ); + } + + return ; +} + +export default CardField; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts new file mode 100644 index 0000000000..ca0b060473 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/index.ts @@ -0,0 +1 @@ +export * from './Card'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx new file mode 100644 index 0000000000..765bbb0a19 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/Column.tsx @@ -0,0 +1,130 @@ +import { Row } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import { Tag } from '@/components/_shared/tag'; +import ListItem from '@/components/database/components/board/column/ListItem'; +import { useRenderColumn } from '@/components/database/components/board/column/useRenderColumn'; +import { useMeasureHeight } from '@/components/database/components/cell/useMeasure'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { Draggable, DraggableProvided, Droppable } from 'react-beautiful-dnd'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { VariableSizeList } from 'react-window'; + +export interface ColumnProps { + id: string; + rows?: Row[]; + fieldId: string; + provided: DraggableProvided; +} + +export function Column({ id, rows, fieldId, provided }: ColumnProps) { + const { header } = useRenderColumn(id, fieldId); + const ref = React.useRef(null); + const forceUpdate = useCallback((index: number) => { + ref.current?.resetAfterIndex(index, true); + }, []); + + useEffect(() => { + forceUpdate(0); + }, [rows, forceUpdate]); + + const measureRows = useMemo( + () => + rows?.map((row) => { + return { + rowId: row.id, + }; + }) || [], + [rows] + ); + const { rowHeight, onResize } = useMeasureHeight({ forceUpdate, rows: measureRows }); + + const Row = useCallback( + ({ index, style, data }: { index: number; style: React.CSSProperties; data: Row[] }) => { + const item = data[index]; + + // We are rendering an extra item for the placeholder + if (!item) { + return null; + } + + const onResizeCallback = (height: number) => { + onResize(index, 0, { + width: 0, + height: height + 8, + }); + }; + + return ( + + {(provided) => ( + + )} + + ); + }, + [fieldId, onResize] + ); + + const getItemSize = useCallback( + (index: number) => { + if (!rows || index >= rows.length) return 0; + const row = rows[index]; + + if (!row) return 0; + return rowHeight(index); + }, + [rowHeight, rows] + ); + + if (!rows) return
; + return ( +
+
+ +
+ +
+ ( + + )} + > + {(provided, snapshot) => { + // Add an extra item to our list to make space for a dragging item + // Usually the DroppableProvided.placeholder does this, but that won't + // work in a virtual list + const itemCount = snapshot.isUsingPlaceholder ? rows.length + 1 : rows.length; + + return ( + + {({ height, width }: { height: number; width: number }) => { + return ( + + {Row} + + ); + }} + + ); + }} + +
+
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx b/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx new file mode 100644 index 0000000000..ac1e3bb82b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/ListItem.tsx @@ -0,0 +1,74 @@ +import { Row } from '@/application/database-yjs'; +import React from 'react'; +import { DraggableProvided, DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd'; +import Card from 'src/components/database/components/board/card/Card'; + +export const ListItem = ({ + provided, + item, + style, + onResize, + fieldId, + isDragging, +}: { + provided: DraggableProvided; + item: Row; + style?: React.CSSProperties; + fieldId: string; + onResize?: (height: number) => void; + isDragging?: boolean; +}) => { + return ( +
+ +
+ ); +}; + +function getStyle({ + draggableStyle, + virtualStyle, + isDragging, +}: { + draggableStyle?: DraggingStyle | NotDraggingStyle; + virtualStyle?: React.CSSProperties; + isDragging?: boolean; +}) { + // If you don't want any spacing between your items + // then you could just return this. + // I do a little bit of magic to have some nice visual space + // between the row items + const combined = { + ...virtualStyle, + ...draggableStyle, + } as { + height: number; + left: number; + width: number; + }; + + // Being lazy: this is defined in our css file + const grid = 1; + + // when dragging we want to use the draggable style for placement, otherwise use the virtual style + + return { + ...combined, + height: isDragging ? combined.height : combined.height - grid, + left: isDragging ? combined.left : combined.left + grid, + width: isDragging ? (draggableStyle as DraggingStyle)?.width : `calc(${combined.width} - ${grid * 2}px)`, + marginBottom: grid, + }; +} + +export default ListItem; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts new file mode 100644 index 0000000000..f59b699c20 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/index.ts @@ -0,0 +1 @@ +export * from './Column'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts new file mode 100644 index 0000000000..c845d4b5a3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/column/useRenderColumn.ts @@ -0,0 +1,31 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { FieldType, parseSelectOptionTypeOptions, useFieldSelector } from '@/application/database-yjs'; +import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; +import { useMemo } from 'react'; + +export function useRenderColumn(id: string, fieldId: string) { + const { field } = useFieldSelector(fieldId); + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const fieldName = field?.get(YjsDatabaseKey.name) || ''; + const header = useMemo(() => { + if (!field) return null; + switch (fieldType) { + case FieldType.SingleSelect: + case FieldType.MultiSelect: { + const option = parseSelectOptionTypeOptions(field)?.options.find((option) => option.id === id); + + return { + name: option?.name || `No ${fieldName}`, + color: option?.color ? SelectOptionColorMap[option?.color] : 'transparent', + }; + } + + default: + return null; + } + }, [field, fieldName, fieldType, id]); + + return { + header, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx new file mode 100644 index 0000000000..7d5e3630be --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx @@ -0,0 +1,71 @@ +import { useRowsByGroup } from '@/application/database-yjs'; +import { AFScroller } from '@/components/_shared/scroller'; +import React from 'react'; +import { Draggable, Droppable } from 'react-beautiful-dnd'; +import { useTranslation } from 'react-i18next'; +import { Column } from '../column'; + +export interface GroupProps { + groupId: string; +} + +export const Group = ({ groupId }: GroupProps) => { + const { columns, groupResult, fieldId, notFound } = useRowsByGroup(groupId); + + const { t } = useTranslation(); + + if (notFound) { + return ( +
+
{t('board.noGroup')}
+
{t('board.noGroupDesc')}
+
+ ); + } + + if (columns.length === 0 || !fieldId) return null; + return ( + + { + // we have a transform: * on one of the parents of a then the positioning logic will be incorrect while dragging + // https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md + const id = columns[rubric.source.index].id; + + return ; + }} + > + {(provided) => { + return ( +
+ {columns.map((data, index) => ( + + {(provided) => { + return ( + + ); + }} + + ))} +
+ ); + }} +
+
+ ); +}; + +export default Group; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts new file mode 100644 index 0000000000..8401278d65 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/index.ts @@ -0,0 +1 @@ +export * from './Group'; diff --git a/frontend/appflowy_web_app/src/components/database/components/board/index.ts b/frontend/appflowy_web_app/src/components/database/components/board/index.ts new file mode 100644 index 0000000000..8a78f59377 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/board/index.ts @@ -0,0 +1 @@ +export * from './group'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts deleted file mode 100644 index ef44e2e745..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/cell.type.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CalculationType } from '@/application/database-yjs/database.type'; - -export interface CalulationCell { - value: string; - fieldId: string; - id: string; - type: CalculationType; -} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts index 1012dd4543..2e752f8f6e 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.hooks.ts @@ -26,7 +26,7 @@ export function useDateTypeCellDispatcher(fieldId: string) { const getDateTimeStr = useCallback( (timeStamp: string, includeTime?: boolean) => { - if (!typeOptionValue) return null; + if (!typeOptionValue || !timeStamp) return null; const timeFormat = getTimeFormat(typeOptionValue.timeFormat); const dateFormat = getDateFormat(typeOptionValue.dateFormat); const format = [dateFormat]; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx index ee3cde673b..e299c922c9 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/Cell.tsx @@ -1,31 +1,26 @@ -import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; +import { YjsDatabaseKey } from '@/application/collab.type'; import { FieldType } from '@/application/database-yjs/database.type'; import { useFieldSelector } from '@/application/database-yjs/selector'; -import RowCreateModifiedTime from '@/components/database/components/cell/RowCreateModifiedTime'; +import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified'; import React, { FC, useMemo } from 'react'; -import RichTextCell from '@/components/database/components/cell/TextCell'; -import UrlCell from '@/components/database/components/cell/UrlCell'; -import NumberCell from '@/components/database/components/cell/NumberCell'; -import CheckboxCell from '@/components/database/components/cell/CheckboxCell'; -import SelectCell from '@/components/database/components/cell/SelectionCell'; -import DateTimeCell from '@/components/database/components/cell/DateTimeCell'; -import ChecklistCell from '@/components/database/components/cell/ChecklistCell'; -import { Cell as CellValue } from '@/components/database/components/cell/cell.type'; -import RelationCell from '@/components/database/components/cell/RelationCell'; +import { TextCell } from '@/components/database/components/cell/text'; +import { UrlCell } from '@/components/database/components/cell/url'; +import { NumberCell } from '@/components/database/components/cell/number'; +import { CheckboxCell } from '@/components/database/components/cell/checkbox'; +import { SelectOptionCell } from '@/components/database/components/cell/select-option'; +import { DateTimeCell } from '@/components/database/components/cell/date'; +import { ChecklistCell } from '@/components/database/components/cell/checklist'; +import { CellProps, Cell as CellType } from '@/components/database/components/cell/cell.type'; +import { RelationCell } from '@/components/database/components/cell/relation'; -export interface CellProps { - rowId: string; - fieldId: FieldId; - cell?: CellValue; -} - -export function Cell({ cell, rowId, fieldId }: CellProps) { +export function Cell(props: CellProps) { + const { cell, rowId, fieldId, style } = props; const { field } = useFieldSelector(fieldId); const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; const Component = useMemo(() => { switch (fieldType) { case FieldType.RichText: - return RichTextCell; + return TextCell; case FieldType.URL: return UrlCell; case FieldType.Number: @@ -34,7 +29,7 @@ export function Cell({ cell, rowId, fieldId }: CellProps) { return CheckboxCell; case FieldType.SingleSelect: case FieldType.MultiSelect: - return SelectCell; + return SelectOptionCell; case FieldType.DateTime: return DateTimeCell; case FieldType.Checklist: @@ -42,21 +37,21 @@ export function Cell({ cell, rowId, fieldId }: CellProps) { case FieldType.Relation: return RelationCell; default: - return RichTextCell; + return TextCell; } - }, [fieldType]) as FC<{ cell?: CellValue; rowId: string; fieldId: FieldId }>; + }, [fieldType]) as FC>; if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; - return ; + return ; } - if (cell?.fieldType !== fieldType) { + if (cell && cell.fieldType !== fieldType) { return null; } - return ; + return ; } export default Cell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx deleted file mode 100644 index 558c424f62..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/CheckboxCell.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { FieldId } from '@/application/collab.type'; -import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; -import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; -import { CheckboxCell } from '@/components/database/components/cell/cell.type'; - -export default function ({ cell }: { cell?: CheckboxCell; rowId: string; fieldId: FieldId }) { - const checked = cell?.data; - - return ( -
- {checked ? : } -
- ); -} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx deleted file mode 100644 index f9c8749258..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/cell/TextCell.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { FieldId } from '@/application/collab.type'; -import { useReadOnly } from '@/application/database-yjs'; -import { TextCell } from '@/components/database/components/cell/cell.type'; -import React from 'react'; - -function TextCellComponent({ cell }: { cell?: TextCell; rowId: string; fieldId: FieldId }) { - const readOnly = useReadOnly(); - - return
{cell?.data}
; -} - -export default TextCellComponent; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts index 185cca9409..bd13ef29d0 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts @@ -1,6 +1,7 @@ -import { RowId } from '@/application/collab.type'; -import { DateFormat, SelectOption, TimeFormat } from '@/application/database-yjs'; +import { FieldId, RowId } from '@/application/collab.type'; +import { DateFormat, TimeFormat } from '@/application/database-yjs'; import { FieldType } from '@/application/database-yjs/database.type'; +import React from 'react'; import { YArray } from 'yjs/dist/src/types/YArray'; export interface Cell { @@ -32,7 +33,7 @@ export interface UrlCell extends Cell { export type SelectionId = string; -export interface SelectCell extends Cell { +export interface SelectOptionCell extends Cell { fieldType: FieldType.SingleSelect | FieldType.MultiSelect; data: SelectionId; } @@ -51,11 +52,6 @@ export interface DateTimeCell extends Cell { reminderId?: string; } -export interface TimeStampCell extends Cell { - fieldType: FieldType.LastEditedTime | FieldType.CreatedTime; - data: TimestampCellData; -} - export interface DateTimeCellData { date?: string; time?: string; @@ -67,11 +63,6 @@ export interface DateTimeCellData { isRange?: boolean; } -export interface TimestampCellData { - dataTime?: string; - timestamp?: number; -} - export interface ChecklistCell extends Cell { fieldType: FieldType.Checklist; data: string; @@ -84,7 +75,10 @@ export interface RelationCell extends Cell { export type RelationCellData = RowId[]; -export interface ChecklistCellData { - selected_option_ids?: string[]; - options?: SelectOption[]; +export interface CellProps { + cell?: T; + rowId: string; + fieldId: FieldId; + style?: React.CSSProperties; + readOnly?: boolean; } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx new file mode 100644 index 0000000000..cc665474ec --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/CheckboxCell.tsx @@ -0,0 +1,13 @@ +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; +import { CellProps, CheckboxCell as CheckboxCellType } from '@/components/database/components/cell/cell.type'; + +export function CheckboxCell({ cell, style }: CellProps) { + const checked = cell?.data; + + return ( +
+ {checked ? : } +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts new file mode 100644 index 0000000000..f1cb1ac4bf --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checkbox/index.ts @@ -0,0 +1 @@ +export * from './CheckboxCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx similarity index 68% rename from frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx index 32d97d758f..3eaa8254a4 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/ChecklistCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx @@ -1,10 +1,9 @@ -import { FieldId } from '@/application/collab.type'; import { parseChecklistData } from '@/application/database-yjs'; -import { ChecklistCell } from '@/components/database/components/cell/cell.type'; +import { CellProps, ChecklistCell as ChecklistCellType } from '@/components/database/components/cell/cell.type'; import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel'; import React, { useMemo } from 'react'; -export default function ({ cell }: { cell?: ChecklistCell; rowId: string; fieldId: FieldId }) { +export function ChecklistCell({ cell, style }: CellProps) { const data = useMemo(() => { return parseChecklistData(cell?.data ?? ''); }, [cell?.data]); @@ -14,7 +13,7 @@ export default function ({ cell }: { cell?: ChecklistCell; rowId: string; fieldI if (!data || !options || !selectedOptions) return null; return ( -
+
); diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts new file mode 100644 index 0000000000..b12d47b6c5 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/index.ts @@ -0,0 +1 @@ +export * from './ChecklistCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx similarity index 88% rename from frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx index d685b53cf9..7716ba1552 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/RowCreateModifiedTime.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx @@ -3,13 +3,15 @@ import { useRowMeta } from '@/application/database-yjs'; import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; import React, { useEffect, useMemo, useState } from 'react'; -function RowCreateModifiedTime({ +export function RowCreateModifiedTime({ rowId, fieldId, attrName, + style, }: { rowId: string; fieldId: string; + style?: React.CSSProperties; attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at; }) { const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); @@ -37,7 +39,8 @@ function RowCreateModifiedTime({ return getDateTimeStr(value, false); }, [value, getDateTimeStr]); - return
{time}
; + if (!time) return null; + return
{time}
; } export default RowCreateModifiedTime; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts new file mode 100644 index 0000000000..ed951f3521 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/index.ts @@ -0,0 +1 @@ +export * from './RowCreateModifiedTime'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx similarity index 75% rename from frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx index 490a2bd95e..bc90a9fa7a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/DateTimeCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx @@ -1,10 +1,9 @@ -import { FieldId } from '@/application/collab.type'; import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; -import { DateTimeCell } from '@/components/database/components/cell/cell.type'; +import { CellProps, DateTimeCell as DateTimeCellType } from '@/components/database/components/cell/cell.type'; import React, { useMemo } from 'react'; import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg'; -export default function ({ cell, fieldId }: { cell?: DateTimeCell; rowId: string; fieldId: FieldId }) { +export function DateTimeCell({ cell, fieldId, style }: CellProps) { const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); const startDateTime = useMemo(() => { @@ -26,8 +25,9 @@ export default function ({ cell, fieldId }: { cell?: DateTimeCell; rowId: string const hasReminder = !!cell?.reminderId; + if (!cell?.data) return null; return ( -
+
{hasReminder && } {dateStr}
diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts new file mode 100644 index 0000000000..e05bb1674a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/index.ts @@ -0,0 +1 @@ +export * from './DateTimeCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx similarity index 69% rename from frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx index 851e14a34e..4d6ce6d44a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/NumberCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx @@ -1,10 +1,9 @@ -import { FieldId } from '@/application/collab.type'; import { currencyFormaterMap, NumberFormat, useFieldSelector, parseNumberTypeOptions } from '@/application/database-yjs'; -import { UrlCell } from '@/components/database/components/cell/cell.type'; +import { CellProps, NumberCell as NumberCellType } from '@/components/database/components/cell/cell.type'; import React, { useMemo } from 'react'; import Decimal from 'decimal.js'; -export default function ({ cell, fieldId }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) { +export function NumberCell({ cell, fieldId, style }: CellProps) { const { field } = useFieldSelector(fieldId); const format = useMemo(() => (field ? parseNumberTypeOptions(field).format : NumberFormat.Num), [field]); @@ -23,5 +22,10 @@ export default function ({ cell, fieldId }: { cell?: UrlCell; rowId: string; fie return numberFormater(new Decimal(cell.data).toNumber()); }, [cell, format]); - return
{value}
; + if (value === undefined) return null; + return ( +
+ {value} +
+ ); } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts new file mode 100644 index 0000000000..3e1686c783 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/index.ts @@ -0,0 +1 @@ +export * from './NumberCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx new file mode 100644 index 0000000000..3545bc026b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx @@ -0,0 +1,7 @@ +import { CellProps, RelationCell as RelationCellType } from '@/components/database/components/cell/cell.type'; +import RelationItems from '@/components/database/components/cell/relation/RelationItems'; + +export function RelationCell({ cell, fieldId, style }: CellProps) { + if (!cell?.data) return null; + return ; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx similarity index 58% rename from frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx index 56c1e8d27b..a91dfe57af 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/RelationCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx @@ -1,24 +1,16 @@ -import { - FieldId, - YDatabaseField, - YDatabaseFields, - YDatabaseRow, - YDoc, - YjsDatabaseKey, - YjsEditorKey, -} from '@/application/collab.type'; -import { useFieldSelector, parseRelationTypeOption } from '@/application/database-yjs'; +import { YDatabaseField, YDatabaseFields, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { parseRelationTypeOption, useFieldSelector } from '@/application/database-yjs'; import { useId } from '@/components/_shared/context-provider/IdProvider'; import { AFConfigContext } from '@/components/app/AppConfig'; -import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; import { RelationCell, RelationCellData } from '@/components/database/components/cell/cell.type'; +import { RelationPrimaryValue } from '@/components/database/components/cell/relation/RelationPrimaryValue'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import * as Y from 'yjs'; -export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: string; rowId: string }) { +function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: string; style?: React.CSSProperties }) { const { field } = useFieldSelector(fieldId); const workspaceId = useId()?.workspaceId; - const rowIds = useMemo(() => (cell?.data.toJSON() as RelationCellData) ?? [], [cell?.data]); + const rowIds = useMemo(() => (cell.data.toJSON() as RelationCellData) ?? [], [cell.data]); const databaseId = rowIds.length > 0 && field ? parseRelationTypeOption(field).database_id : undefined; const databaseService = useContext(AFConfigContext)?.service?.databaseService; const [databasePrimaryFieldId, setDatabasePrimaryFieldId] = useState(undefined); @@ -43,7 +35,7 @@ export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: stri }, [workspaceId, databaseId, databaseService]); return ( -
+
{rowIds.map((rowId) => { const rowDoc = rows?.get(rowId); @@ -59,26 +51,4 @@ export default function ({ cell, fieldId }: { cell?: RelationCell; fieldId: stri ); } -function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) { - const [text, setText] = useState(null); - - useEffect(() => { - const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; - const cells = row.get(YjsDatabaseKey.cells); - const primaryCell = cells.get(fieldId); - - if (!primaryCell) return; - const observeHandler = () => { - setText(parseYDatabaseCellToCell(primaryCell).data as string); - }; - - observeHandler(); - - primaryCell.observe(observeHandler); - return () => { - primaryCell.unobserve(observeHandler); - }; - }, [rowDoc, fieldId]); - - return
{text}
; -} +export default RelationItems; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx new file mode 100644 index 0000000000..174a3693f0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationPrimaryValue.tsx @@ -0,0 +1,27 @@ +import { FieldId, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; +import React, { useEffect, useState } from 'react'; + +export function RelationPrimaryValue({ rowDoc, fieldId }: { rowDoc: YDoc; fieldId: FieldId }) { + const [text, setText] = useState(null); + + useEffect(() => { + const row = rowDoc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + const cells = row.get(YjsDatabaseKey.cells); + const primaryCell = cells.get(fieldId); + + if (!primaryCell) return; + const observeHandler = () => { + setText(parseYDatabaseCellToCell(primaryCell).data as string); + }; + + observeHandler(); + + primaryCell.observe(observeHandler); + return () => { + primaryCell.unobserve(observeHandler); + }; + }, [rowDoc, fieldId]); + + return
{text}
; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts new file mode 100644 index 0000000000..95a0aa3668 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/index.ts @@ -0,0 +1 @@ +export * from './RelationCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx similarity index 68% rename from frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx index a915d31a9b..bd7038a8a7 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/SelectionCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx @@ -1,11 +1,10 @@ -import { FieldId } from '@/application/collab.type'; import { useFieldSelector, parseSelectOptionTypeOptions } from '@/application/database-yjs'; import { Tag } from '@/components/_shared/tag'; import { SelectOptionColorMap } from '@/components/database/components/cell/cell.const'; -import { SelectCell } from '@/components/database/components/cell/cell.type'; +import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/components/database/components/cell/cell.type'; import React, { useCallback, useMemo } from 'react'; -export default function ({ cell, fieldId }: { cell?: SelectCell; rowId: string; fieldId: FieldId }) { +export function SelectOptionCell({ cell, fieldId, style }: CellProps) { const selectOptionIds = useMemo(() => cell?.data.split(','), [cell]); const { field } = useFieldSelector(fieldId); const typeOption = useMemo(() => { @@ -24,9 +23,11 @@ export default function ({ cell, fieldId }: { cell?: SelectCell; rowId: string; [typeOption] ); + if (!typeOption || !selectOptionIds?.length) return null; + return ( -
- {selectOptionIds ? renderSelectedOptions(selectOptionIds) : null} +
+ {renderSelectedOptions(selectOptionIds)}
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts new file mode 100644 index 0000000000..40df2f3d7d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/index.ts @@ -0,0 +1 @@ +export * from './SelectOptionCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx new file mode 100644 index 0000000000..e27f1e835f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx @@ -0,0 +1,14 @@ +import { useReadOnly } from '@/application/database-yjs'; +import { CellProps, TextCell as TextCellType } from '@/components/database/components/cell/cell.type'; +import React from 'react'; + +export function TextCell({ cell, style }: CellProps) { + const readOnly = useReadOnly(); + + if (!cell?.data) return null; + return ( +
+ {cell?.data} +
+ ); +} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts new file mode 100644 index 0000000000..64bcb41a7f --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/index.ts @@ -0,0 +1 @@ +export * from './TextCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx similarity index 77% rename from frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx index e2d3d2c87f..97de0c0fdb 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/UrlCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx @@ -1,10 +1,9 @@ -import { FieldId } from '@/application/collab.type'; import { useReadOnly } from '@/application/database-yjs'; -import { UrlCell } from '@/components/database/components/cell/cell.type'; +import { CellProps, UrlCell as UrlCellType } from '@/components/database/components/cell/cell.type'; import { openUrl, processUrl } from '@/utils/url'; import React, { useMemo } from 'react'; -export default function ({ cell }: { cell?: UrlCell; rowId: string; fieldId: FieldId }) { +export function UrlCell({ cell, style }: CellProps) { const readOnly = useReadOnly(); const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]); @@ -21,8 +20,11 @@ export default function ({ cell }: { cell?: UrlCell; rowId: string; fieldId: Fie return classList.join(' '); }, [isUrl]); + if (!cell?.data) return null; + return (
{ if (!isUrl || !cell) return; if (readOnly) { diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts new file mode 100644 index 0000000000..9f45924c97 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/index.ts @@ -0,0 +1 @@ +export * from './UrlCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts b/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts new file mode 100644 index 0000000000..d4d7020523 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/useMeasure.ts @@ -0,0 +1,53 @@ +import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs'; +import { useCallback, useRef } from 'react'; + +export function useMeasureHeight({ + forceUpdate, + rows, +}: { + forceUpdate: (index: number) => void; + rows: { + rowId?: string; + }[]; +}) { + const heightRef = useRef<{ [rowId: string]: number }>({}); + const rowHeight = useCallback( + (index: number) => { + const row = rows[index]; + + if (!row || !row.rowId) return DEFAULT_ROW_HEIGHT; + + return heightRef.current[row.rowId] || DEFAULT_ROW_HEIGHT; + }, + [rows] + ); + + const setRowHeight = useCallback( + (index: number, height: number) => { + const row = rows[index]; + const rowId = row.rowId; + + if (!row || !rowId) return; + const oldHeight = heightRef.current[rowId]; + + heightRef.current[rowId] = Math.max(oldHeight || DEFAULT_ROW_HEIGHT, height); + + if (oldHeight !== height) { + forceUpdate(index); + } + }, + [forceUpdate, rows] + ); + + const onResize = useCallback( + (rowIndex: number, columnIndex: number, size: { width: number; height: number }) => { + setRowHeight(rowIndex, size.height); + }, + [setRowHeight] + ); + + return { + rowHeight, + onResize, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts deleted file mode 100644 index 6de83c7026..0000000000 --- a/frontend/appflowy_web_app/src/components/database/components/grid-column/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './GridColumn'; -export * from './useRenderColumns'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx similarity index 83% rename from frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx index eeefee18bb..1ddb4e2d32 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/CalculationCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/CalculationCell.tsx @@ -1,11 +1,21 @@ import { CalculationType } from '@/application/database-yjs/database.type'; -import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -export function CalculationCell({ cell }: { cell?: CalulationCell }) { +export interface ICalculationCell { + value: string; + fieldId: string; + id: string; + type: CalculationType; +} + +export interface CalculationCellProps { + cell?: ICalculationCell; +} + +export function CalculationCell({ cell }: CalculationCellProps) { const { t } = useTranslation(); - + const prefix = useMemo(() => { if (!cell) return ''; diff --git a/frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/calculation-cell/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-calculation-cell/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx similarity index 57% rename from frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx index b9a5017b38..ce47153c70 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-cell/GridCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx @@ -1,9 +1,8 @@ -import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; -import { useRowMeta } from '@/application/database-yjs'; +import { FieldId } from '@/application/collab.type'; +import { useCellSelector } from '@/application/database-yjs'; import { useFieldSelector } from '@/application/database-yjs/selector'; import { Cell } from '@/components/database/components/cell'; -import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; export interface GridCellProps { rowId: string; @@ -16,21 +15,10 @@ export interface GridCellProps { export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) { const ref = React.useRef(null); const field = useFieldSelector(fieldId); - const row = useRowMeta(rowId); - const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId); - const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined)); - - useEffect(() => { - if (!cell) return; - setCellValue(parseYDatabaseCellToCell(cell)); - const observerEvent = () => setCellValue(parseYDatabaseCellToCell(cell)); - - cell.observe(observerEvent); - - return () => { - cell.unobserve(observerEvent); - }; - }, [cell]); + const cell = useCellSelector({ + rowId, + fieldId, + }); useEffect(() => { const el = ref.current; @@ -56,7 +44,7 @@ export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: Gr if (!field) return null; return (
- +
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-cell/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-column/GridColumn.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-column/GridColumn.tsx diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts new file mode 100644 index 0000000000..3c71a6b899 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/index.ts @@ -0,0 +1,2 @@ +export * from './GridColumn'; +export * from 'src/components/database/components/grid/grid-column/useRenderFields'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx similarity index 75% rename from frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx index c0041b5c5e..2e5c42e93a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-column/useRenderColumns.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx @@ -1,6 +1,6 @@ import { FieldId } from '@/application/collab.type'; import { FieldVisibility } from '@/application/database-yjs/database.type'; -import { useGridColumnsSelector } from '@/application/database-yjs/selector'; +import { useFieldsSelector } from '@/application/database-yjs/selector'; import { useCallback, useMemo } from 'react'; export enum GridColumnType { @@ -9,8 +9,6 @@ export enum GridColumnType { NewProperty, } -const defaultVisibilitys = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmpty]; - export type RenderColumn = { type: GridColumnType; visibility?: FieldVisibility; @@ -19,12 +17,12 @@ export type RenderColumn = { wrap?: boolean; }; -export function useRenderColumns(viewId: string) { - const columns = useGridColumnsSelector(viewId, defaultVisibilitys); +export function useRenderFields() { + const fields = useFieldsSelector(); - console.log('columns', columns); + console.log('columns', fields); const renderColumns = useMemo(() => { - const fields = columns.map((column) => ({ + const data = fields.map((column) => ({ ...column, type: GridColumnType.Field, })); @@ -34,7 +32,7 @@ export function useRenderColumns(viewId: string) { type: GridColumnType.Action, width: 96, }, - ...fields, + ...data, { type: GridColumnType.NewProperty, width: 150, @@ -44,7 +42,7 @@ export function useRenderColumns(viewId: string) { width: 96, }, ].filter(Boolean) as RenderColumn[]; - }, [columns]); + }, [fields]); const columnWidth = useCallback( (index: number, containerWidth: number) => { @@ -67,7 +65,7 @@ export function useRenderColumns(viewId: string) { ); return { - columns: renderColumns, + fields: renderColumns, columnWidth, }; } diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-header/GridHeader.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-header/GridHeader.tsx diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-header/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-header/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx similarity index 83% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx index 650ed3bfbe..4d7abb7a2c 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridCalculateRowCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridCalculateRowCell.tsx @@ -1,8 +1,7 @@ import { YjsDatabaseKey } from '@/application/collab.type'; import { useDatabaseView } from '@/application/database-yjs'; import { CalculationType } from '@/application/database-yjs/database.type'; -import { CalculationCell } from '@/components/database/components/calculation-cell'; -import { CalulationCell } from '@/components/database/components/calculation-cell/cell.type'; +import { CalculationCell, ICalculationCell } from '../grid-calculation-cell'; import React, { useEffect, useState } from 'react'; export interface GridCalculateRowCellProps { @@ -11,7 +10,7 @@ export interface GridCalculateRowCellProps { export function GridCalculateRowCell({ fieldId }: GridCalculateRowCellProps) { const calculations = useDatabaseView()?.get(YjsDatabaseKey.calculations); - const [calculation, setCalculation] = useState(); + const [calculation, setCalculation] = useState(); useEffect(() => { if (!calculations) return; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx similarity index 81% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx index ef4be68406..11f14135e3 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-row/GridRowCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/GridRowCell.tsx @@ -1,6 +1,6 @@ -import { GridColumnType } from '@/components/database/components/grid-column'; +import { GridColumnType } from '../grid-column'; import React from 'react'; -import GridCell from 'src/components/database/components/grid-cell/GridCell'; +import GridCell from '../grid-cell/GridCell'; export interface GridRowCellProps { rowId: string; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx similarity index 76% rename from frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx index e5038cafff..8b2e6597b8 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-row/useRenderRows.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-row/useRenderRows.tsx @@ -1,6 +1,5 @@ -import { useReadOnly } from '@/application/database-yjs'; -import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const'; -import { useGridRowsSelector } from '@/application/database-yjs/selector'; +import { DEFAULT_ROW_HEIGHT, useReadOnly, useRowsSelector } from '@/application/database-yjs'; + import { useMemo } from 'react'; export enum RenderRowType { @@ -16,7 +15,7 @@ export type RenderRow = { }; export function useRenderRows() { - const rows = useGridRowsSelector(); + const rows = useRowsSelector(); const readOnly = useReadOnly(); const renderRows = useMemo(() => { diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx similarity index 95% rename from frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx index dd3ed13bfe..b855c8b4cb 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid-table/GridTable.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx @@ -1,12 +1,7 @@ import { DEFAULT_ROW_HEIGHT } from '@/application/database-yjs/const'; import { AFScroller } from '@/components/_shared/scroller'; -import { GridColumnType, RenderColumn } from '@/components/database/components/grid-column'; -import { - GridCalculateRowCell, - GridRowCell, - RenderRowType, - useRenderRows, -} from '@/components/database/components/grid-row'; +import { GridColumnType, RenderColumn } from '../grid-column'; +import { GridCalculateRowCell, GridRowCell, RenderRowType, useRenderRows } from '../grid-row'; import React, { useCallback, useEffect, useRef } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GridChildComponentProps, VariableSizeGrid } from 'react-window'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/database/components/grid-table/index.ts rename to frontend/appflowy_web_app/src/components/database/components/grid/grid-table/index.ts diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/index.ts b/frontend/appflowy_web_app/src/components/database/components/grid/index.ts new file mode 100644 index 0000000000..2e9a6988f4 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/grid/index.ts @@ -0,0 +1,3 @@ +export * from './grid-table'; +export * from './grid-header'; +export * from './grid-column'; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx index 65a1b238bb..e7d78e7033 100644 --- a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -52,7 +52,10 @@ export const DatabaseTabs = forwardRef( if (viewIds.length === 0) return null; return ( -
+
- +
- + ); } diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss index bac3baae69..fceeab367c 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -45,7 +45,7 @@ opacity: 60%; } -.workspaces, .database-conditions, .grid-scroll-table { +.workspaces, .database-conditions, .grid-scroll-table, .grid-board { ::-webkit-scrollbar { width: 0; height: 0; diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 1b286e2498..ca0a190367 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1252,7 +1252,9 @@ "showGroup": "Show group", "showGroupContent": "Are you sure you want to show this group on the board?", "failedToLoad": "Failed to load board view" - } + }, + "noGroup": "No group by property", + "noGroupDesc": "Board views require a property to group by in order to display" }, "calendar": { "menuName": "Calendar", From b2978e0d6c5d7220bc715d412e58716f0b76aa3e Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Tue, 21 May 2024 11:34:36 +0200 Subject: [PATCH 19/30] fix: launch review 0.5.8 (#5367) --- .../document/document_more_actions_test.dart | 32 +++ .../document/document_test_runner.dart | 6 +- .../shared/common_operations.dart | 49 +++- .../setting/font/font_picker_screen.dart | 6 +- .../lib/plugins/base/emoji/emoji_picker.dart | 5 +- .../lib/user/application/user_listener.dart | 14 +- .../lib/util/theme_extension.dart | 5 + .../settings/date_time/time_format_ext.dart | 4 +- .../workspace/workspace_settings_bloc.dart | 8 +- .../workspace/_sidebar_workspace_icon.dart | 1 + .../settings/pages/settings_account_view.dart | 81 +++--- .../pages/settings_manage_data_view.dart | 10 +- .../pages/settings_workspace_view.dart | 240 ++++++++++++------ .../shared/af_dropdown_menu_entry.dart | 14 +- .../settings/shared/settings_dropdown.dart | 12 + .../members/workspace_member_page.dart | 3 - .../24x/settings_selected_theme.svg | 10 + frontend/resources/translations/en.json | 8 +- .../rust-lib/flowy-folder/src/notification.rs | 2 +- 19 files changed, 365 insertions(+), 145 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart create mode 100644 frontend/appflowy_flutter/lib/util/theme_extension.dart create mode 100644 frontend/resources/flowy_icons/24x/settings_selected_theme.svg diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart new file mode 100644 index 0000000000..646a4eb565 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart @@ -0,0 +1,32 @@ +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('MoreViewActions', () { + testWidgets('can duplicate and delete from menu', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapAnonymousSignInButton(); + await tester.pumpAndSettle(); + + final pageFinder = find.byType(ViewItem); + expect(pageFinder, findsNWidgets(1)); + + // Duplicate + await tester.openMoreViewActions(); + await tester.duplicateByMoreViewActions(); + + expect(pageFinder, findsNWidgets(2)); + + // Delete + await tester.openMoreViewActions(); + await tester.deleteByMoreViewActions(); + + expect(pageFinder, findsNWidgets(1)); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart index 42462c2658..239e7e09a8 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart @@ -6,6 +6,9 @@ import 'document_copy_and_paste_test.dart' as document_copy_and_paste_test; import 'document_create_and_delete_test.dart' as document_create_and_delete_test; import 'document_option_action_test.dart' as document_option_action_test; +import 'document_inline_page_reference_test.dart' + as document_inline_page_reference_test; +import 'document_more_actions_test.dart' as document_more_actions_test; import 'document_text_direction_test.dart' as document_text_direction_test; import 'document_with_cover_image_test.dart' as document_with_cover_image_test; import 'document_with_database_test.dart' as document_with_database_test; @@ -16,8 +19,6 @@ import 'document_with_inline_page_test.dart' as document_with_inline_page_test; import 'document_with_outline_block_test.dart' as document_with_outline_block; import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test; import 'edit_document_test.dart' as document_edit_test; -import 'document_inline_page_reference_test.dart' - as document_inline_page_reference_test; void startTesting() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -38,4 +39,5 @@ void startTesting() { document_option_action_test.main(); document_with_image_block_test.main(); document_inline_page_reference_test.main(); + document_more_actions_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index 08d6fd0ec6..a2a6318c3d 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -1,5 +1,10 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -22,15 +27,13 @@ import 'package:appflowy/workspace/presentation/notifications/widgets/flowy_tab. import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_tab_bar.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; +import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart'; +import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart'; import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'emoji.dart'; @@ -564,6 +567,44 @@ extension CommonOperations on WidgetTester { ); await tapButton(button); } + + Future openMoreViewActions() async { + final button = find.byType(MoreViewActions); + await tap(button); + await pumpAndSettle(); + } + + /// Presses on the Duplicate ViewAction in the [MoreViewActions] popup. + /// + /// [openMoreViewActions] must be called beforehand! + /// + Future duplicateByMoreViewActions() async { + final button = find.descendant( + of: find.byType(ListView), + matching: find.byWidgetPredicate( + (widget) => + widget is ViewAction && widget.type == ViewActionType.duplicate, + ), + ); + await tap(button); + await pump(); + } + + /// Presses on the Delete ViewAction in the [MoreViewActions] popup. + /// + /// [openMoreViewActions] must be called beforehand! + /// + Future deleteByMoreViewActions() async { + final button = find.descendant( + of: find.byType(ListView), + matching: find.byWidgetPredicate( + (widget) => + widget is ViewAction && widget.type == ViewActionType.delete, + ), + ); + await tap(button); + await pump(); + } } extension SettingsFinder on CommonFinders { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart index 64dd62729c..390f0824de 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_picker_screen.dart @@ -29,9 +29,7 @@ class FontPickerScreen extends StatelessWidget { } class LanguagePickerPage extends StatefulWidget { - const LanguagePickerPage({ - super.key, - }); + const LanguagePickerPage({super.key}); @override State createState() => _LanguagePickerPageState(); @@ -43,7 +41,6 @@ class _LanguagePickerPageState extends State { @override void initState() { super.initState(); - availableFonts = _availableFonts; } @@ -90,7 +87,6 @@ class _FontSelectorState extends State { @override void initState() { super.initState(); - availableFonts = _availableFonts; } diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart index 1297bccc37..86c3b1e625 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart'; import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart'; import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; @@ -83,7 +84,9 @@ class _FlowyEmojiPickerState extends State { }, itemBuilder: (context, emojiId, emoji, callback) { return FlowyIconButton( - iconPadding: const EdgeInsets.all(2.0), + iconPadding: PlatformExtension.isWindows + ? const EdgeInsets.only(bottom: 2.0) + : const EdgeInsets.all(2), icon: FlowyText( emoji, fontSize: 28.0, diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index b3e7d6fbe0..81a081b4e3 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -113,7 +113,7 @@ typedef WorkspaceSettingNotifyValue class UserWorkspaceListener { UserWorkspaceListener(); - PublishNotifier? _settingChangedNotifier = + final PublishNotifier _settingChangedNotifier = PublishNotifier(); FolderNotificationListener? _listener; @@ -122,7 +122,7 @@ class UserWorkspaceListener { void Function(WorkspaceSettingNotifyValue)? onSettingUpdated, }) { if (onSettingUpdated != null) { - _settingChangedNotifier?.addPublishListener(onSettingUpdated); + _settingChangedNotifier.addPublishListener(onSettingUpdated); } // The "current-workspace" is predefined in the backend. Do not try to @@ -140,13 +140,11 @@ class UserWorkspaceListener { switch (ty) { case FolderNotification.DidUpdateWorkspaceSetting: result.fold( - (payload) => _settingChangedNotifier?.value = + (payload) => _settingChangedNotifier.value = FlowyResult.success(WorkspaceSettingPB.fromBuffer(payload)), - (error) => - _settingChangedNotifier?.value = FlowyResult.failure(error), + (error) => _settingChangedNotifier.value = FlowyResult.failure(error), ); break; - default: break; } @@ -154,8 +152,6 @@ class UserWorkspaceListener { Future stop() async { await _listener?.stop(); - - _settingChangedNotifier?.dispose(); - _settingChangedNotifier = null; + _settingChangedNotifier.dispose(); } } diff --git a/frontend/appflowy_flutter/lib/util/theme_extension.dart b/frontend/appflowy_flutter/lib/util/theme_extension.dart new file mode 100644 index 0000000000..c7b56699d3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/theme_extension.dart @@ -0,0 +1,5 @@ +import 'package:flutter/material.dart'; + +extension IsLightMode on ThemeData { + bool get isLightMode => brightness == Brightness.light; +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart index 5da3caa5b9..0dfa2807b9 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/date_time/time_format_ext.dart @@ -8,6 +8,6 @@ extension TimeFormatter on UserTimeFormatPB { } final _toFormat = { - UserTimeFormatPB.TwelveHour: DateFormat.Hm(), - UserTimeFormatPB.TwentyFourHour: DateFormat.jm(), + UserTimeFormatPB.TwentyFourHour: DateFormat.Hm(), + UserTimeFormatPB.TwelveHour: DateFormat.jm(), }; diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart index 5c02ea6b11..d7980e031a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/workspace/workspace_settings_bloc.dart @@ -56,7 +56,13 @@ class WorkspaceSettingsBloc ?.role ?? AFRolePB.Guest; - emit(state.copyWith(members: members, myRole: role)); + emit( + state.copyWith( + workspace: currentWorkspaceInList, + members: members, + myRole: role, + ), + ); } catch (e) { Log.error('Failed to get or create current workspace'); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index f3f78ee99b..101c12c3a6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -38,6 +38,7 @@ class _WorkspaceIconState extends State { child: EmojiText( emoji: widget.workspace.icon, fontSize: widget.iconSize, + lineHeight: 1, ), ) : Container( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index 9ee584e5dc..ebbb4f0e07 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; @@ -78,46 +79,46 @@ class _SettingsAccountViewState extends State { ], ), - // Enable when/if we need change email feature - // // Only show change email if the user is authenticated and not using local auth - // if (isAuthEnabled && - // state.userProfile.authenticator != AuthenticatorPB.Local) ...[ - // const SettingsCategorySpacer(), - // SettingsCategory( - // title: LocaleKeys.settings_accountPage_email_title.tr(), - // children: [ - // SingleSettingAction( - // label: state.userProfile.email, - // buttonLabel: LocaleKeys - // .settings_accountPage_email_actions_change - // .tr(), - // onPressed: () => SettingsAlertDialog( - // title: LocaleKeys - // .settings_accountPage_email_actions_change - // .tr(), - // confirmLabel: LocaleKeys.button_save.tr(), - // confirm: () { - // context.read().add( - // SettingsUserEvent.updateUserEmail( - // _emailController.text, - // ), - // ); - // Navigator.of(context).pop(); - // }, - // children: [ - // SettingsInputField( - // label: LocaleKeys.settings_accountPage_email_title - // .tr(), - // value: state.userProfile.email, - // hideActions: true, - // textController: _emailController, - // ), - // ], - // ).show(context), - // ), - // ], - // ), - // ], + // Only show email if the user is authenticated and not using local auth + if (isAuthEnabled && + state.userProfile.authenticator != AuthenticatorPB.Local) ...[ + SettingsCategory( + title: LocaleKeys.settings_accountPage_email_title.tr(), + children: [ + FlowyText.regular(state.userProfile.email), + // Enable when/if we need change email feature + // SingleSettingAction( + // label: state.userProfile.email, + // buttonLabel: LocaleKeys + // .settings_accountPage_email_actions_change + // .tr(), + // onPressed: () => SettingsAlertDialog( + // title: LocaleKeys + // .settings_accountPage_email_actions_change + // .tr(), + // confirmLabel: LocaleKeys.button_save.tr(), + // confirm: () { + // context.read().add( + // SettingsUserEvent.updateUserEmail( + // _emailController.text, + // ), + // ); + // Navigator.of(context).pop(); + // }, + // children: [ + // SettingsInputField( + // label: LocaleKeys.settings_accountPage_email_title + // .tr(), + // value: state.userProfile.email, + // hideActions: true, + // textController: _emailController, + // ), + // ], + // ).show(context), + // ), + ], + ), + ], /// Enable when we have change password feature and 2FA // const SettingsCategorySpacer(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index 17d76b4fe1..4ad5d00e1f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -9,6 +9,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/appflowy_cache_manager.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/tasks/rust_sdk.dart'; +import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/settings/setting_file_importer_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_location_cubit.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; @@ -55,6 +56,9 @@ class SettingsManageDataView extends StatelessWidget { actions: [ if (state.mapOrNull(didReceivedPath: (_) => true) == true) SettingAction( + tooltip: LocaleKeys + .settings_manageDataPage_dataStorage_actions_resetTooltip + .tr(), icon: const FlowySvg(FlowySvgs.restore_s), label: LocaleKeys.settings_common_reset.tr(), onPressed: () => SettingsAlertDialog( @@ -375,6 +379,8 @@ class _CurrentPathState extends State<_CurrentPath> { @override Widget build(BuildContext context) { + final isLM = Theme.of(context).isLightMode; + return Column( children: [ Row( @@ -392,7 +398,9 @@ class _CurrentPathState extends State<_CurrentPath> { maxLines: 2, overflow: TextOverflow.ellipsis, decoration: isHovering ? TextDecoration.underline : null, - color: const Color(0xFF005483), + color: isLM + ? const Color(0xFF005483) + : Theme.of(context).colorScheme.primary, ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index c1d167844a..12a932aabc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -18,12 +19,12 @@ import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu import 'package:appflowy/workspace/presentation/settings/shared/document_color_setting_button.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_action.dart'; import 'package:appflowy/workspace/presentation/settings/shared/setting_list_tile.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_actionable_input.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_category.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_dashed_divider.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_input_field.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart'; import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart'; @@ -41,39 +42,22 @@ import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; -class SettingsWorkspaceView extends StatefulWidget { +class SettingsWorkspaceView extends StatelessWidget { const SettingsWorkspaceView({super.key, required this.userProfile}); final UserProfilePB userProfile; - @override - State createState() => _SettingsWorkspaceViewState(); -} - -class _SettingsWorkspaceViewState extends State { - final TextEditingController _workspaceNameController = - TextEditingController(); - - @override - void dispose() { - _workspaceNameController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { return BlocProvider( create: (context) => WorkspaceSettingsBloc() - ..add(WorkspaceSettingsEvent.initial(userProfile: widget.userProfile)), + ..add(WorkspaceSettingsEvent.initial(userProfile: userProfile)), child: BlocConsumer( listener: (context, state) { - if ((state.workspace?.name ?? '') != _workspaceNameController.text) { - _workspaceNameController.text = state.workspace?.name ?? ''; - } - if (state.deleteWorkspace) { context.read().add( UserWorkspaceEvent.deleteWorkspace( @@ -97,44 +81,11 @@ class _SettingsWorkspaceViewState extends State { description: LocaleKeys.settings_workspacePage_description.tr(), children: [ // We don't allow changing workspace name/icon for local/offline - if (state.workspace != null && - widget.userProfile.authenticator != - AuthenticatorPB.Local) ...[ + if (userProfile.authenticator != AuthenticatorPB.Local) ...[ SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceName_title .tr(), - children: [ - SettingsActionableInput( - controller: _workspaceNameController, - onSave: (value) => _saveWorkspaceName( - context, - current: state.workspace!.name, - name: value, - ), - actions: [ - SizedBox( - height: 48, - child: FlowyTextButton( - LocaleKeys.button_save.tr(), - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - fontWeight: FontWeight.w600, - radius: BorderRadius.circular(12), - fillColor: Theme.of(context).colorScheme.primary, - hoverColor: const Color(0xFF005483), - fontHoverColor: Colors.white, - onPressed: () => _saveWorkspaceName( - context, - current: state.workspace!.name, - name: _workspaceNameController.text, - ), - ), - ), - ], - ), - ], + children: const [_WorkspaceNameSetting()], ), SettingsCategory( title: LocaleKeys.settings_workspacePage_workspaceIcon_title @@ -143,7 +94,10 @@ class _SettingsWorkspaceViewState extends State { .settings_workspacePage_workspaceIcon_description .tr(), children: [ - _WorkspaceIconSetting(workspace: state.workspace!), + _WorkspaceIconSetting( + enableEdit: state.myRole.isOwner, + workspace: state.workspace, + ), ], ), ], @@ -195,9 +149,7 @@ class _SettingsWorkspaceViewState extends State { title: LocaleKeys.settings_workspacePage_language_title.tr(), children: const [LanguageDropdown()], ), - if (state.workspace != null && - widget.userProfile.authenticator != - AuthenticatorPB.Local) ...[ + if (userProfile.authenticator != AuthenticatorPB.Local) ...[ SingleSettingAction( label: LocaleKeys.settings_workspacePage_manageWorkspace_title .tr(), @@ -244,17 +196,115 @@ class _SettingsWorkspaceViewState extends State { ), ); } +} - void _saveWorkspaceName( - BuildContext context, { - required String current, +class _WorkspaceNameSetting extends StatefulWidget { + const _WorkspaceNameSetting(); + + @override + State<_WorkspaceNameSetting> createState() => _WorkspaceNameSettingState(); +} + +class _WorkspaceNameSettingState extends State<_WorkspaceNameSetting> { + final TextEditingController workspaceNameController = TextEditingController(); + late final FocusNode focusNode; + bool isEditing = false; + + @override + void initState() { + super.initState(); + focusNode = FocusNode( + onKeyEvent: (_, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape && + isEditing && + mounted) { + setState(() => isEditing = false); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + )..addListener(() { + if (!focusNode.hasFocus && isEditing && mounted) { + _saveWorkspaceName(name: workspaceNameController.text); + setState(() => isEditing = false); + } + }); + } + + @override + void dispose() { + focusNode.dispose(); + workspaceNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (_, state) { + final newName = state.workspace?.name; + if (newName != null && newName != workspaceNameController.text) { + workspaceNameController.text = newName; + } + }, + builder: (_, state) { + if (isEditing) { + return Flexible( + child: SettingsInputField( + textController: workspaceNameController, + value: workspaceNameController.text, + focusNode: focusNode..requestFocus(), + onCancel: () => setState(() => isEditing = false), + onSave: (_) { + _saveWorkspaceName(name: workspaceNameController.text); + setState(() => isEditing = false); + }, + ), + ); + } + + return Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 2.5), + child: FlowyText.regular( + workspaceNameController.text, + fontSize: 14, + ), + ), + if (state.myRole.isOwner) ...[ + const HSpace(4), + FlowyTooltip( + message: LocaleKeys + .settings_workspacePage_workspaceName_editTooltip + .tr(), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => isEditing = true), + child: const FlowyHover( + resetHoverOnRebuild: false, + child: Padding( + padding: EdgeInsets.all(4), + child: FlowySvg(FlowySvgs.edit_s), + ), + ), + ), + ), + ], + ], + ); + }, + ); + } + + void _saveWorkspaceName({ required String name, }) { - if (name.isNotEmpty && name != current) { + if (name.isNotEmpty) { context.read().add( - WorkspaceSettingsEvent.updateWorkspaceName( - _workspaceNameController.text, - ), + WorkspaceSettingsEvent.updateWorkspaceName(name), ); if (context.mounted) { @@ -300,12 +350,21 @@ class LanguageDropdown extends StatelessWidget { } class _WorkspaceIconSetting extends StatelessWidget { - const _WorkspaceIconSetting({required this.workspace}); + const _WorkspaceIconSetting({required this.enableEdit, this.workspace}); - final UserWorkspacePB workspace; + final bool enableEdit; + final UserWorkspacePB? workspace; @override Widget build(BuildContext context) { + if (workspace == null) { + return const SizedBox( + height: 64, + width: 64, + child: CircularProgressIndicator(), + ); + } + return Container( height: 64, width: 64, @@ -316,9 +375,9 @@ class _WorkspaceIconSetting extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(1), child: WorkspaceIcon( - workspace: workspace, - iconSize: workspace.icon.isNotEmpty == true ? 46 : 20, - enableEdit: true, + workspace: workspace!, + iconSize: workspace!.icon.isNotEmpty == true ? 46 : 20, + enableEdit: enableEdit, onSelected: (r) => context .read() .add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)), @@ -508,6 +567,7 @@ class _DateTimeFormatLabel extends StatelessWidget { now.timeZoneName, ], ), + maxLines: 2, fontSize: 16, color: AFThemeExtension.of(context).secondaryTextColor, ); @@ -712,6 +772,9 @@ class AppearanceSelector extends StatelessWidget { ), ), ), + child: t != themeMode + ? null + : const _SelectedModeIndicator(), ), const VSpace(6), FlowyText.regular(getLabel(t), textAlign: TextAlign.center), @@ -735,6 +798,38 @@ class AppearanceSelector extends StatelessWidget { }; } +class _SelectedModeIndicator extends StatelessWidget { + const _SelectedModeIndicator(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Positioned( + top: 4, + left: 4, + child: Material( + shape: const CircleBorder(), + elevation: 2, + child: Container( + decoration: const BoxDecoration( + shape: BoxShape.circle, + ), + height: 16, + width: 16, + child: const FlowySvg( + FlowySvgs.settings_selected_theme_m, + size: Size.square(16), + blendMode: BlendMode.dstIn, + ), + ), + ), + ), + ], + ); + } +} + class _FontSelectorDropdown extends StatelessWidget { const _FontSelectorDropdown(); @@ -777,6 +872,7 @@ class _FontSelectorDropdown extends StatelessWidget { selectedValue: appearance.font, value: font, label: font.fontFamilyDisplayName, + fontFamily: font, ), ) .toList(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart index ef4c374239..c234f538b0 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -1,3 +1,5 @@ +import 'package:appflowy/shared/google_fonts_extension.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -10,7 +12,12 @@ DropdownMenuEntry buildDropdownMenuEntry( T? selectedValue, Widget? leadingWidget, Widget? trailingWidget, + String? fontFamily, }) { + final fontFamilyUsed = fontFamily != null + ? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily + : defaultFontFamily; + return DropdownMenuEntry( style: ButtonStyle( foregroundColor: @@ -26,7 +33,12 @@ DropdownMenuEntry buildDropdownMenuEntry( leadingIcon: leadingWidget, labelWidget: Padding( padding: const EdgeInsets.symmetric(vertical: 4), - child: FlowyText.medium(label, fontSize: 14, textAlign: TextAlign.start), + child: FlowyText.medium( + label, + fontSize: 14, + textAlign: TextAlign.start, + fontFamily: fontFamilyUsed, + ), ), trailingIcon: Row( children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index 67c0dc4cf9..8b28289670 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -1,9 +1,13 @@ +import 'package:appflowy/shared/google_fonts_extension.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/flutter/af_dropdown_menu.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsDropdown extends StatefulWidget { const SettingsDropdown({ @@ -37,6 +41,10 @@ class _SettingsDropdownState extends State> { @override Widget build(BuildContext context) { + final fontFamily = context.read().state.font; + final fontFamilyUsed = + getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily; + return Row( children: [ Expanded( @@ -45,6 +53,10 @@ class _SettingsDropdownState extends State> { expandedInsets: widget.expandWidth ? EdgeInsets.zero : null, initialSelection: widget.selectedOption, dropdownMenuEntries: widget.options, + textStyle: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontFamily: fontFamilyUsed), menuStyle: MenuStyle( maximumSize: const MaterialStatePropertyAll(Size(double.infinity, 250)), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index e104879752..6b9e55fc7d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -5,7 +5,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; -import 'package:appflowy/workspace/presentation/settings/shared/settings_category_spacer.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; @@ -37,8 +36,6 @@ class WorkspaceMembersPage extends StatelessWidget { title: LocaleKeys.settings_appearance_members_title.tr(), children: [ if (state.myRole.canInvite) const _InviteMember(), - if (state.myRole.canInvite && state.members.isNotEmpty) - const SettingsCategorySpacer(), if (state.members.isNotEmpty) _MemberList( members: state.members, diff --git a/frontend/resources/flowy_icons/24x/settings_selected_theme.svg b/frontend/resources/flowy_icons/24x/settings_selected_theme.svg new file mode 100644 index 0000000000..d6c6b6d809 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/settings_selected_theme.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index ca0a190367..0ff64fedb1 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -353,7 +353,8 @@ "description": "Customize your workspace appearance, theme, font, text layout, date-/time-format, and language.", "workspaceName": { "title": "Workspace name", - "savedMessage": "Saved workspace name" + "savedMessage": "Saved workspace name", + "editTooltip": "Edit workspace name" }, "workspaceIcon": { "title": "Workspace icon", @@ -429,7 +430,8 @@ "open": "Open folder", "openTooltip": "Open current data folder location", "copy": "Copy path", - "copiedHint": "Link copied!" + "copiedHint": "Path copied!", + "resetTooltip": "Reset to default location" }, "resetDialog": { "title": "Are you sure?", @@ -1673,4 +1675,4 @@ "betaTooltip": "We currently only support searching for pages", "fromTrashHint": "From trash" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-folder/src/notification.rs b/frontend/rust-lib/flowy-folder/src/notification.rs index c57450a5d6..1ddcebcafd 100644 --- a/frontend/rust-lib/flowy-folder/src/notification.rs +++ b/frontend/rust-lib/flowy-folder/src/notification.rs @@ -12,7 +12,7 @@ pub enum FolderNotification { Unknown = 0, /// Trigger after creating a workspace DidCreateWorkspace = 1, - // /// Trigger after updating a workspace + /// Trigger after updating a workspace DidUpdateWorkspace = 2, DidUpdateWorkspaceViews = 3, From 68c4e19f910ddfbe771251c1360f009887ff57e9 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 22 May 2024 14:24:11 +0800 Subject: [PATCH 20/30] chore: set min version number (#5390) * chore: set min version number * chore: fix test --- frontend/appflowy_tauri/src-tauri/Cargo.toml | 1 + frontend/appflowy_tauri/src-tauri/src/init.rs | 3 ++- frontend/appflowy_web_app/src-tauri/Cargo.toml | 1 + frontend/appflowy_web_app/src-tauri/src/init.rs | 1 + frontend/rust-lib/Cargo.lock | 7 +++++-- frontend/rust-lib/dart-ffi/Cargo.toml | 1 + frontend/rust-lib/dart-ffi/src/lib.rs | 16 ++++++++++++---- .../rust-lib/event-integration-test/Cargo.toml | 1 + .../rust-lib/event-integration-test/src/lib.rs | 3 ++- frontend/rust-lib/flowy-core/src/config.rs | 5 +++-- .../rust-lib/flowy-core/src/integrate/server.rs | 2 +- frontend/rust-lib/flowy-core/src/lib.rs | 4 +--- frontend/rust-lib/flowy-server/Cargo.toml | 1 + .../rust-lib/flowy-server/src/af_cloud/server.rs | 5 +++-- .../flowy-server/tests/af_cloud_test/util.rs | 3 ++- 15 files changed, 37 insertions(+), 17 deletions(-) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 9206c93d27..6b3a671077 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -94,6 +94,7 @@ flowy-notification = { path = "../../rust-lib/flowy-notification", features = [ uuid = "1.5.0" tauri-plugin-deep-link = "0.1.2" dotenv = "0.15.0" +semver = "1.0.23" [features] # by default Tauri runs in production mode diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index 1702c02923..7591ba37ff 100644 --- a/frontend/appflowy_tauri/src-tauri/src/init.rs +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -34,7 +34,8 @@ pub fn init_flowy_core() -> AppFlowyCore { .version .clone() .map(|v| v.to_string()) - .unwrap_or_else(|| "0.0.0".to_string()); + .unwrap_or_else(|| "0.5.8".to_string()); + let app_version = semver::Version::parse(&app_version).unwrap_or_else(|_| semver::Version::new(0, 5, 8)); let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); if cfg!(debug_assertions) { data_path.push("data_dev"); diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml index 45d822b46e..54e09acf6c 100644 --- a/frontend/appflowy_web_app/src-tauri/Cargo.toml +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -93,6 +93,7 @@ flowy-notification = { path = "../../rust-lib/flowy-notification", features = [ uuid = "1.5.0" tauri-plugin-deep-link = "0.1.2" dotenv = "0.15.0" +semver = "1.0.23" [features] # by default Tauri runs in production mode diff --git a/frontend/appflowy_web_app/src-tauri/src/init.rs b/frontend/appflowy_web_app/src-tauri/src/init.rs index 923ab0ff8f..42c857abdf 100644 --- a/frontend/appflowy_web_app/src-tauri/src/init.rs +++ b/frontend/appflowy_web_app/src-tauri/src/init.rs @@ -30,6 +30,7 @@ pub fn init_flowy_core() -> AppFlowyCore { let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap(); let app_version = config.package.version.clone().map(|v| v.to_string()).unwrap_or_else(|| "0.0.0".to_string()); + let app_version = semver::Version::parse(&app_version).unwrap_or_else(|_| semver::Version::new(0, 5, 8)); let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); if cfg!(debug_assertions) { data_path.push("data_dev"); diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 0560f222de..d9a9d047e5 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1218,6 +1218,7 @@ dependencies = [ "lib-log", "parking_lot 0.12.1", "protobuf", + "semver", "serde", "serde_json", "serde_repr", @@ -1509,6 +1510,7 @@ dependencies = [ "parking_lot 0.12.1", "protobuf", "rand 0.8.5", + "semver", "serde", "serde_json", "strum", @@ -2035,6 +2037,7 @@ dependencies = [ "postgrest", "rand 0.8.5", "reqwest", + "semver", "serde", "serde_json", "thiserror", @@ -4800,9 +4803,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 4e5148da95..e17ede23e5 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -25,6 +25,7 @@ lazy_static = "1.4.0" parking_lot.workspace = true tracing.workspace = true lib-log.workspace = true +semver = "1.0.22" # workspace lib-dispatch = { workspace = true } diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 48f3e485a2..a7f3e2ee19 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -1,11 +1,11 @@ #![allow(clippy::not_unsafe_ptr_arg_deref)] use allo_isolate::Isolate; -use std::sync::Arc; -use std::{ffi::CStr, os::raw::c_char}; - use lazy_static::lazy_static; use parking_lot::Mutex; +use semver::Version; +use std::sync::Arc; +use std::{ffi::CStr, os::raw::c_char}; use tracing::{debug, error, info, trace, warn}; use flowy_core::config::AppFlowyCoreConfig; @@ -67,8 +67,16 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { let _ = save_appflowy_cloud_config(&configuration.root, &configuration.appflowy_cloud_config); } + let mut app_version = + Version::parse(&configuration.app_version).unwrap_or_else(|_| Version::new(0, 5, 8)); + + let min_version = Version::new(0, 5, 8); + if app_version < min_version { + app_version = min_version; + } + let config = AppFlowyCoreConfig::new( - configuration.app_version, + app_version, configuration.custom_app_path, configuration.origin_app_path, configuration.device_id, diff --git a/frontend/rust-lib/event-integration-test/Cargo.toml b/frontend/rust-lib/event-integration-test/Cargo.toml index 26d561e993..d427a8f16c 100644 --- a/frontend/rust-lib/event-integration-test/Cargo.toml +++ b/frontend/rust-lib/event-integration-test/Cargo.toml @@ -24,6 +24,7 @@ flowy-notification = { workspace = true } anyhow.workspace = true flowy-storage = { workspace = true } flowy-search = { workspace = true } +semver = "1.0.23" serde.workspace = true serde_json.workspace = true diff --git a/frontend/rust-lib/event-integration-test/src/lib.rs b/frontend/rust-lib/event-integration-test/src/lib.rs index 180623d8d5..cddd304dde 100644 --- a/frontend/rust-lib/event-integration-test/src/lib.rs +++ b/frontend/rust-lib/event-integration-test/src/lib.rs @@ -10,6 +10,7 @@ use std::time::Duration; use nanoid::nanoid; use parking_lot::RwLock; +use semver::Version; use tokio::select; use tokio::time::sleep; @@ -55,7 +56,7 @@ impl EventIntegrationTest { let device_id = uuid::Uuid::new_v4().to_string(); let config = AppFlowyCoreConfig::new( - "".to_string(), + Version::new(0, 5, 8), path.clone(), path, device_id, diff --git a/frontend/rust-lib/flowy-core/src/config.rs b/frontend/rust-lib/flowy-core/src/config.rs index 53919ad9b0..fd8fbe335f 100644 --- a/frontend/rust-lib/flowy-core/src/config.rs +++ b/frontend/rust-lib/flowy-core/src/config.rs @@ -2,6 +2,7 @@ use std::fmt; use std::path::Path; use base64::Engine; +use semver::Version; use tracing::{error, info}; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; @@ -15,7 +16,7 @@ use crate::integrate::log::create_log_filter; #[derive(Clone)] pub struct AppFlowyCoreConfig { /// Different `AppFlowyCoreConfig` instance should have different name - pub(crate) app_version: String, + pub(crate) app_version: Version, pub name: String, pub(crate) device_id: String, pub platform: String, @@ -75,7 +76,7 @@ fn make_user_data_folder(root: &str, url: &str) -> String { impl AppFlowyCoreConfig { pub fn new( - app_version: String, + app_version: Version, custom_application_path: String, application_path: String, device_id: String, diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index b0c0acbdcf..2959ae7f21 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -133,7 +133,7 @@ impl ServerProvider { config, *self.user_enable_sync.read(), self.config.device_id.clone(), - &self.config.app_version, + self.config.app_version.clone(), self.user.clone(), )); diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 36addf0fe7..2ba29b83fd 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -3,7 +3,6 @@ use flowy_search::folder::indexer::FolderIndexManagerImpl; use flowy_search::services::manager::SearchManager; use flowy_storage::ObjectStorageService; -use semver::Version; use std::sync::{Arc, Weak}; use std::time::Duration; use sysinfo::System; @@ -106,13 +105,12 @@ impl AppFlowyCore { let task_dispatcher = Arc::new(RwLock::new(task_scheduler)); runtime.spawn(TaskRunner::run(task_dispatcher.clone())); - let app_version = Version::parse(&config.app_version).unwrap_or_else(|_| Version::new(0, 5, 4)); let user_config = UserConfig::new( &config.name, &config.storage_path, &config.application_path, &config.device_id, - app_version, + config.app_version.clone(), ); let authenticate_user = Arc::new(AuthenticateUser::new( diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index b64b30fa49..d11cb2b594 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -48,6 +48,7 @@ tokio-stream = { workspace = true, features = ["sync"] } lib-dispatch = { workspace = true } yrs.workspace = true rand = "0.8.5" +semver = "1.0.23" [dependencies.client-api] workspace = true diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 1fd6a5b03f..c758be253a 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -12,6 +12,7 @@ use client_api::ws::{ use client_api::{Client, ClientConfiguration}; use flowy_storage::ObjectStorageService; use rand::Rng; +use semver::Version; use tokio::select; use tokio::sync::{watch, Mutex}; use tokio_stream::wrappers::WatchStream; @@ -53,7 +54,7 @@ impl AppFlowyCloudServer { config: AFCloudConfiguration, enable_sync: bool, mut device_id: String, - client_version: &str, + client_version: Version, user: Arc, ) -> Self { // The device id can't be empty, so we generate a new one if it is. @@ -70,7 +71,7 @@ impl AppFlowyCloudServer { ClientConfiguration::default() .with_compression_buffer_size(10240) .with_compression_quality(8), - client_version, + &client_version.to_string(), ); let token_state_rx = api_client.subscribe_token_state(); let enable_sync = Arc::new(AtomicBool::new(enable_sync)); diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index 224e10cd95..71dacfab04 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -1,4 +1,5 @@ use client_api::ClientConfiguration; +use semver::Version; use std::collections::HashMap; use std::sync::Arc; @@ -32,7 +33,7 @@ pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc config, true, fake_device_id, - "0.5.1", + Version::new(0, 5, 8), Arc::new(FakeServerUserImpl), )) } From acae34836ec7aba9779dad6fccc1d5a1f96acc38 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Wed, 22 May 2024 21:00:56 +0800 Subject: [PATCH 21/30] feat: support preview the calendar view on web (#5394) --- frontend/appflowy_web_app/pnpm-lock.yaml | 16 +-- .../src/application/collab.type.ts | 19 ++- .../src/application/database-yjs/const.ts | 14 +++ .../application/database-yjs/database.type.ts | 14 +++ .../database-yjs/fields/checklist/parse.ts | 2 +- .../src/application/database-yjs/filter.ts | 4 +- .../src/application/database-yjs/group.ts | 10 +- .../src/application/database-yjs/selector.ts | 119 +++++++++++++++++- .../src/application/folder-yjs/selector.ts | 29 ++--- .../_shared/popover/RichTooltip.tsx | 63 ++++++++++ .../src/components/_shared/popover/index.ts | 1 + .../src/components/database/DatabaseViews.tsx | 60 +++------ .../database/calendar/Calendar.hooks.ts | 41 ++++++ .../components/database/calendar/Calendar.tsx | 31 +++-- .../database/calendar/calendar.scss | 63 +++++++++- .../database/components/board/card/Card.tsx | 2 +- .../database/components/board/group/Group.tsx | 14 +-- .../components/calendar/event/Event.tsx | 34 +++++ .../components/calendar/event/EventPaper.tsx | 28 +++++ .../components/calendar/event/index.ts | 1 + .../database/components/calendar/index.ts | 2 + .../components/calendar/toolbar/NoDate.tsx | 46 +++++++ .../components/calendar/toolbar/NoDateRow.tsx | 44 +++++++ .../components/calendar/toolbar/Toolbar.tsx | 59 +++++++++ .../components/calendar/toolbar/index.ts | 1 + .../database/components/cell/cell.type.ts | 2 + .../cell/checklist/ChecklistCell.tsx | 2 +- .../components/cell/date/DateTimeCell.tsx | 9 +- .../components/cell/number/NumberCell.tsx | 9 +- .../components/cell/relation/RelationCell.tsx | 10 +- .../cell/select-option/SelectOptionCell.tsx | 11 +- .../database/components/cell/url/UrlCell.tsx | 11 +- .../components/conditions/DatabaseActions.tsx | 9 +- .../{board/card => field}/CardField.tsx | 0 .../database/components/property/Property.tsx | 78 ++++++++++++ .../components/property/PropertyWrapper.tsx | 15 +++ .../database/components/property/index.ts | 1 + .../components/property/text/TextProperty.tsx | 29 +++++ .../components/property/text/index.ts | 1 + .../database/components/tabs/DatabaseTabs.tsx | 5 +- .../src/components/layout/layout.scss | 21 +++- frontend/resources/translations/en.json | 11 +- 42 files changed, 816 insertions(+), 125 deletions(-) create mode 100644 frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/calendar/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts rename frontend/appflowy_web_app/src/components/database/components/{board/card => field}/CardField.tsx (100%) create mode 100644 frontend/appflowy_web_app/src/components/database/components/property/Property.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/property/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/property/text/index.ts diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml index 4cc4e224c2..7796ae7db8 100644 --- a/frontend/appflowy_web_app/pnpm-lock.yaml +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -34,7 +34,7 @@ dependencies: version: 6.0.0-alpha.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mui/x-date-pickers-pro': specifier: ^6.18.2 - version: 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + version: 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) '@reduxjs/toolkit': specifier: 2.0.0 version: 2.0.0(react-redux@8.0.5)(react@18.2.0) @@ -1763,8 +1763,8 @@ packages: react: 18.2.0 dev: false - /@mui/system@5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0): - resolution: {integrity: sha512-oPgfWS97QNfHcDBapdkZIs4G5i85BJt69Hp6wbXF6s7vi3Evcmhdk8AbCRW6n0sX4vTj8oe0mh0RIm1G2A1KDA==} + /@mui/system@5.15.15(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==} engines: {node: '>=12.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1870,7 +1870,7 @@ packages: react-is: 18.2.0 dev: false - /@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-8lEVEOtCQssKWel4Ey1pRulGPXUQ73TnkHKzHWsjdv03FjiUs3eYB+Ej0Uk5yWPmsqlShWhOzOlOGDpzsYJsUg==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1912,9 +1912,9 @@ packages: '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) '@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mui/material': 6.0.0-alpha.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) - '@mui/x-date-pickers': 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-date-pickers': 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) '@mui/x-license-pro': 6.10.2(@types/react@18.2.66)(react@18.2.0) clsx: 2.1.0 dayjs: 1.11.9 @@ -1926,7 +1926,7 @@ packages: - '@types/react' dev: false - /@mui/x-date-pickers@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + /@mui/x-date-pickers@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@6.0.0-alpha.2)(@mui/system@5.15.15)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-HJq4uoFQSu5isa/mesWw2BKh8KBRYUQb+KaSlVlWfJNgP3YhPvWZ6yqCNYyxOAiPMxb0n3nBjS9ErO27OHjFMA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1968,7 +1968,7 @@ packages: '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) '@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) '@mui/material': 6.0.0-alpha.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) - '@mui/system': 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/system': 5.15.15(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) '@types/react-transition-group': 4.4.10 clsx: 2.1.0 diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index ac6be1f3f8..7e8491bac5 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -244,6 +244,10 @@ export enum YjsDatabaseKey { visible = 'visible', hide_ungrouped_column = 'hide_ungrouped_column', collapse_hidden_groups = 'collapse_hidden_groups', + first_day_of_week = 'first_day_of_week', + show_week_numbers = 'show_week_numbers', + show_weekends = 'show_weekends', + layout_ty = 'layout_ty', } export interface YDoc extends Y.Doc { @@ -434,23 +438,30 @@ export type YDatabaseFilters = Y.Array; export type YDatabaseSorts = Y.Array; -export type YDatabaseLayoutSettings = Y.Map; - export type YDatabaseCalculations = Y.Array; export type SortId = string; export type GroupId = string; -export interface YDatabaseLayoutSetting extends Y.Map { +export interface YDatabaseLayoutSettings extends Y.Map { // DatabaseViewLayout.Board - get(key: '2'): YDatabaseBoardLayoutSetting; + get(key: '1'): YDatabaseBoardLayoutSetting; + + // DatabaseViewLayout.Calendar + get(key: '2'): YDatabaseCalendarLayoutSetting; } export interface YDatabaseBoardLayoutSetting extends Y.Map { get(key: YjsDatabaseKey.hide_ungrouped_column | YjsDatabaseKey.collapse_hidden_groups): boolean; } +export interface YDatabaseCalendarLayoutSetting extends Y.Map { + get(key: YjsDatabaseKey.first_day_of_week | YjsDatabaseKey.field_id | YjsDatabaseKey.layout_ty): string; + + get(key: YjsDatabaseKey.show_week_numbers | YjsDatabaseKey.show_weekends): boolean; +} + export interface YDatabaseGroup extends Y.Map { get(key: YjsDatabaseKey.id): GroupId; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/const.ts b/frontend/appflowy_web_app/src/application/database-yjs/const.ts index b082acc6a4..b1dc0a0810 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/const.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/const.ts @@ -1,2 +1,16 @@ +import { YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import * as Y from 'yjs'; + export const DEFAULT_ROW_HEIGHT = 37; export const MIN_COLUMN_WIDTH = 100; + +export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map) => { + const rowMeta = rowMetas.get(rowId); + const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; + + return meta?.get(YjsDatabaseKey.cells)?.get(fieldId); +}; + +export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map) => { + return getCell(rowId, fieldId, rowMetas)?.get(YjsDatabaseKey.data); +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts index f5d4aeac61..403a630ef2 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts @@ -49,3 +49,17 @@ export interface Filter { id: string; content: string; } + +export enum CalendarLayout { + MonthLayout = 0, + WeekLayout = 1, + DayLayout = 2, +} + +export interface CalendarLayoutSetting { + fieldId: string; + firstDayOfWeek: number; + showWeekNumbers: boolean; + showWeekends: boolean; + layout: CalendarLayout; +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts index 6dd14c71e0..c93fee7a38 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/fields/checklist/parse.ts @@ -9,7 +9,7 @@ export interface ChecklistCellData { export function parseChecklistData(data: string): ChecklistCellData | null { try { const { options, selected_option_ids } = JSON.parse(data); - const percentage = (selected_option_ids.length / options.length) * 100; + const percentage = selected_option_ids.length / options.length; return { percentage, diff --git a/frontend/appflowy_web_app/src/application/database-yjs/filter.ts b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts index 73a8663371..5f0919c4d9 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/filter.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/filter.ts @@ -181,10 +181,10 @@ export function checklistFilterCheck(data: string, content: string, condition: n const percentage = parseChecklistData(data)?.percentage ?? 0; if (condition === ChecklistFilterCondition.IsComplete) { - return percentage === 100; + return percentage === 1; } - return percentage !== 100; + return percentage !== 1; } export function selectOptionFilterCheck(data: string, content: string, condition: number) { diff --git a/frontend/appflowy_web_app/src/application/database-yjs/group.ts b/frontend/appflowy_web_app/src/application/database-yjs/group.ts index ddefab9a26..709053aa32 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/group.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/group.ts @@ -1,4 +1,5 @@ -import { YDatabaseField, YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { YDatabaseField, YDoc, YjsDatabaseKey } from '@/application/collab.type'; +import { getCellData } from '@/application/database-yjs/const'; import { FieldType } from '@/application/database-yjs/database.type'; import { parseSelectOptionTypeOptions } from '@/application/database-yjs/fields'; import { Row } from '@/application/database-yjs/selector'; @@ -12,13 +13,6 @@ export function groupByField(rows: Row[], rowMetas: Y.Map, field: YDatabas return groupBySelectOption(rows, rowMetas, field); } -function getCellData(rowId: string, fieldId: string, rowMetas: Y.Map) { - const rowMeta = rowMetas.get(rowId); - const meta = rowMeta?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; - - return meta?.get(YjsDatabaseKey.cells)?.get(fieldId)?.get(YjsDatabaseKey.data); -} - export function groupBySelectOption(rows: Row[], rowMetas: Y.Map, field: YDatabaseField) { const fieldId = field.get(YjsDatabaseKey.id); const result = new Map(); diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts index f115ff5eb4..bb35599254 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -1,5 +1,5 @@ -import { FieldId, SortId, YDatabaseField, YjsDatabaseKey } from '@/application/collab.type'; -import { MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; +import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsFolderKey } from '@/application/collab.type'; +import { getCell, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; import { DatabaseContext, useDatabase, @@ -13,10 +13,13 @@ import { filterBy, parseFilter } from '@/application/database-yjs/filter'; import { groupByField } from '@/application/database-yjs/group'; import { sortBy } from '@/application/database-yjs/sort'; import { useViewsIdSelector } from '@/application/folder-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; import { parseYDatabaseCellToCell } from '@/components/database/components/cell/cell.parse'; +import { DateTimeCell } from '@/components/database/components/cell/cell.type'; +import dayjs from 'dayjs'; import debounce from 'lodash-es/debounce'; import { useContext, useEffect, useMemo, useState } from 'react'; -import { FieldType, FieldVisibility, Filter, SortCondition } from './database.type'; +import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, SortCondition } from './database.type'; export interface Column { fieldId: string; @@ -34,7 +37,8 @@ const defaultVisible = [FieldVisibility.AlwaysShown, FieldVisibility.HideWhenEmp export function useDatabaseViewsSelector() { const database = useDatabase(); - const { viewsId: visibleViewsId } = useViewsIdSelector(); + const { objectId: currentViewId } = useId(); + const { viewsId: visibleViewsId, views: folderViews } = useViewsIdSelector(); const views = database?.get(YjsDatabaseKey.views); const [viewIds, setViewIds] = useState([]); const childViews = useMemo(() => { @@ -45,7 +49,16 @@ export function useDatabaseViewsSelector() { if (!views) return; const observerEvent = () => { - setViewIds(Array.from(views.keys()).filter((id) => visibleViewsId.includes(id))); + setViewIds( + Array.from(views.keys()).filter((id) => { + const view = folderViews?.get(id); + + return ( + visibleViewsId.includes(id) && + (view?.get(YjsFolderKey.bid) === currentViewId || view?.get(YjsFolderKey.id) === currentViewId) + ); + }) + ); }; observerEvent(); @@ -54,7 +67,7 @@ export function useDatabaseViewsSelector() { return () => { views.unobserve(observerEvent); }; - }, [visibleViewsId, views]); + }, [visibleViewsId, views, folderViews, currentViewId]); return { childViews, @@ -478,3 +491,97 @@ export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: st return cellValue; } + +export interface CalendarEvent { + start?: Date; + end?: Date; + id: string; +} + +export function useCalendarEventsSelector() { + const setting = useCalendarLayoutSetting(); + const filedId = setting.fieldId; + const { field } = useFieldSelector(filedId); + const rowOrders = useRowOrdersSelector(); + const rows = useContext(DatabaseContext)?.rowDocMap; + const [events, setEvents] = useState([]); + const [emptyEvents, setEmptyEvents] = useState([]); + + useEffect(() => { + if (!field || !rowOrders || !rows) return; + const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + + if (fieldType !== FieldType.DateTime) return; + const newEvents: CalendarEvent[] = []; + const emptyEvents: CalendarEvent[] = []; + + rowOrders?.forEach((row) => { + const cell = getCell(row.id, filedId, rows); + + if (!cell) { + emptyEvents.push({ + id: `${row.id}:${filedId}`, + }); + return; + } + + const value = parseYDatabaseCellToCell(cell) as DateTimeCell; + + if (!value || !value.data) { + emptyEvents.push({ + id: `${row.id}:${filedId}`, + }); + return; + } + + const getDate = (timestamp: string) => { + const dayjsResult = timestamp.length === 10 ? dayjs.unix(Number(timestamp)) : dayjs(timestamp); + + return dayjsResult.toDate(); + }; + + newEvents.push({ + id: `${row.id}:${filedId}`, + start: getDate(value.data), + end: value.endTimestamp && value.isRange ? getDate(value.endTimestamp) : getDate(value.data), + }); + }); + + setEvents(newEvents); + setEmptyEvents(emptyEvents); + }, [field, rowOrders, rows, filedId]); + + return { events, emptyEvents }; +} + +export function useCalendarLayoutSetting() { + const view = useDatabaseView(); + const layoutSetting = view?.get(YjsDatabaseKey.layout_settings)?.get('2'); + const [setting, setSetting] = useState({ + fieldId: '', + firstDayOfWeek: 0, + showWeekNumbers: true, + showWeekends: true, + layout: 0, + }); + + useEffect(() => { + const observerHandler = () => { + setSetting({ + fieldId: layoutSetting?.get(YjsDatabaseKey.field_id) as string, + firstDayOfWeek: Number(layoutSetting?.get(YjsDatabaseKey.first_day_of_week)), + showWeekNumbers: Boolean(layoutSetting?.get(YjsDatabaseKey.show_week_numbers)), + showWeekends: Boolean(layoutSetting?.get(YjsDatabaseKey.show_weekends)), + layout: Number(layoutSetting?.get(YjsDatabaseKey.layout_ty)), + }); + }; + + observerHandler(); + layoutSetting?.observe(observerHandler); + return () => { + layoutSetting?.unobserve(observerHandler); + }; + }, [layoutSetting]); + + return setting; +} diff --git a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts index 648e27c9d3..e5eb68203e 100644 --- a/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/folder-yjs/selector.ts @@ -5,38 +5,39 @@ import { useEffect, useState } from 'react'; export function useViewsIdSelector() { const folder = useFolderContext(); const [viewsId, setViewsId] = useState([]); + const views = folder?.get(YjsFolderKey.views); + const trash = folder?.get(YjsFolderKey.section)?.get(YjsFolderKey.trash); + const meta = folder?.get(YjsFolderKey.meta); useEffect(() => { - if (!folder) return; + if (!views) return; - const views = folder.get(YjsFolderKey.views); - const trash = folder.get(YjsFolderKey.section)?.get(YjsFolderKey.trash); - const meta = folder.get(YjsFolderKey.meta); - const trashUid = Array.from(trash?.keys())[0]; - const userTrash = trash?.get(trashUid); + const trashUid = trash ? Array.from(trash.keys())[0] : null; + const userTrash = trashUid ? trash?.get(trashUid) : null; const collectIds = () => { const trashIds = userTrash?.toJSON()?.map((item) => item.id) || []; - return Array.from(views.keys()).filter( - (id) => !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace) - ); + return Array.from(views.keys()).filter((id) => { + return !trashIds.includes(id) && id !== meta?.get(YjsFolderKey.current_workspace); + }); }; setViewsId(collectIds()); const observerEvent = () => setViewsId(collectIds()); - folder.observe(observerEvent); - userTrash.observe(observerEvent); + views.observe(observerEvent); + userTrash?.observe(observerEvent); return () => { - folder.unobserve(observerEvent); - userTrash.unobserve(observerEvent); + views.unobserve(observerEvent); + userTrash?.unobserve(observerEvent); }; - }, [folder]); + }, [views, trash, meta]); return { viewsId, + views, }; } diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx new file mode 100644 index 0000000000..06e6f3c51b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx @@ -0,0 +1,63 @@ +import { Box, ClickAwayListener, Fade, Paper, Popper, PopperPlacementType } from '@mui/material'; +import React, { ReactElement, useEffect } from 'react'; + +interface Props { + content: ReactElement; + children: ReactElement; + open: boolean; + onClose: () => void; + placement?: PopperPlacementType; +} + +export const RichTooltip = ({ placement = 'top', open, onClose, content, children }: Props) => { + const [childNode, setChildNode] = React.useState(null); + const [, setTransitioning] = React.useState(false); + + useEffect(() => { + if (open) { + setTransitioning(true); + } + }, [open]); + return ( + <> + {React.cloneElement(children, { ...children.props, ref: setChildNode })} + + {({ TransitionProps }) => ( + { + setTransitioning(false); + }} + > + + + + {content} + + + + + )} + + + ); +}; + +export default RichTooltip; diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/index.ts b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts index 8f473de4b9..f1c61c79c4 100644 --- a/frontend/appflowy_web_app/src/components/_shared/popover/index.ts +++ b/frontend/appflowy_web_app/src/components/_shared/popover/index.ts @@ -1 +1,2 @@ export * from './Popover'; +export * from './RichTooltip'; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx index 5d055780b9..8c02c124b0 100644 --- a/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx +++ b/frontend/appflowy_web_app/src/components/database/DatabaseViews.tsx @@ -3,10 +3,9 @@ import { useDatabaseViewsSelector } from '@/application/database-yjs'; import { Board } from '@/components/database/board'; import { Calendar } from '@/components/database/calendar'; import { DatabaseConditionsContext } from '@/components/database/components/conditions/context'; -import { DatabaseTabs, TabPanel } from '@/components/database/components/tabs'; +import { DatabaseTabs } from '@/components/database/components/tabs'; import { Grid } from '@/components/database/grid'; import React, { useCallback, useMemo, useState } from 'react'; -import SwipeableViews from 'react-swipeable-views'; import DatabaseConditions from 'src/components/database/components/conditions/DatabaseConditions'; function DatabaseViews({ @@ -25,22 +24,29 @@ function DatabaseViews({ ); }, [currentViewId, viewIds]); - const getDatabaseViewComponent = useCallback((layout: DatabaseViewLayout) => { - switch (layout) { - case DatabaseViewLayout.Grid: - return Grid; - case DatabaseViewLayout.Board: - return Board; - case DatabaseViewLayout.Calendar: - return Calendar; - } - }, []); - const [conditionsExpanded, setConditionsExpanded] = useState(false); const toggleExpanded = useCallback(() => { setConditionsExpanded((prev) => !prev); }, []); + const activeView = useMemo(() => { + return childViews[value]; + }, [childViews, value]); + + const view = useMemo(() => { + if (!activeView) return null; + const layout = Number(activeView.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; + + switch (layout) { + case DatabaseViewLayout.Grid: + return ; + case DatabaseViewLayout.Board: + return ; + case DatabaseViewLayout.Calendar: + return ; + } + }, [activeView]); + return ( <> - - {childViews.map((view, index) => { - const layout = Number(view.get(YjsDatabaseKey.layout)) as DatabaseViewLayout; - const Component = getDatabaseViewComponent(layout); - const viewId = viewIds[index]; - - return ( - - - - ); - })} - +
{view}
); } diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts new file mode 100644 index 0000000000..b3ec014505 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.hooks.ts @@ -0,0 +1,41 @@ +import { useCalendarEventsSelector, useCalendarLayoutSetting } from '@/application/database-yjs'; +import { useCallback, useEffect, useMemo } from 'react'; +import { dayjsLocalizer } from 'react-big-calendar'; +import dayjs from 'dayjs'; +import en from 'dayjs/locale/en'; + +export function useCalendarSetup() { + const layoutSetting = useCalendarLayoutSetting(); + const { events, emptyEvents } = useCalendarEventsSelector(); + + const dayPropGetter = useCallback((date: Date) => { + const day = date.getDay(); + + return { + className: `day-${day}`, + }; + }, []); + + useEffect(() => { + dayjs.locale({ + ...en, + weekStart: layoutSetting.firstDayOfWeek, + }); + }, [layoutSetting]); + + const localizer = useMemo(() => dayjsLocalizer(dayjs), []); + + const formats = useMemo(() => { + return { + weekdayFormat: 'ddd', + }; + }, []); + + return { + localizer, + formats, + dayPropGetter, + events, + emptyEvents, + }; +} diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx index 4face8913f..9e99135957 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -1,15 +1,32 @@ +import { AFScroller } from '@/components/_shared/scroller'; +import { useCalendarSetup } from '@/components/database/calendar/Calendar.hooks'; +import { Toolbar, Event } from '@/components/database/components/calendar'; import React from 'react'; -import { Calendar as BigCalendar, dayjsLocalizer } from 'react-big-calendar'; -import dayjs from 'dayjs'; +import { Calendar as BigCalendar } from 'react-big-calendar'; import './calendar.scss'; -const localizer = dayjsLocalizer(dayjs); - export function Calendar() { + const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup(); + return ( -
- -
+ +
+ , + eventWrapper: Event, + }} + events={events} + views={['month']} + localizer={localizer} + formats={formats} + dayPropGetter={dayPropGetter} + showMultiDayTimes={true} + step={1} + showAllEvents={true} + /> +
+
); } diff --git a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss index 3a3aebd3db..eac536443c 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss +++ b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss @@ -1,2 +1,63 @@ +$today-highlight-bg: transparent; @import 'react-big-calendar/lib/sass/styles'; -@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD \ No newline at end of file +@import 'react-big-calendar/lib/addons/dragAndDrop/styles'; // if using DnD + +.rbc-calendar { + font-size: 12px; +} + +.rbc-button-link { + @apply rounded-full w-[20px] h-[20px] my-1.5; +} + +.rbc-date-cell { + min-width: 100px; +} + +.rbc-date-cell.rbc-now { + + color: var(--content-on-fill); + + .rbc-button-link { + background-color: var(--function-error); + } +} + +.rbc-month-view { + border: none; + + .rbc-month-row { + border: 1px solid var(--line-divider); + border-bottom: none; + + &:last-child { + border-bottom: 1px solid var(--line-divider); + } + } +} + +.rbc-month-header { + height: 40px; + + .rbc-header { + border: none; + @apply flex items-end py-2 justify-center font-normal text-text-caption; + } +} + +.rbc-month-row .rbc-row-bg { + .rbc-off-range-bg { + background-color: transparent; + color: var(--text-caption); + } + + .rbc-day-bg.day-0, .rbc-day-bg.day-6 { + background-color: var(--fill-list-active); + } +} + +.rbc-month-row { + display: inline-table !important; + flex: 0 0 0 !important; + min-height: 120px !important; +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx index 7dbe829662..6f3b5e7b76 100644 --- a/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/board/card/Card.tsx @@ -1,5 +1,5 @@ import { useFieldsSelector } from '@/application/database-yjs'; -import CardField from '@/components/database/components/board/card/CardField'; +import CardField from '@/components/database/components/field/CardField'; import React, { useEffect, useMemo } from 'react'; export interface CardProps { diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx index 7d5e3630be..73188873de 100644 --- a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx @@ -26,18 +26,7 @@ export const Group = ({ groupId }: GroupProps) => { if (columns.length === 0 || !fieldId) return null; return ( - { - // we have a transform: * on one of the parents of a then the positioning logic will be incorrect while dragging - // https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/guides/reparenting.md - const id = columns[rubric.source.index].id; - - return ; - }} - > + {(provided) => { return (
{ }} ))} + {provided.placeholder}
); }} diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx new file mode 100644 index 0000000000..1e8d33ebcd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/Event.tsx @@ -0,0 +1,34 @@ +import { CalendarEvent, useFieldsSelector } from '@/application/database-yjs'; +import { RichTooltip } from '@/components/_shared/popover'; +import EventPaper from '@/components/database/components/calendar/event/EventPaper'; +import CardField from '@/components/database/components/field/CardField'; +import React, { useMemo } from 'react'; +import { EventWrapperProps } from 'react-big-calendar'; + +export function Event({ event }: EventWrapperProps) { + const { id } = event; + const [rowId, fieldId] = id.split(':'); + const fields = useFieldsSelector(); + const showFields = useMemo(() => fields.filter((field) => field.fieldId !== fieldId), [fields, fieldId]); + + const [open, setOpen] = React.useState(false); + + return ( +
+ } open={open} placement='right' onClose={() => setOpen(false)}> +
setOpen((prev) => !prev)} + className={ + 'flex min-h-[24px] cursor-pointer flex-col gap-2 rounded-md border border-line-border bg-bg-body p-2 shadow-sm hover:bg-fill-list-active hover:shadow' + } + > + {showFields.map((field) => { + return ; + })} +
+
+
+ ); +} + +export default Event; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx new file mode 100644 index 0000000000..fed3fdd295 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx @@ -0,0 +1,28 @@ +import { useFieldsSelector } from '@/application/database-yjs'; +import { Property } from '@/components/database/components/property'; +import { IconButton } from '@mui/material'; +import React from 'react'; +import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; + +function EventPaper({ rowId }: { rowId: string }) { + const fields = useFieldsSelector(); + + return ( +
+
+
+ + + +
+
+ {fields.map((field) => { + return ; + })} +
+
+
+ ); +} + +export default EventPaper; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts b/frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts new file mode 100644 index 0000000000..e59a119814 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/index.ts @@ -0,0 +1 @@ +export * from './Event'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/index.ts b/frontend/appflowy_web_app/src/components/database/components/calendar/index.ts new file mode 100644 index 0000000000..7b631093dc --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/index.ts @@ -0,0 +1,2 @@ +export * from './toolbar'; +export * from './event'; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx new file mode 100644 index 0000000000..7965bc33b7 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDate.tsx @@ -0,0 +1,46 @@ +import { CalendarEvent } from '@/application/database-yjs'; +import { RichTooltip } from '@/components/_shared/popover'; +import NoDateRow from '@/components/database/components/calendar/toolbar/NoDateRow'; +import Button from '@mui/material/Button'; +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +function NoDate({ emptyEvents }: { emptyEvents: CalendarEvent[] }) { + const [open, setOpen] = React.useState(false); + const { t } = useTranslation(); + const content = useMemo(() => { + return ( +
+
{t('calendar.settings.clickToOpen')}
+ {emptyEvents.map((event) => { + const rowId = event.id.split(':')[0]; + + return ; + })} +
+ ); + }, [emptyEvents, t]); + + return ( + { + setOpen(false); + }} + > + + + ); +} + +export default NoDate; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx new file mode 100644 index 0000000000..63b442775a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx @@ -0,0 +1,44 @@ +import { YjsDatabaseKey } from '@/application/collab.type'; +import { useCellSelector, useDatabase } from '@/application/database-yjs'; +import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import Cell from 'src/components/database/components/cell/Cell'; + +function NoDateRow({ rowId }: { rowId: string }) { + const database = useDatabase(); + const [primaryFieldId, setPrimaryFieldId] = React.useState(null); + const cell = useCellSelector({ + rowId, + fieldId: primaryFieldId || '', + }); + const { t } = useTranslation(); + + useEffect(() => { + const fields = database?.get(YjsDatabaseKey.fields); + const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => { + return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary); + }); + + setPrimaryFieldId(primaryFieldId || null); + }, [database]); + + if (!primaryFieldId || !cell?.data) { + return
{t('grid.row.titlePlaceholder')}
; + } + + return ( +
+ +
+ ); +} + +export default NoDateRow; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx new file mode 100644 index 0000000000..ed082d4afe --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/Toolbar.tsx @@ -0,0 +1,59 @@ +import { CalendarEvent } from '@/application/database-yjs'; +import NoDate from '@/components/database/components/calendar/toolbar/NoDate'; +import { IconButton } from '@mui/material'; +import Button from '@mui/material/Button'; +import dayjs from 'dayjs'; +import React, { useMemo } from 'react'; +import { ToolbarProps } from 'react-big-calendar'; +import { ReactComponent as LeftArrow } from '$icons/16x/arrow_left.svg'; +import { ReactComponent as RightArrow } from '$icons/16x/arrow_right.svg'; +import { ReactComponent as DownArrow } from '$icons/16x/arrow_down.svg'; + +import { useTranslation } from 'react-i18next'; + +export function Toolbar({ + onNavigate, + date, + emptyEvents, +}: ToolbarProps & { + emptyEvents: CalendarEvent[]; +}) { + const dateStr = useMemo(() => dayjs(date).format('MMM YYYY'), [date]); + const { t } = useTranslation(); + + return ( +
+
{dateStr}
+
+ onNavigate('PREV')}> + + + + onNavigate('NEXT')}> + + + + +
+
+ ); +} + +export default Toolbar; diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts new file mode 100644 index 0000000000..7c6430332b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/index.ts @@ -0,0 +1 @@ +export * from './Toolbar'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts index bd13ef29d0..1c82465a84 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts +++ b/frontend/appflowy_web_app/src/components/database/components/cell/cell.type.ts @@ -81,4 +81,6 @@ export interface CellProps { fieldId: FieldId; style?: React.CSSProperties; readOnly?: boolean; + placeholder?: string; + className?: string; } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx index 3eaa8254a4..fa6133bad7 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx @@ -13,7 +13,7 @@ export function ChecklistCell({ cell, style }: CellProps) { if (!data || !options || !selectedOptions) return null; return ( -
+
); diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx index bc90a9fa7a..324737df4d 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/date/DateTimeCell.tsx @@ -3,7 +3,7 @@ import { CellProps, DateTimeCell as DateTimeCellType } from '@/components/databa import React, { useMemo } from 'react'; import { ReactComponent as ReminderSvg } from '$icons/16x/clock_alarm.svg'; -export function DateTimeCell({ cell, fieldId, style }: CellProps) { +export function DateTimeCell({ cell, fieldId, style, placeholder }: CellProps) { const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); const startDateTime = useMemo(() => { @@ -25,7 +25,12 @@ export function DateTimeCell({ cell, fieldId, style }: CellProps + {placeholder} +
+ ) : null; return (
{hasReminder && } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx index 4d6ce6d44a..56ac39ef8d 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/number/NumberCell.tsx @@ -3,7 +3,7 @@ import { CellProps, NumberCell as NumberCellType } from '@/components/database/c import React, { useMemo } from 'react'; import Decimal from 'decimal.js'; -export function NumberCell({ cell, fieldId, style }: CellProps) { +export function NumberCell({ cell, fieldId, style, placeholder }: CellProps) { const { field } = useFieldSelector(fieldId); const format = useMemo(() => (field ? parseNumberTypeOptions(field).format : NumberFormat.Num), [field]); @@ -22,7 +22,12 @@ export function NumberCell({ cell, fieldId, style }: CellProps) return numberFormater(new Decimal(cell.data).toNumber()); }, [cell, format]); - if (value === undefined) return null; + if (value === undefined) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; return (
{value} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx index 3545bc026b..5bd8eb7b87 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationCell.tsx @@ -1,7 +1,13 @@ import { CellProps, RelationCell as RelationCellType } from '@/components/database/components/cell/cell.type'; import RelationItems from '@/components/database/components/cell/relation/RelationItems'; +import React from 'react'; -export function RelationCell({ cell, fieldId, style }: CellProps) { - if (!cell?.data) return null; +export function RelationCell({ cell, fieldId, style, placeholder }: CellProps) { + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; return ; } diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx index bd7038a8a7..60db55b3cb 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx @@ -4,7 +4,7 @@ import { SelectOptionColorMap } from '@/components/database/components/cell/cell import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/components/database/components/cell/cell.type'; import React, { useCallback, useMemo } from 'react'; -export function SelectOptionCell({ cell, fieldId, style }: CellProps) { +export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps) { const selectOptionIds = useMemo(() => cell?.data.split(','), [cell]); const { field } = useFieldSelector(fieldId); const typeOption = useMemo(() => { @@ -18,12 +18,17 @@ export function SelectOptionCell({ cell, fieldId, style }: CellProps option.id === id); if (!option) return null; - return ; + return ; }), [typeOption] ); - if (!typeOption || !selectOptionIds?.length) return null; + if (!typeOption || !selectOptionIds?.length) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; return (
diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx index 97de0c0fdb..4829e9e1f6 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx @@ -3,13 +3,13 @@ import { CellProps, UrlCell as UrlCellType } from '@/components/database/compone import { openUrl, processUrl } from '@/utils/url'; import React, { useMemo } from 'react'; -export function UrlCell({ cell, style }: CellProps) { +export function UrlCell({ cell, style, placeholder }: CellProps) { const readOnly = useReadOnly(); const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]); const className = useMemo(() => { - const classList = ['select-text']; + const classList = ['select-text', 'w-fit']; if (isUrl) { classList.push('text-content-blue-400', 'underline', 'cursor-pointer'); @@ -20,7 +20,12 @@ export function UrlCell({ cell, style }: CellProps) { return classList.join(' '); }, [isUrl]); - if (!cell?.data) return null; + if (!cell?.data) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; return (
{ + switch (fieldType) { + case FieldType.URL: + return UrlCell; + case FieldType.Number: + return NumberCell; + case FieldType.Checkbox: + return CheckboxCell; + case FieldType.SingleSelect: + case FieldType.MultiSelect: + return SelectOptionCell; + case FieldType.DateTime: + return DateTimeCell; + case FieldType.Checklist: + return ChecklistCell; + case FieldType.Relation: + return RelationCell; + default: + return TextProperty; + } + }, [fieldType]) as FC>; + + const style = useMemo( + () => ({ + fontSize: '12px', + }), + [] + ); + + if (fieldType === FieldType.RichText) { + return ; + } + + if (fieldType === FieldType.CreatedTime || fieldType === FieldType.LastEditedTime) { + const attrName = fieldType === FieldType.CreatedTime ? YjsDatabaseKey.created_at : YjsDatabaseKey.last_modified; + + return ( + + + + ); + } + + return ( + + + + ); +} + +export default Property; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx new file mode 100644 index 0000000000..48dc80764c --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx @@ -0,0 +1,15 @@ +import { FieldDisplay } from '@/components/database/components/field'; +import React from 'react'; + +function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) { + return ( +
+
+ +
+
{children}
+
+ ); +} + +export default PropertyWrapper; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/index.ts b/frontend/appflowy_web_app/src/components/database/components/property/index.ts new file mode 100644 index 0000000000..1a4ad04e85 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/index.ts @@ -0,0 +1 @@ +export * from './Property'; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx b/frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx new file mode 100644 index 0000000000..64589c0ea3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/text/TextProperty.tsx @@ -0,0 +1,29 @@ +import { CellProps, TextCell } from '@/components/database/components/cell/cell.type'; +import { TextField } from '@mui/material'; +import React from 'react'; + +export function TextProperty({ cell }: CellProps) { + return ( + + ); +} + +export default TextProperty; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/text/index.ts b/frontend/appflowy_web_app/src/components/database/components/property/text/index.ts new file mode 100644 index 0000000000..c10e4ed3d0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/text/index.ts @@ -0,0 +1 @@ +export * from './TextProperty'; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx index e7d78e7033..f9b79efcff 100644 --- a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -69,7 +69,7 @@ export const DatabaseTabs = forwardRef( value={isSelected ? selectedViewId : objectId} onChange={handleChange} > - {viewIds.map((viewId, index) => { + {viewIds.map((viewId) => { const view = getFolderView(viewId); if (!view) return null; @@ -80,9 +80,6 @@ export const DatabaseTabs = forwardRef( return ( } iconPosition='start' color='inherit' diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss index fceeab367c..5d56c04f0c 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -45,7 +45,7 @@ opacity: 60%; } -.workspaces, .database-conditions, .grid-scroll-table, .grid-board { +.workspaces, .database-conditions, .grid-scroll-table, .grid-board, .MuiPaper-root, .appflowy-database { ::-webkit-scrollbar { width: 0; height: 0; @@ -79,3 +79,22 @@ @apply items-center; } } + +.tooltip-arrow { + overflow: hidden; + position: absolute; + width: 1em; + height: 0.71em; + color: var(--bg-body); + + &:before { + content: '""'; + margin: auto; + display: block; + width: 100%; + height: 100%; + boxShadow: var(--shadow); + backgroundColor: var(--bg-body); + transform: rotate(45deg); + } +} \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 0ff64fedb1..a35c95eb4a 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1266,7 +1266,13 @@ "today": "Today", "jumpToday": "Jump to Today", "previousMonth": "Previous Month", - "nextMonth": "Next Month" + "nextMonth": "Next Month", + "views": { + "day": "Day", + "week": "Week", + "month": "Month", + "year": "Year" + } }, "mobileEventScreen": { "emptyTitle": "No events yet", @@ -1286,7 +1292,8 @@ }, "unscheduledEventsTitle": "Unscheduled events", "clickToAdd": "Click to add to the calendar", - "name": "Calendar settings" + "name": "Calendar settings", + "clickToOpen": "Click to open the record" }, "referencedCalendarPrefix": "View of", "quickJumpYear": "Jump to", From 9a5dbbb3cec1449f4710bfc60b5c5249d41968fc Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Thu, 23 May 2024 11:07:09 +0800 Subject: [PATCH 22/30] chore: upgrade to Flutter 3.22.0 (#5395) * chore: upgrade flutter to 3.22.0 * chore: upgrade editor version * chore: upgrade CI files * fix: failed tests --- .github/workflows/android_ci.yaml.bak | 2 +- .github/workflows/flutter_ci.yaml | 2 +- .github/workflows/ios_ci.yaml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/rust_coverage.yml | 2 +- CHANGELOG.md | 2 +- .../document/document_alignment_test.dart | 3 + .../document_inline_page_reference_test.dart | 2 + .../integration_test/shared/keyboard.dart | 8 +++ frontend/appflowy_flutter/ios/Podfile.lock | 2 +- .../lib/flutter/af_dropdown_menu.dart | 14 ++-- .../base/view_page/app_bar_buttons.dart | 5 +- .../bottom_sheet_action_widget.dart | 3 +- .../default_mobile_action_pane.dart | 3 +- .../database/board/mobile_board_page.dart | 3 +- .../board/widgets/mobile_board_trailing.dart | 10 +-- .../widgets/mobile_hidden_groups_column.dart | 3 +- .../mobile_card_detail_screen.dart | 3 +- .../widgets/mobile_create_field_button.dart | 6 +- .../field/mobile_field_bottom_sheets.dart | 3 +- .../view/database_sort_bottom_sheet.dart | 2 +- .../database/view/database_view_list.dart | 3 +- .../view/edit_database_view_screen.dart | 1 - .../home/mobile_home_trash_page.dart | 2 +- .../mobile_home_recent_views.dart | 6 +- .../setting/appearance/rtl_setting.dart | 1 - .../appearance/text_scale_setting.dart | 1 - .../setting/appearance/theme_setting.dart | 1 - .../flowy_mobile_quick_action_button.dart | 2 +- .../lib/plugins/base/icon/icon_picker.dart | 2 +- .../board/presentation/board_page.dart | 3 +- .../calendar/presentation/calendar_day.dart | 6 +- .../presentation/calendar_event_card.dart | 4 +- .../calendar/presentation/calendar_page.dart | 4 +- .../grid/presentation/widgets/mobile_fab.dart | 4 +- .../tab_bar/mobile/mobile_tab_bar_header.dart | 13 ++-- .../mobile_grid_relation_cell.dart | 1 - .../mobile_grid/mobile_grid_url_cell.dart | 3 +- .../mobile_row_detail_checkbox_cell.dart | 5 +- .../mobile_row_detail_checklist_cell.dart | 5 +- .../mobile_row_detail_relation_cell.dart | 1 - .../mobile_row_detail_url_cell.dart | 6 +- .../cell_editor/checklist_cell_editor.dart | 18 ++--- .../cell_editor/relation_cell_editor.dart | 21 +++--- .../select_option_cell_editor.dart | 4 +- .../database/widgets/row/row_property.dart | 6 +- .../setting/mobile_database_controls.dart | 4 +- .../plugins/document/presentation/banner.dart | 2 +- .../document/presentation/editor_page.dart | 2 +- .../actions/mobile_block_action_buttons.dart | 1 - .../editor_plugins/actions/option_action.dart | 2 +- .../code_block_language_selector.dart | 6 +- .../error/error_block_component_builder.dart | 2 +- .../image/image_placeholder.dart | 2 +- .../image/unsupport_image_widget.dart | 2 +- .../image/upload_image_menu.dart | 2 +- .../inline_math_equation.dart | 2 +- .../math_equation_block_component.dart | 2 +- .../mobile_toolbar_item/utils.dart | 2 - .../aa_menu/_color_list.dart | 2 - .../page_style/_page_style_cover_image.dart | 5 +- .../page_style/_page_style_icon.dart | 4 +- .../page_style/_page_style_layout.dart | 4 +- .../table/table_option_action.dart | 2 +- .../document/presentation/editor_style.dart | 24 ++++--- .../lib/shared/window_title_bar.dart | 2 +- .../widgets/third_party_sign_in_buttons.dart | 8 +-- .../settings/appearance/base_appearance.dart | 11 ++- .../appearance/desktop_appearance.dart | 10 +-- .../appearance/mobile_appearance.dart | 69 ++++++++++--------- .../home/menu/sidebar/sidebar.dart | 2 +- .../presentation/home/tabs/flowy_tab.dart | 2 +- .../presentation/home/tabs/tabs_manager.dart | 2 +- .../workspace/presentation/home/toast.dart | 2 +- .../widgets/inbox_action_bar.dart | 14 ++-- .../pages/settings_workspace_view.dart | 4 +- .../shared/af_dropdown_menu_entry.dart | 8 +-- .../shared/setting_value_dropdown.dart | 6 +- .../settings/shared/settings_dropdown.dart | 10 +-- .../src/flowy_emoji_picker_config.dart | 2 +- .../widgets/setting_appflowy_cloud.dart | 3 +- .../settings/widgets/setting_cloud.dart | 7 +- .../widgets/setting_supabase_cloud.dart | 10 +-- .../settings/widgets/settings_menu.dart | 2 +- .../theme_upload/theme_upload_decoration.dart | 5 +- .../theme_upload_failure_widget.dart | 3 +- .../theme_upload_learn_more_button.dart | 3 +- .../theme_upload_loading_widget.dart | 2 +- .../theme_upload/upload_new_theme_widget.dart | 5 +- .../presentation/widgets/dialogs.dart | 2 +- .../flowy_infra/lib/theme_extension.dart | 14 ++++ .../flowy_infra_ui/example/pubspec.yaml | 2 +- .../flowy_infra_ui/lib/flowy_infra_ui.dart | 25 +++---- .../lib/style_widget/button.dart | 20 +++--- .../lib/style_widget/snap_bar.dart | 2 +- .../flowy_infra_ui/lib/widget/error_page.dart | 3 +- .../lib/widget/rounded_button.dart | 2 +- frontend/appflowy_flutter/pubspec.lock | 52 +++++++------- frontend/appflowy_flutter/pubspec.yaml | 8 +-- frontend/scripts/docker-buildfiles/Dockerfile | 2 +- .../scripts/install_dev_env/install_ios.sh | 12 ++-- .../scripts/install_dev_env/install_linux.sh | 12 ++-- .../scripts/install_dev_env/install_macos.sh | 12 ++-- .../install_dev_env/install_windows.sh | 12 ++-- 104 files changed, 340 insertions(+), 309 deletions(-) diff --git a/.github/workflows/android_ci.yaml.bak b/.github/workflows/android_ci.yaml.bak index 8732558927..0cb110bae4 100644 --- a/.github/workflows/android_ci.yaml.bak +++ b/.github/workflows/android_ci.yaml.bak @@ -19,7 +19,7 @@ # env: # CARGO_TERM_COLOR: always -# FLUTTER_VERSION: "3.19.0" +# FLUTTER_VERSION: "3.22.0" # RUST_TOOLCHAIN: "1.77.2" # CARGO_MAKE_VERSION: "0.36.6" diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index 6ec1c76682..b944187ec4 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -25,7 +25,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.19.0" + FLUTTER_VERSION: "3.22.0" RUST_TOOLCHAIN: "1.77.2" CARGO_MAKE_VERSION: "0.36.6" diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml index c32a7f93c7..d24eaed1f3 100644 --- a/.github/workflows/ios_ci.yaml +++ b/.github/workflows/ios_ci.yaml @@ -20,7 +20,7 @@ on: - "!frontend/appflowy_web_app/**" env: - FLUTTER_VERSION: "3.19.0" + FLUTTER_VERSION: "3.22.0" RUST_TOOLCHAIN: "1.77.2" concurrency: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee065c6e9e..e91d95f969 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - "*" env: - FLUTTER_VERSION: "3.19.0" + FLUTTER_VERSION: "3.22.0" RUST_TOOLCHAIN: "1.77.2" jobs: diff --git a/.github/workflows/rust_coverage.yml b/.github/workflows/rust_coverage.yml index 4d8e9cbad8..12e728698f 100644 --- a/.github/workflows/rust_coverage.yml +++ b/.github/workflows/rust_coverage.yml @@ -10,7 +10,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.19.0" + FLUTTER_VERSION: "3.22.0" RUST_TOOLCHAIN: "1.77.2" jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index e612d832f7..0cc8d05b68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,7 +105,7 @@ - Fixed a bug where newly created rows were not being automatically sorted. - Fixed issues related to deleting a sorting field or sort not removing existing sorts properly. ### Notes -- Windows 7, Windows 8, and iOS 11 are not yet supported due to the upgrade to Flutter 3.19.0. +- Windows 7, Windows 8, and iOS 11 are not yet supported due to the upgrade to Flutter 3.22.0. ## Version 0.4.9 - 02/17/2024 ### Bug Fixes diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart index de7401652e..d95d907881 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_alignment_test.dart @@ -66,6 +66,7 @@ void main() { LogicalKeyboardKey.keyR, ], tester: tester, + withKeyUp: true, ); expect(first.attributes[blockComponentAlign], rightAlignmentKey); @@ -77,6 +78,7 @@ void main() { LogicalKeyboardKey.keyE, ], tester: tester, + withKeyUp: true, ); expect(first.attributes[blockComponentAlign], centerAlignmentKey); @@ -88,6 +90,7 @@ void main() { LogicalKeyboardKey.keyL, ], tester: tester, + withKeyUp: true, ); expect(first.attributes[blockComponentAlign], leftAlignmentKey); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart index 0bbd64c82b..f169910840 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_inline_page_reference_test.dart @@ -111,6 +111,7 @@ Future triggerReferenceDocumentBySlashMenu(WidgetTester tester) async { LogicalKeyboardKey.enter, ], tester: tester, + withKeyUp: true, ); await tester.pumpAndSettle(); @@ -129,6 +130,7 @@ Future enterDocumentText(WidgetTester tester) async { LogicalKeyboardKey.keyT, ], tester: tester, + withKeyUp: true, ); await tester.pumpAndSettle(); } diff --git a/frontend/appflowy_flutter/integration_test/shared/keyboard.dart b/frontend/appflowy_flutter/integration_test/shared/keyboard.dart index d792b92c66..567e7e548c 100644 --- a/frontend/appflowy_flutter/integration_test/shared/keyboard.dart +++ b/frontend/appflowy_flutter/integration_test/shared/keyboard.dart @@ -5,10 +5,18 @@ class FlowyTestKeyboard { static Future simulateKeyDownEvent( List keys, { required flutter_test.WidgetTester tester, + bool withKeyUp = false, }) async { for (final LogicalKeyboardKey key in keys) { await flutter_test.simulateKeyDownEvent(key); await tester.pumpAndSettle(); } + + if (withKeyUp) { + for (final LogicalKeyboardKey key in keys) { + await flutter_test.simulateKeyUpEvent(key); + await tester.pumpAndSettle(); + } + } } } diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index e62299792d..93a8eb77e1 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -173,7 +173,7 @@ SPEC CHECKSUMS: fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 - integration_test: 13825b8a9334a850581300559b8839134b124670 + integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9 keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c diff --git a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart index da9f4649c3..9d18bb14f7 100644 --- a/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart +++ b/frontend/appflowy_flutter/lib/flutter/af_dropdown_menu.dart @@ -481,7 +481,7 @@ class _AFDropdownMenuState extends State> { ButtonStyle effectiveStyle = entry.style ?? defaultStyle; final Color focusedBackgroundColor = effectiveStyle.foregroundColor - ?.resolve({MaterialState.focused}) ?? + ?.resolve({WidgetState.focused}) ?? Theme.of(context).colorScheme.onSurface; Widget label = entry.labelWidget ?? Text(entry.label); @@ -499,7 +499,7 @@ class _AFDropdownMenuState extends State> { // color will also change to foregroundColor.withOpacity(0.12). effectiveStyle = entry.enabled && i == focusedIndex ? effectiveStyle.copyWith( - backgroundColor: MaterialStatePropertyAll( + backgroundColor: WidgetStatePropertyAll( focusedBackgroundColor.withOpacity(0.12), ), ) @@ -628,17 +628,17 @@ class _AFDropdownMenuState extends State> { final double? anchorWidth = getWidth(_anchorKey); if (widget.width != null) { effectiveMenuStyle = effectiveMenuStyle.copyWith( - minimumSize: MaterialStatePropertyAll(Size(widget.width!, 0.0)), + minimumSize: WidgetStatePropertyAll(Size(widget.width!, 0.0)), ); } else if (anchorWidth != null) { effectiveMenuStyle = effectiveMenuStyle.copyWith( - minimumSize: MaterialStatePropertyAll(Size(anchorWidth, 0.0)), + minimumSize: WidgetStatePropertyAll(Size(anchorWidth, 0.0)), ); } if (widget.menuHeight != null) { effectiveMenuStyle = effectiveMenuStyle.copyWith( - maximumSize: MaterialStatePropertyAll( + maximumSize: WidgetStatePropertyAll( Size(double.infinity, widget.menuHeight!), ), ); @@ -1029,8 +1029,8 @@ class _DropdownMenuDefaultsM3 extends DropdownMenuThemeData { @override MenuStyle get menuStyle { return const MenuStyle( - minimumSize: MaterialStatePropertyAll(Size(_kMinimumWidth, 0.0)), - maximumSize: MaterialStatePropertyAll(Size.infinite), + minimumSize: WidgetStatePropertyAll(Size(_kMinimumWidth, 0.0)), + maximumSize: WidgetStatePropertyAll(Size.infinite), visualDensity: VisualDensity.standard, ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart index d712aa5aec..1301719c41 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/app_bar_buttons.dart @@ -12,6 +12,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -92,7 +93,7 @@ class MobileViewPageMoreButton extends StatelessWidget { context, showDragHandle: true, showDivider: false, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) => MultiBlocProvider( providers: [ BlocProvider.value(value: context.read()), @@ -144,7 +145,7 @@ class MobileViewPageLayoutButton extends StatelessWidget { showDoneButton: true, showHeader: true, title: LocaleKeys.pageStyle_title.tr(), - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) => MultiBlocProvider( providers: [ BlocProvider.value(value: context.read()), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart index 3e594b47f9..6b54b1fda3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_action_widget.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -19,7 +20,7 @@ class BottomSheetActionWidget extends StatelessWidget { @override Widget build(BuildContext context) { final iconColor = - this.iconColor ?? Theme.of(context).colorScheme.onBackground; + this.iconColor ?? AFThemeExtension.of(context).onBackground; if (svg == null) { return OutlinedButton( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart index f27c5b3b6f..80330de3c7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart @@ -3,6 +3,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -54,7 +55,7 @@ enum MobilePaneActionType { context, showDragHandle: true, showDivider: false, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, useRootNavigator: true, builder: (context) { return MultiBlocProvider( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart index 7938f47462..a1fc2a70a3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart @@ -15,6 +15,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -265,7 +266,7 @@ class _BoardContentState extends State<_BoardContent> { BoxDecoration _makeBoxDecoration(BuildContext context) { final themeMode = context.read().state.themeMode; return BoxDecoration( - color: Theme.of(context).colorScheme.background, + color: AFThemeExtension.of(context).background, borderRadius: const BorderRadius.all(Radius.circular(8)), border: themeMode == ThemeMode.light ? Border.fromBorderSide( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart index 76deaa6f0a..184bd901c1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart @@ -60,7 +60,7 @@ class _MobileBoardTrailingState extends State { child: IconButton( icon: Icon( Icons.close, - color: style.colorScheme.onBackground, + color: style.colorScheme.onSurface, ), onPressed: () => setState(() => _textController.clear()), @@ -86,7 +86,7 @@ class _MobileBoardTrailingState extends State { child: Text( LocaleKeys.button_cancel.tr(), style: style.textTheme.titleSmall?.copyWith( - color: style.colorScheme.onBackground, + color: style.colorScheme.onSurface, ), ), onPressed: () => setState(() => isEditing = false), @@ -96,7 +96,7 @@ class _MobileBoardTrailingState extends State { LocaleKeys.button_add.tr(), style: style.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.bold, - color: style.colorScheme.onBackground, + color: style.colorScheme.onSurface, ), ), onPressed: () { @@ -117,14 +117,14 @@ class _MobileBoardTrailingState extends State { ) : ElevatedButton.icon( style: ElevatedButton.styleFrom( - foregroundColor: style.colorScheme.onBackground, + foregroundColor: style.colorScheme.onSurface, backgroundColor: style.colorScheme.secondary, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ).copyWith( overlayColor: - MaterialStateProperty.all(Theme.of(context).hoverColor), + WidgetStateProperty.all(Theme.of(context).hoverColor), ), icon: const Icon(Icons.add), label: Text( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart index 2e2367b9bb..0b0c16f951 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart @@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_c import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -168,7 +169,7 @@ class MobileHiddenGroup extends StatelessWidget { return TextButton( style: TextButton.styleFrom( textStyle: Theme.of(context).textTheme.bodyMedium, - foregroundColor: Theme.of(context).colorScheme.onBackground, + foregroundColor: AFThemeExtension.of(context).onBackground, visualDensity: VisualDensity.compact, ), child: CardCellBuilder( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart index e3cad565ab..08f6c0b48b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart @@ -20,6 +20,7 @@ import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart' import 'package:appflowy/plugins/database/widgets/row/row_property.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -131,7 +132,7 @@ class _MobileRowDetailPageState extends State { void _showCardActions(BuildContext context) { showMobileBottomSheet( context, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, showDragHandle: true, builder: (_) => Column( mainAxisSize: MainAxisSize.min, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart index e62ddeb872..cc6c9b43aa 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart @@ -22,17 +22,17 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget { constraints: const BoxConstraints(minWidth: double.infinity), child: TextButton.icon( style: Theme.of(context).textButtonTheme.style?.copyWith( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), ), - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( Theme.of(context).hoverColor, ), alignment: AlignmentDirectional.centerStart, splashFactory: NoSplash.splashFactory, - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( EdgeInsets.symmetric(vertical: 14, horizontal: 6), ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart index a4ef722ea7..266a06de7f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -40,7 +41,7 @@ Future showFieldTypeGridBottomSheet( showCloseButton: true, elevation: 20, title: title, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, enableDraggableScrollable: true, builder: (context) { final typeOptionMenuItemValue = mobileSupportedFieldTypes diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart index 909018d1b1..8dd224390c 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_sort_bottom_sheet.dart @@ -438,7 +438,7 @@ class _SortDetailContent extends StatelessWidget { color: Theme.of(context).colorScheme.surface, ), splashFactory: NoSplash.splashFactory, - overlayColor: const MaterialStatePropertyAll( + overlayColor: const WidgetStatePropertyAll( Colors.transparent, ), onTap: (index) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart index 9ef8ddefb1..763da36918 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_list.dart @@ -11,6 +11,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -183,7 +184,7 @@ class MobileDatabaseViewListButton extends StatelessWidget { showMobileBottomSheet( context, showDragHandle: true, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return BlocProvider( create: (_) => diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart index a2771ece26..4d8acbbeba 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart @@ -234,7 +234,6 @@ class DatabaseViewSettingTile extends StatelessWidget { showHeader: true, showBackButton: true, title: LocaleKeys.grid_settings_properties.tr(), - showDivider: true, builder: (_) { return BlocProvider.value( value: context.read(), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart index a9c2f1b933..2ee9e14175 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart @@ -178,7 +178,7 @@ class _DeletedFilesListView extends StatelessWidget { title: Text( deletedFile.name, style: theme.textTheme.labelMedium - ?.copyWith(color: theme.colorScheme.onBackground), + ?.copyWith(color: theme.colorScheme.onSurface), ), horizontalTitleGap: 0, tileColor: theme.colorScheme.onSurface.withOpacity(0.1), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart index 4dc9f28155..fa585903f3 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -9,7 +7,9 @@ import 'package:appflowy/workspace/application/recent/prelude.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -91,7 +91,7 @@ class _RecentViews extends StatelessWidget { context, showDivider: false, showDragHandle: true, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return Column( children: [ diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart index 39a0fdae4c..e61281c5c4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/rtl_setting.dart @@ -37,7 +37,6 @@ class RTLSetting extends StatelessWidget { showHeader: true, showDragHandle: true, showDivider: false, - showCloseButton: false, title: LocaleKeys.settings_appearance_textDirection_label.tr(), builder: (context) { final layoutDirection = diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart index e3526c3df0..3bdb836a71 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/text_scale_setting.dart @@ -45,7 +45,6 @@ class TextScaleSetting extends StatelessWidget { showHeader: true, showDragHandle: true, showDivider: false, - showCloseButton: false, title: LocaleKeys.settings_appearance_fontScaleFactor.tr(), builder: (context) { return FontSizeStepper( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart index 1291804af6..8893eab105 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/appearance/theme_setting.dart @@ -38,7 +38,6 @@ class ThemeSetting extends StatelessWidget { showHeader: true, showDragHandle: true, showDivider: false, - showCloseButton: false, title: LocaleKeys.settings_appearance_themeMode_label.tr(), builder: (context) { final themeMode = diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart index 17b61849da..ba2d2b00cc 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart @@ -29,7 +29,7 @@ class MobileQuickActionButton extends StatelessWidget { onTap: enable ? onTap : null, borderRadius: BorderRadius.circular(12), overlayColor: - enable ? null : const MaterialStatePropertyAll(Colors.transparent), + enable ? null : const WidgetStatePropertyAll(Colors.transparent), splashColor: Colors.transparent, child: Container( height: 44, diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart index a77b4b2f27..1ecea2b787 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -92,7 +92,7 @@ class FlowyIconPicker extends StatelessWidget { child: TabBar( indicatorSize: TabBarIndicatorSize.label, isScrollable: true, - overlayColor: MaterialStatePropertyAll( + overlayColor: WidgetStatePropertyAll( Theme.of(context).colorScheme.secondary, ), padding: EdgeInsets.zero, diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index 64415ff85b..a1f3a78f75 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -20,6 +20,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; @@ -671,7 +672,7 @@ class _BoardCardState extends State<_BoardCard> { ? const Color(0x0F1F2329) : const Color(0x0FEFF4FB), foregroundColorOnHover: - Theme.of(context).colorScheme.onBackground, + AFThemeExtension.of(context).onBackground, ), ), onStartEditing: () => diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart index 5d1a29141e..6dfa3313f5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart @@ -238,7 +238,7 @@ class NewEventButton extends StatelessWidget { child: FlowyIconButton( onPressed: onCreate, icon: const FlowySvg(FlowySvgs.add_s), - fillColor: Theme.of(context).colorScheme.background, + fillColor: Theme.of(context).colorScheme.surface, hoverColor: AFThemeExtension.of(context).lightGreyHover, width: 22, tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(), @@ -289,8 +289,8 @@ class _DayBadge extends StatelessWidget { @override Widget build(BuildContext context) { - Color dayTextColor = Theme.of(context).colorScheme.onBackground; - Color monthTextColor = Theme.of(context).colorScheme.onBackground; + Color dayTextColor = AFThemeExtension.of(context).onBackground; + Color monthTextColor = AFThemeExtension.of(context).onBackground; final String monthString = DateFormat("MMM ", context.locale.toLanguageTag()).format(date); final String dayString = date.day.toString(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart index 6baf2ecd2f..e105914908 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_event_card.dart @@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; @@ -15,7 +16,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../application/calendar_bloc.dart'; - import 'calendar_event_editor.dart'; class EventCard extends StatefulWidget { @@ -102,7 +102,7 @@ class _EventCardState extends State { hoverColor: Theme.of(context).brightness == Brightness.light ? const Color(0x0F1F2329) : const Color(0x0FEFF4FB), - foregroundColorOnHover: Theme.of(context).colorScheme.onBackground, + foregroundColorOnHover: AFThemeExtension.of(context).onBackground, ), ), onStartEditing: () {}, diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart index c38c7647ba..ff695eea0e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -21,12 +19,12 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../application/row/row_controller.dart'; import '../../widgets/row/row_detail.dart'; - import 'calendar_day.dart'; import 'layout/sizes.dart'; import 'toolbar/calendar_setting_bar.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart index 29af8e4355..7c18bb927f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/mobile_fab.dart @@ -43,7 +43,7 @@ Widget getGridFabs(BuildContext context) { .read() .add(const GridEvent.createRow(openRowDetail: true)); }, - overlayColor: const MaterialStatePropertyAll(Color(0xFF009FD1)), + overlayColor: const WidgetStatePropertyAll(Color(0xFF009FD1)), boxShadow: const BoxShadow( offset: Offset(0, 8), color: Color(0x6612BFEF), @@ -75,7 +75,7 @@ class MobileGridFab extends StatelessWidget { final VoidCallback onTap; final FlowySvgData icon; final Size iconSize; - final MaterialStateProperty? overlayColor; + final WidgetStateProperty? overlayColor; @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart index 55394ec33c..ddddd90fb4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart @@ -77,22 +77,22 @@ class _DatabaseViewSelectorButton extends StatelessWidget { return TextButton( style: ButtonStyle( - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( EdgeInsets.fromLTRB(12, 8, 8, 8), ), - maximumSize: const MaterialStatePropertyAll(Size(200, 48)), - minimumSize: const MaterialStatePropertyAll(Size(48, 0)), - shape: const MaterialStatePropertyAll( + maximumSize: const WidgetStatePropertyAll(Size(200, 48)), + minimumSize: const WidgetStatePropertyAll(Size(48, 0)), + shape: const WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(12)), ), ), - backgroundColor: MaterialStatePropertyAll( + backgroundColor: WidgetStatePropertyAll( Theme.of(context).brightness == Brightness.light ? const Color(0x0F212729) : const Color(0x0FFFFFFF), ), - overlayColor: MaterialStatePropertyAll( + overlayColor: WidgetStatePropertyAll( Theme.of(context).colorScheme.secondary, ), ), @@ -119,7 +119,6 @@ class _DatabaseViewSelectorButton extends StatelessWidget { showTransitionMobileBottomSheet( context, showDivider: false, - initialStop: 1.0, builder: (_) { return MultiBlocProvider( providers: [ diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart index 0e411440ef..5c31d41b30 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_relation_cell.dart @@ -42,7 +42,6 @@ class MobileGridRelationCellSkin extends IEditableRelationCellSkin { onTap: () { showMobileBottomSheet( context, - padding: EdgeInsets.zero, backgroundColor: Theme.of(context).colorScheme.secondaryContainer, builder: (context) { return const FlowyText("Coming soon"); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart index 4dffae3022..cddb821943 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart @@ -2,6 +2,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_she import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -51,7 +52,7 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin { showMobileBottomSheet( context, showDragHandle: true, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (context) => BlocProvider.value( value: bloc, child: MobileURLEditor( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart index 23b5c31c75..2e9e4b1a24 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checkbox_cell.dart @@ -1,6 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import '../editable_cell_skeleton/checkbox.dart'; @@ -31,7 +32,7 @@ class MobileRowDetailCheckboxCellSkin extends IEditableCheckboxCellSkin { alignment: AlignmentDirectional.centerStart, child: FlowySvg( state.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, blendMode: BlendMode.dst, size: const Size.square(24), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart index dd981795d2..67f9f1c53f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_checklist_cell.dart @@ -1,11 +1,12 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; -import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/checklist_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/mobile_checklist_cell_editor.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -25,7 +26,7 @@ class MobileRowDetailChecklistCellSkin extends IEditableChecklistCellSkin { borderRadius: const BorderRadius.all(Radius.circular(14)), onTap: () => showMobileBottomSheet( context, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (context) { return BlocProvider.value( value: bloc, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart index eebb3e1c75..cdbcef64c7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_relation_cell.dart @@ -19,7 +19,6 @@ class MobileRowDetailRelationCellSkin extends IEditableRelationCellSkin { borderRadius: const BorderRadius.all(Radius.circular(14)), onTap: () => showMobileBottomSheet( context, - padding: EdgeInsets.zero, builder: (context) { return const FlowyText("Coming soon"); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart index f97eabe830..f87b225492 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart @@ -1,12 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_infra/theme_extension.dart'; import '../editable_cell_skeleton/url.dart'; @@ -28,7 +28,7 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { onTap: () => showMobileBottomSheet( context, showDragHandle: true, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return BlocProvider.value( value: bloc, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart index 3ab883329a..18c9428ede 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart @@ -423,7 +423,7 @@ class _DeleteTaskButton extends StatefulWidget { } class _DeleteTaskButtonState extends State<_DeleteTaskButton> { - final _materialStatesController = MaterialStatesController(); + final _materialStatesController = WidgetStatesController(); @override void dispose() { @@ -438,16 +438,16 @@ class _DeleteTaskButtonState extends State<_DeleteTaskButton> { onHover: (_) => setState(() {}), onFocusChange: (_) => setState(() {}), style: ButtonStyle( - fixedSize: const MaterialStatePropertyAll(Size.square(32)), - minimumSize: const MaterialStatePropertyAll(Size.square(32)), - maximumSize: const MaterialStatePropertyAll(Size.square(32)), - overlayColor: MaterialStateProperty.resolveWith((state) { - if (state.contains(MaterialState.focused)) { + fixedSize: const WidgetStatePropertyAll(Size.square(32)), + minimumSize: const WidgetStatePropertyAll(Size.square(32)), + maximumSize: const WidgetStatePropertyAll(Size.square(32)), + overlayColor: WidgetStateProperty.resolveWith((state) { + if (state.contains(WidgetState.focused)) { return AFThemeExtension.of(context).greyHover; } return Colors.transparent; }), - shape: const MaterialStatePropertyAll( + shape: const WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: Corners.s6Border), ), ), @@ -455,8 +455,8 @@ class _DeleteTaskButtonState extends State<_DeleteTaskButton> { child: FlowySvg( FlowySvgs.delete_s, color: _materialStatesController.value - .contains(MaterialState.hovered) || - _materialStatesController.value.contains(MaterialState.focused) + .contains(WidgetState.hovered) || + _materialStatesController.value.contains(WidgetState.focused) ? Theme.of(context).colorScheme.error : null, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart index 70383361d7..63ea44008e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -422,7 +422,7 @@ class _UnselectRowButton extends StatefulWidget { } class _UnselectRowButtonState extends State<_UnselectRowButton> { - final _materialStatesController = MaterialStatesController(); + final _materialStatesController = WidgetStatesController(); @override void dispose() { @@ -437,26 +437,25 @@ class _UnselectRowButtonState extends State<_UnselectRowButton> { onHover: (_) => setState(() {}), onFocusChange: (_) => setState(() {}), style: ButtonStyle( - fixedSize: const MaterialStatePropertyAll(Size.square(32)), - minimumSize: const MaterialStatePropertyAll(Size.square(32)), - maximumSize: const MaterialStatePropertyAll(Size.square(32)), - overlayColor: MaterialStateProperty.resolveWith((state) { - if (state.contains(MaterialState.focused)) { + fixedSize: const WidgetStatePropertyAll(Size.square(32)), + minimumSize: const WidgetStatePropertyAll(Size.square(32)), + maximumSize: const WidgetStatePropertyAll(Size.square(32)), + overlayColor: WidgetStateProperty.resolveWith((state) { + if (state.contains(WidgetState.focused)) { return AFThemeExtension.of(context).greyHover; } return Colors.transparent; }), - shape: const MaterialStatePropertyAll( + shape: const WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: Corners.s6Border), ), ), statesController: _materialStatesController, child: Container( - color: _materialStatesController.value - .contains(MaterialState.hovered) || - _materialStatesController.value.contains(MaterialState.focused) + color: _materialStatesController.value.contains(WidgetState.hovered) || + _materialStatesController.value.contains(WidgetState.focused) ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onBackground, + : AFThemeExtension.of(context).onBackground, width: 12, height: 1, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart index 3094c97887..d3a41c6c3f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart @@ -380,7 +380,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> { icon: FlowySvg( FlowySvgs.three_dots_s, size: const Size.square(16), - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), ), ], @@ -462,7 +462,7 @@ class SelectOptionTagCell extends StatelessWidget { child: FlowySvg( FlowySvgs.drag_element_s, size: const Size.square(14), - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart index 969c4e6e0c..8762918141 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart @@ -314,17 +314,17 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { constraints: const BoxConstraints(minWidth: double.infinity), child: TextButton.icon( style: Theme.of(context).textButtonTheme.style?.copyWith( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(12.0), ), ), - overlayColor: MaterialStateProperty.all( + overlayColor: WidgetStateProperty.all( Theme.of(context).hoverColor, ), alignment: AlignmentDirectional.centerStart, splashFactory: NoSplash.splashFactory, - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( EdgeInsets.symmetric(vertical: 14, horizontal: 6), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart index d5f3ce293a..f3a548932d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart @@ -9,6 +9,7 @@ import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -128,7 +129,6 @@ void _showDatabaseFieldListFromToolbar( showHeader: true, showBackButton: true, title: LocaleKeys.grid_settings_properties.tr(), - showDivider: true, builder: (_) { return BlocProvider.value( value: context.read(), @@ -150,7 +150,7 @@ void _showEditSortPanelFromToolbar( showDragHandle: true, showDivider: false, useSafeArea: false, - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return BlocProvider.value( value: context.read(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart index 3936bf6968..e5fd6b6b8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/banner.dart @@ -23,7 +23,7 @@ class DocumentBanner extends StatelessWidget { constraints: const BoxConstraints(minHeight: 60), child: Container( width: double.infinity, - color: colorScheme.surfaceVariant, + color: colorScheme.surfaceContainerHighest, child: FittedBox( fit: BoxFit.scaleDown, child: Row( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 0b7a473176..9642a4a8cc 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -437,7 +437,7 @@ class _AppFlowyEditorPageState extends State { Material( child: DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FindAndReplaceMenuWidget( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart index eb4487fa49..b80bb034a6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart @@ -74,7 +74,6 @@ class MobileBlockActionButtons extends StatelessWidget { context, showHeader: true, showCloseButton: true, - showDivider: true, showDragHandle: true, title: LocaleKeys.document_plugins_action.tr(), builder: (context) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart index 09906e1429..cfff8d4938 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -300,7 +300,7 @@ class ColorOptionAction extends PopoverActionCell { colors: colors, selected: selectedColor, border: Border.all( - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), onTap: (option, index) async { final transaction = editorState.transaction; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart index 323c15be1e..97ac54e9f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/base/string_extension.dart'; @@ -8,7 +6,9 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; CodeBlockLanguagePickerBuilder codeBlockLanguagePickerBuilder = ( @@ -70,7 +70,7 @@ class _CodeBlockLanguageSelectorState widget.language?.capitalize() ?? LocaleKeys.document_codeBlock_language_auto.tr(), constraints: const BoxConstraints(minWidth: 50), - fontColor: Theme.of(context).colorScheme.onBackground, + fontColor: AFThemeExtension.of(context).onBackground, padding: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4), fillColor: Colors.transparent, hoverColor: Theme.of(context).colorScheme.secondaryContainer, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart index d16c035115..ba6f01e908 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/error/error_block_component_builder.dart @@ -61,7 +61,7 @@ class _ErrorBlockComponentWidgetState extends State Widget build(BuildContext context) { Widget child = DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FlowyButton( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart index f193f91617..5d51c1b390 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart @@ -49,7 +49,7 @@ class ImagePlaceholderState extends State { Widget build(BuildContext context) { final Widget child = DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FlowyHover( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart index 3403a1ff31..017e5a94b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsupport_image_widget.dart @@ -14,7 +14,7 @@ class UnSupportImageWidget extends StatelessWidget { Widget build(BuildContext context) { return DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FlowyHover( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart index 4c9de6b07d..0679f87996 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart @@ -107,7 +107,7 @@ class _UploadImageMenuState extends State { }), indicatorSize: TabBarIndicatorSize.label, isScrollable: true, - overlayColor: MaterialStatePropertyAll( + overlayColor: WidgetStatePropertyAll( PlatformExtension.isDesktop ? Theme.of(context).colorScheme.secondary : Colors.transparent, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart index 543cee1207..7ce143acba 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart @@ -77,7 +77,7 @@ class _InlineMathEquationState extends State { ), fontSize: 14.0, color: widget.textStyle?.color ?? - theme.colorScheme.onBackground, + theme.colorScheme.onSurface, ), ), const HSpace(2), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index 56a9739531..c7d298ff09 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -124,7 +124,7 @@ class MathEquationBlockComponentWidgetState decoration: BoxDecoration( color: formula.isNotEmpty ? Colors.transparent - : Theme.of(context).colorScheme.surfaceVariant, + : Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), ), child: FlowyHover( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart index 21f3b7ea68..ad4d523812 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart @@ -10,8 +10,6 @@ Future showEditLinkBottomSheet( ) { return showMobileBottomSheet( context, - showHeader: false, - showCloseButton: false, showDragHandle: true, padding: const EdgeInsets.symmetric(horizontal: 16), builder: (context) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart index 2de13ed459..438aa1264b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart @@ -22,8 +22,6 @@ Future showTextColorAndBackgroundColorPicker( await showMobileBottomSheet( context, showHeader: true, - showCloseButton: false, - showDivider: true, showDragHandle: true, showDoneButton: true, barrierColor: Colors.transparent, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 214fb7456f..84d8c751f4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -26,6 +26,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:flowy_infra/theme_extension.dart'; class PageStyleCoverImage extends StatelessWidget { PageStyleCoverImage({ @@ -188,7 +189,7 @@ class PageStyleCoverImage extends StatelessWidget { ); }, title: LocaleKeys.pageStyle_presets.tr(), - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, builder: (_) { return BlocProvider.value( value: pageStyleBloc, @@ -266,7 +267,7 @@ class PageStyleCoverImage extends StatelessWidget { showHeader: true, showRemoveButton: true, title: LocaleKeys.pageStyle_unsplash.tr(), - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: AFThemeExtension.of(context).background, onRemove: () { pageStyleBloc.add( DocumentPageStyleEvent.updateCoverImage( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart index a3ccc974f7..434dba7337 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_util.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -77,8 +78,7 @@ class _PageStyleIconState extends State { showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_pageIcon.tr(), - backgroundColor: Theme.of(context).colorScheme.background, - isScrollControlled: true, + backgroundColor: AFThemeExtension.of(context).background, enableDraggableScrollable: true, minChildSize: 0.6, initialChildSize: 0.61, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart index 054f544c8f..211e287d15 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_layout.dart @@ -8,6 +8,7 @@ import 'package:appflowy/shared/feedback_gesture_detector.dart'; import 'package:appflowy/util/font_family_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -208,8 +209,7 @@ class _FontButton extends StatelessWidget { showDoneButton: true, showHeader: true, title: LocaleKeys.titleBar_font.tr(), - backgroundColor: Theme.of(context).colorScheme.background, - isScrollControlled: true, + backgroundColor: AFThemeExtension.of(context).background, enableDraggableScrollable: true, minChildSize: 0.6, initialChildSize: 0.61, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart index afb2b63f49..6d0597319c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_option_action.dart @@ -137,7 +137,7 @@ class TableColorOptionAction extends PopoverActionCell { colors: colors, selected: selectedColor, border: Border.all( - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), onTap: (option, index) async { final backgroundColor = diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart index 6196388bd1..271aa0e340 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_style.dart @@ -15,6 +15,7 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:collection/collection.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -42,6 +43,7 @@ class EditorStyleCustomizer { EditorStyle desktop() { final theme = Theme.of(context); + final afThemeExtension = AFThemeExtension.of(context); final appearanceFont = context.read().state.font; final appearance = context.read().state; final fontSize = appearance.fontSize; @@ -60,8 +62,7 @@ class EditorStyleCustomizer { textStyleConfiguration: TextStyleConfiguration( text: baseTextStyle(fontFamily).copyWith( fontSize: fontSize, - color: theme.colorScheme.onBackground, - height: 1.5, + color: afThemeExtension.onBackground, ), bold: baseTextStyle(fontFamily, fontWeight: FontWeight.bold).copyWith( fontWeight: FontWeight.w600, @@ -93,6 +94,7 @@ class EditorStyleCustomizer { } EditorStyle mobile() { + final afThemeExtension = AFThemeExtension.of(context); final pageStyle = context.read().state; final theme = Theme.of(context); final fontSize = pageStyle.fontLayout.fontSize; @@ -108,10 +110,10 @@ class EditorStyleCustomizer { padding: padding, defaultTextDirection: defaultTextDirection, textStyleConfiguration: TextStyleConfiguration( + lineHeight: lineHeight, text: baseTextStyle.copyWith( fontSize: fontSize, - color: theme.colorScheme.onBackground, - height: lineHeight, + color: afThemeExtension.onBackground, ), bold: baseTextStyle.copyWith(fontWeight: FontWeight.w600), italic: baseTextStyle.copyWith(fontStyle: FontStyle.italic), @@ -177,7 +179,7 @@ class EditorStyleCustomizer { return baseTextStyle(fontFamily).copyWith( fontSize: fontSize, height: 1.5, - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ); } @@ -187,16 +189,17 @@ class EditorStyleCustomizer { fontFamily: defaultFontFamily, fontSize: fontSize, height: 1.5, - color: Theme.of(context).colorScheme.onBackground.withOpacity(0.6), + color: AFThemeExtension.of(context).onBackground.withOpacity(0.6), ); } SelectionMenuStyle selectionMenuStyleBuilder() { final theme = Theme.of(context); + final afThemeExtension = AFThemeExtension.of(context); return SelectionMenuStyle( selectionMenuBackgroundColor: theme.cardColor, - selectionMenuItemTextColor: theme.colorScheme.onBackground, - selectionMenuItemIconColor: theme.colorScheme.onBackground, + selectionMenuItemTextColor: afThemeExtension.onBackground, + selectionMenuItemIconColor: afThemeExtension.onBackground, selectionMenuItemSelectedIconColor: theme.colorScheme.onSurface, selectionMenuItemSelectedTextColor: theme.colorScheme.onSurface, selectionMenuItemSelectedColor: theme.hoverColor, @@ -205,10 +208,11 @@ class EditorStyleCustomizer { InlineActionsMenuStyle inlineActionsMenuStyleBuilder() { final theme = Theme.of(context); + final afThemeExtension = AFThemeExtension.of(context); return InlineActionsMenuStyle( backgroundColor: theme.cardColor, - groupTextColor: theme.colorScheme.onBackground.withOpacity(.8), - menuItemTextColor: theme.colorScheme.onBackground, + groupTextColor: afThemeExtension.onBackground.withOpacity(.8), + menuItemTextColor: afThemeExtension.onBackground, menuItemSelectedColor: theme.colorScheme.secondary, menuItemSelectedTextColor: theme.colorScheme.onSurface, ); diff --git a/frontend/appflowy_flutter/lib/shared/window_title_bar.dart b/frontend/appflowy_flutter/lib/shared/window_title_bar.dart index 51a8b51c86..1640383588 100644 --- a/frontend/appflowy_flutter/lib/shared/window_title_bar.dart +++ b/frontend/appflowy_flutter/lib/shared/window_title_bar.dart @@ -73,7 +73,7 @@ class _WindowTitleBarState extends State { return Container( height: 40, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, ), child: DragToMoveArea( child: Row( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart index ce446320a3..09d24ecd83 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/sign_in_screen/widgets/third_party_sign_in_buttons.dart @@ -154,20 +154,20 @@ class _DesktopSignInButton extends StatelessWidget { ), ), style: ButtonStyle( - overlayColor: MaterialStateProperty.resolveWith( + overlayColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return style.colorScheme.onSecondaryContainer; } return null; }, ), - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( const RoundedRectangleBorder( borderRadius: Corners.s6Border, ), ), - side: MaterialStateProperty.all( + side: WidgetStateProperty.all( BorderSide( color: style.dividerColor, ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart index 2fba33263f..a70073236a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/shared/google_fonts_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; +import 'package:flutter/material.dart'; // the default font family is empty, so we can use the default font family of the platform // the system will choose the default font family of the platform @@ -20,10 +19,10 @@ const builtInCodeFontFamily = 'RobotoMono'; abstract class BaseAppearance { final white = const Color(0xFFFFFFFF); - final Set scrollbarInteractiveStates = { - MaterialState.pressed, - MaterialState.hovered, - MaterialState.dragged, + final Set scrollbarInteractiveStates = { + WidgetState.pressed, + WidgetState.hovered, + WidgetState.dragged, }; TextStyle getFontStyle({ diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart index 001de8af4e..1727eedd55 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/desktop_appearance.dart @@ -34,8 +34,6 @@ class DesktopAppearance extends BaseAppearance { // Editor: toolbarColor onTertiary: theme.toolbarColor, tertiaryContainer: theme.questionBubbleBG, - background: theme.surface, - onBackground: theme.text, surface: theme.surface, // text&icon color when it is hovered onSurface: theme.hoverFG, @@ -44,7 +42,7 @@ class DesktopAppearance extends BaseAppearance { onError: theme.onPrimary, error: theme.red, outline: theme.shader4, - surfaceVariant: theme.sidebarBg, + surfaceContainerHighest: theme.sidebarBg, shadow: theme.shadow, ); @@ -76,13 +74,13 @@ class DesktopAppearance extends BaseAppearance { contentTextStyle: TextStyle(color: colorScheme.onSurface), ), scrollbarTheme: ScrollbarThemeData( - thumbColor: MaterialStateProperty.resolveWith((states) { + thumbColor: WidgetStateProperty.resolveWith((states) { if (states.any(scrollbarInteractiveStates.contains)) { return theme.shader7; } return theme.shader5; }), - thickness: MaterialStateProperty.resolveWith((states) { + thickness: WidgetStateProperty.resolveWith((states) { if (states.any(scrollbarInteractiveStates.contains)) { return 4; } @@ -144,6 +142,8 @@ class DesktopAppearance extends BaseAppearance { fontWeight: FontWeight.w400, fontColor: theme.hint, ), + onBackground: theme.text, + background: theme.surface, ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 19cd87f4f4..09db07ed11 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; - // ThemeData in mobile import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra/theme_extension.dart'; +import 'package:flutter/material.dart'; class MobileAppearance extends BaseAppearance { static const _primaryColor = Color(0xFF00BCF0); //primary 100 @@ -49,14 +48,12 @@ class MobileAppearance extends BaseAppearance { tertiary: const Color(0xff858585), // for light text error: const Color(0xffFB006D), onError: const Color(0xffFB006D), - background: Colors.white, - onBackground: _onBackgroundColor, outline: const Color(0xffe3e3e3), outlineVariant: const Color(0xffCBD5E0).withOpacity(0.24), //Snack bar surface: Colors.white, onSurface: _onSurfaceColor, // text/body color - surfaceVariant: const Color.fromARGB(255, 216, 216, 216), + surfaceContainerHighest: const Color.fromARGB(255, 216, 216, 216), ) : ColorScheme( brightness: brightness, @@ -67,8 +64,6 @@ class MobileAppearance extends BaseAppearance { tertiary: const Color(0xff858585), // temp error: const Color(0xffFB006D), onError: const Color(0xffFB006D), - background: const Color(0xff121212), // temp - onBackground: Colors.white, outline: _hintColorInDarkMode, outlineVariant: Colors.black, //Snack bar @@ -78,6 +73,10 @@ class MobileAppearance extends BaseAppearance { final hintColor = brightness == Brightness.light ? const Color(0x991F2329) : _hintColorInDarkMode; + final onBackground = + brightness == Brightness.light ? _onBackgroundColor : Colors.white; + final background = + brightness == Brightness.light ? Colors.white : const Color(0xff121212); return ThemeData( useMaterial3: false, @@ -86,14 +85,14 @@ class MobileAppearance extends BaseAppearance { dividerColor: colorTheme.outline, //caption hintColor: hintColor, disabledColor: colorTheme.outline, - scaffoldBackgroundColor: colorTheme.background, + scaffoldBackgroundColor: background, appBarTheme: AppBarTheme( toolbarHeight: 44.0, - foregroundColor: colorTheme.onBackground, - backgroundColor: colorTheme.background, + foregroundColor: onBackground, + backgroundColor: background, centerTitle: false, titleTextStyle: TextStyle( - color: colorTheme.onBackground, + color: onBackground, fontSize: 18, fontWeight: FontWeight.w600, letterSpacing: 0.05, @@ -101,8 +100,8 @@ class MobileAppearance extends BaseAppearance { shadowColor: colorTheme.outlineVariant, ), radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return colorTheme.primary; } return colorTheme.outline; @@ -111,20 +110,20 @@ class MobileAppearance extends BaseAppearance { // button elevatedButtonTheme: ElevatedButtonThemeData( style: ButtonStyle( - fixedSize: MaterialStateProperty.all(const Size.fromHeight(48)), - elevation: MaterialStateProperty.all(0), - textStyle: MaterialStateProperty.all( + fixedSize: WidgetStateProperty.all(const Size.fromHeight(48)), + elevation: WidgetStateProperty.all(0), + textStyle: WidgetStateProperty.all( TextStyle( fontSize: 14, fontFamily: fontStyle.fontFamily, fontWeight: FontWeight.w600, ), ), - shadowColor: MaterialStateProperty.all(null), - foregroundColor: MaterialStateProperty.all(Colors.white), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.disabled)) { + shadowColor: WidgetStateProperty.all(null), + foregroundColor: WidgetStateProperty.all(Colors.white), + backgroundColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.disabled)) { return _primaryColor; } return colorTheme.primary; @@ -134,29 +133,29 @@ class MobileAppearance extends BaseAppearance { ), outlinedButtonTheme: OutlinedButtonThemeData( style: ButtonStyle( - textStyle: MaterialStateProperty.all( + textStyle: WidgetStateProperty.all( TextStyle( fontSize: 14, fontFamily: fontStyle.fontFamily, fontWeight: FontWeight.w500, ), ), - foregroundColor: MaterialStateProperty.all(colorTheme.onBackground), - backgroundColor: MaterialStateProperty.all(colorTheme.background), - shape: MaterialStateProperty.all( + foregroundColor: WidgetStateProperty.all(onBackground), + backgroundColor: WidgetStateProperty.all(background), + shape: WidgetStateProperty.all( RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), - side: MaterialStateProperty.all( + side: WidgetStateProperty.all( BorderSide(color: colorTheme.outline, width: 0.5), ), - padding: MaterialStateProperty.all( + padding: WidgetStateProperty.all( const EdgeInsets.symmetric(horizontal: 8, vertical: 12), ), ), ), textButtonTheme: TextButtonThemeData( style: ButtonStyle( - textStyle: MaterialStateProperty.all(fontStyle), + textStyle: WidgetStateProperty.all(fontStyle), ), ), // text @@ -170,7 +169,7 @@ class MobileAppearance extends BaseAppearance { letterSpacing: 0.16, ), displayMedium: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontSize: 32, fontWeight: FontWeight.w600, height: 1.20, @@ -178,33 +177,33 @@ class MobileAppearance extends BaseAppearance { ), // H1 Semi 26 displaySmall: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontWeight: FontWeight.w600, height: 1.10, letterSpacing: 0.13, ), // body2 14 Regular bodyMedium: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontWeight: FontWeight.w400, letterSpacing: 0.07, ), // Trash empty title labelLarge: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontSize: 22, fontWeight: FontWeight.w600, letterSpacing: -0.3, ), // setting item title labelMedium: fontStyle.copyWith( - color: colorTheme.onSurface, + color: onBackground, fontSize: 18, fontWeight: FontWeight.w500, ), // setting group title labelSmall: fontStyle.copyWith( - color: colorTheme.onBackground, + color: onBackground, fontSize: 16, fontWeight: FontWeight.w600, letterSpacing: 0.05, @@ -273,6 +272,8 @@ class MobileAppearance extends BaseAppearance { fontWeight: FontWeight.w400, color: theme.hint, ), + onBackground: onBackground, + background: background, ), ToolbarColorExtension.fromBrightness(brightness), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 1e5aec2f0e..9c20ac015b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -211,7 +211,7 @@ class _SidebarState extends State<_Sidebar> { final userState = context.read().state; return DecoratedBox( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, border: Border( right: BorderSide(color: Theme.of(context).dividerColor), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart index 81601cbfd4..ee4f1d6d00 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart @@ -86,7 +86,7 @@ class _FlowyTabState extends State { return AFThemeExtension.of(context).lightGreyHover; } - return Theme.of(context).colorScheme.surfaceVariant; + return Theme.of(context).colorScheme.surfaceContainerHighest; } void _closeTab([TapUpDetails? details]) => context diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart index 436e58f1bf..6e24544820 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart @@ -59,7 +59,7 @@ class _TabsManagerState extends State alignment: Alignment.bottomLeft, height: HomeSizes.tabBarHeigth, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, ), /// TODO(Xazin): Custom Reorderable TabBar diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index 6e9d575dce..60c19a3051 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -57,7 +57,7 @@ void showSnackBarMessage( }) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, duration: duration, action: !showCancel ? null diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart index 8ee7a0f102..988ca40fca 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/inbox_action_bar.dart @@ -81,30 +81,30 @@ class _ToggleUnreadsButtonState extends State<_ToggleUnreadsButton> { showSelectedIcon: false, style: ButtonStyle( tapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: MaterialStatePropertyAll( + side: WidgetStatePropertyAll( BorderSide(color: Theme.of(context).dividerColor), ), - shape: const MaterialStatePropertyAll( + shape: const WidgetStatePropertyAll( RoundedRectangleBorder( borderRadius: Corners.s6Border, ), ), - foregroundColor: MaterialStateProperty.resolveWith( + foregroundColor: WidgetStateProperty.resolveWith( (state) { - if (state.contains(MaterialState.selected)) { + if (state.contains(WidgetState.selected)) { return Theme.of(context).colorScheme.onPrimary; } return AFThemeExtension.of(context).textColor; }, ), - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (state) { - if (state.contains(MaterialState.selected)) { + if (state.contains(WidgetState.selected)) { return Theme.of(context).colorScheme.primary; } - if (state.contains(MaterialState.hovered)) { + if (state.contains(WidgetState.hovered)) { return AFThemeExtension.of(context).lightGreyHover; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 12a932aabc..56e1bb93fc 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -930,7 +930,7 @@ class _CursorColorValueWidget extends StatelessWidget { FlowyText( LocaleKeys.appName.tr(), // To avoid the text color changes when it is hovered in dark mode - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), ], ); @@ -981,7 +981,7 @@ class _SelectionColorValueWidget extends StatelessWidget { @override Widget build(BuildContext context) { // To avoid the text color changes when it is hovered in dark mode - final textColor = Theme.of(context).colorScheme.onBackground; + final textColor = AFThemeExtension.of(context).onBackground; return Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart index c234f538b0..31e5959002 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart @@ -21,12 +21,12 @@ DropdownMenuEntry buildDropdownMenuEntry( return DropdownMenuEntry( style: ButtonStyle( foregroundColor: - MaterialStatePropertyAll(Theme.of(context).colorScheme.primary), - padding: MaterialStateProperty.all( + WidgetStatePropertyAll(Theme.of(context).colorScheme.primary), + padding: WidgetStateProperty.all( const EdgeInsets.symmetric(horizontal: 6, vertical: 4), ), - minimumSize: const MaterialStatePropertyAll(Size(double.infinity, 29)), - maximumSize: const MaterialStatePropertyAll(Size(double.infinity, 29)), + minimumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), + maximumSize: const WidgetStatePropertyAll(Size(double.infinity, 29)), ), value: value, label: label, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart index 12bf1c1480..ba6ae1416f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/setting_value_dropdown.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class SettingValueDropDown extends StatefulWidget { const SettingValueDropDown({ @@ -45,7 +45,7 @@ class _SettingValueDropDownState extends State { child: widget.child ?? FlowyTextButton( widget.currentValue, - fontColor: Theme.of(context).colorScheme.onBackground, + fontColor: AFThemeExtension.maybeOf(context)?.onBackground, fillColor: Colors.transparent, onPressed: () {}, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart index 8b28289670..42f407f97a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/shared/settings_dropdown.dart @@ -59,14 +59,14 @@ class _SettingsDropdownState extends State> { ?.copyWith(fontFamily: fontFamilyUsed), menuStyle: MenuStyle( maximumSize: - const MaterialStatePropertyAll(Size(double.infinity, 250)), - elevation: const MaterialStatePropertyAll(10), + const WidgetStatePropertyAll(Size(double.infinity, 250)), + elevation: const WidgetStatePropertyAll(10), shadowColor: - MaterialStatePropertyAll(Colors.black.withOpacity(0.4)), - backgroundColor: MaterialStatePropertyAll( + WidgetStatePropertyAll(Colors.black.withOpacity(0.4)), + backgroundColor: WidgetStatePropertyAll( Theme.of(context).cardColor, ), - padding: const MaterialStatePropertyAll( + padding: const WidgetStatePropertyAll( EdgeInsets.symmetric(horizontal: 6, vertical: 8), ), alignment: Alignment.bottomLeft, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart index 066d63f380..4ef6e00994 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/flowy_emoji_picker_config.dart @@ -29,6 +29,6 @@ EmojiPickerConfig buildFlowyEmojiPickerConfig(BuildContext context) { noRecentsText: LocaleKeys.emoji_noRecent.tr(), noRecentsStyle: style.textTheme.bodyMedium, noEmojiFoundText: LocaleKeys.emoji_noEmojiFound.tr(), - scrollBarHandleColor: style.colorScheme.onBackground, + scrollBarHandleColor: style.colorScheme.onSurface, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index 6805633f74..a75cd87171 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -18,6 +18,7 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_infra/theme_extension.dart'; class AppFlowyCloudViewSetting extends StatelessWidget { const AppFlowyCloudViewSetting({ @@ -289,7 +290,7 @@ class CloudURLInputState extends State { .copyWith(fontWeight: FontWeight.w400, fontSize: 16), enabledBorder: UnderlineInputBorder( borderSide: - BorderSide(color: Theme.of(context).colorScheme.onBackground), + BorderSide(color: AFThemeExtension.of(context).onBackground), ), focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart index 99d272d122..10e69be87a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_cloud.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -15,7 +13,9 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -127,7 +127,7 @@ class CloudTypeSwitcher extends StatelessWidget { child: FlowyTextButton( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 6), titleFromCloudType(cloudType), - fontColor: Theme.of(context).colorScheme.onBackground, + fontColor: AFThemeExtension.of(context).onBackground, fillColor: Colors.transparent, onPressed: () {}, ), @@ -159,7 +159,6 @@ class CloudTypeSwitcher extends StatelessWidget { showHeader: true, showDragHandle: true, showDivider: false, - showCloseButton: false, title: LocaleKeys.settings_menu_cloudServerType.tr(), builder: (context) { return Column( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart index c014cdf516..29fab1805f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart @@ -1,7 +1,3 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/supabase_cloud_setting_bloc.dart'; @@ -15,9 +11,13 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingSupabaseCloudView extends StatelessWidget { @@ -293,7 +293,7 @@ class SupabaseInputState extends State { .copyWith(fontWeight: FontWeight.w400, fontSize: 16), enabledBorder: UnderlineInputBorder( borderSide: - BorderSide(color: Theme.of(context).colorScheme.onBackground), + BorderSide(color: AFThemeExtension.of(context).onBackground), ), focusedBorder: UnderlineInputBorder( borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart index 0148db171b..e44af72edd 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -32,7 +32,7 @@ class SettingsMenu extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8) + const EdgeInsets.only(left: 8, right: 4), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), bottomLeft: Radius.circular(8), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart index 260918c7de..f3cd25afde 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_decoration.dart @@ -1,4 +1,5 @@ import 'package:dotted_border/dotted_border.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flutter/material.dart'; import 'theme_upload_view.dart'; @@ -15,7 +16,7 @@ class ThemeUploadDecoration extends StatelessWidget { borderRadius: BorderRadius.circular(ThemeUploadWidget.borderRadius), color: Theme.of(context).colorScheme.surface, border: Border.all( - color: Theme.of(context).colorScheme.onBackground.withOpacity( + color: AFThemeExtension.of(context).onBackground.withOpacity( ThemeUploadWidget.fadeOpacity, ), ), @@ -26,7 +27,7 @@ class ThemeUploadDecoration extends StatelessWidget { dashPattern: const [6, 6], color: Theme.of(context) .colorScheme - .onBackground + .onSurface .withOpacity(ThemeUploadWidget.fadeOpacity), radius: const Radius.circular(ThemeUploadWidget.borderRadius), child: ClipRRect( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart index 379c78acd5..edb382d6ee 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_failure_widget.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flutter/material.dart'; @@ -24,7 +25,7 @@ class ThemeUploadFailureWidget extends StatelessWidget { FlowySvg( FlowySvgs.close_m, size: ThemeUploadWidget.iconSize, - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), FlowyText.medium( errorMessage, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart index 628232bd71..d57d2d2a00 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_learn_more_button.dart @@ -8,6 +8,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flowy_infra/theme_extension.dart'; class ThemeUploadLearnMoreButton extends StatelessWidget { const ThemeUploadLearnMoreButton({super.key}); @@ -21,7 +22,7 @@ class ThemeUploadLearnMoreButton extends StatelessWidget { height: ThemeUploadWidget.buttonSize.height, child: IntrinsicWidth( child: SecondaryButton( - outlineColor: Theme.of(context).colorScheme.onBackground, + outlineColor: AFThemeExtension.of(context).onBackground, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: FlowyText.medium( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart index 643189a38f..5e0ad15f38 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_loading_widget.dart @@ -13,7 +13,7 @@ class ThemeUploadLoadingWidget extends StatelessWidget { padding: ThemeUploadWidget.padding, color: Theme.of(context) .colorScheme - .background + .surface .withOpacity(ThemeUploadWidget.fadeOpacity), constraints: const BoxConstraints.expand(), child: Column( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart index 967f5b0b0f..0113d26a37 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/upload_new_theme_widget.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,7 @@ class UploadNewThemeWidget extends StatelessWidget { return Container( color: Theme.of(context) .colorScheme - .background + .surface .withOpacity(ThemeUploadWidget.fadeOpacity), padding: ThemeUploadWidget.padding, child: Column( @@ -23,7 +24,7 @@ class UploadNewThemeWidget extends StatelessWidget { FlowySvg( FlowySvgs.folder_m, size: ThemeUploadWidget.iconSize, - color: Theme.of(context).colorScheme.onBackground, + color: AFThemeExtension.of(context).onBackground, ), FlowyText.medium( LocaleKeys.settings_appearance_themeUpload_description.tr(), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 513f72b4ed..66cad4ab38 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -214,7 +214,7 @@ class NavigatorOkCancelDialog extends StatelessWidget { ), VSpace(Insets.sm * 1.5), Container( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, height: 1, ), VSpace(Insets.m * 1.5), diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart index 2eeb901ef4..70128c44bd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -5,6 +5,9 @@ class AFThemeExtension extends ThemeExtension { static AFThemeExtension of(BuildContext context) => Theme.of(context).extension()!; + static AFThemeExtension? maybeOf(BuildContext context) => + Theme.of(context).extension(); + const AFThemeExtension({ required this.warning, required this.success, @@ -32,6 +35,8 @@ class AFThemeExtension extends ThemeExtension { required this.progressBarBGColor, required this.toggleButtonBGColor, required this.gridRowCountColor, + required this.background, + required this.onBackground, }); final Color? warning; @@ -64,6 +69,9 @@ class AFThemeExtension extends ThemeExtension { final TextStyle callout; final TextStyle caption; + final Color background; + final Color onBackground; + @override AFThemeExtension copyWith({ Color? warning, @@ -92,6 +100,8 @@ class AFThemeExtension extends ThemeExtension { TextStyle? code, TextStyle? callout, TextStyle? caption, + Color? background, + Color? onBackground, }) => AFThemeExtension( warning: warning ?? this.warning, @@ -121,6 +131,8 @@ class AFThemeExtension extends ThemeExtension { code: code ?? this.code, callout: callout ?? this.callout, caption: caption ?? this.caption, + onBackground: onBackground ?? this.onBackground, + background: background ?? this.background, ); @override @@ -165,6 +177,8 @@ class AFThemeExtension extends ThemeExtension { code: other.code, callout: other.callout, caption: other.caption, + onBackground: Color.lerp(onBackground, other.onBackground, t)!, + background: Color.lerp(background, other.background, t)!, ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml index 3cd07738f8..8c7793d7cc 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml @@ -4,7 +4,7 @@ description: Demonstrates how to use the flowy_infra_ui plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - flutter: ">=3.19.0" + flutter: ">=3.22.0" sdk: ">=3.1.5 <4.0.0" dependencies: diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart index cb147e2782..bbdeda8de3 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/flowy_infra_ui.dart @@ -1,22 +1,19 @@ // Basis +export '/widget/separated_flex.dart'; +export '/widget/spacing.dart'; export 'basis.dart'; - -// Keyboard -export 'src/keyboard/keyboard_visibility_detector.dart'; - +export 'src/flowy_overlay/appflowy_popover.dart'; +export 'src/flowy_overlay/flowy_dialog.dart'; // Overlay export 'src/flowy_overlay/flowy_overlay.dart'; export 'src/flowy_overlay/list_overlay.dart'; export 'src/flowy_overlay/option_overlay.dart'; -export 'src/flowy_overlay/flowy_dialog.dart'; -export 'src/flowy_overlay/appflowy_popover.dart'; +// Keyboard +export 'src/keyboard/keyboard_visibility_detector.dart'; +export 'style_widget/button.dart'; +export 'style_widget/color_picker.dart'; +export 'style_widget/icon_button.dart'; +export 'style_widget/scrolling/styled_list.dart'; +export 'style_widget/scrolling/styled_scroll_bar.dart'; export 'style_widget/text.dart'; export 'style_widget/text_field.dart'; - -export 'style_widget/button.dart'; -export 'style_widget/icon_button.dart'; -export 'style_widget/scrolling/styled_scroll_bar.dart'; -export '/widget/spacing.dart'; -export '/widget/separated_flex.dart'; -export 'style_widget/scrolling/styled_list.dart'; -export 'style_widget/color_picker.dart'; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index 832eca88e0..f402c171d2 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -128,7 +128,7 @@ class FlowyButton extends StatelessWidget { (Platform.isIOS || Platform.isAndroid) ? BoxDecoration( border: Border.all( - color: Theme.of(context).colorScheme.surfaceVariant, + color: Theme.of(context).colorScheme.surfaceContainerHighest, width: 1.0, )) : null); @@ -210,12 +210,12 @@ class FlowyTextButton extends StatelessWidget { onPressed: onPressed ?? () {}, focusNode: FocusNode(skipTraversal: onPressed == null), style: ButtonStyle( - overlayColor: const MaterialStatePropertyAll(Colors.transparent), + overlayColor: const WidgetStatePropertyAll(Colors.transparent), splashFactory: NoSplash.splashFactory, tapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: MaterialStateProperty.all(padding), - elevation: MaterialStateProperty.all(0), - shape: MaterialStateProperty.all( + padding: WidgetStateProperty.all(padding), + elevation: WidgetStateProperty.all(0), + shape: WidgetStateProperty.all( RoundedRectangleBorder( side: BorderSide( color: isDangerous @@ -225,7 +225,7 @@ class FlowyTextButton extends StatelessWidget { borderRadius: radius ?? Corners.s6Border, ), ), - textStyle: MaterialStateProperty.all( + textStyle: WidgetStateProperty.all( TextStyle( fontWeight: fontWeight ?? FontWeight.w500, fontSize: fontSize, @@ -233,9 +233,9 @@ class FlowyTextButton extends StatelessWidget { fontFamily: fontFamily, ), ), - backgroundColor: MaterialStateProperty.resolveWith( + backgroundColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return hoverColor ?? (isDangerous ? Theme.of(context).colorScheme.error @@ -248,9 +248,9 @@ class FlowyTextButton extends StatelessWidget { : Theme.of(context).colorScheme.secondaryContainer); }, ), - foregroundColor: MaterialStateProperty.resolveWith( + foregroundColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.hovered)) { + if (states.contains(WidgetState.hovered)) { return fontHoverColor ?? (fontColor ?? Theme.of(context).colorScheme.onSurface); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart index 778aee0a74..8752a5985f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/snap_bar.dart @@ -9,7 +9,7 @@ void showSnapBar(BuildContext context, String title, {VoidCallback? onClosed}) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( - backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest, duration: const Duration(milliseconds: 8000), content: FlowyText( title, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart index ac44197abe..c79f430942 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/error_page.dart @@ -1,3 +1,4 @@ +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; @@ -155,7 +156,7 @@ class StackTracePreview extends StatelessWidget { Align( alignment: Alignment.centerRight, child: FlowyButton( - hoverColor: Theme.of(context).colorScheme.onBackground, + hoverColor: AFThemeExtension.of(context).onBackground, text: const FlowyText( "Copy", ), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart index 11b71b7d28..a73d96f454 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart @@ -83,7 +83,7 @@ class RoundedImageButton extends StatelessWidget { child: TextButton( onPressed: press, style: ButtonStyle( - shape: MaterialStateProperty.all( + shape: WidgetStateProperty.all( RoundedRectangleBorder(borderRadius: borderRadius))), child: child, ), diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index b46a113767..dab9ec0475 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,11 +53,11 @@ packages: dependency: "direct main" description: path: "." - ref: b827d08 - resolved-ref: b827d089b6e97762806075953a433cfcbe697a73 + ref: "0c79b870586f4bc5c23b61b327c51fe6a8856b47" + resolved-ref: "0c79b870586f4bc5c23b61b327c51fe6a8856b47" url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "2.4.0" + version: "2.5.1" appflowy_editor_plugins: dependency: "direct main" description: @@ -949,10 +949,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" intl_utils: dependency: transitive description: @@ -1037,26 +1037,26 @@ packages: dependency: "direct main" description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.4" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.3" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" linked_scroll_controller: dependency: "direct main" description: @@ -1149,10 +1149,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -1525,10 +1525,10 @@ packages: dependency: "direct main" description: name: scaled_app - sha256: "3415fad16d1cf283112988985ccd14c4cd28bf48cbe6432d59e158f3b632d58d" + sha256: a2ad9f22cf2200a5ce455b59c5ea7bfb09a84acfc52452d1db54f4958c99d76a url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" screen_retriever: dependency: transitive description: @@ -1860,10 +1860,10 @@ packages: dependency: "direct main" description: name: table_calendar - sha256: "1e3521a3e6d3fc7f645a58b135ab663d458ab12504f1ea7f9b4b81d47086c478" + sha256: b759eb6caa88dda8e51c70ee43c19d1682f8244458f84cced9138ee35b2ce416 url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.1.1" term_glyph: dependency: transitive description: @@ -1876,26 +1876,26 @@ packages: dependency: transitive description: name: test - sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" url: "https://pub.dev" source: hosted - version: "1.24.9" + version: "1.25.2" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.0" test_core: dependency: transitive description: name: test_core - sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" url: "https://pub.dev" source: hosted - version: "0.5.9" + version: "0.6.0" textstyle_extensions: dependency: transitive description: @@ -2101,10 +2101,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.1" watcher: dependency: transitive description: @@ -2203,4 +2203,4 @@ packages: version: "2.0.0" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.22.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 8ad4bd55be..35050161f7 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 0.5.8 environment: - flutter: ">=3.19.0" + flutter: ">=3.22.0" sdk: ">=3.3.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. @@ -55,7 +55,7 @@ dependencies: path: packages/appflowy_popover # third party packages - intl: ^0.18.0 + intl: ^0.19.0 time: ^2.1.3 equatable: ^2.0.5 freezed_annotation: ^2.2.0 @@ -135,7 +135,7 @@ dependencies: numerus: ^2.1.2 flutter_animate: ^4.5.0 permission_handler: ^11.3.1 - scaled_app: ^2.2.0 + scaled_app: ^2.3.0 dev_dependencies: flutter_lints: ^3.0.1 @@ -172,7 +172,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "b827d08" + ref: "0c79b870586f4bc5c23b61b327c51fe6a8856b47" sheet: git: diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index 0624ec053b..8aad410e94 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -39,7 +39,7 @@ RUN source ~/.cargo/env && \ RUN sudo pacman -S --noconfirm git tar gtk3 RUN curl -sSfL \ --output flutter.tar.xz \ - https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.19.0-stable.tar.xz && \ + https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.22.0-stable.tar.xz && \ tar -xf flutter.tar.xz && \ rm flutter.tar.xz RUN flutter config --enable-linux-desktop diff --git a/frontend/scripts/install_dev_env/install_ios.sh b/frontend/scripts/install_dev_env/install_ios.sh index 0ce5cfb5d4..653eb8f1b3 100644 --- a/frontend/scripts/install_dev_env/install_ios.sh +++ b/frontend/scripts/install_dev_env/install_ios.sh @@ -44,9 +44,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.19.0 -if [ "$FLUTTER_VERSION" = "3.19.0" ]; then - echo "Flutter version is already 3.19.0" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -55,12 +55,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.19.0 of Flutter - git checkout 3.19.0 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.19.0" + echo "Switched to Flutter version 3.22.0" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh index d1f85445a2..22b491321b 100755 --- a/frontend/scripts/install_dev_env/install_linux.sh +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -38,9 +38,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.19.0 -if [ "$FLUTTER_VERSION" = "3.19.0" ]; then - echo "Flutter version is already 3.19.0" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -49,12 +49,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.19.0 of Flutter - git checkout 3.19.0 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.19.0" + echo "Switched to Flutter version 3.22.0" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index 10f894e13c..8613b904c6 100755 --- a/frontend/scripts/install_dev_env/install_macos.sh +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -41,9 +41,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.19.0 -if [ "$FLUTTER_VERSION" = "3.19.0" ]; then - echo "Flutter version is already 3.19.0" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -52,12 +52,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.19.0 of Flutter - git checkout 3.19.0 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.19.0" + echo "Switched to Flutter version 3.22.0" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_windows.sh b/frontend/scripts/install_dev_env/install_windows.sh index aef80844a0..1d68a677ae 100644 --- a/frontend/scripts/install_dev_env/install_windows.sh +++ b/frontend/scripts/install_dev_env/install_windows.sh @@ -48,9 +48,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.19.0 -if [ "$FLUTTER_VERSION" = "3.19.0" ]; then - echo "Flutter version is already 3.19.0" +# Check if the current version is 3.22.0 +if [ "$FLUTTER_VERSION" = "3.22.0" ]; then + echo "Flutter version is already 3.22.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -59,12 +59,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.19.0 of Flutter - git checkout 3.19.0 + # Use git to checkout version 3.22.0 of Flutter + git checkout 3.22.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.19.0" + echo "Switched to Flutter version 3.22.0" fi # Add pub cache and cargo to PATH From a0139dd475b15da87f2563cc8c5da39b137ec9ec Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Thu, 23 May 2024 16:35:45 +0800 Subject: [PATCH 23/30] feat: support open row page (#5400) --- .../src/application/database-yjs/const.ts | 8 ++ .../src/application/database-yjs/context.ts | 17 ++-- .../application/database-yjs/database.type.ts | 7 ++ .../src/application/database-yjs/selector.ts | 69 ++++++++++++++-- .../src/application/database-yjs/sort.ts | 1 + .../services/js-services/database.service.ts | 68 ++++++++-------- .../src/application/services/services.type.ts | 6 +- .../application/slate-yjs/plugins/withYjs.ts | 34 +++++--- .../utils/translateYjsEvent/arrayEvent.ts | 7 +- .../utils/translateYjsEvent/index.ts | 1 - .../utils/translateYjsEvent/mapEvent.ts | 7 +- .../utils/translateYjsEvent/textEvent.ts | 3 +- .../src/components/database/Database.tsx | 32 +++++--- .../src/components/database/DatabaseRow.tsx | 79 +++++++++++++++++++ .../src/components/database/DatabaseTitle.tsx | 2 +- .../components/database/calendar/Calendar.tsx | 38 ++++----- .../database/calendar/calendar.scss | 32 +++++++- .../database/components/board/group/Group.tsx | 4 +- .../components/calendar/event/EventPaper.tsx | 12 ++- .../components/calendar/toolbar/NoDateRow.tsx | 27 +++---- .../database/components/cell/Cell.tsx | 1 + .../cell/checklist/ChecklistCell.tsx | 9 ++- .../RowCreateModifiedTime.tsx | 20 +++-- .../components/cell/primary/PrimaryCell.tsx | 46 +++++++++++ .../database/components/cell/primary/index.ts | 1 + .../cell/relation/RelationItems.tsx | 4 +- .../cell/select-option/SelectOptionCell.tsx | 2 +- .../database/components/cell/url/UrlCell.tsx | 2 +- .../conditions/DatabaseConditions.tsx | 2 +- .../database-row/DatabaseRowProperties.tsx | 18 +++++ .../database-row/DatabaseRowSubDocument.tsx | 53 +++++++++++++ .../database/components/database-row/index.ts | 2 + .../components/grid/grid-cell/GridCell.tsx | 38 +++++---- .../grid/grid-column/useRenderFields.tsx | 5 +- .../components/header/DatabaseHeader.tsx | 11 +++ .../components/header/DatabaseRowHeader.tsx | 16 ++++ .../database/components/header/Title.tsx | 19 +++++ .../database/components/header/index.ts | 2 + .../database/components/property/Property.tsx | 4 +- .../components/property/PropertyWrapper.tsx | 4 +- .../property/cheklist/ChecklistProperty.tsx | 34 ++++++++ .../components/property/cheklist/index.ts | 1 + .../database/components/tabs/DatabaseTabs.tsx | 2 +- .../src/components/database/index.ts | 1 + .../src/components/document/Document.tsx | 2 +- .../components/editor/CollaborativeEditor.tsx | 33 +++++--- .../src/components/editor/Editor.tsx | 12 ++- .../components/blocks/callout/Callout.tsx | 5 +- .../components/blocks/callout/CalloutIcon.tsx | 2 +- .../editor/components/blocks/code/Code.tsx | 2 +- .../blocks/math_equation/MathEquation.tsx | 2 +- .../src/components/editor/editor.scss | 8 +- .../src/components/folder/ViewItem.tsx | 2 +- .../src/components/layout/Layout.tsx | 1 + .../src/components/layout/layout.scss | 6 +- .../src/pages/DatabasePage.tsx | 18 +++-- .../appflowy_web_app/src/styles/template.css | 15 ---- frontend/appflowy_web_app/vite.config.ts | 12 +-- 58 files changed, 658 insertions(+), 213 deletions(-) create mode 100644 frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/database-row/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/header/Title.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/header/index.ts create mode 100644 frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx create mode 100644 frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts diff --git a/frontend/appflowy_web_app/src/application/database-yjs/const.ts b/frontend/appflowy_web_app/src/application/database-yjs/const.ts index b1dc0a0810..b8ebe46a3a 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/const.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/const.ts @@ -1,5 +1,7 @@ import { YDatabaseRow, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type'; +import { RowMetaKey } from '@/application/database-yjs/database.type'; import * as Y from 'yjs'; +import { v5 as uuidv5, parse as uuidParse } from 'uuid'; export const DEFAULT_ROW_HEIGHT = 37; export const MIN_COLUMN_WIDTH = 100; @@ -14,3 +16,9 @@ export const getCell = (rowId: string, fieldId: string, rowMetas: Y.Map) = export const getCellData = (rowId: string, fieldId: string, rowMetas: Y.Map) => { return getCell(rowId, fieldId, rowMetas)?.get(YjsDatabaseKey.data); }; + +export const metaIdFromRowId = (rowId: string) => { + const namespace = uuidParse(rowId); + + return (key: RowMetaKey) => uuidv5(key, namespace).toString(); +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/context.ts b/frontend/appflowy_web_app/src/application/database-yjs/context.ts index 73feb8d0d7..96a0f067c4 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/context.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/context.ts @@ -8,6 +8,7 @@ export interface DatabaseContextState { doc: YDoc; viewId: string; rowDocMap: Y.Map; + navigateToRow?: (rowId: string) => void; } export const DatabaseContext = createContext(null); @@ -20,12 +21,18 @@ export const useDatabase = () => { return database; }; -export const useRowMeta = (rowId: string) => { - const rows = useContext(DatabaseContext)?.rowDocMap; - const rowMetaDoc = rows?.get(rowId); - const rowMeta = rowMetaDoc?.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database_row) as YDatabaseRow; +export const useNavigateToRow = () => { + return useContext(DatabaseContext)?.navigateToRow; +}; - return rowMeta; +export const useRow = (rowId: string) => { + const rows = useContext(DatabaseContext)?.rowDocMap; + + return rows?.get(rowId)?.getMap(YjsEditorKey.data_section); +}; + +export const useRowData = (rowId: string) => { + return useRow(rowId)?.get(YjsEditorKey.database_row) as YDatabaseRow; }; export const useViewId = () => { diff --git a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts index 403a630ef2..c8ac7da5b0 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/database.type.ts @@ -63,3 +63,10 @@ export interface CalendarLayoutSetting { showWeekends: boolean; layout: CalendarLayout; } + +export enum RowMetaKey { + DocumentId = 'document_id', + IconId = 'icon_id', + CoverId = 'cover_id', + IsDocumentEmpty = 'is_document_empty', +} diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts index bb35599254..ebd91ae726 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -1,11 +1,12 @@ -import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsFolderKey } from '@/application/collab.type'; -import { getCell, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; +import { FieldId, SortId, YDatabaseField, YjsDatabaseKey, YjsEditorKey, YjsFolderKey } from '@/application/collab.type'; +import { getCell, metaIdFromRowId, MIN_COLUMN_WIDTH } from '@/application/database-yjs/const'; import { DatabaseContext, useDatabase, useDatabaseFields, useDatabaseView, - useRowMeta, + useRow, + useRowData, useRows, useViewId, } from '@/application/database-yjs/context'; @@ -18,8 +19,8 @@ import { parseYDatabaseCellToCell } from '@/components/database/components/cell/ import { DateTimeCell } from '@/components/database/components/cell/cell.type'; import dayjs from 'dayjs'; import debounce from 'lodash-es/debounce'; -import { useContext, useEffect, useMemo, useState } from 'react'; -import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, SortCondition } from './database.type'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { CalendarLayoutSetting, FieldType, FieldVisibility, Filter, RowMetaKey, SortCondition } from './database.type'; export interface Column { fieldId: string; @@ -473,7 +474,7 @@ export function useRowOrdersSelector() { } export function useCellSelector({ rowId, fieldId }: { rowId: string; fieldId: string }) { - const row = useRowMeta(rowId); + const row = useRowData(rowId); const cell = row?.get(YjsDatabaseKey.cells)?.get(fieldId); const [cellValue, setCellValue] = useState(() => (cell ? parseYDatabaseCellToCell(cell) : undefined)); @@ -585,3 +586,59 @@ export function useCalendarLayoutSetting() { return setting; } + +export function usePrimaryFieldId() { + const database = useDatabase(); + const [primaryFieldId, setPrimaryFieldId] = React.useState(null); + + useEffect(() => { + const fields = database?.get(YjsDatabaseKey.fields); + const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => { + return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary); + }); + + setPrimaryFieldId(primaryFieldId || null); + }, [database]); + + return primaryFieldId; +} + +export interface RowMeta { + documentId: string; + cover: string; + icon: string; + isEmptyDocument: boolean; +} + +export const useRowMetaSelector = (rowId: string) => { + const [meta, setMeta] = useState(); + const yMeta = useRow(rowId)?.get(YjsEditorKey.meta); + + useEffect(() => { + if (!yMeta) return; + const onChange = () => { + const metaJson = yMeta.toJSON(); + const getData = metaIdFromRowId(rowId); + const icon = metaJson[getData(RowMetaKey.IconId)]; + const cover = metaJson[getData(RowMetaKey.CoverId)]; + const documentId = getData(RowMetaKey.DocumentId); + const isEmptyDocument = metaJson[getData(RowMetaKey.IsDocumentEmpty)]; + + return setMeta({ + icon, + cover, + documentId, + isEmptyDocument, + }); + }; + + onChange(); + + yMeta.observe(onChange); + return () => { + yMeta.unobserve(onChange); + }; + }, [rowId, yMeta]); + + return meta; +}; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/sort.ts b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts index 355d4b4ad9..f229ce8f74 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/sort.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/sort.ts @@ -62,6 +62,7 @@ export function parseCellDataForSort(field: YDatabaseField, data: string | boole switch (fieldType) { case FieldType.RichText: case FieldType.URL: + return data ? data : '\uFFFF'; case FieldType.Number: return data; case FieldType.Checkbox: diff --git a/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts index a1bfcdbf21..e80915c393 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/database.service.ts @@ -17,7 +17,8 @@ export class JSDatabaseService implements DatabaseService { async getDatabase( workspaceId: string, - databaseId: string + databaseId: string, + rowIds?: string[] ): Promise<{ databaseDoc: YDoc; rows: Y.Map; @@ -36,25 +37,24 @@ export class JSDatabaseService implements DatabaseService { const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; const viewId = database.get(YjsDatabaseKey.metas)?.get(YjsDatabaseKey.iid)?.toString(); const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); - const rowIds = rowOrders.toJSON() as { + const rowOrdersIds = rowOrders.toJSON() as { id: string; }[]; - if (!rowIds) { + if (!rowOrdersIds) { throw new Error('Database rows not found'); } - if (isLoaded) { - for (const row of rowIds) { - const { doc } = await getCollabStorage(row.id, CollabType.DatabaseRow); + const ids = rowIds ? rowIds : rowOrdersIds.map((item) => item.id); - rowsFolder.set(row.id, doc); + if (isLoaded) { + for (const id of ids) { + const { doc } = await getCollabStorage(id, CollabType.DatabaseRow); + + rowsFolder.set(id, doc); } } else { - const rows = await this.loadDatabaseRows( - workspaceId, - rowIds.map((item) => item.id) - ); + const rows = await this.loadDatabaseRows(workspaceId, ids); rows.forEach((row, id) => { rowsFolder.set(id, row); @@ -63,6 +63,27 @@ export class JSDatabaseService implements DatabaseService { this.loadedDatabaseId.add(databaseId); + if (!rowIds) { + // Update rows if new rows are added + rowOrders?.observe((event) => { + if (event.changes.added.size > 0) { + const rowIds = rowOrders.toJSON() as { + id: string; + }[]; + + console.log('Update rows', rowIds); + void this.loadDatabaseRows( + workspaceId, + rowIds.map((item) => item.id) + ).then((newRows) => { + newRows.forEach((row, id) => { + rowsFolder.set(id, row); + }); + }); + } + }); + } + return { databaseDoc, rows: rowsFolder as Y.Map, @@ -71,7 +92,8 @@ export class JSDatabaseService implements DatabaseService { async openDatabase( workspaceId: string, - viewId: string + viewId: string, + rowIds?: string[] ): Promise<{ databaseDoc: YDoc; rows: Y.Map; @@ -112,28 +134,8 @@ export class JSDatabaseService implements DatabaseService { throw new Error('Database not found'); } - const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id); - const database = databaseDoc.getMap(YjsEditorKey.data_section)?.get(YjsEditorKey.database) as YDatabase; - const rowOrders = database.get(YjsDatabaseKey.views)?.get(viewId)?.get(YjsDatabaseKey.row_orders); + const { databaseDoc, rows } = await this.getDatabase(workspaceId, databaseMeta.database_id, rowIds); - // Update rows if new rows are added - rowOrders?.observe((event) => { - if (event.changes.added.size > 0) { - const rowIds = rowOrders.toJSON() as { - id: string; - }[]; - - console.log('Update rows', rowIds); - void this.loadDatabaseRows( - workspaceId, - rowIds.map((item) => item.id) - ).then((newRows) => { - newRows.forEach((row, id) => { - rows.set(id, row); - }); - }); - } - }); const handleUpdate = (update: Uint8Array, origin: CollabOrigin) => { if (origin === CollabOrigin.LocalSync) { // Send the update to the server diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts index 7e170b683b..1d6a7d45b0 100644 --- a/frontend/appflowy_web_app/src/application/services/services.type.ts +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -37,14 +37,16 @@ export interface DocumentService { export interface DatabaseService { openDatabase: ( workspaceId: string, - viewId: string + viewId: string, + rowIds?: string[] ) => Promise<{ databaseDoc: YDoc; rows: Y.Map; }>; getDatabase: ( workspaceId: string, - databaseId: string + databaseId: string, + rowIds?: string[] ) => Promise<{ databaseDoc: YDoc; rows: Y.Map; diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts index efa2044622..dd3ec137d7 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/plugins/withYjs.ts @@ -57,15 +57,36 @@ export const YjsEditor = { export function withYjs( editor: T, doc: Y.Doc, - localOrigin: CollabOrigin = CollabOrigin.Local + { + localOrigin, + includeRoot = true, + }: { + localOrigin: CollabOrigin; + includeRoot?: boolean; + } ): T & YjsEditor { const e = editor as T & YjsEditor; const { apply, onChange } = e; e.sharedRoot = doc.getMap(YjsEditorKey.data_section) as YSharedRoot; + + const initializeDocumentContent = () => { + const content = yDocToSlateContent(doc, includeRoot); + + if (!content) { + return; + } + + e.children = content.children; + Editor.normalize(editor, { force: true }); + }; + e.applyRemoteEvents = (events: Array>, _: Transaction) => { YjsEditor.flushLocalChanges(e); + // TODO: handle remote events + // This is a temporary implementation to apply remote events to slate + initializeDocumentContent(); Editor.withoutNormalizing(editor, () => { events.forEach((event) => { translateYjsEvent(e.sharedRoot, editor, event).forEach((op) => { @@ -87,17 +108,8 @@ export function withYjs( throw new Error('Already connected'); } - const content = yDocToSlateContent(doc, true); - - if (!content) { - return; - } - - console.log(content); - + initializeDocumentContent(); e.sharedRoot.observeDeep(handleYEvents); - e.children = content.children; - Editor.normalize(editor, { force: true }); connectSet.add(e); }; diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts index 8be1dbc297..0565e8de92 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/arrayEvent.ts @@ -3,10 +3,9 @@ import * as Y from 'yjs'; import { Editor, Operation } from 'slate'; export function translateYArrayEvent( - sharedRoot: YSharedRoot, - editor: Editor, - event: Y.YEvent> + _sharedRoot: YSharedRoot, + _editor: Editor, + _event: Y.YEvent> ): Operation[] { - console.log('translateYArrayEvent', sharedRoot, editor, event); return []; } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts index 10af76fcde..c3f3bfd903 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/index.ts @@ -13,7 +13,6 @@ import { translateYTextEvent } from 'src/application/slate-yjs/utils/translateYj * @param op */ export function translateYjsEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent): Operation[] { - console.log('translateYjsEvent', event); if (event instanceof Y.YMapEvent) { return translateYMapEvent(sharedRoot, editor, event); } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts index fd50bb6df8..cab9831833 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/mapEvent.ts @@ -3,10 +3,9 @@ import * as Y from 'yjs'; import { Editor, Operation } from 'slate'; export function translateYMapEvent( - sharedRoot: YSharedRoot, - editor: Editor, - event: Y.YEvent> + _sharedRoot: YSharedRoot, + _editor: Editor, + _event: Y.YEvent> ): Operation[] { - console.log('translateYMapEvent', sharedRoot, editor, event); return []; } diff --git a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts index dfe5c029e9..2fc6deca73 100644 --- a/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts +++ b/frontend/appflowy_web_app/src/application/slate-yjs/utils/translateYjsEvent/textEvent.ts @@ -2,7 +2,6 @@ import { YSharedRoot } from '@/application/collab.type'; import * as Y from 'yjs'; import { Editor, Operation } from 'slate'; -export function translateYTextEvent(sharedRoot: YSharedRoot, editor: Editor, event: Y.YEvent): Operation[] { - console.log('translateYTextEvent', sharedRoot, editor, event); +export function translateYTextEvent(_sharedRoot: YSharedRoot, _editor: Editor, _event: Y.YEvent): Operation[] { return []; } diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx index fd09fadd41..9e8399d315 100644 --- a/frontend/appflowy_web_app/src/components/database/Database.tsx +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -2,9 +2,9 @@ import { YDoc, YjsEditorKey } from '@/application/collab.type'; import { useId } from '@/components/_shared/context-provider/IdProvider'; import RecordNotFound from '@/components/_shared/not-found/RecordNotFound'; import { AFConfigContext } from '@/components/app/AppConfig'; +import { DatabaseHeader } from '@/components/database/components/header'; import DatabaseViews from '@/components/database/DatabaseViews'; import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; -import DatabaseTitle from '@/components/database/DatabaseTitle'; import { Log } from '@/utils/log'; import CircularProgress from '@mui/material/CircularProgress'; import React, { memo, useCallback, useContext, useEffect, useState } from 'react'; @@ -14,13 +14,14 @@ import * as Y from 'yjs'; export const Database = memo(() => { const { objectId, workspaceId } = useId() || {}; const [search, setSearch] = useSearchParams(); + const viewId = search.get('v'); const [doc, setDoc] = useState(null); const [rows, setRows] = useState | null>(null); // Map(false); const databaseService = useContext(AFConfigContext)?.service?.databaseService; - const handleOpenDocument = useCallback(async () => { + const handleOpenDatabase = useCallback(async () => { if (!databaseService || !workspaceId || !objectId) return; try { @@ -28,6 +29,8 @@ export const Database = memo(() => { const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId); console.log('databaseDoc', databaseDoc.getMap(YjsEditorKey.data_section).toJSON()); + console.log('rows', rows); + setDoc(databaseDoc); setRows(rows); } catch (e) { @@ -38,8 +41,8 @@ export const Database = memo(() => { useEffect(() => { setNotFound(false); - void handleOpenDocument(); - }, [handleOpenDocument]); + void handleOpenDatabase(); + }, [handleOpenDatabase]); const handleChangeView = useCallback( (viewId: string) => { @@ -48,13 +51,18 @@ export const Database = memo(() => { [setSearch] ); - if (!objectId) return null; + const navigateToRow = useCallback( + (rowId: string) => { + setSearch({ r: rowId }); + }, + [setSearch] + ); - if (!doc) { + if (notFound || !objectId) { return ; } - if (!rows) { + if (!rows || !doc) { return (
@@ -64,9 +72,15 @@ export const Database = memo(() => { return (
- +
- +
diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx new file mode 100644 index 0000000000..6928cc2992 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/DatabaseRow.tsx @@ -0,0 +1,79 @@ +import { YDoc, YjsEditorKey } from '@/application/collab.type'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { DatabaseRowProperties, DatabaseRowSubDocument } from '@/components/database/components/database-row'; +import DatabaseRowHeader from '@/components/database/components/header/DatabaseRowHeader'; +import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; +import { Log } from '@/utils/log'; +import { Divider } from '@mui/material'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import RecordNotFound from 'src/components/_shared/not-found/RecordNotFound'; +import * as Y from 'yjs'; + +function DatabaseRow({ rowId }: { rowId: string }) { + const { objectId, workspaceId } = useId() || {}; + const [doc, setDoc] = useState(null); + const [rows, setRows] = useState | null>(null); // Map(false); + const handleOpenDatabaseRow = useCallback(async () => { + if (!databaseService || !workspaceId || !objectId) return; + + try { + setDoc(null); + const { databaseDoc, rows } = await databaseService.openDatabase(workspaceId, objectId, [rowId]); + + console.log('database', databaseDoc.getMap(YjsEditorKey.data_section).toJSON()); + console.log('row', rows.get(rowId)?.getMap(YjsEditorKey.data_section).toJSON()); + + const row = rows.get(rowId); + + if (!row) { + setNotFound(true); + return; + } + + setDoc(databaseDoc); + setRows(rows); + } catch (e) { + Log.error(e); + setNotFound(true); + } + }, [databaseService, workspaceId, objectId, rowId]); + + useEffect(() => { + setNotFound(false); + void handleOpenDatabaseRow(); + }, [handleOpenDatabaseRow]); + + if (notFound || !objectId) { + return ; + } + + if (!rows || !doc) { + return ( +
+ +
+ ); + } + + return ( +
+
+ + + +
+ + + +
+
+
+
+ ); +} + +export default DatabaseRow; diff --git a/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx index baf314130e..fb996978ff 100644 --- a/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx +++ b/frontend/appflowy_web_app/src/components/database/DatabaseTitle.tsx @@ -6,7 +6,7 @@ function DatabaseTitle({ viewId }: { viewId: string }) { return (
-
+
{icon}
{name}
diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx index 9e99135957..800687fce2 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -1,4 +1,3 @@ -import { AFScroller } from '@/components/_shared/scroller'; import { useCalendarSetup } from '@/components/database/calendar/Calendar.hooks'; import { Toolbar, Event } from '@/components/database/components/calendar'; import React from 'react'; @@ -9,24 +8,25 @@ export function Calendar() { const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup(); return ( - -
- , - eventWrapper: Event, - }} - events={events} - views={['month']} - localizer={localizer} - formats={formats} - dayPropGetter={dayPropGetter} - showMultiDayTimes={true} - step={1} - showAllEvents={true} - /> -
-
+
+ , + eventWrapper: Event, + }} + style={{ + marginBottom: '24px', + }} + events={events} + views={['month']} + localizer={localizer} + formats={formats} + dayPropGetter={dayPropGetter} + showMultiDayTimes={true} + step={1} + showAllEvents={true} + /> +
); } diff --git a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss index eac536443c..0b3f29658a 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss +++ b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss @@ -12,6 +12,7 @@ $today-highlight-bg: transparent; .rbc-date-cell { min-width: 100px; + max-width: 180px; } .rbc-date-cell.rbc-now { @@ -25,19 +26,38 @@ $today-highlight-bg: transparent; .rbc-month-view { border: none; + @apply h-full overflow-auto; .rbc-month-row { border: 1px solid var(--line-divider); - border-bottom: none; + border-top: none; + } - &:last-child { - border-bottom: 1px solid var(--line-divider); + &::-webkit-scrollbar { + width: 4px; + height: 4px; + } + + + &:hover { + + &::-webkit-scrollbar-thumb, & *::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: var(--scrollbar-thumb); } } + + } + .rbc-month-header { height: 40px; + position: sticky; + top: 0; + background: var(--bg-body); + z-index: 50; + @apply border-b border-line-divider; .rbc-header { border: none; @@ -60,4 +80,10 @@ $today-highlight-bg: transparent; display: inline-table !important; flex: 0 0 0 !important; min-height: 120px !important; +} + +.event-properties { + .property-label { + @apply text-text-caption; + } } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx index 73188873de..0f67403beb 100644 --- a/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/board/group/Group.tsx @@ -16,7 +16,7 @@ export const Group = ({ groupId }: GroupProps) => { if (notFound) { return ( -
+
{t('board.noGroup')}
{t('board.noGroupDesc')}
@@ -25,7 +25,7 @@ export const Group = ({ groupId }: GroupProps) => { if (columns.length === 0 || !fieldId) return null; return ( - + {(provided) => { return ( diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx index fed3fdd295..993c87c6ae 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx @@ -1,4 +1,4 @@ -import { useFieldsSelector } from '@/application/database-yjs'; +import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs'; import { Property } from '@/components/database/components/property'; import { IconButton } from '@mui/material'; import React from 'react'; @@ -6,16 +6,22 @@ import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; function EventPaper({ rowId }: { rowId: string }) { const fields = useFieldsSelector(); + const navigateToRow = useNavigateToRow(); return (
- + { + navigateToRow?.(rowId); + }} + size={'small'} + >
-
+
{fields.map((field) => { return ; })} diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx index 63b442775a..5e2eaa61d2 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/toolbar/NoDateRow.tsx @@ -1,33 +1,28 @@ -import { YjsDatabaseKey } from '@/application/collab.type'; -import { useCellSelector, useDatabase } from '@/application/database-yjs'; -import React, { useEffect } from 'react'; +import { useCellSelector, useNavigateToRow, usePrimaryFieldId } from '@/application/database-yjs'; +import { Cell } from '@/components/database/components/cell'; +import React from 'react'; import { useTranslation } from 'react-i18next'; -import Cell from 'src/components/database/components/cell/Cell'; function NoDateRow({ rowId }: { rowId: string }) { - const database = useDatabase(); - const [primaryFieldId, setPrimaryFieldId] = React.useState(null); + const navigateToRow = useNavigateToRow(); + const primaryFieldId = usePrimaryFieldId(); const cell = useCellSelector({ rowId, fieldId: primaryFieldId || '', }); const { t } = useTranslation(); - useEffect(() => { - const fields = database?.get(YjsDatabaseKey.fields); - const primaryFieldId = Array.from(fields?.keys() || []).find((fieldId) => { - return fields?.get(fieldId)?.get(YjsDatabaseKey.is_primary); - }); - - setPrimaryFieldId(primaryFieldId || null); - }, [database]); - if (!primaryFieldId || !cell?.data) { return
{t('grid.row.titlePlaceholder')}
; } return ( -
+
{ + navigateToRow?.(rowId); + }} + className={'w-full hover:text-fill-default'} + > ) { const { cell, rowId, fieldId, style } = props; const { field } = useFieldSelector(fieldId); const fieldType = Number(field?.get(YjsDatabaseKey.type)) as FieldType; + const Component = useMemo(() => { switch (fieldType) { case FieldType.RichText: diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx index fa6133bad7..3d6b53e9fc 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/checklist/ChecklistCell.tsx @@ -3,7 +3,7 @@ import { CellProps, ChecklistCell as ChecklistCellType } from '@/components/data import LinearProgressWithLabel from '@/components/_shared/progress/LinearProgressWithLabel'; import React, { useMemo } from 'react'; -export function ChecklistCell({ cell, style }: CellProps) { +export function ChecklistCell({ cell, style, placeholder }: CellProps) { const data = useMemo(() => { return parseChecklistData(cell?.data ?? ''); }, [cell?.data]); @@ -11,7 +11,12 @@ export function ChecklistCell({ cell, style }: CellProps) { const options = data?.options; const selectedOptions = data?.selectedOptionIds; - if (!data || !options || !selectedOptions) return null; + if (!data || !options || !selectedOptions) + return placeholder ? ( +
+ {placeholder} +
+ ) : null; return (
diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx index 7716ba1552..d3ec145c67 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/created-modified/RowCreateModifiedTime.tsx @@ -1,5 +1,5 @@ import { YjsDatabaseKey } from '@/application/collab.type'; -import { useRowMeta } from '@/application/database-yjs'; +import { useRowData } from '@/application/database-yjs'; import { useDateTypeCellDispatcher } from '@/components/database/components/cell/Cell.hooks'; import React, { useEffect, useMemo, useState } from 'react'; @@ -15,24 +15,24 @@ export function RowCreateModifiedTime({ attrName: YjsDatabaseKey.last_modified | YjsDatabaseKey.created_at; }) { const { getDateTimeStr } = useDateTypeCellDispatcher(fieldId); - const rowMeta = useRowMeta(rowId); + const rowData = useRowData(rowId); const [value, setValue] = useState(null); useEffect(() => { - if (!rowMeta) return; + if (!rowData) return; const observeHandler = () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error - setValue(rowMeta.get(attrName)); + setValue(rowData.get(attrName)); }; observeHandler(); - rowMeta.observe(observeHandler); + rowData.observe(observeHandler); return () => { - rowMeta.unobserve(observeHandler); + rowData.unobserve(observeHandler); }; - }, [rowMeta, attrName]); + }, [rowData, attrName]); const time = useMemo(() => { if (!value) return null; @@ -40,7 +40,11 @@ export function RowCreateModifiedTime({ }, [value, getDateTimeStr]); if (!time) return null; - return
{time}
; + return ( +
+ {time} +
+ ); } export default RowCreateModifiedTime; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx new file mode 100644 index 0000000000..f29773a8ea --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx @@ -0,0 +1,46 @@ +import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; +import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs'; +import { TextCell as CellType, CellProps } from '@/components/database/components/cell/cell.type'; +import { TextCell } from '@/components/database/components/cell/text'; +import { Tooltip } from '@mui/material'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +export function PrimaryCell(props: CellProps) { + const navigateToRow = useNavigateToRow(); + const { rowId } = props; + // const icon = null; + const icon = useRowMetaSelector(rowId)?.icon; + + const [hover, setHover] = useState(false); + const { t } = useTranslation(); + + return ( +
setHover(true)} + onMouseLeave={() => setHover(false)} + className={'primary-cell relative flex w-full items-center gap-2'} + > + {icon &&
{icon}
} + + + {hover && ( + + + + )} +
+ ); +} + +export default PrimaryCell; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts b/frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts new file mode 100644 index 0000000000..c854b4e336 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/index.ts @@ -0,0 +1 @@ +export * from './PrimaryCell'; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx index a91dfe57af..e2008c7552 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx @@ -18,7 +18,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: useEffect(() => { if (!workspaceId || !databaseId) return; - void databaseService?.getDatabase(workspaceId, databaseId).then(({ databaseDoc: doc, rows }) => { + void databaseService?.getDatabase(workspaceId, databaseId, rowIds).then(({ databaseDoc: doc, rows }) => { const fields = doc .getMap(YjsEditorKey.data_section) .get(YjsEditorKey.database) @@ -32,7 +32,7 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: setRows(rows); }); - }, [workspaceId, databaseId, databaseService]); + }, [workspaceId, databaseId, databaseService, rowIds]); return (
diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx index 60db55b3cb..37a98659d1 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx @@ -5,7 +5,7 @@ import { CellProps, SelectOptionCell as SelectOptionCellType } from '@/component import React, { useCallback, useMemo } from 'react'; export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProps) { - const selectOptionIds = useMemo(() => cell?.data.split(','), [cell]); + const selectOptionIds = useMemo(() => (!cell?.data ? [] : cell?.data.split(',')), [cell]); const { field } = useFieldSelector(fieldId); const typeOption = useMemo(() => { if (!field) return null; diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx index 4829e9e1f6..63e3c4cb10 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/url/UrlCell.tsx @@ -9,7 +9,7 @@ export function UrlCell({ cell, style, placeholder }: CellProps) { const isUrl = useMemo(() => (cell ? processUrl(cell.data) : false), [cell]); const className = useMemo(() => { - const classList = ['select-text', 'w-fit']; + const classList = ['select-text', 'w-fit', 'flex', 'w-full', 'items-center']; if (isUrl) { classList.push('text-content-blue-400', 'underline', 'cursor-pointer'); diff --git a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx index fc36c470d6..7c74e0fb8a 100644 --- a/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/conditions/DatabaseConditions.tsx @@ -18,7 +18,7 @@ export function DatabaseConditions() { borderTopWidth: expanded ? '1px' : '0', }} className={ - 'database-conditions relative mx-24 transform overflow-hidden border-t border-line-divider transition-all max-md:mx-4' + 'database-conditions relative mx-16 transform overflow-hidden border-t border-line-divider transition-all max-md:mx-4' } > diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx new file mode 100644 index 0000000000..59405d6c15 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowProperties.tsx @@ -0,0 +1,18 @@ +import { useFieldsSelector, usePrimaryFieldId } from '@/application/database-yjs'; +import { Property } from '@/components/database/components/property'; +import React from 'react'; + +export function DatabaseRowProperties({ rowId }: { rowId: string }) { + const primaryFieldId = usePrimaryFieldId(); + const fields = useFieldsSelector().filter((column) => column.fieldId !== primaryFieldId); + + return ( +
+ {fields.map((field) => { + return ; + })} +
+ ); +} + +export default DatabaseRowProperties; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx new file mode 100644 index 0000000000..b2dd815e7a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/DatabaseRowSubDocument.tsx @@ -0,0 +1,53 @@ +import { YDoc } from '@/application/collab.type'; +import { useRowMetaSelector } from '@/application/database-yjs'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { AFConfigContext } from '@/components/app/AppConfig'; +import { Editor } from '@/components/editor'; +import { Log } from '@/utils/log'; +import CircularProgress from '@mui/material/CircularProgress'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; +import RecordNotFound from '@/components/_shared/not-found/RecordNotFound'; + +export function DatabaseRowSubDocument({ rowId }: { rowId: string }) { + const { workspaceId } = useId() || {}; + const documentId = useRowMetaSelector(rowId)?.documentId; + + const [doc, setDoc] = useState(null); + const [notFound, setNotFound] = useState(false); + + const documentService = useContext(AFConfigContext)?.service?.documentService; + + const handleOpenDocument = useCallback(async () => { + if (!documentService || !workspaceId || !documentId) return; + try { + setDoc(null); + const doc = await documentService.openDocument(workspaceId, documentId); + + setDoc(doc); + } catch (e) { + Log.error(e); + setNotFound(true); + } + }, [documentService, workspaceId, documentId]); + + useEffect(() => { + setNotFound(false); + void handleOpenDocument(); + }, [handleOpenDocument]); + + if (notFound || !documentId) { + return ; + } + + if (!doc) { + return ( +
+ +
+ ); + } + + return ; +} + +export default DatabaseRowSubDocument; diff --git a/frontend/appflowy_web_app/src/components/database/components/database-row/index.ts b/frontend/appflowy_web_app/src/components/database/components/database-row/index.ts new file mode 100644 index 0000000000..a5f4ddd8aa --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/database-row/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseRowProperties'; +export * from './DatabaseRowSubDocument'; diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx index ce47153c70..0d3c7dfc11 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-cell/GridCell.tsx @@ -1,8 +1,10 @@ -import { FieldId } from '@/application/collab.type'; +import { FieldId, YjsDatabaseKey } from '@/application/collab.type'; import { useCellSelector } from '@/application/database-yjs'; import { useFieldSelector } from '@/application/database-yjs/selector'; import { Cell } from '@/components/database/components/cell'; -import React, { useEffect } from 'react'; +import { CellProps, Cell as CellType } from '@/components/database/components/cell/cell.type'; +import { PrimaryCell } from '@/components/database/components/cell/primary'; +import React, { useEffect, useMemo, useRef } from 'react'; export interface GridCellProps { rowId: string; @@ -13,8 +15,9 @@ export interface GridCellProps { } export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: GridCellProps) { - const ref = React.useRef(null); - const field = useFieldSelector(fieldId); + const ref = useRef(null); + const { field } = useFieldSelector(fieldId); + const isPrimary = field?.get(YjsDatabaseKey.is_primary); const cell = useCellSelector({ rowId, fieldId, @@ -23,15 +26,13 @@ export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: Gr useEffect(() => { const el = ref.current; - if (!el) return; + if (!el || !cell) return; const observer = new ResizeObserver(() => { - if (onResize) { - onResize(rowIndex, columnIndex, { - width: el.offsetWidth, - height: el.offsetHeight, - }); - } + onResize?.(rowIndex, columnIndex, { + width: el.offsetWidth, + height: el.offsetHeight, + }); }); observer.observe(el); @@ -39,12 +40,21 @@ export function GridCell({ onResize, rowId, fieldId, columnIndex, rowIndex }: Gr return () => { observer.disconnect(); }; - }, [columnIndex, onResize, rowIndex]); + }, [columnIndex, onResize, rowIndex, cell]); + + const Component = useMemo(() => { + if (isPrimary) { + return PrimaryCell; + } + + return Cell; + }, [isPrimary]) as React.FC>; if (!field) return null; + return ( -
- +
+
); } diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx index 2e5c42e93a..7bacc7b882 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-column/useRenderFields.tsx @@ -20,7 +20,6 @@ export type RenderColumn = { export function useRenderFields() { const fields = useFieldsSelector(); - console.log('columns', fields); const renderColumns = useMemo(() => { const data = fields.map((column) => ({ ...column, @@ -30,7 +29,7 @@ export function useRenderFields() { return [ { type: GridColumnType.Action, - width: 96, + width: 64, }, ...data, { @@ -39,7 +38,7 @@ export function useRenderFields() { }, { type: GridColumnType.Action, - width: 96, + width: 64, }, ].filter(Boolean) as RenderColumn[]; }, [fields]); diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx new file mode 100644 index 0000000000..f84da67aa2 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseHeader.tsx @@ -0,0 +1,11 @@ +import { usePageInfo } from '@/components/_shared/page/usePageInfo'; +import Title from './Title'; +import React from 'react'; + +export function DatabaseHeader({ viewId }: { viewId: string }) { + const { name, icon } = usePageInfo(viewId); + + return ; +} + +export default DatabaseHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx new file mode 100644 index 0000000000..cbbfcaee49 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/DatabaseRowHeader.tsx @@ -0,0 +1,16 @@ +import { useCellSelector, usePrimaryFieldId, useRowMetaSelector } from '@/application/database-yjs'; +import Title from '@/components/database/components/header/Title'; +import React from 'react'; + +function DatabaseRowHeader({ rowId }: { rowId: string }) { + const fieldId = usePrimaryFieldId() || ''; + const meta = useRowMetaSelector(rowId); + const cell = useCellSelector({ + rowId, + fieldId, + }); + + return <Title icon={meta?.icon} name={cell?.data as string} />; +} + +export default DatabaseRowHeader; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/Title.tsx b/frontend/appflowy_web_app/src/components/database/components/header/Title.tsx new file mode 100644 index 0000000000..8bb1979196 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/Title.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +export function Title({ icon, name }: { icon?: string; name?: string }) { + const { t } = useTranslation(); + + return ( + <div className={'flex w-full flex-col py-4'}> + <div className={'flex w-full items-center px-16 max-md:px-4'}> + <div className={'flex items-center gap-2 text-3xl'}> + <div>{icon}</div> + <div className={'font-bold'}>{name || t('document.title.placeholder')}</div> + </div> + </div> + </div> + ); +} + +export default Title; diff --git a/frontend/appflowy_web_app/src/components/database/components/header/index.ts b/frontend/appflowy_web_app/src/components/database/components/header/index.ts new file mode 100644 index 0000000000..452eceafe1 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/header/index.ts @@ -0,0 +1,2 @@ +export * from './DatabaseHeader'; +export * from './DatabaseRowHeader'; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx b/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx index 73cd63ca64..f194a65a72 100644 --- a/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/property/Property.tsx @@ -2,7 +2,6 @@ import { YjsDatabaseKey } from '@/application/collab.type'; import { FieldType, useCellSelector, useFieldSelector } from '@/application/database-yjs'; import { Cell as CellType, CellProps, TextCell } from '@/components/database/components/cell/cell.type'; import { CheckboxCell } from '@/components/database/components/cell/checkbox'; -import { ChecklistCell } from '@/components/database/components/cell/checklist'; import { RowCreateModifiedTime } from '@/components/database/components/cell/created-modified'; import { DateTimeCell } from '@/components/database/components/cell/date'; import { NumberCell } from '@/components/database/components/cell/number'; @@ -11,6 +10,7 @@ import { SelectOptionCell } from '@/components/database/components/cell/select-o import { UrlCell } from '@/components/database/components/cell/url'; import PropertyWrapper from '@/components/database/components/property/PropertyWrapper'; import { TextProperty } from '@/components/database/components/property/text'; +import { ChecklistProperty } from 'src/components/database/components/property/cheklist'; import React, { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -39,7 +39,7 @@ export function Property({ fieldId, rowId }: { fieldId: string; rowId: string }) case FieldType.DateTime: return DateTimeCell; case FieldType.Checklist: - return ChecklistCell; + return ChecklistProperty; case FieldType.Relation: return RelationCell; default: diff --git a/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx index 48dc80764c..9e970abaee 100644 --- a/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/property/PropertyWrapper.tsx @@ -3,8 +3,8 @@ import React from 'react'; function PropertyWrapper({ fieldId, children }: { fieldId: string; children: React.ReactNode }) { return ( - <div className={'flex w-full items-center gap-2'}> - <div className={'w-[100px] text-text-caption'}> + <div className={'flex min-h-[28px] w-full gap-2'}> + <div className={'property-label flex h-[28px] w-[30%] items-center'}> <FieldDisplay fieldId={fieldId} /> </div> <div className={'flex flex-1 flex-wrap pr-1'}>{children}</div> diff --git a/frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx new file mode 100644 index 0000000000..6e47ada5fe --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/ChecklistProperty.tsx @@ -0,0 +1,34 @@ +import { parseChecklistData } from '@/application/database-yjs'; +import { CellProps, ChecklistCell as CellType } from '@/components/database/components/cell/cell.type'; +import { ChecklistCell } from '@/components/database/components/cell/checklist'; +import React, { useMemo } from 'react'; +import { ReactComponent as CheckboxCheckSvg } from '$icons/16x/check_filled.svg'; +import { ReactComponent as CheckboxUncheckSvg } from '$icons/16x/uncheck.svg'; + +export function ChecklistProperty(props: CellProps<CellType>) { + const { cell } = props; + const data = useMemo(() => { + return parseChecklistData(cell?.data ?? ''); + }, [cell?.data]); + + const options = data?.options; + const selectedOptions = data?.selectedOptionIds; + + return ( + <div className={'flex w-full flex-col gap-2'}> + <ChecklistCell {...props} /> + {options?.map((option) => { + const isSelected = selectedOptions?.includes(option.id); + + return ( + <div key={option.id} className={'flex items-center gap-2 text-xs font-medium'}> + {isSelected ? <CheckboxCheckSvg className={'h-4 w-4'} /> : <CheckboxUncheckSvg className={'h-4 w-4'} />} + <div>{option.name}</div> + </div> + ); + })} + </div> + ); +} + +export default ChecklistProperty; diff --git a/frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts new file mode 100644 index 0000000000..413d3c884b --- /dev/null +++ b/frontend/appflowy_web_app/src/components/database/components/property/cheklist/index.ts @@ -0,0 +1 @@ +export * from './ChecklistProperty'; diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx index f9b79efcff..c79545da38 100644 --- a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -54,7 +54,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( return ( <div ref={ref} - className='mx-24 -mb-[0.5px] flex items-center overflow-hidden border-b border-line-divider text-text-title max-md:mx-4' + className='mx-16 -mb-[0.5px] flex items-center overflow-hidden border-b border-line-divider text-text-title max-md:mx-4' > <div style={{ diff --git a/frontend/appflowy_web_app/src/components/database/index.ts b/frontend/appflowy_web_app/src/components/database/index.ts index 8ef9c34dc1..7cef3e91fb 100644 --- a/frontend/appflowy_web_app/src/components/database/index.ts +++ b/frontend/appflowy_web_app/src/components/database/index.ts @@ -1,3 +1,4 @@ import { lazy } from 'react'; export const Database = lazy(() => import('./Database')); +export const DatabaseRow = lazy(() => import('./DatabaseRow')); diff --git a/frontend/appflowy_web_app/src/components/document/Document.tsx b/frontend/appflowy_web_app/src/components/document/Document.tsx index 82e20bed4d..c8578fa2a6 100644 --- a/frontend/appflowy_web_app/src/components/document/Document.tsx +++ b/frontend/appflowy_web_app/src/components/document/Document.tsx @@ -41,7 +41,7 @@ export const Document = () => { <DocumentHeader doc={doc} viewId={documentId} /> <div className={'flex w-full justify-center'}> <div className={'max-w-screen w-[964px] min-w-0'}> - <Editor doc={doc} readOnly={true} /> + <Editor doc={doc} readOnly={true} includeRoot={true} /> </div> </div> </div> diff --git a/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx b/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx index f55dde01a4..7e5ebbc28c 100644 --- a/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx +++ b/frontend/appflowy_web_app/src/components/editor/CollaborativeEditor.tsx @@ -13,18 +13,27 @@ import * as Y from 'yjs'; const defaultInitialValue: Descendant[] = []; -function CollaborativeEditor({ doc }: { doc: Y.Doc }) { - const context = useEditorContext(); - // if readOnly, collabOrigin is Local, otherwise RemoteSync - const collabOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync; - const editor = useMemo( - () => doc && (withPlugins(withReact(withYjs(createEditor(), doc, collabOrigin))) as YjsEditor), - [doc, collabOrigin] - ); - const [connected, setIsConnected] = useState(false); +function CollaborativeEditor({ doc, includeRoot = true }: { doc: Y.Doc; includeRoot?: boolean }) { const viewId = useId()?.objectId || ''; const { view } = useViewSelector(viewId); - const title = view?.get(YjsFolderKey.name); + const title = includeRoot ? view?.get(YjsFolderKey.name) : undefined; + const context = useEditorContext(); + // if readOnly, collabOrigin is Local, otherwise RemoteSync + const localOrigin = context.readOnly ? CollabOrigin.Local : CollabOrigin.LocalSync; + const editor = useMemo( + () => + doc && + (withPlugins( + withReact( + withYjs(createEditor(), doc, { + localOrigin, + includeRoot, + }) + ) + ) as YjsEditor), + [doc, localOrigin, includeRoot] + ); + const [connected, setIsConnected] = useState(false); useEffect(() => { if (!editor) return; @@ -37,8 +46,8 @@ function CollaborativeEditor({ doc }: { doc: Y.Doc }) { }, [editor]); useEffect(() => { - if (!editor || !connected) return; - CustomEditor.setDocumentTitle(editor, title || ''); + if (!editor || !connected || title === undefined) return; + CustomEditor.setDocumentTitle(editor, title); }, [editor, title, connected]); return ( diff --git a/frontend/appflowy_web_app/src/components/editor/Editor.tsx b/frontend/appflowy_web_app/src/components/editor/Editor.tsx index 7777973061..601b5d44b2 100644 --- a/frontend/appflowy_web_app/src/components/editor/Editor.tsx +++ b/frontend/appflowy_web_app/src/components/editor/Editor.tsx @@ -4,10 +4,18 @@ import { EditorContextProvider } from '@/components/editor/EditorContext'; import React from 'react'; import './editor.scss'; -export const Editor = ({ readOnly, doc }: { readOnly: boolean; doc: YDoc }) => { +export const Editor = ({ + readOnly, + doc, + includeRoot = true, +}: { + readOnly: boolean; + doc: YDoc; + includeRoot?: boolean; +}) => { return ( <EditorContextProvider readOnly={readOnly}> - <CollaborativeEditor doc={doc} /> + <CollaborativeEditor doc={doc} includeRoot={includeRoot} /> </EditorContextProvider> ); }; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx index a60255f951..4b4f02ea6b 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/Callout.tsx @@ -10,7 +10,10 @@ export const Callout = memo( <CalloutIcon node={node} /> </div> <div ref={ref} className={`${attributes.className ?? ''} w-full bg-bg-body py-2`}> - <div {...attributes} className={`flex w-full flex-col rounded bg-content-blue-50 py-2 pl-10`}> + <div + {...attributes} + className={`flex w-full flex-col rounded border border-line-divider bg-fill-list-active py-2 pl-10`} + > {children} </div> </div> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx index 6f4f9b53ed..0c72c971d8 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/callout/CalloutIcon.tsx @@ -6,7 +6,7 @@ function CalloutIcon({ node }: { node: CalloutNode }) { return ( <> - <span contentEditable={false} ref={ref} className={`h-8 w-8 p-1`}> + <span contentEditable={false} ref={ref} className={`flex h-8 w-8 items-center p-1`}> {node.data.icon} </span> </> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx index 4a3b0be961..be86b4bd98 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/code/Code.tsx @@ -15,7 +15,7 @@ export const CodeBlock = memo( <div {...attributes} ref={ref} className={`${attributes.className ?? ''} flex w-full bg-bg-body py-2`}> <pre spellCheck={false} - className={`flex w-full rounded border border-solid border-line-divider bg-content-blue-50 p-5 pt-20`} + className={`flex w-full rounded border border-line-divider bg-fill-list-active p-5 pt-20`} > <code>{children}</code> </pre> diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/MathEquation.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/MathEquation.tsx index db00aeb777..86eb6117ad 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/MathEquation.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/MathEquation.tsx @@ -20,7 +20,7 @@ export const MathEquation = memo( > <div contentEditable={false} - className={`container-bg w-full select-none rounded border border-line-divider bg-content-blue-50 px-3`} + className={`container-bg w-full select-none rounded border border-line-divider bg-fill-list-active px-3`} > {formula ? ( <KatexMath latex={formula} /> diff --git a/frontend/appflowy_web_app/src/components/editor/editor.scss b/frontend/appflowy_web_app/src/components/editor/editor.scss index 338e21de7b..abf09c726e 100644 --- a/frontend/appflowy_web_app/src/components/editor/editor.scss +++ b/frontend/appflowy_web_app/src/components/editor/editor.scss @@ -197,12 +197,14 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { .bulleted-icon { &:after { content: attr(data-letter); + font-weight: 500; } } .numbered-icon { &:after { content: attr(data-number) "."; + font-weight: 500; } } @@ -238,7 +240,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { &:hover { .container-bg { - background: var(--content-blue-100) !important; + background: var(--fill-list-hover) !important; } } } @@ -270,4 +272,8 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { @apply ml-5; } +} + +.text-block-icon { + @apply flex items-center justify-center; } \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx b/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx index 7465857b17..c95cd305f9 100644 --- a/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx +++ b/frontend/appflowy_web_app/src/components/folder/ViewItem.tsx @@ -1,7 +1,7 @@ import { layoutMap, ViewLayout, YjsFolderKey } from '@/application/collab.type'; import React from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import Page from 'src/components/_shared/page/Page'; +import Page from '@/components/_shared/page/Page'; function ViewItem({ id }: { id: string }) { const navigate = useNavigate(); diff --git a/frontend/appflowy_web_app/src/components/layout/Layout.tsx b/frontend/appflowy_web_app/src/components/layout/Layout.tsx index c10048aa82..46e4619cf8 100644 --- a/frontend/appflowy_web_app/src/components/layout/Layout.tsx +++ b/frontend/appflowy_web_app/src/components/layout/Layout.tsx @@ -19,6 +19,7 @@ function Layout({ children }: { children: React.ReactNode }) { if (!folder) return; + console.log(folder.toJSON()); setFolder(folder); }, [folderService] diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss index 5d56c04f0c..53b1c59eaa 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -45,6 +45,7 @@ opacity: 60%; } + .workspaces, .database-conditions, .grid-scroll-table, .grid-board, .MuiPaper-root, .appflowy-database { ::-webkit-scrollbar { width: 0; @@ -58,9 +59,6 @@ font-family: "Apple Color Emoji", "Segoe UI Emoji", NotoColorEmoji, "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", EmojiSymbols; line-height: 1em; white-space: nowrap; - //&:hover { - // background-color: rgba(156, 156, 156, 0.20); - //} } .theme-mode-item { @@ -97,4 +95,4 @@ backgroundColor: var(--bg-body); transform: rotate(45deg); } -} \ No newline at end of file +} diff --git a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx index e1fbfb8067..58ddfbdd32 100644 --- a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx +++ b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx @@ -1,10 +1,16 @@ -import { Database } from '@/components/database'; +import { Database, DatabaseRow } from '@/components/database'; import React from 'react'; +import { useSearchParams } from 'react-router-dom'; -function DatabasePage () { - return ( - <Database /> - ); +function DatabasePage() { + const [search] = useSearchParams(); + const rowId = search.get('r'); + + if (rowId) { + return <DatabaseRow rowId={rowId} />; + } + + return <Database />; } -export default DatabasePage; \ No newline at end of file +export default DatabasePage; diff --git a/frontend/appflowy_web_app/src/styles/template.css b/frontend/appflowy_web_app/src/styles/template.css index b255483f4e..b6f7dd3361 100644 --- a/frontend/appflowy_web_app/src/styles/template.css +++ b/frontend/appflowy_web_app/src/styles/template.css @@ -31,21 +31,6 @@ textarea { } -::-webkit-scrollbar { - width: 8px; -} - - -:root[data-dark-mode=true] body { - scrollbar-color: #fff var(--bg-body); -} - -body { - scrollbar-track-color: var(--bg-body); - scrollbar-shadow-color: var(--bg-body); -} - - .btn { @apply rounded-xl border border-line-divider px-4 py-3; } diff --git a/frontend/appflowy_web_app/vite.config.ts b/frontend/appflowy_web_app/vite.config.ts index 0e4cfebb4b..e5b8d1cba6 100644 --- a/frontend/appflowy_web_app/vite.config.ts +++ b/frontend/appflowy_web_app/vite.config.ts @@ -71,6 +71,9 @@ export default defineConfig({ cors: false, }, envPrefix: ['AF', 'TAURI_'], + esbuild: { + drop: ['console', 'debugger'], + }, build: !!process.env.TAURI_PLATFORM ? { // Tauri supports es2021 @@ -82,15 +85,6 @@ export default defineConfig({ } : { target: `esnext`, - terserOptions: !isDev - ? { - compress: { - keep_infinity: true, - drop_console: true, - drop_debugger: true, - }, - } - : {}, reportCompressedSize: true, sourcemap: isDev, rollupOptions: !isDev From 857b3aa1063a00266f0874e2b89038eaef8aa317 Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Thu, 23 May 2024 21:16:52 +0800 Subject: [PATCH 24/30] feat: support preview grid/board/calendar block on web (#5401) --- .../src/application/collab.type.ts | 6 ++ .../src/application/database-yjs/index.ts | 2 - .../src/application/database-yjs/selector.ts | 2 +- .../services/js-services/storage/collab.ts | 6 +- .../ydoc/apply/__tests__/document.test.ts | 4 +- .../src/application/ydoc/apply/document.ts | 18 ------ .../src/application/ydoc/apply/index.ts | 19 +++++- .../_shared/context-provider/IdProvider.tsx | 2 - .../_shared/popover/RichTooltip.tsx | 2 +- .../src/components/database/Database.tsx | 24 ++++---- .../components/database/calendar/Calendar.tsx | 2 +- .../database/calendar/calendar.scss | 11 ++-- .../components/calendar/event/EventPaper.tsx | 23 ++++--- .../components/cell/primary/PrimaryCell.tsx | 35 ++++++++--- .../cell/relation/RelationItems.tsx | 4 +- .../cell/select-option/SelectOptionCell.tsx | 5 +- .../components/cell/text/TextCell.tsx | 2 +- .../components/grid/grid-table/GridTable.tsx | 6 +- .../database/components/tabs/DatabaseTabs.tsx | 2 +- .../src/components/database/index.ts | 1 - .../src/components/document/Document.tsx | 2 + .../src/components/editor/Editor.cy.tsx | 4 +- .../BulletedList.tsx | 0 .../BulletedListIcon.tsx | 0 .../{bulleted_list => bulleted-list}/index.ts | 0 .../components/blocks/database/BoardBlock.tsx | 7 +++ .../blocks/database/CalendarBlock.tsx | 7 +++ .../blocks/database/DatabaseBlock.tsx | 60 +++++++++++++++++++ .../components/blocks/database/GridBlock.tsx | 7 +++ .../components/blocks/database/index.ts | 1 + .../MathEquation.tsx | 0 .../{math_equation => math-equation}/index.ts | 0 .../NumberListIcon.tsx | 0 .../NumberedList.tsx | 0 .../{numbered_list => numbered-list}/index.ts | 0 .../blocks/text/StartIcon.hooks.tsx | 10 ++-- .../{todo_list => todo-list}/CheckboxIcon.tsx | 0 .../{todo_list => todo-list}/TodoList.tsx | 0 .../blocks/{todo_list => todo-list}/index.ts | 0 .../ToggleIcon.tsx | 0 .../ToggleList.tsx | 0 .../{toggle_list => toggle-list}/index.ts | 0 .../editor/components/element/Element.tsx | 15 +++-- .../editor/components/leaf/Leaf.tsx | 4 +- .../src/components/editor/editor.type.ts | 7 +++ .../src/components/layout/layout.scss | 18 +++++- .../src/pages/DatabasePage.tsx | 13 +++- .../src/pages/ProductPage.tsx | 15 ++--- frontend/appflowy_web_app/vite.config.ts | 2 +- 49 files changed, 243 insertions(+), 105 deletions(-) delete mode 100644 frontend/appflowy_web_app/src/application/ydoc/apply/document.ts rename frontend/appflowy_web_app/src/components/editor/components/blocks/{bulleted_list => bulleted-list}/BulletedList.tsx (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{bulleted_list => bulleted-list}/BulletedListIcon.tsx (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{bulleted_list => bulleted-list}/index.ts (100%) create mode 100644 frontend/appflowy_web_app/src/components/editor/components/blocks/database/BoardBlock.tsx create mode 100644 frontend/appflowy_web_app/src/components/editor/components/blocks/database/CalendarBlock.tsx create mode 100644 frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx create mode 100644 frontend/appflowy_web_app/src/components/editor/components/blocks/database/GridBlock.tsx create mode 100644 frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts rename frontend/appflowy_web_app/src/components/editor/components/blocks/{math_equation => math-equation}/MathEquation.tsx (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{math_equation => math-equation}/index.ts (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{numbered_list => numbered-list}/NumberListIcon.tsx (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{numbered_list => numbered-list}/NumberedList.tsx (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{numbered_list => numbered-list}/index.ts (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{todo_list => todo-list}/CheckboxIcon.tsx (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{todo_list => todo-list}/TodoList.tsx (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{todo_list => todo-list}/index.ts (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{toggle_list => toggle-list}/ToggleIcon.tsx (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{toggle_list => toggle-list}/ToggleList.tsx (100%) rename frontend/appflowy_web_app/src/components/editor/components/blocks/{toggle_list => toggle-list}/index.ts (100%) diff --git a/frontend/appflowy_web_app/src/application/collab.type.ts b/frontend/appflowy_web_app/src/application/collab.type.ts index 7e8491bac5..affc19e921 100644 --- a/frontend/appflowy_web_app/src/application/collab.type.ts +++ b/frontend/appflowy_web_app/src/application/collab.type.ts @@ -27,6 +27,8 @@ export enum BlockType { DividerBlock = 'divider', ImageBlock = 'image', GridBlock = 'grid', + BoardBlock = 'board', + CalendarBlock = 'calendar', OutlineBlock = 'outline', TableBlock = 'table', TableCell = 'table/cell', @@ -111,6 +113,10 @@ export interface TableCellBlockData extends BlockData { width: number; } +export interface DatabaseNodeData extends BlockData { + view_id: ViewId; +} + export enum MentionType { PageRef = 'page', Date = 'date', diff --git a/frontend/appflowy_web_app/src/application/database-yjs/index.ts b/frontend/appflowy_web_app/src/application/database-yjs/index.ts index 708ae080d2..1d5aa0ce3d 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/index.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/index.ts @@ -4,5 +4,3 @@ export * from './context'; export * from './selector'; export * from './database.type'; export * from './const'; -export * from './filter'; -export * from './sort'; diff --git a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts index ebd91ae726..d42399d882 100644 --- a/frontend/appflowy_web_app/src/application/database-yjs/selector.ts +++ b/frontend/appflowy_web_app/src/application/database-yjs/selector.ts @@ -101,7 +101,7 @@ export function useFieldsSelector(visibilitys: FieldVisibility[] = defaultVisibl visibility: Number( setting?.get(YjsDatabaseKey.visibility) || FieldVisibility.AlwaysShown ) as FieldVisibility, - wrap: setting?.get(YjsDatabaseKey.wrap), + wrap: setting?.get(YjsDatabaseKey.wrap) ?? true, }; }) .filter((column) => { diff --git a/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts b/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts index 27ce771d74..bdc1392024 100644 --- a/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts +++ b/frontend/appflowy_web_app/src/application/services/js-services/storage/collab.ts @@ -1,7 +1,7 @@ import { CollabType, YDoc, YjsEditorKey } from '@/application/collab.type'; import { getDBName, openCollabDB } from '@/application/services/js-services/db'; import { APIService } from '@/application/services/js-services/wasm'; -import { applyDocument } from '@/application/ydoc/apply'; +import { applyYDoc } from '@/application/ydoc/apply'; export function fetchCollab(workspaceId: string, id: string, type: CollabType) { return APIService.getCollab(workspaceId, id, type); @@ -47,7 +47,7 @@ export async function getCollabStorageWithAPICall(workspaceId: string, id: strin const asyncApply = async () => { const res = await fetchCollab(workspaceId, id, type); - applyDocument(doc, res.state); + applyYDoc(doc, res.state); }; // If the document exists locally, apply the state asynchronously, @@ -95,7 +95,7 @@ export async function batchCollabs( const { doc } = await getCollabStorage(id, type); - applyDocument(doc, data); + applyYDoc(doc, data); } })(); } diff --git a/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts index 512c28ae6a..8a332f4b60 100644 --- a/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts +++ b/frontend/appflowy_web_app/src/application/ydoc/apply/__tests__/document.test.ts @@ -1,5 +1,5 @@ import { YjsEditorKey } from '@/application/collab.type'; -import { applyDocument } from '@/application/ydoc/apply'; +import { applyYDoc } from '@/application/ydoc/apply'; import * as Y from 'yjs'; import * as docJson from '../../../../../cypress/fixtures/simple_doc.json'; @@ -11,7 +11,7 @@ describe('apply document', () => { data.set(YjsEditorKey.document, document); const state = new Uint8Array(docJson.data.doc_state); - applyDocument(collab, state); + applyYDoc(collab, state); }); }); diff --git a/frontend/appflowy_web_app/src/application/ydoc/apply/document.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/document.ts deleted file mode 100644 index 60d02d0450..0000000000 --- a/frontend/appflowy_web_app/src/application/ydoc/apply/document.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CollabOrigin } from '@/application/collab.type'; -import * as Y from 'yjs'; - -/** - * Apply doc state from server to client - * Note: origin is always remote - * @param doc local Y.Doc - * @param state state from server - */ -export function applyDocument(doc: Y.Doc, state: Uint8Array) { - Y.transact( - doc, - () => { - Y.applyUpdate(doc, state); - }, - CollabOrigin.Remote - ); -} diff --git a/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts b/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts index 8147823035..b19cb43328 100644 --- a/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts +++ b/frontend/appflowy_web_app/src/application/ydoc/apply/index.ts @@ -1 +1,18 @@ -export * from 'src/application/ydoc/apply/document'; +import { CollabOrigin } from '@/application/collab.type'; +import * as Y from 'yjs'; + +/** + * Apply doc state from server to client + * Note: origin is always remote + * @param doc local Y.Doc + * @param state state from server + */ +export function applyYDoc(doc: Y.Doc, state: Uint8Array) { + Y.transact( + doc, + () => { + Y.applyUpdate(doc, state); + }, + CollabOrigin.Remote + ); +} diff --git a/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx b/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx index 789642420d..15682f1c82 100644 --- a/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/context-provider/IdProvider.tsx @@ -1,4 +1,3 @@ -import { CollabType } from '@/application/collab.type'; import { useContext, createContext } from 'react'; export const IdContext = createContext<IdProviderProps | null>(null); @@ -6,7 +5,6 @@ export const IdContext = createContext<IdProviderProps | null>(null); interface IdProviderProps { workspaceId: string; objectId: string; - collabType: CollabType; } export const IdProvider = ({ children, ...props }: IdProviderProps & { children: React.ReactNode }) => { diff --git a/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx index 06e6f3c51b..437b08eaf5 100644 --- a/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx +++ b/frontend/appflowy_web_app/src/components/_shared/popover/RichTooltip.tsx @@ -26,7 +26,7 @@ export const RichTooltip = ({ placement = 'top', open, onClose, content, childre anchorEl={childNode} placement={placement} transition - style={{ zIndex: 2000 }} + style={{ zIndex: 1200 }} modifiers={[ { name: 'flip', diff --git a/frontend/appflowy_web_app/src/components/database/Database.tsx b/frontend/appflowy_web_app/src/components/database/Database.tsx index 9e8399d315..fe35a99cc6 100644 --- a/frontend/appflowy_web_app/src/components/database/Database.tsx +++ b/frontend/appflowy_web_app/src/components/database/Database.tsx @@ -2,7 +2,6 @@ import { YDoc, YjsEditorKey } from '@/application/collab.type'; import { useId } from '@/components/_shared/context-provider/IdProvider'; import RecordNotFound from '@/components/_shared/not-found/RecordNotFound'; import { AFConfigContext } from '@/components/app/AppConfig'; -import { DatabaseHeader } from '@/components/database/components/header'; import DatabaseViews from '@/components/database/DatabaseViews'; import { DatabaseContextProvider } from '@/components/database/DatabaseContext'; import { Log } from '@/utils/log'; @@ -71,19 +70,16 @@ export const Database = memo(() => { } return ( - <div className={'relative flex h-full w-full flex-col'}> - <DatabaseHeader viewId={objectId} /> - <div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'> - <DatabaseContextProvider - navigateToRow={navigateToRow} - viewId={viewId || objectId} - doc={doc} - rowDocMap={rows} - readOnly={true} - > - <DatabaseViews onChangeView={handleChangeView} currentViewId={viewId || objectId} /> - </DatabaseContextProvider> - </div> + <div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'> + <DatabaseContextProvider + navigateToRow={navigateToRow} + viewId={viewId || objectId} + doc={doc} + rowDocMap={rows} + readOnly={true} + > + <DatabaseViews onChangeView={handleChangeView} currentViewId={viewId || objectId} /> + </DatabaseContextProvider> </div> ); }); diff --git a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx index 800687fce2..046d558254 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx +++ b/frontend/appflowy_web_app/src/components/database/calendar/Calendar.tsx @@ -8,7 +8,7 @@ export function Calendar() { const { dayPropGetter, localizer, formats, events, emptyEvents } = useCalendarSetup(); return ( - <div className={'appflowy-calendar h-full max-h-[960px] min-h-[560px] px-16 pt-4 max-md:px-4'}> + <div className={'appflowy-calendar h-full max-h-[960px] px-16 pt-4 max-md:px-4'}> <BigCalendar components={{ toolbar: (props) => <Toolbar {...props} emptyEvents={emptyEvents} />, diff --git a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss index 0b3f29658a..4d2877154a 100644 --- a/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss +++ b/frontend/appflowy_web_app/src/components/database/calendar/calendar.scss @@ -10,8 +10,9 @@ $today-highlight-bg: transparent; @apply rounded-full w-[20px] h-[20px] my-1.5; } -.rbc-date-cell { - min-width: 100px; + +.rbc-date-cell, .rbc-header { + min-width: 120px; max-width: 180px; } @@ -31,6 +32,7 @@ $today-highlight-bg: transparent; .rbc-month-row { border: 1px solid var(--line-divider); border-top: none; + } &::-webkit-scrollbar { @@ -57,11 +59,12 @@ $today-highlight-bg: transparent; top: 0; background: var(--bg-body); z-index: 50; - @apply border-b border-line-divider; .rbc-header { border: none; - @apply flex items-end py-2 justify-center font-normal text-text-caption; + border-bottom: 1px solid var(--line-divider); + @apply flex items-end py-2 justify-center font-normal text-text-caption bg-bg-body; + } } diff --git a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx index 993c87c6ae..3bc31dd942 100644 --- a/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/calendar/event/EventPaper.tsx @@ -1,25 +1,30 @@ import { useFieldsSelector, useNavigateToRow } from '@/application/database-yjs'; import { Property } from '@/components/database/components/property'; -import { IconButton } from '@mui/material'; +import { Tooltip } from '@mui/material'; import React from 'react'; import { ReactComponent as ExpandMoreIcon } from '$icons/16x/full_view.svg'; +import { useTranslation } from 'react-i18next'; function EventPaper({ rowId }: { rowId: string }) { const fields = useFieldsSelector(); const navigateToRow = useNavigateToRow(); + const { t } = useTranslation(); return ( <div className={'max-h-[260px] w-[360px] overflow-y-auto'}> <div className={'flex h-fit w-full flex-col items-center justify-center py-2 px-3'}> <div className={'flex w-full items-center justify-end'}> - <IconButton - onClick={() => { - navigateToRow?.(rowId); - }} - size={'small'} - > - <ExpandMoreIcon /> - </IconButton> + <Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}> + <button + color={'primary'} + className={'rounded bg-bg-body p-1 hover:bg-fill-list-hover'} + onClick={() => { + navigateToRow?.(rowId); + }} + > + <ExpandMoreIcon /> + </button> + </Tooltip> </div> <div className={'event-properties flex w-full flex-1 flex-col gap-4 overflow-y-auto py-2'}> {fields.map((field) => { diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx index f29773a8ea..b9014f02f7 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/primary/PrimaryCell.tsx @@ -3,26 +3,45 @@ import { useNavigateToRow, useRowMetaSelector } from '@/application/database-yjs import { TextCell as CellType, CellProps } from '@/components/database/components/cell/cell.type'; import { TextCell } from '@/components/database/components/cell/text'; import { Tooltip } from '@mui/material'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; export function PrimaryCell(props: CellProps<CellType>) { const navigateToRow = useNavigateToRow(); const { rowId } = props; - // const icon = null; const icon = useRowMetaSelector(rowId)?.icon; const [hover, setHover] = useState(false); const { t } = useTranslation(); + useEffect(() => { + const table = document.querySelector('.grid-table'); + + if (!table) { + return; + } + + const onMouseMove = (e: Event) => { + const target = e.target as HTMLElement; + + if (target.closest('.grid-row-cell')?.getAttribute('data-row-id') === rowId) { + setHover(true); + } else { + setHover(false); + } + }; + + table.addEventListener('mousemove', onMouseMove); + return () => { + table.removeEventListener('mousemove', onMouseMove); + }; + }, [rowId]); return ( - <div - onMouseEnter={() => setHover(true)} - onMouseLeave={() => setHover(false)} - className={'primary-cell relative flex w-full items-center gap-2'} - > + <div className={'primary-cell relative flex min-h-full w-full items-center gap-2'}> {icon && <div className={'h-4 w-4'}>{icon}</div>} - <TextCell {...props} /> + <div className={'flex-1 overflow-x-hidden'}> + <TextCell {...props} /> + </div> {hover && ( <Tooltip placement={'bottom'} title={t('tooltip.openAsPage')}> diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx index e2008c7552..6d68eee4af 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/relation/RelationItems.tsx @@ -35,12 +35,12 @@ function RelationItems({ style, cell, fieldId }: { cell: RelationCell; fieldId: }, [workspaceId, databaseId, databaseService, rowIds]); return ( - <div style={style} className={'flex items-center gap-2'}> + <div style={style} className={'relation-cell flex w-full items-center gap-2'}> {rowIds.map((rowId) => { const rowDoc = rows?.get(rowId); return ( - <div key={rowId} className={'cursor-pointer underline'}> + <div key={rowId} className={'w-full cursor-pointer underline'}> {rowDoc && databasePrimaryFieldId && ( <RelationPrimaryValue rowDoc={rowDoc} fieldId={databasePrimaryFieldId} /> )} diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx index 37a98659d1..4d3318297f 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/select-option/SelectOptionCell.tsx @@ -31,7 +31,10 @@ export function SelectOptionCell({ cell, fieldId, style, placeholder }: CellProp ) : null; return ( - <div style={style} className={'flex h-full w-full cursor-pointer items-center gap-1 overflow-x-hidden'}> + <div + style={style} + className={'select-option-cell flex h-full w-full cursor-pointer items-center gap-1 overflow-x-hidden'} + > {renderSelectedOptions(selectOptionIds)} </div> ); diff --git a/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx index e27f1e835f..4d882b8c28 100644 --- a/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/cell/text/TextCell.tsx @@ -7,7 +7,7 @@ export function TextCell({ cell, style }: CellProps<TextCellType>) { if (!cell?.data) return null; return ( - <div style={style} className={`cursor-text leading-[1.2] ${readOnly ? 'select-text' : ''}`}> + <div style={style} className={`text-cell w-full cursor-text leading-[1.2] ${readOnly ? 'select-text' : ''}`}> {cell?.data} </div> ); diff --git a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx index b855c8b4cb..ee39bfb957 100644 --- a/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/grid/grid-table/GridTable.tsx @@ -94,10 +94,10 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr const row = data.rows[rowIndex]; const column = data.columns[columnIndex] as RenderColumn; - const classList = ['flex', 'items-center', 'overflow-hidden']; + const classList = ['flex', 'items-center', 'overflow-hidden', 'grid-row-cell']; if (column.wrap) { - classList.push('whitespace-pre-wrap', 'break-words'); + classList.push('wrap-cell'); } else { classList.push('whitespace-nowrap'); } @@ -113,6 +113,7 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr if (row.type === RenderRowType.Row) { return ( <div + data-row-id={row.rowId} className={classList.join(' ')} style={{ ...style, borderLeftWidth: columnIndex === 1 || column.type === GridColumnType.Action ? 0 : 1 }} > @@ -153,6 +154,7 @@ export const GridTable = ({ scrollLeft, columnWidth, columns, onScrollLeft }: Gr columnCount={columns.length} columnWidth={(index) => columnWidth(index, width)} rowHeight={rowHeight} + className={'grid-table'} overscanRowCount={5} overscanColumnCount={5} style={{ diff --git a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx index c79545da38..8e33c7eaba 100644 --- a/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx +++ b/frontend/appflowy_web_app/src/components/database/components/tabs/DatabaseTabs.tsx @@ -83,7 +83,7 @@ export const DatabaseTabs = forwardRef<HTMLDivElement, DatabaseTabBarProps>( icon={<Icon className={'h-4 w-4'} />} iconPosition='start' color='inherit' - label={name || t('grid.title.placeholder')} + label={<span className={'max-w-[120px] truncate'}>{name || t('grid.title.placeholder')}</span>} value={viewId} /> ); diff --git a/frontend/appflowy_web_app/src/components/database/index.ts b/frontend/appflowy_web_app/src/components/database/index.ts index 7cef3e91fb..8ef9c34dc1 100644 --- a/frontend/appflowy_web_app/src/components/database/index.ts +++ b/frontend/appflowy_web_app/src/components/database/index.ts @@ -1,4 +1,3 @@ import { lazy } from 'react'; export const Database = lazy(() => import('./Database')); -export const DatabaseRow = lazy(() => import('./DatabaseRow')); diff --git a/frontend/appflowy_web_app/src/components/document/Document.tsx b/frontend/appflowy_web_app/src/components/document/Document.tsx index c8578fa2a6..219b76e7e5 100644 --- a/frontend/appflowy_web_app/src/components/document/Document.tsx +++ b/frontend/appflowy_web_app/src/components/document/Document.tsx @@ -51,3 +51,5 @@ export const Document = () => { </> ); }; + +export default Document; diff --git a/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx b/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx index 12c2feb435..766e1e6d29 100644 --- a/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx +++ b/frontend/appflowy_web_app/src/components/editor/Editor.cy.tsx @@ -1,6 +1,6 @@ import { YDoc } from '@/application/collab.type'; import { DocumentTest } from '@/../cypress/support/document'; -import { applyDocument } from '@/application/ydoc/apply'; +import { applyYDoc } from '@/application/ydoc/apply'; import React from 'react'; import * as Y from 'yjs'; import { Editor } from './Editor'; @@ -20,7 +20,7 @@ describe('<Editor />', () => { const doc = new Y.Doc(); const state = new Uint8Array(docJson.data.doc_state); - applyDocument(doc, state); + applyYDoc(doc, state); renderEditor(doc); }); }); diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/BulletedList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedList.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/BulletedList.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedList.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/BulletedListIcon.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted_list/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/bulleted-list/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/BoardBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/BoardBlock.tsx new file mode 100644 index 0000000000..88e3790551 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/BoardBlock.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function BoardBlock() { + return <div></div>; +} + +export default BoardBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/CalendarBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/CalendarBlock.tsx new file mode 100644 index 0000000000..19c2b32bf0 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/CalendarBlock.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function CalendarBlock() { + return <div></div>; +} + +export default CalendarBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx new file mode 100644 index 0000000000..b9bc174969 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/DatabaseBlock.tsx @@ -0,0 +1,60 @@ +import { IdProvider, useId } from '@/components/_shared/context-provider/IdProvider'; +import { Database } from '@/components/database'; +import { DatabaseNode, EditorElementProps } from '@/components/editor/editor.type'; +import React, { forwardRef, memo, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { BlockType } from '@/application/collab.type'; + +export const DatabaseBlock = memo( + forwardRef<HTMLDivElement, EditorElementProps<DatabaseNode>>(({ node, children, ...attributes }, ref) => { + const { t } = useTranslation(); + const viewId = node.data.view_id; + const workspaceId = useId()?.workspaceId; + const type = node.type; + + const style = useMemo(() => { + const style = {}; + + switch (type) { + case BlockType.GridBlock: + Object.assign(style, { + height: 360, + }); + break; + case BlockType.CalendarBlock: + case BlockType.BoardBlock: + Object.assign(style, { + height: 560, + }); + } + + return style; + }, [type]); + + return ( + <> + <div {...attributes} className={`relative w-full cursor-pointer py-2`}> + <div ref={ref} className={'absolute left-0 top-0 h-full w-full caret-transparent'}> + {children} + </div> + <div contentEditable={false} style={style} className={`container-bg flex w-full flex-col px-3`}> + {viewId ? ( + <IdProvider workspaceId={workspaceId} objectId={viewId}> + <Database /> + </IdProvider> + ) : ( + <div + className={'mt-[10%] flex h-full w-full flex-col items-center gap-2 px-16 text-text-caption max-md:px-4'} + > + <div className={'text-sm font-medium'}>{t('document.plugins.database.noDataSource')}</div> + <div className={'text-xs'}>{t('grid.relation.noDatabaseSelected')}</div> + </div> + )} + </div> + </div> + </> + ); + }) +); + +export default DatabaseBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/GridBlock.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/GridBlock.tsx new file mode 100644 index 0000000000..eaf2742ceb --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/GridBlock.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +function GridBlock() { + return <div></div>; +} + +export default GridBlock; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts new file mode 100644 index 0000000000..8eaf478025 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/database/index.ts @@ -0,0 +1 @@ +export * from './DatabaseBlock'; diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/MathEquation.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/MathEquation.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/MathEquation.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/math_equation/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/math-equation/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/NumberListIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/NumberListIcon.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberListIcon.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/NumberedList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberedList.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/NumberedList.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/NumberedList.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/numbered_list/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/numbered-list/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx index 119c6fe9fe..2ab5996b09 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/blocks/text/StartIcon.hooks.tsx @@ -1,12 +1,12 @@ import { BlockType } from '@/application/collab.type'; -import { BulletedListIcon } from '@/components/editor/components/blocks/bulleted_list'; -import { NumberListIcon } from '@/components/editor/components/blocks/numbered_list'; -import ToggleIcon from '@/components/editor/components/blocks/toggle_list/ToggleIcon'; +import { BulletedListIcon } from '@/components/editor/components/blocks/bulleted-list'; +import { NumberListIcon } from '@/components/editor/components/blocks/numbered-list'; +import ToggleIcon from '@/components/editor/components/blocks/toggle-list/ToggleIcon'; import { TextNode } from '@/components/editor/editor.type'; import React, { FC, useCallback, useMemo } from 'react'; import { ReactEditor, useSlate } from 'slate-react'; import { Editor, Element } from 'slate'; -import CheckboxIcon from '../todo_list/CheckboxIcon'; +import CheckboxIcon from '@/components/editor/components/blocks/todo-list/CheckboxIcon'; export function useStartIcon(node: TextNode) { const editor = useSlate(); @@ -37,7 +37,7 @@ export function useStartIcon(node: TextNode) { return null; } - return <Component className={`text-block-icon relative`} block={block} />; + return <Component className={`text-block-icon relative h-[24px] w-[24px]`} block={block} />; }, [Component, block]); return { diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/CheckboxIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/CheckboxIcon.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/CheckboxIcon.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/TodoList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/TodoList.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/TodoList.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/TodoList.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/todo_list/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/todo-list/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/ToggleIcon.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/ToggleIcon.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleIcon.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/ToggleList.tsx b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleList.tsx similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/ToggleList.tsx rename to frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/ToggleList.tsx diff --git a/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/index.ts b/frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/index.ts similarity index 100% rename from frontend/appflowy_web_app/src/components/editor/components/blocks/toggle_list/index.ts rename to frontend/appflowy_web_app/src/components/editor/components/blocks/toggle-list/index.ts diff --git a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx index f535bde10c..46a784dd37 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/element/Element.tsx @@ -1,20 +1,20 @@ import { BlockData, BlockType, InlineBlockType, YjsEditorKey } from '@/application/collab.type'; -import { BulletedList } from '@/components/editor/components/blocks/bulleted_list'; +import { BulletedList } from '@/components/editor/components/blocks/bulleted-list'; import { Callout } from '@/components/editor/components/blocks/callout'; import { CodeBlock } from '@/components/editor/components/blocks/code'; import { DividerNode } from '@/components/editor/components/blocks/divider'; import { Heading } from '@/components/editor/components/blocks/heading'; import { ImageBlock } from '@/components/editor/components/blocks/image'; -import { MathEquation } from '@/components/editor/components/blocks/math_equation'; -import { NumberedList } from '@/components/editor/components/blocks/numbered_list'; +import { MathEquation } from '@/components/editor/components/blocks/math-equation'; +import { NumberedList } from '@/components/editor/components/blocks/numbered-list'; import { Outline } from '@/components/editor/components/blocks/outline'; import { Page } from '@/components/editor/components/blocks/page'; import { Paragraph } from '@/components/editor/components/blocks/paragraph'; import { Quote } from '@/components/editor/components/blocks/quote'; import { TableBlock, TableCellBlock } from '@/components/editor/components/blocks/table'; import { Text } from '@/components/editor/components/blocks/text'; -import { TodoList } from '@/components/editor/components/blocks/todo_list'; -import { ToggleList } from '@/components/editor/components/blocks/toggle_list'; +import { TodoList } from 'src/components/editor/components/blocks/todo-list'; +import { ToggleList } from 'src/components/editor/components/blocks/toggle-list'; import { UnSupportedBlock } from '@/components/editor/components/element/UnSupportedBlock'; import { Formula } from '@/components/editor/components/leaf/formula'; import { Mention } from '@/components/editor/components/leaf/mention'; @@ -22,6 +22,7 @@ import { EditorElementProps, TextNode } from '@/components/editor/editor.type'; import { renderColor } from '@/utils/color'; import React, { FC, useMemo } from 'react'; import { RenderElementProps } from 'slate-react'; +import { DatabaseBlock } from 'src/components/editor/components/blocks/database'; export const Element = ({ element: node, @@ -64,6 +65,10 @@ export const Element = ({ return TableBlock; case BlockType.TableCell: return TableCellBlock; + case BlockType.GridBlock: + case BlockType.BoardBlock: + case BlockType.CalendarBlock: + return DatabaseBlock; default: return UnSupportedBlock; } diff --git a/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx b/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx index 714b41db97..38ba65e3f2 100644 --- a/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx +++ b/frontend/appflowy_web_app/src/components/editor/components/leaf/Leaf.tsx @@ -10,9 +10,7 @@ export function Leaf({ attributes, children, leaf }: RenderLeafProps) { const classList = [leaf.prism_token, leaf.prism_token && 'token', leaf.class_name].filter(Boolean); if (leaf.code) { - newChildren = ( - <span className={'bg-fill-list-active bg-opacity-50 text-xs font-medium text-[#EB5757]'}>{newChildren}</span> - ); + newChildren = <span className={'bg-line-divider font-medium text-[#EB5757]'}>{newChildren}</span>; } if (leaf.underline) { diff --git a/frontend/appflowy_web_app/src/components/editor/editor.type.ts b/frontend/appflowy_web_app/src/components/editor/editor.type.ts index fec9ffbcbf..d21f75cd3a 100644 --- a/frontend/appflowy_web_app/src/components/editor/editor.type.ts +++ b/frontend/appflowy_web_app/src/components/editor/editor.type.ts @@ -16,6 +16,7 @@ import { TableCellBlockData, BlockId, BlockData, + DatabaseNodeData, } from '@/application/collab.type'; import { HTMLAttributes } from 'react'; import { Element } from 'slate'; @@ -120,6 +121,12 @@ export interface TableCellNode extends BlockNode { data: TableCellBlockData; } +export interface DatabaseNode extends BlockNode { + type: BlockType.GridBlock | BlockType.BoardBlock | BlockType.CalendarBlock; + blockId: string; + data: DatabaseNodeData; +} + export interface EditorElementProps<T = Element> extends HTMLAttributes<HTMLDivElement> { node: T; } diff --git a/frontend/appflowy_web_app/src/components/layout/layout.scss b/frontend/appflowy_web_app/src/components/layout/layout.scss index 53b1c59eaa..2aa965dd15 100644 --- a/frontend/appflowy_web_app/src/components/layout/layout.scss +++ b/frontend/appflowy_web_app/src/components/layout/layout.scss @@ -91,8 +91,22 @@ display: block; width: 100%; height: 100%; - boxShadow: var(--shadow); - backgroundColor: var(--bg-body); + box-shadow: var(--shadow); + background-color: var(--bg-body); transform: rotate(45deg); } } + +.grid-row-cell.wrap-cell { + .text-cell { + @apply py-2 break-words whitespace-pre-wrap; + } + + .relation-cell { + @apply py-2 break-words whitespace-pre-wrap flex-wrap; + } + + .select-option-cell { + @apply flex-wrap py-2; + } +} diff --git a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx index 58ddfbdd32..f5b4839924 100644 --- a/frontend/appflowy_web_app/src/pages/DatabasePage.tsx +++ b/frontend/appflowy_web_app/src/pages/DatabasePage.tsx @@ -1,8 +1,12 @@ -import { Database, DatabaseRow } from '@/components/database'; +import { useId } from '@/components/_shared/context-provider/IdProvider'; +import { DatabaseHeader } from '@/components/database/components/header'; import React from 'react'; import { useSearchParams } from 'react-router-dom'; +import DatabaseRow from '@/components/database/DatabaseRow'; +import Database from '@/components/database/Database'; function DatabasePage() { + const objectId = useId()?.objectId; const [search] = useSearchParams(); const rowId = search.get('r'); @@ -10,7 +14,12 @@ function DatabasePage() { return <DatabaseRow rowId={rowId} />; } - return <Database />; + return ( + <div className={'relative flex h-full w-full flex-col'}> + <DatabaseHeader viewId={objectId} /> + <Database /> + </div> + ); } export default DatabasePage; diff --git a/frontend/appflowy_web_app/src/pages/ProductPage.tsx b/frontend/appflowy_web_app/src/pages/ProductPage.tsx index f7b5615a77..0cd4d16cac 100644 --- a/frontend/appflowy_web_app/src/pages/ProductPage.tsx +++ b/frontend/appflowy_web_app/src/pages/ProductPage.tsx @@ -1,10 +1,10 @@ -import { CollabType } from '@/application/collab.type'; import { IdProvider } from '@/components/_shared/context-provider/IdProvider'; -import DatabasePage from '@/pages/DatabasePage'; -import React, { useMemo } from 'react'; +import React, { lazy, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import DocumentPage from '@/pages/DocumentPage'; +const DatabasePage = lazy(() => import('./DatabasePage')); + enum URL_COLLAB_TYPE { DOCUMENT = 'document', GRID = 'grid', @@ -12,13 +12,6 @@ enum URL_COLLAB_TYPE { CALENDAR = 'calendar', } -const collabTypeMap: Record<string, CollabType> = { - [URL_COLLAB_TYPE.DOCUMENT]: CollabType.Document, - [URL_COLLAB_TYPE.GRID]: CollabType.WorkspaceDatabase, - [URL_COLLAB_TYPE.BOARD]: CollabType.WorkspaceDatabase, - [URL_COLLAB_TYPE.CALENDAR]: CollabType.WorkspaceDatabase, -}; - function ProductPage() { const { workspaceId, type, objectId } = useParams(); const PageComponent = useMemo(() => { @@ -38,7 +31,7 @@ function ProductPage() { if (!workspaceId || !type || !objectId) return null; return ( - <IdProvider workspaceId={workspaceId} objectId={objectId} collabType={collabTypeMap[type]}> + <IdProvider workspaceId={workspaceId} objectId={objectId}> {PageComponent && <PageComponent />} </IdProvider> ); diff --git a/frontend/appflowy_web_app/vite.config.ts b/frontend/appflowy_web_app/vite.config.ts index e5b8d1cba6..87a5e284bf 100644 --- a/frontend/appflowy_web_app/vite.config.ts +++ b/frontend/appflowy_web_app/vite.config.ts @@ -72,7 +72,7 @@ export default defineConfig({ }, envPrefix: ['AF', 'TAURI_'], esbuild: { - drop: ['console', 'debugger'], + drop: isDev ? [] : ['console', 'debugger'], }, build: !!process.env.TAURI_PLATFORM ? { From 13a35672abda1b69cbacd0fc62729491ba952695 Mon Sep 17 00:00:00 2001 From: Stefan Weiberg <2744377+suntorytimed@users.noreply.github.com> Date: Fri, 24 May 2024 07:24:41 +0200 Subject: [PATCH 25/30] chore: update German translations (#5382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update translations with Fink 🐦 * Manually editing German translation Had to edit the German translation manually, as inlang didn't want to change the strings marked for translation. * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 * chore: update translations with Fink 🐦 --- frontend/resources/translations/ar-SA.json | 2 +- frontend/resources/translations/ca-ES.json | 2 +- frontend/resources/translations/ckb-KU.json | 2 +- frontend/resources/translations/cs-CZ.json | 2 +- frontend/resources/translations/de-DE.json | 91 +++++++++++---------- frontend/resources/translations/en.json | 4 +- frontend/resources/translations/es-VE.json | 2 +- frontend/resources/translations/eu-ES.json | 2 +- frontend/resources/translations/fa.json | 2 +- frontend/resources/translations/fr-CA.json | 2 +- frontend/resources/translations/fr-FR.json | 2 +- frontend/resources/translations/hu-HU.json | 2 +- frontend/resources/translations/id-ID.json | 2 +- frontend/resources/translations/it-IT.json | 2 +- frontend/resources/translations/ja-JP.json | 2 +- frontend/resources/translations/ko-KR.json | 2 +- frontend/resources/translations/pl-PL.json | 2 +- frontend/resources/translations/pt-BR.json | 2 +- frontend/resources/translations/pt-PT.json | 2 +- frontend/resources/translations/ru-RU.json | 2 +- frontend/resources/translations/sv-SE.json | 2 +- frontend/resources/translations/tr-TR.json | 2 +- frontend/resources/translations/uk-UA.json | 10 +-- frontend/resources/translations/vi-VN.json | 2 +- frontend/resources/translations/vi.json | 2 +- frontend/resources/translations/zh-CN.json | 2 +- frontend/resources/translations/zh-TW.json | 2 +- 27 files changed, 81 insertions(+), 72 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 077ca23e03..136bfb3fa1 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -1169,4 +1169,4 @@ "addField": "إضافة حقل", "userIcon": "رمز المستخدم" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index a9a86fb5fb..d00c2c4114 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -812,4 +812,4 @@ "deleteContentTitle": "Esteu segur que voleu suprimir {pageType}?", "deleteContentCaption": "si suprimiu aquest {pageType}, podeu restaurar-lo des de la paperera." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index 1bbd3ccd25..cb8befcd8a 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -946,4 +946,4 @@ "frequentlyUsed": "زۆرجار بەکارت هێناوە" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index 76453591a5..dab1d101e2 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -1094,4 +1094,4 @@ "font": "Písmo", "actions": "Příkazy" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 11c8da8e62..3f4732539b 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -70,32 +70,32 @@ "LogInWithDiscord": "Mit Discord-Account anmelden" }, "workspace": { - "chooseWorkspace": "Workspace wählen", - "create": "Workspace erstellen", - "reset": "Workspace zurücksetzen", - "resetWorkspacePrompt": "Das Zurücksetzen des Workspace löscht alle enthaltenen Seiten und Daten. Bist du sicher, dass du den Arbeitsbereich zurücksetzen möchstest? ", - "hint": "Workspace", - "notFoundError": "Workspace nicht gefunden", - "failedToLoad": "Etwas ist schief gelaufen! Der Workspace konnte nicht geladen werden. Versuche, alle @:appName-Instanzen zu schließen & versuche es erneut.", + "chooseWorkspace": "Arbeitsbereich wählen", + "create": "Arbeitsbereich erstellen", + "reset": "Arbeitsbereich zurücksetzen", + "resetWorkspacePrompt": "Das Zurücksetzen des Arbeitsbereiches löscht alle enthaltenen Seiten und Daten. Bist du sicher, dass du den Arbeitsbereich zurücksetzen möchtest? ", + "hint": "Arbeitsbereich", + "notFoundError": "Arbeitsbereich nicht gefunden", + "failedToLoad": "Etwas ist schief gelaufen! Der Arbeitsbereich konnte nicht geladen werden. Versuche, alle @:appName Instanzen zu schließen und versuche es erneut.", "errorActions": { "reportIssue": "Problem melden", - "reportIssueOnGithub": "Melde ein Problem auf Github", + "reportIssueOnGithub": "Melde ein Problem auf GitHub", "exportLogFiles": "Exportiere Log-Dateien", "reachOut": "Kontaktiere uns auf Discord" }, "menuTitle": "Arbeitsbereiche", - "deleteWorkspaceHintText": "Sicher, dass du dein Workspace löschen möchtest?\nDies kann nicht mehr Rückgängig gemacht werden.", - "createSuccess": "Workspace erfolgreich erstellt", - "createFailed": "Der Workspace konnte nicht erstellt werden", - "createLimitExceeded": "Du hast die für dein Benutzerkonto maximal zulässige Anzahl an Arbeitsbereichen erreicht. Benötigst du zum fortsetzen deiner Arbeit noch weitere Arbeitsbereiche, erstelle auf Github bitte eine entsprechende Anfrage.", - "deleteSuccess": "Workspace erfolgreich gelöscht", - "deleteFailed": "Der Workspace konnte nicht gelöscht werden", - "openSuccess": "Workspace erfolgreich geöffnet", - "openFailed": "Der Workspace konnte nicht geöffnet werden", - "renameSuccess": "Workspace erfolgreich umbenannt", - "renameFailed": "Der Workspace konnte nicht umbenannt werden", - "updateIconSuccess": "Workspace erfolgreich zurückgesetzt", - "updateIconFailed": "Der Workspace konnte nicht zurückgesetzt werden", + "deleteWorkspaceHintText": "Sicher, dass du deinen Arbeitsbereich löschen möchtest? Dies kann nicht mehr Rückgängig gemacht werden.", + "createSuccess": "Arbeitsbereich erfolgreich erstellt", + "createFailed": "Der Arbeitsbereich konnte nicht erstellt werden", + "createLimitExceeded": "Du hast die für dein Benutzerkonto maximal zulässige Anzahl an Arbeitsbereichen erreicht. Benötigst du zum fortsetzen deiner Arbeit noch weitere Arbeitsbereiche, erstelle auf GitHub bitte eine entsprechende Anfrage.", + "deleteSuccess": "Arbeitsbereich erfolgreich gelöscht", + "deleteFailed": "Der Arbeitsbereich konnte nicht gelöscht werden", + "openSuccess": "Arbeitsbereich erfolgreich geöffnet", + "openFailed": "Der Arbeitsbereich konnte nicht geöffnet werden", + "renameSuccess": "Arbeitsbereich erfolgreich umbenannt", + "renameFailed": "Der Arbeitsbereich konnte nicht umbenannt werden", + "updateIconSuccess": "Arbeitsbereich erfolgreich zurückgesetzt", + "updateIconFailed": "Der Arbeitsbereich konnte nicht zurückgesetzt werden", "cannotDeleteTheOnlyWorkspace": "Der einzig vorhandene Arbeitsbereich kann nicht gelöscht werden", "fetchWorkspacesFailed": "Arbeitsbereiche konnten nicht abgerufen werden!", "leaveCurrentWorkspace": "Arbeitsbereich verlassen", @@ -358,18 +358,19 @@ "workspacePage": { "menuLabel": "Arbeitsbereich", "title": "Arbeitsbereich", - "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereichs an.", + "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereiches an.", "workspaceName": { - "title": "Name des Arbeitsbereichs", - "savedMessage": "Name des Arbeitsbereichs gespeichert" + "title": "Name des Arbeitsbereiches", + "savedMessage": "Name des Arbeitsbereiches gespeichert", + "editTooltip": "Name des Arbeitsbereiches ändern" }, "workspaceIcon": { - "title": "Arbeitsbereich-Symbol", - "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datum, die Uhrzeit und die Sprache deines Arbeitsbereichs an." + "title": "Symbol", + "description": "Lade ein Bild hoch oder verwende ein Emoji für deinen Arbeitsbereich. Das Symbol wird in deiner Seitenleiste und in deinen Benachrichtigungen angezeigt." }, "appearance": { "title": "Aussehen", - "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereichs an.", + "description": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datums-/Zeitformat und die Sprache deines Arbeitsbereiches an.", "options": { "system": "Auto", "light": "Hell", @@ -381,7 +382,7 @@ "description": "Wähle ein voreingestelltes Design aus oder lade dein eigenes benutzerdefiniertes Design hoch." }, "workspaceFont": { - "title": "Schriftart Arbeitsbereich" + "title": "Schriftart" }, "textDirection": { "title": "Textrichtung", @@ -428,7 +429,7 @@ "manageDataPage": { "menuLabel": "Daten verwalten", "title": "Daten verwalten", - "description": "Verwalte den lokalen Datenspeicher oder importiere deine vorhandenen Daten in @:appName. Du kannst deine Daten mit End-to-End-Verschlüsselung absichern.", + "description": "Verwalte den lokalen Datenspeicher oder importiere deine vorhandenen Daten in @:appName. Du kannst deine Daten mit Ende-zu-Ende-Verschlüsselung absichern.", "dataStorage": { "title": "Speicherort", "tooltip": "Das Verzeichnis, in dem deine Dateien gespeichert sind", @@ -437,7 +438,8 @@ "open": "Ordner öffnen", "openTooltip": "Aktuellen Speicherort des Datenordners öffnen", "copy": "Pfad kopieren", - "copiedHint": "Link kopiert!" + "copiedHint": "Link kopiert!", + "resetTooltip": "Auf Standardspeicherort zurücksetzen" }, "resetDialog": { "title": "Bist du sicher?", @@ -446,8 +448,8 @@ }, "importData": { "title": "Daten importieren", - "tooltip": "Daten aus @:appName-Backups/Datenordnern importieren", - "description": "Daten aus einem externen @:appName-Datenordner kopieren und in den aktuellen @:appName-Datenordner importieren", + "tooltip": "Daten aus @:appName Backups-/Datenordnern importieren", + "description": "Daten aus einem externen @:appName Datenordner kopieren und in den aktuellen @:appName Datenordner importieren", "action": "Ordner durchsuchen" }, "encryption": { @@ -549,8 +551,8 @@ }, "fontScaleFactor": "Schriftgröße", "documentSettings": { - "cursorColor": "Dokument Cursor-Farbe", - "selectionColor": "Dokument Auswahl-Farbe", + "cursorColor": "Cursor-Farbe", + "selectionColor": "Auswahl-Farbe", "hexEmptyError": "Hex-Farbe darf nicht leer sein", "hexLengthError": "Hex-Wert muss 6 Zeichen lang sein", "hexInvalidError": "Ungültiger Hex-Wert", @@ -611,7 +613,7 @@ "label": "Mitglieder", "user": "Nutzer", "role": "Rolle", - "removeFromWorkspace": "Vom Workspace entfernen", + "removeFromWorkspace": "Vom Arbeitsbereich entfernen", "owner": "Besitzer", "guest": "Gast", "member": "Mitglied", @@ -654,7 +656,7 @@ "openFolderDesc": "Öffnen und speichern im vorhandenen @:appName-Ordner", "folderHintText": "Ordnernamen", "location": "Ein neuen Ordner erstellen", - "locationDesc": "Einen Namen für den @:appName-Datenordner festlegen", + "locationDesc": "Einen Namen für den @:appName Datenordner festlegen", "browser": "Durchsuchen", "create": "Erstellen", "set": "Festlegen", @@ -742,7 +744,11 @@ "typeAValue": "Einen Wert eingeben...", "layout": "Layout", "databaseLayout": "Layout", - "viewList": "Datenbank-Ansichten", + "viewList": { + "zero": "0 Aufrufe", + "one": "{count} Aufruf", + "other": "{count} Aufrufe" + }, "editView": "Ansicht editieren", "boardSettings": "Board-Einstellungen", "calendarSettings": "Kalender-Einstellungen", @@ -1210,6 +1216,7 @@ }, "errorBlock": { "theBlockIsNotSupported": "Die aktuelle Version unterstützt diesen Block nicht.", + "clickToCopyTheBlockContent": "Hier klicken, um den Blockinhalt zu kopieren", "blockContentHasBeenCopied": "Der Blockinhalt wurde kopiert." }, "mobilePageSelector": { @@ -1257,7 +1264,9 @@ "showGroup": "Zeige die Gruppe", "showGroupContent": "Sicher, dass diese Gruppe auf dem Board angezeigt werden soll?", "failedToLoad": "Boardansicht konnte nicht geladen werden" - } + }, + "noGroup": "Keine Gruppierung nach Eigenschaft", + "noGroupDesc": "Board-Ansichten benötigen eine Eigenschaft zum Gruppieren, um angezeigt zu werden" }, "calendar": { "menuName": "Kalender", @@ -1618,7 +1627,7 @@ "deleteMyAccount": "Mein Benutzerkonto löschen", "dialogTitle": "Benutzerkonto löschen", "dialogContent1": "Bist du sicher, dass du dein Benutzerkonto unwiderruflich löschen möchtest?", - "dialogContent2": "Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, Ihr gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und Sie aus allen freigegebenen Arbeitsbereichen entfernt werden." + "dialogContent2": "Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, dein gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und du aus allen freigegebenen Arbeitsbereichen entfernt wirst." } }, "workplace": { @@ -1627,8 +1636,8 @@ "subtitle": "Passe das Erscheinungsbild, das Design, die Schriftart, das Textlayout, das Datum, die Uhrzeit und die Sprache deines Arbeitsbereiches an.", "workplaceName": "Name des Arbeitsbereiches", "workplaceNamePlaceholder": "Gib den Namen des Arbeitsbereiches ein", - "workplaceIcon": "Symbol für den Arbeitsbereich", - "workplaceIconSubtitle": "Lade ein Bild hoch oder verwende ein Emoji für deinen Arbeitsbereich. Das Symbol wird in deiner Seitenleiste und in deinen Benachrichtigungen angezeigt", + "workplaceIcon": "Symbol", + "workplaceIconSubtitle": "Lade ein Bild hoch oder verwende ein Emoji für deinen Arbeitsbereich. Das Symbol wird in deiner Seitenleiste und in deinen Benachrichtigungen angezeigt.", "renameError": "Umbenennen des Arbeitsbereiches fehlgeschlagen", "updateIconError": "Symbol konnte nicht aktualisiert werden", "appearance": { @@ -1676,4 +1685,4 @@ "betaTooltip": "Wir unterstützen derzeit nur die Suche nach Seiten", "fromTrashHint": "Aus dem Mülleimer" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index a35c95eb4a..3f9f4178a0 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -358,7 +358,7 @@ }, "workspaceIcon": { "title": "Workspace icon", - "description": "Customize your workspace appearance, theme, font, text layout, date, time, and language." + "description": "Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications." }, "appearance": { "title": "Appearance", @@ -1634,7 +1634,7 @@ "workplaceName": "Workplace name", "workplaceNamePlaceholder": "Enter workplace name", "workplaceIcon": "Workplace icon", - "workplaceIconSubtitle": "Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications", + "workplaceIconSubtitle": "Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications.", "renameError": "Failed to rename workplace", "updateIconError": "Failed to update icon", "appearance": { diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 1393cbc7e1..57cabeb120 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -1543,4 +1543,4 @@ "betaTooltip": "Actualmente solo admitimos la búsqueda de páginas.", "fromTrashHint": "De la papelera" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index a070a59c54..e7029a0d38 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -601,4 +601,4 @@ "deleteContentTitle": "Ziur {pageType} ezabatu nahi duzula?", "deleteContentCaption": "{pageType} hau ezabatzen baduzu, zaborrontzitik leheneratu dezakezu." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 1f9b3526de..147bca02b6 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -674,4 +674,4 @@ "frequentlyUsed": "استفاده‌شده" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 9fd37affce..f5bbb4b2db 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -1262,4 +1262,4 @@ "userIcon": "Icône utilisateur" }, "noLogFiles": "Il n'y a pas de log" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index c24ce58aa7..e258e9099b 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -1573,4 +1573,4 @@ "loadingTooltip": "Nous recherchons des résultats...", "betaTooltip": "Nous ne prenons actuellement en charge que la recherche de pages" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index 7c25d96d14..1a60a1c6f5 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -599,4 +599,4 @@ "deleteContentTitle": "Biztosan törli a következőt: {pageType}?", "deleteContentCaption": "ha törli ezt a {pageType} oldalt, visszaállíthatja a kukából." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index 90c1835f57..286f5a3e06 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -1022,4 +1022,4 @@ "noFavorite": "Tidak ada halaman favorit", "noFavoriteHintText": "Geser halaman ke kiri untuk menambahkannya ke favorit Anda" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index afed354749..74282485d2 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -1262,4 +1262,4 @@ "userIcon": "Icona utente" }, "noLogFiles": "Non ci sono file di log" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index 5dc6c63285..00738dde42 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -686,4 +686,4 @@ "deleteContentTitle": "{pageType} を削除してもよろしいですか?", "deleteContentCaption": "この {pageType} を削除しても、ゴミ箱から復元できます。" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index ecfc4dfe5b..b0b7a472b4 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -598,4 +598,4 @@ "deleteContentTitle": "{pageType}을(를) 삭제하시겠습니까?", "deleteContentCaption": "이 {pageType}을(를) 삭제하면 휴지통에서 복원할 수 있습니다." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 5ee2bc91e5..389eaa068d 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -1077,4 +1077,4 @@ "language": "Język", "font": "Czcionka" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index de9dca7320..dc1340d3b8 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -1219,4 +1219,4 @@ "addField": "Adicionar campo", "userIcon": "Ícone do usuário" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index 699be162e8..1b5ee1fcd1 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -857,4 +857,4 @@ "noResult": "Nenhum resultado", "caseSensitive": "Maiúsculas e minúsculas" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 8034301d0e..89b81271dc 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -1311,4 +1311,4 @@ "userIcon": "Пользовательская иконка" }, "noLogFiles": "Нет файлов журналов" -} +} \ No newline at end of file diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index 2231ec75fb..50be68350d 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -668,4 +668,4 @@ "deleteContentTitle": "Är du säker på att du vill ta bort {pageType}?", "deleteContentCaption": "om du tar bort denna {pageType} kan du återställa den från papperskorgen." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 8648129b27..66dd2e0d69 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -1477,4 +1477,4 @@ "betaTooltip": "Şu anda yalnızca sayfaları aramayı destekliyoruz", "fromTrashHint": "Çöp kutusundan" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/uk-UA.json b/frontend/resources/translations/uk-UA.json index 3c36875f96..9e0cdf64f4 100644 --- a/frontend/resources/translations/uk-UA.json +++ b/frontend/resources/translations/uk-UA.json @@ -43,10 +43,10 @@ "unmatchedPasswordError": "Повторний пароль не співпадає з паролем", "syncPromptMessage": "Синхронізація даних може зайняти трохи часу. Будь ласка, не закривайте цю сторінку", "or": "АБО", + "signInWith": "Увійти за допомогою:", "LogInWithGoogle": "Увійти за допомогою Google", "LogInWithGithub": "Увійти за допомогою Github", - "LogInWithDiscord": "Увійти за допомогою Discord", - "signInWith": "Увійти за допомогою:" + "LogInWithDiscord": "Увійти за допомогою Discord" }, "workspace": { "chooseWorkspace": "Виберіть свій робочий простір", @@ -194,6 +194,7 @@ }, "button": { "ok": "OK", + "done": "Готово", "cancel": "Скасувати", "signIn": "Увійти", "signOut": "Вийти", @@ -210,7 +211,6 @@ "edit": "Редагувати", "delete": "Видалити", "duplicate": "Дублювати", - "done": "Готово", "putback": "Повернути" }, "label": { @@ -285,12 +285,12 @@ "button": "Завантажити", "uploadTheme": "Завантажити тему", "description": "Завантажте свою власну тему AppFlowy, скориставшись кнопкою нижче.", - "failure": "Тему, яка була завантажена, має неправильний формат.", "loading": "Будь ласка, зачекайте, поки ми перевіряємо та завантажуємо вашу тему...", "uploadSuccess": "Вашу тему успішно завантажено", "deletionFailure": "Не вдалося видалити тему. Спробуйте видалити її вручну.", "filePickerDialogTitle": "Виберіть файл .flowy_plugin", - "urlUploadFailure": "Не вдалося відкрити URL: {}" + "urlUploadFailure": "Не вдалося відкрити URL: {}", + "failure": "Тему, яка була завантажена, має неправильний формат." }, "theme": "Тема", "builtInsLabel": "Вбудовані теми", diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 8be445a75d..f0b4c0b10d 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -868,4 +868,4 @@ "font": "Phông chữ", "date": "Ngày" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/vi.json b/frontend/resources/translations/vi.json index b921c1844e..4d1716447a 100644 --- a/frontend/resources/translations/vi.json +++ b/frontend/resources/translations/vi.json @@ -6,4 +6,4 @@ "failedToLoad": "Không tải được chế độ xem bảng" } } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index ed9cf5e0bd..72240f857a 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -1357,4 +1357,4 @@ "addField": "添加字段", "userIcon": "用户图标" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index c6f0520f05..e49eca0e7e 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -1479,4 +1479,4 @@ "betaLabel": "BETA", "betaTooltip": "目前我們只支援搜尋頁面" } -} +} \ No newline at end of file From 13b3439bd631c385c6bdc53adc9f05a29c3e5af4 Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari <zidom72@gmail.com> Date: Fri, 24 May 2024 16:47:35 +0330 Subject: [PATCH 26/30] fix: default text direction not synced (#5405) * fix: default text direction not synced * test: annotate visibleForTesting --- .../pages/settings_workspace_view.dart | 20 ++- .../widget_test/direction_setting_test.dart | 159 ++++++++++++++++++ 2 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 56e1bb93fc..8bf9415e3f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -125,7 +125,7 @@ class SettingsWorkspaceView extends StatelessWidget { title: LocaleKeys.settings_workspacePage_textDirection_title.tr(), children: const [ - _TextDirectionSelect(), + TextDirectionSelect(), EnableRTLItemsSwitcher(), ], ), @@ -387,19 +387,25 @@ class _WorkspaceIconSetting extends StatelessWidget { } } -class _TextDirectionSelect extends StatelessWidget { - const _TextDirectionSelect(); +@visibleForTesting +class TextDirectionSelect extends StatelessWidget { + const TextDirectionSelect({super.key}); @override Widget build(BuildContext context) { return BlocBuilder<AppearanceSettingsCubit, AppearanceSettingsState>( builder: (context, state) { - final selectedItem = state.textDirection ?? AppFlowyTextDirection.auto; + final selectedItem = state.textDirection ?? AppFlowyTextDirection.ltr; return SettingsRadioSelect<AppFlowyTextDirection>( - onChanged: (item) => context - .read<AppearanceSettingsCubit>() - .setTextDirection(item.value), + onChanged: (item) { + context + .read<AppearanceSettingsCubit>() + .setTextDirection(item.value); + context + .read<DocumentAppearanceCubit>() + .syncDefaultTextDirection(item.value.name); + }, items: [ SettingsRadioItem( value: AppFlowyTextDirection.ltr, diff --git a/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart b/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart new file mode 100644 index 0000000000..34472193f9 --- /dev/null +++ b/frontend/appflowy_flutter/test/widget_test/direction_setting_test.dart @@ -0,0 +1,159 @@ +import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; +import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; +import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_setting.pb.dart'; +import 'package:appflowy/user/application/user_settings_service.dart'; + +import '../util.dart'; + +class MockAppearanceSettingsBloc + extends MockBloc<AppearanceSettingsCubit, AppearanceSettingsState> + implements AppearanceSettingsCubit {} + +class MockDocumentAppearanceCubit extends Mock + implements DocumentAppearanceCubit {} + +class MockDocumentAppearance extends Mock implements DocumentAppearance {} + +void main() { + late AppearanceSettingsPB appearanceSettings; + late DateTimeSettingsPB dateTimeSettings; + + setUp(() async { + await AppFlowyUnitTest.ensureInitialized(); + appearanceSettings = + await UserSettingsBackendService().getAppearanceSetting(); + dateTimeSettings = await UserSettingsBackendService().getDateTimeSettings(); + }); + + testWidgets('TextDirectionSelect update default text direction setting', + (WidgetTester tester) async { + final appearanceSettingsState = AppearanceSettingsState.initial( + AppTheme.fallback, + appearanceSettings.themeMode, + appearanceSettings.font, + appearanceSettings.monospaceFont, + appearanceSettings.layoutDirection, + appearanceSettings.textDirection, + appearanceSettings.enableRtlToolbarItems, + appearanceSettings.locale, + appearanceSettings.isMenuCollapsed, + appearanceSettings.menuOffset, + dateTimeSettings.dateFormat, + dateTimeSettings.timeFormat, + dateTimeSettings.timezoneId, + appearanceSettings.documentSetting.cursorColor.isEmpty + ? null + : Color( + int.parse(appearanceSettings.documentSetting.cursorColor), + ), + appearanceSettings.documentSetting.selectionColor.isEmpty + ? null + : Color( + int.parse( + appearanceSettings.documentSetting.selectionColor, + ), + ), + 1.0, + ); + final mockAppearanceSettingsBloc = MockAppearanceSettingsBloc(); + when(() => mockAppearanceSettingsBloc.state).thenReturn( + appearanceSettingsState, + ); + + final mockDocumentAppearanceCubit = MockDocumentAppearanceCubit(); + when(() => mockDocumentAppearanceCubit.stream).thenAnswer( + (_) => Stream.fromIterable([MockDocumentAppearance()]), + ); + + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider<AppearanceSettingsCubit>.value( + value: mockAppearanceSettingsBloc, + ), + BlocProvider<DocumentAppearanceCubit>.value( + value: mockDocumentAppearanceCubit, + ), + ], + child: MaterialApp( + theme: appearanceSettingsState.lightTheme, + home: MultiBlocProvider( + providers: [ + BlocProvider<AppearanceSettingsCubit>.value( + value: mockAppearanceSettingsBloc, + ), + BlocProvider<DocumentAppearanceCubit>.value( + value: mockDocumentAppearanceCubit, + ), + ], + child: const Scaffold( + body: TextDirectionSelect(), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect( + find.text( + LocaleKeys.settings_workspacePage_textDirection_leftToRight.tr(), + ), + findsOne, + ); + expect( + find.text( + LocaleKeys.settings_workspacePage_textDirection_rightToLeft.tr(), + ), + findsOne, + ); + expect( + find.text( + LocaleKeys.settings_workspacePage_textDirection_auto.tr(), + ), + findsOne, + ); + + final radioSelectFinder = + find.byType(SettingsRadioSelect<AppFlowyTextDirection>); + expect(radioSelectFinder, findsOne); + + when( + () => mockAppearanceSettingsBloc.setTextDirection( + any<AppFlowyTextDirection?>(), + ), + ).thenAnswer((_) async => {}); + when( + () => mockDocumentAppearanceCubit.syncDefaultTextDirection( + any<String?>(), + ), + ).thenAnswer((_) async {}); + + final radioSelect = tester.widget(radioSelectFinder) + as SettingsRadioSelect<AppFlowyTextDirection>; + final rtlSelect = radioSelect.items + .firstWhere((select) => select.value == AppFlowyTextDirection.rtl); + radioSelect.onChanged(rtlSelect); + + verify( + () => mockAppearanceSettingsBloc.setTextDirection( + any<AppFlowyTextDirection?>(), + ), + ).called(1); + verify( + () => mockDocumentAppearanceCubit.syncDefaultTextDirection( + any<String?>(), + ), + ).called(1); + }); +} From a8f136eda2b1da2b7c0b6de0295646e4bf1ac5d0 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Mon, 27 May 2024 08:51:49 +0800 Subject: [PATCH 27/30] feat: sidebar UI Revamp on Desktop (#5343) --- .../collaborative_workspace_test.dart | 5 +- .../desktop/board/board_test_runner.dart | 4 +- .../document/document_more_actions_test.dart | 2 + .../document_with_cover_image_test.dart | 12 +- .../desktop/sidebar/sidebar_expand_test.dart | 12 +- .../sidebar/sidebar_favorites_test.dart | 12 +- .../desktop/sidebar/sidebar_test_runner.dart | 2 +- .../integration_test/desktop_runner_3.dart | 4 +- .../shared/common_operations.dart | 16 +- .../shared/editor_test_operations.dart | 15 +- .../integration_test/shared/expectation.dart | 4 +- .../integration_test/shared/settings.dart | 3 +- .../integration_test/shared/workspace.dart | 23 +- .../base/type_option_menu_item.dart | 2 +- .../base/view_page/more_bottom_sheet.dart | 1 + .../show_mobile_bottom_sheet.dart | 2 +- .../mobile_home_favorite_folder.dart | 9 +- .../presentation/home/mobile_folders.dart | 6 +- .../home/mobile_home_page_header.dart | 4 +- .../mobile_home_section_folder.dart | 18 +- .../workspace_menu_bottom_sheet.dart | 4 +- .../page_item/mobile_view_item.dart | 32 +- .../lib/plugins/base/emoji/emoji_picker.dart | 33 +- .../base/emoji/emoji_picker_header.dart | 9 +- .../plugins/base/emoji/emoji_search_bar.dart | 64 ++-- .../plugins/base/emoji/emoji_skin_tone.dart | 41 +-- .../lib/plugins/base/emoji/emoji_text.dart | 1 + .../lib/plugins/base/icon/icon_picker.dart | 64 +--- .../database/tab_bar/tab_bar_view.dart | 5 +- .../database/widgets/share_button.dart | 11 +- .../lib/plugins/document/document.dart | 7 +- .../document/presentation/editor_page.dart | 6 - .../editor_plugins/actions/option_action.dart | 2 +- .../base/emoji_picker_button.dart | 43 ++- .../find_and_replace_menu.dart | 2 +- .../header/document_header_node_widget.dart | 19 +- .../image/unsplash_image_widget.dart | 2 +- .../presentation/share/share_button.dart | 11 +- .../application/favorite/favorite_bloc.dart | 46 ++- .../favorite/favorite_service.dart | 36 +- .../settings/appearance/base_appearance.dart | 2 +- .../sidebar/folder/folder_bloc.dart | 14 +- .../workspace/application/view/view_bloc.dart | 36 +- .../workspace/application/view/view_ext.dart | 13 + .../view_title/view_title_bar_bloc.dart | 48 +++ .../view_title/view_title_bloc.dart | 73 +++++ .../presentation/home/home_sizes.dart | 15 +- .../presentation/home/home_stack.dart | 13 +- .../workspace/presentation/home/hotkeys.dart | 2 +- .../sidebar/favorites/favorite_folder.dart | 183 +++++++++++ .../menu/sidebar/favorites/favorite_menu.dart | 193 +++++++++++ .../sidebar/favorites/favorite_menu_bloc.dart | 124 +++++++ .../favorites/favorite_more_actions.dart | 67 ++++ .../favorites/favorite_pin_action.dart | 43 +++ .../sidebar/favorites/favorite_pin_bloc.dart | 59 ++++ .../menu/sidebar/folder/_favorite_folder.dart | 115 ------- .../menu/sidebar/folder/_folder_header.dart | 59 ++-- .../menu/sidebar/folder/_section_folder.dart | 219 ++++++++----- .../menu/sidebar/footer/sidebar_footer.dart | 70 ++++ .../{ => header}/sidebar_top_menu.dart | 32 +- .../sidebar/{ => header}/sidebar_user.dart | 10 +- .../{ => shared}/rename_view_dialog.dart | 0 .../sidebar/{ => shared}/sidebar_folder.dart | 13 +- .../shared/sidebar_new_page_button.dart | 62 ++++ .../sidebar/{ => shared}/sidebar_setting.dart | 22 +- .../home/menu/sidebar/sidebar.dart | 40 ++- .../menu/sidebar/sidebar_new_page_button.dart | 70 ---- .../home/menu/sidebar/sidebar_trash.dart | 63 ---- .../workspace/_sidebar_workspace_actions.dart | 71 +++- .../workspace/_sidebar_workspace_icon.dart | 14 +- .../workspace/_sidebar_workspace_menu.dart | 237 ++++++++++---- .../{ => workspace}/sidebar_workspace.dart | 72 ++-- .../home/menu/view/draggable_view_item.dart | 34 +- .../home/menu/view/view_action_type.dart | 59 +++- .../home/menu/view/view_add_button.dart | 12 +- .../home/menu/view/view_item.dart | 272 ++++++++++------ .../menu/view/view_more_action_button.dart | 185 +++++++++-- .../presentation/home/tabs/flowy_tab.dart | 2 +- .../presentation/home/tabs/tabs_manager.dart | 2 +- .../widgets/notification_button.dart | 6 +- .../settings/pages/settings_account_view.dart | 8 +- .../pages/settings_workspace_view.dart | 8 +- .../feature_flags/feature_flag_page.dart | 6 +- .../files/settings_file_exporter_widget.dart | 5 +- .../presentation/widgets/favorite_button.dart | 9 +- .../widgets/float_bubble/question_bubble.dart | 7 +- .../more_view_actions/more_view_actions.dart | 7 +- .../presentation/widgets/pop_up_action.dart | 6 +- .../widgets/rename_view_popover.dart | 21 +- .../presentation/widgets/user_avatar.dart | 27 +- .../presentation/widgets/view_title_bar.dart | 308 +++++++----------- .../macos/Runner/MainFlutterWindow.swift | 12 +- .../appflowy_popover/lib/src/popover.dart | 10 +- .../packages/flowy_infra_ui/lib/basis.dart | 5 - .../lib/style_widget/button.dart | 3 +- .../lib/style_widget/decoration.dart | 3 +- .../flowy_infra_ui/lib/style_widget/text.dart | 40 +-- .../resources/flowy_icons/16x/add_cover.svg | 7 + .../resources/flowy_icons/16x/add_icon.svg | 5 + .../flowy_icons/16x/add_workspace.svg | 6 + .../resources/flowy_icons/16x/change_icon.svg | 4 + .../flowy_icons/16x/collapse_all_page.svg | 14 + .../resources/flowy_icons/16x/duplicate.svg | 4 + .../resources/flowy_icons/16x/favorite.svg | 2 +- .../flowy_icons/16x/favorite_header_icon.svg | 3 + .../flowy_icons/16x/favorite_section_pin.svg | 5 + .../favorite_section_remove_from_favorite.svg | 5 + .../16x/favorite_section_unpin.svg | 5 + .../resources/flowy_icons/16x/hide_menu.svg | 10 +- .../flowy_icons/16x/icon_shuffle.svg | 7 + frontend/resources/flowy_icons/16x/more.svg | 8 +- .../resources/flowy_icons/16x/move_to.svg | 5 + .../flowy_icons/16x/notification.svg | 5 + frontend/resources/flowy_icons/16x/search.svg | 6 +- .../resources/flowy_icons/16x/settings.svg | 4 +- .../flowy_icons/16x/sidebar_footer_trash.svg | 9 + .../flowy_icons/16x/sidebar_footer_widget.svg | 15 + .../resources/flowy_icons/16x/three-dots.svg | 6 +- .../flowy_icons/16x/title_bar_divider.svg | 5 + frontend/resources/flowy_icons/16x/trash.svg | 7 + .../resources/flowy_icons/16x/unfavorite.svg | 4 +- .../flowy_icons/16x/view_item_add.svg | 6 + .../flowy_icons/16x/view_item_expand.svg | 5 + .../16x/view_item_open_in_new_tab.svg | 5 + .../flowy_icons/16x/view_item_rename.svg | 5 + .../flowy_icons/16x/view_item_right_arrow.svg | 10 + .../flowy_icons/16x/view_item_unexpand.svg | 5 + .../16x/workspace_drop_down_menu_hide.svg | 8 + .../16x/workspace_drop_down_menu_show.svg | 8 + .../flowy_icons/16x/workspace_logout.svg | 5 + .../flowy_icons/16x/workspace_selected.svg | 10 + .../flowy_icons/16x/workspace_three_dots.svg | 7 + frontend/resources/translations/en.json | 18 +- .../tests/folder/local_test/script.rs | 6 +- .../flowy-folder/src/entities/view.rs | 14 + .../flowy-folder/src/event_handler.rs | 9 +- .../rust-lib/flowy-folder/src/event_map.rs | 2 +- frontend/scripts/tool/update_collab_source.sh | 4 +- 138 files changed, 2678 insertions(+), 1305 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => header}/sidebar_top_menu.dart (74%) rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => header}/sidebar_user.dart (93%) rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => shared}/rename_view_dialog.dart (100%) rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => shared}/sidebar_folder.dart (94%) create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => shared}/sidebar_setting.dart (86%) delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart rename frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/{ => workspace}/sidebar_workspace.dart (79%) create mode 100644 frontend/resources/flowy_icons/16x/add_cover.svg create mode 100644 frontend/resources/flowy_icons/16x/add_icon.svg create mode 100644 frontend/resources/flowy_icons/16x/add_workspace.svg create mode 100644 frontend/resources/flowy_icons/16x/change_icon.svg create mode 100644 frontend/resources/flowy_icons/16x/collapse_all_page.svg create mode 100644 frontend/resources/flowy_icons/16x/duplicate.svg create mode 100644 frontend/resources/flowy_icons/16x/favorite_header_icon.svg create mode 100644 frontend/resources/flowy_icons/16x/favorite_section_pin.svg create mode 100644 frontend/resources/flowy_icons/16x/favorite_section_remove_from_favorite.svg create mode 100644 frontend/resources/flowy_icons/16x/favorite_section_unpin.svg create mode 100644 frontend/resources/flowy_icons/16x/icon_shuffle.svg create mode 100644 frontend/resources/flowy_icons/16x/move_to.svg create mode 100644 frontend/resources/flowy_icons/16x/notification.svg create mode 100644 frontend/resources/flowy_icons/16x/sidebar_footer_trash.svg create mode 100644 frontend/resources/flowy_icons/16x/sidebar_footer_widget.svg create mode 100644 frontend/resources/flowy_icons/16x/title_bar_divider.svg create mode 100644 frontend/resources/flowy_icons/16x/trash.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_add.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_expand.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_open_in_new_tab.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_rename.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_right_arrow.svg create mode 100644 frontend/resources/flowy_icons/16x/view_item_unexpand.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_drop_down_menu_hide.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_drop_down_menu_show.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_logout.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_selected.svg create mode 100644 frontend/resources/flowy_icons/16x/workspace_three_dots.svg diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart index 11af75af1d..9886c2228e 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart @@ -2,8 +2,6 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; @@ -12,14 +10,15 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart index 932c266bda..a786367fff 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_test_runner.dart @@ -1,10 +1,10 @@ import 'package:integration_test/integration_test.dart'; -import 'board_row_test.dart' as board_row_test; import 'board_add_row_test.dart' as board_add_row_test; import 'board_group_test.dart' as board_group_test; +import 'board_row_test.dart' as board_row_test; -void startTesting() { +void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // Board integration tests diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart index 646a4eb565..d4cc11d7f0 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_more_actions_test.dart @@ -19,12 +19,14 @@ void main() { // Duplicate await tester.openMoreViewActions(); await tester.duplicateByMoreViewActions(); + await tester.pumpAndSettle(); expect(pageFinder, findsNWidgets(2)); // Delete await tester.openMoreViewActions(); await tester.deleteByMoreViewActions(); + await tester.pumpAndSettle(); expect(pageFinder, findsNWidgets(1)); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart index 2b42ef7451..f7a88d7bec 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_cover_image_test.dart @@ -130,24 +130,24 @@ void main() { final searchEmojiTextField = find.byWidgetPredicate( (widget) => widget is TextField && - widget.decoration!.hintText == LocaleKeys.emoji_search.tr(), + widget.decoration!.hintText == LocaleKeys.search_label.tr(), ); await tester.enterText( searchEmojiTextField, - 'hand', + 'punch', ); // change skin tone await tester.editor.changeEmojiSkinTone(EmojiSkinTone.dark); // select an icon with skin tone - const hand = '👋🏿'; - await tester.tapEmoji(hand); - tester.expectToSeeDocumentIcon(hand); + const punch = '👊🏿'; + await tester.tapEmoji(punch); + tester.expectToSeeDocumentIcon(punch); tester.expectViewHasIcon( gettingStarted, ViewLayoutPB.Document, - hand, + punch, ); }); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart index ff0df1c7da..f2a1fae8ae 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,8 +12,8 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('sidebar expand test', () { - bool isExpanded({required FolderCategoryType type}) { - if (type == FolderCategoryType.private) { + bool isExpanded({required FolderSpaceType type}) { + if (type == FolderSpaceType.private) { return find .descendant( of: find.byType(PrivateSectionFolder), @@ -30,19 +30,19 @@ void main() { await tester.tapAnonymousSignInButton(); // first time is expanded - expect(isExpanded(type: FolderCategoryType.private), true); + expect(isExpanded(type: FolderSpaceType.private), true); // collapse the personal folder await tester.tapButton( find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.private), false); + expect(isExpanded(type: FolderSpaceType.private), false); // expand the personal folder await tester.tapButton( find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.private), true); + expect(isExpanded(type: FolderSpaceType.private), true); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart index 072764217c..729ee62a3e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; @@ -46,7 +46,7 @@ void main() { await tester.favoriteViewByName(names[1]); expect( tester.findFavoritePageName(names[1]), - findsNWidgets(2), + findsNWidgets(1), ); await tester.unfavoriteViewByName(gettingStarted); @@ -120,9 +120,9 @@ void main() { (widget) => widget is SingleInnerViewItem && widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite, + widget.spaceType == FolderSpaceType.favorite, ), - findsNWidgets(6), + findsNWidgets(3), ); await tester.hoverOnPageName( @@ -135,7 +135,7 @@ void main() { expect( tester.findAllFavoritePages(), - findsNWidgets(3), + findsNWidgets(2), ); await tester.hoverOnPageName( @@ -168,7 +168,7 @@ void main() { widget.isSelected != null && widget.isSelected!(), ), - findsNWidgets(2), + findsNWidgets(1), ); }, ); diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart index 35bcf599ab..3bc41d78c0 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart @@ -4,7 +4,7 @@ import 'sidebar_favorites_test.dart' as sidebar_favorite_test; import 'sidebar_icon_test.dart' as sidebar_icon_test; import 'sidebar_test.dart' as sidebar_test; -void startTesting() { +void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); // Sidebar integration tests diff --git a/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart b/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart index 72bf8a4fae..f1025b8f1e 100644 --- a/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart +++ b/frontend/appflowy_flutter/integration_test/desktop_runner_3.dart @@ -27,7 +27,7 @@ Future<void> runIntegration3OnDesktop() async { settings_test_runner.main(); share_markdown_test.main(); import_files_test.main(); - sidebar_test_runner.startTesting(); - board_test_runner.startTesting(); + sidebar_test_runner.main(); + board_test_runner.main(); tabs_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart index a2a6318c3d..8bc7236994 100644 --- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart @@ -1,10 +1,5 @@ import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -16,9 +11,9 @@ import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/presentation/screens/screens.dart'; import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; @@ -34,6 +29,10 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'emoji.dart'; @@ -60,6 +59,7 @@ extension CommonOperations on WidgetTester { /// Tap the + button on the home page. Future<void> tapAddViewButton({ String name = gettingStarted, + ViewLayoutPB layout = ViewLayoutPB.Document, }) async { await hoverOnPageName( name, @@ -279,7 +279,7 @@ extension CommonOperations on WidgetTester { bool openAfterCreated = true, }) async { // create a new page - await tapAddViewButton(name: parentName ?? gettingStarted); + await tapAddViewButton(name: parentName ?? gettingStarted, layout: layout); await tapButtonWithName(layout.menuName); final settingsOrFailure = await getIt<KeyValueStorage>().getWithFormat( KVKeys.showRenameDialogWhenCreatingNewFile, diff --git a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart index 0bdcf06367..4eff62321a 100644 --- a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart +++ b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart @@ -81,15 +81,12 @@ class EditorOperations { /// Taps the 'Remove Icon' button in the cover toolbar and the icon popover Future<void> tapRemoveIconButton({bool isInPicker = false}) async { - Finder button = - find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()); - if (isInPicker) { - button = find.descendant( - of: find.byType(FlowyIconPicker), - matching: button, - ); - } - + final Finder button = !isInPicker + ? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr()) + : find.descendant( + of: find.byType(FlowyIconPicker), + matching: find.text(LocaleKeys.button_remove.tr()), + ); await tester.tapButton(button); } diff --git a/frontend/appflowy_flutter/integration_test/shared/expectation.dart b/frontend/appflowy_flutter/integration_test/shared/expectation.dart index aeb3f04cb8..c4b54a0fda 100644 --- a/frontend/appflowy_flutter/integration_test/shared/expectation.dart +++ b/frontend/appflowy_flutter/integration_test/shared/expectation.dart @@ -165,7 +165,7 @@ extension Expectation on WidgetTester { (widget) => widget is SingleInnerViewItem && widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite && + widget.spaceType == FolderSpaceType.favorite && widget.view.name == name && widget.view.layout == layout, skipOffstage: false, @@ -175,7 +175,7 @@ extension Expectation on WidgetTester { (widget) => widget is SingleInnerViewItem && widget.view.isFavorite && - widget.categoryType == FolderCategoryType.favorite, + widget.spaceType == FolderSpaceType.favorite, ); Finder findPageName( diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart index dd13bd088f..3b25c32111 100644 --- a/frontend/appflowy_flutter/integration_test/shared/settings.dart +++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart'; import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; @@ -12,7 +12,6 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flutter_test/flutter_test.dart'; import '../desktop/board/board_hide_groups_test.dart'; - import 'base.dart'; import 'common_operations.dart'; diff --git a/frontend/appflowy_flutter/integration_test/shared/workspace.dart b/frontend/appflowy_flutter/integration_test/shared/workspace.dart index 5137944364..4d20d88ce1 100644 --- a/frontend/appflowy_flutter/integration_test/shared/workspace.dart +++ b/frontend/appflowy_flutter/integration_test/shared/workspace.dart @@ -1,14 +1,14 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'base.dart'; +import 'util.dart'; extension AppFlowyWorkspace on WidgetTester { /// Open workspace menu @@ -36,12 +36,19 @@ extension AppFlowyWorkspace on WidgetTester { matching: find.byType(WorkspaceMoreActionList), ); expect(moreButton, findsOneWidget); - await tapButton(moreButton); - await tapButton(find.findTextInFlowyText(LocaleKeys.button_rename.tr())); - final input = find.byType(TextFormField); - expect(input, findsOneWidget); - await enterText(input, name); - await tapButton(find.text(LocaleKeys.button_ok.tr())); + await hoverOnWidget( + moreButton, + onHover: () async { + await tapButton(moreButton); + await tapButton( + find.findTextInFlowyText(LocaleKeys.button_rename.tr()), + ); + final input = find.byType(TextFormField); + expect(input, findsOneWidget); + await enterText(input, name); + await tapButton(find.text(LocaleKeys.button_ok.tr())); + }, + ); } Future<void> changeWorkspaceIcon(String icon) async { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart index 04149c8238..497f769354 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; class TypeOptionMenuItemValue<T> { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart index b94242eceb..6394ca9647 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/view_page/more_bottom_sheet.dart @@ -38,6 +38,7 @@ class MobileViewPageMoreBottomSheet extends StatelessWidget { case MobileViewBottomSheetBodyAction.removeFromFavorites: context.pop(); context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view)); + break; case MobileViewBottomSheetBodyAction.undo: EditorNotification.undo().post(); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index 718ac5c4e6..be815b6550 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -1,6 +1,6 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; extension BottomSheetPaddingExtension on BuildContext { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart index c56d369676..d683cf3507 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder_header.dart'; @@ -7,6 +5,7 @@ import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MobileFavoriteFolder extends StatelessWidget { @@ -28,7 +27,7 @@ class MobileFavoriteFolder extends StatelessWidget { } return BlocProvider<FolderBloc>( - create: (context) => FolderBloc(type: FolderCategoryType.favorite) + create: (context) => FolderBloc(type: FolderSpaceType.favorite) ..add( const FolderEvent.initial(), ), @@ -55,9 +54,9 @@ class MobileFavoriteFolder extends StatelessWidget { ...views.map( (view) => MobileViewItem( key: ValueKey( - '${FolderCategoryType.favorite.name} ${view.id}', + '${FolderSpaceType.favorite.name} ${view.id}', ), - categoryType: FolderCategoryType.favorite, + spaceType: FolderSpaceType.favorite, isDraggable: false, isFirstChild: view.id == views.first.id, isFeedback: false, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index 7631383faa..6e695ea0e4 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -70,20 +70,20 @@ class MobileFolders extends StatelessWidget { ? [ MobileSectionFolder( title: LocaleKeys.sideBar_workspace.tr(), - categoryType: FolderCategoryType.public, + spaceType: FolderSpaceType.public, views: state.section.publicViews, ), const VSpace(8.0), MobileSectionFolder( title: LocaleKeys.sideBar_private.tr(), - categoryType: FolderCategoryType.private, + spaceType: FolderSpaceType.private, views: state.section.privateViews, ), ] : [ MobileSectionFolder( title: LocaleKeys.sideBar_personal.tr(), - categoryType: FolderCategoryType.public, + spaceType: FolderSpaceType.public, views: state.section.publicViews, ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 1759d32aaf..be47ef1b32 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -15,6 +13,7 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -126,6 +125,7 @@ class _MobileWorkspace extends StatelessWidget { child: WorkspaceIcon( workspace: currentWorkspace, iconSize: 26, + fontSize: 16.0, enableEdit: false, onSelected: (result) => context.read<UserWorkspaceBloc>().add( UserWorkspaceEvent.updateWorkspaceIcon( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart index c9ea1453c9..4d9d109d3f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; @@ -8,9 +6,11 @@ import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MobileSectionFolder extends StatelessWidget { @@ -18,17 +18,17 @@ class MobileSectionFolder extends StatelessWidget { super.key, required this.title, required this.views, - required this.categoryType, + required this.spaceType, }); final String title; final List<ViewPB> views; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; @override Widget build(BuildContext context) { return BlocProvider<FolderBloc>( - create: (context) => FolderBloc(type: categoryType) + create: (context) => FolderBloc(type: spaceType) ..add( const FolderEvent.initial(), ), @@ -48,7 +48,7 @@ class MobileSectionFolder extends StatelessWidget { name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), index: 0, - viewSection: categoryType.toViewSectionPB, + viewSection: spaceType.toViewSectionPB, ), ); context.read<FolderBloc>().add( @@ -64,13 +64,13 @@ class MobileSectionFolder extends StatelessWidget { ...views.map( (view) => MobileViewItem( key: ValueKey( - '${FolderCategoryType.private.name} ${view.id}', + '${FolderSpaceType.private.name} ${view.id}', ), - categoryType: categoryType, + spaceType: spaceType, isFirstChild: view.id == views.first.id, view: view, level: 0, - leftPadding: 16, + leftPadding: HomeSpaceViewSizes.leftPadding, isFeedback: false, onSelected: context.pushView, endActionPane: (context) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart index 4745b248c3..496c5607a5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; @@ -9,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/members/workspa import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; // Only works on mobile. @@ -106,6 +105,7 @@ class _WorkspaceMenuItem extends StatelessWidget { leftIcon: WorkspaceIcon( enableEdit: false, iconSize: 26, + fontSize: 16.0, workspace: workspace, onSelected: (result) => context.read<UserWorkspaceBloc>().add( UserWorkspaceEvent.updateWorkspaceIcon( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index 34fd517613..862ce794b6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; @@ -12,6 +10,7 @@ import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_it import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; @@ -25,7 +24,7 @@ class MobileViewItem extends StatelessWidget { super.key, required this.view, this.parentView, - required this.categoryType, + required this.spaceType, required this.level, this.leftPadding = 10, required this.onSelected, @@ -39,7 +38,7 @@ class MobileViewItem extends StatelessWidget { final ViewPB view; final ViewPB? parentView; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; // indicate the level of the view item // used to calculate the left padding @@ -80,7 +79,7 @@ class MobileViewItem extends StatelessWidget { view: state.view, parentView: parentView, childViews: state.view.childViews, - categoryType: categoryType, + spaceType: spaceType, level: level, leftPadding: leftPadding, showActions: true, @@ -104,7 +103,7 @@ class InnerMobileViewItem extends StatelessWidget { required this.view, required this.parentView, required this.childViews, - required this.categoryType, + required this.spaceType, this.isDraggable = true, this.isExpanded = true, required this.level, @@ -120,7 +119,7 @@ class InnerMobileViewItem extends StatelessWidget { final ViewPB view; final ViewPB? parentView; final List<ViewPB> childViews; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final bool isDraggable; final bool isExpanded; @@ -144,7 +143,7 @@ class InnerMobileViewItem extends StatelessWidget { parentView: parentView, level: level, showActions: showActions, - categoryType: categoryType, + spaceType: spaceType, onSelected: onSelected, isExpanded: isExpanded, isDraggable: isDraggable, @@ -159,9 +158,9 @@ class InnerMobileViewItem extends StatelessWidget { if (childViews.isNotEmpty) { final children = childViews.map((childView) { return MobileViewItem( - key: ValueKey('${categoryType.name} ${childView.id}'), + key: ValueKey('${spaceType.name} ${childView.id}'), parentView: view, - categoryType: categoryType, + spaceType: spaceType, isFirstChild: childView.id == childViews.first.id, view: childView, level: level + 1, @@ -235,7 +234,7 @@ class InnerMobileViewItem extends StatelessWidget { return MobileViewItem( view: view, parentView: parentView, - categoryType: categoryType, + spaceType: spaceType, level: level, onSelected: onSelected, isDraggable: false, @@ -262,7 +261,7 @@ class SingleMobileInnerViewItem extends StatefulWidget { required this.level, required this.leftPadding, this.isDraggable = true, - required this.categoryType, + required this.spaceType, required this.showActions, required this.onSelected, required this.isFeedback, @@ -282,7 +281,7 @@ class SingleMobileInnerViewItem extends StatefulWidget { final bool isDraggable; final bool showActions; final ViewItemOnSelected onSelected; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final ActionPaneBuilder? startActionPane; final ActionPaneBuilder? endActionPane; @@ -407,10 +406,9 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> { ViewEvent.createView( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layout, - section: - widget.categoryType != FolderCategoryType.favorite - ? widget.categoryType.toViewSectionPB - : null, + section: widget.spaceType != FolderSpaceType.favorite + ? widget.spaceType.toViewSectionPB + : null, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart index 86c3b1e625..d7a7b9ceb2 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker.dart @@ -1,13 +1,10 @@ -import 'dart:io'; - import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart'; import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart'; import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; -import 'package:google_fonts/google_fonts.dart'; // use a global value to store the selected emoji to prevent reloading every time. EmojiData? kCachedEmojiData; @@ -28,7 +25,6 @@ class FlowyEmojiPicker extends StatefulWidget { class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> { EmojiData? emojiData; - List<String>? fallbackFontFamily; @override void initState() { @@ -47,13 +43,6 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> { }, ); } - - if (Platform.isAndroid || Platform.isLinux) { - final notoColorEmoji = GoogleFonts.notoColorEmoji().fontFamily; - if (notoColorEmoji != null) { - fallbackFontFamily = [notoColorEmoji]; - } - } } @override @@ -83,16 +72,18 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> { ); }, itemBuilder: (context, emojiId, emoji, callback) { - return FlowyIconButton( - iconPadding: PlatformExtension.isWindows - ? const EdgeInsets.only(bottom: 2.0) - : const EdgeInsets.all(2), - icon: FlowyText( - emoji, - fontSize: 28.0, - fallbackFontFamily: fallbackFontFamily, + return SizedBox( + width: 36, + height: 36, + child: FlowyButton( + margin: EdgeInsets.zero, + radius: Corners.s8Border, + text: FlowyText.emoji( + emoji, + fontSize: 24.0, + ), + onTap: () => callback(emojiId, emoji), ), - onPressed: () => callback(emojiId, emoji), ); }, searchBarBuilder: (context, keyword, skinTone) { diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart index 9619f00d30..9f05c80f09 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_picker_header.dart @@ -16,9 +16,14 @@ class FlowyEmojiHeader extends StatelessWidget { if (PlatformExtension.isDesktopOrWeb) { return Container( height: 22, - padding: const EdgeInsets.symmetric(horizontal: 8.0), color: Theme.of(context).cardColor, - child: FlowyText.regular(category.id), + child: Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: FlowyText.regular( + category.id, + color: Theme.of(context).hintColor, + ), + ), ); } else { return Column( diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart index 1b01e6aee8..c6cec89ecc 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_search_bar.dart @@ -42,7 +42,7 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> { Widget build(BuildContext context) { return Padding( padding: EdgeInsets.symmetric( - vertical: 8.0, + vertical: 12.0, horizontal: PlatformExtension.isDesktopOrWeb ? 0.0 : 8.0, ), child: Row( @@ -52,16 +52,15 @@ class _FlowyEmojiSearchBarState extends State<FlowyEmojiSearchBar> { onKeywordChanged: widget.onKeywordChanged, ), ), - const HSpace(6.0), + const HSpace(8.0), _RandomEmojiButton( emojiData: widget.emojiData, onRandomEmojiSelected: widget.onRandomEmojiSelected, ), - const HSpace(6.0), + const HSpace(8.0), FlowyEmojiSkinToneSelector( onEmojiSkinToneChanged: widget.onSkinToneChanged, ), - const HSpace(6.0), ], ), ); @@ -79,20 +78,30 @@ class _RandomEmojiButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.emoji_random.tr(), - child: FlowyButton( - useIntrinsicWidth: true, - text: const Icon( - Icons.shuffle_rounded, + return Container( + width: 36, + height: 36, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide(color: Color(0x1E171717)), + borderRadius: BorderRadius.circular(8), + ), + ), + child: FlowyTooltip( + message: LocaleKeys.emoji_random.tr(), + child: FlowyButton( + useIntrinsicWidth: true, + text: const FlowySvg( + FlowySvgs.icon_shuffle_s, + ), + onTap: () { + final random = emojiData.random; + onRandomEmojiSelected( + random.$1, + random.$2, + ); + }, ), - onTap: () { - final random = emojiData.random; - onRandomEmojiSelected( - random.$1, - random.$2, - ); - }, ), ); } @@ -123,32 +132,35 @@ class _SearchTextFieldState extends State<_SearchTextField> { @override Widget build(BuildContext context) { - return ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 32.0, - ), + return SizedBox( + height: 36.0, child: FlowyTextField( focusNode: focusNode, - hintText: LocaleKeys.emoji_search.tr(), + hintText: LocaleKeys.search_label.tr(), + hintStyle: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: 14.0, + fontWeight: FontWeight.w400, + color: Theme.of(context).hintColor, + ), controller: controller, onChanged: widget.onKeywordChanged, prefixIcon: const Padding( padding: EdgeInsets.only( - left: 8.0, - right: 4.0, + left: 14.0, + right: 8.0, ), child: FlowySvg( FlowySvgs.search_s, ), ), prefixIconConstraints: const BoxConstraints( - maxHeight: 18.0, + maxHeight: 20.0, ), suffixIcon: Padding( padding: const EdgeInsets.all(4.0), child: FlowyButton( text: const FlowySvg( - FlowySvgs.close_lg, + FlowySvgs.m_app_bar_close_s, ), margin: EdgeInsets.zero, useIntrinsicWidth: true, diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart index e8da112660..3add90773d 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_skin_tone.dart @@ -57,7 +57,7 @@ class _FlowyEmojiSkinToneSelectorState child: FlowyTooltip( message: LocaleKeys.emoji_selectSkinTone.tr(), child: _buildIconButton( - lastSelectedEmojiSkinTone?.icon ?? '✋', + lastSelectedEmojiSkinTone?.icon ?? '👋', () => controller.show(), ), ), @@ -65,19 +65,22 @@ class _FlowyEmojiSkinToneSelectorState } Widget _buildIconButton(String icon, VoidCallback onPressed) { - return FlowyIconButton( - key: emojiSkinToneKey(icon), - icon: Padding( - // add a left padding to align the emoji center - padding: const EdgeInsets.only( - left: 3.0, - ), - child: FlowyText( - icon, - fontSize: 22.0, - ), + return Container( + width: 36, + height: 36, + decoration: BoxDecoration( + border: Border.all(color: const Color(0x1E171717)), + borderRadius: BorderRadius.circular(8), + ), + child: FlowyButton( + key: emojiSkinToneKey(icon), + margin: EdgeInsets.zero, + text: FlowyText.emoji( + icon, + fontSize: 24.0, + ), + onTap: onPressed, ), - onPressed: onPressed, ); } } @@ -86,17 +89,17 @@ extension EmojiSkinToneIcon on EmojiSkinTone { String get icon { switch (this) { case EmojiSkinTone.none: - return '✋'; + return '👋'; case EmojiSkinTone.light: - return '✋🏻'; + return '👋🏻'; case EmojiSkinTone.mediumLight: - return '✋🏼'; + return '👋🏼'; case EmojiSkinTone.medium: - return '✋🏽'; + return '👋🏽'; case EmojiSkinTone.mediumDark: - return '✋🏾'; + return '👋🏾'; case EmojiSkinTone.dark: - return '✋🏿'; + return '👋🏿'; } } } diff --git a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart index e9fed800d4..5481c7676a 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/emoji/emoji_text.dart @@ -29,6 +29,7 @@ class EmojiText extends StatelessWidget { emoji, fontSize: fontSize, textAlign: textAlign, + strutStyle: const StrutStyle(forceStrutHeight: true), fallbackFontFamily: _cachedFallbackFontFamily, lineHeight: lineHeight, ); diff --git a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart index 1ecea2b787..08e63251e0 100644 --- a/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/base/icon/icon_picker.dart @@ -1,13 +1,10 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/icon.pbenum.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; +import 'package:flutter/material.dart'; extension ToProto on FlowyIconType { ViewIconTypePB toProto() { @@ -54,57 +51,28 @@ class FlowyIconPicker extends StatelessWidget { @override Widget build(BuildContext context) { - // ONLY supports emoji picker for now - return DefaultTabController( - length: 1, + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ + const VSpace(8.0), Row( children: [ - _buildTabs(context), + FlowyText(LocaleKeys.newSettings_workplace_chooseAnIcon.tr()), const Spacer(), _RemoveIconButton( onTap: () => onSelected(EmojiPickerResult.none()), ), ], ), - const Divider(height: 2), + const VSpace(12.0), + const Divider(height: 0.5), Expanded( - child: TabBarView( - children: [ - FlowyEmojiPicker( - emojiPerLine: _getEmojiPerLine(context), - onEmojiSelected: (_, emoji) => - onSelected(EmojiPickerResult.emoji(emoji)), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildTabs(BuildContext context) { - return Align( - alignment: Alignment.centerLeft, - child: TabBar( - indicatorSize: TabBarIndicatorSize.label, - isScrollable: true, - overlayColor: WidgetStatePropertyAll( - Theme.of(context).colorScheme.secondary, - ), - padding: EdgeInsets.zero, - tabs: [ - FlowyHover( - style: const HoverStyle(borderRadius: BorderRadius.zero), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 8.0, - ), - child: FlowyText(LocaleKeys.emoji_emojiTab.tr()), + child: FlowyEmojiPicker( + emojiPerLine: _getEmojiPerLine(context), + onEmojiSelected: (_, emoji) => + onSelected(EmojiPickerResult.emoji(emoji)), ), ), ], @@ -117,7 +85,7 @@ class FlowyIconPicker extends StatelessWidget { return 9; } final width = MediaQuery.of(context).size.width; - return width ~/ 46.0; // the size of the emoji + return width ~/ 40.0; // the size of the emoji } } @@ -129,14 +97,14 @@ class _RemoveIconButton extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( - height: 28, + height: 24, child: FlowyButton( onTap: onTap, useIntrinsicWidth: true, - text: FlowyText.small( - LocaleKeys.document_plugins_cover_removeIcon.tr(), + text: FlowyText.regular( + LocaleKeys.button_remove.tr(), + color: Theme.of(context).hintColor, ), - leftIcon: const FlowySvg(FlowySvgs.delete_s), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index d8aa978e12..787e08760b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -236,7 +236,8 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { final String? initialRowId; @override - Widget get leftBarItem => ViewTitleBar(view: notifier.view); + Widget get leftBarItem => + ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view); @override Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); @@ -278,7 +279,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { ] : [], DatabaseShareButton(key: ValueKey(view.id), view: view), - const HSpace(4), + const HSpace(10), ViewFavoriteButton(view: view), const HSpace(4), MoreViewActions(view: view, isDocument: false), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart index d6c84e39fe..7ede902085 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/share_button.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/share_bloc.dart'; import 'package:appflowy/startup/startup.dart'; @@ -13,6 +11,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DatabaseShareButton extends StatelessWidget { @@ -39,11 +38,7 @@ class DatabaseShareButton extends StatelessWidget { ); }, child: BlocBuilder<DatabaseShareBloc, DatabaseShareState>( - builder: (context, state) => ConstrainedBox( - constraints: const BoxConstraints.expand( - height: 30, - width: 100, - ), + builder: (context, state) => IntrinsicWidth( child: DatabaseShareActionList(view: view), ), ), @@ -106,6 +101,8 @@ class DatabaseShareActionListState extends State<DatabaseShareActionList> { onPointerDown: (_) => controller.show(), child: RoundedTextButton( title: LocaleKeys.shareAction_buttonText.tr(), + padding: const EdgeInsets.symmetric(horizontal: 12.0), + fontSize: 14.0, textColor: Theme.of(context).colorScheme.onPrimary, onPressed: () {}, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 13242bbe0a..bd9e4891d2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -1,7 +1,5 @@ library document_plugin; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; @@ -22,6 +20,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DocumentPluginBuilder extends PluginBuilder { @@ -130,7 +129,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder } @override - Widget get leftBarItem => ViewTitleBar(view: view); + Widget get leftBarItem => ViewTitleBar(key: ValueKey(view.id), view: view); @override Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view); @@ -162,7 +161,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder key: ValueKey('share_button_${view.id}'), view: view, ), - const HSpace(4), + const HSpace(10), ViewFavoriteButton( key: ValueKey('favorite_button_${view.id}'), view: view, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 9642a4a8cc..2d125ba647 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -392,12 +392,6 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { if (widget.editorState.document.isEmpty) { return (true, Selection.collapsed(Position(path: [0]))); } - final nodes = - widget.editorState.document.root.children.where((e) => e.delta != null); - final isAllEmpty = nodes.isNotEmpty && nodes.every((e) => e.delta!.isEmpty); - if (isAllEmpty) { - return (true, Selection.collapsed(Position(path: nodes.first.path))); - } return const (false, null); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart index cfff8d4938..d5e99e13f8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/option_action.dart @@ -141,7 +141,7 @@ enum OptionDepthType { class DividerOptionAction extends CustomActionCell { @override - Widget buildWithContext(BuildContext context) { + Widget buildWithContext(BuildContext context, PopoverController controller) { return const Divider( height: 1.0, thickness: 1.0, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart index df78f6261e..2a1101794a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart @@ -1,11 +1,10 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; class EmojiPickerButton extends StatelessWidget { @@ -19,6 +18,7 @@ class EmojiPickerButton extends StatelessWidget { this.offset, this.direction, this.title, + this.showBorder = true, }); final String emoji; @@ -30,6 +30,7 @@ class EmojiPickerButton extends StatelessWidget { final Offset? offset; final PopoverDirection? direction; final String? title; + final bool showBorder; @override Widget build(BuildContext context) { @@ -51,22 +52,28 @@ class EmojiPickerButton extends StatelessWidget { onExit: () {}, ), ), - child: emoji.isEmpty && defaultIcon != null - ? FlowyButton( - useIntrinsicWidth: true, - text: defaultIcon!, - onTap: popoverController.show, - ) - : FlowyTextButton( - emoji, - overflow: TextOverflow.visible, - fontSize: emojiSize, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(minWidth: 35.0), - fillColor: Colors.transparent, - mainAxisAlignment: MainAxisAlignment.center, - onPressed: popoverController.show, - ), + child: Container( + width: 30.0, + height: 30.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: showBorder + ? Border.all( + color: Theme.of(context).dividerColor, + ) + : null, + ), + child: FlowyButton( + margin: emoji.isEmpty && defaultIcon != null + ? EdgeInsets.zero + : const EdgeInsets.only(left: 2.0), + expandText: false, + text: emoji.isEmpty && defaultIcon != null + ? defaultIcon! + : FlowyText.emoji(emoji, fontSize: emojiSize), + onTap: popoverController.show, + ), + ), ); } return FlowyTextButton( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart index ce756b9ffd..7c84f2c31b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/text_input.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 7a606f33ef..3a22909744 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -300,9 +300,10 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> { : (CoverType.color, '0xffe8e0ff'), ), useIntrinsicWidth: true, - leftIcon: const FlowySvg(FlowySvgs.image_s), + leftIcon: const FlowySvg(FlowySvgs.add_cover_s), text: FlowyText.small( LocaleKeys.document_plugins_cover_addCover.tr(), + color: Theme.of(context).hintColor, ), ), ); @@ -311,28 +312,24 @@ class _DocumentHeaderToolbarState extends State<DocumentHeaderToolbar> { if (widget.hasIcon) { children.add( FlowyButton( - leftIconSize: const Size.square(18), onTap: () => widget.onIconOrCoverChanged(icon: ""), useIntrinsicWidth: true, - leftIcon: const Icon( - Icons.emoji_emotions_outlined, - size: 18, - ), + leftIcon: const FlowySvg(FlowySvgs.add_icon_s), + iconPadding: 4.0, text: FlowyText.small( LocaleKeys.document_plugins_cover_removeIcon.tr(), + color: Theme.of(context).hintColor, ), ), ); } else { Widget child = FlowyButton( - leftIconSize: const Size.square(18), useIntrinsicWidth: true, - leftIcon: const Icon( - Icons.emoji_emotions_outlined, - size: 18, - ), + leftIcon: const FlowySvg(FlowySvgs.add_icon_s), + iconPadding: 4.0, text: FlowyText.small( LocaleKeys.document_plugins_cover_addIcon.tr(), + color: Theme.of(context).hintColor, ), onTap: PlatformExtension.isDesktop ? null diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart index 36d90ac6fb..84ab2f7380 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart @@ -148,7 +148,7 @@ class _UnsplashImages extends StatelessWidget { type: type, photo: photo, onTap: () => onSelectUnsplashImage( - photo.urls.regular.toString(), + photo.urls.full.toString(), ), ), ) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart index fa15bc10d5..eb82f3d1fa 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/share/share_button.dart @@ -41,12 +41,9 @@ class DocumentShareButton extends StatelessWidget { ); }, child: BlocBuilder<DocumentShareBloc, DocumentShareState>( - builder: (context, state) => ConstrainedBox( - constraints: const BoxConstraints.expand( - height: 30, - width: 100, - ), - child: ShareActionList(view: view), + builder: (context, state) => SizedBox( + height: 32.0, + child: IntrinsicWidth(child: ShareActionList(view: view)), ), ), ), @@ -120,7 +117,9 @@ class ShareActionListState extends State<ShareActionList> { onPointerDown: (_) => controller.show(), child: RoundedTextButton( title: LocaleKeys.shareAction_buttonText.tr(), + padding: const EdgeInsets.symmetric(horizontal: 12.0), onPressed: () {}, + fontSize: 14.0, textColor: Theme.of(context).colorScheme.onPrimary, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart index 7b9e86e16b..6ce62acae8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart @@ -1,4 +1,5 @@ import 'package:appflowy/workspace/application/favorite/favorite_service.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -32,25 +33,23 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> { _listener.start( favoritesUpdated: _onFavoritesUpdated, ); - final result = await _service.readFavorites(); - emit( - result.fold( - (view) => state.copyWith( - views: view.items, - ), - (error) => state.copyWith( - views: [], - ), - ), - ); + add(const FavoriteEvent.fetchFavorites()); }, fetchFavorites: () async { final result = await _service.readFavorites(); emit( result.fold( - (view) => state.copyWith( - views: view.items, - ), + (favoriteViews) { + final views = favoriteViews.items.map((v) => v.item).toList(); + final pinnedViews = views.where((v) => v.isPinned).toList(); + final unpinnedViews = + views.where((v) => !v.isPinned).toList(); + return state.copyWith( + views: views, + pinnedViews: pinnedViews, + unpinnedViews: unpinnedViews, + ); + }, (error) => state.copyWith( views: [], ), @@ -58,11 +57,26 @@ class FavoriteBloc extends Bloc<FavoriteEvent, FavoriteState> { ); }, toggle: (view) async { + if (view.isFavorite) { + await _service.unpinFavorite(view); + } else if (state.pinnedViews.length < 3) { + // pin the view if there are less than 3 pinned views + await _service.pinFavorite(view); + } + await _service.toggleFavorite( view.id, !view.isFavorite, ); }, + pin: (view) async { + await _service.pinFavorite(view); + add(const FavoriteEvent.fetchFavorites()); + }, + unpin: (view) async { + await _service.unpinFavorite(view); + add(const FavoriteEvent.fetchFavorites()); + }, ); }, ); @@ -84,12 +98,16 @@ class FavoriteEvent with _$FavoriteEvent { const factory FavoriteEvent.initial() = Initial; const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite; const factory FavoriteEvent.fetchFavorites() = FetchFavorites; + const factory FavoriteEvent.pin(ViewPB view) = PinFavorite; + const factory FavoriteEvent.unpin(ViewPB view) = UnpinFavorite; } @freezed class FavoriteState with _$FavoriteState { const factory FavoriteState({ required List<ViewPB> views, + @Default([]) List<ViewPB> pinnedViews, + @Default([]) List<ViewPB> unpinnedViews, }) = _FavoriteState; factory FavoriteState.initial() => const FavoriteState( diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart index d9343e2ee3..71bb8423df 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_service.dart @@ -1,10 +1,15 @@ +import 'dart:convert'; + +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:collection/collection.dart'; class FavoriteService { - Future<FlowyResult<RepeatedViewPB, FlowyError>> readFavorites() { + Future<FlowyResult<RepeatedFavoriteViewPB, FlowyError>> readFavorites() { return FolderEventReadFavorites().send(); } @@ -15,4 +20,33 @@ class FavoriteService { final id = RepeatedViewIdPB.create()..items.add(viewId); return FolderEventToggleFavorite(id).send(); } + + Future<FlowyResult<void, FlowyError>> pinFavorite(ViewPB view) async { + return pinOrUnpinFavorite(view, true); + } + + Future<FlowyResult<void, FlowyError>> unpinFavorite(ViewPB view) async { + return pinOrUnpinFavorite(view, false); + } + + Future<FlowyResult<void, FlowyError>> pinOrUnpinFavorite( + ViewPB view, + bool isPinned, + ) async { + try { + final current = view.extra.isNotEmpty ? jsonDecode(view.extra) : {}; + final merged = mergeMaps( + current, + <String, dynamic>{ViewExtKeys.isPinnedKey: isPinned}, + ); + await ViewBackendService.updateView( + viewId: view.id, + extra: jsonEncode(merged), + ); + } catch (e) { + return FlowyResult.failure(FlowyError(msg: 'Failed to pin favorite: $e')); + } + + return FlowyResult.success(null); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart index a70073236a..0c6fe9aaed 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/base_appearance.dart @@ -33,7 +33,7 @@ abstract class BaseAppearance { double? letterSpacing, double? lineHeight, }) { - fontSize = fontSize ?? FontSizes.s12; + fontSize = fontSize ?? FontSizes.s14; fontWeight = fontWeight ?? (PlatformExtension.isDesktopOrWeb ? FontWeight.w500 : FontWeight.w400); letterSpacing = fontSize * (letterSpacing ?? 0.005); diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart index 4dd934f60b..c85f3bd0b0 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart @@ -9,18 +9,18 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'folder_bloc.freezed.dart'; -enum FolderCategoryType { +enum FolderSpaceType { favorite, private, public; ViewSectionPB get toViewSectionPB { switch (this) { - case FolderCategoryType.private: + case FolderSpaceType.private: return ViewSectionPB.Private; - case FolderCategoryType.public: + case FolderSpaceType.public: return ViewSectionPB.Public; - case FolderCategoryType.favorite: + case FolderSpaceType.favorite: throw UnimplementedError(); } } @@ -28,7 +28,7 @@ enum FolderCategoryType { class FolderBloc extends Bloc<FolderEvent, FolderState> { FolderBloc({ - required FolderCategoryType type, + required FolderSpaceType type, }) : super(FolderState.initial(type)) { on<FolderEvent>((event, emit) async { await event.map( @@ -84,12 +84,12 @@ class FolderEvent with _$FolderEvent { @freezed class FolderState with _$FolderState { const factory FolderState({ - required FolderCategoryType type, + required FolderSpaceType type, required bool isExpanded, }) = _FolderState; factory FolderState.initial( - FolderCategoryType type, + FolderSpaceType type, ) => FolderState( type: type, diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index 1cfb89c8f9..ebf1cf2b3f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -75,7 +75,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> { ); final isExpanded = await _getViewIsExpanded(view); emit(state.copyWith(isExpanded: isExpanded)); - await _loadViewsWhenExpanded(emit, isExpanded); + await _loadChildViews(emit); }, setIsEditing: (e) { emit(state.copyWith(isEditing: e.isEditing)); @@ -222,6 +222,12 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> { viewIcon: value.icon ?? '', ); }, + collapseAllPages: (value) async { + for (final childView in view.childViews) { + await _setViewIsExpanded(childView, false); + } + add(const ViewEvent.setIsExpanded(false)); + }, ); }, ); @@ -270,6 +276,33 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> { ); } + Future<void> _loadChildViews( + Emitter<ViewState> emit, + ) async { + final viewsOrFailed = + await ViewBackendService.getChildViews(viewId: state.view.id); + + viewsOrFailed.fold( + (childViews) { + state.view.freeze(); + final viewWithChildViews = state.view.rebuild((b) { + b.childViews.clear(); + b.childViews.addAll(childViews); + }); + emit( + state.copyWith( + view: viewWithChildViews, + ), + ); + }, + (error) => emit( + state.copyWith( + successOrFailure: FlowyResult.failure(error), + ), + ), + ); + } + Future<void> _setViewIsExpanded(ViewPB view, bool isExpanded) async { final result = await getIt<KeyValueStorage>().get(KVKeys.expandedViews); final Map map; @@ -388,6 +421,7 @@ class ViewEvent with _$ViewEvent { bool isPublic, ) = UpdateViewVisibility; const factory ViewEvent.updateIcon(String? icon) = UpdateIcon; + const factory ViewEvent.collapseAllPages() = CollapseAllPages; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index ca3bb62fbb..c3b17cb742 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -32,6 +32,9 @@ class ViewExtKeys { static String coverKey = 'cover'; static String coverTypeKey = 'type'; static String coverValueKey = 'value'; + + // is pinned + static String isPinnedKey = 'is_pinned'; } extension ViewExtension on ViewPB { @@ -96,6 +99,16 @@ extension ViewExtension on ViewPB { FlowySvgData get iconData => layout.icon; + bool get isPinned { + try { + final ext = jsonDecode(extra); + final isPinned = ext[ViewExtKeys.isPinnedKey] ?? false; + return isPinned; + } catch (e) { + return false; + } + } + PageStyleCover? get cover { if (layout != ViewLayoutPB.Document) { return null; diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart new file mode 100644 index 0000000000..491ff36786 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bar_bloc.dart @@ -0,0 +1,48 @@ +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'view_title_bar_bloc.freezed.dart'; + +class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> { + ViewTitleBarBloc({ + required this.view, + }) : super(ViewTitleBarState.initial()) { + on<ViewTitleBarEvent>( + (event, emit) async { + await event.when( + initial: () async { + add(const ViewTitleBarEvent.reload()); + }, + reload: () async { + final List<ViewPB> ancestors = + await ViewBackendService.getViewAncestors(view.id).fold( + (s) => s.items, + (f) => [], + ); + emit(state.copyWith(ancestors: ancestors)); + }, + ); + }, + ); + } + + final ViewPB view; +} + +@freezed +class ViewTitleBarEvent with _$ViewTitleBarEvent { + const factory ViewTitleBarEvent.initial() = Initial; + const factory ViewTitleBarEvent.reload() = Reload; +} + +@freezed +class ViewTitleBarState with _$ViewTitleBarState { + const factory ViewTitleBarState({ + required List<ViewPB> ancestors, + }) = _ViewTitleBarState; + + factory ViewTitleBarState.initial() => const ViewTitleBarState(ancestors: []); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart new file mode 100644 index 0000000000..737539763a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/view_title/view_title_bloc.dart @@ -0,0 +1,73 @@ +import 'package:appflowy/workspace/application/view/prelude.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'view_title_bloc.freezed.dart'; + +class ViewTitleBloc extends Bloc<ViewTitleEvent, ViewTitleState> { + ViewTitleBloc({ + required this.view, + }) : viewListener = ViewListener(viewId: view.id), + super(ViewTitleState.initial()) { + on<ViewTitleEvent>( + (event, emit) async { + await event.when( + initial: () async { + emit( + state.copyWith( + name: view.name, + icon: view.icon.value, + ), + ); + + viewListener.start( + onViewUpdated: (view) { + add( + ViewTitleEvent.updateNameOrIcon( + view.name, + view.icon.value, + ), + ); + }, + ); + }, + updateNameOrIcon: (name, icon) async { + emit( + state.copyWith( + name: name, + icon: icon, + ), + ); + }, + ); + }, + ); + } + + final ViewPB view; + final ViewListener viewListener; + + @override + Future<void> close() { + viewListener.stop(); + return super.close(); + } +} + +@freezed +class ViewTitleEvent with _$ViewTitleEvent { + const factory ViewTitleEvent.initial() = Initial; + const factory ViewTitleEvent.updateNameOrIcon(String name, String icon) = + UpdateNameOrIcon; +} + +@freezed +class ViewTitleState with _$ViewTitleState { + const factory ViewTitleState({ + required String name, + required String icon, + }) = _ViewTitleState; + + factory ViewTitleState.initial() => const ViewTitleState(name: '', icon: ''); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart index fd15cf8d23..bb7eb3600b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_sizes.dart @@ -1,12 +1,21 @@ class HomeSizes { static const double menuAddButtonHeight = 60; - static const double topBarHeight = 60; + static const double topBarHeight = 44; static const double editPanelTopBarHeight = 60; static const double editPanelWidth = 400; - static const double tabBarHeigth = 40; + static const double tabBarHeight = 40; static const double tabBarWidth = 200; + static const double workspaceSectionHeight = 32; + static const double searchSectionHeight = 30; + static const double newPageSectionHeight = 30; } class HomeInsets { - static const double topBarTitlePadding = 12; + static const double topBarTitleHorizontalPadding = 12; + static const double topBarTitleVerticalPadding = 12; +} + +class HomeSpaceViewSizes { + static const double leftPadding = 16.0; + static const double viewHeight = 30.0; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index 2a0eb6ec7d..b54a170e93 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; - import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/plugins/blank/blank.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; @@ -13,6 +10,8 @@ import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; import 'package:time/time.dart'; @@ -275,14 +274,12 @@ class HomeTopBar extends StatelessWidget { return Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.onSecondaryContainer, - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor), - ), ), - height: HomeSizes.topBarHeight, + height: HomeSizes.topBarHeight + HomeInsets.topBarTitleVerticalPadding, child: Padding( padding: const EdgeInsets.symmetric( - horizontal: HomeInsets.topBarTitlePadding, + horizontal: HomeInsets.topBarTitleHorizontalPadding, + vertical: HomeInsets.topBarTitleVerticalPadding, ), child: Row( children: [ diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart index ed5a667e91..e979f102e3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart @@ -6,7 +6,7 @@ import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart new file mode 100644 index 0000000000..16415e5e93 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart @@ -0,0 +1,183 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FavoriteFolder extends StatefulWidget { + const FavoriteFolder({ + super.key, + required this.views, + }); + + final List<ViewPB> views; + + @override + State<FavoriteFolder> createState() => _FavoriteFolderState(); +} + +class _FavoriteFolderState extends State<FavoriteFolder> { + final isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (widget.views.isEmpty) { + return const SizedBox.shrink(); + } + + return BlocProvider<FolderBloc>( + create: (context) => FolderBloc(type: FolderSpaceType.favorite) + ..add(const FolderEvent.initial()), + child: BlocBuilder<FolderBloc, FolderState>( + builder: (context, state) { + return MouseRegion( + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: Column( + children: [ + FavoriteHeader( + onPressed: () => context + .read<FolderBloc>() + .add(const FolderEvent.expandOrUnExpand()), + ), + // pages + ..._buildViews(context, state, isHovered), + if (state.isExpanded) ...[ + // more button + const VSpace(2), + const FavoriteMoreButton(), + ], + ], + ), + ); + }, + ), + ); + } + + Iterable<Widget> _buildViews( + BuildContext context, + FolderState state, + ValueNotifier<bool> isHovered, + ) { + if (!state.isExpanded) { + return []; + } + + return context.read<FavoriteBloc>().state.pinnedViews.map( + (view) => ViewItem( + key: ValueKey( + '${FolderSpaceType.favorite.name} ${view.id}', + ), + spaceType: FolderSpaceType.favorite, + isDraggable: false, + isFirstChild: view.id == widget.views.first.id, + isFeedback: false, + view: view, + leftPadding: HomeSpaceViewSizes.leftPadding, + leftIconBuilder: (_, __) => + const HSpace(HomeSpaceViewSizes.leftPadding), + level: 0, + isHovered: isHovered, + rightIconsBuilder: (context, view) => [ + FavoriteMoreActions(view: view), + const HSpace(8.0), + FavoritePinAction(view: view), + const HSpace(4.0), + ], + shouldRenderChildren: false, + onTertiarySelected: (_, view) => + context.read<TabsBloc>().openTab(view), + onSelected: (_, view) { + if (HardwareKeyboard.instance.isControlPressed) { + context.read<TabsBloc>().openTab(view); + } + + context.read<TabsBloc>().openPlugin(view); + }, + ), + ); + } +} + +class FavoriteHeader extends StatelessWidget { + const FavoriteHeader({ + super.key, + required this.onPressed, + }); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return FlowyButton( + onTap: onPressed, + margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0), + leftIcon: const FlowySvg( + FlowySvgs.favorite_header_icon_s, + blendMode: null, + ), + iconPadding: 10.0, + text: FlowyText.regular(LocaleKeys.sideBar_favorites.tr()), + ); + } +} + +class FavoriteMoreButton extends StatelessWidget { + const FavoriteMoreButton({super.key}); + + @override + Widget build(BuildContext context) { + final favoriteBloc = context.watch<FavoriteBloc>(); + final unpinnedViews = favoriteBloc.state.unpinnedViews; + // only show the more button if there are unpinned views + if (unpinnedViews.isEmpty) { + return const SizedBox.shrink(); + } + + const minWidth = 260.0; + return AppFlowyPopover( + constraints: const BoxConstraints( + minWidth: minWidth, + ), + decoration: FlowyDecoration.decoration( + Theme.of(context).cardColor, + Theme.of(context).colorScheme.shadow, + borderRadius: 10.0, + ), + popupBuilder: (_) { + return BlocProvider.value( + value: favoriteBloc, + child: const FavoriteMenu(minWidth: minWidth), + ); + }, + margin: EdgeInsets.zero, + child: FlowyButton( + onTap: () {}, + margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 7.0), + leftIcon: const FlowySvg( + FlowySvgs.workspace_three_dots_s, + ), + text: FlowyText.regular(LocaleKeys.button_more.tr()), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart new file mode 100644 index 0000000000..4ec75da1d2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu.dart @@ -0,0 +1,193 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +const double _kHorizontalPadding = 10.0; +const double _kVerticalPadding = 10.0; + +class FavoriteMenu extends StatelessWidget { + const FavoriteMenu({super.key, required this.minWidth}); + + final double minWidth; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only( + left: _kHorizontalPadding, + right: _kHorizontalPadding, + top: _kVerticalPadding, + bottom: _kVerticalPadding, + ), + child: BlocProvider( + create: (context) => + FavoriteMenuBloc()..add(const FavoriteMenuEvent.initial()), + child: BlocBuilder<FavoriteMenuBloc, FavoriteMenuState>( + builder: (context, state) { + if (state.views.isEmpty) { + return const SizedBox.shrink(); + } + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const VSpace(4), + _FavoriteSearchField( + width: minWidth - 2 * _kHorizontalPadding, + onSearch: (context, text) { + context + .read<FavoriteMenuBloc>() + .add(FavoriteMenuEvent.search(text)); + }, + ), + const VSpace(12), + _buildViews(context, state), + ], + ); + }, + ), + ), + ); + } + + Widget _buildViews(BuildContext context, FavoriteMenuState state) { + return Container( + width: minWidth - 2 * _kHorizontalPadding, + constraints: const BoxConstraints( + maxHeight: 300, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ..._buildGroups( + context, + state.todayViews, + LocaleKeys.sideBar_today.tr(), + ), + ..._buildGroups( + context, + state.thisWeekViews, + LocaleKeys.sideBar_thisWeek.tr(), + ), + ..._buildGroups( + context, + state.otherViews, + LocaleKeys.sideBar_others.tr(), + ), + ], + ), + ), + ); + } + + List<Widget> _buildGroups( + BuildContext context, + List<ViewPB> views, + String title, + ) { + return [ + if (views.isNotEmpty) ...[ + SizedBox( + height: 24, + child: FlowyText( + title, + fontSize: 12.0, + color: Theme.of(context).hintColor, + ), + ), + const VSpace(2), + _buildGroupedViews(context, views), + const VSpace(8), + const Divider(height: 1), + const VSpace(8), + ], + ]; + } + + Widget _buildGroupedViews(BuildContext context, List<ViewPB> views) { + return Column( + mainAxisSize: MainAxisSize.min, + children: views + .map( + (e) => ViewItem( + key: ValueKey(e.id), + view: e, + spaceType: FolderSpaceType.favorite, + level: 0, + onSelected: (view, _) {}, + isFeedback: false, + isDraggable: false, + shouldRenderChildren: false, + leftIconBuilder: (_, __) => const HSpace(4.0), + rightIconsBuilder: (_, view) => [ + FavoriteMoreActions(view: view), + const HSpace(6.0), + FavoritePinAction(view: view), + const HSpace(4.0), + ], + ), + ) + .toList(), + ); + } +} + +class _FavoriteSearchField extends StatelessWidget { + const _FavoriteSearchField({ + required this.width, + required this.onSearch, + }); + + final double width; + final void Function(BuildContext context, String text) onSearch; + + @override + Widget build(BuildContext context) { + return Container( + height: 30, + width: width, + clipBehavior: Clip.antiAlias, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + side: const BorderSide( + width: 1.20, + strokeAlign: BorderSide.strokeAlignOutside, + color: Color(0xFF00BCF0), + ), + borderRadius: BorderRadius.circular(8), + ), + ), + child: CupertinoSearchTextField( + onChanged: (text) => onSearch(context, text), + padding: EdgeInsets.zero, + placeholder: LocaleKeys.search_label.tr(), + prefixIcon: const FlowySvg(FlowySvgs.m_search_m), + prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0), + suffixIcon: const Icon(Icons.close), + suffixInsets: const EdgeInsets.only(right: 8.0), + itemSize: 16.0, + decoration: const BoxDecoration( + color: Colors.transparent, + ), + placeholderStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).hintColor, + fontWeight: FontWeight.w400, + ), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w400, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart new file mode 100644 index 0000000000..4ad963f50d --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart @@ -0,0 +1,124 @@ +import 'package:appflowy/workspace/application/favorite/favorite_service.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'favorite_menu_bloc.freezed.dart'; + +class FavoriteMenuBloc extends Bloc<FavoriteMenuEvent, FavoriteMenuState> { + FavoriteMenuBloc() : super(FavoriteMenuState.initial()) { + on<FavoriteMenuEvent>( + (event, emit) async { + await event.when( + initial: () async { + final favoriteViews = await _service.readFavorites(); + List<ViewPB> views = []; + List<ViewPB> todayViews = []; + List<ViewPB> thisWeekViews = []; + List<ViewPB> otherViews = []; + + favoriteViews.onSuccess((s) { + _source = s; + (views, todayViews, thisWeekViews, otherViews) = _getViews(s); + }); + + emit( + state.copyWith( + views: views, + queriedViews: views, + todayViews: todayViews, + thisWeekViews: thisWeekViews, + otherViews: otherViews, + ), + ); + }, + search: (query) async { + if (_source == null) { + return; + } + var (views, todayViews, thisWeekViews, otherViews) = + _getViews(_source!); + var queriedViews = views; + + if (query.isNotEmpty) { + queriedViews = _filter(views, query); + todayViews = _filter(state.todayViews, query); + thisWeekViews = _filter(state.thisWeekViews, query); + otherViews = _filter(state.otherViews, query); + } + + emit( + state.copyWith( + views: views, + queriedViews: queriedViews, + todayViews: todayViews, + thisWeekViews: thisWeekViews, + otherViews: otherViews, + ), + ); + }, + ); + }, + ); + } + + final FavoriteService _service = FavoriteService(); + RepeatedFavoriteViewPB? _source; + + List<ViewPB> _filter(List<ViewPB> views, String query) { + return views + .where( + (view) => view.name.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + } + + // all, today, last week, other + (List<ViewPB>, List<ViewPB>, List<ViewPB>, List<ViewPB>) _getViews( + RepeatedFavoriteViewPB source, + ) { + final List<ViewPB> views = + source.items.map((v) => v.item).where((e) => !e.isPinned).toList(); + final List<ViewPB> todayViews = []; + final List<ViewPB> thisWeekViews = []; + final List<ViewPB> otherViews = []; + for (final favoriteView in source.items) { + final view = favoriteView.item; + if (view.isPinned) { + continue; + } + final date = DateTime.fromMillisecondsSinceEpoch( + favoriteView.timestamp.toInt() * 1000, + ); + final diff = DateTime.now().difference(date).inDays; + if (diff == 0) { + todayViews.add(view); + } else if (diff < 7) { + thisWeekViews.add(view); + } else { + otherViews.add(view); + } + } + return (views, todayViews, thisWeekViews, otherViews); + } +} + +@freezed +class FavoriteMenuEvent with _$FavoriteMenuEvent { + const factory FavoriteMenuEvent.initial() = Initial; + const factory FavoriteMenuEvent.search(String query) = Search; +} + +@freezed +class FavoriteMenuState with _$FavoriteMenuState { + const factory FavoriteMenuState({ + @Default([]) List<ViewPB> views, + @Default([]) List<ViewPB> queriedViews, + @Default([]) List<ViewPB> todayViews, + @Default([]) List<ViewPB> thisWeekViews, + @Default([]) List<ViewPB> otherViews, + }) = _FavoriteMenuState; + + factory FavoriteMenuState.initial() => const FavoriteMenuState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart new file mode 100644 index 0000000000..40c2167626 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart @@ -0,0 +1,67 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FavoriteMoreActions extends StatelessWidget { + const FavoriteMoreActions({super.key, required this.view}); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return FlowyTooltip( + message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), + child: ViewMoreActionButton( + view: view, + spaceType: FolderSpaceType.favorite, + onEditing: (value) => + context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)), + onAction: (action, _) { + switch (action) { + case ViewMoreActionType.favorite: + case ViewMoreActionType.unFavorite: + context.read<FavoriteBloc>().add(FavoriteEvent.toggle(view)); + PopoverContainer.maybeOf(context)?.closeAll(); + break; + case ViewMoreActionType.rename: + NavigatorTextFieldDialog( + title: LocaleKeys.disclosureAction_rename.tr(), + autoSelectAllText: true, + value: view.name, + maxLength: 256, + onConfirm: (newValue, _) { + // can not use bloc here because it has been disposed. + ViewBackendService.updateView( + viewId: view.id, + name: newValue, + ); + }, + ).show(context); + PopoverContainer.maybeOf(context)?.closeAll(); + break; + + case ViewMoreActionType.openInNewTab: + context.read<TabsBloc>().openTab(view); + break; + case ViewMoreActionType.delete: + case ViewMoreActionType.duplicate: + default: + throw UnsupportedError('$action is not supported'); + } + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart new file mode 100644 index 0000000000..53a53e5ae9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart @@ -0,0 +1,43 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FavoritePinAction extends StatelessWidget { + const FavoritePinAction({super.key, required this.view}); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + final tooltip = view.isPinned + ? LocaleKeys.favorite_removeFromSidebar.tr() + : LocaleKeys.favorite_addToSidebar.tr(); + final icon = FlowySvg( + view.isPinned + ? FlowySvgs.favorite_section_pin_s + : FlowySvgs.favorite_section_unpin_s, + ); + return FlowyTooltip( + message: tooltip, + child: FlowyIconButton( + width: 24, + icon: icon, + onPressed: () { + PopoverContainer.maybeOf(context)?.closeAll(); + + view.isPinned + ? context.read<FavoriteBloc>().add(FavoriteEvent.unpin(view)) + : context.read<FavoriteBloc>().add(FavoriteEvent.pin(view)); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart new file mode 100644 index 0000000000..e4a335e34b --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_bloc.dart @@ -0,0 +1,59 @@ +import 'package:appflowy/workspace/application/favorite/favorite_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'favorite_pin_bloc.freezed.dart'; + +class FavoritePinBloc extends Bloc<FavoritePinEvent, FavoritePinState> { + FavoritePinBloc() : super(FavoritePinState.initial()) { + on<FavoritePinEvent>( + (event, emit) async { + await event.when( + initial: () async { + final List<ViewPB> views = await _service + .readFavorites() + .fold((s) => s.items.map((v) => v.item).toList(), (f) => []); + emit(state.copyWith(views: views, queriedViews: views)); + }, + search: (query) async { + if (query.isEmpty) { + emit(state.copyWith(queriedViews: state.views)); + return; + } + + final queriedViews = state.views + .where( + (view) => + view.name.toLowerCase().contains(query.toLowerCase()), + ) + .toList(); + emit(state.copyWith(queriedViews: queriedViews)); + }, + ); + }, + ); + } + + final FavoriteService _service = FavoriteService(); +} + +@freezed +class FavoritePinEvent with _$FavoritePinEvent { + const factory FavoritePinEvent.initial() = Initial; + const factory FavoritePinEvent.search(String query) = Search; +} + +@freezed +class FavoritePinState with _$FavoritePinState { + const factory FavoritePinState({ + @Default([]) List<ViewPB> views, + @Default([]) List<ViewPB> queriedViews, + @Default([]) List<List<ViewPB>> todayViews, + @Default([]) List<List<ViewPB>> lastWeekViews, + @Default([]) List<List<ViewPB>> otherViews, + }) = _FavoritePinState; + + factory FavoritePinState.initial() => const FavoritePinState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart deleted file mode 100644 index 364e12644c..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart +++ /dev/null @@ -1,115 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class FavoriteFolder extends StatelessWidget { - const FavoriteFolder({ - super.key, - required this.views, - }); - - final List<ViewPB> views; - - @override - Widget build(BuildContext context) { - if (views.isEmpty) { - return const SizedBox.shrink(); - } - - return BlocProvider<FolderBloc>( - create: (context) => FolderBloc(type: FolderCategoryType.favorite) - ..add( - const FolderEvent.initial(), - ), - child: BlocBuilder<FolderBloc, FolderState>( - builder: (context, state) { - return Column( - children: [ - FavoriteHeader( - onPressed: () => context - .read<FolderBloc>() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => context - .read<FolderBloc>() - .add(const FolderEvent.expandOrUnExpand(isExpanded: true)), - ), - if (state.isExpanded) - ...views.map( - (view) => ViewItem( - key: ValueKey( - '${FolderCategoryType.favorite.name} ${view.id}', - ), - categoryType: FolderCategoryType.favorite, - isDraggable: false, - isFirstChild: view.id == views.first.id, - isFeedback: false, - view: view, - level: 0, - onSelected: (view, _) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read<TabsBloc>().openTab(view); - } - - context.read<TabsBloc>().openPlugin(view); - }, - onTertiarySelected: (view, _) => - context.read<TabsBloc>().openTab(view), - ), - ), - ], - ); - }, - ), - ); - } -} - -class FavoriteHeader extends StatefulWidget { - const FavoriteHeader({ - super.key, - required this.onPressed, - required this.onAdded, - }); - - final VoidCallback onPressed; - final VoidCallback onAdded; - - @override - State<FavoriteHeader> createState() => _FavoriteHeaderState(); -} - -class _FavoriteHeaderState extends State<FavoriteHeader> { - bool onHover = false; - - @override - Widget build(BuildContext context) { - const iconSize = 26.0; - return MouseRegion( - onEnter: (event) => setState(() => onHover = true), - onExit: (event) => setState(() => onHover = false), - child: Row( - children: [ - FlowyTextButton( - LocaleKeys.sideBar_favorites.tr(), - fontColor: AFThemeExtension.of(context).textColor, - fontHoverColor: Theme.of(context).colorScheme.onSurface, - tooltip: LocaleKeys.sideBar_clickToHideFavorites.tr(), - constraints: const BoxConstraints(maxHeight: iconSize), - padding: const EdgeInsets.all(4), - fillColor: Colors.transparent, - onPressed: widget.onPressed, - ), - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart index 422003fdd9..c5042c4de4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart @@ -1,8 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class FolderHeader extends StatefulWidget { const FolderHeader({ @@ -25,42 +24,34 @@ class FolderHeader extends StatefulWidget { } class _FolderHeaderState extends State<FolderHeader> { - bool onHover = false; + final isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - const iconSize = 26.0; - const textPadding = 4.0; return MouseRegion( - onEnter: (event) => setState(() => onHover = true), - onExit: (event) => setState(() => onHover = false), - child: Row( - children: [ - FlowyTextButton( - widget.title, - tooltip: widget.expandButtonTooltip, - constraints: const BoxConstraints( - minHeight: iconSize + textPadding * 2, - ), - fontColor: AFThemeExtension.of(context).textColor, - fontHoverColor: Theme.of(context).colorScheme.onSurface, - padding: const EdgeInsets.all(textPadding), - fillColor: Colors.transparent, - onPressed: widget.onPressed, + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: FlowyButton( + onTap: widget.onPressed, + margin: const EdgeInsets.symmetric(horizontal: 6.0), + rightIcon: ValueListenableBuilder( + valueListenable: isHovered, + builder: (context, onHover, child) => + Opacity(opacity: onHover ? 1 : 0, child: child), + child: FlowyIconButton( + tooltipText: widget.addButtonTooltip, + icon: const FlowySvg(FlowySvgs.view_item_add_s), + onPressed: widget.onAdded, ), - if (onHover) ...[ - const Spacer(), - FlowyIconButton( - tooltipText: widget.addButtonTooltip, - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - iconPadding: const EdgeInsets.all(2), - height: iconSize, - width: iconSize, - icon: const FlowySvg(FlowySvgs.add_s), - onPressed: widget.onAdded, - ), - ], - ], + ), + iconPadding: 10.0, + text: FlowyText(widget.title), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart index ecd7b5f9de..bd83e06934 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart @@ -3,20 +3,22 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class SectionFolder extends StatelessWidget { +class SectionFolder extends StatefulWidget { const SectionFolder({ super.key, required this.title, - required this.categoryType, + required this.spaceType, required this.views, this.isHoverEnabled = true, required this.expandButtonTooltip, @@ -24,101 +26,140 @@ class SectionFolder extends StatelessWidget { }); final String title; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final List<ViewPB> views; final bool isHoverEnabled; final String expandButtonTooltip; final String addButtonTooltip; + @override + State<SectionFolder> createState() => _SectionFolderState(); +} + +class _SectionFolderState extends State<SectionFolder> { + final ValueNotifier<bool> isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - return BlocProvider<FolderBloc>( - create: (context) => FolderBloc(type: categoryType) - ..add( - const FolderEvent.initial(), + return MouseRegion( + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: BlocProvider<FolderBloc>( + create: (context) => FolderBloc(type: widget.spaceType) + ..add( + const FolderEvent.initial(), + ), + child: BlocBuilder<FolderBloc, FolderState>( + builder: (context, state) { + return Column( + children: [ + _buildHeader(context), + // Pages + const VSpace(4.0), + ..._buildViews(context, state, isHovered), + // Add a placeholder if there are no views + _buildDraggablePlaceholder(context), + ], + ); + }, ), - child: BlocBuilder<FolderBloc, FolderState>( - builder: (context, state) { - return Column( - children: [ - FolderHeader( - title: title, - expandButtonTooltip: expandButtonTooltip, - addButtonTooltip: addButtonTooltip, - onPressed: () => context - .read<FolderBloc>() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () { - createViewAndShowRenameDialogIfNeeded( - context, - LocaleKeys.newPageText.tr(), - (viewName, _) { - if (viewName.isNotEmpty) { - context.read<SidebarSectionsBloc>().add( - SidebarSectionsEvent.createRootViewInSection( - name: viewName, - index: 0, - viewSection: categoryType.toViewSectionPB, - ), - ); - - context.read<FolderBloc>().add( - const FolderEvent.expandOrUnExpand( - isExpanded: true, - ), - ); - } - }, - ); - }, - ), - if (state.isExpanded) - ...views.map( - (view) => ViewItem( - key: ValueKey( - '${categoryType.name} ${view.id}', - ), - categoryType: categoryType, - isFirstChild: view.id == views.first.id, - view: view, - level: 0, - leftPadding: 16, - isFeedback: false, - onSelected: (view, viewContext) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read<TabsBloc>().openTab(view); - } - - context.read<TabsBloc>().openPlugin(view); - }, - onTertiarySelected: (view, viewContext) => - context.read<TabsBloc>().openTab(view), - isHoverEnabled: isHoverEnabled, - ), - ), - if (views.isEmpty) - ViewItem( - categoryType: categoryType, - view: ViewPB( - parentViewId: context - .read<UserWorkspaceBloc>() - .state - .currentWorkspace - ?.workspaceId ?? - '', - ), - level: 0, - leftPadding: 16, - isFeedback: false, - onSelected: (_, __) {}, - onTertiarySelected: (_, __) {}, - isHoverEnabled: isHoverEnabled, - isPlaceholder: true, - ), - ], - ); - }, ), ); } + + Widget _buildHeader(BuildContext context) { + return FolderHeader( + title: widget.title, + expandButtonTooltip: widget.expandButtonTooltip, + addButtonTooltip: widget.addButtonTooltip, + onPressed: () => + context.read<FolderBloc>().add(const FolderEvent.expandOrUnExpand()), + onAdded: () { + createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + context.read<SidebarSectionsBloc>().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + index: 0, + viewSection: widget.spaceType.toViewSectionPB, + ), + ); + + context.read<FolderBloc>().add( + const FolderEvent.expandOrUnExpand( + isExpanded: true, + ), + ); + } + }, + ); + }, + ); + } + + Iterable<Widget> _buildViews( + BuildContext context, + FolderState state, + ValueNotifier<bool> isHovered, + ) { + if (!state.isExpanded) { + return []; + } + + return widget.views.map( + (view) => ViewItem( + key: ValueKey('${widget.spaceType.name} ${view.id}'), + spaceType: widget.spaceType, + isFirstChild: view.id == widget.views.first.id, + view: view, + level: 0, + leftPadding: HomeSpaceViewSizes.leftPadding, + isFeedback: false, + isHovered: isHovered, + onSelected: (viewContext, view) { + if (HardwareKeyboard.instance.isControlPressed) { + context.read<TabsBloc>().openTab(view); + } + + context.read<TabsBloc>().openPlugin(view); + }, + onTertiarySelected: (viewContext, view) => + context.read<TabsBloc>().openTab(view), + isHoverEnabled: widget.isHoverEnabled, + ), + ); + } + + Widget _buildDraggablePlaceholder(BuildContext context) { + if (widget.views.isNotEmpty) { + return const SizedBox.shrink(); + } + return ViewItem( + spaceType: widget.spaceType, + view: ViewPB( + parentViewId: context + .read<UserWorkspaceBloc>() + .state + .currentWorkspace + ?.workspaceId ?? + '', + ), + level: 0, + leftPadding: HomeSpaceViewSizes.leftPadding, + isFeedback: false, + onSelected: (_, __) {}, + onTertiarySelected: (_, __) {}, + isHoverEnabled: widget.isHoverEnabled, + isPlaceholder: true, + ); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart new file mode 100644 index 0000000000..c10d14105a --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart @@ -0,0 +1,70 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; +import 'package:flutter/material.dart'; + +class SidebarFooter extends StatelessWidget { + const SidebarFooter({super.key}); + + @override + Widget build(BuildContext context) { + return const Row( + children: [ + Expanded(child: SidebarTrashButton()), + SizedBox( + height: 16, + child: VerticalDivider(width: 1, color: Color(0x141F2329)), + ), + Expanded(child: SidebarWidgetButton()), + ], + ); + } +} + +class SidebarTrashButton extends StatelessWidget { + const SidebarTrashButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: getIt<MenuSharedState>().notifier, + builder: (context, value, child) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + getIt<MenuSharedState>().latestOpenView = null; + getIt<TabsBloc>().add( + TabsEvent.openPlugin( + plugin: makePlugin(pluginType: PluginType.trash), + ), + ); + }, + child: const FlowySvg(FlowySvgs.sidebar_footer_trash_s), + ), + ); + }, + ); + } +} + +class SidebarWidgetButton extends StatelessWidget { + const SidebarWidgetButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () {}, + child: const FlowySvg(FlowySvgs.sidebar_footer_widget_s), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart similarity index 74% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart index 4ef5480507..051386b7b2 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart @@ -8,6 +8,7 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; @@ -62,22 +63,33 @@ class SidebarTopMenu extends StatelessWidget { children: [ TextSpan( text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Colors.white), ), TextSpan( text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).hintColor), ), ], ); - return FlowyTooltip( - richMessage: textSpan, - child: FlowyIconButton( - width: PlatformExtension.isWindows ? 30 : 28, - hoverColor: Colors.transparent, - onPressed: () => context - .read<HomeSettingBloc>() - .add(const HomeSettingEvent.collapseMenu()), - iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), - icon: const FlowySvg(FlowySvgs.hide_menu_m), + + return Padding( + padding: const EdgeInsets.only(top: 12.0), + child: FlowyTooltip( + richMessage: textSpan, + child: FlowyIconButton( + width: 24, + onPressed: () => context + .read<HomeSettingBloc>() + .add(const HomeSettingEvent.collapseMenu()), + iconPadding: const EdgeInsets.all(2), + icon: const FlowySvg(FlowySvgs.hide_menu_s), + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart similarity index 93% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart index dc089e27a2..8d7b0efd50 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart @@ -1,8 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' @@ -10,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; // keep this widget in case we need to roll back (lucas.xu) @@ -29,11 +28,14 @@ class SidebarUser extends StatelessWidget { child: BlocBuilder<MenuUserBloc, MenuUserState>( builder: (context, state) => Row( children: [ + const HSpace(6), UserAvatar( iconUrl: state.userProfile.iconUrl, name: state.userProfile.name, + size: 16.0, + fontSize: 10.0, ), - const HSpace(8), + const HSpace(10), Expanded(child: _buildUserName(context, state)), UserSettingButton(userProfile: state.userProfile), const HSpace(4), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart similarity index 100% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart similarity index 94% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart index dbbf3f0d0e..93bcccdf06 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; @@ -7,11 +5,12 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarFolder extends StatelessWidget { @@ -38,7 +37,7 @@ class SidebarFolder extends StatelessWidget { return const SizedBox.shrink(); } return Padding( - padding: const EdgeInsets.only(bottom: 10), + padding: const EdgeInsets.only(top: 16.0, bottom: 10), child: FavoriteFolder(views: state.views), ); }, @@ -85,7 +84,7 @@ class PrivateSectionFolder extends SectionFolder { PrivateSectionFolder({super.key, required super.views}) : super( title: LocaleKeys.sideBar_private.tr(), - categoryType: FolderCategoryType.private, + spaceType: FolderSpaceType.private, expandButtonTooltip: LocaleKeys.sideBar_clickToHidePrivate.tr(), addButtonTooltip: LocaleKeys.sideBar_addAPageToPrivate.tr(), ); @@ -95,7 +94,7 @@ class PublicSectionFolder extends SectionFolder { PublicSectionFolder({super.key, required super.views}) : super( title: LocaleKeys.sideBar_workspace.tr(), - categoryType: FolderCategoryType.public, + spaceType: FolderSpaceType.public, expandButtonTooltip: LocaleKeys.sideBar_clickToHideWorkspace.tr(), addButtonTooltip: LocaleKeys.sideBar_addAPageToWorkspace.tr(), ); @@ -105,7 +104,7 @@ class PersonalSectionFolder extends SectionFolder { PersonalSectionFolder({super.key, required super.views}) : super( title: LocaleKeys.sideBar_personal.tr(), - categoryType: FolderCategoryType.public, + spaceType: FolderSpaceType.public, expandButtonTooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(), addButtonTooltip: LocaleKeys.sideBar_addAPage.tr(), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart new file mode 100644 index 0000000000..fa8d7785cf --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart @@ -0,0 +1,62 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SidebarNewPageButton extends StatelessWidget { + const SidebarNewPageButton({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + height: HomeSizes.newPageSectionHeight, + child: FlowyButton( + onTap: () async => _createNewPage(context), + leftIcon: FlowySvg( + FlowySvgs.new_app_s, + color: Theme.of(context).colorScheme.primary, + ), + iconPadding: 10.0, + text: SizedBox( + height: 18.0, + child: FlowyText.regular( + LocaleKeys.newPageText.tr(), + ), + ), + ), + ); + } + + Future<void> _createNewPage(BuildContext context) async { + return createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + // if the workspace is collaborative, create the view in the private section by default. + final section = + context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn + ? ViewSectionPB.Private + : ViewSectionPB.Public; + context.read<SidebarSectionsBloc>().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + viewSection: section, + index: 0, + ), + ); + } + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart similarity index 86% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart index 0e2d020f67..8c6d9c5cf7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_setting.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy_backend/log.dart'; @@ -12,7 +11,9 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; @@ -47,15 +48,14 @@ class UserSettingButton extends StatelessWidget { @override Widget build(BuildContext context) { - return FlowyTooltip( - message: LocaleKeys.settings_menu_open.tr(), - child: IconButton( - onPressed: () => showSettingsDialog(context, userProfile), - icon: SizedBox.square( - dimension: 20, - child: FlowySvg( - FlowySvgs.settings_m, - color: Theme.of(context).colorScheme.tertiary, + return SizedBox.square( + dimension: HomeSizes.workspaceSectionHeight, + child: FlowyTooltip( + message: LocaleKeys.settings_menu_open.tr(), + child: FlowyButton( + onTap: () => showSettingsDialog(context, userProfile), + text: const FlowySvg( + FlowySvgs.settings_s, ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index 9c20ac015b..323ec1428c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,7 +1,5 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/feature_flags.dart'; @@ -17,12 +15,13 @@ import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_top_menu.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/header/sidebar_user.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; @@ -31,6 +30,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// Home Sidebar is the left side bar of the home page. @@ -222,7 +222,8 @@ class _SidebarState extends State<_Sidebar> { // top menu const Padding(padding: menuHorizontalInset, child: SidebarTopMenu()), // user or workspace, setting - Padding( + Container( + height: HomeSizes.workspaceSectionHeight, padding: menuHorizontalInset, child: // if the workspaces are empty, show the user profile instead @@ -231,12 +232,15 @@ class _SidebarState extends State<_Sidebar> { : SidebarUser(userProfile: widget.userProfile), ), if (FeatureFlag.search.isOn) ...[ - const VSpace(8), - const Padding( + const VSpace(6), + Container( padding: menuHorizontalInset, - child: _SidebarSearchButton(), + height: HomeSizes.searchSectionHeight, + child: const _SidebarSearchButton(), ), ], + // new page button + const SidebarNewPageButton(), // scrollable document list Expanded( child: Padding( @@ -256,11 +260,14 @@ class _SidebarState extends State<_Sidebar> { // trash const Padding( padding: menuHorizontalInset, - child: SidebarTrashButton(), + child: Divider(height: 1.0, color: Color(0x141F2329)), + ), + const VSpace(14), + const Padding( + padding: menuHorizontalInset, + child: SidebarFooter(), ), const VSpace(10), - // new page button - const SidebarNewPageButton(), ], ), ); @@ -289,7 +296,8 @@ class _SidebarSearchButton extends StatelessWidget { return FlowyButton( onTap: () => CommandPalette.of(context).toggle(), leftIcon: const FlowySvg(FlowySvgs.search_s), - text: FlowyText(LocaleKeys.search_label.tr()), + iconPadding: 10.0, + text: FlowyText.regular(LocaleKeys.search_label.tr()), ); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart deleted file mode 100644 index eac80118b4..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class SidebarNewPageButton extends StatelessWidget { - const SidebarNewPageButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final child = FlowyTextButton( - LocaleKeys.newPageText.tr(), - fillColor: Colors.transparent, - hoverColor: Colors.transparent, - fontColor: Theme.of(context).colorScheme.tertiary, - onPressed: () async => createViewAndShowRenameDialogIfNeeded( - context, - LocaleKeys.newPageText.tr(), - (viewName, _) { - if (viewName.isNotEmpty) { - // if the workspace is collaborative, create the view in the private section by default. - final section = - context.read<UserWorkspaceBloc>().state.isCollabWorkspaceOn - ? ViewSectionPB.Private - : ViewSectionPB.Public; - context.read<SidebarSectionsBloc>().add( - SidebarSectionsEvent.createRootViewInSection( - name: viewName, - viewSection: section, - ), - ); - } - }, - ), - heading: Container( - width: 16, - height: 16, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.surface, - ), - child: FlowySvg( - FlowySvgs.new_app_s, - color: Theme.of(context).colorScheme.primary, - ), - ), - padding: const EdgeInsets.all(0), - ); - - return SizedBox( - height: 60, - child: TopBorder( - color: Theme.of(context).dividerColor, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 18), - child: child, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart deleted file mode 100644 index b4a6eb344a..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_trash.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/startup/plugin/plugin.dart'; -import 'package:appflowy/startup/startup.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; - -class SidebarTrashButton extends StatelessWidget { - const SidebarTrashButton({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: getIt<MenuSharedState>().notifier, - builder: (context, value, child) { - return FlowyHover( - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).greySelect, - ), - isSelected: () => getIt<MenuSharedState>().latestOpenView == null, - child: SizedBox( - height: 26, - child: InkWell( - onTap: () { - getIt<MenuSharedState>().latestOpenView = null; - getIt<TabsBloc>().add( - TabsEvent.openPlugin( - plugin: makePlugin(pluginType: PluginType.trash), - ), - ); - }, - child: _buildTextButton(context), - ), - ), - ); - }, - ); - } - - Widget _buildTextButton(BuildContext context) { - return Row( - children: [ - const HSpace(6), - const FlowySvg( - FlowySvgs.trash_m, - size: Size(16, 16), - ), - const HSpace(6), - FlowyText.medium( - LocaleKeys.trash_text.tr(), - ), - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart index 39301799d6..53baa2599e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart @@ -16,6 +16,7 @@ enum WorkspaceMoreAction { rename, delete, leave, + divider, } class WorkspaceMoreActionList extends StatelessWidget { @@ -32,6 +33,7 @@ class WorkspaceMoreActionList extends StatelessWidget { final actions = []; if (myRole.isOwner) { actions.add(WorkspaceMoreAction.rename); + actions.add(WorkspaceMoreAction.divider); actions.add(WorkspaceMoreAction.delete); } else if (myRole.canLeave) { actions.add(WorkspaceMoreAction.leave); @@ -40,20 +42,23 @@ class WorkspaceMoreActionList extends StatelessWidget { return const SizedBox.shrink(); } return PopoverActionList<_WorkspaceMoreActionWrapper>( - direction: PopoverDirection.bottomWithCenterAligned, + direction: PopoverDirection.bottomWithLeftAligned, actions: actions .map((e) => _WorkspaceMoreActionWrapper(e, workspace)) .toList(), + constraints: const BoxConstraints(minWidth: 220), buildChild: (controller) { - return FlowyButton( - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - useIntrinsicWidth: true, - text: const FlowySvg( - FlowySvgs.three_dots_vertical_s, + return SizedBox.square( + dimension: 24.0, + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 4.0), + text: const FlowySvg( + FlowySvgs.workspace_three_dots_s, + ), + onTap: () { + controller.show(); + }, ), - onTap: () { - controller.show(); - }, ); }, onSelected: (action, controller) {}, @@ -68,20 +73,37 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { final UserWorkspacePB workspace; @override - Widget buildWithContext(BuildContext context) { + Widget buildWithContext(BuildContext context, PopoverController controller) { + if (inner == WorkspaceMoreAction.divider) { + return const Divider(); + } + + return _buildActionButton(context, controller); + } + + Widget _buildActionButton( + BuildContext context, + PopoverController controller, + ) { return FlowyButton( - text: FlowyText( + leftIcon: buildLeftIcon(context), + iconPadding: 10.0, + text: FlowyText.regular( name, - color: inner == WorkspaceMoreAction.delete + fontSize: 14.0, + color: [WorkspaceMoreAction.delete, WorkspaceMoreAction.leave] + .contains(inner) ? Theme.of(context).colorScheme.error : null, ), - margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + margin: const EdgeInsets.all(6), onTap: () async { PopoverContainer.of(context).closeAll(); final workspaceBloc = context.read<UserWorkspaceBloc>(); switch (inner) { + case WorkspaceMoreAction.divider: + break; case WorkspaceMoreAction.delete: await NavigatorAlertDialog( title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(), @@ -93,7 +115,7 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { ).show(context); case WorkspaceMoreAction.rename: await NavigatorTextFieldDialog( - title: LocaleKeys.workspace_create.tr(), + title: LocaleKeys.workspace_renameWorkspace.tr(), value: workspace.name, hintText: '', autoSelectAllText: true, @@ -132,6 +154,27 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { return LocaleKeys.button_rename.tr(); case WorkspaceMoreAction.leave: return LocaleKeys.workspace_leaveCurrentWorkspace.tr(); + case WorkspaceMoreAction.divider: + return ''; + } + } + + Widget buildLeftIcon(BuildContext context) { + switch (inner) { + case WorkspaceMoreAction.delete: + return FlowySvg( + FlowySvgs.delete_s, + color: Theme.of(context).colorScheme.error, + ); + case WorkspaceMoreAction.rename: + return const FlowySvg(FlowySvgs.view_item_rename_s); + case WorkspaceMoreAction.leave: + return FlowySvg( + FlowySvgs.logout_s, + color: Theme.of(context).colorScheme.error, + ); + case WorkspaceMoreAction.divider: + return const SizedBox.shrink(); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index 101c12c3a6..ea0826e82a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -1,6 +1,5 @@ import 'dart:math'; -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; @@ -14,12 +13,14 @@ class WorkspaceIcon extends StatefulWidget { required this.workspace, required this.enableEdit, required this.iconSize, + required this.fontSize, required this.onSelected, }); final UserWorkspacePB workspace; final double iconSize; final bool enableEdit; + final double fontSize; final void Function(EmojiPickerResult) onSelected; @override @@ -35,10 +36,9 @@ class _WorkspaceIconState extends State<WorkspaceIcon> { ? Container( width: widget.iconSize, alignment: Alignment.center, - child: EmojiText( - emoji: widget.workspace.icon, + child: FlowyText.emoji( + widget.workspace.icon, fontSize: widget.iconSize, - lineHeight: 1, ), ) : Container( @@ -47,13 +47,13 @@ class _WorkspaceIconState extends State<WorkspaceIcon> { height: max(widget.iconSize, 26), decoration: BoxDecoration( color: ColorGenerator(widget.workspace.name).toColor(), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(4), ), child: FlowyText( widget.workspace.name.isEmpty ? '' : widget.workspace.name.substring(0, 1), - fontSize: 16, + fontSize: widget.fontSize, color: Colors.black, ), ); @@ -63,7 +63,7 @@ class _WorkspaceIconState extends State<WorkspaceIcon> { offset: const Offset(0, 8), controller: controller, direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(360, 380)), + constraints: BoxConstraints.loose(const Size(364, 356)), clickHandler: PopoverClickHandler.gestureDetector, popupBuilder: (_) => FlowyIconPicker( onSelected: (result) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index efc4372ee8..0572ae107c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; @@ -13,6 +13,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @visibleForTesting @@ -38,7 +39,7 @@ class WorkspacesMenu extends StatelessWidget { children: [ // user email Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + padding: const EdgeInsets.symmetric(horizontal: 4.0), child: Row( children: [ Expanded( @@ -50,18 +51,16 @@ class WorkspacesMenu extends StatelessWidget { ), ), const HSpace(4.0), - FlowyButton( - key: createWorkspaceButtonKey, - useIntrinsicWidth: true, - text: const FlowySvg(FlowySvgs.add_m), - onTap: () { - _showCreateWorkspaceDialog(context); - PopoverContainer.of(context).closeAll(); - }, - ), + const _WorkspaceMoreButton(), + const HSpace(8.0), ], ), ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider(height: 1.0), + ), + // workspace list for (final workspace in workspaces) ...[ WorkspaceMenuItem( key: ValueKey(workspace.workspaceId), @@ -69,8 +68,11 @@ class WorkspacesMenu extends StatelessWidget { userProfile: userProfile, isSelected: workspace.workspaceId == currentWorkspace.workspaceId, ), - const VSpace(4.0), + const VSpace(6.0), ], + // add new workspace + const _CreateWorkspaceButton(), + const VSpace(6.0), ], ); } @@ -86,20 +88,9 @@ class WorkspacesMenu extends StatelessWidget { return LocaleKeys.defaultUsername.tr(); } - - Future<void> _showCreateWorkspaceDialog(BuildContext context) async { - if (context.mounted) { - final workspaceBloc = context.read<UserWorkspaceBloc>(); - await CreateWorkspaceDialog( - onConfirm: (name) { - workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name)); - }, - ).show(context); - } - } } -class WorkspaceMenuItem extends StatelessWidget { +class WorkspaceMenuItem extends StatefulWidget { const WorkspaceMenuItem({ super.key, required this.workspace, @@ -111,32 +102,50 @@ class WorkspaceMenuItem extends StatelessWidget { final UserWorkspacePB workspace; final bool isSelected; + @override + State<WorkspaceMenuItem> createState() => _WorkspaceMenuItemState(); +} + +class _WorkspaceMenuItemState extends State<WorkspaceMenuItem> { + final ValueNotifier<bool> isHovered = ValueNotifier(false); + + @override + void dispose() { + isHovered.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => - WorkspaceMemberBloc(userProfile: userProfile, workspace: workspace) - ..add(const WorkspaceMemberEvent.initial()), + create: (_) => WorkspaceMemberBloc( + userProfile: widget.userProfile, + workspace: widget.workspace, + )..add(const WorkspaceMemberEvent.initial()), child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>( builder: (context, state) { // settings right icon inside the flowy button will // cause the popover dismiss intermediately when click the right icon. // so using the stack to put the right icon on the flowy button. return SizedBox( - height: 52, - child: Stack( - alignment: Alignment.center, - children: [ - _WorkspaceInfo( - isSelected: isSelected, - workspace: workspace, - ), - Positioned(left: 8, child: _buildLeftIcon(context)), - Positioned( - right: 12.0, - child: Align(child: _buildRightIcon(context)), - ), - ], + height: 40, + child: MouseRegion( + onEnter: (_) => isHovered.value = true, + onExit: (_) => isHovered.value = false, + child: Stack( + alignment: Alignment.center, + children: [ + _WorkspaceInfo( + isSelected: widget.isSelected, + workspace: widget.workspace, + ), + Positioned(left: 4, child: _buildLeftIcon(context)), + Positioned( + right: 4.0, + child: Align(child: _buildRightIcon(context, isHovered)), + ), + ], + ), ), ); }, @@ -145,17 +154,26 @@ class WorkspaceMenuItem extends StatelessWidget { } Widget _buildLeftIcon(BuildContext context) { - return SizedBox.square( - dimension: 32, + return Container( + width: 32.0, + height: 32.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0x01717171).withOpacity(0.12), + width: 0.8, + ), + ), child: FlowyTooltip( message: LocaleKeys.document_plugins_cover_changeIcon.tr(), child: WorkspaceIcon( - workspace: workspace, - iconSize: 26, + workspace: widget.workspace, + iconSize: 22, + fontSize: 16, enableEdit: true, onSelected: (result) => context.read<UserWorkspaceBloc>().add( UserWorkspaceEvent.updateWorkspaceIcon( - workspace.workspaceId, + widget.workspace.workspaceId, result.emoji, ), ), @@ -164,19 +182,39 @@ class WorkspaceMenuItem extends StatelessWidget { ); } - Widget _buildRightIcon(BuildContext context) { + Widget _buildRightIcon(BuildContext context, ValueNotifier<bool> isHovered) { // only the owner can update or delete workspace. - // only show the more action button when the workspace is selected. - if (!isSelected || context.read<WorkspaceMemberBloc>().state.isLoading) { + if (context.read<WorkspaceMemberBloc>().state.isLoading) { return const SizedBox.shrink(); } return Row( children: [ - WorkspaceMoreActionList(workspace: workspace), - const FlowySvg( - FlowySvgs.blue_check_s, + ValueListenableBuilder( + valueListenable: isHovered, + builder: (context, value, child) { + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Opacity( + opacity: value ? 1.0 : 0.0, + child: child, + ), + ); + }, + child: WorkspaceMoreActionList(workspace: widget.workspace), ), + const HSpace(8.0), + if (widget.isSelected) ...[ + const Padding( + padding: EdgeInsets.all(5.0), + child: FlowySvg( + FlowySvgs.workspace_selected_s, + blendMode: null, + size: Size.square(14.0), + ), + ), + const HSpace(8.0), + ], ], ); } @@ -199,10 +237,9 @@ class _WorkspaceInfo extends StatelessWidget { return FlowyButton( onTap: () => _openWorkspace(context), iconPadding: 10.0, - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), leftIconSize: const Size.square(32), leftIcon: const SizedBox.square(dimension: 32), - rightIcon: const HSpace(42.0), + rightIcon: const HSpace(32.0), text: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -213,8 +250,9 @@ class _WorkspaceInfo extends StatelessWidget { overflow: TextOverflow.ellipsis, withTooltip: true, ), + const VSpace(2.0), // workspace members count - FlowyText( + FlowyText.regular( state.isLoading ? '' : LocaleKeys.settings_appearance_members_membersCount @@ -263,3 +301,90 @@ class CreateWorkspaceDialog extends StatelessWidget { ); } } + +class _CreateWorkspaceButton extends StatelessWidget { + const _CreateWorkspaceButton(); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 40, + child: FlowyButton( + key: createWorkspaceButtonKey, + onTap: () { + _showCreateWorkspaceDialog(context); + PopoverContainer.of(context).closeAll(); + }, + margin: const EdgeInsets.symmetric(horizontal: 4.0), + text: Row( + children: [ + _buildLeftIcon(context), + const HSpace(10.0), + FlowyText.regular(LocaleKeys.workspace_create.tr()), + ], + ), + ), + ); + } + + Widget _buildLeftIcon(BuildContext context) { + return Container( + width: 32.0, + height: 32.0, + padding: const EdgeInsets.all(7.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: const Color(0x01717171).withOpacity(0.12), + width: 0.8, + ), + ), + child: const FlowySvg(FlowySvgs.add_workspace_s), + ); + } + + Future<void> _showCreateWorkspaceDialog(BuildContext context) async { + if (context.mounted) { + final workspaceBloc = context.read<UserWorkspaceBloc>(); + await CreateWorkspaceDialog( + onConfirm: (name) { + workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name)); + }, + ).show(context); + } + } +} + +class _WorkspaceMoreButton extends StatelessWidget { + const _WorkspaceMoreButton(); + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 6), + popupBuilder: (_) => FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 6.0, vertical: 7.0), + leftIcon: const FlowySvg(FlowySvgs.workspace_logout_s), + iconPadding: 10.0, + text: FlowyText.regular(LocaleKeys.button_logout.tr()), + onTap: () async { + await getIt<AuthService>().signOut(); + await runAppFlowy(); + }, + ), + child: SizedBox.square( + dimension: 24.0, + child: FlowyButton( + useIntrinsicWidth: true, + margin: EdgeInsets.zero, + text: const FlowySvg( + FlowySvgs.workspace_three_dots_s, + size: Size.square(16.0), + ), + onTap: () {}, + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart similarity index 79% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart index 141d7d7f4c..bb4d18ab46 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart @@ -1,10 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; @@ -16,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarWorkspace extends StatefulWidget { @@ -50,8 +49,9 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> { ), ), UserSettingButton(userProfile: widget.userProfile), - const HSpace(4), + const HSpace(8), const NotificationButton(), + const HSpace(4), ], ); }, @@ -144,7 +144,7 @@ class _SidebarWorkspaceState extends State<SidebarWorkspace> { } } -class SidebarSwitchWorkspaceButton extends StatelessWidget { +class SidebarSwitchWorkspaceButton extends StatefulWidget { const SidebarSwitchWorkspaceButton({ super.key, required this.userProfile, @@ -154,16 +154,31 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget { final UserWorkspacePB currentWorkspace; final UserProfilePB userProfile; + @override + State<SidebarSwitchWorkspaceButton> createState() => + _SidebarSwitchWorkspaceButtonState(); +} + +class _SidebarSwitchWorkspaceButtonState + extends State<SidebarSwitchWorkspaceButton> { + final ValueNotifier<bool> _isWorkSpaceMenuExpanded = ValueNotifier(false); + @override Widget build(BuildContext context) { return AppFlowyPopover( - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 10), - constraints: const BoxConstraints(maxWidth: 260, maxHeight: 600), - onOpen: () => context - .read<UserWorkspaceBloc>() - .add(const UserWorkspaceEvent.fetchWorkspaces()), - onClose: () => Log.info('close workspace menu'), + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 5), + constraints: const BoxConstraints(maxWidth: 300, maxHeight: 600), + onOpen: () { + _isWorkSpaceMenuExpanded.value = true; + context + .read<UserWorkspaceBloc>() + .add(const UserWorkspaceEvent.fetchWorkspaces()); + }, + onClose: () { + _isWorkSpaceMenuExpanded.value = false; + Log.info('close workspace menu'); + }, popupBuilder: (_) { return BlocProvider<UserWorkspaceBloc>.value( value: context.read<UserWorkspaceBloc>(), @@ -176,7 +191,7 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget { } Log.info('open workspace menu'); return WorkspacesMenu( - userProfile: userProfile, + userProfile: widget.userProfile, currentWorkspace: currentWorkspace, workspaces: workspaces, ); @@ -185,33 +200,42 @@ class SidebarSwitchWorkspaceButton extends StatelessWidget { ); }, child: FlowyButton( - margin: const EdgeInsets.symmetric(vertical: 8), + margin: EdgeInsets.zero, text: Row( children: [ - const HSpace(2.0), - SizedBox.square( - dimension: 30.0, + const HSpace(6.0), + SizedBox( + width: 16.0, child: WorkspaceIcon( - workspace: currentWorkspace, - iconSize: 20, + workspace: widget.currentWorkspace, + iconSize: 16, + fontSize: 10, enableEdit: false, onSelected: (result) => context.read<UserWorkspaceBloc>().add( UserWorkspaceEvent.updateWorkspaceIcon( - currentWorkspace.workspaceId, + widget.currentWorkspace.workspaceId, result.emoji, ), ), ), ), - const HSpace(6), - Expanded( + const HSpace(10), + Flexible( child: FlowyText.medium( - currentWorkspace.name, + widget.currentWorkspace.name, overflow: TextOverflow.ellipsis, withTooltip: true, ), ), - const FlowySvg(FlowySvgs.drop_menu_show_m), + const HSpace(4), + ValueListenableBuilder( + valueListenable: _isWorkSpaceMenuExpanded, + builder: (context, value, _) => FlowySvg( + value + ? FlowySvgs.workspace_drop_down_menu_hide_s + : FlowySvgs.workspace_drop_down_menu_show_s, + ), + ), ], ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart index 658d60bfe7..881d926df3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart @@ -15,6 +15,8 @@ enum DraggableHoverPosition { bottom, } +const kDraggableViewItemDividerHeight = 2.0; + class DraggableViewItem extends StatefulWidget { const DraggableViewItem({ super.key, @@ -45,8 +47,7 @@ class DraggableViewItem extends StatefulWidget { class _DraggableViewItemState extends State<DraggableViewItem> { DraggableHoverPosition position = DraggableHoverPosition.none; - - final _dividerHeight = 2.0; + final hoverColor = const Color(0xFF00C8FF); @override Widget build(BuildContext context) { @@ -100,29 +101,26 @@ class _DraggableViewItemState extends State<DraggableViewItem> { // only show the top border when the draggable item is the first child if (widget.isFirstChild) Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.top - ? widget.topHighlightColor ?? - Theme.of(context).colorScheme.secondary + ? widget.topHighlightColor ?? hoverColor : Colors.transparent, ), DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.0), color: position == DraggableHoverPosition.center - ? widget.centerHighlightColor ?? - Theme.of(context).colorScheme.secondary.withOpacity(0.5) + ? widget.centerHighlightColor ?? hoverColor.withOpacity(0.5) : Colors.transparent, ), child: widget.child, ), Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.bottom - ? widget.bottomHighlightColor ?? - Theme.of(context).colorScheme.secondary + ? widget.bottomHighlightColor ?? hoverColor : Colors.transparent, ), ], @@ -137,10 +135,10 @@ class _DraggableViewItemState extends State<DraggableViewItem> { top: 0, left: 0, right: 0, - height: _dividerHeight, + height: kDraggableViewItemDividerHeight, child: Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.top ? widget.topHighlightColor ?? Theme.of(context).colorScheme.secondary @@ -161,10 +159,10 @@ class _DraggableViewItemState extends State<DraggableViewItem> { bottom: 0, left: 0, right: 0, - height: _dividerHeight, + height: kDraggableViewItemDividerHeight, child: Divider( - height: _dividerHeight, - thickness: _dividerHeight, + height: kDraggableViewItemDividerHeight, + thickness: kDraggableViewItemDividerHeight, color: position == DraggableHoverPosition.bottom ? widget.bottomHighlightColor ?? Theme.of(context).colorScheme.secondary diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart index 9fda07d7d2..5b04e50501 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_action_type.dart @@ -10,8 +10,13 @@ enum ViewMoreActionType { duplicate, copyLink, // not supported yet. rename, - moveTo, // not supported yet. + moveTo, openInNewTab, + changeIcon, + collapseAllPages, // including sub pages + divider, + lastModified, + created, } extension ViewMoreActionTypeExtension on ViewMoreActionType { @@ -33,27 +38,63 @@ extension ViewMoreActionTypeExtension on ViewMoreActionType { return LocaleKeys.disclosureAction_moveTo.tr(); case ViewMoreActionType.openInNewTab: return LocaleKeys.disclosureAction_openNewTab.tr(); + case ViewMoreActionType.changeIcon: + return LocaleKeys.disclosureAction_changeIcon.tr(); + case ViewMoreActionType.collapseAllPages: + return LocaleKeys.disclosureAction_collapseAllPages.tr(); + case ViewMoreActionType.divider: + case ViewMoreActionType.lastModified: + case ViewMoreActionType.created: + return ''; } } - Widget icon(Color iconColor) { + Widget get leftIcon { switch (this) { case ViewMoreActionType.delete: - return const FlowySvg(FlowySvgs.delete_s); + return const FlowySvg(FlowySvgs.trash_s, blendMode: null); case ViewMoreActionType.favorite: - return const FlowySvg(FlowySvgs.unfavorite_s); - case ViewMoreActionType.unFavorite: return const FlowySvg(FlowySvgs.favorite_s); + case ViewMoreActionType.unFavorite: + return const FlowySvg(FlowySvgs.unfavorite_s); case ViewMoreActionType.duplicate: - return const FlowySvg(FlowySvgs.copy_s); + return const FlowySvg(FlowySvgs.duplicate_s); case ViewMoreActionType.copyLink: return const Icon(Icons.copy); case ViewMoreActionType.rename: - return const FlowySvg(FlowySvgs.edit_s); + return const FlowySvg(FlowySvgs.view_item_rename_s); case ViewMoreActionType.moveTo: - return const Icon(Icons.move_to_inbox); + return const FlowySvg(FlowySvgs.move_to_s); case ViewMoreActionType.openInNewTab: - return const FlowySvg(FlowySvgs.full_view_s); + return const FlowySvg(FlowySvgs.view_item_open_in_new_tab_s); + case ViewMoreActionType.changeIcon: + return const FlowySvg(FlowySvgs.change_icon_s); + case ViewMoreActionType.collapseAllPages: + return const FlowySvg(FlowySvgs.collapse_all_page_s); + case ViewMoreActionType.divider: + case ViewMoreActionType.lastModified: + case ViewMoreActionType.created: + return const SizedBox.shrink(); + } + } + + Widget get rightIcon { + switch (this) { + case ViewMoreActionType.changeIcon: + case ViewMoreActionType.moveTo: + return const FlowySvg(FlowySvgs.view_item_right_arrow_s); + case ViewMoreActionType.favorite: + case ViewMoreActionType.unFavorite: + case ViewMoreActionType.duplicate: + case ViewMoreActionType.copyLink: + case ViewMoreActionType.rename: + case ViewMoreActionType.openInNewTab: + case ViewMoreActionType.collapseAllPages: + case ViewMoreActionType.divider: + case ViewMoreActionType.delete: + case ViewMoreActionType.lastModified: + case ViewMoreActionType.created: + return const SizedBox.shrink(); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart index e020751e0b..1cfff3c6e6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_add_button.dart @@ -1,15 +1,14 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/document.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_panel.dart'; - import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:easy_localization/easy_localization.dart'; class ViewAddButton extends StatelessWidget { const ViewAddButton({ @@ -51,13 +50,12 @@ class ViewAddButton extends StatelessWidget { return PopoverActionList<PopoverAction>( direction: PopoverDirection.bottomWithLeftAligned, actions: _actions, - offset: const Offset(0, 8), + offset: const Offset(0, 4), buildChild: (popover) { return FlowyIconButton( hoverColor: Colors.transparent, - iconPadding: const EdgeInsets.all(2), - width: 26, - icon: const FlowySvg(FlowySvgs.add_s), + width: 24, + icon: const FlowySvg(FlowySvgs.view_item_add_s), onPressed: () { onEditing(true); popover.show(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 057b3d8f99..0ebb919318 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -1,8 +1,5 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; @@ -11,8 +8,9 @@ import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_b import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; +import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/rename_view_dialog.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.dart'; @@ -26,16 +24,25 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -typedef ViewItemOnSelected = void Function(ViewPB, BuildContext); +typedef ViewItemOnSelected = void Function(BuildContext context, ViewPB view); +typedef ViewItemLeftIconBuilder = Widget Function( + BuildContext context, + ViewPB view, +); +typedef ViewItemRightIconsBuilder = List<Widget> Function( + BuildContext context, + ViewPB view, +); class ViewItem extends StatelessWidget { const ViewItem({ super.key, required this.view, this.parentView, - required this.categoryType, + required this.spaceType, required this.level, this.leftPadding = 10, required this.onSelected, @@ -43,15 +50,19 @@ class ViewItem extends StatelessWidget { this.isFirstChild = false, this.isDraggable = true, required this.isFeedback, - this.height = 28.0, + this.height = HomeSpaceViewSizes.viewHeight, this.isHoverEnabled = true, this.isPlaceholder = false, + this.isHovered, + this.shouldRenderChildren = true, + this.leftIconBuilder, + this.rightIconsBuilder, }); final ViewPB view; final ViewPB? parentView; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; // indicate the level of the view item // used to calculate the left padding @@ -85,6 +96,17 @@ class ViewItem extends StatelessWidget { // placeholder widget to receive the drop event when moving view across sections. final bool isPlaceholder; + // used for control the expand/collapse icon + final ValueNotifier<bool>? isHovered; + + // render the child views of the view + final bool shouldRenderChildren; + + // custom the left icon widget, if it's null, the default expand/collapse icon will be used + final ViewItemLeftIconBuilder? leftIconBuilder; + // custom the right icon widget, if it's null, the default ... and + button will be used + final ViewItemRightIconsBuilder? rightIconsBuilder; + @override Widget build(BuildContext context) { return BlocProvider( @@ -100,7 +122,7 @@ class ViewItem extends StatelessWidget { view: state.view, parentView: parentView, childViews: state.view.childViews, - categoryType: categoryType, + spaceType: spaceType, level: level, leftPadding: leftPadding, showActions: state.isEditing, @@ -113,6 +135,10 @@ class ViewItem extends StatelessWidget { height: height, isHoverEnabled: isHoverEnabled, isPlaceholder: isPlaceholder, + isHovered: isHovered, + shouldRenderChildren: shouldRenderChildren, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, ); }, ), @@ -128,7 +154,7 @@ class InnerViewItem extends StatelessWidget { required this.view, required this.parentView, required this.childViews, - required this.categoryType, + required this.spaceType, this.isDraggable = true, this.isExpanded = true, required this.level, @@ -141,12 +167,16 @@ class InnerViewItem extends StatelessWidget { required this.height, this.isHoverEnabled = true, this.isPlaceholder = false, + this.isHovered, + this.shouldRenderChildren = true, + required this.leftIconBuilder, + required this.rightIconsBuilder, }); final ViewPB view; final ViewPB? parentView; final List<ViewPB> childViews; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final bool isDraggable; final bool isExpanded; @@ -164,6 +194,10 @@ class InnerViewItem extends StatelessWidget { final bool isHoverEnabled; final bool isPlaceholder; + final ValueNotifier<bool>? isHovered; + final bool shouldRenderChildren; + final ViewItemLeftIconBuilder? leftIconBuilder; + final ViewItemRightIconsBuilder? rightIconsBuilder; @override Widget build(BuildContext context) { @@ -172,7 +206,7 @@ class InnerViewItem extends StatelessWidget { parentView: parentView, level: level, showActions: showActions, - categoryType: categoryType, + spaceType: spaceType, onSelected: onSelected, onTertiarySelected: onTertiarySelected, isExpanded: isExpanded, @@ -181,56 +215,40 @@ class InnerViewItem extends StatelessWidget { isFeedback: isFeedback, height: height, isPlaceholder: isPlaceholder, + isHovered: isHovered, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, ); // if the view is expanded and has child views, render its child views - if (isExpanded) { - if (childViews.isNotEmpty) { - final children = childViews.map((childView) { - return ViewItem( - key: ValueKey('${categoryType.name} ${childView.id}'), - parentView: view, - categoryType: categoryType, - isFirstChild: childView.id == childViews.first.id, - view: childView, - level: level + 1, - onSelected: onSelected, - onTertiarySelected: onTertiarySelected, - isDraggable: isDraggable, - leftPadding: leftPadding, - isFeedback: isFeedback, - isPlaceholder: isPlaceholder, - ); - }).toList(); + if (isExpanded && shouldRenderChildren && childViews.isNotEmpty) { + final children = childViews.map((childView) { + return ViewItem( + key: ValueKey('${spaceType.name} ${childView.id}'), + parentView: view, + spaceType: spaceType, + isFirstChild: childView.id == childViews.first.id, + view: childView, + level: level + 1, + onSelected: onSelected, + onTertiarySelected: onTertiarySelected, + isDraggable: isDraggable, + leftPadding: leftPadding, + isFeedback: isFeedback, + isPlaceholder: isPlaceholder, + isHovered: isHovered, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, + ); + }).toList(); - child = Column( - mainAxisSize: MainAxisSize.min, - children: [ - child, - ...children, - ], - ); - } else { - child = Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - Container( - height: height, - alignment: Alignment.centerLeft, - child: Padding( - // add 2px to make the text align with the view item - padding: EdgeInsets.only(left: (level + 1) * leftPadding + 2), - child: FlowyText.medium( - LocaleKeys.noPagesInside.tr(), - color: Theme.of(context).hintColor, - ), - ), - ), - ], - ); - } + child = Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + ...children, + ], + ); } // wrap the child with DraggableItem if isDraggable is true @@ -246,16 +264,27 @@ class InnerViewItem extends StatelessWidget { ? (from, to) => _moveViewCrossSection(context, from, to) : null, feedback: (context) { - return ViewItem( - view: view, - parentView: parentView, - categoryType: categoryType, - level: level, - onSelected: onSelected, - onTertiarySelected: onTertiarySelected, - isDraggable: false, - leftPadding: leftPadding, - isFeedback: true, + return Container( + width: 250, + decoration: BoxDecoration( + color: Brightness.light == Theme.of(context).brightness + ? Colors.white + : Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: ViewItem( + view: view, + parentView: parentView, + spaceType: spaceType, + level: level, + onSelected: onSelected, + onTertiarySelected: onTertiarySelected, + isDraggable: false, + leftPadding: leftPadding, + isFeedback: true, + leftIconBuilder: leftIconBuilder, + rightIconsBuilder: rightIconsBuilder, + ), ); }, child: child, @@ -263,7 +292,7 @@ class InnerViewItem extends StatelessWidget { } else { // keep the same height of the DraggableItem child = Padding( - padding: const EdgeInsets.only(top: 2.0), + padding: const EdgeInsets.only(top: kDraggableViewItemDividerHeight), child: child, ); } @@ -279,10 +308,10 @@ class InnerViewItem extends StatelessWidget { if (isReferencedDatabaseView(view, parentView)) { return; } - final fromSection = categoryType == FolderCategoryType.public + final fromSection = spaceType == FolderSpaceType.public ? ViewSectionPB.Private : ViewSectionPB.Public; - final toSection = categoryType == FolderCategoryType.public + final toSection = spaceType == FolderSpaceType.public ? ViewSectionPB.Public : ViewSectionPB.Private; context.read<ViewBloc>().add( @@ -297,7 +326,7 @@ class InnerViewItem extends StatelessWidget { context.read<ViewBloc>().add( ViewEvent.updateViewVisibility( from, - categoryType == FolderCategoryType.public, + spaceType == FolderSpaceType.public, ), ); } @@ -312,7 +341,7 @@ class SingleInnerViewItem extends StatefulWidget { required this.level, required this.leftPadding, this.isDraggable = true, - required this.categoryType, + required this.spaceType, required this.showActions, required this.onSelected, this.onTertiarySelected, @@ -320,6 +349,9 @@ class SingleInnerViewItem extends StatefulWidget { required this.height, this.isHoverEnabled = true, this.isPlaceholder = false, + this.isHovered, + required this.leftIconBuilder, + required this.rightIconsBuilder, }); final ViewPB view; @@ -335,11 +367,14 @@ class SingleInnerViewItem extends StatefulWidget { final bool showActions; final ViewItemOnSelected onSelected; final ViewItemOnSelected? onTertiarySelected; - final FolderCategoryType categoryType; + final FolderSpaceType spaceType; final double height; final bool isHoverEnabled; final bool isPlaceholder; + final ValueNotifier<bool>? isHovered; + final ViewItemLeftIconBuilder? leftIconBuilder; + final ViewItemRightIconsBuilder? rightIconsBuilder; @override State<SingleInnerViewItem> createState() => _SingleInnerViewItemState(); @@ -382,11 +417,11 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> { Widget _buildViewItem(bool onHover, [bool isSelected = false]) { final children = [ - // expand icon - _buildLeftIcon(), + // expand icon or placeholder + widget.leftIconBuilder?.call(context, widget.view) ?? _buildLeftIcon(), // icon _buildViewIconButton(), - const HSpace(5), + const HSpace(6), // title Expanded( child: FlowyText.regular( @@ -398,25 +433,32 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> { // hover action if (widget.showActions || onHover) { - // ··· more action button - children.add(_buildViewMoreActionButton(context)); - // only support add button for document layout - if (widget.view.layout == ViewLayoutPB.Document) { - // + button - children.add(_buildViewAddButton(context)); + if (widget.rightIconsBuilder != null) { + children.addAll(widget.rightIconsBuilder!(context, widget.view)); + } else { + // ··· more action button + children.add(_buildViewMoreActionButton(context)); + children.add(const HSpace(8.0)); + // only support add button for document layout + if (widget.view.layout == ViewLayoutPB.Document) { + // + button + children.add(_buildViewAddButton(context)); + } + children.add(const HSpace(4.0)); } } final child = GestureDetector( behavior: HitTestBehavior.translucent, - onTap: () => widget.onSelected(widget.view, context), + onTap: () => widget.onSelected(context, widget.view), onTertiaryTapDown: (_) => - widget.onTertiarySelected?.call(widget.view, context), + widget.onTertiarySelected?.call(context, widget.view), child: SizedBox( height: widget.height, child: Padding( padding: EdgeInsets.only(left: widget.level * widget.leftPadding), child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: children, ), ), @@ -446,26 +488,24 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> { Widget _buildViewIconButton() { final icon = widget.view.icon.value.isNotEmpty - ? EmojiText( - emoji: widget.view.icon.value, - fontSize: 18.0, + ? FlowyText.emoji( + widget.view.icon.value, + fontSize: 16.0, ) - : SizedBox.square( - dimension: 20.0, - child: widget.view.defaultIcon(), - ); + : widget.view.defaultIcon(); + return AppFlowyPopover( offset: const Offset(20, 0), controller: controller, direction: PopoverDirection.rightWithCenterAligned, - constraints: BoxConstraints.loose(const Size(360, 380)), + constraints: BoxConstraints.loose(const Size(364, 356)), onClose: () => setState(() => isIconPickerOpened = false), child: GestureDetector( // prevent the tap event from being passed to the parent widget onTap: () {}, child: FlowyTooltip( message: LocaleKeys.document_plugins_cover_changeIcon.tr(), - child: icon, + child: SizedBox(width: 16.0, child: icon), ), ), popupBuilder: (context) { @@ -492,18 +532,33 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> { return const _DotIconWidget(); } - final svg = widget.isExpanded - ? FlowySvgs.drop_menu_show_m - : FlowySvgs.drop_menu_hide_m; - return GestureDetector( + if (context.read<ViewBloc>().state.view.childViews.isEmpty) { + return HSpace(widget.leftPadding); + } + + final child = GestureDetector( child: FlowySvg( - svg, + widget.isExpanded + ? FlowySvgs.view_item_expand_s + : FlowySvgs.view_item_unexpand_s, size: const Size.square(16.0), ), onTap: () => context .read<ViewBloc>() .add(ViewEvent.setIsExpanded(!widget.isExpanded)), ); + + if (widget.isHovered != null) { + return ValueListenableBuilder<bool>( + valueListenable: widget.isHovered!, + builder: (_, isHovered, child) { + return Opacity(opacity: isHovered ? 1.0 : 0.0, child: child); + }, + child: child, + ); + } + + return child; } // + button @@ -533,7 +588,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> { viewName, pluginBuilder.layoutType!, openAfterCreated: openAfterCreated, - section: widget.categoryType.toViewSectionPB, + section: widget.spaceType.toViewSectionPB, ), ); } @@ -554,9 +609,10 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> { message: LocaleKeys.menuAppHeader_moreButtonToolTip.tr(), child: ViewMoreActionButton( view: widget.view, + spaceType: widget.spaceType, onEditing: (value) => context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)), - onAction: (action) { + onAction: (action, data) { switch (action) { case ViewMoreActionType.favorite: case ViewMoreActionType.unFavorite: @@ -584,6 +640,20 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> { case ViewMoreActionType.openInNewTab: context.read<TabsBloc>().openTab(widget.view); break; + case ViewMoreActionType.collapseAllPages: + context.read<ViewBloc>().add(const ViewEvent.collapseAllPages()); + break; + case ViewMoreActionType.changeIcon: + if (data is! EmojiPickerResult) { + return; + } + final result = data; + ViewBackendService.updateViewIcon( + viewId: widget.view.id, + viewIcon: result.emoji, + iconType: result.type.toProto(), + ); + break; default: throw UnsupportedError('$action is not supported'); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 0cefde700e..68d179da50 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -1,11 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; - import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; /// ··· button beside the view name class ViewMoreActionButton extends StatelessWidget { @@ -14,59 +15,179 @@ class ViewMoreActionButton extends StatelessWidget { required this.view, required this.onEditing, required this.onAction, + required this.spaceType, }); final ViewPB view; final void Function(bool value) onEditing; - final void Function(ViewMoreActionType) onAction; + final void Function(ViewMoreActionType type, dynamic data) onAction; + final FolderSpaceType spaceType; @override Widget build(BuildContext context) { - final supportedActionTypes = [ - ViewMoreActionType.rename, - ViewMoreActionType.delete, - ViewMoreActionType.duplicate, - ViewMoreActionType.openInNewTab, - view.isFavorite - ? ViewMoreActionType.unFavorite - : ViewMoreActionType.favorite, - ]; + final wrappers = _buildActionTypeWrappers(); return PopoverActionList<ViewMoreActionTypeWrapper>( - direction: PopoverDirection.bottomWithCenterAligned, + direction: PopoverDirection.bottomWithLeftAligned, offset: const Offset(0, 8), - actions: supportedActionTypes - .map((e) => ViewMoreActionTypeWrapper(e)) - .toList(), + actions: wrappers, + constraints: const BoxConstraints( + minWidth: 260, + ), buildChild: (popover) { return FlowyIconButton( - hoverColor: Colors.transparent, - iconPadding: const EdgeInsets.all(2), - width: 26, - icon: const FlowySvg(FlowySvgs.details_s), + width: 24, + icon: const FlowySvg(FlowySvgs.workspace_three_dots_s), onPressed: () { onEditing(true); popover.show(); }, ); }, - onSelected: (action, popover) { - onEditing(false); - onAction(action.inner); - popover.close(); - }, + onSelected: (_, __) {}, onClosed: () => onEditing(false), ); } + + List<ViewMoreActionTypeWrapper> _buildActionTypeWrappers() { + final actionTypes = _buildActionTypes(); + return actionTypes + .map( + (e) => ViewMoreActionTypeWrapper(e, (controller, data) { + onEditing(false); + onAction(e, data); + controller.close(); + }), + ) + .toList(); + } + + List<ViewMoreActionType> _buildActionTypes() { + final List<ViewMoreActionType> actionTypes = []; + switch (spaceType) { + case FolderSpaceType.favorite: + actionTypes.addAll([ + ViewMoreActionType.unFavorite, + ViewMoreActionType.divider, + ViewMoreActionType.rename, + ViewMoreActionType.openInNewTab, + ]); + break; + default: + actionTypes.addAll([ + view.isFavorite + ? ViewMoreActionType.unFavorite + : ViewMoreActionType.favorite, + ViewMoreActionType.divider, + ViewMoreActionType.rename, + ViewMoreActionType.changeIcon, + ViewMoreActionType.duplicate, + ViewMoreActionType.delete, + ViewMoreActionType.divider, + ViewMoreActionType.collapseAllPages, + ViewMoreActionType.divider, + ViewMoreActionType.openInNewTab, + ]); + } + return actionTypes; + } } -class ViewMoreActionTypeWrapper extends ActionCell { - ViewMoreActionTypeWrapper(this.inner); +class ViewMoreActionTypeWrapper extends CustomActionCell { + ViewMoreActionTypeWrapper(this.inner, this.onTap); final ViewMoreActionType inner; + final void Function(PopoverController controller, dynamic data) onTap; @override - Widget? leftIcon(Color iconColor) => inner.icon(iconColor); + Widget buildWithContext(BuildContext context, PopoverController controller) { + if (inner == ViewMoreActionType.divider) { + return _buildDivider(); + } else if (inner == ViewMoreActionType.lastModified) { + return _buildLastModified(context); + } else if (inner == ViewMoreActionType.created) { + return _buildCreated(context); + } else if (inner == ViewMoreActionType.changeIcon) { + return _buildEmojiActionButton(context, controller); + } else { + return _buildNormalActionButton(context, controller); + } + } - @override - String get name => inner.name; + Widget _buildNormalActionButton( + BuildContext context, + PopoverController controller, + ) { + return _buildActionButton(context, () => onTap(controller, null)); + } + + Widget _buildEmojiActionButton( + BuildContext context, + PopoverController controller, + ) { + final child = _buildActionButton(context, null); + + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(364, 356)), + clickHandler: PopoverClickHandler.gestureDetector, + popupBuilder: (_) => FlowyIconPicker( + onSelected: (result) => onTap(controller, result), + ), + child: child, + ); + } + + Widget _buildDivider() { + return const Padding( + padding: EdgeInsets.all(8.0), + child: Divider(height: 1.0), + ); + } + + Widget _buildLastModified(BuildContext context) { + return Container( + height: 40, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + ), + ); + } + + Widget _buildCreated(BuildContext context) { + return Container( + height: 40, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + ), + ); + } + + Widget _buildActionButton( + BuildContext context, + VoidCallback? onTap, + ) { + return Container( + height: 34, + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: FlowyButton( + margin: const EdgeInsets.symmetric(horizontal: 6), + leftIcon: inner.leftIcon, + rightIcon: inner.rightIcon, + iconPadding: 10.0, + text: SizedBox( + height: 18.0, + child: FlowyText.regular( + inner.name, + color: inner == ViewMoreActionType.delete + ? Theme.of(context).colorScheme.error + : null, + ), + ), + onTap: onTap, + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart index ee4f1d6d00..4760378f4b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/flowy_tab.dart @@ -33,7 +33,7 @@ class _FlowyTabState extends State<FlowyTab> { onExit: (_) => _setHovering(), child: Container( width: HomeSizes.tabBarWidth, - height: HomeSizes.tabBarHeigth, + height: HomeSizes.tabBarHeight, decoration: BoxDecoration( color: _getBackgroundColor(), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart index 6e24544820..064d64477f 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/tabs/tabs_manager.dart @@ -57,7 +57,7 @@ class _TabsManagerState extends State<TabsManager> return Container( alignment: Alignment.bottomLeft, - height: HomeSizes.tabBarHeigth, + height: HomeSizes.tabBarHeight, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart index a7925dc3f7..049ee6481b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart @@ -48,10 +48,8 @@ class NotificationButton extends StatelessWidget { Widget _buildNotificationIcon(BuildContext context, bool hasUnreads) { return Stack( children: [ - FlowySvg( - FlowySvgs.clock_alarm_s, - size: const Size.square(24), - color: Theme.of(context).colorScheme.tertiary, + const FlowySvg( + FlowySvgs.notification_s, ), if (hasUnreads) Positioned( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart index ebbb4f0e07..68829d7b66 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_account_view.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -23,6 +20,8 @@ import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SettingsAccountView extends StatefulWidget { @@ -342,7 +341,8 @@ class _UserProfileSettingState extends State<UserProfileSetting> { child: UserAvatar( iconUrl: widget.iconUrl, name: widget.name, - isLarge: true, + size: 48, + fontSize: 24, isHovering: isHovering, ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart index 8bf9415e3f..aa67ea3158 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_workspace_view.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; @@ -43,6 +40,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -377,7 +376,8 @@ class _WorkspaceIconSetting extends StatelessWidget { child: WorkspaceIcon( workspace: workspace!, iconSize: workspace!.icon.isNotEmpty == true ? 46 : 20, - enableEdit: enableEdit, + fontSize: 16.0, + enableEdit: true, onSelected: (r) => context .read<WorkspaceSettingsBloc>() .add(WorkspaceSettingsEvent.updateWorkspaceIcon(r.emoji)), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart index 43f9438ff7..e4e15da75d 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; class FeatureFlagsPage extends StatelessWidget { const FeatureFlagsPage({ @@ -50,7 +49,8 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> { subtitle: FlowyText.small(widget.featureFlag.description, maxLines: 3), trailing: Switch.adaptive( value: widget.featureFlag.isOn, - onChanged: (value) => setState(() => widget.featureFlag.update(value)), + onChanged: (value) => + setState(() async => widget.featureFlag.update(value)), ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart index 751599def3..decf74f874 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/files/settings_file_exporter_widget.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/export/document_exporter.dart'; import 'package:appflowy/workspace/application/settings/settings_file_exporter_cubit.dart'; @@ -16,7 +14,8 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/file_picker/file_picker_service.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:path/path.dart' as p; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart index 4c52e6c278..e25c81b182 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/favorite_button.dart @@ -1,13 +1,11 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ViewFavoriteButton extends StatelessWidget { @@ -35,9 +33,8 @@ class ViewFavoriteButton extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(6), child: FlowySvg( - isFavorite ? FlowySvgs.favorite_s : FlowySvgs.unfavorite_s, - size: const Size(18, 18), - color: AFThemeExtension.of(context).warning, + isFavorite ? FlowySvgs.unfavorite_s : FlowySvgs.favorite_s, + size: const Size.square(18), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index e72dfa098c..1c8c8d20ff 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -14,6 +11,8 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -147,7 +146,7 @@ class _DebugToast { class FlowyVersionDescription extends CustomActionCell { @override - Widget buildWithContext(BuildContext context) { + Widget buildWithContext(BuildContext context, PopoverController controller) { return FutureBuilder( future: PackageInfo.fromPlatform(), builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index 3d0416f589..e4bf942c6a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; @@ -14,6 +12,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MoreViewActions extends StatefulWidget { @@ -101,8 +100,8 @@ class _MoreViewActionsState extends State<MoreViewActions> { builder: (context, isHovering) => Padding( padding: const EdgeInsets.all(6), child: FlowySvg( - FlowySvgs.three_dots_vertical_s, - size: const Size.square(16), + FlowySvgs.three_dots_s, + size: const Size.square(18), color: isHovering ? Theme.of(context).colorScheme.onSecondary : Theme.of(context).iconTheme.color, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart index bb285a7917..d4103cc6bf 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/pop_up_action.dart @@ -1,5 +1,5 @@ import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flutter/material.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -83,7 +83,7 @@ class _PopoverActionListState<T extends PopoverAction> ); } else { final custom = action as CustomActionCell; - return custom.buildWithContext(context); + return custom.buildWithContext(context, popoverController); } }).toList(); @@ -121,7 +121,7 @@ abstract class PopoverActionCell extends PopoverAction { } abstract class CustomActionCell extends PopoverAction { - Widget buildWithContext(BuildContext context); + Widget buildWithContext(BuildContext context, PopoverController controller); } abstract class PopoverAction {} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart index b12ae6644a..ab67ecb5b4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart @@ -1,10 +1,10 @@ -import 'package:flutter/widgets.dart'; - import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/text_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; class RenameViewPopover extends StatefulWidget { const RenameViewPopover({ @@ -51,17 +51,20 @@ class _RenameViewPopoverState extends State<RenameViewPopover> { mainAxisSize: MainAxisSize.min, children: [ if (widget.showIconChanger) ...[ - EmojiPickerButton( - emoji: widget.emoji, - defaultIcon: widget.icon, - direction: PopoverDirection.bottomWithCenterAligned, - offset: const Offset(0, 18), - onSubmitted: _updateViewIcon, + SizedBox( + width: 30.0, + child: EmojiPickerButton( + emoji: widget.emoji, + defaultIcon: widget.icon, + direction: PopoverDirection.bottomWithCenterAligned, + offset: const Offset(0, 18), + onSubmitted: _updateViewIcon, + ), ), const HSpace(6), ], SizedBox( - height: 36.0, + height: 32.0, width: 220, child: FlowyTextField( controller: _controller, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index 4b6708c151..5ad85efe31 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -1,37 +1,32 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/util/built_in_svgs.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; - -const double _smallSize = 28; -const double _largeSize = 64; +import 'package:flutter/material.dart'; class UserAvatar extends StatelessWidget { const UserAvatar({ super.key, required this.iconUrl, required this.name, - this.isLarge = false, + required this.size, + required this.fontSize, this.isHovering = false, }); final String iconUrl; final String name; - final bool isLarge; + final double size; + final double fontSize; // If true, a border will be applied on top of the avatar final bool isHovering; @override Widget build(BuildContext context) { - final size = isLarge ? _largeSize : _smallSize; - if (iconUrl.isEmpty) { final String nameOrDefault = _userName(name); final Color color = ColorGenerator(name).toColor(); @@ -59,16 +54,10 @@ class UserAvatar extends StatelessWidget { ) : null, ), - child: FlowyText.semibold( + child: FlowyText.regular( nameInitials, color: Colors.black, - fontSize: isLarge - ? nameInitials.length == initialsCount - ? 20 - : 26 - : nameInitials.length == initialsCount - ? 12 - : 14, + fontSize: fontSize, ), ); } @@ -94,7 +83,7 @@ class UserAvatar extends StatelessWidget { FlowySvgData('emoji/$iconUrl'), blendMode: null, ) - : EmojiText(emoji: iconUrl, fontSize: isLarge ? 36 : 18), + : FlowyText.emoji(iconUrl, fontSize: fontSize), ), ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart index 9d65cc06a1..18b206bd9e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/view_title_bar.dart @@ -1,23 +1,18 @@ -import 'dart:math'; - -import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; -import 'package:appflowy/startup/tasks/app_window_size_manager.dart'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; -import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart'; +import 'package:appflowy/workspace/application/view_title/view_title_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -// workspace name / ... / view_title -class ViewTitleBar extends StatefulWidget { +// workspace name > ... > view_title +class ViewTitleBar extends StatelessWidget { const ViewTitleBar({ super.key, required this.view, @@ -25,133 +20,83 @@ class ViewTitleBar extends StatefulWidget { final ViewPB view; - @override - State<ViewTitleBar> createState() => _ViewTitleBarState(); -} - -class _ViewTitleBarState extends State<ViewTitleBar> { - late Future<List<ViewPB>> ancestors; - late String viewId; - - @override - void initState() { - super.initState(); - - viewId = widget.view.id; - _reloadAncestors(viewId); - } - - @override - void didUpdateWidget(covariant ViewTitleBar oldWidget) { - super.didUpdateWidget(oldWidget); - - if (oldWidget.view.id != widget.view.id) { - viewId = widget.view.id; - _reloadAncestors(viewId); - } - } - + // late Future<List<ViewPB>> ancestors; @override Widget build(BuildContext context) { - return FutureBuilder<List<ViewPB>>( - future: ancestors, - builder: (context, snapshot) { - final ancestors = snapshot.data; - if (ancestors == null || - snapshot.connectionState != ConnectionState.done) { - return const SizedBox.shrink(); - } - const maxWidth = WindowSizeManager.minWindowWidth / 2.0; - final replacement = Row( - // refresh the view title bar when the ancestors changed - key: ValueKey(ancestors.hashCode), - children: _buildViewTitles(context, ancestors), - ); - return LayoutBuilder( - builder: (context, constraints) { - return Visibility( - visible: constraints.maxWidth < maxWidth, - replacement: replacement, - // if the width is too small, only show one view title bar without the ancestors - child: _ViewTitle( - key: ValueKey(ancestors.last), - view: ancestors.last, - maxTitleWidth: constraints.maxWidth, - onUpdated: () => setState(() => _reloadAncestors(viewId)), - ), - ); - }, - ); - }, + return BlocProvider( + create: (_) => + ViewTitleBarBloc(view: view)..add(const ViewTitleBarEvent.initial()), + child: BlocBuilder<ViewTitleBarBloc, ViewTitleBarState>( + builder: (context, state) { + final ancestors = state.ancestors; + if (ancestors.isEmpty) { + return const SizedBox.shrink(); + } + return SingleChildScrollView( + child: SizedBox( + height: 24, + child: Row(children: _buildViewTitles(context, ancestors)), + ), + ); + }, + ), ); } List<Widget> _buildViewTitles(BuildContext context, List<ViewPB> views) { // if the level is too deep, only show the last two view, the first one view and the root view + // for example: + // if the views are [root, view1, view2, view3, view4, view5], only show [root, view1, ..., view4, view5] + // if the views are [root, view1, view2, view3], show [root, view1, view2, view3] + const lowerBound = 2; + final upperBound = views.length - 2; bool hasAddedEllipsis = false; final children = <Widget>[]; - for (var i = 0; i < views.length; i++) { + if (views.length <= 1) { + return []; + } + + // ignore the workspace name, use section name instead in the future + // skip the workspace view + for (var i = 1; i < views.length; i++) { final view = views[i]; - if (i >= 1 && i < views.length - 2) { + if (i >= lowerBound && i < upperBound) { if (!hasAddedEllipsis) { hasAddedEllipsis = true; - children.add( - const FlowyText.regular(' ... /'), - ); + children.addAll([ + const FlowyText.regular(' ... '), + const FlowySvg(FlowySvgs.title_bar_divider_s), + ]); } continue; } - Widget child; - if (i == 0) { - final currentWorkspace = - context.read<UserWorkspaceBloc>().state.currentWorkspace; - final icon = currentWorkspace?.icon ?? ''; - final name = currentWorkspace?.name ?? view.name; - // the first one is the workspace name - child = FlowyTooltip( - message: name, - child: Row( - children: [ - EmojiText( - emoji: icon, - fontSize: 18.0, - ), - const HSpace(2.0), - FlowyText.regular(name), - const HSpace(4.0), - ], - ), - ); - } else { - child = FlowyTooltip( - message: view.name, - child: _ViewTitle( - view: view, - behavior: i == views.length - 1 - ? _ViewTitleBehavior.editable // only the last one is editable - : _ViewTitleBehavior.uneditable, // others are not editable - onUpdated: () => setState(() => _reloadAncestors(viewId)), - ), - ); - } + final child = FlowyTooltip( + message: view.name, + child: _ViewTitle( + view: view, + behavior: i == views.length - 1 + ? _ViewTitleBehavior.editable // only the last one is editable + : _ViewTitleBehavior.uneditable, // others are not editable + onUpdated: () { + context + .read<ViewTitleBarBloc>() + .add(const ViewTitleBarEvent.reload()); + }, + ), + ); children.add(child); if (i != views.length - 1) { // if not the last one, add a divider - children.add(const FlowyText.regular('/')); + children.add(const FlowySvg(FlowySvgs.title_bar_divider_s)); } } return children; } - - void _reloadAncestors(String viewId) { - ancestors = ViewBackendService.getViewAncestors(viewId) - .fold((s) => s.items, (f) => []); - } } enum _ViewTitleBehavior { @@ -161,16 +106,13 @@ enum _ViewTitleBehavior { class _ViewTitle extends StatefulWidget { const _ViewTitle({ - super.key, required this.view, this.behavior = _ViewTitleBehavior.editable, - this.maxTitleWidth = 180, required this.onUpdated, }); final ViewPB view; final _ViewTitleBehavior behavior; - final double maxTitleWidth; final VoidCallback onUpdated; @override @@ -180,87 +122,58 @@ class _ViewTitle extends StatefulWidget { class _ViewTitleState extends State<_ViewTitle> { final popoverController = PopoverController(); final textEditingController = TextEditingController(); - late final viewListener = ViewListener(viewId: widget.view.id); - - String name = ''; - String icon = ''; - String inputtingName = ''; - - @override - void initState() { - super.initState(); - - name = widget.view.name; - icon = widget.view.icon.value; - - _resetTextEditingController(); - viewListener.start( - onViewUpdated: (view) { - if (name != view.name || icon != view.icon.value) { - widget.onUpdated(); - } - setState(() { - name = view.name; - icon = view.icon.value; - _resetTextEditingController(); - }); - }, - ); - } @override void dispose() { textEditingController.dispose(); popoverController.close(); - viewListener.stop(); super.dispose(); } @override Widget build(BuildContext context) { - // root view - if (widget.view.parentViewId.isEmpty) { - return Row( - children: [ - FlowyText.regular(name), - const HSpace(4.0), - ], - ); - } + final isEditable = widget.behavior == _ViewTitleBehavior.editable; - final child = SingleChildScrollView( - child: Row( - children: [ - EmojiText( - emoji: icon, - fontSize: 18.0, - ), - const HSpace(2.0), - ConstrainedBox( - constraints: BoxConstraints( - maxWidth: max(0, widget.maxTitleWidth), - ), - child: FlowyText.regular( - name, - overflow: TextOverflow.ellipsis, - ), - ), - ], + return BlocProvider( + create: (_) => + ViewTitleBloc(view: widget.view)..add(const ViewTitleEvent.initial()), + child: BlocConsumer<ViewTitleBloc, ViewTitleState>( + listener: (_, state) { + _resetTextEditingController(state); + widget.onUpdated(); + }, + builder: (context, state) { + // root view + if (widget.view.parentViewId.isEmpty) { + return Row( + children: [ + FlowyText.regular(state.name), + const HSpace(4.0), + ], + ); + } else if (isEditable) { + return _buildEditableViewTitle(context, state); + } else { + return _buildUnEditableViewTitle(context, state); + } + }, ), ); + } - if (widget.behavior == _ViewTitleBehavior.uneditable) { - return Listener( - onPointerDown: (_) => context.read<TabsBloc>().openPlugin(widget.view), - child: FlowyButton( - useIntrinsicWidth: true, - onTap: () {}, - text: child, - ), - ); - } + Widget _buildUnEditableViewTitle(BuildContext context, ViewTitleState state) { + return Listener( + onPointerDown: (_) => context.read<TabsBloc>().openPlugin(widget.view), + child: FlowyButton( + useIntrinsicWidth: true, + onTap: () {}, + text: _buildIconAndName(state), + ), + ); + } + Widget _buildEditableViewTitle(BuildContext context, ViewTitleState state) { return AppFlowyPopover( constraints: const BoxConstraints( maxWidth: 300, @@ -268,32 +181,55 @@ class _ViewTitleState extends State<_ViewTitle> { ), controller: popoverController, direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 18), + offset: const Offset(0, 6), popupBuilder: (context) { // icon + textfield - _resetTextEditingController(); + _resetTextEditingController(state); return RenameViewPopover( viewId: widget.view.id, name: widget.view.name, popoverController: popoverController, icon: widget.view.defaultIcon(), - emoji: icon, + emoji: state.icon, ); }, child: FlowyButton( useIntrinsicWidth: true, - text: child, + margin: const EdgeInsets.symmetric(horizontal: 6.0), + text: _buildIconAndName(state), ), ); } - void _resetTextEditingController() { - inputtingName = name; + Widget _buildIconAndName(ViewTitleState state) { + return SingleChildScrollView( + child: Row( + children: [ + if (state.icon.isNotEmpty) ...[ + FlowyText.emoji( + state.icon, + fontSize: 14.0, + ), + const HSpace(6.0), + ], + ConstrainedBox( + constraints: const BoxConstraints(), + child: FlowyText.regular( + state.name, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + void _resetTextEditingController(ViewTitleState state) { textEditingController - ..text = name + ..text = state.name ..selection = TextSelection( baseOffset: 0, - extentOffset: name.length, + extentOffset: state.name.length, ); } } diff --git a/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift b/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift index 8e357d7ca1..65498b121f 100644 --- a/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift +++ b/frontend/appflowy_flutter/macos/Runner/MainFlutterWindow.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -private let kTrafficLightOffetTop = 22 +private let kTrafficLightOffetTop = 14 class MainFlutterWindow: NSWindow { func registerMethodChannel(flutterViewController: FlutterViewController) { @@ -17,7 +17,7 @@ class MainFlutterWindow: NSWindow { let nY = position[1] as! NSNumber let x = nX.doubleValue let y = nY.doubleValue - + self.setFrameOrigin(NSPoint(x: x, y: y)) result(nil) return @@ -30,7 +30,7 @@ class MainFlutterWindow: NSWindow { result(nil) return } - + result(FlutterMethodNotImplemented) }) } @@ -51,9 +51,9 @@ class MainFlutterWindow: NSWindow { let zoomButton = self.standardWindowButton(ButtonType.zoomButton)! let titlebarView = closeButton.superview! - self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 20) - self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 38) - self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 56) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: closeButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 12) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: minButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 30) + self.layoutTrafficLightButton(titlebarView: titlebarView, button: zoomButton, offsetTop: CGFloat(kTrafficLightOffetTop), offsetLeft: 48) let customToolbar = NSTitlebarAccessoryViewController() let newView = NSView() diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index d9420944fb..405db2bee8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -1,8 +1,7 @@ +import 'package:appflowy_popover/src/layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:appflowy_popover/src/layout.dart'; - import 'mask.dart'; import 'mutex.dart'; @@ -291,6 +290,13 @@ class PopoverContainer extends StatefulWidget { context.findAncestorStateOfType<PopoverContainerState>(); return result!; } + + static PopoverContainerState? maybeOf(BuildContext context) { + if (context is StatefulElement && context.state is PopoverContainerState) { + return context.state as PopoverContainerState; + } + return context.findAncestorStateOfType<PopoverContainerState>(); + } } class PopoverContainerState extends State<PopoverContainer> { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart index 59a2bc6533..7bbcbf0949 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/basis.dart @@ -1,8 +1,3 @@ -import 'package:flutter/material.dart'; - // MARK: - Shared Builder - -typedef WidgetBuilder = Widget Function(); - typedef IndexedCallback = void Function(int index); typedef IndexedValueCallback<T> = void Function(T value, int index); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index f402c171d2..acd628a222 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -1,12 +1,11 @@ import 'dart:io'; -import 'package:flutter/material.dart'; - import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/ignore_parent_gesture.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; class FlowyButton extends StatelessWidget { final Widget text; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart index 1a4b96ecb5..0902d78835 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/decoration.dart @@ -7,10 +7,11 @@ class FlowyDecoration { double spreadRadius = 0, double blurRadius = 20, Offset offset = Offset.zero, + double borderRadius = 6, }) { return BoxDecoration( color: boxColor, - borderRadius: const BorderRadius.all(Radius.circular(6)), + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), boxShadow: [ BoxShadow( color: boxShadow, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index 65d0c19c59..88ebc735b7 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -16,6 +16,7 @@ class FlowyText extends StatelessWidget { final List<String>? fallbackFontFamily; final double? lineHeight; final bool withTooltip; + final StrutStyle? strutStyle; const FlowyText( this.text, { @@ -32,6 +33,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }); FlowyText.small( @@ -47,6 +49,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }) : fontWeight = FontWeight.w400, fontSize = (Platform.isIOS || Platform.isAndroid) ? 14 : 12; @@ -64,6 +67,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }) : fontWeight = FontWeight.w400; const FlowyText.medium( @@ -80,6 +84,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }) : fontWeight = FontWeight.w500; const FlowyText.semibold( @@ -96,6 +101,7 @@ class FlowyText extends StatelessWidget { this.fallbackFontFamily, this.lineHeight, this.withTooltip = false, + this.strutStyle, }) : fontWeight = FontWeight.w600; // Some emojis are not supported on Linux and Android, fallback to noto color emoji @@ -105,12 +111,13 @@ class FlowyText extends StatelessWidget { this.fontSize, this.overflow, this.color, - this.textAlign, + this.textAlign = TextAlign.center, this.maxLines = 1, this.decoration, this.selectable = false, this.lineHeight, this.withTooltip = false, + this.strutStyle = const StrutStyle(forceStrutHeight: true), }) : fontWeight = FontWeight.w400, fontFamily = 'noto color emoji', fallbackFontFamily = null; @@ -119,20 +126,23 @@ class FlowyText extends StatelessWidget { Widget build(BuildContext context) { Widget child; + final textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + decoration: decoration, + fontFamily: fontFamily, + fontFamilyFallback: fallbackFontFamily, + height: lineHeight, + ); + if (selectable) { child = SelectableText( text, maxLines: maxLines, textAlign: textAlign, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: fontSize, - fontWeight: fontWeight, - color: color, - decoration: decoration, - fontFamily: fontFamily, - fontFamilyFallback: fallbackFontFamily, - height: lineHeight, - ), + strutStyle: strutStyle, + style: textStyle, ); } else { child = Text( @@ -140,15 +150,7 @@ class FlowyText extends StatelessWidget { maxLines: maxLines, textAlign: textAlign, overflow: overflow ?? TextOverflow.clip, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - fontSize: fontSize, - fontWeight: fontWeight, - color: color, - decoration: decoration, - fontFamily: fontFamily, - fontFamilyFallback: fallbackFontFamily, - height: lineHeight, - ), + style: textStyle, ); } diff --git a/frontend/resources/flowy_icons/16x/add_cover.svg b/frontend/resources/flowy_icons/16x/add_cover.svg new file mode 100644 index 0000000000..ac83855416 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_cover.svg @@ -0,0 +1,7 @@ +<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.3"> +<path d="M1.72052 13.1735L1.70719 13.1868C1.52719 12.7935 1.41385 12.3468 1.36719 11.8535C1.41385 12.3402 1.54052 12.7802 1.72052 13.1735Z" fill="#171717"/> +<path d="M6.00073 7.41943C6.87702 7.41943 7.5874 6.70905 7.5874 5.83276C7.5874 4.95647 6.87702 4.24609 6.00073 4.24609C5.12444 4.24609 4.41406 4.95647 4.41406 5.83276C4.41406 6.70905 5.12444 7.41943 6.00073 7.41943Z" fill="#171717"/> +<path d="M10.792 1.83398H5.20536C2.7787 1.83398 1.33203 3.28065 1.33203 5.70732V11.294C1.33203 12.0207 1.4587 12.654 1.70536 13.1873C2.2787 14.454 3.50536 15.1673 5.20536 15.1673H10.792C13.2187 15.1673 14.6654 13.7207 14.6654 11.294V9.76732V5.70732C14.6654 3.28065 13.2187 1.83398 10.792 1.83398ZM13.5787 8.83398C13.0587 8.38732 12.2187 8.38732 11.6987 8.83398L8.92537 11.214C8.40537 11.6607 7.56536 11.6607 7.04536 11.214L6.8187 11.0273C6.34536 10.614 5.59203 10.574 5.0587 10.934L2.56536 12.6073C2.4187 12.234 2.33203 11.8007 2.33203 11.294V5.70732C2.33203 3.82732 3.32536 2.83398 5.20536 2.83398H10.792C12.672 2.83398 13.6654 3.82732 13.6654 5.70732V8.90732L13.5787 8.83398Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/add_icon.svg b/frontend/resources/flowy_icons/16x/add_icon.svg new file mode 100644 index 0000000000..e49b54ec14 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_icon.svg @@ -0,0 +1,5 @@ +<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.3"> +<path d="M12.739 3.80656C10.1323 1.1999 5.899 1.1999 3.29233 3.80656C0.639001 6.4599 0.685667 10.7866 3.42567 13.3866C5.959 15.7799 10.0657 15.7799 12.599 13.3866C15.3457 10.7866 15.3923 6.4599 12.739 3.80656ZM10.919 11.5999C10.119 12.3599 9.06567 12.7399 8.01233 12.7399C6.959 12.7399 5.90567 12.3599 5.10567 11.5999C4.90567 11.4066 4.899 11.0932 5.08567 10.8932C5.279 10.6932 5.59233 10.6866 5.79233 10.8732C7.01233 12.0266 9.00567 12.0332 10.2323 10.8732C10.4323 10.6866 10.7523 10.6932 10.939 10.8932C11.1323 11.0932 11.119 11.4066 10.919 11.5999Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/add_workspace.svg b/frontend/resources/flowy_icons/16x/add_workspace.svg new file mode 100644 index 0000000000..83dabfe0a1 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/add_workspace.svg @@ -0,0 +1,6 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<rect x="2" y="7.3999" width="12" height="1.2" rx="0.6" fill="#171717"/> +<rect x="7.40234" y="14" width="12" height="1.2" rx="0.6" transform="rotate(-90 7.40234 14)" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/change_icon.svg b/frontend/resources/flowy_icons/16x/change_icon.svg new file mode 100644 index 0000000000..38c7e41710 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/change_icon.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="#171717" stroke-width="1.02857" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.14453 10.4004C5.77453 11.3064 6.83053 11.9004 8.01853 11.9004C9.20653 11.9004 10.2565 11.3064 10.8925 10.4004" stroke="#171717" stroke-width="1.02857" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/collapse_all_page.svg b/frontend/resources/flowy_icons/16x/collapse_all_page.svg new file mode 100644 index 0000000000..2760daaaef --- /dev/null +++ b/frontend/resources/flowy_icons/16x/collapse_all_page.svg @@ -0,0 +1,14 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<mask id="path-1-outside-1_517_23658" maskUnits="userSpaceOnUse" x="3.69922" y="1.62109" width="9" height="6" fill="black"> +<rect fill="white" x="3.69922" y="1.62109" width="9" height="6"/> +<path d="M11.5638 2.6337C11.3831 2.45486 11.0903 2.45486 10.9097 2.6337L7.99922 5.51551L5.08876 2.6337C4.90814 2.45486 4.6153 2.45486 4.43468 2.6337C4.25406 2.81254 4.25406 3.1025 4.43468 3.28134L7.67218 6.48696C7.75892 6.57285 7.87656 6.62109 7.99922 6.62109C8.12188 6.62109 8.23952 6.57285 8.32626 6.48696L11.5638 3.28134C11.7444 3.1025 11.7444 2.81254 11.5638 2.6337Z"/> +</mask> +<path d="M11.5638 2.6337C11.3831 2.45486 11.0903 2.45486 10.9097 2.6337L7.99922 5.51551L5.08876 2.6337C4.90814 2.45486 4.6153 2.45486 4.43468 2.6337C4.25406 2.81254 4.25406 3.1025 4.43468 3.28134L7.67218 6.48696C7.75892 6.57285 7.87656 6.62109 7.99922 6.62109C8.12188 6.62109 8.23952 6.57285 8.32626 6.48696L11.5638 3.28134C11.7444 3.1025 11.7444 2.81254 11.5638 2.6337Z" fill="#171717"/> +<path d="M10.9097 2.6337L10.9881 2.71286L10.9097 2.6337ZM11.5638 2.6337L11.4854 2.71286H11.4854L11.5638 2.6337ZM7.99922 5.51551L7.92084 5.59467C7.96425 5.63765 8.03418 5.63765 8.07759 5.59467L7.99922 5.51551ZM5.08876 2.6337L5.01038 2.71286L5.08876 2.6337ZM4.43468 2.6337L4.51306 2.71286V2.71286L4.43468 2.6337ZM4.43468 3.28134L4.51306 3.20218V3.20218L4.43468 3.28134ZM7.67218 6.48696L7.59381 6.56612V6.56612L7.67218 6.48696ZM8.32626 6.48696L8.24788 6.40781V6.40781L8.32626 6.48696ZM11.5638 3.28134L11.6421 3.36049H11.6421L11.5638 3.28134ZM10.9881 2.71286C11.1253 2.577 11.3482 2.577 11.4854 2.71286L11.6421 2.55455C11.4181 2.33273 11.0553 2.33273 10.8313 2.55455L10.9881 2.71286ZM8.07759 5.59467L10.9881 2.71286L10.8313 2.55455L7.92084 5.43636L8.07759 5.59467ZM5.01038 2.71286L7.92084 5.59467L8.07759 5.43636L5.16713 2.55455L5.01038 2.71286ZM4.51306 2.71286C4.65026 2.577 4.87317 2.577 5.01038 2.71286L5.16713 2.55455C4.9431 2.33273 4.58033 2.33273 4.35631 2.55455L4.51306 2.71286ZM4.51306 3.20218C4.37646 3.06693 4.37646 2.84811 4.51306 2.71286L4.35631 2.55455C4.13167 2.77698 4.13167 3.13807 4.35631 3.36049L4.51306 3.20218ZM7.75056 6.40781L4.51306 3.20218L4.35631 3.36049L7.59381 6.56612L7.75056 6.40781ZM7.99922 6.5097C7.90575 6.5097 7.81632 6.47292 7.75056 6.40781L7.59381 6.56612C7.70151 6.67277 7.84737 6.73249 7.99922 6.73249V6.5097ZM8.24788 6.40781C8.18212 6.47292 8.09269 6.5097 7.99922 6.5097V6.73249C8.15107 6.73249 8.29692 6.67277 8.40463 6.56612L8.24788 6.40781ZM11.4854 3.20218L8.24788 6.40781L8.40463 6.56612L11.6421 3.36049L11.4854 3.20218ZM11.4854 2.71286C11.622 2.84811 11.622 3.06693 11.4854 3.20218L11.6421 3.36049C11.8668 3.13807 11.8668 2.77698 11.6421 2.55455L11.4854 2.71286Z" fill="#171717" mask="url(#path-1-outside-1_517_23658)"/> +<mask id="path-3-outside-2_517_23658" maskUnits="userSpaceOnUse" x="3.30078" y="8.37891" width="9" height="6" fill="black"> +<rect fill="white" x="3.30078" y="8.37891" width="9" height="6"/> +<path d="M4.43624 13.3663C4.61686 13.5451 4.9097 13.5451 5.09032 13.3663L8.00078 10.4845L10.9112 13.3663C11.0919 13.5451 11.3847 13.5451 11.5653 13.3663C11.7459 13.1875 11.7459 12.8975 11.5653 12.7187L8.32782 9.51304C8.24108 9.42715 8.12344 9.37891 8.00078 9.37891C7.87812 9.37891 7.76048 9.42715 7.67374 9.51304L4.43624 12.7187C4.25563 12.8975 4.25563 13.1875 4.43624 13.3663Z"/> +</mask> +<path d="M4.43624 13.3663C4.61686 13.5451 4.9097 13.5451 5.09032 13.3663L8.00078 10.4845L10.9112 13.3663C11.0919 13.5451 11.3847 13.5451 11.5653 13.3663C11.7459 13.1875 11.7459 12.8975 11.5653 12.7187L8.32782 9.51304C8.24108 9.42715 8.12344 9.37891 8.00078 9.37891C7.87812 9.37891 7.76048 9.42715 7.67374 9.51304L4.43624 12.7187C4.25563 12.8975 4.25563 13.1875 4.43624 13.3663Z" fill="#171717"/> +<path d="M5.09032 13.3663L5.01194 13.2871L5.09032 13.3663ZM4.43624 13.3663L4.51462 13.2871H4.51462L4.43624 13.3663ZM8.00078 10.4845L8.07916 10.4053C8.03575 10.3623 7.96582 10.3623 7.92241 10.4053L8.00078 10.4845ZM10.9112 13.3663L10.9896 13.2871L10.9112 13.3663ZM11.5653 13.3663L11.4869 13.2871V13.2871L11.5653 13.3663ZM11.5653 12.7187L11.4869 12.7978V12.7978L11.5653 12.7187ZM8.32782 9.51304L8.40619 9.43388V9.43388L8.32782 9.51304ZM7.67374 9.51304L7.75212 9.59219V9.59219L7.67374 9.51304ZM4.43624 12.7187L4.35787 12.6395H4.35787L4.43624 12.7187ZM5.01194 13.2871C4.87474 13.423 4.65183 13.423 4.51462 13.2871L4.35787 13.4455C4.5819 13.6673 4.94467 13.6673 5.16869 13.4455L5.01194 13.2871ZM7.92241 10.4053L5.01194 13.2871L5.16869 13.4455L8.07916 10.5636L7.92241 10.4053ZM10.9896 13.2871L8.07916 10.4053L7.92241 10.5636L10.8329 13.4455L10.9896 13.2871ZM11.4869 13.2871C11.3497 13.423 11.1268 13.423 10.9896 13.2871L10.8329 13.4455C11.0569 13.6673 11.4197 13.6673 11.6437 13.4455L11.4869 13.2871ZM11.4869 12.7978C11.6235 12.9331 11.6235 13.1519 11.4869 13.2871L11.6437 13.4455C11.8683 13.223 11.8683 12.8619 11.6437 12.6395L11.4869 12.7978ZM8.24944 9.59219L11.4869 12.7978L11.6437 12.6395L8.40619 9.43388L8.24944 9.59219ZM8.00078 9.4903C8.09425 9.4903 8.18368 9.52708 8.24944 9.59219L8.40619 9.43388C8.29849 9.32723 8.15263 9.26751 8.00078 9.26751V9.4903ZM7.75212 9.59219C7.81788 9.52708 7.90731 9.4903 8.00078 9.4903V9.26751C7.84893 9.26751 7.70308 9.32723 7.59537 9.43388L7.75212 9.59219ZM4.51462 12.7978L7.75212 9.59219L7.59537 9.43388L4.35787 12.6395L4.51462 12.7978ZM4.51462 13.2871C4.37802 13.1519 4.37802 12.9331 4.51462 12.7978L4.35787 12.6395C4.13323 12.8619 4.13323 13.223 4.35787 13.4455L4.51462 13.2871Z" fill="#171717" mask="url(#path-3-outside-2_517_23658)"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/duplicate.svg b/frontend/resources/flowy_icons/16x/duplicate.svg new file mode 100644 index 0000000000..8830822589 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/duplicate.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10.6654 8.60065V11.4007C10.6654 13.734 9.73203 14.6673 7.3987 14.6673H4.5987C2.26536 14.6673 1.33203 13.734 1.33203 11.4007V8.60065C1.33203 6.26732 2.26536 5.33398 4.5987 5.33398H7.3987C9.73203 5.33398 10.6654 6.26732 10.6654 8.60065Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.6654 4.60065V7.40065C14.6654 9.73398 13.732 10.6673 11.3987 10.6673H10.6654V8.60065C10.6654 6.26732 9.73203 5.33398 7.3987 5.33398H5.33203V4.60065C5.33203 2.26732 6.26536 1.33398 8.5987 1.33398H11.3987C13.732 1.33398 14.6654 2.26732 14.6654 4.60065Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/favorite.svg b/frontend/resources/flowy_icons/16x/favorite.svg index 8ad54bbbb5..addd7d4915 100644 --- a/frontend/resources/flowy_icons/16x/favorite.svg +++ b/frontend/resources/flowy_icons/16x/favorite.svg @@ -1,3 +1,3 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8 3L9.3905 5.96215L12.5 6.44006L10.25 8.74448L10.781 12L8 10.4621L5.219 12L5.75 8.74448L3.5 6.44006L6.6095 5.96215L8 3Z" fill="#FFD667" stroke="#FFD667" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M9.15132 2.33977L10.3247 4.68643C10.4847 5.0131 10.9113 5.32643 11.2713 5.38643L13.398 5.73977C14.758 5.96643 15.078 6.9531 14.098 7.92643L12.4447 9.57977C12.1647 9.85977 12.0113 10.3998 12.098 10.7864L12.5713 12.8331C12.9447 14.4531 12.0847 15.0798 10.6513 14.2331L8.65799 13.0531C8.29799 12.8398 7.70465 12.8398 7.33799 13.0531L5.34465 14.2331C3.91799 15.0798 3.05132 14.4464 3.42465 12.8331L3.89799 10.7864C3.98465 10.3998 3.83132 9.85977 3.55132 9.57977L1.89799 7.92643C0.924653 6.9531 1.23799 5.96643 2.59799 5.73977L4.72465 5.38643C5.07799 5.32643 5.50465 5.0131 5.66465 4.68643L6.83799 2.33977C7.47799 1.06643 8.51799 1.06643 9.15132 2.33977Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/favorite_header_icon.svg b/frontend/resources/flowy_icons/16x/favorite_header_icon.svg new file mode 100644 index 0000000000..8296f888f3 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_header_icon.svg @@ -0,0 +1,3 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.15132 2.34025L10.3247 4.68692C10.4847 5.01359 10.9113 5.32692 11.2713 5.38692L13.398 5.74025C14.758 5.96692 15.078 6.95359 14.098 7.92692L12.4447 9.58025C12.1647 9.86025 12.0113 10.4003 12.098 10.7869L12.5713 12.8336C12.9447 14.4536 12.0847 15.0803 10.6513 14.2336L8.65799 13.0536C8.29799 12.8403 7.70465 12.8403 7.33799 13.0536L5.34465 14.2336C3.91799 15.0803 3.05132 14.4469 3.42465 12.8336L3.89799 10.7869C3.98465 10.4003 3.83132 9.86025 3.55132 9.58025L1.89799 7.92692C0.924653 6.95359 1.23799 5.96692 2.59799 5.74025L4.72465 5.38692C5.07799 5.32692 5.50465 5.01359 5.66465 4.68692L6.83799 2.34025C7.47799 1.06692 8.51799 1.06692 9.15132 2.34025Z" fill="#FFBA00"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/favorite_section_pin.svg b/frontend/resources/flowy_icons/16x/favorite_section_pin.svg new file mode 100644 index 0000000000..0402120e41 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_section_pin.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<path d="M8.38274 2.1912C8.61919 1.95467 8.99451 1.91977 9.2623 2.18766L14.145 7.07205C14.3765 7.30363 14.411 7.68431 14.1468 7.94865C13.9854 8.11011 13.5919 8.23414 13.2876 7.92972L12.5611 7.20735L10.6204 9.1481C10.5628 9.20571 10.5477 9.30158 10.5477 9.38042L10.2799 13.2031C10.2799 13.285 10.2475 13.3638 10.1899 13.4214L9.84485 13.7657C9.60524 14.0063 9.21857 14.0071 8.98279 13.7713L6.20394 10.9936L3.37596 13.8224C3.1392 14.0592 2.75536 14.0592 2.51861 13.8224L2.5096 13.8134C2.27301 13.5767 2.27282 13.193 2.50916 12.9561L5.33522 10.1233L2.5568 7.34311C2.31101 7.09723 2.31763 6.71613 2.55769 6.47598L2.90214 6.13221C2.95976 6.07459 3.03787 6.04124 3.11932 6.04124L6.94541 5.77806C7.02695 5.77806 7.13094 5.77049 7.18854 5.71288L9.12891 3.77213L8.40493 3.04532C8.17437 2.81725 8.15367 2.42036 8.38274 2.1912Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/favorite_section_remove_from_favorite.svg b/frontend/resources/flowy_icons/16x/favorite_section_remove_from_favorite.svg new file mode 100644 index 0000000000..b984afe017 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_section_remove_from_favorite.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M3.52189 12.4331L3.90189 10.7864C3.98856 10.3998 3.83523 9.85977 3.55523 9.57977L1.90189 7.92643C0.928559 6.9531 1.24189 5.96643 2.60189 5.73977L4.72856 5.38643C5.08189 5.32643 5.50856 5.0131 5.66856 4.68643L6.84189 2.33977C7.47523 1.06643 8.51523 1.06643 9.15523 2.33977L10.3286 4.68643C10.4019 4.83977 10.5419 4.98643 10.6952 5.1131" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.401 5.73975C14.761 5.96641 15.081 6.95308 14.101 7.92641L12.4477 9.57975C12.1677 9.85975 12.0143 10.3997 12.101 10.7864L12.5743 12.8331C12.9477 14.4531 12.0877 15.0797 10.6543 14.2331L8.66099 13.0531C8.30099 12.8397 7.70766 12.8397 7.34099 13.0531L5.34766 14.2331" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.6693 1.33301L1.33594 14.6663" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg b/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg new file mode 100644 index 0000000000..3e72f90f4b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/favorite_section_unpin.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<path d="M9.2623 2.18766C8.99451 1.91977 8.61919 1.95467 8.38274 2.1912C8.15367 2.42036 8.17437 2.81725 8.40493 3.04532L9.12891 3.77213L7.18854 5.71288C7.13094 5.77049 7.02695 5.77806 6.94541 5.77806L3.11932 6.04124C3.03787 6.04124 2.95976 6.07459 2.90214 6.13221L2.55769 6.47598C2.31763 6.71613 2.31101 7.09723 2.5568 7.34311L5.33522 10.1233L2.50916 12.9561C2.27282 13.193 2.27301 13.5767 2.5096 13.8134L2.51861 13.8224C2.75536 14.0592 3.1392 14.0592 3.37596 13.8224L6.20394 10.9936L8.98279 13.7713C9.21857 14.0071 9.60524 14.0063 9.84485 13.7657L10.1899 13.4214C10.2475 13.3638 10.2799 13.285 10.2799 13.2031L10.5477 9.38042C10.5477 9.30159 10.5628 9.20571 10.6204 9.1481L12.5611 7.20735L13.2876 7.92972C13.5919 8.23414 13.9854 8.11011 14.1468 7.94865C14.411 7.68431 14.3765 7.30363 14.145 7.07205L9.2623 2.18766ZM10.0059 4.64872L11.7235 6.36508L9.40296 8.69111L9.14988 12.226L4.1365 7.20788L7.69325 6.96485L10.0059 4.64872Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/hide_menu.svg b/frontend/resources/flowy_icons/16x/hide_menu.svg index ce88af8ea7..9e301210c4 100644 --- a/frontend/resources/flowy_icons/16x/hide_menu.svg +++ b/frontend/resources/flowy_icons/16x/hide_menu.svg @@ -1,6 +1,6 @@ -<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6 5L3 8L6 11" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> -<rect width="4" height="1" rx="0.5" transform="matrix(-1 0 0 1 13 5)" fill="#333333"/> -<rect width="6" height="1" rx="0.5" transform="matrix(-1 0 0 1 13 7.5)" fill="#333333"/> -<rect width="4" height="1" rx="0.5" transform="matrix(-1 0 0 1 13 10)" fill="#333333"/> +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<path d="M8.25 3.75L3 9L8.25 14.25" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.4375 3.75L9.1875 9L14.4375 14.25" stroke="#171717" stroke-width="1.2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +</g> </svg> diff --git a/frontend/resources/flowy_icons/16x/icon_shuffle.svg b/frontend/resources/flowy_icons/16x/icon_shuffle.svg new file mode 100644 index 0000000000..9953ebe30c --- /dev/null +++ b/frontend/resources/flowy_icons/16x/icon_shuffle.svg @@ -0,0 +1,7 @@ +<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M2.75 16.4824L5.08749 16.4916C5.92166 16.4916 6.70083 16.0791 7.15916 15.3916L13.0167 6.60992C13.475 5.92242 14.2542 5.50075 15.0883 5.50991L19.2592 5.52826" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M17.418 18.3158L19.2513 16.4824" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.14918 7.90182L7.15916 6.52682C6.69166 5.87599 5.93999 5.49099 5.14249 5.50016L2.75 5.50934" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.8867 14.0996L13.0051 15.5388C13.4726 16.1438 14.2059 16.5013 14.9759 16.5013L19.2567 16.4829" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M19.2513 5.51693L17.418 3.68359" stroke="#171717" stroke-width="1.375" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/more.svg b/frontend/resources/flowy_icons/16x/more.svg index b191e64a10..da54e4b6e6 100644 --- a/frontend/resources/flowy_icons/16x/more.svg +++ b/frontend/resources/flowy_icons/16x/more.svg @@ -1,3 +1,7 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M9.39568 7.6963L6.91032 5.56599C6.65085 5.34358 6.25 5.52795 6.25 5.86969L6.25 10.1303C6.25 10.4721 6.65085 10.6564 6.91032 10.434L9.39568 8.3037C9.58192 8.14406 9.58192 7.85594 9.39568 7.6963Z" fill="#333333"/> -</svg> + <g opacity="0.6"> + <path d="M4.23516 7.99941C4.23516 8.60693 3.74267 9.09941 3.13516 9.09941C2.52764 9.09941 2.03516 8.60693 2.03516 7.99941C2.03516 7.3919 2.52764 6.89941 3.13516 6.89941C3.74267 6.89941 4.23516 7.3919 4.23516 7.99941Z" fill="#171717"/> + <path d="M9.10234 7.99941C9.10234 8.60693 8.60986 9.09941 8.00234 9.09941C7.39483 9.09941 6.90234 8.60693 6.90234 7.99941C6.90234 7.3919 7.39483 6.89941 8.00234 6.89941C8.60986 6.89941 9.10234 7.3919 9.10234 7.99941Z" fill="#171717"/> + <path d="M13.9695 7.99941C13.9695 8.60693 13.477 9.09941 12.8695 9.09941C12.262 9.09941 11.7695 8.60693 11.7695 7.99941C11.7695 7.3919 12.262 6.89941 12.8695 6.89941C13.477 6.89941 13.9695 7.3919 13.9695 7.99941Z" fill="#171717"/> + </g> + </svg> diff --git a/frontend/resources/flowy_icons/16x/move_to.svg b/frontend/resources/flowy_icons/16x/move_to.svg new file mode 100644 index 0000000000..1c7d6144ee --- /dev/null +++ b/frontend/resources/flowy_icons/16x/move_to.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7.9987 14.6673C11.6806 14.6673 14.6654 11.6825 14.6654 8.00065C14.6654 4.31875 11.6806 1.33398 7.9987 1.33398C4.3168 1.33398 1.33203 4.31875 1.33203 8.00065C1.33203 11.6825 4.3168 14.6673 7.9987 14.6673Z" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.66797 8H9.66797" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.33203 10L10.332 8L8.33203 6" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/notification.svg b/frontend/resources/flowy_icons/16x/notification.svg new file mode 100644 index 0000000000..feb63cb9b3 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/notification.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.01242 1.93994C5.80575 1.93994 4.01242 3.73327 4.01242 5.93994V7.86661C4.01242 8.27328 3.83908 8.89327 3.63242 9.23994L2.86575 10.5133C2.39242 11.2999 2.71908 12.1733 3.58575 12.4666C6.45908 13.4266 9.55908 13.4266 12.4324 12.4666C13.2391 12.1999 13.5924 11.2466 13.1524 10.5133L12.3857 9.23994C12.1857 8.89327 12.0124 8.27328 12.0124 7.86661V5.93994C12.0124 3.73994 10.2124 1.93994 8.01242 1.93994Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round"/> +<path d="M9.24792 2.13346C9.04125 2.07346 8.82792 2.02679 8.60792 2.00012C7.96792 1.92012 7.35458 1.96679 6.78125 2.13346C6.97458 1.64012 7.45458 1.29346 8.01458 1.29346C8.57458 1.29346 9.05458 1.64012 9.24792 2.13346Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M10.0117 12.7065C10.0117 13.8065 9.11172 14.7065 8.01172 14.7065C7.46505 14.7065 6.95838 14.4799 6.59838 14.1199C6.23838 13.7599 6.01172 13.2532 6.01172 12.7065" stroke="#171717" stroke-miterlimit="10"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/search.svg b/frontend/resources/flowy_icons/16x/search.svg index 1efb2d475c..ed7e6de9d2 100644 --- a/frontend/resources/flowy_icons/16x/search.svg +++ b/frontend/resources/flowy_icons/16x/search.svg @@ -1,4 +1,6 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<circle cx="7.5" cy="7.5" r="5" stroke="#333333"/> -<path d="M12.6464 13.354C12.8416 13.5493 13.1582 13.5493 13.3535 13.3541C13.5488 13.1588 13.5488 12.8422 13.3536 12.647L12.6464 13.354ZM10.6464 11.3535L12.6464 13.354L13.3536 12.647L11.3536 10.6465L10.6464 11.3535Z" fill="#333333"/> +<g opacity="0.7"> +<path d="M7.66927 13.9999C11.1671 13.9999 14.0026 11.1644 14.0026 7.66659C14.0026 4.16878 11.1671 1.33325 7.66927 1.33325C4.17147 1.33325 1.33594 4.16878 1.33594 7.66659C1.33594 11.1644 4.17147 13.9999 7.66927 13.9999Z" stroke="#171717" stroke-width="1.03333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.6693 14.6666L13.3359 13.3333" stroke="#171717" stroke-width="1.03333" stroke-linecap="round" stroke-linejoin="round"/> +</g> </svg> diff --git a/frontend/resources/flowy_icons/16x/settings.svg b/frontend/resources/flowy_icons/16x/settings.svg index f9896aad52..bcc96b817b 100644 --- a/frontend/resources/flowy_icons/16x/settings.svg +++ b/frontend/resources/flowy_icons/16x/settings.svg @@ -1,4 +1,4 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.01471 2.14621C7.62441 1.7942 8.37559 1.7942 8.98529 2.14621L12.5769 4.21982C13.1866 4.57183 13.5622 5.22237 13.5622 5.92639V10.0736C13.5622 10.7776 13.1866 11.4282 12.5769 11.7802L8.98529 13.8538C8.37559 14.2058 7.62441 14.2058 7.01471 13.8538L3.42312 11.7802C2.81341 11.4282 2.43782 10.7776 2.43782 10.0736V5.92639C2.43782 5.22237 2.81341 4.57183 3.42312 4.21982L7.01471 2.14621Z" stroke="#333333"/> -<circle cx="8" cy="8" r="2.5" stroke="#333333"/> +<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M1.33203 8.58679V7.41345C1.33203 6.72012 1.8987 6.14679 2.5987 6.14679C3.80536 6.14679 4.2987 5.29345 3.69203 4.24679C3.34536 3.64679 3.55203 2.86679 4.1587 2.52012L5.31203 1.86012C5.8387 1.54679 6.5187 1.73345 6.83203 2.26012L6.90536 2.38679C7.50536 3.43345 8.49203 3.43345 9.0987 2.38679L9.17203 2.26012C9.48536 1.73345 10.1654 1.54679 10.692 1.86012L11.8454 2.52012C12.452 2.86679 12.6587 3.64679 12.312 4.24679C11.7054 5.29345 12.1987 6.14679 13.4054 6.14679C14.0987 6.14679 14.672 6.71345 14.672 7.41345V8.58679C14.672 9.28012 14.1054 9.85345 13.4054 9.85345C12.1987 9.85345 11.7054 10.7068 12.312 11.7535C12.6587 12.3601 12.452 13.1335 11.8454 13.4801L10.692 14.1401C10.1654 14.4535 9.48536 14.2668 9.17203 13.7401L9.0987 13.6135C8.4987 12.5668 7.51203 12.5668 6.90536 13.6135L6.83203 13.7401C6.5187 14.2668 5.8387 14.4535 5.31203 14.1401L4.1587 13.4801C3.55203 13.1335 3.34536 12.3535 3.69203 11.7535C4.2987 10.7068 3.80536 9.85345 2.5987 9.85345C1.8987 9.85345 1.33203 9.28012 1.33203 8.58679Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/sidebar_footer_trash.svg b/frontend/resources/flowy_icons/16x/sidebar_footer_trash.svg new file mode 100644 index 0000000000..f412bb86fd --- /dev/null +++ b/frontend/resources/flowy_icons/16x/sidebar_footer_trash.svg @@ -0,0 +1,9 @@ +<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.6"> +<path d="M16.5 4.48535C14.0025 4.23785 11.49 4.11035 8.985 4.11035C7.5 4.11035 6.015 4.18535 4.53 4.33535L3 4.48535" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.125 3.7275L7.29 2.745C7.41 2.0325 7.5 1.5 8.7675 1.5H10.7325C12 1.5 12.0975 2.0625 12.21 2.7525L12.375 3.7275" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.8844 6.85449L14.3969 14.407C14.3144 15.5845 14.2469 16.4995 12.1544 16.4995H7.33937C5.24687 16.4995 5.17938 15.5845 5.09688 14.407L4.60938 6.85449" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.49609 12.375H10.9936" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.875 9.375H11.625" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/sidebar_footer_widget.svg b/frontend/resources/flowy_icons/16x/sidebar_footer_widget.svg new file mode 100644 index 0000000000..2dbb6f52cf --- /dev/null +++ b/frontend/resources/flowy_icons/16x/sidebar_footer_widget.svg @@ -0,0 +1,15 @@ +<svg width="19" height="18" viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.6"> +<g clip-path="url(#clip0_611_31756)"> +<path d="M6.25 12H4.3225C2.605 12 1.75 11.145 1.75 9.4275V4.0725C1.75 2.355 2.605 1.5 4.3225 1.5H7.75C9.4675 1.5 10.3225 2.355 10.3225 4.0725" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.1758 16.5H10.7483C9.03078 16.5 8.17578 15.645 8.17578 13.9275V8.5725C8.17578 6.855 9.03078 6 10.7483 6H14.1758C15.8933 6 16.7483 6.855 16.7483 8.5725V13.9275C16.7483 15.645 15.8933 16.5 14.1758 16.5Z" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.4023 11.25H13.8473" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12.625 12.4723V10.0273" stroke="#171717" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/> +</g> +</g> +<defs> +<clipPath id="clip0_611_31756"> +<rect width="18" height="18" fill="white" transform="translate(0.25)"/> +</clipPath> +</defs> +</svg> diff --git a/frontend/resources/flowy_icons/16x/three-dots.svg b/frontend/resources/flowy_icons/16x/three-dots.svg index 4d37a346a0..07aa7ca706 100644 --- a/frontend/resources/flowy_icons/16x/three-dots.svg +++ b/frontend/resources/flowy_icons/16x/three-dots.svg @@ -1,3 +1,5 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.4 10.1001C3.35025 10.1001 2.5 10.9503 2.5 12.0001C2.5 13.0498 3.35025 13.9001 4.4 13.9001C5.44975 13.9001 6.3 13.0498 6.3 12.0001C6.3 10.9503 5.44975 10.1001 4.4 10.1001ZM12 10.1001C10.9502 10.1001 10.1 10.9503 10.1 12.0001C10.1 13.0498 10.9502 13.9001 12 13.9001C13.0498 13.9001 13.9 13.0498 13.9 12.0001C13.9 10.9503 13.0498 10.1001 12 10.1001ZM19.6 10.1001C18.5502 10.1001 17.7 10.9503 17.7 12.0001C17.7 13.0498 18.5502 13.9001 19.6 13.9001C20.6497 13.9001 21.5 13.0498 21.5 12.0001C21.5 10.9503 20.6497 10.1001 19.6 10.1001Z" fill="#C5C7CB"/> +<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M4.76406 8.99995C4.76406 9.6834 4.21001 10.2375 3.52656 10.2375C2.84311 10.2375 2.28906 9.6834 2.28906 8.99995C2.28906 8.3165 2.84311 7.76245 3.52656 7.76245C4.21001 7.76245 4.76406 8.3165 4.76406 8.99995Z" fill="#171717"/> +<path d="M10.2406 8.99995C10.2406 9.6834 9.68658 10.2375 9.00312 10.2375C8.31967 10.2375 7.76562 9.6834 7.76562 8.99995C7.76562 8.3165 8.31967 7.76245 9.00312 7.76245C9.68658 7.76245 10.2406 8.3165 10.2406 8.99995Z" fill="#171717"/> +<path d="M15.7133 8.99995C15.7133 9.6834 15.1592 10.2375 14.4758 10.2375C13.7923 10.2375 13.2383 9.6834 13.2383 8.99995C13.2383 8.3165 13.7923 7.76245 14.4758 7.76245C15.1592 7.76245 15.7133 8.3165 15.7133 8.99995Z" fill="#171717"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/title_bar_divider.svg b/frontend/resources/flowy_icons/16x/title_bar_divider.svg new file mode 100644 index 0000000000..5f92484836 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/title_bar_divider.svg @@ -0,0 +1,5 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<path d="M4.5 9.375L7.875 6L4.5 2.625" stroke="#171717" stroke-width="0.9" stroke-linecap="round" stroke-linejoin="round"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/trash.svg b/frontend/resources/flowy_icons/16x/trash.svg new file mode 100644 index 0000000000..487a57001f --- /dev/null +++ b/frontend/resources/flowy_icons/16x/trash.svg @@ -0,0 +1,7 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M14 3.98763C11.78 3.76763 9.54667 3.6543 7.32 3.6543C6 3.6543 4.68 3.72096 3.36 3.8543L2 3.98763" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.66797 3.31398L5.81464 2.44065C5.9213 1.80732 6.0013 1.33398 7.12797 1.33398H8.87464C10.0013 1.33398 10.088 1.83398 10.188 2.44732L10.3346 3.31398" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12.5669 6.09375L12.1336 12.8071C12.0603 13.8537 12.0003 14.6671 10.1403 14.6671H5.86026C4.00026 14.6671 3.94026 13.8537 3.86693 12.8071L3.43359 6.09375" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.88672 11H9.10672" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.33203 8.33398H9.66536" stroke="#FB006D" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/unfavorite.svg b/frontend/resources/flowy_icons/16x/unfavorite.svg index 0ccfc1edff..b984afe017 100644 --- a/frontend/resources/flowy_icons/16x/unfavorite.svg +++ b/frontend/resources/flowy_icons/16x/unfavorite.svg @@ -1,3 +1,5 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8 3L9.3905 5.96215L12.5 6.44006L10.25 8.74448L10.781 12L8 10.4621L5.219 12L5.75 8.74448L3.5 6.44006L6.6095 5.96215L8 3Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3.52189 12.4331L3.90189 10.7864C3.98856 10.3998 3.83523 9.85977 3.55523 9.57977L1.90189 7.92643C0.928559 6.9531 1.24189 5.96643 2.60189 5.73977L4.72856 5.38643C5.08189 5.32643 5.50856 5.0131 5.66856 4.68643L6.84189 2.33977C7.47523 1.06643 8.51523 1.06643 9.15523 2.33977L10.3286 4.68643C10.4019 4.83977 10.5419 4.98643 10.6952 5.1131" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.401 5.73975C14.761 5.96641 15.081 6.95308 14.101 7.92641L12.4477 9.57975C12.1677 9.85975 12.0143 10.3997 12.101 10.7864L12.5743 12.8331C12.9477 14.4531 12.0877 15.0797 10.6543 14.2331L8.66099 13.0531C8.30099 12.8397 7.70766 12.8397 7.34099 13.0531L5.34766 14.2331" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.6693 1.33301L1.33594 14.6663" stroke="#171717" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/frontend/resources/flowy_icons/16x/view_item_add.svg b/frontend/resources/flowy_icons/16x/view_item_add.svg new file mode 100644 index 0000000000..9373ef6dd1 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_add.svg @@ -0,0 +1,6 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.5"> +<rect x="2" y="7.40039" width="12" height="1.2" rx="0.6" fill="#171717"/> +<rect x="7.40234" y="14" width="12" height="1.2" rx="0.6" transform="rotate(-90 7.40234 14)" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/view_item_expand.svg b/frontend/resources/flowy_icons/16x/view_item_expand.svg new file mode 100644 index 0000000000..5fd5a1e719 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_expand.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.45"> +<path d="M7.83607 10.0988L5.51977 6.9139C5.30345 6.61646 5.51592 6.19922 5.8837 6.19922H10.5163C10.8841 6.19922 11.0966 6.61646 10.8802 6.9139L8.56393 10.0988C8.38422 10.3459 8.01578 10.3459 7.83607 10.0988Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/view_item_open_in_new_tab.svg b/frontend/resources/flowy_icons/16x/view_item_open_in_new_tab.svg new file mode 100644 index 0000000000..87f9f11949 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_open_in_new_tab.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.62891 7.36667L13.8222 2.17334" stroke="#171717" stroke-width="1.06667" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.3291 4.70699V1.66699H11.2891" stroke="#171717" stroke-width="1.06667" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.36406 1.66699H6.0974C2.93073 1.66699 1.66406 2.93366 1.66406 6.10033V9.90033C1.66406 13.067 2.93073 14.3337 6.0974 14.3337H9.8974C13.0641 14.3337 14.3307 13.067 14.3307 9.90033V8.63366" stroke="#171717" stroke-width="1.06667" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/view_item_rename.svg b/frontend/resources/flowy_icons/16x/view_item_rename.svg new file mode 100644 index 0000000000..c890184915 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_rename.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8.83958 2.40031L3.36624 8.19364C3.15958 8.41364 2.95958 8.84697 2.91958 9.14697L2.67291 11.307C2.58624 12.087 3.14624 12.6203 3.91958 12.487L6.06624 12.1203C6.36624 12.067 6.78624 11.847 6.99291 11.6203L12.4662 5.82697C13.4129 4.82697 13.8396 3.68697 12.3662 2.29364C10.8996 0.913641 9.78624 1.40031 8.83958 2.40031Z" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.92578 3.3667C8.21245 5.2067 9.70578 6.61337 11.5591 6.80003" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M2 14.6665H14" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/view_item_right_arrow.svg b/frontend/resources/flowy_icons/16x/view_item_right_arrow.svg new file mode 100644 index 0000000000..de5db7fd68 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_right_arrow.svg @@ -0,0 +1,10 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.4"> +<mask id="path-1-outside-1_517_47838" maskUnits="userSpaceOnUse" x="5.19922" y="2.20117" width="7" height="11" fill="black"> +<rect fill="white" x="5.19922" y="2.20117" width="7" height="11"/> +<path d="M6.03945 3.37691C5.80803 3.61122 5.80803 3.99112 6.03945 4.22544L9.76857 8.00117L6.03945 11.7769C5.80803 12.0112 5.80803 12.3911 6.03945 12.6254C6.27087 12.8598 6.64608 12.8598 6.8775 12.6254L11.0257 8.42544C11.1368 8.31291 11.1992 8.1603 11.1992 8.00117C11.1992 7.84204 11.1368 7.68943 11.0257 7.57691L6.8775 3.37691C6.64608 3.14259 6.27087 3.14259 6.03945 3.37691Z"/> +</mask> +<path d="M6.03945 3.37691C5.80803 3.61122 5.80803 3.99112 6.03945 4.22544L9.76857 8.00117L6.03945 11.7769C5.80803 12.0112 5.80803 12.3911 6.03945 12.6254C6.27087 12.8598 6.64608 12.8598 6.8775 12.6254L11.0257 8.42544C11.1368 8.31291 11.1992 8.1603 11.1992 8.00117C11.1992 7.84204 11.1368 7.68943 11.0257 7.57691L6.8775 3.37691C6.64608 3.14259 6.27087 3.14259 6.03945 3.37691Z" fill="#171717"/> +<path d="M6.03945 4.22544L6.13432 4.13174L6.13432 4.13174L6.03945 4.22544ZM6.03945 3.37691L6.13432 3.4706L6.13432 3.4706L6.03945 3.37691ZM9.76857 8.00117L9.86344 8.09487C9.91473 8.04293 9.91473 7.95941 9.86344 7.90748L9.76857 8.00117ZM6.03945 11.7769L6.13432 11.8706L6.13432 11.8706L6.03945 11.7769ZM6.03945 12.6254L6.13432 12.5317L6.13432 12.5317L6.03945 12.6254ZM6.8775 12.6254L6.78264 12.5317L6.78264 12.5317L6.8775 12.6254ZM11.0257 8.42544L11.1205 8.51913L11.1205 8.51913L11.0257 8.42544ZM11.0257 7.57691L10.9308 7.6706L10.9308 7.6706L11.0257 7.57691ZM6.8775 3.37691L6.97237 3.28321L6.97237 3.28321L6.8775 3.37691ZM6.13432 4.13174C5.95419 3.94936 5.95419 3.65298 6.13432 3.4706L5.94459 3.28321C5.66187 3.56946 5.66187 4.03288 5.94459 4.31913L6.13432 4.13174ZM9.86344 7.90748L6.13432 4.13174L5.94459 4.31913L9.67371 8.09487L9.86344 7.90748ZM6.13432 11.8706L9.86344 8.09487L9.67371 7.90748L5.94459 11.6832L6.13432 11.8706ZM6.13432 12.5317C5.95419 12.3494 5.95419 12.053 6.13432 11.8706L5.94459 11.6832C5.66187 11.9695 5.66187 12.4329 5.94459 12.7191L6.13432 12.5317ZM6.78264 12.5317C6.60342 12.7132 6.31354 12.7132 6.13432 12.5317L5.94459 12.7191C6.22821 13.0063 6.68875 13.0063 6.97237 12.7191L6.78264 12.5317ZM10.9308 8.33174L6.78264 12.5317L6.97237 12.7191L11.1205 8.51913L10.9308 8.33174ZM11.0659 8.00117C11.0659 8.12547 11.0171 8.24435 10.9308 8.33174L11.1205 8.51913C11.2565 8.38148 11.3326 8.19513 11.3326 8.00117H11.0659ZM10.9308 7.6706C11.0171 7.75799 11.0659 7.87687 11.0659 8.00117H11.3326C11.3326 7.80721 11.2565 7.62086 11.1205 7.48321L10.9308 7.6706ZM6.78264 3.4706L10.9308 7.6706L11.1205 7.48321L6.97237 3.28321L6.78264 3.4706ZM6.13432 3.4706C6.31354 3.28914 6.60342 3.28914 6.78264 3.4706L6.97237 3.28321C6.68875 2.99605 6.22821 2.99605 5.94459 3.28321L6.13432 3.4706Z" fill="#171717" mask="url(#path-1-outside-1_517_47838)"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/view_item_unexpand.svg b/frontend/resources/flowy_icons/16x/view_item_unexpand.svg new file mode 100644 index 0000000000..8971910251 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/view_item_unexpand.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.45"> +<path d="M10.3338 8.33716L7.06207 10.7166C6.78588 10.9174 6.39844 10.7202 6.39844 10.3786V5.61979C6.39844 5.27828 6.78588 5.08099 7.06207 5.28186L10.3338 7.66128C10.5632 7.82815 10.5632 8.17028 10.3338 8.33716Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_hide.svg b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_hide.svg new file mode 100644 index 0000000000..d264b4734e --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_hide.svg @@ -0,0 +1,8 @@ +<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> +<mask id="path-1-outside-1_598_58130" maskUnits="userSpaceOnUse" x="1.33203" y="3" width="8" height="5" fill="black"> +<rect fill="white" x="1.33203" y="3" width="8" height="5"/> +<path d="M8.20999 7.62796C8.04727 7.79068 7.78346 7.79068 7.62074 7.62796L4.9987 5.00592L2.37666 7.62796C2.21394 7.79068 1.95012 7.79068 1.7874 7.62796C1.62468 7.46524 1.62468 7.20142 1.7874 7.03871L4.70407 4.12204C4.78221 4.0439 4.88819 4 4.9987 4C5.1092 4 5.21519 4.0439 5.29333 4.12204L8.20999 7.03871C8.37271 7.20142 8.37271 7.46524 8.20999 7.62796Z"/> +</mask> +<path d="M8.20999 7.62796C8.04727 7.79068 7.78346 7.79068 7.62074 7.62796L4.9987 5.00592L2.37666 7.62796C2.21394 7.79068 1.95012 7.79068 1.7874 7.62796C1.62468 7.46524 1.62468 7.20142 1.7874 7.03871L4.70407 4.12204C4.78221 4.0439 4.88819 4 4.9987 4C5.1092 4 5.21519 4.0439 5.29333 4.12204L8.20999 7.03871C8.37271 7.20142 8.37271 7.46524 8.20999 7.62796Z" fill="#171717"/> +<path d="M7.62074 7.62796L7.67966 7.56904L7.62074 7.62796ZM8.20999 7.62796L8.15107 7.56904H8.15107L8.20999 7.62796ZM4.9987 5.00592L4.93977 4.947C4.9554 4.93137 4.9766 4.92259 4.9987 4.92259C5.0208 4.92259 5.042 4.93137 5.05762 4.947L4.9987 5.00592ZM2.37666 7.62796L2.31773 7.56904L2.37666 7.62796ZM1.7874 7.62796L1.84633 7.56904L1.7874 7.62796ZM1.7874 7.03871L1.84633 7.09763L1.7874 7.03871ZM4.70407 4.12204L4.64514 4.06311V4.06311L4.70407 4.12204ZM5.29333 4.12204L5.2344 4.18096L5.2344 4.18096L5.29333 4.12204ZM8.20999 7.03871L8.26892 6.97978L8.20999 7.03871ZM7.67966 7.56904C7.80984 7.69921 8.02089 7.69921 8.15107 7.56904L8.26892 7.68689C8.07366 7.88215 7.75707 7.88215 7.56181 7.68689L7.67966 7.56904ZM5.05762 4.947L7.67966 7.56904L7.56181 7.68689L4.93977 5.06485L5.05762 4.947ZM2.31773 7.56904L4.93977 4.947L5.05762 5.06485L2.43558 7.68689L2.31773 7.56904ZM1.84633 7.56904C1.9765 7.69921 2.18756 7.69921 2.31773 7.56904L2.43558 7.68689C2.24032 7.88215 1.92374 7.88215 1.72848 7.68689L1.84633 7.56904ZM1.84633 7.09763C1.71615 7.22781 1.71615 7.43886 1.84633 7.56904L1.72848 7.68689C1.53322 7.49163 1.53322 7.17504 1.72848 6.97978L1.84633 7.09763ZM4.763 4.18096L1.84633 7.09763L1.72848 6.97978L4.64514 4.06311L4.763 4.18096ZM4.9987 4.08333C4.91029 4.08333 4.82551 4.11845 4.763 4.18096L4.64514 4.06311C4.73891 3.96935 4.86609 3.91667 4.9987 3.91667V4.08333ZM5.2344 4.18096C5.17189 4.11845 5.0871 4.08333 4.9987 4.08333V3.91667C5.13131 3.91667 5.25848 3.96935 5.35225 4.06311L5.2344 4.18096ZM8.15107 7.09763L5.2344 4.18096L5.35225 4.06311L8.26892 6.97978L8.15107 7.09763ZM8.15107 7.56904C8.28124 7.43886 8.28124 7.22781 8.15107 7.09763L8.26892 6.97978C8.46418 7.17504 8.46418 7.49163 8.26892 7.68689L8.15107 7.56904Z" fill="#171717" fill-opacity="0.8" mask="url(#path-1-outside-1_598_58130)"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_show.svg b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_show.svg new file mode 100644 index 0000000000..4682de7aa7 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_drop_down_menu_show.svg @@ -0,0 +1,8 @@ +<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> +<mask id="path-1-outside-1_628_27017" maskUnits="userSpaceOnUse" x="0.667969" y="4" width="8" height="5" fill="black"> +<rect fill="white" x="0.667969" y="4" width="8" height="5"/> +<path d="M1.79001 4.37204C1.95273 4.20932 2.21654 4.20932 2.37926 4.37204L5.0013 6.99408L7.62334 4.37204C7.78606 4.20932 8.04988 4.20932 8.2126 4.37204C8.37532 4.53476 8.37532 4.79858 8.2126 4.96129L5.29593 7.87796C5.21779 7.9561 5.11181 8 5.0013 8C4.8908 8 4.78481 7.9561 4.70667 7.87796L1.79001 4.96129C1.62729 4.79858 1.62729 4.53476 1.79001 4.37204Z"/> +</mask> +<path d="M1.79001 4.37204C1.95273 4.20932 2.21654 4.20932 2.37926 4.37204L5.0013 6.99408L7.62334 4.37204C7.78606 4.20932 8.04988 4.20932 8.2126 4.37204C8.37532 4.53476 8.37532 4.79858 8.2126 4.96129L5.29593 7.87796C5.21779 7.9561 5.11181 8 5.0013 8C4.8908 8 4.78481 7.9561 4.70667 7.87796L1.79001 4.96129C1.62729 4.79858 1.62729 4.53476 1.79001 4.37204Z" fill="#171717"/> +<path d="M2.37926 4.37204L2.32034 4.43096L2.37926 4.37204ZM1.79001 4.37204L1.84893 4.43096H1.84893L1.79001 4.37204ZM5.0013 6.99408L5.06023 7.053C5.0446 7.06863 5.0234 7.07741 5.0013 7.07741C4.9792 7.07741 4.958 7.06863 4.94238 7.053L5.0013 6.99408ZM7.62334 4.37204L7.68227 4.43096L7.62334 4.37204ZM8.2126 4.37204L8.15367 4.43096L8.2126 4.37204ZM8.2126 4.96129L8.15367 4.90237L8.2126 4.96129ZM5.29593 7.87796L5.35486 7.93689V7.93689L5.29593 7.87796ZM4.70667 7.87796L4.7656 7.81904L4.7656 7.81904L4.70667 7.87796ZM1.79001 4.96129L1.73108 5.02022L1.79001 4.96129ZM2.32034 4.43096C2.19016 4.30079 1.97911 4.30079 1.84893 4.43096L1.73108 4.31311C1.92634 4.11785 2.24293 4.11785 2.43819 4.31311L2.32034 4.43096ZM4.94238 7.053L2.32034 4.43096L2.43819 4.31311L5.06023 6.93515L4.94238 7.053ZM7.68227 4.43096L5.06023 7.053L4.94238 6.93515L7.56442 4.31311L7.68227 4.43096ZM8.15367 4.43096C8.0235 4.30079 7.81244 4.30079 7.68227 4.43096L7.56442 4.31311C7.75968 4.11785 8.07626 4.11785 8.27152 4.31311L8.15367 4.43096ZM8.15367 4.90237C8.28385 4.77219 8.28385 4.56114 8.15367 4.43096L8.27152 4.31311C8.46678 4.50837 8.46678 4.82496 8.27152 5.02022L8.15367 4.90237ZM5.237 7.81904L8.15367 4.90237L8.27152 5.02022L5.35486 7.93689L5.237 7.81904ZM5.0013 7.91667C5.08971 7.91667 5.17449 7.88155 5.237 7.81904L5.35486 7.93689C5.26109 8.03065 5.13391 8.08333 5.0013 8.08333V7.91667ZM4.7656 7.81904C4.82811 7.88155 4.9129 7.91667 5.0013 7.91667V8.08333C4.86869 8.08333 4.74152 8.03065 4.64775 7.93689L4.7656 7.81904ZM1.84893 4.90237L4.7656 7.81904L4.64775 7.93689L1.73108 5.02022L1.84893 4.90237ZM1.84893 4.43096C1.71876 4.56114 1.71876 4.77219 1.84893 4.90237L1.73108 5.02022C1.53582 4.82496 1.53582 4.50837 1.73108 4.31311L1.84893 4.43096Z" fill="#171717" fill-opacity="0.8" mask="url(#path-1-outside-1_628_27017)"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/workspace_logout.svg b/frontend/resources/flowy_icons/16x/workspace_logout.svg new file mode 100644 index 0000000000..6e44da6b14 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_logout.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M12.1094 9.84729L14.001 7.95563L12.1094 6.06396" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.43359 7.95557H13.9485" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M7.91144 13.8229C4.64537 13.8229 2 11.6061 2 7.91144C2 4.21679 4.64537 2 7.91144 2" stroke="#171717" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/frontend/resources/flowy_icons/16x/workspace_selected.svg b/frontend/resources/flowy_icons/16x/workspace_selected.svg new file mode 100644 index 0000000000..73d86f8e02 --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_selected.svg @@ -0,0 +1,10 @@ +<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g id="Frame 1948756070" clip-path="url(#clip0_648_32496)"> +<path id="Vector" d="M0.699219 7.70904L4.5196 11.4628L13.4042 2.80029" stroke="#00C6F1" stroke-width="1.68" stroke-linecap="round" stroke-linejoin="round"/> +</g> +<defs> +<clipPath id="clip0_648_32496"> +<rect width="14" height="14" fill="white"/> +</clipPath> +</defs> +</svg> diff --git a/frontend/resources/flowy_icons/16x/workspace_three_dots.svg b/frontend/resources/flowy_icons/16x/workspace_three_dots.svg new file mode 100644 index 0000000000..93aa13693a --- /dev/null +++ b/frontend/resources/flowy_icons/16x/workspace_three_dots.svg @@ -0,0 +1,7 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<g opacity="0.6"> +<path d="M4.23516 7.9999C4.23516 8.60742 3.74267 9.0999 3.13516 9.0999C2.52764 9.0999 2.03516 8.60742 2.03516 7.9999C2.03516 7.39239 2.52764 6.8999 3.13516 6.8999C3.74267 6.8999 4.23516 7.39239 4.23516 7.9999Z" fill="#171717"/> +<path d="M9.10234 7.9999C9.10234 8.60742 8.60986 9.0999 8.00234 9.0999C7.39483 9.0999 6.90234 8.60742 6.90234 7.9999C6.90234 7.39239 7.39483 6.8999 8.00234 6.8999C8.60986 6.8999 9.10234 7.39239 9.10234 7.9999Z" fill="#171717"/> +<path d="M13.9695 7.9999C13.9695 8.60742 13.477 9.0999 12.8695 9.0999C12.262 9.0999 11.7695 8.60742 11.7695 7.9999C11.7695 7.39239 12.262 6.8999 12.8695 6.8999C13.477 6.8999 13.9695 7.39239 13.9695 7.9999Z" fill="#171717"/> +</g> +</svg> diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 3f9f4178a0..04ab72f795 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -70,6 +70,7 @@ "chooseWorkspace": "Choose your workspace", "create": "Create workspace", "reset": "Reset workspace", + "renameWorkspace": "Rename workspace", "resetWorkspacePrompt": "Resetting the workspace will delete all pages and data within it. Are you sure you want to reset the workspace? Alternatively, you can contact the support team to restore the workspace", "hint": "workspace", "notFoundError": "Workspace not found", @@ -136,7 +137,9 @@ "openNewTab": "Open in a new tab", "moveTo": "Move to", "addToFavorites": "Add to Favorites", - "copyLink": "Copy Link" + "copyLink": "Copy Link", + "changeIcon":"Change icon", + "collapseAllPages": "Collapse all pages" }, "blankPageTitle": "Blank page", "newPageText": "New page", @@ -239,7 +242,10 @@ "addAPage": "Add a page", "addAPageToPrivate": "Add a page to private space", "addAPageToWorkspace": "Add a page to workspace", - "recent": "Recent" + "recent": "Recent", + "today": "Today", + "thisWeek": "This week", + "others": "Others" }, "notifications": { "export": { @@ -294,7 +300,8 @@ "back": "Back", "signInGoogle": "Sign in with Google", "signInGithub": "Sign in with Github", - "signInDiscord": "Sign in with Discord" + "signInDiscord": "Sign in with Discord", + "more": "More" }, "label": { "welcome": "Welcome!", @@ -1584,7 +1591,9 @@ }, "favorite": { "noFavorite": "No favorite page", - "noFavoriteHintText": "Swipe the page to the left to add it to your favorites" + "noFavoriteHintText": "Swipe the page to the left to add it to your favorites", + "removeFromSidebar": "Remove from sidebar", + "addToSidebar": "Pin to sidebar" }, "cardDetails": { "notesPlaceholder": "Enter a / to insert a block, or start typing" @@ -1637,6 +1646,7 @@ "workplaceIconSubtitle": "Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications.", "renameError": "Failed to rename workplace", "updateIconError": "Failed to update icon", + "chooseAnIcon": "Choose an icon", "appearance": { "name": "Appearance", "themeMode": { diff --git a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs index eb311cfea7..0a2f34ca0a 100644 --- a/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs +++ b/frontend/rust-lib/event-integration-test/tests/folder/local_test/script.rs @@ -196,7 +196,7 @@ impl FolderTest { }, FolderScript::ReadFavorites => { let favorites = read_favorites(sdk).await; - self.favorites = favorites.to_vec(); + self.favorites = favorites.items.iter().map(|x| x.item.clone()).collect(); }, } } @@ -375,10 +375,10 @@ pub async fn toggle_favorites(sdk: &EventIntegrationTest, view_id: Vec<String>) .await; } -pub async fn read_favorites(sdk: &EventIntegrationTest) -> RepeatedViewPB { +pub async fn read_favorites(sdk: &EventIntegrationTest) -> RepeatedFavoriteViewPB { EventBuilder::new(sdk.clone()) .event(ReadFavorites) .async_send() .await - .parse::<RepeatedViewPB>() + .parse::<RepeatedFavoriteViewPB>() } diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 004f793e11..466d30d06d 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -152,6 +152,20 @@ pub struct RepeatedViewPB { pub items: Vec<ViewPB>, } +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct RepeatedFavoriteViewPB { + #[pb(index = 1)] + pub items: Vec<FavoriteViewPB>, +} + +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct FavoriteViewPB { + #[pb(index = 1)] + pub item: ViewPB, + #[pb(index = 2)] + pub timestamp: i64, +} + impl std::convert::From<Vec<ViewPB>> for RepeatedViewPB { fn from(items: Vec<ViewPB>) -> Self { RepeatedViewPB { items } diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index d6d36b683e..06daab5a2f 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -278,16 +278,19 @@ pub(crate) async fn duplicate_view_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn read_favorites_handler( folder: AFPluginState<Weak<FolderManager>>, -) -> DataResult<RepeatedViewPB, FlowyError> { +) -> DataResult<RepeatedFavoriteViewPB, FlowyError> { let folder = upgrade_folder(folder)?; let favorite_items = folder.get_all_favorites().await; let mut views = vec![]; for item in favorite_items { if let Ok(view) = folder.get_view_pb(&item.id).await { - views.push(view); + views.push(FavoriteViewPB { + item: view, + timestamp: item.timestamp, + }); } } - data_result_ok(RepeatedViewPB { items: views }) + data_result_ok(RepeatedFavoriteViewPB { items: views }) } #[tracing::instrument(level = "debug", skip(folder), err)] diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index 31034bd143..2901d19a63 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -146,7 +146,7 @@ pub enum FolderEvent { #[event(input = "MoveNestedViewPayloadPB")] MoveNestedView = 32, - #[event(output = "RepeatedViewPB")] + #[event(output = "RepeatedFavoriteViewPB")] ReadFavorites = 33, #[event(input = "RepeatedViewIdPB")] diff --git a/frontend/scripts/tool/update_collab_source.sh b/frontend/scripts/tool/update_collab_source.sh index 29892de1de..094e5caf14 100755 --- a/frontend/scripts/tool/update_collab_source.sh +++ b/frontend/scripts/tool/update_collab_source.sh @@ -17,7 +17,7 @@ switch_deps() { # Switch to local paths for crate in collab collab-folder collab-document collab-database collab-plugins collab-user collab-entity collab-sync-protocol collab-persistence; do sed -i '' \ - -e "s#${crate} = { git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\", rev = \"[a-f0-9]*\" }#${crate} = { path = \"$repo_path/$crate\" }#g" \ + -e "s#${crate} = { .*git = \"https://github.com/AppFlowy-IO/AppFlowy-Collab\".* }#${crate} = { path = \"$repo_path/$crate\" }#g" \ "$cargo_toml" done echo "Switched to local paths in $cargo_toml." @@ -38,4 +38,4 @@ fi # Switch dependencies in both Cargo.toml files switch_deps "$CARGO_TOML_1" "$REPO_RELATIVE_PATH_1" -switch_deps "$CARGO_TOML_2" "$REPO_RELATIVE_PATH_2" +switch_deps "$CARGO_TOML_2" "$REPO_RELATIVE_PATH_2" \ No newline at end of file From 8b6575d1ee21741a5d058c8591effec55b7e6e6d Mon Sep 17 00:00:00 2001 From: Mayur Mahajan <47064215+MayurSMahajan@users.noreply.github.com> Date: Mon, 27 May 2024 06:27:51 +0530 Subject: [PATCH 28/30] fix: accept multi-key combination for customizing shortcuts & removes duplicates (#5414) --- .../document/presentation/editor_page.dart | 29 ++++++------ .../shortcuts/settings_shortcuts_cubit.dart | 13 +++--- .../settings_customize_shortcuts_view.dart | 44 ++++++++++++++----- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index 2d125ba647..13d2ba6ab9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -56,7 +56,18 @@ final List<CommandShortcutEvent> commandShortcutEvents = [ customPasteCommand, customCutCommand, ...customTextAlignCommands, - ...standardCommandShortcutEvents, + + // remove standard shortcuts for copy, cut, paste, todo + ...standardCommandShortcutEvents + ..removeWhere( + (shortcut) => [ + copyCommand, + cutCommand, + pasteCommand, + toggleTodoListCommand, + ].contains(shortcut), + ), + emojiShortcutEvent, ]; @@ -90,7 +101,6 @@ class AppFlowyEditorPage extends StatefulWidget { final String Function(Node)? placeholderText; /// Used to provide an initial selection on Page-load - /// final Selection? initialSelection; final bool useViewInfoBloc; @@ -111,15 +121,8 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { ], ); - late final List<CommandShortcutEvent> commandShortcutEvents = [ - toggleToggleListCommand, - ...localizedCodeBlockCommands, - customCopyCommand, - customPasteCommand, - customCutCommand, - ...customTextAlignCommands, - ...standardCommandShortcutEvents, - emojiShortcutEvent, + late final List<CommandShortcutEvent> cmdShortcutEvents = [ + ...commandShortcutEvents, ..._buildFindAndReplaceCommands(), ]; @@ -309,7 +312,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { ), // customize the shortcuts characterShortcutEvents: characterShortcutEvents, - commandShortcutEvents: commandShortcutEvents, + commandShortcutEvents: cmdShortcutEvents, // customize the context menu items contextMenuItems: customContextMenuItems, // customize the header and footer. @@ -401,7 +404,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> { final customizeShortcuts = await settingsShortcutService.getCustomizeShortcuts(); await settingsShortcutService.updateCommandShortcuts( - commandShortcutEvents, + cmdShortcutEvents, customizeShortcuts, ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart index 790375fc20..0fdaa49128 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/shortcuts/settings_shortcuts_cubit.dart @@ -32,6 +32,7 @@ class ShortcutsCubit extends Cubit<ShortcutsState> { error: '', ), ); + try { final customizeShortcuts = await service.getCustomizeShortcuts(); await service.updateCommandShortcuts( @@ -40,7 +41,9 @@ class ShortcutsCubit extends Cubit<ShortcutsState> { ); //sort the shortcuts - commandShortcutEvents.sort((a, b) => a.key.compareTo(b.key)); + commandShortcutEvents.sort( + (a, b) => a.key.toLowerCase().compareTo(b.key.toLowerCase()), + ); emit( state.copyWith( @@ -104,11 +107,11 @@ class ShortcutsCubit extends Cubit<ShortcutsState> { } } - ///Checks if the new command is conflicting with other shortcut - ///We also check using the key, whether this command is a codeblock - ///shortcut, if so we only check a conflict with other codeblock shortcut. + /// Checks if the new command is conflicting with other shortcut + /// We also check using the key, whether this command is a codeblock + /// shortcut, if so we only check a conflict with other codeblock shortcut. String getConflict(CommandShortcutEvent currentShortcut, String command) { - //check if currentShortcut is a codeblock shortcut. + // check if currentShortcut is a codeblock shortcut. final isCodeBlockCommand = currentShortcut.isCodeBlockCommand; for (final e in state.commandShortcutEvents) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart index ed9b4dcd89..8066d9b65e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart @@ -84,7 +84,7 @@ class ShortcutsListView extends StatelessWidget { } } -class ShortcutsListTile extends StatelessWidget { +class ShortcutsListTile extends StatefulWidget { const ShortcutsListTile({ super.key, required this.shortcutEvent, @@ -92,6 +92,25 @@ class ShortcutsListTile extends StatelessWidget { final CommandShortcutEvent shortcutEvent; + @override + State<ShortcutsListTile> createState() => _ShortcutsListTileState(); +} + +class _ShortcutsListTileState extends State<ShortcutsListTile> { + late final TextEditingController controller; + + @override + void initState() { + controller = TextEditingController(text: widget.shortcutEvent.command); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Column( @@ -100,16 +119,16 @@ class ShortcutsListTile extends StatelessWidget { children: [ Expanded( child: FlowyText.medium( - key: Key(shortcutEvent.key), - shortcutEvent.description!.capitalize(), + key: Key(widget.shortcutEvent.key), + widget.shortcutEvent.description!.capitalize(), overflow: TextOverflow.ellipsis, ), ), FlowyTextButton( - shortcutEvent.command, + widget.shortcutEvent.command, fontColor: AFThemeExtension.of(context).textColor, fillColor: Colors.transparent, - onPressed: () => showKeyListenerDialog(context), + onPressed: () => showKeyListenerDialog(context, controller), ), ], ), @@ -120,8 +139,10 @@ class ShortcutsListTile extends StatelessWidget { ); } - void showKeyListenerDialog(BuildContext widgetContext) { - final controller = TextEditingController(text: shortcutEvent.command); + void showKeyListenerDialog( + BuildContext widgetContext, + TextEditingController controller, + ) { showDialog( context: widgetContext, builder: (builderContext) { @@ -131,9 +152,10 @@ class ShortcutsListTile extends StatelessWidget { content: KeyboardListener( focusNode: FocusNode(), onKeyEvent: (key) { + if (key is! KeyDownEvent) return; if (key.logicalKey == LogicalKeyboardKey.enter && !HardwareKeyboard.instance.isShiftPressed) { - if (controller.text == shortcutEvent.command) { + if (controller.text == widget.shortcutEvent.command) { _dismiss(builderContext); } if (formKey.currentState!.validate()) { @@ -166,12 +188,12 @@ class ShortcutsListTile extends StatelessWidget { ), ); }, - ).then((_) => controller.dispose()); + ); } String? _validateForConflicts(BuildContext context, String command) { final conflict = BlocProvider.of<ShortcutsCubit>(context).getConflict( - shortcutEvent, + widget.shortcutEvent, command, ); if (conflict.isEmpty) return null; @@ -182,7 +204,7 @@ class ShortcutsListTile extends StatelessWidget { } void _updateKey(BuildContext context, String command) { - shortcutEvent.updateCommand(command: command); + widget.shortcutEvent.updateCommand(command: command); BlocProvider.of<ShortcutsCubit>(context).updateAllShortcuts(); } From 083be32fa63bf7093d7df3dd205f1334e0682951 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Mon, 27 May 2024 15:36:45 +0800 Subject: [PATCH 29/30] chore: improve hover and text colors in dark mode (#5416) --- .../lib/workspace/presentation/home/home_stack.dart | 2 +- .../home/menu/sidebar/shared/sidebar_new_page_button.dart | 4 ++-- .../flowy_infra/lib/colorscheme/default_colorscheme.dart | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index b54a170e93..8fdfb9bb44 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -273,7 +273,7 @@ class HomeTopBar extends StatelessWidget { Widget build(BuildContext context) { return Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onSecondaryContainer, + color: Theme.of(context).colorScheme.surface, ), height: HomeSizes.topBarHeight + HomeInsets.topBarTitleVerticalPadding, child: Padding( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart index fa8d7785cf..f58a1d43c9 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart @@ -22,9 +22,9 @@ class SidebarNewPageButton extends StatelessWidget { height: HomeSizes.newPageSectionHeight, child: FlowyButton( onTap: () async => _createNewPage(context), - leftIcon: FlowySvg( + leftIcon: const FlowySvg( FlowySvgs.new_app_s, - color: Theme.of(context).colorScheme.primary, + blendMode: null, ), iconPadding: 10.0, text: SizedBox( diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart index 9f0cc50c93..b1c80fab64 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/default_colorscheme.dart @@ -13,12 +13,12 @@ const _lightShader5 = Color(0xFFe0e0e0); const _lightShader6 = Color(0xFFf2f2f2); const _lightMain1 = Color(0xFF00bcf0); const _lightTint9 = Color(0xFFe1fbFF); -const _darkShader1 = Color(0xFF131720); +const _darkShader1 = Color(0xE5FFFFFF); const _darkShader2 = Color(0xFF1A202C); const _darkShader3 = Color(0xFF363D49); const _darkShader5 = Color(0xFFBBC3CD); const _darkShader6 = Color(0xFFF2F2F2); -const _darkMain1 = Color(0xFF00BCF0); +const _darkMain1 = Color(0x19FFFFFF); const _darkInput = Color(0xFF282E3A); class DefaultColorScheme extends FlowyColorScheme { From cb44a885a17f2e1cc15a8b93526bec2f847a34cf Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" <lucas.xu@appflowy.io> Date: Mon, 27 May 2024 15:37:05 +0800 Subject: [PATCH 30/30] fix: dragging the Unsplash cover triggers an assertion error (#5404) --- .../page_style/_page_style_cover_image.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart index 84d8c751f4..fd0aa86fa7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/page_style/_page_style_cover_image.dart @@ -20,13 +20,13 @@ import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:flowy_infra/theme_extension.dart'; class PageStyleCoverImage extends StatelessWidget { PageStyleCoverImage({ @@ -257,6 +257,9 @@ class PageStyleCoverImage extends StatelessWidget { void _showUnsplash(BuildContext context) { final pageStyleBloc = context.read<DocumentPageStyleBloc>(); + final backgroundColor = AFThemeExtension.of(context).background; + final maxHeight = MediaQuery.of(context).size.height * 0.6; + context.pop(); showMobileBottomSheet( @@ -267,7 +270,7 @@ class PageStyleCoverImage extends StatelessWidget { showHeader: true, showRemoveButton: true, title: LocaleKeys.pageStyle_unsplash.tr(), - backgroundColor: AFThemeExtension.of(context).background, + backgroundColor: backgroundColor, onRemove: () { pageStyleBloc.add( DocumentPageStyleEvent.updateCoverImage( @@ -278,11 +281,11 @@ class PageStyleCoverImage extends StatelessWidget { builder: (_) { return ConstrainedBox( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.6, + maxHeight: maxHeight, minHeight: 80, ), child: BlocProvider.value( - value: context.read<DocumentPageStyleBloc>(), + value: pageStyleBloc, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: UnsplashImageWidget(