Quellcode durchsuchen

Merge branch 'main' of github.com:AppFlowy-IO/AppFlowy

Sean Riley Hawkins vor 2 Jahren
Ursprung
Commit
ef43e349bd
22 geänderte Dateien mit 939 neuen und 302 gelöschten Zeilen
  1. 2 5
      .github/workflows/translation_notify.yml
  2. 1 0
      frontend/app_flowy/assets/translations/en.json
  3. 75 3
      frontend/app_flowy/assets/translations/es-VE.json
  4. 1 1
      frontend/app_flowy/assets/translations/fr-FR.json
  5. 218 0
      frontend/app_flowy/assets/translations/id-ID.json
  6. 218 0
      frontend/app_flowy/assets/translations/zh-TW.json
  7. 12 0
      frontend/app_flowy/lib/startup/deps_resolver.dart
  8. 1 0
      frontend/app_flowy/lib/workspace/application/settings/prelude.dart
  9. 67 0
      frontend/app_flowy/lib/workspace/application/settings/settings_dialog_bloc.dart
  10. 1 0
      frontend/app_flowy/lib/workspace/application/user/prelude.dart
  11. 79 0
      frontend/app_flowy/lib/workspace/application/user/settings_user_bloc.dart
  12. 2 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart
  13. 60 55
      frontend/app_flowy/lib/workspace/presentation/settings/settings_dialog.dart
  14. 10 0
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_menu.dart
  15. 50 0
      frontend/app_flowy/lib/workspace/presentation/settings/widgets/settings_user_view.dart
  16. 3 0
      frontend/app_flowy/pubspec.yaml
  17. 32 18
      frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs
  18. 5 5
      frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs
  19. 42 165
      frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs
  20. 39 32
      frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs
  21. 19 15
      frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs
  22. 2 2
      frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs

+ 2 - 5
.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:

+ 1 - 0
frontend/app_flowy/assets/translations/en.json

@@ -141,6 +141,7 @@
     "menu": {
       "appearance": "Appearance",
       "language": "Language",
+      "user": "User",
       "open": "Open Settings"
     },
     "appearance": {

+ 75 - 3
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"
+    }
   }
 }

+ 1 - 1
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"
     }
   },

+ 218 - 0
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"
+    }
+  }
+}

+ 218 - 0
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"
+    }
+  }
+}

+ 12 - 0
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';
@@ -101,6 +103,16 @@ void _resolveFolderDeps(GetIt getIt) {
     (user, _) => MenuUserBloc(user),
   );
 
