Browse Source

feat: tauri kanban fixes (#2273)

* chore: group cards count

* chore: delete board card

* chore: date time format read and update

* fix: move field

* fix: dnd fields

* chore: number format popup

* chore: refactor date options

* chore: replace button in DateFormatPopup with PopupItem

---------

Co-authored-by: qinluhe <[email protected]>
Askarbek Zadauly 2 years ago
parent
commit
55cb7acc7f
18 changed files with 674 additions and 84 deletions
  1. 6 0
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  2. 96 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DateFormatPopup.tsx
  3. 4 29
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx
  4. 35 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DateTimeFormat.hooks.ts
  5. 149 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DateTypeOptions.tsx
  6. 2 1
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx
  7. 2 2
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellWrapper.tsx
  8. 59 15
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx
  9. 25 1
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx
  10. 26 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/NumberFormat.hooks.ts
  11. 108 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/NumberFormatPopup.tsx
  12. 72 0
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/TimeFormatPopup.tsx
  13. 3 3
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts
  14. 7 3
      frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts
  15. 70 26
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCard.tsx
  16. 1 1
      frontend/appflowy_tauri/src/appflowy_app/components/board/BoardGroup.tsx
  17. 3 3
      frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts
  18. 6 0
      frontend/rust-lib/Cargo.lock

+ 6 - 0
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -640,6 +640,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "anyhow",
  "bytes",
@@ -657,6 +658,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "anyhow",
  "chrono",
@@ -678,6 +680,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -689,6 +692,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "anyhow",
  "collab",
@@ -705,6 +709,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "anyhow",
  "collab",
@@ -722,6 +727,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "bincode",
  "chrono",

+ 96 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DateFormatPopup.tsx

@@ -0,0 +1,96 @@
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+import { PopupWindow } from '$app/components/_shared/PopupWindow';
+import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
+import { useTranslation } from 'react-i18next';
+import { DateFormatPB } from '@/services/backend';
+import { useDateTimeFormat } from '$app/components/_shared/EditRow/DateTimeFormat.hooks';
+import { useAppSelector } from '$app/stores/store';
+import { useEffect, useState } from 'react';
+import { IDateType } from '$app/stores/reducers/database/slice';
+
+export const DateFormatPopup = ({
+  left,
+  top,
+  cellIdentifier,
+  fieldController,
+  onOutsideClick,
+}: {
+  left: number;
+  top: number;
+  cellIdentifier: CellIdentifier;
+  fieldController: FieldController;
+  onOutsideClick: () => void;
+}) => {
+  const { t } = useTranslation('');
+  const { changeDateFormat } = useDateTimeFormat(cellIdentifier, fieldController);
+  const databaseStore = useAppSelector((state) => state.database);
+  const [dateType, setDateType] = useState<IDateType | undefined>();
+
+  useEffect(() => {
+    setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType);
+  }, [databaseStore]);
+
+  const changeFormat = async (format: DateFormatPB) => {
+    await changeDateFormat(format);
+    onOutsideClick();
+  };
+
+  return (
+    <PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
+      <PopupItem
+        changeFormat={changeFormat}
+        format={DateFormatPB.Friendly}
+        checked={dateType?.dateFormat === DateFormatPB.Friendly}
+        text={t('grid.field.dateFormatFriendly')}
+      />
+      <PopupItem
+        changeFormat={changeFormat}
+        format={DateFormatPB.ISO}
+        checked={dateType?.dateFormat === DateFormatPB.ISO}
+        text={t('grid.field.dateFormatISO')}
+      />
+      <PopupItem
+        changeFormat={changeFormat}
+        format={DateFormatPB.Local}
+        checked={dateType?.dateFormat === DateFormatPB.Local}
+        text={t('grid.field.dateFormatLocal')}
+      />
+      <PopupItem
+        changeFormat={changeFormat}
+        format={DateFormatPB.US}
+        checked={dateType?.dateFormat === DateFormatPB.US}
+        text={t('grid.field.dateFormatUS')}
+      />
+    </PopupWindow>
+  );
+};
+
+function PopupItem({
+  format,
+  text,
+  changeFormat,
+  checked,
+}: {
+  format: DateFormatPB;
+  text: string;
+  changeFormat: (_: DateFormatPB) => Promise<void>;
+  checked: boolean;
+}) {
+  return (
+    <button
+      onClick={() => changeFormat(format)}
+      className={
+        'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary'
+      }
+    >
+      {text}
+
+      {checked && (
+        <div className={'ml-8 h-5 w-5 p-1'}>
+          <CheckmarkSvg></CheckmarkSvg>
+        </div>
+      )}
+    </button>
+  );
+}

+ 4 - 29
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx

@@ -1,17 +1,14 @@
 import { useEffect, useState } from 'react';
-import { useTranslation } from 'react-i18next';
 import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
 import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
 import { FieldController } from '$app/stores/effects/database/field/field_controller';
 import Calendar from 'react-calendar';
 import dayjs from 'dayjs';
