diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 9cac4d87c2..6f9ba16df5 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -23,6 +23,7 @@ "@slate-yjs/core": "^0.3.1", "@tanstack/react-virtual": "3.0.0-beta.54", "@tauri-apps/api": "^1.2.0", + "dayjs": "^1.11.7", "events": "^3.3.0", "google-protobuf": "^3.21.2", "i18next": "^22.4.10", @@ -32,6 +33,8 @@ "nanoid": "^4.0.0", "protoc-gen-ts": "^0.8.5", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-calendar": "^4.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", "react-i18next": "^12.2.0", @@ -44,8 +47,8 @@ "slate-react": "^0.91.9", "ts-results": "^3.3.0", "utf8": "^3.0.0", - "yjs": "^13.5.51", - "y-indexeddb": "^9.0.9" + "y-indexeddb": "^9.0.9", + "yjs": "^13.5.51" }, "devDependencies": { "@tauri-apps/cli": "^1.2.2", @@ -53,6 +56,7 @@ "@types/is-hotkey": "^0.1.7", "@types/node": "^18.7.10", "@types/react": "^18.0.15", + "@types/react-beautiful-dnd": "^13.1.3", "@types/react-dom": "^18.0.6", "@types/utf8": "^3.0.1", "@types/uuid": "^9.0.1", diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptions.tsx new file mode 100644 index 0000000000..63aeeab62a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptions.tsx @@ -0,0 +1,34 @@ +import { SelectOptionCellDataPB } from '@/services/backend'; +import { getBgColor } from '$app/components/_shared/getColor'; +import { useRef } from 'react'; + +export const CellOptions = ({ + data, + onEditClick, +}: { + data: SelectOptionCellDataPB | undefined; + onEditClick: (left: number, top: number) => void; +}) => { + const ref = useRef(null); + + const onClick = () => { + if (!ref.current) return; + const { left, top } = ref.current.getBoundingClientRect(); + onEditClick(left, top); + }; + + return ( +
onClick()} + className={'flex flex-wrap items-center gap-2 px-4 py-2 text-xs text-black'} + > + {data?.select_options?.map((option, index) => ( +
+ {option?.name || ''} +
+ )) || ''} +   +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptionsPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptionsPopup.tsx new file mode 100644 index 0000000000..0a2b1cf09b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CellOptionsPopup.tsx @@ -0,0 +1,152 @@ +import { KeyboardEventHandler, useEffect, useRef, useState } from 'react'; +import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc'; +import { useCell } from '$app/components/_shared/database-hooks/useCell'; +import { CellCache } from '$app/stores/effects/database/cell/cell_cache'; +import { FieldController } from '$app/stores/effects/database/field/field_controller'; +import { SelectOptionCellDataPB, SelectOptionColorPB, SelectOptionPB } from '@/services/backend'; +import { getBgColor } from '$app/components/_shared/getColor'; +import { useTranslation } from 'react-i18next'; +import { Details2Svg } from '$app/components/_shared/svg/Details2Svg'; +import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg'; +import { CloseSvg } from '$app/components/_shared/svg/CloseSvg'; +import useOutsideClick from '$app/components/_shared/useOutsideClick'; +import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc'; +import { useAppSelector } from '$app/stores/store'; +import { ISelectOptionType } from '$app/stores/reducers/database/slice'; + +export const CellOptionsPopup = ({ + top, + left, + cellIdentifier, + cellCache, + fieldController, + onOutsideClick, +}: { + top: number; + left: number; + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; + onOutsideClick: () => void; +}) => { + const ref = useRef(null); + const { t } = useTranslation(''); + const [adjustedTop, setAdjustedTop] = useState(-100); + const [value, setValue] = useState(''); + const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController); + const databaseStore = useAppSelector((state) => state.database); + + useEffect(() => { + if (!ref.current) return; + const { height } = ref.current.getBoundingClientRect(); + if (top + height + 40 > window.innerHeight) { + setAdjustedTop(window.innerHeight - height - 40); + } else { + setAdjustedTop(top); + } + }, [ref, window, top, left]); + + useOutsideClick(ref, async () => { + onOutsideClick(); + }); + + const onKeyDown: KeyboardEventHandler = async (e) => { + if (e.key === 'Enter' && value.length > 0) { + await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value }); + setValue(''); + } + }; + + const onUnselectOptionClick = async (option: SelectOptionPB) => { + await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]); + setValue(''); + }; + + const onToggleOptionClick = async (option: SelectOptionPB) => { + if ( + (data as SelectOptionCellDataPB | undefined)?.select_options?.find( + (selectedOption) => selectedOption.id === option.id + ) + ) { + await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]); + } else { + await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]); + } + setValue(''); + }; + + useEffect(() => { + console.log('loaded data: ', data); + console.log('have stored ', databaseStore.fields[cellIdentifier.fieldId]); + }, [data]); + + return ( +
+
+
+
+ {(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => ( +
+ {option?.name || ''} + +
+ )) || ''} +
+ setValue(e.target.value)} + placeholder={t('grid.selectOption.searchOption') || ''} + onKeyDown={onKeyDown} + /> +
{value.length}/30
+
+
+
{t('grid.selectOption.panelTitle') || ''}
+
+ {(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map( + (option, index) => ( +
+ onToggleOptionClick( + new SelectOptionPB({ + id: option.selectOptionId, + name: option.title, + color: option.color || SelectOptionColorPB.Purple, + }) + ) + } + className={ + 'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary' + } + > +
{option.title}
+
+ {(data as SelectOptionCellDataPB | undefined)?.select_options?.find( + (selectedOption) => selectedOption.id === option.selectOptionId + ) && ( + + )} + +
+
+ ) + )} +
+
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx new file mode 100644 index 0000000000..da7e8b0375 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx @@ -0,0 +1,71 @@ +import { FieldType } from '@/services/backend'; +import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon'; +import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import useOutsideClick from '$app/components/_shared/useOutsideClick'; + +const typesOrder: FieldType[] = [ + FieldType.RichText, + FieldType.Number, + FieldType.DateTime, + FieldType.SingleSelect, + FieldType.MultiSelect, + FieldType.Checkbox, + FieldType.URL, + FieldType.Checklist, +]; + +export const ChangeFieldTypePopup = ({ + top, + right, + onClick, + onOutsideClick, +}: { + top: number; + right: number; + onClick: (newType: FieldType) => void; + onOutsideClick: () => void; +}) => { + const ref = useRef(null); + const [adjustedTop, setAdjustedTop] = useState(-100); + useOutsideClick(ref, async () => { + onOutsideClick(); + }); + + useEffect(() => { + if (!ref.current) return; + const { height } = ref.current.getBoundingClientRect(); + if (top + height > window.innerHeight) { + setAdjustedTop(window.innerHeight - height); + } else { + setAdjustedTop(top); + } + }, [ref, window, top, right]); + + return ( +
+
+ {typesOrder.map((t, i) => ( + + ))} +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx new file mode 100644 index 0000000000..d7af34ec23 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/DatePickerPopup.tsx @@ -0,0 +1,97 @@ +import { useEffect, useRef, 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 useOutsideClick from '$app/components/_shared/useOutsideClick'; +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'; + +export const DatePickerPopup = ({ + left, + top, + cellIdentifier, + cellCache, + fieldController, + onOutsideClick, +}: { + left: number; + top: number; + cellIdentifier: CellIdentifier; + cellCache: CellCache; + fieldController: FieldController; + onOutsideClick: () => void; +}) => { + const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController); + const ref = useRef(null); + const [adjustedTop, setAdjustedTop] = useState(-100); + // const [value, setValue] = useState(); + const { t } = useTranslation(''); + const [selectedDate, setSelectedDate] = useState(new Date()); + + useEffect(() => { + if (!ref.current) return; + const { height } = ref.current.getBoundingClientRect(); + if (top + height + 40 > window.innerHeight) { + setAdjustedTop(top - height - 40); + } else { + setAdjustedTop(top); + } + }, [ref, window, top, left]); + + useOutsideClick(ref, async () => { + onOutsideClick(); + }); + + useEffect(() => { + // console.log((data as DateCellDataPB).date); + // setSelectedDate(new Date((data as DateCellDataPB).date)); + }, [data]); + + const onChange = (v: Date | null | (Date | null)[]) => { + if (v instanceof Date) { + console.log(dayjs(v).format('YYYY-MM-DD')); + setSelectedDate(v); + // void cellController?.saveCellData(new DateCellDataPB({ date: dayjs(v).format('YYYY-MM-DD') })); + } + }; + + return ( +
+
+ onChange(d)} value={selectedDate} /> +
+
+
+
+ + + + {t('grid.field.includeTime')} +
+ + + +
+
+
+ + {t('grid.field.dateFormat')} & {t('grid.field.timeFormat')} + + + + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellDate.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellDate.tsx new file mode 100644 index 0000000000..faf24eaf3a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellDate.tsx @@ -0,0 +1,24 @@ +import { useRef } from 'react'; +import { DateCellDataPB } from '@/services/backend'; + +export const EditCellDate = ({ + data, + onEditClick, +}: { + data?: DateCellDataPB; + onEditClick: (left: number, top: number) => void; +}) => { + const ref = useRef(null); + + const onClick = () => { + if (!ref.current) return; + const { left, top } = ref.current.getBoundingClientRect(); + onEditClick(left, top); + }; + + return ( +
onClick()} className={'px-4 py-2'}> + {data?.date || <> } +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellNumber.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellNumber.tsx new file mode 100644 index 0000000000..205ddd9257 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellNumber.tsx @@ -0,0 +1,29 @@ +import { CellController } from '$app/stores/effects/database/cell/cell_controller'; +import { useEffect, useState } from 'react'; + +export const EditCellNumber = ({ + data, + cellController, +}: { + data: string | undefined; + cellController: CellController; +}) => { + const [value, setValue] = useState(''); + + useEffect(() => { + setValue(data || ''); + }, [data]); + + const save = async () => { + await cellController?.saveCellData(value); + }; + + return ( + setValue(e.target.value)} + onBlur={() => save()} + className={'w-full px-4 py-2'} + > + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx new file mode 100644 index 0000000000..65c09e9880 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditCellText.tsx @@ -0,0 +1,41 @@ +import { CellController } from '$app/stores/effects/database/cell/cell_controller'; +import { useEffect, useState, KeyboardEvent, useMemo } from 'react'; + +export const EditCellText = ({ + data, + cellController, +}: { + data: string | undefined; + cellController: CellController; +}) => { + const [value, setValue] = useState(''); + const [contentRows, setContentRows] = useState(1); + + useEffect(() => { + setValue(data || ''); + }, [data]); + + useEffect(() => { + setContentRows(Math.max(1, (value || '').split('\n').length)); + }, [value]); + + const onTextFieldChange = async (v: string) => { + setValue(v); + }; + + const save = async () => { + await cellController?.saveCellData(value); + }; + + return ( +
+