+  //Settings
+  getIt.registerFactoryParam<SettingsDialogBloc, UserProfilePB, void>(
+    (user, _) => SettingsDialogBloc(user),
+  );
+
+  //User
+  getIt.registerFactoryParam<SettingsUserViewBloc, UserProfilePB, void>(
+    (user, _) => SettingsUserViewBloc(user),
+  );
+
   // AppPB
   getIt.registerFactoryParam<AppBloc, AppPB, void>(
     (app, _) => AppBloc(

+ 1 - 0
frontend/app_flowy/lib/workspace/application/settings/prelude.dart

@@ -0,0 +1 @@
+export 'settings_dialog_bloc.dart';

+ 67 - 0
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<SettingsDialogEvent, SettingsDialogState> {
+  final UserListener _userListener;
+  final UserProfilePB userProfile;
+
+  SettingsDialogBloc(this.userProfile)
+      : _userListener = UserListener(userProfile: userProfile),
+        super(SettingsDialogState.initial(userProfile)) {
+    on<SettingsDialogEvent>((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<void> close() async {
+    await _userListener.stop();
+    super.close();
+  }
+
+  void _profileUpdated(Either<UserProfilePB, FlowyError> 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<Unit, String> successOrFailure,
+    required int viewIndex,
+  }) = _SettingsDialogState;
+
+  factory SettingsDialogState.initial(UserProfilePB userProfile) => SettingsDialogState(
+        userProfile: userProfile,
+        successOrFailure: left(unit),
+        viewIndex: 0,
+      );
+}

+ 1 - 0
frontend/app_flowy/lib/workspace/application/user/prelude.dart

@@ -0,0 +1 @@
+export 'settings_user_bloc.dart';

+ 79 - 0
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<SettingsUserEvent, SettingsUserState> {
+  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<SettingsUserEvent>((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<void> close() async {
+    await _userListener.stop();
+    super.close();
+  }
+
+  Future<void> _initUser() async {
+    final result = await _userService.initUser();
+    result.fold((l) => null, (error) => Log.error(error));
+  }
+
+  void _profileUpdated(Either<UserProfilePB, FlowyError> 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<Unit, String> successOrFailure,
+  }) = _SettingsUserState;
+
+  factory SettingsUserState.initial(UserProfilePB userProfile) => SettingsUserState(
+        userProfile: userProfile,
+        successOrFailure: left(unit),
+      );
+}

+ 2 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/menu_user.dart

@@ -67,6 +67,7 @@ class MenuUser extends StatelessWidget {
 
   Widget _renderSettingsButton(BuildContext context) {
     final theme = context.watch<AppTheme>();
+    final userProfile = context.read<MenuUserBloc>().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);
             },
           );
         },

+ 60 - 55
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<SettingsDialog> createState() => _SettingsDialogState();
-}
-
-class _SettingsDialogState extends State<SettingsDialog> {
-  int _selectedViewIndex = 0;
-
-  final List<Widget> settingsViews = const [
-    SettingsAppearanceView(),
-    SettingsLanguageView(),
-  ];
+  Widget getSettingsView(int index, UserProfilePB user) {
+    final List<Widget> settingsViews = [
+      const SettingsAppearanceView(),
+      const SettingsLanguageView(),
+      SettingsUserView(user),
+    ];
+    return settingsViews[index];
+  }
 
   @override
   Widget build(BuildContext context) {
-    return ChangeNotifierProvider.value(
-      value: Provider.of<AppearanceSettingModel>(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<SettingsDialogBloc>(
+        create: (context) => getIt<SettingsDialogBloc>(param1: user)..add(const SettingsDialogEvent.initial()),
+        child: BlocBuilder<SettingsDialogBloc, SettingsDialogState>(
+            builder: (context, state) => ChangeNotifierProvider.value(
+                  value: Provider.of<AppearanceSettingModel>(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<SettingsDialogBloc>().add(SettingsDialogEvent.setViewIndex(index));
+                              },
+                              currentIndex: context.read<SettingsDialogBloc>().state.viewIndex,
+                            ),
+                          ),
+                          const VerticalDivider(),
+                          const SizedBox(width: 10),
+                          Expanded(
+                            child: getSettingsView(context.read<SettingsDialogBloc>().state.viewIndex,
+                                context.read<SettingsDialogBloc>().state.userProfile),
+                          )
+                        ],
+                      ),
+                    ),
+                  ),
+                )));
   }
 }

+ 10 - 0
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,
+        ),
       ],
     );
   }

+ 50 - 0
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<SettingsUserViewBloc>(
+      create: (context) => getIt<SettingsUserViewBloc>(param1: user)..add(const SettingsUserEvent.initial()),
+      child: BlocBuilder<SettingsUserViewBloc, SettingsUserState>(
+        builder: (context, state) => SingleChildScrollView(
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [_renderUserNameInput(context)],
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _renderUserNameInput(BuildContext context) {
+    String name = context.read<SettingsUserViewBloc>().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<SettingsUserViewBloc>().add(SettingsUserEvent.updateUserName(val));
+        });
+  }
+}

+ 3 - 0
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

+ 32 - 18
frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs

@@ -1,30 +1,44 @@
 #[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;
+    use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data, 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 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 type_option = CheckboxTypeOption::default();
+        let field_type = FieldType::Checkbox;
+        let field_rev = FieldBuilder::from_field_type(&field_type).build();
 