-import { ClockSvg } from '$app/components/_shared/svg/ClockSvg';
-import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
-import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
 import { useCell } from '$app/components/_shared/database-hooks/useCell';
 import { CalendarData } from '$app/stores/effects/database/cell/controller_builder';
 import { DateCellDataPB } from '@/services/backend';
 import { PopupWindow } from '$app/components/_shared/PopupWindow';
+import { DateTypeOptions } from '$app/components/_shared/EditRow/DateTypeOptions';
 
 export const DatePickerPopup = ({
   left,
@@ -29,15 +26,13 @@ export const DatePickerPopup = ({
   onOutsideClick: () => void;
 }) => {
   const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
-  const { t } = useTranslation('');
   const [selectedDate, setSelectedDate] = useState<Date>(new Date());
 
   useEffect(() => {
     const date_pb = data as DateCellDataPB | undefined;
     if (!date_pb || !date_pb?.date.length) return;
 
-    // should be changed after we can modify date format
-    setSelectedDate(dayjs(date_pb.date, 'MMM DD, YYYY').toDate());
+    setSelectedDate(dayjs(date_pb.date).toDate());
   }, [data]);
 
   const onChange = async (v: Date | null | (Date | null)[]) => {
@@ -50,30 +45,10 @@ export const DatePickerPopup = ({
 
   return (
     <PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
-      <div className={'px-2'}>
+      <div className={'px-2 pb-2'}>
         <Calendar onChange={(d) => onChange(d)} value={selectedDate} />
       </div>
-      <hr className={'-mx-2 my-4 border-shade-6'} />
-      <div className={'flex items-center justify-between px-4'}>
-        <div className={'flex items-center gap-2'}>
-          <i className={'h-4 w-4'}>
-            <ClockSvg></ClockSvg>
-          </i>
-          <span>{t('grid.field.includeTime')}</span>
-        </div>
-        <i className={'h-5 w-5'}>
-          <EditorUncheckSvg></EditorUncheckSvg>
-        </i>
-      </div>
-      <hr className={'-mx-2 my-4 border-shade-6'} />
-      <div className={'flex items-center justify-between px-4 pb-2'}>
-        <span>
-          {t('grid.field.dateFormat')} & {t('grid.field.timeFormat')}
-        </span>
-        <i className={'h-5 w-5'}>
-          <MoreSvg></MoreSvg>
-        </i>
-      </div>
+      <DateTypeOptions cellIdentifier={cellIdentifier} fieldController={fieldController}></DateTypeOptions>
     </PopupWindow>
   );
 };

+ 35 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DateTimeFormat.hooks.ts

@@ -0,0 +1,35 @@
+import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
+import { Some } from 'ts-results';
+import { DateFormatPB, DateTypeOptionPB, FieldType, TimeFormatPB } from '@/services/backend';
+import { makeDateTypeOptionContext } from '$app/stores/effects/database/field/type_option/type_option_context';
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+
+export const useDateTimeFormat = (cellIdentifier: CellIdentifier, fieldController: FieldController) => {
+  const changeFormat = async (change: (option: DateTypeOptionPB) => void) => {
+    const fieldInfo = fieldController.getField(cellIdentifier.fieldId);
+    if (!fieldInfo) return;
+    const typeOptionController = new TypeOptionController(cellIdentifier.viewId, Some(fieldInfo), FieldType.DateTime);
+    await typeOptionController.initialize();
+    const dateTypeOptionContext = makeDateTypeOptionContext(typeOptionController);
+    const typeOption = await dateTypeOptionContext.getTypeOption().then((a) => a.unwrap());
+    change(typeOption);
+    await dateTypeOptionContext.setTypeOption(typeOption);
+  };
+
+  const changeDateFormat = async (format: DateFormatPB) => {
+    await changeFormat((option) => (option.date_format = format));
+  };
+  const changeTimeFormat = async (format: TimeFormatPB) => {
+    await changeFormat((option) => (option.time_format = format));
+  };
+  const includeTime = async (include: boolean) => {
+    await changeFormat((option) => (option.include_time = include));
+  };
+
+  return {
+    changeDateFormat,
+    changeTimeFormat,
+    includeTime,
+  };
+};

+ 149 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DateTypeOptions.tsx

@@ -0,0 +1,149 @@
+import { DateFormatPopup } from '$app/components/_shared/EditRow/DateFormatPopup';
+import { TimeFormatPopup } from '$app/components/_shared/EditRow/TimeFormatPopup';
+import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
+import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
+import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
+import { MouseEventHandler, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { IDateType } from '$app/stores/reducers/database/slice';
+import { useAppSelector } from '$app/stores/store';
+import { useDateTimeFormat } from '$app/components/_shared/EditRow/DateTimeFormat.hooks';
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+
+export const DateTypeOptions = ({
+  cellIdentifier,
+  fieldController,
+}: {
+  cellIdentifier: CellIdentifier;
+  fieldController: FieldController;
+}) => {
+  const { t } = useTranslation('');
+
+  const [showDateFormatPopup, setShowDateFormatPopup] = useState(false);
+  const [dateFormatTop, setDateFormatTop] = useState(0);
+  const [dateFormatLeft, setDateFormatLeft] = useState(0);
+
+  const [showTimeFormatPopup, setShowTimeFormatPopup] = useState(false);
+  const [timeFormatTop, setTimeFormatTop] = useState(0);
+  const [timeFormatLeft, setTimeFormatLeft] = useState(0);
+
+  const [dateType, setDateType] = useState<IDateType | undefined>();
+
+  const databaseStore = useAppSelector((state) => state.database);
+  const { includeTime } = useDateTimeFormat(cellIdentifier, fieldController);
+
+  useEffect(() => {
+    setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType);
+  }, [databaseStore]);
+
+  const onDateFormatClick = (_left: number, _top: number) => {
+    setShowDateFormatPopup(true);
+    setDateFormatLeft(_left + 10);
+    setDateFormatTop(_top);
+  };
+
+  const onTimeFormatClick = (_left: number, _top: number) => {
+    setShowTimeFormatPopup(true);
+    setTimeFormatLeft(_left + 10);
+    setTimeFormatTop(_top);
+  };
+
+  const _onDateFormatClick: MouseEventHandler = (e) => {
+    e.stopPropagation();
+    let target = e.target as HTMLElement;
+
+    while (!(target instanceof HTMLButtonElement)) {
+      if (target.parentElement === null) return;
+      target = target.parentElement;
+    }
+
+    const { right: _left, top: _top } = target.getBoundingClientRect();
+    onDateFormatClick(_left, _top);
+  };
+
+  const _onTimeFormatClick: MouseEventHandler = (e) => {
+    e.stopPropagation();
+    let target = e.target as HTMLElement;
+
+    while (!(target instanceof HTMLButtonElement)) {
+      if (target.parentElement === null) return;
+      target = target.parentElement;
+    }
+
+    const { right: _left, top: _top } = target.getBoundingClientRect();
+    onTimeFormatClick(_left, _top);
+  };
+
+  const toggleIncludeTime = async () => {
+    if (dateType?.includeTime) {
+      await includeTime(false);
+    } else {
+      await includeTime(true);
+    }
+  };
+
+  return (
+    <div className={'flex flex-col'}>
+      <hr className={'-mx-2 my-2 border-shade-6'} />
+      <button
+        onClick={_onDateFormatClick}
+        className={
+          'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-main-secondary'
+        }
+      >
+        <span>{t('grid.field.dateFormat')}</span>
+        <i className={'h-5 w-5'}>
+          <MoreSvg></MoreSvg>
+        </i>
+      </button>
+      <hr className={'-mx-2 my-2 border-shade-6'} />
+      <button
+        onClick={() => toggleIncludeTime()}
+        className={
+          'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-main-secondary'
+        }
+      >
+        <div className={'flex items-center gap-2'}>
+          {/*<i className={'h-4 w-4'}>
+            <ClockSvg></ClockSvg>
+          </i>*/}
+          <span>{t('grid.field.includeTime')}</span>
+        </div>
+        <i className={'h-5 w-5'}>
+          {dateType?.includeTime ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
+        </i>
+      </button>
+
+      <button
+        onClick={_onTimeFormatClick}
+        className={
+          'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-main-secondary'
+        }
+      >
+        <span>{t('grid.field.timeFormat')}</span>
+        <i className={'h-5 w-5'}>
+          <MoreSvg></MoreSvg>
+        </i>
+      </button>
+      {showDateFormatPopup && (
+        <DateFormatPopup
+          top={dateFormatTop}
+          left={dateFormatLeft}
+          cellIdentifier={cellIdentifier}
+          fieldController={fieldController}
+          onOutsideClick={() => setShowDateFormatPopup(false)}
+        ></DateFormatPopup>
+      )}
+      {showTimeFormatPopup && (
+        <TimeFormatPopup
+          top={timeFormatTop}
+          left={timeFormatLeft}
+          cellIdentifier={cellIdentifier}
+          fieldController={fieldController}
+          onOutsideClick={() => setShowTimeFormatPopup(false)}
+        ></TimeFormatPopup>
+      )}
+    </div>
+  );
+};

+ 2 - 1
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx

@@ -1,5 +1,5 @@
 import { CellController } from '$app/stores/effects/database/cell/cell_controller';
-import { useEffect, useState, KeyboardEvent, useMemo } from 'react';
+import { useEffect, useState } from 'react';
 
 export const EditCellText = ({
   data,
@@ -16,6 +16,7 @@ export const EditCellText = ({
   }, [data]);
 
   useEffect(() => {
+    if (!value?.length) return;
     setContentRows(Math.max(1, (value || '').split('\n').length));
   }, [value]);
 

+ 2 - 2
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellWrapper.tsx

@@ -42,7 +42,7 @@ export const EditCellWrapper = ({
   };
 
   return (
-    <Draggable draggableId={cellIdentifier.fieldId} index={index}>
+    <Draggable draggableId={cellIdentifier.fieldId} index={index} key={cellIdentifier.fieldId}>
       {(provided) => (
         <div
           ref={provided.innerRef}
@@ -61,7 +61,7 @@ export const EditCellWrapper = ({
               <FieldTypeIcon fieldType={cellIdentifier.fieldType}></FieldTypeIcon>
             </div>
             <span className={'overflow-hidden text-ellipsis whitespace-nowrap'}>
-              {databaseStore.fields[cellIdentifier.fieldId].title}
+              {databaseStore.fields[cellIdentifier.fieldId]?.title || ''}
             </span>
           </div>
           <div className={'flex-1 cursor-pointer rounded-lg hover:bg-shade-6'}>

+ 59 - 15
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx

@@ -1,15 +1,17 @@
-import { useEffect, useRef, useState } from 'react';
+import { MouseEventHandler, useEffect, useRef, useState } from 'react';
 import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
 import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
 import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
 import { useTranslation } from 'react-i18next';
 import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
 import { Some } from 'ts-results';
-import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
+import { FieldController, FieldInfo } from '$app/stores/effects/database/field/field_controller';
 import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
 import { useAppSelector } from '$app/stores/store';
 import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
 import { PopupWindow } from '$app/components/_shared/PopupWindow';
+import { FieldType } from '@/services/backend';
+import { DateTypeOptions } from '$app/components/_shared/EditRow/DateTypeOptions';
 
 export const EditFieldPopup = ({
   top,
@@ -18,7 +20,9 @@ export const EditFieldPopup = ({
   viewId,
   onOutsideClick,
   fieldInfo,
+  fieldController,
   changeFieldTypeClick,
+  onNumberFormat,
 }: {
   top: number;
   left: number;
@@ -26,7 +30,9 @@ export const EditFieldPopup = ({
   viewId: string;
   onOutsideClick: () => void;
   fieldInfo: FieldInfo | undefined;
+  fieldController?: FieldController;
   changeFieldTypeClick: (buttonTop: number, buttonRight: number) => void;
+  onNumberFormat?: (buttonLeft: number, buttonTop: number) => void;
 }) => {
   const databaseStore = useAppSelector((state) => state.database);
   const { t } = useTranslation('');
@@ -59,6 +65,19 @@ export const EditFieldPopup = ({
     onOutsideClick();
   };
 
+  const onNumberFormatClick: MouseEventHandler = (e) => {
+    e.stopPropagation();
+    let target = e.target as HTMLElement;
+
+    while (!(target instanceof HTMLButtonElement)) {
+      if (target.parentElement === null) return;
+      target = target.parentElement;
+    }
+
+    const { right: _left, top: _top } = target.getBoundingClientRect();
+    onNumberFormat?.(_left, _top);
+  };
+
   return (
     <PopupWindow
       className={'px-2 py-2 text-xs'}
@@ -69,7 +88,7 @@ export const EditFieldPopup = ({
       left={left}
       top={top}
     >
-      <div className={'flex flex-col gap-2 p-2'}>
+      <div className={'flex flex-col gap-2'}>
         <input
           value={name}
           onChange={(e) => setName(e.target.value)}
@@ -79,24 +98,24 @@ export const EditFieldPopup = ({
 
         <button
           onClick={() => onDeleteFieldClick()}
-          className={
-            'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-main-alert hover:bg-main-secondary'
-          }
+          className={'flex cursor-pointer items-center gap-2 rounded-lg py-2 text-main-alert hover:bg-main-secondary'}
         >
-          <i className={'h-5 w-5'}>
-            <TrashSvg></TrashSvg>
-          </i>
-          <span>{t('grid.field.delete')}</span>
+          <span className={'flex items-center gap-2 pl-2'}>
+            <i className={'block h-5 w-5'}>
+              <TrashSvg></TrashSvg>
+            </i>
+            <span>{t('grid.field.delete')}</span>
+          </span>
         </button>
 
         <div
           ref={changeTypeButtonRef}
           onClick={() => onChangeFieldTypeClick()}
           className={
-            'relative flex cursor-pointer items-center justify-between rounded-lg text-black hover:bg-main-secondary'
+            'relative flex cursor-pointer items-center justify-between rounded-lg py-2 text-black hover:bg-main-secondary'
           }
         >
-          <button className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2'}>
+          <button className={'flex cursor-pointer items-center gap-2 rounded-lg pl-2'}>
             <i className={'h-5 w-5'}>
               <FieldTypeIcon fieldType={cellIdentifier.fieldType}></FieldTypeIcon>
             </i>
@@ -104,10 +123,35 @@ export const EditFieldPopup = ({
               <FieldTypeName fieldType={cellIdentifier.fieldType}></FieldTypeName>
             </span>
           </button>
-          <i className={'h-5 w-5'}>
-            <MoreSvg></MoreSvg>
-          </i>
+          <span className={'pr-2'}>
+            <i className={' block h-5 w-5'}>
+              <MoreSvg></MoreSvg>
+            </i>
+          </span>
         </div>
+
+        {cellIdentifier.fieldType === FieldType.Number && (
+          <>
+            <hr className={'-mx-2 border-shade-6'} />
+            <button
+              onClick={onNumberFormatClick}
+              className={
+                'flex w-full cursor-pointer items-center justify-between rounded-lg py-2 hover:bg-main-secondary'
+              }
+            >
+              <span className={'pl-2'}>{t('grid.field.numberFormat')}</span>
+              <span className={'pr-2'}>
+                <i className={'block h-5 w-5'}>
+                  <MoreSvg></MoreSvg>
+                </i>
+              </span>
+            </button>
+          </>
+        )}
+
+        {cellIdentifier.fieldType === FieldType.DateTime && fieldController && (
+          <DateTypeOptions cellIdentifier={cellIdentifier} fieldController={fieldController}></DateTypeOptions>
+        )}
       </div>
     </PopupWindow>
   );

+ 25 - 1
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx

@@ -16,6 +16,7 @@ import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPop
 import { DatePickerPopup } from '$app/components/_shared/EditRow/DatePickerPopup';
 import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd';
 import { EditCellOptionPopup } from '$app/components/_shared/EditRow/EditCellOptionPopup';
+import { NumberFormatPopup } from '$app/components/_shared/EditRow/NumberFormatPopup';
 
 export const EditRow = ({
   onClose,
@@ -55,6 +56,10 @@ export const EditRow = ({
 
   const [editingSelectOption, setEditingSelectOption] = useState<SelectOptionPB | undefined>();
 
+  const [showNumberFormatPopup, setShowNumberFormatPopup] = useState(false);
+  const [numberFormatTop, setNumberFormatTop] = useState(0);
+  const [numberFormatLeft, setNumberFormatLeft] = useState(0);
+
   useEffect(() => {
     setUnveil(true);
   }, []);
@@ -120,10 +125,16 @@ export const EditRow = ({
     setEditCellOptionTop(_top);
   };
 
+  const onNumberFormat = (_left: number, _top: number) => {
+    setShowNumberFormatPopup(true);
+    setNumberFormatLeft(_left + 10);
+    setNumberFormatTop(_top);
+  };
+
   const onDragEnd: OnDragEndResponder = (result) => {
     if (!result.destination?.index) return;
     void controller.moveField({
-      fieldId: result.source.droppableId,
+      fieldId: result.draggableId,
       fromIndex: result.source.index,
       toIndex: result.destination.index,
     });
@@ -195,7 +206,9 @@ export const EditRow = ({
             viewId={viewId}
             onOutsideClick={onOutsideEditFieldClick}
             fieldInfo={controller.fieldController.getField(editingCell.fieldId)}
+            fieldController={controller.fieldController}
             changeFieldTypeClick={onChangeFieldTypeClick}
+            onNumberFormat={onNumberFormat}
           ></EditFieldPopup>
         )}
         {showChangeFieldTypePopup && (
@@ -238,6 +251,17 @@ export const EditRow = ({
             }}
           ></EditCellOptionPopup>
         )}
+        {showNumberFormatPopup && editingCell && (
+          <NumberFormatPopup
+            top={numberFormatTop}
+            left={numberFormatLeft}
+            cellIdentifier={editingCell}
+            fieldController={controller.fieldController}
+            onOutsideClick={() => {
+              setShowNumberFormatPopup(false);
+            }}
+          ></NumberFormatPopup>
+        )}
       </div>
     </div>
   );

+ 26 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/NumberFormat.hooks.ts

@@ -0,0 +1,26 @@
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+import { FieldType, NumberFormatPB } from '@/services/backend';
+import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
+import { Some } from 'ts-results';
+import {
+  makeDateTypeOptionContext,
+  makeNumberTypeOptionContext,
+} from '$app/stores/effects/database/field/type_option/type_option_context';
+
+export const useNumberFormat = (cellIdentifier: CellIdentifier, fieldController: FieldController) => {
+  const changeNumberFormat = async (format: NumberFormatPB) => {
+    const fieldInfo = fieldController.getField(cellIdentifier.fieldId);
+    if (!fieldInfo) return;
+    const typeOptionController = new TypeOptionController(cellIdentifier.viewId, Some(fieldInfo), FieldType.Number);
+    await typeOptionController.initialize();
+    const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController);
+    const typeOption = await numberTypeOptionContext.getTypeOption().then((a) => a.unwrap());
+    typeOption.format = format;
+    await numberTypeOptionContext.setTypeOption(typeOption);
+  };
+
+  return {
+    changeNumberFormat,
+  };
+};

+ 108 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/NumberFormatPopup.tsx

@@ -0,0 +1,108 @@
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+import { PopupWindow } from '$app/components/_shared/PopupWindow';
+import { useNumberFormat } from '$app/components/_shared/EditRow/NumberFormat.hooks';
+import { NumberFormatPB } from '@/services/backend';
+import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
+import { useAppSelector } from '$app/stores/store';
+import { useEffect, useState } from 'react';
+import { INumberType } from '$app/stores/reducers/database/slice';
+
+const list = [
+  { format: NumberFormatPB.Num, title: 'Num' },
+  { format: NumberFormatPB.USD, title: 'USD' },
+  { format: NumberFormatPB.CanadianDollar, title: 'CanadianDollar' },
+  { format: NumberFormatPB.EUR, title: 'EUR' },
+  { format: NumberFormatPB.Pound, title: 'Pound' },
+  { format: NumberFormatPB.Yen, title: 'Yen' },
+  { format: NumberFormatPB.Ruble, title: 'Ruble' },
+  { format: NumberFormatPB.Rupee, title: 'Rupee' },
+  { format: NumberFormatPB.Won, title: 'Won' },
+  { format: NumberFormatPB.Yuan, title: 'Yuan' },
+  { format: NumberFormatPB.Real, title: 'Real' },
+  { format: NumberFormatPB.Lira, title: 'Lira' },
+  { format: NumberFormatPB.Rupiah, title: 'Rupiah' },
+  { format: NumberFormatPB.Franc, title: 'Franc' },
+  { format: NumberFormatPB.HongKongDollar, title: 'HongKongDollar' },
+  { format: NumberFormatPB.NewZealandDollar, title: 'NewZealandDollar' },
+  { format: NumberFormatPB.Krona, title: 'Krona' },
+  { format: NumberFormatPB.NorwegianKrone, title: 'NorwegianKrone' },
+  { format: NumberFormatPB.MexicanPeso, title: 'MexicanPeso' },
+  { format: NumberFormatPB.Rand, title: 'Rand' },
+  { format: NumberFormatPB.NewTaiwanDollar, title: 'NewTaiwanDollar' },
+  { format: NumberFormatPB.DanishKrone, title: 'DanishKrone' },
+  { format: NumberFormatPB.Baht, title: 'Baht' },
+  { format: NumberFormatPB.Forint, title: 'Forint' },
+  { format: NumberFormatPB.Koruna, title: 'Koruna' },
+  { format: NumberFormatPB.Shekel, title: 'Shekel' },
+  { format: NumberFormatPB.ChileanPeso, title: 'ChileanPeso' },
+  { format: NumberFormatPB.PhilippinePeso, title: 'PhilippinePeso' },
+  { format: NumberFormatPB.Dirham, title: 'Dirham' },
+  { format: NumberFormatPB.ColombianPeso, title: 'ColombianPeso' },
+  { format: NumberFormatPB.Riyal, title: 'Riyal' },
+  { format: NumberFormatPB.Ringgit, title: 'Ringgit' },
+  { format: NumberFormatPB.Leu, title: 'Leu' },
+  { format: NumberFormatPB.ArgentinePeso, title: 'ArgentinePeso' },
+  { format: NumberFormatPB.UruguayanPeso, title: 'UruguayanPeso' },
+  { format: NumberFormatPB.Percent, title: 'Percent' },
+];
+
+export const NumberFormatPopup = ({
+  left,
+  top,
+  cellIdentifier,
+  fieldController,
+  onOutsideClick,
+}: {
+  left: number;
+  top: number;
+  cellIdentifier: CellIdentifier;
+  fieldController: FieldController;
+  onOutsideClick: () => void;
+}) => {
+  const { changeNumberFormat } = useNumberFormat(cellIdentifier, fieldController);
+  const databaseStore = useAppSelector((state) => state.database);
+  const [numberType, setNumberType] = useState<INumberType | undefined>();
+
+  useEffect(() => {
+    setNumberType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as INumberType);
+  }, [databaseStore]);
+
+  const changeNumberFormatClick = async (format: NumberFormatPB) => {
+    await changeNumberFormat(format);
+    onOutsideClick();
+  };
+
+  return (
+    <PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
+      <div className={'h-[400px] overflow-auto'}>
+        {list.map((item, index) => (
+          <FormatButton
+            key={index}
+            title={item.title}
+            checked={numberType?.numberFormat === item.format}
+            onClick={() => changeNumberFormatClick(item.format)}
+          ></FormatButton>
+        ))}
+      </div>
+    </PopupWindow>
+  );
+};
+
+const FormatButton = ({ title, checked, onClick }: { title: string; checked: boolean; onClick: () => void }) => {
+  return (
+    <button
+      onClick={() => onClick()}
+      className={
+        'flex w-full cursor-pointer items-center justify-between rounded-lg py-1.5 px-2 hover:bg-main-secondary'
+      }
+    >
+      <span className={'block pr-8'}>{title}</span>
+      {checked && (
+        <div className={'h-5 w-5 p-1'}>
+          <CheckmarkSvg></CheckmarkSvg>
+        </div>
+      )}
+    </button>
+  );
+};

+ 72 - 0
frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/TimeFormatPopup.tsx

@@ -0,0 +1,72 @@
+import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
+import { FieldController } from '$app/stores/effects/database/field/field_controller';
+import { useTranslation } from 'react-i18next';
+import { PopupWindow } from '$app/components/_shared/PopupWindow';
+import { TimeFormatPB } from '@/services/backend';
+import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
+import { useDateTimeFormat } from '$app/components/_shared/EditRow/DateTimeFormat.hooks';
+import { useAppSelector } from '$app/stores/store';
+import { useEffect, useState } from 'react';
+import { IDateType } from '$app/stores/reducers/database/slice';
+
+export const TimeFormatPopup = ({
+  left,
+  top,
+  cellIdentifier,
+  fieldController,
+  onOutsideClick,
+}: {
+  left: number;
+  top: number;
+  cellIdentifier: CellIdentifier;
+  fieldController: FieldController;
+  onOutsideClick: () => void;
+}) => {
+  const { t } = useTranslation('');
+  const databaseStore = useAppSelector((state) => state.database);
+  const [dateType, setDateType] = useState<IDateType | undefined>();
+
+  useEffect(() => {
+    setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType);
+  }, [databaseStore]);
+
+  const { changeTimeFormat } = useDateTimeFormat(cellIdentifier, fieldController);
+
+  const changeFormat = async (format: TimeFormatPB) => {
+    await changeTimeFormat(format);
+    onOutsideClick();
+  };
+
+  return (
+    <PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
+      <button
+        onClick={() => changeFormat(TimeFormatPB.TwelveHour)}
+        className={
+          'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary'
+        }
+      >
+        {t('grid.field.timeFormatTwelveHour')}
+
+        {dateType?.timeFormat === TimeFormatPB.TwelveHour && (
+          <div className={'ml-8 h-5 w-5 p-1'}>
+            <CheckmarkSvg></CheckmarkSvg>
+          </div>
+        )}
+      </button>
+      <button
+        onClick={() => changeFormat(TimeFormatPB.TwentyFourHour)}
+        className={
+          'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary'
+        }
+      >
+        {t('grid.field.timeFormatTwentyFourHour')}
+
+        {dateType?.timeFormat === TimeFormatPB.TwentyFourHour && (
+          <div className={'ml-8 h-5 w-5 p-1'}>
+            <CheckmarkSvg></CheckmarkSvg>
+          </div>
+        )}
+      </button>
+    </PopupWindow>
+  );
+};

+ 3 - 3
frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/loadField.ts

@@ -70,7 +70,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
         title: field.name,
         fieldType: field.field_type,
         fieldOptions: {
-          NumberFormatPB: typeOption.format,
+          numberFormat: typeOption.format,
         },
       };
     }
@@ -82,8 +82,8 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
         title: field.name,
         fieldType: field.field_type,
         fieldOptions: {
-          DateFormatPB: typeOption.date_format,
-          TimeFormatPB: typeOption.time_format,
+          dateFormat: typeOption.date_format,
+          timeFormat: typeOption.time_format,
           includeTime: typeOption.include_time,
         },
       };

+ 7 - 3
frontend/appflowy_tauri/src/appflowy_app/components/_shared/database-hooks/useCell.ts

@@ -25,9 +25,13 @@ export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fi
     });
 
     void (async () => {
-      const cellData = await c.getCellData();
-      if (cellData.some) {
-        setData(cellData.unwrap());
+      try {
+        const cellData = await c.getCellData();
+        if (cellData.some) {
+          setData(cellData.unwrap());
+        }
+      } catch (e) {
+        // mute for now
       }
     })();
 

+ 70 - 26
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardCard.tsx

@@ -4,7 +4,10 @@ import { useRow } from '../_shared/database-hooks/useRow';
 import { DatabaseController } from '$app/stores/effects/database/database_controller';
 import { BoardCell } from './BoardCell';
 import { Draggable } from 'react-beautiful-dnd';
-import { MouseEventHandler } from 'react';
+import { MouseEventHandler, useState } from 'react';
+import { PopupWindow } from '$app/components/_shared/PopupWindow';
+import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
+import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
 
 export const BoardCard = ({
   index,
@@ -23,38 +26,79 @@ export const BoardCard = ({
 }) => {
   const { cells } = useRow(viewId, controller, rowInfo);
 
+  const [showCardPopup, setShowCardPopup] = useState(false);
+  const [cardPopupLeft, setCardPopupLeft] = useState(0);
+  const [cardPopupTop, setCardPopupTop] = useState(0);
+
   const onDetailClick: MouseEventHandler = (e) => {
     e.stopPropagation();
-    // onOpenRow(rowInfo);
+    let target = e.target as HTMLElement;
+
+    while (!(target instanceof HTMLButtonElement)) {
+      if (target.parentElement === null) return;
+      target = target.parentElement;
+    }
+
+    const { right: left, top } = target.getBoundingClientRect();
+    setCardPopupLeft(left);
+    setCardPopupTop(top);
+    setShowCardPopup(true);
+  };
+
+  const onDeleteRowClick = async () => {
+    setShowCardPopup(false);
+    const svc = new RowBackendService(viewId);
+    await svc.deleteRow(rowInfo.row.id);
   };
 
   return (
-    <Draggable draggableId={rowInfo.row.id} index={index}>
-      {(provided) => (
-        <div
-          ref={provided.innerRef}
-          {...provided.draggableProps}
-          {...provided.dragHandleProps}
-          onClick={() => onOpenRow(rowInfo)}
-          className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `}
+    <>
+      <Draggable draggableId={rowInfo.row.id} key={rowInfo.row.id} index={index}>
+        {(provided) => (
+          <div
+            ref={provided.innerRef}
+            {...provided.draggableProps}
+            {...provided.dragHandleProps}
+            onClick={() => onOpenRow(rowInfo)}
+            className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `}
+          >
+            <button onClick={onDetailClick} className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
+              <Details2Svg></Details2Svg>
+            </button>
+            <div className={'flex flex-col gap-3'}>
+              {cells
+                .filter((cell) => cell.fieldId !== groupByFieldId)
+                .map((cell, cellIndex) => (
+                  <BoardCell
+                    key={cellIndex}
+                    cellIdentifier={cell.cellIdentifier}
+                    cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
+                    fieldController={controller.fieldController}
+                  ></BoardCell>
+                ))}
+            </div>
+          </div>
+        )}
+      </Draggable>
+      {showCardPopup && (
+        <PopupWindow
+          className={'p-2 text-xs'}
+          onOutsideClick={() => setShowCardPopup(false)}
+          left={cardPopupLeft}
+          top={cardPopupTop}
         >
-          <button onClick={onDetailClick} className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
-            <Details2Svg></Details2Svg>
+          <button
+            key={index}
+            className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-main-secondary'}
+            onClick={() => onDeleteRowClick()}
+          >
+            <i className={'h-5 w-5'}>
+              <TrashSvg></TrashSvg>
+            </i>
+            <span className={'flex-shrink-0'}>Delete</span>
           </button>
-          <div className={'flex flex-col gap-3'}>
-            {cells
-              .filter((cell) => cell.fieldId !== groupByFieldId)
-              .map((cell, cellIndex) => (
-                <BoardCell
-                  key={cellIndex}
-                  cellIdentifier={cell.cellIdentifier}
-                  cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
-                  fieldController={controller.fieldController}
-                ></BoardCell>
-              ))}
-          </div>
-        </div>
+        </PopupWindow>
       )}
-    </Draggable>
+    </>
   );
 };

+ 1 - 1
frontend/appflowy_tauri/src/appflowy_app/components/board/BoardGroup.tsx

@@ -28,7 +28,7 @@ export const BoardGroup = ({
       <div className={'flex items-center justify-between p-4'}>
         <div className={'flex items-center gap-2'}>
           <span>{group.name}</span>
-          <span className={'text-shade-4'}>()</span>
+          <span className={'text-shade-4'}>({group.rows.length})</span>
         </div>
         <div className={'flex items-center gap-2'}>
           <button className={'h-5 w-5 rounded hover:bg-surface-2'}>

+ 3 - 3
frontend/appflowy_tauri/src/appflowy_app/stores/reducers/database/slice.ts

@@ -13,13 +13,13 @@ export interface ISelectOptionType {
 }
 
 export interface IDateType {
-  DateFormatPB: DateFormatPB;
-  TimeFormatPB: TimeFormatPB;
+  dateFormat: DateFormatPB;
+  timeFormat: TimeFormatPB;
   includeTime: boolean;
 }
 
 export interface INumberType {
-  NumberFormatPB: NumberFormatPB;
+  numberFormat: NumberFormatPB;
 }
 
 export interface IDatabaseField {

+ 6 - 0
frontend/rust-lib/Cargo.lock

@@ -544,6 +544,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "anyhow",
  "bytes",
@@ -561,6 +562,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "anyhow",
  "chrono",
@@ -582,6 +584,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -593,6 +596,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "anyhow",
  "collab",
@@ -609,6 +613,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "anyhow",
  "collab",
@@ -626,6 +631,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=935868#93586873d1982d3b4ab96993a39810e4bb4d1993"
 dependencies = [
  "bincode",
  "chrono",