diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000000..48a347456b --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,51 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. + +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" + +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + +printMessage "Running the AppFlowy commit-msg hook." + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} + +.githooks/gitlint \ + --msg-file=$1 \ + --subject-regex="^(build|chore|ci|docs|feat|feature|fix|perf|refactor|revert|style|test)(.*)?:\s?.*" \ + --subject-maxlen=100 \ + --subject-minlen=10 \ + --body-regex=".*" \ + --body-maxlen=200 \ + --max-parents=1 + +if [ $? -ne 0 ] +then + printError "Please fix your commit message to match AppFlowy coding standards" + printError "https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/software-contributions/submitting-code/style-guides" + exit 1 +fi + diff --git a/.githooks/pre-commit b/.githooks/pre-commit old mode 100644 new mode 100755 index d7617246c9..be42c93834 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -1,5 +1,24 @@ #!/usr/bin/env bash +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" + +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + +printMessage "Running local AppFlowy pre-commit hook." + #flutter format . ##https://gist.github.com/benmccallum/28e4f216d9d72f5965133e6c43aaff6e limit=$(( 1 * 2**20 )) # 1MB @@ -31,4 +50,4 @@ for file in $( git diff-index --cached --name-only $against ); do file_too_large $filename $file_size exit 1; fi -done \ No newline at end of file +done diff --git a/.githooks/pre-push b/.githooks/pre-push old mode 100644 new mode 100755 index 1636fb8661..ee7c8cb79c --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,21 +1,36 @@ #!/usr/bin/env bash -if [[ `git status --porcelain` ]]; then - printf "\e[31;1m%s\e[0m\n" 'This script needs to run against committed code only. Please commit or stash you changes.' - exit 1 -fi -printf "\e[33;1m%s\e[0m\n" 'Running the Flutter analyzer' -flutter analyze -if [ $? -ne 0 ]; then - printf "\e[31;1m%s\e[0m\n" 'Flutter analyzer error' - exit 1 -fi -printf "\e[33;1m%s\e[0m\n" 'Finished running the Flutter analyzer' -printf "\e[33;1m%s\e[0m\n" 'Running unit tests' +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" -#flutter test +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + +printMessage "Running local AppFlowy pre-push hook." + +if [[ `git status --porcelain` ]]; then + printError "This script needs to run against committed code only. Please commit or stash you changes." + exit 1 +fi + +# +#printMessage "Running the Flutter analyzer" +#flutter analyze +# #if [ $? -ne 0 ]; then -# printf "\e[31;1m%s\e[0m\n" 'Unit tests error' +# printError "Flutter analyzer error" # exit 1 #fi -#printf "\e[33;1m%s\e[0m\n" 'Finished running unit tests' +# +#printMessage "Finished running the Flutter analyzer" diff --git a/.github/workflows/rust_lint.yml b/.github/workflows/rust_lint.yml index 4f364a616a..83ecd62280 100644 --- a/.github/workflows/rust_lint.yml +++ b/.github/workflows/rust_lint.yml @@ -17,9 +17,13 @@ jobs: steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 - with: - toolchain: 'stable-2022-01-20' - override: true + with: + toolchain: 'stable-2022-01-20' + override: true + - uses: subosito/flutter-action@v1 + with: + flutter-version: '3.0.0' + channel: "stable" - name: Rust Deps working-directory: frontend @@ -40,7 +44,7 @@ jobs: - run: rustup component add clippy - working-directory: frontend/rust-lib + working-directory: frontend/rust-lib - name: clippy run: cargo clippy --no-default-features working-directory: frontend/rust-lib diff --git a/.github/workflows/translation_notify.yml b/.github/workflows/translation_notify.yml index 8c12ccb0b8..00f1576b6f 100644 --- a/.github/workflows/translation_notify.yml +++ b/.github/workflows/translation_notify.yml @@ -4,15 +4,12 @@ on: branches: [ main ] paths: - "frontend/app_flowy/assets/translations/en.json" - pull_request: - branches: [ main ] - paths: - - "frontend/app_flowy/assets/translations/en.json" + jobs: Discord-Notify: runs-on: ubuntu-latest steps: - - uses: Ilshidur/action-discord@0.3.2 + - uses: Ilshidur/action-discord@master env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} with: diff --git a/.gitignore b/.gitignore index ab5f64486f..3d544c712e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,12 @@ frontend/.vscode/* !frontend/.vscode/tasks.json !frontend/.vscode/launch.json !frontend/.vscode/extensions.json -!frontend/.vscode/*.code-snippets \ No newline at end of file +!frontend/.vscode/*.code-snippets + +# Commit the highest level pubspec.lock, but ignore the others +pubspec.lock +!frontend/app_flowy/pubspec.lock + +# ignore tool used for commit linting +.githooks/gitlint +.githooks/gitlint.exe diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100755 index 7fed48507b..0000000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -npx --no -- commitlint --edit diff --git a/Makefile.toml b/Makefile.toml deleted file mode 100644 index 2e75c10277..0000000000 --- a/Makefile.toml +++ /dev/null @@ -1,35 +0,0 @@ -[tasks.install-commitlint.mac] -script = [ - """ - brew install npm - yarn install - yarn husky install - """, -] -script_runner = "@shell" - -[tasks.install-commitlint.windows] -script = [ - """ - echo "WIP" - """, -] -script_runner = "@duckscript" - -[tasks.install-commitlint.linux] -script = [ - """ - if command -v apt &> /dev/null - then - echo "Installing node.js and yarn (sudo apt install nodejs yarn)" - sudo apt install nodejs yarn - else - echo "Installing node.js and yarn (sudo pacman -S nodejs yarn)" - sudo pacman -S nodejs yarn - fi - - yarn install - yarn husky install - """, -] -script_runner = "@shell" diff --git a/README.md b/README.md index 9aa15e40ae..e441496c96 100644 --- a/README.md +++ b/README.md @@ -38,14 +38,15 @@ Please view the [documentation](https://appflowy.gitbook.io/docs/essential-docum ## Stay Up-to-Date -

AppFlowy Github

+

AppFlowy Github - how to star the repo

## Getting Started with development Please view the [documentation](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy) for OS specific development instructions ## Roadmap -[AppFlowy Roadmap](https://trello.com/b/NCyXCXXh/appflowy-roadmap) +- [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap) +- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) If you'd like to propose a feature, submit an issue [here](https://github.com/AppFlowy-IO/appflowy/issues). diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000000..53ac484771 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,3 @@ +## Our [roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) is where you can learn about the features we’re working on, their status, when we expect to release them, and how you can help us. + +## Find more information about how to use our official AppFlowy public roadmap on [Gitbook](https://appflowy.gitbook.io/docs/essential-documentation/roadmap). diff --git a/commitlint.config.js b/commitlint.config.js index c59a378e48..1e1af55752 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -22,3 +22,4 @@ module.exports = { 'footer-max-line-length': [2, 'always', 100] }, }; + diff --git a/doc/roadmap.md b/doc/roadmap.md index b5d7e368c9..54bef7f99b 100644 --- a/doc/roadmap.md +++ b/doc/roadmap.md @@ -1 +1,3 @@ -https://trello.com/b/NCyXCXXh/appflowy-roadmap +[AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap) + +[AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) diff --git a/frontend/Brewfile b/frontend/Brewfile deleted file mode 100644 index 470312025d..0000000000 --- a/frontend/Brewfile +++ /dev/null @@ -1,2 +0,0 @@ -brew 'sqlite3' -brew 'rustup-init' diff --git a/frontend/Makefile b/frontend/Makefile deleted file mode 100644 index c393bec0e0..0000000000 --- a/frontend/Makefile +++ /dev/null @@ -1,14 +0,0 @@ -.PHONY: flowy_dev_install flowy_clean - -flowy_dev_install: - brew bundle - rustup-init -y --default-toolchain=stable - cargo install --force cargo-make - cargo install --force duckscript_cli - cargo make flowy_dev - - -flowy_clean: - sh ./scripts/clean.sh - - diff --git a/frontend/app_flowy/.gitignore b/frontend/app_flowy/.gitignore index 505fd9136b..e43862d70e 100644 --- a/frontend/app_flowy/.gitignore +++ b/frontend/app_flowy/.gitignore @@ -64,4 +64,5 @@ windows/flutter/dart_ffi/ **/**/*.dll **/**/*.so **/**/Brewfile.lock.json -**/.sandbox \ No newline at end of file +**/.sandbox +**/.vscode/ \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index a6f9b4d3ae..2dbb970611 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -141,6 +141,7 @@ "menu": { "appearance": "Appearance", "language": "Language", + "user": "User", "open": "Open Settings" }, "appearance": { @@ -172,8 +173,8 @@ "includeTime": " Include time", "dateFormatFriendly": "Month Day,Year", "dateFormatISO": "Year-Month-Day", - "dateFormatLocal": "Month/Month/Day", - "dateFormatUS": "Month/Month/Day", + "dateFormatLocal": "Year/Month/Day", + "dateFormatUS": "Year/Month/Day", "timeFormat": " Time format", "invalidTimeFormat": "Invalid format", "timeFormatTwelveHour": "12 hour", @@ -214,4 +215,4 @@ "timeHintTextInTwentyFourHour": "12:00" } } -} +} \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/es-VE.json b/frontend/app_flowy/assets/translations/es-VE.json index 4aabe23674..d3740db8ec 100644 --- a/frontend/app_flowy/assets/translations/es-VE.json +++ b/frontend/app_flowy/assets/translations/es-VE.json @@ -96,6 +96,12 @@ "lightMode": "Cambiar a modo Claro", "darkMode": "Cambiar a modo Oscuro" }, + "notifications": { + "export": { + "markdown": "Nota exportada a Markdown", + "path": "Documentos/flowy" + } + }, "contactsPage": { "title": "Contactos", "whatsHappening": "¿Qué está pasando esta semana?", @@ -120,13 +126,13 @@ "oAuth": { "err": { "failedTitle": "Imposible conectarse con sus credenciales.", - "failedMsg": "Por favor asegurese haber completado el proceso de ingreso en su buscador." + "failedMsg": "Por favor asegurese haber completado el proceso de ingreso en su navegador." }, "google": { "title": "Ingresar con Google", - "instruction1": "Para importar sus contactos de Google, debe autorizar esta aplicación usando su buscador web.", + "instruction1": "Para importar sus contactos de Google, debe autorizar esta aplicación usando su navegador web.", "instruction2": "Copie este código al presionar el icono o al seleccionar el texto:", - "instruction3": "Navege al siguiente enlace en su buscador web, e ingrese el código anterior:", + "instruction3": "Navege al siguiente enlace en su navegador web, e ingrese el código anterior:", "instruction4": "Presione el botón de abajo cuando haya completado su registro:" } }, @@ -141,5 +147,71 @@ "lightLabel": "Modo Claro", "darkLabel": "Modo Oscuro" } + }, + "grid": { + "settings": { + "filter": "Filtrar", + "sortBy": "Ordenar por", + "Properties": "Propiedades" + }, + "field": { + "hide": "Ocultar", + "insertLeft": "Insertar a la Izquierda", + "insertRight": "Insertar a la Derecha", + "duplicate": "Duplicar", + "delete": "Eliminar", + "textFieldName": "Texto", + "checkboxFieldName": "Casilla de verificación", + "dateFieldName": "Fecha", + "numberFieldName": "Números", + "singleSelectFieldName": "Seleccionar", + "multiSelectFieldName": "Selección múltiple", + "urlFieldName": "URL", + "numberFormat": " Formato numérico", + "dateFormat": " Formato de fecha", + "includeTime": " Incluir tiempo", + "dateFormatFriendly": "Mes Día, Año", + "dateFormatISO": "Año-Mes-Día", + "dateFormatLocal": "Año/Mes/Día", + "dateFormatUS": "Año/Mes/Día", + "timeFormat": " Time format", + "invalidTimeFormat": "Formato de tiempo", + "timeFormatTwelveHour": "12 horas", + "timeFormatTwentyFourHour": "24 horas", + "addSelectOption": "Añadir una opción", + "optionTitle": "Opciones", + "addOption": "Añadir opción", + "editProperty": "Editar propiedad" + }, + "row": { + "duplicate": "Duplicar", + "delete": "Eliminar", + "textPlaceholder": "Vacío", + "copyProperty": "Propiedad copiada al portapapeles" + }, + "selectOption": { + "create": "Crear", + "purpleColor": "Morado", + "pinkColor": "Rosa", + "lightPinkColor": "Rosa Claro", + "orangeColor": "Naranja", + "yellowColor": "Amarillo", + "limeColor": "Lima", + "greenColor": "Verde", + "aquaColor": "Agua", + "blueColor": "Azul", + "deleteTag": "Borrar etiqueta", + "colorPannelTitle": "Colores", + "pannelTitle": "Selecciona una opción o crea una", + "searchOption": "Buscar una opción" + }, + "menuName": "Grid" + }, + "document": { + "menuName": "Doc", + "date": { + "timeHintTextInTwelveHour": "12:00 AM", + "timeHintTextInTwentyFourHour": "12:00" + } } } diff --git a/frontend/app_flowy/assets/translations/fr-FR.json b/frontend/app_flowy/assets/translations/fr-FR.json index c05c9aba27..ed1d56bce8 100644 --- a/frontend/app_flowy/assets/translations/fr-FR.json +++ b/frontend/app_flowy/assets/translations/fr-FR.json @@ -68,7 +68,7 @@ "help": "Aide et Support", "debug": { "name": "Informations de Débogage", - "success": "Informations de Débogage copiées dans le presse-papiers!", + "success": "Informations de Débogage copiées dans le presse-papiers !", "fail": "Impossible de copier les informations de Débogage dans le presse-papiers" } }, diff --git a/frontend/app_flowy/assets/translations/id-ID.json b/frontend/app_flowy/assets/translations/id-ID.json new file mode 100644 index 0000000000..cbe721c2b8 --- /dev/null +++ b/frontend/app_flowy/assets/translations/id-ID.json @@ -0,0 +1,218 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Saya", + "welcomeText": "Selamat datang di @:appName", + "githubStarText": "Bintangi GitHub", + "subscribeNewsletterText": "Berlangganan buletin", + "letsGoButtonText": "Ayo", + "title": "Judul", + "signUp": { + "buttonText": "Daftar", + "title": "Daftar ke @:appName", + "getStartedText": "Mulai", + "emptyPasswordError": "Sandi tidak boleh kosong", + "repeatPasswordEmptyError": "Sandi ulang tidak boleh kosong", + "unmatchedPasswordError": "Sandi ulang tidak sama dengan sandi", + "alreadyHaveAnAccount": "Sudah punya akun?", + "emailHint": "Email", + "passwordHint": "Sandi", + "repeatPasswordHint": "Sandi ulang" + }, + "signIn": { + "loginTitle": "Masuk ke @:appName", + "loginButtonText": "Masuk", + "buttonText": "Masuk", + "forgotPassword": "Lupa Sandi?", + "emailHint": "Email", + "passwordHint": "Sandi", + "dontHaveAnAccount": "Belum punya akun?", + "repeatPasswordEmptyError": "Sandi ulang tidak boleh kosong", + "unmatchedPasswordError": "Sandi ulang tidak sama dengan sandi" + }, + "workspace": { + "create": "Buat workspace", + "hint": "workspace", + "notFoundError": "Workspace tidak ditemukan" + }, + "shareAction": { + "buttonText": "Bagikan", + "workInProgress": "Segera", + "markdown": "Markdown", + "copyLink": "Salin tautan" + }, + "disclosureAction": { + "rename": "Ganti nama", + "delete": "Hapus", + "duplicate": "Duplikat" + }, + "blankPageTitle": "Halaman kosong", + "newPageText": "Halaman baru", + "trash": { + "text": "Sampah", + "restoreAll": "Pulihkan Semua", + "deleteAll": "Hapus semua", + "pageHeader": { + "fileName": "Nama file", + "lastModified": "Terakhir diubah", + "created": "Dibuat" + } + }, + "deletePagePrompt": { + "text": "Halaman ini di tempat sampah", + "restore": "Pulihkan halaman", + "deletePermanent": "Hapus secara permanen" + }, + "dialogCreatePageNameHint": "Nama halaman", + "questionBubble": { + "whatsNew": "Apa yang baru?", + "help": "Bantuan & Dukungan", + "debug": { + "name": "Info debug", + "success": "Info debug disalin ke papan klip!", + "fail": "Tidak dapat menyalin info debug ke papan klip" + } + }, + "menuAppHeader": { + "addPageTooltip": "Menambahkan halaman di dalam dengan cepat", + "defaultNewPageName": "Tanpa Judul", + "renameDialog": "Ganti nama" + }, + "toolbar": { + "undo": "Undo", + "redo": "Redo", + "bold": "Tebal", + "italic": "Miring", + "underline": "Garis bawah", + "strike": "Dicoret", + "numList": "Daftar bernomor", + "bulletList": "Daftar berpoin", + "checkList": "Daftar periksa", + "inlineCode": "Kode sebaris", + "quote": "Blok kutipan", + "header": "Tajuk", + "highlight": "Sorotan" + }, + "tooltip": { + "lightMode": "Ganti mode terang", + "darkMode": "Ganti mode gelap" + }, + "notifications": { + "export": { + "markdown": "Mengekspor Catatan ke Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "Kontak", + "whatsHappening": "Apa yang terjadi minggu ini?", + "addContact": "Tambahkan Kontak", + "editContact": "Ubah Kontak" + }, + "button": { + "OK": "Ya", + "Cancel": "Batal", + "signIn": "Masuk", + "signOut": "Keluar", + "complete": "Selesai", + "save": "Simpan" + }, + "label": { + "welcome": "Selamat datang!", + "firstName": "Nama Depan", + "middleName": "Nama Tengah", + "lastName": "Nama Akhir", + "stepX": "Langkah {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Tidak dapat terhubung ke akun anda", + "failedMsg": "Mohon pastikan anda menyelesaikan proses pendaftaran pada browser anda." + }, + "google": { + "title": "MASUK GOOGLE", + "instruction1": "Untuk mengimpor kontak Google Contacts anda, anda harus mengizinkan aplikasi ini menggunakan browser web anda.", + "instruction2": "Salin kode ini ke papan klip anda dengan cara mengklik ikon atau memilih teks:", + "instruction3": "Arahkan ke tautan berikut di browser web Anda, dan masukkan kode di atas:", + "instruction4": "Tekan tombol di bawah ini setelah Anda menyelesaikan pendaftaran:" + } + }, + "settings": { + "title": "Pengaturan", + "menu": { + "appearance": "Tampilan", + "language": "Bahasa", + "user": "Pengguna", + "open": "Buka Pengaturan" + }, + "appearance": { + "lightLabel": "Mode Terang", + "darkLabel": "Mode Gelap" + } + }, + "grid": { + "settings": { + "filter": "Filter", + "sortBy": "Sortir dengan", + "Properties": "Properti" + }, + "field": { + "hide": "Sembunyikan", + "insertLeft": "Sisipkan Kiri", + "insertRight": "Sisipkan Kanan", + "duplicate": "Duplikasi", + "delete": "Hapus", + "textFieldName": "Teks", + "checkboxFieldName": "Kotak Centang", + "dateFieldName": "Tanggal", + "numberFieldName": "Angka", + "singleSelectFieldName": "seleksi", + "multiSelectFieldName": "Multi seleksi", + "urlFieldName": "URL", + "numberFormat": " Format angka", + "dateFormat": " Format tanggal", + "includeTime": " Sertakan waktu", + "dateFormatFriendly": "Bulan Hari,Tahun", + "dateFormatISO": "Tahun-Bulan-Hari", + "dateFormatLocal": "Tahun/Bulan/Hari", + "dateFormatUS": "Tahun/Bulan/Hari", + "timeFormat": " Format waktu", + "invalidTimeFormat": "Format yang tidak valid", + "timeFormatTwelveHour": "12 jam", + "timeFormatTwentyFourHour": "24 jam", + "addSelectOption": "Tambahkan opsi", + "optionTitle": "Opsi", + "addOption": "Tambahkan opsi", + "editProperty": "Ubah properti" + }, + "row": { + "duplicate": "Duplikasi", + "delete": "Hapus", + "textPlaceholder": "Kosong", + "copyProperty": "Salin properti ke papan klip" + }, + "selectOption": { + "create": "Buat", + "purpleColor": "Ungu", + "pinkColor": "Merah Jambu", + "lightPinkColor": "Merah Jambu Muda", + "orangeColor": "Oranye", + "yellowColor": "Kuning", + "limeColor": "Limau", + "greenColor": "Hijau", + "aquaColor": "Air", + "blueColor": "Biru", + "deleteTag": "Hapus tag", + "colorPannelTitle": "Warna", + "pannelTitle": "Pilih opsi atau buat baru", + "searchOption": "Cari opsi" + }, + "menuName": "Grid" + }, + "document": { + "menuName": "Doc", + "date": { + "timeHintTextInTwelveHour": "12:00 AM", + "timeHintTextInTwentyFourHour": "12:00" + } + } +} \ No newline at end of file diff --git a/frontend/app_flowy/assets/translations/pl-PL.json b/frontend/app_flowy/assets/translations/pl-PL.json new file mode 100644 index 0000000000..0105e7aec7 --- /dev/null +++ b/frontend/app_flowy/assets/translations/pl-PL.json @@ -0,0 +1,145 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Ja", + "welcomeText": "Witaj w @:appName", + "githubStarText": "Gwiazdka na GitHub-ie", + "subscribeNewsletterText": "Zapisz się do naszego Newslettera", + "letsGoButtonText": "Start!", + "title": "Tytuł", + "signUp": { + "buttonText": "Zarejestruj", + "title": "Zarejestruj się w @:appName", + "getStartedText": "Zaczynamy", + "emptyPasswordError": "Hasło nie moze być puste", + "repeatPasswordEmptyError": "Powtórzone hasło nie moze być puste", + "unmatchedPasswordError": "Hasła nie są takie same", + "alreadyHaveAnAccount": "Masz juz konto?", + "emailHint": "Email", + "passwordHint": "Hasło", + "repeatPasswordHint": "Powtórz hasło" + }, + "signIn": { + "loginTitle": "Zaloguj do @:appName", + "loginButtonText": "Logowanie", + "buttonText": "Zaloguj", + "forgotPassword": "Zapomniałem hasła?", + "emailHint": "Email", + "passwordHint": "Password", + "dontHaveAnAccount": "Nie masz konta?", + "repeatPasswordEmptyError": "Powtórzone hasło nie moze być puste", + "unmatchedPasswordError": "Hasła nie są takie same" + }, + "workspace": { + "create": "Utwórz przestrzeń", + "hint": "przestrzeń robocza", + "notFoundError": "Przestrzeni nie znaleziono" + }, + "shareAction": { + "buttonText": "Udostępnij", + "workInProgress": "Wkrótce", + "markdown": "Markdown", + "copyLink": "Skopiuj link" + }, + "disclosureAction": { + "rename": "Zmień nazwę", + "delete": "Usuń", + "duplicate": "Duplikuj" + }, + "blankPageTitle": "Pusta strona", + "newPageText": "Nowa strona", + "trash": { + "text": "Kosz", + "restoreAll": "Przywróć Wszystko", + "deleteAll": "Usuń Wszystko", + "pageHeader": { + "fileName": "Nazwa Pliku", + "lastModified": "Ostatnio Zmodyfikowano", + "created": "Utworzono" + } + }, + "deletePagePrompt": { + "text": "Ta strona jest w Koszu", + "restore": "Przywróć strone", + "deletePermanent": "Usuń bezpowrotnie" + }, + "dialogCreatePageNameHint": "Nazwa Strony", + "questionBubble": { + "whatsNew": "What's new?", + "help": "Pomoc & Wsparcie", + "debug": { + "name": "Informacje Debugowania", + "success": "Skopiowano informacje debugowania do schowka!", + "fail": "Nie mozna skopiować informacji debugowania do schowka" + } + }, + "menuAppHeader": { + "addPageTooltip": "Szybko dodaj stronę do środka", + "defaultNewPageName": "Brak tytułu", + "renameDialog": "Zmień nazwę" + }, + "toolbar": { + "undo": "Cofnij", + "redo": "Powtórz", + "bold": "Pogrubiony", + "italic": "Kursywa", + "underline": "Podkreśl", + "strike": "Przekreśl", + "numList": "Lista Numerowana", + "bulletList": "Lista Punktowana", + "checkList": "Lista Kontrolna", + "inlineCode": "Kod Wbudowany", + "quote": "Blok cytat", + "header": "Nagłówek", + "highlight": "Podświetl" + }, + "tooltip": { + "lightMode": "Przełącz w Tryb Jasny", + "darkMode": "Przełącz w Tryb Ciemny" + }, + "contactsPage": { + "title": "Kontakty", + "whatsHappening": "Co się dzieje w tym tygodniu?", + "addContact": "Dodaj Kontakt", + "editContact": "Edytuj Kontakt" + }, + "button": { + "OK": "OK", + "Cancel": "Anuluj", + "signIn": "Zaloguj", + "signOut": "Wyloguj", + "complete": "Zakończono", + "save": "Zapisz" + }, + "label": { + "welcome": "Witaj!", + "firstName": "Imię Pierwsze", + "middleName": "Imię Drugie", + "lastName": "Nazwisko", + "stepX": "Krok {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Nie można połączyć się z Twoim kontem.", + "failedMsg": "Upewnij się, że zakończyłeś proces logowania w przeglądarce." + }, + "google": { + "title": "LOGOWANIE GOOGLE", + "instruction1": "Aby zaimportować Kontakty Google, musisz autoryzować tę aplikację za pomocą przeglądarki internetowej.", + "instruction2": "Skopiuj ten kod do schowka, klikając ikonę lub zaznaczając tekst:", + "instruction3": "Przejdź do następującego linku w przeglądarce internetowej i wprowadź powyższy kod:", + "instruction4": "Naciśnij poniższy przycisk po zakończeniu rejestracji:" + } + }, + "settings": { + "title": "Ustawienia", + "menu": { + "appearance": "Wygląd", + "language": "Język", + "open": "Otwórz Ustawienia" + }, + "appearance": { + "lightLabel": "Tryb Jasny", + "darkLabel": "Tryb Ciemny" + } + } +} diff --git a/frontend/app_flowy/assets/translations/zh-TW.json b/frontend/app_flowy/assets/translations/zh-TW.json new file mode 100644 index 0000000000..9f72b90512 --- /dev/null +++ b/frontend/app_flowy/assets/translations/zh-TW.json @@ -0,0 +1,218 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "我", + "welcomeText": "歡迎使用 @:appName", + "githubStarText": "在 GitHub 點星", + "subscribeNewsletterText": "訂閱電子報", + "letsGoButtonText": "出發吧", + "title": "標題", + "signUp": { + "buttonText": "註冊", + "title": "註冊 @:appName", + "getStartedText": "開始使用", + "emptyPasswordError": "密碼不能為空", + "repeatPasswordEmptyError": "確認密碼不能為空", + "unmatchedPasswordError": "確認密碼與密碼不符", + "alreadyHaveAnAccount": "已經有帳號了嗎?", + "emailHint": "電子郵件地址", + "passwordHint": "密碼", + "repeatPasswordHint": "確認密碼" + }, + "signIn": { + "loginTitle": "登入 @:appName", + "loginButtonText": "登入", + "buttonText": "登入", + "forgotPassword": "忘記密碼?", + "emailHint": "電子郵件地址", + "passwordHint": "密碼", + "dontHaveAnAccount": "沒有帳號?", + "repeatPasswordEmptyError": "確認密碼不能為空", + "unmatchedPasswordError": "確認密碼與密碼不符" + }, + "workspace": { + "create": "建立工作區", + "hint": "工作區", + "notFoundError": "找不到工作區" + }, + "shareAction": { + "buttonText": "分享", + "workInProgress": "即將推出", + "markdown": "Markdown", + "copyLink": "複製連結" + }, + "disclosureAction": { + "rename": "重新命名", + "delete": "刪除", + "duplicate": "複製" + }, + "blankPageTitle": "空白頁面", + "newPageText": "新頁面", + "trash": { + "text": "垃圾筒", + "restoreAll": "全部復原", + "deleteAll": "全部刪除", + "pageHeader": { + "fileName": "檔案名稱", + "lastModified": "最後修改時間", + "created": "建立時間" + } + }, + "deletePagePrompt": { + "text": "此頁面在垃圾筒中", + "restore": "復原頁面", + "deletePermanent": "永久刪除" + }, + "dialogCreatePageNameHint": "頁面名稱", + "questionBubble": { + "whatsNew": "新功能", + "help": "幫助 & 支援", + "debug": { + "name": "除錯資訊", + "success": "已將除錯資訊複製至剪貼簿!", + "fail": "無法將除錯資訊複製至剪貼簿" + } + }, + "menuAppHeader": { + "addPageTooltip": "快速新增頁面", + "defaultNewPageName": "未命名", + "renameDialog": "重新命名" + }, + "toolbar": { + "undo": "復原", + "redo": "取消復原", + "bold": "粗體", + "italic": "斜體", + "underline": "底線", + "strike": "刪除線", + "numList": "有序清單", + "bulletList": "無序清單", + "checkList": "核取清單", + "inlineCode": "程式碼", + "quote": "區塊引言", + "header": "標題", + "highlight": "反白" + }, + "tooltip": { + "lightMode": "切換至亮色模式", + "darkMode": "切換至暗色模式" + }, + "notifications": { + "export": { + "markdown": "已將筆記匯出成 Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "聯絡人", + "whatsHappening": "這周有甚麼新鮮事?", + "addContact": "新增聯絡人", + "editContact": "編輯聯絡人" + }, + "button": { + "OK": "OK", + "Cancel": "取消", + "signIn": "登入", + "signOut": "登出", + "complete": "完成", + "save": "儲存" + }, + "label": { + "welcome": "歡迎!", + "firstName": "名", + "middleName": "中間名", + "lastName": "姓", + "stepX": "步驟 {X}" + }, + "oAuth": { + "err": { + "failedTitle": "無法連接至您的帳號。", + "failedMsg": "請確認您已在瀏覽器中完成登入程序:" + }, + "google": { + "title": "GOOGLE 登入", + "instruction1": "若要匯入您的 Google 聯絡人,您必須透過瀏覽器授權此應用程式:", + "instruction2": "點擊圖示或選取文字以複製代碼:", + "instruction3": "前往下列網址,並輸入上述代碼:", + "instruction4": "完成註冊後,請點擊下方按鈕:" + } + }, + "settings": { + "title": "設定", + "menu": { + "appearance": "外觀", + "language": "語言", + "user": "使用者", + "open": "開啟設定" + }, + "appearance": { + "lightLabel": "亮色模式", + "darkLabel": "暗色模式" + } + }, + "grid": { + "settings": { + "filter": "篩選", + "sortBy": "排序方式", + "Properties": "內容" + }, + "field": { + "hide": "隱藏", + "insertLeft": "插入左方欄", + "insertRight": "插入右方欄", + "duplicate": "複製", + "delete": "刪除", + "textFieldName": "文字", + "checkboxFieldName": "核取方塊", + "dateFieldName": "日期", + "numberFieldName": "數字", + "singleSelectFieldName": "單選", + "multiSelectFieldName": "多選", + "urlFieldName": "網址", + "numberFormat": " 數字格式", + "dateFormat": " 日期格式", + "includeTime": " 包含時間", + "dateFormatFriendly": "月 日,年", + "dateFormatISO": "年-月-日", + "dateFormatLocal": "年/月/日", + "dateFormatUS": "年/月/日", + "timeFormat": " 時間格式", + "invalidTimeFormat": "格式無效", + "timeFormatTwelveHour": "12 小時", + "timeFormatTwentyFourHour": "24 小時", + "addSelectOption": "新增選項", + "optionTitle": "選項", + "addOption": "新增選項", + "editProperty": "編輯內容" + }, + "row": { + "duplicate": "複製", + "delete": "刪除", + "textPlaceholder": "空", + "copyProperty": "已將內容複製至剪貼簿" + }, + "selectOption": { + "create": "建立", + "purpleColor": "紫色", + "pinkColor": "粉色", + "lightPinkColor": "淡粉色", + "orangeColor": "橘色", + "yellowColor": "黃色", + "limeColor": "萊姆色", + "greenColor": "綠色", + "aquaColor": "水藍色", + "blueColor": "藍色", + "deleteTag": "刪除標籤", + "colorPannelTitle": "顏色", + "pannelTitle": "搜尋或建立選項", + "searchOption": "搜尋選項" + }, + "menuName": "網格" + }, + "document": { + "menuName": "檔案", + "date": { + "timeHintTextInTwelveHour": "12:00 AM", + "timeHintTextInTwentyFourHour": "12:00" + } + } +} \ No newline at end of file diff --git a/frontend/app_flowy/lib/core/grid_notification.dart b/frontend/app_flowy/lib/core/grid_notification.dart index e45bf5efe2..9a2429a417 100644 --- a/frontend/app_flowy/lib/core/grid_notification.dart +++ b/frontend/app_flowy/lib/core/grid_notification.dart @@ -8,7 +8,7 @@ import 'package:flowy_sdk/rust_stream.dart'; import 'notification_helper.dart'; -// Grid +// GridPB typedef GridNotificationCallback = void Function(GridNotification, Either); class GridNotificationParser extends NotificationParser { diff --git a/frontend/app_flowy/lib/startup/deps_resolver.dart b/frontend/app_flowy/lib/startup/deps_resolver.dart index 0cf248e7e4..e0ab40eea7 100644 --- a/frontend/app_flowy/lib/startup/deps_resolver.dart +++ b/frontend/app_flowy/lib/startup/deps_resolver.dart @@ -5,10 +5,12 @@ import 'package:app_flowy/workspace/application/app/prelude.dart'; import 'package:app_flowy/workspace/application/doc/prelude.dart'; import 'package:app_flowy/workspace/application/grid/prelude.dart'; import 'package:app_flowy/workspace/application/trash/prelude.dart'; +import 'package:app_flowy/workspace/application/user/prelude.dart'; import 'package:app_flowy/workspace/application/workspace/prelude.dart'; import 'package:app_flowy/workspace/application/edit_pannel/edit_pannel_bloc.dart'; import 'package:app_flowy/workspace/application/view/prelude.dart'; import 'package:app_flowy/workspace/application/menu/prelude.dart'; +import 'package:app_flowy/workspace/application/settings/prelude.dart'; import 'package:app_flowy/user/application/prelude.dart'; import 'package:app_flowy/user/presentation/router.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; @@ -51,14 +53,14 @@ void _resolveHomeDeps(GetIt getIt) { getIt.registerSingleton(MenuSharedState()); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (user, _) => UserListener(userProfile: user), ); // getIt.registerLazySingleton(() => HomeStackManager()); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (user, _) => WelcomeBloc( userService: UserService(userId: user.id), userWorkspaceListener: UserWorkspaceListener(userProfile: user), @@ -67,21 +69,21 @@ void _resolveHomeDeps(GetIt getIt) { // share getIt.registerLazySingleton(() => ShareService()); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (view, _) => DocShareBloc(view: view, service: getIt())); } void _resolveFolderDeps(GetIt getIt) { //workspace - getIt.registerFactoryParam( + getIt.registerFactoryParam( (user, workspaceId) => WorkspaceListener(user: user, workspaceId: workspaceId)); - // View - getIt.registerFactoryParam( + // ViewPB + getIt.registerFactoryParam( (view, _) => ViewListener(view: view), ); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (view, _) => ViewBloc( view: view, service: ViewService(), @@ -90,19 +92,29 @@ void _resolveFolderDeps(GetIt getIt) { ); //Menu - getIt.registerFactoryParam( + getIt.registerFactoryParam( (user, workspaceId) => MenuBloc( workspaceId: workspaceId, listener: getIt(param1: user, param2: workspaceId), ), ); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (user, _) => MenuUserBloc(user), ); - // App - getIt.registerFactoryParam( + //Settings + getIt.registerFactoryParam( + (user, _) => SettingsDialogBloc(user), + ); + + //User + getIt.registerFactoryParam( + (user, _) => SettingsUserViewBloc(user), + ); + + // AppPB + getIt.registerFactoryParam( (app, _) => AppBloc( app: app, appService: AppService(appId: app.id), @@ -123,7 +135,7 @@ void _resolveFolderDeps(GetIt getIt) { void _resolveDocDeps(GetIt getIt) { // Doc - getIt.registerFactoryParam( + getIt.registerFactoryParam( (view, _) => DocumentBloc( view: view, service: DocumentService(), @@ -134,8 +146,8 @@ void _resolveDocDeps(GetIt getIt) { } void _resolveGridDeps(GetIt getIt) { - // Grid - getIt.registerFactoryParam( + // GridPB + getIt.registerFactoryParam( (view, _) => GridBloc(view: view), ); @@ -153,31 +165,31 @@ void _resolveGridDeps(GetIt getIt) { ), ); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (context, _) => TextCellBloc( cellContext: context, ), ); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (context, _) => SelectOptionCellBloc( cellContext: context, ), ); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (context, _) => NumberCellBloc( cellContext: context, ), ); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (context, _) => DateCellBloc( cellContext: context, ), ); - getIt.registerFactoryParam( + getIt.registerFactoryParam( (cellData, _) => CheckboxCellBloc( service: CellService(), cellContext: cellData, diff --git a/frontend/app_flowy/lib/startup/tasks/app_widget.dart b/frontend/app_flowy/lib/startup/tasks/app_widget.dart index c8addc9d6f..6dae04a1c0 100644 --- a/frontend/app_flowy/lib/startup/tasks/app_widget.dart +++ b/frontend/app_flowy/lib/startup/tasks/app_widget.dart @@ -36,8 +36,10 @@ class InitAppWidgetTask extends LaunchTask { Locale('fr', 'FR'), Locale('fr', 'CA'), Locale('hu', 'HU'), + Locale('id', 'ID'), Locale('it', 'IT'), Locale('ja', 'JP'), + Locale('pl', 'PL'), Locale('pt', 'BR'), Locale('ru', 'RU'), Locale('tr', 'TR'), diff --git a/frontend/app_flowy/lib/user/application/auth_service.dart b/frontend/app_flowy/lib/user/application/auth_service.dart index 28f7dc5d0e..c2ce625ccf 100644 --- a/frontend/app_flowy/lib/user/application/auth_service.dart +++ b/frontend/app_flowy/lib/user/application/auth_service.dart @@ -1,21 +1,21 @@ import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show SignInPayload, SignUpPayload, UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show SignInPayloadPB, SignUpPayloadPB, UserProfilePB; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; class AuthService { - Future> signIn({required String? email, required String? password}) { + Future> signIn({required String? email, required String? password}) { // - final request = SignInPayload.create() + final request = SignInPayloadPB.create() ..email = email ?? '' ..password = password ?? ''; return UserEventSignIn(request).send(); } - Future> signUp( + Future> signUp( {required String? name, required String? password, required String? email}) { - final request = SignUpPayload.create() + final request = SignUpPayloadPB.create() ..email = email ?? '' ..name = name ?? '' ..password = password ?? ''; diff --git a/frontend/app_flowy/lib/user/application/sign_in_bloc.dart b/frontend/app_flowy/lib/user/application/sign_in_bloc.dart index 45b6ee1eb5..1b5838d9c4 100644 --- a/frontend/app_flowy/lib/user/application/sign_in_bloc.dart +++ b/frontend/app_flowy/lib/user/application/sign_in_bloc.dart @@ -2,7 +2,7 @@ import 'package:app_flowy/user/application/auth_service.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -69,7 +69,7 @@ class SignInState with _$SignInState { required bool isSubmitting, required Option passwordError, required Option emailError, - required Option> successOrFail, + required Option> successOrFail, }) = _SignInState; factory SignInState.initial() => SignInState( diff --git a/frontend/app_flowy/lib/user/application/sign_up_bloc.dart b/frontend/app_flowy/lib/user/application/sign_up_bloc.dart index b94a02bf09..0c103fd4d7 100644 --- a/frontend/app_flowy/lib/user/application/sign_up_bloc.dart +++ b/frontend/app_flowy/lib/user/application/sign_up_bloc.dart @@ -2,7 +2,7 @@ import 'package:app_flowy/user/application/auth_service.dart'; import 'package:dartz/dartz.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -120,7 +120,7 @@ class SignUpState with _$SignUpState { required Option passwordError, required Option repeatPasswordError, required Option emailError, - required Option> successOrFail, + required Option> successOrFail, }) = _SignUpState; factory SignUpState.initial() => SignUpState( diff --git a/frontend/app_flowy/lib/user/application/user_listener.dart b/frontend/app_flowy/lib/user/application/user_listener.dart index 89fc393e2d..5483926e71 100644 --- a/frontend/app_flowy/lib/user/application/user_listener.dart +++ b/frontend/app_flowy/lib/user/application/user_listener.dart @@ -13,7 +13,7 @@ import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-user/dart_notification.pb.dart' as user; import 'package:flowy_sdk/rust_stream.dart'; -typedef UserProfileNotifyValue = Either; +typedef UserProfileNotifyValue = Either; typedef AuthNotifyValue = Either; class UserListener { @@ -22,9 +22,9 @@ class UserListener { PublishNotifier? _profileNotifier = PublishNotifier(); UserNotificationParser? _userParser; - final UserProfile _userProfile; + final UserProfilePB _userProfile; UserListener({ - required UserProfile userProfile, + required UserProfilePB userProfile, }) : _userProfile = userProfile; void start({ @@ -65,7 +65,7 @@ class UserListener { break; case user.UserNotification.UserProfileUpdated: result.fold( - (payload) => _profileNotifier?.value = left(UserProfile.fromBuffer(payload)), + (payload) => _profileNotifier?.value = left(UserProfilePB.fromBuffer(payload)), (error) => _profileNotifier?.value = right(error), ); break; @@ -75,8 +75,8 @@ class UserListener { } } -typedef WorkspaceListNotifyValue = Either, FlowyError>; -typedef WorkspaceSettingNotifyValue = Either; +typedef WorkspaceListNotifyValue = Either, FlowyError>; +typedef WorkspaceSettingNotifyValue = Either; class UserWorkspaceListener { PublishNotifier? _authNotifier = PublishNotifier(); @@ -84,10 +84,10 @@ class UserWorkspaceListener { PublishNotifier? _settingChangedNotifier = PublishNotifier(); FolderNotificationListener? _listener; - final UserProfile _userProfile; + final UserProfilePB _userProfile; UserWorkspaceListener({ - required UserProfile userProfile, + required UserProfilePB userProfile, }) : _userProfile = userProfile; void start({ @@ -119,13 +119,13 @@ class UserWorkspaceListener { case FolderNotification.UserDeleteWorkspace: case FolderNotification.WorkspaceListUpdated: result.fold( - (payload) => _workspacesChangedNotifier?.value = left(RepeatedWorkspace.fromBuffer(payload).items), + (payload) => _workspacesChangedNotifier?.value = left(RepeatedWorkspacePB.fromBuffer(payload).items), (error) => _workspacesChangedNotifier?.value = right(error), ); break; case FolderNotification.WorkspaceSetting: result.fold( - (payload) => _settingChangedNotifier?.value = left(CurrentWorkspaceSetting.fromBuffer(payload)), + (payload) => _settingChangedNotifier?.value = left(CurrentWorkspaceSettingPB.fromBuffer(payload)), (error) => _settingChangedNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/user/application/user_service.dart b/frontend/app_flowy/lib/user/application/user_service.dart index 8417a069f9..48bea6aa41 100644 --- a/frontend/app_flowy/lib/user/application/user_service.dart +++ b/frontend/app_flowy/lib/user/application/user_service.dart @@ -11,7 +11,7 @@ class UserService { UserService({ required this.userId, }); - Future> getUserProfile({required String userId}) { + Future> getUserProfile({required String userId}) { return UserEventGetUserProfile().send(); } @@ -20,7 +20,7 @@ class UserService { String? password, String? email, }) { - var payload = UpdateUserProfilePayload.create()..id = userId; + var payload = UpdateUserProfilePayloadPB.create()..id = userId; if (name != null) { payload.name = name; @@ -49,8 +49,8 @@ class UserService { return UserEventInitUser().send(); } - Future, FlowyError>> getWorkspaces() { - final request = WorkspaceId.create(); + Future, FlowyError>> getWorkspaces() { + final request = WorkspaceIdPB.create(); return FolderEventReadWorkspaces(request).send().then((result) { return result.fold( @@ -60,8 +60,8 @@ class UserService { }); } - Future> openWorkspace(String workspaceId) { - final request = WorkspaceId.create()..value = workspaceId; + Future> openWorkspace(String workspaceId) { + final request = WorkspaceIdPB.create()..value = workspaceId; return FolderEventOpenWorkspace(request).send().then((result) { return result.fold( (workspace) => left(workspace), @@ -70,8 +70,8 @@ class UserService { }); } - Future> createWorkspace(String name, String desc) { - final request = CreateWorkspacePayload.create() + Future> createWorkspace(String name, String desc) { + final request = CreateWorkspacePayloadPB.create() ..name = name ..desc = desc; return FolderEventCreateWorkspace(request).send().then((result) { diff --git a/frontend/app_flowy/lib/user/application/user_settings_service.dart b/frontend/app_flowy/lib/user/application/user_settings_service.dart index eb93ab150d..28309d202c 100644 --- a/frontend/app_flowy/lib/user/application/user_settings_service.dart +++ b/frontend/app_flowy/lib/user/application/user_settings_service.dart @@ -5,11 +5,11 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-user/user_setting.pb.dart'; class UserSettingsService { - Future getAppearanceSettings() async { + Future getAppearanceSettings() async { final result = await UserEventGetAppearanceSetting().send(); return result.fold( - (AppearanceSettings setting) { + (AppearanceSettingsPB setting) { return setting; }, (error) { @@ -18,7 +18,7 @@ class UserSettingsService { ); } - Future> setAppearanceSettings(AppearanceSettings settings) { + Future> setAppearanceSettings(AppearanceSettingsPB settings) { return UserEventSetAppearanceSetting(settings).send(); } } diff --git a/frontend/app_flowy/lib/user/domain/auth_state.dart b/frontend/app_flowy/lib/user/domain/auth_state.dart index e2d3a33b09..ae0c259573 100644 --- a/frontend/app_flowy/lib/user/domain/auth_state.dart +++ b/frontend/app_flowy/lib/user/domain/auth_state.dart @@ -1,11 +1,11 @@ -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'auth_state.freezed.dart'; @freezed class AuthState with _$AuthState { - const factory AuthState.authenticated(UserProfile userProfile) = Authenticated; + const factory AuthState.authenticated(UserProfilePB userProfile) = Authenticated; const factory AuthState.unauthenticated(FlowyError error) = Unauthenticated; const factory AuthState.initial() = _Initial; } diff --git a/frontend/app_flowy/lib/user/presentation/router.dart b/frontend/app_flowy/lib/user/presentation/router.dart index 3bcfa11502..2928154ebe 100644 --- a/frontend/app_flowy/lib/user/presentation/router.dart +++ b/frontend/app_flowy/lib/user/presentation/router.dart @@ -7,7 +7,7 @@ import 'package:app_flowy/user/presentation/welcome_screen.dart'; import 'package:app_flowy/workspace/presentation/home/home_screen.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_infra_ui/widget/route/animation.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart'; import 'package:flutter/material.dart'; @@ -16,7 +16,7 @@ class AuthRouter { // TODO: implement showForgetPasswordScreen } - void pushWelcomeScreen(BuildContext context, UserProfile userProfile) { + void pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) { getIt().pushWelcomeScreen(context, userProfile); } @@ -28,7 +28,7 @@ class AuthRouter { ); } - void pushHomeScreen(BuildContext context, UserProfile profile, CurrentWorkspaceSetting workspaceSetting) { + void pushHomeScreen(BuildContext context, UserProfilePB profile, CurrentWorkspaceSettingPB workspaceSetting) { Navigator.push( context, PageRoutes.fade(() => HomeScreen(profile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001), @@ -37,7 +37,7 @@ class AuthRouter { } class SplashRoute { - Future pushWelcomeScreen(BuildContext context, UserProfile userProfile) async { + Future pushWelcomeScreen(BuildContext context, UserProfilePB userProfile) async { final screen = WelcomeScreen(userProfile: userProfile); final workspaceId = await Navigator.of(context).push( PageRoutes.fade( @@ -49,7 +49,7 @@ class SplashRoute { pushHomeScreen(context, userProfile, workspaceId); } - void pushHomeScreen(BuildContext context, UserProfile userProfile, CurrentWorkspaceSetting workspaceSetting) { + void pushHomeScreen(BuildContext context, UserProfilePB userProfile, CurrentWorkspaceSettingPB workspaceSetting) { Navigator.push( context, PageRoutes.fade(() => HomeScreen(userProfile, workspaceSetting), RouteDurations.slow.inMilliseconds * .001), diff --git a/frontend/app_flowy/lib/user/presentation/sign_in_screen.dart b/frontend/app_flowy/lib/user/presentation/sign_in_screen.dart index baa8ef0ccb..ee3600e782 100644 --- a/frontend/app_flowy/lib/user/presentation/sign_in_screen.dart +++ b/frontend/app_flowy/lib/user/presentation/sign_in_screen.dart @@ -10,7 +10,7 @@ import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:dartz/dartz.dart'; @@ -39,7 +39,7 @@ class SignInScreen extends StatelessWidget { ); } - void _handleSuccessOrFail(Either result, BuildContext context) { + void _handleSuccessOrFail(Either result, BuildContext context) { result.fold( (user) => router.pushWelcomeScreen(context, user), (error) => showSnapBar(context, error.msg), diff --git a/frontend/app_flowy/lib/user/presentation/sign_up_screen.dart b/frontend/app_flowy/lib/user/presentation/sign_up_screen.dart index 6a0b8f1d85..d0cb7f8b90 100644 --- a/frontend/app_flowy/lib/user/presentation/sign_up_screen.dart +++ b/frontend/app_flowy/lib/user/presentation/sign_up_screen.dart @@ -8,7 +8,7 @@ import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_infra_ui/style_widget/snap_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -36,7 +36,7 @@ class SignUpScreen extends StatelessWidget { ); } - void _handleSuccessOrFail(BuildContext context, Either result) { + void _handleSuccessOrFail(BuildContext context, Either result) { result.fold( (user) => router.pushWelcomeScreen(context, user), (error) => showSnapBar(context, error.msg), diff --git a/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart b/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart index c8fb9ba06b..6e3ae5ea52 100644 --- a/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart +++ b/frontend/app_flowy/lib/user/presentation/skip_log_in_screen.dart @@ -116,8 +116,8 @@ class _SkipLogInScreenState extends State { void _openCurrentWorkspace( BuildContext context, - UserProfile user, - dartz.Either workspacesOrError, + UserProfilePB user, + dartz.Either workspacesOrError, ) { workspacesOrError.fold( (workspaceSetting) { diff --git a/frontend/app_flowy/lib/user/presentation/welcome_screen.dart b/frontend/app_flowy/lib/user/presentation/welcome_screen.dart index 60ec3bf8a2..31b06d8bd1 100644 --- a/frontend/app_flowy/lib/user/presentation/welcome_screen.dart +++ b/frontend/app_flowy/lib/user/presentation/welcome_screen.dart @@ -12,7 +12,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; class WelcomeScreen extends StatelessWidget { - final UserProfile userProfile; + final UserProfilePB userProfile; const WelcomeScreen({ Key? key, required this.userProfile, @@ -65,7 +65,7 @@ class WelcomeScreen extends StatelessWidget { ); } - Widget _renderList(List workspaces) { + Widget _renderList(List workspaces) { return Expanded( child: StyledListView( itemBuilder: (BuildContext context, int index) { @@ -80,7 +80,7 @@ class WelcomeScreen extends StatelessWidget { ); } - void _handleOnPress(BuildContext context, Workspace workspace) { + void _handleOnPress(BuildContext context, WorkspacePB workspace) { context.read().add(WelcomeEvent.openWorkspace(workspace)); Navigator.of(context).pop(workspace.id); @@ -88,8 +88,8 @@ class WelcomeScreen extends StatelessWidget { } class WorkspaceItem extends StatelessWidget { - final Workspace workspace; - final void Function(Workspace workspace) onPressed; + final WorkspacePB workspace; + final void Function(WorkspacePB workspace) onPressed; const WorkspaceItem({Key? key, required this.workspace, required this.onPressed}) : super(key: key); @override diff --git a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart index 18f93abd6f..e8d2335168 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_bloc.dart @@ -18,7 +18,7 @@ import 'package:dartz/dartz.dart'; part 'app_bloc.freezed.dart'; class AppBloc extends Bloc { - final App app; + final AppPB app; final AppService appService; final AppListener appListener; @@ -103,7 +103,7 @@ class AppBloc extends Bloc { return super.close(); } - Future _didReceiveViewUpdated(List views, Emitter emit) async { + Future _didReceiveViewUpdated(List views, Emitter emit) async { final latestCreatedView = state.latestCreatedView; AppState newState = state.copyWith(views: views); if (latestCreatedView != null) { @@ -139,20 +139,20 @@ class AppEvent with _$AppEvent { ) = CreateView; const factory AppEvent.delete() = Delete; const factory AppEvent.rename(String newName) = Rename; - const factory AppEvent.didReceiveViewUpdated(List views) = ReceiveViews; - const factory AppEvent.appDidUpdate(App app) = AppDidUpdate; + const factory AppEvent.didReceiveViewUpdated(List views) = ReceiveViews; + const factory AppEvent.appDidUpdate(AppPB app) = AppDidUpdate; } @freezed class AppState with _$AppState { const factory AppState({ - required App app, - required List views, - View? latestCreatedView, + required AppPB app, + required List views, + ViewPB? latestCreatedView, required Either successOrFailure, }) = _AppState; - factory AppState.initial(App app) => AppState( + factory AppState.initial(AppPB app) => AppState( app: app, views: [], successOrFailure: left(unit), @@ -161,8 +161,8 @@ class AppState with _$AppState { class AppViewDataContext extends ChangeNotifier { final String appId; - final ValueNotifier> _viewsNotifier = ValueNotifier([]); - final ValueNotifier _selectedViewNotifier = ValueNotifier(null); + final ValueNotifier> _viewsNotifier = ValueNotifier([]); + final ValueNotifier _selectedViewNotifier = ValueNotifier(null); VoidCallback? _menuSharedStateListener; ExpandableController expandController = ExpandableController(initialExpanded: false); @@ -173,7 +173,7 @@ class AppViewDataContext extends ChangeNotifier { }); } - VoidCallback addSelectedViewChangeListener(void Function(View?) callback) { + VoidCallback addSelectedViewChangeListener(void Function(ViewPB?) callback) { listener() { callback(_selectedViewNotifier.value); } @@ -186,7 +186,7 @@ class AppViewDataContext extends ChangeNotifier { _selectedViewNotifier.removeListener(listener); } - void _setLatestView(View? view) { + void _setLatestView(ViewPB? view) { view?.freeze(); if (_selectedViewNotifier.value != view) { @@ -196,9 +196,9 @@ class AppViewDataContext extends ChangeNotifier { } } - View? get selectedView => _selectedViewNotifier.value; + ViewPB? get selectedView => _selectedViewNotifier.value; - set views(List views) { + set views(List views) { if (_viewsNotifier.value != views) { _viewsNotifier.value = views; _expandIfNeed(); @@ -206,9 +206,9 @@ class AppViewDataContext extends ChangeNotifier { } } - UnmodifiableListView get views => UnmodifiableListView(_viewsNotifier.value); + UnmodifiableListView get views => UnmodifiableListView(_viewsNotifier.value); - VoidCallback addViewsChangeListener(void Function(UnmodifiableListView) callback) { + VoidCallback addViewsChangeListener(void Function(UnmodifiableListView) callback) { listener() { callback(views); } diff --git a/frontend/app_flowy/lib/workspace/application/app/app_listener.dart b/frontend/app_flowy/lib/workspace/application/app/app_listener.dart index 6a30b270af..6edf1a4df2 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_listener.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_listener.dart @@ -10,8 +10,8 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart'; import 'package:flowy_sdk/rust_stream.dart'; -typedef AppDidUpdateCallback = void Function(App app); -typedef ViewsDidChangeCallback = void Function(Either, FlowyError> viewsOrFailed); +typedef AppDidUpdateCallback = void Function(AppPB app); +typedef ViewsDidChangeCallback = void Function(Either, FlowyError> viewsOrFailed); class AppListener { StreamSubscription? _subscription; @@ -37,7 +37,7 @@ class AppListener { if (_viewsChanged != null) { result.fold( (payload) { - final repeatedView = RepeatedView.fromBuffer(payload); + final repeatedView = RepeatedViewPB.fromBuffer(payload); _viewsChanged!(left(repeatedView.items)); }, (error) => _viewsChanged!(right(error)), @@ -48,7 +48,7 @@ class AppListener { if (_updated != null) { result.fold( (payload) { - final app = App.fromBuffer(payload); + final app = AppPB.fromBuffer(payload); _updated!(app); }, (error) => Log.error(error), diff --git a/frontend/app_flowy/lib/workspace/application/app/app_service.dart b/frontend/app_flowy/lib/workspace/application/app/app_service.dart index 51513a4032..cc75751e49 100644 --- a/frontend/app_flowy/lib/workspace/application/app/app_service.dart +++ b/frontend/app_flowy/lib/workspace/application/app/app_service.dart @@ -14,20 +14,20 @@ class AppService { required this.appId, }); - Future> getAppDesc({required String appId}) { - final payload = AppId.create()..value = appId; + Future> getAppDesc({required String appId}) { + final payload = AppIdPB.create()..value = appId; return FolderEventReadApp(payload).send(); } - Future> createView({ + Future> createView({ required String appId, required String name, required String desc, required PluginDataType dataType, required PluginType pluginType, }) { - final payload = CreateViewPayload.create() + final payload = CreateViewPayloadPB.create() ..belongToId = appId ..name = name ..desc = desc @@ -37,8 +37,8 @@ class AppService { return FolderEventCreateView(payload).send(); } - Future, FlowyError>> getViews({required String appId}) { - final payload = AppId.create()..value = appId; + Future, FlowyError>> getViews({required String appId}) { + final payload = AppIdPB.create()..value = appId; return FolderEventReadApp(payload).send().then((result) { return result.fold( @@ -49,12 +49,12 @@ class AppService { } Future> delete({required String appId}) { - final request = AppId.create()..value = appId; + final request = AppIdPB.create()..value = appId; return FolderEventDeleteApp(request).send(); } Future> updateApp({required String appId, String? name}) { - UpdateAppPayload payload = UpdateAppPayload.create()..appId = appId; + UpdateAppPayloadPB payload = UpdateAppPayloadPB.create()..appId = appId; if (name != null) { payload.name = name; @@ -67,7 +67,7 @@ class AppService { required int fromIndex, required int toIndex, }) { - final payload = MoveFolderItemPayload.create() + final payload = MoveFolderItemPayloadPB.create() ..itemId = viewId ..from = fromIndex ..to = toIndex diff --git a/frontend/app_flowy/lib/workspace/application/appearance.dart b/frontend/app_flowy/lib/workspace/application/appearance.dart index f3ff7801ce..0e47fa2be6 100644 --- a/frontend/app_flowy/lib/workspace/application/appearance.dart +++ b/frontend/app_flowy/lib/workspace/application/appearance.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:easy_localization/easy_localization.dart'; class AppearanceSettingModel extends ChangeNotifier with EquatableMixin { - AppearanceSettings setting; + AppearanceSettingsPB setting; AppTheme _theme; Locale _locale; Timer? _saveOperation; diff --git a/frontend/app_flowy/lib/workspace/application/doc/doc_bloc.dart b/frontend/app_flowy/lib/workspace/application/doc/doc_bloc.dart index 1c77fa1cbd..8be4c40d85 100644 --- a/frontend/app_flowy/lib/workspace/application/doc/doc_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/doc/doc_bloc.dart @@ -17,7 +17,7 @@ part 'doc_bloc.freezed.dart'; typedef FlutterQuillDocument = Document; class DocumentBloc extends Bloc { - final View view; + final ViewPB view; final DocumentService service; final ViewListener listener; diff --git a/frontend/app_flowy/lib/workspace/application/doc/doc_service.dart b/frontend/app_flowy/lib/workspace/application/doc/doc_service.dart index 992dfdeb63..659a99e371 100644 --- a/frontend/app_flowy/lib/workspace/application/doc/doc_service.dart +++ b/frontend/app_flowy/lib/workspace/application/doc/doc_service.dart @@ -6,24 +6,24 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-sync/text_block.pb.dart'; class DocumentService { - Future> openDocument({ + Future> openDocument({ required String docId, }) async { - await FolderEventSetLatestView(ViewId(value: docId)).send(); + await FolderEventSetLatestView(ViewIdPB(value: docId)).send(); - final payload = TextBlockId(value: docId); + final payload = TextBlockIdPB(value: docId); return TextBlockEventGetBlockData(payload).send(); } - Future> composeDelta({required String docId, required String data}) { - final payload = TextBlockDelta.create() + Future> composeDelta({required String docId, required String data}) { + final payload = TextBlockDeltaPB.create() ..blockId = docId ..deltaStr = data; return TextBlockEventApplyDelta(payload).send(); } Future> closeDocument({required String docId}) { - final request = ViewId(value: docId); + final request = ViewIdPB(value: docId); return FolderEventCloseView(request).send(); } } diff --git a/frontend/app_flowy/lib/workspace/application/doc/share_bloc.dart b/frontend/app_flowy/lib/workspace/application/doc/share_bloc.dart index 90c041eb75..ba3c7676c6 100644 --- a/frontend/app_flowy/lib/workspace/application/doc/share_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/doc/share_bloc.dart @@ -13,7 +13,7 @@ part 'share_bloc.freezed.dart'; class DocShareBloc extends Bloc { ShareService service; - View view; + ViewPB view; DocShareBloc({required this.view, required this.service}) : super(const DocShareState.initial()) { on((event, emit) async { await event.map( @@ -33,7 +33,7 @@ class DocShareBloc extends Bloc { }); } - ExportData _convertDeltaToMarkdown(ExportData value) { + ExportDataPB _convertDeltaToMarkdown(ExportDataPB value) { final result = deltaToMarkdown(value.data); value.data = result; writeFile(result); @@ -73,5 +73,5 @@ class DocShareEvent with _$DocShareEvent { class DocShareState with _$DocShareState { const factory DocShareState.initial() = _Initial; const factory DocShareState.loading() = _Loading; - const factory DocShareState.finish(Either successOrFail) = _Finish; + const factory DocShareState.finish(Either successOrFail) = _Finish; } diff --git a/frontend/app_flowy/lib/workspace/application/doc/share_service.dart b/frontend/app_flowy/lib/workspace/application/doc/share_service.dart index 7e5545f109..db6ad406b7 100644 --- a/frontend/app_flowy/lib/workspace/application/doc/share_service.dart +++ b/frontend/app_flowy/lib/workspace/application/doc/share_service.dart @@ -5,23 +5,23 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-text-block/protobuf.dart'; class ShareService { - Future> export(String docId, ExportType type) { - final request = ExportPayload.create() + Future> export(String docId, ExportType type) { + final request = ExportPayloadPB.create() ..viewId = docId ..exportType = type; return TextBlockEventExportDocument(request).send(); } - Future> exportText(String docId) { + Future> exportText(String docId) { return export(docId, ExportType.Text); } - Future> exportMarkdown(String docId) { + Future> exportMarkdown(String docId) { return export(docId, ExportType.Markdown); } - Future> exportURL(String docId) { + Future> exportURL(String docId) { return export(docId, ExportType.Link); } } diff --git a/frontend/app_flowy/lib/workspace/application/grid/block/block_service.dart b/frontend/app_flowy/lib/workspace/application/grid/block/block_cache.dart similarity index 73% rename from frontend/app_flowy/lib/workspace/application/grid/block/block_service.dart rename to frontend/app_flowy/lib/workspace/application/grid/block/block_cache.dart index 3fb734db70..3c99c0e0e1 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/block/block_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/block/block_cache.dart @@ -6,24 +6,25 @@ import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'block_listener.dart'; -class GridBlockCacheService { +/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information +class GridBlockCache { final String gridId; - final GridBlock block; - late GridRowCacheService _rowCache; + final GridBlockPB block; + late GridRowCache _rowCache; late GridBlockListener _listener; - List get rows => _rowCache.rows; - GridRowCacheService get rowCache => _rowCache; + List get rows => _rowCache.rows; + GridRowCache get rowCache => _rowCache; - GridBlockCacheService({ + GridBlockCache({ required this.gridId, required this.block, required GridFieldCache fieldCache, }) { - _rowCache = GridRowCacheService( + _rowCache = GridRowCache( gridId: gridId, block: block, - delegate: GridRowCacheDelegateImpl(fieldCache), + notifier: GridRowCacheFieldNotifierImpl(fieldCache), ); _listener = GridBlockListener(blockId: block.id); diff --git a/frontend/app_flowy/lib/workspace/application/grid/block/block_listener.dart b/frontend/app_flowy/lib/workspace/application/grid/block/block_listener.dart index 7a2bbe0cb3..91f93c61fe 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/block/block_listener.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/block/block_listener.dart @@ -7,7 +7,7 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart'; -typedef GridBlockUpdateNotifierValue = Either, FlowyError>; +typedef GridBlockUpdateNotifierValue = Either, FlowyError>; class GridBlockListener { final String blockId; @@ -33,7 +33,7 @@ class GridBlockListener { switch (ty) { case GridNotification.DidUpdateGridBlock: result.fold( - (payload) => _rowsUpdateNotifier?.value = left([GridBlockChangeset.fromBuffer(payload)]), + (payload) => _rowsUpdateNotifier?.value = left([GridBlockChangesetPB.fromBuffer(payload)]), (error) => _rowsUpdateNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cache.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cache.dart deleted file mode 100644 index ccf47fddb3..0000000000 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cache.dart +++ /dev/null @@ -1,109 +0,0 @@ -part of 'cell_service.dart'; - -typedef GridCellMap = LinkedHashMap; - -class _GridCellCacheObject { - _GridCellCacheKey key; - dynamic object; - _GridCellCacheObject({ - required this.key, - required this.object, - }); -} - -class _GridCellCacheKey { - final String fieldId; - final String rowId; - _GridCellCacheKey({ - required this.fieldId, - required this.rowId, - }); -} - -abstract class GridCellCacheDelegate { - void onFieldUpdated(void Function(Field) callback); -} - -class GridCellCacheService { - final String gridId; - final GridCellCacheDelegate delegate; - - /// fieldId: {objectId: callback} - final Map>> _fieldListenerByFieldId = {}; - - /// fieldId: {cacheKey: cacheData} - final Map> _cellDataByFieldId = {}; - GridCellCacheService({ - required this.gridId, - required this.delegate, - }) { - delegate.onFieldUpdated((field) { - _cellDataByFieldId.remove(field.id); - final map = _fieldListenerByFieldId[field.id]; - if (map != null) { - for (final callbacks in map.values) { - for (final callback in callbacks) { - callback(); - } - } - } - }); - } - - void addFieldListener(_GridCellCacheKey cacheKey, VoidCallback onFieldChanged) { - var map = _fieldListenerByFieldId[cacheKey.fieldId]; - if (map == null) { - _fieldListenerByFieldId[cacheKey.fieldId] = {}; - map = _fieldListenerByFieldId[cacheKey.fieldId]; - map![cacheKey.rowId] = [onFieldChanged]; - } else { - var objects = map[cacheKey.rowId]; - if (objects == null) { - map[cacheKey.rowId] = [onFieldChanged]; - } else { - objects.add(onFieldChanged); - } - } - } - - void removeFieldListener(_GridCellCacheKey cacheKey, VoidCallback fn) { - var callbacks = _fieldListenerByFieldId[cacheKey.fieldId]?[cacheKey.rowId]; - final index = callbacks?.indexWhere((callback) => callback == fn); - if (index != null && index != -1) { - callbacks?.removeAt(index); - } - } - - void insert(T item) { - var map = _cellDataByFieldId[item.key.fieldId]; - if (map == null) { - _cellDataByFieldId[item.key.fieldId] = {}; - map = _cellDataByFieldId[item.key.fieldId]; - } - - map![item.key.rowId] = item.object; - } - - T? get(_GridCellCacheKey key) { - final map = _cellDataByFieldId[key.fieldId]; - if (map == null) { - return null; - } else { - final object = map[key.rowId]; - if (object is T) { - return object; - } else { - if (object != null) { - Log.error("Cache data type does not match the cache data type"); - } - - return null; - } - } - } - - Future dispose() async { - _fieldListenerByFieldId.clear(); - _cellDataByFieldId.clear(); - } -} diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_cache.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_cache.dart new file mode 100644 index 0000000000..1f14c7c54a --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_cache.dart @@ -0,0 +1,70 @@ +part of 'cell_service.dart'; + +typedef GridCellMap = LinkedHashMap; + +class GridCell { + dynamic object; + GridCell({ + required this.object, + }); +} + +/// Use to index the cell in the grid. +/// We use [fieldId + rowId] to identify the cell. +class GridCellCacheKey { + final String fieldId; + final String rowId; + GridCellCacheKey({ + required this.fieldId, + required this.rowId, + }); +} + +/// GridCellCache is used to cache cell data of each block. +/// We use GridCellCacheKey to index the cell in the cache. +/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid +/// for more information +class GridCellCache { + final String gridId; + + /// fieldId: {cacheKey: GridCell} + final Map> _cellDataByFieldId = {}; + GridCellCache({ + required this.gridId, + }); + + void remove(String fieldId) { + _cellDataByFieldId.remove(fieldId); + } + + void insert(GridCellCacheKey key, T value) { + var map = _cellDataByFieldId[key.fieldId]; + if (map == null) { + _cellDataByFieldId[key.fieldId] = {}; + map = _cellDataByFieldId[key.fieldId]; + } + + map![key.rowId] = value.object; + } + + T? get(GridCellCacheKey key) { + final map = _cellDataByFieldId[key.fieldId]; + if (map == null) { + return null; + } else { + final value = map[key.rowId]; + if (value is T) { + return value; + } else { + if (value != null) { + Log.error("Expected value type: $T, but receive $value"); + } + return null; + } + } + } + + Future dispose() async { + _cellDataByFieldId.clear(); + } +} diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_loader.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_loader.dart index 676e3f66d0..c4b3430199 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_loader.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_loader.dart @@ -3,60 +3,28 @@ part of 'cell_service.dart'; abstract class IGridCellDataConfig { // The cell data will reload if it receives the field's change notification. bool get reloadOnFieldChanged; - - // When the reloadOnCellChanged is true, it will load the cell data after user input. - // For example: The number cell reload the cell data that carries the format - // user input: 12 - // cell display: $12 - bool get reloadOnCellChanged; } -class GridCellDataConfig implements IGridCellDataConfig { - @override - final bool reloadOnCellChanged; - - @override - final bool reloadOnFieldChanged; - - const GridCellDataConfig({ - this.reloadOnCellChanged = false, - this.reloadOnFieldChanged = false, - }); -} - -abstract class IGridCellDataLoader { - Future loadData(); - - IGridCellDataConfig get config; -} - -abstract class ICellDataParser { +abstract class IGridCellDataParser { T? parserData(List data); } -class GridCellDataLoader extends IGridCellDataLoader { +class GridCellDataLoader { final CellService service = CellService(); - final GridCell gridCell; - final ICellDataParser parser; - - @override - final IGridCellDataConfig config; + final GridCellIdentifier cellId; + final IGridCellDataParser parser; + final bool reloadOnFieldChanged; GridCellDataLoader({ - required this.gridCell, + required this.cellId, required this.parser, - this.config = const GridCellDataConfig(), + this.reloadOnFieldChanged = false, }); - @override Future loadData() { - final fut = service.getCell( - gridId: gridCell.gridId, - fieldId: gridCell.field.id, - rowId: gridCell.rowId, - ); + final fut = service.getCell(cellId: cellId); return fut.then( - (result) => result.fold((Cell cell) { + (result) => result.fold((GridCellPB cell) { try { return parser.parserData(cell.data); } catch (e, s) { @@ -72,30 +40,7 @@ class GridCellDataLoader extends IGridCellDataLoader { } } -class SelectOptionCellDataLoader extends IGridCellDataLoader { - final SelectOptionService service; - final GridCell gridCell; - SelectOptionCellDataLoader({ - required this.gridCell, - }) : service = SelectOptionService(gridCell: gridCell); - @override - Future loadData() async { - return service.getOpitonContext().then((result) { - return result.fold( - (data) => data, - (err) { - Log.error(err); - return null; - }, - ); - }); - } - - @override - IGridCellDataConfig get config => const GridCellDataConfig(reloadOnFieldChanged: true); -} - -class StringCellDataParser implements ICellDataParser { +class StringCellDataParser implements IGridCellDataParser { @override String? parserData(List data) { final s = utf8.decode(data); @@ -103,32 +48,32 @@ class StringCellDataParser implements ICellDataParser { } } -class DateCellDataParser implements ICellDataParser { +class DateCellDataParser implements IGridCellDataParser { @override - DateCellData? parserData(List data) { + DateCellDataPB? parserData(List data) { if (data.isEmpty) { return null; } - return DateCellData.fromBuffer(data); + return DateCellDataPB.fromBuffer(data); } } -class SelectOptionCellDataParser implements ICellDataParser { +class SelectOptionCellDataParser implements IGridCellDataParser { @override - SelectOptionCellData? parserData(List data) { + SelectOptionCellDataPB? parserData(List data) { if (data.isEmpty) { return null; } - return SelectOptionCellData.fromBuffer(data); + return SelectOptionCellDataPB.fromBuffer(data); } } -class URLCellDataParser implements ICellDataParser { +class URLCellDataParser implements IGridCellDataParser { @override - URLCellData? parserData(List data) { + URLCellDataPB? parserData(List data) { if (data.isEmpty) { return null; } - return URLCellData.fromBuffer(data); + return URLCellDataPB.fromBuffer(data); } } diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart index 2ad217e062..71927bae14 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_data_persistence.dart @@ -1,25 +1,22 @@ part of 'cell_service.dart'; -abstract class _GridCellDataPersistence { +/// Save the cell data to disk +/// You can extend this class to do custom operations. For example, the DateCellDataPersistence. +abstract class IGridCellDataPersistence { Future> save(D data); } -class CellDataPersistence implements _GridCellDataPersistence { - final GridCell gridCell; +class CellDataPersistence implements IGridCellDataPersistence { + final GridCellIdentifier cellId; CellDataPersistence({ - required this.gridCell, + required this.cellId, }); final CellService _cellService = CellService(); @override Future> save(String data) async { - final fut = _cellService.updateCell( - gridId: gridCell.gridId, - fieldId: gridCell.field.id, - rowId: gridCell.rowId, - data: data, - ); + final fut = _cellService.updateCell(cellId: cellId, data: data); return fut.then((result) { return result.fold( @@ -35,15 +32,15 @@ class CalendarData with _$CalendarData { const factory CalendarData({required DateTime date, String? time}) = _CalendarData; } -class DateCellDataPersistence implements _GridCellDataPersistence { - final GridCell gridCell; +class DateCellDataPersistence implements IGridCellDataPersistence { + final GridCellIdentifier cellId; DateCellDataPersistence({ - required this.gridCell, + required this.cellId, }); @override Future> save(CalendarData data) { - var payload = DateChangesetPayload.create()..cellIdentifier = _cellIdentifier(gridCell); + var payload = DateChangesetPayloadPB.create()..cellIdentifier = _makeCellIdPayload(cellId); final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString(); payload.date = date; @@ -61,9 +58,9 @@ class DateCellDataPersistence implements _GridCellDataPersistence } } -CellIdentifierPayload _cellIdentifier(GridCell gridCell) { - return CellIdentifierPayload.create() - ..gridId = gridCell.gridId - ..fieldId = gridCell.field.id - ..rowId = gridCell.rowId; +GridCellIdPB _makeCellIdPayload(GridCellIdentifier cellId) { + return GridCellIdPB.create() + ..gridId = cellId.gridId + ..fieldId = cellId.fieldId + ..rowId = cellId.rowId; } diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_field_notifier.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_field_notifier.dart new file mode 100644 index 0000000000..72f1bc787d --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_field_notifier.dart @@ -0,0 +1,60 @@ +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter/foundation.dart'; + +import 'cell_service.dart'; + +abstract class GridFieldChangedNotifier { + void onFieldChanged(void Function(GridFieldPB) callback); + void dispose(); +} + +/// GridPB's cell helper wrapper that enables each cell will get notified when the corresponding field was changed. +/// You Register an onFieldChanged callback to listen to the cell changes, and unregister if you don't want to listen. +class GridCellFieldNotifier { + /// fieldId: {objectId: callback} + final Map>> _fieldListenerByFieldId = {}; + + GridCellFieldNotifier({required GridFieldChangedNotifier notifier}) { + notifier.onFieldChanged( + (field) { + final map = _fieldListenerByFieldId[field.id]; + if (map != null) { + for (final callbacks in map.values) { + for (final callback in callbacks) { + callback(); + } + } + } + }, + ); + } + + /// + void register(GridCellCacheKey cacheKey, VoidCallback onFieldChanged) { + var map = _fieldListenerByFieldId[cacheKey.fieldId]; + if (map == null) { + _fieldListenerByFieldId[cacheKey.fieldId] = {}; + map = _fieldListenerByFieldId[cacheKey.fieldId]; + map![cacheKey.rowId] = [onFieldChanged]; + } else { + var objects = map[cacheKey.rowId]; + if (objects == null) { + map[cacheKey.rowId] = [onFieldChanged]; + } else { + objects.add(onFieldChanged); + } + } + } + + void unregister(GridCellCacheKey cacheKey, VoidCallback fn) { + var callbacks = _fieldListenerByFieldId[cacheKey.fieldId]?[cacheKey.rowId]; + final index = callbacks?.indexWhere((callback) => callback == fn); + if (index != null && index != -1) { + callbacks?.removeAt(index); + } + } + + Future dispose() async { + _fieldListenerByFieldId.clear(); + } +} diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart index 5e406c0c26..47cd67a55f 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart @@ -1,26 +1,29 @@ import 'dart:async'; import 'dart:collection'; +import 'package:app_flowy/workspace/application/grid/grid_service.dart'; import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart'; -import 'package:app_flowy/workspace/application/grid/cell/select_option_service.dart'; import 'package:app_flowy/workspace/application/grid/field/field_service.dart'; import 'dart:convert' show utf8; + +import '../../field/type_option/type_option_service.dart'; +import 'cell_field_notifier.dart'; part 'cell_service.freezed.dart'; part 'cell_data_loader.dart'; part 'context_builder.dart'; -part 'cache.dart'; +part 'cell_cache.dart'; part 'cell_data_persistence.dart'; // key: rowId @@ -29,44 +32,46 @@ class CellService { CellService(); Future> updateCell({ - required String gridId, - required String fieldId, - required String rowId, + required GridCellIdentifier cellId, required String data, }) { - final payload = CellChangeset.create() - ..gridId = gridId - ..fieldId = fieldId - ..rowId = rowId + final payload = CellChangesetPB.create() + ..gridId = cellId.gridId + ..fieldId = cellId.fieldId + ..rowId = cellId.rowId ..content = data; return GridEventUpdateCell(payload).send(); } - Future> getCell({ - required String gridId, - required String fieldId, - required String rowId, + Future> getCell({ + required GridCellIdentifier cellId, }) { - final payload = CellIdentifierPayload.create() - ..gridId = gridId - ..fieldId = fieldId - ..rowId = rowId; + final payload = GridCellIdPB.create() + ..gridId = cellId.gridId + ..fieldId = cellId.fieldId + ..rowId = cellId.rowId; return GridEventGetCell(payload).send(); } } +/// Id of the cell +/// We can locate the cell by using gridId + rowId + field.id. @freezed -class GridCell with _$GridCell { - const factory GridCell({ +class GridCellIdentifier with _$GridCellIdentifier { + const factory GridCellIdentifier({ required String gridId, required String rowId, - required Field field, - }) = _GridCell; + required GridFieldPB field, + }) = _GridCellIdentifier; // ignore: unused_element - const GridCell._(); + const GridCellIdentifier._(); - String cellId() { - return rowId + field.id + "${field.fieldType}"; + String get fieldId => field.id; + + FieldType get fieldType => field.fieldType; + + ValueKey key() { + return ValueKey(rowId + fieldId + "${field.fieldType}"); } } diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart index 00264ad4b0..48e88e56ac 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart @@ -1,154 +1,183 @@ part of 'cell_service.dart'; -typedef GridCellContext = _GridCellContext; -typedef GridSelectOptionCellContext = _GridCellContext; -typedef GridDateCellContext = _GridCellContext; -typedef GridURLCellContext = _GridCellContext; +typedef GridCellController = IGridCellController; +typedef GridSelectOptionCellController = IGridCellController; +typedef GridDateCellController = IGridCellController; +typedef GridURLCellController = IGridCellController; -class GridCellContextBuilder { - final GridCellCacheService _cellCache; - final GridCell _gridCell; - GridCellContextBuilder({ - required GridCellCacheService cellCache, - required GridCell gridCell, +class GridCellControllerBuilder { + final GridCellIdentifier _cellId; + final GridCellCache _cellCache; + final GridFieldCache _fieldCache; + + GridCellControllerBuilder({ + required GridCellIdentifier cellId, + required GridCellCache cellCache, + required GridFieldCache fieldCache, }) : _cellCache = cellCache, - _gridCell = gridCell; + _fieldCache = fieldCache, + _cellId = cellId; - _GridCellContext build() { - switch (_gridCell.field.fieldType) { + IGridCellController build() { + final cellFieldNotifier = GridCellFieldNotifier(notifier: _GridFieldChangedNotifierImpl(_fieldCache)); + + switch (_cellId.fieldType) { case FieldType.Checkbox: final cellDataLoader = GridCellDataLoader( - gridCell: _gridCell, + cellId: _cellId, parser: StringCellDataParser(), ); - return GridCellContext( - gridCell: _gridCell, + return GridCellController( + cellId: _cellId, cellCache: _cellCache, cellDataLoader: cellDataLoader, - cellDataPersistence: CellDataPersistence(gridCell: _gridCell), + fieldNotifier: cellFieldNotifier, + cellDataPersistence: CellDataPersistence(cellId: _cellId), ); case FieldType.DateTime: final cellDataLoader = GridCellDataLoader( - gridCell: _gridCell, + cellId: _cellId, parser: DateCellDataParser(), - config: const GridCellDataConfig(reloadOnFieldChanged: true), + reloadOnFieldChanged: true, ); - return GridDateCellContext( - gridCell: _gridCell, + return GridDateCellController( + cellId: _cellId, cellCache: _cellCache, cellDataLoader: cellDataLoader, - cellDataPersistence: DateCellDataPersistence(gridCell: _gridCell), + fieldNotifier: cellFieldNotifier, + cellDataPersistence: DateCellDataPersistence(cellId: _cellId), ); case FieldType.Number: final cellDataLoader = GridCellDataLoader( - gridCell: _gridCell, + cellId: _cellId, parser: StringCellDataParser(), - config: const GridCellDataConfig(reloadOnCellChanged: true, reloadOnFieldChanged: true), + reloadOnFieldChanged: true, ); - return GridCellContext( - gridCell: _gridCell, + return GridCellController( + cellId: _cellId, cellCache: _cellCache, cellDataLoader: cellDataLoader, - cellDataPersistence: CellDataPersistence(gridCell: _gridCell), + fieldNotifier: cellFieldNotifier, + cellDataPersistence: CellDataPersistence(cellId: _cellId), ); case FieldType.RichText: final cellDataLoader = GridCellDataLoader( - gridCell: _gridCell, + cellId: _cellId, parser: StringCellDataParser(), ); - return GridCellContext( - gridCell: _gridCell, + return GridCellController( + cellId: _cellId, cellCache: _cellCache, cellDataLoader: cellDataLoader, - cellDataPersistence: CellDataPersistence(gridCell: _gridCell), + fieldNotifier: cellFieldNotifier, + cellDataPersistence: CellDataPersistence(cellId: _cellId), ); case FieldType.MultiSelect: case FieldType.SingleSelect: final cellDataLoader = GridCellDataLoader( - gridCell: _gridCell, + cellId: _cellId, parser: SelectOptionCellDataParser(), - config: const GridCellDataConfig(reloadOnFieldChanged: true), + reloadOnFieldChanged: true, ); - return GridSelectOptionCellContext( - gridCell: _gridCell, + return GridSelectOptionCellController( + cellId: _cellId, cellCache: _cellCache, cellDataLoader: cellDataLoader, - cellDataPersistence: CellDataPersistence(gridCell: _gridCell), + fieldNotifier: cellFieldNotifier, + cellDataPersistence: CellDataPersistence(cellId: _cellId), ); case FieldType.URL: final cellDataLoader = GridCellDataLoader( - gridCell: _gridCell, + cellId: _cellId, parser: URLCellDataParser(), ); - return GridURLCellContext( - gridCell: _gridCell, + return GridURLCellController( + cellId: _cellId, cellCache: _cellCache, cellDataLoader: cellDataLoader, - cellDataPersistence: CellDataPersistence(gridCell: _gridCell), + fieldNotifier: cellFieldNotifier, + cellDataPersistence: CellDataPersistence(cellId: _cellId), ); } throw UnimplementedError; } } -// T: the type of the CellData -// D: the type of the data that will be save to disk +/// IGridCellController is used to manipulate the cell and receive notifications. +/// * Read/Write cell data +/// * Listen on field/cell notifications. +/// +/// Generic T represents the type of the cell data. +/// Generic D represents the type of data that will be saved to the disk +/// // ignore: must_be_immutable -class _GridCellContext extends Equatable { - final GridCell gridCell; - final GridCellCacheService cellCache; - final _GridCellCacheKey _cacheKey; - final IGridCellDataLoader cellDataLoader; - final _GridCellDataPersistence cellDataPersistence; +class IGridCellController extends Equatable { + final GridCellIdentifier cellId; + final GridCellCache _cellsCache; + final GridCellCacheKey _cacheKey; final FieldService _fieldService; + final GridCellFieldNotifier _fieldNotifier; + final GridCellDataLoader _cellDataLoader; + final IGridCellDataPersistence _cellDataPersistence; late final CellListener _cellListener; - late final ValueNotifier? _cellDataNotifier; + ValueNotifier? _cellDataNotifier; + bool isListening = false; VoidCallback? _onFieldChangedFn; Timer? _loadDataOperation; Timer? _saveDataOperation; + bool _isDispose = false; - _GridCellContext({ - required this.gridCell, - required this.cellCache, - required this.cellDataLoader, - required this.cellDataPersistence, - }) : _fieldService = FieldService(gridId: gridCell.gridId, fieldId: gridCell.field.id), - _cacheKey = _GridCellCacheKey(rowId: gridCell.rowId, fieldId: gridCell.field.id); + IGridCellController({ + required this.cellId, + required GridCellCache cellCache, + required GridCellFieldNotifier fieldNotifier, + required GridCellDataLoader cellDataLoader, + required IGridCellDataPersistence cellDataPersistence, + }) : _cellsCache = cellCache, + _cellDataLoader = cellDataLoader, + _cellDataPersistence = cellDataPersistence, + _fieldNotifier = fieldNotifier, + _fieldService = FieldService(gridId: cellId.gridId, fieldId: cellId.field.id), + _cacheKey = GridCellCacheKey(rowId: cellId.rowId, fieldId: cellId.field.id); - _GridCellContext clone() { - return _GridCellContext( - gridCell: gridCell, - cellDataLoader: cellDataLoader, - cellCache: cellCache, - cellDataPersistence: cellDataPersistence); + IGridCellController clone() { + return IGridCellController( + cellId: cellId, + cellDataLoader: _cellDataLoader, + cellCache: _cellsCache, + fieldNotifier: _fieldNotifier, + cellDataPersistence: _cellDataPersistence); } - String get gridId => gridCell.gridId; + String get gridId => cellId.gridId; - String get rowId => gridCell.rowId; + String get rowId => cellId.rowId; - String get cellId => gridCell.rowId + gridCell.field.id; + String get fieldId => cellId.field.id; - String get fieldId => gridCell.field.id; + GridFieldPB get field => cellId.field; - Field get field => gridCell.field; + FieldType get fieldType => cellId.field.fieldType; - FieldType get fieldType => gridCell.field.fieldType; - - VoidCallback? startListening({required void Function(T?) onCellChanged}) { + VoidCallback? startListening({required void Function(T?) onCellChanged, VoidCallback? onCellFieldChanged}) { if (isListening) { Log.error("Already started. It seems like you should call clone first"); return null; } - isListening = true; - _cellDataNotifier = ValueNotifier(cellCache.get(_cacheKey)); - _cellListener = CellListener(rowId: gridCell.rowId, fieldId: gridCell.field.id); + + _cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey)); + _cellListener = CellListener(rowId: cellId.rowId, fieldId: cellId.field.id); + + /// 1.Listen on user edit event and load the new cell data if needed. + /// For example: + /// user input: 12 + /// cell display: $12 _cellListener.start(onCellChanged: (result) { result.fold( (_) => _loadData(), @@ -156,22 +185,27 @@ class _GridCellContext extends Equatable { ); }); - if (cellDataLoader.config.reloadOnFieldChanged) { - _onFieldChangedFn = () { - _loadData(); - }; - cellCache.addFieldListener(_cacheKey, _onFieldChangedFn!); - } + /// 2.Listen on the field event and load the cell data if needed. + _onFieldChangedFn = () { + if (onCellFieldChanged != null) { + onCellFieldChanged(); + } - onCellChangedFn() { - onCellChanged(_cellDataNotifier?.value); - - if (cellDataLoader.config.reloadOnCellChanged) { + /// reloadOnFieldChanged should be true if you need to load the data when the corresponding field is changed + /// For example: + /// ¥12 -> $12 + if (_cellDataLoader.reloadOnFieldChanged) { _loadData(); } - } + }; + _fieldNotifier.register(_cacheKey, _onFieldChangedFn!); + + /// Notify the listener, the cell data was changed. + onCellChangedFn() => onCellChanged(_cellDataNotifier?.value); _cellDataNotifier?.addListener(onCellChangedFn); + + // Return the function pointer that can be used when calling removeListener. return onCellChangedFn; } @@ -179,29 +213,45 @@ class _GridCellContext extends Equatable { _cellDataNotifier?.removeListener(fn); } - T? getCellData({bool loadIfNoCache = true}) { - final data = cellCache.get(_cacheKey); - if (data == null && loadIfNoCache) { + /// Return the cell data. + /// The cell data will be read from the Cache first, and load from disk if it does not exist. + /// You can set [loadIfNotExist] to false (default is true) to disable loading the cell data. + T? getCellData({bool loadIfNotExist = true}) { + final data = _cellsCache.get(_cacheKey); + if (data == null && loadIfNotExist) { _loadData(); } return data; } - Future> getTypeOptionData() { - return _fieldService.getFieldTypeOptionData(fieldType: fieldType); + /// Return the FieldTypeOptionDataPB that can be parsed into corresponding class using the [parser]. + /// [PD] is the type that the parser return. + Future> getFieldTypeOption(P parser) { + return _fieldService.getFieldTypeOptionData(fieldType: fieldType).then((result) { + return result.fold( + (data) => parser.fromBuffer(data.typeOptionData), + (err) => right(err), + ); + }); } + /// Save the cell data to disk + /// You can set [dedeplicate] to true (default is false) to reduce the save operation. + /// It's useful when you call this method when user editing the [TextField]. + /// The default debounce interval is 300 milliseconds. void saveCellData(D data, {bool deduplicate = false, void Function(Option)? resultCallback}) async { if (deduplicate) { _loadDataOperation?.cancel(); - _loadDataOperation = Timer(const Duration(milliseconds: 300), () async { - final result = await cellDataPersistence.save(data); + + _saveDataOperation?.cancel(); + _saveDataOperation = Timer(const Duration(milliseconds: 300), () async { + final result = await _cellDataPersistence.save(data); if (resultCallback != null) { resultCallback(result); } }); } else { - final result = await cellDataPersistence.save(data); + final result = await _cellDataPersistence.save(data); if (resultCallback != null) { resultCallback(result); } @@ -209,26 +259,59 @@ class _GridCellContext extends Equatable { } void _loadData() { + _saveDataOperation?.cancel(); + _loadDataOperation?.cancel(); _loadDataOperation = Timer(const Duration(milliseconds: 10), () { - cellDataLoader.loadData().then((data) { + _cellDataLoader.loadData().then((data) { _cellDataNotifier?.value = data; - cellCache.insert(_GridCellCacheObject(key: _cacheKey, object: data)); + _cellsCache.insert(_cacheKey, GridCell(object: data)); }); }); } void dispose() { + if (_isDispose) { + Log.error("$this should only dispose once"); + return; + } + _isDispose = true; _cellListener.stop(); _loadDataOperation?.cancel(); _saveDataOperation?.cancel(); + _cellDataNotifier = null; if (_onFieldChangedFn != null) { - cellCache.removeFieldListener(_cacheKey, _onFieldChangedFn!); + _fieldNotifier.unregister(_cacheKey, _onFieldChangedFn!); _onFieldChangedFn = null; } } @override - List get props => [cellCache.get(_cacheKey) ?? "", cellId]; + List get props => [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.field.id]; +} + +class _GridFieldChangedNotifierImpl extends GridFieldChangedNotifier { + final GridFieldCache _cache; + FieldChangesetCallback? _onChangesetFn; + + _GridFieldChangedNotifierImpl(GridFieldCache cache) : _cache = cache; + + @override + void dispose() { + if (_onChangesetFn != null) { + _cache.removeListener(onChangsetListener: _onChangesetFn!); + _onChangesetFn = null; + } + } + + @override + void onFieldChanged(void Function(GridFieldPB p1) callback) { + _onChangesetFn = (GridFieldChangesetPB changeset) { + for (final updatedField in changeset.updatedFields) { + callback(updatedField); + } + }; + _cache.addListener(onChangeset: _onChangesetFn); + } } diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart index b8e2b13bbc..041e687c9b 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/checkbox_cell_bloc.dart @@ -6,7 +6,7 @@ import 'cell_service/cell_service.dart'; part 'checkbox_cell_bloc.freezed.dart'; class CheckboxCellBloc extends Bloc { - final GridCellContext cellContext; + final GridCellController cellContext; void Function()? _onCellChangedFn; CheckboxCellBloc({ @@ -67,7 +67,7 @@ class CheckboxCellState with _$CheckboxCellState { required bool isSelected, }) = _CheckboxCellState; - factory CheckboxCellState.initial(GridCellContext context) { + factory CheckboxCellState.initial(GridCellController context) { return CheckboxCellState(isSelected: _isSelected(context.getCellData())); } } diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart index 28f8bf1b10..d99bdbf817 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart @@ -5,6 +5,7 @@ import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:table_calendar/table_calendar.dart'; @@ -16,12 +17,12 @@ import 'package:fixnum/fixnum.dart' as $fixnum; part 'date_cal_bloc.freezed.dart'; class DateCalBloc extends Bloc { - final GridDateCellContext cellContext; + final GridDateCellController cellContext; void Function()? _onCellChangedFn; DateCalBloc({ required DateTypeOption dateTypeOption, - required DateCellData? cellData, + required DateCellDataPB? cellData, required this.cellContext, }) : super(DateCalState.initial(dateTypeOption, cellData)) { on( @@ -37,7 +38,7 @@ class DateCalBloc extends Bloc { setFocusedDay: (focusedDay) { emit(state.copyWith(focusedDay: focusedDay)); }, - didReceiveCellUpdate: (DateCellData? cellData) { + didReceiveCellUpdate: (DateCellDataPB? cellData) { final calData = calDataFromCellData(cellData); final time = calData.foldRight("", (dateData, previous) => dateData.time); emit(state.copyWith(calData: calData, time: time)); @@ -187,7 +188,7 @@ class DateCalEvent with _$DateCalEvent { const factory DateCalEvent.setDateFormat(DateFormat dateFormat) = _DateFormat; const factory DateCalEvent.setIncludeTime(bool includeTime) = _IncludeTime; const factory DateCalEvent.setTime(String time) = _Time; - const factory DateCalEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate; + const factory DateCalEvent.didReceiveCellUpdate(DateCellDataPB? data) = _DidReceiveCellUpdate; const factory DateCalEvent.didUpdateCalData(Option data, Option timeFormatError) = _DidUpdateCalData; } @@ -206,7 +207,7 @@ class DateCalState with _$DateCalState { factory DateCalState.initial( DateTypeOption dateTypeOption, - DateCellData? cellData, + DateCellDataPB? cellData, ) { Option calData = calDataFromCellData(cellData); final time = calData.foldRight("", (dateData, previous) => dateData.time); @@ -232,7 +233,7 @@ String _timeHintText(DateTypeOption typeOption) { return ""; } -Option calDataFromCellData(DateCellData? cellData) { +Option calDataFromCellData(DateCellDataPB? cellData) { String? time = timeFromCellData(cellData); Option calData = none(); if (cellData != null) { @@ -248,7 +249,7 @@ $fixnum.Int64 timestampFromDateTime(DateTime dateTime) { return $fixnum.Int64(timestamp); } -String? timeFromCellData(DateCellData? cellData) { +String? timeFromCellData(DateCellDataPB? cellData) { String? time; if (cellData?.hasTime() ?? false) { time = cellData?.time; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart index 00780143cc..c4f79f1f90 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart @@ -1,4 +1,4 @@ -import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -7,7 +7,7 @@ import 'cell_service/cell_service.dart'; part 'date_cell_bloc.freezed.dart'; class DateCellBloc extends Bloc { - final GridDateCellContext cellContext; + final GridDateCellController cellContext; void Function()? _onCellChangedFn; DateCellBloc({required this.cellContext}) : super(DateCellState.initial(cellContext)) { @@ -15,10 +15,10 @@ class DateCellBloc extends Bloc { (event, emit) async { event.when( initial: () => _startListening(), - didReceiveCellUpdate: (DateCellData? cellData) { + didReceiveCellUpdate: (DateCellDataPB? cellData) { emit(state.copyWith(data: cellData, dateStr: _dateStrFromCellData(cellData))); }, - didReceiveFieldUpdate: (Field value) => emit(state.copyWith(field: value)), + didReceiveFieldUpdate: (GridFieldPB value) => emit(state.copyWith(field: value)), ); }, ); @@ -48,19 +48,19 @@ class DateCellBloc extends Bloc { @freezed class DateCellEvent with _$DateCellEvent { const factory DateCellEvent.initial() = _InitialCell; - const factory DateCellEvent.didReceiveCellUpdate(DateCellData? data) = _DidReceiveCellUpdate; - const factory DateCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate; + const factory DateCellEvent.didReceiveCellUpdate(DateCellDataPB? data) = _DidReceiveCellUpdate; + const factory DateCellEvent.didReceiveFieldUpdate(GridFieldPB field) = _DidReceiveFieldUpdate; } @freezed class DateCellState with _$DateCellState { const factory DateCellState({ - required DateCellData? data, + required DateCellDataPB? data, required String dateStr, - required Field field, + required GridFieldPB field, }) = _DateCellState; - factory DateCellState.initial(GridDateCellContext context) { + factory DateCellState.initial(GridDateCellController context) { final cellData = context.getCellData(); return DateCellState( @@ -71,7 +71,7 @@ class DateCellState with _$DateCellState { } } -String _dateStrFromCellData(DateCellData? cellData) { +String _dateStrFromCellData(DateCellDataPB? cellData) { String dateStr = ""; if (cellData != null) { dateStr = cellData.date + " " + cellData.time; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart index adcfee71e6..65eec13e6c 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/number_cell_bloc.dart @@ -8,7 +8,7 @@ import 'cell_service/cell_service.dart'; part 'number_cell_bloc.freezed.dart'; class NumberCellBloc extends Bloc { - final GridCellContext cellContext; + final GridCellController cellContext; void Function()? _onCellChangedFn; NumberCellBloc({ @@ -72,7 +72,7 @@ class NumberCellState with _$NumberCellState { required Either content, }) = _NumberCellState; - factory NumberCellState.initial(GridCellContext context) { + factory NumberCellState.initial(GridCellController context) { final cellContent = context.getCellData() ?? ""; return NumberCellState( content: left(cellContent), diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart index a6e140f707..d0db8669fa 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_cell_bloc.dart @@ -7,7 +7,7 @@ import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_serv part 'select_option_cell_bloc.freezed.dart'; class SelectOptionCellBloc extends Bloc { - final GridSelectOptionCellContext cellContext; + final GridSelectOptionCellController cellContext; void Function()? _onCellChangedFn; SelectOptionCellBloc({ @@ -56,17 +56,17 @@ class SelectOptionCellBloc extends Bloc selectedOptions, + List selectedOptions, ) = _DidReceiveOptions; } @freezed class SelectOptionCellState with _$SelectOptionCellState { const factory SelectOptionCellState({ - required List selectedOptions, + required List selectedOptions, }) = _SelectOptionCellState; - factory SelectOptionCellState.initial(GridSelectOptionCellContext context) { + factory SelectOptionCellState.initial(GridSelectOptionCellController context) { final data = context.getCellData(); return SelectOptionCellState( diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_editor_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_editor_bloc.dart index 9ce003b154..0990dca28a 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_editor_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_editor_bloc.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'package:app_flowy/workspace/application/grid/field/grid_listenr.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; @@ -13,16 +12,13 @@ part 'select_option_editor_bloc.freezed.dart'; class SelectOptionCellEditorBloc extends Bloc { final SelectOptionService _selectOptionService; - final GridSelectOptionCellContext cellContext; - late final GridFieldsListener _fieldListener; - void Function()? _onCellChangedFn; + final GridSelectOptionCellController cellController; Timer? _delayOperation; SelectOptionCellEditorBloc({ - required this.cellContext, - }) : _selectOptionService = SelectOptionService(gridCell: cellContext.gridCell), - _fieldListener = GridFieldsListener(gridId: cellContext.gridId), - super(SelectOptionEditorState.initial(cellContext)) { + required this.cellController, + }) : _selectOptionService = SelectOptionService(cellId: cellController.cellId), + super(SelectOptionEditorState.initial(cellController)) { on( (event, emit) async { await event.map( @@ -64,13 +60,8 @@ class SelectOptionCellEditorBloc extends Bloc close() async { - if (_onCellChangedFn != null) { - cellContext.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } _delayOperation?.cancel(); - await _fieldListener.stop(); - cellContext.dispose(); + cellController.dispose(); return super.close(); } @@ -79,7 +70,7 @@ class SelectOptionCellEditorBloc extends Bloc {}, (err) => Log.error(err)); } - void _deleteOption(SelectOption option) async { + void _deleteOption(SelectOptionPB option) async { final result = await _selectOptionService.delete( option: option, ); @@ -87,7 +78,7 @@ class SelectOptionCellEditorBloc extends Bloc null, (err) => Log.error(err)); } - void _updateOption(SelectOption option) async { + void _updateOption(SelectOptionPB option) async { final result = await _selectOptionService.update( option: option, ); @@ -131,8 +122,8 @@ class SelectOptionCellEditorBloc extends Bloc filter, List allOptions) { - final List options = List.from(allOptions); + _MakeOptionResult _makeOptions(Option filter, List allOptions) { + final List options = List.from(allOptions); Option createOption = filter; filter.foldRight(null, (filter, previous) { @@ -157,24 +148,16 @@ class SelectOptionCellEditorBloc extends Bloc Log.error(err), - ); - }); } } @@ -182,26 +165,26 @@ class SelectOptionCellEditorBloc extends Bloc options, List selectedOptions) = _DidReceiveOptions; + List options, List selectedOptions) = _DidReceiveOptions; const factory SelectOptionEditorEvent.newOption(String optionName) = _NewOption; const factory SelectOptionEditorEvent.selectOption(String optionId) = _SelectOption; - const factory SelectOptionEditorEvent.updateOption(SelectOption option) = _UpdateOption; - const factory SelectOptionEditorEvent.deleteOption(SelectOption option) = _DeleteOption; + const factory SelectOptionEditorEvent.updateOption(SelectOptionPB option) = _UpdateOption; + const factory SelectOptionEditorEvent.deleteOption(SelectOptionPB option) = _DeleteOption; const factory SelectOptionEditorEvent.filterOption(String optionName) = _SelectOptionFilter; } @freezed class SelectOptionEditorState with _$SelectOptionEditorState { const factory SelectOptionEditorState({ - required List options, - required List allOptions, - required List selectedOptions, + required List options, + required List allOptions, + required List selectedOptions, required Option createOption, required Option filter, }) = _SelectOptionEditorState; - factory SelectOptionEditorState.initial(GridSelectOptionCellContext context) { - final data = context.getCellData(loadIfNoCache: false); + factory SelectOptionEditorState.initial(GridSelectOptionCellController context) { + final data = context.getCellData(loadIfNotExist: false); return SelectOptionEditorState( options: data?.options ?? [], allOptions: data?.options ?? [], @@ -213,7 +196,7 @@ class SelectOptionEditorState with _$SelectOptionEditorState { } class _MakeOptionResult { - List options; + List options; Option createOption; _MakeOptionResult({ diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart index f3458454d1..b6966a3e4e 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/select_option_service.dart @@ -7,23 +7,23 @@ import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; import 'cell_service/cell_service.dart'; class SelectOptionService { - final GridCell gridCell; - SelectOptionService({required this.gridCell}); + final GridCellIdentifier cellId; + SelectOptionService({required this.cellId}); - String get gridId => gridCell.gridId; - String get fieldId => gridCell.field.id; - String get rowId => gridCell.rowId; + String get gridId => cellId.gridId; + String get fieldId => cellId.field.id; + String get rowId => cellId.rowId; Future> create({required String name}) { return TypeOptionService(gridId: gridId, fieldId: fieldId).newOption(name: name).then( (result) { return result.fold( (option) { - final cellIdentifier = CellIdentifierPayload.create() + final cellIdentifier = GridCellIdPB.create() ..gridId = gridId ..fieldId = fieldId ..rowId = rowId; - final payload = SelectOptionChangesetPayload.create() + final payload = SelectOptionChangesetPayloadPB.create() ..insertOption = option ..cellIdentifier = cellIdentifier; return GridEventUpdateSelectOption(payload).send(); @@ -35,26 +35,26 @@ class SelectOptionService { } Future> update({ - required SelectOption option, + required SelectOptionPB option, }) { - final payload = SelectOptionChangesetPayload.create() + final payload = SelectOptionChangesetPayloadPB.create() ..updateOption = option ..cellIdentifier = _cellIdentifier(); return GridEventUpdateSelectOption(payload).send(); } Future> delete({ - required SelectOption option, + required SelectOptionPB option, }) { - final payload = SelectOptionChangesetPayload.create() + final payload = SelectOptionChangesetPayloadPB.create() ..deleteOption = option ..cellIdentifier = _cellIdentifier(); return GridEventUpdateSelectOption(payload).send(); } - Future> getOpitonContext() { - final payload = CellIdentifierPayload.create() + Future> getOpitonContext() { + final payload = GridCellIdPB.create() ..gridId = gridId ..fieldId = fieldId ..rowId = rowId; @@ -63,21 +63,21 @@ class SelectOptionService { } Future> select({required String optionId}) { - final payload = SelectOptionCellChangesetPayload.create() + final payload = SelectOptionCellChangesetPayloadPB.create() ..cellIdentifier = _cellIdentifier() ..insertOptionId = optionId; return GridEventUpdateSelectOptionCell(payload).send(); } Future> unSelect({required String optionId}) { - final payload = SelectOptionCellChangesetPayload.create() + final payload = SelectOptionCellChangesetPayloadPB.create() ..cellIdentifier = _cellIdentifier() ..deleteOptionId = optionId; return GridEventUpdateSelectOptionCell(payload).send(); } - CellIdentifierPayload _cellIdentifier() { - return CellIdentifierPayload.create() + GridCellIdPB _cellIdentifier() { + return GridCellIdPB.create() ..gridId = gridId ..fieldId = fieldId ..rowId = rowId; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart index e3b7fd2dca..783564b5fa 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/text_cell_bloc.dart @@ -6,7 +6,7 @@ import 'cell_service/cell_service.dart'; part 'text_cell_bloc.freezed.dart'; class TextCellBloc extends Bloc { - final GridCellContext cellContext; + final GridCellController cellContext; void Function()? _onCellChangedFn; TextCellBloc({ required this.cellContext, @@ -63,7 +63,7 @@ class TextCellState with _$TextCellState { required String content, }) = _TextCellState; - factory TextCellState.initial(GridCellContext context) => TextCellState( + factory TextCellState.initial(GridCellController context) => TextCellState( content: context.getCellData() ?? "", ); } diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart index e1fe39c3bf..e43f561542 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart @@ -1,4 +1,4 @@ -import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; @@ -7,7 +7,7 @@ import 'cell_service/cell_service.dart'; part 'url_cell_bloc.freezed.dart'; class URLCellBloc extends Bloc { - final GridURLCellContext cellContext; + final GridURLCellController cellContext; void Function()? _onCellChangedFn; URLCellBloc({ required this.cellContext, @@ -57,7 +57,7 @@ class URLCellBloc extends Bloc { class URLCellEvent with _$URLCellEvent { const factory URLCellEvent.initial() = _InitialCell; const factory URLCellEvent.updateURL(String url) = _UpdateURL; - const factory URLCellEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate; + const factory URLCellEvent.didReceiveCellUpdate(URLCellDataPB? cell) = _DidReceiveCellUpdate; } @freezed @@ -67,7 +67,7 @@ class URLCellState with _$URLCellState { required String url, }) = _URLCellState; - factory URLCellState.initial(GridURLCellContext context) { + factory URLCellState.initial(GridURLCellController context) { final cellData = context.getCellData(); return URLCellState( content: cellData?.content ?? "", diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart index 6e4990943f..067be84b7b 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart @@ -1,4 +1,4 @@ -import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; @@ -7,7 +7,7 @@ import 'cell_service/cell_service.dart'; part 'url_cell_editor_bloc.freezed.dart'; class URLCellEditorBloc extends Bloc { - final GridURLCellContext cellContext; + final GridURLCellController cellContext; void Function()? _onCellChangedFn; URLCellEditorBloc({ required this.cellContext, @@ -54,7 +54,7 @@ class URLCellEditorBloc extends Bloc { @freezed class URLCellEditorEvent with _$URLCellEditorEvent { const factory URLCellEditorEvent.initial() = _InitialCell; - const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellData? cell) = _DidReceiveCellUpdate; + const factory URLCellEditorEvent.didReceiveCellUpdate(URLCellDataPB? cell) = _DidReceiveCellUpdate; const factory URLCellEditorEvent.updateText(String text) = _UpdateText; } @@ -64,7 +64,7 @@ class URLCellEditorState with _$URLCellEditorState { required String content, }) = _URLCellEditorState; - factory URLCellEditorState.initial(GridURLCellContext context) { + factory URLCellEditorState.initial(GridURLCellController context) { final cellData = context.getCellData(); return URLCellEditorState( content: cellData?.content ?? "", diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart index 801678d929..3caef12f73 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/field_action_sheet_bloc.dart @@ -10,8 +10,8 @@ part 'field_action_sheet_bloc.freezed.dart'; class FieldActionSheetBloc extends Bloc { final FieldService fieldService; - FieldActionSheetBloc({required Field field, required this.fieldService}) - : super(FieldActionSheetState.initial(FieldTypeOptionData.create()..field_2 = field)) { + FieldActionSheetBloc({required GridFieldPB field, required this.fieldService}) + : super(FieldActionSheetState.initial(FieldTypeOptionDataPB.create()..field_2 = field)) { on( (event, emit) async { await event.map( @@ -67,12 +67,12 @@ class FieldActionSheetEvent with _$FieldActionSheetEvent { @freezed class FieldActionSheetState with _$FieldActionSheetState { const factory FieldActionSheetState({ - required FieldTypeOptionData fieldTypeOptionData, + required FieldTypeOptionDataPB fieldTypeOptionData, required String errorText, required String fieldName, }) = _FieldActionSheetState; - factory FieldActionSheetState.initial(FieldTypeOptionData data) => FieldActionSheetState( + factory FieldActionSheetState.initial(FieldTypeOptionDataPB data) => FieldActionSheetState( fieldTypeOptionData: data, errorText: '', fieldName: data.field_2.name, diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/field_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/field_cell_bloc.dart index 774a7fb82b..8a2a0e2d06 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/field_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/field_cell_bloc.dart @@ -62,7 +62,7 @@ class FieldCellBloc extends Bloc { @freezed class FieldCellEvent with _$FieldCellEvent { const factory FieldCellEvent.initial() = _InitialCell; - const factory FieldCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate; + const factory FieldCellEvent.didReceiveFieldUpdate(GridFieldPB field) = _DidReceiveFieldUpdate; const factory FieldCellEvent.startUpdateWidth(double offset) = _StartUpdateWidth; const factory FieldCellEvent.endUpdateWidth() = _EndUpdateWidth; } @@ -71,7 +71,7 @@ class FieldCellEvent with _$FieldCellEvent { class FieldCellState with _$FieldCellState { const factory FieldCellState({ required String gridId, - required Field field, + required GridFieldPB field, required double width, }) = _FieldCellState; diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/field_editor_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/field_editor_bloc.dart index 2815d5519d..8d44edf1ff 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/field_editor_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/field_editor_bloc.dart @@ -1,3 +1,4 @@ +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; @@ -6,27 +7,32 @@ import 'package:dartz/dartz.dart'; part 'field_editor_bloc.freezed.dart'; class FieldEditorBloc extends Bloc { + final TypeOptionDataController dataController; + FieldEditorBloc({ required String gridId, required String fieldName, - required IFieldContextLoader fieldContextLoader, - }) : super(FieldEditorState.initial(gridId, fieldName, fieldContextLoader)) { + required IFieldTypeOptionLoader loader, + }) : dataController = TypeOptionDataController(gridId: gridId, loader: loader), + super(FieldEditorState.initial(gridId, fieldName)) { on( (event, emit) async { await event.when( initial: () async { - final fieldContext = GridFieldContext(gridId: gridId, loader: fieldContextLoader); - await fieldContext.loadData().then((result) { - result.fold( - (l) => emit(state.copyWith(fieldContext: Some(fieldContext), name: fieldContext.field.name)), - (r) => null, - ); + dataController.addFieldListener((field) { + if (!isClosed) { + add(FieldEditorEvent.didReceiveFieldChanged(field)); + } }); + await dataController.loadData(); }, updateName: (name) { - state.fieldContext.fold(() => null, (fieldContext) => fieldContext.fieldName = name); + dataController.fieldName = name; emit(state.copyWith(name: name)); }, + didReceiveFieldChanged: (GridFieldPB field) { + emit(state.copyWith(field: Some(field))); + }, ); }, ); @@ -42,6 +48,7 @@ class FieldEditorBloc extends Bloc { class FieldEditorEvent with _$FieldEditorEvent { const factory FieldEditorEvent.initial() = _InitialField; const factory FieldEditorEvent.updateName(String name) = _UpdateName; + const factory FieldEditorEvent.didReceiveFieldChanged(GridFieldPB field) = _DidReceiveFieldChanged; } @freezed @@ -50,13 +57,17 @@ class FieldEditorState with _$FieldEditorState { required String gridId, required String errorText, required String name, - required Option fieldContext, + required Option field, }) = _FieldEditorState; - factory FieldEditorState.initial(String gridId, String fieldName, IFieldContextLoader loader) => FieldEditorState( + factory FieldEditorState.initial( + String gridId, + String fieldName, + ) => + FieldEditorState( gridId: gridId, - fieldContext: none(), errorText: '', + field: none(), name: fieldName, ); } diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/field_editor_pannel_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/field_editor_pannel_bloc.dart deleted file mode 100644 index 6310452d48..0000000000 --- a/frontend/app_flowy/lib/workspace/application/grid/field/field_editor_pannel_bloc.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'dart:async'; - -import 'field_service.dart'; - -part 'field_editor_pannel_bloc.freezed.dart'; - -class FieldEditorPannelBloc extends Bloc { - final GridFieldContext _fieldContext; - void Function()? _fieldListenFn; - - FieldEditorPannelBloc(GridFieldContext fieldContext) - : _fieldContext = fieldContext, - super(FieldEditorPannelState.initial(fieldContext)) { - on( - (event, emit) async { - event.when( - initial: () { - _fieldListenFn = fieldContext.addFieldListener((field) { - add(FieldEditorPannelEvent.didReceiveFieldUpdated(field)); - }); - }, - didReceiveFieldUpdated: (field) { - emit(state.copyWith(field: field)); - }, - ); - }, - ); - } - - @override - Future close() async { - if (_fieldListenFn != null) { - _fieldContext.removeFieldListener(_fieldListenFn!); - } - return super.close(); - } -} - -@freezed -class FieldEditorPannelEvent with _$FieldEditorPannelEvent { - const factory FieldEditorPannelEvent.initial() = _Initial; - const factory FieldEditorPannelEvent.didReceiveFieldUpdated(Field field) = _DidReceiveFieldUpdated; -} - -@freezed -class FieldEditorPannelState with _$FieldEditorPannelState { - const factory FieldEditorPannelState({ - required Field field, - }) = _FieldEditorPannelState; - - factory FieldEditorPannelState.initial(GridFieldContext fieldContext) => FieldEditorPannelState( - field: fieldContext.field, - ); -} diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/field_listener.dart b/frontend/app_flowy/lib/workspace/application/grid/field/field_listener.dart index 21bd5befeb..d3b35e2bfc 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/field_listener.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/field_listener.dart @@ -7,7 +7,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -typedef UpdateFieldNotifiedValue = Either; +typedef UpdateFieldNotifiedValue = Either; class SingleFieldListener { final String fieldId; @@ -31,7 +31,7 @@ class SingleFieldListener { switch (ty) { case GridNotification.DidUpdateField: result.fold( - (payload) => _updateFieldNotifier?.value = left(Field.fromBuffer(payload)), + (payload) => _updateFieldNotifier?.value = left(GridFieldPB.fromBuffer(payload)), (error) => _updateFieldNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart b/frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart index 99fef626c0..9274770b21 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/field_service.dart @@ -1,4 +1,5 @@ import 'package:dartz/dartz.dart'; +import 'package:flowy_infra/notifier.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; @@ -9,6 +10,10 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; part 'field_service.freezed.dart'; +/// FieldService consists of lots of event functions. We define the events in the backend(Rust), +/// you can find the corresponding event implementation in event_map.rs of the corresponding crate. +/// +/// You could check out the rust-lib/flowy-grid/event_map.rs for more information. class FieldService { final String gridId; final String fieldId; @@ -16,10 +21,10 @@ class FieldService { FieldService({required this.gridId, required this.fieldId}); Future> moveField(int fromIndex, int toIndex) { - final payload = MoveItemPayload.create() + final payload = MoveItemPayloadPB.create() ..gridId = gridId ..itemId = fieldId - ..ty = MoveItemType.MoveField + ..ty = MoveItemTypePB.MoveField ..fromIndex = fromIndex ..toIndex = toIndex; @@ -34,7 +39,7 @@ class FieldService { double? width, List? typeOptionData, }) { - var payload = FieldChangesetPayload.create() + var payload = FieldChangesetPayloadPB.create() ..gridId = gridId ..fieldId = fieldId; @@ -68,11 +73,11 @@ class FieldService { // Create the field if it does not exist. Otherwise, update the field. static Future> insertField({ required String gridId, - required Field field, + required GridFieldPB field, List? typeOptionData, String? startFieldId, }) { - var payload = InsertFieldPayload.create() + var payload = InsertFieldPayloadPB.create() ..gridId = gridId ..field_2 = field ..typeOptionData = typeOptionData ?? []; @@ -89,7 +94,7 @@ class FieldService { required String fieldId, required List typeOptionData, }) { - var payload = UpdateFieldTypeOptionPayload.create() + var payload = UpdateFieldTypeOptionPayloadPB.create() ..gridId = gridId ..fieldId = fieldId ..typeOptionData = typeOptionData; @@ -98,7 +103,7 @@ class FieldService { } Future> deleteField() { - final payload = FieldIdentifierPayload.create() + final payload = DeleteFieldPayloadPB.create() ..gridId = gridId ..fieldId = fieldId; @@ -106,17 +111,17 @@ class FieldService { } Future> duplicateField() { - final payload = FieldIdentifierPayload.create() + final payload = DuplicateFieldPayloadPB.create() ..gridId = gridId ..fieldId = fieldId; return GridEventDuplicateField(payload).send(); } - Future> getFieldTypeOptionData({ + Future> getFieldTypeOptionData({ required FieldType fieldType, }) { - final payload = EditFieldPayload.create() + final payload = GridFieldTypeOptionIdPB.create() ..gridId = gridId ..fieldId = fieldId ..fieldType = fieldType; @@ -133,16 +138,16 @@ class FieldService { class GridFieldCellContext with _$GridFieldCellContext { const factory GridFieldCellContext({ required String gridId, - required Field field, + required GridFieldPB field, }) = _GridFieldCellContext; } -abstract class IFieldContextLoader { +abstract class IFieldTypeOptionLoader { String get gridId; - Future> load(); + Future> load(); - Future> switchToField(String fieldId, FieldType fieldType) { - final payload = EditFieldPayload.create() + Future> switchToField(String fieldId, FieldType fieldType) { + final payload = EditFieldPayloadPB.create() ..gridId = gridId ..fieldId = fieldId ..fieldType = fieldType; @@ -151,16 +156,16 @@ abstract class IFieldContextLoader { } } -class NewFieldContextLoader extends IFieldContextLoader { +class NewFieldTypeOptionLoader extends IFieldTypeOptionLoader { @override final String gridId; - NewFieldContextLoader({ + NewFieldTypeOptionLoader({ required this.gridId, }); @override - Future> load() { - final payload = EditFieldPayload.create() + Future> load() { + final payload = CreateFieldPayloadPB.create() ..gridId = gridId ..fieldType = FieldType.RichText; @@ -168,19 +173,19 @@ class NewFieldContextLoader extends IFieldContextLoader { } } -class FieldContextLoader extends IFieldContextLoader { +class FieldTypeOptionLoader extends IFieldTypeOptionLoader { @override final String gridId; - final Field field; + final GridFieldPB field; - FieldContextLoader({ + FieldTypeOptionLoader({ required this.gridId, required this.field, }); @override - Future> load() { - final payload = EditFieldPayload.create() + Future> load() { + final payload = GridFieldTypeOptionIdPB.create() ..gridId = gridId ..fieldId = field.id ..fieldType = field.fieldType; @@ -189,16 +194,16 @@ class FieldContextLoader extends IFieldContextLoader { } } -class GridFieldContext { +class TypeOptionDataController { final String gridId; - final IFieldContextLoader _loader; + final IFieldTypeOptionLoader _loader; - late FieldTypeOptionData _data; - ValueNotifier? _fieldNotifier; + late FieldTypeOptionDataPB _data; + final PublishNotifier _fieldNotifier = PublishNotifier(); - GridFieldContext({ + TypeOptionDataController({ required this.gridId, - required IFieldContextLoader loader, + required IFieldTypeOptionLoader loader, }) : _loader = loader; Future> loadData() async { @@ -207,13 +212,7 @@ class GridFieldContext { (data) { data.freeze(); _data = data; - - if (_fieldNotifier == null) { - _fieldNotifier = ValueNotifier(data.field_2); - } else { - _fieldNotifier?.value = data.field_2; - } - + _fieldNotifier.value = data.field_2; return left(unit); }, (err) { @@ -223,9 +222,9 @@ class GridFieldContext { ); } - Field get field => _data.field_2; + GridFieldPB get field => _data.field_2; - set field(Field field) { + set field(GridFieldPB field) { _updateData(newField: field); } @@ -239,7 +238,7 @@ class GridFieldContext { _updateData(newTypeOptionData: typeOptionData); } - void _updateData({String? newName, Field? newField, List? newTypeOptionData}) { + void _updateData({String? newName, GridFieldPB? newField, List? newTypeOptionData}) { _data = _data.rebuild((rebuildData) { if (newName != null) { rebuildData.field_2 = rebuildData.field_2.rebuild((rebuildField) { @@ -256,9 +255,7 @@ class GridFieldContext { } }); - if (_data.field_2 != _fieldNotifier?.value) { - _fieldNotifier?.value = _data.field_2; - } + _fieldNotifier.value = _data.field_2; FieldService.insertField( gridId: gridId, @@ -283,16 +280,16 @@ class GridFieldContext { }); } - void Function() addFieldListener(void Function(Field) callback) { + void Function() addFieldListener(void Function(GridFieldPB) callback) { listener() { callback(field); } - _fieldNotifier?.addListener(listener); + _fieldNotifier.addListener(listener); return listener; } void removeFieldListener(void Function() listener) { - _fieldNotifier?.removeListener(listener); + _fieldNotifier.removeListener(listener); } } diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/field_type_option_edit_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/field_type_option_edit_bloc.dart new file mode 100644 index 0000000000..e098f87d86 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/grid/field/field_type_option_edit_bloc.dart @@ -0,0 +1,57 @@ +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +import 'field_service.dart'; + +part 'field_type_option_edit_bloc.freezed.dart'; + +class FieldTypeOptionEditBloc extends Bloc { + final TypeOptionDataController _dataController; + void Function()? _fieldListenFn; + + FieldTypeOptionEditBloc(TypeOptionDataController dataController) + : _dataController = dataController, + super(FieldTypeOptionEditState.initial(dataController)) { + on( + (event, emit) async { + event.when( + initial: () { + _fieldListenFn = dataController.addFieldListener((field) { + add(FieldTypeOptionEditEvent.didReceiveFieldUpdated(field)); + }); + }, + didReceiveFieldUpdated: (field) { + emit(state.copyWith(field: field)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_fieldListenFn != null) { + _dataController.removeFieldListener(_fieldListenFn!); + } + return super.close(); + } +} + +@freezed +class FieldTypeOptionEditEvent with _$FieldTypeOptionEditEvent { + const factory FieldTypeOptionEditEvent.initial() = _Initial; + const factory FieldTypeOptionEditEvent.didReceiveFieldUpdated(GridFieldPB field) = _DidReceiveFieldUpdated; +} + +@freezed +class FieldTypeOptionEditState with _$FieldTypeOptionEditState { + const factory FieldTypeOptionEditState({ + required GridFieldPB field, + }) = _FieldTypeOptionEditState; + + factory FieldTypeOptionEditState.initial(TypeOptionDataController fieldContext) => FieldTypeOptionEditState( + field: fieldContext.field, + ); +} diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/grid_listenr.dart b/frontend/app_flowy/lib/workspace/application/grid/field/grid_listenr.dart index 00e94bb3e1..67bec17be7 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/grid_listenr.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/grid_listenr.dart @@ -7,7 +7,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -typedef UpdateFieldNotifiedValue = Either; +typedef UpdateFieldNotifiedValue = Either; class GridFieldsListener { final String gridId; @@ -27,7 +27,7 @@ class GridFieldsListener { switch (ty) { case GridNotification.DidUpdateGridField: result.fold( - (payload) => updateFieldsNotifier?.value = left(GridFieldChangeset.fromBuffer(payload)), + (payload) => updateFieldsNotifier?.value = left(GridFieldChangesetPB.fromBuffer(payload)), (error) => updateFieldsNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart index 8784422c54..00deff56e7 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart @@ -1,14 +1,15 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; import 'package:protobuf/protobuf.dart'; part 'date_bloc.freezed.dart'; -typedef DateTypeOptionContext = TypeOptionContext; +typedef DateTypeOptionContext = TypeOptionWidgetContext; -class DateTypeOptionDataBuilder extends TypeOptionDataBuilder { +class DateTypeOptionDataParser extends TypeOptionDataParser { @override DateTypeOption fromBuffer(List buffer) { return DateTypeOption.fromBuffer(buffer); diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/edit_select_option_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/edit_select_option_bloc.dart index f0969e2793..9f1dd4dd1d 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/edit_select_option_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/edit_select_option_bloc.dart @@ -7,7 +7,7 @@ import 'package:dartz/dartz.dart'; part 'edit_select_option_bloc.freezed.dart'; class EditSelectOptionBloc extends Bloc { - EditSelectOptionBloc({required SelectOption option}) : super(EditSelectOptionState.initial(option)) { + EditSelectOptionBloc({required SelectOptionPB option}) : super(EditSelectOptionState.initial(option)) { on( (event, emit) async { event.map( @@ -30,14 +30,14 @@ class EditSelectOptionBloc extends Bloc deleted, }) = _EditSelectOptionState; - factory EditSelectOptionState.initial(SelectOption option) => EditSelectOptionState( + factory EditSelectOptionState.initial(SelectOptionPB option) => EditSelectOptionState( option: option, deleted: none(), ); diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_type_option.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_type_option.dart index 0a6e2de14c..e0e242305b 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_type_option.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/multi_select_type_option.dart @@ -7,21 +7,22 @@ import 'package:protobuf/protobuf.dart'; import 'select_option_type_option_bloc.dart'; import 'type_option_service.dart'; -class MultiSelectTypeOptionContext extends TypeOptionContext with SelectOptionTypeOptionAction { +class MultiSelectTypeOptionContext extends TypeOptionWidgetContext + with SelectOptionTypeOptionAction { final TypeOptionService service; MultiSelectTypeOptionContext({ - required MultiSelectTypeOptionDataBuilder dataBuilder, - required GridFieldContext fieldContext, + required MultiSelectTypeOptionWidgetDataParser dataBuilder, + required TypeOptionDataController dataController, }) : service = TypeOptionService( - gridId: fieldContext.gridId, - fieldId: fieldContext.field.id, + gridId: dataController.gridId, + fieldId: dataController.field.id, ), - super(dataBuilder: dataBuilder, fieldContext: fieldContext); + super(dataParser: dataBuilder, dataController: dataController); @override - List Function(SelectOption) get deleteOption { - return (SelectOption option) { + List Function(SelectOptionPB) get deleteOption { + return (SelectOptionPB option) { typeOption.freeze(); typeOption = typeOption.rebuild((typeOption) { final index = typeOption.options.indexWhere((element) => element.id == option.id); @@ -34,7 +35,7 @@ class MultiSelectTypeOptionContext extends TypeOptionContext> Function(String) get insertOption { + Future> Function(String) get insertOption { return (String optionName) { return service.newOption(name: optionName).then((result) { return result.fold( @@ -56,8 +57,8 @@ class MultiSelectTypeOptionContext extends TypeOptionContext Function(SelectOption) get udpateOption { - return (SelectOption option) { + List Function(SelectOptionPB) get udpateOption { + return (SelectOptionPB option) { typeOption.freeze(); typeOption = typeOption.rebuild((typeOption) { final index = typeOption.options.indexWhere((element) => element.id == option.id); @@ -70,7 +71,7 @@ class MultiSelectTypeOptionContext extends TypeOptionContext { +class MultiSelectTypeOptionWidgetDataParser extends TypeOptionDataParser { @override MultiSelectTypeOption fromBuffer(List buffer) { return MultiSelectTypeOption.fromBuffer(buffer); diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/number_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/number_bloc.dart index a708668066..804ce3ee11 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/number_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/number_bloc.dart @@ -8,9 +8,9 @@ import 'package:protobuf/protobuf.dart'; part 'number_bloc.freezed.dart'; -typedef NumberTypeOptionContext = TypeOptionContext; +typedef NumberTypeOptionContext = TypeOptionWidgetContext; -class NumberTypeOptionDataBuilder extends TypeOptionDataBuilder { +class NumberTypeOptionWidgetDataParser extends TypeOptionDataParser { @override NumberTypeOption fromBuffer(List buffer) { return NumberTypeOption.fromBuffer(buffer); diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/select_option_type_option_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/select_option_type_option_bloc.dart index ff8244546b..b77b9b86cd 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/select_option_type_option_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/select_option_type_option_bloc.dart @@ -6,25 +6,25 @@ import 'package:dartz/dartz.dart'; part 'select_option_type_option_bloc.freezed.dart'; abstract class SelectOptionTypeOptionAction { - Future> Function(String) get insertOption; + Future> Function(String) get insertOption; - List Function(SelectOption) get deleteOption; + List Function(SelectOptionPB) get deleteOption; - List Function(SelectOption) get udpateOption; + List Function(SelectOptionPB) get udpateOption; } class SelectOptionTypeOptionBloc extends Bloc { final SelectOptionTypeOptionAction typeOptionAction; SelectOptionTypeOptionBloc({ - required List options, + required List options, required this.typeOptionAction, }) : super(SelectOptionTypeOptionState.initial(options)) { on( (event, emit) async { await event.when( createOption: (optionName) async { - final List options = await typeOptionAction.insertOption(optionName); + final List options = await typeOptionAction.insertOption(optionName); emit(state.copyWith(options: options)); }, addingOption: () { @@ -34,11 +34,11 @@ class SelectOptionTypeOptionBloc extends Bloc options = typeOptionAction.udpateOption(option); + final List options = typeOptionAction.udpateOption(option); emit(state.copyWith(options: options)); }, deleteOption: (option) { - final List options = typeOptionAction.deleteOption(option); + final List options = typeOptionAction.deleteOption(option); emit(state.copyWith(options: options)); }, ); @@ -57,19 +57,19 @@ class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent { const factory SelectOptionTypeOptionEvent.createOption(String optionName) = _CreateOption; const factory SelectOptionTypeOptionEvent.addingOption() = _AddingOption; const factory SelectOptionTypeOptionEvent.endAddingOption() = _EndAddingOption; - const factory SelectOptionTypeOptionEvent.updateOption(SelectOption option) = _UpdateOption; - const factory SelectOptionTypeOptionEvent.deleteOption(SelectOption option) = _DeleteOption; + const factory SelectOptionTypeOptionEvent.updateOption(SelectOptionPB option) = _UpdateOption; + const factory SelectOptionTypeOptionEvent.deleteOption(SelectOptionPB option) = _DeleteOption; } @freezed class SelectOptionTypeOptionState with _$SelectOptionTypeOptionState { const factory SelectOptionTypeOptionState({ - required List options, + required List options, required bool isEditingOption, required Option newOptionName, }) = _SelectOptionTyepOptionState; - factory SelectOptionTypeOptionState.initial(List options) => SelectOptionTypeOptionState( + factory SelectOptionTypeOptionState.initial(List options) => SelectOptionTypeOptionState( options: options, isEditingOption: false, newOptionName: none(), diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_type_option.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_type_option.dart index c09d17f11e..857838de38 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_type_option.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/single_select_type_option.dart @@ -7,22 +7,22 @@ import 'package:protobuf/protobuf.dart'; import 'select_option_type_option_bloc.dart'; import 'type_option_service.dart'; -class SingleSelectTypeOptionContext extends TypeOptionContext +class SingleSelectTypeOptionContext extends TypeOptionWidgetContext with SelectOptionTypeOptionAction { final TypeOptionService service; SingleSelectTypeOptionContext({ - required SingleSelectTypeOptionDataBuilder dataBuilder, - required GridFieldContext fieldContext, + required SingleSelectTypeOptionWidgetDataParser dataBuilder, + required TypeOptionDataController fieldContext, }) : service = TypeOptionService( gridId: fieldContext.gridId, fieldId: fieldContext.field.id, ), - super(dataBuilder: dataBuilder, fieldContext: fieldContext); + super(dataParser: dataBuilder, dataController: fieldContext); @override - List Function(SelectOption) get deleteOption { - return (SelectOption option) { + List Function(SelectOptionPB) get deleteOption { + return (SelectOptionPB option) { typeOption.freeze(); typeOption = typeOption.rebuild((typeOption) { final index = typeOption.options.indexWhere((element) => element.id == option.id); @@ -35,7 +35,7 @@ class SingleSelectTypeOptionContext extends TypeOptionContext> Function(String) get insertOption { + Future> Function(String) get insertOption { return (String optionName) { return service.newOption(name: optionName).then((result) { return result.fold( @@ -57,8 +57,8 @@ class SingleSelectTypeOptionContext extends TypeOptionContext Function(SelectOption) get udpateOption { - return (SelectOption option) { + List Function(SelectOptionPB) get udpateOption { + return (SelectOptionPB option) { typeOption.freeze(); typeOption = typeOption.rebuild((typeOption) { final index = typeOption.options.indexWhere((element) => element.id == option.id); @@ -71,9 +71,9 @@ class SingleSelectTypeOptionContext extends TypeOptionContext { +class SingleSelectTypeOptionWidgetDataParser extends TypeOptionDataParser { @override - SingleSelectTypeOption fromBuffer(List buffer) { - return SingleSelectTypeOption.fromBuffer(buffer); + SingleSelectTypeOptionPB fromBuffer(List buffer) { + return SingleSelectTypeOptionPB.fromBuffer(buffer); } } diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart index 3a30ef30ca..fca6995b3f 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/type_option_service.dart @@ -18,51 +18,48 @@ class TypeOptionService { required this.fieldId, }); - Future> newOption({ + Future> newOption({ required String name, }) { - final fieldIdentifier = FieldIdentifierPayload.create() + final payload = CreateSelectOptionPayloadPB.create() + ..optionName = name ..gridId = gridId ..fieldId = fieldId; - final payload = CreateSelectOptionPayload.create() - ..optionName = name - ..fieldIdentifier = fieldIdentifier; - return GridEventNewSelectOption(payload).send(); } } -abstract class TypeOptionDataBuilder { +abstract class TypeOptionDataParser { T fromBuffer(List buffer); } -class TypeOptionContext { +class TypeOptionWidgetContext { T? _typeOptionObject; - final GridFieldContext _fieldContext; - final TypeOptionDataBuilder dataBuilder; + final TypeOptionDataController _dataController; + final TypeOptionDataParser dataParser; - TypeOptionContext({ - required this.dataBuilder, - required GridFieldContext fieldContext, - }) : _fieldContext = fieldContext; + TypeOptionWidgetContext({ + required this.dataParser, + required TypeOptionDataController dataController, + }) : _dataController = dataController; - String get gridId => _fieldContext.gridId; + String get gridId => _dataController.gridId; - Field get field => _fieldContext.field; + GridFieldPB get field => _dataController.field; T get typeOption { if (_typeOptionObject != null) { return _typeOptionObject!; } - final T object = dataBuilder.fromBuffer(_fieldContext.typeOptionData); + final T object = dataParser.fromBuffer(_dataController.typeOptionData); _typeOptionObject = object; return object; } set typeOption(T typeOption) { - _fieldContext.typeOptionData = typeOption.writeToBuffer(); + _dataController.typeOptionData = typeOption.writeToBuffer(); _typeOptionObject = typeOption; } } @@ -74,10 +71,10 @@ abstract class TypeOptionFieldDelegate { class TypeOptionContext2 { final String gridId; - final Field field; + final GridFieldPB field; final FieldService _fieldService; T? _data; - final TypeOptionDataBuilder dataBuilder; + final TypeOptionDataParser dataBuilder; TypeOptionContext2({ required this.gridId, diff --git a/frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart index 49e90b6a9e..19c4049224 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/grid_bloc.dart @@ -7,7 +7,7 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'block/block_service.dart'; +import 'block/block_cache.dart'; import 'grid_service.dart'; import 'row/row_service.dart'; import 'dart:collection'; @@ -20,17 +20,17 @@ class GridBloc extends Bloc { final GridFieldCache fieldCache; // key: the block id - final LinkedHashMap _blocks; + final LinkedHashMap _blocks; - List get rows { - final List rows = []; + List get rowInfos { + final List rows = []; for (var block in _blocks.values) { rows.addAll(block.rows); } return rows; } - GridBloc({required View view}) + GridBloc({required ViewPB view}) : gridId = view.id, _blocks = LinkedHashMap.identity(), _gridService = GridService(gridId: view.id), @@ -46,11 +46,11 @@ class GridBloc extends Bloc { createRow: () { _gridService.createRow(); }, - didReceiveRowUpdate: (rows, reason) { - emit(state.copyWith(rows: rows, reason: reason)); + didReceiveRowUpdate: (newRowInfos, reason) { + emit(state.copyWith(rowInfos: newRowInfos, reason: reason)); }, didReceiveFieldUpdate: (fields) { - emit(state.copyWith(rows: rows, fields: GridFieldEquatable(fields))); + emit(state.copyWith(rowInfos: rowInfos, fields: GridFieldEquatable(fields))); }, ); }, @@ -68,8 +68,8 @@ class GridBloc extends Bloc { return super.close(); } - GridRowCacheService? getRowCache(String blockId, String rowId) { - final GridBlockCacheService? blockCache = _blocks[blockId]; + GridRowCache? getRowCache(String blockId, String rowId) { + final GridBlockCache? blockCache = _blocks[blockId]; return blockCache?.rowCache; } @@ -93,8 +93,8 @@ class GridBloc extends Bloc { ); } - Future _loadFields(Grid grid, Emitter emit) async { - final result = await _gridService.getFields(fieldOrders: grid.fieldOrders); + Future _loadFields(GridPB grid, Emitter emit) async { + final result = await _gridService.getFields(fieldIds: grid.fields); return Future( () => result.fold( (fields) { @@ -103,7 +103,7 @@ class GridBloc extends Bloc { emit(state.copyWith( grid: Some(grid), fields: GridFieldEquatable(fieldCache.fields), - rows: rows, + rowInfos: rowInfos, loadingState: GridLoadingState.finish(left(unit)), )); }, @@ -112,14 +112,14 @@ class GridBloc extends Bloc { ); } - void _initialBlocks(List blocks) { + void _initialBlocks(List blocks) { for (final block in blocks) { if (_blocks[block.id] != null) { Log.warn("Intial duplicate block's cache: ${block.id}"); return; } - final cache = GridBlockCacheService( + final cache = GridBlockCache( gridId: gridId, block: block, fieldCache: fieldCache, @@ -127,7 +127,7 @@ class GridBloc extends Bloc { cache.addListener( listenWhen: () => !isClosed, - onChangeReason: (reason) => add(GridEvent.didReceiveRowUpdate(rows, reason)), + onChangeReason: (reason) => add(GridEvent.didReceiveRowUpdate(rowInfos, reason)), ); _blocks[block.id] = cache; @@ -139,24 +139,25 @@ class GridBloc extends Bloc { class GridEvent with _$GridEvent { const factory GridEvent.initial() = InitialGrid; const factory GridEvent.createRow() = _CreateRow; - const factory GridEvent.didReceiveRowUpdate(List rows, GridRowChangeReason listState) = _DidReceiveRowUpdate; - const factory GridEvent.didReceiveFieldUpdate(List fields) = _DidReceiveFieldUpdate; + const factory GridEvent.didReceiveRowUpdate(List rows, GridRowChangeReason listState) = + _DidReceiveRowUpdate; + const factory GridEvent.didReceiveFieldUpdate(List fields) = _DidReceiveFieldUpdate; } @freezed class GridState with _$GridState { const factory GridState({ required String gridId, - required Option grid, + required Option grid, required GridFieldEquatable fields, - required List rows, + required List rowInfos, required GridLoadingState loadingState, required GridRowChangeReason reason, }) = _GridState; factory GridState.initial(String gridId) => GridState( fields: const GridFieldEquatable([]), - rows: [], + rowInfos: [], grid: none(), gridId: gridId, loadingState: const _Loading(), @@ -171,8 +172,8 @@ class GridLoadingState with _$GridLoadingState { } class GridFieldEquatable extends Equatable { - final List _fields; - const GridFieldEquatable(List fields) : _fields = fields; + final List _fields; + const GridFieldEquatable(List fields) : _fields = fields; @override List get props { @@ -182,5 +183,5 @@ class GridFieldEquatable extends Equatable { ]; } - UnmodifiableListView get value => UnmodifiableListView(_fields); + UnmodifiableListView get value => UnmodifiableListView(_fields); } diff --git a/frontend/app_flowy/lib/workspace/application/grid/grid_header_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/grid_header_bloc.dart index 3e4db25b3d..b34064da13 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/grid_header_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/grid_header_bloc.dart @@ -34,7 +34,7 @@ class GridHeaderBloc extends Bloc { } Future _moveField(_MoveField value, Emitter emit) async { - final fields = List.from(state.fields); + final fields = List.from(state.fields); fields.insert(value.toIndex, fields.removeAt(value.fromIndex)); emit(state.copyWith(fields: fields)); @@ -62,16 +62,16 @@ class GridHeaderBloc extends Bloc { @freezed class GridHeaderEvent with _$GridHeaderEvent { const factory GridHeaderEvent.initial() = _InitialHeader; - const factory GridHeaderEvent.didReceiveFieldUpdate(List fields) = _DidReceiveFieldUpdate; - const factory GridHeaderEvent.moveField(Field field, int fromIndex, int toIndex) = _MoveField; + const factory GridHeaderEvent.didReceiveFieldUpdate(List fields) = _DidReceiveFieldUpdate; + const factory GridHeaderEvent.moveField(GridFieldPB field, int fromIndex, int toIndex) = _MoveField; } @freezed class GridHeaderState with _$GridHeaderState { - const factory GridHeaderState({required List fields}) = _GridHeaderState; + const factory GridHeaderState({required List fields}) = _GridHeaderState; - factory GridHeaderState.initial(List fields) { - // final List newFields = List.from(fields); + factory GridHeaderState.initial(List fields) { + // final List newFields = List.from(fields); // newFields.retainWhere((field) => field.visibility); return GridHeaderState(fields: fields); } diff --git a/frontend/app_flowy/lib/workspace/application/grid/grid_service.dart b/frontend/app_flowy/lib/workspace/application/grid/grid_service.dart index 391a5d13a2..1f8b4336fc 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/grid_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/grid_service.dart @@ -19,55 +19,54 @@ class GridService { required this.gridId, }); - Future> loadGrid() async { - await FolderEventSetLatestView(ViewId(value: gridId)).send(); + Future> loadGrid() async { + await FolderEventSetLatestView(ViewIdPB(value: gridId)).send(); - final payload = GridId(value: gridId); + final payload = GridIdPB(value: gridId); return GridEventGetGrid(payload).send(); } - Future> createRow({Option? startRowId}) { - CreateRowPayload payload = CreateRowPayload.create()..gridId = gridId; + Future> createRow({Option? startRowId}) { + CreateRowPayloadPB payload = CreateRowPayloadPB.create()..gridId = gridId; startRowId?.fold(() => null, (id) => payload.startRowId = id); return GridEventCreateRow(payload).send(); } - Future> getFields({required List fieldOrders}) { - final payload = QueryFieldPayload.create() + Future> getFields({required List fieldIds}) { + final payload = QueryFieldPayloadPB.create() ..gridId = gridId - ..fieldOrders = RepeatedFieldOrder(items: fieldOrders); + ..fieldIds = RepeatedGridFieldIdPB(items: fieldIds); return GridEventGetFields(payload).send(); } Future> closeGrid() { - final request = ViewId(value: gridId); + final request = ViewIdPB(value: gridId); return FolderEventCloseView(request).send(); } } class FieldsNotifier extends ChangeNotifier { - List _fields = []; + List _fields = []; - set fields(List fields) { + set fields(List fields) { _fields = fields; notifyListeners(); } - List get fields => _fields; + List get fields => _fields; } -typedef FieldChangesetCallback = void Function(GridFieldChangeset); -typedef FieldsCallback = void Function(List); +typedef FieldChangesetCallback = void Function(GridFieldChangesetPB); +typedef FieldsCallback = void Function(List); class GridFieldCache { final String gridId; - late final GridFieldsListener _fieldListener; + final GridFieldsListener _fieldListener; FieldsNotifier? _fieldNotifier = FieldsNotifier(); final Map _fieldsCallbackMap = {}; final Map _changesetCallbackMap = {}; - GridFieldCache({required this.gridId}) { - _fieldListener = GridFieldsListener(gridId: gridId); + GridFieldCache({required this.gridId}) : _fieldListener = GridFieldsListener(gridId: gridId) { _fieldListener.start(onFieldsChanged: (result) { result.fold( (changeset) { @@ -89,11 +88,11 @@ class GridFieldCache { _fieldNotifier = null; } - UnmodifiableListView get unmodifiableFields => UnmodifiableListView(_fieldNotifier?.fields ?? []); + UnmodifiableListView get unmodifiableFields => UnmodifiableListView(_fieldNotifier?.fields ?? []); - List get fields => [..._fieldNotifier?.fields ?? []]; + List get fields => [..._fieldNotifier?.fields ?? []]; - set fields(List fields) { + set fields(List fields) { _fieldNotifier?.fields = [...fields]; } @@ -142,12 +141,12 @@ class GridFieldCache { } } - void _deleteFields(List deletedFields) { + void _deleteFields(List deletedFields) { if (deletedFields.isEmpty) { return; } - final List newFields = fields; - final Map deletedFieldMap = { + final List newFields = fields; + final Map deletedFieldMap = { for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder }; @@ -155,11 +154,11 @@ class GridFieldCache { _fieldNotifier?.fields = newFields; } - void _insertFields(List insertedFields) { + void _insertFields(List insertedFields) { if (insertedFields.isEmpty) { return; } - final List newFields = fields; + final List newFields = fields; for (final indexField in insertedFields) { if (newFields.length > indexField.index) { newFields.insert(indexField.index, indexField.field_1); @@ -170,11 +169,11 @@ class GridFieldCache { _fieldNotifier?.fields = newFields; } - void _updateFields(List updatedFields) { + void _updateFields(List updatedFields) { if (updatedFields.isEmpty) { return; } - final List newFields = fields; + final List newFields = fields; for (final updatedField in updatedFields) { final index = newFields.indexWhere((field) => field.id == updatedField.id); if (index != -1) { @@ -186,14 +185,14 @@ class GridFieldCache { } } -class GridRowCacheDelegateImpl extends GridRowCacheDelegate { +class GridRowCacheFieldNotifierImpl extends GridRowCacheFieldNotifier { final GridFieldCache _cache; FieldChangesetCallback? _onChangesetFn; FieldsCallback? _onFieldFn; - GridRowCacheDelegateImpl(GridFieldCache cache) : _cache = cache; + GridRowCacheFieldNotifierImpl(GridFieldCache cache) : _cache = cache; @override - UnmodifiableListView get fields => _cache.unmodifiableFields; + UnmodifiableListView get fields => _cache.unmodifiableFields; @override void onFieldsChanged(VoidCallback callback) { @@ -202,8 +201,8 @@ class GridRowCacheDelegateImpl extends GridRowCacheDelegate { } @override - void onFieldUpdated(void Function(Field) callback) { - _onChangesetFn = (GridFieldChangeset changeset) { + void onFieldChanged(void Function(GridFieldPB) callback) { + _onChangesetFn = (GridFieldChangesetPB changeset) { for (final updatedField in changeset.updatedFields) { callback(updatedField); } diff --git a/frontend/app_flowy/lib/workspace/application/grid/prelude.dart b/frontend/app_flowy/lib/workspace/application/grid/prelude.dart index 6ea198f303..2a19ca1134 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/prelude.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/prelude.dart @@ -4,18 +4,18 @@ export 'row/row_service.dart'; export 'grid_service.dart'; export 'grid_header_bloc.dart'; -// Field +// GridFieldPB export 'field/field_service.dart'; export 'field/field_action_sheet_bloc.dart'; export 'field/field_editor_bloc.dart'; -export 'field/field_editor_pannel_bloc.dart'; +export 'field/field_type_option_edit_bloc.dart'; -// Field Type Option +// GridFieldPB Type Option export 'field/type_option/date_bloc.dart'; export 'field/type_option/number_bloc.dart'; export 'field/type_option/single_select_type_option.dart'; -// Cell +// GridCellPB export 'cell/text_cell_bloc.dart'; export 'cell/number_cell_bloc.dart'; export 'cell/select_option_cell_bloc.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/grid/row/row_action_sheet_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/row/row_action_sheet_bloc.dart index 8754917223..9fe12f3ff5 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/row/row_action_sheet_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/row/row_action_sheet_bloc.dart @@ -11,11 +11,11 @@ part 'row_action_sheet_bloc.freezed.dart'; class RowActionSheetBloc extends Bloc { final RowService _rowService; - RowActionSheetBloc({required GridRow rowData}) + RowActionSheetBloc({required GridRowInfo rowData}) : _rowService = RowService( gridId: rowData.gridId, blockId: rowData.blockId, - rowId: rowData.rowId, + rowId: rowData.id, ), super(RowActionSheetState.initial(rowData)) { on( @@ -53,10 +53,10 @@ class RowActionSheetEvent with _$RowActionSheetEvent { @freezed class RowActionSheetState with _$RowActionSheetState { const factory RowActionSheetState({ - required GridRow rowData, + required GridRowInfo rowData, }) = _RowActionSheetState; - factory RowActionSheetState.initial(GridRow rowData) => RowActionSheetState( + factory RowActionSheetState.initial(GridRowInfo rowData) => RowActionSheetState( rowData: rowData, ); } diff --git a/frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart index 7f565b500a..1d7224383d 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/row/row_bloc.dart @@ -11,19 +11,19 @@ part 'row_bloc.freezed.dart'; class RowBloc extends Bloc { final RowService _rowService; - final GridRowCacheService _rowCache; + final GridRowCache _rowCache; void Function()? _rowListenFn; RowBloc({ - required GridRow rowData, - required GridRowCacheService rowCache, + required GridRowInfo rowInfo, + required GridRowCache rowCache, }) : _rowService = RowService( - gridId: rowData.gridId, - blockId: rowData.blockId, - rowId: rowData.rowId, + gridId: rowInfo.gridId, + blockId: rowInfo.blockId, + rowId: rowInfo.id, ), _rowCache = rowCache, - super(RowState.initial(rowData, rowCache.loadGridCells(rowData.rowId))) { + super(RowState.initial(rowInfo, rowCache.loadGridCells(rowInfo.id))) { on( (event, emit) async { await event.map( @@ -58,7 +58,7 @@ class RowBloc extends Bloc { Future _startListening() async { _rowListenFn = _rowCache.addListener( - rowId: state.rowData.rowId, + rowId: state.rowInfo.id, onCellUpdated: (cellDatas, reason) => add(RowEvent.didReceiveCellDatas(cellDatas, reason)), listenWhen: () => !isClosed, ); @@ -76,23 +76,23 @@ class RowEvent with _$RowEvent { @freezed class RowState with _$RowState { const factory RowState({ - required GridRow rowData, + required GridRowInfo rowInfo, required GridCellMap gridCellMap, required UnmodifiableListView snapshots, GridRowChangeReason? changeReason, }) = _RowState; - factory RowState.initial(GridRow rowData, GridCellMap cellDataMap) => RowState( - rowData: rowData, + factory RowState.initial(GridRowInfo rowInfo, GridCellMap cellDataMap) => RowState( + rowInfo: rowInfo, gridCellMap: cellDataMap, snapshots: UnmodifiableListView(cellDataMap.values.map((e) => GridCellEquatable(e.field)).toList()), ); } class GridCellEquatable extends Equatable { - final Field _field; + final GridFieldPB _field; - const GridCellEquatable(Field field) : _field = field; + const GridCellEquatable(GridFieldPB field) : _field = field; @override List get props => [ diff --git a/frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart index f17f0f4cb6..966310fe8c 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/row/row_detail_bloc.dart @@ -7,13 +7,13 @@ import 'row_service.dart'; part 'row_detail_bloc.freezed.dart'; class RowDetailBloc extends Bloc { - final GridRow rowData; - final GridRowCacheService _rowCache; + final GridRowInfo rowInfo; + final GridRowCache _rowCache; void Function()? _rowListenFn; RowDetailBloc({ - required this.rowData, - required GridRowCacheService rowCache, + required this.rowInfo, + required GridRowCache rowCache, }) : _rowCache = rowCache, super(RowDetailState.initial()) { on( @@ -41,14 +41,14 @@ class RowDetailBloc extends Bloc { Future _startListening() async { _rowListenFn = _rowCache.addListener( - rowId: rowData.rowId, + rowId: rowInfo.id, onCellUpdated: (cellDatas, reason) => add(RowDetailEvent.didReceiveCellDatas(cellDatas.values.toList())), listenWhen: () => !isClosed, ); } Future _loadCellData() async { - final cellDataMap = _rowCache.loadGridCells(rowData.rowId); + final cellDataMap = _rowCache.loadGridCells(rowInfo.id); if (!isClosed) { add(RowDetailEvent.didReceiveCellDatas(cellDataMap.values.toList())); } @@ -58,13 +58,13 @@ class RowDetailBloc extends Bloc { @freezed class RowDetailEvent with _$RowDetailEvent { const factory RowDetailEvent.initial() = _Initial; - const factory RowDetailEvent.didReceiveCellDatas(List gridCells) = _DidReceiveCellDatas; + const factory RowDetailEvent.didReceiveCellDatas(List gridCells) = _DidReceiveCellDatas; } @freezed class RowDetailState with _$RowDetailState { const factory RowDetailState({ - required List gridCells, + required List gridCells, }) = _RowDetailState; factory RowDetailState.initial() => RowDetailState( diff --git a/frontend/app_flowy/lib/workspace/application/grid/row/row_listener.dart b/frontend/app_flowy/lib/workspace/application/grid/row/row_listener.dart index 98fddaeccf..9aa829d617 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/row/row_listener.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/row/row_listener.dart @@ -8,8 +8,8 @@ import 'dart:typed_data'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -typedef UpdateRowNotifiedValue = Either; -typedef UpdateFieldNotifiedValue = Either, FlowyError>; +typedef UpdateRowNotifiedValue = Either; +typedef UpdateFieldNotifiedValue = Either, FlowyError>; class RowListener { final String rowId; @@ -26,7 +26,7 @@ class RowListener { switch (ty) { case GridNotification.DidUpdateRow: result.fold( - (payload) => updateRowNotifier?.value = left(Row.fromBuffer(payload)), + (payload) => updateRowNotifier?.value = left(GridRowPB.fromBuffer(payload)), (error) => updateRowNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart b/frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart index 78e4e6715f..893ebb719b 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/row/row_service.dart @@ -1,5 +1,4 @@ import 'dart:collection'; - import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; @@ -15,44 +14,57 @@ part 'row_service.freezed.dart'; typedef RowUpdateCallback = void Function(); -abstract class GridRowCacheDelegate with GridCellCacheDelegate { - UnmodifiableListView get fields; - void onFieldsChanged(void Function() callback); +abstract class GridRowCacheFieldNotifier { + UnmodifiableListView get fields; + void onFieldsChanged(VoidCallback callback); + void onFieldChanged(void Function(GridFieldPB) callback); void dispose(); } -class GridRowCacheService { +/// Cache the rows in memory +/// Insert / delete / update row +/// +/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information. + +class GridRowCache { final String gridId; - final GridBlock block; - final _Notifier _notifier; - List _rows = []; - final HashMap _rowByRowId; - final GridRowCacheDelegate _delegate; - final GridCellCacheService _cellCache; + final GridBlockPB block; - List get rows => _rows; - GridCellCacheService get cellCache => _cellCache; + /// _rows containers the current block's rows + /// Use List to reverse the order of the GridRow. + List _rowInfos = []; - GridRowCacheService({ + /// Use Map for faster access the raw row data. + final HashMap _rowByRowId; + + final GridCellCache _cellCache; + final GridRowCacheFieldNotifier _fieldNotifier; + final _GridRowChangesetNotifier _rowChangeReasonNotifier; + + UnmodifiableListView get rows => UnmodifiableListView(_rowInfos); + GridCellCache get cellCache => _cellCache; + + GridRowCache({ required this.gridId, required this.block, - required GridRowCacheDelegate delegate, - }) : _cellCache = GridCellCacheService(gridId: gridId, delegate: delegate), + required GridRowCacheFieldNotifier notifier, + }) : _cellCache = GridCellCache(gridId: gridId), _rowByRowId = HashMap(), - _notifier = _Notifier(), - _delegate = delegate { + _rowChangeReasonNotifier = _GridRowChangesetNotifier(), + _fieldNotifier = notifier { // - delegate.onFieldsChanged(() => _notifier.receive(const GridRowChangeReason.fieldDidChange())); - _rows = block.rowInfos.map((rowInfo) => buildGridRow(rowInfo.rowId, rowInfo.height.toDouble())).toList(); + notifier.onFieldsChanged(() => _rowChangeReasonNotifier.receive(const GridRowChangeReason.fieldDidChange())); + notifier.onFieldChanged((field) => _cellCache.remove(field.id)); + _rowInfos = block.rows.map((rowInfo) => buildGridRow(rowInfo.id, rowInfo.height.toDouble())).toList(); } Future dispose() async { - _delegate.dispose(); - _notifier.dispose(); + _fieldNotifier.dispose(); + _rowChangeReasonNotifier.dispose(); await _cellCache.dispose(); } - void applyChangesets(List changesets) { + void applyChangesets(List changesets) { for (final changeset in changesets) { _deleteRows(changeset.deletedRows); _insertRows(changeset.insertedRows); @@ -67,60 +79,59 @@ class GridRowCacheService { return; } - final List newRows = []; + final List newRows = []; final DeletedIndexs deletedIndex = []; final Map deletedRowByRowId = {for (var rowId in deletedRows) rowId: rowId}; - _rows.asMap().forEach((index, row) { - if (deletedRowByRowId[row.rowId] == null) { + _rowInfos.asMap().forEach((index, row) { + if (deletedRowByRowId[row.id] == null) { newRows.add(row); } else { + _rowByRowId.remove(row.id); deletedIndex.add(DeletedIndex(index: index, row: row)); } }); - _rows = newRows; - _notifier.receive(GridRowChangeReason.delete(deletedIndex)); + _rowInfos = newRows; + _rowChangeReasonNotifier.receive(GridRowChangeReason.delete(deletedIndex)); } - void _insertRows(List insertRows) { + void _insertRows(List insertRows) { if (insertRows.isEmpty) { return; } InsertedIndexs insertIndexs = []; - final List newRows = _rows; for (final insertRow in insertRows) { final insertIndex = InsertedIndex( index: insertRow.index, rowId: insertRow.rowId, ); insertIndexs.add(insertIndex); - newRows.insert(insertRow.index, (buildGridRow(insertRow.rowId, insertRow.height.toDouble()))); + _rowInfos.insert(insertRow.index, (buildGridRow(insertRow.rowId, insertRow.height.toDouble()))); } - _notifier.receive(GridRowChangeReason.insert(insertIndexs)); + _rowChangeReasonNotifier.receive(GridRowChangeReason.insert(insertIndexs)); } - void _updateRows(List updatedRows) { + void _updateRows(List updatedRows) { if (updatedRows.isEmpty) { return; } final UpdatedIndexs updatedIndexs = UpdatedIndexs(); - final List newRows = _rows; for (final updatedRow in updatedRows) { final rowId = updatedRow.rowId; - final index = newRows.indexWhere((row) => row.rowId == rowId); + final index = _rowInfos.indexWhere((row) => row.id == rowId); if (index != -1) { _rowByRowId[rowId] = updatedRow.row; - newRows.removeAt(index); - newRows.insert(index, buildGridRow(rowId, updatedRow.row.height.toDouble())); + _rowInfos.removeAt(index); + _rowInfos.insert(index, buildGridRow(rowId, updatedRow.row.height.toDouble())); updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId); } } - _notifier.receive(GridRowChangeReason.update(updatedIndexs)); + _rowChangeReasonNotifier.receive(GridRowChangeReason.update(updatedIndexs)); } void _hideRows(List hideRows) {} @@ -130,8 +141,8 @@ class GridRowCacheService { void onRowsChanged( void Function(GridRowChangeReason) onRowChanged, ) { - _notifier.addListener(() { - onRowChanged(_notifier._reason); + _rowChangeReasonNotifier.addListener(() { + onRowChanged(_rowChangeReasonNotifier.reason); }); } @@ -150,12 +161,12 @@ class GridRowCacheService { final row = _rowByRowId[rowId]; if (row != null) { final GridCellMap cellDataMap = _makeGridCells(rowId, row); - onCellUpdated(cellDataMap, _notifier._reason); + onCellUpdated(cellDataMap, _rowChangeReasonNotifier.reason); } } } - _notifier._reason.whenOrNull( + _rowChangeReasonNotifier.reason.whenOrNull( update: (indexs) { if (indexs[rowId] != null) notifyUpdate(); }, @@ -163,16 +174,16 @@ class GridRowCacheService { ); } - _notifier.addListener(listenrHandler); + _rowChangeReasonNotifier.addListener(listenrHandler); return listenrHandler; } void removeRowListener(VoidCallback callback) { - _notifier.removeListener(callback); + _rowChangeReasonNotifier.removeListener(callback); } GridCellMap loadGridCells(String rowId) { - final Row? data = _rowByRowId[rowId]; + final GridRowPB? data = _rowByRowId[rowId]; if (data == null) { _loadRow(rowId); } @@ -180,7 +191,7 @@ class GridRowCacheService { } Future _loadRow(String rowId) async { - final payload = GridRowIdPayload.create() + final payload = GridRowIdPB.create() ..gridId = gridId ..blockId = block.id ..rowId = rowId; @@ -192,11 +203,11 @@ class GridRowCacheService { ); } - GridCellMap _makeGridCells(String rowId, Row? row) { + GridCellMap _makeGridCells(String rowId, GridRowPB? row) { var cellDataMap = GridCellMap.new(); - for (final field in _delegate.fields) { + for (final field in _fieldNotifier.fields) { if (field.visibility) { - cellDataMap[field.id] = GridCell( + cellDataMap[field.id] = GridCellIdentifier( rowId: rowId, gridId: gridId, field: field, @@ -206,7 +217,7 @@ class GridRowCacheService { return cellDataMap; } - void _refreshRow(OptionalRow optionRow) { + void _refreshRow(OptionalRowPB optionRow) { if (!optionRow.hasRow()) { return; } @@ -214,41 +225,41 @@ class GridRowCacheService { updatedRow.freeze(); _rowByRowId[updatedRow.id] = updatedRow; - final index = _rows.indexWhere((gridRow) => gridRow.rowId == updatedRow.id); + final index = _rowInfos.indexWhere((gridRow) => gridRow.id == updatedRow.id); if (index != -1) { // update the corresponding row in _rows if they are not the same - if (_rows[index].data != updatedRow) { - final row = _rows.removeAt(index).copyWith(data: updatedRow); - _rows.insert(index, row); + if (_rowInfos[index].rawRow != updatedRow) { + final row = _rowInfos.removeAt(index).copyWith(rawRow: updatedRow); + _rowInfos.insert(index, row); // Calculate the update index final UpdatedIndexs updatedIndexs = UpdatedIndexs(); - updatedIndexs[row.rowId] = UpdatedIndex(index: index, rowId: row.rowId); + updatedIndexs[row.id] = UpdatedIndex(index: index, rowId: row.id); // - _notifier.receive(GridRowChangeReason.update(updatedIndexs)); + _rowChangeReasonNotifier.receive(GridRowChangeReason.update(updatedIndexs)); } } } - GridRow buildGridRow(String rowId, double rowHeight) { - return GridRow( + GridRowInfo buildGridRow(String rowId, double rowHeight) { + return GridRowInfo( gridId: gridId, blockId: block.id, - fields: _delegate.fields, - rowId: rowId, + fields: _fieldNotifier.fields, + id: rowId, height: rowHeight, ); } } -class _Notifier extends ChangeNotifier { - GridRowChangeReason _reason = const InitialListState(); +class _GridRowChangesetNotifier extends ChangeNotifier { + GridRowChangeReason reason = const InitialListState(); - _Notifier(); + _GridRowChangesetNotifier(); - void receive(GridRowChangeReason reason) { - _reason = reason; + void receive(GridRowChangeReason newReason) { + reason = newReason; reason.map( insert: (_) => notifyListeners(), delete: (_) => notifyListeners(), @@ -266,8 +277,8 @@ class RowService { RowService({required this.gridId, required this.blockId, required this.rowId}); - Future> createRow() { - CreateRowPayload payload = CreateRowPayload.create() + Future> createRow() { + CreateRowPayloadPB payload = CreateRowPayloadPB.create() ..gridId = gridId ..startRowId = rowId; @@ -275,18 +286,18 @@ class RowService { } Future> moveRow(String rowId, int fromIndex, int toIndex) { - final payload = MoveItemPayload.create() + final payload = MoveItemPayloadPB.create() ..gridId = gridId ..itemId = rowId - ..ty = MoveItemType.MoveRow + ..ty = MoveItemTypePB.MoveRow ..fromIndex = fromIndex ..toIndex = toIndex; return GridEventMoveItem(payload).send(); } - Future> getRow() { - final payload = GridRowIdPayload.create() + Future> getRow() { + final payload = GridRowIdPB.create() ..gridId = gridId ..blockId = blockId ..rowId = rowId; @@ -295,7 +306,7 @@ class RowService { } Future> deleteRow() { - final payload = GridRowIdPayload.create() + final payload = GridRowIdPB.create() ..gridId = gridId ..blockId = blockId ..rowId = rowId; @@ -304,7 +315,7 @@ class RowService { } Future> duplicateRow() { - final payload = GridRowIdPayload.create() + final payload = GridRowIdPB.create() ..gridId = gridId ..blockId = blockId ..rowId = rowId; @@ -314,15 +325,15 @@ class RowService { } @freezed -class GridRow with _$GridRow { - const factory GridRow({ +class GridRowInfo with _$GridRowInfo { + const factory GridRowInfo({ required String gridId, required String blockId, - required String rowId, - required UnmodifiableListView fields, + required String id, + required UnmodifiableListView fields, required double height, - Row? data, - }) = _GridRow; + GridRowPB? rawRow, + }) = _GridRowInfo; } typedef InsertedIndexs = List; @@ -349,7 +360,7 @@ class InsertedIndex { class DeletedIndex { final int index; - final GridRow row; + final GridRowInfo row; DeletedIndex({ required this.index, required this.row, diff --git a/frontend/app_flowy/lib/workspace/application/grid/setting/property_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/setting/property_bloc.dart index 10c59559d3..ee16eb0455 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/setting/property_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/setting/property_bloc.dart @@ -10,7 +10,7 @@ part 'property_bloc.freezed.dart'; class GridPropertyBloc extends Bloc { final GridFieldCache _fieldCache; - Function(List)? _onFieldsFn; + Function(List)? _onFieldsFn; GridPropertyBloc({required String gridId, required GridFieldCache fieldCache}) : _fieldCache = fieldCache, @@ -62,7 +62,7 @@ class GridPropertyBloc extends Bloc { class GridPropertyEvent with _$GridPropertyEvent { const factory GridPropertyEvent.initial() = _Initial; const factory GridPropertyEvent.setFieldVisibility(String fieldId, bool visibility) = _SetFieldVisibility; - const factory GridPropertyEvent.didReceiveFieldUpdate(List fields) = _DidReceiveFieldUpdate; + const factory GridPropertyEvent.didReceiveFieldUpdate(List fields) = _DidReceiveFieldUpdate; const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) = _MoveField; } @@ -70,10 +70,10 @@ class GridPropertyEvent with _$GridPropertyEvent { class GridPropertyState with _$GridPropertyState { const factory GridPropertyState({ required String gridId, - required List fields, + required List fields, }) = _GridPropertyState; - factory GridPropertyState.initial(String gridId, List fields) => GridPropertyState( + factory GridPropertyState.initial(String gridId, List fields) => GridPropertyState( gridId: gridId, fields: fields, ); diff --git a/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart b/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart index 4b840634f5..af637ea1c7 100644 --- a/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart @@ -3,7 +3,7 @@ import 'package:app_flowy/workspace/application/edit_pannel/edit_context.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart' show CurrentWorkspaceSetting; +import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart' show CurrentWorkspaceSettingPB; import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -13,45 +13,51 @@ part 'home_bloc.freezed.dart'; class HomeBloc extends Bloc { final UserWorkspaceListener _listener; - HomeBloc(UserProfile user, CurrentWorkspaceSetting workspaceSetting) + HomeBloc(UserProfilePB user, CurrentWorkspaceSettingPB workspaceSetting) : _listener = UserWorkspaceListener(userProfile: user), super(HomeState.initial(workspaceSetting)) { - on((event, emit) async { - await event.map( - initial: (_Initial value) { - _listener.start( - onAuthChanged: (result) => _authDidChanged(result), - onSettingUpdated: (result) { - result.fold( - (setting) => add(HomeEvent.didReceiveWorkspaceSetting(setting)), - (r) => Log.error(r), - ); - }, - ); - }, - showLoading: (e) async { - emit(state.copyWith(isLoading: e.isLoading)); - }, - setEditPannel: (e) async { - emit(state.copyWith(pannelContext: some(e.editContext))); - }, - dismissEditPannel: (value) async { - emit(state.copyWith(pannelContext: none())); - }, - forceCollapse: (e) async { - emit(state.copyWith(forceCollapse: e.forceCollapse)); - }, - didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { - emit(state.copyWith(workspaceSetting: value.setting)); - }, - unauthorized: (_Unauthorized value) { - emit(state.copyWith(unauthorized: true)); - }, - collapseMenu: (e) { - emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed)); - }, - ); - }); + on( + (event, emit) async { + await event.map( + initial: (_Initial value) { + _listener.start( + onAuthChanged: (result) => _authDidChanged(result), + onSettingUpdated: (result) { + result.fold( + (setting) => add(HomeEvent.didReceiveWorkspaceSetting(setting)), + (r) => Log.error(r), + ); + }, + ); + }, + showLoading: (e) async { + emit(state.copyWith(isLoading: e.isLoading)); + }, + setEditPannel: (e) async { + emit(state.copyWith(pannelContext: some(e.editContext))); + }, + dismissEditPannel: (value) async { + emit(state.copyWith(pannelContext: none())); + }, + forceCollapse: (e) async { + emit(state.copyWith(forceCollapse: e.forceCollapse)); + }, + didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { + emit(state.copyWith(workspaceSetting: value.setting)); + }, + unauthorized: (_Unauthorized value) { + emit(state.copyWith(unauthorized: true)); + }, + collapseMenu: (e) { + emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed)); + }, + editPannelResized: (e) { + final newOffset = (state.resizeOffset + e.offset).clamp(-50, 200).toDouble(); + emit(state.copyWith(resizeOffset: newOffset)); + }, + ); + }, + ); } @override @@ -76,9 +82,10 @@ class HomeEvent with _$HomeEvent { const factory HomeEvent.forceCollapse(bool forceCollapse) = _ForceCollapse; const factory HomeEvent.setEditPannel(EditPannelContext editContext) = _ShowEditPannel; const factory HomeEvent.dismissEditPannel() = _DismissEditPannel; - const factory HomeEvent.didReceiveWorkspaceSetting(CurrentWorkspaceSetting setting) = _DidReceiveWorkspaceSetting; + const factory HomeEvent.didReceiveWorkspaceSetting(CurrentWorkspaceSettingPB setting) = _DidReceiveWorkspaceSetting; const factory HomeEvent.unauthorized(String msg) = _Unauthorized; const factory HomeEvent.collapseMenu() = _CollapseMenu; + const factory HomeEvent.editPannelResized(double offset) = _EditPannelResized; } @freezed @@ -87,17 +94,19 @@ class HomeState with _$HomeState { required bool isLoading, required bool forceCollapse, required Option pannelContext, - required CurrentWorkspaceSetting workspaceSetting, + required CurrentWorkspaceSettingPB workspaceSetting, required bool unauthorized, required bool isMenuCollapsed, + required double resizeOffset, }) = _HomeState; - factory HomeState.initial(CurrentWorkspaceSetting workspaceSetting) => HomeState( + factory HomeState.initial(CurrentWorkspaceSettingPB workspaceSetting) => HomeState( isLoading: false, forceCollapse: false, pannelContext: none(), workspaceSetting: workspaceSetting, unauthorized: false, isMenuCollapsed: false, + resizeOffset: 0, ); } diff --git a/frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart b/frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart index b3885d7b6b..21d51f5a93 100644 --- a/frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/menu/menu_bloc.dart @@ -41,7 +41,7 @@ class MenuBloc extends Bloc { if (state.apps.length > value.fromIndex) { final app = state.apps[value.fromIndex]; _workspaceService.moveApp(appId: app.id, fromIndex: value.fromIndex, toIndex: value.toIndex); - final apps = List.from(state.apps); + final apps = List.from(state.apps); apps.insert(value.toIndex, apps.removeAt(value.fromIndex)); emit(state.copyWith(apps: apps)); } @@ -79,7 +79,7 @@ class MenuBloc extends Bloc { )); } - void _handleAppsOrFail(Either, FlowyError> appsOrFail) { + void _handleAppsOrFail(Either, FlowyError> appsOrFail) { appsOrFail.fold( (apps) => add(MenuEvent.didReceiveApps(left(apps))), (error) => add(MenuEvent.didReceiveApps(right(error))), @@ -93,13 +93,13 @@ class MenuEvent with _$MenuEvent { const factory MenuEvent.openPage(Plugin plugin) = _OpenPage; const factory MenuEvent.createApp(String name, {String? desc}) = _CreateApp; const factory MenuEvent.moveApp(int fromIndex, int toIndex) = _MoveApp; - const factory MenuEvent.didReceiveApps(Either, FlowyError> appsOrFail) = _ReceiveApps; + const factory MenuEvent.didReceiveApps(Either, FlowyError> appsOrFail) = _ReceiveApps; } @freezed class MenuState with _$MenuState { const factory MenuState({ - required List apps, + required List apps, required Either successOrFailure, required Plugin plugin, }) = _MenuState; diff --git a/frontend/app_flowy/lib/workspace/application/menu/menu_user_bloc.dart b/frontend/app_flowy/lib/workspace/application/menu/menu_user_bloc.dart index 7433c758b0..0f30bb9d45 100644 --- a/frontend/app_flowy/lib/workspace/application/menu/menu_user_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/menu/menu_user_bloc.dart @@ -14,7 +14,7 @@ class MenuUserBloc extends Bloc { final UserService _userService; final UserListener _userListener; final UserWorkspaceListener _userWorkspaceListener; - final UserProfile userProfile; + final UserProfilePB userProfile; MenuUserBloc(this.userProfile) : _userListener = UserListener(userProfile: userProfile), @@ -31,7 +31,7 @@ class MenuUserBloc extends Bloc { fetchWorkspaces: () async { // }, - didReceiveUserProfile: (UserProfile newUserProfile) { + didReceiveUserProfile: (UserProfilePB newUserProfile) { emit(state.copyWith(userProfile: newUserProfile)); }, updateUserName: (String name) { @@ -58,14 +58,14 @@ class MenuUserBloc extends Bloc { result.fold((l) => null, (error) => Log.error(error)); } - void _profileUpdated(Either userProfileOrFailed) { + void _profileUpdated(Either userProfileOrFailed) { userProfileOrFailed.fold( (newUserProfile) => add(MenuUserEvent.didReceiveUserProfile(newUserProfile)), (err) => Log.error(err), ); } - void _workspaceListUpdated(Either, FlowyError> workspacesOrFailed) { + void _workspaceListUpdated(Either, FlowyError> workspacesOrFailed) { // Do nothing by now } } @@ -75,18 +75,18 @@ class MenuUserEvent with _$MenuUserEvent { const factory MenuUserEvent.initial() = _Initial; const factory MenuUserEvent.fetchWorkspaces() = _FetchWorkspaces; const factory MenuUserEvent.updateUserName(String name) = _UpdateUserName; - const factory MenuUserEvent.didReceiveUserProfile(UserProfile newUserProfile) = _DidReceiveUserProfile; + const factory MenuUserEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; } @freezed class MenuUserState with _$MenuUserState { const factory MenuUserState({ - required UserProfile userProfile, - required Option> workspaces, + required UserProfilePB userProfile, + required Option> workspaces, required Either successOrFailure, }) = _MenuUserState; - factory MenuUserState.initial(UserProfile userProfile) => MenuUserState( + factory MenuUserState.initial(UserProfilePB userProfile) => MenuUserState( userProfile: userProfile, workspaces: none(), successOrFailure: left(unit), diff --git a/frontend/app_flowy/lib/workspace/application/menu/menu_view_section_bloc.dart b/frontend/app_flowy/lib/workspace/application/menu/menu_view_section_bloc.dart index 6d9f4ce9ef..e70dfd179a 100644 --- a/frontend/app_flowy/lib/workspace/application/menu/menu_view_section_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/menu/menu_view_section_bloc.dart @@ -62,7 +62,7 @@ class ViewSectionBloc extends Bloc { Future _moveView(_MoveView value, Emitter emit) async { if (value.fromIndex < state.views.length) { final viewId = state.views[value.fromIndex].id; - final views = List.from(state.views); + final views = List.from(state.views); views.insert(value.toIndex, views.removeAt(value.fromIndex)); emit(state.copyWith(views: views)); @@ -92,16 +92,16 @@ class ViewSectionBloc extends Bloc { @freezed class ViewSectionEvent with _$ViewSectionEvent { const factory ViewSectionEvent.initial() = _Initial; - const factory ViewSectionEvent.setSelectedView(View? view) = _SetSelectedView; + const factory ViewSectionEvent.setSelectedView(ViewPB? view) = _SetSelectedView; const factory ViewSectionEvent.moveView(int fromIndex, int toIndex) = _MoveView; - const factory ViewSectionEvent.didReceiveViewUpdated(List views) = _DidReceiveViewUpdated; + const factory ViewSectionEvent.didReceiveViewUpdated(List views) = _DidReceiveViewUpdated; } @freezed class ViewSectionState with _$ViewSectionState { const factory ViewSectionState({ - required List views, - View? selectedView, + required List views, + ViewPB? selectedView, }) = _ViewSectionState; factory ViewSectionState.initial(AppViewDataContext appViewData) => ViewSectionState( diff --git a/frontend/app_flowy/lib/workspace/application/settings/prelude.dart b/frontend/app_flowy/lib/workspace/application/settings/prelude.dart new file mode 100644 index 0000000000..3917b54aaf --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/settings/prelude.dart @@ -0,0 +1 @@ +export 'settings_dialog_bloc.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart b/frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart new file mode 100644 index 0000000000..3c40f767b1 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart @@ -0,0 +1,67 @@ +import 'package:app_flowy/user/application/user_listener.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; + +part 'settings_dialog_bloc.freezed.dart'; + +class SettingsDialogBloc extends Bloc { + final UserListener _userListener; + final UserProfilePB userProfile; + + SettingsDialogBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + super(SettingsDialogState.initial(userProfile)) { + on((event, emit) async { + await event.when( + initial: () async { + _userListener.start(onProfileUpdated: _profileUpdated); + }, + didReceiveUserProfile: (UserProfilePB newUserProfile) { + emit(state.copyWith(userProfile: newUserProfile)); + }, + setViewIndex: (int viewIndex) { + emit(state.copyWith(viewIndex: viewIndex)); + }, + ); + }); + } + + @override + Future close() async { + await _userListener.stop(); + super.close(); + } + + void _profileUpdated(Either userProfileOrFailed) { + userProfileOrFailed.fold( + (newUserProfile) => add(SettingsDialogEvent.didReceiveUserProfile(newUserProfile)), + (err) => Log.error(err), + ); + } +} + +@freezed +class SettingsDialogEvent with _$SettingsDialogEvent { + const factory SettingsDialogEvent.initial() = _Initial; + const factory SettingsDialogEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; + const factory SettingsDialogEvent.setViewIndex(int index) = _SetViewIndex; +} + +@freezed +class SettingsDialogState with _$SettingsDialogState { + const factory SettingsDialogState({ + required UserProfilePB userProfile, + required Either successOrFailure, + required int viewIndex, + }) = _SettingsDialogState; + + factory SettingsDialogState.initial(UserProfilePB userProfile) => SettingsDialogState( + userProfile: userProfile, + successOrFailure: left(unit), + viewIndex: 0, + ); +} diff --git a/frontend/app_flowy/lib/workspace/application/trash/trash_bloc.dart b/frontend/app_flowy/lib/workspace/application/trash/trash_bloc.dart index 8c83cd4d2f..a7bcf0588c 100644 --- a/frontend/app_flowy/lib/workspace/application/trash/trash_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/trash/trash_bloc.dart @@ -45,7 +45,7 @@ class TrashBloc extends Bloc { )); } - void _listenTrashUpdated(Either, FlowyError> trashOrFailed) { + void _listenTrashUpdated(Either, FlowyError> trashOrFailed) { trashOrFailed.fold( (trash) { add(TrashEvent.didReceiveTrash(trash)); @@ -66,9 +66,9 @@ class TrashBloc extends Bloc { @freezed class TrashEvent with _$TrashEvent { const factory TrashEvent.initial() = Initial; - const factory TrashEvent.didReceiveTrash(List trash) = ReceiveTrash; + const factory TrashEvent.didReceiveTrash(List trash) = ReceiveTrash; const factory TrashEvent.putback(String trashId) = Putback; - const factory TrashEvent.delete(Trash trash) = Delete; + const factory TrashEvent.delete(TrashPB trash) = Delete; const factory TrashEvent.restoreAll() = RestoreAll; const factory TrashEvent.deleteAll() = DeleteAll; } @@ -76,7 +76,7 @@ class TrashEvent with _$TrashEvent { @freezed class TrashState with _$TrashState { const factory TrashState({ - required List objects, + required List objects, required Either successOrFailure, }) = _TrashState; diff --git a/frontend/app_flowy/lib/workspace/application/trash/trash_listener.dart b/frontend/app_flowy/lib/workspace/application/trash/trash_listener.dart index 51244e23bb..0b4e142058 100644 --- a/frontend/app_flowy/lib/workspace/application/trash/trash_listener.dart +++ b/frontend/app_flowy/lib/workspace/application/trash/trash_listener.dart @@ -8,7 +8,7 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart'; import 'package:flowy_sdk/rust_stream.dart'; -typedef TrashUpdatedCallback = void Function(Either, FlowyError> trashOrFailed); +typedef TrashUpdatedCallback = void Function(Either, FlowyError> trashOrFailed); class TrashListener { StreamSubscription? _subscription; @@ -27,7 +27,7 @@ class TrashListener { if (_trashUpdated != null) { result.fold( (payload) { - final repeatedTrash = RepeatedTrash.fromBuffer(payload); + final repeatedTrash = RepeatedTrashPB.fromBuffer(payload); _trashUpdated!(left(repeatedTrash.items)); }, (error) => _trashUpdated!(right(error)), diff --git a/frontend/app_flowy/lib/workspace/application/trash/trash_service.dart b/frontend/app_flowy/lib/workspace/application/trash/trash_service.dart index a2b6d47147..e782120a74 100644 --- a/frontend/app_flowy/lib/workspace/application/trash/trash_service.dart +++ b/frontend/app_flowy/lib/workspace/application/trash/trash_service.dart @@ -5,24 +5,24 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/trash.pb.dart'; class TrashService { - Future> readTrash() { + Future> readTrash() { return FolderEventReadTrash().send(); } Future> putback(String trashId) { - final id = TrashId.create()..id = trashId; + final id = TrashIdPB.create()..id = trashId; return FolderEventPutbackTrash(id).send(); } Future> deleteViews(List> trashList) { final items = trashList.map((trash) { - return TrashId.create() + return TrashIdPB.create() ..id = trash.value1 ..ty = trash.value2; }); - final ids = RepeatedTrashId(items: items); + final ids = RepeatedTrashIdPB(items: items); return FolderEventDeleteTrash(ids).send(); } diff --git a/frontend/app_flowy/lib/workspace/application/user/prelude.dart b/frontend/app_flowy/lib/workspace/application/user/prelude.dart new file mode 100644 index 0000000000..f698497db9 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/user/prelude.dart @@ -0,0 +1 @@ +export 'settings_user_bloc.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart new file mode 100644 index 0000000000..7435778471 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart @@ -0,0 +1,79 @@ +import 'package:app_flowy/user/application/user_listener.dart'; +import 'package:app_flowy/user/application/user_service.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; + +part 'settings_user_bloc.freezed.dart'; + +class SettingsUserViewBloc extends Bloc { + final UserService _userService; + final UserListener _userListener; + final UserProfilePB userProfile; + + SettingsUserViewBloc(this.userProfile) + : _userListener = UserListener(userProfile: userProfile), + _userService = UserService(userId: userProfile.id), + super(SettingsUserState.initial(userProfile)) { + on((event, emit) async { + await event.when( + initial: () async { + _userListener.start(onProfileUpdated: _profileUpdated); + await _initUser(); + }, + didReceiveUserProfile: (UserProfilePB newUserProfile) { + emit(state.copyWith(userProfile: newUserProfile)); + }, + updateUserName: (String name) { + _userService.updateUserProfile(name: name).then((result) { + result.fold( + (l) => null, + (err) => Log.error(err), + ); + }); + }, + ); + }); + } + + @override + Future close() async { + await _userListener.stop(); + super.close(); + } + + Future _initUser() async { + final result = await _userService.initUser(); + result.fold((l) => null, (error) => Log.error(error)); + } + + void _profileUpdated(Either userProfileOrFailed) { + userProfileOrFailed.fold( + (newUserProfile) => add(SettingsUserEvent.didReceiveUserProfile(newUserProfile)), + (err) => Log.error(err), + ); + } +} + +@freezed +class SettingsUserEvent with _$SettingsUserEvent { + const factory SettingsUserEvent.initial() = _Initial; + const factory SettingsUserEvent.updateUserName(String name) = _UpdateUserName; + const factory SettingsUserEvent.didReceiveUserProfile(UserProfilePB newUserProfile) = _DidReceiveUserProfile; +} + +@freezed +class SettingsUserState with _$SettingsUserState { + const factory SettingsUserState({ + required UserProfilePB userProfile, + required Either successOrFailure, + }) = _SettingsUserState; + + factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState( + userProfile: userProfile, + successOrFailure: left(unit), + ); +} diff --git a/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart b/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart index f87e863d45..bf3cfee6e3 100644 --- a/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/view/view_bloc.dart @@ -11,7 +11,7 @@ part 'view_bloc.freezed.dart'; class ViewBloc extends Bloc { final ViewService service; final ViewListener listener; - final View view; + final ViewPB view; ViewBloc({ required this.view, @@ -81,18 +81,18 @@ class ViewEvent with _$ViewEvent { const factory ViewEvent.rename(String newName) = Rename; const factory ViewEvent.delete() = Delete; const factory ViewEvent.duplicate() = Duplicate; - const factory ViewEvent.viewDidUpdate(Either result) = ViewDidUpdate; + const factory ViewEvent.viewDidUpdate(Either result) = ViewDidUpdate; } @freezed class ViewState with _$ViewState { const factory ViewState({ - required View view, + required ViewPB view, required bool isEditing, required Either successOrFailure, }) = _ViewState; - factory ViewState.init(View view) => ViewState( + factory ViewState.init(ViewPB view) => ViewState( view: view, isEditing: false, successOrFailure: left(unit), diff --git a/frontend/app_flowy/lib/workspace/application/view/view_ext.dart b/frontend/app_flowy/lib/workspace/application/view/view_ext.dart index 3987226a20..a54d5ecf40 100644 --- a/frontend/app_flowy/lib/workspace/application/view/view_ext.dart +++ b/frontend/app_flowy/lib/workspace/application/view/view_ext.dart @@ -32,7 +32,7 @@ extension FlowyPluginExtension on FlowyPlugin { } } -extension ViewExtension on View { +extension ViewExtension on ViewPB { Widget renderThumbnail({Color? iconColor}) { String thumbnail = "file_icon"; diff --git a/frontend/app_flowy/lib/workspace/application/view/view_listener.dart b/frontend/app_flowy/lib/workspace/application/view/view_listener.dart index 4acb5e021f..2b080ec194 100644 --- a/frontend/app_flowy/lib/workspace/application/view/view_listener.dart +++ b/frontend/app_flowy/lib/workspace/application/view/view_listener.dart @@ -9,9 +9,9 @@ import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart'; import 'package:flowy_sdk/rust_stream.dart'; import 'package:flowy_infra/notifier.dart'; -typedef DeleteViewNotifyValue = Either; -typedef UpdateViewNotifiedValue = Either; -typedef RestoreViewNotifiedValue = Either; +typedef DeleteViewNotifyValue = Either; +typedef UpdateViewNotifiedValue = Either; +typedef RestoreViewNotifiedValue = Either; class ViewListener { StreamSubscription? _subscription; @@ -19,7 +19,7 @@ class ViewListener { final PublishNotifier _deletedNotifier = PublishNotifier(); final PublishNotifier _restoredNotifier = PublishNotifier(); FolderNotificationParser? _parser; - View view; + ViewPB view; ViewListener({ required this.view, @@ -62,19 +62,19 @@ class ViewListener { switch (ty) { case FolderNotification.ViewUpdated: result.fold( - (payload) => _updatedViewNotifier.value = left(View.fromBuffer(payload)), + (payload) => _updatedViewNotifier.value = left(ViewPB.fromBuffer(payload)), (error) => _updatedViewNotifier.value = right(error), ); break; case FolderNotification.ViewDeleted: result.fold( - (payload) => _deletedNotifier.value = left(View.fromBuffer(payload)), + (payload) => _deletedNotifier.value = left(ViewPB.fromBuffer(payload)), (error) => _deletedNotifier.value = right(error), ); break; case FolderNotification.ViewRestored: result.fold( - (payload) => _restoredNotifier.value = left(View.fromBuffer(payload)), + (payload) => _restoredNotifier.value = left(ViewPB.fromBuffer(payload)), (error) => _restoredNotifier.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/workspace/application/view/view_service.dart b/frontend/app_flowy/lib/workspace/application/view/view_service.dart index c8edd37782..b73cf25cad 100644 --- a/frontend/app_flowy/lib/workspace/application/view/view_service.dart +++ b/frontend/app_flowy/lib/workspace/application/view/view_service.dart @@ -5,13 +5,13 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; class ViewService { - Future> readView({required String viewId}) { - final request = ViewId(value: viewId); + Future> readView({required String viewId}) { + final request = ViewIdPB(value: viewId); return FolderEventReadView(request).send(); } - Future> updateView({required String viewId, String? name, String? desc}) { - final request = UpdateViewPayload.create()..viewId = viewId; + Future> updateView({required String viewId, String? name, String? desc}) { + final request = UpdateViewPayloadPB.create()..viewId = viewId; if (name != null) { request.name = name; @@ -25,12 +25,12 @@ class ViewService { } Future> delete({required String viewId}) { - final request = RepeatedViewId.create()..items.add(viewId); + final request = RepeatedViewIdPB.create()..items.add(viewId); return FolderEventDeleteView(request).send(); } Future> duplicate({required String viewId}) { - final request = ViewId(value: viewId); + final request = ViewIdPB(value: viewId); return FolderEventDuplicateView(request).send(); } } diff --git a/frontend/app_flowy/lib/workspace/application/workspace/welcome_bloc.dart b/frontend/app_flowy/lib/workspace/application/workspace/welcome_bloc.dart index dd3edb27c1..49c3ba19be 100644 --- a/frontend/app_flowy/lib/workspace/application/workspace/welcome_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/workspace/welcome_bloc.dart @@ -52,7 +52,7 @@ class WelcomeBloc extends Bloc { )); } - Future _openWorkspace(Workspace workspace, Emitter emit) async { + Future _openWorkspace(WorkspacePB workspace, Emitter emit) async { final result = await userService.openWorkspace(workspace.id); emit(result.fold( (workspaces) => state.copyWith(successOrFailure: left(unit)), @@ -82,8 +82,8 @@ class WelcomeEvent with _$WelcomeEvent { const factory WelcomeEvent.initial() = Initial; // const factory WelcomeEvent.fetchWorkspaces() = FetchWorkspace; const factory WelcomeEvent.createWorkspace(String name, String desc) = CreateWorkspace; - const factory WelcomeEvent.openWorkspace(Workspace workspace) = OpenWorkspace; - const factory WelcomeEvent.workspacesReveived(Either, FlowyError> workspacesOrFail) = + const factory WelcomeEvent.openWorkspace(WorkspacePB workspace) = OpenWorkspace; + const factory WelcomeEvent.workspacesReveived(Either, FlowyError> workspacesOrFail) = WorkspacesReceived; } @@ -91,7 +91,7 @@ class WelcomeEvent with _$WelcomeEvent { class WelcomeState with _$WelcomeState { const factory WelcomeState({ required bool isLoading, - required List workspaces, + required List workspaces, required Either successOrFailure, }) = _WelcomeState; diff --git a/frontend/app_flowy/lib/workspace/application/workspace/workspace_listener.dart b/frontend/app_flowy/lib/workspace/application/workspace/workspace_listener.dart index 1ee8de94fd..2d7a100e8b 100644 --- a/frontend/app_flowy/lib/workspace/application/workspace/workspace_listener.dart +++ b/frontend/app_flowy/lib/workspace/application/workspace/workspace_listener.dart @@ -3,21 +3,21 @@ import 'dart:typed_data'; import 'package:app_flowy/core/folder_notification.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_infra/notifier.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/dart_notification.pb.dart'; -typedef AppListNotifyValue = Either, FlowyError>; -typedef WorkspaceNotifyValue = Either; +typedef AppListNotifyValue = Either, FlowyError>; +typedef WorkspaceNotifyValue = Either; class WorkspaceListener { PublishNotifier? _appsChangedNotifier = PublishNotifier(); PublishNotifier? _workspaceUpdatedNotifier = PublishNotifier(); FolderNotificationListener? _listener; - final UserProfile user; + final UserProfilePB user; final String workspaceId; WorkspaceListener({ @@ -47,13 +47,13 @@ class WorkspaceListener { switch (ty) { case FolderNotification.WorkspaceUpdated: result.fold( - (payload) => _workspaceUpdatedNotifier?.value = left(Workspace.fromBuffer(payload)), + (payload) => _workspaceUpdatedNotifier?.value = left(WorkspacePB.fromBuffer(payload)), (error) => _workspaceUpdatedNotifier?.value = right(error), ); break; case FolderNotification.WorkspaceAppsChanged: result.fold( - (payload) => _appsChangedNotifier?.value = left(RepeatedApp.fromBuffer(payload).items), + (payload) => _appsChangedNotifier?.value = left(RepeatedAppPB.fromBuffer(payload).items), (error) => _appsChangedNotifier?.value = right(error), ); break; diff --git a/frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart b/frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart index 678794b594..4f68d4776a 100644 --- a/frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/app_flowy/lib/workspace/application/workspace/workspace_service.dart @@ -5,7 +5,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/app.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart' show MoveFolderItemPayload, MoveFolderItemType; +import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart' show MoveFolderItemPayloadPB, MoveFolderItemType; import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; @@ -15,16 +15,16 @@ class WorkspaceService { WorkspaceService({ required this.workspaceId, }); - Future> createApp({required String name, required String desc}) { - final payload = CreateAppPayload.create() + Future> createApp({required String name, required String desc}) { + final payload = CreateAppPayloadPB.create() ..name = name ..workspaceId = workspaceId ..desc = desc; return FolderEventCreateApp(payload).send(); } - Future> getWorkspace() { - final payload = WorkspaceId.create()..value = workspaceId; + Future> getWorkspace() { + final payload = WorkspaceIdPB.create()..value = workspaceId; return FolderEventReadWorkspaces(payload).send().then((result) { return result.fold( (workspaces) { @@ -41,8 +41,8 @@ class WorkspaceService { }); } - Future, FlowyError>> getApps() { - final payload = WorkspaceId.create()..value = workspaceId; + Future, FlowyError>> getApps() { + final payload = WorkspaceIdPB.create()..value = workspaceId; return FolderEventReadWorkspaceApps(payload).send().then((result) { return result.fold( (apps) => left(apps.items), @@ -56,7 +56,7 @@ class WorkspaceService { required int fromIndex, required int toIndex, }) { - final payload = MoveFolderItemPayload.create() + final payload = MoveFolderItemPayloadPB.create() ..itemId = appId ..from = fromIndex ..to = toIndex diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart index 0405279851..e7e02b9767 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart @@ -27,6 +27,8 @@ class HomeLayout { menuWidth = Sizes.sideBarLg; } + menuWidth += homeBlocState.resizeOffset; + if (forceCollapse) { showMenu = false; } else { diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart index f205daabac..abe89c14cb 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart @@ -5,8 +5,9 @@ import 'package:app_flowy/workspace/presentation/widgets/float_bubble/question_b import 'package:app_flowy/startup/startup.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_infra_ui/style_widget/container.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_sdk/protobuf/flowy-folder/protobuf.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -18,8 +19,8 @@ import 'home_stack.dart'; import 'menu/menu.dart'; class HomeScreen extends StatefulWidget { - final UserProfile user; - final CurrentWorkspaceSetting workspaceSetting; + final UserProfilePB user; + final CurrentWorkspaceSettingPB workspaceSetting; const HomeScreen(this.user, this.workspaceSetting, {Key? key}) : super(key: key); @override @@ -27,7 +28,7 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { - View? initialView; + ViewPB? initialView; @override void initState() { @@ -87,6 +88,7 @@ class _HomeScreenState extends State { context: context, state: state, ); + final homeMenuResizer = _buildHomeMenuResizer(context: context); final editPannel = _buildEditPannel( homeState: state, layout: layout, @@ -99,6 +101,7 @@ class _HomeScreenState extends State { homeMenu: menu, editPannel: editPannel, bubble: bubble, + homeMenuResizer: homeMenuResizer, ); }, ); @@ -122,7 +125,10 @@ class _HomeScreenState extends State { ); final latestView = workspaceSetting.hasLatestView() ? workspaceSetting.latestView : null; - getIt().latestOpenView = latestView; + if (getIt().latestOpenView == null) { + /// AppFlowy will open the view that the last time the user opened it. The _buildHomeMenu will get called when AppFlowy's screen resizes. So we only set the latestOpenView when it's null. + getIt().latestOpenView = latestView; + } return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu)); } @@ -147,12 +153,31 @@ class _HomeScreenState extends State { ); } + Widget _buildHomeMenuResizer({ + required BuildContext context, + }) { + return MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: GestureDetector( + dragStartBehavior: DragStartBehavior.down, + onPanUpdate: ((details) { + context.read().add(HomeEvent.editPannelResized(details.delta.dx)); + }), + behavior: HitTestBehavior.translucent, + child: SizedBox( + width: 10, + height: MediaQuery.of(context).size.height, + )), + ); + } + Widget _layoutWidgets({ required HomeLayout layout, required Widget homeMenu, required Widget homeStack, required Widget editPannel, required Widget bubble, + required Widget homeMenuResizer, }) { return Stack( children: [ @@ -167,6 +192,7 @@ class _HomeScreenState extends State { .constrained(minWidth: 500) .positioned(left: layout.homePageLOffset, right: layout.homePageROffset, bottom: 0, top: 0, animate: true) .animate(layout.animDuration, Curves.easeOut), + homeMenuResizer.positioned(left: layout.homePageLOffset - 5), bubble .positioned( right: 20, diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart index 2d26aabc0b..dbeb2248cc 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart @@ -19,7 +19,7 @@ import 'add_button.dart'; import 'right_click_action.dart'; class MenuAppHeader extends StatelessWidget { - final App app; + final AppPB app; const MenuAppHeader( this.app, { Key? key, @@ -85,7 +85,7 @@ class MenuAppHeader extends StatelessWidget { anchorDirection: AnchorDirection.bottomWithCenterAligned, ); }, - child: BlocSelector( + child: BlocSelector( selector: (state) => state.app, builder: (context, app) => FlowyText.medium( app.name, diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart index 462ce1ba8a..e636c87d9a 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/menu_app.dart @@ -10,7 +10,7 @@ import 'package:provider/provider.dart'; import 'section/section.dart'; class MenuApp extends StatefulWidget { - final App app; + final AppPB app; const MenuApp(this.app, {Key? key}) : super(key: key); @override diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart index f5de13892f..97ae1dae52 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart @@ -21,8 +21,8 @@ import 'disclosure_action.dart'; // ignore: must_be_immutable class ViewSectionItem extends StatelessWidget { final bool isSelected; - final View view; - final void Function(View) onSelected; + final ViewPB view; + final void Function(ViewPB) onSelected; ViewSectionItem({ Key? key, diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart index 01debf6196..3976598b7b 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart @@ -10,7 +10,7 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart'; import 'package:flutter/material.dart'; @@ -32,8 +32,8 @@ import 'menu_user.dart'; class HomeMenu extends StatelessWidget { final PublishNotifier _collapsedNotifier; - final UserProfile user; - final CurrentWorkspaceSetting workspaceSetting; + final UserProfilePB user; + final CurrentWorkspaceSettingPB workspaceSetting; const HomeMenu({ Key? key, @@ -155,19 +155,21 @@ class HomeMenu extends StatelessWidget { } class MenuSharedState { - final ValueNotifier _latestOpenView = ValueNotifier(null); + final ValueNotifier _latestOpenView = ValueNotifier(null); - MenuSharedState({View? view}) { + MenuSharedState({ViewPB? view}) { _latestOpenView.value = view; } - View? get latestOpenView => _latestOpenView.value; + ViewPB? get latestOpenView => _latestOpenView.value; - set latestOpenView(View? view) { - _latestOpenView.value = view; + set latestOpenView(ViewPB? view) { + if (_latestOpenView.value != view) { + _latestOpenView.value = view; + } } - VoidCallback addLatestViewListener(void Function(View?) callback) { + VoidCallback addLatestViewListener(void Function(ViewPB?) callback) { listener() { callback(_latestOpenView.value); } diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart index 6b75c9ef3f..d7b8dce4af 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart @@ -6,14 +6,14 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfile; +import 'package:flowy_sdk/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; class MenuUser extends StatelessWidget { - final UserProfile user; + final UserProfilePB user; MenuUser(this.user, {Key? key}) : super(key: ValueKey(user.id)); @override @@ -67,6 +67,7 @@ class MenuUser extends StatelessWidget { Widget _renderSettingsButton(BuildContext context) { final theme = context.watch(); + final userProfile = context.read().state.userProfile; return Tooltip( message: LocaleKeys.settings_menu_open.tr(), child: IconButton( @@ -74,7 +75,7 @@ class MenuUser extends StatelessWidget { showDialog( context: context, builder: (context) { - return const SettingsDialog(); + return SettingsDialog(userProfile); }, ); }, diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/board/board.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/board/board.dart index b8692168c6..ab58176e50 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/board/board.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/board/board.dart @@ -9,7 +9,7 @@ import 'src/board_page.dart'; class BoardPluginBuilder implements PluginBuilder { @override Plugin build(dynamic data) { - if (data is View) { + if (data is ViewPB) { return BoardPlugin(pluginType: pluginType, view: data); } else { throw FlowyPluginException.invalidData; @@ -32,11 +32,11 @@ class BoardPluginConfig implements PluginConfig { } class BoardPlugin extends Plugin { - final View _view; + final ViewPB _view; final PluginType _pluginType; BoardPlugin({ - required View view, + required ViewPB view, required PluginType pluginType, }) : _pluginType = pluginType, _view = view; @@ -52,8 +52,8 @@ class BoardPlugin extends Plugin { } class GridPluginDisplay extends PluginDisplay { - final View _view; - GridPluginDisplay({required View view, Key? key}) : _view = view; + final ViewPB _view; + GridPluginDisplay({required ViewPB view, Key? key}) : _view = view; @override Widget get leftBarItem => ViewLeftBarItem(view: _view); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/board/src/board_page.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/board/src/board_page.dart index 1cdfea5480..612e7c6770 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/board/src/board_page.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/board/src/board_page.dart @@ -4,9 +4,9 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter/material.dart'; class BoardPage extends StatelessWidget { - final View _view; + final ViewPB _view; - const BoardPage({required View view, Key? key}) + const BoardPage({required ViewPB view, Key? key}) : _view = view, super(key: key); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/doc/document.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/doc/document.dart index 0759894844..c09843873d 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/doc/document.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/doc/document.dart @@ -36,7 +36,7 @@ export './src/widget/toolbar/toolbar_icon_button.dart'; class DocumentPluginBuilder extends PluginBuilder { @override Plugin build(dynamic data) { - if (data is View) { + if (data is ViewPB) { return DocumentPlugin(pluginType: pluginType, view: data); } else { throw FlowyPluginException.invalidData; @@ -54,11 +54,11 @@ class DocumentPluginBuilder extends PluginBuilder { } class DocumentPlugin implements Plugin { - late View _view; + late ViewPB _view; ViewListener? _listener; late PluginType _pluginType; - DocumentPlugin({required PluginType pluginType, required View view, Key? key}) : _view = view { + DocumentPlugin({required PluginType pluginType, required ViewPB view, Key? key}) : _view = view { _pluginType = pluginType; _listener = getIt(param1: view); _listener?.start(onViewUpdated: (result) { @@ -90,9 +90,9 @@ class DocumentPlugin implements Plugin { class DocumentPluginDisplay extends PluginDisplay with NavigationItem { final PublishNotifier _displayNotifier = PublishNotifier(); - final View _view; + final ViewPB _view; - DocumentPluginDisplay({required View view, Key? key}) : _view = view; + DocumentPluginDisplay({required ViewPB view, Key? key}) : _view = view; @override Widget buildWidget() => DocumentPage(view: _view, key: ValueKey(_view.id)); @@ -111,7 +111,7 @@ class DocumentPluginDisplay extends PluginDisplay with NavigationItem { } class DocumentShareButton extends StatelessWidget { - final View view; + final ViewPB view; DocumentShareButton({Key? key, required this.view}) : super(key: ValueKey(view.hashCode)); @override @@ -160,7 +160,7 @@ class DocumentShareButton extends StatelessWidget { ); } - void _handleExportData(ExportData exportData) { + void _handleExportData(ExportDataPB exportData) { switch (exportData.exportType) { case ExportType.Link: break; diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/document_page.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/document_page.dart index a02289a555..3598c5a5ac 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/document_page.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/doc/src/document_page.dart @@ -14,7 +14,7 @@ import 'styles.dart'; import 'widget/banner.dart'; class DocumentPage extends StatefulWidget { - final View view; + final ViewPB view; DocumentPage({Key? key, required this.view}) : super(key: ValueKey(view.id)); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid.dart index da634d8833..776dd833ea 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/grid.dart @@ -11,7 +11,7 @@ import 'src/grid_page.dart'; class GridPluginBuilder implements PluginBuilder { @override Plugin build(dynamic data) { - if (data is View) { + if (data is ViewPB) { return GridPlugin(pluginType: pluginType, view: data); } else { throw FlowyPluginException.invalidData; @@ -34,11 +34,11 @@ class GridPluginConfig implements PluginConfig { } class GridPlugin extends Plugin { - final View _view; + final ViewPB _view; final PluginType _pluginType; GridPlugin({ - required View view, + required ViewPB view, required PluginType pluginType, }) : _pluginType = pluginType, _view = view; @@ -54,8 +54,8 @@ class GridPlugin extends Plugin { } class GridPluginDisplay extends PluginDisplay { - final View _view; - GridPluginDisplay({required View view, Key? key}) : _view = view; + final ViewPB _view; + GridPluginDisplay({required ViewPB view, Key? key}) : _view = view; @override Widget get leftBarItem => ViewLeftBarItem(view: _view); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart index 6daf2c50fa..4aae99acbe 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/grid_page.dart @@ -1,9 +1,11 @@ import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/grid/grid_bloc.dart'; import 'package:app_flowy/workspace/application/grid/row/row_service.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -19,7 +21,7 @@ import 'widgets/shortcuts.dart'; import 'widgets/toolbar/grid_toolbar.dart'; class GridPage extends StatefulWidget { - final View view; + final ViewPB view; GridPage({Key? key, required this.view}) : super(key: ValueKey(view.id)); @@ -212,10 +214,10 @@ class _GridRowsState extends State<_GridRows> { builder: (context, state) { return SliverAnimatedList( key: _key, - initialItemCount: context.read().state.rows.length, + initialItemCount: context.read().state.rowInfos.length, itemBuilder: (BuildContext context, int index, Animation animation) { - final GridRow rowData = context.read().state.rows[index]; - return _renderRow(context, rowData, animation); + final GridRowInfo rowInfo = context.read().state.rowInfos[index]; + return _renderRow(context, rowInfo, animation); }, ); }, @@ -224,17 +226,19 @@ class _GridRowsState extends State<_GridRows> { Widget _renderRow( BuildContext context, - GridRow rowData, + GridRowInfo rowInfo, Animation animation, ) { - final rowCache = context.read().getRowCache(rowData.blockId, rowData.rowId); + final rowCache = context.read().getRowCache(rowInfo.blockId, rowInfo.id); + final fieldCache = context.read().fieldCache; if (rowCache != null) { return SizeTransition( sizeFactor: animation, child: GridRowWidget( - rowData: rowData, + rowData: rowInfo, rowCache: rowCache, - key: ValueKey(rowData.rowId), + fieldCache: fieldCache, + key: ValueKey(rowInfo.id), ), ); } else { @@ -248,6 +252,8 @@ class _GridFooter extends StatelessWidget { @override Widget build(BuildContext context) { + final rowCount = context.watch().state.rowInfos.length; + final theme = context.watch(); return SliverPadding( padding: const EdgeInsets.only(bottom: 200), sliver: SliverToBoxAdapter( @@ -258,7 +264,14 @@ class _GridFooter extends StatelessWidget { child: Row( children: [ SizedBox(width: GridSize.leadingHeaderPadding), - const SizedBox(width: 120, child: GridAddRowButton()), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 120, child: GridAddRowButton()), + const SizedBox(height: 30), + _rowCountTextWidget(theme: theme,count: rowCount) + ], + ), ], ), ), @@ -266,4 +279,19 @@ class _GridFooter extends StatelessWidget { ), ); } + + Widget _rowCountTextWidget({required AppTheme theme, required int count}){ + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + FlowyText.regular('Count : ', + fontSize: 13, + color: theme.shader3, + ), + FlowyText.regular(count.toString(), + fontSize: 13, + ), + ], + ); + } } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/layout.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/layout.dart index 36c603b7b4..0b289ecd4b 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/layout.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/layout.dart @@ -2,7 +2,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'sizes.dart'; class GridLayout { - static double headerWidth(List fields) { + static double headerWidth(List fields) { if (fields.isEmpty) return 0; final fieldsWidth = fields.map((field) => field.width.toDouble()).reduce((value, element) => value + element); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart index 61e8e0151c..0a137b6d46 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/layout/sizes.dart @@ -5,7 +5,7 @@ class GridSize { static double get scrollBarSize => 12 * scale; static double get headerHeight => 40 * scale; - static double get footerHeight => 40 * scale; + static double get footerHeight => 100 * scale; static double get leadingHeaderPadding => 50 * scale; static double get trailHeaderPadding => 140 * scale; static double get headerContainerPadding => 0 * scale; diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart index 9f3c7a632e..f785672f71 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/cell_builder.dart @@ -1,4 +1,5 @@ import 'package:app_flowy/workspace/application/grid/cell/cell_service/cell_service.dart'; +import 'package:app_flowy/workspace/application/grid/grid_service.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -12,28 +13,39 @@ import 'select_option_cell/select_option_cell.dart'; import 'text_cell.dart'; import 'url_cell/url_cell.dart'; -GridCellWidget buildGridCellWidget(GridCell gridCell, GridCellCacheService cellCache, {GridCellStyle? style}) { - final key = ValueKey(gridCell.cellId()); +class GridCellBuilder { + final GridCellCache cellCache; + final GridFieldCache fieldCache; + GridCellBuilder({ + required this.cellCache, + required this.fieldCache, + }); - final cellContextBuilder = GridCellContextBuilder(gridCell: gridCell, cellCache: cellCache); - - switch (gridCell.field.fieldType) { - case FieldType.Checkbox: - return CheckboxCell(cellContextBuilder: cellContextBuilder, key: key); - case FieldType.DateTime: - return DateCell(cellContextBuilder: cellContextBuilder, key: key, style: style); - case FieldType.SingleSelect: - return SingleSelectCell(cellContextBuilder: cellContextBuilder, style: style, key: key); - case FieldType.MultiSelect: - return MultiSelectCell(cellContextBuilder: cellContextBuilder, style: style, key: key); - case FieldType.Number: - return NumberCell(cellContextBuilder: cellContextBuilder, key: key); - case FieldType.RichText: - return GridTextCell(cellContextBuilder: cellContextBuilder, style: style, key: key); - case FieldType.URL: - return GridURLCell(cellContextBuilder: cellContextBuilder, style: style, key: key); + GridCellWidget build(GridCellIdentifier cell, {GridCellStyle? style}) { + final cellControllerBuilder = GridCellControllerBuilder( + cellId: cell, + cellCache: cellCache, + fieldCache: fieldCache, + ); + final key = cell.key(); + switch (cell.fieldType) { + case FieldType.Checkbox: + return GridCheckboxCell(cellControllerBuilder: cellControllerBuilder, key: key); + case FieldType.DateTime: + return GridDateCell(cellControllerBuilder: cellControllerBuilder, key: key, style: style); + case FieldType.SingleSelect: + return GridSingleSelectCell(cellContorllerBuilder: cellControllerBuilder, style: style, key: key); + case FieldType.MultiSelect: + return GridMultiSelectCell(cellContorllerBuilder: cellControllerBuilder, style: style, key: key); + case FieldType.Number: + return GridNumberCell(cellContorllerBuilder: cellControllerBuilder, key: key); + case FieldType.RichText: + return GridTextCell(cellContorllerBuilder: cellControllerBuilder, style: style, key: key); + case FieldType.URL: + return GridURLCell(cellContorllerBuilder: cellControllerBuilder, style: style, key: key); + } + throw UnimplementedError; } - throw UnimplementedError; } class BlankCell extends StatelessWidget { diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart index 384d85737f..9fcbaf751f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/checkbox_cell.dart @@ -6,23 +6,23 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'cell_builder.dart'; -class CheckboxCell extends GridCellWidget { - final GridCellContextBuilder cellContextBuilder; - CheckboxCell({ - required this.cellContextBuilder, +class GridCheckboxCell extends GridCellWidget { + final GridCellControllerBuilder cellControllerBuilder; + GridCheckboxCell({ + required this.cellControllerBuilder, Key? key, }) : super(key: key); @override - GridCellState createState() => _CheckboxCellState(); + GridCellState createState() => _CheckboxCellState(); } -class _CheckboxCellState extends GridCellState { +class _CheckboxCellState extends GridCellState { late CheckboxCellBloc _cellBloc; @override void initState() { - final cellContext = widget.cellContextBuilder.build(); + final cellContext = widget.cellControllerBuilder.build(); _cellBloc = getIt(param1: cellContext)..add(const CheckboxCellEvent.initial()); super.initState(); } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart index 3e7d40c796..78d18a50e8 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart @@ -18,13 +18,13 @@ abstract class GridCellDelegate { GridCellDelegate get delegate; } -class DateCell extends GridCellWidget { - final GridCellContextBuilder cellContextBuilder; +class GridDateCell extends GridCellWidget { + final GridCellControllerBuilder cellControllerBuilder; late final DateCellStyle? cellStyle; - DateCell({ + GridDateCell({ GridCellStyle? style, - required this.cellContextBuilder, + required this.cellControllerBuilder, Key? key, }) : super(key: key) { if (style != null) { @@ -35,15 +35,15 @@ class DateCell extends GridCellWidget { } @override - GridCellState createState() => _DateCellState(); + GridCellState createState() => _DateCellState(); } -class _DateCellState extends GridCellState { +class _DateCellState extends GridCellState { late DateCellBloc _cellBloc; @override void initState() { - final cellContext = widget.cellContextBuilder.build(); + final cellContext = widget.cellControllerBuilder.build(); _cellBloc = getIt(param1: cellContext)..add(const DateCellEvent.initial()); super.initState(); } @@ -80,7 +80,7 @@ class _DateCellState extends GridCellState { final calendar = DateCellEditor(onDismissed: () => widget.onCellEditing.value = false); calendar.show( context, - cellContext: bloc.cellContext.clone(), + cellController: bloc.cellContext.clone(), ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_editor.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_editor.dart index 93d304cd1a..36ad22ec53 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_editor.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_editor.dart @@ -31,16 +31,16 @@ class DateCellEditor with FlowyOverlayDelegate { Future show( BuildContext context, { - required GridDateCellContext cellContext, + required GridDateCellController cellController, }) async { DateCellEditor.remove(context); - final result = await cellContext.getTypeOptionData(); + final result = await cellController.getFieldTypeOption(DateTypeOptionDataParser()); result.fold( - (data) { + (dateTypeOption) { final calendar = _CellCalendarWidget( - cellContext: cellContext, - dateTypeOption: DateTypeOption.fromBuffer(data.typeOptionData), + cellContext: cellController, + dateTypeOption: dateTypeOption, ); FlowyOverlay.of(context).insertWithAnchor( @@ -75,7 +75,7 @@ class DateCellEditor with FlowyOverlayDelegate { } class _CellCalendarWidget extends StatelessWidget { - final GridDateCellContext cellContext; + final GridDateCellController cellContext; final DateTypeOption dateTypeOption; const _CellCalendarWidget({ diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart index 7d16b16ef0..573a4168c9 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/number_cell.dart @@ -6,26 +6,26 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'cell_builder.dart'; -class NumberCell extends GridCellWidget { - final GridCellContextBuilder cellContextBuilder; +class GridNumberCell extends GridCellWidget { + final GridCellControllerBuilder cellContorllerBuilder; - NumberCell({ - required this.cellContextBuilder, + GridNumberCell({ + required this.cellContorllerBuilder, Key? key, }) : super(key: key); @override - GridFocusNodeCellState createState() => _NumberCellState(); + GridFocusNodeCellState createState() => _NumberCellState(); } -class _NumberCellState extends GridFocusNodeCellState { +class _NumberCellState extends GridFocusNodeCellState { late NumberCellBloc _cellBloc; late TextEditingController _controller; Timer? _delayOperation; @override void initState() { - final cellContext = widget.cellContextBuilder.build(); + final cellContext = widget.cellContorllerBuilder.build(); _cellBloc = getIt(param1: cellContext)..add(const NumberCellEvent.initial()); _controller = TextEditingController(text: contentFromState(_cellBloc.state)); super.initState(); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/extension.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/extension.dart index 90a6503079..6946993bae 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/extension.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/extension.dart @@ -7,27 +7,27 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -extension SelectOptionColorExtension on SelectOptionColor { +extension SelectOptionColorExtension on SelectOptionColorPB { Color make(BuildContext context) { final theme = context.watch(); switch (this) { - case SelectOptionColor.Purple: + case SelectOptionColorPB.Purple: return theme.tint1; - case SelectOptionColor.Pink: + case SelectOptionColorPB.Pink: return theme.tint2; - case SelectOptionColor.LightPink: + case SelectOptionColorPB.LightPink: return theme.tint3; - case SelectOptionColor.Orange: + case SelectOptionColorPB.Orange: return theme.tint4; - case SelectOptionColor.Yellow: + case SelectOptionColorPB.Yellow: return theme.tint5; - case SelectOptionColor.Lime: + case SelectOptionColorPB.Lime: return theme.tint6; - case SelectOptionColor.Green: + case SelectOptionColorPB.Green: return theme.tint7; - case SelectOptionColor.Aqua: + case SelectOptionColorPB.Aqua: return theme.tint8; - case SelectOptionColor.Blue: + case SelectOptionColorPB.Blue: return theme.tint9; default: throw ArgumentError; @@ -36,23 +36,23 @@ extension SelectOptionColorExtension on SelectOptionColor { String optionName() { switch (this) { - case SelectOptionColor.Purple: + case SelectOptionColorPB.Purple: return LocaleKeys.grid_selectOption_purpleColor.tr(); - case SelectOptionColor.Pink: + case SelectOptionColorPB.Pink: return LocaleKeys.grid_selectOption_pinkColor.tr(); - case SelectOptionColor.LightPink: + case SelectOptionColorPB.LightPink: return LocaleKeys.grid_selectOption_lightPinkColor.tr(); - case SelectOptionColor.Orange: + case SelectOptionColorPB.Orange: return LocaleKeys.grid_selectOption_orangeColor.tr(); - case SelectOptionColor.Yellow: + case SelectOptionColorPB.Yellow: return LocaleKeys.grid_selectOption_yellowColor.tr(); - case SelectOptionColor.Lime: + case SelectOptionColorPB.Lime: return LocaleKeys.grid_selectOption_limeColor.tr(); - case SelectOptionColor.Green: + case SelectOptionColorPB.Green: return LocaleKeys.grid_selectOption_greenColor.tr(); - case SelectOptionColor.Aqua: + case SelectOptionColorPB.Aqua: return LocaleKeys.grid_selectOption_aquaColor.tr(); - case SelectOptionColor.Blue: + case SelectOptionColorPB.Blue: return LocaleKeys.grid_selectOption_blueColor.tr(); default: throw ArgumentError; @@ -75,7 +75,7 @@ class SelectOptionTag extends StatelessWidget { factory SelectOptionTag.fromSelectOption({ required BuildContext context, - required SelectOption option, + required SelectOptionPB option, VoidCallback? onSelected, bool isSelected = false, }) { @@ -107,8 +107,8 @@ class SelectOptionTag extends StatelessWidget { class SelectOptionTagCell extends StatelessWidget { final List children; - final void Function(SelectOption) onSelected; - final SelectOption option; + final void Function(SelectOptionPB) onSelected; + final SelectOptionPB option; const SelectOptionTagCell({ required this.option, required this.onSelected, diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_cell.dart index 63f7e0fc28..f822e503d2 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_cell.dart @@ -20,12 +20,12 @@ class SelectOptionCellStyle extends GridCellStyle { }); } -class SingleSelectCell extends GridCellWidget { - final GridCellContextBuilder cellContextBuilder; +class GridSingleSelectCell extends GridCellWidget { + final GridCellControllerBuilder cellContorllerBuilder; late final SelectOptionCellStyle? cellStyle; - SingleSelectCell({ - required this.cellContextBuilder, + GridSingleSelectCell({ + required this.cellContorllerBuilder, GridCellStyle? style, Key? key, }) : super(key: key) { @@ -37,15 +37,15 @@ class SingleSelectCell extends GridCellWidget { } @override - State createState() => _SingleSelectCellState(); + State createState() => _SingleSelectCellState(); } -class _SingleSelectCellState extends State { +class _SingleSelectCellState extends State { late SelectOptionCellBloc _cellBloc; @override void initState() { - final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext; + final cellContext = widget.cellContorllerBuilder.build() as GridSelectOptionCellController; _cellBloc = getIt(param1: cellContext)..add(const SelectOptionCellEvent.initial()); super.initState(); } @@ -60,7 +60,7 @@ class _SingleSelectCellState extends State { selectOptions: state.selectedOptions, cellStyle: widget.cellStyle, onFocus: (value) => widget.onCellEditing.value = value, - cellContextBuilder: widget.cellContextBuilder); + cellContorllerBuilder: widget.cellContorllerBuilder); }, ), ); @@ -74,12 +74,12 @@ class _SingleSelectCellState extends State { } //---------------------------------------------------------------- -class MultiSelectCell extends GridCellWidget { - final GridCellContextBuilder cellContextBuilder; +class GridMultiSelectCell extends GridCellWidget { + final GridCellControllerBuilder cellContorllerBuilder; late final SelectOptionCellStyle? cellStyle; - MultiSelectCell({ - required this.cellContextBuilder, + GridMultiSelectCell({ + required this.cellContorllerBuilder, GridCellStyle? style, Key? key, }) : super(key: key) { @@ -91,15 +91,15 @@ class MultiSelectCell extends GridCellWidget { } @override - State createState() => _MultiSelectCellState(); + State createState() => _MultiSelectCellState(); } -class _MultiSelectCellState extends State { +class _MultiSelectCellState extends State { late SelectOptionCellBloc _cellBloc; @override void initState() { - final cellContext = widget.cellContextBuilder.build() as GridSelectOptionCellContext; + final cellContext = widget.cellContorllerBuilder.build() as GridSelectOptionCellController; _cellBloc = getIt(param1: cellContext)..add(const SelectOptionCellEvent.initial()); super.initState(); } @@ -114,7 +114,7 @@ class _MultiSelectCellState extends State { selectOptions: state.selectedOptions, cellStyle: widget.cellStyle, onFocus: (value) => widget.onCellEditing.value = value, - cellContextBuilder: widget.cellContextBuilder); + cellContorllerBuilder: widget.cellContorllerBuilder); }, ), ); @@ -128,15 +128,15 @@ class _MultiSelectCellState extends State { } class _SelectOptionCell extends StatelessWidget { - final List selectOptions; + final List selectOptions; final void Function(bool) onFocus; final SelectOptionCellStyle? cellStyle; - final GridCellContextBuilder cellContextBuilder; + final GridCellControllerBuilder cellContorllerBuilder; const _SelectOptionCell({ required this.selectOptions, required this.onFocus, required this.cellStyle, - required this.cellContextBuilder, + required this.cellContorllerBuilder, Key? key, }) : super(key: key); @@ -172,7 +172,7 @@ class _SelectOptionCell extends StatelessWidget { InkWell( onTap: () { onFocus(true); - final cellContext = cellContextBuilder.build() as GridSelectOptionCellContext; + final cellContext = cellContorllerBuilder.build() as GridSelectOptionCellController; SelectOptionCellEditor.show(context, cellContext, () => onFocus(false)); }, ), diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_editor.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_editor.dart index 76d9a379d7..95f2bf1e03 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_editor.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/select_option_editor.dart @@ -24,11 +24,11 @@ import 'text_field.dart'; const double _editorPannelWidth = 300; class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate { - final GridSelectOptionCellContext cellContext; + final GridSelectOptionCellController cellController; final VoidCallback onDismissed; const SelectOptionCellEditor({ - required this.cellContext, + required this.cellController, required this.onDismissed, Key? key, }) : super(key: key); @@ -37,7 +37,7 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate { Widget build(BuildContext context) { return BlocProvider( create: (context) => SelectOptionCellEditorBloc( - cellContext: cellContext, + cellController: cellController, )..add(const SelectOptionEditorEvent.initial()), child: BlocBuilder( builder: (context, state) { @@ -59,12 +59,12 @@ class SelectOptionCellEditor extends StatelessWidget with FlowyOverlayDelegate { static void show( BuildContext context, - GridSelectOptionCellContext cellContext, + GridSelectOptionCellController cellContext, VoidCallback onDismissed, ) { SelectOptionCellEditor.remove(context); final editor = SelectOptionCellEditor( - cellContext: cellContext, + cellController: cellContext, onDismissed: onDismissed, ); @@ -146,7 +146,7 @@ class _TextField extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final optionMap = LinkedHashMap.fromIterable(state.selectedOptions, + final optionMap = LinkedHashMap.fromIterable(state.selectedOptions, key: (option) => option.name, value: (option) => option); return SizedBox( @@ -216,7 +216,7 @@ class _CreateOptionCell extends StatelessWidget { } class _SelectOptionCell extends StatelessWidget { - final SelectOption option; + final SelectOptionPB option; final bool isSelected; const _SelectOptionCell(this.option, this.isSelected, {Key? key}) : super(key: key); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/text_field.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/text_field.dart index 9bffd4554f..10b04cfb58 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/text_field.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/text_field.dart @@ -15,8 +15,8 @@ class SelectOptionTextField extends StatelessWidget { final FocusNode _focusNode; final TextEditingController _controller; final TextfieldTagsController tagController; - final List options; - final LinkedHashMap selectedOptionMap; + final List options; + final LinkedHashMap selectedOptionMap; final double distanceToText; diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart index 1bece5a3d7..55bf757ba4 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/text_cell.dart @@ -14,10 +14,10 @@ class GridTextCellStyle extends GridCellStyle { } class GridTextCell extends GridCellWidget { - final GridCellContextBuilder cellContextBuilder; + final GridCellControllerBuilder cellContorllerBuilder; late final GridTextCellStyle? cellStyle; GridTextCell({ - required this.cellContextBuilder, + required this.cellContorllerBuilder, GridCellStyle? style, Key? key, }) : super(key: key) { @@ -39,7 +39,7 @@ class _GridTextCellState extends GridFocusNodeCellState { @override void initState() { - final cellContext = widget.cellContextBuilder.build(); + final cellContext = widget.cellContorllerBuilder.build(); _cellBloc = getIt(param1: cellContext); _cellBloc.add(const TextCellEvent.initial()); _controller = TextEditingController(text: _cellBloc.state.content); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart index f4da18be86..b6bc8daa21 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/cell_editor.dart @@ -7,21 +7,21 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; class URLCellEditor extends StatefulWidget with FlowyOverlayDelegate { - final GridURLCellContext cellContext; + final GridURLCellController cellController; final VoidCallback completed; - const URLCellEditor({required this.cellContext, required this.completed, Key? key}) : super(key: key); + const URLCellEditor({required this.cellController, required this.completed, Key? key}) : super(key: key); @override State createState() => _URLCellEditorState(); static void show( BuildContext context, - GridURLCellContext cellContext, + GridURLCellController cellContext, VoidCallback completed, ) { FlowyOverlay.of(context).remove(identifier()); final editor = URLCellEditor( - cellContext: cellContext, + cellController: cellContext, completed: completed, ); @@ -62,7 +62,7 @@ class _URLCellEditorState extends State { @override void initState() { - _cellBloc = URLCellEditorBloc(cellContext: widget.cellContext); + _cellBloc = URLCellEditorBloc(cellContext: widget.cellController); _cellBloc.add(const URLCellEditorEvent.initial()); _controller = TextEditingController(text: _cellBloc.state.content); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart index e37dca6632..9cc14fbbda 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/url_cell/url_cell.dart @@ -31,10 +31,10 @@ enum GridURLCellAccessoryType { } class GridURLCell extends GridCellWidget { - final GridCellContextBuilder cellContextBuilder; + final GridCellControllerBuilder cellContorllerBuilder; late final GridURLCellStyle? cellStyle; GridURLCell({ - required this.cellContextBuilder, + required this.cellContorllerBuilder, GridCellStyle? style, Key? key, }) : super(key: key) { @@ -51,11 +51,11 @@ class GridURLCell extends GridCellWidget { GridCellAccessory accessoryFromType(GridURLCellAccessoryType ty, GridCellAccessoryBuildContext buildContext) { switch (ty) { case GridURLCellAccessoryType.edit: - final cellContext = cellContextBuilder.build() as GridURLCellContext; + final cellContext = cellContorllerBuilder.build() as GridURLCellController; return _EditURLAccessory(cellContext: cellContext, anchorContext: buildContext.anchorContext); case GridURLCellAccessoryType.copyURL: - final cellContext = cellContextBuilder.build() as GridURLCellContext; + final cellContext = cellContorllerBuilder.build() as GridURLCellController; return _CopyURLAccessory(cellContext: cellContext); } } @@ -83,7 +83,7 @@ class _GridURLCellState extends GridCellState { @override void initState() { - final cellContext = widget.cellContextBuilder.build() as GridURLCellContext; + final cellContext = widget.cellContorllerBuilder.build() as GridURLCellController; _cellBloc = URLCellBloc(cellContext: cellContext); _cellBloc.add(const URLCellEvent.initial()); super.initState(); @@ -132,7 +132,7 @@ class _GridURLCellState extends GridCellState { if (url.isNotEmpty && await canLaunchUrl(uri)) { await launchUrl(uri); } else { - final cellContext = widget.cellContextBuilder.build() as GridURLCellContext; + final cellContext = widget.cellContorllerBuilder.build() as GridURLCellController; widget.onCellEditing.value = true; URLCellEditor.show(context, cellContext, () { widget.onCellEditing.value = false; @@ -155,7 +155,7 @@ class _GridURLCellState extends GridCellState { } class _EditURLAccessory extends StatelessWidget with GridCellAccessory { - final GridURLCellContext cellContext; + final GridURLCellController cellContext; final BuildContext anchorContext; const _EditURLAccessory({ required this.cellContext, @@ -176,7 +176,7 @@ class _EditURLAccessory extends StatelessWidget with GridCellAccessory { } class _CopyURLAccessory extends StatelessWidget with GridCellAccessory { - final GridURLCellContext cellContext; + final GridURLCellController cellContext; const _CopyURLAccessory({required this.cellContext, Key? key}) : super(key: key); @override @@ -187,7 +187,7 @@ class _CopyURLAccessory extends StatelessWidget with GridCellAccessory { @override void onTap() { - final content = cellContext.getCellData(loadIfNoCache: false)?.content ?? ""; + final content = cellContext.getCellData(loadIfNotExist: false)?.content ?? ""; Clipboard.setData(ClipboardData(text: content)); showMessageToast(LocaleKeys.grid_row_copyProperty.tr()); } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart index 9237c71be0..8202a3537f 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_cell.dart @@ -65,7 +65,7 @@ class GridFieldCell extends StatelessWidget { FieldEditor( gridId: state.gridId, fieldName: field.name, - contextLoader: FieldContextLoader( + typeOptionLoader: FieldTypeOptionLoader( gridId: state.gridId, field: field, ), @@ -135,7 +135,7 @@ class _DragToExpandLine extends StatelessWidget { class FieldCellButton extends StatelessWidget { final VoidCallback onTap; - final Field field; + final GridFieldPB field; const FieldCellButton({ required this.field, required this.onTap, diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart index 3390351663..3a857a9fb4 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor.dart @@ -8,17 +8,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'field_name_input.dart'; -import 'field_editor_pannel.dart'; +import 'field_type_option_editor.dart'; class FieldEditor extends StatelessWidget with FlowyOverlayDelegate { final String gridId; final String fieldName; - final IFieldContextLoader contextLoader; + final IFieldTypeOptionLoader typeOptionLoader; const FieldEditor({ required this.gridId, required this.fieldName, - required this.contextLoader, + required this.typeOptionLoader, Key? key, }) : super(key: key); @@ -28,7 +28,7 @@ class FieldEditor extends StatelessWidget with FlowyOverlayDelegate { create: (context) => FieldEditorBloc( gridId: gridId, fieldName: fieldName, - fieldContextLoader: contextLoader, + loader: typeOptionLoader, )..add(const FieldEditorEvent.initial()), child: BlocBuilder( buildWhen: (p, c) => false, @@ -38,9 +38,9 @@ class FieldEditor extends StatelessWidget with FlowyOverlayDelegate { children: [ FlowyText.medium(LocaleKeys.grid_field_editProperty.tr(), fontSize: 12), const VSpace(10), - const _FieldNameTextField(), + const _FieldNameCell(), const VSpace(10), - const _FieldPannel(), + const _FieldTypeOptionCell(), ], ); }, @@ -74,25 +74,28 @@ class FieldEditor extends StatelessWidget with FlowyOverlayDelegate { bool asBarrier() => true; } -class _FieldPannel extends StatelessWidget { - const _FieldPannel({Key? key}) : super(key: key); +class _FieldTypeOptionCell extends StatelessWidget { + const _FieldTypeOptionCell({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocBuilder( - buildWhen: (p, c) => p.fieldContext != c.fieldContext, + buildWhen: (p, c) => p.field != c.field, builder: (context, state) { - return state.fieldContext.fold( + return state.field.fold( () => const SizedBox(), - (fieldContext) => FieldEditorPannel(fieldContext: fieldContext), + (fieldContext) { + final dataController = context.read().dataController; + return FieldTypeOptionEditor(dataController: dataController); + }, ); }, ); } } -class _FieldNameTextField extends StatelessWidget { - const _FieldNameTextField({Key? key}) : super(key: key); +class _FieldNameCell extends StatelessWidget { + const _FieldNameCell({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart deleted file mode 100644 index 7ec5144458..0000000000 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart +++ /dev/null @@ -1,243 +0,0 @@ -import 'dart:typed_data'; - -import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_type_option.dart'; -import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/checkbox.dart'; -import 'package:dartz/dartz.dart' show Either; -import 'package:flowy_infra/image.dart'; -import 'package:flowy_infra/theme.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/button.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:app_flowy/workspace/application/grid/prelude.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_type_list.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart'; -import 'field_type_extension.dart'; -import 'type_option/multi_select.dart'; -import 'type_option/number.dart'; -import 'type_option/rich_text.dart'; -import 'type_option/single_select.dart'; -import 'type_option/url.dart'; - -typedef UpdateFieldCallback = void Function(Field, Uint8List); -typedef SwitchToFieldCallback = Future> Function( - String fieldId, - FieldType fieldType, -); - -class FieldEditorPannel extends StatefulWidget { - final GridFieldContext fieldContext; - - const FieldEditorPannel({ - required this.fieldContext, - Key? key, - }) : super(key: key); - - @override - State createState() => _FieldEditorPannelState(); -} - -class _FieldEditorPannelState extends State { - String? currentOverlayIdentifier; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FieldEditorPannelBloc(widget.fieldContext)..add(const FieldEditorPannelEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - List children = [_switchFieldTypeButton(context, widget.fieldContext.field)]; - final typeOptionWidget = _typeOptionWidget(context: context, state: state); - - if (typeOptionWidget != null) { - children.add(typeOptionWidget); - } - - return ListView( - shrinkWrap: true, - children: children, - ); - }, - ), - ); - } - - Widget _switchFieldTypeButton(BuildContext context, Field field) { - final theme = context.watch(); - return SizedBox( - height: GridSize.typeOptionItemHeight, - child: FlowyButton( - text: FlowyText.medium(field.fieldType.title(), fontSize: 12), - margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - hoverColor: theme.hover, - onTap: () { - final list = FieldTypeList(onSelectField: (newFieldType) { - widget.fieldContext.switchToField(newFieldType); - }); - _showOverlay(context, list); - }, - leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor), - rightIcon: svgWidget("grid/more", color: theme.iconColor), - ), - ); - } - - Widget? _typeOptionWidget({ - required BuildContext context, - required FieldEditorPannelState state, - }) { - final overlayDelegate = TypeOptionOverlayDelegate( - showOverlay: _showOverlay, - hideOverlay: _hideOverlay, - ); - - final builder = _makeTypeOptionBuild( - typeOptionContext: _makeTypeOptionContext(widget.fieldContext), - overlayDelegate: overlayDelegate, - ); - - return builder.customWidget; - } - - void _showOverlay(BuildContext context, Widget child, {VoidCallback? onRemoved}) { - final identifier = child.toString(); - if (currentOverlayIdentifier != null) { - FlowyOverlay.of(context).remove(currentOverlayIdentifier!); - } - - currentOverlayIdentifier = identifier; - FlowyOverlay.of(context).insertWithAnchor( - widget: OverlayContainer( - child: child, - constraints: BoxConstraints.loose(const Size(460, 440)), - ), - identifier: identifier, - anchorContext: context, - anchorDirection: AnchorDirection.leftWithCenterAligned, - style: FlowyOverlayStyle(blur: false), - anchorOffset: const Offset(-20, 0), - ); - } - - void _hideOverlay(BuildContext context) { - if (currentOverlayIdentifier != null) { - FlowyOverlay.of(context).remove(currentOverlayIdentifier!); - } - } -} - -abstract class TypeOptionBuilder { - Widget? get customWidget; -} - -TypeOptionBuilder _makeTypeOptionBuild({ - required TypeOptionContext typeOptionContext, - required TypeOptionOverlayDelegate overlayDelegate, -}) { - switch (typeOptionContext.field.fieldType) { - case FieldType.Checkbox: - return CheckboxTypeOptionBuilder( - typeOptionContext as CheckboxTypeOptionContext, - ); - case FieldType.DateTime: - return DateTypeOptionBuilder( - typeOptionContext as DateTypeOptionContext, - overlayDelegate, - ); - case FieldType.SingleSelect: - return SingleSelectTypeOptionBuilder( - typeOptionContext as SingleSelectTypeOptionContext, - overlayDelegate, - ); - case FieldType.MultiSelect: - return MultiSelectTypeOptionBuilder( - typeOptionContext as MultiSelectTypeOptionContext, - overlayDelegate, - ); - case FieldType.Number: - return NumberTypeOptionBuilder( - typeOptionContext as NumberTypeOptionContext, - overlayDelegate, - ); - case FieldType.RichText: - return RichTextTypeOptionBuilder( - typeOptionContext as RichTextTypeOptionContext, - ); - - case FieldType.URL: - return URLTypeOptionBuilder( - typeOptionContext as URLTypeOptionContext, - ); - } - throw UnimplementedError; -} - -TypeOptionContext _makeTypeOptionContext(GridFieldContext fieldContext) { - switch (fieldContext.field.fieldType) { - case FieldType.Checkbox: - return CheckboxTypeOptionContext( - fieldContext: fieldContext, - dataBuilder: CheckboxTypeOptionDataBuilder(), - ); - case FieldType.DateTime: - return DateTypeOptionContext( - fieldContext: fieldContext, - dataBuilder: DateTypeOptionDataBuilder(), - ); - case FieldType.MultiSelect: - return MultiSelectTypeOptionContext( - fieldContext: fieldContext, - dataBuilder: MultiSelectTypeOptionDataBuilder(), - ); - case FieldType.Number: - return NumberTypeOptionContext( - fieldContext: fieldContext, - dataBuilder: NumberTypeOptionDataBuilder(), - ); - case FieldType.RichText: - return RichTextTypeOptionContext( - fieldContext: fieldContext, - dataBuilder: RichTextTypeOptionDataBuilder(), - ); - case FieldType.SingleSelect: - return SingleSelectTypeOptionContext( - fieldContext: fieldContext, - dataBuilder: SingleSelectTypeOptionDataBuilder(), - ); - - case FieldType.URL: - return URLTypeOptionContext( - fieldContext: fieldContext, - dataBuilder: URLTypeOptionDataBuilder(), - ); - } - - throw UnimplementedError(); -} - -abstract class TypeOptionWidget extends StatelessWidget { - const TypeOptionWidget({Key? key}) : super(key: key); -} - -typedef TypeOptionData = Uint8List; -typedef TypeOptionDataCallback = void Function(TypeOptionData typeOptionData); -typedef ShowOverlayCallback = void Function( - BuildContext anchorContext, - Widget child, { - VoidCallback? onRemoved, -}); -typedef HideOverlayCallback = void Function(BuildContext anchorContext); - -class TypeOptionOverlayDelegate { - ShowOverlayCallback showOverlay; - HideOverlayCallback hideOverlay; - TypeOptionOverlayDelegate({ - required this.showOverlay, - required this.hideOverlay, - }); -} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_option_editor.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_option_editor.dart new file mode 100644 index 0000000000..95dd39bae9 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/field_type_option_editor.dart @@ -0,0 +1,127 @@ +import 'dart:typed_data'; +import 'package:dartz/dartz.dart' show Either; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra/theme.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:app_flowy/workspace/application/grid/prelude.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_type_list.dart'; +import 'field_type_extension.dart'; +import 'type_option/builder.dart'; + +typedef UpdateFieldCallback = void Function(GridFieldPB, Uint8List); +typedef SwitchToFieldCallback = Future> Function( + String fieldId, + FieldType fieldType, +); + +class FieldTypeOptionEditor extends StatefulWidget { + final TypeOptionDataController dataController; + + const FieldTypeOptionEditor({ + required this.dataController, + Key? key, + }) : super(key: key); + + @override + State createState() => _FieldTypeOptionEditorState(); +} + +class _FieldTypeOptionEditorState extends State { + String? currentOverlayIdentifier; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + FieldTypeOptionEditBloc(widget.dataController)..add(const FieldTypeOptionEditEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + List children = [_switchFieldTypeButton(context, widget.dataController.field)]; + final typeOptionWidget = _typeOptionWidget(context: context, state: state); + + if (typeOptionWidget != null) { + children.add(typeOptionWidget); + } + + return ListView( + shrinkWrap: true, + children: children, + ); + }, + ), + ); + } + + Widget _switchFieldTypeButton(BuildContext context, GridFieldPB field) { + final theme = context.watch(); + return SizedBox( + height: GridSize.typeOptionItemHeight, + child: FlowyButton( + text: FlowyText.medium(field.fieldType.title(), fontSize: 12), + margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + hoverColor: theme.hover, + onTap: () { + final list = FieldTypeList(onSelectField: (newFieldType) { + widget.dataController.switchToField(newFieldType); + }); + _showOverlay(context, list); + }, + leftIcon: svgWidget(field.fieldType.iconName(), color: theme.iconColor), + rightIcon: svgWidget("grid/more", color: theme.iconColor), + ), + ); + } + + Widget? _typeOptionWidget({ + required BuildContext context, + required FieldTypeOptionEditState state, + }) { + final overlayDelegate = TypeOptionOverlayDelegate( + showOverlay: _showOverlay, + hideOverlay: _hideOverlay, + ); + + return makeTypeOptionWidget( + context: context, + dataController: widget.dataController, + overlayDelegate: overlayDelegate, + ); + } + + void _showOverlay(BuildContext context, Widget child, {VoidCallback? onRemoved}) { + final identifier = child.toString(); + if (currentOverlayIdentifier != null) { + FlowyOverlay.of(context).remove(currentOverlayIdentifier!); + } + + currentOverlayIdentifier = identifier; + FlowyOverlay.of(context).insertWithAnchor( + widget: OverlayContainer( + child: child, + constraints: BoxConstraints.loose(const Size(460, 440)), + ), + identifier: identifier, + anchorContext: context, + anchorDirection: AnchorDirection.leftWithCenterAligned, + style: FlowyOverlayStyle(blur: false), + anchorOffset: const Offset(-20, 0), + ); + } + + void _hideOverlay(BuildContext context) { + if (currentOverlayIdentifier != null) { + FlowyOverlay.of(context).remove(currentOverlayIdentifier!); + } + } +} + +abstract class TypeOptionWidget extends StatelessWidget { + const TypeOptionWidget({Key? key}) : super(key: key); +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart index cc968d15cb..5a888129b5 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/grid_header.dart @@ -151,7 +151,7 @@ class CreateFieldButton extends StatelessWidget { onTap: () => FieldEditor( gridId: gridId, fieldName: "", - contextLoader: NewFieldContextLoader(gridId: gridId), + typeOptionLoader: NewFieldTypeOptionLoader(gridId: gridId), ).show(context), leftIcon: svgWidget("home/add"), ); @@ -160,7 +160,7 @@ class CreateFieldButton extends StatelessWidget { class SliverHeaderDelegateImplementation extends SliverPersistentHeaderDelegate { final String gridId; - final List fields; + final List fields; SliverHeaderDelegateImplementation({required this.gridId, required this.fields}); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/builder.dart new file mode 100644 index 0000000000..63fa761d98 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/builder.dart @@ -0,0 +1,108 @@ +import 'dart:typed_data'; + +import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_type_option.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/type_option/checkbox.dart'; +import 'package:app_flowy/workspace/application/grid/prelude.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flutter/material.dart'; +import 'date.dart'; +import 'multi_select.dart'; +import 'number.dart'; +import 'rich_text.dart'; +import 'single_select.dart'; +import 'url.dart'; + +typedef TypeOptionData = Uint8List; +typedef TypeOptionDataCallback = void Function(TypeOptionData typeOptionData); +typedef ShowOverlayCallback = void Function( + BuildContext anchorContext, + Widget child, { + VoidCallback? onRemoved, +}); +typedef HideOverlayCallback = void Function(BuildContext anchorContext); + +class TypeOptionOverlayDelegate { + ShowOverlayCallback showOverlay; + HideOverlayCallback hideOverlay; + TypeOptionOverlayDelegate({ + required this.showOverlay, + required this.hideOverlay, + }); +} + +abstract class TypeOptionWidgetBuilder { + Widget? build(BuildContext context); +} + +Widget? makeTypeOptionWidget({ + required BuildContext context, + required TypeOptionDataController dataController, + required TypeOptionOverlayDelegate overlayDelegate, +}) { + final builder = makeTypeOptionWidgetBuilder(dataController, overlayDelegate); + return builder.build(context); +} + +TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder( + TypeOptionDataController dataController, + TypeOptionOverlayDelegate overlayDelegate, +) { + switch (dataController.field.fieldType) { + case FieldType.Checkbox: + final context = CheckboxTypeOptionContext( + dataController: dataController, + dataParser: CheckboxTypeOptionWidgetDataParser(), + ); + return CheckboxTypeOptionWidgetBuilder(context); + case FieldType.DateTime: + final context = DateTypeOptionContext( + dataController: dataController, + dataParser: DateTypeOptionDataParser(), + ); + return DateTypeOptionWidgetBuilder( + context, + overlayDelegate, + ); + case FieldType.SingleSelect: + final context = SingleSelectTypeOptionContext( + fieldContext: dataController, + dataBuilder: SingleSelectTypeOptionWidgetDataParser(), + ); + return SingleSelectTypeOptionWidgetBuilder( + context, + overlayDelegate, + ); + case FieldType.MultiSelect: + final context = MultiSelectTypeOptionContext( + dataController: dataController, + dataBuilder: MultiSelectTypeOptionWidgetDataParser(), + ); + return MultiSelectTypeOptionWidgetBuilder( + context, + overlayDelegate, + ); + case FieldType.Number: + final context = NumberTypeOptionContext( + dataController: dataController, + dataParser: NumberTypeOptionWidgetDataParser(), + ); + return NumberTypeOptionWidgetBuilder( + context, + overlayDelegate, + ); + case FieldType.RichText: + final context = RichTextTypeOptionContext( + dataController: dataController, + dataParser: RichTextTypeOptionWidgetDataParser(), + ); + return RichTextTypeOptionWidgetBuilder(context); + + case FieldType.URL: + final context = URLTypeOptionContext( + dataController: dataController, + dataParser: URLTypeOptionWidgetDataParser(), + ); + return URLTypeOptionWidgetBuilder(context); + } + throw UnimplementedError; +} diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/checkbox.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/checkbox.dart index c1e202f358..beca8acd09 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/checkbox.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/checkbox.dart @@ -1,20 +1,20 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart'; import 'package:flutter/material.dart'; +import 'builder.dart'; -typedef CheckboxTypeOptionContext = TypeOptionContext; +typedef CheckboxTypeOptionContext = TypeOptionWidgetContext; -class CheckboxTypeOptionDataBuilder extends TypeOptionDataBuilder { +class CheckboxTypeOptionWidgetDataParser extends TypeOptionDataParser { @override CheckboxTypeOption fromBuffer(List buffer) { return CheckboxTypeOption.fromBuffer(buffer); } } -class CheckboxTypeOptionBuilder extends TypeOptionBuilder { - CheckboxTypeOptionBuilder(CheckboxTypeOptionContext typeOptionContext); +class CheckboxTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + CheckboxTypeOptionWidgetBuilder(CheckboxTypeOptionContext typeOptionContext); @override - Widget? get customWidget => null; + Widget? build(BuildContext context) => null; } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart index f4031dc9ce..39408c9ee3 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart @@ -1,6 +1,6 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/date_bloc.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_type_option_editor.dart'; import 'package:easy_localization/easy_localization.dart' hide DateFormat; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:flowy_infra/image.dart'; @@ -9,14 +9,15 @@ import 'package:flowy_infra_ui/flowy_infra_ui.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:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'builder.dart'; -class DateTypeOptionBuilder extends TypeOptionBuilder { +class DateTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { final DateTypeOptionWidget _widget; - DateTypeOptionBuilder( + DateTypeOptionWidgetBuilder( DateTypeOptionContext typeOptionContext, TypeOptionOverlayDelegate overlayDelegate, ) : _widget = DateTypeOptionWidget( @@ -25,7 +26,9 @@ class DateTypeOptionBuilder extends TypeOptionBuilder { ); @override - Widget? get customWidget => _widget; + Widget? build(BuildContext context) { + return _widget; + } } class DateTypeOptionWidget extends TypeOptionWidget { diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/multi_select.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/multi_select.dart index fc310410f9..8c7ab40a6f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/multi_select.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/multi_select.dart @@ -1,13 +1,14 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/multi_select_type_option.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_type_option_editor.dart'; import 'package:flutter/material.dart'; +import 'builder.dart'; import 'select_option.dart'; -class MultiSelectTypeOptionBuilder extends TypeOptionBuilder { +class MultiSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { final MultiSelectTypeOptionWidget _widget; - MultiSelectTypeOptionBuilder( + MultiSelectTypeOptionWidgetBuilder( MultiSelectTypeOptionContext typeOptionContext, TypeOptionOverlayDelegate overlayDelegate, ) : _widget = MultiSelectTypeOptionWidget( @@ -16,7 +17,7 @@ class MultiSelectTypeOptionBuilder extends TypeOptionBuilder { ); @override - Widget? get customWidget => _widget; + Widget? build(BuildContext context) => _widget; } class MultiSelectTypeOptionWidget extends TypeOptionWidget { diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/number.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/number.dart index bca17e1cd8..e15abb1728 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/number.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/number.dart @@ -2,7 +2,7 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/number_bl import 'package:app_flowy/workspace/application/grid/field/type_option/number_format_bloc.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/common/text_field.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_type_option_editor.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -15,10 +15,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:easy_localization/easy_localization.dart' hide NumberFormat; import 'package:app_flowy/generated/locale_keys.g.dart'; -class NumberTypeOptionBuilder extends TypeOptionBuilder { +import 'builder.dart'; + +class NumberTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { final NumberTypeOptionWidget _widget; - NumberTypeOptionBuilder( + NumberTypeOptionWidgetBuilder( NumberTypeOptionContext typeOptionContext, TypeOptionOverlayDelegate overlayDelegate, ) : _widget = NumberTypeOptionWidget( @@ -27,7 +29,7 @@ class NumberTypeOptionBuilder extends TypeOptionBuilder { ); @override - Widget? get customWidget => _widget; + Widget? build(BuildContext context) => _widget; } class NumberTypeOptionWidget extends TypeOptionWidget { diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/rich_text.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/rich_text.dart index 03f9ed347c..2375918f11 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/rich_text.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/rich_text.dart @@ -1,21 +1,20 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; - import 'package:flutter/material.dart'; +import 'builder.dart'; -typedef RichTextTypeOptionContext = TypeOptionContext; +typedef RichTextTypeOptionContext = TypeOptionWidgetContext; -class RichTextTypeOptionDataBuilder extends TypeOptionDataBuilder { +class RichTextTypeOptionWidgetDataParser extends TypeOptionDataParser { @override RichTextTypeOption fromBuffer(List buffer) { return RichTextTypeOption.fromBuffer(buffer); } } -class RichTextTypeOptionBuilder extends TypeOptionBuilder { - RichTextTypeOptionBuilder(RichTextTypeOptionContext typeOptionContext); +class RichTextTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + RichTextTypeOptionWidgetBuilder(RichTextTypeOptionContext typeOptionContext); @override - Widget? get customWidget => null; + Widget? build(BuildContext context) => null; } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/select_option.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/select_option.dart index 60c1bfcdd6..c0d54159e8 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/select_option.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/select_option.dart @@ -2,7 +2,6 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/select_op import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/cell/select_option_cell/extension.dart'; import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/common/text_field.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; @@ -14,10 +13,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; +import 'builder.dart'; import 'select_option_editor.dart'; class SelectOptionTypeOptionWidget extends StatelessWidget { - final List options; + final List options; final VoidCallback beginEdit; final TypeOptionOverlayDelegate overlayDelegate; final SelectOptionTypeOptionAction typeOptionAction; @@ -131,7 +131,7 @@ class _OptionList extends StatelessWidget { ); } - _OptionCell _makeOptionCell(BuildContext context, SelectOption option) { + _OptionCell _makeOptionCell(BuildContext context, SelectOptionPB option) { return _OptionCell( option: option, onSelected: (option) { @@ -154,8 +154,8 @@ class _OptionList extends StatelessWidget { } class _OptionCell extends StatelessWidget { - final SelectOption option; - final Function(SelectOption) onSelected; + final SelectOptionPB option; + final Function(SelectOptionPB) onSelected; const _OptionCell({ required this.option, required this.onSelected, diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/select_option_editor.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/select_option_editor.dart index 3abb0445c7..5381342d20 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/select_option_editor.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/select_option_editor.dart @@ -15,9 +15,9 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; class SelectOptionTypeOptionEditor extends StatelessWidget { - final SelectOption option; + final SelectOptionPB option; final VoidCallback onDeleted; - final Function(SelectOption) onUpdated; + final Function(SelectOptionPB) onUpdated; const SelectOptionTypeOptionEditor({ required this.option, required this.onDeleted, @@ -110,12 +110,12 @@ class _OptionNameTextField extends StatelessWidget { } class SelectOptionColorList extends StatelessWidget { - final SelectOptionColor selectedColor; + final SelectOptionColorPB selectedColor; const SelectOptionColorList({required this.selectedColor, Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final cells = SelectOptionColor.values.map((color) { + final cells = SelectOptionColorPB.values.map((color) { return _SelectOptionColorCell(color: color, isSelected: selectedColor == color); }).toList(); @@ -152,7 +152,7 @@ class SelectOptionColorList extends StatelessWidget { } class _SelectOptionColorCell extends StatelessWidget { - final SelectOptionColor color; + final SelectOptionColorPB color; final bool isSelected; const _SelectOptionColorCell({required this.color, required this.isSelected, Key? key}) : super(key: key); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/single_select.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/single_select.dart index fedddec11d..a1ed4e857a 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/single_select.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/single_select.dart @@ -1,12 +1,13 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/single_select_type_option.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart'; +import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_type_option_editor.dart'; import 'package:flutter/material.dart'; +import 'builder.dart'; import 'select_option.dart'; -class SingleSelectTypeOptionBuilder extends TypeOptionBuilder { +class SingleSelectTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { final SingleSelectTypeOptionWidget _widget; - SingleSelectTypeOptionBuilder( + SingleSelectTypeOptionWidgetBuilder( SingleSelectTypeOptionContext typeOptionContext, TypeOptionOverlayDelegate overlayDelegate, ) : _widget = SingleSelectTypeOptionWidget( @@ -15,7 +16,7 @@ class SingleSelectTypeOptionBuilder extends TypeOptionBuilder { ); @override - Widget? get customWidget => _widget; + Widget? build(BuildContext context) => _widget; } class SingleSelectTypeOptionWidget extends TypeOptionWidget { diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/url.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/url.dart index f4e73f7fdc..97c0db0814 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/url.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/url.dart @@ -1,20 +1,20 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart'; -import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/header/field_editor_pannel.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; import 'package:flutter/material.dart'; +import 'builder.dart'; -typedef URLTypeOptionContext = TypeOptionContext; +typedef URLTypeOptionContext = TypeOptionWidgetContext; -class URLTypeOptionDataBuilder extends TypeOptionDataBuilder { +class URLTypeOptionWidgetDataParser extends TypeOptionDataParser { @override URLTypeOption fromBuffer(List buffer) { return URLTypeOption.fromBuffer(buffer); } } -class URLTypeOptionBuilder extends TypeOptionBuilder { - URLTypeOptionBuilder(URLTypeOptionContext typeOptionContext); +class URLTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + URLTypeOptionWidgetBuilder(URLTypeOptionContext typeOptionContext); @override - Widget? get customWidget => null; + Widget? build(BuildContext context) => null; } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart index de7f117d51..c3e2e17b7e 100755 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart @@ -10,19 +10,25 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; -import 'row_action_sheet.dart'; +import 'row_action_sheet.dart'; import 'row_detail.dart'; class GridRowWidget extends StatefulWidget { - final GridRow rowData; - final GridRowCacheService rowCache; + final GridRowInfo rowData; + final GridRowCache rowCache; + final GridCellBuilder cellBuilder; - const GridRowWidget({ + GridRowWidget({ required this.rowData, required this.rowCache, + required GridFieldCache fieldCache, Key? key, - }) : super(key: key); + }) : cellBuilder = GridCellBuilder( + cellCache: rowCache.cellCache, + fieldCache: fieldCache, + ), + super(key: key); @override State createState() => _GridRowWidgetState(); @@ -34,7 +40,7 @@ class _GridRowWidgetState extends State { @override void initState() { _rowBloc = RowBloc( - rowData: widget.rowData, + rowInfo: widget.rowData, rowCache: widget.rowCache, ); _rowBloc.add(const RowEvent.initial()); @@ -47,12 +53,16 @@ class _GridRowWidgetState extends State { value: _rowBloc, child: _RowEnterRegion( child: BlocBuilder( - buildWhen: (p, c) => p.rowData.height != c.rowData.height, + buildWhen: (p, c) => p.rowInfo.height != c.rowInfo.height, builder: (context, state) { return Row( children: [ const _RowLeading(), - Expanded(child: _RowCells(cellCache: widget.rowCache.cellCache, onExpand: () => _expandRow(context))), + Expanded( + child: _RowCells( + builder: widget.cellBuilder, + onExpand: () => _expandRow(context), + )), const _RowTrailing(), ], ); @@ -70,8 +80,9 @@ class _GridRowWidgetState extends State { void _expandRow(BuildContext context) { final page = RowDetailPage( - rowData: widget.rowData, + rowInfo: widget.rowData, rowCache: widget.rowCache, + cellBuilder: widget.cellBuilder, ); page.show(context); } @@ -137,7 +148,7 @@ class _DeleteRowButton extends StatelessWidget { width: 20, height: 30, onPressed: () => GridRowActionSheet( - rowData: context.read().state.rowData, + rowData: context.read().state.rowInfo, ).show(context), iconPadding: const EdgeInsets.all(3), icon: svgWidget("editor/details"), @@ -146,9 +157,13 @@ class _DeleteRowButton extends StatelessWidget { } class _RowCells extends StatelessWidget { - final GridCellCacheService cellCache; final VoidCallback onExpand; - const _RowCells({required this.cellCache, required this.onExpand, Key? key}) : super(key: key); + final GridCellBuilder builder; + const _RowCells({ + required this.builder, + required this.onExpand, + Key? key, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -168,13 +183,12 @@ class _RowCells extends StatelessWidget { List _makeCells(BuildContext context, GridCellMap gridCellMap) { return gridCellMap.values.map( - (gridCell) { - final GridCellWidget child = buildGridCellWidget(gridCell, cellCache); - + (cellId) { + final GridCellWidget child = builder.build(cellId); accessoryBuilder(GridCellAccessoryBuildContext buildContext) { final builder = child.accessoryBuilder; List accessories = []; - if (gridCell.field.isPrimary) { + if (cellId.field.isPrimary) { accessories.add(PrimaryCellAccessory( onTapCallback: onExpand, isCellEditing: buildContext.isCellEditing, @@ -188,7 +202,7 @@ class _RowCells extends StatelessWidget { } return CellContainer( - width: gridCell.field.width.toDouble(), + width: cellId.field.width.toDouble(), child: child, rowStateNotifier: Provider.of(context, listen: false), accessoryBuilder: accessoryBuilder, diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart index 1d7886c86c..06e9c68d19 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_action_sheet.dart @@ -14,7 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class GridRowActionSheet extends StatelessWidget { - final GridRow rowData; + final GridRowInfo rowData; const GridRowActionSheet({required this.rowData, Key? key}) : super(key: key); @override diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart index c4ccc32a1a..a01f97b847 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/row/row_detail.dart @@ -21,12 +21,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class RowDetailPage extends StatefulWidget with FlowyOverlayDelegate { - final GridRow rowData; - final GridRowCacheService rowCache; + final GridRowInfo rowInfo; + final GridRowCache rowCache; + final GridCellBuilder cellBuilder; const RowDetailPage({ - required this.rowData, + required this.rowInfo, required this.rowCache, + required this.cellBuilder, Key? key, }) : super(key: key); @@ -60,7 +62,7 @@ class _RowDetailPageState extends State { Widget build(BuildContext context) { return BlocProvider( create: (context) { - final bloc = RowDetailBloc(rowData: widget.rowData, rowCache: widget.rowCache); + final bloc = RowDetailBloc(rowInfo: widget.rowInfo, rowCache: widget.rowCache); bloc.add(const RowDetailEvent.initial()); return bloc; }, @@ -74,7 +76,7 @@ class _RowDetailPageState extends State { children: const [Spacer(), _CloseButton()], ), ), - Expanded(child: _PropertyList(cellCache: widget.rowCache.cellCache)), + Expanded(child: _PropertyList(cellBuilder: widget.cellBuilder)), ], ), ), @@ -98,10 +100,10 @@ class _CloseButton extends StatelessWidget { } class _PropertyList extends StatelessWidget { - final GridCellCacheService cellCache; + final GridCellBuilder cellBuilder; final ScrollController _scrollController; _PropertyList({ - required this.cellCache, + required this.cellBuilder, Key? key, }) : _scrollController = ScrollController(), super(key: key); @@ -120,8 +122,8 @@ class _PropertyList extends StatelessWidget { itemCount: state.gridCells.length, itemBuilder: (BuildContext context, int index) { return _RowDetailCell( - gridCell: state.gridCells[index], - cellCache: cellCache, + cellId: state.gridCells[index], + cellBuilder: cellBuilder, ); }, separatorBuilder: (BuildContext context, int index) { @@ -135,19 +137,19 @@ class _PropertyList extends StatelessWidget { } class _RowDetailCell extends StatelessWidget { - final GridCell gridCell; - final GridCellCacheService cellCache; + final GridCellIdentifier cellId; + final GridCellBuilder cellBuilder; const _RowDetailCell({ - required this.gridCell, - required this.cellCache, + required this.cellId, + required this.cellBuilder, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { final theme = context.watch(); - final style = _customCellStyle(theme, gridCell.field.fieldType); - final cell = buildGridCellWidget(gridCell, cellCache, style: style); + final style = _customCellStyle(theme, cellId.fieldType); + final cell = cellBuilder.build(cellId, style: style); final gesture = GestureDetector( behavior: HitTestBehavior.translucent, @@ -167,7 +169,7 @@ class _RowDetailCell extends StatelessWidget { children: [ SizedBox( width: 150, - child: FieldCellButton(field: gridCell.field, onTap: () => _showFieldEditor(context)), + child: FieldCellButton(field: cellId.field, onTap: () => _showFieldEditor(context)), ), const HSpace(10), Expanded(child: gesture), @@ -179,11 +181,11 @@ class _RowDetailCell extends StatelessWidget { void _showFieldEditor(BuildContext context) { FieldEditor( - gridId: gridCell.gridId, - fieldName: gridCell.field.name, - contextLoader: FieldContextLoader( - gridId: gridCell.gridId, - field: gridCell.field, + gridId: cellId.gridId, + fieldName: cellId.field.name, + typeOptionLoader: FieldTypeOptionLoader( + gridId: cellId.gridId, + field: cellId.field, ), ).show(context); } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_property.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_property.dart index e19c90ecea..b34007493b 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_property.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/toolbar/grid_property.dart @@ -75,7 +75,7 @@ class GridPropertyList extends StatelessWidget with FlowyOverlayDelegate { } class _GridPropertyCell extends StatelessWidget { - final Field field; + final GridFieldPB field; final String gridId; const _GridPropertyCell({required this.gridId, required this.field, Key? key}) : super(key: key); @@ -116,7 +116,7 @@ class _GridPropertyCell extends StatelessWidget { FieldEditor( gridId: gridId, fieldName: field.name, - contextLoader: FieldContextLoader(gridId: gridId, field: field), + typeOptionLoader: FieldTypeOptionLoader(gridId: gridId, field: field), ).show(context, anchorDirection: AnchorDirection.bottomRight); }, ); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/trash_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/trash_cell.dart index 92b1f61cd0..4d6604e4af 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/trash_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/trash/src/trash_cell.dart @@ -14,7 +14,7 @@ import 'sizes.dart'; class TrashCell extends StatelessWidget { final VoidCallback onRestore; final VoidCallback onDelete; - final Trash object; + final TrashPB object; const TrashCell({required this.object, required this.onRestore, required this.onDelete, Key? key}) : super(key: key); @override diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/trash/trash.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/trash/trash.dart index 324b676fc3..e098eddf96 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/trash/trash.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/trash/trash.dart @@ -31,7 +31,7 @@ class TrashPluginBuilder extends PluginBuilder { } @override - String get menuName => "Trash"; + String get menuName => "TrashPB"; @override PluginType get pluginType => DefaultPlugin.trash.type(); diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/widgets/left_bar_item.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/widgets/left_bar_item.dart index bd9f1441e8..0d06ec56ce 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/widgets/left_bar_item.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/widgets/left_bar_item.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class ViewLeftBarItem extends StatefulWidget { - final View view; + final ViewPB view; ViewLeftBarItem({required this.view, Key? key}) : super(key: ValueKey(view.hashCode)); @@ -20,7 +20,7 @@ class _ViewLeftBarItemState extends State { final _focusNode = FocusNode(); late ViewService _viewService; late ViewListener _viewListener; - late View view; + late ViewPB view; @override void initState() { diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart index 8c7bb8f494..eaf09d770f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart @@ -1,70 +1,75 @@ +import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/workspace/application/appearance.dart'; import 'package:app_flowy/workspace/presentation/settings/widgets/settings_appearance_view.dart'; import 'package:app_flowy/workspace/presentation/settings/widgets/settings_language_view.dart'; +import 'package:app_flowy/workspace/presentation/settings/widgets/settings_user_view.dart'; import 'package:app_flowy/workspace/presentation/settings/widgets/settings_menu.dart'; +import 'package:app_flowy/workspace/application/settings/settings_dialog_bloc.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; -class SettingsDialog extends StatefulWidget { - const SettingsDialog({Key? key}) : super(key: key); +class SettingsDialog extends StatelessWidget { + final UserProfilePB user; + SettingsDialog(this.user, {Key? key}) : super(key: ValueKey(user.id)); - @override - State createState() => _SettingsDialogState(); -} - -class _SettingsDialogState extends State { - int _selectedViewIndex = 0; - - final List settingsViews = const [ - SettingsAppearanceView(), - SettingsLanguageView(), - ]; + Widget getSettingsView(int index, UserProfilePB user) { + final List settingsViews = [ + const SettingsAppearanceView(), + const SettingsLanguageView(), + SettingsUserView(user), + ]; + return settingsViews[index]; + } @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: Provider.of(context, listen: true), - child: AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - title: Text( - LocaleKeys.settings_title.tr(), - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - content: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 600, - minWidth: 600, - maxWidth: 1000, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 200, - child: SettingsMenu( - changeSelectedIndex: (index) { - setState(() { - _selectedViewIndex = index; - }); - }, - currentIndex: _selectedViewIndex, - ), - ), - const VerticalDivider(), - const SizedBox(width: 10), - Expanded( - child: settingsViews[_selectedViewIndex], - ) - ], - ), - ), - ), - ); + return BlocProvider( + create: (context) => getIt(param1: user)..add(const SettingsDialogEvent.initial()), + child: BlocBuilder( + builder: (context, state) => ChangeNotifierProvider.value( + value: Provider.of(context, listen: true), + child: AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + title: Text( + LocaleKeys.settings_title.tr(), + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + content: ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 600, + minWidth: 600, + maxWidth: 1000, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 200, + child: SettingsMenu( + changeSelectedIndex: (index) { + context.read().add(SettingsDialogEvent.setViewIndex(index)); + }, + currentIndex: context.read().state.viewIndex, + ), + ), + const VerticalDivider(), + const SizedBox(width: 10), + Expanded( + child: getSettingsView(context.read().state.viewIndex, + context.read().state.userProfile), + ) + ], + ), + ), + ), + ))); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart index e03f1fbc3b..85b78ae3b0 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_appearance_view.dart @@ -1,10 +1,13 @@ import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/workspace/application/appearance.dart'; +import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../widgets/toggle/toggle.dart'; + class SettingsAppearanceView extends StatelessWidget { const SettingsAppearanceView({Key? key}) : super(key: key); @@ -25,11 +28,12 @@ class SettingsAppearanceView extends StatelessWidget { fontWeight: FontWeight.w500, ), ), - Switch( + Toggle( value: theme.isDark, onChanged: (val) { context.read().swapTheme(); }, + style: ToggleStyle.big(theme), ), Text( LocaleKeys.settings_appearance_darkLabel.tr(), diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart index 5617750a77..e95e6e83ab 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_language_view.dart @@ -1,18 +1,37 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/workspace/application/appearance.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme.dart'; import 'package:flutter/material.dart'; import 'package:flowy_infra/language.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:provider/provider.dart'; class SettingsLanguageView extends StatelessWidget { const SettingsLanguageView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: const [LanguageSelectorDropdown()], + context.watch(); + return ChangeNotifierProvider.value( + value: Provider.of(context, listen: true), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + LocaleKeys.settings_menu_language.tr(), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const LanguageSelectorDropdown(), + ], + ), + ], + ), ), ); } @@ -28,22 +47,66 @@ class LanguageSelectorDropdown extends StatefulWidget { } class _LanguageSelectorDropdownState extends State { + Color currHoverColor = Colors.white.withOpacity(0.0); + late Color themedHoverColor; + void hoverExitLanguage() { + setState(() { + currHoverColor = Colors.white.withOpacity(0.0); + }); + } + + void hoverEnterLanguage() { + setState(() { + currHoverColor = themedHoverColor; + }); + } + @override Widget build(BuildContext context) { - return DropdownButton( - value: context.read().locale, - onChanged: (val) { - setState(() { - context.read().setLocale(context, val!); - }); - }, - autofocus: true, - items: EasyLocalization.of(context)!.supportedLocales.map((locale) { - return DropdownMenuItem( - value: locale, - child: Text(languageFromLocale(locale)), - ); - }).toList(), + final theme = context.watch(); + themedHoverColor = theme.main2; + + return MouseRegion( + onEnter: (event) => {hoverEnterLanguage()}, + onExit: (event) => {hoverExitLanguage()}, + child: Container( + margin: const EdgeInsets.only(left: 8, right: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: currHoverColor, + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: context.read().locale, + onChanged: (val) { + setState(() { + context.read().setLocale(context, val!); + }); + }, + icon: const Visibility( + child: (Icon(Icons.arrow_downward)), + visible: false, + ), + borderRadius: BorderRadius.circular(8), + items: EasyLocalization.of(context)!.supportedLocales.map((locale) { + return DropdownMenuItem( + value: locale, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + languageFromLocale(locale), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: theme.textColor, + ), + ), + ), + ); + }).toList(), + ), + ), + ), ); } } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart index 241c337705..a27d9861c4 100644 --- a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart @@ -34,6 +34,16 @@ class SettingsMenu extends StatelessWidget { icon: Icons.translate, changeSelectedIndex: changeSelectedIndex, ), + const SizedBox( + height: 10, + ), + SettingsMenuElement( + index: 2, + currentIndex: currentIndex, + label: LocaleKeys.settings_menu_user.tr(), + icon: Icons.account_box_outlined, + changeSelectedIndex: changeSelectedIndex, + ), ], ); } diff --git a/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart new file mode 100644 index 0000000000..f8f094d1b0 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -0,0 +1,50 @@ +import 'package:app_flowy/startup/startup.dart'; +import 'package:flutter/material.dart'; +import 'package:app_flowy/workspace/application/user/settings_user_bloc.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; + +class SettingsUserView extends StatelessWidget { + final UserProfilePB user; + SettingsUserView(this.user, {Key? key}) : super(key: ValueKey(user.id)); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => getIt(param1: user)..add(const SettingsUserEvent.initial()), + child: BlocBuilder( + builder: (context, state) => SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [_renderUserNameInput(context)], + ), + ), + ), + ); + } + + Widget _renderUserNameInput(BuildContext context) { + String name = context.read().state.userProfile.name; + return _UserNameInput(name); + } +} + +class _UserNameInput extends StatelessWidget { + final String name; + const _UserNameInput( + this.name, { + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return TextField( + controller: TextEditingController()..text = name, + decoration: const InputDecoration( + labelText: 'Name', + ), + onSubmitted: (val) { + context.read().add(SettingsUserEvent.updateUserName(val)); + }); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart index 8bb45b51cd..b5d364f67e 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_lists.dart @@ -1632,7 +1632,7 @@ final Map activities = Map.fromIterables([ 'Flying Disc', 'Bowling', 'Cricket Game', - 'Field Hockey', + 'GridFieldPB Hockey', 'Ice Hockey', 'Lacrosse', 'Ping Pong', diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle.dart new file mode 100644 index 0000000000..4de1c7d376 --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle.dart @@ -0,0 +1,52 @@ +import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:flutter/widgets.dart'; + +class Toggle extends StatelessWidget { + final ToggleStyle style; + final bool value; + final void Function(bool) onChanged; + final EdgeInsets padding; + + const Toggle({ + Key? key, + required this.value, + required this.onChanged, + required this.style, + this.padding = const EdgeInsets.all(8.0), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: (() => onChanged(value)), + child: Padding( + padding: padding, + child: Stack( + children: [ + Container( + height: style.height, + width: style.width, + decoration: BoxDecoration( + color: value ? style.activeBackgroundColor : style.inactiveBackgroundColor, + borderRadius: BorderRadius.circular(style.height / 2), + ), + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 150), + top: (style.height - style.thumbRadius) / 2, + left: value ? style.width - style.thumbRadius - 1 : 1, + child: Container( + height: style.thumbRadius, + width: style.thumbRadius, + decoration: BoxDecoration( + color: style.thumbColor, + borderRadius: BorderRadius.circular(style.thumbRadius / 2), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle_style.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle_style.dart new file mode 100644 index 0000000000..683abfab5f --- /dev/null +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/toggle/toggle_style.dart @@ -0,0 +1,38 @@ +import 'package:flowy_infra/theme.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter/widgets.dart'; + +class ToggleStyle { + final double height; + final double width; + + final double thumbRadius; + final Color thumbColor; + final Color activeBackgroundColor; + final Color inactiveBackgroundColor; + + ToggleStyle({ + required this.height, + required this.width, + required this.thumbRadius, + required this.thumbColor, + required this.activeBackgroundColor, + required this.inactiveBackgroundColor, + }); + + ToggleStyle.big(AppTheme theme) + : height = 16, + width = 27, + thumbRadius = 14, + activeBackgroundColor = theme.main1, + inactiveBackgroundColor = theme.shader5, + thumbColor = theme.surface; + + ToggleStyle.small(AppTheme theme) + : height = 10, + width = 16, + thumbRadius = 8, + activeBackgroundColor = theme.main1, + inactiveBackgroundColor = theme.shader5, + thumbColor = theme.surface; +} diff --git a/frontend/app_flowy/packages/flowy_infra/lib/language.dart b/frontend/app_flowy/packages/flowy_infra/lib/language.dart index 2b98263cfb..752956b33e 100644 --- a/frontend/app_flowy/packages/flowy_infra/lib/language.dart +++ b/frontend/app_flowy/packages/flowy_infra/lib/language.dart @@ -26,10 +26,14 @@ String languageFromLocale(Locale locale) { } case "hu": return "Magyar"; + case "id": + return "Bahasa"; case "it": return "Italiano"; case "ja": return "日本語"; + case "pl": + return "Polski"; case "pt": return "Português"; case "ru": diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart index 16d3b55f47..0b247267eb 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart @@ -41,20 +41,22 @@ class ListOverlay extends StatelessWidget { return OverlayContainer( constraints: BoxConstraints.tight(Size(width, totalHeight)), padding: padding, - child: Column( - children: [ - ListView.builder( - shrinkWrap: true, - itemBuilder: itemBuilder, - itemCount: itemCount, - controller: controller, - ), - if (footer != null) - Padding( - padding: footer!.padding, - child: footer!.widget, + child: SingleChildScrollView( + child: Column( + children: [ + ListView.builder( + shrinkWrap: true, + itemBuilder: itemBuilder, + itemCount: itemCount, + controller: controller, ), - ], + if (footer != null) + Padding( + padding: footer!.padding, + child: footer!.widget, + ), + ], + ), ), ); } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart index 85de2c71a7..96b566b308 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart +++ b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/overlay_container.dart @@ -18,7 +18,7 @@ class OverlayContainer extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = context.watch(); + final theme = context.watch() ?? AppTheme.fromType(ThemeType.light); return Material( type: MaterialType.transparency, child: Container( diff --git a/frontend/app_flowy/pubspec.lock b/frontend/app_flowy/pubspec.lock index 958debd9dd..505280115f 100644 --- a/frontend/app_flowy/pubspec.lock +++ b/frontend/app_flowy/pubspec.lock @@ -7,14 +7,14 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "38.0.0" + version: "42.0.0" analyzer: - dependency: transitive + dependency: "direct overridden" description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "3.4.1" + version: "4.3.0" animations: dependency: transitive description: diff --git a/frontend/app_flowy/pubspec.yaml b/frontend/app_flowy/pubspec.yaml index 8dbc3b4c70..5f4ff9c9b8 100644 --- a/frontend/app_flowy/pubspec.yaml +++ b/frontend/app_flowy/pubspec.yaml @@ -85,6 +85,9 @@ dev_dependencies: freezed: bloc_test: ^9.0.2 +dependency_overrides: + analyzer: ">=4.2.0 <5.0.0" + # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your diff --git a/frontend/app_flowy/test/util/test_env.dart b/frontend/app_flowy/test/util/test_env.dart index 40006295a4..1743e4b920 100644 --- a/frontend/app_flowy/test/util/test_env.dart +++ b/frontend/app_flowy/test/util/test_env.dart @@ -14,7 +14,7 @@ class FlowyTest { return FlowyTest(); } - Future signIn() async { + Future signIn() async { final authService = getIt(); const password = "AppFlowy123@"; final uid = uuid(); diff --git a/frontend/app_flowy/test/workspace_bloc_test.dart b/frontend/app_flowy/test/workspace_bloc_test.dart index 680726b401..d0b898cc42 100644 --- a/frontend/app_flowy/test/workspace_bloc_test.dart +++ b/frontend/app_flowy/test/workspace_bloc_test.dart @@ -7,7 +7,7 @@ import 'package:bloc_test/bloc_test.dart'; import 'util/test_env.dart'; void main() { - UserProfile? userInfo; + UserProfilePB? userInfo; setUpAll(() async { final flowyTest = await FlowyTest.setup(); userInfo = await flowyTest.signIn(); diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index d452c1a025..643237bfe6 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -754,9 +754,9 @@ dependencies = [ [[package]] name = "faccess" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e039175679baf763ddddf4f76900b92d4dae9411ee88cf42d2f11b976b09e07c" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" dependencies = [ "bitflags", "libc", diff --git a/frontend/rust-lib/flowy-database/migrations/2022-06-10-140131_revision-snapshot/down.sql b/frontend/rust-lib/flowy-database/migrations/2022-06-10-140131_revision-snapshot/down.sql new file mode 100644 index 0000000000..01a87a1e99 --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-06-10-140131_revision-snapshot/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE rev_snapshot; \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database/migrations/2022-06-10-140131_revision-snapshot/up.sql b/frontend/rust-lib/flowy-database/migrations/2022-06-10-140131_revision-snapshot/up.sql new file mode 100644 index 0000000000..9656dabc3c --- /dev/null +++ b/frontend/rust-lib/flowy-database/migrations/2022-06-10-140131_revision-snapshot/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE rev_snapshot ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + object_id TEXT NOT NULL DEFAULT '', + rev_id BIGINT NOT NULL DEFAULT 0, + data BLOB NOT NULL DEFAULT (x'') +); \ No newline at end of file diff --git a/frontend/rust-lib/flowy-database/src/schema.rs b/frontend/rust-lib/flowy-database/src/schema.rs index b131adf05f..e41fd6d865 100644 --- a/frontend/rust-lib/flowy-database/src/schema.rs +++ b/frontend/rust-lib/flowy-database/src/schema.rs @@ -49,6 +49,15 @@ table! { } } +table! { + rev_snapshot (id) { + id -> Integer, + object_id -> Text, + rev_id -> BigInt, + data -> Binary, + } +} + table! { rev_table (id) { id -> Integer, @@ -116,6 +125,7 @@ allow_tables_to_appear_in_same_query!( grid_meta_rev_table, grid_rev_table, kv_table, + rev_snapshot, rev_table, trash_table, user_table, diff --git a/frontend/rust-lib/flowy-folder/src/entities/app.rs b/frontend/rust-lib/flowy-folder/src/entities/app.rs index 1c705d5b0e..3a5dc3099e 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/app.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/app.rs @@ -3,7 +3,7 @@ use crate::{ app::{AppColorStyle, AppIdentify, AppName}, workspace::WorkspaceIdentify, }, - entities::view::RepeatedView, + entities::view::RepeatedViewPB, errors::ErrorCode, impl_def_and_def_mut, }; @@ -12,7 +12,7 @@ use flowy_folder_data_model::revision::AppRevision; use std::convert::TryInto; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct App { +pub struct AppPB { #[pb(index = 1)] pub id: String, @@ -26,7 +26,7 @@ pub struct App { pub desc: String, #[pb(index = 5)] - pub belongings: RepeatedView, + pub belongings: RepeatedViewPB, #[pb(index = 6)] pub version: i64, @@ -38,9 +38,9 @@ pub struct App { pub create_time: i64, } -impl std::convert::From for App { +impl std::convert::From for AppPB { fn from(app_serde: AppRevision) -> Self { - App { + AppPB { id: app_serde.id, workspace_id: app_serde.workspace_id, name: app_serde.name, @@ -53,21 +53,21 @@ impl std::convert::From for App { } } #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] -pub struct RepeatedApp { +pub struct RepeatedAppPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl_def_and_def_mut!(RepeatedApp, App); +impl_def_and_def_mut!(RepeatedAppPB, AppPB); -impl std::convert::From> for RepeatedApp { +impl std::convert::From> for RepeatedAppPB { fn from(values: Vec) -> Self { - let items = values.into_iter().map(|value| value.into()).collect::>(); - RepeatedApp { items } + let items = values.into_iter().map(|value| value.into()).collect::>(); + RepeatedAppPB { items } } } #[derive(ProtoBuf, Default)] -pub struct CreateAppPayload { +pub struct CreateAppPayloadPB { #[pb(index = 1)] pub workspace_id: String, @@ -78,31 +78,24 @@ pub struct CreateAppPayload { pub desc: String, #[pb(index = 4)] - pub color_style: ColorStyle, + pub color_style: ColorStylePB, } #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct ColorStyle { +pub struct ColorStylePB { #[pb(index = 1)] pub theme_color: String, } -#[derive(ProtoBuf, Default, Debug)] +#[derive(Debug)] pub struct CreateAppParams { - #[pb(index = 1)] pub workspace_id: String, - - #[pb(index = 2)] pub name: String, - - #[pb(index = 3)] pub desc: String, - - #[pb(index = 4)] - pub color_style: ColorStyle, + pub color_style: ColorStylePB, } -impl TryInto for CreateAppPayload { +impl TryInto for CreateAppPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -119,21 +112,21 @@ impl TryInto for CreateAppPayload { } } -impl std::convert::From for ColorStyle { +impl std::convert::From for ColorStylePB { fn from(data: AppColorStyle) -> Self { - ColorStyle { + ColorStylePB { theme_color: data.theme_color, } } } #[derive(ProtoBuf, Default, Clone, Debug)] -pub struct AppId { +pub struct AppIdPB { #[pb(index = 1)] pub value: String, } -impl AppId { +impl AppIdPB { pub fn new(app_id: &str) -> Self { Self { value: app_id.to_string(), @@ -142,7 +135,7 @@ impl AppId { } #[derive(ProtoBuf, Default)] -pub struct UpdateAppPayload { +pub struct UpdateAppPayloadPB { #[pb(index = 1)] pub app_id: String, @@ -153,27 +146,22 @@ pub struct UpdateAppPayload { pub desc: Option, #[pb(index = 4, one_of)] - pub color_style: Option, + pub color_style: Option, #[pb(index = 5, one_of)] pub is_trash: Option, } -#[derive(ProtoBuf, Default, Clone, Debug)] +#[derive(Debug, Clone)] pub struct UpdateAppParams { - #[pb(index = 1)] pub app_id: String, - #[pb(index = 2, one_of)] pub name: Option, - #[pb(index = 3, one_of)] pub desc: Option, - #[pb(index = 4, one_of)] - pub color_style: Option, + pub color_style: Option, - #[pb(index = 5, one_of)] pub is_trash: Option, } @@ -181,7 +169,10 @@ impl UpdateAppParams { pub fn new(app_id: &str) -> Self { Self { app_id: app_id.to_string(), - ..Default::default() + name: None, + desc: None, + color_style: None, + is_trash: None, } } @@ -201,7 +192,7 @@ impl UpdateAppParams { } } -impl TryInto for UpdateAppPayload { +impl TryInto for UpdateAppPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-folder/src/entities/trash.rs b/frontend/rust-lib/flowy-folder/src/entities/trash.rs index c332dded18..15358ba3e5 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/trash.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/trash.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Formatter; #[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)] -pub struct Trash { +pub struct TrashPB { #[pb(index = 1)] pub id: String, @@ -22,9 +22,9 @@ pub struct Trash { pub ty: TrashType, } -impl std::convert::From for Trash { +impl std::convert::From for TrashPB { fn from(trash_rev: TrashRevision) -> Self { - Trash { + TrashPB { id: trash_rev.id, name: trash_rev.name, modified_time: trash_rev.modified_time, @@ -34,8 +34,8 @@ impl std::convert::From for Trash { } } -impl std::convert::From for TrashRevision { - fn from(trash: Trash) -> Self { +impl std::convert::From for TrashRevision { + fn from(trash: TrashPB) -> Self { TrashRevision { id: trash.id, name: trash.name, @@ -46,16 +46,16 @@ impl std::convert::From for TrashRevision { } } #[derive(PartialEq, Debug, Default, ProtoBuf, Clone)] -pub struct RepeatedTrash { +pub struct RepeatedTrashPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl_def_and_def_mut!(RepeatedTrash, Trash); -impl std::convert::From> for RepeatedTrash { +impl_def_and_def_mut!(RepeatedTrashPB, TrashPB); +impl std::convert::From> for RepeatedTrashPB { fn from(trash_revs: Vec) -> Self { - let items: Vec = trash_revs.into_iter().map(|trash_rev| trash_rev.into()).collect(); - RepeatedTrash { items } + let items: Vec = trash_revs.into_iter().map(|trash_rev| trash_rev.into()).collect(); + RepeatedTrashPB { items } } } @@ -106,15 +106,15 @@ impl std::default::Default for TrashType { } #[derive(PartialEq, ProtoBuf, Default, Debug, Clone)] -pub struct RepeatedTrashId { +pub struct RepeatedTrashIdPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, #[pb(index = 2)] pub delete_all: bool, } -impl std::fmt::Display for RepeatedTrashId { +impl std::fmt::Display for RepeatedTrashIdPB { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(&format!( "{:?}", @@ -123,35 +123,35 @@ impl std::fmt::Display for RepeatedTrashId { } } -impl RepeatedTrashId { - pub fn all() -> RepeatedTrashId { - RepeatedTrashId { +impl RepeatedTrashIdPB { + pub fn all() -> RepeatedTrashIdPB { + RepeatedTrashIdPB { items: vec![], delete_all: true, } } } -impl std::convert::From> for RepeatedTrashId { - fn from(items: Vec) -> Self { - RepeatedTrashId { +impl std::convert::From> for RepeatedTrashIdPB { + fn from(items: Vec) -> Self { + RepeatedTrashIdPB { items, delete_all: false, } } } -impl std::convert::From> for RepeatedTrashId { +impl std::convert::From> for RepeatedTrashIdPB { fn from(trash: Vec) -> Self { let items = trash .into_iter() - .map(|t| TrashId { + .map(|t| TrashIdPB { id: t.id, ty: t.ty.into(), }) .collect::>(); - RepeatedTrashId { + RepeatedTrashIdPB { items, delete_all: false, } @@ -159,7 +159,7 @@ impl std::convert::From> for RepeatedTrashId { } #[derive(PartialEq, ProtoBuf, Default, Debug, Clone)] -pub struct TrashId { +pub struct TrashIdPB { #[pb(index = 1)] pub id: String, @@ -167,15 +167,15 @@ pub struct TrashId { pub ty: TrashType, } -impl std::fmt::Display for TrashId { +impl std::fmt::Display for TrashIdPB { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(&format!("{:?}:{}", self.ty, self.id)) } } -impl std::convert::From<&TrashRevision> for TrashId { +impl std::convert::From<&TrashRevision> for TrashIdPB { fn from(trash: &TrashRevision) -> Self { - TrashId { + TrashIdPB { id: trash.id.clone(), ty: trash.ty.clone().into(), } diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index ca54ec8cff..8f3a26c8fd 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -11,7 +11,7 @@ use flowy_folder_data_model::revision::{gen_view_id, ViewDataTypeRevision, ViewR use std::convert::TryInto; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct View { +pub struct ViewPB { #[pb(index = 1)] pub id: String, @@ -34,9 +34,9 @@ pub struct View { pub plugin_type: i32, } -impl std::convert::From for View { +impl std::convert::From for ViewPB { fn from(rev: ViewRevision) -> Self { - View { + ViewPB { id: rev.id, belong_to_id: rev.belong_to_id, name: rev.name, @@ -79,27 +79,27 @@ impl std::convert::From for ViewDataTypeRevision { } #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] -pub struct RepeatedView { +pub struct RepeatedViewPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl_def_and_def_mut!(RepeatedView, View); +impl_def_and_def_mut!(RepeatedViewPB, ViewPB); -impl std::convert::From> for RepeatedView { +impl std::convert::From> for RepeatedViewPB { fn from(values: Vec) -> Self { - let items = values.into_iter().map(|value| value.into()).collect::>(); - RepeatedView { items } + let items = values.into_iter().map(|value| value.into()).collect::>(); + RepeatedViewPB { items } } } #[derive(Default, ProtoBuf)] -pub struct RepeatedViewId { +pub struct RepeatedViewIdPB { #[pb(index = 1)] pub items: Vec, } #[derive(Default, ProtoBuf)] -pub struct CreateViewPayload { +pub struct CreateViewPayloadPB { #[pb(index = 1)] pub belong_to_id: String, @@ -122,34 +122,19 @@ pub struct CreateViewPayload { pub data: Vec, } -#[derive(Default, ProtoBuf, Debug, Clone)] +#[derive(Debug, Clone)] pub struct CreateViewParams { - #[pb(index = 1)] pub belong_to_id: String, - - #[pb(index = 2)] pub name: String, - - #[pb(index = 3)] pub desc: String, - - #[pb(index = 4)] pub thumbnail: String, - - #[pb(index = 5)] pub data_type: ViewDataType, - - #[pb(index = 6)] pub view_id: String, - - #[pb(index = 7)] pub data: Vec, - - #[pb(index = 8)] pub plugin_type: i32, } -impl TryInto for CreateViewPayload { +impl TryInto for CreateViewPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -175,20 +160,20 @@ impl TryInto for CreateViewPayload { } #[derive(Default, ProtoBuf, Clone, Debug)] -pub struct ViewId { +pub struct ViewIdPB { #[pb(index = 1)] pub value: String, } -impl std::convert::From<&str> for ViewId { +impl std::convert::From<&str> for ViewIdPB { fn from(value: &str) -> Self { - ViewId { + ViewIdPB { value: value.to_string(), } } } -impl std::ops::Deref for ViewId { +impl std::ops::Deref for ViewIdPB { type Target = str; fn deref(&self) -> &Self::Target { @@ -197,7 +182,7 @@ impl std::ops::Deref for ViewId { } #[derive(Default, ProtoBuf)] -pub struct UpdateViewPayload { +pub struct UpdateViewPayloadPB { #[pb(index = 1)] pub view_id: String, @@ -211,22 +196,15 @@ pub struct UpdateViewPayload { pub thumbnail: Option, } -#[derive(Default, ProtoBuf, Clone, Debug)] +#[derive(Clone, Debug)] pub struct UpdateViewParams { - #[pb(index = 1)] pub view_id: String, - - #[pb(index = 2, one_of)] pub name: Option, - - #[pb(index = 3, one_of)] pub desc: Option, - - #[pb(index = 4, one_of)] pub thumbnail: Option, } -impl TryInto for UpdateViewPayload { +impl TryInto for UpdateViewPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -269,7 +247,7 @@ impl std::default::Default for MoveFolderItemType { } #[derive(Default, ProtoBuf)] -pub struct MoveFolderItemPayload { +pub struct MoveFolderItemPayloadPB { #[pb(index = 1)] pub item_id: String, @@ -290,7 +268,7 @@ pub struct MoveFolderItemParams { pub ty: MoveFolderItemType, } -impl TryInto for MoveFolderItemPayload { +impl TryInto for MoveFolderItemPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-folder/src/entities/view_info.rs b/frontend/rust-lib/flowy-folder/src/entities/view_info.rs index a772396e9f..92ec785821 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view_info.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view_info.rs @@ -1,8 +1,8 @@ -use crate::entities::{RepeatedView, ViewDataType}; +use crate::entities::{RepeatedViewPB, ViewDataType}; use flowy_derive::ProtoBuf; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct ViewInfo { +pub struct ViewInfoPB { #[pb(index = 1)] pub id: String, @@ -19,7 +19,7 @@ pub struct ViewInfo { pub data_type: ViewDataType, #[pb(index = 6)] - pub belongings: RepeatedView, + pub belongings: RepeatedViewPB, #[pb(index = 7)] pub ext_data: String, diff --git a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs index a9f61b50c8..20c9750940 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs @@ -1,6 +1,6 @@ use crate::{ entities::parser::workspace::{WorkspaceDesc, WorkspaceIdentify, WorkspaceName}, - entities::{app::RepeatedApp, view::View}, + entities::{app::RepeatedAppPB, view::ViewPB}, errors::*, impl_def_and_def_mut, }; @@ -9,7 +9,7 @@ use flowy_folder_data_model::revision::WorkspaceRevision; use std::convert::TryInto; #[derive(Eq, PartialEq, ProtoBuf, Default, Debug, Clone)] -pub struct Workspace { +pub struct WorkspacePB { #[pb(index = 1)] pub id: String, @@ -20,7 +20,7 @@ pub struct Workspace { pub desc: String, #[pb(index = 4)] - pub apps: RepeatedApp, + pub apps: RepeatedAppPB, #[pb(index = 5)] pub modified_time: i64, @@ -29,9 +29,9 @@ pub struct Workspace { pub create_time: i64, } -impl std::convert::From for Workspace { +impl std::convert::From for WorkspacePB { fn from(workspace_serde: WorkspaceRevision) -> Self { - Workspace { + WorkspacePB { id: workspace_serde.id, name: workspace_serde.name, desc: workspace_serde.desc, @@ -42,15 +42,15 @@ impl std::convert::From for Workspace { } } #[derive(PartialEq, Debug, Default, ProtoBuf)] -pub struct RepeatedWorkspace { +pub struct RepeatedWorkspacePB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl_def_and_def_mut!(RepeatedWorkspace, Workspace); +impl_def_and_def_mut!(RepeatedWorkspacePB, WorkspacePB); #[derive(ProtoBuf, Default)] -pub struct CreateWorkspacePayload { +pub struct CreateWorkspacePayloadPB { #[pb(index = 1)] pub name: String, @@ -58,16 +58,13 @@ pub struct CreateWorkspacePayload { pub desc: String, } -#[derive(Clone, ProtoBuf, Default, Debug)] +#[derive(Clone, Debug)] pub struct CreateWorkspaceParams { - #[pb(index = 1)] pub name: String, - - #[pb(index = 2)] pub desc: String, } -impl TryInto for CreateWorkspacePayload { +impl TryInto for CreateWorkspacePayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -83,28 +80,28 @@ impl TryInto for CreateWorkspacePayload { // Read all workspaces if the workspace_id is None #[derive(Clone, ProtoBuf, Default, Debug)] -pub struct WorkspaceId { +pub struct WorkspaceIdPB { #[pb(index = 1, one_of)] pub value: Option, } -impl WorkspaceId { +impl WorkspaceIdPB { pub fn new(workspace_id: Option) -> Self { Self { value: workspace_id } } } #[derive(Default, ProtoBuf, Clone)] -pub struct CurrentWorkspaceSetting { +pub struct CurrentWorkspaceSettingPB { #[pb(index = 1)] - pub workspace: Workspace, + pub workspace: WorkspacePB, #[pb(index = 2, one_of)] - pub latest_view: Option, + pub latest_view: Option, } #[derive(ProtoBuf, Default)] -pub struct UpdateWorkspaceRequest { +pub struct UpdateWorkspacePayloadPB { #[pb(index = 1)] pub id: String, @@ -115,19 +112,14 @@ pub struct UpdateWorkspaceRequest { pub desc: Option, } -#[derive(Clone, ProtoBuf, Default, Debug)] +#[derive(Clone, Debug)] pub struct UpdateWorkspaceParams { - #[pb(index = 1)] pub id: String, - - #[pb(index = 2, one_of)] pub name: Option, - - #[pb(index = 3, one_of)] pub desc: Option, } -impl TryInto for UpdateWorkspaceRequest { +impl TryInto for UpdateWorkspacePayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index abb168b5e3..71e436b848 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -1,9 +1,9 @@ use crate::{ entities::{ - app::{AppId, CreateAppParams, UpdateAppParams}, - trash::RepeatedTrashId, - view::{CreateViewParams, RepeatedViewId, UpdateViewParams, ViewId}, - workspace::{CreateWorkspaceParams, UpdateWorkspaceParams, WorkspaceId}, + app::{AppIdPB, CreateAppParams, UpdateAppParams}, + trash::RepeatedTrashIdPB, + view::{CreateViewParams, RepeatedViewIdPB, UpdateViewParams, ViewIdPB}, + workspace::{CreateWorkspaceParams, UpdateWorkspaceParams, WorkspaceIdPB}, }, errors::FlowyError, manager::FolderManager, @@ -84,73 +84,73 @@ pub fn create(folder: Arc) -> Module { #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum FolderEvent { - #[event(input = "CreateWorkspacePayload", output = "Workspace")] + #[event(input = "CreateWorkspacePayloadPB", output = "WorkspacePB")] CreateWorkspace = 0, - #[event(output = "CurrentWorkspaceSetting")] + #[event(output = "CurrentWorkspaceSettingPB")] ReadCurWorkspace = 1, - #[event(input = "WorkspaceId", output = "RepeatedWorkspace")] + #[event(input = "WorkspaceIdPB", output = "RepeatedWorkspacePB")] ReadWorkspaces = 2, - #[event(input = "WorkspaceId")] + #[event(input = "WorkspaceIdPB")] DeleteWorkspace = 3, - #[event(input = "WorkspaceId", output = "Workspace")] + #[event(input = "WorkspaceIdPB", output = "WorkspacePB")] OpenWorkspace = 4, - #[event(input = "WorkspaceId", output = "RepeatedApp")] + #[event(input = "WorkspaceIdPB", output = "RepeatedAppPB")] ReadWorkspaceApps = 5, - #[event(input = "CreateAppPayload", output = "App")] + #[event(input = "CreateAppPayloadPB", output = "AppPB")] CreateApp = 101, - #[event(input = "AppId")] + #[event(input = "AppIdPB")] DeleteApp = 102, - #[event(input = "AppId", output = "App")] + #[event(input = "AppIdPB", output = "AppPB")] ReadApp = 103, - #[event(input = "UpdateAppPayload")] + #[event(input = "UpdateAppPayloadPB")] UpdateApp = 104, - #[event(input = "CreateViewPayload", output = "View")] + #[event(input = "CreateViewPayloadPB", output = "ViewPB")] CreateView = 201, - #[event(input = "ViewId", output = "View")] + #[event(input = "ViewIdPB", output = "ViewPB")] ReadView = 202, - #[event(input = "UpdateViewPayload", output = "View")] + #[event(input = "UpdateViewPayloadPB", output = "ViewPB")] UpdateView = 203, - #[event(input = "RepeatedViewId")] + #[event(input = "RepeatedViewIdPB")] DeleteView = 204, - #[event(input = "ViewId")] + #[event(input = "ViewIdPB")] DuplicateView = 205, - #[event(input = "ViewId")] + #[event(input = "ViewIdPB")] CloseView = 206, - #[event(input = "ViewId", output = "ViewInfo")] + #[event(input = "ViewIdPB", output = "ViewInfoPB")] ReadViewInfo = 207, #[event()] CopyLink = 220, - #[event(input = "ViewId")] + #[event(input = "ViewIdPB")] SetLatestView = 221, - #[event(input = "MoveFolderItemPayload")] + #[event(input = "MoveFolderItemPayloadPB")] MoveFolderItem = 230, - #[event(output = "RepeatedTrash")] + #[event(output = "RepeatedTrashPB")] ReadTrash = 300, - #[event(input = "TrashId")] + #[event(input = "TrashIdPB")] PutbackTrash = 301, - #[event(input = "RepeatedTrashId")] + #[event(input = "RepeatedTrashIdPB")] DeleteTrash = 302, #[event()] @@ -170,34 +170,34 @@ pub trait FolderCouldServiceV1: Send + Sync { params: CreateWorkspaceParams, ) -> FutureResult; - fn read_workspace(&self, token: &str, params: WorkspaceId) -> FutureResult, FlowyError>; + fn read_workspace(&self, token: &str, params: WorkspaceIdPB) -> FutureResult, FlowyError>; fn update_workspace(&self, token: &str, params: UpdateWorkspaceParams) -> FutureResult<(), FlowyError>; - fn delete_workspace(&self, token: &str, params: WorkspaceId) -> FutureResult<(), FlowyError>; + fn delete_workspace(&self, token: &str, params: WorkspaceIdPB) -> FutureResult<(), FlowyError>; // View fn create_view(&self, token: &str, params: CreateViewParams) -> FutureResult; - fn read_view(&self, token: &str, params: ViewId) -> FutureResult, FlowyError>; + fn read_view(&self, token: &str, params: ViewIdPB) -> FutureResult, FlowyError>; - fn delete_view(&self, token: &str, params: RepeatedViewId) -> FutureResult<(), FlowyError>; + fn delete_view(&self, token: &str, params: RepeatedViewIdPB) -> FutureResult<(), FlowyError>; fn update_view(&self, token: &str, params: UpdateViewParams) -> FutureResult<(), FlowyError>; // App fn create_app(&self, token: &str, params: CreateAppParams) -> FutureResult; - fn read_app(&self, token: &str, params: AppId) -> FutureResult, FlowyError>; + fn read_app(&self, token: &str, params: AppIdPB) -> FutureResult, FlowyError>; fn update_app(&self, token: &str, params: UpdateAppParams) -> FutureResult<(), FlowyError>; - fn delete_app(&self, token: &str, params: AppId) -> FutureResult<(), FlowyError>; + fn delete_app(&self, token: &str, params: AppIdPB) -> FutureResult<(), FlowyError>; // Trash - fn create_trash(&self, token: &str, params: RepeatedTrashId) -> FutureResult<(), FlowyError>; + fn create_trash(&self, token: &str, params: RepeatedTrashIdPB) -> FutureResult<(), FlowyError>; - fn delete_trash(&self, token: &str, params: RepeatedTrashId) -> FutureResult<(), FlowyError>; + fn delete_trash(&self, token: &str, params: RepeatedTrashIdPB) -> FutureResult<(), FlowyError>; fn read_trash(&self, token: &str) -> FutureResult, FlowyError>; } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index b6555035d3..df070d90f3 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -1,7 +1,8 @@ use crate::entities::view::ViewDataType; +use crate::services::folder_editor::FolderRevisionCompactor; use crate::{ dart_notification::{send_dart_notification, FolderNotification}, - entities::workspace::RepeatedWorkspace, + entities::workspace::RepeatedWorkspacePB, errors::FlowyResult, event_map::{FolderCouldServiceV1, WorkspaceDatabase, WorkspaceUser}, services::{ @@ -13,7 +14,7 @@ use bytes::Bytes; use flowy_error::FlowyError; use flowy_folder_data_model::user_default; use flowy_revision::disk::SQLiteTextBlockRevisionPersistence; -use flowy_revision::{RevisionManager, RevisionPersistence, RevisionWebSocket}; +use flowy_revision::{RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence}; use flowy_sync::client_document::default::{initial_quill_delta_string, initial_read_me}; use flowy_sync::{client_folder::FolderPad, entities::ws_data::ServerRevisionWSData}; use lazy_static::lazy_static; @@ -161,9 +162,20 @@ impl FolderManager { let _ = self.persistence.initialize(user_id, &folder_id).await?; let pool = self.persistence.db_pool()?; - let disk_cache = Arc::new(SQLiteTextBlockRevisionPersistence::new(user_id, pool)); - let rev_persistence = Arc::new(RevisionPersistence::new(user_id, folder_id.as_ref(), disk_cache)); - let rev_manager = RevisionManager::new(user_id, folder_id.as_ref(), rev_persistence); + let object_id = folder_id.as_ref(); + let disk_cache = SQLiteTextBlockRevisionPersistence::new(user_id, pool.clone()); + let rev_persistence = RevisionPersistence::new(user_id, object_id, disk_cache); + let rev_compactor = FolderRevisionCompactor(); + // let history_persistence = SQLiteRevisionHistoryPersistence::new(object_id, pool.clone()); + let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(object_id, pool); + let rev_manager = RevisionManager::new( + user_id, + folder_id.as_ref(), + rev_persistence, + rev_compactor, + // history_persistence, + snapshot_persistence, + ); let folder_editor = FolderEditor::new(user_id, &folder_id, token, rev_manager, self.web_socket.clone()).await?; *self.folder_editor.write().await = Some(Arc::new(folder_editor)); @@ -203,7 +215,7 @@ impl DefaultFolderBuilder { for app in workspace_rev.apps.iter() { for (index, view) in app.belongings.iter().enumerate() { let view_data = if index == 0 { - initial_read_me().to_delta_str() + initial_read_me().json_str() } else { initial_quill_delta_string() }; @@ -216,7 +228,7 @@ impl DefaultFolderBuilder { let folder = FolderPad::new(vec![workspace_rev.clone()], vec![])?; let folder_id = FolderId::new(user_id); let _ = persistence.save_folder(user_id, &folder_id, folder).await?; - let repeated_workspace = RepeatedWorkspace { + let repeated_workspace = RepeatedWorkspacePB { items: vec![workspace_rev.into()], }; send_dart_notification(token, FolderNotification::UserCreateWorkspace) diff --git a/frontend/rust-lib/flowy-folder/src/services/app/controller.rs b/frontend/rust-lib/flowy-folder/src/services/app/controller.rs index 481af85ccd..112ad052ce 100644 --- a/frontend/rust-lib/flowy-folder/src/services/app/controller.rs +++ b/frontend/rust-lib/flowy-folder/src/services/app/controller.rs @@ -1,7 +1,7 @@ use crate::{ dart_notification::*, entities::{ - app::{App, CreateAppParams, *}, + app::{AppPB, CreateAppParams, *}, trash::TrashType, }, errors::*, @@ -44,12 +44,12 @@ impl AppController { } #[tracing::instrument(level = "debug", skip(self, params), fields(name = %params.name) err)] - pub(crate) async fn create_app_from_params(&self, params: CreateAppParams) -> Result { + pub(crate) async fn create_app_from_params(&self, params: CreateAppParams) -> Result { let app = self.create_app_on_server(params).await?; self.create_app_on_local(app).await } - pub(crate) async fn create_app_on_local(&self, app: AppRevision) -> Result { + pub(crate) async fn create_app_on_local(&self, app: AppRevision) -> Result { let _ = self .persistence .begin_transaction(|transaction| { @@ -61,7 +61,7 @@ impl AppController { Ok(app.into()) } - pub(crate) async fn read_app(&self, params: AppId) -> Result { + pub(crate) async fn read_app(&self, params: AppIdPB) -> Result { let app = self .persistence .begin_transaction(|transaction| { @@ -81,7 +81,7 @@ impl AppController { let changeset = AppChangeset::new(params.clone()); let app_id = changeset.id.clone(); - let app: App = self + let app: AppPB = self .persistence .begin_transaction(|transaction| { let _ = transaction.update_app(changeset)?; @@ -150,7 +150,7 @@ impl AppController { } #[tracing::instrument(level = "trace", skip(self), err)] - fn read_app_on_server(&self, params: AppId) -> Result<(), FlowyError> { + fn read_app_on_server(&self, params: AppIdPB) -> Result<(), FlowyError> { let token = self.user.token()?; let server = self.cloud_service.clone(); let persistence = self.persistence.clone(); @@ -162,7 +162,7 @@ impl AppController { .await { Ok(_) => { - let app: App = app_rev.into(); + let app: AppPB = app_rev.into(); send_dart_notification(&app.id, FolderNotification::AppUpdated) .payload(app) .send(); @@ -247,7 +247,7 @@ fn notify_apps_changed<'a>( .into_iter() .map(|app_rev| app_rev.into()) .collect(); - let repeated_app = RepeatedApp { items }; + let repeated_app = RepeatedAppPB { items }; send_dart_notification(workspace_id, FolderNotification::WorkspaceAppsChanged) .payload(repeated_app) .send(); diff --git a/frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs b/frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs index 28885c69d7..e2087c167e 100644 --- a/frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/services/app/event_handler.rs @@ -1,5 +1,5 @@ use crate::{ - entities::app::{App, AppId, CreateAppParams, CreateAppPayload, UpdateAppParams, UpdateAppPayload}, + entities::app::{AppIdPB, AppPB, CreateAppParams, CreateAppPayloadPB, UpdateAppParams, UpdateAppPayloadPB}, errors::FlowyError, services::{AppController, TrashController, ViewController}, }; @@ -8,9 +8,9 @@ use lib_dispatch::prelude::{data_result, AppData, Data, DataResult}; use std::{convert::TryInto, sync::Arc}; pub(crate) async fn create_app_handler( - data: Data, + data: Data, controller: AppData>, -) -> DataResult { +) -> DataResult { let params: CreateAppParams = data.into_inner().try_into()?; let detail = controller.create_app_from_params(params).await?; @@ -18,11 +18,11 @@ pub(crate) async fn create_app_handler( } pub(crate) async fn delete_app_handler( - data: Data, + data: Data, app_controller: AppData>, trash_controller: AppData>, ) -> Result<(), FlowyError> { - let params: AppId = data.into_inner(); + let params: AppIdPB = data.into_inner(); let trash = app_controller .read_local_apps(vec![params.value]) .await? @@ -36,7 +36,7 @@ pub(crate) async fn delete_app_handler( #[tracing::instrument(level = "debug", skip(data, controller))] pub(crate) async fn update_app_handler( - data: Data, + data: Data, controller: AppData>, ) -> Result<(), FlowyError> { let params: UpdateAppParams = data.into_inner().try_into()?; @@ -46,11 +46,11 @@ pub(crate) async fn update_app_handler( #[tracing::instrument(level = "trace", skip(data, app_controller, view_controller))] pub(crate) async fn read_app_handler( - data: Data, + data: Data, app_controller: AppData>, view_controller: AppData>, -) -> DataResult { - let params: AppId = data.into_inner(); +) -> DataResult { + let params: AppIdPB = data.into_inner(); let mut app_rev = app_controller.read_app(params.clone()).await?; app_rev.belongings = view_controller.read_views_belong_to(¶ms.value).await?; diff --git a/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs b/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs index 4fa40013d0..de461379e1 100644 --- a/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs +++ b/frontend/rust-lib/flowy-folder/src/services/folder_editor.rs @@ -10,7 +10,7 @@ use flowy_sync::{ entities::{revision::Revision, ws_data::ServerRevisionWSData}, }; use lib_infra::future::FutureResult; -use lib_ot::core::PlainTextAttributes; +use lib_ot::core::PhantomAttributes; use parking_lot::RwLock; use std::sync::Arc; @@ -80,7 +80,7 @@ impl FolderEditor { pub(crate) fn apply_change(&self, change: FolderChange) -> FlowyResult<()> { let FolderChange { delta, md5 } = change; let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); - let delta_data = delta.to_delta_bytes(); + let delta_data = delta.json_bytes(); let revision = Revision::new( &self.rev_manager.object_id, base_rev_id, @@ -89,11 +89,7 @@ impl FolderEditor { &self.user_id, md5, ); - let _ = futures::executor::block_on(async { - self.rev_manager - .add_local_revision(&revision, Box::new(FolderRevisionCompactor())) - .await - })?; + let _ = futures::executor::block_on(async { self.rev_manager.add_local_revision(&revision).await })?; Ok(()) } @@ -133,10 +129,10 @@ impl FolderEditor { } } -struct FolderRevisionCompactor(); +pub struct FolderRevisionCompactor(); impl RevisionCompactor for FolderRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { - let delta = make_delta_from_revisions::(revisions)?; - Ok(delta.to_delta_bytes()) + let delta = make_delta_from_revisions::(revisions)?; + Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs index 4c147ad0d4..d67d062d25 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/migration.rs @@ -85,7 +85,7 @@ impl FolderMigration { return Ok(None); } let pool = self.database.db_pool()?; - let disk_cache = Arc::new(SQLiteTextBlockRevisionPersistence::new(user_id, pool)); + let disk_cache = SQLiteTextBlockRevisionPersistence::new(user_id, pool); let rev_persistence = Arc::new(RevisionPersistence::new(user_id, folder_id.as_ref(), disk_cache)); let (revisions, _) = RevisionLoader { object_id: folder_id.as_ref().to_owned(), diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs index 54fcc3b522..ffa5219aa8 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/mod.rs @@ -11,9 +11,9 @@ use flowy_database::ConnectionPool; use flowy_error::{FlowyError, FlowyResult}; use flowy_folder_data_model::revision::{AppRevision, TrashRevision, ViewRevision, WorkspaceRevision}; use flowy_revision::disk::{RevisionRecord, RevisionState}; -use flowy_revision::mk_revision_disk_cache; -use flowy_sync::client_folder::initial_folder_delta; +use flowy_revision::mk_text_block_revision_disk_cache; use flowy_sync::{client_folder::FolderPad, entities::revision::Revision}; +use lib_ot::core::TextDeltaBuilder; use std::sync::Arc; use tokio::sync::RwLock; pub use version_1::{app_sql::*, trash_sql::*, v1_impl::V1Transaction, view_sql::*, workspace_sql::*}; @@ -109,16 +109,16 @@ impl FolderPersistence { pub async fn save_folder(&self, user_id: &str, folder_id: &FolderId, folder: FolderPad) -> FlowyResult<()> { let pool = self.database.db_pool()?; - let delta_data = initial_folder_delta(&folder)?.to_delta_bytes(); - let md5 = folder.md5(); - let revision = Revision::new(folder_id.as_ref(), 0, 0, delta_data, user_id, md5); + let json = folder.to_json()?; + let delta_data = TextDeltaBuilder::new().insert(&json).build().json_bytes(); + let revision = Revision::initial_revision(user_id, folder_id.as_ref(), delta_data); let record = RevisionRecord { revision, state: RevisionState::Sync, write_to_disk: true, }; - let disk_cache = mk_revision_disk_cache(user_id, pool); + let disk_cache = mk_text_block_revision_disk_cache(user_id, pool); disk_cache.delete_and_insert_records(folder_id.as_ref(), None, vec![record]) } } diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/app_sql.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/app_sql.rs index 6b5f47c2b7..cf643e7dff 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/app_sql.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/app_sql.rs @@ -1,6 +1,6 @@ use crate::entities::{ app::UpdateAppParams, - trash::{Trash, TrashType}, + trash::{TrashPB, TrashType}, }; use crate::{errors::FlowyError, services::persistence::version_1::workspace_sql::WorkspaceTable}; use flowy_database::{ @@ -107,9 +107,9 @@ impl AppTable { } } -impl std::convert::From for Trash { +impl std::convert::From for TrashPB { fn from(table: AppTable) -> Self { - Trash { + TrashPB { id: table.id, name: table.name, modified_time: table.modified_time, diff --git a/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs b/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs index ec1bbf38bf..6223cd3366 100644 --- a/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs +++ b/frontend/rust-lib/flowy-folder/src/services/persistence/version_1/view_sql.rs @@ -1,6 +1,6 @@ use crate::{ entities::{ - trash::{Trash, TrashType}, + trash::{TrashPB, TrashType}, view::UpdateViewParams, }, errors::FlowyError, @@ -133,9 +133,9 @@ impl std::convert::From for ViewRevision { } } -impl std::convert::From for Trash { +impl std::convert::From for TrashPB { fn from(table: ViewTable) -> Self { - Trash { + TrashPB { id: table.id, name: table.name, modified_time: table.modified_time, diff --git a/frontend/rust-lib/flowy-folder/src/services/trash/controller.rs b/frontend/rust-lib/flowy-folder/src/services/trash/controller.rs index 134e8a0d84..8ac9618917 100644 --- a/frontend/rust-lib/flowy-folder/src/services/trash/controller.rs +++ b/frontend/rust-lib/flowy-folder/src/services/trash/controller.rs @@ -1,6 +1,6 @@ use crate::{ dart_notification::{send_anonymous_dart_notification, FolderNotification}, - entities::trash::{RepeatedTrash, RepeatedTrashId, Trash, TrashId, TrashType}, + entities::trash::{RepeatedTrashIdPB, RepeatedTrashPB, TrashIdPB, TrashPB, TrashType}, errors::{FlowyError, FlowyResult}, event_map::{FolderCouldServiceV1, WorkspaceUser}, services::persistence::{FolderPersistence, FolderPersistenceTransaction}, @@ -49,12 +49,12 @@ impl TrashController { }) .await?; - let identifier = TrashId { + let identifier = TrashIdPB { id: trash.id, ty: trash.ty.into(), }; - let _ = self.delete_trash_on_server(RepeatedTrashId { + let _ = self.delete_trash_on_server(RepeatedTrashIdPB { items: vec![identifier.clone()], delete_all: false, })?; @@ -67,7 +67,7 @@ impl TrashController { #[tracing::instrument(level = "debug", skip(self) err)] pub async fn restore_all_trash(&self) -> FlowyResult<()> { - let trash_identifier: RepeatedTrashId = self + let trash_identifier: RepeatedTrashIdPB = self .persistence .begin_transaction(|transaction| { let trash = transaction.read_trash(None); @@ -81,14 +81,14 @@ impl TrashController { let _ = self.notify.send(TrashEvent::Putback(trash_identifier, tx)); let _ = rx.recv().await; - notify_trash_changed(RepeatedTrash { items: vec![] }); + notify_trash_changed(RepeatedTrashPB { items: vec![] }); let _ = self.delete_all_trash_on_server().await?; Ok(()) } #[tracing::instrument(level = "debug", skip(self), err)] pub async fn delete_all_trash(&self) -> FlowyResult<()> { - let all_trash_identifiers: RepeatedTrashId = self + let all_trash_identifiers: RepeatedTrashIdPB = self .persistence .begin_transaction(|transaction| transaction.read_trash(None)) .await? @@ -96,13 +96,13 @@ impl TrashController { let _ = self.delete_with_identifiers(all_trash_identifiers).await?; - notify_trash_changed(RepeatedTrash { items: vec![] }); + notify_trash_changed(RepeatedTrashPB { items: vec![] }); let _ = self.delete_all_trash_on_server().await?; Ok(()) } #[tracing::instrument(level = "debug", skip(self), err)] - pub async fn delete(&self, trash_identifiers: RepeatedTrashId) -> FlowyResult<()> { + pub async fn delete(&self, trash_identifiers: RepeatedTrashIdPB) -> FlowyResult<()> { let _ = self.delete_with_identifiers(trash_identifiers.clone()).await?; let trash_revs = self .persistence @@ -116,7 +116,7 @@ impl TrashController { } #[tracing::instrument(level = "debug", skip(self), fields(delete_trash_ids), err)] - pub async fn delete_with_identifiers(&self, trash_identifiers: RepeatedTrashId) -> FlowyResult<()> { + pub async fn delete_with_identifiers(&self, trash_identifiers: RepeatedTrashIdPB) -> FlowyResult<()> { let (tx, mut rx) = mpsc::channel::>(1); tracing::Span::current().record("delete_trash_ids", &format!("{}", trash_identifiers).as_str()); let _ = self.notify.send(TrashEvent::Delete(trash_identifiers.clone(), tx)); @@ -153,7 +153,7 @@ impl TrashController { pub async fn add>(&self, trash: Vec) -> Result<(), FlowyError> { let (tx, mut rx) = mpsc::channel::>(1); let trash_revs: Vec = trash.into_iter().map(|t| t.into()).collect(); - let identifiers = trash_revs.iter().map(|t| t.into()).collect::>(); + let identifiers = trash_revs.iter().map(|t| t.into()).collect::>(); tracing::Span::current().record( "trash_ids", @@ -187,8 +187,8 @@ impl TrashController { self.notify.subscribe() } - pub async fn read_trash(&self) -> Result { - let items: Vec = self + pub async fn read_trash(&self) -> Result { + let items: Vec = self .persistence .begin_transaction(|transaction| transaction.read_trash(None)) .await? @@ -197,7 +197,7 @@ impl TrashController { .collect(); let _ = self.read_trash_on_server()?; - Ok(RepeatedTrash { items }) + Ok(RepeatedTrashPB { items }) } pub fn read_trash_ids<'a>( @@ -215,7 +215,7 @@ impl TrashController { impl TrashController { #[tracing::instrument(level = "trace", skip(self, trash), err)] - fn create_trash_on_server>(&self, trash: T) -> FlowyResult<()> { + fn create_trash_on_server>(&self, trash: T) -> FlowyResult<()> { let token = self.user.token()?; let trash_identifiers = trash.into(); let server = self.cloud_service.clone(); @@ -230,7 +230,7 @@ impl TrashController { } #[tracing::instrument(level = "trace", skip(self, trash), err)] - fn delete_trash_on_server>(&self, trash: T) -> FlowyResult<()> { + fn delete_trash_on_server>(&self, trash: T) -> FlowyResult<()> { let token = self.user.token()?; let trash_identifiers = trash.into(); let server = self.cloud_service.clone(); @@ -277,12 +277,12 @@ impl TrashController { async fn delete_all_trash_on_server(&self) -> FlowyResult<()> { let token = self.user.token()?; let server = self.cloud_service.clone(); - server.delete_trash(&token, RepeatedTrashId::all()).await + server.delete_trash(&token, RepeatedTrashIdPB::all()).await } } #[tracing::instrument(level = "debug", skip(repeated_trash), fields(n_trash))] -fn notify_trash_changed>(repeated_trash: T) { +fn notify_trash_changed>(repeated_trash: T) { let repeated_trash = repeated_trash.into(); tracing::Span::current().record("n_trash", &repeated_trash.len()); send_anonymous_dart_notification(FolderNotification::TrashUpdated) @@ -292,9 +292,9 @@ fn notify_trash_changed>(repeated_trash: T) { #[derive(Clone)] pub enum TrashEvent { - NewTrash(RepeatedTrashId, mpsc::Sender>), - Putback(RepeatedTrashId, mpsc::Sender>), - Delete(RepeatedTrashId, mpsc::Sender>), + NewTrash(RepeatedTrashIdPB, mpsc::Sender>), + Putback(RepeatedTrashIdPB, mpsc::Sender>), + Delete(RepeatedTrashIdPB, mpsc::Sender>), } impl std::fmt::Debug for TrashEvent { diff --git a/frontend/rust-lib/flowy-folder/src/services/trash/event_handler.rs b/frontend/rust-lib/flowy-folder/src/services/trash/event_handler.rs index b1853faaf5..0034745253 100644 --- a/frontend/rust-lib/flowy-folder/src/services/trash/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/services/trash/event_handler.rs @@ -1,5 +1,5 @@ use crate::{ - entities::trash::{RepeatedTrash, RepeatedTrashId, TrashId}, + entities::trash::{RepeatedTrashIdPB, RepeatedTrashPB, TrashIdPB}, errors::FlowyError, services::TrashController, }; @@ -9,14 +9,14 @@ use std::sync::Arc; #[tracing::instrument(level = "debug", skip(controller), err)] pub(crate) async fn read_trash_handler( controller: AppData>, -) -> DataResult { +) -> DataResult { let repeated_trash = controller.read_trash().await?; data_result(repeated_trash) } #[tracing::instrument(level = "debug", skip(identifier, controller), err)] pub(crate) async fn putback_trash_handler( - identifier: Data, + identifier: Data, controller: AppData>, ) -> Result<(), FlowyError> { let _ = controller.putback(&identifier.id).await?; @@ -25,7 +25,7 @@ pub(crate) async fn putback_trash_handler( #[tracing::instrument(level = "debug", skip(identifiers, controller), err)] pub(crate) async fn delete_trash_handler( - identifiers: Data, + identifiers: Data, controller: AppData>, ) -> Result<(), FlowyError> { let _ = controller.delete(identifiers.into_inner()).await?; diff --git a/frontend/rust-lib/flowy-folder/src/services/view/controller.rs b/frontend/rust-lib/flowy-folder/src/services/view/controller.rs index d573b096f6..df1f668941 100644 --- a/frontend/rust-lib/flowy-folder/src/services/view/controller.rs +++ b/frontend/rust-lib/flowy-folder/src/services/view/controller.rs @@ -1,11 +1,11 @@ pub use crate::entities::view::ViewDataType; -use crate::entities::ViewInfo; +use crate::entities::ViewInfoPB; use crate::manager::{ViewDataProcessor, ViewDataProcessorMap}; use crate::{ dart_notification::{send_dart_notification, FolderNotification}, entities::{ - trash::{RepeatedTrashId, TrashType}, - view::{CreateViewParams, RepeatedView, UpdateViewParams, View, ViewId}, + trash::{RepeatedTrashIdPB, TrashType}, + view::{CreateViewParams, RepeatedViewPB, UpdateViewParams, ViewIdPB, ViewPB}, }, errors::{FlowyError, FlowyResult}, event_map::{FolderCouldServiceV1, WorkspaceUser}, @@ -17,7 +17,7 @@ use crate::{ use bytes::Bytes; use flowy_database::kv::KV; use flowy_folder_data_model::revision::{gen_view_id, ViewRevision}; -use flowy_sync::entities::text_block::TextBlockId; +use flowy_sync::entities::text_block::TextBlockIdPB; use futures::{FutureExt, StreamExt}; use std::{collections::HashSet, sync::Arc}; @@ -106,7 +106,7 @@ impl ViewController { } #[tracing::instrument(level = "debug", skip(self, view_id), fields(view_id = %view_id.value), err)] - pub(crate) async fn read_view(&self, view_id: ViewId) -> Result { + pub(crate) async fn read_view(&self, view_id: ViewIdPB) -> Result { let view_rev = self .persistence .begin_transaction(|transaction| { @@ -123,25 +123,25 @@ impl ViewController { } #[tracing::instrument(level = "debug", skip(self, view_id), fields(view_id = %view_id.value), err)] - pub(crate) async fn read_view_info(&self, view_id: ViewId) -> Result { + pub(crate) async fn read_view_info(&self, view_id: ViewIdPB) -> Result { let view_info = self .persistence .begin_transaction(|transaction| { let view_rev = transaction.read_view(&view_id.value)?; - let items: Vec = view_rev + let items: Vec = view_rev .belongings .into_iter() .map(|view_rev| view_rev.into()) .collect(); - let view_info = ViewInfo { + let view_info = ViewInfoPB { id: view_rev.id, belong_to_id: view_rev.belong_to_id, name: view_rev.name, desc: view_rev.desc, data_type: view_rev.data_type.into(), - belongings: RepeatedView { items }, + belongings: RepeatedViewPB { items }, ext_data: view_rev.ext_data, }; Ok(view_info) @@ -177,7 +177,7 @@ impl ViewController { } #[tracing::instrument(level = "debug", skip(self,params), fields(doc_id = %params.value), err)] - pub(crate) async fn delete_view(&self, params: TextBlockId) -> Result<(), FlowyError> { + pub(crate) async fn delete_view(&self, params: TextBlockIdPB) -> Result<(), FlowyError> { if let Some(view_id) = KV::get_str(LATEST_VIEW_ID) { if view_id == params.value { let _ = KV::remove(LATEST_VIEW_ID); @@ -245,7 +245,7 @@ impl ViewController { .begin_transaction(|transaction| { let _ = transaction.update_view(changeset)?; let view_rev = transaction.read_view(&view_id)?; - let view: View = view_rev.clone().into(); + let view: ViewPB = view_rev.clone().into(); send_dart_notification(&view_id, FolderNotification::ViewUpdated) .payload(view) .send(); @@ -297,7 +297,7 @@ impl ViewController { } #[tracing::instrument(level = "debug", skip(self), err)] - fn read_view_on_server(&self, params: ViewId) -> Result<(), FlowyError> { + fn read_view_on_server(&self, params: ViewIdPB) -> Result<(), FlowyError> { let token = self.user.token()?; let server = self.cloud_service.clone(); let persistence = self.persistence.clone(); @@ -310,7 +310,7 @@ impl ViewController { .await { Ok(_) => { - let view: View = view_rev.into(); + let view: ViewPB = view_rev.into(); send_dart_notification(&view.id, FolderNotification::ViewUpdated) .payload(view) .send(); @@ -464,7 +464,7 @@ fn get_data_processor( } fn read_local_views_with_transaction<'a>( - identifiers: RepeatedTrashId, + identifiers: RepeatedTrashIdPB, transaction: &'a (dyn FolderPersistenceTransaction + 'a), ) -> Result, FlowyError> { let mut view_revs = vec![]; @@ -474,7 +474,7 @@ fn read_local_views_with_transaction<'a>( Ok(view_revs) } -fn notify_dart(view: View, notification: FolderNotification) { +fn notify_dart(view: ViewPB, notification: FolderNotification) { send_dart_notification(&view.id, notification).payload(view).send(); } @@ -489,13 +489,13 @@ fn notify_views_changed<'a>( trash_controller: Arc, transaction: &'a (dyn FolderPersistenceTransaction + 'a), ) -> FlowyResult<()> { - let items: Vec = read_belonging_views_on_local(belong_to_id, trash_controller.clone(), transaction)? + let items: Vec = read_belonging_views_on_local(belong_to_id, trash_controller.clone(), transaction)? .into_iter() .map(|view_rev| view_rev.into()) .collect(); tracing::Span::current().record("view_count", &format!("{}", items.len()).as_str()); - let repeated_view = RepeatedView { items }; + let repeated_view = RepeatedViewPB { items }; send_dart_notification(belong_to_id, FolderNotification::AppViewsChanged) .payload(repeated_view) .send(); diff --git a/frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs b/frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs index f970905f30..925ece6ba2 100644 --- a/frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/services/view/event_handler.rs @@ -1,12 +1,13 @@ -use crate::entities::view::{MoveFolderItemParams, MoveFolderItemPayload, MoveFolderItemType}; -use crate::entities::ViewInfo; +use crate::entities::view::{MoveFolderItemParams, MoveFolderItemPayloadPB, MoveFolderItemType}; +use crate::entities::ViewInfoPB; use crate::manager::FolderManager; use crate::services::{notify_workspace_setting_did_change, AppController}; use crate::{ entities::{ - trash::Trash, + trash::TrashPB, view::{ - CreateViewParams, CreateViewPayload, RepeatedViewId, UpdateViewParams, UpdateViewPayload, View, ViewId, + CreateViewParams, CreateViewPayloadPB, RepeatedViewIdPB, UpdateViewParams, UpdateViewPayloadPB, ViewIdPB, + ViewPB, }, }, errors::FlowyError, @@ -17,35 +18,35 @@ use lib_dispatch::prelude::{data_result, AppData, Data, DataResult}; use std::{convert::TryInto, sync::Arc}; pub(crate) async fn create_view_handler( - data: Data, + data: Data, controller: AppData>, -) -> DataResult { +) -> DataResult { let params: CreateViewParams = data.into_inner().try_into()?; let view_rev = controller.create_view_from_params(params).await?; data_result(view_rev.into()) } pub(crate) async fn read_view_handler( - data: Data, + data: Data, controller: AppData>, -) -> DataResult { - let view_id: ViewId = data.into_inner(); +) -> DataResult { + let view_id: ViewIdPB = data.into_inner(); let view_rev = controller.read_view(view_id.clone()).await?; data_result(view_rev.into()) } pub(crate) async fn read_view_info_handler( - data: Data, + data: Data, controller: AppData>, -) -> DataResult { - let view_id: ViewId = data.into_inner(); +) -> DataResult { + let view_id: ViewIdPB = data.into_inner(); let view_info = controller.read_view_info(view_id.clone()).await?; data_result(view_info) } #[tracing::instrument(level = "debug", skip(data, controller), err)] pub(crate) async fn update_view_handler( - data: Data, + data: Data, controller: AppData>, ) -> Result<(), FlowyError> { let params: UpdateViewParams = data.into_inner().try_into()?; @@ -55,11 +56,11 @@ pub(crate) async fn update_view_handler( } pub(crate) async fn delete_view_handler( - data: Data, + data: Data, view_controller: AppData>, trash_controller: AppData>, ) -> Result<(), FlowyError> { - let params: RepeatedViewId = data.into_inner(); + let params: RepeatedViewIdPB = data.into_inner(); for view_id in ¶ms.items { let _ = view_controller.delete_view(view_id.into()).await; } @@ -72,35 +73,35 @@ pub(crate) async fn delete_view_handler( let trash_rev: TrashRevision = view.into(); trash_rev.into() }) - .collect::>(); + .collect::>(); let _ = trash_controller.add(trash).await?; Ok(()) } pub(crate) async fn set_latest_view_handler( - data: Data, + data: Data, folder: AppData>, controller: AppData>, ) -> Result<(), FlowyError> { - let view_id: ViewId = data.into_inner(); + let view_id: ViewIdPB = data.into_inner(); let _ = controller.set_latest_view(&view_id.value)?; let _ = notify_workspace_setting_did_change(&folder, &view_id).await?; Ok(()) } pub(crate) async fn close_view_handler( - data: Data, + data: Data, controller: AppData>, ) -> Result<(), FlowyError> { - let view_id: ViewId = data.into_inner(); + let view_id: ViewIdPB = data.into_inner(); let _ = controller.close_view(&view_id.value).await?; Ok(()) } #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn move_item_handler( - data: Data, + data: Data, view_controller: AppData>, app_controller: AppData>, ) -> Result<(), FlowyError> { @@ -120,10 +121,10 @@ pub(crate) async fn move_item_handler( #[tracing::instrument(level = "debug", skip(data, controller), err)] pub(crate) async fn duplicate_view_handler( - data: Data, + data: Data, controller: AppData>, ) -> Result<(), FlowyError> { - let view_id: ViewId = data.into_inner(); + let view_id: ViewIdPB = data.into_inner(); let _ = controller.duplicate_view(&view_id.value).await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-folder/src/services/web_socket.rs b/frontend/rust-lib/flowy-folder/src/services/web_socket.rs index 75db905d63..993e556f8a 100644 --- a/frontend/rust-lib/flowy-folder/src/services/web_socket.rs +++ b/frontend/rust-lib/flowy-folder/src/services/web_socket.rs @@ -10,7 +10,7 @@ use flowy_sync::{ }, }; use lib_infra::future::{BoxResultFuture, FutureResult}; -use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta}; +use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta}; use parking_lot::RwLock; use std::{sync::Arc, time::Duration}; @@ -24,7 +24,7 @@ pub(crate) async fn make_folder_ws_manager( ) -> Arc { let ws_data_provider = Arc::new(WSDataProvider::new(folder_id, Arc::new(rev_manager.clone()))); let resolver = Arc::new(FolderConflictResolver { folder_pad }); - let conflict_controller = ConflictController::::new( + let conflict_controller = ConflictController::::new( user_id, resolver, Arc::new(ws_data_provider.clone()), @@ -55,8 +55,8 @@ struct FolderConflictResolver { folder_pad: Arc>, } -impl ConflictResolver for FolderConflictResolver { - fn compose_delta(&self, delta: PlainTextDelta) -> BoxResultFuture { +impl ConflictResolver for FolderConflictResolver { + fn compose_delta(&self, delta: TextDelta) -> BoxResultFuture { let folder_pad = self.folder_pad.clone(); Box::pin(async move { let md5 = folder_pad.write().compose_remote_delta(delta)?; @@ -64,15 +64,12 @@ impl ConflictResolver for FolderConflictResolver { }) } - fn transform_delta( - &self, - delta: PlainTextDelta, - ) -> BoxResultFuture, FlowyError> { + fn transform_delta(&self, delta: TextDelta) -> BoxResultFuture, FlowyError> { let folder_pad = self.folder_pad.clone(); Box::pin(async move { let read_guard = folder_pad.read(); - let mut server_prime: Option = None; - let client_prime: PlainTextDelta; + let mut server_prime: Option = None; + let client_prime: TextDelta; if read_guard.is_empty() { // Do nothing client_prime = delta; @@ -89,7 +86,7 @@ impl ConflictResolver for FolderConflictResolver { }) } - fn reset_delta(&self, delta: PlainTextDelta) -> BoxResultFuture { + fn reset_delta(&self, delta: TextDelta) -> BoxResultFuture { let folder_pad = self.folder_pad.clone(); Box::pin(async move { let md5 = folder_pad.write().reset_folder(delta)?; diff --git a/frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs b/frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs index 9863cafdc5..42f3f65fa1 100644 --- a/frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs +++ b/frontend/rust-lib/flowy-folder/src/services/workspace/controller.rs @@ -52,7 +52,7 @@ impl WorkspaceController { .into_iter() .map(|workspace_rev| workspace_rev.into()) .collect(); - let repeated_workspace = RepeatedWorkspace { items: workspaces }; + let repeated_workspace = RepeatedWorkspacePB { items: workspaces }; send_dart_notification(&token, FolderNotification::UserCreateWorkspace) .payload(repeated_workspace) .send(); @@ -99,7 +99,7 @@ impl WorkspaceController { Ok(()) } - pub(crate) async fn open_workspace(&self, params: WorkspaceId) -> Result { + pub(crate) async fn open_workspace(&self, params: WorkspaceIdPB) -> Result { let user_id = self.user.user_id()?; if let Some(workspace_id) = params.value { let workspace = self @@ -131,14 +131,14 @@ impl WorkspaceController { workspace_id: Option, user_id: &str, transaction: &'a (dyn FolderPersistenceTransaction + 'a), - ) -> Result { + ) -> Result { let workspace_id = workspace_id.to_owned(); let workspaces = transaction .read_workspaces(user_id, workspace_id)? .into_iter() .map(|workspace_rev| workspace_rev.into()) .collect(); - Ok(RepeatedWorkspace { items: workspaces }) + Ok(RepeatedWorkspacePB { items: workspaces }) } pub(crate) fn read_local_workspace<'a>( @@ -146,7 +146,7 @@ impl WorkspaceController { workspace_id: String, user_id: &str, transaction: &'a (dyn FolderPersistenceTransaction + 'a), - ) -> Result { + ) -> Result { let mut workspace_revs = transaction.read_workspaces(user_id, Some(workspace_id.clone()))?; if workspace_revs.is_empty() { return Err(FlowyError::record_not_found().context(format!("{} workspace not found", workspace_id))); @@ -155,7 +155,7 @@ impl WorkspaceController { let workspace = workspace_revs .drain(..1) .map(|workspace_rev| workspace_rev.into()) - .collect::>() + .collect::>() .pop() .unwrap(); Ok(workspace) @@ -186,7 +186,7 @@ impl WorkspaceController { #[tracing::instrument(level = "trace", skip(self), err)] fn delete_workspace_on_server(&self, workspace_id: &str) -> Result<(), FlowyError> { - let params = WorkspaceId { + let params = WorkspaceIdPB { value: Some(workspace_id.to_string()), }; let (token, server) = (self.user.token()?, self.cloud_service.clone()); @@ -221,11 +221,11 @@ pub async fn notify_workspace_setting_did_change( )?; let setting = match transaction.read_view(view_id) { - Ok(latest_view) => CurrentWorkspaceSetting { + Ok(latest_view) => CurrentWorkspaceSettingPB { workspace, latest_view: Some(latest_view.into()), }, - Err(_) => CurrentWorkspaceSetting { + Err(_) => CurrentWorkspaceSettingPB { workspace, latest_view: None, }, diff --git a/frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs b/frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs index 02fb37a12b..234fa3b7de 100644 --- a/frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/services/workspace/event_handler.rs @@ -1,7 +1,7 @@ use crate::entities::{ - app::RepeatedApp, - view::View, - workspace::{CurrentWorkspaceSetting, RepeatedWorkspace, WorkspaceId, *}, + app::RepeatedAppPB, + view::ViewPB, + workspace::{CurrentWorkspaceSettingPB, RepeatedWorkspacePB, WorkspaceIdPB, *}, }; use crate::{ dart_notification::{send_dart_notification, FolderNotification}, @@ -14,9 +14,9 @@ use std::{convert::TryInto, sync::Arc}; #[tracing::instrument(level = "debug", skip(data, controller), err)] pub(crate) async fn create_workspace_handler( - data: Data, + data: Data, controller: AppData>, -) -> DataResult { +) -> DataResult { let controller = controller.get_ref().clone(); let params: CreateWorkspaceParams = data.into_inner().try_into()?; let workspace_rev = controller.create_workspace_from_params(params).await?; @@ -26,33 +26,33 @@ pub(crate) async fn create_workspace_handler( #[tracing::instrument(level = "debug", skip(controller), err)] pub(crate) async fn read_workspace_apps_handler( controller: AppData>, -) -> DataResult { +) -> DataResult { let items = controller .read_current_workspace_apps() .await? .into_iter() .map(|app_rev| app_rev.into()) .collect(); - let repeated_app = RepeatedApp { items }; + let repeated_app = RepeatedAppPB { items }; data_result(repeated_app) } #[tracing::instrument(level = "debug", skip(data, controller), err)] pub(crate) async fn open_workspace_handler( - data: Data, + data: Data, controller: AppData>, -) -> DataResult { - let params: WorkspaceId = data.into_inner(); +) -> DataResult { + let params: WorkspaceIdPB = data.into_inner(); let workspaces = controller.open_workspace(params).await?; data_result(workspaces) } #[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn read_workspaces_handler( - data: Data, + data: Data, folder: AppData>, -) -> DataResult { - let params: WorkspaceId = data.into_inner(); +) -> DataResult { + let params: WorkspaceIdPB = data.into_inner(); let user_id = folder.user.user_id()?; let workspace_controller = folder.workspace_controller.clone(); @@ -79,10 +79,10 @@ pub(crate) async fn read_workspaces_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub async fn read_cur_workspace_handler( folder: AppData>, -) -> DataResult { +) -> DataResult { let workspace_id = get_current_workspace()?; let user_id = folder.user.user_id()?; - let params = WorkspaceId { + let params = WorkspaceIdPB { value: Some(workspace_id.clone()), }; @@ -95,13 +95,13 @@ pub async fn read_cur_workspace_handler( }) .await?; - let latest_view: Option = folder + let latest_view: Option = folder .view_controller .latest_visit_view() .await .unwrap_or(None) .map(|view_rev| view_rev.into()); - let setting = CurrentWorkspaceSetting { workspace, latest_view }; + let setting = CurrentWorkspaceSettingPB { workspace, latest_view }; let _ = read_workspaces_on_server(folder, user_id, params); data_result(setting) } @@ -110,7 +110,7 @@ pub async fn read_cur_workspace_handler( fn read_workspaces_on_server( folder_manager: AppData>, user_id: String, - params: WorkspaceId, + params: WorkspaceIdPB, ) -> Result<(), FlowyError> { let (token, server) = (folder_manager.user.token()?, folder_manager.cloud_service.clone()); let persistence = folder_manager.persistence.clone(); @@ -145,7 +145,7 @@ fn read_workspaces_on_server( }) .await?; - let repeated_workspace = RepeatedWorkspace { + let repeated_workspace = RepeatedWorkspacePB { items: workspace_revs .into_iter() .map(|workspace_rev| workspace_rev.into()) diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs b/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs index 86c3dc7478..f17986b484 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/folder_test.rs @@ -1,6 +1,6 @@ use crate::script::{invalid_workspace_name_test_case, FolderScript::*, FolderTest}; use flowy_folder::entities::view::ViewDataType; -use flowy_folder::entities::workspace::CreateWorkspacePayload; +use flowy_folder::entities::workspace::CreateWorkspacePayloadPB; use flowy_revision::disk::RevisionState; use flowy_test::{event_builder::*, FlowySDKTest}; @@ -63,7 +63,7 @@ async fn workspace_create_with_apps() { async fn workspace_create_with_invalid_name() { for (name, code) in invalid_workspace_name_test_case() { let sdk = FlowySDKTest::default(); - let request = CreateWorkspacePayload { + let request = CreateWorkspacePayloadPB { name, desc: "".to_owned(), }; diff --git a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs index b461730cf9..ed16664d90 100644 --- a/frontend/rust-lib/flowy-folder/tests/workspace/script.rs +++ b/frontend/rust-lib/flowy-folder/tests/workspace/script.rs @@ -1,23 +1,23 @@ -use flowy_folder::entities::view::{RepeatedViewId, ViewId}; -use flowy_folder::entities::workspace::WorkspaceId; +use flowy_folder::entities::view::{RepeatedViewIdPB, ViewIdPB}; +use flowy_folder::entities::workspace::WorkspaceIdPB; use flowy_folder::entities::{ - app::{App, RepeatedApp}, - trash::Trash, - view::{RepeatedView, View, ViewDataType}, - workspace::Workspace, + app::{AppIdPB, CreateAppPayloadPB, UpdateAppPayloadPB}, + trash::{RepeatedTrashPB, TrashIdPB, TrashType}, + view::{CreateViewPayloadPB, UpdateViewPayloadPB}, + workspace::{CreateWorkspacePayloadPB, RepeatedWorkspacePB}, }; use flowy_folder::entities::{ - app::{AppId, CreateAppPayload, UpdateAppPayload}, - trash::{RepeatedTrash, TrashId, TrashType}, - view::{CreateViewPayload, UpdateViewPayload}, - workspace::{CreateWorkspacePayload, RepeatedWorkspace}, + app::{AppPB, RepeatedAppPB}, + trash::TrashPB, + view::{RepeatedViewPB, ViewDataType, ViewPB}, + workspace::WorkspacePB, }; use flowy_folder::event_map::FolderEvent::*; use flowy_folder::{errors::ErrorCode, services::folder_editor::FolderEditor}; use flowy_revision::disk::RevisionState; use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS; -use flowy_sync::entities::text_block::TextBlockInfo; +use flowy_sync::entities::text_block::DocumentPB; use flowy_test::{event_builder::*, FlowySDKTest}; use std::{sync::Arc, time::Duration}; use tokio::time::sleep; @@ -30,7 +30,7 @@ pub enum FolderScript { desc: String, }, // AssertWorkspaceRevisionJson(String), - AssertWorkspace(Workspace), + AssertWorkspace(WorkspacePB), ReadWorkspace(Option), // App @@ -39,7 +39,7 @@ pub enum FolderScript { desc: String, }, // AssertAppRevisionJson(String), - AssertApp(App), + AssertApp(AppPB), ReadApp(String), UpdateApp { name: Option, @@ -53,7 +53,7 @@ pub enum FolderScript { desc: String, data_type: ViewDataType, }, - AssertView(View), + AssertView(ViewPB), ReadView(String), UpdateView { name: Option, @@ -79,11 +79,11 @@ pub enum FolderScript { pub struct FolderTest { pub sdk: FlowySDKTest, - pub all_workspace: Vec, - pub workspace: Workspace, - pub app: App, - pub view: View, - pub trash: Vec, + pub all_workspace: Vec, + pub workspace: WorkspacePB, + pub app: AppPB, + pub view: ViewPB, + pub trash: Vec, // pub folder_editor: } @@ -101,11 +101,11 @@ impl FolderTest { ViewDataType::TextBlock, ) .await; - app.belongings = RepeatedView { + app.belongings = RepeatedViewPB { items: vec![view.clone()], }; - workspace.apps = RepeatedApp { + workspace.apps = RepeatedAppPB { items: vec![app.clone()], }; Self { @@ -247,8 +247,8 @@ pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> { ] } -pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspace { - let request = CreateWorkspacePayload { +pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> WorkspacePB { + let request = CreateWorkspacePayloadPB { name: name.to_owned(), desc: desc.to_owned(), }; @@ -258,18 +258,18 @@ pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Wor .payload(request) .async_send() .await - .parse::(); + .parse::(); workspace } -pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option) -> Vec { - let request = WorkspaceId { value: workspace_id }; +pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option) -> Vec { + let request = WorkspaceIdPB { value: workspace_id }; let mut repeated_workspace = FolderEventBuilder::new(sdk.clone()) .event(ReadWorkspaces) .payload(request.clone()) .async_send() .await - .parse::(); + .parse::(); let workspaces; if let Some(workspace_id) = &request.value { @@ -277,7 +277,7 @@ pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option) -> .into_inner() .into_iter() .filter(|workspace| &workspace.id == workspace_id) - .collect::>(); + .collect::>(); debug_assert_eq!(workspaces.len(), 1); } else { workspaces = repeated_workspace.items; @@ -286,8 +286,8 @@ pub async fn read_workspace(sdk: &FlowySDKTest, workspace_id: Option) -> workspaces } -pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc: &str) -> App { - let create_app_request = CreateAppPayload { +pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc: &str) -> AppPB { + let create_app_request = CreateAppPayloadPB { workspace_id: workspace_id.to_owned(), name: name.to_string(), desc: desc.to_string(), @@ -299,12 +299,12 @@ pub async fn create_app(sdk: &FlowySDKTest, workspace_id: &str, name: &str, desc .payload(create_app_request) .async_send() .await - .parse::(); + .parse::(); app } -pub async fn read_app(sdk: &FlowySDKTest, app_id: &str) -> App { - let request = AppId { +pub async fn read_app(sdk: &FlowySDKTest, app_id: &str) -> AppPB { + let request = AppIdPB { value: app_id.to_owned(), }; @@ -313,13 +313,13 @@ pub async fn read_app(sdk: &FlowySDKTest, app_id: &str) -> App { .payload(request) .async_send() .await - .parse::(); + .parse::(); app } pub async fn update_app(sdk: &FlowySDKTest, app_id: &str, name: Option, desc: Option) { - let request = UpdateAppPayload { + let request = UpdateAppPayloadPB { app_id: app_id.to_string(), name, desc, @@ -335,7 +335,7 @@ pub async fn update_app(sdk: &FlowySDKTest, app_id: &str, name: Option, } pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) { - let request = AppId { + let request = AppIdPB { value: app_id.to_string(), }; @@ -346,8 +346,8 @@ pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) { .await; } -pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &str, data_type: ViewDataType) -> View { - let request = CreateViewPayload { +pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &str, data_type: ViewDataType) -> ViewPB { + let request = CreateViewPayloadPB { belong_to_id: app_id.to_string(), name: name.to_string(), desc: desc.to_string(), @@ -361,22 +361,22 @@ pub async fn create_view(sdk: &FlowySDKTest, app_id: &str, name: &str, desc: &st .payload(request) .async_send() .await - .parse::(); + .parse::(); view } -pub async fn read_view(sdk: &FlowySDKTest, view_id: &str) -> View { - let view_id: ViewId = view_id.into(); +pub async fn read_view(sdk: &FlowySDKTest, view_id: &str) -> ViewPB { + let view_id: ViewIdPB = view_id.into(); FolderEventBuilder::new(sdk.clone()) .event(ReadView) .payload(view_id) .async_send() .await - .parse::() + .parse::() } pub async fn update_view(sdk: &FlowySDKTest, view_id: &str, name: Option, desc: Option) { - let request = UpdateViewPayload { + let request = UpdateViewPayloadPB { view_id: view_id.to_string(), name, desc, @@ -390,7 +390,7 @@ pub async fn update_view(sdk: &FlowySDKTest, view_id: &str, name: Option } pub async fn delete_view(sdk: &FlowySDKTest, view_ids: Vec) { - let request = RepeatedViewId { items: view_ids }; + let request = RepeatedViewIdPB { items: view_ids }; FolderEventBuilder::new(sdk.clone()) .event(DeleteView) .payload(request) @@ -399,26 +399,26 @@ pub async fn delete_view(sdk: &FlowySDKTest, view_ids: Vec) { } #[allow(dead_code)] -pub async fn set_latest_view(sdk: &FlowySDKTest, view_id: &str) -> TextBlockInfo { - let view_id: ViewId = view_id.into(); +pub async fn set_latest_view(sdk: &FlowySDKTest, view_id: &str) -> DocumentPB { + let view_id: ViewIdPB = view_id.into(); FolderEventBuilder::new(sdk.clone()) .event(SetLatestView) .payload(view_id) .async_send() .await - .parse::() + .parse::() } -pub async fn read_trash(sdk: &FlowySDKTest) -> RepeatedTrash { +pub async fn read_trash(sdk: &FlowySDKTest) -> RepeatedTrashPB { FolderEventBuilder::new(sdk.clone()) .event(ReadTrash) .async_send() .await - .parse::() + .parse::() } pub async fn restore_app_from_trash(sdk: &FlowySDKTest, app_id: &str) { - let id = TrashId { + let id = TrashIdPB { id: app_id.to_owned(), ty: TrashType::TrashApp, }; @@ -430,7 +430,7 @@ pub async fn restore_app_from_trash(sdk: &FlowySDKTest, app_id: &str) { } pub async fn restore_view_from_trash(sdk: &FlowySDKTest, view_id: &str) { - let id = TrashId { + let id = TrashIdPB { id: view_id.to_owned(), ty: TrashType::TrashView, }; diff --git a/frontend/rust-lib/flowy-grid/Cargo.toml b/frontend/rust-lib/flowy-grid/Cargo.toml index e09e156772..ba3702038e 100644 --- a/frontend/rust-lib/flowy-grid/Cargo.toml +++ b/frontend/rust-lib/flowy-grid/Cargo.toml @@ -50,7 +50,6 @@ lib-infra = { path = "../../../shared-lib/lib-infra", features = ["protobuf_file [features] -default = ["filter"] +default = [] dart = ["lib-infra/dart"] -filter = [] flowy_unit_test = ["flowy-revision/flowy_unit_test"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-grid/Flowy.toml b/frontend/rust-lib/flowy-grid/Flowy.toml index 94bd0c0225..00e9cf7c04 100644 --- a/frontend/rust-lib/flowy-grid/Flowy.toml +++ b/frontend/rust-lib/flowy-grid/Flowy.toml @@ -2,7 +2,6 @@ proto_input = [ "src/event_map.rs", "src/services/field/type_options", - "src/services/field/select_option.rs", "src/entities", "src/dart_notification.rs" ] diff --git a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs index 9cfdbc088c..f778186903 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs @@ -4,39 +4,47 @@ use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::RowRevision; use std::sync::Arc; +/// [GridBlockPB] contains list of row ids. The rows here does not contain any data, just the id +/// of the row. Check out [GridRowPB] for more details. +/// +/// +/// A grid can have many rows. Rows are therefore grouped into Blocks in order to make +/// things more efficient. +/// | #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct GridBlock { +pub struct GridBlockPB { #[pb(index = 1)] pub id: String, #[pb(index = 2)] - pub row_infos: Vec, + pub rows: Vec, } -impl GridBlock { - pub fn new(block_id: &str, row_orders: Vec) -> Self { +impl GridBlockPB { + pub fn new(block_id: &str, rows: Vec) -> Self { Self { id: block_id.to_owned(), - row_infos: row_orders, + rows, } } } +/// [GridRowPB] Describes a row. Has the id of the parent Block. Has the metadata of the row. #[derive(Debug, Default, Clone, ProtoBuf)] -pub struct RowInfo { +pub struct GridRowPB { #[pb(index = 1)] pub block_id: String, #[pb(index = 2)] - pub row_id: String, + pub id: String, #[pb(index = 3)] pub height: i32, } -impl RowInfo { +impl GridRowPB { pub fn row_id(&self) -> &str { - &self.row_id + &self.id } pub fn block_id(&self) -> &str { @@ -44,66 +52,59 @@ impl RowInfo { } } -impl std::convert::From<&RowRevision> for RowInfo { +impl std::convert::From<&RowRevision> for GridRowPB { fn from(rev: &RowRevision) -> Self { Self { block_id: rev.block_id.clone(), - row_id: rev.id.clone(), + id: rev.id.clone(), height: rev.height, } } } -impl std::convert::From<&Arc> for RowInfo { +impl std::convert::From<&Arc> for GridRowPB { fn from(rev: &Arc) -> Self { Self { block_id: rev.block_id.clone(), - row_id: rev.id.clone(), + id: rev.id.clone(), height: rev.height, } } } #[derive(Debug, Default, ProtoBuf)] -pub struct Row { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2)] - pub height: i32, -} - -#[derive(Debug, Default, ProtoBuf)] -pub struct OptionalRow { +pub struct OptionalRowPB { #[pb(index = 1, one_of)] - pub row: Option, + pub row: Option, } #[derive(Debug, Default, ProtoBuf)] -pub struct RepeatedRow { +pub struct RepeatedRowPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::convert::From> for RepeatedRow { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedRowPB { + fn from(items: Vec) -> Self { Self { items } } } + +/// [RepeatedGridBlockPB] contains list of [GridBlockPB] #[derive(Debug, Default, ProtoBuf)] -pub struct RepeatedGridBlock { +pub struct RepeatedGridBlockPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::convert::From> for RepeatedGridBlock { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedGridBlockPB { + fn from(items: Vec) -> Self { Self { items } } } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct InsertedRow { +pub struct InsertedRowPB { #[pb(index = 1)] pub block_id: String, @@ -118,7 +119,7 @@ pub struct InsertedRow { } #[derive(Debug, Default, ProtoBuf)] -pub struct UpdatedRow { +pub struct UpdatedRowPB { #[pb(index = 1)] pub block_id: String, @@ -126,11 +127,11 @@ pub struct UpdatedRow { pub row_id: String, #[pb(index = 3)] - pub row: Row, + pub row: GridRowPB, } -impl UpdatedRow { - pub fn new(row_rev: &RowRevision, row: Row) -> Self { +impl UpdatedRowPB { + pub fn new(row_rev: &RowRevision, row: GridRowPB) -> Self { Self { row_id: row_rev.id.clone(), block_id: row_rev.block_id.clone(), @@ -139,10 +140,10 @@ impl UpdatedRow { } } -impl std::convert::From for InsertedRow { - fn from(row_info: RowInfo) -> Self { +impl std::convert::From for InsertedRowPB { + fn from(row_info: GridRowPB) -> Self { Self { - row_id: row_info.row_id, + row_id: row_info.id, block_id: row_info.block_id, height: row_info.height, index: None, @@ -150,26 +151,26 @@ impl std::convert::From for InsertedRow { } } -impl std::convert::From<&RowRevision> for InsertedRow { +impl std::convert::From<&RowRevision> for InsertedRowPB { fn from(row: &RowRevision) -> Self { - let row_order = RowInfo::from(row); + let row_order = GridRowPB::from(row); Self::from(row_order) } } #[derive(Debug, Default, ProtoBuf)] -pub struct GridBlockChangeset { +pub struct GridBlockChangesetPB { #[pb(index = 1)] pub block_id: String, #[pb(index = 2)] - pub inserted_rows: Vec, + pub inserted_rows: Vec, #[pb(index = 3)] pub deleted_rows: Vec, #[pb(index = 4)] - pub updated_rows: Vec, + pub updated_rows: Vec, #[pb(index = 5)] pub visible_rows: Vec, @@ -177,8 +178,8 @@ pub struct GridBlockChangeset { #[pb(index = 6)] pub hide_rows: Vec, } -impl GridBlockChangeset { - pub fn insert(block_id: &str, inserted_rows: Vec) -> Self { +impl GridBlockChangesetPB { + pub fn insert(block_id: &str, inserted_rows: Vec) -> Self { Self { block_id: block_id.to_owned(), inserted_rows, @@ -194,7 +195,7 @@ impl GridBlockChangeset { } } - pub fn update(block_id: &str, updated_rows: Vec) -> Self { + pub fn update(block_id: &str, updated_rows: Vec) -> Self { Self { block_id: block_id.to_owned(), updated_rows, @@ -203,8 +204,10 @@ impl GridBlockChangeset { } } +/// [QueryGridBlocksPayloadPB] is used to query the data of the block that belongs to the grid whose +/// id is grid_id. #[derive(ProtoBuf, Default)] -pub struct QueryGridBlocksPayload { +pub struct QueryGridBlocksPayloadPB { #[pb(index = 1)] pub grid_id: String, @@ -217,7 +220,7 @@ pub struct QueryGridBlocksParams { pub block_ids: Vec, } -impl TryInto for QueryGridBlocksPayload { +impl TryInto for QueryGridBlocksPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs index f7daa84147..1b86eb1e65 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/cell_entities.rs @@ -1,4 +1,3 @@ -use crate::entities::{FieldIdentifier, FieldIdentifierPayload}; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; @@ -6,42 +5,40 @@ use flowy_grid_data_model::revision::{CellRevision, RowMetaChangeset}; use std::collections::HashMap; #[derive(ProtoBuf, Default)] -pub struct CreateSelectOptionPayload { +pub struct CreateSelectOptionPayloadPB { #[pb(index = 1)] - pub field_identifier: FieldIdentifierPayload, + pub field_id: String, #[pb(index = 2)] + pub grid_id: String, + + #[pb(index = 3)] pub option_name: String, } pub struct CreateSelectOptionParams { - pub field_identifier: FieldIdentifier, + pub field_id: String, + pub grid_id: String, pub option_name: String, } -impl std::ops::Deref for CreateSelectOptionParams { - type Target = FieldIdentifier; - - fn deref(&self) -> &Self::Target { - &self.field_identifier - } -} - -impl TryInto for CreateSelectOptionPayload { +impl TryInto for CreateSelectOptionPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { let option_name = NotEmptyStr::parse(self.option_name).map_err(|_| ErrorCode::SelectOptionNameIsEmpty)?; - let field_identifier = self.field_identifier.try_into()?; + let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; Ok(CreateSelectOptionParams { - field_identifier, + field_id: field_id.0, option_name: option_name.0, + grid_id: grid_id.0, }) } } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct CellIdentifierPayload { +pub struct GridCellIdPB { #[pb(index = 1)] pub grid_id: String, @@ -52,20 +49,20 @@ pub struct CellIdentifierPayload { pub row_id: String, } -pub struct CellIdentifier { +pub struct GridCellIdParams { pub grid_id: String, pub field_id: String, pub row_id: String, } -impl TryInto for CellIdentifierPayload { +impl TryInto for GridCellIdPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; - Ok(CellIdentifier { + Ok(GridCellIdParams { grid_id: grid_id.0, field_id: field_id.0, row_id: row_id.0, @@ -73,7 +70,7 @@ impl TryInto for CellIdentifierPayload { } } #[derive(Debug, Default, ProtoBuf)] -pub struct Cell { +pub struct GridCellPB { #[pb(index = 1)] pub field_id: String, @@ -81,7 +78,7 @@ pub struct Cell { pub data: Vec, } -impl Cell { +impl GridCellPB { pub fn new(field_id: &str, data: Vec) -> Self { Self { field_id: field_id.to_owned(), @@ -98,32 +95,33 @@ impl Cell { } #[derive(Debug, Default, ProtoBuf)] -pub struct RepeatedCell { +pub struct RepeatedCellPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::ops::Deref for RepeatedCell { - type Target = Vec; +impl std::ops::Deref for RepeatedCellPB { + type Target = Vec; fn deref(&self) -> &Self::Target { &self.items } } -impl std::ops::DerefMut for RepeatedCell { +impl std::ops::DerefMut for RepeatedCellPB { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items } } -impl std::convert::From> for RepeatedCell { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedCellPB { + fn from(items: Vec) -> Self { Self { items } } } +/// #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct CellChangeset { +pub struct CellChangesetPB { #[pb(index = 1)] pub grid_id: String, @@ -137,8 +135,8 @@ pub struct CellChangeset { pub content: Option, } -impl std::convert::From for RowMetaChangeset { - fn from(changeset: CellChangeset) -> Self { +impl std::convert::From for RowMetaChangeset { + fn from(changeset: CellChangesetPB) -> Self { let mut cell_by_field_id = HashMap::with_capacity(1); let field_id = changeset.field_id; let cell_rev = CellRevision { diff --git a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs index d43e77e9ae..dce77787c3 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs @@ -8,8 +8,9 @@ use std::sync::Arc; use strum_macros::{Display, EnumCount as EnumCountMacro, EnumIter, EnumString}; +/// [GridFieldPB] defines a Field's attributes. Such as the name, field_type, and width. etc. #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct Field { +pub struct GridFieldPB { #[pb(index = 1)] pub id: String, @@ -35,7 +36,7 @@ pub struct Field { pub is_primary: bool, } -impl std::convert::From for Field { +impl std::convert::From for GridFieldPB { fn from(field_rev: FieldRevision) -> Self { Self { id: field_rev.id, @@ -50,31 +51,33 @@ impl std::convert::From for Field { } } -impl std::convert::From> for Field { +impl std::convert::From> for GridFieldPB { fn from(field_rev: Arc) -> Self { let field_rev = field_rev.as_ref().clone(); - Field::from(field_rev) + GridFieldPB::from(field_rev) } } + +/// [GridFieldIdPB] id of the [Field] #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct FieldOrder { +pub struct GridFieldIdPB { #[pb(index = 1)] pub field_id: String, } -impl std::convert::From<&str> for FieldOrder { +impl std::convert::From<&str> for GridFieldIdPB { fn from(s: &str) -> Self { - FieldOrder { field_id: s.to_owned() } + GridFieldIdPB { field_id: s.to_owned() } } } -impl std::convert::From for FieldOrder { +impl std::convert::From for GridFieldIdPB { fn from(s: String) -> Self { - FieldOrder { field_id: s } + GridFieldIdPB { field_id: s } } } -impl std::convert::From<&Arc> for FieldOrder { +impl std::convert::From<&Arc> for GridFieldIdPB { fn from(field_rev: &Arc) -> Self { Self { field_id: field_rev.id.clone(), @@ -82,22 +85,22 @@ impl std::convert::From<&Arc> for FieldOrder { } } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct GridFieldChangeset { +pub struct GridFieldChangesetPB { #[pb(index = 1)] pub grid_id: String, #[pb(index = 2)] - pub inserted_fields: Vec, + pub inserted_fields: Vec, #[pb(index = 3)] - pub deleted_fields: Vec, + pub deleted_fields: Vec, #[pb(index = 4)] - pub updated_fields: Vec, + pub updated_fields: Vec, } -impl GridFieldChangeset { - pub fn insert(grid_id: &str, inserted_fields: Vec) -> Self { +impl GridFieldChangesetPB { + pub fn insert(grid_id: &str, inserted_fields: Vec) -> Self { Self { grid_id: grid_id.to_owned(), inserted_fields, @@ -106,7 +109,7 @@ impl GridFieldChangeset { } } - pub fn delete(grid_id: &str, deleted_fields: Vec) -> Self { + pub fn delete(grid_id: &str, deleted_fields: Vec) -> Self { Self { grid_id: grid_id.to_string(), inserted_fields: vec![], @@ -115,7 +118,7 @@ impl GridFieldChangeset { } } - pub fn update(grid_id: &str, updated_fields: Vec) -> Self { + pub fn update(grid_id: &str, updated_fields: Vec) -> Self { Self { grid_id: grid_id.to_string(), inserted_fields: vec![], @@ -126,25 +129,25 @@ impl GridFieldChangeset { } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct IndexField { +pub struct IndexFieldPB { #[pb(index = 1)] - pub field: Field, + pub field: GridFieldPB, #[pb(index = 2)] pub index: i32, } -impl IndexField { +impl IndexFieldPB { pub fn from_field_rev(field_rev: &Arc, index: usize) -> Self { Self { - field: Field::from(field_rev.as_ref().clone()), + field: GridFieldPB::from(field_rev.as_ref().clone()), index: index as i32, } } } #[derive(Debug, Default, ProtoBuf)] -pub struct GetEditFieldContextPayload { +pub struct GetEditFieldContextPayloadPB { #[pb(index = 1)] pub grid_id: String, @@ -156,7 +159,42 @@ pub struct GetEditFieldContextPayload { } #[derive(Debug, Default, ProtoBuf)] -pub struct EditFieldPayload { +pub struct CreateFieldPayloadPB { + #[pb(index = 1)] + pub grid_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub field_type: FieldType, + + #[pb(index = 4)] + pub create_if_not_exist: bool, +} + +pub struct CreateFieldParams { + pub grid_id: String, + pub field_id: String, + pub field_type: FieldType, +} + +impl TryInto for CreateFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(CreateFieldParams { + grid_id: grid_id.0, + field_id: field_id.0, + field_type: self.field_type, + }) + } +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct EditFieldPayloadPB { #[pb(index = 1)] pub grid_id: String, @@ -176,7 +214,7 @@ pub struct EditFieldParams { pub field_type: FieldType, } -impl TryInto for EditFieldPayload { +impl TryInto for EditFieldPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -190,106 +228,116 @@ impl TryInto for EditFieldPayload { } } -pub struct CreateFieldParams { +#[derive(Debug, Default, ProtoBuf)] +pub struct GridFieldTypeOptionIdPB { + #[pb(index = 1)] pub grid_id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] pub field_type: FieldType, } -impl TryInto for EditFieldPayload { +pub struct GridFieldTypeOptionIdParams { + pub grid_id: String, + pub field_id: String, + pub field_type: FieldType, +} + +impl TryInto for GridFieldTypeOptionIdPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; - - Ok(CreateFieldParams { + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(GridFieldTypeOptionIdParams { grid_id: grid_id.0, + field_id: field_id.0, field_type: self.field_type, }) } } +/// Certain field types have user-defined options such as color, date format, number format, +/// or a list of values for a multi-select list. These options are defined within a specialization +/// of the FieldTypeOption class. +/// +/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid#fieldtype) +/// for more information. +/// #[derive(Debug, Default, ProtoBuf)] -pub struct FieldTypeOptionContext { +pub struct FieldTypeOptionDataPB { #[pb(index = 1)] pub grid_id: String, #[pb(index = 2)] - pub grid_field: Field, + pub field: GridFieldPB, #[pb(index = 3)] pub type_option_data: Vec, } +/// Collection of the [GridFieldPB] #[derive(Debug, Default, ProtoBuf)] -pub struct FieldTypeOptionData { +pub struct RepeatedGridFieldPB { #[pb(index = 1)] - pub grid_id: String, - - #[pb(index = 2)] - pub field: Field, - - #[pb(index = 3)] - pub type_option_data: Vec, + pub items: Vec, } - -#[derive(Debug, Default, ProtoBuf)] -pub struct RepeatedField { - #[pb(index = 1)] - pub items: Vec, -} -impl std::ops::Deref for RepeatedField { - type Target = Vec; +impl std::ops::Deref for RepeatedGridFieldPB { + type Target = Vec; fn deref(&self) -> &Self::Target { &self.items } } -impl std::ops::DerefMut for RepeatedField { +impl std::ops::DerefMut for RepeatedGridFieldPB { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items } } -impl std::convert::From> for RepeatedField { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedGridFieldPB { + fn from(items: Vec) -> Self { Self { items } } } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct RepeatedFieldOrder { +pub struct RepeatedGridFieldIdPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::ops::Deref for RepeatedFieldOrder { - type Target = Vec; +impl std::ops::Deref for RepeatedGridFieldIdPB { + type Target = Vec; fn deref(&self) -> &Self::Target { &self.items } } -impl std::convert::From> for RepeatedFieldOrder { - fn from(field_orders: Vec) -> Self { - RepeatedFieldOrder { items: field_orders } +impl std::convert::From> for RepeatedGridFieldIdPB { + fn from(items: Vec) -> Self { + RepeatedGridFieldIdPB { items } } } -impl std::convert::From for RepeatedFieldOrder { +impl std::convert::From for RepeatedGridFieldIdPB { fn from(s: String) -> Self { - RepeatedFieldOrder { - items: vec![FieldOrder::from(s)], + RepeatedGridFieldIdPB { + items: vec![GridFieldIdPB::from(s)], } } } #[derive(ProtoBuf, Default)] -pub struct InsertFieldPayload { +pub struct InsertFieldPayloadPB { #[pb(index = 1)] pub grid_id: String, #[pb(index = 2)] - pub field: Field, + pub field: GridFieldPB, #[pb(index = 3)] pub type_option_data: Vec, @@ -301,12 +349,12 @@ pub struct InsertFieldPayload { #[derive(Clone)] pub struct InsertFieldParams { pub grid_id: String, - pub field: Field, + pub field: GridFieldPB, pub type_option_data: Vec, pub start_field_id: Option, } -impl TryInto for InsertFieldPayload { +impl TryInto for InsertFieldPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -327,14 +375,16 @@ impl TryInto for InsertFieldPayload { } } +/// [UpdateFieldTypeOptionPayloadPB] is used to update the type option data. #[derive(ProtoBuf, Default)] -pub struct UpdateFieldTypeOptionPayload { +pub struct UpdateFieldTypeOptionPayloadPB { #[pb(index = 1)] pub grid_id: String, #[pb(index = 2)] pub field_id: String, + /// Check out [FieldTypeOptionDataPB] for more details. #[pb(index = 3)] pub type_option_data: Vec, } @@ -346,7 +396,7 @@ pub struct UpdateFieldTypeOptionParams { pub type_option_data: Vec, } -impl TryInto for UpdateFieldTypeOptionPayload { +impl TryInto for UpdateFieldTypeOptionPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -362,33 +412,39 @@ impl TryInto for UpdateFieldTypeOptionPayload { } #[derive(ProtoBuf, Default)] -pub struct QueryFieldPayload { +pub struct QueryFieldPayloadPB { #[pb(index = 1)] pub grid_id: String, #[pb(index = 2)] - pub field_orders: RepeatedFieldOrder, + pub field_ids: RepeatedGridFieldIdPB, } pub struct QueryFieldParams { pub grid_id: String, - pub field_orders: RepeatedFieldOrder, + pub field_ids: RepeatedGridFieldIdPB, } -impl TryInto for QueryFieldPayload { +impl TryInto for QueryFieldPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; Ok(QueryFieldParams { grid_id: grid_id.0, - field_orders: self.field_orders, + field_ids: self.field_ids, }) } } +/// [FieldChangesetPayloadPB] is used to modify the corresponding field. It defines which properties of +/// the field can be modified. +/// +/// Pass in None if you don't want to modify a property +/// Pass in Some(Value) if you want to modify a property +/// #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct FieldChangesetPayload { +pub struct FieldChangesetPayloadPB { #[pb(index = 1)] pub field_id: String, @@ -417,7 +473,7 @@ pub struct FieldChangesetPayload { pub type_option_data: Option>, } -impl TryInto for FieldChangesetPayload { +impl TryInto for FieldChangesetPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -569,7 +625,7 @@ impl std::convert::From for FieldType { } } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct FieldIdentifierPayload { +pub struct DuplicateFieldPayloadPB { #[pb(index = 1)] pub field_id: String, @@ -577,20 +633,51 @@ pub struct FieldIdentifierPayload { pub grid_id: String, } -pub struct FieldIdentifier { +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct GridFieldIdentifierPayloadPB { + #[pb(index = 1)] pub field_id: String, + + #[pb(index = 2)] pub grid_id: String, } -impl TryInto for FieldIdentifierPayload { +impl TryInto for DuplicateFieldPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; - Ok(FieldIdentifier { + Ok(GridFieldIdParams { grid_id: grid_id.0, field_id: field_id.0, }) } } + +#[derive(Debug, Clone, Default, ProtoBuf)] +pub struct DeleteFieldPayloadPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] + pub grid_id: String, +} + +impl TryInto for DeleteFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(GridFieldIdParams { + grid_id: grid_id.0, + field_id: field_id.0, + }) + } +} + +pub struct GridFieldIdParams { + pub field_id: String, + pub grid_id: String, +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs index 73c39cb657..9eb4ff3fe9 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs @@ -1,4 +1,4 @@ -use crate::services::field::select_option::SelectOptionIds; +use crate::services::field::SelectOptionIds; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::revision::GridFilterRevision; diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs index bc520dea54..03cc3d6111 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs @@ -17,7 +17,7 @@ pub struct GridFilter { } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct RepeatedGridFilter { +pub struct RepeatedGridFilterPB { #[pb(index = 1)] pub items: Vec, } @@ -28,22 +28,22 @@ impl std::convert::From<&GridFilterRevision> for GridFilter { } } -impl std::convert::From>> for RepeatedGridFilter { +impl std::convert::From>> for RepeatedGridFilterPB { fn from(revs: Vec>) -> Self { - RepeatedGridFilter { + RepeatedGridFilterPB { items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(), } } } -impl std::convert::From> for RepeatedGridFilter { +impl std::convert::From> for RepeatedGridFilterPB { fn from(items: Vec) -> Self { Self { items } } } #[derive(ProtoBuf, Debug, Default, Clone)] -pub struct DeleteFilterPayload { +pub struct DeleteFilterPayloadPB { #[pb(index = 1)] pub field_id: String, @@ -54,7 +54,7 @@ pub struct DeleteFilterPayload { pub field_type: FieldType, } -impl TryInto for DeleteFilterPayload { +impl TryInto for DeleteFilterPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -73,7 +73,7 @@ impl TryInto for DeleteFilterPayload { } #[derive(ProtoBuf, Debug, Default, Clone)] -pub struct CreateGridFilterPayload { +pub struct CreateGridFilterPayloadPB { #[pb(index = 1)] pub field_id: String, @@ -87,7 +87,7 @@ pub struct CreateGridFilterPayload { pub content: Option, } -impl CreateGridFilterPayload { +impl CreateGridFilterPayloadPB { #[allow(dead_code)] pub fn new>(field_rev: &FieldRevision, condition: T, content: Option) -> Self { Self { @@ -99,7 +99,7 @@ impl CreateGridFilterPayload { } } -impl TryInto for CreateGridFilterPayload { +impl TryInto for CreateGridFilterPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs index c05df11414..49278afc54 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/grid_entities.rs @@ -1,69 +1,71 @@ -use crate::entities::{FieldOrder, GridBlock}; +use crate::entities::{GridBlockPB, GridFieldIdPB}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; + +/// [GridPB] describes how many fields and blocks the grid has #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct Grid { +pub struct GridPB { #[pb(index = 1)] pub id: String, #[pb(index = 2)] - pub field_orders: Vec, + pub fields: Vec, #[pb(index = 3)] - pub blocks: Vec, + pub blocks: Vec, } #[derive(ProtoBuf, Default)] -pub struct CreateGridPayload { +pub struct CreateGridPayloadPB { #[pb(index = 1)] pub name: String, } #[derive(Clone, ProtoBuf, Default, Debug)] -pub struct GridId { +pub struct GridIdPB { #[pb(index = 1)] pub value: String, } -impl AsRef for GridId { +impl AsRef for GridIdPB { fn as_ref(&self) -> &str { &self.value } } #[derive(Clone, ProtoBuf, Default, Debug)] -pub struct GridBlockId { +pub struct GridBlockIdPB { #[pb(index = 1)] pub value: String, } -impl AsRef for GridBlockId { +impl AsRef for GridBlockIdPB { fn as_ref(&self) -> &str { &self.value } } -impl std::convert::From<&str> for GridBlockId { +impl std::convert::From<&str> for GridBlockIdPB { fn from(s: &str) -> Self { - GridBlockId { value: s.to_owned() } + GridBlockIdPB { value: s.to_owned() } } } #[derive(Debug, Clone, ProtoBuf_Enum)] -pub enum MoveItemType { +pub enum MoveItemTypePB { MoveField = 0, MoveRow = 1, } -impl std::default::Default for MoveItemType { +impl std::default::Default for MoveItemTypePB { fn default() -> Self { - MoveItemType::MoveField + MoveItemTypePB::MoveField } } #[derive(Debug, Clone, Default, ProtoBuf)] -pub struct MoveItemPayload { +pub struct MoveItemPayloadPB { #[pb(index = 1)] pub grid_id: String, @@ -77,7 +79,7 @@ pub struct MoveItemPayload { pub to_index: i32, #[pb(index = 5)] - pub ty: MoveItemType, + pub ty: MoveItemTypePB, } #[derive(Clone)] @@ -86,10 +88,10 @@ pub struct MoveItemParams { pub item_id: String, pub from_index: i32, pub to_index: i32, - pub ty: MoveItemType, + pub ty: MoveItemTypePB, } -impl TryInto for MoveItemPayload { +impl TryInto for MoveItemPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities.rs index 53d20bbc57..dda624fc67 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities.rs @@ -7,7 +7,7 @@ use std::convert::TryInto; use std::sync::Arc; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridGroup { +pub struct GridGroupPB { #[pb(index = 1)] pub id: String, @@ -18,9 +18,9 @@ pub struct GridGroup { pub sub_group_field_id: Option, } -impl std::convert::From<&GridGroupRevision> for GridGroup { +impl std::convert::From<&GridGroupRevision> for GridGroupPB { fn from(rev: &GridGroupRevision) -> Self { - GridGroup { + GridGroupPB { id: rev.id.clone(), group_field_id: rev.field_id.clone(), sub_group_field_id: rev.sub_field_id.clone(), @@ -29,27 +29,27 @@ impl std::convert::From<&GridGroupRevision> for GridGroup { } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct RepeatedGridGroup { +pub struct RepeatedGridGroupPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::convert::From> for RepeatedGridGroup { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedGridGroupPB { + fn from(items: Vec) -> Self { Self { items } } } -impl std::convert::From>> for RepeatedGridGroup { +impl std::convert::From>> for RepeatedGridGroupPB { fn from(revs: Vec>) -> Self { - RepeatedGridGroup { + RepeatedGridGroupPB { items: revs.iter().map(|rev| rev.as_ref().into()).collect(), } } } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct CreateGridGroupPayload { +pub struct CreateGridGroupPayloadPB { #[pb(index = 1, one_of)] pub field_id: Option, @@ -57,7 +57,7 @@ pub struct CreateGridGroupPayload { pub sub_field_id: Option, } -impl TryInto for CreateGridGroupPayload { +impl TryInto for CreateGridGroupPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs index 4b3c8a7a78..745a5dc368 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/row_entities.rs @@ -2,20 +2,8 @@ use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; -#[derive(ProtoBuf, Default)] -pub struct GridRowIdPayload { - #[pb(index = 1)] - pub grid_id: String, - - #[pb(index = 2)] - pub block_id: String, - - #[pb(index = 3)] - pub row_id: String, -} - #[derive(Debug, Default, Clone, ProtoBuf)] -pub struct GridRowId { +pub struct GridRowIdPB { #[pb(index = 1)] pub grid_id: String, @@ -26,15 +14,21 @@ pub struct GridRowId { pub row_id: String, } -impl TryInto for GridRowIdPayload { +pub struct GridRowIdParams { + pub grid_id: String, + pub block_id: String, + pub row_id: String, +} + +impl TryInto for GridRowIdPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let block_id = NotEmptyStr::parse(self.block_id).map_err(|_| ErrorCode::BlockIdIsEmpty)?; let row_id = NotEmptyStr::parse(self.row_id).map_err(|_| ErrorCode::RowIdIsEmpty)?; - Ok(GridRowId { + Ok(GridRowIdParams { grid_id: grid_id.0, block_id: block_id.0, row_id: row_id.0, @@ -43,7 +37,7 @@ impl TryInto for GridRowIdPayload { } #[derive(Debug, Default, Clone, ProtoBuf)] -pub struct BlockRowId { +pub struct BlockRowIdPB { #[pb(index = 1)] pub block_id: String, @@ -52,7 +46,7 @@ pub struct BlockRowId { } #[derive(ProtoBuf, Default)] -pub struct CreateRowPayload { +pub struct CreateRowPayloadPB { #[pb(index = 1)] pub grid_id: String, @@ -66,7 +60,7 @@ pub struct CreateRowParams { pub start_row_id: Option, } -impl TryInto for CreateRowPayload { +impl TryInto for CreateRowPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index 740ab38def..3564886c4a 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -1,6 +1,6 @@ use crate::entities::{ - CreateGridFilterPayload, CreateGridGroupPayload, CreateGridSortPayload, DeleteFilterPayload, RepeatedGridFilter, - RepeatedGridGroup, RepeatedGridSort, + CreateGridFilterPayloadPB, CreateGridGroupPayloadPB, CreateGridSortPayloadPB, DeleteFilterPayloadPB, + RepeatedGridFilterPB, RepeatedGridGroupPB, RepeatedGridSortPB, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; @@ -12,35 +12,36 @@ use std::convert::TryInto; use strum::IntoEnumIterator; use strum_macros::EnumIter; +/// [GridSettingPB] defines the setting options for the grid. Such as the filter, group, and sort. #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridSetting { +pub struct GridSettingPB { #[pb(index = 1)] - pub layouts: Vec, + pub layouts: Vec, #[pb(index = 2)] pub current_layout_type: GridLayoutType, #[pb(index = 3)] - pub filters_by_field_id: HashMap, + pub filters_by_field_id: HashMap, #[pb(index = 4)] - pub groups_by_field_id: HashMap, + pub groups_by_field_id: HashMap, #[pb(index = 5)] - pub sorts_by_field_id: HashMap, + pub sorts_by_field_id: HashMap, } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridLayout { +pub struct GridLayoutPB { #[pb(index = 1)] ty: GridLayoutType, } -impl GridLayout { - pub fn all() -> Vec { +impl GridLayoutPB { + pub fn all() -> Vec { let mut layouts = vec![]; for layout_ty in GridLayoutType::iter() { - layouts.push(GridLayout { ty: layout_ty }) + layouts.push(GridLayoutPB { ty: layout_ty }) } layouts @@ -79,7 +80,7 @@ impl std::convert::From for GridLayoutRevision { } #[derive(Default, ProtoBuf)] -pub struct GridSettingChangesetPayload { +pub struct GridSettingChangesetPayloadPB { #[pb(index = 1)] pub grid_id: String, @@ -87,25 +88,25 @@ pub struct GridSettingChangesetPayload { pub layout_type: GridLayoutType, #[pb(index = 3, one_of)] - pub insert_filter: Option, + pub insert_filter: Option, #[pb(index = 4, one_of)] - pub delete_filter: Option, + pub delete_filter: Option, #[pb(index = 5, one_of)] - pub insert_group: Option, + pub insert_group: Option, #[pb(index = 6, one_of)] pub delete_group: Option, #[pb(index = 7, one_of)] - pub insert_sort: Option, + pub insert_sort: Option, #[pb(index = 8, one_of)] pub delete_sort: Option, } -impl TryInto for GridSettingChangesetPayload { +impl TryInto for GridSettingChangesetPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs index 3a560460fe..b630b000c5 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs @@ -26,32 +26,32 @@ impl std::convert::From<&GridSortRevision> for GridSort { } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct RepeatedGridSort { +pub struct RepeatedGridSortPB { #[pb(index = 1)] pub items: Vec, } -impl std::convert::From>> for RepeatedGridSort { +impl std::convert::From>> for RepeatedGridSortPB { fn from(revs: Vec>) -> Self { - RepeatedGridSort { + RepeatedGridSortPB { items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(), } } } -impl std::convert::From> for RepeatedGridSort { +impl std::convert::From> for RepeatedGridSortPB { fn from(items: Vec) -> Self { Self { items } } } #[derive(ProtoBuf, Debug, Default, Clone)] -pub struct CreateGridSortPayload { +pub struct CreateGridSortPayloadPB { #[pb(index = 1, one_of)] pub field_id: Option, } -impl TryInto for CreateGridSortPayload { +impl TryInto for CreateGridSortPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 7d470bdfb9..b0ef43f5ad 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -1,10 +1,13 @@ use crate::entities::*; use crate::manager::GridManager; use crate::services::cell::AnyCellData; -use crate::services::field::select_option::*; use crate::services::field::{ - default_type_option_builder_from_type, type_option_builder_from_json_str, DateChangesetParams, DateChangesetPayload, + default_type_option_builder_from_type, select_option_operation, type_option_builder_from_json_str, + DateChangesetParams, DateChangesetPayloadPB, SelectOptionCellChangeset, SelectOptionCellChangesetParams, + SelectOptionCellChangesetPayloadPB, SelectOptionCellDataPB, SelectOptionChangeset, SelectOptionChangesetPayloadPB, + SelectOptionPB, }; +use crate::services::row::make_row_from_row_rev; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::FieldRevision; use flowy_sync::entities::grid::{FieldChangesetParams, GridSettingChangesetParams}; @@ -13,10 +16,10 @@ use std::sync::Arc; #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn get_grid_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { - let grid_id: GridId = data.into_inner(); +) -> DataResult { + let grid_id: GridIdPB = data.into_inner(); let editor = manager.open_grid(grid_id).await?; let grid = editor.get_grid_data().await?; data_result(grid) @@ -24,10 +27,10 @@ pub(crate) async fn get_grid_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn get_grid_setting_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { - let grid_id: GridId = data.into_inner(); +) -> DataResult { + let grid_id: GridIdPB = data.into_inner(); let editor = manager.open_grid(grid_id).await?; let grid_setting = editor.get_grid_setting().await?; data_result(grid_setting) @@ -35,7 +38,7 @@ pub(crate) async fn get_grid_setting_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn update_grid_setting_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { let params: GridSettingChangesetParams = data.into_inner().try_into()?; @@ -46,9 +49,9 @@ pub(crate) async fn update_grid_setting_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn get_grid_blocks_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let params: QueryGridBlocksParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let repeated_grid_block = editor.get_blocks(Some(params.block_ids)).await?; @@ -57,25 +60,25 @@ pub(crate) async fn get_grid_blocks_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn get_fields_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let params: QueryFieldParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let field_orders = params - .field_orders + .field_ids .items .into_iter() .map(|field_order| field_order.field_id) .collect(); let field_revs = editor.get_field_revs(Some(field_orders)).await?; - let repeated_field: RepeatedField = field_revs.into_iter().map(Field::from).collect::>().into(); + let repeated_field: RepeatedGridFieldPB = field_revs.into_iter().map(GridFieldPB::from).collect::>().into(); data_result(repeated_field) } #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn update_field_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { let changeset: FieldChangesetParams = data.into_inner().try_into()?; @@ -86,7 +89,7 @@ pub(crate) async fn update_field_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn insert_field_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { let params: InsertFieldParams = data.into_inner().try_into()?; @@ -97,7 +100,7 @@ pub(crate) async fn insert_field_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn update_field_type_option_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { let params: UpdateFieldTypeOptionParams = data.into_inner().try_into()?; @@ -110,10 +113,10 @@ pub(crate) async fn update_field_type_option_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn delete_field_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: FieldIdentifier = data.into_inner().try_into()?; + let params: GridFieldIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.delete_field(¶ms.field_id).await?; Ok(()) @@ -121,9 +124,9 @@ pub(crate) async fn delete_field_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn switch_to_field_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let params: EditFieldParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; editor @@ -137,7 +140,7 @@ pub(crate) async fn switch_to_field_handler( .unwrap_or(Arc::new(editor.next_field_rev(¶ms.field_type).await?)); let type_option_data = get_type_option_data(&field_rev, ¶ms.field_type).await?; - let data = FieldTypeOptionData { + let data = FieldTypeOptionDataPB { grid_id: params.grid_id, field: field_rev.into(), type_option_data, @@ -148,10 +151,10 @@ pub(crate) async fn switch_to_field_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn duplicate_field_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: FieldIdentifier = data.into_inner().try_into()?; + let params: GridFieldIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.duplicate_field(¶ms.field_id).await?; Ok(()) @@ -160,17 +163,17 @@ pub(crate) async fn duplicate_field_handler( /// Return the FieldTypeOptionData if the Field exists otherwise return record not found error. #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn get_field_type_option_data_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { - let params: EditFieldParams = data.into_inner().try_into()?; +) -> DataResult { + let params: GridFieldTypeOptionIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; match editor.get_field_rev(¶ms.field_id).await { None => Err(FlowyError::record_not_found()), Some(field_rev) => { let field_type = field_rev.field_type_rev.into(); let type_option_data = get_type_option_data(&field_rev, &field_type).await?; - let data = FieldTypeOptionData { + let data = FieldTypeOptionDataPB { grid_id: params.grid_id, field: field_rev.into(), type_option_data, @@ -183,16 +186,16 @@ pub(crate) async fn get_field_type_option_data_handler( /// Create FieldMeta and save it. Return the FieldTypeOptionData. #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn create_field_type_option_data_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let params: CreateFieldParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let field_rev = editor.create_next_field_rev(¶ms.field_type).await?; let field_type: FieldType = field_rev.field_type_rev.into(); let type_option_data = get_type_option_data(&field_rev, &field_type).await?; - data_result(FieldTypeOptionData { + data_result(FieldTypeOptionDataPB { grid_id: params.grid_id, field: field_rev.into(), type_option_data, @@ -201,7 +204,7 @@ pub(crate) async fn create_field_type_option_data_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn move_item_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { let params: MoveItemParams = data.into_inner().try_into()?; @@ -224,23 +227,25 @@ async fn get_type_option_data(field_rev: &FieldRevision, field_type: &FieldType) #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn get_row_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { - let params: GridRowId = data.into_inner().try_into()?; +) -> DataResult { + let params: GridRowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; - let row = OptionalRow { - row: editor.get_row(¶ms.row_id).await?, - }; - data_result(row) + let row = editor + .get_row_rev(¶ms.row_id) + .await? + .and_then(make_row_from_row_rev); + + data_result(OptionalRowPB { row }) } #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn delete_row_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: GridRowId = data.into_inner().try_into()?; + let params: GridRowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.delete_row(¶ms.row_id).await?; Ok(()) @@ -248,10 +253,10 @@ pub(crate) async fn delete_row_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn duplicate_row_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let params: GridRowId = data.into_inner().try_into()?; + let params: GridRowIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; let _ = editor.duplicate_row(¶ms.row_id).await?; Ok(()) @@ -259,7 +264,7 @@ pub(crate) async fn duplicate_row_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn create_row_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { let params: CreateRowParams = data.into_inner().try_into()?; @@ -270,23 +275,23 @@ pub(crate) async fn create_row_handler( // #[tracing::instrument(level = "debug", skip_all, err)] pub(crate) async fn get_cell_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { - let params: CellIdentifier = data.into_inner().try_into()?; +) -> DataResult { + let params: GridCellIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; match editor.get_cell(¶ms).await { - None => data_result(Cell::empty(¶ms.field_id)), + None => data_result(GridCellPB::empty(¶ms.field_id)), Some(cell) => data_result(cell), } } #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn update_cell_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { - let changeset: CellChangeset = data.into_inner(); + let changeset: CellChangesetPB = data.into_inner(); let editor = manager.get_grid_editor(&changeset.grid_id)?; let _ = editor.update_cell(changeset).await?; Ok(()) @@ -294,9 +299,9 @@ pub(crate) async fn update_cell_handler( #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn new_select_option_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let params: CreateSelectOptionParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; match editor.get_field_rev(¶ms.field_id).await { @@ -311,7 +316,7 @@ pub(crate) async fn new_select_option_handler( #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn update_select_option_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { let changeset: SelectOptionChangeset = data.into_inner().try_into()?; @@ -339,7 +344,7 @@ pub(crate) async fn update_select_option_handler( mut_field_rev.insert_type_option_entry(&*type_option); let _ = editor.replace_field(field_rev).await?; - let changeset = CellChangeset { + let changeset = CellChangesetPB { grid_id: changeset.cell_identifier.grid_id, row_id: changeset.cell_identifier.row_id, field_id: changeset.cell_identifier.field_id, @@ -352,15 +357,15 @@ pub(crate) async fn update_select_option_handler( #[tracing::instrument(level = "trace", skip(data, manager), err)] pub(crate) async fn get_select_option_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { - let params: CellIdentifier = data.into_inner().try_into()?; +) -> DataResult { + let params: GridCellIdParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; match editor.get_field_rev(¶ms.field_id).await { None => { tracing::error!("Can't find the select option field with id: {}", params.field_id); - data_result(SelectOptionCellData::default()) + data_result(SelectOptionCellDataPB::default()) } Some(field_rev) => { // @@ -369,11 +374,11 @@ pub(crate) async fn get_select_option_handler( let any_cell_data: AnyCellData = match cell_rev { None => AnyCellData { data: "".to_string(), - field_type: field_rev.field_type_rev.clone().into(), + field_type: field_rev.field_type_rev.into(), }, Some(cell_rev) => cell_rev.try_into()?, }; - let option_context = type_option.selected_select_option(any_cell_data); + let option_context = type_option.selected_select_option(any_cell_data.into()); data_result(option_context) } } @@ -381,7 +386,7 @@ pub(crate) async fn get_select_option_handler( #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn update_select_option_cell_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { let params: SelectOptionCellChangesetParams = data.into_inner().try_into()?; @@ -392,7 +397,7 @@ pub(crate) async fn update_select_option_cell_handler( #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn update_date_cell_handler( - data: Data, + data: Data, manager: AppData>, ) -> Result<(), FlowyError> { let params: DateChangesetParams = data.into_inner().try_into()?; diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 8739be5257..0855ef0032 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -42,81 +42,166 @@ pub fn create(grid_manager: Arc) -> Module { module } +/// [GridEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf) +/// out, it includes how to use these annotations: input, output, etc. #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum GridEvent { - #[event(input = "GridId", output = "Grid")] + /// [GetGrid] event is used to get the [GridPB] + /// + /// The event handler accepts a [GridIdPB] and returns a [GridPB] if there are no errors. + #[event(input = "GridIdPB", output = "GridPB")] GetGrid = 0, - #[event(input = "QueryGridBlocksPayload", output = "RepeatedGridBlock")] + /// [GetGridBlocks] event is used to get the grid's block. + /// + /// The event handler accepts a [QueryGridBlocksPayloadPB] and returns a [RepeatedGridBlockPB] + /// if there are no errors. + #[event(input = "QueryGridBlocksPayloadPB", output = "RepeatedGridBlockPB")] GetGridBlocks = 1, - #[event(input = "GridId", output = "GridSetting")] + /// [GetGridSetting] event is used to get the grid's settings. + /// + /// The event handler accepts [GridIdPB] and return [GridSettingPB] + /// if there is no errors. + #[event(input = "GridIdPB", output = "GridSettingPB")] GetGridSetting = 2, - #[event(input = "GridId", input = "GridSettingChangesetPayload")] + /// [UpdateGridSetting] event is used to update the grid's settings. + /// + /// The event handler accepts [GridIdPB] and return errors if failed to modify the grid's settings. + #[event(input = "GridIdPB", input = "GridSettingChangesetPayloadPB")] UpdateGridSetting = 3, - #[event(input = "QueryFieldPayload", output = "RepeatedField")] + /// [GetFields] event is used to get the grid's settings. + /// + /// The event handler accepts a [QueryFieldPayloadPB] and returns a [RepeatedGridFieldPB] + /// if there are no errors. + #[event(input = "QueryFieldPayloadPB", output = "RepeatedGridFieldPB")] GetFields = 10, - #[event(input = "FieldChangesetPayload")] + /// [UpdateField] event is used to update a field's attributes. + /// + /// The event handler accepts a [FieldChangesetPayloadPB] and returns errors if failed to modify the + /// field. + #[event(input = "FieldChangesetPayloadPB")] UpdateField = 11, - #[event(input = "UpdateFieldTypeOptionPayload")] + /// [UpdateFieldTypeOption] event is used to update the field's type option data. Certain field + /// types have user-defined options such as color, date format, number format, or a list of values + /// for a multi-select list. These options are defined within a specialization of the + /// FieldTypeOption class. + /// + /// Check out [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid#fieldtype) + /// for more information. + /// + /// The event handler accepts a [UpdateFieldTypeOptionPayloadPB] and returns errors if failed to modify the + /// field. + #[event(input = "UpdateFieldTypeOptionPayloadPB")] UpdateFieldTypeOption = 12, - #[event(input = "InsertFieldPayload")] + /// [InsertField] event is used to insert a new Field. If the Field already exists, the event + /// handler will replace the value with the new Field value. + #[event(input = "InsertFieldPayloadPB")] InsertField = 13, - #[event(input = "FieldIdentifierPayload")] + /// [DeleteField] event is used to delete a Field. [DeleteFieldPayloadPB] is the context that + /// is used to delete the field from the Grid. + #[event(input = "DeleteFieldPayloadPB")] DeleteField = 14, - #[event(input = "EditFieldPayload", output = "FieldTypeOptionData")] + /// [SwitchToField] event is used to update the current Field's type. + /// It will insert a new FieldTypeOptionData if the new FieldType doesn't exist before, otherwise + /// reuse the existing FieldTypeOptionData. You could check the [GridRevisionPad] for more details. + #[event(input = "EditFieldPayloadPB", output = "FieldTypeOptionDataPB")] SwitchToField = 20, - #[event(input = "FieldIdentifierPayload")] + /// [DuplicateField] event is used to duplicate a Field. The duplicated field data is kind of + /// deep copy of the target field. The passed in [DuplicateFieldPayloadPB] is the context that is + /// used to duplicate the field. + /// + /// Return errors if failed to duplicate the field. + /// + #[event(input = "DuplicateFieldPayloadPB")] DuplicateField = 21, - #[event(input = "MoveItemPayload")] + /// [MoveItem] event is used to move an item. For the moment, Item has two types defined in + /// [MoveItemTypePB]. + #[event(input = "MoveItemPayloadPB")] MoveItem = 22, - #[event(input = "EditFieldPayload", output = "FieldTypeOptionData")] + /// [GetFieldTypeOption] event is used to get the FieldTypeOption data for a specific field type. + /// + /// Check out the [FieldTypeOptionDataPB] for more details. If the [FieldTypeOptionData] does exist + /// for the target type, the [TypeOptionBuilder] will create the default data for that type. + /// + /// Return the [FieldTypeOptionDataPB] if there are no errors. + #[event(input = "GridFieldTypeOptionIdPB", output = "FieldTypeOptionDataPB")] GetFieldTypeOption = 23, - #[event(input = "EditFieldPayload", output = "FieldTypeOptionData")] + /// [CreateFieldTypeOption] event is used to create a new FieldTypeOptionData. + #[event(input = "CreateFieldPayloadPB", output = "FieldTypeOptionDataPB")] CreateFieldTypeOption = 24, - #[event(input = "CreateSelectOptionPayload", output = "SelectOption")] + /// [NewSelectOption] event is used to create a new select option. Returns a [SelectOptionPB] if + /// there are no errors. + #[event(input = "CreateSelectOptionPayloadPB", output = "SelectOptionPB")] NewSelectOption = 30, - #[event(input = "CellIdentifierPayload", output = "SelectOptionCellData")] + /// [GetSelectOptionCellData] event is used to get the select option data for cell editing. + /// [GridCellIdPB] locate which cell data that will be read from. The return value, [SelectOptionCellDataPB] + /// contains the available options and the currently selected options. + #[event(input = "GridCellIdPB", output = "SelectOptionCellDataPB")] GetSelectOptionCellData = 31, - #[event(input = "SelectOptionChangesetPayload")] + /// [UpdateSelectOption] event is used to update a FieldTypeOptionData whose field_type is + /// FieldType::SingleSelect or FieldType::MultiSelect. + /// + /// This event may trigger the GridNotification::DidUpdateCell event. + /// For example, GridNotification::DidUpdateCell will be triggered if the [SelectOptionChangesetPayloadPB] + /// carries a change that updates the name of the option. + #[event(input = "SelectOptionChangesetPayloadPB")] UpdateSelectOption = 32, - #[event(input = "CreateRowPayload", output = "Row")] + #[event(input = "CreateRowPayloadPB", output = "GridRowPB")] CreateRow = 50, - #[event(input = "GridRowIdPayload", output = "OptionalRow")] + /// [GetRow] event is used to get the row data,[GridRowPB]. [OptionalRowPB] is a wrapper that enables + /// to return a nullable row data. + #[event(input = "GridRowIdPB", output = "OptionalRowPB")] GetRow = 51, - #[event(input = "GridRowIdPayload")] + #[event(input = "GridRowIdPB")] DeleteRow = 52, - #[event(input = "GridRowIdPayload")] + #[event(input = "GridRowIdPB")] DuplicateRow = 53, - #[event(input = "CellIdentifierPayload", output = "Cell")] + #[event(input = "GridCellIdPB", output = "GridCellPB")] GetCell = 70, - #[event(input = "CellChangeset")] + /// [UpdateCell] event is used to update the cell content. The passed in data, [CellChangesetPB], + /// carries the changes that will be applied to the cell content by calling `update_cell` function. + /// + /// The 'content' property of the [CellChangesetPB] is a String type. It can be used directly if the + /// cell uses string data. For example, the TextCell or NumberCell. + /// + /// But,it can be treated as a generic type, because we can use [serde] to deserialize the string + /// into a specific data type. For the moment, the 'content' will be deserialized to a concrete type + /// when the FieldType is SingleSelect, DateTime, and MultiSelect. Please see + /// the [UpdateSelectOptionCell] and [UpdateDateCell] events for more details. + #[event(input = "CellChangesetPB")] UpdateCell = 71, - #[event(input = "SelectOptionCellChangesetPayload")] + /// [UpdateSelectOptionCell] event is used to update a select option cell's data. [SelectOptionCellChangesetPayloadPB] + /// contains options that will be deleted or inserted. It can be cast to [CellChangesetPB] that + /// will be used by the `update_cell` function. + #[event(input = "SelectOptionCellChangesetPayloadPB")] UpdateSelectOptionCell = 72, - #[event(input = "DateChangesetPayload")] + /// [UpdateDateCell] event is used to update a date cell's data. [DateChangesetPayloadPB] + /// contains the date and the time string. It can be cast to [CellChangesetPB] that + /// will be used by the `update_cell` function. + #[event(input = "DateChangesetPayloadPB")] UpdateDateCell = 80, } diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index 28fa7b0c9e..19a3669c46 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/frontend/rust-lib/flowy-grid/src/manager.rs @@ -1,6 +1,8 @@ -use crate::services::grid_editor::GridRevisionEditor; +use crate::services::block_revision_editor::GridBlockRevisionCompactor; +use crate::services::grid_editor::{GridRevisionCompactor, GridRevisionEditor}; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::persistence::kv::GridKVPersistence; +use crate::services::persistence::migration::GridMigration; use crate::services::persistence::GridDatabase; use crate::services::tasks::GridTaskScheduler; use bytes::Bytes; @@ -8,9 +10,9 @@ use dashmap::DashMap; use flowy_database::ConnectionPool; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{BuildGridContext, GridRevision}; -use flowy_revision::disk::{SQLiteGridBlockMetaRevisionPersistence, SQLiteGridRevisionPersistence}; -use flowy_revision::{RevisionManager, RevisionPersistence, RevisionWebSocket}; -use flowy_sync::client_grid::{make_block_meta_delta, make_grid_delta}; +use flowy_revision::disk::{SQLiteGridBlockRevisionPersistence, SQLiteGridRevisionPersistence}; +use flowy_revision::{RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence}; +use flowy_sync::client_grid::{make_grid_block_delta, make_grid_delta}; use flowy_sync::entities::revision::{RepeatedRevision, Revision}; use std::sync::Arc; use tokio::sync::RwLock; @@ -30,6 +32,7 @@ pub struct GridManager { #[allow(dead_code)] kv_persistence: Arc, task_scheduler: GridTaskSchedulerRwLock, + migration: GridMigration, } impl GridManager { @@ -40,17 +43,27 @@ impl GridManager { ) -> Self { let grid_editors = Arc::new(DashMap::new()); let kv_persistence = Arc::new(GridKVPersistence::new(database.clone())); - let block_index_cache = Arc::new(BlockIndexCache::new(database)); + let block_index_cache = Arc::new(BlockIndexCache::new(database.clone())); let task_scheduler = GridTaskScheduler::new(); + let migration = GridMigration::new(grid_user.clone(), database); Self { grid_editors, grid_user, kv_persistence, block_index_cache, task_scheduler, + migration, } } + pub async fn initialize_with_new_user(&self, _user_id: &str, _token: &str) -> FlowyResult<()> { + Ok(()) + } + + pub async fn initialize(&self, _user_id: &str, _token: &str) -> FlowyResult<()> { + Ok(()) + } + #[tracing::instrument(level = "debug", skip_all, err)] pub async fn create_grid>(&self, grid_id: T, revisions: RepeatedRevision) -> FlowyResult<()> { let grid_id = grid_id.as_ref(); @@ -61,14 +74,10 @@ impl GridManager { } #[tracing::instrument(level = "debug", skip_all, err)] - pub async fn create_grid_block_meta>( - &self, - block_id: T, - revisions: RepeatedRevision, - ) -> FlowyResult<()> { + pub async fn create_grid_block>(&self, block_id: T, revisions: RepeatedRevision) -> FlowyResult<()> { let block_id = block_id.as_ref(); let db_pool = self.grid_user.db_pool()?; - let rev_manager = self.make_grid_block_meta_rev_manager(block_id, db_pool)?; + let rev_manager = self.make_grid_block_rev_manager(block_id, db_pool)?; let _ = rev_manager.reset_object(revisions).await?; Ok(()) } @@ -77,6 +86,7 @@ impl GridManager { pub async fn open_grid>(&self, grid_id: T) -> FlowyResult> { let grid_id = grid_id.as_ref(); tracing::Span::current().record("grid_id", &grid_id); + let _ = self.migration.migration_grid_if_need(grid_id).await; self.get_or_create_grid_editor(grid_id).await } @@ -113,7 +123,7 @@ impl GridManager { None => { tracing::trace!("Create grid editor with id: {}", grid_id); let db_pool = self.grid_user.db_pool()?; - let editor = self.make_grid_editor(grid_id, db_pool).await?; + let editor = self.make_grid_rev_editor(grid_id, db_pool).await?; if self.grid_editors.contains_key(grid_id) { tracing::warn!("Grid:{} already exists in cache", grid_id); @@ -127,7 +137,7 @@ impl GridManager { } #[tracing::instrument(level = "trace", skip(self, pool), err)] - async fn make_grid_editor( + async fn make_grid_rev_editor( &self, grid_id: &str, pool: Arc, @@ -147,22 +157,22 @@ impl GridManager { pub fn make_grid_rev_manager(&self, grid_id: &str, pool: Arc) -> FlowyResult { let user_id = self.grid_user.user_id()?; - - let disk_cache = Arc::new(SQLiteGridRevisionPersistence::new(&user_id, pool)); - let rev_persistence = Arc::new(RevisionPersistence::new(&user_id, grid_id, disk_cache)); - let rev_manager = RevisionManager::new(&user_id, grid_id, rev_persistence); + let disk_cache = SQLiteGridRevisionPersistence::new(&user_id, pool.clone()); + let rev_persistence = RevisionPersistence::new(&user_id, grid_id, disk_cache); + let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(grid_id, pool); + let rev_compactor = GridRevisionCompactor(); + let rev_manager = RevisionManager::new(&user_id, grid_id, rev_persistence, rev_compactor, snapshot_persistence); Ok(rev_manager) } - fn make_grid_block_meta_rev_manager( - &self, - block_d: &str, - pool: Arc, - ) -> FlowyResult { + fn make_grid_block_rev_manager(&self, block_id: &str, pool: Arc) -> FlowyResult { let user_id = self.grid_user.user_id()?; - let disk_cache = Arc::new(SQLiteGridBlockMetaRevisionPersistence::new(&user_id, pool)); - let rev_persistence = Arc::new(RevisionPersistence::new(&user_id, block_d, disk_cache)); - let rev_manager = RevisionManager::new(&user_id, block_d, rev_persistence); + let disk_cache = SQLiteGridBlockRevisionPersistence::new(&user_id, pool.clone()); + let rev_persistence = RevisionPersistence::new(&user_id, block_id, disk_cache); + let rev_compactor = GridBlockRevisionCompactor(); + let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(block_id, pool); + let rev_manager = + RevisionManager::new(&user_id, block_id, rev_persistence, rev_compactor, snapshot_persistence); Ok(rev_manager) } } @@ -181,20 +191,18 @@ pub async fn make_grid_view_data( }); // Create grid's block - let grid_block_meta_delta = make_block_meta_delta(block_meta_data); - let block_meta_delta_data = grid_block_meta_delta.to_delta_bytes(); + let grid_block_delta = make_grid_block_delta(block_meta_data); + let block_delta_data = grid_block_delta.json_bytes(); let repeated_revision: RepeatedRevision = - Revision::initial_revision(user_id, block_id, block_meta_delta_data).into(); - let _ = grid_manager - .create_grid_block_meta(&block_id, repeated_revision) - .await?; + Revision::initial_revision(user_id, block_id, block_delta_data).into(); + let _ = grid_manager.create_grid_block(&block_id, repeated_revision).await?; } let grid_rev = GridRevision::from_build_context(view_id, build_context); // Create grid let grid_meta_delta = make_grid_delta(&grid_rev); - let grid_delta_data = grid_meta_delta.to_delta_bytes(); + let grid_delta_data = grid_meta_delta.json_bytes(); let repeated_revision: RepeatedRevision = Revision::initial_revision(user_id, view_id, grid_delta_data.clone()).into(); let _ = grid_manager.create_grid(view_id, repeated_revision).await?; diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index a905a9640b..c83bfcc2cd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -1,7 +1,7 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; -use crate::entities::{CellChangeset, GridBlockChangeset, InsertedRow, Row, RowInfo, UpdatedRow}; +use crate::entities::{CellChangesetPB, GridBlockChangesetPB, GridRowPB, InsertedRowPB, UpdatedRowPB}; use crate::manager::GridUser; -use crate::services::block_revision_editor::GridBlockRevisionEditor; +use crate::services::block_revision_editor::{GridBlockRevisionCompactor, GridBlockRevisionEditor}; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::row::{block_from_row_orders, GridBlockSnapshot}; use dashmap::DashMap; @@ -9,8 +9,8 @@ use flowy_error::FlowyResult; use flowy_grid_data_model::revision::{ GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision, }; -use flowy_revision::disk::SQLiteGridBlockMetaRevisionPersistence; -use flowy_revision::{RevisionManager, RevisionPersistence}; +use flowy_revision::disk::SQLiteGridBlockRevisionPersistence; +use flowy_revision::{RevisionManager, RevisionPersistence, SQLiteRevisionSnapshotPersistence}; use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; @@ -71,12 +71,12 @@ impl GridBlockManager { let _ = self.persistence.insert(&row_rev.block_id, &row_rev.id)?; let editor = self.get_editor(&row_rev.block_id).await?; - let mut index_row_order = InsertedRow::from(&row_rev); + let mut index_row_order = InsertedRowPB::from(&row_rev); let (row_count, row_index) = editor.create_row(row_rev, start_row_id).await?; index_row_order.index = row_index; let _ = self - .notify_did_update_block(block_id, GridBlockChangeset::insert(block_id, vec![index_row_order])) + .notify_did_update_block(block_id, GridBlockChangesetPB::insert(block_id, vec![index_row_order])) .await?; Ok(row_count) } @@ -92,7 +92,7 @@ impl GridBlockManager { let mut row_count = 0; for row in row_revs { let _ = self.persistence.insert(&row.block_id, &row.id)?; - let mut row_order = InsertedRow::from(&row); + let mut row_order = InsertedRowPB::from(&row); let (count, index) = editor.create_row(row, None).await?; row_count = count; row_order.index = index; @@ -101,7 +101,7 @@ impl GridBlockManager { changesets.push(GridBlockMetaRevisionChangeset::from_row_count(&block_id, row_count)); let _ = self - .notify_did_update_block(&block_id, GridBlockChangeset::insert(&block_id, inserted_row_orders)) + .notify_did_update_block(&block_id, GridBlockChangesetPB::insert(&block_id, inserted_row_orders)) .await?; } @@ -110,7 +110,7 @@ impl GridBlockManager { pub async fn update_row(&self, changeset: RowMetaChangeset, row_builder: F) -> FlowyResult<()> where - F: FnOnce(Arc) -> Option, + F: FnOnce(Arc) -> Option, { let editor = self.get_editor_from_row_id(&changeset.row_id).await?; let _ = editor.update_row(changeset.clone()).await?; @@ -118,8 +118,8 @@ impl GridBlockManager { None => tracing::error!("Internal error: can't find the row with id: {}", changeset.row_id), Some(row_rev) => { if let Some(row) = row_builder(row_rev.clone()) { - let row_order = UpdatedRow::new(&row_rev, row); - let block_order_changeset = GridBlockChangeset::update(&editor.block_id, vec![row_order]); + let row_order = UpdatedRowPB::new(&row_rev, row); + let block_order_changeset = GridBlockChangesetPB::update(&editor.block_id, vec![row_order]); let _ = self .notify_did_update_block(&editor.block_id, block_order_changeset) .await?; @@ -138,7 +138,7 @@ impl GridBlockManager { Some(row_info) => { let _ = editor.delete_rows(vec![Cow::Borrowed(&row_id)]).await?; let _ = self - .notify_did_update_block(&block_id, GridBlockChangeset::delete(&block_id, vec![row_info.row_id])) + .notify_did_update_block(&block_id, GridBlockChangesetPB::delete(&block_id, vec![row_info.id])) .await?; } } @@ -148,13 +148,13 @@ impl GridBlockManager { pub(crate) async fn delete_rows( &self, - row_orders: Vec, + row_orders: Vec, ) -> FlowyResult> { let mut changesets = vec![]; for grid_block in block_from_row_orders(row_orders) { let editor = self.get_editor(&grid_block.id).await?; let row_ids = grid_block - .row_infos + .rows .into_iter() .map(|row_info| Cow::Owned(row_info.row_id().to_owned())) .collect::>>(); @@ -173,14 +173,14 @@ impl GridBlockManager { match editor.get_row_revs(Some(vec![Cow::Borrowed(row_id)])).await?.pop() { None => {} Some(row_rev) => { - let insert_row = InsertedRow { + let insert_row = InsertedRowPB { block_id: row_rev.block_id.clone(), row_id: row_rev.id.clone(), index: Some(to as i32), height: row_rev.height, }; - let notified_changeset = GridBlockChangeset { + let notified_changeset = GridBlockChangesetPB { block_id: editor.block_id.clone(), inserted_rows: vec![insert_row], deleted_rows: vec![row_rev.id.clone()], @@ -196,9 +196,9 @@ impl GridBlockManager { Ok(()) } - pub async fn update_cell(&self, changeset: CellChangeset, row_builder: F) -> FlowyResult<()> + pub async fn update_cell(&self, changeset: CellChangesetPB, row_builder: F) -> FlowyResult<()> where - F: FnOnce(Arc) -> Option, + F: FnOnce(Arc) -> Option, { let row_changeset: RowMetaChangeset = changeset.clone().into(); let _ = self.update_row(row_changeset, row_builder).await?; @@ -217,7 +217,7 @@ impl GridBlockManager { } } - pub async fn get_row_orders(&self, block_id: &str) -> FlowyResult> { + pub async fn get_row_orders(&self, block_id: &str) -> FlowyResult> { let editor = self.get_editor(block_id).await?; editor.get_row_infos::<&str>(None).await } @@ -247,14 +247,14 @@ impl GridBlockManager { Ok(snapshots) } - async fn notify_did_update_block(&self, block_id: &str, changeset: GridBlockChangeset) -> FlowyResult<()> { + async fn notify_did_update_block(&self, block_id: &str, changeset: GridBlockChangesetPB) -> FlowyResult<()> { send_dart_notification(block_id, GridNotification::DidUpdateGridBlock) .payload(changeset) .send(); Ok(()) } - async fn notify_did_update_cell(&self, changeset: CellChangeset) -> FlowyResult<()> { + async fn notify_did_update_cell(&self, changeset: CellChangesetPB) -> FlowyResult<()> { let id = format!("{}:{}", changeset.row_id, changeset.field_id); send_dart_notification(&id, GridNotification::DidUpdateCell).send(); Ok(()) @@ -280,8 +280,10 @@ async fn make_block_editor(user: &Arc, block_id: &str) -> FlowyRes let user_id = user.user_id()?; let pool = user.db_pool()?; - let disk_cache = Arc::new(SQLiteGridBlockMetaRevisionPersistence::new(&user_id, pool)); - let rev_persistence = Arc::new(RevisionPersistence::new(&user_id, block_id, disk_cache)); - let rev_manager = RevisionManager::new(&user_id, block_id, rev_persistence); + let disk_cache = SQLiteGridBlockRevisionPersistence::new(&user_id, pool.clone()); + let rev_persistence = RevisionPersistence::new(&user_id, block_id, disk_cache); + let rev_compactor = GridBlockRevisionCompactor(); + let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(block_id, pool); + let rev_manager = RevisionManager::new(&user_id, block_id, rev_persistence, rev_compactor, snapshot_persistence); GridBlockRevisionEditor::new(&user_id, &token, block_id, rev_manager).await } diff --git a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs b/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs index e73a66c0fa..a9f68c5776 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_revision_editor.rs @@ -1,4 +1,4 @@ -use crate::entities::RowInfo; +use crate::entities::GridRowPB; use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision}; @@ -7,7 +7,7 @@ use flowy_sync::client_grid::{GridBlockMetaChange, GridBlockRevisionPad}; use flowy_sync::entities::revision::Revision; use flowy_sync::util::make_delta_from_revisions; use lib_infra::future::FutureResult; -use lib_ot::core::PlainTextAttributes; +use lib_ot::core::PhantomAttributes; use std::borrow::Cow; use std::sync::Arc; use tokio::sync::RwLock; @@ -26,7 +26,7 @@ impl GridBlockRevisionEditor { block_id: &str, mut rev_manager: RevisionManager, ) -> FlowyResult { - let cloud = Arc::new(GridBlockMetaRevisionCloudService { + let cloud = Arc::new(GridBlockRevisionCloudService { token: token.to_owned(), }); let block_meta_pad = rev_manager.load::(Some(cloud)).await?; @@ -123,12 +123,12 @@ impl GridBlockRevisionEditor { Ok(cell_revs) } - pub async fn get_row_info(&self, row_id: &str) -> FlowyResult> { + pub async fn get_row_info(&self, row_id: &str) -> FlowyResult> { let row_ids = Some(vec![Cow::Borrowed(row_id)]); Ok(self.get_row_infos(row_ids).await?.pop()) } - pub async fn get_row_infos(&self, row_ids: Option>>) -> FlowyResult> + pub async fn get_row_infos(&self, row_ids: Option>>) -> FlowyResult> where T: AsRef + ToOwned + ?Sized, { @@ -138,8 +138,8 @@ impl GridBlockRevisionEditor { .await .get_row_revs(row_ids)? .iter() - .map(RowInfo::from) - .collect::>(); + .map(GridRowPB::from) + .collect::>(); Ok(row_infos) } @@ -161,7 +161,7 @@ impl GridBlockRevisionEditor { let GridBlockMetaChange { delta, md5 } = change; let user_id = self.user_id.clone(); let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); - let delta_data = delta.to_delta_bytes(); + let delta_data = delta.json_bytes(); let revision = Revision::new( &self.rev_manager.object_id, base_rev_id, @@ -170,20 +170,17 @@ impl GridBlockRevisionEditor { &user_id, md5, ); - let _ = self - .rev_manager - .add_local_revision(&revision, Box::new(GridBlockMetaRevisionCompactor())) - .await?; + let _ = self.rev_manager.add_local_revision(&revision).await?; Ok(()) } } -struct GridBlockMetaRevisionCloudService { +struct GridBlockRevisionCloudService { #[allow(dead_code)] token: String, } -impl RevisionCloudService for GridBlockMetaRevisionCloudService { +impl RevisionCloudService for GridBlockRevisionCloudService { #[tracing::instrument(level = "trace", skip(self))] fn fetch_object(&self, _user_id: &str, _object_id: &str) -> FutureResult, FlowyError> { FutureResult::new(async move { Ok(vec![]) }) @@ -200,10 +197,10 @@ impl RevisionObjectBuilder for GridBlockMetaPadBuilder { } } -struct GridBlockMetaRevisionCompactor(); -impl RevisionCompactor for GridBlockMetaRevisionCompactor { +pub struct GridBlockRevisionCompactor(); +impl RevisionCompactor for GridBlockRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { - let delta = make_delta_from_revisions::(revisions)?; - Ok(delta.to_delta_bytes()) + let delta = make_delta_from_revisions::(revisions)?; + Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs b/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs index 259fcc79ef..8ebffcbedc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs @@ -1,9 +1,11 @@ use crate::entities::FieldType; +use crate::services::cell::{CellData, FromCellString}; use bytes::Bytes; use flowy_error::{internal_error, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::CellRevision; use serde::{Deserialize, Serialize}; use std::str::FromStr; + /// AnyCellData is a generic CellData, you can parse the cell_data according to the field_type. /// When the type of field is changed, it's different from the field_type of AnyCellData. /// So it will return an empty data. You could check the CellDataOperation trait for more information. @@ -46,6 +48,15 @@ impl std::convert::TryFrom for AnyCellData { } } +impl std::convert::From for CellData +where + T: FromCellString, +{ + fn from(any_call_data: AnyCellData) -> Self { + CellData::from(any_call_data.data) + } +} + impl AnyCellData { pub fn new(content: String, field_type: FieldType) -> Self { AnyCellData { @@ -100,36 +111,45 @@ impl AnyCellData { /// * Use String to parse the data when the FieldType is RichText, Number, or Checkbox. /// * Check out the implementation of CellDataOperation trait for more information. #[derive(Default)] -pub struct DecodedCellData { - pub data: Vec, +pub struct CellBytes(pub Bytes); + +pub trait CellBytesParser { + type Object; + fn parse(&self, bytes: &Bytes) -> FlowyResult; } -impl DecodedCellData { +impl CellBytes { pub fn new>(data: T) -> Self { - Self { - data: data.as_ref().to_vec(), - } + let bytes = Bytes::from(data.as_ref().to_vec()); + Self(bytes) } - pub fn try_from_bytes>(bytes: T) -> FlowyResult + pub fn from>(bytes: T) -> FlowyResult where >::Error: std::fmt::Debug, { let bytes = bytes.try_into().map_err(internal_error)?; - Ok(Self { data: bytes.to_vec() }) + Ok(Self(bytes)) } - pub fn parse<'a, T: TryFrom<&'a [u8]>>(&'a self) -> FlowyResult + pub fn with_parser

(&self, parser: P) -> FlowyResult where - >::Error: std::fmt::Debug, + P: CellBytesParser, { - T::try_from(self.data.as_ref()).map_err(internal_error) + parser.parse(&self.0) } + + // pub fn parse<'a, T: TryFrom<&'a [u8]>>(&'a self) -> FlowyResult + // where + // >::Error: std::fmt::Debug, + // { + // T::try_from(self.0.as_ref()).map_err(internal_error) + // } } -impl ToString for DecodedCellData { +impl ToString for CellBytes { fn to_string(&self) -> String { - match String::from_utf8(self.data.clone()) { + match String::from_utf8(self.0.to_vec()) { Ok(s) => s, Err(e) => { tracing::error!("DecodedCellData to string failed: {:?}", e); @@ -138,3 +158,11 @@ impl ToString for DecodedCellData { } } } + +impl std::ops::Deref for CellBytes { + type Target = Bytes; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index 5b74c26aa4..e34f3a8430 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -1,33 +1,51 @@ +use crate::entities::FieldType; +use crate::services::cell::{AnyCellData, CellBytes}; +use crate::services::field::*; + use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, FieldTypeRevision}; -use crate::entities::FieldType; -use crate::services::cell::{AnyCellData, DecodedCellData}; -use crate::services::field::*; - +/// This trait is used when doing filter/search on the grid. pub trait CellFilterOperation { /// Return true if any_cell_data match the filter condition. fn apply_filter(&self, any_cell_data: AnyCellData, filter: &T) -> FlowyResult; } -pub trait CellDataOperation { - /// The cell_data is able to parse into the specific data that was impl the FromCellData trait. +/// Return object that describes the cell. +pub trait CellDisplayable { + fn display_data( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult; +} + +// CD: Short for CellData. This type is the type return by apply_changeset function. +// CS: Short for Changeset. Parse the string into specific Changeset type. +pub trait CellDataOperation { + /// The cell_data is able to parse into the specific data if CD impl the FromCellData trait. /// For example: /// URLCellData, DateCellData. etc. fn decode_cell_data( &self, - cell_data: CellData, + cell_data: CellData, decoded_field_type: &FieldType, field_rev: &FieldRevision, - ) -> FlowyResult; + ) -> FlowyResult; - /// The changeset is able to parse into the specific data that was impl the FromCellChangeset trait. + /// The changeset is able to parse into the specific data if CS impl the FromCellChangeset trait. /// For example: /// SelectOptionCellChangeset,DateCellChangeset. etc. - fn apply_changeset(&self, changeset: CellDataChangeset, cell_rev: Option) -> FlowyResult; + fn apply_changeset(&self, changeset: CellDataChangeset, cell_rev: Option) -> FlowyResult; } -/// The changeset will be deserialized into specific data base on the FieldType. -/// For example, it's String on FieldType::RichText, and SelectOptionChangeset on FieldType::SingleSelect + +/// changeset: It will be deserialized into specific data base on the FieldType. +/// For example, +/// FieldType::RichText => String +/// FieldType::SingleSelect => SelectOptionChangeset +/// +/// cell_rev: It will be None if the cell does not contain any data. pub fn apply_cell_data_changeset>( changeset: C, cell_rev: Option, @@ -40,7 +58,9 @@ pub fn apply_cell_data_changeset>( FieldType::RichText => RichTextTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), FieldType::Number => NumberTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), FieldType::DateTime => DateTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), - FieldType::SingleSelect => SingleSelectTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), + FieldType::SingleSelect => { + SingleSelectTypeOptionPB::from(field_rev).apply_changeset(changeset.into(), cell_rev) + } FieldType::MultiSelect => MultiSelectTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), FieldType::Checkbox => CheckboxTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), FieldType::URL => URLTypeOption::from(field_rev).apply_changeset(changeset.into(), cell_rev), @@ -49,23 +69,20 @@ pub fn apply_cell_data_changeset>( Ok(AnyCellData::new(s, field_type).json()) } -pub fn decode_any_cell_data>(data: T, field_rev: &FieldRevision) -> DecodedCellData { +pub fn decode_any_cell_data>(data: T, field_rev: &FieldRevision) -> CellBytes { if let Ok(any_cell_data) = data.try_into() { - let AnyCellData { - data: cell_data, - field_type, - } = any_cell_data; + let AnyCellData { data, field_type } = any_cell_data; let to_field_type = field_rev.field_type_rev.into(); - match try_decode_cell_data(CellData(Some(cell_data)), field_rev, &field_type, &to_field_type) { - Ok(cell_data) => cell_data, + match try_decode_cell_data(data.into(), field_rev, &field_type, &to_field_type) { + Ok(cell_bytes) => cell_bytes, Err(e) => { tracing::error!("Decode cell data failed, {:?}", e); - DecodedCellData::default() + CellBytes::default() } } } else { tracing::error!("Decode type option data failed"); - DecodedCellData::default() + CellBytes::default() } } @@ -74,7 +91,7 @@ pub fn try_decode_cell_data( field_rev: &FieldRevision, s_field_type: &FieldType, t_field_type: &FieldType, -) -> FlowyResult { +) -> FlowyResult { let cell_data = cell_data.try_into_inner()?; let get_cell_data = || { let field_type: FieldTypeRevision = t_field_type.into(); @@ -89,7 +106,7 @@ pub fn try_decode_cell_data( .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::SingleSelect => field_rev - .get_type_option_entry::(field_type)? + .get_type_option_entry::(field_type)? .decode_cell_data(cell_data.into(), s_field_type, field_rev), FieldType::MultiSelect => field_rev .get_type_option_entry::(field_type)? @@ -108,20 +125,22 @@ pub fn try_decode_cell_data( Some(Ok(data)) => Ok(data), Some(Err(err)) => { tracing::error!("{:?}", err); - Ok(DecodedCellData::default()) + Ok(CellBytes::default()) } - None => Ok(DecodedCellData::default()), + None => Ok(CellBytes::default()), } } +/// If the cell data is not String type, it should impl this trait. +/// Deserialize the String into cell specific data type. pub trait FromCellString { fn from_cell_str(s: &str) -> FlowyResult where Self: Sized; } +/// CellData is a helper struct. String will be parser into Option only if the T impl the FromCellString trait. pub struct CellData(pub Option); - impl CellData { pub fn try_into_inner(self) -> FlowyResult { match self.0 { @@ -146,9 +165,9 @@ where } } -impl std::convert::From for CellData { - fn from(s: String) -> Self { - CellData(Some(s)) +impl std::convert::From for CellData { + fn from(val: T) -> Self { + CellData(Some(val)) } } @@ -158,7 +177,8 @@ impl std::convert::From> for String { } } -// CellChangeset +/// If the changeset applying to the cell is not String type, it should impl this trait. +/// Deserialize the string into cell specific changeset. pub trait FromCellChangeset { fn from_changeset(changeset: String) -> FlowyResult where diff --git a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs index 352d3e7f5c..de1f37b04f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/field_builder.rs @@ -1,4 +1,4 @@ -use crate::entities::{Field, FieldType}; +use crate::entities::{FieldType, GridFieldPB}; use crate::services::field::type_options::*; use bytes::Bytes; use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry}; @@ -28,7 +28,7 @@ impl FieldBuilder { Self::new(type_option_builder) } - pub fn from_field(field: Field, type_option_builder: Box) -> Self { + pub fn from_field(field: GridFieldPB, type_option_builder: Box) -> Self { let field_rev = FieldRevision { id: field.id, name: field.name, @@ -93,7 +93,7 @@ pub fn default_type_option_builder_from_type(field_type: &FieldType) -> Box RichTextTypeOption::default().into(), FieldType::Number => NumberTypeOption::default().into(), FieldType::DateTime => DateTypeOption::default().into(), - FieldType::SingleSelect => SingleSelectTypeOption::default().into(), + FieldType::SingleSelect => SingleSelectTypeOptionPB::default().into(), FieldType::MultiSelect => MultiSelectTypeOption::default().into(), FieldType::Checkbox => CheckboxTypeOption::default().into(), FieldType::URL => URLTypeOption::default().into(), diff --git a/frontend/rust-lib/flowy-grid/src/services/field/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/mod.rs index 8a01d90d1f..c1b689fbf4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/mod.rs @@ -1,5 +1,4 @@ mod field_builder; -pub mod select_option; pub(crate) mod type_options; pub use field_builder::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option.rs deleted file mode 100644 index 1808f0dc82..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option.rs +++ /dev/null @@ -1,133 +0,0 @@ -use crate::entities::FieldType; -use crate::impl_type_option; -use crate::services::cell::{AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData}; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; -use bytes::Bytes; -use flowy_derive::ProtoBuf; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; -use serde::{Deserialize, Serialize}; - -#[derive(Default)] -pub struct CheckboxTypeOptionBuilder(CheckboxTypeOption); -impl_into_box_type_option_builder!(CheckboxTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(CheckboxTypeOptionBuilder, CheckboxTypeOption); - -impl CheckboxTypeOptionBuilder { - pub fn set_selected(mut self, is_selected: bool) -> Self { - self.0.is_selected = is_selected; - self - } -} - -impl TypeOptionBuilder for CheckboxTypeOptionBuilder { - fn field_type(&self) -> FieldType { - FieldType::Checkbox - } - - fn entry(&self) -> &dyn TypeOptionDataEntry { - &self.0 - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] -pub struct CheckboxTypeOption { - #[pb(index = 1)] - pub is_selected: bool, -} -impl_type_option!(CheckboxTypeOption, FieldType::Checkbox); - -const YES: &str = "Yes"; -const NO: &str = "No"; - -impl CellDataOperation for CheckboxTypeOption { - fn decode_cell_data( - &self, - cell_data: CellData, - decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { - if !decoded_field_type.is_checkbox() { - return Ok(DecodedCellData::default()); - } - - let s: String = cell_data.try_into_inner()?; - if s == YES || s == NO { - return Ok(DecodedCellData::new(s)); - } - - Ok(DecodedCellData::default()) - } - - fn apply_changeset( - &self, - changeset: CellDataChangeset, - _cell_rev: Option, - ) -> Result { - let changeset = changeset.try_into_inner()?; - let s = match string_to_bool(&changeset) { - true => YES, - false => NO, - }; - Ok(s.to_string()) - } -} - -fn string_to_bool(bool_str: &str) -> bool { - let lower_case_str: &str = &bool_str.to_lowercase(); - match lower_case_str { - "1" => true, - "true" => true, - "yes" => true, - "0" => false, - "false" => false, - "no" => false, - _ => false, - } -} - -pub struct CheckboxCellData(pub String); - -impl CheckboxCellData { - pub fn is_check(&self) -> bool { - string_to_bool(&self.0) - } -} -impl std::convert::TryFrom for CheckboxCellData { - type Error = FlowyError; - - fn try_from(_value: AnyCellData) -> Result { - todo!() - } -} - -#[cfg(test)] -mod tests { - use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data}; - use crate::services::field::type_options::checkbox_type_option::{NO, YES}; - use crate::services::field::FieldBuilder; - - use crate::entities::FieldType; - - #[test] - fn checkout_box_description_test() { - let field_rev = FieldBuilder::from_field_type(&FieldType::Checkbox).build(); - let data = apply_cell_data_changeset("true", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev).to_string(), YES); - - let data = apply_cell_data_changeset("1", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES); - - let data = apply_cell_data_changeset("yes", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES); - - let data = apply_cell_data_changeset("false", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); - - let data = apply_cell_data_changeset("no", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); - - let data = apply_cell_data_changeset("12", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs new file mode 100644 index 0000000000..ddd1ba049d --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs @@ -0,0 +1,44 @@ +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::cell::CellDataOperation; + use crate::services::field::type_options::checkbox_type_option::*; + use crate::services::field::FieldBuilder; + use flowy_grid_data_model::revision::FieldRevision; + + #[test] + fn checkout_box_description_test() { + let type_option = CheckboxTypeOption::default(); + let field_type = FieldType::Checkbox; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + // the checkout value will be checked if the value is "1", "true" or "yes" + assert_checkbox(&type_option, "1", CHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "true", CHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "yes", CHECK, &field_type, &field_rev); + + // the checkout value will be uncheck if the value is "false" or "No" + assert_checkbox(&type_option, "false", UNCHECK, &field_type, &field_rev); + assert_checkbox(&type_option, "No", UNCHECK, &field_type, &field_rev); + + // the checkout value will be empty if the value is letters or empty string + assert_checkbox(&type_option, "abc", "", &field_type, &field_rev); + assert_checkbox(&type_option, "", "", &field_type, &field_rev); + } + + fn assert_checkbox( + type_option: &CheckboxTypeOption, + input_str: &str, + expected_str: &str, + field_type: &FieldType, + field_rev: &FieldRevision, + ) { + assert_eq!( + type_option + .decode_cell_data(input_str.to_owned().into(), field_type, field_rev) + .unwrap() + .to_string(), + expected_str.to_owned() + ); + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs new file mode 100644 index 0000000000..155965f409 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs @@ -0,0 +1,76 @@ +use crate::entities::FieldType; +use crate::impl_type_option; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::{BoxTypeOptionBuilder, CheckboxCellData, TypeOptionBuilder}; +use bytes::Bytes; +use flowy_derive::ProtoBuf; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Default)] +pub struct CheckboxTypeOptionBuilder(CheckboxTypeOption); +impl_into_box_type_option_builder!(CheckboxTypeOptionBuilder); +impl_builder_from_json_str_and_from_bytes!(CheckboxTypeOptionBuilder, CheckboxTypeOption); + +impl CheckboxTypeOptionBuilder { + pub fn set_selected(mut self, is_selected: bool) -> Self { + self.0.is_selected = is_selected; + self + } +} + +impl TypeOptionBuilder for CheckboxTypeOptionBuilder { + fn field_type(&self) -> FieldType { + FieldType::Checkbox + } + + fn entry(&self) -> &dyn TypeOptionDataEntry { + &self.0 + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] +pub struct CheckboxTypeOption { + #[pb(index = 1)] + pub is_selected: bool, +} +impl_type_option!(CheckboxTypeOption, FieldType::Checkbox); + +impl CellDisplayable for CheckboxTypeOption { + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_data = cell_data.try_into_inner()?; + Ok(CellBytes::new(cell_data)) + } +} + +impl CellDataOperation for CheckboxTypeOption { + fn decode_cell_data( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult { + if !decoded_field_type.is_checkbox() { + return Ok(CellBytes::default()); + } + + self.display_data(cell_data, decoded_field_type, field_rev) + } + + fn apply_changeset( + &self, + changeset: CellDataChangeset, + _cell_rev: Option, + ) -> Result { + let changeset = changeset.try_into_inner()?; + let cell_data = CheckboxCellData::from_str(&changeset)?; + Ok(cell_data.to_string()) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs new file mode 100644 index 0000000000..3da9ebd23f --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs @@ -0,0 +1,69 @@ +use crate::services::cell::{CellBytesParser, FromCellString}; +use bytes::Bytes; +use flowy_error::{FlowyError, FlowyResult}; +use std::str::FromStr; + +pub const CHECK: &str = "Yes"; +pub const UNCHECK: &str = "No"; + +pub struct CheckboxCellData(String); + +impl CheckboxCellData { + pub fn is_check(&self) -> bool { + self.0 == CHECK + } +} + +impl AsRef<[u8]> for CheckboxCellData { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl FromStr for CheckboxCellData { + type Err = FlowyError; + + fn from_str(s: &str) -> Result { + let lower_case_str: &str = &s.to_lowercase(); + let val = match lower_case_str { + "1" => Some(true), + "true" => Some(true), + "yes" => Some(true), + "0" => Some(false), + "false" => Some(false), + "no" => Some(false), + _ => None, + }; + + match val { + Some(true) => Ok(Self(CHECK.to_string())), + Some(false) => Ok(Self(UNCHECK.to_string())), + None => Ok(Self("".to_string())), + } + } +} + +impl FromCellString for CheckboxCellData { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + Self::from_str(s) + } +} + +impl ToString for CheckboxCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} +pub struct CheckboxCellDataParser(); +impl CellBytesParser for CheckboxCellDataParser { + type Object = CheckboxCellData; + fn parse(&self, bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => CheckboxCellData::from_str(&s), + Err(_) => Ok(CheckboxCellData("".to_string())), + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/mod.rs new file mode 100644 index 0000000000..ebe5d1a6a8 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/mod.rs @@ -0,0 +1,7 @@ +#![allow(clippy::module_inception)] +mod checkbox_tests; +mod checkbox_type_option; +mod checkbox_type_option_entities; + +pub use checkbox_type_option::*; +pub use checkbox_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs deleted file mode 100644 index 2200dad63c..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs +++ /dev/null @@ -1,661 +0,0 @@ -use crate::entities::{CellChangeset, FieldType}; -use crate::entities::{CellIdentifier, CellIdentifierPayload}; -use crate::impl_type_option; -use crate::services::cell::{ - AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData, FromCellChangeset, FromCellString, -}; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; -use bytes::Bytes; -use chrono::format::strftime::StrftimeItems; -use chrono::{NaiveDateTime, Timelike}; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; -use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; -use serde::{Deserialize, Serialize}; -use strum_macros::EnumIter; - -// Date -#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct DateTypeOption { - #[pb(index = 1)] - pub date_format: DateFormat, - - #[pb(index = 2)] - pub time_format: TimeFormat, - - #[pb(index = 3)] - pub include_time: bool, -} -impl_type_option!(DateTypeOption, FieldType::DateTime); - -impl DateTypeOption { - #[allow(dead_code)] - pub fn new() -> Self { - Self::default() - } - - fn today_desc_from_timestamp(&self, timestamp: i64) -> DateCellData { - let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0); - self.date_from_native(native) - } - - fn date_from_native(&self, native: chrono::NaiveDateTime) -> DateCellData { - if native.timestamp() == 0 { - return DateCellData::default(); - } - - let time = native.time(); - let has_time = time.hour() != 0 || time.second() != 0; - - let utc = self.utc_date_time_from_native(native); - let fmt = self.date_format.format_str(); - let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt))); - - let mut time = "".to_string(); - if has_time { - let fmt = format!("{} {}", self.date_format.format_str(), self.time_format.format_str()); - time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, ""); - } - - let timestamp = native.timestamp(); - DateCellData { date, time, timestamp } - } - - fn date_fmt(&self, time: &Option) -> String { - if self.include_time { - match time.as_ref() { - None => self.date_format.format_str().to_string(), - Some(time_str) => { - if time_str.is_empty() { - self.date_format.format_str().to_string() - } else { - format!("{} {}", self.date_format.format_str(), self.time_format.format_str()) - } - } - } - } else { - self.date_format.format_str().to_string() - } - } - - fn timestamp_from_utc_with_time( - &self, - utc: &chrono::DateTime, - time: &Option, - ) -> FlowyResult { - if let Some(time_str) = time.as_ref() { - if !time_str.is_empty() { - let date_str = format!( - "{}{}", - utc.format_with_items(StrftimeItems::new(self.date_format.format_str())), - &time_str - ); - - return match NaiveDateTime::parse_from_str(&date_str, &self.date_fmt(time)) { - Ok(native) => { - let utc = self.utc_date_time_from_native(native); - Ok(utc.timestamp()) - } - Err(_e) => { - let msg = format!("Parse {} failed", date_str); - Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg)) - } - }; - } - } - - Ok(utc.timestamp()) - } - - fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime { - let native = NaiveDateTime::from_timestamp(timestamp, 0); - let native2 = NaiveDateTime::from_timestamp(timestamp, 0); - - if native > native2 {} - - self.utc_date_time_from_native(native) - } - - fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime { - chrono::DateTime::::from_utc(naive, chrono::Utc) - } -} - -impl CellDataOperation for DateTypeOption { - fn decode_cell_data( - &self, - cell_data: CellData, - decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { - // Return default data if the type_option_cell_data is not FieldType::DateTime. - // It happens when switching from one field to another. - // For example: - // FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen. - if !decoded_field_type.is_date() { - return Ok(DecodedCellData::default()); - } - let timestamp = cell_data.try_into_inner()?; - let date = self.today_desc_from_timestamp(timestamp.0); - DecodedCellData::try_from_bytes(date) - } - - fn apply_changeset( - &self, - changeset: CellDataChangeset, - _cell_rev: Option, - ) -> Result { - let changeset = changeset.try_into_inner()?; - let cell_data = match changeset.date_timestamp() { - None => 0, - Some(date_timestamp) => match (self.include_time, changeset.time) { - (true, Some(time)) => { - let time = Some(time.trim().to_uppercase()); - let utc = self.utc_date_time_from_timestamp(date_timestamp); - self.timestamp_from_utc_with_time(&utc, &time)? - } - _ => date_timestamp, - }, - }; - - Ok(cell_data.to_string()) - } -} - -pub struct DateTimestamp(i64); -impl AsRef for DateTimestamp { - fn as_ref(&self) -> &i64 { - &self.0 - } -} - -impl std::convert::From for i64 { - fn from(timestamp: DateTimestamp) -> Self { - timestamp.0 - } -} - -impl FromCellString for DateTimestamp { - fn from_cell_str(s: &str) -> FlowyResult - where - Self: Sized, - { - let num = s.parse::().unwrap_or(0); - Ok(DateTimestamp(num)) - } -} - -impl std::convert::From for DateTimestamp { - fn from(data: AnyCellData) -> Self { - let num = data.data.parse::().unwrap_or(0); - DateTimestamp(num) - } -} - -#[derive(Default)] -pub struct DateTypeOptionBuilder(DateTypeOption); -impl_into_box_type_option_builder!(DateTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOption); - -impl DateTypeOptionBuilder { - pub fn date_format(mut self, date_format: DateFormat) -> Self { - self.0.date_format = date_format; - self - } - - pub fn time_format(mut self, time_format: TimeFormat) -> Self { - self.0.time_format = time_format; - self - } -} -impl TypeOptionBuilder for DateTypeOptionBuilder { - fn field_type(&self) -> FieldType { - FieldType::DateTime - } - - fn entry(&self) -> &dyn TypeOptionDataEntry { - &self.0 - } -} - -#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)] -pub enum DateFormat { - Local = 0, - US = 1, - ISO = 2, - Friendly = 3, -} -impl std::default::Default for DateFormat { - fn default() -> Self { - DateFormat::Friendly - } -} - -impl std::convert::From for DateFormat { - fn from(value: i32) -> Self { - match value { - 0 => DateFormat::Local, - 1 => DateFormat::US, - 2 => DateFormat::ISO, - 3 => DateFormat::Friendly, - _ => { - tracing::error!("Unsupported date format, fallback to friendly"); - DateFormat::Friendly - } - } - } -} - -impl DateFormat { - pub fn value(&self) -> i32 { - *self as i32 - } - // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html - pub fn format_str(&self) -> &'static str { - match self { - DateFormat::Local => "%Y/%m/%d", - DateFormat::US => "%Y/%m/%d", - DateFormat::ISO => "%Y-%m-%d", - DateFormat::Friendly => "%b %d,%Y", - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, ProtoBuf_Enum)] -pub enum TimeFormat { - TwelveHour = 0, - TwentyFourHour = 1, -} - -impl std::convert::From for TimeFormat { - fn from(value: i32) -> Self { - match value { - 0 => TimeFormat::TwelveHour, - 1 => TimeFormat::TwentyFourHour, - _ => { - tracing::error!("Unsupported time format, fallback to TwentyFourHour"); - TimeFormat::TwentyFourHour - } - } - } -} - -impl TimeFormat { - pub fn value(&self) -> i32 { - *self as i32 - } - - // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html - pub fn format_str(&self) -> &'static str { - match self { - TimeFormat::TwelveHour => "%I:%M %p", - TimeFormat::TwentyFourHour => "%R", - } - } -} - -impl std::default::Default for TimeFormat { - fn default() -> Self { - TimeFormat::TwentyFourHour - } -} - -#[derive(Clone, Debug, Default, ProtoBuf)] -pub struct DateCellData { - #[pb(index = 1)] - pub date: String, - - #[pb(index = 2)] - pub time: String, - - #[pb(index = 3)] - pub timestamp: i64, -} - -#[derive(Clone, Debug, Default, ProtoBuf)] -pub struct DateChangesetPayload { - #[pb(index = 1)] - pub cell_identifier: CellIdentifierPayload, - - #[pb(index = 2, one_of)] - pub date: Option, - - #[pb(index = 3, one_of)] - pub time: Option, -} - -pub struct DateChangesetParams { - pub cell_identifier: CellIdentifier, - pub date: Option, - pub time: Option, -} - -impl TryInto for DateChangesetPayload { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?; - Ok(DateChangesetParams { - cell_identifier, - date: self.date, - time: self.time, - }) - } -} - -impl std::convert::From for CellChangeset { - fn from(params: DateChangesetParams) -> Self { - let changeset = DateCellChangeset { - date: params.date, - time: params.time, - }; - let s = serde_json::to_string(&changeset).unwrap(); - CellChangeset { - grid_id: params.cell_identifier.grid_id, - row_id: params.cell_identifier.row_id, - field_id: params.cell_identifier.field_id, - content: Some(s), - } - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct DateCellChangeset { - pub date: Option, - pub time: Option, -} - -impl DateCellChangeset { - pub fn date_timestamp(&self) -> Option { - if let Some(date) = &self.date { - match date.parse::() { - Ok(date_timestamp) => Some(date_timestamp), - Err(_) => None, - } - } else { - None - } - } -} - -impl FromCellChangeset for DateCellChangeset { - fn from_changeset(changeset: String) -> FlowyResult - where - Self: Sized, - { - serde_json::from_str::(&changeset).map_err(internal_error) - } -} - -#[cfg(test)] -mod tests { - use crate::entities::FieldType; - use crate::services::cell::{CellDataChangeset, CellDataOperation}; - use crate::services::field::FieldBuilder; - use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat}; - use flowy_grid_data_model::revision::FieldRevision; - use strum::IntoEnumIterator; - - #[test] - fn date_type_option_invalid_input_test() { - let type_option = DateTypeOption::default(); - let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some("1e".to_string()), - time: Some("23:00".to_owned()), - }, - &field_type, - &field_rev, - "", - ); - } - - #[test] - fn date_type_option_date_format_test() { - let mut type_option = DateTypeOption::default(); - let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); - for date_format in DateFormat::iter() { - type_option.date_format = date_format; - match date_format { - DateFormat::Friendly => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "Mar 14,2022"); - } - DateFormat::US => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14"); - } - DateFormat::ISO => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022-03-14"); - } - DateFormat::Local => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14"); - } - } - } - } - - #[test] - fn date_type_option_time_format_test() { - let mut type_option = DateTypeOption::default(); - let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - - for time_format in TimeFormat::iter() { - type_option.time_format = time_format; - type_option.include_time = true; - match time_format { - TimeFormat::TwentyFourHour => { - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: Some("23:00".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022 23:00", - ); - } - TimeFormat::TwelveHour => { - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - // - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: Some("".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022", - ); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: Some("11:23 pm".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022 11:23 PM", - ); - } - } - } - } - - #[test] - fn date_type_option_apply_changeset_test() { - let mut type_option = DateTypeOption::new(); - let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - let date_timestamp = "1653609600".to_owned(); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp.clone()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - - type_option.include_time = true; - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp.clone()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp.clone()), - time: Some("1:00".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022 01:00", - ); - - type_option.time_format = TimeFormat::TwelveHour; - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp), - time: Some("1:00 am".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022 01:00 AM", - ); - } - - #[test] - #[should_panic] - fn date_type_option_apply_changeset_error_test() { - let mut type_option = DateTypeOption::new(); - type_option.include_time = true; - let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); - let date_timestamp = "1653609600".to_owned(); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp.clone()), - time: Some("1:".to_owned()), - }, - &FieldType::DateTime, - &field_rev, - "May 27,2022 01:00", - ); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp), - time: Some("1:00".to_owned()), - }, - &FieldType::DateTime, - &field_rev, - "May 27,2022 01:00", - ); - } - - #[test] - #[should_panic] - fn date_type_option_twelve_hours_to_twenty_four_hours() { - let mut type_option = DateTypeOption::new(); - type_option.include_time = true; - let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); - let date_timestamp = "1653609600".to_owned(); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp), - time: Some("1:00 am".to_owned()), - }, - &FieldType::DateTime, - &field_rev, - "May 27,2022 01:00", - ); - } - - fn assert_changeset_result( - type_option: &DateTypeOption, - changeset: DateCellChangeset, - _field_type: &FieldType, - field_rev: &FieldRevision, - expected: &str, - ) { - let changeset = CellDataChangeset(Some(changeset)); - let encoded_data = type_option.apply_changeset(changeset, None).unwrap(); - assert_eq!( - expected.to_owned(), - decode_cell_data(encoded_data, type_option, field_rev) - ); - } - - fn assert_decode_timestamp( - timestamp: i64, - type_option: &DateTypeOption, - field_rev: &FieldRevision, - expected: &str, - ) { - let s = serde_json::to_string(&DateCellChangeset { - date: Some(timestamp.to_string()), - time: None, - }) - .unwrap(); - let encoded_data = type_option.apply_changeset(s.into(), None).unwrap(); - - assert_eq!( - expected.to_owned(), - decode_cell_data(encoded_data, type_option, field_rev) - ); - } - - fn decode_cell_data(encoded_data: String, type_option: &DateTypeOption, field_rev: &FieldRevision) -> String { - let decoded_data = type_option - .decode_cell_data(encoded_data.into(), &FieldType::DateTime, field_rev) - .unwrap() - .parse::() - .unwrap(); - - if type_option.include_time { - format!("{}{}", decoded_data.date, decoded_data.time) - } else { - decoded_data.date - } - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs new file mode 100644 index 0000000000..1229a8f5e1 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs @@ -0,0 +1,149 @@ +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::cell::CellDataOperation; + use crate::services::field::*; + // use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat}; + use flowy_grid_data_model::revision::FieldRevision; + use strum::IntoEnumIterator; + + #[test] + fn date_type_option_date_format_test() { + let mut type_option = DateTypeOption::default(); + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + for date_format in DateFormat::iter() { + type_option.date_format = date_format; + match date_format { + DateFormat::Friendly => { + assert_date(&type_option, 1647251762, None, "Mar 14,2022", &field_rev); + } + DateFormat::US => { + assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev); + } + DateFormat::ISO => { + assert_date(&type_option, 1647251762, None, "2022-03-14", &field_rev); + } + DateFormat::Local => { + assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev); + } + } + } + } + + #[test] + fn date_type_option_different_time_format_test() { + let mut type_option = DateTypeOption::default(); + let field_type = FieldType::DateTime; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + for time_format in TimeFormat::iter() { + type_option.time_format = time_format; + type_option.include_time = true; + match time_format { + TimeFormat::TwentyFourHour => { + assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev); + assert_date( + &type_option, + 1653609600, + Some("23:00".to_owned()), + "May 27,2022 23:00", + &field_rev, + ); + } + TimeFormat::TwelveHour => { + assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev); + assert_date( + &type_option, + 1653609600, + Some("11:23 pm".to_owned()), + "May 27,2022 11:23 PM", + &field_rev, + ); + } + } + } + } + + #[test] + fn date_type_option_invalid_date_str_test() { + let type_option = DateTypeOption::default(); + let field_type = FieldType::DateTime; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_date(&type_option, "abc", None, "", &field_rev); + } + + #[test] + #[should_panic] + fn date_type_option_invalid_include_time_str_test() { + let mut type_option = DateTypeOption::new(); + type_option.include_time = true; + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + + assert_date( + &type_option, + 1653609600, + Some("1:".to_owned()), + "May 27,2022 01:00", + &field_rev, + ); + } + + #[test] + fn date_type_option_empty_include_time_str_test() { + let mut type_option = DateTypeOption::new(); + type_option.include_time = true; + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + + assert_date(&type_option, 1653609600, Some("".to_owned()), "May 27,2022", &field_rev); + } + + /// The default time format is TwentyFourHour, so the include_time_str in twelve_hours_format will cause parser error. + #[test] + #[should_panic] + fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() { + let mut type_option = DateTypeOption::new(); + type_option.include_time = true; + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + + assert_date( + &type_option, + 1653609600, + Some("1:00 am".to_owned()), + "May 27,2022 01:00 AM", + &field_rev, + ); + } + fn assert_date( + type_option: &DateTypeOption, + timestamp: T, + include_time_str: Option, + expected_str: &str, + field_rev: &FieldRevision, + ) { + let s = serde_json::to_string(&DateCellChangesetPB { + date: Some(timestamp.to_string()), + time: include_time_str, + }) + .unwrap(); + let encoded_data = type_option.apply_changeset(s.into(), None).unwrap(); + + assert_eq!( + decode_cell_data(encoded_data, type_option, field_rev), + expected_str.to_owned(), + ); + } + + fn decode_cell_data(encoded_data: String, type_option: &DateTypeOption, field_rev: &FieldRevision) -> String { + let decoded_data = type_option + .decode_cell_data(encoded_data.into(), &FieldType::DateTime, field_rev) + .unwrap() + .with_parser(DateCellDataParser()) + .unwrap(); + + if type_option.include_time { + format!("{}{}", decoded_data.date, decoded_data.time) + } else { + decoded_data.date + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs new file mode 100644 index 0000000000..17b897b1f0 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs @@ -0,0 +1,195 @@ +use crate::entities::FieldType; +use crate::impl_type_option; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::{ + BoxTypeOptionBuilder, DateCellChangesetPB, DateCellDataPB, DateFormat, DateTimestamp, TimeFormat, TypeOptionBuilder, +}; +use bytes::Bytes; +use chrono::format::strftime::StrftimeItems; +use chrono::{NaiveDateTime, Timelike}; +use flowy_derive::ProtoBuf; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; +use serde::{Deserialize, Serialize}; + +// Date +#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] +pub struct DateTypeOption { + #[pb(index = 1)] + pub date_format: DateFormat, + + #[pb(index = 2)] + pub time_format: TimeFormat, + + #[pb(index = 3)] + pub include_time: bool, +} +impl_type_option!(DateTypeOption, FieldType::DateTime); + +impl DateTypeOption { + #[allow(dead_code)] + pub fn new() -> Self { + Self::default() + } + + fn today_desc_from_timestamp>(&self, timestamp: T) -> DateCellDataPB { + let timestamp = *timestamp.as_ref(); + let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0); + self.date_from_native(native) + } + + fn date_from_native(&self, native: chrono::NaiveDateTime) -> DateCellDataPB { + if native.timestamp() == 0 { + return DateCellDataPB::default(); + } + + let time = native.time(); + let has_time = time.hour() != 0 || time.second() != 0; + + let utc = self.utc_date_time_from_native(native); + let fmt = self.date_format.format_str(); + let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt))); + + let mut time = "".to_string(); + if has_time { + let fmt = format!("{} {}", self.date_format.format_str(), self.time_format.format_str()); + time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, ""); + } + + let timestamp = native.timestamp(); + DateCellDataPB { date, time, timestamp } + } + + fn date_fmt(&self, time: &Option) -> String { + if self.include_time { + match time.as_ref() { + None => self.date_format.format_str().to_string(), + Some(time_str) => { + if time_str.is_empty() { + self.date_format.format_str().to_string() + } else { + format!("{} {}", self.date_format.format_str(), self.time_format.format_str()) + } + } + } + } else { + self.date_format.format_str().to_string() + } + } + + fn timestamp_from_utc_with_time( + &self, + utc: &chrono::DateTime, + time: &Option, + ) -> FlowyResult { + if let Some(time_str) = time.as_ref() { + if !time_str.is_empty() { + let date_str = format!( + "{}{}", + utc.format_with_items(StrftimeItems::new(self.date_format.format_str())), + &time_str + ); + + return match NaiveDateTime::parse_from_str(&date_str, &self.date_fmt(time)) { + Ok(native) => { + let utc = self.utc_date_time_from_native(native); + Ok(utc.timestamp()) + } + Err(_e) => { + let msg = format!("Parse {} failed", date_str); + Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg)) + } + }; + } + } + + Ok(utc.timestamp()) + } + + fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime { + let native = NaiveDateTime::from_timestamp(timestamp, 0); + self.utc_date_time_from_native(native) + } + + fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime { + chrono::DateTime::::from_utc(naive, chrono::Utc) + } +} + +impl CellDisplayable for DateTypeOption { + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let timestamp = cell_data.try_into_inner()?; + let date_cell_data = self.today_desc_from_timestamp(timestamp); + CellBytes::from(date_cell_data) + } +} + +impl CellDataOperation for DateTypeOption { + fn decode_cell_data( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult { + // Return default data if the type_option_cell_data is not FieldType::DateTime. + // It happens when switching from one field to another. + // For example: + // FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen. + if !decoded_field_type.is_date() { + return Ok(CellBytes::default()); + } + self.display_data(cell_data, decoded_field_type, field_rev) + } + + fn apply_changeset( + &self, + changeset: CellDataChangeset, + _cell_rev: Option, + ) -> Result { + let changeset = changeset.try_into_inner()?; + let cell_data = match changeset.date_timestamp() { + None => 0, + Some(date_timestamp) => match (self.include_time, changeset.time) { + (true, Some(time)) => { + let time = Some(time.trim().to_uppercase()); + let utc = self.utc_date_time_from_timestamp(date_timestamp); + self.timestamp_from_utc_with_time(&utc, &time)? + } + _ => date_timestamp, + }, + }; + + Ok(cell_data.to_string()) + } +} + +#[derive(Default)] +pub struct DateTypeOptionBuilder(DateTypeOption); +impl_into_box_type_option_builder!(DateTypeOptionBuilder); +impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOption); + +impl DateTypeOptionBuilder { + pub fn date_format(mut self, date_format: DateFormat) -> Self { + self.0.date_format = date_format; + self + } + + pub fn time_format(mut self, time_format: TimeFormat) -> Self { + self.0.time_format = time_format; + self + } +} +impl TypeOptionBuilder for DateTypeOptionBuilder { + fn field_type(&self) -> FieldType { + FieldType::DateTime + } + + fn entry(&self) -> &dyn TypeOptionDataEntry { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs new file mode 100644 index 0000000000..1c54606f75 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -0,0 +1,210 @@ +use crate::entities::CellChangesetPB; +use crate::entities::{GridCellIdPB, GridCellIdParams}; +use crate::services::cell::{CellBytesParser, FromCellChangeset, FromCellString}; +use bytes::Bytes; + +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::{internal_error, ErrorCode, FlowyResult}; + +use serde::{Deserialize, Serialize}; +use strum_macros::EnumIter; + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct DateCellDataPB { + #[pb(index = 1)] + pub date: String, + + #[pb(index = 2)] + pub time: String, + + #[pb(index = 3)] + pub timestamp: i64, +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct DateChangesetPayloadPB { + #[pb(index = 1)] + pub cell_identifier: GridCellIdPB, + + #[pb(index = 2, one_of)] + pub date: Option, + + #[pb(index = 3, one_of)] + pub time: Option, +} + +pub struct DateChangesetParams { + pub cell_identifier: GridCellIdParams, + pub date: Option, + pub time: Option, +} + +impl TryInto for DateChangesetPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let cell_identifier: GridCellIdParams = self.cell_identifier.try_into()?; + Ok(DateChangesetParams { + cell_identifier, + date: self.date, + time: self.time, + }) + } +} + +impl std::convert::From for CellChangesetPB { + fn from(params: DateChangesetParams) -> Self { + let changeset = DateCellChangesetPB { + date: params.date, + time: params.time, + }; + let s = serde_json::to_string(&changeset).unwrap(); + CellChangesetPB { + grid_id: params.cell_identifier.grid_id, + row_id: params.cell_identifier.row_id, + field_id: params.cell_identifier.field_id, + content: Some(s), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct DateCellChangesetPB { + pub date: Option, + pub time: Option, +} + +impl DateCellChangesetPB { + pub fn date_timestamp(&self) -> Option { + if let Some(date) = &self.date { + match date.parse::() { + Ok(date_timestamp) => Some(date_timestamp), + Err(_) => None, + } + } else { + None + } + } +} + +impl FromCellChangeset for DateCellChangesetPB { + fn from_changeset(changeset: String) -> FlowyResult + where + Self: Sized, + { + serde_json::from_str::(&changeset).map_err(internal_error) + } +} +pub struct DateTimestamp(i64); +impl AsRef for DateTimestamp { + fn as_ref(&self) -> &i64 { + &self.0 + } +} + +impl std::convert::From for i64 { + fn from(timestamp: DateTimestamp) -> Self { + timestamp.0 + } +} + +impl FromCellString for DateTimestamp { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + let num = s.parse::().unwrap_or(0); + Ok(DateTimestamp(num)) + } +} + +#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)] +pub enum DateFormat { + Local = 0, + US = 1, + ISO = 2, + Friendly = 3, +} +impl std::default::Default for DateFormat { + fn default() -> Self { + DateFormat::Friendly + } +} + +impl std::convert::From for DateFormat { + fn from(value: i32) -> Self { + match value { + 0 => DateFormat::Local, + 1 => DateFormat::US, + 2 => DateFormat::ISO, + 3 => DateFormat::Friendly, + _ => { + tracing::error!("Unsupported date format, fallback to friendly"); + DateFormat::Friendly + } + } + } +} + +impl DateFormat { + pub fn value(&self) -> i32 { + *self as i32 + } + // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html + pub fn format_str(&self) -> &'static str { + match self { + DateFormat::Local => "%Y/%m/%d", + DateFormat::US => "%Y/%m/%d", + DateFormat::ISO => "%Y-%m-%d", + DateFormat::Friendly => "%b %d,%Y", + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, ProtoBuf_Enum)] +pub enum TimeFormat { + TwelveHour = 0, + TwentyFourHour = 1, +} + +impl std::convert::From for TimeFormat { + fn from(value: i32) -> Self { + match value { + 0 => TimeFormat::TwelveHour, + 1 => TimeFormat::TwentyFourHour, + _ => { + tracing::error!("Unsupported time format, fallback to TwentyFourHour"); + TimeFormat::TwentyFourHour + } + } + } +} + +impl TimeFormat { + pub fn value(&self) -> i32 { + *self as i32 + } + + // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html + pub fn format_str(&self) -> &'static str { + match self { + TimeFormat::TwelveHour => "%I:%M %p", + TimeFormat::TwentyFourHour => "%R", + } + } +} + +impl std::default::Default for TimeFormat { + fn default() -> Self { + TimeFormat::TwentyFourHour + } +} + +pub struct DateCellDataParser(); +impl CellBytesParser for DateCellDataParser { + type Object = DateCellDataPB; + + fn parse(&self, bytes: &Bytes) -> FlowyResult { + DateCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/mod.rs new file mode 100644 index 0000000000..395f2c9104 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/mod.rs @@ -0,0 +1,7 @@ +#![allow(clippy::module_inception)] +mod date_tests; +mod date_type_option; +mod date_type_option_entities; + +pub use date_type_option::*; +pub use date_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs index 754e441876..32fd9ef0a4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs @@ -1,17 +1,14 @@ -mod checkbox_type_option; -mod date_type_option; -mod multi_select_type_option; -mod number_type_option; -mod single_select_type_option; -mod text_type_option; -mod url_type_option; +pub mod checkbox_type_option; +pub mod date_type_option; +pub mod number_type_option; +pub mod selection_type_option; +pub mod text_type_option; +pub mod url_type_option; mod util; pub use checkbox_type_option::*; pub use date_type_option::*; -pub use multi_select_type_option::*; -pub use multi_select_type_option::*; pub use number_type_option::*; -pub use single_select_type_option::*; +pub use selection_type_option::*; pub use text_type_option::*; pub use url_type_option::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/mod.rs index fffbad97bf..4b2bcc1ecd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/mod.rs @@ -1,6 +1,9 @@ #![allow(clippy::module_inception)] mod format; +mod number_tests; mod number_type_option; +mod number_type_option_entities; pub use format::*; pub use number_type_option::*; +pub use number_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs new file mode 100644 index 0000000000..41132ecfe4 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs @@ -0,0 +1,146 @@ +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::cell::CellDataOperation; + use crate::services::field::FieldBuilder; + use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption}; + use flowy_grid_data_model::revision::FieldRevision; + use strum::IntoEnumIterator; + + /// Testing when the input is not a number. + #[test] + fn number_type_option_invalid_input_test() { + let type_option = NumberTypeOption::default(); + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + // Input is empty String + assert_number(&type_option, "", "", &field_type, &field_rev); + + // Input is letter + assert_number(&type_option, "abc", "", &field_type, &field_rev); + } + + /// Testing the strip_currency_symbol function. It should return the string without the input symbol. + #[test] + fn number_type_option_strip_symbol_test() { + // Remove the $ symbol + assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned()); + // Remove the ¥ symbol + assert_eq!(strip_currency_symbol("¥0.2"), "0.2".to_owned()); + } + + /// Format the input number to the corresponding format string. + #[test] + fn number_type_option_format_number_test() { + let mut type_option = NumberTypeOption::default(); + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + for format in NumberFormat::iter() { + type_option.format = format; + match format { + NumberFormat::Num => { + assert_number(&type_option, "18443", "18443", &field_type, &field_rev); + } + NumberFormat::USD => { + assert_number(&type_option, "18443", "$18,443", &field_type, &field_rev); + } + NumberFormat::Yen => { + assert_number(&type_option, "18443", "¥18,443", &field_type, &field_rev); + } + NumberFormat::Yuan => { + assert_number(&type_option, "18443", "CN¥18,443", &field_type, &field_rev); + } + NumberFormat::EUR => { + assert_number(&type_option, "18443", "€18.443", &field_type, &field_rev); + } + _ => {} + } + } + } + + /// Format the input String to the corresponding format string. + #[test] + fn number_type_option_format_str_test() { + let mut type_option = NumberTypeOption::default(); + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + for format in NumberFormat::iter() { + type_option.format = format; + match format { + NumberFormat::Num => { + assert_number(&type_option, "18443", "18443", &field_type, &field_rev); + assert_number(&type_option, "0.2", "0.2", &field_type, &field_rev); + } + NumberFormat::USD => { + assert_number(&type_option, "$18,44", "$1,844", &field_type, &field_rev); + assert_number(&type_option, "$0.2", "$0.2", &field_type, &field_rev); + assert_number(&type_option, "", "", &field_type, &field_rev); + assert_number(&type_option, "abc", "", &field_type, &field_rev); + } + NumberFormat::Yen => { + assert_number(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev); + assert_number(&type_option, "¥1844", "¥1,844", &field_type, &field_rev); + } + NumberFormat::Yuan => { + assert_number(&type_option, "CN¥18,44", "CN¥1,844", &field_type, &field_rev); + assert_number(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_rev); + } + NumberFormat::EUR => { + assert_number(&type_option, "€18.44", "€18,44", &field_type, &field_rev); + assert_number(&type_option, "€0.5", "€0,5", &field_type, &field_rev); + assert_number(&type_option, "€1844", "€1.844", &field_type, &field_rev); + } + _ => {} + } + } + } + + /// Carry out the sign positive to input number + #[test] + fn number_description_sign_test() { + let mut type_option = NumberTypeOption { + sign_positive: false, + ..Default::default() + }; + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + for format in NumberFormat::iter() { + type_option.format = format; + match format { + NumberFormat::Num => { + assert_number(&type_option, "18443", "18443", &field_type, &field_rev); + } + NumberFormat::USD => { + assert_number(&type_option, "18443", "-$18,443", &field_type, &field_rev); + } + NumberFormat::Yen => { + assert_number(&type_option, "18443", "-¥18,443", &field_type, &field_rev); + } + NumberFormat::EUR => { + assert_number(&type_option, "18443", "-€18.443", &field_type, &field_rev); + } + _ => {} + } + } + } + + fn assert_number( + type_option: &NumberTypeOption, + input_str: &str, + expected_str: &str, + field_type: &FieldType, + field_rev: &FieldRevision, + ) { + assert_eq!( + type_option + .decode_cell_data(input_str.to_owned().into(), field_type, field_rev) + .unwrap() + .to_string(), + expected_str.to_owned() + ); + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs index c8f79df20c..26d1d64248 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs @@ -1,17 +1,15 @@ -use crate::impl_type_option; - use crate::entities::FieldType; -use crate::services::cell::{CellData, CellDataChangeset, CellDataOperation, DecodedCellData}; -use crate::services::field::number_currency::Currency; +use crate::impl_type_option; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation}; use crate::services::field::type_options::number_type_option::format::*; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; +use crate::services::field::{BoxTypeOptionBuilder, NumberCellData, TypeOptionBuilder}; use bytes::Bytes; use flowy_derive::ProtoBuf; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; use rust_decimal::Decimal; -use rusty_money::Money; + use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -110,15 +108,15 @@ impl CellDataOperation for NumberTypeOption { cell_data: CellData, decoded_field_type: &FieldType, _field_rev: &FieldRevision, - ) -> FlowyResult { + ) -> FlowyResult { if decoded_field_type.is_date() { - return Ok(DecodedCellData::default()); + return Ok(CellBytes::default()); } let cell_data: String = cell_data.try_into_inner()?; match self.format_cell_data(&cell_data) { - Ok(num) => Ok(DecodedCellData::new(num.to_string())), - Err(_) => Ok(DecodedCellData::default()), + Ok(num) => Ok(CellBytes::new(num.to_string())), + Err(_) => Ok(CellBytes::default()), } } @@ -147,230 +145,3 @@ impl std::default::Default for NumberTypeOption { } } } - -#[derive(Default)] -pub struct NumberCellData { - decimal: Option, - money: Option, -} - -impl NumberCellData { - pub fn new() -> Self { - Self { - decimal: Default::default(), - money: None, - } - } - - pub fn from_format_str(s: &str, sign_positive: bool, format: &NumberFormat) -> FlowyResult { - let mut num_str = strip_currency_symbol(s); - let currency = format.currency(); - if num_str.is_empty() { - return Ok(Self::default()); - } - match Decimal::from_str(&num_str) { - Ok(mut decimal) => { - decimal.set_sign_positive(sign_positive); - let money = Money::from_decimal(decimal, currency); - Ok(Self::from_money(money)) - } - Err(_) => match Money::from_str(&num_str, currency) { - Ok(money) => Ok(NumberCellData::from_money(money)), - Err(_) => { - num_str.retain(|c| !STRIP_SYMBOL.contains(&c.to_string())); - if num_str.chars().all(char::is_numeric) { - Self::from_format_str(&num_str, sign_positive, format) - } else { - Err(FlowyError::invalid_data().context("Should only contain numbers")) - } - } - }, - } - } - - pub fn from_decimal(decimal: Decimal) -> Self { - Self { - decimal: Some(decimal), - money: None, - } - } - - pub fn from_money(money: Money) -> Self { - Self { - decimal: Some(*money.amount()), - money: Some(money.to_string()), - } - } - - pub fn decimal(&self) -> &Option { - &self.decimal - } - - pub fn is_empty(&self) -> bool { - self.decimal.is_none() - } -} - -impl FromStr for NumberCellData { - type Err = rust_decimal::Error; - - fn from_str(s: &str) -> Result { - if s.is_empty() { - return Ok(Self::default()); - } - let decimal = Decimal::from_str(s)?; - Ok(Self::from_decimal(decimal)) - } -} - -impl ToString for NumberCellData { - fn to_string(&self) -> String { - match &self.money { - None => match self.decimal { - None => String::default(), - Some(decimal) => decimal.to_string(), - }, - Some(money) => money.to_string(), - } - } -} - -#[cfg(test)] -mod tests { - use crate::entities::FieldType; - use crate::services::cell::CellDataOperation; - use crate::services::field::FieldBuilder; - use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption}; - use flowy_grid_data_model::revision::FieldRevision; - use strum::IntoEnumIterator; - - #[test] - fn number_type_option_invalid_input_test() { - let type_option = NumberTypeOption::default(); - let field_type = FieldType::Number; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_equal(&type_option, "", "", &field_type, &field_rev); - assert_equal(&type_option, "abc", "", &field_type, &field_rev); - } - - #[test] - fn number_type_option_strip_symbol_test() { - let mut type_option = NumberTypeOption::new(); - type_option.format = NumberFormat::USD; - assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned()); - - type_option.format = NumberFormat::Yuan; - assert_eq!(strip_currency_symbol("$0.2"), "0.2".to_owned()); - } - - #[test] - fn number_type_option_format_number_test() { - let mut type_option = NumberTypeOption::default(); - let field_type = FieldType::Number; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - - for format in NumberFormat::iter() { - type_option.format = format; - match format { - NumberFormat::Num => { - assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); - } - NumberFormat::USD => { - assert_equal(&type_option, "18443", "$18,443", &field_type, &field_rev); - } - NumberFormat::Yen => { - assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_rev); - } - NumberFormat::Yuan => { - assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_rev); - } - NumberFormat::EUR => { - assert_equal(&type_option, "18443", "€18.443", &field_type, &field_rev); - } - _ => {} - } - } - } - - #[test] - fn number_type_option_format_str_test() { - let mut type_option = NumberTypeOption::default(); - let field_type = FieldType::Number; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - - for format in NumberFormat::iter() { - type_option.format = format; - match format { - NumberFormat::Num => { - assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); - assert_equal(&type_option, "0.2", "0.2", &field_type, &field_rev); - } - NumberFormat::USD => { - assert_equal(&type_option, "$18,44", "$1,844", &field_type, &field_rev); - assert_equal(&type_option, "$0.2", "$0.2", &field_type, &field_rev); - assert_equal(&type_option, "", "", &field_type, &field_rev); - assert_equal(&type_option, "abc", "", &field_type, &field_rev); - } - NumberFormat::Yen => { - assert_equal(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev); - assert_equal(&type_option, "¥1844", "¥1,844", &field_type, &field_rev); - } - NumberFormat::Yuan => { - assert_equal(&type_option, "CN¥18,44", "CN¥1,844", &field_type, &field_rev); - assert_equal(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_rev); - } - NumberFormat::EUR => { - assert_equal(&type_option, "€18.44", "€18,44", &field_type, &field_rev); - assert_equal(&type_option, "€0.5", "€0,5", &field_type, &field_rev); - assert_equal(&type_option, "€1844", "€1.844", &field_type, &field_rev); - } - _ => {} - } - } - } - - #[test] - fn number_description_sign_test() { - let mut type_option = NumberTypeOption { - sign_positive: false, - ..Default::default() - }; - let field_type = FieldType::Number; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - - for format in NumberFormat::iter() { - type_option.format = format; - match format { - NumberFormat::Num => { - assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); - } - NumberFormat::USD => { - assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_rev); - } - NumberFormat::Yen => { - assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_rev); - } - NumberFormat::EUR => { - assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_rev); - } - _ => {} - } - } - } - - fn assert_equal( - type_option: &NumberTypeOption, - cell_data: &str, - expected_str: &str, - field_type: &FieldType, - field_rev: &FieldRevision, - ) { - assert_eq!( - type_option - .decode_cell_data(cell_data.to_owned().into(), field_type, field_rev) - .unwrap() - .to_string(), - expected_str.to_owned() - ); - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs new file mode 100644 index 0000000000..6297114a07 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs @@ -0,0 +1,105 @@ +use crate::services::cell::CellBytesParser; +use crate::services::field::number_currency::Currency; +use crate::services::field::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL}; +use bytes::Bytes; +use flowy_error::{FlowyError, FlowyResult}; +use rust_decimal::Decimal; +use rusty_money::Money; +use std::str::FromStr; + +#[derive(Default)] +pub struct NumberCellData { + decimal: Option, + money: Option, +} + +impl NumberCellData { + pub fn new() -> Self { + Self { + decimal: Default::default(), + money: None, + } + } + + pub fn from_format_str(s: &str, sign_positive: bool, format: &NumberFormat) -> FlowyResult { + let mut num_str = strip_currency_symbol(s); + let currency = format.currency(); + if num_str.is_empty() { + return Ok(Self::default()); + } + match Decimal::from_str(&num_str) { + Ok(mut decimal) => { + decimal.set_sign_positive(sign_positive); + let money = Money::from_decimal(decimal, currency); + Ok(Self::from_money(money)) + } + Err(_) => match Money::from_str(&num_str, currency) { + Ok(money) => Ok(NumberCellData::from_money(money)), + Err(_) => { + num_str.retain(|c| !STRIP_SYMBOL.contains(&c.to_string())); + if num_str.chars().all(char::is_numeric) { + Self::from_format_str(&num_str, sign_positive, format) + } else { + Err(FlowyError::invalid_data().context("Should only contain numbers")) + } + } + }, + } + } + + pub fn from_decimal(decimal: Decimal) -> Self { + Self { + decimal: Some(decimal), + money: None, + } + } + + pub fn from_money(money: Money) -> Self { + Self { + decimal: Some(*money.amount()), + money: Some(money.to_string()), + } + } + + pub fn decimal(&self) -> &Option { + &self.decimal + } + + pub fn is_empty(&self) -> bool { + self.decimal.is_none() + } +} + +// impl FromStr for NumberCellData { +// type Err = FlowyError; +// +// fn from_str(s: &str) -> Result { +// if s.is_empty() { +// return Ok(Self::default()); +// } +// let decimal = Decimal::from_str(s).map_err(internal_error)?; +// Ok(Self::from_decimal(decimal)) +// } +// } + +impl ToString for NumberCellData { + fn to_string(&self) -> String { + match &self.money { + None => match self.decimal { + None => String::default(), + Some(decimal) => decimal.to_string(), + }, + Some(money) => money.to_string(), + } + } +} +pub struct NumberCellDataParser(pub NumberFormat); +impl CellBytesParser for NumberCellDataParser { + type Object = NumberCellData; + fn parse(&self, bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => NumberCellData::from_format_str(&s, true, &self.0), + Err(_) => Ok(NumberCellData::default()), + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs new file mode 100644 index 0000000000..45d2b96e83 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs @@ -0,0 +1,7 @@ +mod multi_select_type_option; +mod select_option; +mod single_select_type_option; + +pub use multi_select_type_option::*; +pub use select_option::*; +pub use single_select_type_option::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/multi_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs similarity index 77% rename from frontend/rust-lib/flowy-grid/src/services/field/type_options/multi_select_type_option.rs rename to frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index 209ed234f7..187b81d060 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -1,26 +1,22 @@ use crate::entities::FieldType; - use crate::impl_type_option; -use crate::services::cell::{AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData}; -use crate::services::field::select_option::{ - make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds, - SelectOptionOperation, SELECTION_IDS_SEPARATOR, -}; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; use crate::services::field::type_options::util::get_cell_data; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; +use crate::services::field::{ + make_selected_select_options, BoxTypeOptionBuilder, SelectOptionCellChangeset, SelectOptionCellDataPB, + SelectOptionIds, SelectOptionOperation, SelectOptionPB, TypeOptionBuilder, SELECTION_IDS_SEPARATOR, +}; use bytes::Bytes; use flowy_derive::ProtoBuf; use flowy_error::{FlowyError, FlowyResult}; - use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; - use serde::{Deserialize, Serialize}; // Multiple select #[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] pub struct MultiSelectTypeOption { #[pb(index = 1)] - pub options: Vec, + pub options: Vec, #[pb(index = 2)] pub disable_color: bool, @@ -28,19 +24,19 @@ pub struct MultiSelectTypeOption { impl_type_option!(MultiSelectTypeOption, FieldType::MultiSelect); impl SelectOptionOperation for MultiSelectTypeOption { - fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData { - let select_options = make_selected_select_options(any_cell_data, &self.options); - SelectOptionCellData { + fn selected_select_option(&self, cell_data: CellData) -> SelectOptionCellDataPB { + let select_options = make_selected_select_options(cell_data, &self.options); + SelectOptionCellDataPB { options: self.options.clone(), select_options, } } - fn options(&self) -> &Vec { + fn options(&self) -> &Vec { &self.options } - fn mut_options(&mut self) -> &mut Vec { + fn mut_options(&mut self) -> &mut Vec { &mut self.options } } @@ -50,24 +46,13 @@ impl CellDataOperation for MultiSele &self, cell_data: CellData, decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { + field_rev: &FieldRevision, + ) -> FlowyResult { if !decoded_field_type.is_select_option() { - return Ok(DecodedCellData::default()); + return Ok(CellBytes::default()); } - let ids: SelectOptionIds = cell_data.try_into_inner()?; - let select_options = ids - .iter() - .flat_map(|option_id| self.options.iter().find(|option| &option.id == option_id).cloned()) - .collect::>(); - - let cell_data = SelectOptionCellData { - options: self.options.clone(), - select_options, - }; - - DecodedCellData::try_from_bytes(cell_data) + self.display_data(cell_data, decoded_field_type, field_rev) } fn apply_changeset( @@ -112,7 +97,7 @@ pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOption); impl_into_box_type_option_builder!(MultiSelectTypeOptionBuilder); impl_builder_from_json_str_and_from_bytes!(MultiSelectTypeOptionBuilder, MultiSelectTypeOption); impl MultiSelectTypeOptionBuilder { - pub fn option(mut self, opt: SelectOption) -> Self { + pub fn option(mut self, opt: SelectOptionPB) -> Self { self.0.options.push(opt); self } @@ -131,16 +116,16 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder { mod tests { use crate::entities::FieldType; use crate::services::cell::CellDataOperation; - use crate::services::field::select_option::*; + use crate::services::field::type_options::selection_type_option::*; use crate::services::field::FieldBuilder; use crate::services::field::{MultiSelectTypeOption, MultiSelectTypeOptionBuilder}; use flowy_grid_data_model::revision::FieldRevision; #[test] fn multi_select_test() { - let google_option = SelectOption::new("Google"); - let facebook_option = SelectOption::new("Facebook"); - let twitter_option = SelectOption::new("Twitter"); + let google_option = SelectOptionPB::new("Google"); + let facebook_option = SelectOptionPB::new("Facebook"); + let twitter_option = SelectOptionPB::new("Twitter"); let multi_select = MultiSelectTypeOptionBuilder::default() .option(google_option.clone()) .option(facebook_option.clone()) @@ -187,7 +172,7 @@ mod tests { cell_data: String, type_option: &MultiSelectTypeOption, field_rev: &FieldRevision, - expected: Vec, + expected: Vec, ) { let field_type: FieldType = field_rev.field_type_rev.into(); assert_eq!( @@ -195,7 +180,7 @@ mod tests { type_option .decode_cell_data(cell_data.into(), &field_type, field_rev) .unwrap() - .parse::() + .with_parser(SelectOptionCellDataParser()) .unwrap() .select_options, ); diff --git a/frontend/rust-lib/flowy-grid/src/services/field/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs similarity index 63% rename from frontend/rust-lib/flowy-grid/src/services/field/select_option.rs rename to frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index 3e974e9f56..d426844427 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -1,8 +1,9 @@ -use crate::entities::{CellChangeset, CellIdentifier, CellIdentifierPayload, FieldType}; -use crate::services::cell::{AnyCellData, FromCellChangeset, FromCellString}; -use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; +use crate::entities::{CellChangesetPB, FieldType, GridCellIdPB, GridCellIdParams}; +use crate::services::cell::{CellBytes, CellBytesParser, CellData, CellDisplayable, FromCellChangeset, FromCellString}; +use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOptionPB}; +use bytes::Bytes; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; +use flowy_error::{internal_error, ErrorCode, FlowyResult}; use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry}; use nanoid::nanoid; @@ -10,8 +11,9 @@ use serde::{Deserialize, Serialize}; pub const SELECTION_IDS_SEPARATOR: &str = ","; +/// [SelectOptionPB] represents an option for a single select, and multiple select. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ProtoBuf)] -pub struct SelectOption { +pub struct SelectOptionPB { #[pb(index = 1)] pub id: String, @@ -19,20 +21,20 @@ pub struct SelectOption { pub name: String, #[pb(index = 3)] - pub color: SelectOptionColor, + pub color: SelectOptionColorPB, } -impl SelectOption { +impl SelectOptionPB { pub fn new(name: &str) -> Self { - SelectOption { + SelectOptionPB { id: nanoid!(4), name: name.to_owned(), - color: SelectOptionColor::default(), + color: SelectOptionColorPB::default(), } } - pub fn with_color(name: &str, color: SelectOptionColor) -> Self { - SelectOption { + pub fn with_color(name: &str, color: SelectOptionColorPB) -> Self { + SelectOptionPB { id: nanoid!(4), name: name.to_owned(), color, @@ -42,7 +44,7 @@ impl SelectOption { #[derive(ProtoBuf_Enum, PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] #[repr(u8)] -pub enum SelectOptionColor { +pub enum SelectOptionColorPB { Purple = 0, Pink = 1, LightPink = 2, @@ -54,18 +56,17 @@ pub enum SelectOptionColor { Blue = 8, } -impl std::default::Default for SelectOptionColor { +impl std::default::Default for SelectOptionColorPB { fn default() -> Self { - SelectOptionColor::Purple + SelectOptionColorPB::Purple } } -pub fn make_selected_select_options>( - any_cell_data: T, - options: &[SelectOption], -) -> Vec { - if let Ok(type_option_cell_data) = any_cell_data.try_into() { - let ids = SelectOptionIds::from(type_option_cell_data.data); +pub fn make_selected_select_options( + cell_data: CellData, + options: &[SelectOptionPB], +) -> Vec { + if let Ok(ids) = cell_data.try_into_inner() { ids.iter() .flat_map(|option_id| options.iter().find(|option| &option.id == option_id).cloned()) .collect() @@ -75,7 +76,7 @@ pub fn make_selected_select_options>( } pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync { - fn insert_option(&mut self, new_option: SelectOption) { + fn insert_option(&mut self, new_option: SelectOptionPB) { let options = self.mut_options(); if let Some(index) = options .iter() @@ -88,30 +89,44 @@ pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync { } } - fn delete_option(&mut self, delete_option: SelectOption) { + fn delete_option(&mut self, delete_option: SelectOptionPB) { let options = self.mut_options(); if let Some(index) = options.iter().position(|option| option.id == delete_option.id) { options.remove(index); } } - fn create_option(&self, name: &str) -> SelectOption { + fn create_option(&self, name: &str) -> SelectOptionPB { let color = select_option_color_from_index(self.options().len()); - SelectOption::with_color(name, color) + SelectOptionPB::with_color(name, color) } - fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData; + fn selected_select_option(&self, cell_data: CellData) -> SelectOptionCellDataPB; - fn options(&self) -> &Vec; + fn options(&self) -> &Vec; - fn mut_options(&mut self) -> &mut Vec; + fn mut_options(&mut self) -> &mut Vec; +} + +impl CellDisplayable for T +where + T: SelectOptionOperation, +{ + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + CellBytes::from(self.selected_select_option(cell_data)) + } } pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult> { let field_type: FieldType = field_rev.field_type_rev.into(); match &field_type { FieldType::SingleSelect => { - let type_option = SingleSelectTypeOption::from(field_rev); + let type_option = SingleSelectTypeOptionPB::from(field_rev); Ok(Box::new(type_option)) } FieldType::MultiSelect => { @@ -125,18 +140,18 @@ pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult SelectOptionColor { +pub fn select_option_color_from_index(index: usize) -> SelectOptionColorPB { match index % 8 { - 0 => SelectOptionColor::Purple, - 1 => SelectOptionColor::Pink, - 2 => SelectOptionColor::LightPink, - 3 => SelectOptionColor::Orange, - 4 => SelectOptionColor::Yellow, - 5 => SelectOptionColor::Lime, - 6 => SelectOptionColor::Green, - 7 => SelectOptionColor::Aqua, - 8 => SelectOptionColor::Blue, - _ => SelectOptionColor::Purple, + 0 => SelectOptionColorPB::Purple, + 1 => SelectOptionColorPB::Pink, + 2 => SelectOptionColorPB::LightPink, + 3 => SelectOptionColorPB::Orange, + 4 => SelectOptionColorPB::Yellow, + 5 => SelectOptionColorPB::Lime, + 6 => SelectOptionColorPB::Green, + 7 => SelectOptionColorPB::Aqua, + 8 => SelectOptionColorPB::Blue, + _ => SelectOptionColorPB::Purple, } } pub struct SelectOptionIds(Vec); @@ -147,14 +162,6 @@ impl SelectOptionIds { } } -impl std::convert::TryFrom for SelectOptionIds { - type Error = FlowyError; - - fn try_from(value: AnyCellData) -> Result { - Ok(Self::from(value.data)) - } -} - impl FromCellString for SelectOptionIds { fn from_cell_str(s: &str) -> FlowyResult where @@ -196,11 +203,30 @@ impl std::ops::DerefMut for SelectOptionIds { &mut self.0 } } +pub struct SelectOptionIdsParser(); +impl CellBytesParser for SelectOptionIdsParser { + type Object = SelectOptionIds; + fn parse(&self, bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => Ok(SelectOptionIds::from(s)), + Err(_) => Ok(SelectOptionIds::from("".to_owned())), + } + } +} + +pub struct SelectOptionCellDataParser(); +impl CellBytesParser for SelectOptionCellDataParser { + type Object = SelectOptionCellDataPB; + + fn parse(&self, bytes: &Bytes) -> FlowyResult { + SelectOptionCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) + } +} #[derive(Clone, Debug, Default, ProtoBuf)] -pub struct SelectOptionCellChangesetPayload { +pub struct SelectOptionCellChangesetPayloadPB { #[pb(index = 1)] - pub cell_identifier: CellIdentifierPayload, + pub cell_identifier: GridCellIdPB, #[pb(index = 2, one_of)] pub insert_option_id: Option, @@ -210,19 +236,19 @@ pub struct SelectOptionCellChangesetPayload { } pub struct SelectOptionCellChangesetParams { - pub cell_identifier: CellIdentifier, + pub cell_identifier: GridCellIdParams, pub insert_option_id: Option, pub delete_option_id: Option, } -impl std::convert::From for CellChangeset { +impl std::convert::From for CellChangesetPB { fn from(params: SelectOptionCellChangesetParams) -> Self { let changeset = SelectOptionCellChangeset { insert_option_id: params.insert_option_id, delete_option_id: params.delete_option_id, }; let s = serde_json::to_string(&changeset).unwrap(); - CellChangeset { + CellChangesetPB { grid_id: params.cell_identifier.grid_id, row_id: params.cell_identifier.row_id, field_id: params.cell_identifier.field_id, @@ -231,11 +257,11 @@ impl std::convert::From for CellChangeset { } } -impl TryInto for SelectOptionCellChangesetPayload { +impl TryInto for SelectOptionCellChangesetPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { - let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?; + let cell_identifier: GridCellIdParams = self.cell_identifier.try_into()?; let insert_option_id = match self.insert_option_id { None => None, Some(insert_option_id) => Some( @@ -297,38 +323,44 @@ impl SelectOptionCellChangeset { } } +/// [SelectOptionCellDataPB] contains a list of user's selected options and a list of all the options +/// that the cell can use. #[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct SelectOptionCellData { +pub struct SelectOptionCellDataPB { + /// The available options that the cell can use. #[pb(index = 1)] - pub options: Vec, + pub options: Vec, + /// The selected options for the cell. #[pb(index = 2)] - pub select_options: Vec, + pub select_options: Vec, } +/// [SelectOptionChangesetPayloadPB] describes the changes of a FieldTypeOptionData. For the moment, +/// it is used by [MultiSelectTypeOptionPB] and [SingleSelectTypeOptionPB]. #[derive(Clone, Debug, Default, ProtoBuf)] -pub struct SelectOptionChangesetPayload { +pub struct SelectOptionChangesetPayloadPB { #[pb(index = 1)] - pub cell_identifier: CellIdentifierPayload, + pub cell_identifier: GridCellIdPB, #[pb(index = 2, one_of)] - pub insert_option: Option, + pub insert_option: Option, #[pb(index = 3, one_of)] - pub update_option: Option, + pub update_option: Option, #[pb(index = 4, one_of)] - pub delete_option: Option, + pub delete_option: Option, } pub struct SelectOptionChangeset { - pub cell_identifier: CellIdentifier, - pub insert_option: Option, - pub update_option: Option, - pub delete_option: Option, + pub cell_identifier: GridCellIdParams, + pub insert_option: Option, + pub update_option: Option, + pub delete_option: Option, } -impl TryInto for SelectOptionChangesetPayload { +impl TryInto for SelectOptionChangesetPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -343,11 +375,11 @@ impl TryInto for SelectOptionChangesetPayload { } pub struct SelectedSelectOptions { - pub(crate) options: Vec, + pub(crate) options: Vec, } -impl std::convert::From for SelectedSelectOptions { - fn from(data: SelectOptionCellData) -> Self { +impl std::convert::From for SelectedSelectOptions { + fn from(data: SelectOptionCellDataPB) -> Self { Self { options: data.select_options, } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/single_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs similarity index 68% rename from frontend/rust-lib/flowy-grid/src/services/field/type_options/single_select_type_option.rs rename to frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs index 6094f8567b..553425222e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/single_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs @@ -1,9 +1,9 @@ use crate::entities::FieldType; use crate::impl_type_option; -use crate::services::cell::{AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData}; -use crate::services::field::select_option::{ - make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds, - SelectOptionOperation, +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::{ + make_selected_select_options, SelectOptionCellChangeset, SelectOptionCellDataPB, SelectOptionIds, + SelectOptionOperation, SelectOptionPB, }; use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; use bytes::Bytes; @@ -14,56 +14,47 @@ use serde::{Deserialize, Serialize}; // Single select #[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct SingleSelectTypeOption { +pub struct SingleSelectTypeOptionPB { #[pb(index = 1)] - pub options: Vec, + pub options: Vec, #[pb(index = 2)] pub disable_color: bool, } -impl_type_option!(SingleSelectTypeOption, FieldType::SingleSelect); +impl_type_option!(SingleSelectTypeOptionPB, FieldType::SingleSelect); -impl SelectOptionOperation for SingleSelectTypeOption { - fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData { - let select_options = make_selected_select_options(any_cell_data, &self.options); - SelectOptionCellData { +impl SelectOptionOperation for SingleSelectTypeOptionPB { + fn selected_select_option(&self, cell_data: CellData) -> SelectOptionCellDataPB { + let mut select_options = make_selected_select_options(cell_data, &self.options); + // only keep option in single select + select_options.truncate(1); + SelectOptionCellDataPB { options: self.options.clone(), select_options, } } - fn options(&self) -> &Vec { + fn options(&self) -> &Vec { &self.options } - fn mut_options(&mut self) -> &mut Vec { + fn mut_options(&mut self) -> &mut Vec { &mut self.options } } -impl CellDataOperation for SingleSelectTypeOption { +impl CellDataOperation for SingleSelectTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { + field_rev: &FieldRevision, + ) -> FlowyResult { if !decoded_field_type.is_select_option() { - return Ok(DecodedCellData::default()); + return Ok(CellBytes::default()); } - let ids: SelectOptionIds = cell_data.try_into_inner()?; - let mut cell_data = SelectOptionCellData { - options: self.options.clone(), - select_options: vec![], - }; - if let Some(option_id) = ids.first() { - if let Some(option) = self.options.iter().find(|option| &option.id == option_id) { - cell_data.select_options.push(option.clone()); - } - } - - DecodedCellData::try_from_bytes(cell_data) + self.display_data(cell_data, decoded_field_type, field_rev) } fn apply_changeset( @@ -86,12 +77,12 @@ impl CellDataOperation for SingleSel } #[derive(Default)] -pub struct SingleSelectTypeOptionBuilder(SingleSelectTypeOption); +pub struct SingleSelectTypeOptionBuilder(SingleSelectTypeOptionPB); impl_into_box_type_option_builder!(SingleSelectTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(SingleSelectTypeOptionBuilder, SingleSelectTypeOption); +impl_builder_from_json_str_and_from_bytes!(SingleSelectTypeOptionBuilder, SingleSelectTypeOptionPB); impl SingleSelectTypeOptionBuilder { - pub fn option(mut self, opt: SelectOption) -> Self { + pub fn option(mut self, opt: SelectOptionPB) -> Self { self.0.options.push(opt); self } @@ -111,16 +102,16 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder { mod tests { use crate::entities::FieldType; use crate::services::cell::CellDataOperation; - use crate::services::field::select_option::*; + use crate::services::field::type_options::*; use crate::services::field::FieldBuilder; use flowy_grid_data_model::revision::FieldRevision; #[test] fn single_select_test() { - let google_option = SelectOption::new("Google"); - let facebook_option = SelectOption::new("Facebook"); - let twitter_option = SelectOption::new("Twitter"); + let google_option = SelectOptionPB::new("Google"); + let facebook_option = SelectOptionPB::new("Facebook"); + let twitter_option = SelectOptionPB::new("Twitter"); let single_select = SingleSelectTypeOptionBuilder::default() .option(google_option.clone()) .option(facebook_option.clone()) @@ -131,7 +122,7 @@ mod tests { .visibility(true) .build(); - let type_option = SingleSelectTypeOption::from(&field_rev); + let type_option = SingleSelectTypeOptionPB::from(&field_rev); let option_ids = vec![google_option.id.clone(), facebook_option.id].join(SELECTION_IDS_SEPARATOR); let data = SelectOptionCellChangeset::from_insert(&option_ids).to_str(); @@ -161,9 +152,9 @@ mod tests { fn assert_single_select_options( cell_data: String, - type_option: &SingleSelectTypeOption, + type_option: &SingleSelectTypeOptionPB, field_rev: &FieldRevision, - expected: Vec, + expected: Vec, ) { let field_type: FieldType = field_rev.field_type_rev.into(); assert_eq!( @@ -171,7 +162,7 @@ mod tests { type_option .decode_cell_data(cell_data.into(), &field_type, field_rev) .unwrap() - .parse::() + .with_parser(SelectOptionCellDataParser()) .unwrap() .select_options, ); diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/mod.rs new file mode 100644 index 0000000000..c7e518f103 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/mod.rs @@ -0,0 +1,3 @@ +#![allow(clippy::module_inception)] +mod text_type_option; +pub use text_type_option::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs similarity index 77% rename from frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs rename to frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index a23366e2a4..b11bd026d2 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -1,7 +1,8 @@ use crate::entities::FieldType; use crate::impl_type_option; use crate::services::cell::{ - try_decode_cell_data, AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData, + try_decode_cell_data, CellBytes, CellBytesParser, CellData, CellDataChangeset, CellDataOperation, CellDisplayable, + FromCellString, }; use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; use bytes::Bytes; @@ -32,13 +33,25 @@ pub struct RichTextTypeOption { } impl_type_option!(RichTextTypeOption, FieldType::RichText); +impl CellDisplayable for RichTextTypeOption { + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_str: String = cell_data.try_into_inner()?; + Ok(CellBytes::new(cell_str)) + } +} + impl CellDataOperation for RichTextTypeOption { fn decode_cell_data( &self, cell_data: CellData, decoded_field_type: &FieldType, field_rev: &FieldRevision, - ) -> FlowyResult { + ) -> FlowyResult { if decoded_field_type.is_date() || decoded_field_type.is_single_select() || decoded_field_type.is_multi_select() @@ -46,8 +59,7 @@ impl CellDataOperation for RichTextTypeOption { { try_decode_cell_data(cell_data, field_rev, decoded_field_type, decoded_field_type) } else { - let cell_data: String = cell_data.try_into_inner()?; - Ok(DecodedCellData::new(cell_data)) + self.display_data(cell_data, decoded_field_type, field_rev) } } @@ -72,11 +84,23 @@ impl AsRef for TextCellData { } } -impl std::convert::TryFrom for TextCellData { - type Error = FlowyError; +impl FromCellString for TextCellData { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + Ok(TextCellData(s.to_owned())) + } +} - fn try_from(value: AnyCellData) -> Result { - Ok(TextCellData(value.data)) +pub struct TextCellDataParser(); +impl CellBytesParser for TextCellDataParser { + type Object = TextCellData; + fn parse(&self, bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => Ok(TextCellData(s)), + Err(_) => Ok(TextCellData("".to_owned())), + } } } @@ -84,7 +108,7 @@ impl std::convert::TryFrom for TextCellData { mod tests { use crate::entities::FieldType; use crate::services::cell::CellDataOperation; - use crate::services::field::select_option::*; + use crate::services::field::FieldBuilder; use crate::services::field::*; @@ -100,14 +124,14 @@ mod tests { type_option .decode_cell_data(1647251762.to_string().into(), &field_type, &date_time_field_rev) .unwrap() - .parse::() + .with_parser(DateCellDataParser()) .unwrap() .date, "Mar 14,2022".to_owned() ); // Single select - let done_option = SelectOption::new("Done"); + let done_option = SelectOptionPB::new("Done"); let done_option_id = done_option.id.clone(); let single_select = SingleSelectTypeOptionBuilder::default().option(done_option.clone()); let single_select_field_rev = FieldBuilder::new(single_select).build(); @@ -120,15 +144,15 @@ mod tests { &single_select_field_rev ) .unwrap() - .parse::() + .with_parser(SelectOptionCellDataParser()) .unwrap() .select_options, vec![done_option], ); // Multiple select - let google_option = SelectOption::new("Google"); - let facebook_option = SelectOption::new("Facebook"); + let google_option = SelectOptionPB::new("Google"); + let facebook_option = SelectOptionPB::new("Facebook"); let ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR); let cell_data_changeset = SelectOptionCellChangeset::from_insert(&ids).to_str(); let multi_select = MultiSelectTypeOptionBuilder::default() @@ -143,7 +167,7 @@ mod tests { type_option .decode_cell_data(cell_data.into(), &FieldType::MultiSelect, &multi_select_field_rev) .unwrap() - .parse::() + .with_parser(SelectOptionCellDataParser()) .unwrap() .select_options, vec![google_option, facebook_option] diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs deleted file mode 100644 index 74fc9fc26b..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::entities::FieldType; -use crate::impl_type_option; -use crate::services::cell::{ - AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData, FromCellString, -}; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; -use bytes::Bytes; -use fancy_regex::Regex; -use flowy_derive::ProtoBuf; -use flowy_error::{internal_error, FlowyError, FlowyResult}; -use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; - -#[derive(Default)] -pub struct URLTypeOptionBuilder(URLTypeOption); -impl_into_box_type_option_builder!(URLTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(URLTypeOptionBuilder, URLTypeOption); - -impl TypeOptionBuilder for URLTypeOptionBuilder { - fn field_type(&self) -> FieldType { - FieldType::URL - } - - fn entry(&self) -> &dyn TypeOptionDataEntry { - &self.0 - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] -pub struct URLTypeOption { - #[pb(index = 1)] - data: String, //It's not used yet. -} -impl_type_option!(URLTypeOption, FieldType::URL); - -impl CellDataOperation for URLTypeOption { - fn decode_cell_data( - &self, - cell_data: CellData, - decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { - if !decoded_field_type.is_url() { - return Ok(DecodedCellData::default()); - } - let cell_data: URLCellData = cell_data.try_into_inner()?; - DecodedCellData::try_from_bytes(cell_data) - } - - fn apply_changeset( - &self, - changeset: CellDataChangeset, - _cell_rev: Option, - ) -> Result { - let changeset = changeset.try_into_inner()?; - let mut url = "".to_string(); - if let Ok(Some(m)) = URL_REGEX.find(&changeset) { - url = auto_append_scheme(m.as_str()); - } - URLCellData { - url, - content: changeset, - } - .to_json() - } -} - -fn auto_append_scheme(s: &str) -> String { - // Only support https scheme by now - match url::Url::parse(s) { - Ok(url) => { - if url.scheme() == "https" { - url.into() - } else { - format!("https://{}", s) - } - } - Err(_) => { - format!("https://{}", s) - } - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct URLCellData { - #[pb(index = 1)] - pub url: String, - - #[pb(index = 2)] - pub content: String, -} - -impl URLCellData { - pub fn new(s: &str) -> Self { - Self { - url: "".to_string(), - content: s.to_string(), - } - } - - fn to_json(&self) -> FlowyResult { - serde_json::to_string(self).map_err(internal_error) - } -} - -impl FromCellString for URLCellData { - fn from_cell_str(s: &str) -> FlowyResult { - serde_json::from_str::(s).map_err(internal_error) - } -} - -impl std::convert::TryFrom for URLCellData { - type Error = FlowyError; - - fn try_from(data: AnyCellData) -> Result { - serde_json::from_str::(&data.data).map_err(internal_error) - } -} - -lazy_static! { - static ref URL_REGEX: Regex = Regex::new( - "[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)" - ) - .unwrap(); -} - -#[cfg(test)] -mod tests { - use crate::entities::FieldType; - use crate::services::cell::{CellData, CellDataOperation}; - use crate::services::field::FieldBuilder; - use crate::services::field::{URLCellData, URLTypeOption}; - use flowy_grid_data_model::revision::FieldRevision; - - #[test] - fn url_type_option_test_no_url() { - let type_option = URLTypeOption::default(); - let field_type = FieldType::URL; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_changeset(&type_option, "123", &field_type, &field_rev, "123", ""); - } - - #[test] - fn url_type_option_test_contains_url() { - let type_option = URLTypeOption::default(); - let field_type = FieldType::URL; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_changeset( - &type_option, - "AppFlowy website - https://www.appflowy.io", - &field_type, - &field_rev, - "AppFlowy website - https://www.appflowy.io", - "https://www.appflowy.io/", - ); - - assert_changeset( - &type_option, - "AppFlowy website appflowy.io", - &field_type, - &field_rev, - "AppFlowy website appflowy.io", - "https://appflowy.io", - ); - } - - fn assert_changeset( - type_option: &URLTypeOption, - cell_data: &str, - field_type: &FieldType, - field_rev: &FieldRevision, - expected: &str, - expected_url: &str, - ) { - let encoded_data = type_option.apply_changeset(cell_data.to_owned().into(), None).unwrap(); - let decode_cell_data = decode_cell_data(encoded_data, type_option, field_rev, field_type); - assert_eq!(expected.to_owned(), decode_cell_data.content); - assert_eq!(expected_url.to_owned(), decode_cell_data.url); - } - - fn decode_cell_data>>( - encoded_data: T, - type_option: &URLTypeOption, - field_rev: &FieldRevision, - field_type: &FieldType, - ) -> URLCellData { - type_option - .decode_cell_data(encoded_data.into(), field_type, field_rev) - .unwrap() - .parse::() - .unwrap() - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/mod.rs new file mode 100644 index 0000000000..8f6cb884df --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/mod.rs @@ -0,0 +1,7 @@ +#![allow(clippy::module_inception)] +mod url_tests; +mod url_type_option; +mod url_type_option_entities; + +pub use url_type_option::*; +pub use url_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs new file mode 100644 index 0000000000..ba221853bb --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs @@ -0,0 +1,71 @@ +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::cell::{CellData, CellDataOperation}; + use crate::services::field::{FieldBuilder, URLCellDataParser}; + use crate::services::field::{URLCellDataPB, URLTypeOption}; + use flowy_grid_data_model::revision::FieldRevision; + + /// The expected_str will equal to the input string, but the expected_url will be empty if there's no + /// http url in the input string. + #[test] + fn url_type_option_does_not_contain_url_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_url(&type_option, "123", "123", "", &field_type, &field_rev); + } + + /// The expected_str will equal to the input string, but the expected_url will not be empty + /// if there's a http url in the input string. + #[test] + fn url_type_option_contains_url_test() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_url( + &type_option, + "AppFlowy website - https://www.appflowy.io", + "AppFlowy website - https://www.appflowy.io", + "https://www.appflowy.io/", + &field_type, + &field_rev, + ); + + assert_url( + &type_option, + "AppFlowy website appflowy.io", + "AppFlowy website appflowy.io", + "https://appflowy.io", + &field_type, + &field_rev, + ); + } + + fn assert_url( + type_option: &URLTypeOption, + input_str: &str, + expected_str: &str, + expected_url: &str, + field_type: &FieldType, + field_rev: &FieldRevision, + ) { + let encoded_data = type_option.apply_changeset(input_str.to_owned().into(), None).unwrap(); + let decode_cell_data = decode_cell_data(encoded_data, type_option, field_rev, field_type); + assert_eq!(expected_str.to_owned(), decode_cell_data.content); + assert_eq!(expected_url.to_owned(), decode_cell_data.url); + } + + fn decode_cell_data>>( + encoded_data: T, + type_option: &URLTypeOption, + field_rev: &FieldRevision, + field_type: &FieldType, + ) -> URLCellDataPB { + type_option + .decode_cell_data(encoded_data.into(), field_type, field_rev) + .unwrap() + .with_parser(URLCellDataParser()) + .unwrap() + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs new file mode 100644 index 0000000000..3fa49dac09 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs @@ -0,0 +1,95 @@ +use crate::entities::FieldType; +use crate::impl_type_option; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder, URLCellDataPB}; +use bytes::Bytes; +use fancy_regex::Regex; +use flowy_derive::ProtoBuf; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; + +#[derive(Default)] +pub struct URLTypeOptionBuilder(URLTypeOption); +impl_into_box_type_option_builder!(URLTypeOptionBuilder); +impl_builder_from_json_str_and_from_bytes!(URLTypeOptionBuilder, URLTypeOption); + +impl TypeOptionBuilder for URLTypeOptionBuilder { + fn field_type(&self) -> FieldType { + FieldType::URL + } + + fn entry(&self) -> &dyn TypeOptionDataEntry { + &self.0 + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] +pub struct URLTypeOption { + #[pb(index = 1)] + data: String, //It's not used yet. +} +impl_type_option!(URLTypeOption, FieldType::URL); + +impl CellDisplayable for URLTypeOption { + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_data: URLCellDataPB = cell_data.try_into_inner()?; + CellBytes::from(cell_data) + } +} + +impl CellDataOperation for URLTypeOption { + fn decode_cell_data( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult { + if !decoded_field_type.is_url() { + return Ok(CellBytes::default()); + } + self.display_data(cell_data, decoded_field_type, field_rev) + } + + fn apply_changeset( + &self, + changeset: CellDataChangeset, + _cell_rev: Option, + ) -> Result { + let content = changeset.try_into_inner()?; + let mut url = "".to_string(); + if let Ok(Some(m)) = URL_REGEX.find(&content) { + url = auto_append_scheme(m.as_str()); + } + URLCellDataPB { url, content }.to_json() + } +} + +fn auto_append_scheme(s: &str) -> String { + // Only support https scheme by now + match url::Url::parse(s) { + Ok(url) => { + if url.scheme() == "https" { + url.into() + } else { + format!("https://{}", s) + } + } + Err(_) => { + format!("https://{}", s) + } + } +} + +lazy_static! { + static ref URL_REGEX: Regex = Regex::new( + "[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)" + ) + .unwrap(); +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs new file mode 100644 index 0000000000..6ff77cea9a --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs @@ -0,0 +1,42 @@ +use crate::services::cell::{CellBytesParser, FromCellString}; +use bytes::Bytes; +use flowy_derive::ProtoBuf; +use flowy_error::{internal_error, FlowyResult}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] +pub struct URLCellDataPB { + #[pb(index = 1)] + pub url: String, + + #[pb(index = 2)] + pub content: String, +} + +impl URLCellDataPB { + pub fn new(s: &str) -> Self { + Self { + url: "".to_string(), + content: s.to_string(), + } + } + + pub(crate) fn to_json(&self) -> FlowyResult { + serde_json::to_string(self).map_err(internal_error) + } +} + +pub struct URLCellDataParser(); +impl CellBytesParser for URLCellDataParser { + type Object = URLCellDataPB; + + fn parse(&self, bytes: &Bytes) -> FlowyResult { + URLCellDataPB::try_from(bytes.as_ref()).map_err(internal_error) + } +} + +impl FromCellString for URLCellDataPB { + fn from_cell_str(s: &str) -> FlowyResult { + serde_json::from_str::(s).map_err(internal_error) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs index c7aa386fe9..dfadd1c5a0 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs @@ -1,4 +1,3 @@ mod cell_data_util; -pub use crate::services::field::select_option::*; pub use cell_data_util::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs index 6442dd4710..f3b1d72fb8 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/filter_service.rs @@ -1,10 +1,10 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; -use crate::entities::{FieldType, GridBlockChangeset}; +use crate::entities::{FieldType, GridBlockChangesetPB}; use crate::services::block_manager::GridBlockManager; use crate::services::cell::{AnyCellData, CellFilterOperation}; use crate::services::field::{ CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption, - SingleSelectTypeOption, URLTypeOption, + SingleSelectTypeOptionPB, URLTypeOption, }; use crate::services::filter::filter_cache::{ refresh_filter_cache, FilterCache, FilterId, FilterResult, FilterResultCache, @@ -90,7 +90,7 @@ impl GridFilterService { } } - let changeset = GridBlockChangeset { + let changeset = GridBlockChangesetPB { block_id: block.block_id, hide_rows, visible_rows, @@ -130,14 +130,10 @@ impl GridFilterService { let handler_id = self.grid_pad.read().await.grid_id(); let context = FilterTaskContext { blocks }; - Task { - handler_id, - id: task_id, - content: TaskContent::Filter(context), - } + Task::new(&handler_id, task_id, TaskContent::Filter(context)) } - async fn notify(&self, changesets: Vec) { + async fn notify(&self, changesets: Vec) { for changeset in changesets { send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridBlock) .payload(changeset) @@ -213,7 +209,7 @@ fn filter_cell( FieldType::SingleSelect => filter_cache.select_option_filter.get(&filter_id).and_then(|filter| { Some( field_rev - .get_type_option_entry::(field_type_rev)? + .get_type_option_entry::(field_type_rev)? .apply_filter(any_cell_data, filter.value()) .ok(), ) diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs index c5cf95d34b..24e21bbeb7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs @@ -1,5 +1,5 @@ use crate::entities::{CheckboxCondition, GridCheckboxFilter}; -use crate::services::cell::{AnyCellData, CellFilterOperation}; +use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; use crate::services::field::{CheckboxCellData, CheckboxTypeOption}; use flowy_error::FlowyResult; @@ -18,7 +18,8 @@ impl CellFilterOperation for CheckboxTypeOption { if !any_cell_data.is_checkbox() { return Ok(true); } - let checkbox_cell_data: CheckboxCellData = any_cell_data.try_into()?; + let cell_data: CellData = any_cell_data.into(); + let checkbox_cell_data = cell_data.try_into_inner()?; Ok(filter.is_visible(&checkbox_cell_data)) } } @@ -27,6 +28,7 @@ impl CellFilterOperation for CheckboxTypeOption { mod tests { use crate::entities::{CheckboxCondition, GridCheckboxFilter}; use crate::services::field::CheckboxCellData; + use std::str::FromStr; #[test] fn checkbox_filter_is_check_test() { @@ -34,7 +36,7 @@ mod tests { condition: CheckboxCondition::IsChecked, }; for (value, visible) in [("true", true), ("yes", true), ("false", false), ("no", false)] { - let data = CheckboxCellData(value.to_owned()); + let data = CheckboxCellData::from_str(value).unwrap(); assert_eq!(checkbox_filter.is_visible(&data), visible); } } @@ -45,7 +47,7 @@ mod tests { condition: CheckboxCondition::IsUnChecked, }; for (value, visible) in [("false", true), ("no", true), ("true", false), ("yes", false)] { - let data = CheckboxCellData(value.to_owned()); + let data = CheckboxCellData::from_str(value).unwrap(); assert_eq!(checkbox_filter.is_visible(&data), visible); } } diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs index ae920c4edb..46e2571438 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs @@ -1,5 +1,5 @@ use crate::entities::{DateFilterCondition, GridDateFilter}; -use crate::services::cell::{AnyCellData, CellFilterOperation}; +use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; use crate::services::field::{DateTimestamp, DateTypeOption}; use flowy_error::FlowyResult; @@ -34,7 +34,8 @@ impl CellFilterOperation for DateTypeOption { if !any_cell_data.is_date() { return Ok(true); } - let timestamp: DateTimestamp = any_cell_data.into(); + let cell_data: CellData = any_cell_data.into(); + let timestamp = cell_data.try_into_inner()?; Ok(filter.is_visible(timestamp)) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs index 7f3a5dd212..f44c1d2d62 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs @@ -47,9 +47,7 @@ impl CellFilterOperation for NumberTypeOption { #[cfg(test)] mod tests { use crate::entities::{GridNumberFilter, NumberFilterCondition}; - use crate::services::field::{NumberCellData, NumberFormat}; - use std::str::FromStr; #[test] fn number_filter_equal_test() { let number_filter = GridNumberFilter { @@ -58,7 +56,7 @@ mod tests { }; for (num_str, visible) in [("123", true), ("1234", false), ("", false)] { - let data = NumberCellData::from_str(num_str).unwrap(); + let data = NumberCellData::from_format_str(num_str, true, &NumberFormat::Num).unwrap(); assert_eq!(number_filter.is_visible(&data), visible); } @@ -75,7 +73,7 @@ mod tests { content: Some("12".to_owned()), }; for (num_str, visible) in [("123", true), ("10", false), ("30", true), ("", false)] { - let data = NumberCellData::from_str(num_str).unwrap(); + let data = NumberCellData::from_format_str(num_str, true, &NumberFormat::Num).unwrap(); assert_eq!(number_filter.is_visible(&data), visible); } } @@ -87,7 +85,7 @@ mod tests { content: Some("100".to_owned()), }; for (num_str, visible) in [("12", true), ("1234", false), ("30", true), ("", true)] { - let data = NumberCellData::from_str(num_str).unwrap(); + let data = NumberCellData::from_format_str(num_str, true, &NumberFormat::Num).unwrap(); assert_eq!(number_filter.is_visible(&data), visible); } } diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs index cd25bfef0e..e6cb9ff846 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs @@ -2,8 +2,8 @@ use crate::entities::{GridSelectOptionFilter, SelectOptionCondition}; use crate::services::cell::{AnyCellData, CellFilterOperation}; -use crate::services::field::select_option::{SelectOptionOperation, SelectedSelectOptions}; -use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; +use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOptionPB}; +use crate::services::field::{SelectOptionOperation, SelectedSelectOptions}; use flowy_error::FlowyResult; impl GridSelectOptionFilter { @@ -45,17 +45,17 @@ impl CellFilterOperation for MultiSelectTypeOption { return Ok(true); } - let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data)); + let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data.into())); Ok(filter.is_visible(&selected_options)) } } -impl CellFilterOperation for SingleSelectTypeOption { +impl CellFilterOperation for SingleSelectTypeOptionPB { fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridSelectOptionFilter) -> FlowyResult { if !any_cell_data.is_single_select() { return Ok(true); } - let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data)); + let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data.into())); Ok(filter.is_visible(&selected_options)) } } @@ -64,13 +64,13 @@ impl CellFilterOperation for SingleSelectTypeOption { mod tests { #![allow(clippy::all)] use crate::entities::{GridSelectOptionFilter, SelectOptionCondition}; - use crate::services::field::select_option::{SelectOption, SelectedSelectOptions}; + use crate::services::field::selection_type_option::{SelectOptionPB, SelectedSelectOptions}; #[test] fn select_option_filter_is_test() { - let option_1 = SelectOption::new("A"); - let option_2 = SelectOption::new("B"); - let option_3 = SelectOption::new("C"); + let option_1 = SelectOptionPB::new("A"); + let option_2 = SelectOptionPB::new("B"); + let option_3 = SelectOptionPB::new("C"); let filter_1 = GridSelectOptionFilter { condition: SelectOptionCondition::OptionIs, diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs index 45165e35e2..25f3902ceb 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs @@ -1,5 +1,5 @@ use crate::entities::{GridTextFilter, TextFilterCondition}; -use crate::services::cell::{AnyCellData, CellFilterOperation}; +use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; use crate::services::field::{RichTextTypeOption, TextCellData}; use flowy_error::FlowyResult; @@ -30,7 +30,8 @@ impl CellFilterOperation for RichTextTypeOption { return Ok(true); } - let text_cell_data: TextCellData = any_cell_data.try_into()?; + let cell_data: CellData = any_cell_data.into(); + let text_cell_data = cell_data.try_into_inner()?; Ok(filter.is_visible(text_cell_data)) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs index 50f9c815fc..15254d4713 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs @@ -1,5 +1,5 @@ use crate::entities::GridTextFilter; -use crate::services::cell::{AnyCellData, CellFilterOperation}; +use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; use crate::services::field::{TextCellData, URLTypeOption}; use flowy_error::FlowyResult; @@ -9,7 +9,8 @@ impl CellFilterOperation for URLTypeOption { return Ok(true); } - let text_cell_data: TextCellData = any_cell_data.try_into()?; + let cell_data: CellData = any_cell_data.into(); + let text_cell_data = cell_data.try_into_inner()?; Ok(filter.is_visible(&text_cell_data)) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 1d63b6dd69..44626b96fd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -1,15 +1,14 @@ use crate::dart_notification::{send_dart_notification, GridNotification}; -use crate::entities::CellIdentifier; +use crate::entities::GridCellIdParams; use crate::entities::*; use crate::manager::{GridTaskSchedulerRwLock, GridUser}; use crate::services::block_manager::GridBlockManager; -use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data}; +use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data, CellBytes}; use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_bytes, FieldBuilder}; use crate::services::filter::{GridFilterChangeset, GridFilterService}; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::row::{ - make_grid_blocks, make_row_from_row_rev, make_row_rev_from_context, make_rows_from_row_revs, - CreateRowRevisionBuilder, CreateRowRevisionPayload, GridBlockSnapshot, + make_grid_blocks, make_row_from_row_rev, make_rows_from_row_revs, GridBlockSnapshot, RowRevisionBuilder, }; use crate::services::setting::make_grid_setting; use bytes::Bytes; @@ -22,7 +21,7 @@ use flowy_sync::entities::revision::Revision; use flowy_sync::errors::CollaborateResult; use flowy_sync::util::make_delta_from_revisions; use lib_infra::future::FutureResult; -use lib_ot::core::PlainTextAttributes; +use lib_ot::core::PhantomAttributes; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; @@ -104,7 +103,6 @@ impl GridRevisionEditor { .modify(|grid| { let builder = type_option_builder_from_bytes(type_option_data, &field.field_type); let field_rev = FieldBuilder::from_field(field, builder).build(); - Ok(grid.create_field_rev(field_rev, start_field_id)?) }) .await?; @@ -189,8 +187,8 @@ impl GridRevisionEditor { pub async fn delete_field(&self, field_id: &str) -> FlowyResult<()> { let _ = self.modify(|grid_pad| Ok(grid_pad.delete_field_rev(field_id)?)).await?; - let field_order = FieldOrder::from(field_id); - let notified_changeset = GridFieldChangeset::delete(&self.grid_id, vec![field_order]); + let field_order = GridFieldIdPB::from(field_id); + let notified_changeset = GridFieldChangesetPB::delete(&self.grid_id, vec![field_order]); let _ = self.notify_did_update_grid(notified_changeset).await?; Ok(()) } @@ -269,14 +267,13 @@ impl GridRevisionEditor { Ok(()) } - pub async fn create_row(&self, start_row_id: Option) -> FlowyResult { + pub async fn create_row(&self, start_row_id: Option) -> FlowyResult { let field_revs = self.grid_pad.read().await.get_field_revs(None)?; let block_id = self.block_id().await?; // insert empty row below the row whose id is upper_row_id - let row_rev_ctx = CreateRowRevisionBuilder::new(&field_revs).build(); - let row_rev = make_row_rev_from_context(&block_id, row_rev_ctx); - let row_order = RowInfo::from(&row_rev); + let row_rev = RowRevisionBuilder::new(&field_revs).build(&block_id); + let row_order = GridRowPB::from(&row_rev); // insert the row let row_count = self.block_manager.create_row(&block_id, row_rev, start_row_id).await?; @@ -287,13 +284,12 @@ impl GridRevisionEditor { Ok(row_order) } - pub async fn insert_rows(&self, contexts: Vec) -> FlowyResult> { + pub async fn insert_rows(&self, row_revs: Vec) -> FlowyResult> { let block_id = self.block_id().await?; let mut rows_by_block_id: HashMap> = HashMap::new(); let mut row_orders = vec![]; - for ctx in contexts { - let row_rev = make_row_rev_from_context(&block_id, ctx); - row_orders.push(RowInfo::from(&row_rev)); + for row_rev in row_revs { + row_orders.push(GridRowPB::from(&row_rev)); rows_by_block_id .entry(block_id.clone()) .or_insert_with(Vec::new) @@ -307,13 +303,10 @@ impl GridRevisionEditor { } pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> { - let field_revs = self.get_field_revs(None).await?; - self.block_manager - .update_row(changeset, |row_rev| make_row_from_row_rev(&field_revs, row_rev)) - .await + self.block_manager.update_row(changeset, make_row_from_row_rev).await } - pub async fn get_rows(&self, block_id: &str) -> FlowyResult { + pub async fn get_rows(&self, block_id: &str) -> FlowyResult { let block_ids = vec![block_id.to_owned()]; let mut grid_block_snapshot = self.grid_block_snapshots(Some(block_ids)).await?; @@ -322,26 +315,20 @@ impl GridRevisionEditor { debug_assert_eq!(grid_block_snapshot.len(), 1); if grid_block_snapshot.len() == 1 { let snapshot = grid_block_snapshot.pop().unwrap(); - let field_revs = self.get_field_revs(None).await?; - let rows = make_rows_from_row_revs(&field_revs, &snapshot.row_revs); + let rows = make_rows_from_row_revs(&snapshot.row_revs); Ok(rows.into()) } else { Ok(vec![].into()) } } - pub async fn get_row(&self, row_id: &str) -> FlowyResult> { + pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult>> { match self.block_manager.get_row_rev(row_id).await? { None => Ok(None), - Some(row_rev) => { - let field_revs = self.get_field_revs(None).await?; - let row_revs = vec![row_rev]; - let mut rows = make_rows_from_row_revs(&field_revs, &row_revs); - debug_assert!(rows.len() == 1); - Ok(rows.pop()) - } + Some(row_rev) => Ok(Some(row_rev)), } } + pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> { let _ = self.block_manager.delete_row(row_id).await?; Ok(()) @@ -351,13 +338,17 @@ impl GridRevisionEditor { Ok(()) } - pub async fn get_cell(&self, params: &CellIdentifier) -> Option { + pub async fn get_cell(&self, params: &GridCellIdParams) -> Option { + let cell_bytes = self.get_cell_bytes(params).await?; + Some(GridCellPB::new(¶ms.field_id, cell_bytes.to_vec())) + } + + pub async fn get_cell_bytes(&self, params: &GridCellIdParams) -> Option { let field_rev = self.get_field_rev(¶ms.field_id).await?; let row_rev = self.block_manager.get_row_rev(¶ms.row_id).await.ok()??; let cell_rev = row_rev.cells.get(¶ms.field_id)?.clone(); - let data = decode_any_cell_data(cell_rev.data, &field_rev).data; - Some(Cell::new(¶ms.field_id, data)) + Some(decode_any_cell_data(cell_rev.data, &field_rev)) } pub async fn get_cell_rev(&self, row_id: &str, field_id: &str) -> FlowyResult> { @@ -372,12 +363,12 @@ impl GridRevisionEditor { } #[tracing::instrument(level = "trace", skip_all, err)] - pub async fn update_cell(&self, cell_changeset: CellChangeset) -> FlowyResult<()> { + pub async fn update_cell(&self, cell_changeset: CellChangesetPB) -> FlowyResult<()> { if cell_changeset.content.as_ref().is_none() { return Ok(()); } - let CellChangeset { + let CellChangesetPB { grid_id, row_id, field_id, @@ -395,8 +386,7 @@ impl GridRevisionEditor { let cell_rev = self.get_cell_rev(&row_id, &field_id).await?; // Update the changeset.data property with the return value. content = Some(apply_cell_data_changeset(content.unwrap(), cell_rev, field_rev)?); - let field_revs = self.get_field_revs(None).await?; - let cell_changeset = CellChangeset { + let cell_changeset = CellChangesetPB { grid_id, row_id, field_id, @@ -404,14 +394,14 @@ impl GridRevisionEditor { }; let _ = self .block_manager - .update_cell(cell_changeset, |row_rev| make_row_from_row_rev(&field_revs, row_rev)) + .update_cell(cell_changeset, make_row_from_row_rev) .await?; Ok(()) } } } - pub async fn get_blocks(&self, block_ids: Option>) -> FlowyResult { + pub async fn get_blocks(&self, block_ids: Option>) -> FlowyResult { let block_snapshots = self.grid_block_snapshots(block_ids.clone()).await?; make_grid_blocks(block_ids, block_snapshots) } @@ -421,7 +411,7 @@ impl GridRevisionEditor { Ok(block_meta_revs) } - pub async fn delete_rows(&self, row_orders: Vec) -> FlowyResult<()> { + pub async fn delete_rows(&self, row_orders: Vec) -> FlowyResult<()> { let changesets = self.block_manager.delete_rows(row_orders).await?; for changeset in changesets { let _ = self.update_block(changeset).await?; @@ -429,31 +419,31 @@ impl GridRevisionEditor { Ok(()) } - pub async fn get_grid_data(&self) -> FlowyResult { + pub async fn get_grid_data(&self) -> FlowyResult { let pad_read_guard = self.grid_pad.read().await; let field_orders = pad_read_guard .get_field_revs(None)? .iter() - .map(FieldOrder::from) + .map(GridFieldIdPB::from) .collect(); let mut block_orders = vec![]; for block_rev in pad_read_guard.get_block_meta_revs() { let row_orders = self.block_manager.get_row_orders(&block_rev.block_id).await?; - let block_order = GridBlock { + let block_order = GridBlockPB { id: block_rev.block_id.clone(), - row_infos: row_orders, + rows: row_orders, }; block_orders.push(block_order); } - Ok(Grid { + Ok(GridPB { id: self.grid_id.clone(), - field_orders, + fields: field_orders, blocks: block_orders, }) } - pub async fn get_grid_setting(&self) -> FlowyResult { + pub async fn get_grid_setting(&self) -> FlowyResult { let read_guard = self.grid_pad.read().await; let grid_setting_rev = read_guard.get_grid_setting_rev(); let field_revs = read_guard.get_field_revs(None)?; @@ -504,11 +494,11 @@ impl GridRevisionEditor { pub async fn move_item(&self, params: MoveItemParams) -> FlowyResult<()> { match params.ty { - MoveItemType::MoveField => { + MoveItemTypePB::MoveField => { self.move_field(¶ms.item_id, params.from_index, params.to_index) .await } - MoveItemType::MoveRow => self.move_row(¶ms.item_id, params.from_index, params.to_index).await, + MoveItemTypePB::MoveRow => self.move_row(¶ms.item_id, params.from_index, params.to_index).await, } } @@ -517,9 +507,9 @@ impl GridRevisionEditor { .modify(|grid_pad| Ok(grid_pad.move_field(field_id, from as usize, to as usize)?)) .await?; if let Some((index, field_rev)) = self.grid_pad.read().await.get_field_rev(field_id) { - let delete_field_order = FieldOrder::from(field_id); - let insert_field = IndexField::from_field_rev(field_rev, index); - let notified_changeset = GridFieldChangeset { + let delete_field_order = GridFieldIdPB::from(field_id); + let insert_field = IndexFieldPB::from_field_rev(field_rev, index); + let notified_changeset = GridFieldChangesetPB { grid_id: self.grid_id.clone(), inserted_fields: vec![insert_field], deleted_fields: vec![delete_field_order], @@ -561,7 +551,7 @@ impl GridRevisionEditor { drop(grid_pad); Ok(BuildGridContext { - field_revs: duplicated_fields, + field_revs: duplicated_fields.into_iter().map(Arc::new).collect(), blocks: duplicated_blocks, blocks_meta_data, }) @@ -582,7 +572,7 @@ impl GridRevisionEditor { let GridChangeset { delta, md5 } = change; let user_id = self.user.user_id()?; let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); - let delta_data = delta.to_delta_bytes(); + let delta_data = delta.json_bytes(); let revision = Revision::new( &self.rev_manager.object_id, base_rev_id, @@ -591,10 +581,7 @@ impl GridRevisionEditor { &user_id, md5, ); - let _ = self - .rev_manager - .add_local_revision(&revision, Box::new(GridRevisionCompactor())) - .await?; + let _ = self.rev_manager.add_local_revision(&revision).await?; Ok(()) } @@ -608,8 +595,8 @@ impl GridRevisionEditor { #[tracing::instrument(level = "trace", skip_all, err)] async fn notify_did_insert_grid_field(&self, field_id: &str) -> FlowyResult<()> { if let Some((index, field_rev)) = self.grid_pad.read().await.get_field_rev(field_id) { - let index_field = IndexField::from_field_rev(field_rev, index); - let notified_changeset = GridFieldChangeset::insert(&self.grid_id, vec![index_field]); + let index_field = IndexFieldPB::from_field_rev(field_rev, index); + let notified_changeset = GridFieldChangesetPB::insert(&self.grid_id, vec![index_field]); let _ = self.notify_did_update_grid(notified_changeset).await?; } Ok(()) @@ -624,8 +611,8 @@ impl GridRevisionEditor { .get_field_rev(field_id) .map(|(index, field)| (index, field.clone())) { - let updated_field = Field::from(field_rev); - let notified_changeset = GridFieldChangeset::update(&self.grid_id, vec![updated_field.clone()]); + let updated_field = GridFieldPB::from(field_rev); + let notified_changeset = GridFieldChangesetPB::update(&self.grid_id, vec![updated_field.clone()]); let _ = self.notify_did_update_grid(notified_changeset).await?; send_dart_notification(field_id, GridNotification::DidUpdateField) @@ -636,7 +623,7 @@ impl GridRevisionEditor { Ok(()) } - async fn notify_did_update_grid(&self, changeset: GridFieldChangeset) -> FlowyResult<()> { + async fn notify_did_update_grid(&self, changeset: GridFieldChangesetPB) -> FlowyResult<()> { send_dart_notification(&self.grid_id, GridNotification::DidUpdateGridField) .payload(changeset) .send(); @@ -655,8 +642,8 @@ pub struct GridPadBuilder(); impl RevisionObjectBuilder for GridPadBuilder { type Output = GridRevisionPad; - fn build_object(object_id: &str, revisions: Vec) -> FlowyResult { - let pad = GridRevisionPad::from_revisions(object_id, revisions)?; + fn build_object(_object_id: &str, revisions: Vec) -> FlowyResult { + let pad = GridRevisionPad::from_revisions(revisions)?; Ok(pad) } } @@ -673,11 +660,11 @@ impl RevisionCloudService for GridRevisionCloudService { } } -struct GridRevisionCompactor(); +pub struct GridRevisionCompactor(); impl RevisionCompactor for GridRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { - let delta = make_delta_from_revisions::(revisions)?; - Ok(delta.to_delta_bytes()) + let delta = make_delta_from_revisions::(revisions)?; + Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor_task.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor_task.rs index 1b3a364833..0338730818 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor_task.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor_task.rs @@ -1,23 +1,23 @@ use crate::manager::GridTaskSchedulerRwLock; use crate::services::grid_editor::GridRevisionEditor; -use crate::services::tasks::{GridTaskHandler, Task, TaskContent, TaskHandlerId, TaskId}; +use crate::services::tasks::{GridTaskHandler, Task, TaskContent, TaskId}; use flowy_error::FlowyError; use futures::future::BoxFuture; use lib_infra::future::BoxResultFuture; pub(crate) trait GridServiceTaskScheduler: Send + Sync + 'static { fn gen_task_id(&self) -> BoxFuture; - fn register_task(&self, task: Task) -> BoxFuture<()>; + fn add_task(&self, task: Task) -> BoxFuture<()>; } impl GridTaskHandler for GridRevisionEditor { - fn handler_id(&self) -> &TaskHandlerId { + fn handler_id(&self) -> &str { &self.grid_id } - fn process_task(&self, task: Task) -> BoxResultFuture<(), FlowyError> { + fn process_content(&self, content: TaskContent) -> BoxResultFuture<(), FlowyError> { Box::pin(async move { - match task.content { + match content { TaskContent::Snapshot => {} TaskContent::Filter(context) => self.filter_service.process(context).await?, } @@ -32,10 +32,10 @@ impl GridServiceTaskScheduler for GridTaskSchedulerRwLock { Box::pin(async move { this.read().await.next_task_id() }) } - fn register_task(&self, task: Task) -> BoxFuture<()> { + fn add_task(&self, task: Task) -> BoxFuture<()> { let this = self.clone(); Box::pin(async move { - this.write().await.register_task(task); + this.write().await.add_task(task); }) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/persistence/block_index.rs b/frontend/rust-lib/flowy-grid/src/services/persistence/block_index.rs index c62dc502ad..798819fb98 100644 --- a/frontend/rust-lib/flowy-grid/src/services/persistence/block_index.rs +++ b/frontend/rust-lib/flowy-grid/src/services/persistence/block_index.rs @@ -7,6 +7,7 @@ use flowy_database::{ use flowy_error::FlowyResult; use std::sync::Arc; +/// Allow getting the block id from row id. pub struct BlockIndexCache { database: Arc, } diff --git a/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs b/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs new file mode 100644 index 0000000000..bf3aa7adfb --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/persistence/migration.rs @@ -0,0 +1,113 @@ +use crate::manager::GridUser; + +use crate::services::persistence::GridDatabase; +use flowy_database::kv::KV; +use flowy_error::FlowyResult; +use flowy_grid_data_model::revision::GridRevision; +use flowy_revision::disk::{RevisionRecord, SQLiteGridRevisionPersistence}; +use flowy_revision::{mk_grid_block_revision_disk_cache, RevisionLoader, RevisionPersistence}; +use flowy_sync::client_grid::{make_grid_rev_json_str, GridRevisionPad}; +use flowy_sync::entities::revision::Revision; + +use lib_ot::core::TextDeltaBuilder; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; +use std::sync::Arc; + +pub(crate) struct GridMigration { + user: Arc, + database: Arc, +} + +impl GridMigration { + pub fn new(user: Arc, database: Arc) -> Self { + Self { user, database } + } + + pub async fn migration_grid_if_need(&self, grid_id: &str) -> FlowyResult<()> { + match KV::get_str(grid_id) { + None => { + let _ = self.reset_grid_rev(grid_id).await?; + let _ = self.save_migrate_record(grid_id)?; + } + Some(s) => { + let mut record = MigrationGridRecord::from_str(&s)?; + let empty_json = self.empty_grid_rev_json()?; + if record.len < empty_json.len() { + let _ = self.reset_grid_rev(grid_id).await?; + record.len = empty_json.len(); + KV::set_str(grid_id, record.to_string()); + } + } + } + Ok(()) + } + + async fn reset_grid_rev(&self, grid_id: &str) -> FlowyResult<()> { + let user_id = self.user.user_id()?; + let pool = self.database.db_pool()?; + let grid_rev_pad = self.get_grid_revision_pad(grid_id).await?; + let json = grid_rev_pad.json_str()?; + let delta_data = TextDeltaBuilder::new().insert(&json).build().json_bytes(); + let revision = Revision::initial_revision(&user_id, grid_id, delta_data); + let record = RevisionRecord::new(revision); + // + let disk_cache = mk_grid_block_revision_disk_cache(&user_id, pool); + let _ = disk_cache.delete_and_insert_records(grid_id, None, vec![record]); + Ok(()) + } + + fn save_migrate_record(&self, grid_id: &str) -> FlowyResult<()> { + let empty_json_str = self.empty_grid_rev_json()?; + let record = MigrationGridRecord { + grid_id: grid_id.to_owned(), + len: empty_json_str.len(), + }; + KV::set_str(grid_id, record.to_string()); + Ok(()) + } + + fn empty_grid_rev_json(&self) -> FlowyResult { + let empty_grid_rev = GridRevision::default(); + let empty_json = make_grid_rev_json_str(&empty_grid_rev)?; + Ok(empty_json) + } + + async fn get_grid_revision_pad(&self, grid_id: &str) -> FlowyResult { + let pool = self.database.db_pool()?; + let user_id = self.user.user_id()?; + let disk_cache = SQLiteGridRevisionPersistence::new(&user_id, pool); + let rev_persistence = Arc::new(RevisionPersistence::new(&user_id, grid_id, disk_cache)); + let (revisions, _) = RevisionLoader { + object_id: grid_id.to_owned(), + user_id, + cloud: None, + rev_persistence, + } + .load() + .await?; + + let pad = GridRevisionPad::from_revisions(revisions)?; + Ok(pad) + } +} + +#[derive(Serialize, Deserialize)] +struct MigrationGridRecord { + grid_id: String, + len: usize, +} + +impl FromStr for MigrationGridRecord { + type Err = serde_json::Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str::(s) + } +} + +impl ToString for MigrationGridRecord { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "".to_string()) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/persistence/mod.rs b/frontend/rust-lib/flowy-grid/src/services/persistence/mod.rs index d6167cf8e6..7bd196acc7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/persistence/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/persistence/mod.rs @@ -4,6 +4,7 @@ use std::sync::Arc; pub mod block_index; pub mod kv; +pub mod migration; pub trait GridDatabase: Send + Sync { fn db_pool(&self) -> Result, FlowyError>; diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs index 3302276cd8..43153f1311 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs @@ -1,22 +1,22 @@ use crate::services::cell::apply_cell_data_changeset; -use crate::services::field::select_option::SelectOptionCellChangeset; +use crate::services::field::SelectOptionCellChangeset; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT}; use indexmap::IndexMap; use std::collections::HashMap; use std::sync::Arc; -pub struct CreateRowRevisionBuilder<'a> { - field_rev_map: HashMap<&'a String, &'a Arc>, +pub struct RowRevisionBuilder<'a> { + field_rev_map: HashMap<&'a String, Arc>, payload: CreateRowRevisionPayload, } -impl<'a> CreateRowRevisionBuilder<'a> { +impl<'a> RowRevisionBuilder<'a> { pub fn new(fields: &'a [Arc]) -> Self { let field_rev_map = fields .iter() - .map(|field| (&field.id, field)) - .collect::>>(); + .map(|field| (&field.id, field.clone())) + .collect::>>(); let payload = CreateRowRevisionPayload { row_id: gen_row_id(), @@ -28,10 +28,10 @@ impl<'a> CreateRowRevisionBuilder<'a> { Self { field_rev_map, payload } } - pub fn add_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { + pub fn insert_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { match self.field_rev_map.get(&field_id.to_owned()) { None => { - let msg = format!("Invalid field_id: {}", field_id); + let msg = format!("Can't find the field with id: {}", field_id); Err(FlowyError::internal().context(msg)) } Some(field_rev) => { @@ -43,7 +43,7 @@ impl<'a> CreateRowRevisionBuilder<'a> { } } - pub fn add_select_option_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { + pub fn insert_select_option_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { match self.field_rev_map.get(&field_id.to_owned()) { None => { let msg = format!("Invalid field_id: {}", field_id); @@ -71,18 +71,14 @@ impl<'a> CreateRowRevisionBuilder<'a> { self } - pub fn build(self) -> CreateRowRevisionPayload { - self.payload - } -} - -pub fn make_row_rev_from_context(block_id: &str, payload: CreateRowRevisionPayload) -> RowRevision { - RowRevision { - id: payload.row_id, - block_id: block_id.to_owned(), - cells: payload.cell_by_field_id, - height: payload.height, - visibility: payload.visibility, + pub fn build(self, block_id: &str) -> RowRevision { + RowRevision { + id: self.payload.row_id, + block_id: block_id.to_owned(), + cells: self.payload.cell_by_field_id, + height: self.payload.height, + visibility: self.payload.visibility, + } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs index 1b9ce80101..3a5aa58c6d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs @@ -1,6 +1,6 @@ -use crate::entities::{GridBlock, RepeatedGridBlock, Row, RowInfo}; +use crate::entities::{GridBlockPB, GridRowPB, RepeatedGridBlockPB}; use flowy_error::FlowyResult; -use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; +use flowy_grid_data_model::revision::RowRevision; use std::collections::HashMap; use std::sync::Arc; @@ -9,15 +9,15 @@ pub struct GridBlockSnapshot { pub row_revs: Vec>, } -pub(crate) fn block_from_row_orders(row_orders: Vec) -> Vec { - let mut map: HashMap = HashMap::new(); +pub(crate) fn block_from_row_orders(row_orders: Vec) -> Vec { + let mut map: HashMap = HashMap::new(); row_orders.into_iter().for_each(|row_info| { // Memory Optimization: escape clone block_id let block_id = row_info.block_id().to_owned(); let cloned_block_id = block_id.clone(); map.entry(block_id) - .or_insert_with(|| GridBlock::new(&cloned_block_id, vec![])) - .row_infos + .or_insert_with(|| GridBlockPB::new(&cloned_block_id, vec![])) + .rows .push(row_info); }); map.into_values().collect::>() @@ -35,32 +35,19 @@ pub(crate) fn block_from_row_orders(row_orders: Vec) -> Vec // Some((field_id, cell)) // } -pub(crate) fn make_row_orders_from_row_revs(row_revs: &[Arc]) -> Vec { - row_revs.iter().map(RowInfo::from).collect::>() +pub(crate) fn make_row_orders_from_row_revs(row_revs: &[Arc]) -> Vec { + row_revs.iter().map(GridRowPB::from).collect::>() } -pub(crate) fn make_row_from_row_rev(fields: &[Arc], row_rev: Arc) -> Option { - make_rows_from_row_revs(fields, &[row_rev]).pop() +pub(crate) fn make_row_from_row_rev(row_rev: Arc) -> Option { + make_rows_from_row_revs(&[row_rev]).pop() } -pub(crate) fn make_rows_from_row_revs(_fields: &[Arc], row_revs: &[Arc]) -> Vec { - // let field_rev_map = fields - // .iter() - // .map(|field_rev| (&field_rev.id, field_rev)) - // .collect::>(); - - let make_row = |row_rev: &Arc| { - // let cell_by_field_id = row_rev - // .cells - // .clone() - // .into_iter() - // .flat_map(|(field_id, cell_rev)| make_cell_by_field_id(&field_rev_map, field_id, cell_rev)) - // .collect::>(); - - Row { - id: row_rev.id.clone(), - height: row_rev.height, - } +pub(crate) fn make_rows_from_row_revs(row_revs: &[Arc]) -> Vec { + let make_row = |row_rev: &Arc| GridRowPB { + block_id: row_rev.block_id.clone(), + id: row_rev.id.clone(), + height: row_rev.height, }; row_revs.iter().map(make_row).collect::>() @@ -69,15 +56,15 @@ pub(crate) fn make_rows_from_row_revs(_fields: &[Arc], row_revs: pub(crate) fn make_grid_blocks( block_ids: Option>, block_snapshots: Vec, -) -> FlowyResult { +) -> FlowyResult { match block_ids { None => Ok(block_snapshots .into_iter() .map(|snapshot| { let row_orders = make_row_orders_from_row_revs(&snapshot.row_revs); - GridBlock::new(&snapshot.block_id, row_orders) + GridBlockPB::new(&snapshot.block_id, row_orders) }) - .collect::>() + .collect::>() .into()), Some(block_ids) => { let block_meta_data_map: HashMap<&String, &Vec>> = block_snapshots @@ -91,7 +78,7 @@ pub(crate) fn make_grid_blocks( None => {} Some(row_revs) => { let row_orders = make_row_orders_from_row_revs(row_revs); - grid_blocks.push(GridBlock::new(&block_id, row_orders)); + grid_blocks.push(GridBlockPB::new(&block_id, row_orders)); } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs index 6880db5277..be283981cf 100644 --- a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs @@ -1,5 +1,5 @@ use crate::entities::{ - GridLayout, GridLayoutType, GridSetting, RepeatedGridFilter, RepeatedGridGroup, RepeatedGridSort, + GridLayoutPB, GridLayoutType, GridSettingPB, RepeatedGridFilterPB, RepeatedGridGroupPB, RepeatedGridSortPB, }; use flowy_grid_data_model::revision::{FieldRevision, GridSettingRevision}; use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, GridSettingChangesetParams}; @@ -40,7 +40,7 @@ impl GridSettingChangesetBuilder { } } -pub fn make_grid_setting(grid_setting_rev: &GridSettingRevision, field_revs: &[Arc]) -> GridSetting { +pub fn make_grid_setting(grid_setting_rev: &GridSettingRevision, field_revs: &[Arc]) -> GridSettingPB { let current_layout_type: GridLayoutType = grid_setting_rev.layout.clone().into(); let filters_by_field_id = grid_setting_rev .get_all_filter(field_revs) @@ -48,7 +48,7 @@ pub fn make_grid_setting(grid_setting_rev: &GridSettingRevision, field_revs: &[A filters_by_field_id .into_iter() .map(|(k, v)| (k, v.into())) - .collect::>() + .collect::>() }) .unwrap_or_default(); let groups_by_field_id = grid_setting_rev @@ -57,7 +57,7 @@ pub fn make_grid_setting(grid_setting_rev: &GridSettingRevision, field_revs: &[A groups_by_field_id .into_iter() .map(|(k, v)| (k, v.into())) - .collect::>() + .collect::>() }) .unwrap_or_default(); let sorts_by_field_id = grid_setting_rev @@ -66,12 +66,12 @@ pub fn make_grid_setting(grid_setting_rev: &GridSettingRevision, field_revs: &[A sorts_by_field_id .into_iter() .map(|(k, v)| (k, v.into())) - .collect::>() + .collect::>() }) .unwrap_or_default(); - GridSetting { - layouts: GridLayout::all(), + GridSettingPB { + layouts: GridLayoutPB::all(), current_layout_type, filters_by_field_id, groups_by_field_id, diff --git a/frontend/rust-lib/flowy-grid/src/services/tasks/queue.rs b/frontend/rust-lib/flowy-grid/src/services/tasks/queue.rs index 537f789f9b..1ba97afd9a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/tasks/queue.rs +++ b/frontend/rust-lib/flowy-grid/src/services/tasks/queue.rs @@ -20,7 +20,12 @@ impl GridTaskQueue { } pub(crate) fn push(&mut self, task: &Task) { - let task_type = match task.content { + if task.content.is_none() { + tracing::warn!("Ignore task: {} with empty content", task.id); + return; + } + + let task_type = match task.content.as_ref().unwrap() { TaskContent::Snapshot => TaskType::Snapshot, TaskContent::Filter { .. } => TaskType::Filter, }; @@ -28,7 +33,7 @@ impl GridTaskQueue { ty: task_type, id: task.id, }; - match self.index_tasks.entry("1".to_owned()) { + match self.index_tasks.entry(task.handler_id.clone()) { Entry::Occupied(entry) => { let mut list = entry.get().borrow_mut(); assert!(list.peek().map(|old_id| pending_task.id >= old_id.id).unwrap_or(true)); @@ -44,6 +49,11 @@ impl GridTaskQueue { } } + #[allow(dead_code)] + pub(crate) fn clear(&mut self) { + self.queue.clear(); + } + pub(crate) fn mut_head(&mut self, mut f: F) -> Option where F: FnMut(&mut TaskList) -> Option, diff --git a/frontend/rust-lib/flowy-grid/src/services/tasks/runner.rs b/frontend/rust-lib/flowy-grid/src/services/tasks/runner.rs index 713fc15c86..121329edfb 100644 --- a/frontend/rust-lib/flowy-grid/src/services/tasks/runner.rs +++ b/frontend/rust-lib/flowy-grid/src/services/tasks/runner.rs @@ -1,4 +1,5 @@ use crate::services::tasks::scheduler::GridTaskScheduler; + use std::sync::Arc; use std::time::Duration; use tokio::sync::{watch, RwLock}; @@ -7,13 +8,13 @@ use tokio::time::interval; pub struct GridTaskRunner { scheduler: Arc>, debounce_duration: Duration, - notifier: Option>, + notifier: Option>, } impl GridTaskRunner { pub fn new( scheduler: Arc>, - notifier: watch::Receiver<()>, + notifier: watch::Receiver, debounce_duration: Duration, ) -> Self { Self { @@ -34,12 +35,13 @@ impl GridTaskRunner { // The runner will be stopped if the corresponding Sender drop. break; } + + if *notifier.borrow() { + break; + } let mut interval = interval(self.debounce_duration); interval.tick().await; - - if let Err(e) = self.scheduler.write().await.process_next_task().await { - tracing::error!("{:?}", e); - } + let _ = self.scheduler.write().await.process_next_task().await; } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/tasks/scheduler.rs b/frontend/rust-lib/flowy-grid/src/services/tasks/scheduler.rs index 14494842ec..73ba298d9b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/tasks/scheduler.rs +++ b/frontend/rust-lib/flowy-grid/src/services/tasks/scheduler.rs @@ -3,8 +3,8 @@ use crate::services::tasks::runner::GridTaskRunner; use crate::services::tasks::store::GridTaskStore; use crate::services::tasks::task::Task; -use crate::services::tasks::TaskId; -use flowy_error::{FlowyError, FlowyResult}; +use crate::services::tasks::{TaskContent, TaskId, TaskStatus}; +use flowy_error::FlowyError; use lib_infra::future::BoxResultFuture; use std::collections::HashMap; use std::sync::Arc; @@ -12,21 +12,21 @@ use std::time::Duration; use tokio::sync::{watch, RwLock}; pub(crate) trait GridTaskHandler: Send + Sync + 'static { - fn handler_id(&self) -> &TaskHandlerId; + fn handler_id(&self) -> &str; - fn process_task(&self, task: Task) -> BoxResultFuture<(), FlowyError>; + fn process_content(&self, content: TaskContent) -> BoxResultFuture<(), FlowyError>; } pub struct GridTaskScheduler { queue: GridTaskQueue, store: GridTaskStore, - notifier: watch::Sender<()>, + notifier: watch::Sender, handlers: HashMap>, } impl GridTaskScheduler { pub(crate) fn new() -> Arc> { - let (notifier, rx) = watch::channel(()); + let (notifier, rx) = watch::channel(false); let scheduler = Self { queue: GridTaskQueue::new(), @@ -57,25 +57,38 @@ impl GridTaskScheduler { let _ = self.handlers.remove(handler_id.as_ref()); } - pub(crate) async fn process_next_task(&mut self) -> FlowyResult<()> { - let mut get_next_task = || { - let pending_task = self.queue.mut_head(|list| list.pop())?; - let task = self.store.remove_task(&pending_task.id)?; - Some(task) - }; - - if let Some(task) = get_next_task() { - match self.handlers.get(&task.handler_id) { - None => {} - Some(handler) => { - let _ = handler.process_task(task).await; - } - } - } - Ok(()) + #[allow(dead_code)] + pub(crate) fn stop(&mut self) { + let _ = self.notifier.send(true); + self.queue.clear(); + self.store.clear(); } - pub(crate) fn register_task(&mut self, task: Task) { + pub(crate) async fn process_next_task(&mut self) -> Option<()> { + let pending_task = self.queue.mut_head(|list| list.pop())?; + let mut task = self.store.remove_task(&pending_task.id)?; + let handler = self.handlers.get(&task.handler_id)?; + + let ret = task.ret.take()?; + let content = task.content.take()?; + + task.set_status(TaskStatus::Processing); + let _ = match handler.process_content(content).await { + Ok(_) => { + task.set_status(TaskStatus::Done); + let _ = ret.send(task.into()); + } + Err(e) => { + tracing::error!("Process task failed: {:?}", e); + task.set_status(TaskStatus::Failure); + let _ = ret.send(task.into()); + } + }; + self.notify(); + None + } + + pub(crate) fn add_task(&mut self, task: Task) { assert!(!task.is_finished()); self.queue.push(&task); self.store.insert_task(task); @@ -87,6 +100,87 @@ impl GridTaskScheduler { } pub(crate) fn notify(&self) { - let _ = self.notifier.send(()); + let _ = self.notifier.send(false); + } +} + +#[cfg(test)] +mod tests { + use crate::services::grid_editor_task::GridServiceTaskScheduler; + use crate::services::tasks::{GridTaskHandler, GridTaskScheduler, Task, TaskContent, TaskStatus}; + use flowy_error::FlowyError; + use lib_infra::future::BoxResultFuture; + use std::sync::Arc; + use std::time::Duration; + use tokio::time::interval; + + #[tokio::test] + async fn task_scheduler_snapshot_task_test() { + let scheduler = GridTaskScheduler::new(); + scheduler + .write() + .await + .register_handler(Arc::new(MockGridTaskHandler())); + + let task_id = scheduler.gen_task_id().await; + let mut task = Task::new("1", task_id, TaskContent::Snapshot); + let rx = task.rx.take().unwrap(); + scheduler.write().await.add_task(task); + assert_eq!(rx.await.unwrap().status, TaskStatus::Done); + } + + #[tokio::test] + async fn task_scheduler_snapshot_task_cancel_test() { + let scheduler = GridTaskScheduler::new(); + scheduler + .write() + .await + .register_handler(Arc::new(MockGridTaskHandler())); + + let task_id = scheduler.gen_task_id().await; + let mut task = Task::new("1", task_id, TaskContent::Snapshot); + let rx = task.rx.take().unwrap(); + scheduler.write().await.add_task(task); + scheduler.write().await.stop(); + + assert_eq!(rx.await.unwrap().status, TaskStatus::Cancel); + } + + #[tokio::test] + async fn task_scheduler_multi_task_test() { + let scheduler = GridTaskScheduler::new(); + scheduler + .write() + .await + .register_handler(Arc::new(MockGridTaskHandler())); + + let task_id = scheduler.gen_task_id().await; + let mut task_1 = Task::new("1", task_id, TaskContent::Snapshot); + let rx_1 = task_1.rx.take().unwrap(); + + let task_id = scheduler.gen_task_id().await; + let mut task_2 = Task::new("1", task_id, TaskContent::Snapshot); + let rx_2 = task_2.rx.take().unwrap(); + + scheduler.write().await.add_task(task_1); + scheduler.write().await.add_task(task_2); + + assert_eq!(rx_1.await.unwrap().status, TaskStatus::Done); + assert_eq!(rx_2.await.unwrap().status, TaskStatus::Done); + } + struct MockGridTaskHandler(); + impl GridTaskHandler for MockGridTaskHandler { + fn handler_id(&self) -> &str { + "1" + } + + fn process_content(&self, _content: TaskContent) -> BoxResultFuture<(), FlowyError> { + Box::pin(async move { + let mut interval = interval(Duration::from_secs(1)); + interval.tick().await; + interval.tick().await; + Ok(()) + }) + } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/tasks/store.rs b/frontend/rust-lib/flowy-grid/src/services/tasks/store.rs index 21aae60bc4..9f14889e4d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/tasks/store.rs +++ b/frontend/rust-lib/flowy-grid/src/services/tasks/store.rs @@ -1,6 +1,7 @@ use crate::services::tasks::task::Task; -use crate::services::tasks::TaskId; +use crate::services::tasks::{TaskId, TaskStatus}; use std::collections::HashMap; +use std::mem; use std::sync::atomic::AtomicU32; use std::sync::atomic::Ordering::SeqCst; @@ -25,6 +26,18 @@ impl GridTaskStore { self.tasks.remove(task_id) } + #[allow(dead_code)] + pub(crate) fn clear(&mut self) { + let tasks = mem::take(&mut self.tasks); + tasks.into_values().for_each(|mut task| { + if task.ret.is_some() { + let ret = task.ret.take().unwrap(); + task.set_status(TaskStatus::Cancel); + let _ = ret.send(task.into()); + } + }); + } + pub(crate) fn next_task_id(&self) -> TaskId { let _ = self.task_id_counter.fetch_add(1, SeqCst); self.task_id_counter.load(SeqCst) diff --git a/frontend/rust-lib/flowy-grid/src/services/tasks/task.rs b/frontend/rust-lib/flowy-grid/src/services/tasks/task.rs index 92575dabdc..92950b02aa 100644 --- a/frontend/rust-lib/flowy-grid/src/services/tasks/task.rs +++ b/frontend/rust-lib/flowy-grid/src/services/tasks/task.rs @@ -1,3 +1,5 @@ +#![allow(clippy::all)] +#![allow(dead_code)] use crate::services::row::GridBlockSnapshot; use crate::services::tasks::queue::TaskHandlerId; use std::cmp::Ordering; @@ -60,14 +62,59 @@ pub(crate) enum TaskContent { Filter(FilterTaskContext), } +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum TaskStatus { + Pending, + Processing, + Done, + Failure, + Cancel, +} + pub(crate) struct Task { - pub handler_id: TaskHandlerId, pub id: TaskId, - pub content: TaskContent, + pub handler_id: TaskHandlerId, + pub content: Option, + status: TaskStatus, + pub ret: Option>, + pub rx: Option>, +} + +pub(crate) struct TaskResult { + pub id: TaskId, + pub(crate) status: TaskStatus, +} + +impl std::convert::From for TaskResult { + fn from(task: Task) -> Self { + TaskResult { + id: task.id, + status: task.status, + } + } } impl Task { + pub fn new(handler_id: &str, id: TaskId, content: TaskContent) -> Self { + let (ret, rx) = tokio::sync::oneshot::channel(); + Self { + handler_id: handler_id.to_owned(), + id, + content: Some(content), + ret: Some(ret), + rx: Some(rx), + status: TaskStatus::Pending, + } + } + + pub fn set_status(&mut self, status: TaskStatus) { + self.status = status; + } + pub fn is_finished(&self) -> bool { - todo!() + match self.status { + TaskStatus::Done => true, + _ => false, + } } } diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index e0055d09b6..3b48e313a9 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -4,29 +4,29 @@ use flowy_grid_data_model::revision::BuildGridContext; use flowy_sync::client_grid::GridBuilder; pub fn make_default_grid() -> BuildGridContext { + let mut grid_builder = GridBuilder::new(); // text let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) .name("Name") .visibility(true) .primary(true) .build(); + grid_builder.add_field(text_field); // single select let single_select = SingleSelectTypeOptionBuilder::default(); let single_select_field = FieldBuilder::new(single_select).name("Type").visibility(true).build(); + grid_builder.add_field(single_select_field); // checkbox let checkbox_field = FieldBuilder::from_field_type(&FieldType::Checkbox) .name("Done") .visibility(true) .build(); + grid_builder.add_field(checkbox_field); - GridBuilder::default() - .add_field(text_field) - .add_field(single_select_field) - .add_field(checkbox_field) - .add_empty_row() - .add_empty_row() - .add_empty_row() - .build() + grid_builder.add_empty_row(); + grid_builder.add_empty_row(); + grid_builder.add_empty_row(); + grid_builder.build() } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/block_test.rs similarity index 82% rename from frontend/rust-lib/flowy-grid/tests/grid/block_test.rs rename to frontend/rust-lib/flowy-grid/tests/grid/block_test/block_test.rs index 51094e2b3f..6ded1f7060 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/block_test.rs @@ -1,5 +1,6 @@ -use crate::grid::script::EditorScript::*; -use crate::grid::script::*; +use crate::grid::block_test::script::GridRowTest; +use crate::grid::block_test::script::RowScript::*; + use flowy_grid_data_model::revision::{GridBlockMetaRevision, GridBlockMetaRevisionChangeset}; #[tokio::test] @@ -10,7 +11,7 @@ async fn grid_create_block() { CreateBlock { block: block_meta_rev }, AssertBlockCount(2), ]; - GridEditorTest::new().await.run_scripts(scripts).await; + GridRowTest::new().await.run_scripts(scripts).await; } #[tokio::test] @@ -36,5 +37,5 @@ async fn grid_update_block() { block: cloned_grid_block, }, ]; - GridEditorTest::new().await.run_scripts(scripts).await; + GridRowTest::new().await.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/mod.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/mod.rs new file mode 100644 index 0000000000..c982869655 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/mod.rs @@ -0,0 +1,5 @@ +#![allow(clippy::module_inception)] +mod block_test; +mod row_test; +mod script; +pub mod util; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs new file mode 100644 index 0000000000..85bfed576d --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs @@ -0,0 +1,135 @@ +use crate::grid::block_test::script::RowScript::*; +use crate::grid::block_test::script::{CreateRowScriptBuilder, GridRowTest}; +use crate::grid::grid_editor::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, TWITTER}; +use flowy_grid::entities::FieldType; +use flowy_grid::services::field::{SELECTION_IDS_SEPARATOR, UNCHECK}; +use flowy_grid_data_model::revision::RowMetaChangeset; + +#[tokio::test] +async fn grid_create_row_count_test() { + let mut test = GridRowTest::new().await; + let scripts = vec![ + AssertRowCount(5), + CreateEmptyRow, + CreateEmptyRow, + CreateRow { + row_rev: test.row_builder().build(), + }, + AssertRowCount(8), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_update_row() { + let mut test = GridRowTest::new().await; + let row_rev = test.row_builder().build(); + let changeset = RowMetaChangeset { + row_id: row_rev.id.clone(), + height: None, + visibility: None, + cell_by_field_id: Default::default(), + }; + + let scripts = vec![AssertRowCount(5), CreateRow { row_rev }, UpdateRow { changeset }]; + test.run_scripts(scripts).await; + + let expected_row = test.last_row().unwrap(); + let scripts = vec![AssertRow { expected_row }, AssertRowCount(6)]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_delete_row() { + let mut test = GridRowTest::new().await; + let row_1 = test.row_builder().build(); + let row_2 = test.row_builder().build(); + let row_ids = vec![row_1.id.clone(), row_2.id.clone()]; + let scripts = vec![ + AssertRowCount(5), + CreateRow { row_rev: row_1 }, + CreateRow { row_rev: row_2 }, + AssertBlockCount(1), + AssertBlock { + block_index: 0, + row_count: 7, + start_row_index: 0, + }, + DeleteRows { row_ids }, + AssertBlock { + block_index: 0, + row_count: 5, + start_row_index: 0, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_row_add_cells_test() { + let mut test = GridRowTest::new().await; + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert(FieldType::RichText, "hello world", "hello world"); + builder.insert(FieldType::DateTime, "1647251762", "2022/03/14"); + builder.insert(FieldType::Number, "18,443", "$18,443.00"); + builder.insert(FieldType::Checkbox, "false", UNCHECK); + builder.insert(FieldType::URL, "https://appflowy.io", "https://appflowy.io"); + builder.insert_single_select_cell(|mut options| options.remove(0), COMPLETED); + builder.insert_multi_select_cell( + |options| options, + &vec![GOOGLE, FACEBOOK, TWITTER].join(SELECTION_IDS_SEPARATOR), + ); + let scripts = builder.build(); + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_row_insert_number_test() { + let mut test = GridRowTest::new().await; + for (val, expected) in &[("1647251762", "2022/03/14"), ("2022/03/14", ""), ("", "")] { + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert(FieldType::DateTime, val, expected); + let scripts = builder.build(); + test.run_scripts(scripts).await; + } +} + +#[tokio::test] +async fn grid_row_insert_date_test() { + let mut test = GridRowTest::new().await; + for (val, expected) in &[ + ("18,443", "$18,443.00"), + ("0", "$0.00"), + ("100000", "$100,000.00"), + ("$100,000.00", "$100,000.00"), + ("", ""), + ] { + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert(FieldType::Number, val, expected); + let scripts = builder.build(); + test.run_scripts(scripts).await; + } +} +#[tokio::test] +async fn grid_row_insert_single_select_test() { + let mut test = GridRowTest::new().await; + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert_single_select_cell(|mut options| options.pop().unwrap(), PAUSED); + let scripts = builder.build(); + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_row_insert_multi_select_test() { + let mut test = GridRowTest::new().await; + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert_multi_select_cell( + |mut options| { + options.remove(0); + options + }, + &vec![FACEBOOK, TWITTER].join(SELECTION_IDS_SEPARATOR), + ); + let scripts = builder.build(); + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs new file mode 100644 index 0000000000..14671c2eb2 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs @@ -0,0 +1,373 @@ +use crate::grid::block_test::script::RowScript::{AssertCell, CreateRow}; +use crate::grid::block_test::util::GridRowTestBuilder; +use crate::grid::grid_editor::GridEditorTest; + +use flowy_grid::entities::{FieldType, GridCellIdParams, GridRowPB}; +use flowy_grid::services::field::*; +use flowy_grid_data_model::revision::{ + GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision, +}; +use std::collections::HashMap; +use std::sync::Arc; +use strum::IntoEnumIterator; + +pub enum RowScript { + CreateEmptyRow, + CreateRow { + row_rev: RowRevision, + }, + UpdateRow { + changeset: RowMetaChangeset, + }, + AssertRow { + expected_row: RowRevision, + }, + DeleteRows { + row_ids: Vec, + }, + AssertCell { + row_id: String, + field_id: String, + field_type: FieldType, + expected: String, + }, + AssertRowCount(usize), + CreateBlock { + block: GridBlockMetaRevision, + }, + UpdateBlock { + changeset: GridBlockMetaRevisionChangeset, + }, + AssertBlockCount(usize), + AssertBlock { + block_index: usize, + row_count: i32, + start_row_index: i32, + }, + AssertBlockEqual { + block_index: usize, + block: GridBlockMetaRevision, + }, +} + +pub struct GridRowTest { + inner: GridEditorTest, +} + +impl GridRowTest { + pub async fn new() -> Self { + let editor_test = GridEditorTest::new().await; + Self { inner: editor_test } + } + + pub fn last_row(&self) -> Option { + self.row_revs.last().map(|a| a.clone().as_ref().clone()) + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub fn row_builder(&self) -> GridRowTestBuilder { + GridRowTestBuilder::new(self.block_id(), &self.field_revs) + } + + pub async fn run_script(&mut self, script: RowScript) { + match script { + RowScript::CreateEmptyRow => { + let row_order = self.editor.create_row(None).await.unwrap(); + self.row_order_by_row_id + .insert(row_order.row_id().to_owned(), row_order); + self.row_revs = self.get_row_revs().await; + self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); + } + RowScript::CreateRow { row_rev } => { + let row_orders = self.editor.insert_rows(vec![row_rev]).await.unwrap(); + for row_order in row_orders { + self.row_order_by_row_id + .insert(row_order.row_id().to_owned(), row_order); + } + self.row_revs = self.get_row_revs().await; + self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); + } + RowScript::UpdateRow { changeset: change } => self.editor.update_row(change).await.unwrap(), + RowScript::DeleteRows { row_ids } => { + let row_orders = row_ids + .into_iter() + .map(|row_id| self.row_order_by_row_id.get(&row_id).unwrap().clone()) + .collect::>(); + + self.editor.delete_rows(row_orders).await.unwrap(); + self.row_revs = self.get_row_revs().await; + self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); + } + RowScript::AssertCell { + row_id, + field_id, + field_type, + expected, + } => { + let id = GridCellIdParams { + grid_id: self.grid_id.clone(), + field_id, + row_id, + }; + self.compare_cell_content(id, field_type, expected).await; + } + RowScript::AssertRow { expected_row } => { + let row = &*self + .row_revs + .iter() + .find(|row| row.id == expected_row.id) + .cloned() + .unwrap(); + assert_eq!(&expected_row, row); + } + RowScript::AssertRowCount(expected_row_count) => { + assert_eq!(expected_row_count, self.row_revs.len()); + } + RowScript::CreateBlock { block } => { + self.editor.create_block(block).await.unwrap(); + self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); + } + RowScript::UpdateBlock { changeset: change } => { + self.editor.update_block(change).await.unwrap(); + } + RowScript::AssertBlockCount(count) => { + assert_eq!(self.editor.get_block_meta_revs().await.unwrap().len(), count); + } + RowScript::AssertBlock { + block_index, + row_count, + start_row_index, + } => { + assert_eq!(self.block_meta_revs[block_index].row_count, row_count); + assert_eq!(self.block_meta_revs[block_index].start_row_index, start_row_index); + } + RowScript::AssertBlockEqual { block_index, block } => { + let blocks = self.editor.get_block_meta_revs().await.unwrap(); + let compared_block = blocks[block_index].clone(); + assert_eq!(compared_block, Arc::new(block)); + } + } + } + + async fn compare_cell_content(&self, cell_id: GridCellIdParams, field_type: FieldType, expected: String) { + match field_type { + FieldType::RichText => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(TextCellDataParser()) + .unwrap(); + + assert_eq!(cell_data.as_ref(), &expected); + } + FieldType::Number => { + let field_rev = self.editor.get_field_rev(&cell_id.field_id).await.unwrap(); + let number_type_option = field_rev + .get_type_option_entry::(FieldType::Number.into()) + .unwrap(); + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(NumberCellDataParser(number_type_option.format)) + .unwrap(); + assert_eq!(cell_data.to_string(), expected); + } + FieldType::DateTime => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(DateCellDataParser()) + .unwrap(); + + assert_eq!(cell_data.date, expected); + } + FieldType::SingleSelect => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(SelectOptionCellDataParser()) + .unwrap(); + let select_option = cell_data.select_options.first().unwrap(); + assert_eq!(select_option.name, expected); + } + FieldType::MultiSelect => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(SelectOptionCellDataParser()) + .unwrap(); + + let s = cell_data + .select_options + .into_iter() + .map(|option| option.name) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + assert_eq!(s, expected); + } + + FieldType::Checkbox => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(CheckboxCellDataParser()) + .unwrap(); + assert_eq!(cell_data.to_string(), expected); + } + FieldType::URL => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(URLCellDataParser()) + .unwrap(); + + assert_eq!(cell_data.content, expected); + // assert_eq!(cell_data.url, expected); + } + } + } +} + +impl std::ops::Deref for GridRowTest { + type Target = GridEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for GridRowTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +pub struct CreateRowScriptBuilder<'a> { + builder: GridRowTestBuilder<'a>, + data_by_field_type: HashMap, + output_by_field_type: HashMap, +} + +impl<'a> CreateRowScriptBuilder<'a> { + pub fn new(test: &'a GridRowTest) -> Self { + Self { + builder: test.row_builder(), + data_by_field_type: HashMap::new(), + output_by_field_type: HashMap::new(), + } + } + + pub fn insert(&mut self, field_type: FieldType, input: &str, expected: &str) { + self.data_by_field_type.insert( + field_type, + CellTestData { + input: input.to_string(), + expected: expected.to_owned(), + }, + ); + } + + pub fn insert_single_select_cell(&mut self, f: F, expected: &str) + where + F: Fn(Vec) -> SelectOptionPB, + { + let field_id = self.builder.insert_single_select_cell(f); + self.output_by_field_type.insert( + FieldType::SingleSelect, + CellTestOutput { + field_id, + expected: expected.to_owned(), + }, + ); + } + + pub fn insert_multi_select_cell(&mut self, f: F, expected: &str) + where + F: Fn(Vec) -> Vec, + { + let field_id = self.builder.insert_multi_select_cell(f); + self.output_by_field_type.insert( + FieldType::MultiSelect, + CellTestOutput { + field_id, + expected: expected.to_owned(), + }, + ); + } + + pub fn build(mut self) -> Vec { + let mut scripts = vec![]; + let output_by_field_type = &mut self.output_by_field_type; + + for field_type in FieldType::iter() { + let field_type: FieldType = field_type; + if let Some(data) = self.data_by_field_type.get(&field_type) { + let field_id = match field_type { + FieldType::RichText => self.builder.insert_text_cell(&data.input), + FieldType::Number => self.builder.insert_number_cell(&data.input), + FieldType::DateTime => self.builder.insert_date_cell(&data.input), + FieldType::Checkbox => self.builder.insert_checkbox_cell(&data.input), + FieldType::URL => self.builder.insert_url_cell(&data.input), + _ => "".to_owned(), + }; + + if !field_id.is_empty() { + output_by_field_type.insert( + field_type, + CellTestOutput { + field_id, + expected: data.expected.clone(), + }, + ); + } + } + } + + let row_rev = self.builder.build(); + let row_id = row_rev.id.clone(); + scripts.push(CreateRow { row_rev }); + + for field_type in FieldType::iter() { + if let Some(data) = output_by_field_type.get(&field_type) { + let script = AssertCell { + row_id: row_id.clone(), + field_id: data.field_id.clone(), + field_type, + expected: data.expected.clone(), + }; + scripts.push(script); + } + } + scripts + } +} + +pub struct CellTestData { + pub input: String, + pub expected: String, +} + +struct CellTestOutput { + field_id: String, + expected: String, +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs new file mode 100644 index 0000000000..216a4d4acd --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs @@ -0,0 +1,134 @@ +use flowy_grid::entities::FieldType; +use std::sync::Arc; + +use flowy_grid::services::field::{ + DateCellChangesetPB, MultiSelectTypeOption, SelectOptionPB, SingleSelectTypeOptionPB, SELECTION_IDS_SEPARATOR, +}; +use flowy_grid::services::row::RowRevisionBuilder; +use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; + +use strum::EnumCount; + +pub struct GridRowTestBuilder<'a> { + block_id: String, + field_revs: &'a [Arc], + inner_builder: RowRevisionBuilder<'a>, +} + +impl<'a> GridRowTestBuilder<'a> { + pub fn new(block_id: &str, field_revs: &'a [Arc]) -> Self { + assert_eq!(field_revs.len(), FieldType::COUNT); + let inner_builder = RowRevisionBuilder::new(field_revs); + Self { + block_id: block_id.to_owned(), + field_revs, + inner_builder, + } + } + + pub fn insert_text_cell(&mut self, data: &str) -> String { + let text_field = self.field_rev_with_type(&FieldType::RichText); + self.inner_builder + .insert_cell(&text_field.id, data.to_string()) + .unwrap(); + + text_field.id.clone() + } + + pub fn insert_number_cell(&mut self, data: &str) -> String { + let number_field = self.field_rev_with_type(&FieldType::Number); + self.inner_builder + .insert_cell(&number_field.id, data.to_string()) + .unwrap(); + number_field.id.clone() + } + + pub fn insert_date_cell(&mut self, data: &str) -> String { + let value = serde_json::to_string(&DateCellChangesetPB { + date: Some(data.to_string()), + time: None, + }) + .unwrap(); + let date_field = self.field_rev_with_type(&FieldType::DateTime); + self.inner_builder.insert_cell(&date_field.id, value).unwrap(); + date_field.id.clone() + } + + pub fn insert_checkbox_cell(&mut self, data: &str) -> String { + let checkbox_field = self.field_rev_with_type(&FieldType::Checkbox); + self.inner_builder + .insert_cell(&checkbox_field.id, data.to_string()) + .unwrap(); + + checkbox_field.id.clone() + } + + pub fn insert_url_cell(&mut self, data: &str) -> String { + let url_field = self.field_rev_with_type(&FieldType::URL); + self.inner_builder.insert_cell(&url_field.id, data.to_string()).unwrap(); + url_field.id.clone() + } + + pub fn insert_single_select_cell(&mut self, f: F) -> String + where + F: Fn(Vec) -> SelectOptionPB, + { + let single_select_field = self.field_rev_with_type(&FieldType::SingleSelect); + let type_option = SingleSelectTypeOptionPB::from(&single_select_field); + let option = f(type_option.options); + self.inner_builder + .insert_select_option_cell(&single_select_field.id, option.id) + .unwrap(); + + single_select_field.id.clone() + } + + pub fn insert_multi_select_cell(&mut self, f: F) -> String + where + F: Fn(Vec) -> Vec, + { + let multi_select_field = self.field_rev_with_type(&FieldType::MultiSelect); + let type_option = MultiSelectTypeOption::from(&multi_select_field); + let options = f(type_option.options); + let ops_ids = options + .iter() + .map(|option| option.id.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + self.inner_builder + .insert_select_option_cell(&multi_select_field.id, ops_ids) + .unwrap(); + + multi_select_field.id.clone() + } + + pub fn field_rev_with_type(&self, field_type: &FieldType) -> FieldRevision { + self.field_revs + .iter() + .find(|field_rev| { + let t_field_type: FieldType = field_rev.field_type_rev.into(); + &t_field_type == field_type + }) + .unwrap() + .as_ref() + .clone() + } + + pub fn build(self) -> RowRevision { + self.inner_builder.build(&self.block_id) + } +} + +impl<'a> std::ops::Deref for GridRowTestBuilder<'a> { + type Target = RowRevisionBuilder<'a>; + + fn deref(&self) -> &Self::Target { + &self.inner_builder + } +} + +impl<'a> std::ops::DerefMut for GridRowTestBuilder<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner_builder + } +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/cell_test/mod.rs b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/mod.rs new file mode 100644 index 0000000000..63d424afaf --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/mod.rs @@ -0,0 +1,2 @@ +mod script; +mod test; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/cell_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/script.rs new file mode 100644 index 0000000000..4def72696d --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/script.rs @@ -0,0 +1,61 @@ +use crate::grid::grid_editor::GridEditorTest; +use flowy_grid::entities::CellChangesetPB; + +pub enum CellScript { + UpdateCell { changeset: CellChangesetPB, is_err: bool }, +} + +pub struct GridCellTest { + inner: GridEditorTest, +} + +impl GridCellTest { + pub async fn new() -> Self { + let inner = GridEditorTest::new().await; + Self { inner } + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: CellScript) { + // let grid_manager = self.sdk.grid_manager.clone(); + // let pool = self.sdk.user_session.db_pool().unwrap(); + let rev_manager = self.editor.rev_manager(); + let _cache = rev_manager.revision_cache().await; + + match script { + CellScript::UpdateCell { changeset, is_err } => { + let result = self.editor.update_cell(changeset).await; + if is_err { + assert!(result.is_err()) + } else { + let _ = result.unwrap(); + self.row_revs = self.get_row_revs().await; + } + } // CellScript::AssertGridRevisionPad => { + // sleep(Duration::from_millis(2 * REVISION_WRITE_INTERVAL_IN_MILLIS)).await; + // let mut grid_rev_manager = grid_manager.make_grid_rev_manager(&self.grid_id, pool.clone()).unwrap(); + // let grid_pad = grid_rev_manager.load::(None).await.unwrap(); + // println!("{}", grid_pad.delta_str()); + // } + } + } +} + +impl std::ops::Deref for GridCellTest { + type Target = GridEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for GridCellTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/cell_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs similarity index 77% rename from frontend/rust-lib/flowy-grid/tests/grid/cell_test.rs rename to frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs index 01d2310278..e8435c2d00 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/cell_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs @@ -1,13 +1,13 @@ -use crate::grid::field_util::make_date_cell_string; -use crate::grid::script::EditorScript::*; -use crate::grid::script::*; -use flowy_grid::entities::{CellChangeset, FieldType}; -use flowy_grid::services::field::select_option::SelectOptionCellChangeset; -use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; +use crate::grid::cell_test::script::CellScript::*; +use crate::grid::cell_test::script::GridCellTest; +use crate::grid::field_test::util::make_date_cell_string; +use flowy_grid::entities::{CellChangesetPB, FieldType}; +use flowy_grid::services::field::selection_type_option::SelectOptionCellChangeset; +use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOptionPB}; #[tokio::test] async fn grid_cell_update() { - let mut test = GridEditorTest::new().await; + let mut test = GridCellTest::new().await; let field_revs = &test.field_revs; let row_revs = &test.row_revs; let grid_blocks = &test.block_meta_revs; @@ -24,7 +24,7 @@ async fn grid_cell_update() { FieldType::Number => "123".to_string(), FieldType::DateTime => make_date_cell_string("123"), FieldType::SingleSelect => { - let type_option = SingleSelectTypeOption::from(field_rev); + let type_option = SingleSelectTypeOptionPB::from(field_rev); SelectOptionCellChangeset::from_insert(&type_option.options.first().unwrap().id).to_str() } FieldType::MultiSelect => { @@ -36,7 +36,7 @@ async fn grid_cell_update() { }; scripts.push(UpdateCell { - changeset: CellChangeset { + changeset: CellChangesetPB { grid_id: block_id.to_string(), row_id: row_rev.id.clone(), field_id: field_rev.id.clone(), diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/mod.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/mod.rs new file mode 100644 index 0000000000..5ac4da9f24 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/mod.rs @@ -0,0 +1,3 @@ +mod script; +mod test; +pub mod util; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs new file mode 100644 index 0000000000..6d910f3516 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs @@ -0,0 +1,94 @@ +use crate::grid::grid_editor::GridEditorTest; +use flowy_grid::entities::InsertFieldParams; +use flowy_grid_data_model::revision::FieldRevision; +use flowy_sync::entities::grid::FieldChangesetParams; + +pub enum FieldScript { + CreateField { + params: InsertFieldParams, + }, + UpdateField { + changeset: FieldChangesetParams, + }, + DeleteField { + field_rev: FieldRevision, + }, + AssertFieldCount(usize), + AssertFieldEqual { + field_index: usize, + field_rev: FieldRevision, + }, +} + +pub struct GridFieldTest { + inner: GridEditorTest, +} + +impl GridFieldTest { + pub async fn new() -> Self { + let editor_test = GridEditorTest::new().await; + Self { inner: editor_test } + } + + pub fn grid_id(&self) -> String { + self.grid_id.clone() + } + + pub fn field_count(&self) -> usize { + self.field_count + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: FieldScript) { + match script { + FieldScript::CreateField { params } => { + if !self.editor.contain_field(¶ms.field.id).await { + self.field_count += 1; + } + + self.editor.insert_field(params).await.unwrap(); + self.field_revs = self.editor.get_field_revs(None).await.unwrap(); + assert_eq!(self.field_count, self.field_revs.len()); + } + FieldScript::UpdateField { changeset: change } => { + self.editor.update_field(change).await.unwrap(); + self.field_revs = self.editor.get_field_revs(None).await.unwrap(); + } + FieldScript::DeleteField { field_rev } => { + if self.editor.contain_field(&field_rev.id).await { + self.field_count -= 1; + } + + self.editor.delete_field(&field_rev.id).await.unwrap(); + self.field_revs = self.editor.get_field_revs(None).await.unwrap(); + assert_eq!(self.field_count, self.field_revs.len()); + } + FieldScript::AssertFieldCount(count) => { + assert_eq!(self.editor.get_field_revs(None).await.unwrap().len(), count); + } + FieldScript::AssertFieldEqual { field_index, field_rev } => { + let field_revs = self.editor.get_field_revs(None).await.unwrap(); + assert_eq!(field_revs[field_index].as_ref(), &field_rev); + } + } + } +} + +impl std::ops::Deref for GridFieldTest { + type Target = GridEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for GridFieldTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs similarity index 67% rename from frontend/rust-lib/flowy-grid/tests/grid/field_test.rs rename to frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs index 00428a8fc4..98cb3d0324 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs @@ -1,30 +1,30 @@ -use crate::grid::field_util::*; -use crate::grid::script::EditorScript::*; -use crate::grid::script::*; -use flowy_grid::services::field::select_option::SelectOption; -use flowy_grid::services::field::SingleSelectTypeOption; +use crate::grid::field_test::script::FieldScript::*; +use crate::grid::field_test::script::GridFieldTest; +use crate::grid::field_test::util::*; +use flowy_grid::services::field::selection_type_option::SelectOptionPB; +use flowy_grid::services::field::SingleSelectTypeOptionPB; use flowy_grid_data_model::revision::TypeOptionDataEntry; use flowy_sync::entities::grid::FieldChangesetParams; #[tokio::test] async fn grid_create_field() { - let mut test = GridEditorTest::new().await; - let (params, field_rev) = create_text_field(&test.grid_id); + let mut test = GridFieldTest::new().await; + let (params, field_rev) = create_text_field(&test.grid_id()); let scripts = vec![ CreateField { params }, AssertFieldEqual { - field_index: test.field_count, + field_index: test.field_count(), field_rev, }, ]; test.run_scripts(scripts).await; - let (params, field_rev) = create_single_select_field(&test.grid_id); + let (params, field_rev) = create_single_select_field(&test.grid_id()); let scripts = vec![ CreateField { params }, AssertFieldEqual { - field_index: test.field_count, + field_index: test.field_count(), field_rev, }, ]; @@ -33,9 +33,9 @@ async fn grid_create_field() { #[tokio::test] async fn grid_create_duplicate_field() { - let mut test = GridEditorTest::new().await; - let (params, _) = create_text_field(&test.grid_id); - let field_count = test.field_count; + let mut test = GridFieldTest::new().await; + let (params, _) = create_text_field(&test.grid_id()); + let field_count = test.field_count(); let expected_field_count = field_count + 1; let scripts = vec![ CreateField { params: params.clone() }, @@ -47,11 +47,11 @@ async fn grid_create_duplicate_field() { #[tokio::test] async fn grid_update_field_with_empty_change() { - let mut test = GridEditorTest::new().await; - let (params, field_rev) = create_single_select_field(&test.grid_id); + let mut test = GridFieldTest::new().await; + let (params, field_rev) = create_single_select_field(&test.grid_id()); let changeset = FieldChangesetParams { field_id: field_rev.id.clone(), - grid_id: test.grid_id.clone(), + grid_id: test.grid_id(), ..Default::default() }; @@ -59,7 +59,7 @@ async fn grid_update_field_with_empty_change() { CreateField { params }, UpdateField { changeset }, AssertFieldEqual { - field_index: test.field_count, + field_index: test.field_count(), field_rev, }, ]; @@ -68,14 +68,14 @@ async fn grid_update_field_with_empty_change() { #[tokio::test] async fn grid_update_field() { - let mut test = GridEditorTest::new().await; - let (params, single_select_field) = create_single_select_field(&test.grid_id); + let mut test = GridFieldTest::new().await; + let (params, single_select_field) = create_single_select_field(&test.grid_id()); - let mut single_select_type_option = SingleSelectTypeOption::from(&single_select_field); - single_select_type_option.options.push(SelectOption::new("Unknown")); + let mut single_select_type_option = SingleSelectTypeOptionPB::from(&single_select_field); + single_select_type_option.options.push(SelectOptionPB::new("Unknown")); let changeset = FieldChangesetParams { field_id: single_select_field.id.clone(), - grid_id: test.grid_id.clone(), + grid_id: test.grid_id(), frozen: Some(true), width: Some(1000), type_option_data: Some(single_select_type_option.protobuf_bytes().to_vec()), @@ -92,7 +92,7 @@ async fn grid_update_field() { CreateField { params }, UpdateField { changeset }, AssertFieldEqual { - field_index: test.field_count, + field_index: test.field_count(), field_rev: expected_field_rev, }, ]; @@ -101,9 +101,9 @@ async fn grid_update_field() { #[tokio::test] async fn grid_delete_field() { - let mut test = GridEditorTest::new().await; - let original_field_count = test.field_count; - let (params, text_field_rev) = create_text_field(&test.grid_id); + let mut test = GridFieldTest::new().await; + let original_field_count = test.field_count(); + let (params, text_field_rev) = create_text_field(&test.grid_id()); let scripts = vec![ CreateField { params }, DeleteField { diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_util.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs similarity index 85% rename from frontend/rust-lib/flowy-grid/tests/grid/field_util.rs rename to frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs index d9055f93ea..01424cef74 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs @@ -1,5 +1,5 @@ use flowy_grid::entities::*; -use flowy_grid::services::field::select_option::SelectOption; +use flowy_grid::services::field::selection_type_option::SelectOptionPB; use flowy_grid::services::field::*; use flowy_grid_data_model::revision::*; @@ -17,7 +17,7 @@ pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) { .protobuf_bytes() .to_vec(); - let field = Field { + let field = GridFieldPB { id: field_rev.id, name: field_rev.name, desc: field_rev.desc, @@ -39,18 +39,18 @@ pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) { pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) { let single_select = SingleSelectTypeOptionBuilder::default() - .option(SelectOption::new("Done")) - .option(SelectOption::new("Progress")); + .option(SelectOptionPB::new("Done")) + .option(SelectOptionPB::new("Progress")); let field_rev = FieldBuilder::new(single_select).name("Name").visibility(true).build(); let cloned_field_rev = field_rev.clone(); let type_option_data = field_rev - .get_type_option_entry::(field_rev.field_type_rev) + .get_type_option_entry::(field_rev.field_type_rev) .unwrap() .protobuf_bytes() .to_vec(); - let field = Field { + let field = GridFieldPB { id: field_rev.id, name: field_rev.name, desc: field_rev.desc, @@ -73,7 +73,7 @@ pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldRev // The grid will contains all existing field types and there are three empty rows in this grid. pub fn make_date_cell_string(s: &str) -> String { - serde_json::to_string(&DateCellChangeset { + serde_json::to_string(&DateCellChangesetPB { date: Some(s.to_string()), time: None, }) diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs index d89b6a66a1..267cb570eb 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs @@ -3,11 +3,11 @@ #![allow(dead_code)] #![allow(unused_imports)] -use flowy_grid::entities::{CreateGridFilterPayload, GridLayoutType, GridSetting}; +use flowy_grid::entities::{CreateGridFilterPayloadPB, GridLayoutType, GridSettingPB}; use flowy_grid::services::setting::GridSettingChangesetBuilder; use flowy_grid_data_model::revision::{FieldRevision, FieldTypeRevision}; use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, GridSettingChangesetParams}; -use crate::grid::script::GridEditorTest; +use crate::grid::grid_editor::GridEditorTest; pub enum FilterScript { #[allow(dead_code)] @@ -15,7 +15,7 @@ pub enum FilterScript { params: GridSettingChangesetParams, }, InsertGridTableFilter { - payload: CreateGridFilterPayload, + payload: CreateGridFilterPayloadPB, }, AssertTableFilterCount { count: i32, @@ -26,19 +26,19 @@ pub enum FilterScript { }, #[allow(dead_code)] AssertGridSetting { - expected_setting: GridSetting, + expected_setting: GridSettingPB, }, } pub struct GridFilterTest { - pub editor_test: GridEditorTest, + inner: GridEditorTest, } impl GridFilterTest { pub async fn new() -> Self { let editor_test = GridEditorTest::new().await; Self { - editor_test + inner: editor_test } } @@ -49,7 +49,6 @@ impl GridFilterTest { } pub async fn run_script(&mut self, script: FilterScript) { - match script { FilterScript::UpdateGridSetting { params } => { let _ = self.editor.update_grid_setting(params).await.unwrap(); @@ -82,10 +81,17 @@ impl GridFilterTest { } } + impl std::ops::Deref for GridFilterTest { type Target = GridEditorTest; fn deref(&self) -> &Self::Target { - &self.editor_test + &self.inner + } +} + +impl std::ops::DerefMut for GridFilterTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs index 29614b21b2..3e45a4053d 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs @@ -1,13 +1,13 @@ use crate::grid::filter_test::script::FilterScript::*; use crate::grid::filter_test::script::*; -use flowy_grid::entities::{CreateGridFilterPayload, TextFilterCondition}; +use flowy_grid::entities::{CreateGridFilterPayloadPB, FieldType, TextFilterCondition}; use flowy_grid_data_model::revision::FieldRevision; #[tokio::test] async fn grid_filter_create_test() { let mut test = GridFilterTest::new().await; - let field_rev = test.text_field(); - let payload = CreateGridFilterPayload::new(field_rev, TextFilterCondition::TextIsEmpty, Some("abc".to_owned())); + let field_rev = test.get_field_rev(FieldType::RichText); + let payload = CreateGridFilterPayloadPB::new(field_rev, TextFilterCondition::TextIsEmpty, Some("abc".to_owned())); let scripts = vec![InsertGridTableFilter { payload }, AssertTableFilterCount { count: 1 }]; test.run_scripts(scripts).await; } @@ -16,10 +16,10 @@ async fn grid_filter_create_test() { #[should_panic] async fn grid_filter_invalid_condition_panic_test() { let mut test = GridFilterTest::new().await; - let field_rev = test.text_field().clone(); + let field_rev = test.get_field_rev(FieldType::RichText).clone(); // 100 is not a valid condition, so this test should be panic. - let payload = CreateGridFilterPayload::new(&field_rev, 100, Some("".to_owned())); + let payload = CreateGridFilterPayloadPB::new(&field_rev, 100, Some("".to_owned())); let scripts = vec![InsertGridTableFilter { payload }]; test.run_scripts(scripts).await; } @@ -27,7 +27,7 @@ async fn grid_filter_invalid_condition_panic_test() { #[tokio::test] async fn grid_filter_delete_test() { let mut test = GridFilterTest::new().await; - let field_rev = test.text_field().clone(); + let field_rev = test.get_field_rev(FieldType::RichText).clone(); let payload = create_filter(&field_rev, TextFilterCondition::TextIsEmpty, "abc"); let scripts = vec![InsertGridTableFilter { payload }, AssertTableFilterCount { count: 1 }]; test.run_scripts(scripts).await; @@ -36,7 +36,7 @@ async fn grid_filter_delete_test() { test.run_scripts(vec![ DeleteGridTableFilter { filter_id: filter.id, - field_rev, + field_rev: field_rev.as_ref().clone(), }, AssertTableFilterCount { count: 0 }, ]) @@ -46,6 +46,6 @@ async fn grid_filter_delete_test() { #[tokio::test] async fn grid_filter_get_rows_test() {} -fn create_filter(field_rev: &FieldRevision, condition: TextFilterCondition, s: &str) -> CreateGridFilterPayload { - CreateGridFilterPayload::new(field_rev, condition, Some(s.to_owned())) +fn create_filter(field_rev: &FieldRevision, condition: TextFilterCondition, s: &str) -> CreateGridFilterPayloadPB { + CreateGridFilterPayloadPB::new(field_rev, condition, Some(s.to_owned())) } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs new file mode 100644 index 0000000000..f924b3e803 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -0,0 +1,258 @@ +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(unused_imports)] +use crate::grid::block_test::util::GridRowTestBuilder; +use bytes::Bytes; +use flowy_grid::entities::*; +use flowy_grid::services::field::SelectOptionPB; +use flowy_grid::services::field::*; +use flowy_grid::services::grid_editor::{GridPadBuilder, GridRevisionEditor}; +use flowy_grid::services::row::{CreateRowRevisionPayload, RowRevisionBuilder}; +use flowy_grid::services::setting::GridSettingChangesetBuilder; +use flowy_grid_data_model::revision::*; +use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS; +use flowy_sync::client_grid::GridBuilder; +use flowy_sync::entities::grid::{ + CreateGridFilterParams, DeleteFilterParams, FieldChangesetParams, GridSettingChangesetParams, +}; +use flowy_test::helper::ViewTest; +use flowy_test::FlowySDKTest; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use strum::EnumCount; +use strum::IntoEnumIterator; +use tokio::time::sleep; + +pub struct GridEditorTest { + pub sdk: FlowySDKTest, + pub grid_id: String, + pub editor: Arc, + pub field_revs: Vec>, + pub block_meta_revs: Vec>, + pub row_revs: Vec>, + pub field_count: usize, + pub row_order_by_row_id: HashMap, +} + +impl GridEditorTest { + pub async fn new() -> Self { + let sdk = FlowySDKTest::default(); + let _ = sdk.init_user().await; + let build_context = make_test_grid(); + let view_data: Bytes = build_context.into(); + let test = ViewTest::new_grid_view(&sdk, view_data.to_vec()).await; + let editor = sdk.grid_manager.open_grid(&test.view.id).await.unwrap(); + let field_revs = editor.get_field_revs(None).await.unwrap(); + let block_meta_revs = editor.get_block_meta_revs().await.unwrap(); + let row_revs = editor.grid_block_snapshots(None).await.unwrap().pop().unwrap().row_revs; + assert_eq!(block_meta_revs.len(), 1); + + // It seems like you should add the field in the make_test_grid() function. + // Because we assert the initialize count of the fields is equal to FieldType::COUNT. + assert_eq!(field_revs.len(), FieldType::COUNT); + + let grid_id = test.view.id; + Self { + sdk, + grid_id, + editor, + field_revs, + block_meta_revs, + row_revs, + field_count: FieldType::COUNT, + row_order_by_row_id: HashMap::default(), + } + } + + pub async fn get_row_revs(&self) -> Vec> { + self.editor + .grid_block_snapshots(None) + .await + .unwrap() + .pop() + .unwrap() + .row_revs + } + + pub async fn grid_filters(&self) -> Vec { + let layout_type = GridLayoutType::Table; + self.editor.get_grid_filter(&layout_type).await.unwrap() + } + + pub fn get_field_rev(&self, field_type: FieldType) -> &Arc { + self.field_revs + .iter() + .filter(|field_rev| { + let t_field_type: FieldType = field_rev.field_type_rev.into(); + t_field_type == field_type + }) + .collect::>() + .pop() + .unwrap() + } + + pub fn block_id(&self) -> &str { + &self.block_meta_revs.last().unwrap().block_id + } +} + +pub const GOOGLE: &str = "Google"; +pub const FACEBOOK: &str = "Facebook"; +pub const TWITTER: &str = "Twitter"; + +pub const COMPLETED: &str = "Completed"; +pub const PLANNED: &str = "Planned"; +pub const PAUSED: &str = "Paused"; + +// This grid is assumed to contain all the Fields. +fn make_test_grid() -> BuildGridContext { + let mut grid_builder = GridBuilder::new(); + // Iterate through the FieldType to create the corresponding Field. + for field_type in FieldType::iter() { + let field_type: FieldType = field_type; + + // The + match field_type { + FieldType::RichText => { + let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) + .name("Name") + .visibility(true) + .build(); + grid_builder.add_field(text_field); + } + FieldType::Number => { + // Number + let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD); + let number_field = FieldBuilder::new(number).name("Price").visibility(true).build(); + grid_builder.add_field(number_field); + } + FieldType::DateTime => { + // Date + let date = DateTypeOptionBuilder::default() + .date_format(DateFormat::US) + .time_format(TimeFormat::TwentyFourHour); + let date_field = FieldBuilder::new(date).name("Time").visibility(true).build(); + grid_builder.add_field(date_field); + } + FieldType::SingleSelect => { + // Single Select + let single_select = SingleSelectTypeOptionBuilder::default() + .option(SelectOptionPB::new(COMPLETED)) + .option(SelectOptionPB::new(PLANNED)) + .option(SelectOptionPB::new(PAUSED)); + let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build(); + grid_builder.add_field(single_select_field); + } + FieldType::MultiSelect => { + // MultiSelect + let multi_select = MultiSelectTypeOptionBuilder::default() + .option(SelectOptionPB::new(GOOGLE)) + .option(SelectOptionPB::new(FACEBOOK)) + .option(SelectOptionPB::new(TWITTER)); + let multi_select_field = FieldBuilder::new(multi_select) + .name("Platform") + .visibility(true) + .build(); + grid_builder.add_field(multi_select_field); + } + FieldType::Checkbox => { + // Checkbox + let checkbox = CheckboxTypeOptionBuilder::default(); + let checkbox_field = FieldBuilder::new(checkbox).name("is urgent").visibility(true).build(); + grid_builder.add_field(checkbox_field); + } + FieldType::URL => { + // URL + let url = URLTypeOptionBuilder::default(); + let url_field = FieldBuilder::new(url).name("link").visibility(true).build(); + grid_builder.add_field(url_field); + } + } + } + + // We have many assumptions base on the number of the rows, so do not change the number of the loop. + for i in 0..5 { + let block_id = grid_builder.block_id().to_owned(); + let field_revs = grid_builder.field_revs(); + let mut row_builder = GridRowTestBuilder::new(&block_id, field_revs); + match i { + 0 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("A"), + FieldType::Number => row_builder.insert_number_cell("1"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(0)) + } + FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), + _ => "".to_owned(), + }; + } + } + 1 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("B"), + FieldType::Number => row_builder.insert_number_cell("2"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(0)) + } + FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), + _ => "".to_owned(), + }; + } + } + 2 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("C"), + FieldType::Number => row_builder.insert_number_cell("3"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(1)) + } + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + } + 3 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("D"), + FieldType::Number => row_builder.insert_number_cell("4"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(1)) + } + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + } + 4 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("E"), + FieldType::Number => row_builder.insert_number_cell("5"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(2)) + } + + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + } + _ => {} + } + + let row_rev = row_builder.build(); + grid_builder.add_row(row_rev); + } + grid_builder.build() +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs new file mode 100644 index 0000000000..63bea4cc3d --- /dev/null +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_test.rs @@ -0,0 +1,381 @@ +use crate::grid::script::EditorScript::*; +use crate::grid::script::*; +use chrono::NaiveDateTime; +use flowy_grid::services::field::{ + DateCellContentChangeset, DateCellData, MultiSelectTypeOption, SelectOption, SelectOptionCellContentChangeset, + SingleSelectTypeOption, SELECTION_IDS_SEPARATOR, +}; +use flowy_grid::services::row::{decode_cell_data_from_type_option_cell_data, CreateRowMetaBuilder}; +use flowy_grid_data_model::entities::{ + CellChangeset, FieldChangesetParams, FieldType, GridBlockInfoChangeset, GridBlockMetaSnapshot, RowMetaChangeset, + TypeOptionDataEntry, +}; + +#[tokio::test] +async fn grid_create_field() { + let mut test = GridEditorTest::new().await; + let (text_field_params, text_field_meta) = create_text_field(&test.grid_id); + let (single_select_params, single_select_field) = create_single_select_field(&test.grid_id); + let scripts = vec![ + CreateField { + params: text_field_params, + }, + AssertFieldEqual { + field_index: test.field_count, + field_meta: text_field_meta, + }, + ]; + test.run_scripts(scripts).await; + + let scripts = vec![ + CreateField { + params: single_select_params, + }, + AssertFieldEqual { + field_index: test.field_count, + field_meta: single_select_field, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_create_duplicate_field() { + let mut test = GridEditorTest::new().await; + let (params, _) = create_text_field(&test.grid_id); + let field_count = test.field_count; + let expected_field_count = field_count + 1; + let scripts = vec![ + CreateField { params: params.clone() }, + CreateField { params }, + AssertFieldCount(expected_field_count), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_update_field_with_empty_change() { + let mut test = GridEditorTest::new().await; + let (params, field_meta) = create_single_select_field(&test.grid_id); + let changeset = FieldChangesetParams { + field_id: field_meta.id.clone(), + grid_id: test.grid_id.clone(), + ..Default::default() + }; + + let scripts = vec![ + CreateField { params }, + UpdateField { changeset }, + AssertFieldEqual { + field_index: test.field_count, + field_meta, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_update_field() { + let mut test = GridEditorTest::new().await; + let (single_select_params, single_select_field) = create_single_select_field(&test.grid_id); + let mut cloned_field = single_select_field.clone(); + + let mut single_select_type_option = SingleSelectTypeOption::from(&single_select_field); + single_select_type_option.options.push(SelectOption::new("Unknown")); + let changeset = FieldChangesetParams { + field_id: single_select_field.id.clone(), + grid_id: test.grid_id.clone(), + frozen: Some(true), + width: Some(1000), + type_option_data: Some(single_select_type_option.protobuf_bytes().to_vec()), + ..Default::default() + }; + + cloned_field.frozen = true; + cloned_field.width = 1000; + cloned_field.insert_type_option_entry(&single_select_type_option); + + let scripts = vec![ + CreateField { + params: single_select_params, + }, + UpdateField { changeset }, + AssertFieldEqual { + field_index: test.field_count, + field_meta: cloned_field, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_delete_field() { + let mut test = GridEditorTest::new().await; + let expected_field_count = test.field_count; + let (text_params, text_field) = create_text_field(&test.grid_id); + let scripts = vec![ + CreateField { params: text_params }, + DeleteField { field_meta: text_field }, + AssertFieldCount(expected_field_count), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_create_block() { + let grid_block = GridBlockMetaSnapshot::new(); + let scripts = vec![ + AssertBlockCount(1), + CreateBlock { block: grid_block }, + AssertBlockCount(2), + ]; + GridEditorTest::new().await.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_update_block() { + let grid_block = GridBlockMetaSnapshot::new(); + let mut cloned_grid_block = grid_block.clone(); + let changeset = GridBlockInfoChangeset { + block_id: grid_block.block_id.clone(), + start_row_index: Some(2), + row_count: Some(10), + }; + + cloned_grid_block.start_row_index = 2; + cloned_grid_block.row_count = 10; + + let scripts = vec![ + AssertBlockCount(1), + CreateBlock { block: grid_block }, + UpdateBlock { changeset }, + AssertBlockCount(2), + AssertBlockEqual { + block_index: 1, + block: cloned_grid_block, + }, + ]; + GridEditorTest::new().await.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_create_row() { + let scripts = vec![AssertRowCount(3), CreateEmptyRow, CreateEmptyRow, AssertRowCount(5)]; + GridEditorTest::new().await.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_create_row2() { + let mut test = GridEditorTest::new().await; + let create_row_context = CreateRowMetaBuilder::new(&test.field_metas).build(); + let scripts = vec![ + AssertRowCount(3), + CreateRow { + context: create_row_context, + }, + AssertRowCount(4), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_update_row() { + let mut test = GridEditorTest::new().await; + let context = CreateRowMetaBuilder::new(&test.field_metas).build(); + let changeset = RowMetaChangeset { + row_id: context.row_id.clone(), + height: None, + visibility: None, + cell_by_field_id: Default::default(), + }; + + let scripts = vec![ + AssertRowCount(3), + CreateRow { context }, + UpdateRow { + changeset: changeset.clone(), + }, + AssertRow { changeset }, + AssertRowCount(4), + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_delete_row() { + let mut test = GridEditorTest::new().await; + let context_1 = CreateRowMetaBuilder::new(&test.field_metas).build(); + let context_2 = CreateRowMetaBuilder::new(&test.field_metas).build(); + let row_ids = vec![context_1.row_id.clone(), context_2.row_id.clone()]; + let scripts = vec![ + AssertRowCount(3), + CreateRow { context: context_1 }, + CreateRow { context: context_2 }, + AssertBlockCount(1), + AssertBlock { + block_index: 0, + row_count: 5, + start_row_index: 0, + }, + DeleteRow { row_ids }, + AssertBlock { + block_index: 0, + row_count: 3, + start_row_index: 0, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_row_add_cells_test() { + let mut test = GridEditorTest::new().await; + let mut builder = CreateRowMetaBuilder::new(&test.field_metas); + for field in &test.field_metas { + match field.field_type { + FieldType::RichText => { + builder.add_cell(&field.id, "hello world".to_owned()).unwrap(); + } + FieldType::Number => { + builder.add_cell(&field.id, "18,443".to_owned()).unwrap(); + } + FieldType::DateTime => { + builder + .add_cell(&field.id, make_date_cell_string("1647251762")) + .unwrap(); + } + FieldType::SingleSelect => { + let type_option = SingleSelectTypeOption::from(field); + let option = type_option.options.first().unwrap(); + builder.add_select_option_cell(&field.id, option.id.clone()).unwrap(); + } + FieldType::MultiSelect => { + let type_option = MultiSelectTypeOption::from(field); + let ops_ids = type_option + .options + .iter() + .map(|option| option.id.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + builder.add_select_option_cell(&field.id, ops_ids).unwrap(); + } + FieldType::Checkbox => { + builder.add_cell(&field.id, "false".to_string()).unwrap(); + } + FieldType::URL => { + builder.add_cell(&field.id, "1".to_string()).unwrap(); + } + } + } + let context = builder.build(); + let scripts = vec![CreateRow { context }, AssertGridMetaPad]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_row_add_date_cell_test() { + let mut test = GridEditorTest::new().await; + let mut builder = CreateRowMetaBuilder::new(&test.field_metas); + let mut date_field = None; + let timestamp = 1647390674; + for field in &test.field_metas { + if field.field_type == FieldType::DateTime { + date_field = Some(field.clone()); + NaiveDateTime::from_timestamp(123, 0); + // The data should not be empty + assert!(builder.add_cell(&field.id, "".to_string()).is_err()); + assert!(builder.add_cell(&field.id, make_date_cell_string("123")).is_ok()); + assert!(builder + .add_cell(&field.id, make_date_cell_string(×tamp.to_string())) + .is_ok()); + } + } + let context = builder.build(); + let date_field = date_field.unwrap(); + let cell_data = context.cell_by_field_id.get(&date_field.id).unwrap().clone(); + assert_eq!( + decode_cell_data_from_type_option_cell_data(cell_data.data.clone(), &date_field, &date_field.field_type) + .parse::() + .unwrap() + .date, + "2022/03/16", + ); + let scripts = vec![CreateRow { context }]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_cell_update() { + let mut test = GridEditorTest::new().await; + let field_metas = &test.field_metas; + let row_metas = &test.row_metas; + let grid_blocks = &test.grid_blocks; + assert_eq!(row_metas.len(), 3); + assert_eq!(grid_blocks.len(), 1); + + let block_id = &grid_blocks.first().unwrap().block_id; + let mut scripts = vec![]; + for (index, row_meta) in row_metas.iter().enumerate() { + for field_meta in field_metas { + if index == 0 { + let data = match field_meta.field_type { + FieldType::RichText => "".to_string(), + FieldType::Number => "123".to_string(), + FieldType::DateTime => make_date_cell_string("123"), + FieldType::SingleSelect => { + let type_option = SingleSelectTypeOption::from(field_meta); + SelectOptionCellContentChangeset::from_insert(&type_option.options.first().unwrap().id).to_str() + } + FieldType::MultiSelect => { + let type_option = MultiSelectTypeOption::from(field_meta); + SelectOptionCellContentChangeset::from_insert(&type_option.options.first().unwrap().id).to_str() + } + FieldType::Checkbox => "1".to_string(), + FieldType::URL => "1".to_string(), + }; + + scripts.push(UpdateCell { + changeset: CellChangeset { + grid_id: block_id.to_string(), + row_id: row_meta.id.clone(), + field_id: field_meta.id.clone(), + cell_content_changeset: Some(data), + }, + is_err: false, + }); + } + + if index == 1 { + let (data, is_err) = match field_meta.field_type { + FieldType::RichText => ("1".to_string().repeat(10001), true), + FieldType::Number => ("abc".to_string(), true), + FieldType::DateTime => ("abc".to_string(), true), + FieldType::SingleSelect => (SelectOptionCellContentChangeset::from_insert("abc").to_str(), false), + FieldType::MultiSelect => (SelectOptionCellContentChangeset::from_insert("abc").to_str(), false), + FieldType::Checkbox => ("2".to_string(), false), + FieldType::URL => ("2".to_string(), false), + }; + + scripts.push(UpdateCell { + changeset: CellChangeset { + grid_id: block_id.to_string(), + row_id: row_meta.id.clone(), + field_id: field_meta.id.clone(), + cell_content_changeset: Some(data), + }, + is_err, + }); + } + } + } + + test.run_scripts(scripts).await; +} + +fn make_date_cell_string(s: &str) -> String { + serde_json::to_string(&DateCellContentChangeset { + date: Some(s.to_string()), + time: None, + }) + .unwrap() +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/mod.rs b/frontend/rust-lib/flowy-grid/tests/grid/mod.rs index 4d746661eb..8865bf01c2 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/mod.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/mod.rs @@ -1,8 +1,5 @@ mod block_test; mod cell_test; mod field_test; -mod field_util; mod filter_test; -mod row_test; -mod row_util; -mod script; +mod grid_editor; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/row_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/row_test.rs deleted file mode 100644 index 73b24f9afd..0000000000 --- a/frontend/rust-lib/flowy-grid/tests/grid/row_test.rs +++ /dev/null @@ -1,150 +0,0 @@ -use crate::grid::field_util::*; -use crate::grid::row_util::GridRowTestBuilder; -use crate::grid::script::EditorScript::*; -use crate::grid::script::*; -use chrono::NaiveDateTime; -use flowy_grid::entities::FieldType; -use flowy_grid::services::cell::decode_any_cell_data; -use flowy_grid::services::field::select_option::SELECTION_IDS_SEPARATOR; -use flowy_grid::services::field::{DateCellData, MultiSelectTypeOption, SingleSelectTypeOption}; -use flowy_grid::services::row::CreateRowRevisionBuilder; -use flowy_grid_data_model::revision::RowMetaChangeset; - -#[tokio::test] -async fn grid_create_row_count_test() { - let test = GridEditorTest::new().await; - let scripts = vec![ - AssertRowCount(3), - CreateEmptyRow, - CreateEmptyRow, - CreateRow { - payload: GridRowTestBuilder::new(&test).build(), - }, - AssertRowCount(6), - ]; - GridEditorTest::new().await.run_scripts(scripts).await; -} - -#[tokio::test] -async fn grid_update_row() { - let mut test = GridEditorTest::new().await; - let payload = GridRowTestBuilder::new(&test).build(); - let changeset = RowMetaChangeset { - row_id: payload.row_id.clone(), - height: None, - visibility: None, - cell_by_field_id: Default::default(), - }; - - let scripts = vec![AssertRowCount(3), CreateRow { payload }, UpdateRow { changeset }]; - test.run_scripts(scripts).await; - - let expected_row = (&*test.row_revs.last().cloned().unwrap()).clone(); - let scripts = vec![AssertRow { expected_row }, AssertRowCount(4)]; - test.run_scripts(scripts).await; -} - -#[tokio::test] -async fn grid_delete_row() { - let mut test = GridEditorTest::new().await; - let payload1 = GridRowTestBuilder::new(&test).build(); - let payload2 = GridRowTestBuilder::new(&test).build(); - let row_ids = vec![payload1.row_id.clone(), payload2.row_id.clone()]; - let scripts = vec![ - AssertRowCount(3), - CreateRow { payload: payload1 }, - CreateRow { payload: payload2 }, - AssertBlockCount(1), - AssertBlock { - block_index: 0, - row_count: 5, - start_row_index: 0, - }, - DeleteRows { row_ids }, - AssertBlock { - block_index: 0, - row_count: 3, - start_row_index: 0, - }, - ]; - test.run_scripts(scripts).await; -} - -#[tokio::test] -async fn grid_row_add_cells_test() { - let mut test = GridEditorTest::new().await; - let mut builder = CreateRowRevisionBuilder::new(&test.field_revs); - for field in &test.field_revs { - let field_type: FieldType = field.field_type_rev.into(); - match field_type { - FieldType::RichText => { - builder.add_cell(&field.id, "hello world".to_owned()).unwrap(); - } - FieldType::Number => { - builder.add_cell(&field.id, "18,443".to_owned()).unwrap(); - } - FieldType::DateTime => { - builder - .add_cell(&field.id, make_date_cell_string("1647251762")) - .unwrap(); - } - FieldType::SingleSelect => { - let type_option = SingleSelectTypeOption::from(field); - let option = type_option.options.first().unwrap(); - builder.add_select_option_cell(&field.id, option.id.clone()).unwrap(); - } - FieldType::MultiSelect => { - let type_option = MultiSelectTypeOption::from(field); - let ops_ids = type_option - .options - .iter() - .map(|option| option.id.clone()) - .collect::>() - .join(SELECTION_IDS_SEPARATOR); - builder.add_select_option_cell(&field.id, ops_ids).unwrap(); - } - FieldType::Checkbox => { - builder.add_cell(&field.id, "false".to_string()).unwrap(); - } - FieldType::URL => { - builder.add_cell(&field.id, "1".to_string()).unwrap(); - } - } - } - let context = builder.build(); - let scripts = vec![CreateRow { payload: context }, AssertGridRevisionPad]; - test.run_scripts(scripts).await; -} - -#[tokio::test] -async fn grid_row_add_date_cell_test() { - let mut test = GridEditorTest::new().await; - let mut builder = CreateRowRevisionBuilder::new(&test.field_revs); - let mut date_field = None; - let timestamp = 1647390674; - for field in &test.field_revs { - let field_type: FieldType = field.field_type_rev.into(); - if field_type == FieldType::DateTime { - date_field = Some(field.clone()); - NaiveDateTime::from_timestamp(123, 0); - // The data should not be empty - assert!(builder.add_cell(&field.id, "".to_string()).is_err()); - assert!(builder.add_cell(&field.id, make_date_cell_string("123")).is_ok()); - assert!(builder - .add_cell(&field.id, make_date_cell_string(×tamp.to_string())) - .is_ok()); - } - } - let context = builder.build(); - let date_field = date_field.unwrap(); - let cell_rev = context.cell_by_field_id.get(&date_field.id).unwrap(); - assert_eq!( - decode_any_cell_data(cell_rev, &date_field) - .parse::() - .unwrap() - .date, - "2022/03/16", - ); - let scripts = vec![CreateRow { payload: context }]; - test.run_scripts(scripts).await; -} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/row_util.rs b/frontend/rust-lib/flowy-grid/tests/grid/row_util.rs deleted file mode 100644 index bdd6dd86dc..0000000000 --- a/frontend/rust-lib/flowy-grid/tests/grid/row_util.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::grid::script::GridEditorTest; -use flowy_grid::entities::FieldType; -use flowy_grid::services::field::DateCellChangeset; -use flowy_grid::services::row::{CreateRowRevisionBuilder, CreateRowRevisionPayload}; -use flowy_grid_data_model::revision::FieldRevision; -use strum::EnumCount; - -pub struct GridRowTestBuilder<'a> { - test: &'a GridEditorTest, - inner_builder: CreateRowRevisionBuilder<'a>, -} - -impl<'a> GridRowTestBuilder<'a> { - pub fn new(test: &'a GridEditorTest) -> Self { - assert_eq!(test.field_revs.len(), FieldType::COUNT); - - let inner_builder = CreateRowRevisionBuilder::new(&test.field_revs); - Self { test, inner_builder } - } - #[allow(dead_code)] - pub fn update_text_cell(mut self, data: String) -> Self { - let text_field = self.field_rev_with_type(&FieldType::DateTime); - self.inner_builder.add_cell(&text_field.id, data).unwrap(); - self - } - - #[allow(dead_code)] - pub fn update_number_cell(mut self, data: String) -> Self { - let number_field = self.field_rev_with_type(&FieldType::DateTime); - self.inner_builder.add_cell(&number_field.id, data).unwrap(); - self - } - - #[allow(dead_code)] - pub fn update_date_cell(mut self, value: i64) -> Self { - let value = serde_json::to_string(&DateCellChangeset { - date: Some(value.to_string()), - time: None, - }) - .unwrap(); - let date_field = self.field_rev_with_type(&FieldType::DateTime); - self.inner_builder.add_cell(&date_field.id, value).unwrap(); - self - } - - #[allow(dead_code)] - pub fn update_checkbox_cell(mut self, data: bool) -> Self { - let number_field = self.field_rev_with_type(&FieldType::Checkbox); - self.inner_builder.add_cell(&number_field.id, data.to_string()).unwrap(); - self - } - - #[allow(dead_code)] - pub fn update_url_cell(mut self, data: String) -> Self { - let number_field = self.field_rev_with_type(&FieldType::Checkbox); - self.inner_builder.add_cell(&number_field.id, data).unwrap(); - self - } - - pub fn field_rev_with_type(&self, field_type: &FieldType) -> FieldRevision { - self.test - .field_revs - .iter() - .find(|field_rev| { - let t_field_type: FieldType = field_rev.field_type_rev.into(); - &t_field_type == field_type - }) - .unwrap() - .as_ref() - .clone() - } - - pub fn build(self) -> CreateRowRevisionPayload { - self.inner_builder.build() - } -} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/script.rs index 915749fb0e..c1fa47c7a9 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/script.rs @@ -1,19 +1,14 @@ -#![allow(clippy::all)] -#![allow(dead_code)] -#![allow(unused_imports)] use bytes::Bytes; -use flowy_grid::entities::*; -use flowy_grid::services::field::select_option::SelectOption; use flowy_grid::services::field::*; -use flowy_grid::services::grid_editor::{GridPadBuilder, GridRevisionEditor}; -use flowy_grid::services::row::CreateRowRevisionPayload; -use flowy_grid::services::setting::GridSettingChangesetBuilder; -use flowy_grid_data_model::revision::*; +use flowy_grid::services::grid_meta_editor::{GridMetaEditor, GridPadBuilder}; +use flowy_grid::services::row::CreateRowMetaPayload; +use flowy_grid_data_model::entities::{ + BuildGridContext, CellChangeset, Field, FieldChangesetParams, FieldMeta, FieldOrder, FieldType, + GridBlockInfoChangeset, GridBlockMetaSnapshot, InsertFieldParams, RowMeta, RowMetaChangeset, RowOrder, + TypeOptionDataEntry, +}; use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS; use flowy_sync::client_grid::GridBuilder; -use flowy_sync::entities::grid::{ - CreateGridFilterParams, DeleteFilterParams, FieldChangesetParams, GridSettingChangesetParams, -}; use flowy_test::helper::ViewTest; use flowy_test::FlowySDKTest; use std::collections::HashMap; @@ -30,18 +25,18 @@ pub enum EditorScript { changeset: FieldChangesetParams, }, DeleteField { - field_rev: FieldRevision, + field_meta: FieldMeta, }, AssertFieldCount(usize), AssertFieldEqual { field_index: usize, - field_rev: FieldRevision, + field_meta: FieldMeta, }, CreateBlock { - block: GridBlockMetaRevision, + block: GridBlockMetaSnapshot, }, UpdateBlock { - changeset: GridBlockMetaRevisionChangeset, + changeset: GridBlockInfoChangeset, }, AssertBlockCount(usize), AssertBlock { @@ -51,19 +46,19 @@ pub enum EditorScript { }, AssertBlockEqual { block_index: usize, - block: GridBlockMetaRevision, + block: GridBlockMetaSnapshot, }, CreateEmptyRow, CreateRow { - payload: CreateRowRevisionPayload, + context: CreateRowMetaPayload, }, UpdateRow { changeset: RowMetaChangeset, }, AssertRow { - expected_row: RowRevision, + changeset: RowMetaChangeset, }, - DeleteRows { + DeleteRow { row_ids: Vec, }, UpdateCell { @@ -71,65 +66,42 @@ pub enum EditorScript { is_err: bool, }, AssertRowCount(usize), - #[allow(dead_code)] - UpdateGridSetting { - params: GridSettingChangesetParams, - }, - InsertGridTableFilter { - payload: CreateGridFilterPayload, - }, - AssertTableFilterCount { - count: i32, - }, - DeleteGridTableFilter { - filter_id: String, - field_rev: FieldRevision, - }, - #[allow(dead_code)] - AssertGridSetting { - expected_setting: GridSetting, - }, - AssertGridRevisionPad, + // AssertRowEqual{ row_index: usize, row: RowMeta}, + AssertGridMetaPad, } pub struct GridEditorTest { pub sdk: FlowySDKTest, pub grid_id: String, - pub editor: Arc, - pub field_revs: Vec>, - pub block_meta_revs: Vec>, - pub row_revs: Vec>, + pub editor: Arc, + pub field_metas: Vec, + pub grid_blocks: Vec, + pub row_metas: Vec>, pub field_count: usize, - pub row_order_by_row_id: HashMap, + pub row_order_by_row_id: HashMap, } impl GridEditorTest { pub async fn new() -> Self { let sdk = FlowySDKTest::default(); let _ = sdk.init_user().await; - let build_context = make_all_field_test_grid(); + let build_context = make_template_1_grid(); let view_data: Bytes = build_context.into(); let test = ViewTest::new_grid_view(&sdk, view_data.to_vec()).await; let editor = sdk.grid_manager.open_grid(&test.view.id).await.unwrap(); - let field_revs = editor.get_field_revs(None).await.unwrap(); - let block_meta_revs = editor.get_block_meta_revs().await.unwrap(); - let row_revs = editor.grid_block_snapshots(None).await.unwrap().pop().unwrap().row_revs; - assert_eq!(row_revs.len(), 3); - assert_eq!(block_meta_revs.len(), 1); - - // It seems like you should add the field in the make_test_grid() function. - // Because we assert the initialize count of the fields is equal to FieldType::COUNT. - assert_eq!(field_revs.len(), FieldType::COUNT); + let field_metas = editor.get_field_metas::(None).await.unwrap(); + let grid_blocks = editor.get_block_metas().await.unwrap(); + let row_metas = get_row_metas(&editor).await; let grid_id = test.view.id; Self { sdk, grid_id, editor, - field_revs, - block_meta_revs, - row_revs, + field_metas, + grid_blocks, + row_metas, field_count: FieldType::COUNT, row_order_by_row_id: HashMap::default(), } @@ -154,94 +126,93 @@ impl GridEditorTest { } self.editor.insert_field(params).await.unwrap(); - self.field_revs = self.editor.get_field_revs(None).await.unwrap(); - assert_eq!(self.field_count, self.field_revs.len()); + self.field_metas = self.editor.get_field_metas::(None).await.unwrap(); + assert_eq!(self.field_count, self.field_metas.len()); } EditorScript::UpdateField { changeset: change } => { self.editor.update_field(change).await.unwrap(); - self.field_revs = self.editor.get_field_revs(None).await.unwrap(); + self.field_metas = self.editor.get_field_metas::(None).await.unwrap(); } - EditorScript::DeleteField { field_rev } => { - if self.editor.contain_field(&field_rev.id).await { + EditorScript::DeleteField { field_meta } => { + if self.editor.contain_field(&field_meta.id).await { self.field_count -= 1; } - self.editor.delete_field(&field_rev.id).await.unwrap(); - self.field_revs = self.editor.get_field_revs(None).await.unwrap(); - assert_eq!(self.field_count, self.field_revs.len()); + self.editor.delete_field(&field_meta.id).await.unwrap(); + self.field_metas = self.editor.get_field_metas::(None).await.unwrap(); + assert_eq!(self.field_count, self.field_metas.len()); } EditorScript::AssertFieldCount(count) => { - assert_eq!(self.editor.get_field_revs(None).await.unwrap().len(), count); + assert_eq!( + self.editor.get_field_metas::(None).await.unwrap().len(), + count + ); } - EditorScript::AssertFieldEqual { field_index, field_rev } => { - let field_revs = self.editor.get_field_revs(None).await.unwrap(); - assert_eq!(field_revs[field_index].as_ref(), &field_rev); + EditorScript::AssertFieldEqual { + field_index, + field_meta, + } => { + let field_metas = self.editor.get_field_metas::(None).await.unwrap(); + assert_eq!(field_metas[field_index].clone(), field_meta); } EditorScript::CreateBlock { block } => { self.editor.create_block(block).await.unwrap(); - self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); + self.grid_blocks = self.editor.get_block_metas().await.unwrap(); } EditorScript::UpdateBlock { changeset: change } => { self.editor.update_block(change).await.unwrap(); } EditorScript::AssertBlockCount(count) => { - assert_eq!(self.editor.get_block_meta_revs().await.unwrap().len(), count); + assert_eq!(self.editor.get_block_metas().await.unwrap().len(), count); } EditorScript::AssertBlock { block_index, row_count, start_row_index, } => { - assert_eq!(self.block_meta_revs[block_index].row_count, row_count); - assert_eq!(self.block_meta_revs[block_index].start_row_index, start_row_index); + assert_eq!(self.grid_blocks[block_index].row_count, row_count); + assert_eq!(self.grid_blocks[block_index].start_row_index, start_row_index); } EditorScript::AssertBlockEqual { block_index, block } => { - let blocks = self.editor.get_block_meta_revs().await.unwrap(); + let blocks = self.editor.get_block_metas().await.unwrap(); let compared_block = blocks[block_index].clone(); - assert_eq!(compared_block, Arc::new(block)); + assert_eq!(compared_block, block); } EditorScript::CreateEmptyRow => { let row_order = self.editor.create_row(None).await.unwrap(); - self.row_order_by_row_id - .insert(row_order.row_id().to_owned(), row_order); - self.row_revs = self.get_row_revs().await; - self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); + self.row_order_by_row_id.insert(row_order.row_id.clone(), row_order); + self.row_metas = self.get_row_metas().await; + self.grid_blocks = self.editor.get_block_metas().await.unwrap(); } - EditorScript::CreateRow { payload: context } => { + EditorScript::CreateRow { context } => { let row_orders = self.editor.insert_rows(vec![context]).await.unwrap(); for row_order in row_orders { - self.row_order_by_row_id - .insert(row_order.row_id().to_owned(), row_order); + self.row_order_by_row_id.insert(row_order.row_id.clone(), row_order); } - self.row_revs = self.get_row_revs().await; - self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); + self.row_metas = self.get_row_metas().await; + self.grid_blocks = self.editor.get_block_metas().await.unwrap(); } EditorScript::UpdateRow { changeset: change } => self.editor.update_row(change).await.unwrap(), - EditorScript::DeleteRows { row_ids } => { + EditorScript::DeleteRow { row_ids } => { let row_orders = row_ids .into_iter() .map(|row_id| self.row_order_by_row_id.get(&row_id).unwrap().clone()) - .collect::>(); + .collect::>(); self.editor.delete_rows(row_orders).await.unwrap(); - self.row_revs = self.get_row_revs().await; - self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); + self.row_metas = self.get_row_metas().await; + self.grid_blocks = self.editor.get_block_metas().await.unwrap(); } - EditorScript::AssertRow { expected_row } => { - let row = &*self - .row_revs - .iter() - .find(|row| row.id == expected_row.id) - .cloned() - .unwrap(); - assert_eq!(&expected_row, row); - // if let Some(visibility) = changeset.visibility { - // assert_eq!(row.visibility, visibility); - // } - // - // if let Some(height) = changeset.height { - // assert_eq!(row.height, height); - // } + EditorScript::AssertRow { changeset } => { + let row = self.row_metas.iter().find(|row| row.id == changeset.row_id).unwrap(); + + if let Some(visibility) = changeset.visibility { + assert_eq!(row.visibility, visibility); + } + + if let Some(height) = changeset.height { + assert_eq!(row.height, height); + } } EditorScript::UpdateCell { changeset, is_err } => { let result = self.editor.update_cell(changeset).await; @@ -249,44 +220,13 @@ impl GridEditorTest { assert!(result.is_err()) } else { let _ = result.unwrap(); - self.row_revs = self.get_row_revs().await; + self.row_metas = self.get_row_metas().await; } } - EditorScript::AssertRowCount(expected_row_count) => { - assert_eq!(expected_row_count, self.row_revs.len()); + EditorScript::AssertRowCount(count) => { + assert_eq!(self.row_metas.len(), count); } - EditorScript::UpdateGridSetting { params } => { - let _ = self.editor.update_grid_setting(params).await.unwrap(); - } - EditorScript::InsertGridTableFilter { payload } => { - let params: CreateGridFilterParams = payload.try_into().unwrap(); - let layout_type = GridLayoutType::Table; - let params = GridSettingChangesetBuilder::new(&self.grid_id, &layout_type) - .insert_filter(params) - .build(); - let _ = self.editor.update_grid_setting(params).await.unwrap(); - } - EditorScript::AssertTableFilterCount { count } => { - let layout_type = GridLayoutType::Table; - let filters = self.editor.get_grid_filter(&layout_type).await.unwrap(); - assert_eq!(count as usize, filters.len()); - } - EditorScript::DeleteGridTableFilter { filter_id, field_rev } => { - let layout_type = GridLayoutType::Table; - let params = GridSettingChangesetBuilder::new(&self.grid_id, &layout_type) - .delete_filter(DeleteFilterParams { - field_id: field_rev.id, - filter_id, - field_type_rev: field_rev.field_type_rev, - }) - .build(); - let _ = self.editor.update_grid_setting(params).await.unwrap(); - } - EditorScript::AssertGridSetting { expected_setting } => { - let setting = self.editor.get_grid_setting().await.unwrap(); - assert_eq!(expected_setting, setting); - } - EditorScript::AssertGridRevisionPad => { + EditorScript::AssertGridMetaPad => { sleep(Duration::from_millis(2 * REVISION_WRITE_INTERVAL_IN_MILLIS)).await; let mut grid_rev_manager = grid_manager.make_grid_rev_manager(&self.grid_id, pool.clone()).unwrap(); let grid_pad = grid_rev_manager.load::(None).await.unwrap(); @@ -295,35 +235,89 @@ impl GridEditorTest { } } - async fn get_row_revs(&self) -> Vec> { - self.editor - .grid_block_snapshots(None) - .await - .unwrap() - .pop() - .unwrap() - .row_revs - } - - pub async fn grid_filters(&self) -> Vec { - let layout_type = GridLayoutType::Table; - self.editor.get_grid_filter(&layout_type).await.unwrap() - } - - pub fn text_field(&self) -> &FieldRevision { - self.field_revs - .iter() - .filter(|field_rev| { - let t_field_type: FieldType = field_rev.field_type_rev.into(); - t_field_type == FieldType::RichText - }) - .collect::>() - .pop() - .unwrap() + async fn get_row_metas(&self) -> Vec> { + get_row_metas(&self.editor).await } } -fn make_all_field_test_grid() -> BuildGridContext { +async fn get_row_metas(editor: &Arc) -> Vec> { + editor + .grid_block_snapshots(None) + .await + .unwrap() + .pop() + .unwrap() + .row_metas +} + +pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) { + let field_meta = FieldBuilder::new(RichTextTypeOptionBuilder::default()) + .name("Name") + .visibility(true) + .build(); + + let cloned_field_meta = field_meta.clone(); + + let type_option_data = field_meta + .get_type_option_entry::(&field_meta.field_type) + .unwrap() + .protobuf_bytes() + .to_vec(); + + let field = Field { + id: field_meta.id, + name: field_meta.name, + desc: field_meta.desc, + field_type: field_meta.field_type, + frozen: field_meta.frozen, + visibility: field_meta.visibility, + width: field_meta.width, + is_primary: false, + }; + + let params = InsertFieldParams { + grid_id: grid_id.to_owned(), + field, + type_option_data, + start_field_id: None, + }; + (params, cloned_field_meta) +} + +pub fn create_single_select_field(grid_id: &str) -> (InsertFieldParams, FieldMeta) { + let single_select = SingleSelectTypeOptionBuilder::default() + .option(SelectOption::new("Done")) + .option(SelectOption::new("Progress")); + + let field_meta = FieldBuilder::new(single_select).name("Name").visibility(true).build(); + let cloned_field_meta = field_meta.clone(); + let type_option_data = field_meta + .get_type_option_entry::(&field_meta.field_type) + .unwrap() + .protobuf_bytes() + .to_vec(); + + let field = Field { + id: field_meta.id, + name: field_meta.name, + desc: field_meta.desc, + field_type: field_meta.field_type, + frozen: field_meta.frozen, + visibility: field_meta.visibility, + width: field_meta.width, + is_primary: false, + }; + + let params = InsertFieldParams { + grid_id: grid_id.to_owned(), + field, + type_option_data, + start_field_id: None, + }; + (params, cloned_field_meta) +} + +fn make_template_1_grid() -> BuildGridContext { let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) .name("Name") .visibility(true) diff --git a/frontend/rust-lib/flowy-net/src/http_server/document.rs b/frontend/rust-lib/flowy-net/src/http_server/document.rs index fe9f31f2ca..ca9cb47955 100644 --- a/frontend/rust-lib/flowy-net/src/http_server/document.rs +++ b/frontend/rust-lib/flowy-net/src/http_server/document.rs @@ -3,7 +3,7 @@ use crate::{ request::{HttpRequestBuilder, ResponseMiddleware}, }; use flowy_error::FlowyError; -use flowy_sync::entities::text_block::{CreateTextBlockParams, ResetTextBlockParams, TextBlockId, TextBlockInfo}; +use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB}; use flowy_text_block::BlockCloudService; use http_flowy::response::FlowyResponse; use lazy_static::lazy_static; @@ -27,7 +27,7 @@ impl BlockCloudService for BlockHttpCloudService { FutureResult::new(async move { create_document_request(&token, params, &url).await }) } - fn read_block(&self, token: &str, params: TextBlockId) -> FutureResult, FlowyError> { + fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult, FlowyError> { let token = token.to_owned(); let url = self.config.doc_url(); FutureResult::new(async move { read_document_request(&token, params, &url).await }) @@ -52,9 +52,9 @@ pub async fn create_document_request(token: &str, params: CreateTextBlockParams, pub async fn read_document_request( token: &str, - params: TextBlockId, + params: TextBlockIdPB, url: &str, -) -> Result, FlowyError> { +) -> Result, FlowyError> { let doc = request_builder() .get(&url.to_owned()) .header(HEADER_TOKEN, token) diff --git a/frontend/rust-lib/flowy-net/src/http_server/folder.rs b/frontend/rust-lib/flowy-net/src/http_server/folder.rs index 788f7fa462..e600d617a9 100644 --- a/frontend/rust-lib/flowy-net/src/http_server/folder.rs +++ b/frontend/rust-lib/flowy-net/src/http_server/folder.rs @@ -1,15 +1,14 @@ use crate::{ - configuration::{ClientServerConfiguration, HEADER_TOKEN}, + configuration::ClientServerConfiguration, request::{HttpRequestBuilder, ResponseMiddleware}, }; use flowy_error::FlowyError; use flowy_folder::entities::{ - trash::RepeatedTrashId, - view::{CreateViewParams, RepeatedViewId, UpdateViewParams, ViewId}, - workspace::{CreateWorkspaceParams, UpdateWorkspaceParams, WorkspaceId}, - {AppId, CreateAppParams, UpdateAppParams}, + trash::RepeatedTrashIdPB, + view::{CreateViewParams, RepeatedViewIdPB, UpdateViewParams, ViewIdPB}, + workspace::{CreateWorkspaceParams, UpdateWorkspaceParams, WorkspaceIdPB}, + {AppIdPB, CreateAppParams, UpdateAppParams}, }; - use flowy_folder::event_map::FolderCouldServiceV1; use flowy_folder_data_model::revision::{AppRevision, TrashRevision, ViewRevision, WorkspaceRevision}; use http_flowy::errors::ServerError; @@ -45,7 +44,7 @@ impl FolderCouldServiceV1 for FolderHttpCloudService { }) } - fn read_workspace(&self, token: &str, params: WorkspaceId) -> FutureResult, FlowyError> { + fn read_workspace(&self, token: &str, params: WorkspaceIdPB) -> FutureResult, FlowyError> { let token = token.to_owned(); let url = self.config.workspace_url(); FutureResult::new(async move { @@ -63,7 +62,7 @@ impl FolderCouldServiceV1 for FolderHttpCloudService { }) } - fn delete_workspace(&self, token: &str, params: WorkspaceId) -> FutureResult<(), FlowyError> { + fn delete_workspace(&self, token: &str, params: WorkspaceIdPB) -> FutureResult<(), FlowyError> { let token = token.to_owned(); let url = self.config.workspace_url(); FutureResult::new(async move { @@ -81,7 +80,7 @@ impl FolderCouldServiceV1 for FolderHttpCloudService { }) } - fn read_view(&self, token: &str, params: ViewId) -> FutureResult, FlowyError> { + fn read_view(&self, token: &str, params: ViewIdPB) -> FutureResult, FlowyError> { let token = token.to_owned(); let url = self.config.view_url(); FutureResult::new(async move { @@ -90,7 +89,7 @@ impl FolderCouldServiceV1 for FolderHttpCloudService { }) } - fn delete_view(&self, token: &str, params: RepeatedViewId) -> FutureResult<(), FlowyError> { + fn delete_view(&self, token: &str, params: RepeatedViewIdPB) -> FutureResult<(), FlowyError> { let token = token.to_owned(); let url = self.config.view_url(); FutureResult::new(async move { @@ -117,7 +116,7 @@ impl FolderCouldServiceV1 for FolderHttpCloudService { }) } - fn read_app(&self, token: &str, params: AppId) -> FutureResult, FlowyError> { + fn read_app(&self, token: &str, params: AppIdPB) -> FutureResult, FlowyError> { let token = token.to_owned(); let url = self.config.app_url(); FutureResult::new(async move { @@ -135,7 +134,7 @@ impl FolderCouldServiceV1 for FolderHttpCloudService { }) } - fn delete_app(&self, token: &str, params: AppId) -> FutureResult<(), FlowyError> { + fn delete_app(&self, token: &str, params: AppIdPB) -> FutureResult<(), FlowyError> { let token = token.to_owned(); let url = self.config.app_url(); FutureResult::new(async move { @@ -144,7 +143,7 @@ impl FolderCouldServiceV1 for FolderHttpCloudService { }) } - fn create_trash(&self, token: &str, params: RepeatedTrashId) -> FutureResult<(), FlowyError> { + fn create_trash(&self, token: &str, params: RepeatedTrashIdPB) -> FutureResult<(), FlowyError> { let token = token.to_owned(); let url = self.config.trash_url(); FutureResult::new(async move { @@ -153,7 +152,7 @@ impl FolderCouldServiceV1 for FolderHttpCloudService { }) } - fn delete_trash(&self, token: &str, params: RepeatedTrashId) -> FutureResult<(), FlowyError> { + fn delete_trash(&self, token: &str, params: RepeatedTrashIdPB) -> FutureResult<(), FlowyError> { let token = token.to_owned(); let url = self.config.trash_url(); FutureResult::new(async move { @@ -172,6 +171,7 @@ impl FolderCouldServiceV1 for FolderHttpCloudService { } } +#[allow(dead_code)] fn request_builder() -> HttpRequestBuilder { HttpRequestBuilder::new().middleware(MIDDLEWARE.clone()) } @@ -193,7 +193,7 @@ pub async fn create_workspace_request( pub async fn read_workspaces_request( _token: &str, - _params: WorkspaceId, + _params: WorkspaceIdPB, _url: &str, ) -> Result, ServerError> { // let repeated_workspace = request_builder() @@ -208,26 +208,26 @@ pub async fn read_workspaces_request( } pub async fn update_workspace_request( - token: &str, - params: UpdateWorkspaceParams, - url: &str, + _token: &str, + _params: UpdateWorkspaceParams, + _url: &str, ) -> Result<(), ServerError> { - let _ = request_builder() - .patch(&url.to_owned()) - .header(HEADER_TOKEN, token) - .protobuf(params)? - .send() - .await?; + // let _ = request_builder() + // .patch(&url.to_owned()) + // .header(HEADER_TOKEN, token) + // .protobuf(params)? + // .send() + // .await?; Ok(()) } -pub async fn delete_workspace_request(token: &str, params: WorkspaceId, url: &str) -> Result<(), ServerError> { - let _ = request_builder() - .delete(url) - .header(HEADER_TOKEN, token) - .protobuf(params)? - .send() - .await?; +pub async fn delete_workspace_request(_token: &str, _params: WorkspaceIdPB, _url: &str) -> Result<(), ServerError> { + // let _ = request_builder() + // .delete(url) + // .header(HEADER_TOKEN, token) + // .protobuf(params)? + // .send() + // .await?; Ok(()) } @@ -247,7 +247,7 @@ pub async fn create_app_request( unimplemented!() } -pub async fn read_app_request(_token: &str, _params: AppId, _url: &str) -> Result, ServerError> { +pub async fn read_app_request(_token: &str, _params: AppIdPB, _url: &str) -> Result, ServerError> { // let app = request_builder() // .get(&url.to_owned()) // .header(HEADER_TOKEN, token) @@ -259,23 +259,23 @@ pub async fn read_app_request(_token: &str, _params: AppId, _url: &str) -> Resul unimplemented!() } -pub async fn update_app_request(token: &str, params: UpdateAppParams, url: &str) -> Result<(), ServerError> { - let _ = request_builder() - .patch(&url.to_owned()) - .header(HEADER_TOKEN, token) - .protobuf(params)? - .send() - .await?; +pub async fn update_app_request(_token: &str, _params: UpdateAppParams, _url: &str) -> Result<(), ServerError> { + // let _ = request_builder() + // .patch(&url.to_owned()) + // .header(HEADER_TOKEN, token) + // .protobuf(params)? + // .send() + // .await?; Ok(()) } -pub async fn delete_app_request(token: &str, params: AppId, url: &str) -> Result<(), ServerError> { - let _ = request_builder() - .delete(&url.to_owned()) - .header(HEADER_TOKEN, token) - .protobuf(params)? - .send() - .await?; +pub async fn delete_app_request(_token: &str, _params: AppIdPB, _url: &str) -> Result<(), ServerError> { + // let _ = request_builder() + // .delete(&url.to_owned()) + // .header(HEADER_TOKEN, token) + // .protobuf(params)? + // .send() + // .await?; Ok(()) } @@ -295,7 +295,11 @@ pub async fn create_view_request( unimplemented!() } -pub async fn read_view_request(_token: &str, _params: ViewId, _url: &str) -> Result, ServerError> { +pub async fn read_view_request( + _token: &str, + _params: ViewIdPB, + _url: &str, +) -> Result, ServerError> { // let view = request_builder() // .get(&url.to_owned()) // .header(HEADER_TOKEN, token) @@ -307,43 +311,43 @@ pub async fn read_view_request(_token: &str, _params: ViewId, _url: &str) -> Res unimplemented!() } -pub async fn update_view_request(token: &str, params: UpdateViewParams, url: &str) -> Result<(), ServerError> { - let _ = request_builder() - .patch(&url.to_owned()) - .header(HEADER_TOKEN, token) - .protobuf(params)? - .send() - .await?; +pub async fn update_view_request(_token: &str, _params: UpdateViewParams, _url: &str) -> Result<(), ServerError> { + // let _ = request_builder() + // .patch(&url.to_owned()) + // .header(HEADER_TOKEN, token) + // .protobuf(params)? + // .send() + // .await?; Ok(()) } -pub async fn delete_view_request(token: &str, params: RepeatedViewId, url: &str) -> Result<(), ServerError> { - let _ = request_builder() - .delete(&url.to_owned()) - .header(HEADER_TOKEN, token) - .protobuf(params)? - .send() - .await?; +pub async fn delete_view_request(_token: &str, _params: RepeatedViewIdPB, _url: &str) -> Result<(), ServerError> { + // let _ = request_builder() + // .delete(&url.to_owned()) + // .header(HEADER_TOKEN, token) + // .protobuf(params)? + // .send() + // .await?; Ok(()) } -pub async fn create_trash_request(token: &str, params: RepeatedTrashId, url: &str) -> Result<(), ServerError> { - let _ = request_builder() - .post(&url.to_owned()) - .header(HEADER_TOKEN, token) - .protobuf(params)? - .send() - .await?; +pub async fn create_trash_request(_token: &str, _params: RepeatedTrashIdPB, _url: &str) -> Result<(), ServerError> { + // let _ = request_builder() + // .post(&url.to_owned()) + // .header(HEADER_TOKEN, token) + // .protobuf(params)? + // .send() + // .await?; Ok(()) } -pub async fn delete_trash_request(token: &str, params: RepeatedTrashId, url: &str) -> Result<(), ServerError> { - let _ = request_builder() - .delete(&url.to_owned()) - .header(HEADER_TOKEN, token) - .protobuf(params)? - .send() - .await?; +pub async fn delete_trash_request(_token: &str, _params: RepeatedTrashIdPB, _url: &str) -> Result<(), ServerError> { + // let _ = request_builder() + // .delete(&url.to_owned()) + // .header(HEADER_TOKEN, token) + // .protobuf(params)? + // .send() + // .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-net/src/http_server/user.rs b/frontend/rust-lib/flowy-net/src/http_server/user.rs index faf5621161..7768c91bb6 100644 --- a/frontend/rust-lib/flowy-net/src/http_server/user.rs +++ b/frontend/rust-lib/flowy-net/src/http_server/user.rs @@ -1,7 +1,7 @@ use crate::{configuration::*, request::HttpRequestBuilder}; use flowy_error::FlowyError; use flowy_user::entities::{ - SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, + SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB, }; use flowy_user::event_map::UserCloudService; use http_flowy::errors::ServerError; @@ -51,7 +51,7 @@ impl UserCloudService for UserHttpCloudService { }) } - fn get_user(&self, token: &str) -> FutureResult { + fn get_user(&self, token: &str) -> FutureResult { let token = token.to_owned(); let url = self.config.user_profile_url(); FutureResult::new(async move { @@ -92,7 +92,7 @@ pub async fn user_sign_out_request(token: &str, url: &str) -> Result<(), ServerE Ok(()) } -pub async fn get_user_profile_request(token: &str, url: &str) -> Result { +pub async fn get_user_profile_request(token: &str, url: &str) -> Result { let user_profile = request_builder() .get(&url.to_owned()) .header(HEADER_TOKEN, token) diff --git a/frontend/rust-lib/flowy-net/src/local_server/persistence.rs b/frontend/rust-lib/flowy-net/src/local_server/persistence.rs index 28151af2f4..17008481b5 100644 --- a/frontend/rust-lib/flowy-net/src/local_server/persistence.rs +++ b/frontend/rust-lib/flowy-net/src/local_server/persistence.rs @@ -1,10 +1,10 @@ +use flowy_sync::entities::revision::{RepeatedRevision, Revision}; use flowy_sync::{ - entities::{folder::FolderInfo, text_block::TextBlockInfo}, + entities::{folder::FolderInfo, text_block::DocumentPB}, errors::CollaborateError, - protobuf::{RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB}, server_document::*, server_folder::FolderCloudPersistence, - util::{make_document_info_from_revisions_pb, make_folder_from_revisions_pb}, + util::{make_document_from_revision_pbs, make_folder_from_revisions_pb}, }; use lib_infra::future::BoxResultFuture; use std::{ @@ -15,17 +15,17 @@ use std::{ // For the moment, we use memory to cache the data, it will be implemented with // other storage. Like the Firestore,Dropbox.etc. pub trait RevisionCloudStorage: Send + Sync { - fn set_revisions(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError>; + fn set_revisions(&self, repeated_revision: RepeatedRevision) -> BoxResultFuture<(), CollaborateError>; fn get_revisions( &self, object_id: &str, rev_ids: Option>, - ) -> BoxResultFuture; + ) -> BoxResultFuture; fn reset_object( &self, object_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> BoxResultFuture<(), CollaborateError>; } @@ -64,7 +64,7 @@ impl FolderCloudPersistence for LocalTextBlockCloudPersistence { &self, _user_id: &str, folder_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> BoxResultFuture, CollaborateError> { let folder_id = folder_id.to_owned(); let storage = self.storage.clone(); @@ -74,7 +74,7 @@ impl FolderCloudPersistence for LocalTextBlockCloudPersistence { }) } - fn save_folder_revisions(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError> { + fn save_folder_revisions(&self, repeated_revision: RepeatedRevision) -> BoxResultFuture<(), CollaborateError> { let storage = self.storage.clone(); Box::pin(async move { let _ = storage.set_revisions(repeated_revision).await?; @@ -86,20 +86,19 @@ impl FolderCloudPersistence for LocalTextBlockCloudPersistence { &self, folder_id: &str, rev_ids: Option>, - ) -> BoxResultFuture, CollaborateError> { + ) -> BoxResultFuture, CollaborateError> { let folder_id = folder_id.to_owned(); let storage = self.storage.clone(); Box::pin(async move { - let mut repeated_revision = storage.get_revisions(&folder_id, rev_ids).await?; - let revisions: Vec = repeated_revision.take_items().into(); - Ok(revisions) + let repeated_revision = storage.get_revisions(&folder_id, rev_ids).await?; + Ok(repeated_revision.into_inner()) }) } fn reset_folder( &self, folder_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> BoxResultFuture<(), CollaborateError> { let storage = self.storage.clone(); let folder_id = folder_id.to_owned(); @@ -111,12 +110,12 @@ impl FolderCloudPersistence for LocalTextBlockCloudPersistence { } impl TextBlockCloudPersistence for LocalTextBlockCloudPersistence { - fn read_text_block(&self, doc_id: &str) -> BoxResultFuture { + fn read_text_block(&self, doc_id: &str) -> BoxResultFuture { let storage = self.storage.clone(); let doc_id = doc_id.to_owned(); Box::pin(async move { let repeated_revision = storage.get_revisions(&doc_id, None).await?; - match make_document_info_from_revisions_pb(&doc_id, repeated_revision)? { + match make_document_from_revision_pbs(&doc_id, repeated_revision)? { Some(document_info) => Ok(document_info), None => Err(CollaborateError::record_not_found()), } @@ -126,13 +125,13 @@ impl TextBlockCloudPersistence for LocalTextBlockCloudPersistence { fn create_text_block( &self, doc_id: &str, - repeated_revision: RepeatedRevisionPB, - ) -> BoxResultFuture, CollaborateError> { + repeated_revision: RepeatedRevision, + ) -> BoxResultFuture, CollaborateError> { let doc_id = doc_id.to_owned(); let storage = self.storage.clone(); Box::pin(async move { let _ = storage.set_revisions(repeated_revision.clone()).await?; - make_document_info_from_revisions_pb(&doc_id, repeated_revision) + make_document_from_revision_pbs(&doc_id, repeated_revision) }) } @@ -140,20 +139,16 @@ impl TextBlockCloudPersistence for LocalTextBlockCloudPersistence { &self, doc_id: &str, rev_ids: Option>, - ) -> BoxResultFuture, CollaborateError> { + ) -> BoxResultFuture, CollaborateError> { let doc_id = doc_id.to_owned(); let storage = self.storage.clone(); Box::pin(async move { - let mut repeated_revision = storage.get_revisions(&doc_id, rev_ids).await?; - let revisions: Vec = repeated_revision.take_items().into(); - Ok(revisions) + let repeated_revision = storage.get_revisions(&doc_id, rev_ids).await?; + Ok(repeated_revision.into_inner()) }) } - fn save_text_block_revisions( - &self, - repeated_revision: RepeatedRevisionPB, - ) -> BoxResultFuture<(), CollaborateError> { + fn save_text_block_revisions(&self, repeated_revision: RepeatedRevision) -> BoxResultFuture<(), CollaborateError> { let storage = self.storage.clone(); Box::pin(async move { let _ = storage.set_revisions(repeated_revision).await?; @@ -161,7 +156,7 @@ impl TextBlockCloudPersistence for LocalTextBlockCloudPersistence { }) } - fn reset_text_block(&self, doc_id: &str, revisions: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError> { + fn reset_text_block(&self, doc_id: &str, revisions: RepeatedRevision) -> BoxResultFuture<(), CollaborateError> { let storage = self.storage.clone(); let doc_id = doc_id.to_owned(); Box::pin(async move { @@ -174,7 +169,7 @@ impl TextBlockCloudPersistence for LocalTextBlockCloudPersistence { #[derive(Default)] struct MemoryDocumentCloudStorage {} impl RevisionCloudStorage for MemoryDocumentCloudStorage { - fn set_revisions(&self, _repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError> { + fn set_revisions(&self, _repeated_revision: RepeatedRevision) -> BoxResultFuture<(), CollaborateError> { Box::pin(async move { Ok(()) }) } @@ -182,9 +177,9 @@ impl RevisionCloudStorage for MemoryDocumentCloudStorage { &self, _doc_id: &str, _rev_ids: Option>, - ) -> BoxResultFuture { + ) -> BoxResultFuture { Box::pin(async move { - let repeated_revisions = RepeatedRevisionPB::new(); + let repeated_revisions = RepeatedRevision::default(); Ok(repeated_revisions) }) } @@ -192,7 +187,7 @@ impl RevisionCloudStorage for MemoryDocumentCloudStorage { fn reset_object( &self, _doc_id: &str, - _repeated_revision: RepeatedRevisionPB, + _repeated_revision: RepeatedRevision, ) -> BoxResultFuture<(), CollaborateError> { Box::pin(async move { Ok(()) }) } diff --git a/frontend/rust-lib/flowy-net/src/local_server/server.rs b/frontend/rust-lib/flowy-net/src/local_server/server.rs index cdec4d0c48..338697f432 100644 --- a/frontend/rust-lib/flowy-net/src/local_server/server.rs +++ b/frontend/rust-lib/flowy-net/src/local_server/server.rs @@ -6,7 +6,7 @@ use flowy_folder::event_map::FolderCouldServiceV1; use flowy_sync::{ client_document::default::initial_quill_delta_string, entities::{ - text_block::{CreateTextBlockParams, ResetTextBlockParams, TextBlockId, TextBlockInfo}, + text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB}, ws_data::{ClientRevisionWSData, ClientRevisionWSDataType}, }, errors::CollaborateError, @@ -253,17 +253,17 @@ impl RevisionUser for LocalRevisionUser { } use flowy_folder::entities::{ - app::{AppId, CreateAppParams, UpdateAppParams}, - trash::RepeatedTrashId, - view::{CreateViewParams, RepeatedViewId, UpdateViewParams, ViewId}, - workspace::{CreateWorkspaceParams, UpdateWorkspaceParams, WorkspaceId}, + app::{AppIdPB, CreateAppParams, UpdateAppParams}, + trash::RepeatedTrashIdPB, + view::{CreateViewParams, RepeatedViewIdPB, UpdateViewParams, ViewIdPB}, + workspace::{CreateWorkspaceParams, UpdateWorkspaceParams, WorkspaceIdPB}, }; use flowy_folder_data_model::revision::{ gen_app_id, gen_workspace_id, AppRevision, TrashRevision, ViewRevision, WorkspaceRevision, }; use flowy_text_block::BlockCloudService; use flowy_user::entities::{ - SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, + SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB, }; use flowy_user::event_map::UserCloudService; use lib_infra::{future::FutureResult, util::timestamp}; @@ -289,7 +289,7 @@ impl FolderCouldServiceV1 for LocalServer { FutureResult::new(async { Ok(workspace) }) } - fn read_workspace(&self, _token: &str, _params: WorkspaceId) -> FutureResult, FlowyError> { + fn read_workspace(&self, _token: &str, _params: WorkspaceIdPB) -> FutureResult, FlowyError> { FutureResult::new(async { Ok(vec![]) }) } @@ -297,7 +297,7 @@ impl FolderCouldServiceV1 for LocalServer { FutureResult::new(async { Ok(()) }) } - fn delete_workspace(&self, _token: &str, _params: WorkspaceId) -> FutureResult<(), FlowyError> { + fn delete_workspace(&self, _token: &str, _params: WorkspaceIdPB) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } @@ -320,11 +320,11 @@ impl FolderCouldServiceV1 for LocalServer { FutureResult::new(async { Ok(view) }) } - fn read_view(&self, _token: &str, _params: ViewId) -> FutureResult, FlowyError> { + fn read_view(&self, _token: &str, _params: ViewIdPB) -> FutureResult, FlowyError> { FutureResult::new(async { Ok(None) }) } - fn delete_view(&self, _token: &str, _params: RepeatedViewId) -> FutureResult<(), FlowyError> { + fn delete_view(&self, _token: &str, _params: RepeatedViewIdPB) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } @@ -347,7 +347,7 @@ impl FolderCouldServiceV1 for LocalServer { FutureResult::new(async { Ok(app) }) } - fn read_app(&self, _token: &str, _params: AppId) -> FutureResult, FlowyError> { + fn read_app(&self, _token: &str, _params: AppIdPB) -> FutureResult, FlowyError> { FutureResult::new(async { Ok(None) }) } @@ -355,15 +355,15 @@ impl FolderCouldServiceV1 for LocalServer { FutureResult::new(async { Ok(()) }) } - fn delete_app(&self, _token: &str, _params: AppId) -> FutureResult<(), FlowyError> { + fn delete_app(&self, _token: &str, _params: AppIdPB) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } - fn create_trash(&self, _token: &str, _params: RepeatedTrashId) -> FutureResult<(), FlowyError> { + fn create_trash(&self, _token: &str, _params: RepeatedTrashIdPB) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } - fn delete_trash(&self, _token: &str, _params: RepeatedTrashId) -> FutureResult<(), FlowyError> { + fn delete_trash(&self, _token: &str, _params: RepeatedTrashIdPB) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } @@ -405,8 +405,8 @@ impl UserCloudService for LocalServer { FutureResult::new(async { Ok(()) }) } - fn get_user(&self, _token: &str) -> FutureResult { - FutureResult::new(async { Ok(UserProfile::default()) }) + fn get_user(&self, _token: &str) -> FutureResult { + FutureResult::new(async { Ok(UserProfilePB::default()) }) } fn ws_addr(&self) -> String { @@ -419,8 +419,8 @@ impl BlockCloudService for LocalServer { FutureResult::new(async { Ok(()) }) } - fn read_block(&self, _token: &str, params: TextBlockId) -> FutureResult, FlowyError> { - let doc = TextBlockInfo { + fn read_block(&self, _token: &str, params: TextBlockIdPB) -> FutureResult, FlowyError> { + let doc = DocumentPB { block_id: params.value, text: initial_quill_delta_string(), rev_id: 0, diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_meta_rev_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_meta_rev_impl.rs similarity index 97% rename from frontend/rust-lib/flowy-revision/src/cache/disk/grid_meta_rev_impl.rs rename to frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_meta_rev_impl.rs index e850119c30..52b01440e6 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_meta_rev_impl.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_block_meta_rev_impl.rs @@ -1,6 +1,5 @@ use crate::cache::disk::RevisionDiskCache; use crate::disk::{RevisionChangeset, RevisionRecord, RevisionState}; - use bytes::Bytes; use diesel::{sql_types::Integer, update, SqliteConnection}; use flowy_database::{ @@ -16,12 +15,12 @@ use flowy_sync::{ }; use std::sync::Arc; -pub struct SQLiteGridBlockMetaRevisionPersistence { +pub struct SQLiteGridBlockRevisionPersistence { user_id: String, pub(crate) pool: Arc, } -impl RevisionDiskCache for SQLiteGridBlockMetaRevisionPersistence { +impl RevisionDiskCache for SQLiteGridBlockRevisionPersistence { type Error = FlowyError; fn create_revision_records(&self, revision_records: Vec) -> Result<(), Self::Error> { @@ -82,7 +81,7 @@ impl RevisionDiskCache for SQLiteGridBlockMetaRevisionPersistence { } } -impl SQLiteGridBlockMetaRevisionPersistence { +impl SQLiteGridBlockRevisionPersistence { pub fn new(user_id: &str, pool: Arc) -> Self { Self { user_id: user_id.to_owned(), diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_rev_impl.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_rev_impl.rs index d51ea8e48c..9ddb21bc8c 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/grid_rev_impl.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/grid_rev_impl.rs @@ -95,7 +95,6 @@ struct GridRevisionSql(); impl GridRevisionSql { fn create(revision_records: Vec, conn: &SqliteConnection) -> Result<(), FlowyError> { // Batch insert: https://diesel.rs/guides/all-about-inserts.html - let records = revision_records .into_iter() .map(|record| { diff --git a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs index 945c6d707f..991d8f9b9f 100644 --- a/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs +++ b/frontend/rust-lib/flowy-revision/src/cache/disk/mod.rs @@ -1,10 +1,10 @@ mod folder_rev_impl; -mod grid_meta_rev_impl; +mod grid_block_meta_rev_impl; mod grid_rev_impl; mod text_rev_impl; pub use folder_rev_impl::*; -pub use grid_meta_rev_impl::*; +pub use grid_block_meta_rev_impl::*; pub use grid_rev_impl::*; pub use text_rev_impl::*; @@ -53,6 +53,14 @@ pub struct RevisionRecord { } impl RevisionRecord { + pub fn new(revision: Revision) -> Self { + Self { + revision, + state: RevisionState::Sync, + write_to_disk: true, + } + } + pub fn ack(&mut self) { self.state = RevisionState::Ack; } @@ -64,6 +72,8 @@ pub struct RevisionChangeset { pub(crate) state: RevisionState, } +/// Sync: revision is not synced to the server +/// Ack: revision is synced to the server #[derive(Debug, Clone, Eq, PartialEq)] pub enum RevisionState { Sync = 0, diff --git a/frontend/rust-lib/flowy-revision/src/conflict_resolve.rs b/frontend/rust-lib/flowy-revision/src/conflict_resolve.rs index 0a63f37f3e..8b36d554cd 100644 --- a/frontend/rust-lib/flowy-revision/src/conflict_resolve.rs +++ b/frontend/rust-lib/flowy-revision/src/conflict_resolve.rs @@ -9,7 +9,7 @@ use flowy_sync::{ util::make_delta_from_revisions, }; use lib_infra::future::BoxResultFuture; -use lib_ot::core::{Attributes, Delta, PlainTextAttributes}; +use lib_ot::core::{Attributes, Delta, PhantomAttributes}; use lib_ot::rich_text::RichTextAttributes; use serde::de::DeserializeOwned; use std::{convert::TryFrom, sync::Arc}; @@ -31,7 +31,7 @@ pub trait ConflictRevisionSink: Send + Sync + 'static { } pub type RichTextConflictController = ConflictController; -pub type PlainTextConflictController = ConflictController; +pub type PlainTextConflictController = ConflictController; pub struct ConflictController where @@ -154,7 +154,7 @@ where &rev_manager.object_id, base_rev_id, rev_id, - client_delta.to_delta_bytes(), + client_delta.json_bytes(), user_id, md5.clone(), ); @@ -166,7 +166,7 @@ where &rev_manager.object_id, base_rev_id, rev_id, - server_delta.to_delta_bytes(), + server_delta.json_bytes(), user_id, md5, ); diff --git a/frontend/rust-lib/flowy-revision/src/history/mod.rs b/frontend/rust-lib/flowy-revision/src/history/mod.rs new file mode 100644 index 0000000000..9de42831ed --- /dev/null +++ b/frontend/rust-lib/flowy-revision/src/history/mod.rs @@ -0,0 +1,5 @@ +mod persistence; +mod rev_history; + +pub use persistence::*; +pub use rev_history::*; diff --git a/frontend/rust-lib/flowy-revision/src/history/persistence.rs b/frontend/rust-lib/flowy-revision/src/history/persistence.rs new file mode 100644 index 0000000000..9c1bdacc9e --- /dev/null +++ b/frontend/rust-lib/flowy-revision/src/history/persistence.rs @@ -0,0 +1,78 @@ +use crate::history::RevisionHistoryDiskCache; +use flowy_database::{ + prelude::*, + schema::{rev_history, rev_history::dsl}, + ConnectionPool, +}; +use flowy_error::{internal_error, FlowyResult}; +use flowy_sync::entities::revision::Revision; +use std::sync::Arc; + +pub struct SQLiteRevisionHistoryPersistence { + object_id: String, + pool: Arc, +} + +impl SQLiteRevisionHistoryPersistence { + pub fn new(object_id: &str, pool: Arc) -> Self { + let object_id = object_id.to_owned(); + Self { object_id, pool } + } +} + +impl RevisionHistoryDiskCache for SQLiteRevisionHistoryPersistence { + fn write_history(&self, revision: Revision) -> FlowyResult<()> { + let record = ( + dsl::object_id.eq(revision.object_id), + dsl::start_rev_id.eq(revision.base_rev_id), + dsl::end_rev_id.eq(revision.rev_id), + dsl::data.eq(revision.delta_data), + ); + let conn = self.pool.get().map_err(internal_error)?; + + let _ = insert_or_ignore_into(dsl::rev_history) + .values(vec![record]) + .execute(&*conn)?; + Ok(()) + } + + fn read_histories(&self) -> FlowyResult> { + let conn = self.pool.get().map_err(internal_error)?; + let records: Vec = dsl::rev_history + .filter(dsl::object_id.eq(&self.object_id)) + .load::(&*conn)?; + + Ok(records + .into_iter() + .map(|record| record.into()) + .collect::>()) + } +} + +#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)] +#[table_name = "rev_history"] +struct RevisionRecord { + id: i32, + object_id: String, + start_rev_id: i64, + end_rev_id: i64, + data: Vec, +} + +pub struct RevisionHistory { + pub object_id: String, + pub start_rev_id: i64, + pub end_rev_id: i64, + pub data: Vec, +} + +impl std::convert::From for RevisionHistory { + fn from(record: RevisionRecord) -> Self { + RevisionHistory { + object_id: record.object_id, + start_rev_id: record.start_rev_id, + end_rev_id: record.end_rev_id, + data: record.data, + } + } +} diff --git a/frontend/rust-lib/flowy-revision/src/history/rev_history.rs b/frontend/rust-lib/flowy-revision/src/history/rev_history.rs new file mode 100644 index 0000000000..71bdc0c333 --- /dev/null +++ b/frontend/rust-lib/flowy-revision/src/history/rev_history.rs @@ -0,0 +1,201 @@ +use crate::{RevisionCompactor, RevisionHistory}; +use async_stream::stream; + +use flowy_error::{FlowyError, FlowyResult}; +use flowy_sync::entities::revision::Revision; +use futures_util::future::BoxFuture; +use futures_util::stream::StreamExt; +use futures_util::FutureExt; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{mpsc, RwLock}; +use tokio::time::interval; + +pub trait RevisionHistoryDiskCache: Send + Sync { + fn write_history(&self, revision: Revision) -> FlowyResult<()>; + + fn read_histories(&self) -> FlowyResult>; +} + +pub struct RevisionHistoryManager { + user_id: String, + stop_tx: mpsc::Sender<()>, + config: RevisionHistoryConfig, + revisions: Arc>>, + disk_cache: Arc, +} + +impl RevisionHistoryManager { + pub fn new( + user_id: &str, + object_id: &str, + config: RevisionHistoryConfig, + disk_cache: Arc, + rev_compactor: Arc, + ) -> Self { + let revisions = Arc::new(RwLock::new(vec![])); + let stop_tx = + spawn_history_checkpoint_runner(user_id, object_id, &disk_cache, &revisions, rev_compactor, &config); + let user_id = user_id.to_owned(); + Self { + user_id, + stop_tx, + config, + revisions, + disk_cache, + } + } + + pub async fn add_revision(&self, revision: &Revision) { + self.revisions.write().await.push(revision.clone()); + } + + pub async fn read_revision_histories(&self) -> FlowyResult> { + self.disk_cache.read_histories() + } +} + +pub struct RevisionHistoryConfig { + check_duration: Duration, +} + +impl std::default::Default for RevisionHistoryConfig { + fn default() -> Self { + Self { + check_duration: Duration::from_secs(5), + } + } +} + +fn spawn_history_checkpoint_runner( + user_id: &str, + object_id: &str, + disk_cache: &Arc, + revisions: &Arc>>, + rev_compactor: Arc, + config: &RevisionHistoryConfig, +) -> mpsc::Sender<()> { + let user_id = user_id.to_string(); + let object_id = object_id.to_string(); + let disk_cache = disk_cache.clone(); + let revisions = revisions.clone(); + + let (checkpoint_tx, checkpoint_rx) = mpsc::channel(1); + let (stop_tx, stop_rx) = mpsc::channel(1); + let checkpoint_sender = FixedDurationCheckpointSender { + user_id, + object_id, + checkpoint_tx, + disk_cache, + revisions, + rev_compactor, + duration: config.check_duration, + }; + tokio::spawn(HistoryCheckpointRunner::new(stop_rx, checkpoint_rx).run()); + tokio::spawn(checkpoint_sender.run()); + stop_tx +} + +struct HistoryCheckpointRunner { + stop_rx: Option>, + checkpoint_rx: Option>, +} + +impl HistoryCheckpointRunner { + fn new(stop_rx: mpsc::Receiver<()>, checkpoint_rx: mpsc::Receiver) -> Self { + Self { + stop_rx: Some(stop_rx), + checkpoint_rx: Some(checkpoint_rx), + } + } + + async fn run(mut self) { + let mut stop_rx = self.stop_rx.take().expect("It should only run once"); + let mut checkpoint_rx = self.checkpoint_rx.take().expect("It should only run once"); + let stream = stream! { + loop { + tokio::select! { + result = checkpoint_rx.recv() => { + match result { + Some(checkpoint) => yield checkpoint, + None => {}, + } + }, + _ = stop_rx.recv() => { + tracing::trace!("Checkpoint runner exit"); + break + }, + }; + } + }; + + stream + .for_each(|checkpoint| async move { + checkpoint.write().await; + }) + .await; + } +} + +struct HistoryCheckpoint { + user_id: String, + object_id: String, + revisions: Vec, + disk_cache: Arc, + rev_compactor: Arc, +} + +impl HistoryCheckpoint { + async fn write(self) { + if self.revisions.is_empty() { + return; + } + + let result = || { + let revision = self + .rev_compactor + .compact(&self.user_id, &self.object_id, self.revisions)?; + let _ = self.disk_cache.write_history(revision)?; + Ok::<(), FlowyError>(()) + }; + + match result() { + Ok(_) => {} + Err(e) => tracing::error!("Write history checkout failed: {:?}", e), + } + } +} + +struct FixedDurationCheckpointSender { + user_id: String, + object_id: String, + checkpoint_tx: mpsc::Sender, + disk_cache: Arc, + revisions: Arc>>, + rev_compactor: Arc, + duration: Duration, +} + +impl FixedDurationCheckpointSender { + fn run(self) -> BoxFuture<'static, ()> { + async move { + let mut interval = interval(self.duration); + let checkpoint_revisions: Vec = self.revisions.write().await.drain(..).collect(); + let checkpoint = HistoryCheckpoint { + user_id: self.user_id.clone(), + object_id: self.object_id.clone(), + revisions: checkpoint_revisions, + disk_cache: self.disk_cache.clone(), + rev_compactor: self.rev_compactor.clone(), + }; + match self.checkpoint_tx.send(checkpoint).await { + Ok(_) => { + interval.tick().await; + self.run(); + } + Err(_) => {} + } + } + .boxed() + } +} diff --git a/frontend/rust-lib/flowy-revision/src/lib.rs b/frontend/rust-lib/flowy-revision/src/lib.rs index 05e60c00e0..b7fd8a12e6 100644 --- a/frontend/rust-lib/flowy-revision/src/lib.rs +++ b/frontend/rust-lib/flowy-revision/src/lib.rs @@ -1,13 +1,17 @@ mod cache; mod conflict_resolve; +// mod history; mod rev_manager; mod rev_persistence; +mod snapshot; mod ws_manager; pub use cache::*; pub use conflict_resolve::*; +// pub use history::*; pub use rev_manager::*; pub use rev_persistence::*; +pub use snapshot::*; pub use ws_manager::*; #[macro_use] diff --git a/frontend/rust-lib/flowy-revision/src/rev_manager.rs b/frontend/rust-lib/flowy-revision/src/rev_manager.rs index 68d8db6f27..bd4d01740d 100644 --- a/frontend/rust-lib/flowy-revision/src/rev_manager.rs +++ b/frontend/rust-lib/flowy-revision/src/rev_manager.rs @@ -1,5 +1,5 @@ use crate::disk::RevisionState; -use crate::{RevisionPersistence, WSDataProviderDataSource}; +use crate::{RevisionPersistence, RevisionSnapshotDiskCache, RevisionSnapshotManager, WSDataProviderDataSource}; use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; use flowy_sync::{ @@ -45,14 +45,31 @@ pub struct RevisionManager { user_id: String, rev_id_counter: RevIdCounter, rev_persistence: Arc, - + #[allow(dead_code)] + rev_snapshot: Arc, + rev_compactor: Arc, #[cfg(feature = "flowy_unit_test")] rev_ack_notifier: tokio::sync::broadcast::Sender, } impl RevisionManager { - pub fn new(user_id: &str, object_id: &str, rev_persistence: Arc) -> Self { + pub fn new( + user_id: &str, + object_id: &str, + rev_persistence: RevisionPersistence, + rev_compactor: C, + snapshot_persistence: SP, + ) -> Self + where + SP: 'static + RevisionSnapshotDiskCache, + C: 'static + RevisionCompactor, + { let rev_id_counter = RevIdCounter::new(0); + let rev_compactor = Arc::new(rev_compactor); + + let rev_persistence = Arc::new(rev_persistence); + + let rev_snapshot = Arc::new(RevisionSnapshotManager::new(user_id, object_id, snapshot_persistence)); #[cfg(feature = "flowy_unit_test")] let (revision_ack_notifier, _) = tokio::sync::broadcast::channel(1); @@ -61,7 +78,8 @@ impl RevisionManager { user_id: user_id.to_owned(), rev_id_counter, rev_persistence, - + rev_snapshot, + rev_compactor, #[cfg(feature = "flowy_unit_test")] rev_ack_notifier: revision_ack_notifier, } @@ -100,20 +118,21 @@ impl RevisionManager { } let _ = self.rev_persistence.add_ack_revision(revision).await?; + // self.rev_history.add_revision(revision).await; self.rev_id_counter.set(revision.rev_id); Ok(()) } #[tracing::instrument(level = "debug", skip_all, err)] - pub async fn add_local_revision<'a>( - &'a self, - revision: &Revision, - compactor: Box, - ) -> Result<(), FlowyError> { + pub async fn add_local_revision(&self, revision: &Revision) -> Result<(), FlowyError> { if revision.delta_data.is_empty() { return Err(FlowyError::internal().context("Delta data should be empty")); } - let rev_id = self.rev_persistence.add_sync_revision(revision, compactor).await?; + let rev_id = self + .rev_persistence + .add_sync_revision(revision, &self.rev_compactor) + .await?; + // self.rev_history.add_revision(revision).await; self.rev_id_counter.set(rev_id); Ok(()) } diff --git a/frontend/rust-lib/flowy-revision/src/rev_persistence.rs b/frontend/rust-lib/flowy-revision/src/rev_persistence.rs index 6412b26ec6..eb3da339b7 100644 --- a/frontend/rust-lib/flowy-revision/src/rev_persistence.rs +++ b/frontend/rust-lib/flowy-revision/src/rev_persistence.rs @@ -2,7 +2,7 @@ use crate::cache::{ disk::{RevisionChangeset, RevisionDiskCache, SQLiteTextBlockRevisionPersistence}, memory::RevisionMemoryCacheDelegate, }; -use crate::disk::{RevisionRecord, RevisionState}; +use crate::disk::{RevisionRecord, RevisionState, SQLiteGridBlockRevisionPersistence}; use crate::memory::RevisionMemoryCache; use crate::RevisionCompactor; use flowy_database::ConnectionPool; @@ -24,13 +24,13 @@ pub struct RevisionPersistence { } impl RevisionPersistence { - pub fn new( - user_id: &str, - object_id: &str, - disk_cache: Arc>, - ) -> RevisionPersistence { + pub fn new(user_id: &str, object_id: &str, disk_cache: C) -> RevisionPersistence + where + C: 'static + RevisionDiskCache, + { let object_id = object_id.to_owned(); let user_id = user_id.to_owned(); + let disk_cache = Arc::new(disk_cache) as Arc>; let sync_seq = RwLock::new(RevisionSyncSequence::new()); let memory_cache = Arc::new(RevisionMemoryCache::new(&object_id, Arc::new(disk_cache.clone()))); Self { @@ -63,7 +63,7 @@ impl RevisionPersistence { pub(crate) async fn add_sync_revision<'a>( &'a self, revision: &'a Revision, - compactor: Box, + compactor: &Arc, ) -> FlowyResult { let result = self.sync_seq.read().await.compact(); match result { @@ -214,13 +214,20 @@ impl RevisionPersistence { } } -pub fn mk_revision_disk_cache( +pub fn mk_text_block_revision_disk_cache( user_id: &str, pool: Arc, ) -> Arc> { Arc::new(SQLiteTextBlockRevisionPersistence::new(user_id, pool)) } +pub fn mk_grid_block_revision_disk_cache( + user_id: &str, + pool: Arc, +) -> Arc> { + Arc::new(SQLiteGridBlockRevisionPersistence::new(user_id, pool)) +} + impl RevisionMemoryCacheDelegate for Arc> { fn checkpoint_tick(&self, mut records: Vec) -> FlowyResult<()> { records.retain(|record| record.write_to_disk); diff --git a/frontend/rust-lib/flowy-revision/src/snapshot/mod.rs b/frontend/rust-lib/flowy-revision/src/snapshot/mod.rs new file mode 100644 index 0000000000..adc4ee2ccc --- /dev/null +++ b/frontend/rust-lib/flowy-revision/src/snapshot/mod.rs @@ -0,0 +1,5 @@ +mod persistence; +mod rev_snapshot; + +pub use persistence::*; +pub use rev_snapshot::*; diff --git a/frontend/rust-lib/flowy-revision/src/snapshot/persistence.rs b/frontend/rust-lib/flowy-revision/src/snapshot/persistence.rs new file mode 100644 index 0000000000..d8d7bae3a6 --- /dev/null +++ b/frontend/rust-lib/flowy-revision/src/snapshot/persistence.rs @@ -0,0 +1,31 @@ +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(unused_variables)] +use crate::{RevisionSnapshotDiskCache, RevisionSnapshotInfo}; +use flowy_database::ConnectionPool; +use flowy_error::FlowyResult; +use std::sync::Arc; + +pub struct SQLiteRevisionSnapshotPersistence { + object_id: String, + pool: Arc, +} + +impl SQLiteRevisionSnapshotPersistence { + pub fn new(object_id: &str, pool: Arc) -> Self { + Self { + object_id: object_id.to_string(), + pool, + } + } +} + +impl RevisionSnapshotDiskCache for SQLiteRevisionSnapshotPersistence { + fn write_snapshot(&self, object_id: &str, rev_id: i64, data: Vec) -> FlowyResult<()> { + todo!() + } + + fn read_snapshot(&self, object_id: &str, rev_id: i64) -> FlowyResult { + todo!() + } +} diff --git a/frontend/rust-lib/flowy-revision/src/snapshot/rev_snapshot.rs b/frontend/rust-lib/flowy-revision/src/snapshot/rev_snapshot.rs new file mode 100644 index 0000000000..047d21607e --- /dev/null +++ b/frontend/rust-lib/flowy-revision/src/snapshot/rev_snapshot.rs @@ -0,0 +1,32 @@ +#![allow(clippy::all)] +#![allow(dead_code)] +#![allow(unused_variables)] +use flowy_error::FlowyResult; +use std::sync::Arc; + +pub trait RevisionSnapshotDiskCache: Send + Sync { + fn write_snapshot(&self, object_id: &str, rev_id: i64, data: Vec) -> FlowyResult<()>; + fn read_snapshot(&self, object_id: &str, rev_id: i64) -> FlowyResult; +} + +pub struct RevisionSnapshotManager { + user_id: String, + object_id: String, + disk_cache: Arc, +} + +impl RevisionSnapshotManager { + pub fn new(user_id: &str, object_id: &str, disk_cache: D) -> Self + where + D: RevisionSnapshotDiskCache + 'static, + { + let disk_cache = Arc::new(disk_cache); + Self { + user_id: user_id.to_string(), + object_id: object_id.to_string(), + disk_cache, + } + } +} + +pub struct RevisionSnapshotInfo {} diff --git a/frontend/rust-lib/flowy-revision/src/ws_manager.rs b/frontend/rust-lib/flowy-revision/src/ws_manager.rs index 42ad39c617..eb7539c380 100644 --- a/frontend/rust-lib/flowy-revision/src/ws_manager.rs +++ b/frontend/rust-lib/flowy-revision/src/ws_manager.rs @@ -1,6 +1,5 @@ use crate::ConflictRevisionSink; use async_stream::stream; - use bytes::Bytes; use flowy_error::{FlowyError, FlowyResult}; use flowy_sync::entities::{ diff --git a/frontend/rust-lib/flowy-sdk/src/deps_resolve/grid_deps.rs b/frontend/rust-lib/flowy-sdk/src/deps_resolve/grid_deps.rs index 88073a3107..f2b862c53a 100644 --- a/frontend/rust-lib/flowy-sdk/src/deps_resolve/grid_deps.rs +++ b/frontend/rust-lib/flowy-sdk/src/deps_resolve/grid_deps.rs @@ -16,14 +16,23 @@ use std::sync::Arc; pub struct GridDepsResolver(); impl GridDepsResolver { - pub fn resolve(ws_conn: Arc, user_session: Arc) -> Arc { + pub async fn resolve(ws_conn: Arc, user_session: Arc) -> Arc { let user = Arc::new(GridUserImpl(user_session.clone())); let rev_web_socket = Arc::new(GridWebSocket(ws_conn)); - Arc::new(GridManager::new( - user, + let grid_manager = Arc::new(GridManager::new( + user.clone(), rev_web_socket, Arc::new(GridDatabaseImpl(user_session)), - )) + )); + + if let (Ok(user_id), Ok(token)) = (user.user_id(), user.token()) { + match grid_manager.initialize(&user_id, &token).await { + Ok(_) => {} + Err(e) => tracing::error!("Initialize grid manager failed: {}", e), + } + } + + grid_manager } } diff --git a/frontend/rust-lib/flowy-sdk/src/lib.rs b/frontend/rust-lib/flowy-sdk/src/lib.rs index 4a37faefef..00a3785122 100644 --- a/frontend/rust-lib/flowy-sdk/src/lib.rs +++ b/frontend/rust-lib/flowy-sdk/src/lib.rs @@ -112,7 +112,7 @@ impl FlowySDK { &config.server_config, ); - let grid_manager = GridDepsResolver::resolve(ws_conn.clone(), user_session.clone()); + let grid_manager = GridDepsResolver::resolve(ws_conn.clone(), user_session.clone()).await; let folder_manager = FolderDepsResolver::resolve( local_server.clone(), @@ -147,7 +147,7 @@ impl FlowySDK { ) })); - _start_listening(&dispatcher, &ws_conn, &user_session, &folder_manager); + _start_listening(&dispatcher, &ws_conn, &user_session, &folder_manager, &grid_manager); Self { config, @@ -171,10 +171,12 @@ fn _start_listening( ws_conn: &Arc, user_session: &Arc, folder_manager: &Arc, + grid_manager: &Arc, ) { let subscribe_user_status = user_session.notifier.subscribe_user_status(); let subscribe_network_type = ws_conn.subscribe_network_ty(); let folder_manager = folder_manager.clone(); + let grid_manager = grid_manager.clone(); let cloned_folder_manager = folder_manager.clone(); let ws_conn = ws_conn.clone(); let user_session = user_session.clone(); @@ -182,7 +184,13 @@ fn _start_listening( dispatch.spawn(async move { user_session.init(); listen_on_websocket(ws_conn.clone()); - _listen_user_status(ws_conn.clone(), subscribe_user_status, folder_manager.clone()).await; + _listen_user_status( + ws_conn.clone(), + subscribe_user_status, + folder_manager.clone(), + grid_manager.clone(), + ) + .await; }); dispatch.spawn(async move { @@ -209,6 +217,7 @@ async fn _listen_user_status( ws_conn: Arc, mut subscribe: broadcast::Receiver, folder_manager: Arc, + grid_manager: Arc, ) { while let Ok(status) = subscribe.recv().await { let result = || async { @@ -216,6 +225,7 @@ async fn _listen_user_status( UserStatus::Login { token, user_id } => { tracing::trace!("User did login"); let _ = folder_manager.initialize(&user_id, &token).await?; + let _ = grid_manager.initialize(&user_id, &token).await?; let _ = ws_conn.start(token, user_id).await?; } UserStatus::Logout { .. } => { @@ -233,6 +243,11 @@ async fn _listen_user_status( let _ = folder_manager .initialize_with_new_user(&profile.id, &profile.token) .await?; + + let _ = grid_manager + .initialize_with_new_user(&profile.id, &profile.token) + .await?; + let _ = ws_conn.start(profile.token.clone(), profile.id.clone()).await?; let _ = ret.send(()); } diff --git a/frontend/rust-lib/flowy-test/src/event_builder.rs b/frontend/rust-lib/flowy-test/src/event_builder.rs index ed6f3c3bc8..d9841d6b4a 100644 --- a/frontend/rust-lib/flowy-test/src/event_builder.rs +++ b/frontend/rust-lib/flowy-test/src/event_builder.rs @@ -1,5 +1,5 @@ use crate::FlowySDKTest; -use flowy_user::{entities::UserProfile, errors::FlowyError}; +use flowy_user::{entities::UserProfilePB, errors::FlowyError}; use lib_dispatch::prelude::{EventDispatcher, EventResponse, FromBytes, ModuleRequest, StatusCode, ToBytes, *}; use std::{ convert::TryFrom, @@ -14,7 +14,7 @@ impl FolderEventBuilder { pub fn new(sdk: FlowySDKTest) -> Self { EventBuilder::test(TestContext::new(sdk)) } - pub fn user_profile(&self) -> &Option { + pub fn user_profile(&self) -> &Option { &self.user_profile } } @@ -24,7 +24,7 @@ pub type UserModuleEventBuilder = FolderEventBuilder; #[derive(Clone)] pub struct EventBuilder { context: TestContext, - user_profile: Option, + user_profile: Option, err_phantom: PhantomData, } diff --git a/frontend/rust-lib/flowy-test/src/helper.rs b/frontend/rust-lib/flowy-test/src/helper.rs index efc4282386..1265bd695c 100644 --- a/frontend/rust-lib/flowy-test/src/helper.rs +++ b/frontend/rust-lib/flowy-test/src/helper.rs @@ -1,15 +1,15 @@ use crate::prelude::*; -use flowy_folder::entities::WorkspaceId; +use flowy_folder::entities::WorkspaceIdPB; use flowy_folder::{ entities::{ app::*, view::*, - workspace::{CreateWorkspacePayload, Workspace}, + workspace::{CreateWorkspacePayloadPB, WorkspacePB}, }, event_map::FolderEvent::{CreateWorkspace, OpenWorkspace, *}, }; use flowy_user::{ - entities::{SignInPayload, SignUpPayload, UserProfile}, + entities::{SignInPayloadPB, SignUpPayloadPB, UserProfilePB}, errors::FlowyError, event_map::UserEvent::{InitUser, SignIn, SignOut, SignUp}, }; @@ -18,9 +18,9 @@ use std::{fs, path::PathBuf, sync::Arc}; pub struct ViewTest { pub sdk: FlowySDKTest, - pub workspace: Workspace, - pub app: App, - pub view: View, + pub workspace: WorkspacePB, + pub app: AppPB, + pub view: ViewPB, } impl ViewTest { @@ -47,8 +47,8 @@ impl ViewTest { } } -async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspace { - let request = CreateWorkspacePayload { +async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> WorkspacePB { + let request = CreateWorkspacePayloadPB { name: name.to_owned(), desc: desc.to_owned(), }; @@ -58,12 +58,12 @@ async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspa .payload(request) .async_send() .await - .parse::(); + .parse::(); workspace } async fn open_workspace(sdk: &FlowySDKTest, workspace_id: &str) { - let payload = WorkspaceId { + let payload = WorkspaceIdPB { value: Some(workspace_id.to_owned()), }; let _ = FolderEventBuilder::new(sdk.clone()) @@ -73,8 +73,8 @@ async fn open_workspace(sdk: &FlowySDKTest, workspace_id: &str) { .await; } -async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &str) -> App { - let create_app_request = CreateAppPayload { +async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &str) -> AppPB { + let create_app_request = CreateAppPayloadPB { workspace_id: workspace_id.to_owned(), name: name.to_string(), desc: desc.to_string(), @@ -86,12 +86,12 @@ async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &s .payload(create_app_request) .async_send() .await - .parse::(); + .parse::(); app } -async fn create_view(sdk: &FlowySDKTest, app_id: &str, data_type: ViewDataType, data: Vec) -> View { - let request = CreateViewPayload { +async fn create_view(sdk: &FlowySDKTest, app_id: &str, data_type: ViewDataType, data: Vec) -> ViewPB { + let request = CreateViewPayloadPB { belong_to_id: app_id.to_string(), name: "View A".to_string(), desc: "".to_string(), @@ -106,7 +106,7 @@ async fn create_view(sdk: &FlowySDKTest, app_id: &str, data_type: ViewDataType, .payload(request) .async_send() .await - .parse::(); + .parse::(); view } @@ -138,13 +138,13 @@ pub fn login_password() -> String { } pub struct SignUpContext { - pub user_profile: UserProfile, + pub user_profile: UserProfilePB, pub password: String, } pub fn sign_up(dispatch: Arc) -> SignUpContext { let password = login_password(); - let payload = SignUpPayload { + let payload = SignUpPayloadPB { email: random_email(), name: "app flowy".to_string(), password: password.clone(), @@ -154,7 +154,7 @@ pub fn sign_up(dispatch: Arc) -> SignUpContext { let request = ModuleRequest::new(SignUp).payload(payload); let user_profile = EventDispatcher::sync_send(dispatch, request) - .parse::() + .parse::() .unwrap() .unwrap(); @@ -164,7 +164,7 @@ pub fn sign_up(dispatch: Arc) -> SignUpContext { pub async fn async_sign_up(dispatch: Arc) -> SignUpContext { let password = login_password(); let email = random_email(); - let payload = SignUpPayload { + let payload = SignUpPayloadPB { email, name: "app flowy".to_string(), password: password.clone(), @@ -175,7 +175,7 @@ pub async fn async_sign_up(dispatch: Arc) -> SignUpContext { let request = ModuleRequest::new(SignUp).payload(payload); let user_profile = EventDispatcher::async_send(dispatch.clone(), request) .await - .parse::() + .parse::() .unwrap() .unwrap(); @@ -189,8 +189,8 @@ pub async fn init_user_setting(dispatch: Arc) { } #[allow(dead_code)] -fn sign_in(dispatch: Arc) -> UserProfile { - let payload = SignInPayload { +fn sign_in(dispatch: Arc) -> UserProfilePB { + let payload = SignInPayloadPB { email: login_email(), password: login_password(), name: "rust".to_owned(), @@ -200,7 +200,7 @@ fn sign_in(dispatch: Arc) -> UserProfile { let request = ModuleRequest::new(SignIn).payload(payload); EventDispatcher::sync_send(dispatch, request) - .parse::() + .parse::() .unwrap() .unwrap() } diff --git a/frontend/rust-lib/flowy-test/src/lib.rs b/frontend/rust-lib/flowy-test/src/lib.rs index 70159edf19..8f6aacb44d 100644 --- a/frontend/rust-lib/flowy-test/src/lib.rs +++ b/frontend/rust-lib/flowy-test/src/lib.rs @@ -4,7 +4,7 @@ pub mod helper; use crate::helper::*; use flowy_net::{get_client_server_configuration, ClientServerConfiguration}; use flowy_sdk::{FlowySDK, FlowySDKConfig}; -use flowy_user::entities::UserProfile; +use flowy_user::entities::UserProfilePB; use nanoid::nanoid; pub mod prelude { @@ -47,7 +47,7 @@ impl FlowySDKTest { context } - pub async fn init_user(&self) -> UserProfile { + pub async fn init_user(&self) -> UserProfilePB { let context = async_sign_up(self.inner.dispatcher()).await; init_user_setting(self.inner.dispatcher()).await; context.user_profile diff --git a/frontend/rust-lib/flowy-text-block/src/editor.rs b/frontend/rust-lib/flowy-text-block/src/editor.rs index 111808109d..2649b2bae1 100644 --- a/frontend/rust-lib/flowy-text-block/src/editor.rs +++ b/frontend/rust-lib/flowy-text-block/src/editor.rs @@ -9,7 +9,7 @@ use flowy_error::{internal_error, FlowyResult}; use flowy_revision::{RevisionCloudService, RevisionManager, RevisionObjectBuilder, RevisionWebSocket}; use flowy_sync::entities::ws_data::ServerRevisionWSData; use flowy_sync::{ - entities::{revision::Revision, text_block::TextBlockInfo}, + entities::{revision::Revision, text_block::DocumentPB}, errors::CollaborateResult, util::make_delta_from_revisions, }; @@ -229,16 +229,16 @@ impl TextBlockEditor { struct TextBlockInfoBuilder(); impl RevisionObjectBuilder for TextBlockInfoBuilder { - type Output = TextBlockInfo; + type Output = DocumentPB; fn build_object(object_id: &str, revisions: Vec) -> FlowyResult { let (base_rev_id, rev_id) = revisions.last().unwrap().pair_rev_id(); let mut delta = make_delta_from_revisions(revisions)?; correct_delta(&mut delta); - Result::::Ok(TextBlockInfo { + Result::::Ok(DocumentPB { block_id: object_id.to_owned(), - text: delta.to_delta_str(), + text: delta.json_str(), rev_id, base_rev_id, }) diff --git a/frontend/rust-lib/flowy-text-block/src/entities.rs b/frontend/rust-lib/flowy-text-block/src/entities.rs index 61dade5113..ec8767285b 100644 --- a/frontend/rust-lib/flowy-text-block/src/entities.rs +++ b/frontend/rust-lib/flowy-text-block/src/entities.rs @@ -30,7 +30,7 @@ impl std::convert::From for ExportType { } #[derive(Default, ProtoBuf)] -pub struct ExportPayload { +pub struct ExportPayloadPB { #[pb(index = 1)] pub view_id: String, @@ -44,7 +44,7 @@ pub struct ExportParams { pub export_type: ExportType, } -impl TryInto for ExportPayload { +impl TryInto for ExportPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { Ok(ExportParams { @@ -55,7 +55,7 @@ impl TryInto for ExportPayload { } #[derive(Default, ProtoBuf)] -pub struct ExportData { +pub struct ExportDataPB { #[pb(index = 1)] pub data: String, diff --git a/frontend/rust-lib/flowy-text-block/src/event_handler.rs b/frontend/rust-lib/flowy-text-block/src/event_handler.rs index 0934d20a29..dc9812d862 100644 --- a/frontend/rust-lib/flowy-text-block/src/event_handler.rs +++ b/frontend/rust-lib/flowy-text-block/src/event_handler.rs @@ -1,41 +1,41 @@ -use crate::entities::{ExportData, ExportParams, ExportPayload}; +use crate::entities::{ExportDataPB, ExportParams, ExportPayloadPB}; use crate::TextBlockManager; use flowy_error::FlowyError; -use flowy_sync::entities::text_block::{TextBlockDelta, TextBlockId}; +use flowy_sync::entities::text_block::{TextBlockDeltaPB, TextBlockIdPB}; use lib_dispatch::prelude::{data_result, AppData, Data, DataResult}; use std::convert::TryInto; use std::sync::Arc; pub(crate) async fn get_block_data_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { - let block_id: TextBlockId = data.into_inner(); +) -> DataResult { + let block_id: TextBlockIdPB = data.into_inner(); let editor = manager.open_block(&block_id).await?; let delta_str = editor.delta_str().await?; - data_result(TextBlockDelta { + data_result(TextBlockDeltaPB { block_id: block_id.into(), delta_str, }) } pub(crate) async fn apply_delta_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let block_delta = manager.receive_local_delta(data.into_inner()).await?; data_result(block_delta) } #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn export_handler( - data: Data, + data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let params: ExportParams = data.into_inner().try_into()?; let editor = manager.open_block(¶ms.view_id).await?; let delta_json = editor.delta_str().await?; - data_result(ExportData { + data_result(ExportDataPB { data: delta_json, export_type: params.export_type, }) diff --git a/frontend/rust-lib/flowy-text-block/src/event_map.rs b/frontend/rust-lib/flowy-text-block/src/event_map.rs index f995fd282b..cfc06bf32c 100644 --- a/frontend/rust-lib/flowy-text-block/src/event_map.rs +++ b/frontend/rust-lib/flowy-text-block/src/event_map.rs @@ -19,12 +19,12 @@ pub fn create(block_manager: Arc) -> Module { #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[event_err = "FlowyError"] pub enum TextBlockEvent { - #[event(input = "TextBlockId", output = "TextBlockDelta")] + #[event(input = "TextBlockIdPB", output = "TextBlockDeltaPB")] GetBlockData = 0, - #[event(input = "TextBlockDelta", output = "TextBlockDelta")] + #[event(input = "TextBlockDeltaPB", output = "TextBlockDeltaPB")] ApplyDelta = 1, - #[event(input = "ExportPayload", output = "ExportData")] + #[event(input = "ExportPayloadPB", output = "ExportDataPB")] ExportDocument = 2, } diff --git a/frontend/rust-lib/flowy-text-block/src/lib.rs b/frontend/rust-lib/flowy-text-block/src/lib.rs index f470739d85..37ddf6ea1e 100644 --- a/frontend/rust-lib/flowy-text-block/src/lib.rs +++ b/frontend/rust-lib/flowy-text-block/src/lib.rs @@ -15,13 +15,13 @@ pub mod errors { pub const TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS: u64 = 1000; use crate::errors::FlowyError; -use flowy_sync::entities::text_block::{CreateTextBlockParams, ResetTextBlockParams, TextBlockId, TextBlockInfo}; +use flowy_sync::entities::text_block::{CreateTextBlockParams, DocumentPB, ResetTextBlockParams, TextBlockIdPB}; use lib_infra::future::FutureResult; pub trait BlockCloudService: Send + Sync { fn create_block(&self, token: &str, params: CreateTextBlockParams) -> FutureResult<(), FlowyError>; - fn read_block(&self, token: &str, params: TextBlockId) -> FutureResult, FlowyError>; + fn read_block(&self, token: &str, params: TextBlockIdPB) -> FutureResult, FlowyError>; fn update_block(&self, token: &str, params: ResetTextBlockParams) -> FutureResult<(), FlowyError>; } diff --git a/frontend/rust-lib/flowy-text-block/src/manager.rs b/frontend/rust-lib/flowy-text-block/src/manager.rs index ead24857be..57e875668e 100644 --- a/frontend/rust-lib/flowy-text-block/src/manager.rs +++ b/frontend/rust-lib/flowy-text-block/src/manager.rs @@ -1,13 +1,16 @@ +use crate::queue::TextBlockRevisionCompactor; use crate::{editor::TextBlockEditor, errors::FlowyError, BlockCloudService}; use bytes::Bytes; use dashmap::DashMap; use flowy_database::ConnectionPool; use flowy_error::FlowyResult; use flowy_revision::disk::SQLiteTextBlockRevisionPersistence; -use flowy_revision::{RevisionCloudService, RevisionManager, RevisionPersistence, RevisionWebSocket}; +use flowy_revision::{ + RevisionCloudService, RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence, +}; use flowy_sync::entities::{ revision::{md5, RepeatedRevision, Revision}, - text_block::{TextBlockDelta, TextBlockId}, + text_block::{TextBlockDeltaPB, TextBlockIdPB}, ws_data::ServerRevisionWSData, }; use lib_infra::future::FutureResult; @@ -71,11 +74,11 @@ impl TextBlockManager { } #[tracing::instrument(level = "debug", skip(self, delta), fields(doc_id = %delta.block_id), err)] - pub async fn receive_local_delta(&self, delta: TextBlockDelta) -> Result { + pub async fn receive_local_delta(&self, delta: TextBlockDeltaPB) -> Result { let editor = self.get_block_editor(&delta.block_id).await?; let _ = editor.compose_local_delta(Bytes::from(delta.delta_str)).await?; let document_json = editor.delta_str().await?; - Ok(TextBlockDelta { + Ok(TextBlockDeltaPB { block_id: delta.block_id.clone(), delta_str: document_json, }) @@ -139,9 +142,20 @@ impl TextBlockManager { fn make_rev_manager(&self, doc_id: &str, pool: Arc) -> Result { let user_id = self.user.user_id()?; - let disk_cache = Arc::new(SQLiteTextBlockRevisionPersistence::new(&user_id, pool)); - let rev_persistence = Arc::new(RevisionPersistence::new(&user_id, doc_id, disk_cache)); - Ok(RevisionManager::new(&user_id, doc_id, rev_persistence)) + let disk_cache = SQLiteTextBlockRevisionPersistence::new(&user_id, pool.clone()); + let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache); + // let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone()); + let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool); + let rev_compactor = TextBlockRevisionCompactor(); + + Ok(RevisionManager::new( + &user_id, + doc_id, + rev_persistence, + rev_compactor, + // history_persistence, + snapshot_persistence, + )) } } @@ -153,7 +167,7 @@ struct TextBlockRevisionCloudService { impl RevisionCloudService for TextBlockRevisionCloudService { #[tracing::instrument(level = "trace", skip(self))] fn fetch_object(&self, user_id: &str, object_id: &str) -> FutureResult, FlowyError> { - let params: TextBlockId = object_id.to_string().into(); + let params: TextBlockIdPB = object_id.to_string().into(); let server = self.server.clone(); let token = self.token.clone(); let user_id = user_id.to_string(); diff --git a/frontend/rust-lib/flowy-text-block/src/queue.rs b/frontend/rust-lib/flowy-text-block/src/queue.rs index 7c11afd0ff..e48c39fb97 100644 --- a/frontend/rust-lib/flowy-text-block/src/queue.rs +++ b/frontend/rust-lib/flowy-text-block/src/queue.rs @@ -12,7 +12,7 @@ use flowy_sync::{ }; use futures::stream::StreamExt; use lib_ot::{ - core::{Interval, OperationTransformable}, + core::{Interval, OperationTransform}, rich_text::{RichTextAttribute, RichTextAttributes, RichTextDelta}, }; use std::sync::Arc; @@ -175,7 +175,7 @@ impl EditBlockQueue { } async fn save_local_delta(&self, delta: RichTextDelta, md5: String) -> Result { - let delta_data = delta.to_delta_bytes(); + let delta_data = delta.json_bytes(); let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair(); let user_id = self.user.user_id()?; let revision = Revision::new( @@ -186,10 +186,7 @@ impl EditBlockQueue { &user_id, md5, ); - let _ = self - .rev_manager - .add_local_revision(&revision, Box::new(TextBlockRevisionCompactor())) - .await?; + let _ = self.rev_manager.add_local_revision(&revision).await?; Ok(rev_id.into()) } } @@ -198,7 +195,7 @@ pub(crate) struct TextBlockRevisionCompactor(); impl RevisionCompactor for TextBlockRevisionCompactor { fn bytes_from_revisions(&self, revisions: Vec) -> FlowyResult { let delta = make_delta_from_revisions::(revisions)?; - Ok(delta.to_delta_bytes()) + Ok(delta.json_bytes()) } } diff --git a/frontend/rust-lib/flowy-text-block/tests/document/script.rs b/frontend/rust-lib/flowy-text-block/tests/document/script.rs index 1722724c3f..76c34a78e0 100644 --- a/frontend/rust-lib/flowy-text-block/tests/document/script.rs +++ b/frontend/rust-lib/flowy-text-block/tests/document/script.rs @@ -75,7 +75,7 @@ impl TextBlockEditorTest { let delta = self.editor.text_block_delta().await.unwrap(); if expected_delta != delta { eprintln!("✅ expect: {}", expected,); - eprintln!("❌ receive: {}", delta.to_delta_str()); + eprintln!("❌ receive: {}", delta.json_str()); } assert_eq!(expected_delta, delta); } diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/attribute_test.rs b/frontend/rust-lib/flowy-text-block/tests/editor/attribute_test.rs index 037b36970b..0653e88567 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/attribute_test.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/attribute_test.rs @@ -1,7 +1,7 @@ #![cfg_attr(rustfmt, rustfmt::skip)] use crate::editor::{TestBuilder, TestOp::*}; use flowy_sync::client_document::{NewlineDoc, PlainDoc}; -use lib_ot::core::{Interval, OperationTransformable, NEW_LINE, WHITESPACE, FlowyStr}; +use lib_ot::core::{Interval, OperationTransform, NEW_LINE, WHITESPACE, OTString}; use unicode_segmentation::UnicodeSegmentation; use lib_ot::rich_text::RichTextDelta; @@ -723,8 +723,8 @@ fn attributes_preserve_header_format_on_merge() { #[test] fn attributes_format_emoji() { let emoji_s = "👋 "; - let s: FlowyStr = emoji_s.into(); - let len = s.utf16_size(); + let s: OTString = emoji_s.into(); + let len = s.utf16_len(); assert_eq!(3, len); assert_eq!(2, s.graphemes(true).count()); @@ -762,19 +762,19 @@ fn attributes_preserve_list_format_on_merge() { #[test] fn delta_compose() { - let mut delta = RichTextDelta::from_delta_str(r#"[{"insert":"\n"}]"#).unwrap(); + let mut delta = RichTextDelta::from_json(r#"[{"insert":"\n"}]"#).unwrap(); let deltas = vec![ - RichTextDelta::from_delta_str(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(), - RichTextDelta::from_delta_str(r#"[{"insert":"a"}]"#).unwrap(), - RichTextDelta::from_delta_str(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(), - RichTextDelta::from_delta_str(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(), + RichTextDelta::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(), + RichTextDelta::from_json(r#"[{"insert":"a"}]"#).unwrap(), + RichTextDelta::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(), + RichTextDelta::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(), ]; for d in deltas { delta = delta.compose(&d).unwrap(); } assert_eq!( - delta.to_delta_str(), + delta.json_str(), r#"[{"insert":"a"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"\n"}]"# ); diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/mod.rs b/frontend/rust-lib/flowy-text-block/tests/editor/mod.rs index 676b9a1cb0..bbfd1da6f8 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/mod.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/mod.rs @@ -108,20 +108,20 @@ impl TestBuilder { TestOp::Insert(delta_i, s, index) => { let document = &mut self.documents[*delta_i]; let delta = document.insert(*index, s).unwrap(); - tracing::debug!("Insert delta: {}", delta.to_delta_str()); + tracing::debug!("Insert delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Delete(delta_i, iv) => { let document = &mut self.documents[*delta_i]; let delta = document.replace(*iv, "").unwrap(); - tracing::trace!("Delete delta: {}", delta.to_delta_str()); + tracing::trace!("Delete delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Replace(delta_i, iv, s) => { let document = &mut self.documents[*delta_i]; let delta = document.replace(*iv, s).unwrap(); - tracing::trace!("Replace delta: {}", delta.to_delta_str()); + tracing::trace!("Replace delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::InsertBold(delta_i, s, iv) => { @@ -133,7 +133,7 @@ impl TestBuilder { let document = &mut self.documents[*delta_i]; let attribute = RichTextAttribute::Bold(*enable); let delta = document.format(*iv, attribute).unwrap(); - tracing::trace!("Bold delta: {}", delta.to_delta_str()); + tracing::trace!("Bold delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Italic(delta_i, iv, enable) => { @@ -143,28 +143,28 @@ impl TestBuilder { false => RichTextAttribute::Italic(false), }; let delta = document.format(*iv, attribute).unwrap(); - tracing::trace!("Italic delta: {}", delta.to_delta_str()); + tracing::trace!("Italic delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Header(delta_i, iv, level) => { let document = &mut self.documents[*delta_i]; let attribute = RichTextAttribute::Header(*level); let delta = document.format(*iv, attribute).unwrap(); - tracing::trace!("Header delta: {}", delta.to_delta_str()); + tracing::trace!("Header delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Link(delta_i, iv, link) => { let document = &mut self.documents[*delta_i]; let attribute = RichTextAttribute::Link(link.to_owned()); let delta = document.format(*iv, attribute).unwrap(); - tracing::trace!("Link delta: {}", delta.to_delta_str()); + tracing::trace!("Link delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } TestOp::Bullet(delta_i, iv, enable) => { let document = &mut self.documents[*delta_i]; let attribute = RichTextAttribute::Bullet(*enable); let delta = document.format(*iv, attribute).unwrap(); - tracing::debug!("Bullet delta: {}", delta.to_delta_str()); + tracing::debug!("Bullet delta: {}", delta.json_str()); self.deltas.insert(*delta_i, Some(delta)); } @@ -194,15 +194,15 @@ impl TestBuilder { let delta_a = &self.documents[*delta_a_i].delta(); let delta_b = &self.documents[*delta_b_i].delta(); tracing::debug!("Invert: "); - tracing::debug!("a: {}", delta_a.to_delta_str()); - tracing::debug!("b: {}", delta_b.to_delta_str()); + tracing::debug!("a: {}", delta_a.json_str()); + tracing::debug!("b: {}", delta_b.json_str()); let (_, b_prime) = delta_a.transform(delta_b).unwrap(); let undo = b_prime.invert(delta_a); let new_delta = delta_a.compose(&b_prime).unwrap(); - tracing::debug!("new delta: {}", new_delta.to_delta_str()); - tracing::debug!("undo delta: {}", undo.to_delta_str()); + tracing::debug!("new delta: {}", new_delta.json_str()); + tracing::debug!("undo delta: {}", undo.json_str()); let new_delta_after_undo = new_delta.compose(&undo).unwrap(); @@ -238,7 +238,7 @@ impl TestBuilder { } TestOp::AssertPrimeJson(doc_i, expected) => { - let prime_json = self.primes[*doc_i].as_ref().unwrap().to_delta_str(); + let prime_json = self.primes[*doc_i].as_ref().unwrap().json_str(); let expected_prime: RichTextDelta = serde_json::from_str(expected).unwrap(); let target_prime: RichTextDelta = serde_json::from_str(&prime_json).unwrap(); @@ -300,9 +300,9 @@ impl Rng { pub fn gen_delta(&mut self, s: &str) -> RichTextDelta { let mut delta = RichTextDelta::default(); - let s = FlowyStr::from(s); + let s = OTString::from(s); loop { - let left = s.utf16_size() - delta.utf16_base_len; + let left = s.utf16_len() - delta.utf16_base_len; if left == 0 { break; } diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/op_test.rs b/frontend/rust-lib/flowy-text-block/tests/editor/op_test.rs index 3f174fa3bc..99d3136e55 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/op_test.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/op_test.rs @@ -1,7 +1,9 @@ #![allow(clippy::all)] use crate::editor::{Rng, TestBuilder, TestOp::*}; use flowy_sync::client_document::{NewlineDoc, PlainDoc}; +use lib_ot::rich_text::RichTextDeltaBuilder; use lib_ot::{ + core::Interval, core::*, rich_text::{AttributeBuilder, RichTextAttribute, RichTextAttributes, RichTextDelta}, }; @@ -38,24 +40,20 @@ fn attributes_insert_text_at_middle() { #[test] fn delta_get_ops_in_interval_1() { - let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("123").build(); - let insert_b = OpBuilder::insert("4").build(); + let operations = OperationsBuilder::new().insert("123").insert("4").build(); + let delta = RichTextDeltaBuilder::from_operations(operations); - delta.add(insert_a.clone()); - delta.add(insert_b.clone()); - - let mut iterator = DeltaIter::from_interval(&delta, Interval::new(0, 4)); + let mut iterator = DeltaIterator::from_interval(&delta, Interval::new(0, 4)); assert_eq!(iterator.ops(), delta.ops); } #[test] fn delta_get_ops_in_interval_2() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("123").build(); - let insert_b = OpBuilder::insert("4").build(); - let insert_c = OpBuilder::insert("5").build(); - let retain_a = OpBuilder::retain(3).build(); + let insert_a = Operation::insert("123"); + let insert_b = Operation::insert("4"); + let insert_c = Operation::insert("5"); + let retain_a = Operation::retain(3); delta.add(insert_a.clone()); delta.add(retain_a.clone()); @@ -63,32 +61,32 @@ fn delta_get_ops_in_interval_2() { delta.add(insert_c.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 2)).ops(), - vec![OpBuilder::insert("12").build()] + DeltaIterator::from_interval(&delta, Interval::new(0, 2)).ops(), + vec![Operation::insert("12")] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(1, 3)).ops(), - vec![OpBuilder::insert("23").build()] + DeltaIterator::from_interval(&delta, Interval::new(1, 3)).ops(), + vec![Operation::insert("23")] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 3)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(0, 3)).ops(), vec![insert_a.clone()] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 4)).ops(), - vec![insert_a.clone(), OpBuilder::retain(1).build()] + DeltaIterator::from_interval(&delta, Interval::new(0, 4)).ops(), + vec![insert_a.clone(), Operation::retain(1)] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 6)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(0, 6)).ops(), vec![insert_a.clone(), retain_a.clone()] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 7)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(0, 7)).ops(), vec![insert_a.clone(), retain_a.clone(), insert_b.clone()] ); } @@ -96,113 +94,113 @@ fn delta_get_ops_in_interval_2() { #[test] fn delta_get_ops_in_interval_3() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("123456").build(); + let insert_a = Operation::insert("123456"); delta.add(insert_a.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(3, 5)).ops(), - vec![OpBuilder::insert("45").build()] + DeltaIterator::from_interval(&delta, Interval::new(3, 5)).ops(), + vec![Operation::insert("45")] ); } #[test] fn delta_get_ops_in_interval_4() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("12").build(); - let insert_b = OpBuilder::insert("34").build(); - let insert_c = OpBuilder::insert("56").build(); + let insert_a = Operation::insert("12"); + let insert_b = Operation::insert("34"); + let insert_c = Operation::insert("56"); delta.ops.push(insert_a.clone()); delta.ops.push(insert_b.clone()); delta.ops.push(insert_c.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(0, 2)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(0, 2)).ops(), vec![insert_a] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(2, 4)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(2, 4)).ops(), vec![insert_b] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(4, 6)).ops(), + DeltaIterator::from_interval(&delta, Interval::new(4, 6)).ops(), vec![insert_c] ); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(2, 5)).ops(), - vec![OpBuilder::insert("34").build(), OpBuilder::insert("5").build()] + DeltaIterator::from_interval(&delta, Interval::new(2, 5)).ops(), + vec![Operation::insert("34"), Operation::insert("5")] ); } #[test] fn delta_get_ops_in_interval_5() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("123456").build(); - let insert_b = OpBuilder::insert("789").build(); + let insert_a = Operation::insert("123456"); + let insert_b = Operation::insert("789"); delta.ops.push(insert_a.clone()); delta.ops.push(insert_b.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(4, 8)).ops(), - vec![OpBuilder::insert("56").build(), OpBuilder::insert("78").build()] + DeltaIterator::from_interval(&delta, Interval::new(4, 8)).ops(), + vec![Operation::insert("56"), Operation::insert("78")] ); // assert_eq!( // DeltaIter::from_interval(&delta, Interval::new(8, 9)).ops(), - // vec![Builder::insert("9").build()] + // vec![Builder::insert("9")] // ); } #[test] fn delta_get_ops_in_interval_6() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("12345678").build(); + let insert_a = Operation::insert("12345678"); delta.add(insert_a.clone()); assert_eq!( - DeltaIter::from_interval(&delta, Interval::new(4, 6)).ops(), - vec![OpBuilder::insert("56").build()] + DeltaIterator::from_interval(&delta, Interval::new(4, 6)).ops(), + vec![Operation::insert("56")] ); } #[test] fn delta_get_ops_in_interval_7() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("12345").build(); - let retain_a = OpBuilder::retain(3).build(); + let insert_a = Operation::insert("12345"); + let retain_a = Operation::retain(3); delta.add(insert_a.clone()); delta.add(retain_a.clone()); - let mut iter_1 = DeltaIter::from_offset(&delta, 2); - assert_eq!(iter_1.next_op().unwrap(), OpBuilder::insert("345").build()); - assert_eq!(iter_1.next_op().unwrap(), OpBuilder::retain(3).build()); + let mut iter_1 = DeltaIterator::from_offset(&delta, 2); + assert_eq!(iter_1.next_op().unwrap(), Operation::insert("345")); + assert_eq!(iter_1.next_op().unwrap(), Operation::retain(3)); - let mut iter_2 = DeltaIter::new(&delta); - assert_eq!(iter_2.next_op_with_len(2).unwrap(), OpBuilder::insert("12").build()); - assert_eq!(iter_2.next_op().unwrap(), OpBuilder::insert("345").build()); + let mut iter_2 = DeltaIterator::new(&delta); + assert_eq!(iter_2.next_op_with_len(2).unwrap(), Operation::insert("12")); + assert_eq!(iter_2.next_op().unwrap(), Operation::insert("345")); - assert_eq!(iter_2.next_op().unwrap(), OpBuilder::retain(3).build()); + assert_eq!(iter_2.next_op().unwrap(), Operation::retain(3)); } #[test] fn delta_op_seek() { let mut delta = RichTextDelta::default(); - let insert_a = OpBuilder::insert("12345").build(); - let retain_a = OpBuilder::retain(3).build(); + let insert_a = Operation::insert("12345"); + let retain_a = Operation::retain(3); delta.add(insert_a.clone()); delta.add(retain_a.clone()); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); iter.seek::(1); - assert_eq!(iter.next_op().unwrap(), OpBuilder::retain(3).build()); + assert_eq!(iter.next_op().unwrap(), retain_a); } #[test] fn delta_utf16_code_unit_seek() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); + delta.add(Operation::insert("12345")); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); iter.seek::(3); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("45").build()); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("45")); } #[test] @@ -213,77 +211,77 @@ fn delta_utf16_code_unit_seek_with_attributes() { .add_attr(RichTextAttribute::Italic(true)) .build(); - delta.add(OpBuilder::insert("1234").attributes(attributes.clone()).build()); - delta.add(OpBuilder::insert("\n").build()); + delta.add(Operation::insert_with_attributes("1234", attributes.clone())); + delta.add(Operation::insert("\n")); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); iter.seek::(0); assert_eq!( iter.next_op_with_len(4).unwrap(), - OpBuilder::insert("1234").attributes(attributes).build(), + Operation::insert_with_attributes("1234", attributes), ); } #[test] fn delta_next_op_len() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - let mut iter = DeltaIter::new(&delta); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("12").build()); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("34").build()); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("5").build()); + delta.add(Operation::insert("12345")); + let mut iter = DeltaIterator::new(&delta); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("12")); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("34")); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("5")); assert_eq!(iter.next_op_with_len(1), None); } #[test] fn delta_next_op_len_with_chinese() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("你好").build()); + delta.add(Operation::insert("你好")); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 2); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("你好").build()); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("你好")); } #[test] fn delta_next_op_len_with_english() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("ab").build()); - let mut iter = DeltaIter::new(&delta); + delta.add(Operation::insert("ab")); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 2); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::insert("ab").build()); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::insert("ab")); } #[test] fn delta_next_op_len_after_seek() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - let mut iter = DeltaIter::new(&delta); + delta.add(Operation::insert("12345")); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 5); iter.seek::(3); assert_eq!(iter.next_op_len().unwrap(), 2); - assert_eq!(iter.next_op_with_len(1).unwrap(), OpBuilder::insert("4").build()); + assert_eq!(iter.next_op_with_len(1).unwrap(), Operation::insert("4")); assert_eq!(iter.next_op_len().unwrap(), 1); - assert_eq!(iter.next_op().unwrap(), OpBuilder::insert("5").build()); + assert_eq!(iter.next_op().unwrap(), Operation::insert("5")); } #[test] fn delta_next_op_len_none() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - let mut iter = DeltaIter::new(&delta); + delta.add(Operation::insert("12345")); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_len().unwrap(), 5); - assert_eq!(iter.next_op_with_len(5).unwrap(), OpBuilder::insert("12345").build()); + assert_eq!(iter.next_op_with_len(5).unwrap(), Operation::insert("12345")); assert_eq!(iter.next_op_len(), None); } #[test] fn delta_next_op_with_len_zero() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - let mut iter = DeltaIter::new(&delta); + delta.add(Operation::insert("12345")); + let mut iter = DeltaIterator::new(&delta); assert_eq!(iter.next_op_with_len(0), None,); assert_eq!(iter.next_op_len().unwrap(), 5); } @@ -291,14 +289,14 @@ fn delta_next_op_with_len_zero() { #[test] fn delta_next_op_with_len_cross_op_return_last() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("12345").build()); - delta.add(OpBuilder::retain(1).build()); - delta.add(OpBuilder::insert("678").build()); + delta.add(Operation::insert("12345")); + delta.add(Operation::retain(1)); + delta.add(Operation::insert("678")); - let mut iter = DeltaIter::new(&delta); + let mut iter = DeltaIterator::new(&delta); iter.seek::(4); assert_eq!(iter.next_op_len().unwrap(), 1); - assert_eq!(iter.next_op_with_len(2).unwrap(), OpBuilder::retain(1).build()); + assert_eq!(iter.next_op_with_len(2).unwrap(), Operation::retain(1)); } #[test] @@ -335,25 +333,21 @@ fn sequence() { fn apply_1000() { for _ in 0..1 { let mut rng = Rng::default(); - let s: FlowyStr = rng.gen_string(50).into(); + let s: OTString = rng.gen_string(50).into(); let delta = rng.gen_delta(&s); - assert_eq!(s.utf16_size(), delta.utf16_base_len); + assert_eq!(s.utf16_len(), delta.utf16_base_len); } } #[test] -fn apply() { - let s = "hello world,".to_owned(); - let mut delta_a = RichTextDelta::default(); - delta_a.insert(&s, RichTextAttributes::default()); +fn apply_test() { + let s = "hello"; + let delta_a = TextDeltaBuilder::new().insert(s).build(); + let delta_b = TextDeltaBuilder::new().retain(s.len()).insert(", AppFlowy").build(); - let mut delta_b = RichTextDelta::default(); - delta_b.retain(s.len(), RichTextAttributes::default()); - delta_b.insert("appflowy", RichTextAttributes::default()); - - let after_a = delta_a.apply("").unwrap(); + let after_a = delta_a.content().unwrap(); let after_b = delta_b.apply(&after_a).unwrap(); - assert_eq!("hello world,appflowy", &after_b); + assert_eq!("hello, AppFlowy", &after_b); } #[test] @@ -384,6 +378,17 @@ fn invert() { } } +#[test] +fn invert_test() { + let s = "hello world"; + let delta = TextDeltaBuilder::new().insert(s).build(); + let invert_delta = delta.invert_str(""); + assert_eq!(delta.utf16_base_len, invert_delta.utf16_target_len); + assert_eq!(delta.utf16_target_len, invert_delta.utf16_base_len); + + assert_eq!(invert_delta.apply(s).unwrap(), "") +} + #[test] fn empty_ops() { let mut delta = RichTextDelta::default(); @@ -415,23 +420,24 @@ fn ops_merging() { assert_eq!(delta.ops.len(), 0); delta.retain(2, RichTextAttributes::default()); assert_eq!(delta.ops.len(), 1); - assert_eq!(delta.ops.last(), Some(&OpBuilder::retain(2).build())); + assert_eq!(delta.ops.last(), Some(&Operation::retain(2))); delta.retain(3, RichTextAttributes::default()); assert_eq!(delta.ops.len(), 1); - assert_eq!(delta.ops.last(), Some(&OpBuilder::retain(5).build())); + assert_eq!(delta.ops.last(), Some(&Operation::retain(5))); delta.insert("abc", RichTextAttributes::default()); assert_eq!(delta.ops.len(), 2); - assert_eq!(delta.ops.last(), Some(&OpBuilder::insert("abc").build())); + assert_eq!(delta.ops.last(), Some(&Operation::insert("abc"))); delta.insert("xyz", RichTextAttributes::default()); assert_eq!(delta.ops.len(), 2); - assert_eq!(delta.ops.last(), Some(&OpBuilder::insert("abcxyz").build())); + assert_eq!(delta.ops.last(), Some(&Operation::insert("abcxyz"))); delta.delete(1); assert_eq!(delta.ops.len(), 3); - assert_eq!(delta.ops.last(), Some(&OpBuilder::delete(1).build())); + assert_eq!(delta.ops.last(), Some(&Operation::delete(1))); delta.delete(1); assert_eq!(delta.ops.len(), 3); - assert_eq!(delta.ops.last(), Some(&OpBuilder::delete(2).build())); + assert_eq!(delta.ops.last(), Some(&Operation::delete(2))); } + #[test] fn is_noop() { let mut delta = RichTextDelta::default(); @@ -449,16 +455,16 @@ fn compose() { let mut rng = Rng::default(); let s = rng.gen_string(20); let a = rng.gen_delta(&s); - let after_a: FlowyStr = a.apply(&s).unwrap().into(); - assert_eq!(a.utf16_target_len, after_a.utf16_size()); + let after_a: OTString = a.apply(&s).unwrap().into(); + assert_eq!(a.utf16_target_len, after_a.utf16_len()); let b = rng.gen_delta(&after_a); - let after_b: FlowyStr = b.apply(&after_a).unwrap().into(); - assert_eq!(b.utf16_target_len, after_b.utf16_size()); + let after_b: OTString = b.apply(&after_a).unwrap().into(); + assert_eq!(b.utf16_target_len, after_b.utf16_len()); let ab = a.compose(&b).unwrap(); assert_eq!(ab.utf16_target_len, b.utf16_target_len); - let after_ab: FlowyStr = ab.apply(&s).unwrap().into(); + let after_ab: OTString = ab.apply(&s).unwrap().into(); assert_eq!(after_b, after_ab); } } @@ -582,11 +588,11 @@ fn transform_two_conflict_non_seq_delta() { #[test] fn delta_invert_no_attribute_delta() { let mut delta = RichTextDelta::default(); - delta.add(OpBuilder::insert("123").build()); + delta.add(Operation::insert("123")); let mut change = RichTextDelta::default(); - change.add(OpBuilder::retain(3).build()); - change.add(OpBuilder::insert("456").build()); + change.add(Operation::retain(3)); + change.add(Operation::insert("456")); let undo = change.invert(&delta); let new_delta = delta.compose(&change).unwrap(); diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/serde_test.rs b/frontend/rust-lib/flowy-text-block/tests/editor/serde_test.rs index 87d425901b..ac4a77412c 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/serde_test.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/serde_test.rs @@ -11,7 +11,7 @@ fn operation_insert_serialize_test() { .add_attr(RichTextAttribute::Bold(true)) .add_attr(RichTextAttribute::Italic(true)) .build(); - let operation = OpBuilder::insert("123").attributes(attributes).build(); + let operation = Operation::insert_with_attributes("123", attributes); let json = serde_json::to_string(&operation).unwrap(); eprintln!("{}", json); @@ -42,7 +42,7 @@ fn attributes_serialize_test() { .add_attr(RichTextAttribute::Bold(true)) .add_attr(RichTextAttribute::Italic(true)) .build(); - let retain = OpBuilder::insert("123").attributes(attributes).build(); + let retain = Operation::insert_with_attributes("123", attributes); let json = serde_json::to_string(&retain).unwrap(); eprintln!("{}", json); @@ -56,7 +56,7 @@ fn delta_serialize_multi_attribute_test() { .add_attr(RichTextAttribute::Bold(true)) .add_attr(RichTextAttribute::Italic(true)) .build(); - let retain = OpBuilder::insert("123").attributes(attributes).build(); + let retain = Operation::insert_with_attributes("123", attributes); delta.add(retain); delta.add(Operation::Retain(5.into())); @@ -65,7 +65,7 @@ fn delta_serialize_multi_attribute_test() { let json = serde_json::to_string(&delta).unwrap(); eprintln!("{}", json); - let delta_from_json = Delta::from_delta_str(&json).unwrap(); + let delta_from_json = Delta::from_json(&json).unwrap(); assert_eq!(delta_from_json, delta); } @@ -77,7 +77,7 @@ fn delta_deserialize_test() { {"retain":2,"attributes":{"italic":"true","bold":"true"}}, {"retain":2,"attributes":{"italic":true,"bold":true}} ]"#; - let delta = RichTextDelta::from_delta_str(json).unwrap(); + let delta = RichTextDelta::from_json(json).unwrap(); eprintln!("{}", delta); } @@ -86,13 +86,13 @@ fn delta_deserialize_null_test() { let json = r#"[ {"retain":7,"attributes":{"bold":null}} ]"#; - let delta1 = RichTextDelta::from_delta_str(json).unwrap(); + let delta1 = RichTextDelta::from_json(json).unwrap(); let mut attribute = RichTextAttribute::Bold(true); attribute.value = RichTextAttributeValue(None); let delta2 = DeltaBuilder::new().retain_with_attributes(7, attribute.into()).build(); - assert_eq!(delta2.to_delta_str(), r#"[{"retain":7,"attributes":{"bold":""}}]"#); + assert_eq!(delta2.json_str(), r#"[{"retain":7,"attributes":{"bold":""}}]"#); assert_eq!(delta1, delta2); } diff --git a/frontend/rust-lib/flowy-text-block/tests/editor/undo_redo_test.rs b/frontend/rust-lib/flowy-text-block/tests/editor/undo_redo_test.rs index e6ea9200ab..78aa034744 100644 --- a/frontend/rust-lib/flowy-text-block/tests/editor/undo_redo_test.rs +++ b/frontend/rust-lib/flowy-text-block/tests/editor/undo_redo_test.rs @@ -108,6 +108,7 @@ fn history_bold_redo_with_lagging() { fn history_delete_undo() { let ops = vec![ Insert(0, "123", 0), + Wait(RECORD_THRESHOLD), AssertDocJson(0, r#"[{"insert":"123"}]"#), Delete(0, Interval::new(0, 3)), AssertDocJson(0, r#"[]"#), diff --git a/frontend/rust-lib/flowy-user/src/entities/auth.rs b/frontend/rust-lib/flowy-user/src/entities/auth.rs index 14224ae231..f7625b37d7 100644 --- a/frontend/rust-lib/flowy-user/src/entities/auth.rs +++ b/frontend/rust-lib/flowy-user/src/entities/auth.rs @@ -4,7 +4,7 @@ use flowy_derive::ProtoBuf; use std::convert::TryInto; #[derive(ProtoBuf, Default)] -pub struct SignInPayload { +pub struct SignInPayloadPB { #[pb(index = 1)] pub email: String, @@ -42,7 +42,7 @@ pub struct SignInResponse { pub token: String, } -impl TryInto for SignInPayload { +impl TryInto for SignInPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { @@ -58,7 +58,7 @@ impl TryInto for SignInPayload { } #[derive(ProtoBuf, Default)] -pub struct SignUpPayload { +pub struct SignUpPayloadPB { #[pb(index = 1)] pub email: String, @@ -68,7 +68,7 @@ pub struct SignUpPayload { #[pb(index = 3)] pub password: String, } -impl TryInto for SignUpPayload { +impl TryInto for SignUpPayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 82553e8050..276894ffc8 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -7,13 +7,13 @@ use crate::{ }; #[derive(Default, ProtoBuf)] -pub struct UserToken { +pub struct UserTokenPB { #[pb(index = 1)] pub token: String, } #[derive(ProtoBuf, Default, Debug, PartialEq, Eq, Clone)] -pub struct UserProfile { +pub struct UserProfilePB { #[pb(index = 1)] pub id: String, @@ -28,7 +28,7 @@ pub struct UserProfile { } #[derive(ProtoBuf, Default)] -pub struct UpdateUserProfilePayload { +pub struct UpdateUserProfilePayloadPB { #[pb(index = 1)] pub id: String, @@ -42,7 +42,7 @@ pub struct UpdateUserProfilePayload { pub password: Option, } -impl UpdateUserProfilePayload { +impl UpdateUserProfilePayloadPB { pub fn new(id: &str) -> Self { Self { id: id.to_owned(), @@ -85,7 +85,9 @@ impl UpdateUserProfileParams { pub fn new(user_id: &str) -> Self { Self { id: user_id.to_owned(), - ..Default::default() + name: None, + email: None, + password: None, } } @@ -105,7 +107,7 @@ impl UpdateUserProfileParams { } } -impl TryInto for UpdateUserProfilePayload { +impl TryInto for UpdateUserProfilePayloadPB { type Error = ErrorCode; fn try_into(self) -> Result { diff --git a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs index 8f7beadb4a..23c74b6d5f 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_setting.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_setting.rs @@ -2,22 +2,22 @@ use flowy_derive::ProtoBuf; use serde::{Deserialize, Serialize}; #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct UserPreferences { +pub struct UserPreferencesPB { #[pb(index = 1)] user_id: String, #[pb(index = 2)] - appearance_setting: AppearanceSettings, + appearance_setting: AppearanceSettingsPB, } #[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)] -pub struct AppearanceSettings { +pub struct AppearanceSettingsPB { #[pb(index = 1)] pub theme: String, #[pb(index = 2)] #[serde(default)] - pub locale: LocaleSettings, + pub locale: LocaleSettingsPB, #[pb(index = 3)] #[serde(default = "DEFAULT_RESET_VALUE")] @@ -27,7 +27,7 @@ pub struct AppearanceSettings { const DEFAULT_RESET_VALUE: fn() -> bool = || APPEARANCE_RESET_AS_DEFAULT; #[derive(ProtoBuf, Serialize, Deserialize, Debug, Clone)] -pub struct LocaleSettings { +pub struct LocaleSettingsPB { #[pb(index = 1)] pub language_code: String, @@ -35,7 +35,7 @@ pub struct LocaleSettings { pub country_code: String, } -impl std::default::Default for LocaleSettings { +impl std::default::Default for LocaleSettingsPB { fn default() -> Self { Self { language_code: "en".to_owned(), @@ -47,11 +47,11 @@ impl std::default::Default for LocaleSettings { pub const APPEARANCE_DEFAULT_THEME: &str = "light"; const APPEARANCE_RESET_AS_DEFAULT: bool = true; -impl std::default::Default for AppearanceSettings { +impl std::default::Default for AppearanceSettingsPB { fn default() -> Self { - AppearanceSettings { + AppearanceSettingsPB { theme: APPEARANCE_DEFAULT_THEME.to_owned(), - locale: LocaleSettings::default(), + locale: LocaleSettingsPB::default(), reset_as_default: APPEARANCE_RESET_AS_DEFAULT, } } diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 348da177d3..ced1ede179 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -1,5 +1,5 @@ use crate::entities::{ - SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, + SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB, }; use crate::{errors::FlowyError, handlers::*, services::UserSession}; use lib_dispatch::prelude::*; @@ -26,7 +26,7 @@ pub trait UserCloudService: Send + Sync { fn sign_in(&self, params: SignInParams) -> FutureResult; fn sign_out(&self, token: &str) -> FutureResult<(), FlowyError>; fn update_user(&self, token: &str, params: UpdateUserProfileParams) -> FutureResult<(), FlowyError>; - fn get_user(&self, token: &str) -> FutureResult; + fn get_user(&self, token: &str) -> FutureResult; fn ws_addr(&self) -> String; } @@ -39,27 +39,27 @@ pub enum UserEvent { #[event()] InitUser = 0, - #[event(input = "SignInPayload", output = "UserProfile")] + #[event(input = "SignInPayloadPB", output = "UserProfilePB")] SignIn = 1, - #[event(input = "SignUpPayload", output = "UserProfile")] + #[event(input = "SignUpPayloadPB", output = "UserProfilePB")] SignUp = 2, #[event(passthrough)] SignOut = 3, - #[event(input = "UpdateUserProfilePayload")] + #[event(input = "UpdateUserProfilePayloadPB")] UpdateUserProfile = 4, - #[event(output = "UserProfile")] + #[event(output = "UserProfilePB")] GetUserProfile = 5, - #[event(output = "UserProfile")] + #[event(output = "UserProfilePB")] CheckUser = 6, - #[event(input = "AppearanceSettings")] + #[event(input = "AppearanceSettingsPB")] SetAppearanceSetting = 7, - #[event(output = "AppearanceSettings")] + #[event(output = "AppearanceSettingsPB")] GetAppearanceSetting = 8, } diff --git a/frontend/rust-lib/flowy-user/src/handlers/auth_handler.rs b/frontend/rust-lib/flowy-user/src/handlers/auth_handler.rs index 22a17d887c..8ff5bdb7e2 100644 --- a/frontend/rust-lib/flowy-user/src/handlers/auth_handler.rs +++ b/frontend/rust-lib/flowy-user/src/handlers/auth_handler.rs @@ -7,9 +7,9 @@ use std::{convert::TryInto, sync::Arc}; // tracing instrument 👉🏻 https://docs.rs/tracing/0.1.26/tracing/attr.instrument.html #[tracing::instrument(level = "debug", name = "sign_in", skip(data, session), fields(email = %data.email), err)] pub async fn sign_in( - data: Data, + data: Data, session: AppData>, -) -> DataResult { +) -> DataResult { let params: SignInParams = data.into_inner().try_into()?; let user_profile = session.sign_in(params).await?; data_result(user_profile) @@ -26,9 +26,9 @@ pub async fn sign_in( err )] pub async fn sign_up( - data: Data, + data: Data, session: AppData>, -) -> DataResult { +) -> DataResult { let params: SignUpParams = data.into_inner().try_into()?; let user_profile = session.sign_up(params).await?; diff --git a/frontend/rust-lib/flowy-user/src/handlers/user_handler.rs b/frontend/rust-lib/flowy-user/src/handlers/user_handler.rs index 6ee62519e7..8a10dd89e2 100644 --- a/frontend/rust-lib/flowy-user/src/handlers/user_handler.rs +++ b/frontend/rust-lib/flowy-user/src/handlers/user_handler.rs @@ -1,5 +1,5 @@ use crate::entities::{ - AppearanceSettings, UpdateUserProfileParams, UpdateUserProfilePayload, UserProfile, APPEARANCE_DEFAULT_THEME, + AppearanceSettingsPB, UpdateUserProfileParams, UpdateUserProfilePayloadPB, UserProfilePB, APPEARANCE_DEFAULT_THEME, }; use crate::{errors::FlowyError, services::UserSession}; use flowy_database::kv::KV; @@ -13,13 +13,13 @@ pub async fn init_user_handler(session: AppData>) -> Result<(), } #[tracing::instrument(level = "debug", skip(session))] -pub async fn check_user_handler(session: AppData>) -> DataResult { +pub async fn check_user_handler(session: AppData>) -> DataResult { let user_profile = session.check_user().await?; data_result(user_profile) } #[tracing::instrument(level = "debug", skip(session))] -pub async fn get_user_profile_handler(session: AppData>) -> DataResult { +pub async fn get_user_profile_handler(session: AppData>) -> DataResult { let user_profile = session.get_user_profile().await?; data_result(user_profile) } @@ -32,7 +32,7 @@ pub async fn sign_out(session: AppData>) -> Result<(), FlowyErr #[tracing::instrument(level = "debug", skip(data, session))] pub async fn update_user_profile_handler( - data: Data, + data: Data, session: AppData>, ) -> Result<(), FlowyError> { let params: UpdateUserProfileParams = data.into_inner().try_into()?; @@ -43,7 +43,7 @@ pub async fn update_user_profile_handler( const APPEARANCE_SETTING_CACHE_KEY: &str = "appearance_settings"; #[tracing::instrument(level = "debug", skip(data), err)] -pub async fn set_appearance_setting(data: Data) -> Result<(), FlowyError> { +pub async fn set_appearance_setting(data: Data) -> Result<(), FlowyError> { let mut setting = data.into_inner(); if setting.theme.is_empty() { setting.theme = APPEARANCE_DEFAULT_THEME.to_string(); @@ -55,15 +55,15 @@ pub async fn set_appearance_setting(data: Data) -> Result<() } #[tracing::instrument(err)] -pub async fn get_appearance_setting() -> DataResult { +pub async fn get_appearance_setting() -> DataResult { match KV::get_str(APPEARANCE_SETTING_CACHE_KEY) { - None => data_result(AppearanceSettings::default()), + None => data_result(AppearanceSettingsPB::default()), Some(s) => { let setting = match serde_json::from_str(&s) { Ok(setting) => setting, Err(e) => { tracing::error!("Deserialize AppearanceSettings failed: {:?}, fallback to default", e); - AppearanceSettings::default() + AppearanceSettingsPB::default() } }; data_result(setting) diff --git a/frontend/rust-lib/flowy-user/src/services/database.rs b/frontend/rust-lib/flowy-user/src/services/database.rs index eaf5b7f6cb..64ebd705a7 100644 --- a/frontend/rust-lib/flowy-user/src/services/database.rs +++ b/frontend/rust-lib/flowy-user/src/services/database.rs @@ -1,4 +1,4 @@ -use crate::entities::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfile}; +use crate::entities::{SignInResponse, SignUpResponse, UpdateUserProfileParams, UserProfilePB}; use flowy_database::ConnectionPool; use flowy_database::{schema::user_table, DBConnection, Database}; use flowy_error::{ErrorCode, FlowyError}; @@ -113,9 +113,9 @@ impl std::convert::From for UserTable { } } -impl std::convert::From for UserProfile { +impl std::convert::From for UserProfilePB { fn from(table: UserTable) -> Self { - UserProfile { + UserProfilePB { id: table.id, email: table.email, name: table.name, diff --git a/frontend/rust-lib/flowy-user/src/services/notifier.rs b/frontend/rust-lib/flowy-user/src/services/notifier.rs index 9500988d99..4efdc301ac 100644 --- a/frontend/rust-lib/flowy-user/src/services/notifier.rs +++ b/frontend/rust-lib/flowy-user/src/services/notifier.rs @@ -1,4 +1,4 @@ -use crate::entities::UserProfile; +use crate::entities::UserProfilePB; use tokio::sync::{broadcast, mpsc}; #[derive(Clone)] @@ -14,7 +14,7 @@ pub enum UserStatus { token: String, }, SignUp { - profile: UserProfile, + profile: UserProfilePB, ret: mpsc::Sender<()>, }, } @@ -42,7 +42,7 @@ impl UserNotifier { }); } - pub(crate) fn notify_sign_up(&self, ret: mpsc::Sender<()>, user_profile: &UserProfile) { + pub(crate) fn notify_sign_up(&self, ret: mpsc::Sender<()>, user_profile: &UserProfilePB) { let _ = self.user_status_notifier.send(UserStatus::SignUp { profile: user_profile.clone(), ret, diff --git a/frontend/rust-lib/flowy-user/src/services/user_session.rs b/frontend/rust-lib/flowy-user/src/services/user_session.rs index 48e6ef9661..3822a8a0db 100644 --- a/frontend/rust-lib/flowy-user/src/services/user_session.rs +++ b/frontend/rust-lib/flowy-user/src/services/user_session.rs @@ -1,5 +1,5 @@ use crate::entities::{ - SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfile, + SignInParams, SignInResponse, SignUpParams, SignUpResponse, UpdateUserProfileParams, UserProfilePB, }; use crate::{ dart_notification::*, @@ -80,7 +80,7 @@ impl UserSession { } #[tracing::instrument(level = "debug", skip(self))] - pub async fn sign_in(&self, params: SignInParams) -> Result { + pub async fn sign_in(&self, params: SignInParams) -> Result { if self.is_user_login(¶ms.email) { self.get_user_profile().await } else { @@ -88,14 +88,14 @@ impl UserSession { let session: Session = resp.clone().into(); let _ = self.set_session(Some(session))?; let user_table = self.save_user(resp.into()).await?; - let user_profile: UserProfile = user_table.into(); + let user_profile: UserProfilePB = user_table.into(); self.notifier.notify_login(&user_profile.token, &user_profile.id); Ok(user_profile) } } #[tracing::instrument(level = "debug", skip(self))] - pub async fn sign_up(&self, params: SignUpParams) -> Result { + pub async fn sign_up(&self, params: SignUpParams) -> Result { if self.is_user_login(¶ms.email) { self.get_user_profile().await } else { @@ -103,7 +103,7 @@ impl UserSession { let session: Session = resp.clone().into(); let _ = self.set_session(Some(session))?; let user_table = self.save_user(resp.into()).await?; - let user_profile: UserProfile = user_table.into(); + let user_profile: UserProfilePB = user_table.into(); let (ret, mut tx) = mpsc::channel(1); self.notifier.notify_sign_up(ret, &user_profile); @@ -143,7 +143,7 @@ impl UserSession { Ok(()) } - pub async fn check_user(&self) -> Result { + pub async fn check_user(&self) -> Result { let (user_id, token) = self.get_session()?.into_part(); let user = dsl::user_table @@ -154,7 +154,7 @@ impl UserSession { Ok(user.into()) } - pub async fn get_user_profile(&self) -> Result { + pub async fn get_user_profile(&self) -> Result { let (user_id, token) = self.get_session()?.into_part(); let user = dsl::user_table .filter(user_table::id.eq(&user_id)) diff --git a/frontend/rust-lib/flowy-user/tests/event/auth_test.rs b/frontend/rust-lib/flowy-user/tests/event/auth_test.rs index 39b22a9af0..288b9d8b08 100644 --- a/frontend/rust-lib/flowy-user/tests/event/auth_test.rs +++ b/frontend/rust-lib/flowy-user/tests/event/auth_test.rs @@ -1,13 +1,13 @@ use crate::helper::*; use flowy_test::{event_builder::UserModuleEventBuilder, FlowySDKTest}; -use flowy_user::entities::{SignInPayload, SignUpPayload, UserProfile}; +use flowy_user::entities::{SignInPayloadPB, SignUpPayloadPB, UserProfilePB}; use flowy_user::{errors::ErrorCode, event_map::UserEvent::*}; #[tokio::test] async fn sign_up_with_invalid_email() { for email in invalid_email_test_case() { let sdk = FlowySDKTest::default(); - let request = SignUpPayload { + let request = SignUpPayloadPB { email: email.to_string(), name: valid_name(), password: login_password(), @@ -29,7 +29,7 @@ async fn sign_up_with_invalid_email() { async fn sign_up_with_invalid_password() { for password in invalid_password_test_case() { let sdk = FlowySDKTest::default(); - let request = SignUpPayload { + let request = SignUpPayloadPB { email: random_email(), name: valid_name(), password, @@ -50,7 +50,7 @@ async fn sign_in_success() { let _ = UserModuleEventBuilder::new(test.clone()).event(SignOut).sync_send(); let sign_up_context = test.sign_up().await; - let request = SignInPayload { + let request = SignInPayloadPB { email: sign_up_context.user_profile.email.clone(), password: sign_up_context.password.clone(), name: "".to_string(), @@ -61,7 +61,7 @@ async fn sign_in_success() { .payload(request) .async_send() .await - .parse::(); + .parse::(); dbg!(&response); } @@ -69,7 +69,7 @@ async fn sign_in_success() { async fn sign_in_with_invalid_email() { for email in invalid_email_test_case() { let sdk = FlowySDKTest::default(); - let request = SignInPayload { + let request = SignInPayloadPB { email: email.to_string(), password: login_password(), name: "".to_string(), @@ -93,7 +93,7 @@ async fn sign_in_with_invalid_password() { for password in invalid_password_test_case() { let sdk = FlowySDKTest::default(); - let request = SignInPayload { + let request = SignInPayloadPB { email: random_email(), password, name: "".to_string(), diff --git a/frontend/rust-lib/flowy-user/tests/event/user_profile_test.rs b/frontend/rust-lib/flowy-user/tests/event/user_profile_test.rs index b01a533336..fa77b69509 100644 --- a/frontend/rust-lib/flowy-user/tests/event/user_profile_test.rs +++ b/frontend/rust-lib/flowy-user/tests/event/user_profile_test.rs @@ -1,6 +1,6 @@ use crate::helper::*; use flowy_test::{event_builder::UserModuleEventBuilder, FlowySDKTest}; -use flowy_user::entities::{UpdateUserProfilePayload, UserProfile}; +use flowy_user::entities::{UpdateUserProfilePayloadPB, UserProfilePB}; use flowy_user::{errors::ErrorCode, event_map::UserEvent::*}; use nanoid::nanoid; @@ -24,7 +24,7 @@ async fn user_profile_get() { let user = UserModuleEventBuilder::new(test.clone()) .event(GetUserProfile) .sync_send() - .parse::(); + .parse::(); assert_eq!(user_profile, user); } @@ -33,7 +33,7 @@ async fn user_update_with_name() { let sdk = FlowySDKTest::default(); let user = sdk.init_user().await; let new_name = "hello_world".to_owned(); - let request = UpdateUserProfilePayload::new(&user.id).name(&new_name); + let request = UpdateUserProfilePayloadPB::new(&user.id).name(&new_name); let _ = UserModuleEventBuilder::new(sdk.clone()) .event(UpdateUserProfile) .payload(request) @@ -43,7 +43,7 @@ async fn user_update_with_name() { .event(GetUserProfile) .assert_error() .sync_send() - .parse::(); + .parse::(); assert_eq!(user_profile.name, new_name,); } @@ -53,7 +53,7 @@ async fn user_update_with_email() { let sdk = FlowySDKTest::default(); let user = sdk.init_user().await; let new_email = format!("{}@gmail.com", nanoid!(6)); - let request = UpdateUserProfilePayload::new(&user.id).email(&new_email); + let request = UpdateUserProfilePayloadPB::new(&user.id).email(&new_email); let _ = UserModuleEventBuilder::new(sdk.clone()) .event(UpdateUserProfile) .payload(request) @@ -62,7 +62,7 @@ async fn user_update_with_email() { .event(GetUserProfile) .assert_error() .sync_send() - .parse::(); + .parse::(); assert_eq!(user_profile.email, new_email,); } @@ -72,7 +72,7 @@ async fn user_update_with_password() { let sdk = FlowySDKTest::default(); let user = sdk.init_user().await; let new_password = "H123world!".to_owned(); - let request = UpdateUserProfilePayload::new(&user.id).password(&new_password); + let request = UpdateUserProfilePayloadPB::new(&user.id).password(&new_password); let _ = UserModuleEventBuilder::new(sdk.clone()) .event(UpdateUserProfile) @@ -86,7 +86,7 @@ async fn user_update_with_invalid_email() { let test = FlowySDKTest::default(); let user = test.init_user().await; for email in invalid_email_test_case() { - let request = UpdateUserProfilePayload::new(&user.id).email(&email); + let request = UpdateUserProfilePayloadPB::new(&user.id).email(&email); assert_eq!( UserModuleEventBuilder::new(test.clone()) .event(UpdateUserProfile) @@ -104,7 +104,7 @@ async fn user_update_with_invalid_password() { let test = FlowySDKTest::default(); let user = test.init_user().await; for password in invalid_password_test_case() { - let request = UpdateUserProfilePayload::new(&user.id).password(&password); + let request = UpdateUserProfilePayloadPB::new(&user.id).password(&password); UserModuleEventBuilder::new(test.clone()) .event(UpdateUserProfile) @@ -118,7 +118,7 @@ async fn user_update_with_invalid_password() { async fn user_update_with_invalid_name() { let test = FlowySDKTest::default(); let user = test.init_user().await; - let request = UpdateUserProfilePayload::new(&user.id).name(""); + let request = UpdateUserProfilePayloadPB::new(&user.id).name(""); UserModuleEventBuilder::new(test.clone()) .event(UpdateUserProfile) .payload(request) diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index 54b79173bc..f050087977 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -33,7 +33,7 @@ source $HOME/.cargo/env && \ cargo install --force cargo-make && \ cargo install --force duckscript_cli && \ cargo make flowy_dev && \ -cargo make -p production-linux-x86 appflowy-linux +cargo make -p production-linux-x86_64 appflowy-linux CMD ["/home/makepkg/appflowy/frontend/app_flowy/build/linux/x64/release/bundle/app_flowy"] diff --git a/frontend/scripts/docker-buildfiles/docker-compose.yml b/frontend/scripts/docker-buildfiles/docker-compose.yml index 581a1232e9..ac1cc29339 100644 --- a/frontend/scripts/docker-buildfiles/docker-compose.yml +++ b/frontend/scripts/docker-buildfiles/docker-compose.yml @@ -6,8 +6,11 @@ services: image: appflowy/appflowy:latest stdin_open: true # tty: true + devices: + - "/dev/dri:/dev/dri" # fixes MESA-LOADER error environment: - DISPLAY=${DISPLAY} + - NO_AT_BRIDGE=1 # fixes dbind-WARNING volumes: - $HOME/.Xauthority:/root/.Xauthority:rw - /tmp/.X11-unix:/tmp/.X11-unix diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh new file mode 100755 index 0000000000..396a66cd63 --- /dev/null +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" + +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + + +# Note: This script does not install applications which are installed by the package manager. There are too many package managers out there. + +# Install Rust +printMessage "The Rust programming language is required to compile AppFlowy." +printMessage "We can install it now if you don't already have it on your system." + +read -p "$(printSuccess "Do you want to install Rust? [y/N]") " installrust + +if [ ${installrust^^} == "Y" ]; then + printMessage "Installing Rust." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + source $HOME/.cargo/env + rustup toolchain install stable + rustup default stable +else + printMessage "Skipping Rust installation." +fi + +# Enable the flutter stable channel +printMessage "Setting up Flutter" +flutter channel stable + +# Enable linux desktop +flutter config --enable-linux-desktop + +# Fix any problems reported by flutter doctor +flutter doctor + +# Add the githooks directory to your git configuration +printMessage "Setting up githooks." +git config core.hooksPath .githooks + +# Install go-gitlint +printMessage "Installing go-gitlint." +GOLINT_FILENAME="go-gitlint_1.1.0_linux_x86_64.tar.gz" +wget https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} +tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint +rm ${GOLINT_FILENAME} + +# Change to the frontend directory +cd frontend + +# Install cargo make +printMessage "Installing cargo-make." +cargo install --force cargo-make + +# Install duckscript +printMessage "Installing duckscript." +cargo install --force duckscript_cli + +# Check prerequisites +printMessage "Checking prerequisites." +cargo make flowy_dev diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh new file mode 100755 index 0000000000..f7e9d73909 --- /dev/null +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" + +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + + +# Install Rust +printMessage "The Rust programming language is required to compile AppFlowy." +printMessage "We can install it now if you don't already have it on your system." + +read -p "$(printSuccess "Do you want to install Rust? [Y/N]") " installrust + +if [ ${installrust} == "Y" ] || [ ${installrust} == "y" ]; then + printMessage "Installing Rust." + brew install rustup-init + rustup-init -y --default-toolchain=stable + + source "$HOME/.cargo/env" +else + printMessage "Skipping Rust installation." +fi + +# Install sqllite +printMessage "Installing sqlLite3." +brew install sqlite3 + +# Enable the flutter stable channel +printMessage "Setting up Flutter" +flutter channel stable + +# Enable linux desktop +flutter config --enable-macos-desktop + +# Fix any problems reported by flutter doctor +flutter doctor + +# Add the githooks directory to your git configuration +printMessage "Setting up githooks." +git config core.hooksPath .githooks + +# Install go-gitlint +printMessage "Installing go-gitlint." +GOLINT_FILENAME="go-gitlint_1.1.0_osx_x86_64.tar.gz" +curl -L https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} --output ${GOLINT_FILENAME} +tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint +rm ${GOLINT_FILENAME} + +# Change to the frontend directory +cd frontend + +# Install cargo make +printMessage "Installing cargo-make." +cargo install --force cargo-make + +# Install duckscript +printMessage "Installing duckscript." +cargo install --force duckscript_cli + +# Check prerequisites +printMessage "Checking prerequisites." +cargo make flowy_dev diff --git a/frontend/scripts/install_dev_env/install_windows.sh b/frontend/scripts/install_dev_env/install_windows.sh new file mode 100644 index 0000000000..bbba4cf2d5 --- /dev/null +++ b/frontend/scripts/install_dev_env/install_windows.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +YELLOW="\e[93m" +GREEN="\e[32m" +RED="\e[31m" +ENDCOLOR="\e[0m" + +printMessage() { + printf "${YELLOW}AppFlowy : $1${ENDCOLOR}\n" +} + +printSuccess() { + printf "${GREEN}AppFlowy : $1${ENDCOLOR}\n" +} + +printError() { + printf "${RED}AppFlowy : $1${ENDCOLOR}\n" +} + + +# Note: This script does not install applications which are installed by the package manager. There are too many package managers out there. + +# Install Rust +if ! rustc --version; then + + printMessage "The Rust programming language is required to compile AppFlowy." + printMessage "It has not been detected on your system." + + read -p "$(printSuccess "Do you want to install Rust? [y/N]") " installrust + + if [ ${installrust^^} == "Y" ]; then + printMessage "Installing Rust." + if ! curl --proto '=https' --tlsv1.2 -sSf https://win.rustup.rs/x86_64 -o rustup-init.exe; then + printError "Failed to download the Rust installer" + exit 1 + fi + start "Rust Installer" rustup-init.exe + read -p "$(printSuccess "Press enter when Rust installation is done") " isDone + rm rustup-init.exe + $USERPROFILE/.cargo/bin/rustup toolchain install stable + $USERPROFILE/.cargo/bin/rustup default stable + else + printMessage "Skipping Rust installation." + fi +else + printSuccess "Rust has been detected on your system, so Rust installation has been skipped" +fi + +# Enable the flutter stable channel +printMessage "Setting up Flutter" +flutter channel stable + +# Add pub cache and cargo to PATH +powershell '[Environment]::SetEnvironmentVariable("PATH", $Env:PATH + ";" + $Env:LOCALAPPDATA + "\Pub\Cache\Bin", [EnvironmentVariableTarget]::User)' +powershell '[Environment]::SetEnvironmentVariable("PATH", $Env:PATH + ";" + $Env:USERPROFILE + "\.cargo\bin", [EnvironmentVariableTarget]::User)' + +# Enable linux desktop +flutter config --enable-windows-desktop + +# Fix any problems reported by flutter doctor +flutter doctor + +# Add the githooks directory to your git configuration +printMessage "Setting up githooks." +git config core.hooksPath .githooks + +# Install go-gitlint +printMessage "Installing go-gitlint." +GOLINT_FILENAME="go-gitlint_1.1.0_windows_x86_64.tar.gz" +if curl --proto '=https' --tlsv1.2 -sSfL https://github.com/llorllale/go-gitlint/releases/download/1.1.0/${GOLINT_FILENAME} -o ${GOLINT_FILENAME}; then + tar -zxv --directory .githooks/. -f ${GOLINT_FILENAME} gitlint.exe + rm ${GOLINT_FILENAME} +else + printError "Failed to install go-gitlint" +fi + +# Change to the frontend directory +cd frontend + +# Install cargo make +printMessage "Installing cargo-make." +$USERPROFILE/.cargo/bin/cargo install --force cargo-make + +# Install duckscript +printMessage "Installing duckscript." +$USERPROFILE/.cargo/bin/cargo install --force duckscript_cli + +# Enable vcpkg integration +# Note: Requires admin +printMessage "Setting up vcpkg." +vcpkg integrate install + +# Check prerequisites +printMessage "Checking prerequisites." +PATH="$PATH;$LOCALAPPDATA\Pub\Cache\bin" bash -c '$USERPROFILE/.cargo/bin/cargo make flowy_dev' diff --git a/package.json b/package.json deleted file mode 100644 index c891bb7d81..0000000000 --- a/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "devDependencies": { - "@commitlint/cli": "16.1.0", - "@commitlint/config-conventional": "16.0.0", - "husky": "7.0.4" - } -} diff --git a/shared-lib/Cargo.lock b/shared-lib/Cargo.lock index 62d70be943..9b8e5f6081 100644 --- a/shared-lib/Cargo.lock +++ b/shared-lib/Cargo.lock @@ -332,9 +332,9 @@ dependencies = [ [[package]] name = "faccess" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e039175679baf763ddddf4f76900b92d4dae9411ee88cf42d2f11b976b09e07c" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" dependencies = [ "bitflags", "libc", diff --git a/shared-lib/flowy-derive/src/proto_buf/deserialize.rs b/shared-lib/flowy-derive/src/proto_buf/deserialize.rs index f0325db3b0..4fbbb1b3a7 100644 --- a/shared-lib/flowy-derive/src/proto_buf/deserialize.rs +++ b/shared-lib/flowy-derive/src/proto_buf/deserialize.rs @@ -25,8 +25,15 @@ pub fn make_de_token_steam(ctxt: &Ctxt, ast: &ASTContainer) -> Option for #struct_ident { type Error = ::protobuf::ProtobufError; fn try_from(bytes: bytes::Bytes) -> Result { - let pb: crate::protobuf::#pb_ty = ::protobuf::Message::parse_from_bytes(&bytes)?; - #struct_ident::try_from(pb) + Self::try_from(&bytes) + } + } + + impl std::convert::TryFrom<&bytes::Bytes> for #struct_ident { + type Error = ::protobuf::ProtobufError; + fn try_from(bytes: &bytes::Bytes) -> Result { + let pb: crate::protobuf::#pb_ty = ::protobuf::Message::parse_from_bytes(bytes)?; + Ok(#struct_ident::from(pb)) } } @@ -34,16 +41,15 @@ pub fn make_de_token_steam(ctxt: &Ctxt, ast: &ASTContainer) -> Option Result { let pb: crate::protobuf::#pb_ty = ::protobuf::Message::parse_from_bytes(bytes)?; - #struct_ident::try_from(pb) + Ok(#struct_ident::from(pb)) } } - impl std::convert::TryFrom for #struct_ident { - type Error = ::protobuf::ProtobufError; - fn try_from(mut pb: crate::protobuf::#pb_ty) -> Result { + impl std::convert::From for #struct_ident { + fn from(mut pb: crate::protobuf::#pb_ty) -> Self { let mut o = Self::default(); #(#build_take_fields)* - Ok(o) + o } } }; @@ -70,7 +76,7 @@ fn token_stream_for_one_of(ctxt: &Ctxt, field: &ASTField) -> Option let ty = bracketed_ty_info.unwrap().ty; Some(quote! { if pb.#has_func() { - let enum_de_from_pb = #ty::try_from(&pb.#get_func()).unwrap(); + let enum_de_from_pb = #ty::from(&pb.#get_func()); o.#member = Some(enum_de_from_pb); } }) @@ -104,7 +110,7 @@ fn token_stream_for_one_of(ctxt: &Ctxt, field: &ASTField) -> Option let ty = bracketed_ty_info.unwrap().ty; Some(quote! { if pb.#has_func() { - let val = #ty::try_from(pb.#take_func()).unwrap(); + let val = #ty::from(pb.#take_func()); o.#member=Some(val); } }) @@ -138,7 +144,7 @@ fn token_stream_for_field(ctxt: &Ctxt, member: &syn::Member, ty: &syn::Type, is_ Some(quote! { let some_value = pb.#member.#take(); if some_value.is_some() { - let struct_de_from_pb = #ty::try_from(some_value.unwrap()).unwrap(); + let struct_de_from_pb = #ty::from(some_value.unwrap()); o.#member = struct_de_from_pb; } }) @@ -147,7 +153,7 @@ fn token_stream_for_field(ctxt: &Ctxt, member: &syn::Member, ty: &syn::Type, is_ TypeCategory::Enum => { let ty = ty_info.ty; Some(quote! { - let enum_de_from_pb = #ty::try_from(&pb.#member).unwrap(); + let enum_de_from_pb = #ty::from(&pb.#member); o.#member = enum_de_from_pb; }) @@ -192,7 +198,7 @@ fn token_stream_for_vec(ctxt: &Ctxt, member: &syn::Member, bracketed_type: &TyIn Some(quote! { o.#member = pb.#take_ident() .into_iter() - .map(|m| #ty::try_from(m).unwrap()) + .map(|m| #ty::from(m)) .collect(); }) } @@ -221,7 +227,7 @@ fn token_stream_for_map(ctxt: &Ctxt, member: &syn::Member, ty_info: &TyInfo) -> TypeCategory::Protobuf => Some(quote! { let mut m: std::collections::HashMap = std::collections::HashMap::new(); pb.#take_ident().into_iter().for_each(|(k,v)| { - m.insert(k.clone(), #ty::try_from(v).unwrap()); + m.insert(k.clone(), #ty::from(v)); }); o.#member = m; }), diff --git a/shared-lib/flowy-derive/src/proto_buf/enum_serde.rs b/shared-lib/flowy-derive/src/proto_buf/enum_serde.rs index 498be05e51..fb0870fde8 100644 --- a/shared-lib/flowy-derive/src/proto_buf/enum_serde.rs +++ b/shared-lib/flowy-derive/src/proto_buf/enum_serde.rs @@ -20,21 +20,19 @@ pub fn make_enum_token_stream(_ctxt: &Ctxt, cont: &ASTContainer) -> Option for #enum_ident { - type Error = String; - fn try_from(pb:&crate::protobuf::#pb_enum) -> Result { - Ok(match pb { + impl std::convert::From<&crate::protobuf::#pb_enum> for #enum_ident { + fn from(pb:&crate::protobuf::#pb_enum) -> Self { + match pb { #(#build_from_pb_enum)* - }) + } } } - impl std::convert::TryInto for #enum_ident { - type Error = String; - fn try_into(self) -> Result { - Ok(match self { + impl std::convert::From<#enum_ident> for crate::protobuf::#pb_enum{ + fn from(o: #enum_ident) -> crate::protobuf::#pb_enum { + match o { #(#build_to_pb_enum)* - }) + } } } }) diff --git a/shared-lib/flowy-derive/src/proto_buf/serialize.rs b/shared-lib/flowy-derive/src/proto_buf/serialize.rs index 324c26651b..a43f337f8f 100644 --- a/shared-lib/flowy-derive/src/proto_buf/serialize.rs +++ b/shared-lib/flowy-derive/src/proto_buf/serialize.rs @@ -19,18 +19,17 @@ pub fn make_se_token_stream(ctxt: &Ctxt, ast: &ASTContainer) -> Option Result { use protobuf::Message; - let pb: crate::protobuf::#pb_ty = self.try_into()?; + let pb: crate::protobuf::#pb_ty = self.into(); let bytes = pb.write_to_bytes()?; Ok(bytes::Bytes::from(bytes)) } } - impl std::convert::TryInto for #struct_ident { - type Error = ::protobuf::ProtobufError; - fn try_into(self) -> Result { + impl std::convert::From<#struct_ident> for crate::protobuf::#pb_ty { + fn from(mut o: #struct_ident) -> crate::protobuf::#pb_ty { let mut pb = crate::protobuf::#pb_ty::new(); #(#build_set_pb_fields)* - Ok(pb) + pb } } }; @@ -41,7 +40,7 @@ pub fn make_se_token_stream(ctxt: &Ctxt, ast: &ASTContainer) -> Option Option { if let Some(func) = &field.attrs.serialize_with() { let member = &field.member; - Some(quote! { pb.#member=self.#func(); }) + Some(quote! { pb.#member=o.#func(); }) } else if field.attrs.is_one_of() { token_stream_for_one_of(ctxt, field) } else { @@ -66,19 +65,19 @@ fn token_stream_for_one_of(ctxt: &Ctxt, field: &ASTField) -> Option match ident_category(bracketed_ty_info.unwrap().ident) { TypeCategory::Protobuf => Some(quote! { - match self.#member { - Some(s) => { pb.#set_func(s.try_into().unwrap()) } + match o.#member { + Some(s) => { pb.#set_func(s.into()) } None => {} } }), TypeCategory::Enum => Some(quote! { - match self.#member { - Some(s) => { pb.#set_func(s.try_into().unwrap()) } + match o.#member { + Some(s) => { pb.#set_func(s.into()) } None => {} } }), _ => Some(quote! { - match self.#member { + match o.#member { Some(ref s) => { pb.#set_func(s.clone()) } None => {} } @@ -100,18 +99,16 @@ fn gen_token_stream(ctxt: &Ctxt, member: &syn::Member, ty: &syn::Type, is_option TypeCategory::Str => { if is_option { Some(quote! { - match self.#member { + match o.#member { Some(ref s) => { pb.#member = s.to_string().clone(); } None => { pb.#member = String::new(); } } }) } else { - Some(quote! { pb.#member = self.#member.clone(); }) + Some(quote! { pb.#member = o.#member.clone(); }) } } - TypeCategory::Protobuf => { - Some(quote! { pb.#member = ::protobuf::SingularPtrField::some(self.#member.try_into().unwrap()); }) - } + TypeCategory::Protobuf => Some(quote! { pb.#member = ::protobuf::SingularPtrField::some(o.#member.into()); }), TypeCategory::Opt => gen_token_stream(ctxt, member, ty_info.bracket_ty_info.unwrap().ty, true), TypeCategory::Enum => { // let pb_enum_ident = format_ident!("{}", ty_info.ident.to_string()); @@ -119,10 +116,10 @@ fn gen_token_stream(ctxt: &Ctxt, member: &syn::Member, ty: &syn::Type, is_option // flowy_protobuf::#pb_enum_ident::from_i32(self.#member.value()).unwrap(); // }) Some(quote! { - pb.#member = self.#member.try_into().unwrap(); + pb.#member = o.#member.into(); }) } - _ => Some(quote! { pb.#member = self.#member; }), + _ => Some(quote! { pb.#member = o.#member; }), } } @@ -139,15 +136,15 @@ fn token_stream_for_vec(ctxt: &Ctxt, member: &syn::Member, ty: &syn::Type) -> Op match ident_category(ty_info.ident) { TypeCategory::Protobuf => Some(quote! { pb.#member = ::protobuf::RepeatedField::from_vec( - self.#member + o.#member .into_iter() - .map(|m| m.try_into().unwrap()) + .map(|m| m.into()) .collect()); }), - TypeCategory::Bytes => Some(quote! { pb.#member = self.#member.clone(); }), + TypeCategory::Bytes => Some(quote! { pb.#member = o.#member.clone(); }), _ => Some(quote! { - pb.#member = ::protobuf::RepeatedField::from_vec(self.#member.clone()); + pb.#member = ::protobuf::RepeatedField::from_vec(o.#member.clone()); }), } } @@ -166,14 +163,14 @@ fn token_stream_for_map(ctxt: &Ctxt, member: &syn::Member, ty: &syn::Type) -> Op match ident_category(ty_info.ident) { TypeCategory::Protobuf => Some(quote! { let mut m: std::collections::HashMap = std::collections::HashMap::new(); - self.#member.into_iter().for_each(|(k,v)| { - m.insert(k.clone(), v.try_into().unwrap()); + o.#member.into_iter().for_each(|(k,v)| { + m.insert(k.clone(), v.into()); }); pb.#member = m; }), _ => Some(quote! { let mut m: std::collections::HashMap = std::collections::HashMap::new(); - self.#member.iter().for_each(|(k,v)| { + o.#member.iter().for_each(|(k,v)| { m.insert(k.clone(), v.clone()); }); pb.#member = m; diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs index edceb25d9c..09056f827d 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs @@ -31,13 +31,8 @@ pub struct GridRevision { pub fields: Vec>, pub blocks: Vec>, - #[cfg(feature = "filter")] #[serde(default)] pub setting: GridSettingRevision, - - #[cfg(not(feature = "filter"))] - #[serde(default, skip)] - pub setting: GridSettingRevision, } impl GridRevision { @@ -53,7 +48,7 @@ impl GridRevision { pub fn from_build_context(grid_id: &str, context: BuildGridContext) -> Self { Self { grid_id: grid_id.to_owned(), - fields: context.field_revs.into_iter().map(Arc::new).collect(), + fields: context.field_revs, blocks: context.blocks.into_iter().map(Arc::new).collect(), setting: Default::default(), } @@ -128,6 +123,7 @@ pub struct FieldRevision { /// type_options contains key/value pairs /// key: id of the FieldType /// value: type option data that can be parsed into specified TypeOptionStruct. + /// /// For example, CheckboxTypeOption, MultiSelectTypeOption etc. #[serde(with = "indexmap::serde_seq")] pub type_options: IndexMap, @@ -190,15 +186,20 @@ impl FieldRevision { } } +/// The macro [impl_type_option] will implement the [TypeOptionDataEntry] for the type that +/// supports the serde trait and the TryInto trait. pub trait TypeOptionDataEntry { fn json_str(&self) -> String; fn protobuf_bytes(&self) -> Bytes; } +/// The macro [impl_type_option] will implement the [TypeOptionDataDeserializer] for the type that +/// supports the serde trait and the TryFrom trait. pub trait TypeOptionDataDeserializer { fn from_json_str(s: &str) -> Self; fn from_protobuf_bytes(bytes: Bytes) -> Self; } + pub type FieldId = String; #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct RowRevision { @@ -245,7 +246,7 @@ impl CellRevision { #[derive(Clone, Default, Deserialize, Serialize)] pub struct BuildGridContext { - pub field_revs: Vec, + pub field_revs: Vec>, pub blocks: Vec, pub blocks_meta_data: Vec, } diff --git a/shared-lib/flowy-grid-data-model/tests/serde_test.rs b/shared-lib/flowy-grid-data-model/tests/serde_test.rs index b544e10588..1dff913547 100644 --- a/shared-lib/flowy-grid-data-model/tests/serde_test.rs +++ b/shared-lib/flowy-grid-data-model/tests/serde_test.rs @@ -6,5 +6,8 @@ fn grid_default_serde_test() { let grid = GridRevision::new(&grid_id); let json = serde_json::to_string(&grid).unwrap(); - assert_eq!(json, r#"{"grid_id":"1","fields":[],"blocks":[]}"#) + assert_eq!( + json, + r#"{"grid_id":"1","fields":[],"blocks":[],"setting":{"layout":0,"filters":[]}}"# + ) } diff --git a/shared-lib/flowy-sync/src/client_document/default/mod.rs b/shared-lib/flowy-sync/src/client_document/default/mod.rs index f5fb180571..ee69e9efd9 100644 --- a/shared-lib/flowy-sync/src/client_document/default/mod.rs +++ b/shared-lib/flowy-sync/src/client_document/default/mod.rs @@ -7,13 +7,13 @@ pub fn initial_quill_delta() -> RichTextDelta { #[inline] pub fn initial_quill_delta_string() -> String { - initial_quill_delta().to_delta_str() + initial_quill_delta().json_str() } #[inline] pub fn initial_read_me() -> RichTextDelta { let json = include_str!("READ_ME.json"); - RichTextDelta::from_delta_str(json).unwrap() + RichTextDelta::from_json(json).unwrap() } #[cfg(test)] @@ -22,6 +22,6 @@ mod tests { #[test] fn load_read_me() { - println!("{}", initial_read_me().to_delta_str()); + println!("{}", initial_read_me().json_str()); } } diff --git a/shared-lib/flowy-sync/src/client_document/document_pad.rs b/shared-lib/flowy-sync/src/client_document/document_pad.rs index 72b52f6415..9a1037d0c8 100644 --- a/shared-lib/flowy-sync/src/client_document/document_pad.rs +++ b/shared-lib/flowy-sync/src/client_document/document_pad.rs @@ -55,16 +55,16 @@ impl ClientDocument { } pub fn from_json(json: &str) -> Result { - let delta = RichTextDelta::from_delta_str(json)?; + let delta = RichTextDelta::from_json(json)?; Ok(Self::from_delta(delta)) } pub fn delta_str(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } pub fn to_bytes(&self) -> Bytes { - self.delta.to_delta_bytes() + self.delta.json_bytes() } pub fn to_plain_string(&self) -> String { @@ -85,7 +85,7 @@ impl ClientDocument { } pub fn set_delta(&mut self, data: RichTextDelta) { - tracing::trace!("document: {}", data.to_delta_str()); + tracing::trace!("document: {}", data.json_str()); self.delta = data; match &self.notify { @@ -97,7 +97,7 @@ impl ClientDocument { } pub fn compose_delta(&mut self, delta: RichTextDelta) -> Result<(), CollaborateError> { - tracing::trace!("{} compose {}", &self.delta.to_delta_str(), delta.to_delta_str()); + tracing::trace!("{} compose {}", &self.delta.json_str(), delta.json_str()); let composed_delta = self.delta.compose(&delta)?; let mut undo_delta = delta.invert(&self.delta); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs b/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs index 4da15e5f06..6c7283e4c7 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/delete/preserve_line_format_merge.rs @@ -1,6 +1,6 @@ use crate::{client_document::DeleteExt, util::is_newline}; use lib_ot::{ - core::{Attributes, DeltaBuilder, DeltaIter, Interval, Utf16CodeUnitMetric, NEW_LINE}, + core::{Attributes, DeltaBuilder, DeltaIterator, Interval, Utf16CodeUnitMetric, NEW_LINE}, rich_text::{plain_attributes, RichTextDelta}, }; @@ -16,7 +16,7 @@ impl DeleteExt for PreserveLineFormatOnMerge { } // seek to the interval start pos. e.g. You backspace enter pos - let mut iter = DeltaIter::from_offset(delta, interval.start); + let mut iter = DeltaIterator::from_offset(delta, interval.start); // op will be the "\n" let newline_op = iter.next_op_with_len(1)?; diff --git a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs index 46a0e0290d..4a4c00a19d 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_block_format.rs @@ -1,5 +1,5 @@ use lib_ot::{ - core::{DeltaBuilder, DeltaIter, Interval}, + core::{DeltaBuilder, DeltaIterator, Interval}, rich_text::{plain_attributes, AttributeScope, RichTextAttribute, RichTextDelta}, }; @@ -20,7 +20,7 @@ impl FormatExt for ResolveBlockFormat { } let mut new_delta = DeltaBuilder::new().retain(interval.start).build(); - let mut iter = DeltaIter::from_offset(delta, interval.start); + let mut iter = DeltaIterator::from_offset(delta, interval.start); let mut start = 0; let end = interval.size(); while start < end && iter.has_next() { diff --git a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs index 383b780aca..567d06d6f2 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/format/resolve_inline_format.rs @@ -1,5 +1,5 @@ use lib_ot::{ - core::{DeltaBuilder, DeltaIter, Interval}, + core::{DeltaBuilder, DeltaIterator, Interval}, rich_text::{AttributeScope, RichTextAttribute, RichTextDelta}, }; @@ -19,7 +19,7 @@ impl FormatExt for ResolveInlineFormat { return None; } let mut new_delta = DeltaBuilder::new().retain(interval.start).build(); - let mut iter = DeltaIter::from_offset(delta, interval.start); + let mut iter = DeltaIterator::from_offset(delta, interval.start); let mut start = 0; let end = interval.size(); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs index e41470b9eb..253833d255 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_exit_block.rs @@ -1,8 +1,6 @@ use crate::{client_document::InsertExt, util::is_newline}; -use lib_ot::{ - core::{is_empty_line_at_index, DeltaBuilder, DeltaIter}, - rich_text::{attributes_except_header, RichTextAttributeKey, RichTextDelta}, -}; +use lib_ot::core::{is_empty_line_at_index, DeltaBuilder, DeltaIterator}; +use lib_ot::rich_text::{attributes_except_header, RichTextAttributeKey, RichTextDelta}; pub struct AutoExitBlock {} @@ -21,7 +19,7 @@ impl InsertExt for AutoExitBlock { return None; } - let mut iter = DeltaIter::from_offset(delta, index); + let mut iter = DeltaIterator::from_offset(delta, index); let next = iter.next_op()?; let mut attributes = next.get_attributes(); diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs index d7f1da4f89..4c7cd642c2 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/auto_format.rs @@ -1,6 +1,6 @@ use crate::{client_document::InsertExt, util::is_whitespace}; use lib_ot::{ - core::{count_utf16_code_units, DeltaBuilder, DeltaIter}, + core::{count_utf16_code_units, DeltaBuilder, DeltaIterator}, rich_text::{plain_attributes, RichTextAttribute, RichTextAttributes, RichTextDelta}, }; use std::cmp::min; @@ -17,7 +17,7 @@ impl InsertExt for AutoFormatExt { if !is_whitespace(text) { return None; } - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); if let Some(prev) = iter.next_op_with_len(index) { match AutoFormat::parse(prev.get_data()) { None => {} diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs index 628d55cb24..c165985493 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/default_insert.rs @@ -1,6 +1,6 @@ use crate::client_document::InsertExt; use lib_ot::{ - core::{Attributes, DeltaBuilder, DeltaIter, NEW_LINE}, + core::{Attributes, DeltaBuilder, DeltaIterator, NEW_LINE}, rich_text::{RichTextAttributeKey, RichTextAttributes, RichTextDelta}, }; @@ -11,7 +11,7 @@ impl InsertExt for DefaultInsertAttribute { } fn apply(&self, delta: &RichTextDelta, replace_len: usize, text: &str, index: usize) -> Option { - let iter = DeltaIter::new(delta); + let iter = DeltaIterator::new(delta); let mut attributes = RichTextAttributes::new(); // Enable each line split by "\n" remains the block attributes. for example: diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs index 2c0ceae111..03c9723d7b 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_block_format.rs @@ -1,6 +1,6 @@ use crate::{client_document::InsertExt, util::is_newline}; use lib_ot::{ - core::{DeltaBuilder, DeltaIter, NEW_LINE}, + core::{DeltaBuilder, DeltaIterator, NEW_LINE}, rich_text::{ attributes_except_header, plain_attributes, RichTextAttribute, RichTextAttributeKey, RichTextAttributes, RichTextDelta, @@ -18,7 +18,7 @@ impl InsertExt for PreserveBlockFormatOnInsert { return None; } - let mut iter = DeltaIter::from_offset(delta, index); + let mut iter = DeltaIterator::from_offset(delta, index); match iter.next_op_with_newline() { None => {} Some((newline_op, offset)) => { diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs index 37b31e3d26..b0de6451aa 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/preserve_inline_format.rs @@ -3,7 +3,7 @@ use crate::{ util::{contain_newline, is_newline}, }; use lib_ot::{ - core::{DeltaBuilder, DeltaIter, OpNewline, NEW_LINE}, + core::{DeltaBuilder, DeltaIterator, OpNewline, NEW_LINE}, rich_text::{plain_attributes, RichTextAttributeKey, RichTextDelta}, }; @@ -18,7 +18,7 @@ impl InsertExt for PreserveInlineFormat { return None; } - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); let prev = iter.next_op_with_len(index)?; if OpNewline::parse(&prev).is_contain() { return None; @@ -64,7 +64,7 @@ impl InsertExt for PreserveLineFormatOnSplit { return None; } - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); let prev = iter.next_op_with_len(index)?; if OpNewline::parse(&prev).is_end() { return None; diff --git a/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs b/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs index f63bf9bddf..8671049ee7 100644 --- a/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs +++ b/shared-lib/flowy-sync/src/client_document/extensions/insert/reset_format_on_new_line.rs @@ -1,6 +1,6 @@ use crate::{client_document::InsertExt, util::is_newline}; use lib_ot::{ - core::{DeltaBuilder, DeltaIter, Utf16CodeUnitMetric, NEW_LINE}, + core::{DeltaBuilder, DeltaIterator, Utf16CodeUnitMetric, NEW_LINE}, rich_text::{RichTextAttributeKey, RichTextAttributes, RichTextDelta}, }; @@ -15,7 +15,7 @@ impl InsertExt for ResetLineFormatOnNewLine { return None; } - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); iter.seek::(index); let next_op = iter.next_op()?; if !next_op.get_data().starts_with(NEW_LINE) { diff --git a/shared-lib/flowy-sync/src/client_folder/builder.rs b/shared-lib/flowy-sync/src/client_folder/builder.rs index 4c27f278f5..3855d62834 100644 --- a/shared-lib/flowy-sync/src/client_folder/builder.rs +++ b/shared-lib/flowy-sync/src/client_folder/builder.rs @@ -7,7 +7,7 @@ use crate::{ }; use flowy_folder_data_model::revision::{TrashRevision, WorkspaceRevision}; -use lib_ot::core::{PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder}; +use lib_ot::core::{PhantomAttributes, TextDelta, TextDeltaBuilder}; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -35,15 +35,15 @@ impl FolderPadBuilder { self } - pub(crate) fn build_with_delta(self, mut delta: PlainTextDelta) -> CollaborateResult { + pub(crate) fn build_with_delta(self, mut delta: TextDelta) -> CollaborateResult { if delta.is_empty() { delta = default_folder_delta(); } // TODO: Reconvert from history if delta.to_str() failed. - let folder_json = delta.to_str()?; - let mut folder: FolderPad = serde_json::from_str(&folder_json).map_err(|e| { - tracing::error!("Deserialize folder from json failed: {}", folder_json); + let content = delta.content()?; + let mut folder: FolderPad = serde_json::from_str(&content).map_err(|e| { + tracing::error!("Deserialize folder from {} failed", content); return CollaborateError::internal().context(format!("Deserialize delta to folder failed: {}", e)); })?; folder.delta = delta; @@ -51,7 +51,7 @@ impl FolderPadBuilder { } pub(crate) fn build_with_revisions(self, revisions: Vec) -> CollaborateResult { - let folder_delta: FolderDelta = make_delta_from_revisions::(revisions)?; + let folder_delta: FolderDelta = make_delta_from_revisions::(revisions)?; self.build_with_delta(folder_delta) } @@ -61,7 +61,7 @@ impl FolderPadBuilder { Ok(FolderPad { workspaces: self.workspaces, trash: self.trash, - delta: PlainTextDeltaBuilder::new().insert(&json).build(), + delta: TextDeltaBuilder::new().insert(&json).build(), }) } } diff --git a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs index f821c03e6f..5927be2c34 100644 --- a/shared-lib/flowy-sync/src/client_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/client_folder/folder_pad.rs @@ -295,7 +295,7 @@ impl FolderPad { } pub fn md5(&self) -> String { - md5(&self.delta.to_delta_bytes()) + md5(&self.delta.json_bytes()) } pub fn to_json(&self) -> CollaborateResult { @@ -315,7 +315,7 @@ impl FolderPad { Some(_) => { let old = cloned_self.to_json()?; let new = self.to_json()?; - match cal_diff::(old, new) { + match cal_diff::(old, new) { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; @@ -350,7 +350,7 @@ impl FolderPad { Some(_) => { let old = cloned_self.to_json()?; let new = self.to_json()?; - match cal_diff::(old, new) { + match cal_diff::(old, new) { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; @@ -400,14 +400,14 @@ impl FolderPad { } pub fn default_folder_delta() -> FolderDelta { - PlainTextDeltaBuilder::new() + TextDeltaBuilder::new() .insert(r#"{"workspaces":[],"trash":[]}"#) .build() } pub fn initial_folder_delta(folder_pad: &FolderPad) -> CollaborateResult { let json = folder_pad.to_json()?; - let delta = PlainTextDeltaBuilder::new().insert(&json).build(); + let delta = TextDeltaBuilder::new().insert(&json).build(); Ok(delta) } @@ -434,7 +434,7 @@ mod tests { use chrono::Utc; use flowy_folder_data_model::revision::{AppRevision, TrashRevision, ViewRevision, WorkspaceRevision}; - use lib_ot::core::{OperationTransformable, PlainTextDelta, PlainTextDeltaBuilder}; + use lib_ot::core::{OperationTransform, TextDelta, TextDeltaBuilder}; #[test] fn folder_add_workspace() { @@ -749,7 +749,7 @@ mod tests { fn test_folder() -> (FolderPad, FolderDelta, WorkspaceRevision) { let mut folder = FolderPad::default(); let folder_json = serde_json::to_string(&folder).unwrap(); - let mut delta = PlainTextDeltaBuilder::new().insert(&folder_json).build(); + let mut delta = TextDeltaBuilder::new().insert(&folder_json).build(); let mut workspace_rev = WorkspaceRevision::default(); workspace_rev.name = "😁 my first workspace".to_owned(); @@ -791,7 +791,7 @@ mod tests { fn test_trash() -> (FolderPad, FolderDelta, TrashRevision) { let mut folder = FolderPad::default(); let folder_json = serde_json::to_string(&folder).unwrap(); - let mut delta = PlainTextDeltaBuilder::new().insert(&folder_json).build(); + let mut delta = TextDeltaBuilder::new().insert(&folder_json).build(); let mut trash_rev = TrashRevision::default(); trash_rev.name = "🚽 my first trash".to_owned(); @@ -810,7 +810,7 @@ mod tests { (folder, delta, trash_rev) } - fn make_folder_from_delta(mut initial_delta: FolderDelta, deltas: Vec) -> FolderPad { + fn make_folder_from_delta(mut initial_delta: FolderDelta, deltas: Vec) -> FolderPad { for delta in deltas { initial_delta = initial_delta.compose(&delta).unwrap(); } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs index 8ee18309ad..51a331ecf7 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_block_revsion_pad.rs @@ -4,13 +4,13 @@ use crate::util::{cal_diff, make_delta_from_revisions}; use flowy_grid_data_model::revision::{ gen_block_id, gen_row_id, CellRevision, GridBlockRevision, RowMetaChangeset, RowRevision, }; -use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder}; +use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; -pub type GridBlockRevisionDelta = PlainTextDelta; -pub type GridBlockRevisionDeltaBuilder = PlainTextDeltaBuilder; +pub type GridBlockRevisionDelta = TextDelta; +pub type GridBlockRevisionDeltaBuilder = TextDeltaBuilder; #[derive(Debug, Clone)] pub struct GridBlockRevisionPad { @@ -46,7 +46,7 @@ impl GridBlockRevisionPad { } pub fn from_delta(delta: GridBlockRevisionDelta) -> CollaborateResult { - let s = delta.to_str()?; + let s = delta.content()?; let block_revision: GridBlockRevision = serde_json::from_str(&s).map_err(|e| { let msg = format!("Deserialize delta to block meta failed: {}", e); tracing::error!("{}", s); @@ -56,7 +56,7 @@ impl GridBlockRevisionPad { } pub fn from_revisions(_grid_id: &str, revisions: Vec) -> CollaborateResult { - let block_delta: GridBlockRevisionDelta = make_delta_from_revisions::(revisions)?; + let block_delta: GridBlockRevisionDelta = make_delta_from_revisions::(revisions)?; Self::from_delta(block_delta) } @@ -195,10 +195,10 @@ impl GridBlockRevisionPad { Some(_) => { let old = cloned_self.to_json()?; let new = self.to_json()?; - match cal_diff::(old, new) { + match cal_diff::(old, new) { None => Ok(None), Some(delta) => { - tracing::trace!("[GridBlockMeta] Composing delta {}", delta.to_delta_str()); + tracing::trace!("[GridBlockMeta] Composing delta {}", delta.json_str()); // tracing::debug!( // "[GridBlockMeta] current delta: {}", // self.delta.to_str().unwrap_or_else(|_| "".to_string()) @@ -231,11 +231,11 @@ impl GridBlockRevisionPad { } pub fn md5(&self) -> String { - md5(&self.delta.to_delta_bytes()) + md5(&self.delta.json_bytes()) } pub fn delta_str(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } } @@ -245,14 +245,14 @@ pub struct GridBlockMetaChange { pub md5: String, } -pub fn make_block_meta_delta(block_rev: &GridBlockRevision) -> GridBlockRevisionDelta { +pub fn make_grid_block_delta(block_rev: &GridBlockRevision) -> GridBlockRevisionDelta { let json = serde_json::to_string(&block_rev).unwrap(); - PlainTextDeltaBuilder::new().insert(&json).build() + TextDeltaBuilder::new().insert(&json).build() } -pub fn make_block_meta_revisions(user_id: &str, grid_block_meta_data: &GridBlockRevision) -> RepeatedRevision { - let delta = make_block_meta_delta(grid_block_meta_data); - let bytes = delta.to_delta_bytes(); +pub fn make_grid_block_revisions(user_id: &str, grid_block_meta_data: &GridBlockRevision) -> RepeatedRevision { + let delta = make_grid_block_delta(grid_block_meta_data); + let bytes = delta.json_bytes(); let revision = Revision::initial_revision(user_id, &grid_block_meta_data.block_id, bytes); revision.into() } @@ -264,7 +264,7 @@ impl std::default::Default for GridBlockRevisionPad { rows: vec![], }; - let delta = make_block_meta_delta(&block_revision); + let delta = make_grid_block_delta(&block_revision); GridBlockRevisionPad { block_revision, delta } } } @@ -289,7 +289,7 @@ mod tests { let change = pad.add_row_rev(row.clone(), None).unwrap().unwrap(); assert_eq!(pad.rows.first().unwrap().as_ref(), &row); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":24},{"insert":"{\"id\":\"1\",\"block_id\":\"1\",\"cells\":[],\"height\":0,\"visibility\":false}"},{"retain":2}]"# ); } @@ -303,19 +303,19 @@ mod tests { let change = pad.add_row_rev(row_1.clone(), None).unwrap().unwrap(); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":24},{"insert":"{\"id\":\"1\",\"block_id\":\"1\",\"cells\":[],\"height\":0,\"visibility\":false}"},{"retain":2}]"# ); let change = pad.add_row_rev(row_2.clone(), None).unwrap().unwrap(); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":90},{"insert":",{\"id\":\"2\",\"block_id\":\"1\",\"cells\":[],\"height\":0,\"visibility\":false}"},{"retain":2}]"# ); let change = pad.add_row_rev(row_3.clone(), Some("2".to_string())).unwrap().unwrap(); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":157},{"insert":",{\"id\":\"3\",\"block_id\":\"1\",\"cells\":[],\"height\":0,\"visibility\":false}"},{"retain":2}]"# ); @@ -380,10 +380,7 @@ mod tests { let _ = pad.add_row_rev(row.clone(), None).unwrap().unwrap(); let change = pad.delete_rows(vec![Cow::Borrowed(&row.id)]).unwrap().unwrap(); - assert_eq!( - change.delta.to_delta_str(), - r#"[{"retain":24},{"delete":66},{"retain":2}]"# - ); + assert_eq!(change.delta.json_str(), r#"[{"retain":24},{"delete":66},{"retain":2}]"#); assert_eq!(pad.delta_str(), pre_delta_str); } @@ -410,7 +407,7 @@ mod tests { let change = pad.update_row(changeset).unwrap().unwrap(); assert_eq!( - change.delta.to_delta_str(), + change.delta.json_str(), r#"[{"retain":69},{"insert":"10"},{"retain":15},{"insert":"tru"},{"delete":4},{"retain":4}]"# ); @@ -421,8 +418,7 @@ mod tests { } fn test_pad() -> GridBlockRevisionPad { - let delta = - GridBlockRevisionDelta::from_delta_str(r#"[{"insert":"{\"block_id\":\"1\",\"rows\":[]}"}]"#).unwrap(); + let delta = GridBlockRevisionDelta::from_json(r#"[{"insert":"{\"block_id\":\"1\",\"rows\":[]}"}]"#).unwrap(); GridBlockRevisionPad::from_delta(delta).unwrap() } } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_builder.rs b/shared-lib/flowy-sync/src/client_grid/grid_builder.rs index d1fbed3f53..ea5d5a9332 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_builder.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_builder.rs @@ -26,18 +26,31 @@ impl std::default::Default for GridBuilder { } impl GridBuilder { - pub fn add_field(mut self, field: FieldRevision) -> Self { - self.build_context.field_revs.push(field); - self + pub fn new() -> Self { + Self::default() + } + pub fn add_field(&mut self, field: FieldRevision) { + self.build_context.field_revs.push(Arc::new(field)); } - pub fn add_empty_row(mut self) -> Self { - let row = RowRevision::new(&self.build_context.blocks.first().unwrap().block_id); + pub fn add_row(&mut self, row_rev: RowRevision) { let block_meta_rev = self.build_context.blocks.first_mut().unwrap(); let block_rev = self.build_context.blocks_meta_data.first_mut().unwrap(); - block_rev.rows.push(Arc::new(row)); + block_rev.rows.push(Arc::new(row_rev)); block_meta_rev.row_count += 1; - self + } + + pub fn add_empty_row(&mut self) { + let row = RowRevision::new(self.block_id()); + self.add_row(row); + } + + pub fn field_revs(&self) -> &Vec> { + &self.build_context.field_revs + } + + pub fn block_id(&self) -> &str { + &self.build_context.blocks.first().unwrap().block_id } pub fn build(self) -> BuildGridContext { diff --git a/shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs new file mode 100644 index 0000000000..3ef9251e4a --- /dev/null +++ b/shared-lib/flowy-sync/src/client_grid/grid_meta_pad.rs @@ -0,0 +1,432 @@ +use crate::entities::revision::{md5, RepeatedRevision, Revision}; +use crate::errors::{internal_error, CollaborateError, CollaborateResult}; +use crate::util::{cal_diff, make_delta_from_revisions}; +use bytes::Bytes; +use flowy_grid_data_model::entities::{ + gen_block_id, gen_grid_id, FieldChangesetParams, FieldMeta, FieldOrder, FieldType, GridBlockInfoChangeset, + GridBlockMetaSnapshot, GridMeta, +}; +use lib_infra::util::move_vec_element; +use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder}; +use std::collections::HashMap; +use std::sync::Arc; + +pub type GridMetaDelta = PlainTextDelta; +pub type GridDeltaBuilder = PlainTextDeltaBuilder; + +pub struct GridMetaPad { + pub(crate) grid_meta: Arc, + pub(crate) delta: GridMetaDelta, +} + +pub trait JsonDeserializer { + fn deserialize(&self, type_option_data: Vec) -> CollaborateResult; +} + +impl GridMetaPad { + pub async fn duplicate_grid_meta(&self) -> (Vec, Vec) { + let fields = self.grid_meta.fields.to_vec(); + + let blocks = self + .grid_meta + .blocks + .iter() + .map(|block| { + let mut duplicated_block = block.clone(); + duplicated_block.block_id = gen_block_id(); + duplicated_block + }) + .collect::>(); + + (fields, blocks) + } + + pub fn from_delta(delta: GridMetaDelta) -> CollaborateResult { + let s = delta.to_str()?; + let grid: GridMeta = serde_json::from_str(&s) + .map_err(|e| CollaborateError::internal().context(format!("Deserialize delta to grid failed: {}", e)))?; + + Ok(Self { + grid_meta: Arc::new(grid), + delta, + }) + } + + pub fn from_revisions(_grid_id: &str, revisions: Vec) -> CollaborateResult { + let grid_delta: GridMetaDelta = make_delta_from_revisions::(revisions)?; + Self::from_delta(grid_delta) + } + + #[tracing::instrument(level = "debug", skip_all, err)] + pub fn create_field_meta( + &mut self, + new_field_meta: FieldMeta, + start_field_id: Option, + ) -> CollaborateResult> { + self.modify_grid(|grid_meta| { + // Check if the field exists or not + if grid_meta + .fields + .iter() + .any(|field_meta| field_meta.id == new_field_meta.id) + { + tracing::error!("Duplicate grid field"); + return Ok(None); + } + + let insert_index = match start_field_id { + None => None, + Some(start_field_id) => grid_meta.fields.iter().position(|field| field.id == start_field_id), + }; + + match insert_index { + None => grid_meta.fields.push(new_field_meta), + Some(index) => grid_meta.fields.insert(index, new_field_meta), + } + Ok(Some(())) + }) + } + + pub fn delete_field_meta(&mut self, field_id: &str) -> CollaborateResult> { + self.modify_grid( + |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) { + None => Ok(None), + Some(index) => { + grid_meta.fields.remove(index); + Ok(Some(())) + } + }, + ) + } + + pub fn duplicate_field_meta( + &mut self, + field_id: &str, + duplicated_field_id: &str, + ) -> CollaborateResult> { + self.modify_grid( + |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) { + None => Ok(None), + Some(index) => { + let mut duplicate_field_meta = grid_meta.fields[index].clone(); + duplicate_field_meta.id = duplicated_field_id.to_string(); + duplicate_field_meta.name = format!("{} (copy)", duplicate_field_meta.name); + grid_meta.fields.insert(index + 1, duplicate_field_meta); + Ok(Some(())) + } + }, + ) + } + + pub fn switch_to_field( + &mut self, + field_id: &str, + field_type: FieldType, + type_option_json_builder: B, + ) -> CollaborateResult> + where + B: FnOnce(&FieldType) -> String, + { + self.modify_grid(|grid_meta| { + // + match grid_meta.fields.iter_mut().find(|field_meta| field_meta.id == field_id) { + None => { + tracing::warn!("Can not find the field with id: {}", field_id); + Ok(None) + } + Some(field_meta) => { + if field_meta.get_type_option_str(&field_type).is_none() { + let type_option_json = type_option_json_builder(&field_type); + field_meta.insert_type_option_str(&field_type, type_option_json); + } + + field_meta.field_type = field_type; + Ok(Some(())) + } + } + }) + } + + pub fn update_field_meta( + &mut self, + changeset: FieldChangesetParams, + deserializer: T, + ) -> CollaborateResult> { + let field_id = changeset.field_id.clone(); + self.modify_field(&field_id, |field| { + let mut is_changed = None; + if let Some(name) = changeset.name { + field.name = name; + is_changed = Some(()) + } + + if let Some(desc) = changeset.desc { + field.desc = desc; + is_changed = Some(()) + } + + if let Some(field_type) = changeset.field_type { + field.field_type = field_type; + is_changed = Some(()) + } + + if let Some(frozen) = changeset.frozen { + field.frozen = frozen; + is_changed = Some(()) + } + + if let Some(visibility) = changeset.visibility { + field.visibility = visibility; + is_changed = Some(()) + } + + if let Some(width) = changeset.width { + field.width = width; + is_changed = Some(()) + } + + if let Some(type_option_data) = changeset.type_option_data { + match deserializer.deserialize(type_option_data) { + Ok(json_str) => { + let field_type = field.field_type.clone(); + field.insert_type_option_str(&field_type, json_str); + is_changed = Some(()) + } + Err(err) => { + tracing::error!("Deserialize data to type option json failed: {}", err); + } + } + } + + Ok(is_changed) + }) + } + + pub fn get_field_meta(&self, field_id: &str) -> Option<(usize, &FieldMeta)> { + self.grid_meta + .fields + .iter() + .enumerate() + .find(|(_, field)| field.id == field_id) + } + + pub fn replace_field_meta(&mut self, field_meta: FieldMeta) -> CollaborateResult> { + self.modify_grid( + |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_meta.id) { + None => Ok(None), + Some(index) => { + grid_meta.fields.remove(index); + grid_meta.fields.insert(index, field_meta); + Ok(Some(())) + } + }, + ) + } + + pub fn move_field( + &mut self, + field_id: &str, + from_index: usize, + to_index: usize, + ) -> CollaborateResult> { + self.modify_grid(|grid_meta| { + match move_vec_element( + &mut grid_meta.fields, + |field| field.id == field_id, + from_index, + to_index, + ) + .map_err(internal_error)? + { + true => Ok(Some(())), + false => Ok(None), + } + }) + } + + pub fn contain_field(&self, field_id: &str) -> bool { + self.grid_meta.fields.iter().any(|field| field.id == field_id) + } + + pub fn get_field_orders(&self) -> Vec { + self.grid_meta.fields.iter().map(FieldOrder::from).collect() + } + + pub fn get_field_metas(&self, field_orders: Option>) -> CollaborateResult> { + match field_orders { + None => Ok(self.grid_meta.fields.clone()), + Some(field_orders) => { + let field_by_field_id = self + .grid_meta + .fields + .iter() + .map(|field| (&field.id, field)) + .collect::>(); + + let fields = field_orders + .iter() + .flat_map(|field_order| match field_by_field_id.get(&field_order.field_id) { + None => { + tracing::error!("Can't find the field with id: {}", field_order.field_id); + None + } + Some(field) => Some((*field).clone()), + }) + .collect::>(); + Ok(fields) + } + } + } + + pub fn create_block_meta(&mut self, block: GridBlockMetaSnapshot) -> CollaborateResult> { + self.modify_grid(|grid_meta| { + if grid_meta.blocks.iter().any(|b| b.block_id == block.block_id) { + tracing::warn!("Duplicate grid block"); + Ok(None) + } else { + match grid_meta.blocks.last() { + None => grid_meta.blocks.push(block), + Some(last_block) => { + if last_block.start_row_index > block.start_row_index + && last_block.len() > block.start_row_index + { + let msg = "GridBlock's start_row_index should be greater than the last_block's start_row_index and its len".to_string(); + return Err(CollaborateError::internal().context(msg)) + } + grid_meta.blocks.push(block); + } + } + Ok(Some(())) + } + }) + } + + pub fn get_block_metas(&self) -> Vec { + self.grid_meta.blocks.clone() + } + + pub fn update_block_meta(&mut self, changeset: GridBlockInfoChangeset) -> CollaborateResult> { + let block_id = changeset.block_id.clone(); + self.modify_block(&block_id, |block| { + let mut is_changed = None; + + if let Some(row_count) = changeset.row_count { + block.row_count = row_count; + is_changed = Some(()); + } + + if let Some(start_row_index) = changeset.start_row_index { + block.start_row_index = start_row_index; + is_changed = Some(()); + } + + Ok(is_changed) + }) + } + + pub fn md5(&self) -> String { + md5(&self.delta.to_delta_bytes()) + } + + pub fn delta_str(&self) -> String { + self.delta.to_delta_str() + } + + pub fn delta_bytes(&self) -> Bytes { + self.delta.to_delta_bytes() + } + + pub fn fields(&self) -> &[FieldMeta] { + &self.grid_meta.fields + } + + fn modify_grid(&mut self, f: F) -> CollaborateResult> + where + F: FnOnce(&mut GridMeta) -> CollaborateResult>, + { + let cloned_grid = self.grid_meta.clone(); + match f(Arc::make_mut(&mut self.grid_meta))? { + None => Ok(None), + Some(_) => { + let old = json_from_grid(&cloned_grid)?; + let new = json_from_grid(&self.grid_meta)?; + match cal_diff::(old, new) { + None => Ok(None), + Some(delta) => { + self.delta = self.delta.compose(&delta)?; + Ok(Some(GridChangeset { delta, md5: self.md5() })) + } + } + } + } + } + + pub fn modify_block(&mut self, block_id: &str, f: F) -> CollaborateResult> + where + F: FnOnce(&mut GridBlockMetaSnapshot) -> CollaborateResult>, + { + self.modify_grid( + |grid_meta| match grid_meta.blocks.iter().position(|block| block.block_id == block_id) { + None => { + tracing::warn!("[GridMetaPad]: Can't find any block with id: {}", block_id); + Ok(None) + } + Some(index) => f(&mut grid_meta.blocks[index]), + }, + ) + } + + pub fn modify_field(&mut self, field_id: &str, f: F) -> CollaborateResult> + where + F: FnOnce(&mut FieldMeta) -> CollaborateResult>, + { + self.modify_grid( + |grid_meta| match grid_meta.fields.iter().position(|field| field.id == field_id) { + None => { + tracing::warn!("[GridMetaPad]: Can't find any field with id: {}", field_id); + Ok(None) + } + Some(index) => f(&mut grid_meta.fields[index]), + }, + ) + } +} + +fn json_from_grid(grid: &Arc) -> CollaborateResult { + let json = serde_json::to_string(grid) + .map_err(|err| internal_error(format!("Serialize grid to json str failed. {:?}", err)))?; + Ok(json) +} + +pub struct GridChangeset { + pub delta: GridMetaDelta, + /// md5: the md5 of the grid after applying the change. + pub md5: String, +} + +pub fn make_grid_delta(grid_meta: &GridMeta) -> GridMetaDelta { + let json = serde_json::to_string(&grid_meta).unwrap(); + PlainTextDeltaBuilder::new().insert(&json).build() +} + +pub fn make_grid_revisions(user_id: &str, grid_meta: &GridMeta) -> RepeatedRevision { + let delta = make_grid_delta(grid_meta); + let bytes = delta.to_delta_bytes(); + let revision = Revision::initial_revision(user_id, &grid_meta.grid_id, bytes); + revision.into() +} + +impl std::default::Default for GridMetaPad { + fn default() -> Self { + let grid = GridMeta { + grid_id: gen_grid_id(), + fields: vec![], + blocks: vec![], + }; + let delta = make_grid_delta(&grid); + GridMetaPad { + grid_meta: Arc::new(grid), + delta, + } + } +} diff --git a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs index e47305f7fa..b0188066ef 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_revision_pad.rs @@ -9,12 +9,12 @@ use flowy_grid_data_model::revision::{ GridLayoutRevision, GridRevision, GridSettingRevision, GridSortRevision, }; use lib_infra::util::move_vec_element; -use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta, PlainTextDeltaBuilder}; +use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta, TextDeltaBuilder}; use std::collections::HashMap; use std::sync::Arc; -pub type GridRevisionDelta = PlainTextDelta; -pub type GridRevisionDeltaBuilder = PlainTextDeltaBuilder; +pub type GridRevisionDelta = TextDelta; +pub type GridRevisionDeltaBuilder = TextDeltaBuilder; pub struct GridRevisionPad { grid_rev: Arc, @@ -52,8 +52,8 @@ impl GridRevisionPad { } pub fn from_delta(delta: GridRevisionDelta) -> CollaborateResult { - let s = delta.to_str()?; - let grid: GridRevision = serde_json::from_str(&s) + let content = delta.content()?; + let grid: GridRevision = serde_json::from_str(&content) .map_err(|e| CollaborateError::internal().context(format!("Deserialize delta to grid failed: {}", e)))?; Ok(Self { @@ -62,8 +62,8 @@ impl GridRevisionPad { }) } - pub fn from_revisions(_grid_id: &str, revisions: Vec) -> CollaborateResult { - let grid_delta: GridRevisionDelta = make_delta_from_revisions::(revisions)?; + pub fn from_revisions(revisions: Vec) -> CollaborateResult { + let grid_delta: GridRevisionDelta = make_delta_from_revisions::(revisions)?; Self::from_delta(grid_delta) } @@ -457,15 +457,15 @@ impl GridRevisionPad { } pub fn md5(&self) -> String { - md5(&self.delta.to_delta_bytes()) + md5(&self.delta.json_bytes()) } pub fn delta_str(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } pub fn delta_bytes(&self) -> Bytes { - self.delta.to_delta_bytes() + self.delta.json_bytes() } pub fn fields(&self) -> &[Arc] { @@ -480,9 +480,9 @@ impl GridRevisionPad { match f(Arc::make_mut(&mut self.grid_rev))? { None => Ok(None), Some(_) => { - let old = json_from_grid(&cloned_grid)?; - let new = json_from_grid(&self.grid_rev)?; - match cal_diff::(old, new) { + let old = make_grid_rev_json_str(&cloned_grid)?; + let new = self.json_str()?; + match cal_diff::(old, new) { None => Ok(None), Some(delta) => { self.delta = self.delta.compose(&delta)?; @@ -528,9 +528,13 @@ impl GridRevisionPad { }, ) } + + pub fn json_str(&self) -> CollaborateResult { + make_grid_rev_json_str(&self.grid_rev) + } } -fn json_from_grid(grid: &Arc) -> CollaborateResult { +pub fn make_grid_rev_json_str(grid: &GridRevision) -> CollaborateResult { let json = serde_json::to_string(grid) .map_err(|err| internal_error(format!("Serialize grid to json str failed. {:?}", err)))?; Ok(json) @@ -544,12 +548,12 @@ pub struct GridChangeset { pub fn make_grid_delta(grid_rev: &GridRevision) -> GridRevisionDelta { let json = serde_json::to_string(&grid_rev).unwrap(); - PlainTextDeltaBuilder::new().insert(&json).build() + TextDeltaBuilder::new().insert(&json).build() } pub fn make_grid_revisions(user_id: &str, grid_rev: &GridRevision) -> RepeatedRevision { let delta = make_grid_delta(grid_rev); - let bytes = delta.to_delta_bytes(); + let bytes = delta.json_bytes(); let revision = Revision::initial_revision(user_id, &grid_rev.grid_id, bytes); revision.into() } diff --git a/shared-lib/flowy-sync/src/entities/folder.rs b/shared-lib/flowy-sync/src/entities/folder.rs index 214190f517..952827d7d6 100644 --- a/shared-lib/flowy-sync/src/entities/folder.rs +++ b/shared-lib/flowy-sync/src/entities/folder.rs @@ -1,7 +1,7 @@ use flowy_derive::ProtoBuf; -use lib_ot::core::PlainTextDelta; +use lib_ot::core::TextDelta; -pub type FolderDelta = PlainTextDelta; +pub type FolderDelta = TextDelta; #[derive(ProtoBuf, Default, Debug, Clone, Eq, PartialEq)] pub struct FolderInfo { diff --git a/shared-lib/flowy-sync/src/entities/revision.rs b/shared-lib/flowy-sync/src/entities/revision.rs index 57276bba1b..55567a43bf 100644 --- a/shared-lib/flowy-sync/src/entities/revision.rs +++ b/shared-lib/flowy-sync/src/entities/revision.rs @@ -89,7 +89,7 @@ impl std::fmt::Debug for Revision { let _ = f.write_fmt(format_args!("rev_id {}, ", self.rev_id))?; match RichTextDelta::from_bytes(&self.delta_data) { Ok(delta) => { - let _ = f.write_fmt(format_args!("delta {:?}", delta.to_delta_str()))?; + let _ = f.write_fmt(format_args!("delta {:?}", delta.json_str()))?; } Err(e) => { let _ = f.write_fmt(format_args!("delta {:?}", e))?; @@ -125,6 +125,12 @@ impl std::convert::From for RepeatedRevision { } } +impl std::convert::From> for RepeatedRevision { + fn from(revisions: Vec) -> Self { + Self { items: revisions } + } +} + impl RepeatedRevision { pub fn new(mut items: Vec) -> Self { items.sort_by(|a, b| a.rev_id.cmp(&b.rev_id)); diff --git a/shared-lib/flowy-sync/src/entities/text_block.rs b/shared-lib/flowy-sync/src/entities/text_block.rs index c325fdad07..7cb8c6df63 100644 --- a/shared-lib/flowy-sync/src/entities/text_block.rs +++ b/shared-lib/flowy-sync/src/entities/text_block.rs @@ -15,7 +15,7 @@ pub struct CreateTextBlockParams { } #[derive(ProtoBuf, Default, Debug, Clone, Eq, PartialEq)] -pub struct TextBlockInfo { +pub struct DocumentPB { #[pb(index = 1)] pub block_id: String, @@ -29,14 +29,14 @@ pub struct TextBlockInfo { pub base_rev_id: i64, } -impl TextBlockInfo { +impl DocumentPB { pub fn delta(&self) -> Result { let delta = RichTextDelta::from_bytes(&self.text)?; Ok(delta) } } -impl std::convert::TryFrom for TextBlockInfo { +impl std::convert::TryFrom for DocumentPB { type Error = CollaborateError; fn try_from(revision: Revision) -> Result { @@ -46,9 +46,9 @@ impl std::convert::TryFrom for TextBlockInfo { } let delta = RichTextDelta::from_bytes(&revision.delta_data)?; - let doc_json = delta.to_delta_str(); + let doc_json = delta.json_str(); - Ok(TextBlockInfo { + Ok(DocumentPB { block_id: revision.object_id, text: doc_json, rev_id: revision.rev_id, @@ -67,7 +67,7 @@ pub struct ResetTextBlockParams { } #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct TextBlockDelta { +pub struct TextBlockDeltaPB { #[pb(index = 1)] pub block_id: String, @@ -76,7 +76,7 @@ pub struct TextBlockDelta { } #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct NewDocUser { +pub struct NewDocUserPB { #[pb(index = 1)] pub user_id: String, @@ -88,30 +88,30 @@ pub struct NewDocUser { } #[derive(ProtoBuf, Default, Debug, Clone)] -pub struct TextBlockId { +pub struct TextBlockIdPB { #[pb(index = 1)] pub value: String, } -impl AsRef for TextBlockId { +impl AsRef for TextBlockIdPB { fn as_ref(&self) -> &str { &self.value } } -impl std::convert::From for TextBlockId { +impl std::convert::From for TextBlockIdPB { fn from(value: String) -> Self { - TextBlockId { value } + TextBlockIdPB { value } } } -impl std::convert::From for String { - fn from(block_id: TextBlockId) -> Self { +impl std::convert::From for String { + fn from(block_id: TextBlockIdPB) -> Self { block_id.value } } -impl std::convert::From<&String> for TextBlockId { +impl std::convert::From<&String> for TextBlockIdPB { fn from(s: &String) -> Self { - TextBlockId { value: s.to_owned() } + TextBlockIdPB { value: s.to_owned() } } } diff --git a/shared-lib/flowy-sync/src/server_document/document_manager.rs b/shared-lib/flowy-sync/src/server_document/document_manager.rs index 8d547a9484..59d3dd3c97 100644 --- a/shared-lib/flowy-sync/src/server_document/document_manager.rs +++ b/shared-lib/flowy-sync/src/server_document/document_manager.rs @@ -1,7 +1,8 @@ +use crate::entities::revision::{RepeatedRevision, Revision}; use crate::{ - entities::{text_block::TextBlockInfo, ws_data::ServerRevisionWSDataBuilder}, + entities::{text_block::DocumentPB, ws_data::ServerRevisionWSDataBuilder}, errors::{internal_error, CollaborateError, CollaborateResult}, - protobuf::{ClientRevisionWSData, RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB}, + protobuf::ClientRevisionWSData, server_document::document_pad::ServerDocument, synchronizer::{RevisionSyncPersistence, RevisionSyncResponse, RevisionSynchronizer, RevisionUser}, util::rev_id_from_str, @@ -18,27 +19,26 @@ use tokio::{ }; pub trait TextBlockCloudPersistence: Send + Sync + Debug { - fn read_text_block(&self, doc_id: &str) -> BoxResultFuture; + fn read_text_block(&self, doc_id: &str) -> BoxResultFuture; fn create_text_block( &self, doc_id: &str, - repeated_revision: RepeatedRevisionPB, - ) -> BoxResultFuture, CollaborateError>; + repeated_revision: RepeatedRevision, + ) -> BoxResultFuture, CollaborateError>; fn read_text_block_revisions( &self, doc_id: &str, rev_ids: Option>, - ) -> BoxResultFuture, CollaborateError>; + ) -> BoxResultFuture, CollaborateError>; - fn save_text_block_revisions(&self, repeated_revision: RepeatedRevisionPB) - -> BoxResultFuture<(), CollaborateError>; + fn save_text_block_revisions(&self, repeated_revision: RepeatedRevision) -> BoxResultFuture<(), CollaborateError>; fn reset_text_block( &self, doc_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> BoxResultFuture<(), CollaborateError>; } @@ -47,18 +47,18 @@ impl RevisionSyncPersistence for Arc { &self, object_id: &str, rev_ids: Option>, - ) -> BoxResultFuture, CollaborateError> { + ) -> BoxResultFuture, CollaborateError> { (**self).read_text_block_revisions(object_id, rev_ids) } - fn save_revisions(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError> { + fn save_revisions(&self, repeated_revision: RepeatedRevision) -> BoxResultFuture<(), CollaborateError> { (**self).save_text_block_revisions(repeated_revision) } fn reset_object( &self, object_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> BoxResultFuture<(), CollaborateError> { (**self).reset_text_block(object_id, repeated_revision) } @@ -82,7 +82,7 @@ impl ServerDocumentManager { user: Arc, mut client_data: ClientRevisionWSData, ) -> Result<(), CollaborateError> { - let repeated_revision = client_data.take_revisions(); + let repeated_revision: RepeatedRevision = client_data.take_revisions().into(); let cloned_user = user.clone(); let ack_id = rev_id_from_str(&client_data.data_id)?; let object_id = client_data.object_id; @@ -131,9 +131,10 @@ impl ServerDocumentManager { pub async fn handle_document_reset( &self, doc_id: &str, - mut repeated_revision: RepeatedRevisionPB, + mut repeated_revision: RepeatedRevision, ) -> Result<(), CollaborateError> { - repeated_revision.mut_items().sort_by(|a, b| a.rev_id.cmp(&b.rev_id)); + repeated_revision.sort_by(|a, b| a.rev_id.cmp(&b.rev_id)); + match self.get_document_handler(doc_id).await { None => { tracing::warn!("Document:{} doesn't exist, ignore document reset", doc_id); @@ -166,7 +167,7 @@ impl ServerDocumentManager { async fn create_document( &self, doc_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> Result, CollaborateError> { match self.persistence.create_text_block(doc_id, repeated_revision).await? { None => Err(CollaborateError::internal().context("Create document info from revisions failed")), @@ -182,7 +183,7 @@ impl ServerDocumentManager { } #[tracing::instrument(level = "debug", skip(self, doc), err)] - async fn create_document_handler(&self, doc: TextBlockInfo) -> Result, CollaborateError> { + async fn create_document_handler(&self, doc: DocumentPB) -> Result, CollaborateError> { let persistence = self.persistence.clone(); let handle = spawn_blocking(|| OpenDocumentHandler::new(doc, persistence)) .await @@ -206,7 +207,7 @@ struct OpenDocumentHandler { } impl OpenDocumentHandler { - fn new(doc: TextBlockInfo, persistence: Arc) -> Result { + fn new(doc: DocumentPB, persistence: Arc) -> Result { let doc_id = doc.block_id.clone(); let (sender, receiver) = mpsc::channel(1000); let users = DashMap::new(); @@ -229,7 +230,7 @@ impl OpenDocumentHandler { async fn apply_revisions( &self, user: Arc, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> Result<(), CollaborateError> { let (ret, rx) = oneshot::channel(); self.users.insert(user.user_id(), user.clone()); @@ -252,7 +253,7 @@ impl OpenDocumentHandler { } #[tracing::instrument(level = "debug", skip(self, repeated_revision), err)] - async fn apply_document_reset(&self, repeated_revision: RepeatedRevisionPB) -> Result<(), CollaborateError> { + async fn apply_document_reset(&self, repeated_revision: RepeatedRevision) -> Result<(), CollaborateError> { let (ret, rx) = oneshot::channel(); let msg = DocumentCommand::Reset { repeated_revision, ret }; let result = self.send(msg, rx).await?; @@ -279,7 +280,7 @@ impl std::ops::Drop for OpenDocumentHandler { enum DocumentCommand { ApplyRevisions { user: Arc, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ret: oneshot::Sender>, }, Ping { @@ -288,7 +289,7 @@ enum DocumentCommand { ret: oneshot::Sender>, }, Reset { - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ret: oneshot::Sender>, }, } diff --git a/shared-lib/flowy-sync/src/server_document/document_pad.rs b/shared-lib/flowy-sync/src/server_document/document_pad.rs index 1f4fa7bda1..28623a0169 100644 --- a/shared-lib/flowy-sync/src/server_document/document_pad.rs +++ b/shared-lib/flowy-sync/src/server_document/document_pad.rs @@ -39,7 +39,7 @@ impl RevisionSyncObject for ServerDocument { } fn to_json(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } fn set_delta(&mut self, new_delta: Delta) { diff --git a/shared-lib/flowy-sync/src/server_folder/folder_manager.rs b/shared-lib/flowy-sync/src/server_folder/folder_manager.rs index 0a1a5e117a..95e3c2330f 100644 --- a/shared-lib/flowy-sync/src/server_folder/folder_manager.rs +++ b/shared-lib/flowy-sync/src/server_folder/folder_manager.rs @@ -1,10 +1,11 @@ +use crate::entities::revision::{RepeatedRevision, Revision}; use crate::{ entities::{ folder::{FolderDelta, FolderInfo}, ws_data::ServerRevisionWSDataBuilder, }, errors::{internal_error, CollaborateError, CollaborateResult}, - protobuf::{ClientRevisionWSData, RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB}, + protobuf::ClientRevisionWSData, server_folder::folder_pad::ServerFolder, synchronizer::{RevisionSyncPersistence, RevisionSyncResponse, RevisionSynchronizer, RevisionUser}, util::rev_id_from_str, @@ -12,7 +13,7 @@ use crate::{ use async_stream::stream; use futures::stream::StreamExt; use lib_infra::future::BoxResultFuture; -use lib_ot::core::PlainTextAttributes; +use lib_ot::core::PhantomAttributes; use std::{collections::HashMap, fmt::Debug, sync::Arc}; use tokio::{ sync::{mpsc, oneshot, RwLock}, @@ -26,21 +27,21 @@ pub trait FolderCloudPersistence: Send + Sync + Debug { &self, user_id: &str, folder_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> BoxResultFuture, CollaborateError>; - fn save_folder_revisions(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError>; + fn save_folder_revisions(&self, repeated_revision: RepeatedRevision) -> BoxResultFuture<(), CollaborateError>; fn read_folder_revisions( &self, folder_id: &str, rev_ids: Option>, - ) -> BoxResultFuture, CollaborateError>; + ) -> BoxResultFuture, CollaborateError>; fn reset_folder( &self, folder_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> BoxResultFuture<(), CollaborateError>; } @@ -49,18 +50,18 @@ impl RevisionSyncPersistence for Arc { &self, object_id: &str, rev_ids: Option>, - ) -> BoxResultFuture, CollaborateError> { + ) -> BoxResultFuture, CollaborateError> { (**self).read_folder_revisions(object_id, rev_ids) } - fn save_revisions(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError> { + fn save_revisions(&self, repeated_revision: RepeatedRevision) -> BoxResultFuture<(), CollaborateError> { (**self).save_folder_revisions(repeated_revision) } fn reset_object( &self, object_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> BoxResultFuture<(), CollaborateError> { (**self).reset_folder(object_id, repeated_revision) } @@ -84,7 +85,7 @@ impl ServerFolderManager { user: Arc, mut client_data: ClientRevisionWSData, ) -> Result<(), CollaborateError> { - let repeated_revision = client_data.take_revisions(); + let repeated_revision: RepeatedRevision = client_data.take_revisions().into(); let cloned_user = user.clone(); let ack_id = rev_id_from_str(&client_data.data_id)?; let folder_id = client_data.object_id; @@ -167,7 +168,7 @@ impl ServerFolderManager { &self, user_id: &str, folder_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> Result, CollaborateError> { match self .persistence @@ -187,7 +188,7 @@ impl ServerFolderManager { } } -type FolderRevisionSynchronizer = RevisionSynchronizer; +type FolderRevisionSynchronizer = RevisionSynchronizer; struct OpenFolderHandler { folder_id: String, @@ -221,7 +222,7 @@ impl OpenFolderHandler { async fn apply_revisions( &self, user: Arc, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> CollaborateResult<()> { let (ret, rx) = oneshot::channel(); let msg = FolderCommand::ApplyRevisions { @@ -258,7 +259,7 @@ impl std::ops::Drop for OpenFolderHandler { enum FolderCommand { ApplyRevisions { user: Arc, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ret: oneshot::Sender>, }, Ping { diff --git a/shared-lib/flowy-sync/src/server_folder/folder_pad.rs b/shared-lib/flowy-sync/src/server_folder/folder_pad.rs index 09b4c9d048..74e164600c 100644 --- a/shared-lib/flowy-sync/src/server_folder/folder_pad.rs +++ b/shared-lib/flowy-sync/src/server_folder/folder_pad.rs @@ -1,5 +1,5 @@ use crate::{entities::folder::FolderDelta, errors::CollaborateError, synchronizer::RevisionSyncObject}; -use lib_ot::core::{OperationTransformable, PlainTextAttributes, PlainTextDelta}; +use lib_ot::core::{OperationTransform, PhantomAttributes, TextDelta}; pub struct ServerFolder { folder_id: String, @@ -15,27 +15,27 @@ impl ServerFolder { } } -impl RevisionSyncObject for ServerFolder { +impl RevisionSyncObject for ServerFolder { fn id(&self) -> &str { &self.folder_id } - fn compose(&mut self, other: &PlainTextDelta) -> Result<(), CollaborateError> { + fn compose(&mut self, other: &TextDelta) -> Result<(), CollaborateError> { let new_delta = self.delta.compose(other)?; self.delta = new_delta; Ok(()) } - fn transform(&self, other: &PlainTextDelta) -> Result<(PlainTextDelta, PlainTextDelta), CollaborateError> { + fn transform(&self, other: &TextDelta) -> Result<(TextDelta, TextDelta), CollaborateError> { let value = self.delta.transform(other)?; Ok(value) } fn to_json(&self) -> String { - self.delta.to_delta_str() + self.delta.json_str() } - fn set_delta(&mut self, new_delta: PlainTextDelta) { + fn set_delta(&mut self, new_delta: TextDelta) { self.delta = new_delta; } } diff --git a/shared-lib/flowy-sync/src/synchronizer.rs b/shared-lib/flowy-sync/src/synchronizer.rs index 1305c471b2..eca422296a 100644 --- a/shared-lib/flowy-sync/src/synchronizer.rs +++ b/shared-lib/flowy-sync/src/synchronizer.rs @@ -1,10 +1,11 @@ +use crate::entities::revision::{RepeatedRevision, Revision}; use crate::{ entities::{ revision::RevisionRange, ws_data::{ServerRevisionWSData, ServerRevisionWSDataBuilder}, }, errors::CollaborateError, - protobuf::{RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB}, + protobuf::Revision as RevisionPB, util::*, }; use lib_infra::future::BoxResultFuture; @@ -31,14 +32,14 @@ pub trait RevisionSyncPersistence: Send + Sync + 'static { &self, object_id: &str, rev_ids: Option>, - ) -> BoxResultFuture, CollaborateError>; + ) -> BoxResultFuture, CollaborateError>; - fn save_revisions(&self, repeated_revision: RepeatedRevisionPB) -> BoxResultFuture<(), CollaborateError>; + fn save_revisions(&self, repeated_revision: RepeatedRevision) -> BoxResultFuture<(), CollaborateError>; fn reset_object( &self, object_id: &str, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> BoxResultFuture<(), CollaborateError>; } @@ -87,20 +88,20 @@ where pub async fn sync_revisions( &self, user: Arc, - repeated_revision: RepeatedRevisionPB, + repeated_revision: RepeatedRevision, ) -> Result<(), CollaborateError> { let object_id = self.object_id.clone(); - if repeated_revision.get_items().is_empty() { + if repeated_revision.is_empty() { // Return all the revisions to client let revisions = self.persistence.read_revisions(&object_id, None).await?; - let repeated_revision = repeated_revision_from_revision_pbs(revisions)?; + let repeated_revision = RepeatedRevision::from(revisions); let data = ServerRevisionWSDataBuilder::build_push_message(&object_id, repeated_revision); user.receive(RevisionSyncResponse::Push(data)); return Ok(()); } let server_base_rev_id = self.rev_id.load(SeqCst); - let first_revision = repeated_revision.get_items().first().unwrap().clone(); + let first_revision = repeated_revision.first().unwrap().clone(); if self.is_applied_before(&first_revision, &self.persistence).await { // Server has received this revision before, so ignore the following revisions return Ok(()); @@ -111,7 +112,7 @@ where let server_rev_id = next(server_base_rev_id); if server_base_rev_id == first_revision.base_rev_id || server_rev_id == first_revision.rev_id { // The rev is in the right order, just compose it. - for revision in repeated_revision.get_items() { + for revision in repeated_revision.iter() { let _ = self.compose_revision(revision)?; } let _ = self.persistence.save_revisions(repeated_revision).await?; @@ -165,10 +166,10 @@ where } #[tracing::instrument(level = "debug", skip(self, repeated_revision), fields(object_id), err)] - pub async fn reset(&self, repeated_revision: RepeatedRevisionPB) -> Result<(), CollaborateError> { + pub async fn reset(&self, repeated_revision: RepeatedRevision) -> Result<(), CollaborateError> { let object_id = self.object_id.clone(); tracing::Span::current().record("object_id", &object_id.as_str()); - let revisions: Vec = repeated_revision.get_items().to_vec(); + let revisions: Vec = repeated_revision.clone().into_inner(); let (_, rev_id) = pair_rev_id_from_revision_pbs(&revisions); let delta = make_delta_from_revision_pb(revisions)?; let _ = self.persistence.reset_object(&object_id, repeated_revision).await?; @@ -181,7 +182,7 @@ where self.object.read().to_json() } - fn compose_revision(&self, revision: &RevisionPB) -> Result<(), CollaborateError> { + fn compose_revision(&self, revision: &Revision) -> Result<(), CollaborateError> { let delta = Delta::::from_bytes(&revision.delta_data)?; let _ = self.compose_delta(delta)?; let _ = self.rev_id.fetch_update(SeqCst, SeqCst, |_e| Some(revision.rev_id)); @@ -213,11 +214,7 @@ where self.rev_id.load(SeqCst) } - async fn is_applied_before( - &self, - new_revision: &RevisionPB, - persistence: &Arc, - ) -> bool { + async fn is_applied_before(&self, new_revision: &Revision, persistence: &Arc) -> bool { let rev_ids = Some(vec![new_revision.rev_id]); if let Ok(revisions) = persistence.read_revisions(&self.object_id, rev_ids).await { if let Some(revision) = revisions.first() { @@ -243,13 +240,10 @@ where tracing::trace!("{}: can not read the revisions in range {:?}", self.object_id, rev_ids); // assert_eq!(revisions.is_empty(), rev_ids.is_empty(),); } - match repeated_revision_from_revision_pbs(revisions) { - Ok(repeated_revision) => { - let data = ServerRevisionWSDataBuilder::build_push_message(&self.object_id, repeated_revision); - user.receive(RevisionSyncResponse::Push(data)); - } - Err(e) => tracing::error!("{}", e), - } + + let repeated_revision = RepeatedRevision::from(revisions); + let data = ServerRevisionWSDataBuilder::build_push_message(&self.object_id, repeated_revision); + user.receive(RevisionSyncResponse::Push(data)); } Err(e) => { tracing::error!("{}", e); diff --git a/shared-lib/flowy-sync/src/util.rs b/shared-lib/flowy-sync/src/util.rs index 3fc09c5932..7dd5c4af5c 100644 --- a/shared-lib/flowy-sync/src/util.rs +++ b/shared-lib/flowy-sync/src/util.rs @@ -2,25 +2,18 @@ use crate::{ entities::{ folder::{FolderDelta, FolderInfo}, revision::{RepeatedRevision, Revision}, - text_block::TextBlockInfo, + text_block::DocumentPB, }, errors::{CollaborateError, CollaborateResult}, - protobuf::{ - FolderInfo as FolderInfoPB, RepeatedRevision as RepeatedRevisionPB, Revision as RevisionPB, - TextBlockInfo as TextBlockInfoPB, - }, }; use dissimilar::Chunk; -use lib_ot::core::{DeltaBuilder, FlowyStr}; +use lib_ot::core::{DeltaBuilder, OTString}; use lib_ot::{ - core::{Attributes, Delta, OperationTransformable, NEW_LINE, WHITESPACE}, + core::{Attributes, Delta, OperationTransform, NEW_LINE, WHITESPACE}, rich_text::RichTextDelta, }; use serde::de::DeserializeOwned; -use std::{ - convert::TryInto, - sync::atomic::{AtomicI64, Ordering::SeqCst}, -}; +use std::sync::atomic::{AtomicI64, Ordering::SeqCst}; #[inline] pub fn find_newline(s: &str) -> Option { @@ -88,7 +81,7 @@ where Ok(delta) } -pub fn make_delta_from_revision_pb(revisions: Vec) -> CollaborateResult> +pub fn make_delta_from_revision_pb(revisions: Vec) -> CollaborateResult> where T: Attributes + DeserializeOwned, { @@ -103,26 +96,7 @@ where Ok(new_delta) } -pub fn repeated_revision_from_revision_pbs(revisions: Vec) -> CollaborateResult { - let repeated_revision_pb = repeated_revision_pb_from_revisions(revisions); - repeated_revision_from_repeated_revision_pb(repeated_revision_pb) -} - -pub fn repeated_revision_pb_from_revisions(revisions: Vec) -> RepeatedRevisionPB { - let mut repeated_revision_pb = RepeatedRevisionPB::new(); - repeated_revision_pb.set_items(revisions.into()); - repeated_revision_pb -} - -pub fn repeated_revision_from_repeated_revision_pb( - repeated_revision: RepeatedRevisionPB, -) -> CollaborateResult { - repeated_revision - .try_into() - .map_err(|e| CollaborateError::internal().context(format!("Cast repeated revision failed: {:?}", e))) -} - -pub fn pair_rev_id_from_revision_pbs(revisions: &[RevisionPB]) -> (i64, i64) { +pub fn pair_rev_id_from_revision_pbs(revisions: &[Revision]) -> (i64, i64) { let mut rev_id = 0; revisions.iter().for_each(|revision| { if rev_id < revision.rev_id { @@ -155,23 +129,9 @@ pub fn pair_rev_id_from_revisions(revisions: &[Revision]) -> (i64, i64) { #[inline] pub fn make_folder_from_revisions_pb( folder_id: &str, - revisions: RepeatedRevisionPB, + revisions: RepeatedRevision, ) -> Result, CollaborateError> { - match make_folder_pb_from_revisions_pb(folder_id, revisions)? { - None => Ok(None), - Some(pb) => { - let folder_info: FolderInfo = pb.try_into().map_err(|e| CollaborateError::internal().context(e))?; - Ok(Some(folder_info)) - } - } -} - -#[inline] -pub fn make_folder_pb_from_revisions_pb( - folder_id: &str, - mut revisions: RepeatedRevisionPB, -) -> Result, CollaborateError> { - let revisions = revisions.take_items(); + let revisions = revisions.into_inner(); if revisions.is_empty() { return Ok(None); } @@ -189,42 +149,26 @@ pub fn make_folder_pb_from_revisions_pb( folder_delta = folder_delta.compose(&delta)?; } - let text = folder_delta.to_delta_str(); - let mut folder_info = FolderInfoPB::new(); - folder_info.set_folder_id(folder_id.to_owned()); - folder_info.set_text(text); - folder_info.set_base_rev_id(base_rev_id); - folder_info.set_rev_id(rev_id); - Ok(Some(folder_info)) + let text = folder_delta.json_str(); + Ok(Some(FolderInfo { + folder_id: folder_id.to_string(), + text, + rev_id, + base_rev_id, + })) } #[inline] -pub fn make_document_info_from_revisions_pb( +pub fn make_document_from_revision_pbs( doc_id: &str, - revisions: RepeatedRevisionPB, -) -> Result, CollaborateError> { - match make_document_info_pb_from_revisions_pb(doc_id, revisions)? { - None => Ok(None), - Some(pb) => { - let document_info: TextBlockInfo = pb.try_into().map_err(|e| { - CollaborateError::internal().context(format!("Deserialize document info from pb failed: {}", e)) - })?; - Ok(Some(document_info)) - } - } -} - -#[inline] -pub fn make_document_info_pb_from_revisions_pb( - doc_id: &str, - mut revisions: RepeatedRevisionPB, -) -> Result, CollaborateError> { - let revisions = revisions.take_items(); + revisions: RepeatedRevision, +) -> Result, CollaborateError> { + let revisions = revisions.into_inner(); if revisions.is_empty() { return Ok(None); } - let mut document_delta = RichTextDelta::new(); + let mut delta = RichTextDelta::new(); let mut base_rev_id = 0; let mut rev_id = 0; for revision in revisions { @@ -235,17 +179,18 @@ pub fn make_document_info_pb_from_revisions_pb( tracing::warn!("revision delta_data is empty"); } - let delta = RichTextDelta::from_bytes(revision.delta_data)?; - document_delta = document_delta.compose(&delta)?; + let new_delta = RichTextDelta::from_bytes(revision.delta_data)?; + delta = delta.compose(&new_delta)?; } - let text = document_delta.to_delta_str(); - let mut block_info = TextBlockInfoPB::new(); - block_info.set_block_id(doc_id.to_owned()); - block_info.set_text(text); - block_info.set_base_rev_id(base_rev_id); - block_info.set_rev_id(rev_id); - Ok(Some(block_info)) + let text = delta.json_str(); + + Ok(Some(DocumentPB { + block_id: doc_id.to_owned(), + text, + rev_id, + base_rev_id, + })) } #[inline] @@ -263,10 +208,10 @@ pub fn cal_diff(old: String, new: String) -> Option> { for chunk in &chunks { match chunk { Chunk::Equal(s) => { - delta_builder = delta_builder.retain(FlowyStr::from(*s).utf16_size()); + delta_builder = delta_builder.retain(OTString::from(*s).utf16_len()); } Chunk::Delete(s) => { - delta_builder = delta_builder.delete(FlowyStr::from(*s).utf16_size()); + delta_builder = delta_builder.delete(OTString::from(*s).utf16_len()); } Chunk::Insert(s) => { delta_builder = delta_builder.insert(*s); diff --git a/shared-lib/lib-ot/src/codec/markdown/mod.rs b/shared-lib/lib-ot/src/codec/markdown/mod.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/shared-lib/lib-ot/src/codec/mod.rs b/shared-lib/lib-ot/src/codec/mod.rs new file mode 100644 index 0000000000..163a4fba82 --- /dev/null +++ b/shared-lib/lib-ot/src/codec/mod.rs @@ -0,0 +1 @@ +pub mod markdown; diff --git a/shared-lib/lib-ot/src/core/delta/builder.rs b/shared-lib/lib-ot/src/core/delta/builder.rs index b065b645af..482b1ca8a9 100644 --- a/shared-lib/lib-ot/src/core/delta/builder.rs +++ b/shared-lib/lib-ot/src/core/delta/builder.rs @@ -1,7 +1,21 @@ -use crate::core::{trim, Attributes, Delta, PlainTextAttributes}; - -pub type PlainTextDeltaBuilder = DeltaBuilder; +use crate::core::delta::{trim, Delta}; +use crate::core::operation::Attributes; +use crate::core::Operation; +/// A builder for creating new [Delta] objects. +/// +/// Note that all edit operations must be sorted; the start point of each +/// interval must be no less than the end point of the previous one. +/// +/// # Examples +/// +/// ``` +/// use lib_ot::core::TextDeltaBuilder; +/// let delta = TextDeltaBuilder::new() +/// .insert("AppFlowy") +/// .build(); +/// assert_eq!(delta.content().unwrap(), "AppFlowy"); +/// ``` pub struct DeltaBuilder { delta: Delta, } @@ -23,6 +37,26 @@ where DeltaBuilder::default() } + pub fn from_operations(operations: Vec>) -> Delta { + let mut delta = DeltaBuilder::default().build(); + operations.into_iter().for_each(|operation| { + delta.add(operation); + }); + delta + } + + /// Retain the 'n' characters with the attributes. Use 'retain' instead if you don't + /// need any attributes. + /// # Examples + /// + /// ``` + /// use lib_ot::rich_text::{RichTextAttribute, RichTextDelta, RichTextDeltaBuilder}; + /// + /// let mut attribute = RichTextAttribute::Bold(true); + /// let delta = RichTextDeltaBuilder::new().retain_with_attributes(7, attribute.into()).build(); + /// + /// assert_eq!(delta.json_str(), r#"[{"retain":7,"attributes":{"bold":true}}]"#); + /// ``` pub fn retain_with_attributes(mut self, n: usize, attrs: T) -> Self { self.delta.retain(n, attrs); self @@ -33,11 +67,32 @@ where self } + /// Deletes the given interval. Panics if interval is not properly sorted. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OperationTransform, TextDeltaBuilder}; + /// + /// let delta = TextDeltaBuilder::new() + /// .insert("AppFlowy...") + /// .build(); + /// + /// let changeset = TextDeltaBuilder::new() + /// .retain(8) + /// .delete(3) + /// .build(); + /// + /// let new_delta = delta.compose(&changeset).unwrap(); + /// assert_eq!(new_delta.content().unwrap(), "AppFlowy"); + /// ``` pub fn delete(mut self, n: usize) -> Self { self.delta.delete(n); self } + /// Inserts the string with attributes. Use 'insert' instead if you don't + /// need any attributes. pub fn insert_with_attributes(mut self, s: &str, attrs: T) -> Self { self.delta.insert(s, attrs); self @@ -48,11 +103,31 @@ where self } + /// Removes trailing retain operation with empty attributes + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OperationTransform, TextDeltaBuilder}; + /// use lib_ot::rich_text::{RichTextAttribute, RichTextDeltaBuilder}; + /// let delta = TextDeltaBuilder::new() + /// .retain(3) + /// .trim() + /// .build(); + /// assert_eq!(delta.ops.len(), 0); + /// + /// let delta = RichTextDeltaBuilder::new() + /// .retain_with_attributes(3, RichTextAttribute::Bold(true).into()) + /// .trim() + /// .build(); + /// assert_eq!(delta.ops.len(), 1); + /// ``` pub fn trim(mut self) -> Self { trim(&mut self.delta); self } + /// Builds the `Delta` pub fn build(self) -> Delta { self.delta } diff --git a/shared-lib/lib-ot/src/core/delta/cursor.rs b/shared-lib/lib-ot/src/core/delta/cursor.rs index d6ebb8b3cc..a068f1c029 100644 --- a/shared-lib/lib-ot/src/core/delta/cursor.rs +++ b/shared-lib/lib-ot/src/core/delta/cursor.rs @@ -1,33 +1,54 @@ #![allow(clippy::while_let_on_iterator)] -use crate::{ - core::{Attributes, Delta, Interval, Operation}, - errors::{ErrorBuilder, OTError, OTErrorCode}, -}; +use crate::core::delta::Delta; +use crate::core::interval::Interval; +use crate::core::operation::{Attributes, Operation}; +use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; use std::{cmp::min, iter::Enumerate, slice::Iter}; +/// A [DeltaCursor] is used to iterate the delta and return the corresponding delta. #[derive(Debug)] -pub struct OpCursor<'a, T: Attributes> { +pub struct DeltaCursor<'a, T: Attributes> { pub(crate) delta: &'a Delta, pub(crate) origin_iv: Interval, pub(crate) consume_iv: Interval, pub(crate) consume_count: usize, - pub(crate) op_index: usize, + pub(crate) op_offset: usize, iter: Enumerate>>, next_op: Option>, } -impl<'a, T> OpCursor<'a, T> +impl<'a, T> DeltaCursor<'a, T> where T: Attributes, { - pub fn new(delta: &'a Delta, interval: Interval) -> OpCursor<'a, T> { + /// # Arguments + /// + /// * `delta`: The delta you want to iterate over. + /// * `interval`: The range for the cursor movement. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{DeltaCursor, DeltaIterator, Interval, Operation}; + /// use lib_ot::rich_text::RichTextDelta; + /// let mut delta = RichTextDelta::default(); + /// delta.add(Operation::insert("123")); + /// delta.add(Operation::insert("4")); + /// + /// let mut cursor = DeltaCursor::new(&delta, Interval::new(0, 3)); + /// assert_eq!(cursor.next_iv(), Interval::new(0,3)); + /// assert_eq!(cursor.next_with_len(Some(2)).unwrap(), Operation::insert("12")); + /// assert_eq!(cursor.get_next_op().unwrap(), Operation::insert("3")); + /// assert_eq!(cursor.get_next_op(), None); + /// ``` + pub fn new(delta: &'a Delta, interval: Interval) -> DeltaCursor<'a, T> { // debug_assert!(interval.start <= delta.target_len); let mut cursor = Self { delta, origin_iv: interval, consume_iv: interval, consume_count: 0, - op_index: 0, + op_offset: 0, iter: delta.ops.iter().enumerate(), next_op: None, }; @@ -35,17 +56,37 @@ where cursor } - // get the next operation interval + /// Returns the next operation interval pub fn next_iv(&self) -> Interval { self.next_iv_with_len(None).unwrap_or_else(|| Interval::new(0, 0)) } - pub fn next_op(&mut self) -> Option> { + /// Returns the next operation + pub fn get_next_op(&mut self) -> Option> { self.next_with_len(None) } - // get the last operation before the end. - // checkout the delta_next_op_with_len_cross_op_return_last test for more detail + /// Returns the reference of the next operation + pub fn next_op(&self) -> Option<&Operation> { + let mut next_op = self.next_op.as_ref(); + if next_op.is_none() { + let mut offset = 0; + for op in &self.delta.ops { + offset += op.len(); + if offset > self.consume_count { + next_op = Some(op); + break; + } + } + } + next_op + } + + /// # Arguments + /// + /// * `expected_len`: Return the next operation with the specified length. + /// + /// pub fn next_with_len(&mut self, expected_len: Option) -> Option> { let mut find_op = None; let holder = self.next_op.clone(); @@ -97,17 +138,24 @@ where } pub fn has_next(&self) -> bool { - self.next_iter_op().is_some() + self.next_op().is_some() } - fn descend(&mut self, index: usize) { - self.consume_iv.start += index; + /// Finds the op within the current offset. + /// This function sets the start of the consume_iv to the offset, updates the consume_count + /// and the next_op reference. + /// + /// # Arguments + /// + /// * `offset`: Represents the offset of the delta string, in Utf16CodeUnit unit. + fn descend(&mut self, offset: usize) { + self.consume_iv.start += offset; if self.consume_count >= self.consume_iv.start { return; } while let Some((o_index, op)) = self.iter.next() { - self.op_index = o_index; + self.op_offset = o_index; let start = self.consume_count; let end = start + op.len(); let intersect = Interval::new(start, end).intersect(self.consume_iv); @@ -121,7 +169,7 @@ where } fn next_iv_with_len(&self, expected_len: Option) -> Option { - let op = self.next_iter_op()?; + let op = self.next_op()?; let start = self.consume_count; let end = match expected_len { None => self.consume_count + op.len(), @@ -132,31 +180,16 @@ where let interval = intersect.translate_neg(start); Some(interval) } - - pub fn next_iter_op(&self) -> Option<&Operation> { - let mut next_op = self.next_op.as_ref(); - if next_op.is_none() { - let mut offset = 0; - for op in &self.delta.ops { - offset += op.len(); - if offset > self.consume_count { - next_op = Some(op); - break; - } - } - } - next_op - } } -fn find_next<'a, T>(cursor: &mut OpCursor<'a, T>) -> Option<&'a Operation> +fn find_next<'a, T>(cursor: &mut DeltaCursor<'a, T>) -> Option<&'a Operation> where T: Attributes, { match cursor.iter.next() { None => None, Some((o_index, op)) => { - cursor.op_index = o_index; + cursor.op_offset = o_index; Some(op) } } @@ -164,31 +197,34 @@ where type SeekResult = Result<(), OTError>; pub trait Metric { - fn seek(cursor: &mut OpCursor, offset: usize) -> SeekResult; + fn seek(cursor: &mut DeltaCursor, offset: usize) -> SeekResult; } +/// [OpMetric] is used by [DeltaIterator] for seeking operations +/// The unit of the movement is Operation pub struct OpMetric(); impl Metric for OpMetric { - fn seek(cursor: &mut OpCursor, offset: usize) -> SeekResult { - let _ = check_bound(cursor.op_index, offset)?; - let mut seek_cursor = OpCursor::new(cursor.delta, cursor.origin_iv); - let mut cur_offset = 0; + fn seek(cursor: &mut DeltaCursor, op_offset: usize) -> SeekResult { + let _ = check_bound(cursor.op_offset, op_offset)?; + let mut seek_cursor = DeltaCursor::new(cursor.delta, cursor.origin_iv); + while let Some((_, op)) = seek_cursor.iter.next() { - cur_offset += op.len(); - if cur_offset > offset { + cursor.descend(op.len()); + if cursor.op_offset >= op_offset { break; } } - cursor.descend(cur_offset); Ok(()) } } +/// [Utf16CodeUnitMetric] is used by [DeltaIterator] for seeking operations. +/// The unit of the movement is Utf16CodeUnit pub struct Utf16CodeUnitMetric(); impl Metric for Utf16CodeUnitMetric { - fn seek(cursor: &mut OpCursor, offset: usize) -> SeekResult { + fn seek(cursor: &mut DeltaCursor, offset: usize) -> SeekResult { if offset > 0 { let _ = check_bound(cursor.consume_count, offset)?; let _ = cursor.next_with_len(Some(offset)); diff --git a/shared-lib/lib-ot/src/core/delta/delta.rs b/shared-lib/lib-ot/src/core/delta/delta.rs index 29fd424dcb..b422205a90 100644 --- a/shared-lib/lib-ot/src/core/delta/delta.rs +++ b/shared-lib/lib-ot/src/core/delta/delta.rs @@ -1,8 +1,10 @@ -use crate::{ - core::{operation::*, DeltaIter, FlowyStr, Interval, OperationTransformable, MAX_IV_LEN}, - errors::{ErrorBuilder, OTError, OTErrorCode}, -}; +use crate::errors::{ErrorBuilder, OTError, OTErrorCode}; +use crate::core::delta::{DeltaIterator, MAX_IV_LEN}; +use crate::core::interval::Interval; +use crate::core::operation::{Attributes, Operation, OperationTransform, PhantomAttributes}; +use crate::core::ot_str::OTString; +use crate::core::DeltaBuilder; use bytes::Bytes; use serde::de::DeserializeOwned; use std::{ @@ -13,13 +15,28 @@ use std::{ str::FromStr, }; -pub type PlainTextDelta = Delta; +pub type TextDelta = Delta; +pub type TextDeltaBuilder = DeltaBuilder; -// TODO: optimize the memory usage with Arc::make_mut or Cow +/// A [Delta] contains list of operations that consists of 'Retain', 'Delete' and 'Insert' operation. +/// Check out the [Operation] for more details. It describes the document as a sequence of +/// operations. +/// +/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/delta) out for more information. +/// +/// If the [T] supports 'serde', that will enable delta to serialize to JSON or deserialize from +/// a JSON string. +/// #[derive(Clone, Debug, PartialEq, Eq)] pub struct Delta { pub ops: Vec>, + + /// 'Delete' and 'Retain' operation will update the [utf16_base_len] + /// Transforming the other delta, it requires the utf16_base_len must be equal. pub utf16_base_len: usize, + + /// Represents the current len of the delta. + /// 'Insert' and 'Retain' operation will update the [utf16_target_len] pub utf16_target_len: usize, } @@ -81,6 +98,7 @@ where } } + /// Adding an operation. It will be added in sequence. pub fn add(&mut self, op: Operation) { match op { Operation::Delete(i) => self.delete(i), @@ -89,6 +107,7 @@ where } } + /// Creating a [Delete] operation with len [n] pub fn delete(&mut self, n: usize) { if n == 0 { return; @@ -97,17 +116,18 @@ where if let Some(Operation::Delete(n_last)) = self.ops.last_mut() { *n_last += n; } else { - self.ops.push(OpBuilder::delete(n).build()); + self.ops.push(Operation::delete(n)); } } + /// Creating a [Insert] operation with string, [s]. pub fn insert(&mut self, s: &str, attributes: T) { - let s: FlowyStr = s.into(); + let s: OTString = s.into(); if s.is_empty() { return; } - self.utf16_target_len += s.utf16_size(); + self.utf16_target_len += s.utf16_len(); let new_last = match self.ops.as_mut_slice() { [.., Operation::::Insert(insert)] => { // @@ -119,10 +139,10 @@ where } [.., op_last @ Operation::::Delete(_)] => { let new_last = op_last.clone(); - *op_last = OpBuilder::::insert(&s).attributes(attributes).build(); + *op_last = Operation::::insert_with_attributes(&s, attributes); Some(new_last) } - _ => Some(OpBuilder::::insert(&s).attributes(attributes).build()), + _ => Some(Operation::::insert_with_attributes(&s, attributes)), }; match new_last { @@ -131,6 +151,7 @@ where } } + /// Creating a [Retain] operation with len, [n]. pub fn retain(&mut self, n: usize, attributes: T) { if n == 0 { return; @@ -143,24 +164,47 @@ where self.ops.push(new_op); } } else { - self.ops.push(OpBuilder::::retain(n).attributes(attributes).build()); + self.ops.push(Operation::::retain_with_attributes(n, attributes)); } } - /// Applies an operation to a string, returning a new string. - pub fn apply(&self, s: &str) -> Result { - let s: FlowyStr = s.into(); - if s.utf16_size() != self.utf16_base_len { + /// Return the a new string described by this delta. The new string will contains the input string. + /// The length of the [applied_str] must be equal to the the [utf16_base_len]. + /// + /// # Arguments + /// + /// * `applied_str`: A string represents the utf16_base_len content. it will be consumed by the [retain] + /// or [delete] operations. + /// + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::TextDeltaBuilder; + /// let s = "hello"; + /// let delta_a = TextDeltaBuilder::new().insert(s).build(); + /// let delta_b = TextDeltaBuilder::new() + /// .retain(s.len()) + /// .insert(", AppFlowy") + /// .build(); + /// + /// let after_a = delta_a.content().unwrap(); + /// let after_b = delta_b.apply(&after_a).unwrap(); + /// assert_eq!("hello, AppFlowy", &after_b); + /// ``` + pub fn apply(&self, applied_str: &str) -> Result { + let applied_str: OTString = applied_str.into(); + if applied_str.utf16_len() != self.utf16_base_len { return Err(ErrorBuilder::new(OTErrorCode::IncompatibleLength) .msg(format!( - "Expected: {}, received: {}", + "Expected: {}, but received: {}", self.utf16_base_len, - s.utf16_size() + applied_str.utf16_len() )) .build()); } let mut new_s = String::new(); - let code_point_iter = &mut s.utf16_code_unit_iter(); + let code_point_iter = &mut applied_str.utf16_iter(); for op in &self.ops { match &op { Operation::Retain(retain) => { @@ -181,34 +225,60 @@ where Ok(new_s) } - /// Computes the inverse of an operation. The inverse of an operation is the - /// operation that reverts the effects of the operation - pub fn invert_str(&self, s: &str) -> Self { + /// Computes the inverse [Delta]. The inverse of an operation is the + /// operation that reverts the effects of the operation + /// # Arguments + /// + /// * `inverted_s`: A string represents the utf16_base_len content. The len of [inverted_s] + /// must equal to the [utf16_base_len], it will be consumed by the [retain] or [delete] operations. + /// + /// If the delta's operations just contain a insert operation. The inverted_s must be empty string. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::TextDeltaBuilder; + /// let s = "hello world"; + /// let delta = TextDeltaBuilder::new().insert(s).build(); + /// let invert_delta = delta.invert_str(s); + /// assert_eq!(delta.utf16_base_len, invert_delta.utf16_target_len); + /// assert_eq!(delta.utf16_target_len, invert_delta.utf16_base_len); + /// + /// assert_eq!(invert_delta.apply(s).unwrap(), "") + /// + /// ``` + /// + pub fn invert_str(&self, inverted_s: &str) -> Self { let mut inverted = Delta::default(); - let chars = &mut s.chars(); + let inverted_s: OTString = inverted_s.into(); + let code_point_iter = &mut inverted_s.utf16_iter(); + for op in &self.ops { match &op { Operation::Retain(retain) => { inverted.retain(retain.n, T::default()); - // TODO: use advance_by instead, but it's unstable now - // chars.advance_by(retain.num) for _ in 0..retain.n { - chars.next(); + code_point_iter.next(); } } Operation::Insert(insert) => { inverted.delete(insert.utf16_size()); } Operation::Delete(delete) => { - inverted.insert(&chars.take(*delete as usize).collect::(), op.get_attributes()); + let bytes = code_point_iter + .take(*delete as usize) + .into_iter() + .flat_map(|a| str::from_utf8(a.0).ok()) + .collect::(); + + inverted.insert(&bytes, op.get_attributes()); } } } inverted } - /// Checks if this operation has no effect. - #[inline] + /// Return true if the delta doesn't contain any [Insert] or [Delete] operations. pub fn is_noop(&self) -> bool { matches!(self.ops.as_slice(), [] | [Operation::Retain(_)]) } @@ -222,7 +292,7 @@ where } } -impl OperationTransformable for Delta +impl OperationTransform for Delta where T: Attributes, { @@ -231,8 +301,8 @@ where Self: Sized, { let mut new_delta = Delta::default(); - let mut iter = DeltaIter::new(self); - let mut other_iter = DeltaIter::new(other); + let mut iter = DeltaIterator::new(self); + let mut other_iter = DeltaIterator::new(other); while iter.has_next() || other_iter.has_next() { if other_iter.is_next_insert() { @@ -252,10 +322,10 @@ where let op = iter .next_op_with_len(length) - .unwrap_or_else(|| OpBuilder::retain(length).build()); + .unwrap_or_else(|| Operation::retain(length)); let other_op = other_iter .next_op_with_len(length) - .unwrap_or_else(|| OpBuilder::retain(length).build()); + .unwrap_or_else(|| Operation::retain(length)); // debug_assert_eq!(op.len(), other_op.len(), "Composing delta failed,"); @@ -263,12 +333,12 @@ where (Operation::Retain(retain), Operation::Retain(other_retain)) => { let composed_attrs = retain.attributes.compose(&other_retain.attributes)?; - new_delta.add(OpBuilder::retain(retain.n).attributes(composed_attrs).build()) + new_delta.add(Operation::retain_with_attributes(retain.n, composed_attrs)) } (Operation::Insert(insert), Operation::Retain(other_retain)) => { let mut composed_attrs = insert.attributes.compose(&other_retain.attributes)?; composed_attrs.remove_empty(); - new_delta.add(OpBuilder::insert(op.get_data()).attributes(composed_attrs).build()) + new_delta.add(Operation::insert_with_attributes(op.get_data(), composed_attrs)) } (Operation::Retain(_), Operation::Delete(_)) => { new_delta.add(other_op); @@ -331,7 +401,7 @@ where Ordering::Less => { a_prime.retain(retain.n, composed_attrs.clone()); b_prime.retain(retain.n, composed_attrs.clone()); - next_op2 = Some(OpBuilder::retain(o_retain.n - retain.n).build()); + next_op2 = Some(Operation::retain(o_retain.n - retain.n)); next_op1 = ops1.next(); } Ordering::Equal => { @@ -343,14 +413,14 @@ where Ordering::Greater => { a_prime.retain(o_retain.n, composed_attrs.clone()); b_prime.retain(o_retain.n, composed_attrs.clone()); - next_op1 = Some(OpBuilder::retain(retain.n - o_retain.n).build()); + next_op1 = Some(Operation::retain(retain.n - o_retain.n)); next_op2 = ops2.next(); } }; } (Some(Operation::Delete(i)), Some(Operation::Delete(j))) => match i.cmp(j) { Ordering::Less => { - next_op2 = Some(OpBuilder::delete(*j - *i).build()); + next_op2 = Some(Operation::delete(*j - *i)); next_op1 = ops1.next(); } Ordering::Equal => { @@ -358,7 +428,7 @@ where next_op2 = ops2.next(); } Ordering::Greater => { - next_op1 = Some(OpBuilder::delete(*i - *j).build()); + next_op1 = Some(Operation::delete(*i - *j)); next_op2 = ops2.next(); } }, @@ -366,7 +436,7 @@ where match i.cmp(o_retain) { Ordering::Less => { a_prime.delete(*i); - next_op2 = Some(OpBuilder::retain(o_retain.n - *i).build()); + next_op2 = Some(Operation::retain(o_retain.n - *i)); next_op1 = ops1.next(); } Ordering::Equal => { @@ -376,7 +446,7 @@ where } Ordering::Greater => { a_prime.delete(o_retain.n); - next_op1 = Some(OpBuilder::delete(*i - o_retain.n).build()); + next_op1 = Some(Operation::delete(*i - o_retain.n)); next_op2 = ops2.next(); } }; @@ -385,7 +455,7 @@ where match retain.cmp(j) { Ordering::Less => { b_prime.delete(retain.n); - next_op2 = Some(OpBuilder::delete(*j - retain.n).build()); + next_op2 = Some(Operation::delete(*j - retain.n)); next_op1 = ops1.next(); } Ordering::Equal => { @@ -395,7 +465,7 @@ where } Ordering::Greater => { b_prime.delete(*j); - next_op1 = Some(OpBuilder::retain(retain.n - *j).build()); + next_op1 = Some(Operation::retain(retain.n - *j)); next_op2 = ops2.next(); } }; @@ -407,21 +477,17 @@ where fn invert(&self, other: &Self) -> Self { let mut inverted = Delta::default(); - if other.is_empty() { - return inverted; - } - let mut index = 0; for op in &self.ops { let len: usize = op.len() as usize; match op { Operation::Delete(n) => { - invert_from_other(&mut inverted, other, op, index, index + *n); + invert_other(&mut inverted, other, op, index, index + *n); index += len; } Operation::Retain(_) => { match op.has_attribute() { - true => invert_from_other(&mut inverted, other, op, index, index + len), + true => invert_other(&mut inverted, other, op, index, index + len), false => { // tracing::trace!("invert retain: {} by retain {} {}", op, len, // op.get_attributes()); @@ -452,7 +518,7 @@ where } } -fn invert_from_other( +fn invert_other( base: &mut Delta, other: &Delta, operation: &Operation, @@ -460,7 +526,7 @@ fn invert_from_other( end: usize, ) { tracing::trace!("invert op: {} [{}:{}]", operation, start, end); - let other_ops = DeltaIter::from_interval(other, Interval::new(start, end)).ops(); + let other_ops = DeltaIterator::from_interval(other, Interval::new(start, end)).ops(); other_ops.into_iter().for_each(|other_op| match operation { Operation::Delete(_n) => { // tracing::trace!("invert delete: {} by add {}", n, other_op); @@ -493,7 +559,7 @@ fn transform_op_attribute( } let left = left.as_ref().unwrap().get_attributes(); let right = right.as_ref().unwrap().get_attributes(); - // TODO: replace with anyhow and thiserror. + // TODO: replace with anyhow and this error. Ok(left.transform(&right)?.0) } @@ -501,7 +567,18 @@ impl Delta where T: Attributes + DeserializeOwned, { - pub fn from_delta_str(json: &str) -> Result { + /// # Examples + /// + /// ``` + /// use lib_ot::core::DeltaBuilder; + /// use lib_ot::rich_text::{RichTextDelta}; + /// let json = r#"[ + /// {"retain":7,"attributes":{"bold":null}} + /// ]"#; + /// let delta = RichTextDelta::from_json(json).unwrap(); + /// assert_eq!(delta.json_str(), r#"[{"retain":7,"attributes":{"bold":""}}]"#); + /// ``` + pub fn from_json(json: &str) -> Result { let delta = serde_json::from_str(json).map_err(|e| { tracing::trace!("Deserialize failed: {:?}", e); tracing::trace!("{:?}", json); @@ -510,9 +587,10 @@ where Ok(delta) } + /// Deserialize the bytes into [Delta]. It requires the bytes is in utf8 format. pub fn from_bytes>(bytes: B) -> Result { let json = str::from_utf8(bytes.as_ref())?.to_owned(); - let val = Self::from_delta_str(&json)?; + let val = Self::from_json(&json)?; Ok(val) } } @@ -521,16 +599,19 @@ impl Delta where T: Attributes + serde::Serialize, { - pub fn to_delta_str(&self) -> String { + /// Serialize the [Delta] into a String in JSON format + pub fn json_str(&self) -> String { serde_json::to_string(self).unwrap_or_else(|_| "".to_owned()) } - pub fn to_str(&self) -> Result { + /// Get the content the [Delta] represents. + pub fn content(&self) -> Result { self.apply("") } - pub fn to_delta_bytes(&self) -> Bytes { - let json = self.to_delta_str(); + /// Serial the [Delta] into a String in Bytes format + pub fn json_bytes(&self) -> Bytes { + let json = self.json_str(); Bytes::from(json.into_bytes()) } } diff --git a/shared-lib/lib-ot/src/core/delta/delta_serde.rs b/shared-lib/lib-ot/src/core/delta/delta_serde.rs index ceac31ee58..7dff063211 100644 --- a/shared-lib/lib-ot/src/core/delta/delta_serde.rs +++ b/shared-lib/lib-ot/src/core/delta/delta_serde.rs @@ -1,4 +1,5 @@ -use crate::core::{Attributes, Delta}; +use crate::core::delta::Delta; +use crate::core::operation::Attributes; use serde::{ de::{SeqAccess, Visitor}, ser::SerializeSeq, diff --git a/shared-lib/lib-ot/src/core/delta/iterator.rs b/shared-lib/lib-ot/src/core/delta/iterator.rs index ccad56845d..7997d23bae 100644 --- a/shared-lib/lib-ot/src/core/delta/iterator.rs +++ b/shared-lib/lib-ot/src/core/delta/iterator.rs @@ -1,17 +1,38 @@ use super::cursor::*; -use crate::{ - core::{Attributes, Delta, Interval, Operation, NEW_LINE}, - rich_text::RichTextAttributes, -}; +use crate::core::delta::{Delta, NEW_LINE}; +use crate::core::interval::Interval; +use crate::core::operation::{Attributes, Operation}; +use crate::rich_text::RichTextAttributes; use std::ops::{Deref, DerefMut}; pub(crate) const MAX_IV_LEN: usize = i32::MAX as usize; -pub struct DeltaIter<'a, T: Attributes> { - cursor: OpCursor<'a, T>, +/// [DeltaIterator] is used to iterate over a delta. +/// # Examples +/// +/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/delta) out for more information. +/// +/// ``` +/// use lib_ot::core::{DeltaIterator, Interval, Operation}; +/// use lib_ot::rich_text::RichTextDelta; +/// let mut delta = RichTextDelta::default(); +/// delta.add(Operation::insert("123")); +/// delta.add(Operation::insert("4")); +/// assert_eq!( +/// DeltaIterator::from_interval(&delta, Interval::new(0, 2)).ops(), +/// vec![Operation::insert("12")] +/// ); +/// +/// assert_eq!( +/// DeltaIterator::from_interval(&delta, Interval::new(1, 3)).ops(), +/// vec![Operation::insert("23")] +/// ); +/// ``` +pub struct DeltaIterator<'a, T: Attributes> { + cursor: DeltaCursor<'a, T>, } -impl<'a, T> DeltaIter<'a, T> +impl<'a, T> DeltaIterator<'a, T> where T: Attributes, { @@ -28,7 +49,7 @@ where } pub fn from_interval(delta: &'a Delta, interval: Interval) -> Self { - let cursor = OpCursor::new(delta, interval); + let cursor = DeltaCursor::new(delta, interval); Self { cursor } } @@ -46,7 +67,7 @@ where } pub fn next_op(&mut self) -> Option> { - self.cursor.next_op() + self.cursor.get_next_op() } pub fn next_op_with_len(&mut self, len: usize) -> Option> { @@ -80,28 +101,28 @@ where } pub fn is_next_insert(&self) -> bool { - match self.cursor.next_iter_op() { + match self.cursor.next_op() { None => false, Some(op) => op.is_insert(), } } pub fn is_next_retain(&self) -> bool { - match self.cursor.next_iter_op() { + match self.cursor.next_op() { None => false, Some(op) => op.is_retain(), } } pub fn is_next_delete(&self) -> bool { - match self.cursor.next_iter_op() { + match self.cursor.next_op() { None => false, Some(op) => op.is_delete(), } } } -impl<'a, T> Iterator for DeltaIter<'a, T> +impl<'a, T> Iterator for DeltaIterator<'a, T> where T: Attributes, { @@ -112,7 +133,7 @@ where } pub fn is_empty_line_at_index(delta: &Delta, index: usize) -> bool { - let mut iter = DeltaIter::new(delta); + let mut iter = DeltaIterator::new(delta); let (prev, next) = (iter.next_op_with_len(index), iter.next_op()); if prev.is_none() { return true; @@ -128,7 +149,7 @@ pub fn is_empty_line_at_index(delta: &Delta, index: usize) - } pub struct AttributesIter<'a, T: Attributes> { - delta_iter: DeltaIter<'a, T>, + delta_iter: DeltaIterator<'a, T>, } impl<'a, T> AttributesIter<'a, T> @@ -141,7 +162,7 @@ where } pub fn from_interval(delta: &'a Delta, interval: Interval) -> Self { - let delta_iter = DeltaIter::from_interval(delta, interval); + let delta_iter = DeltaIterator::from_interval(delta, interval); Self { delta_iter } } @@ -157,7 +178,7 @@ impl<'a, T> Deref for AttributesIter<'a, T> where T: Attributes, { - type Target = DeltaIter<'a, T>; + type Target = DeltaIterator<'a, T>; fn deref(&self) -> &Self::Target { &self.delta_iter diff --git a/shared-lib/lib-ot/src/core/interval.rs b/shared-lib/lib-ot/src/core/interval.rs index a6f3131b6a..cc907ec3ea 100644 --- a/shared-lib/lib-ot/src/core/interval.rs +++ b/shared-lib/lib-ot/src/core/interval.rs @@ -157,7 +157,7 @@ impl From> for Interval { #[cfg(test)] mod tests { - use crate::core::Interval; + use crate::core::interval::Interval; #[test] fn contains() { diff --git a/shared-lib/lib-ot/src/core/mod.rs b/shared-lib/lib-ot/src/core/mod.rs index b5bc594246..7c1ed3f2ef 100644 --- a/shared-lib/lib-ot/src/core/mod.rs +++ b/shared-lib/lib-ot/src/core/mod.rs @@ -1,30 +1,9 @@ mod delta; -mod flowy_str; mod interval; mod operation; +mod ot_str; -use crate::errors::OTError; pub use delta::*; -pub use flowy_str::*; pub use interval::*; pub use operation::*; - -pub trait OperationTransformable { - /// Merges the operation with `other` into one operation while preserving - /// the changes of both. - fn compose(&self, other: &Self) -> Result - where - Self: Sized; - /// Transforms two operations a and b that happened concurrently and - /// produces two operations a' and b'. - /// (a', b') = a.transform(b) - /// a.compose(b') = b.compose(a') - fn transform(&self, other: &Self) -> Result<(Self, Self), OTError> - where - Self: Sized; - /// Inverts the operation with `other` to produces undo operation. - /// undo = a.invert(b) - /// new_b = b.compose(a) - /// b = new_b.compose(undo) - fn invert(&self, other: &Self) -> Self; -} +pub use ot_str::*; diff --git a/shared-lib/lib-ot/src/core/operation/builder.rs b/shared-lib/lib-ot/src/core/operation/builder.rs index dc7b6dd7d9..9483d4cae7 100644 --- a/shared-lib/lib-ot/src/core/operation/builder.rs +++ b/shared-lib/lib-ot/src/core/operation/builder.rs @@ -1,51 +1,51 @@ -use crate::{ - core::{Attributes, Operation, PlainTextAttributes}, - rich_text::RichTextAttributes, -}; +use crate::core::operation::{Attributes, Operation, PhantomAttributes}; +use crate::rich_text::RichTextAttributes; -pub type RichTextOpBuilder = OpBuilder; -pub type PlainTextOpBuilder = OpBuilder; +pub type RichTextOpBuilder = OperationsBuilder; +pub type PlainTextOpBuilder = OperationsBuilder; -pub struct OpBuilder { - ty: Operation, - attrs: T, +pub struct OperationsBuilder { + operations: Vec>, } -impl OpBuilder +impl OperationsBuilder where T: Attributes, { - pub fn new(ty: Operation) -> OpBuilder { - OpBuilder { - ty, - attrs: T::default(), - } + pub fn new() -> OperationsBuilder { + OperationsBuilder { operations: vec![] } } - pub fn retain(n: usize) -> OpBuilder { - OpBuilder::new(Operation::Retain(n.into())) - } - - pub fn delete(n: usize) -> OpBuilder { - OpBuilder::new(Operation::Delete(n)) - } - - pub fn insert(s: &str) -> OpBuilder { - OpBuilder::new(Operation::Insert(s.into())) - } - - pub fn attributes(mut self, attrs: T) -> OpBuilder { - self.attrs = attrs; + pub fn retain_with_attributes(mut self, n: usize, attributes: T) -> OperationsBuilder { + let retain = Operation::retain_with_attributes(n.into(), attributes); + self.operations.push(retain); self } - pub fn build(self) -> Operation { - let mut operation = self.ty; - match &mut operation { - Operation::Delete(_) => {} - Operation::Retain(retain) => retain.attributes = self.attrs, - Operation::Insert(insert) => insert.attributes = self.attrs, - } - operation + pub fn retain(mut self, n: usize) -> OperationsBuilder { + let retain = Operation::retain(n.into()); + self.operations.push(retain); + self + } + + pub fn delete(mut self, n: usize) -> OperationsBuilder { + self.operations.push(Operation::Delete(n)); + self + } + + pub fn insert_with_attributes(mut self, s: &str, attributes: T) -> OperationsBuilder { + let insert = Operation::insert_with_attributes(s.into(), attributes); + self.operations.push(insert); + self + } + + pub fn insert(mut self, s: &str) -> OperationsBuilder { + let insert = Operation::insert(s.into()); + self.operations.push(insert); + self + } + + pub fn build(self) -> Vec> { + self.operations } } diff --git a/shared-lib/lib-ot/src/core/operation/operation.rs b/shared-lib/lib-ot/src/core/operation/operation.rs index 82228c19fc..4bacd1ea7d 100644 --- a/shared-lib/lib-ot/src/core/operation/operation.rs +++ b/shared-lib/lib-ot/src/core/operation/operation.rs @@ -1,8 +1,8 @@ -use crate::{ - core::{FlowyStr, Interval, OpBuilder, OperationTransformable}, - errors::OTError, -}; +use crate::core::interval::Interval; +use crate::core::ot_str::OTString; +use crate::errors::OTError; use serde::{Deserialize, Serialize, __private::Formatter}; +use std::fmt::Display; use std::{ cmp::min, fmt, @@ -10,15 +10,91 @@ use std::{ ops::{Deref, DerefMut}, }; -pub trait Attributes: fmt::Display + Eq + PartialEq + Default + Clone + Debug + OperationTransformable { - fn is_empty(&self) -> bool; +pub trait OperationTransform { + /// Merges the operation with `other` into one operation while preserving + /// the changes of both. + /// + /// # Arguments + /// + /// * `other`: The delta gonna to merge. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OperationTransform, TextDeltaBuilder}; + /// let document = TextDeltaBuilder::new().build(); + /// let delta = TextDeltaBuilder::new().insert("abc").build(); + /// let new_document = document.compose(&delta).unwrap(); + /// assert_eq!(new_document.content().unwrap(), "abc".to_owned()); + /// ``` + fn compose(&self, other: &Self) -> Result + where + Self: Sized; - // Remove the empty attribute which value is None. - fn remove_empty(&mut self); + /// Transforms two operations a and b that happened concurrently and + /// produces two operations a' and b'. + /// (a', b') = a.transform(b) + /// a.compose(b') = b.compose(a') + /// + fn transform(&self, other: &Self) -> Result<(Self, Self), OTError> + where + Self: Sized; - fn extend_other(&mut self, other: Self); + /// Returns the invert delta from the other. It can be used to do the undo operation. + /// + /// # Arguments + /// + /// * `other`: Generate the undo delta for [Other]. [Other] can compose the undo delta to return + /// to the previous state. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OperationTransform, TextDeltaBuilder}; + /// let original_document = TextDeltaBuilder::new().build(); + /// let delta = TextDeltaBuilder::new().insert("abc").build(); + /// + /// let undo_delta = delta.invert(&original_document); + /// let new_document = original_document.compose(&delta).unwrap(); + /// let document = new_document.compose(&undo_delta).unwrap(); + /// + /// assert_eq!(original_document, document); + /// + /// ``` + fn invert(&self, other: &Self) -> Self; } +/// Each operation can carry attributes. For example, the [RichTextAttributes] has a list of key/value attributes. +/// Such as { bold: true, italic: true }. +/// +///Because [Operation] is generic over the T, so you must specify the T. For example, the [TextDelta] uses +///[PhantomAttributes] as the T. [PhantomAttributes] does nothing, just a phantom. +/// +pub trait Attributes: Default + Display + Eq + PartialEq + Clone + Debug + OperationTransform { + fn is_empty(&self) -> bool { + true + } + + /// Remove the empty attribute which value is None. + fn remove_empty(&mut self) { + // Do nothing + } + + fn extend_other(&mut self, _other: Self) { + // Do nothing + } +} + +/// [Operation] consists of three types. +/// * Delete +/// * Retain +/// * Insert +/// +/// You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/delta) out for more information. +/// +/// The [T] should support serde if you want to serialize/deserialize the operation +/// to json string. You could check out the operation_serde.rs for more information. +/// #[derive(Debug, Clone, Eq, PartialEq)] pub enum Operation { Delete(usize), @@ -30,6 +106,40 @@ impl Operation where T: Attributes, { + pub fn delete(n: usize) -> Self { + Self::Delete(n) + } + + /// Create a [Retain] operation with the given attributes + pub fn retain_with_attributes(n: usize, attributes: T) -> Self { + Self::Retain(Retain { n, attributes }) + } + + /// Create a [Retain] operation without attributes + pub fn retain(n: usize) -> Self { + Self::Retain(Retain { + n, + attributes: T::default(), + }) + } + + /// Create a [Insert] operation with the given attributes + pub fn insert_with_attributes(s: &str, attributes: T) -> Self { + Self::Insert(Insert { + s: OTString::from(s), + attributes, + }) + } + + /// Create a [Insert] operation without attributes + pub fn insert(s: &str) -> Self { + Self::Insert(Insert { + s: OTString::from(s), + attributes: T::default(), + }) + } + + /// Return the String if the operation is [Insert] operation, otherwise return the empty string. pub fn get_data(&self) -> &str { match self { Operation::Delete(_) => "", @@ -77,43 +187,58 @@ where let right; match self { Operation::Delete(n) => { - left = Some(OpBuilder::::delete(index).build()); - right = Some(OpBuilder::::delete(*n - index).build()); + left = Some(Operation::::delete(index)); + right = Some(Operation::::delete(*n - index)); } Operation::Retain(retain) => { - left = Some(OpBuilder::::delete(index).build()); - right = Some(OpBuilder::::delete(retain.n - index).build()); + left = Some(Operation::::delete(index)); + right = Some(Operation::::delete(retain.n - index)); } Operation::Insert(insert) => { let attributes = self.get_attributes(); - left = Some( - OpBuilder::::insert(&insert.s[0..index]) - .attributes(attributes.clone()) - .build(), - ); - right = Some( - OpBuilder::::insert(&insert.s[index..insert.utf16_size()]) - .attributes(attributes) - .build(), - ); + left = Some(Operation::::insert_with_attributes( + &insert.s[0..index], + attributes.clone(), + )); + right = Some(Operation::::insert_with_attributes( + &insert.s[index..insert.utf16_size()], + attributes, + )); } } (left, right) } + /// Returns an operation with the specified width. + /// # Arguments + /// + /// * `interval`: Specify the shrink width of the operation. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::{Interval, Operation, PhantomAttributes}; + /// let operation = Operation::::insert("1234"); + /// + /// let op1 = operation.shrink(Interval::new(0,3)).unwrap(); + /// assert_eq!(op1 , Operation::insert("123")); + /// + /// let op2= operation.shrink(Interval::new(3,4)).unwrap(); + /// assert_eq!(op2, Operation::insert("4")); + /// ``` pub fn shrink(&self, interval: Interval) -> Option> { let op = match self { - Operation::Delete(n) => OpBuilder::delete(min(*n, interval.size())).build(), - Operation::Retain(retain) => OpBuilder::retain(min(retain.n, interval.size())) - .attributes(retain.attributes.clone()) - .build(), + Operation::Delete(n) => Operation::delete(min(*n, interval.size())), + Operation::Retain(retain) => { + Operation::retain_with_attributes(min(retain.n, interval.size()), retain.attributes.clone()) + } Operation::Insert(insert) => { if interval.start > insert.utf16_size() { - OpBuilder::insert("").build() + Operation::insert("") } else { let s = insert.s.sub_str(interval).unwrap_or_else(|| "".to_owned()); - OpBuilder::insert(&s).attributes(insert.attributes.clone()).build() + Operation::insert_with_attributes(&s, insert.attributes.clone()) } } }; @@ -178,9 +303,7 @@ where #[derive(Clone, Debug, Eq, PartialEq)] pub struct Retain { - // #[serde(rename(serialize = "retain", deserialize = "retain"))] pub n: usize, - // #[serde(skip_serializing_if = "is_empty")] pub attributes: T, } @@ -212,7 +335,7 @@ where self.n += n; None } else { - Some(OpBuilder::retain(n).attributes(attributes).build()) + Some(Operation::retain_with_attributes(n, attributes)) } } @@ -255,10 +378,7 @@ where #[derive(Clone, Debug, Eq, PartialEq)] pub struct Insert { - // #[serde(rename(serialize = "insert", deserialize = "insert"))] - pub s: FlowyStr, - - // #[serde(skip_serializing_if = "is_empty")] + pub s: OTString, pub attributes: T, } @@ -288,7 +408,7 @@ where T: Attributes, { pub fn utf16_size(&self) -> usize { - self.s.utf16_size() + self.s.utf16_len() } pub fn merge_or_new_op(&mut self, s: &str, attributes: T) -> Option> { @@ -296,7 +416,7 @@ where self.s += s; None } else { - Some(OpBuilder::::insert(s).attributes(attributes).build()) + Some(Operation::::insert_with_attributes(s, attributes)) } } @@ -326,11 +446,11 @@ where } } -impl std::convert::From for Insert +impl std::convert::From for Insert where T: Attributes, { - fn from(s: FlowyStr) -> Self { + fn from(s: OTString) -> Self { Insert { s, attributes: T::default(), @@ -339,24 +459,16 @@ where } #[derive(Debug, Clone, Eq, PartialEq, Default, Serialize, Deserialize)] -pub struct PlainTextAttributes(); -impl fmt::Display for PlainTextAttributes { +pub struct PhantomAttributes(); +impl fmt::Display for PhantomAttributes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("PlainAttributes") + f.write_str("PhantomAttributes") } } -impl Attributes for PlainTextAttributes { - fn is_empty(&self) -> bool { - true - } +impl Attributes for PhantomAttributes {} - fn remove_empty(&mut self) {} - - fn extend_other(&mut self, _other: Self) {} -} - -impl OperationTransformable for PlainTextAttributes { +impl OperationTransform for PhantomAttributes { fn compose(&self, _other: &Self) -> Result { Ok(self.clone()) } diff --git a/shared-lib/lib-ot/src/core/operation/operation_serde.rs b/shared-lib/lib-ot/src/core/operation/operation_serde.rs index aefb909d0a..b7ff7b3c20 100644 --- a/shared-lib/lib-ot/src/core/operation/operation_serde.rs +++ b/shared-lib/lib-ot/src/core/operation/operation_serde.rs @@ -1,4 +1,5 @@ -use crate::core::{Attributes, FlowyStr, Insert, Operation, Retain}; +use crate::core::operation::{Attributes, Insert, Operation, Retain}; +use crate::core::ot_str::OTString; use serde::{ de, de::{MapAccess, SeqAccess, Visitor}, @@ -248,7 +249,7 @@ where where A: SeqAccess<'de>, { - let s = match serde::de::SeqAccess::next_element::(&mut seq)? { + let s = match serde::de::SeqAccess::next_element::(&mut seq)? { Some(val) => val, None => { return Err(de::Error::invalid_length(0, &"struct Insert with 2 elements")); @@ -270,7 +271,7 @@ where where V: MapAccess<'de>, { - let mut s: Option = None; + let mut s: Option = None; let mut attributes: Option = None; while let Some(key) = map.next_key()? { match key { diff --git a/shared-lib/lib-ot/src/core/flowy_str.rs b/shared-lib/lib-ot/src/core/ot_str.rs similarity index 58% rename from shared-lib/lib-ot/src/core/flowy_str.rs rename to shared-lib/lib-ot/src/core/ot_str.rs index 95cc1735cc..7f84de6d20 100644 --- a/shared-lib/lib-ot/src/core/flowy_str.rs +++ b/shared-lib/lib-ot/src/core/ot_str.rs @@ -1,19 +1,51 @@ use serde::{de, de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; use std::{fmt, fmt::Formatter}; +/// [OTString] uses [String] as its inner container. #[derive(Clone, Debug, Eq, PartialEq)] -pub struct FlowyStr(pub String); +pub struct OTString(pub String); -impl FlowyStr { - // https://stackoverflow.com/questions/2241348/what-is-unicode-utf-8-utf-16 - pub fn utf16_size(&self) -> usize { +impl OTString { + /// Returns the number of UTF-16 code units in this string. + /// + /// The length of strings behaves differently in different languages. For example: [Dart] string's + /// length is calculated with UTF-16 code units. The method [utf16_len] returns the length of a + /// String in UTF-16 code units. + /// + /// # Examples + /// + /// ``` + /// use lib_ot::core::OTString; + /// let utf16_len = OTString::from("👋").utf16_len(); + /// assert_eq!(utf16_len, 2); + /// let bytes_len = String::from("👋").len(); + /// assert_eq!(bytes_len, 4); + /// + /// ``` + pub fn utf16_len(&self) -> usize { count_utf16_code_units(&self.0) } - pub fn utf16_code_unit_iter(&self) -> Utf16CodeUnitIterator { + pub fn utf16_iter(&self) -> Utf16CodeUnitIterator { Utf16CodeUnitIterator::new(self) } + /// Returns a new string with the given [Interval] + /// # Examples + /// + /// ``` + /// use lib_ot::core::{OTString, Interval}; + /// let s: OTString = "你好\n😁".into(); + /// assert_eq!(s.utf16_len(), 5); + /// let output1 = s.sub_str(Interval::new(0, 2)).unwrap(); + /// assert_eq!(output1, "你好"); + /// + /// let output2 = s.sub_str(Interval::new(2, 3)).unwrap(); + /// assert_eq!(output2, "\n"); + /// + /// let output3 = s.sub_str(Interval::new(3, 5)).unwrap(); + /// assert_eq!(output3, "😁"); + /// ``` pub fn sub_str(&self, interval: Interval) -> Option { let mut iter = Utf16CodeUnitIterator::new(self); let mut buf = vec![]; @@ -33,13 +65,33 @@ impl FlowyStr { } } + /// Return a new string with the given [Interval] + /// # Examples + /// + /// ``` + /// use lib_ot::core::OTString; + /// let s: OTString = "👋😁👋".into(); /// + /// let mut iter = s.utf16_code_point_iter(); + /// assert_eq!(iter.next().unwrap(), "👋".to_string()); + /// assert_eq!(iter.next().unwrap(), "😁".to_string()); + /// assert_eq!(iter.next().unwrap(), "👋".to_string()); + /// assert_eq!(iter.next(), None); + /// + /// let s: OTString = "👋12ab一二👋".into(); /// + /// let mut iter = s.utf16_code_point_iter(); + /// assert_eq!(iter.next().unwrap(), "👋".to_string()); + /// assert_eq!(iter.next().unwrap(), "1".to_string()); + /// assert_eq!(iter.next().unwrap(), "2".to_string()); + /// + /// assert_eq!(iter.skip(OTString::from("ab一二").utf16_len()).next().unwrap(), "👋".to_string()); + /// ``` #[allow(dead_code)] - fn utf16_code_point_iter(&self) -> FlowyUtf16CodePointIterator { - FlowyUtf16CodePointIterator::new(self, 0) + pub fn utf16_code_point_iter(&self) -> OTUtf16CodePointIterator { + OTUtf16CodePointIterator::new(self, 0) } } -impl std::ops::Deref for FlowyStr { +impl std::ops::Deref for OTString { type Target = String; fn deref(&self) -> &Self::Target { @@ -47,46 +99,46 @@ impl std::ops::Deref for FlowyStr { } } -impl std::ops::DerefMut for FlowyStr { +impl std::ops::DerefMut for OTString { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl std::convert::From for FlowyStr { +impl std::convert::From for OTString { fn from(s: String) -> Self { - FlowyStr(s) + OTString(s) } } -impl std::convert::From<&str> for FlowyStr { +impl std::convert::From<&str> for OTString { fn from(s: &str) -> Self { s.to_owned().into() } } -impl std::fmt::Display for FlowyStr { +impl std::fmt::Display for OTString { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } } -impl std::ops::Add<&str> for FlowyStr { - type Output = FlowyStr; +impl std::ops::Add<&str> for OTString { + type Output = OTString; - fn add(self, rhs: &str) -> FlowyStr { + fn add(self, rhs: &str) -> OTString { let new_value = self.0 + rhs; new_value.into() } } -impl std::ops::AddAssign<&str> for FlowyStr { +impl std::ops::AddAssign<&str> for OTString { fn add_assign(&mut self, rhs: &str) { self.0 += rhs; } } -impl Serialize for FlowyStr { +impl Serialize for OTString { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -95,15 +147,15 @@ impl Serialize for FlowyStr { } } -impl<'de> Deserialize<'de> for FlowyStr { - fn deserialize(deserializer: D) -> Result +impl<'de> Deserialize<'de> for OTString { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - struct FlowyStrVisitor; + struct OTStringVisitor; - impl<'de> Visitor<'de> for FlowyStrVisitor { - type Value = FlowyStr; + impl<'de> Visitor<'de> for OTStringVisitor { + type Value = OTString; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a str") @@ -116,19 +168,19 @@ impl<'de> Deserialize<'de> for FlowyStr { Ok(s.into()) } } - deserializer.deserialize_str(FlowyStrVisitor) + deserializer.deserialize_str(OTStringVisitor) } } pub struct Utf16CodeUnitIterator<'a> { - s: &'a FlowyStr, + s: &'a OTString, byte_offset: usize, utf16_offset: usize, utf16_count: usize, } impl<'a> Utf16CodeUnitIterator<'a> { - pub fn new(s: &'a FlowyStr) -> Self { + pub fn new(s: &'a OTString) -> Self { Utf16CodeUnitIterator { s, byte_offset: 0, @@ -166,21 +218,21 @@ impl<'a> Iterator for Utf16CodeUnitIterator<'a> { } } -pub struct FlowyUtf16CodePointIterator<'a> { - s: &'a FlowyStr, +pub struct OTUtf16CodePointIterator<'a> { + s: &'a OTString, offset: usize, } -impl<'a> FlowyUtf16CodePointIterator<'a> { - pub fn new(s: &'a FlowyStr, offset: usize) -> Self { - FlowyUtf16CodePointIterator { s, offset } +impl<'a> OTUtf16CodePointIterator<'a> { + pub fn new(s: &'a OTString, offset: usize) -> Self { + OTUtf16CodePointIterator { s, offset } } } -use crate::core::Interval; +use crate::core::interval::Interval; use std::str; -impl<'a> Iterator for FlowyUtf16CodePointIterator<'a> { +impl<'a> Iterator for OTUtf16CodePointIterator<'a> { type Item = String; fn next(&mut self) -> Option { @@ -226,14 +278,15 @@ pub fn len_utf8_from_first_byte(b: u8) -> usize { #[cfg(test)] mod tests { - use crate::core::{FlowyStr, Interval}; + use crate::core::interval::Interval; + use crate::core::ot_str::OTString; #[test] fn flowy_str_code_unit() { - let size = FlowyStr::from("👋").utf16_size(); + let size = OTString::from("👋").utf16_len(); assert_eq!(size, 2); - let s: FlowyStr = "👋 \n👋".into(); + let s: OTString = "👋 \n👋".into(); let output = s.sub_str(Interval::new(0, size)).unwrap(); assert_eq!(output, "👋"); @@ -247,24 +300,10 @@ mod tests { assert_eq!(output, "👋"); } - #[test] - fn flowy_str_sub_str_in_chinese() { - let s: FlowyStr = "你好\n😁".into(); - let size = s.utf16_size(); - assert_eq!(size, 5); - - let output1 = s.sub_str(Interval::new(0, 2)).unwrap(); - let output2 = s.sub_str(Interval::new(2, 3)).unwrap(); - let output3 = s.sub_str(Interval::new(3, 5)).unwrap(); - assert_eq!(output1, "你好"); - assert_eq!(output2, "\n"); - assert_eq!(output3, "😁"); - } - #[test] fn flowy_str_sub_str_in_chinese2() { - let s: FlowyStr = "😁 \n".into(); - let size = s.utf16_size(); + let s: OTString = "😁 \n".into(); + let size = s.utf16_len(); assert_eq!(size, 4); let output1 = s.sub_str(Interval::new(0, 3)).unwrap(); @@ -275,27 +314,17 @@ mod tests { #[test] fn flowy_str_sub_str_in_english() { - let s: FlowyStr = "ab".into(); - let size = s.utf16_size(); + let s: OTString = "ab".into(); + let size = s.utf16_len(); assert_eq!(size, 2); let output = s.sub_str(Interval::new(0, 2)).unwrap(); assert_eq!(output, "ab"); } - #[test] - fn flowy_str_utf16_code_point_iter_test1() { - let s: FlowyStr = "👋😁👋".into(); - let mut iter = s.utf16_code_point_iter(); - assert_eq!(iter.next().unwrap(), "👋".to_string()); - assert_eq!(iter.next().unwrap(), "😁".to_string()); - assert_eq!(iter.next().unwrap(), "👋".to_string()); - assert_eq!(iter.next(), None); - } - #[test] fn flowy_str_utf16_code_point_iter_test2() { - let s: FlowyStr = "👋😁👋".into(); + let s: OTString = "👋😁👋".into(); let iter = s.utf16_code_point_iter(); let result = iter.skip(1).take(1).collect::(); assert_eq!(result, "😁".to_string()); diff --git a/shared-lib/lib-ot/src/lib.rs b/shared-lib/lib-ot/src/lib.rs index 5a3be0ede7..93e2e8857c 100644 --- a/shared-lib/lib-ot/src/lib.rs +++ b/shared-lib/lib-ot/src/lib.rs @@ -1,3 +1,4 @@ pub mod core; pub mod errors; +pub mod parser; pub mod rich_text; diff --git a/shared-lib/lib-ot/src/rich_text/attributes.rs b/shared-lib/lib-ot/src/rich_text/attributes.rs index d70e23228b..54826c49a0 100644 --- a/shared-lib/lib-ot/src/rich_text/attributes.rs +++ b/shared-lib/lib-ot/src/rich_text/attributes.rs @@ -1,10 +1,6 @@ #![allow(non_snake_case)] -use crate::{ - block_attribute, - core::{Attributes, Operation, OperationTransformable}, - errors::OTError, - ignore_attribute, inline_attribute, list_attribute, -}; +use crate::core::{Attributes, Operation, OperationTransform}; +use crate::{block_attribute, errors::OTError, ignore_attribute, inline_attribute, list_attribute}; use lazy_static::lazy_static; use std::{ collections::{HashMap, HashSet}, @@ -126,7 +122,7 @@ impl Attributes for RichTextAttributes { } } -impl OperationTransformable for RichTextAttributes { +impl OperationTransform for RichTextAttributes { fn compose(&self, other: &Self) -> Result where Self: Sized, diff --git a/shared-lib/lib-ot/src/rich_text/delta.rs b/shared-lib/lib-ot/src/rich_text/delta.rs index 99cb35f2bc..3a60cf7b02 100644 --- a/shared-lib/lib-ot/src/rich_text/delta.rs +++ b/shared-lib/lib-ot/src/rich_text/delta.rs @@ -1,7 +1,5 @@ -use crate::{ - core::{Delta, DeltaBuilder}, - rich_text::RichTextAttributes, -}; +use crate::core::{Delta, DeltaBuilder}; +use crate::rich_text::RichTextAttributes; pub type RichTextDelta = Delta; pub type RichTextDeltaBuilder = DeltaBuilder;