-        let data = apply_cell_data_changeset("1", None, &field_rev).unwrap();
-        assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES);
+        // 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);
 
-        let data = apply_cell_data_changeset("yes", None, &field_rev).unwrap();
-        assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES);
+        // 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);
 
-        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);
+        // 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);
+    }
 
-        let data = apply_cell_data_changeset("12", None, &field_rev).unwrap();
-        assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), "");
+    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()
+        );
     }
 }

+ 5 - 5
frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs

@@ -3,14 +3,14 @@ use bytes::Bytes;
 use flowy_error::{FlowyError, FlowyResult};
 use std::str::FromStr;
 
-pub const YES: &str = "Yes";
-pub const NO: &str = "No";
+pub const CHECK: &str = "Yes";
+pub const UNCHECK: &str = "No";
 
 pub struct CheckboxCellData(String);
 
 impl CheckboxCellData {
     pub fn is_check(&self) -> bool {
-        self.0 == YES
+        self.0 == CHECK
     }
 }
 
@@ -36,8 +36,8 @@ impl FromStr for CheckboxCellData {
         };
 
         match val {
-            Some(true) => Ok(Self(YES.to_string())),
-            Some(false) => Ok(Self(NO.to_string())),
+            Some(true) => Ok(Self(CHECK.to_string())),
+            Some(false) => Ok(Self(UNCHECK.to_string())),
             None => Ok(Self("".to_string())),
         }
     }

+ 42 - 165
frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs

@@ -7,23 +7,6 @@ mod tests {
     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,
-            DateCellChangesetPB {
-                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();
@@ -32,23 +15,23 @@ mod tests {
             type_option.date_format = date_format;
             match date_format {
                 DateFormat::Friendly => {
-                    assert_decode_timestamp(1647251762, &type_option, &field_rev, "Mar 14,2022");
+                    assert_date(&type_option, 1647251762, None, "Mar 14,2022", &field_rev);
                 }
                 DateFormat::US => {
-                    assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14");
+                    assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev);
                 }
                 DateFormat::ISO => {
-                    assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022-03-14");
+                    assert_date(&type_option, 1647251762, None, "2022-03-14", &field_rev);
                 }
                 DateFormat::Local => {
-                    assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14");
+                    assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev);
                 }
             }
         }
     }
 
     #[test]
-    fn date_type_option_time_format_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();
@@ -58,59 +41,23 @@ mod tests {
             type_option.include_time = true;
             match time_format {
                 TimeFormat::TwentyFourHour => {
-                    assert_changeset_result(
-                        &type_option,
-                        DateCellChangesetPB {
-                            date: Some(1653609600.to_string()),
-                            time: None,
-                        },
-                        &field_type,
-                        &field_rev,
-                        "May 27,2022",
-                    );
-                    assert_changeset_result(
+                    assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev);
+                    assert_date(
                         &type_option,
-                        DateCellChangesetPB {
-                            date: Some(1653609600.to_string()),
-                            time: Some("23:00".to_owned()),
-                        },
-                        &field_type,
-                        &field_rev,
+                        1653609600,
+                        Some("23:00".to_owned()),
                         "May 27,2022 23:00",
+                        &field_rev,
                     );
                 }
                 TimeFormat::TwelveHour => {
-                    assert_changeset_result(
+                    assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev);
+                    assert_date(
                         &type_option,
-                        DateCellChangesetPB {
-                            date: Some(1653609600.to_string()),
-                            time: None,
-                        },
-                        &field_type,
-                        &field_rev,
-                        "May 27,2022",
-                    );
-                    //
-                    assert_changeset_result(
-                        &type_option,
-                        DateCellChangesetPB {
-                            date: Some(1653609600.to_string()),
-                            time: Some("".to_owned()),
-                        },
-                        &field_type,
-                        &field_rev,
-                        "May 27,2022",
-                    );
-
-                    assert_changeset_result(
-                        &type_option,
-                        DateCellChangesetPB {
-                            date: Some(1653609600.to_string()),
-                            time: Some("11:23 pm".to_owned()),
-                        },
-                        &field_type,
-                        &field_rev,
+                        1653609600,
+                        Some("11:23 pm".to_owned()),
                         "May 27,2022 11:23 PM",
+                        &field_rev,
                     );
                 }
             }
@@ -118,141 +65,71 @@ mod tests {
     }
 
     #[test]
-    fn date_type_option_apply_changeset_test() {
-        let mut type_option = DateTypeOption::new();
+    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();
-        let date_timestamp = "1653609600".to_owned();
-
-        assert_changeset_result(
-            &type_option,
-            DateCellChangesetPB {
-                date: Some(date_timestamp.clone()),
-                time: None,
-            },
-            &field_type,
-            &field_rev,
-            "May 27,2022",
-        );
+        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;
-        assert_changeset_result(
-            &type_option,
-            DateCellChangesetPB {
-                date: Some(date_timestamp.clone()),
-                time: None,
-            },
-            &field_type,
-            &field_rev,
-            "May 27,2022",
-        );
+        let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
 
-        assert_changeset_result(
+        assert_date(
             &type_option,
-            DateCellChangesetPB {
-                date: Some(date_timestamp.clone()),
-                time: Some("1:00".to_owned()),
-            },
-            &field_type,
-            &field_rev,
+            1653609600,
+            Some("1:".to_owned()),
             "May 27,2022 01:00",
-        );
-
-        type_option.time_format = TimeFormat::TwelveHour;
-        assert_changeset_result(
-            &type_option,
-            DateCellChangesetPB {
-                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() {
+    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();
-        let date_timestamp = "1653609600".to_owned();
 
-        assert_changeset_result(
-            &type_option,
-            DateCellChangesetPB {
-                date: Some(date_timestamp.clone()),
-                time: Some("1:".to_owned()),
-            },
-            &FieldType::DateTime,
-            &field_rev,
-            "May 27,2022 01:00",
-        );
-
-        assert_changeset_result(
-            &type_option,
-            DateCellChangesetPB {
-                date: Some(date_timestamp),
-                time: Some("1:00".to_owned()),
-            },
-            &FieldType::DateTime,
-            &field_rev,
-            "May 27,2022 01:00",
-        );
+        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_to_twenty_four_hours() {
+    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();
-        let date_timestamp = "1653609600".to_owned();
 
-        assert_changeset_result(
+        assert_date(
             &type_option,
-            DateCellChangesetPB {
-                date: Some(date_timestamp),
-                time: Some("1:00 am".to_owned()),
-            },
-            &FieldType::DateTime,
+            1653609600,
+            Some("1:00 am".to_owned()),
+            "May 27,2022 01:00 AM",
             &field_rev,
-            "May 27,2022 01:00",
         );
     }
-
-    fn assert_changeset_result(
-        type_option: &DateTypeOption,
-        changeset: DateCellChangesetPB,
-        _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,
+    fn assert_date<T: ToString>(
         type_option: &DateTypeOption,
+        timestamp: T,
+        include_time_str: Option<String>,
+        expected_str: &str,
         field_rev: &FieldRevision,
-        expected: &str,
     ) {
         let s = serde_json::to_string(&DateCellChangesetPB {
             date: Some(timestamp.to_string()),
-            time: None,
+            time: include_time_str,
         })
         .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)
+            decode_cell_data(encoded_data, type_option, field_rev),
+            expected_str.to_owned(),
         );
     }
 

+ 39 - 32
frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs

@@ -7,25 +7,30 @@ mod tests {
     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();
-        assert_equal(&type_option, "", "", &field_type, &field_rev);
-        assert_equal(&type_option, "abc", "", &field_type, &field_rev);
+
+        // 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() {
-        let mut type_option = NumberTypeOption::new();
-        type_option.format = NumberFormat::USD;
+        // Remove the $ symbol
         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());
+        // 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();
@@ -36,25 +41,26 @@ mod tests {
             type_option.format = format;
             match format {
                 NumberFormat::Num => {
-                    assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
+                    assert_number(&type_option, "18443", "18443", &field_type, &field_rev);
                 }
                 NumberFormat::USD => {
-                    assert_equal(&type_option, "18443", "$18,443", &field_type, &field_rev);
+                    assert_number(&type_option, "18443", "$18,443", &field_type, &field_rev);
                 }
                 NumberFormat::Yen => {
-                    assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_rev);
+                    assert_number(&type_option, "18443", "¥18,443", &field_type, &field_rev);
                 }
                 NumberFormat::Yuan => {
-                    assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_rev);
+                    assert_number(&type_option, "18443", "CN¥18,443", &field_type, &field_rev);
                 }
                 NumberFormat::EUR => {
-                    assert_equal(&type_option, "18443", "€18.443", &field_type, &field_rev);
+                    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();
@@ -65,33 +71,34 @@ mod tests {
             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);
+                    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_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);
+                    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_equal(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev);
-                    assert_equal(&type_option, "¥1844", "¥1,844", &field_type, &field_rev);
+                    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_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);
+                    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_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);
+                    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 {
@@ -105,32 +112,32 @@ mod tests {
             type_option.format = format;
             match format {
                 NumberFormat::Num => {
-                    assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
+                    assert_number(&type_option, "18443", "18443", &field_type, &field_rev);
                 }
                 NumberFormat::USD => {
-                    assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_rev);
+                    assert_number(&type_option, "18443", "-$18,443", &field_type, &field_rev);
                 }
                 NumberFormat::Yen => {
-                    assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_rev);
+                    assert_number(&type_option, "18443", "-¥18,443", &field_type, &field_rev);
                 }
                 NumberFormat::EUR => {
-                    assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_rev);
+                    assert_number(&type_option, "18443", "-€18.443", &field_type, &field_rev);
                 }
                 _ => {}
             }
         }
     }
 
-    fn assert_equal(
+    fn assert_number(
         type_option: &NumberTypeOption,
-        cell_data: &str,
+        input_str: &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)
+                .decode_cell_data(input_str.to_owned().into(), field_type, field_rev)
                 .unwrap()
                 .to_string(),
             expected_str.to_owned()

+ 19 - 15
frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs

@@ -6,49 +6,53 @@ mod tests {
     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_test_no_url() {
+    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_changeset(&type_option, "123", &field_type, &field_rev, "123", "");
+        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_test_contains_url() {
+    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_changeset(
+        assert_url(
             &type_option,
             "AppFlowy website - https://www.appflowy.io",
-            &field_type,
-            &field_rev,
             "AppFlowy website - https://www.appflowy.io",
             "https://www.appflowy.io/",
+            &field_type,
+            &field_rev,
         );
 
-        assert_changeset(
+        assert_url(
             &type_option,
             "AppFlowy website appflowy.io",
-            &field_type,
-            &field_rev,
             "AppFlowy website appflowy.io",
             "https://appflowy.io",
+            &field_type,
+            &field_rev,
         );
     }
 
-    fn assert_changeset(
+    fn assert_url(
         type_option: &URLTypeOption,
-        cell_data: &str,
+        input_str: &str,
+        expected_str: &str,
+        expected_url: &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 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.to_owned(), decode_cell_data.content);
+        assert_eq!(expected_str.to_owned(), decode_cell_data.content);
         assert_eq!(expected_url.to_owned(), decode_cell_data.url);
     }
 

+ 2 - 2
frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs

@@ -2,7 +2,7 @@ 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::{NO, SELECTION_IDS_SEPARATOR};
+use flowy_grid::services::field::{SELECTION_IDS_SEPARATOR, UNCHECK};
 use flowy_grid_data_model::revision::RowMetaChangeset;
 
 #[tokio::test]
@@ -72,7 +72,7 @@ async fn grid_row_add_cells_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", NO);
+    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(