mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Fix/tauri warning to error (#3869)
* feat: sort basic function * fix: eslint error * fix: deal with conflict * fix: prevent submit eslint warning code * fix: modify tauri warning to error --------- Co-authored-by: fangwufeng-v <fangwufeng.v@gmail.com>
This commit is contained in:
parent
73ea1a0685
commit
5f49c1748f
@ -15,20 +15,20 @@ module.exports = {
|
||||
plugins: ['@typescript-eslint', "react-hooks"],
|
||||
rules: {
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
||||
'@typescript-eslint/no-empty-function': 'error',
|
||||
'@typescript-eslint/no-empty-interface': 'warn',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-empty-interface': 'error',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-namespace': 'error',
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||
'@typescript-eslint/no-redeclare': 'error',
|
||||
'@typescript-eslint/prefer-for-of': 'warn',
|
||||
'@typescript-eslint/prefer-for-of': 'error',
|
||||
'@typescript-eslint/triple-slash-reference': 'error',
|
||||
'@typescript-eslint/unified-signatures': 'warn',
|
||||
'@typescript-eslint/unified-signatures': 'error',
|
||||
'no-shadow': 'off',
|
||||
'@typescript-eslint/no-shadow': 'warn',
|
||||
'@typescript-eslint/no-shadow': 'off',
|
||||
'constructor-super': 'error',
|
||||
eqeqeq: ['error', 'always'],
|
||||
'no-cond-assign': 'error',
|
||||
@ -47,18 +47,18 @@ module.exports = {
|
||||
'no-throw-literal': 'error',
|
||||
'no-unsafe-finally': 'error',
|
||||
'no-unused-labels': 'error',
|
||||
'no-var': 'warn',
|
||||
'no-var': 'error',
|
||||
'no-void': 'off',
|
||||
'prefer-const': 'warn',
|
||||
'prefer-const': 'error',
|
||||
'prefer-spread': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
}
|
||||
],
|
||||
'padding-line-between-statements': [
|
||||
"warn",
|
||||
"error",
|
||||
{ blankLine: "always", prev: ["const", "let", "var"], next: "*"},
|
||||
{ blankLine: "any", prev: ["const", "let", "var"], next: ["const", "let", "var"]},
|
||||
{ blankLine: "always", prev: "import", next: "*" },
|
||||
|
@ -9,7 +9,7 @@
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
|
||||
"test:errors": "pnpm sync:i18n && tsc --noEmit && eslint --quiet --ext .js,.ts,.tsx .",
|
||||
"test:errors": "pnpm sync:i18n && tsc --noEmit && eslint --ext .js,.ts,.tsx .",
|
||||
"test:prettier": "pnpm prettier --list-different src",
|
||||
"tauri:clean": "cargo make --cwd .. tauri_clean",
|
||||
"tauri:dev": "pnpm sync:i18n && tauri dev",
|
||||
|
@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="15.7124" y="7.22656" width="1.5" height="12" rx="0.75" transform="rotate(45 15.7124 7.22656)" fill="#333333"/>
|
||||
<rect x="16.7729" y="15.7109" width="1.5" height="12" rx="0.75" transform="rotate(135 16.7729 15.7109)" fill="#333333"/>
|
||||
</svg>
|
After Width: | Height: | Size: 344 B |
@ -66,7 +66,7 @@ function BlockDragDropContext({ children }: { children: React.ReactNode }) {
|
||||
};
|
||||
|
||||
const onDragEnd = () => {
|
||||
dispatch(onDragEndThunk());
|
||||
void dispatch(onDragEndThunk());
|
||||
unlisten();
|
||||
};
|
||||
|
||||
|
@ -18,7 +18,7 @@ function BlockDraggable(
|
||||
} & HTMLAttributes<HTMLDivElement>,
|
||||
ref: React.Ref<HTMLDivElement>
|
||||
) {
|
||||
const { onDragStart, beforeDropping, afterDropping, childDropping, isDragging } = useDraggableState(id, type);
|
||||
const { onDragStart, beforeDropping, afterDropping, childDropping } = useDraggableState(id, type);
|
||||
|
||||
const commonCls = 'pointer-events-none absolute z-10 w-[100%] bg-fill-hover transition-all duration-200';
|
||||
|
||||
|
@ -1,11 +1,6 @@
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { databaseActions, IDatabase } from '../../stores/reducers/database/slice';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { FieldType } from '../../../services/backend';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const useDatabase = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const database = useAppSelector((state) => state.database);
|
||||
|
||||
const newField = () => {
|
||||
@ -22,7 +17,7 @@ export const useDatabase = () => {
|
||||
console.log('depreciated');
|
||||
};
|
||||
|
||||
const renameField = (fieldId: string, newTitle: string) => {
|
||||
const renameField = (_fieldId: string, _newTitle: string) => {
|
||||
/* const field = database.fields[fieldId];
|
||||
field.title = newTitle;
|
||||
|
||||
|
@ -75,6 +75,7 @@ export const DatabaseFilterItem = ({
|
||||
value: currentValue,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentFieldId, currentFieldType, currentOperator, currentValue, textInputActive]);
|
||||
|
||||
// 1. not all field types support filtering
|
||||
|
@ -54,6 +54,7 @@ export const DatabaseSortItem = ({
|
||||
fieldType: fields[currentFieldId].fieldType,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentFieldId, currentOrder]);
|
||||
|
||||
const onSelectFieldClick = (id: string) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { t } from 'i18next';
|
||||
import { MouseEventHandler, useMemo, useRef, useState } from 'react';
|
||||
import { MouseEventHandler, useMemo, useState } from 'react';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { IDatabaseSort } from '$app_reducers/database/slice';
|
||||
import { DatabaseSortItem } from '$app/components/_shared/DatabaseSort/DatabaseSortItem';
|
||||
|
@ -19,6 +19,7 @@ export const NewCheckListOption = ({
|
||||
|
||||
const updateNewOption = (value: string) => {
|
||||
const newOptionsCopy = [...newOptions];
|
||||
|
||||
newOptionsCopy[index] = value;
|
||||
setNewOptions(newOptionsCopy);
|
||||
};
|
||||
|
@ -29,6 +29,7 @@ export const DateFormatPopup = ({
|
||||
|
||||
useEffect(() => {
|
||||
setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [databaseStore]);
|
||||
|
||||
const changeFormat = async (format: DateFormatPB) => {
|
||||
|
@ -30,6 +30,7 @@ export const DatePickerPopup = ({
|
||||
|
||||
useEffect(() => {
|
||||
const date_pb = data as DateCellDataPB | undefined;
|
||||
|
||||
if (!date_pb || !date_pb?.date.length) return;
|
||||
|
||||
setSelectedDate(dayjs(date_pb.date).toDate());
|
||||
@ -39,6 +40,7 @@ export const DatePickerPopup = ({
|
||||
if (v instanceof Date) {
|
||||
setSelectedDate(v);
|
||||
const date = new CalendarData(dayjs(v).add(dayjs().utcOffset(), 'minutes').toDate(), false);
|
||||
|
||||
await cellController?.saveCellData(date);
|
||||
}
|
||||
};
|
||||
|
@ -8,11 +8,14 @@ import { FieldController } from '$app/stores/effects/database/field/field_contro
|
||||
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);
|
||||
};
|
||||
@ -20,11 +23,13 @@ export const useDateTimeFormat = (cellIdentifier: CellIdentifier, fieldControlle
|
||||
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) => {
|
||||
|
||||
const includeTime = async (_include: boolean) => {
|
||||
await changeFormat((_option) => {
|
||||
// option.include_time = include;
|
||||
});
|
||||
};
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { DateFormatPopup } from '$app/components/_shared/EditRow/Date/DateFormatPopup';
|
||||
import { TimeFormatPopup } from '$app/components/_shared/EditRow/Date/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_reducers/database/slice';
|
||||
@ -27,14 +25,16 @@ export const DateTypeOptions = ({
|
||||
const [showTimeFormatPopup, setShowTimeFormatPopup] = useState(false);
|
||||
const [timeFormatTop, setTimeFormatTop] = useState(0);
|
||||
const [timeFormatLeft, setTimeFormatLeft] = useState(0);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [dateType, setDateType] = useState<IDateType | undefined>();
|
||||
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { includeTime } = useDateTimeFormat(cellIdentifier, fieldController);
|
||||
|
||||
useEffect(() => {
|
||||
setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [databaseStore]);
|
||||
|
||||
const onDateFormatClick = (_left: number, _top: number) => {
|
||||
@ -87,7 +87,7 @@ export const DateTypeOptions = ({
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col'}>
|
||||
<hr className={'border-line-divider -mx-2 my-2'} />
|
||||
<hr className={'-mx-2 my-2 border-line-divider'} />
|
||||
<button
|
||||
onClick={_onDateFormatClick}
|
||||
className={
|
||||
|
@ -13,6 +13,7 @@ export const EditCellDate = ({
|
||||
const onClick: MouseEventHandler = () => {
|
||||
if (!ref.current) return;
|
||||
const { left, top } = ref.current.getBoundingClientRect();
|
||||
|
||||
onEditClick(left, top);
|
||||
};
|
||||
|
||||
|
@ -8,11 +8,14 @@ import { makeNumberTypeOptionContext } from '$app/stores/effects/database/field/
|
||||
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);
|
||||
};
|
||||
|
@ -66,6 +66,7 @@ export const NumberFormatPopup = ({
|
||||
|
||||
useEffect(() => {
|
||||
setNumberType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as INumberType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [databaseStore]);
|
||||
|
||||
const changeNumberFormatClick = async (format: NumberFormatPB) => {
|
||||
|
@ -28,6 +28,7 @@ export const TimeFormatPopup = ({
|
||||
|
||||
useEffect(() => {
|
||||
setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [databaseStore]);
|
||||
|
||||
const { changeTimeFormat } = useDateTimeFormat(cellIdentifier, fieldController);
|
||||
|
@ -59,6 +59,7 @@ export const EditFieldPopup = ({
|
||||
const save = async () => {
|
||||
if (!controller) return;
|
||||
const fieldInfo = controller.fieldController.getField(cellIdentifier.fieldId);
|
||||
|
||||
if (!fieldInfo) return;
|
||||
const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
|
||||
|
@ -66,7 +66,9 @@ export const EditRow = ({
|
||||
const [editCheckListLeft, setEditCheckListLeft] = useState(0);
|
||||
|
||||
const [showNumberFormatPopup, setShowNumberFormatPopup] = useState(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [numberFormatTop, setNumberFormatTop] = useState(0);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [numberFormatLeft, setNumberFormatLeft] = useState(0);
|
||||
|
||||
const [showCheckListPopup, setShowCheckListPopup] = useState(false);
|
||||
|
@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const FieldTypeName = ({ fieldType }: { fieldType: FieldType }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{fieldType === FieldType.RichText && t('grid.field.textFieldName')}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||
import { CheckboxCellController } from '$app/stores/effects/database/cell/controller_builder';
|
||||
|
||||
export const EditCheckboxCell = ({ data, onToggle }: { data: 'Yes' | 'No' | undefined; onToggle: () => void }) => {
|
||||
// const toggleValue = async () => {
|
||||
|
@ -4,7 +4,6 @@ import { CellOption } from '$app/components/_shared/EditRow/Options/CellOption';
|
||||
import { SelectOptionPB } from '@/services/backend';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
|
||||
export const MultiSelectTypeOptions = ({
|
||||
@ -16,7 +15,6 @@ export const MultiSelectTypeOptions = ({
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const inputContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const fieldsStore = useAppSelector((state) => state.database.fields);
|
||||
const [value, setValue] = useState('');
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
|
@ -37,6 +37,7 @@ export const LanguageSelectPopup = ({ onClose }: { onClose: () => void }) => {
|
||||
title: item.title,
|
||||
icon: <></>,
|
||||
}));
|
||||
|
||||
return (
|
||||
<PopupSelect
|
||||
items={items}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { Some } from 'ts-results';
|
||||
import { IDatabaseField, ISelectOption } from '$app_reducers/database/slice';
|
||||
import { ChecklistTypeOptionPB, FieldType, MultiSelectTypeOptionPB, SingleSelectTypeOptionPB } from '@/services/backend';
|
||||
import { FieldType, MultiSelectTypeOptionPB, SingleSelectTypeOptionPB } from '@/services/backend';
|
||||
import {
|
||||
makeChecklistTypeOptionContext,
|
||||
makeDateTypeOptionContext,
|
||||
makeMultiSelectTypeOptionContext,
|
||||
makeNumberTypeOptionContext,
|
||||
@ -32,9 +31,11 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
if (dispatch) {
|
||||
dispatch(boardActions.setGroupingFieldId({ fieldId: field.id }));
|
||||
}
|
||||
|
||||
groupingFieldSelected = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (field.field_type === FieldType.MultiSelect) {
|
||||
typeOption = (await makeMultiSelectTypeOptionContext(typeOptionController).getTypeOption()).unwrap();
|
||||
}
|
||||
@ -63,6 +64,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
|
||||
case FieldType.Number: {
|
||||
const typeOption = (await makeNumberTypeOptionContext(typeOptionController).getTypeOption()).unwrap();
|
||||
|
||||
return {
|
||||
fieldId: field.id,
|
||||
title: field.name,
|
||||
@ -77,6 +79,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
|
||||
case FieldType.DateTime: {
|
||||
const typeOption = (await makeDateTypeOptionContext(typeOptionController).getTypeOption()).unwrap();
|
||||
|
||||
return {
|
||||
fieldId: field.id,
|
||||
title: field.name,
|
||||
|
@ -10,6 +10,7 @@ import { databaseActions, ISelectOptionType } from '$app_reducers/database/slice
|
||||
|
||||
export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fieldController: FieldController) => {
|
||||
const [data, setData] = useState<DateCellDataPB | URLCellDataPB | SelectOptionCellDataPB | string | undefined>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [cellController, setCellController] = useState<CellController<any, any>>();
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const dispatch = useAppDispatch();
|
||||
@ -18,12 +19,14 @@ export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fi
|
||||
if (!cellIdentifier || !cellCache || !fieldController) return;
|
||||
const builder = new CellControllerBuilder(cellIdentifier, cellCache, fieldController);
|
||||
const c = builder.build();
|
||||
|
||||
setCellController(c);
|
||||
|
||||
c.subscribeChanged({
|
||||
onCellChanged: (cellData) => {
|
||||
if (cellData.some) {
|
||||
const value = cellData.val;
|
||||
|
||||
setData(value);
|
||||
|
||||
// update redux store for database field if there are new select options
|
||||
@ -59,6 +62,7 @@ export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fi
|
||||
void (async () => {
|
||||
try {
|
||||
const cellData = await c.getCellData();
|
||||
|
||||
if (cellData.some) {
|
||||
setData(cellData.unwrap());
|
||||
}
|
||||
@ -70,6 +74,7 @@ export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fi
|
||||
return () => {
|
||||
void c.dispose();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cellIdentifier, cellCache, fieldController]);
|
||||
|
||||
return {
|
||||
|
@ -186,6 +186,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
return () => {
|
||||
void controller?.dispose();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [controller, queue]);
|
||||
|
||||
const onNewRowClick = async (index: number) => {
|
||||
|
@ -18,6 +18,7 @@ export const useRow = (viewId: string, databaseController: DatabaseController, r
|
||||
const rowCache = databaseController.databaseViewCache.getRowCache();
|
||||
const fieldController = databaseController.fieldController;
|
||||
const c = new RowController(rowInfo, fieldController, rowCache);
|
||||
|
||||
setRowController(c);
|
||||
|
||||
return () => {
|
||||
@ -46,6 +47,7 @@ export const useRow = (viewId: string, databaseController: DatabaseController, r
|
||||
const onNewColumnClick = async (initialFieldType: FieldType = FieldType.RichText, name?: string) => {
|
||||
if (!databaseController) return;
|
||||
const controller = new TypeOptionController(viewId, None, initialFieldType);
|
||||
|
||||
await controller.initialize();
|
||||
if (name) {
|
||||
await controller.setFieldName(name);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export default function useOutsideClick(ref: any, handler: (e: MouseEvent | TouchEvent) => void) {
|
||||
useEffect(
|
||||
() => {
|
||||
@ -8,8 +9,10 @@ export default function useOutsideClick(ref: any, handler: (e: MouseEvent | Touc
|
||||
if (!ref?.current || ref.current.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler(event);
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', listener);
|
||||
document.addEventListener('touchstart', listener);
|
||||
return () => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { currentUserActions } from '../../../stores/reducers/current-user/slice';
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../auth.hooks';
|
||||
import { nanoid } from 'nanoid';
|
||||
@ -10,7 +10,6 @@ export const useLogin = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const appDispatch = useAppDispatch();
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
const navigate = useNavigate();
|
||||
const { login, register } = useAuth();
|
||||
const [authError, setAuthError] = useState(false);
|
||||
@ -36,6 +35,7 @@ export const useLogin = () => {
|
||||
const fakePassword = 'AppFlowy123@';
|
||||
const userProfile = await register(fakeEmail, fakePassword, 'Me');
|
||||
const { id, name, token } = userProfile;
|
||||
|
||||
appDispatch(
|
||||
currentUserActions.updateUser({
|
||||
id: id,
|
||||
@ -55,6 +55,7 @@ export const useLogin = () => {
|
||||
try {
|
||||
const userProfile = await login(email, password);
|
||||
const { id, name, token } = userProfile;
|
||||
|
||||
appDispatch(
|
||||
currentUserActions.updateUser({
|
||||
id: id,
|
||||
|
@ -21,6 +21,7 @@ export const ProtectedRoutes = () => {
|
||||
throw new Error(result.val.msg);
|
||||
}
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
||||
import { currentUserActions } from '../../../stores/reducers/current-user/slice';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../auth.hooks';
|
||||
|
||||
@ -12,7 +12,6 @@ export const useSignUp = () => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const appDispatch = useAppDispatch();
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
const navigate = useNavigate();
|
||||
const { register } = useAuth();
|
||||
const [authError, setAuthError] = useState(false);
|
||||
@ -49,6 +48,7 @@ export const useSignUp = () => {
|
||||
try {
|
||||
const result = await register(email, password, displayName);
|
||||
const { id, token } = result;
|
||||
|
||||
appDispatch(
|
||||
currentUserActions.updateUser({
|
||||
id,
|
||||
|
@ -15,6 +15,7 @@ export const BoardCheckboxCell = ({
|
||||
fieldController: FieldController;
|
||||
}) => {
|
||||
const { data } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
|
||||
return (
|
||||
<i className={'h-5 w-5'}>
|
||||
{data === 'Yes' ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
||||
|
@ -14,5 +14,6 @@ export const BoardDateCell = ({
|
||||
fieldController: FieldController;
|
||||
}) => {
|
||||
const { data } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
|
||||
return <div>{(data as DateCellDataPB | undefined)?.date ?? ''} </div>;
|
||||
};
|
||||
|
@ -37,6 +37,7 @@ export const BoardSettingsPopup = ({
|
||||
onClick: onGroupClick,
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
|
@ -0,0 +1,84 @@
|
||||
import { RefObject, createContext, createRef, useContext, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { proxy, useSnapshot } from 'valtio';
|
||||
import { DatabaseLayoutPB, DatabaseNotification } from '@/services/backend';
|
||||
import { subscribeNotifications } from '$app/hooks';
|
||||
import {
|
||||
Database,
|
||||
databaseService,
|
||||
fieldService,
|
||||
rowListeners,
|
||||
sortListeners,
|
||||
} from './application';
|
||||
|
||||
const VerticalScrollElementRefContext = createContext<RefObject<Element>>(createRef());
|
||||
|
||||
export const VerticalScrollElementProvider = VerticalScrollElementRefContext.Provider;
|
||||
export const useVerticalScrollElement = () => useContext(VerticalScrollElementRefContext);
|
||||
|
||||
export function useSelectDatabaseView() {
|
||||
const key = 'v';
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const selectedViewId = useMemo(() => searchParams.get(key), [searchParams]);
|
||||
|
||||
const selectViewId = useCallback((value: string) => {
|
||||
setSearchParams({ [key]: value });
|
||||
}, [setSearchParams]);
|
||||
|
||||
return [selectedViewId, selectViewId] as const;
|
||||
}
|
||||
|
||||
const DatabaseContext = createContext<Database>({
|
||||
id: '',
|
||||
isLinked: false,
|
||||
layoutType: DatabaseLayoutPB.Grid,
|
||||
fields: [],
|
||||
rowMetas: [],
|
||||
filters: [],
|
||||
sorts: [],
|
||||
groupSettings: [],
|
||||
groups: [],
|
||||
});
|
||||
|
||||
export const DatabaseProvider = DatabaseContext.Provider;
|
||||
|
||||
export const useDatabase = () => useSnapshot(useContext(DatabaseContext));
|
||||
|
||||
export const useConnectDatabase = (viewId: string) => {
|
||||
const database = useMemo(() => {
|
||||
const proxyDatabase = proxy<Database>({
|
||||
id: '',
|
||||
isLinked: false,
|
||||
layoutType: DatabaseLayoutPB.Grid,
|
||||
fields: [],
|
||||
rowMetas: [],
|
||||
filters: [],
|
||||
sorts: [],
|
||||
groupSettings: [],
|
||||
groups: [],
|
||||
});
|
||||
|
||||
void databaseService.openDatabase(viewId).then(value => Object.assign(proxyDatabase, value));
|
||||
|
||||
return proxyDatabase;
|
||||
}, [viewId]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribePromise = subscribeNotifications({
|
||||
[DatabaseNotification.DidUpdateFields]: async () => {
|
||||
database.fields = await fieldService.getFields(viewId);
|
||||
},
|
||||
|
||||
[DatabaseNotification.DidUpdateViewRows]: changeset => rowListeners.didUpdateViewRows(database, changeset),
|
||||
[DatabaseNotification.DidReorderRows]: changeset => rowListeners.didReorderRows(database, changeset),
|
||||
[DatabaseNotification.DidReorderSingleRow]: changeset => rowListeners.didReorderSingleRow(database, changeset),
|
||||
|
||||
[DatabaseNotification.DidUpdateSort]: changeset => sortListeners.didUpdateSort(database, changeset),
|
||||
}, { id: viewId });
|
||||
|
||||
return () => void unsubscribePromise.then(unsubscribe => unsubscribe());
|
||||
}, [viewId, database]);
|
||||
|
||||
return database;
|
||||
};
|
@ -1,41 +1,42 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { proxy } from 'valtio';
|
||||
import { subscribeKey } from 'valtio/utils';
|
||||
import { DatabaseLayoutPB } from '@/services/backend';
|
||||
import { DndContext, DndContextDescriptor } from './_shared';
|
||||
import { VerticalScrollElementRefContext, DatabaseContext } from './database.context';
|
||||
import { useViewId, useConnectDatabase } from './database.hooks';
|
||||
import { DatabaseHeader } from './DatabaseHeader';
|
||||
import { Grid } from './grid';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { DatabaseView as DatabaseViewType, databaseViewService } from './application';
|
||||
import { DatabaseTabBar } from './components';
|
||||
import { useSelectDatabaseView } from './Database.hooks';
|
||||
import { DatabaseLoader } from './DatabaseLoader';
|
||||
import { DatabaseView } from './DatabaseView';
|
||||
import { DatabaseSettings } from './components/database_settings';
|
||||
|
||||
export const Database = () => {
|
||||
const viewId = useViewId();
|
||||
const verticalScrollElementRef = useRef<HTMLDivElement>(null);
|
||||
const database = useConnectDatabase(viewId);
|
||||
const [ layoutType, setLayoutType ] = useState(database.layoutType);
|
||||
const dndContext = useRef(proxy<DndContextDescriptor>({
|
||||
dragging: null,
|
||||
}));
|
||||
const [views, setViews] = useState<DatabaseViewType[]>([]);
|
||||
const [selectedViewId, selectViewId] = useSelectDatabaseView();
|
||||
const activeView = useMemo(() => views?.find((view) => view.id === selectedViewId), [views, selectedViewId]);
|
||||
|
||||
useEffect(() => {
|
||||
return subscribeKey(database, 'layoutType', (value) => {
|
||||
setLayoutType(value);
|
||||
setViews([]);
|
||||
void databaseViewService.getDatabaseViews(viewId).then((value) => {
|
||||
setViews(value);
|
||||
});
|
||||
}, [database]);
|
||||
}, [viewId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={verticalScrollElementRef}
|
||||
className="h-full overflow-y-auto"
|
||||
>
|
||||
<DatabaseHeader />
|
||||
<VerticalScrollElementRefContext.Provider value={verticalScrollElementRef}>
|
||||
<DndContext.Provider value={dndContext.current}>
|
||||
<DatabaseContext.Provider value={database}>
|
||||
{layoutType === DatabaseLayoutPB.Grid ? <Grid /> : null}
|
||||
</DatabaseContext.Provider>
|
||||
</DndContext.Provider >
|
||||
</VerticalScrollElementRefContext.Provider>
|
||||
</div>
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!activeView) {
|
||||
const firstViewId = views?.[0]?.id;
|
||||
|
||||
if (firstViewId) {
|
||||
selectViewId(firstViewId);
|
||||
}
|
||||
}
|
||||
}, [views, activeView, selectViewId]);
|
||||
|
||||
return activeView ? (
|
||||
<DatabaseLoader viewId={viewId}>
|
||||
<div className='px-16'>
|
||||
<DatabaseTabBar views={views} />
|
||||
<DatabaseSettings />
|
||||
</div>
|
||||
<DatabaseView />
|
||||
</DatabaseLoader>
|
||||
) : null;
|
||||
};
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { FC, PropsWithChildren } from 'react';
|
||||
import { ViewIdProvider } from '$app/hooks';
|
||||
import { DatabaseProvider, useConnectDatabase } from './Database.hooks';
|
||||
|
||||
export interface DatabaseLoaderProps {
|
||||
viewId: string
|
||||
}
|
||||
|
||||
export const DatabaseLoader: FC<PropsWithChildren<DatabaseLoaderProps>> = ({
|
||||
viewId,
|
||||
children,
|
||||
}) => {
|
||||
const database = useConnectDatabase(viewId);
|
||||
|
||||
return (
|
||||
<DatabaseProvider value={database}>
|
||||
<ViewIdProvider value={viewId}>
|
||||
{children}
|
||||
</ViewIdProvider>
|
||||
</DatabaseProvider>
|
||||
);
|
||||
};
|
@ -1,9 +1,9 @@
|
||||
import { FormEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import { PageController } from '$app/stores/effects/workspace/page/page_controller';
|
||||
import { useViewId } from './database.hooks';
|
||||
import { useViewId } from '$app/hooks';
|
||||
|
||||
export const DatabaseHeader = () => {
|
||||
export const DatabaseTitle = () => {
|
||||
const viewId = useViewId();
|
||||
const [ title, setTitle ] = useState('');
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { DatabaseLayoutPB } from '@/services/backend';
|
||||
import { FC } from 'react';
|
||||
import { useDatabase } from './Database.hooks';
|
||||
import { Grid } from './grid';
|
||||
import { Board } from './board';
|
||||
import { Calendar } from './calendar';
|
||||
|
||||
const ViewMap: Record<DatabaseLayoutPB, FC | null> = {
|
||||
[DatabaseLayoutPB.Grid]: Grid,
|
||||
[DatabaseLayoutPB.Board]: Board,
|
||||
[DatabaseLayoutPB.Calendar]: Calendar,
|
||||
};
|
||||
|
||||
export const DatabaseView: FC = () => {
|
||||
const { layoutType } = useDatabase();
|
||||
const View = ViewMap[layoutType];
|
||||
|
||||
return View && <View />;
|
||||
};
|
@ -1,17 +1,11 @@
|
||||
import {
|
||||
DragEventHandler,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { DragEventHandler, useCallback, useContext, useMemo, useRef, useState } from 'react';
|
||||
import { DndContext } from './dnd.context';
|
||||
import { autoScrollOnEdge, EdgeGap, getScrollParent, ScrollDirection } from './utils';
|
||||
|
||||
export interface UseDraggableOptions {
|
||||
type: string;
|
||||
effectAllowed?: DataTransfer['effectAllowed'];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: Record<string, any>;
|
||||
disabled?: boolean;
|
||||
scrollOnEdge?: {
|
||||
@ -34,7 +28,7 @@ export const useDraggable = ({
|
||||
const typeRef = useRef(type);
|
||||
const dataRef = useRef(data);
|
||||
const previewRef = useRef<Element | null>(null);
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
typeRef.current = type;
|
||||
dataRef.current = data;
|
||||
@ -53,49 +47,55 @@ export const useDraggable = ({
|
||||
};
|
||||
}, [disabled]);
|
||||
|
||||
const onDragStart = useCallback<DragEventHandler>((event) => {
|
||||
setIsDragging(true);
|
||||
context.dragging = {
|
||||
type: typeRef.current,
|
||||
data: dataRef.current ?? {},
|
||||
};
|
||||
const onDragStart = useCallback<DragEventHandler>(
|
||||
(event) => {
|
||||
setIsDragging(true);
|
||||
context.dragging = {
|
||||
type: typeRef.current,
|
||||
data: dataRef.current ?? {},
|
||||
};
|
||||
|
||||
const { dataTransfer } = event;
|
||||
const previewNode = previewRef.current;
|
||||
const { dataTransfer } = event;
|
||||
const previewNode = previewRef.current;
|
||||
|
||||
dataTransfer.effectAllowed = effectAllowed;
|
||||
dataTransfer.effectAllowed = effectAllowed;
|
||||
|
||||
if (previewNode) {
|
||||
const { clientX, clientY } = event;
|
||||
const rect = previewNode.getBoundingClientRect();
|
||||
if (previewNode) {
|
||||
const { clientX, clientY } = event;
|
||||
const rect = previewNode.getBoundingClientRect();
|
||||
|
||||
dataTransfer.setDragImage(previewNode, clientX - rect.x, clientY - rect.y);
|
||||
}
|
||||
dataTransfer.setDragImage(previewNode, clientX - rect.x, clientY - rect.y);
|
||||
}
|
||||
|
||||
if (scrollDirection === undefined) {
|
||||
return;
|
||||
}
|
||||
if (scrollDirection === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollParent: HTMLElement | null = getScrollParent(event.target as HTMLElement, scrollDirection);
|
||||
const scrollParent: HTMLElement | null = getScrollParent(event.target as HTMLElement, scrollDirection);
|
||||
|
||||
if (scrollParent) {
|
||||
autoScrollOnEdge({
|
||||
element: scrollParent,
|
||||
direction: scrollDirection,
|
||||
edgeGap,
|
||||
});
|
||||
}
|
||||
}, [ context, effectAllowed, scrollDirection, edgeGap ]);
|
||||
if (scrollParent) {
|
||||
autoScrollOnEdge({
|
||||
element: scrollParent,
|
||||
direction: scrollDirection,
|
||||
edgeGap,
|
||||
});
|
||||
}
|
||||
},
|
||||
[context, effectAllowed, scrollDirection, edgeGap]
|
||||
);
|
||||
|
||||
const onDragEnd = useCallback<DragEventHandler>(() => {
|
||||
setIsDragging(false);
|
||||
context.dragging = null;
|
||||
}, [ context ]);
|
||||
}, [context]);
|
||||
|
||||
const listeners = useMemo(() => ({
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
}), [ onDragStart, onDragEnd]);
|
||||
const listeners = useMemo(
|
||||
() => ({
|
||||
onDragStart,
|
||||
onDragEnd,
|
||||
}),
|
||||
[onDragStart, onDragEnd]
|
||||
);
|
||||
|
||||
return {
|
||||
isDragging,
|
||||
|
@ -162,7 +162,6 @@ export const autoScrollOnEdge = ({
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
console.log('document drag end');
|
||||
keepScroll.cancel();
|
||||
|
||||
element.removeEventListener('dragover', onDragOver);
|
||||
|
@ -0,0 +1,120 @@
|
||||
import {
|
||||
CellIdPB,
|
||||
CellChangesetPB,
|
||||
SelectOptionCellChangesetPB,
|
||||
ChecklistCellDataChangesetPB,
|
||||
DateChangesetPB,
|
||||
FieldType,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventGetCell,
|
||||
DatabaseEventUpdateCell,
|
||||
DatabaseEventUpdateSelectOptionCell,
|
||||
DatabaseEventUpdateChecklistCell,
|
||||
DatabaseEventUpdateDateCell,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { SelectOption } from '../field';
|
||||
import { Cell, pbToCell } from './cell_types';
|
||||
|
||||
export async function getCell(viewId: string, rowId: string, fieldId: string, fieldType?: FieldType): Promise<Cell> {
|
||||
const payload = CellIdPB.fromObject({
|
||||
view_id: viewId,
|
||||
row_id: rowId,
|
||||
field_id: fieldId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventGetCell(payload);
|
||||
|
||||
return result.map(value => pbToCell(value, fieldType)).unwrap();
|
||||
}
|
||||
|
||||
export async function updateCell(viewId: string, rowId: string, fieldId: string, changeset: string): Promise<void> {
|
||||
const payload = CellChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
row_id: rowId,
|
||||
field_id: fieldId,
|
||||
cell_changeset: changeset,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateCell(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function updateSelectCell(
|
||||
viewId: string,
|
||||
rowId: string,
|
||||
fieldId: string,
|
||||
data: {
|
||||
insertOptionIds?: string[];
|
||||
deleteOptionIds?: string[];
|
||||
},
|
||||
): Promise<void> {
|
||||
const payload = SelectOptionCellChangesetPB.fromObject({
|
||||
cell_identifier: {
|
||||
view_id: viewId,
|
||||
row_id: rowId,
|
||||
field_id: fieldId,
|
||||
},
|
||||
insert_option_ids: data.insertOptionIds,
|
||||
delete_option_ids: data.deleteOptionIds,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateSelectOptionCell(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function updateChecklistCell(
|
||||
viewId: string,
|
||||
rowId: string,
|
||||
fieldId: string,
|
||||
data: {
|
||||
insertOptions?: string[];
|
||||
selectedOptionIds?: string[];
|
||||
deleteOptionIds?: string[];
|
||||
updateOptions?: Partial<SelectOption>[];
|
||||
},
|
||||
): Promise<void> {
|
||||
const payload = ChecklistCellDataChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
row_id: rowId,
|
||||
field_id: fieldId,
|
||||
insert_options: data.insertOptions,
|
||||
selected_option_ids: data.selectedOptionIds,
|
||||
delete_option_ids: data.deleteOptionIds,
|
||||
update_options: data.updateOptions,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateChecklistCell(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function updateDateCell(
|
||||
viewId: string,
|
||||
rowId: string,
|
||||
fieldId: string,
|
||||
data: {
|
||||
date?: number;
|
||||
time?: string;
|
||||
includeTime?: boolean;
|
||||
clearFlag?: boolean;
|
||||
},
|
||||
): Promise<void> {
|
||||
const payload = DateChangesetPB.fromObject({
|
||||
cell_id: {
|
||||
view_id: viewId,
|
||||
row_id: rowId,
|
||||
field_id: fieldId,
|
||||
},
|
||||
date: data.date,
|
||||
time: data.time,
|
||||
include_time: data.includeTime,
|
||||
clear_flag: data.clearFlag,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateDateCell(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
import {
|
||||
CellPB,
|
||||
ChecklistCellDataPB,
|
||||
DateCellDataPB,
|
||||
FieldType,
|
||||
SelectOptionCellDataPB,
|
||||
URLCellDataPB,
|
||||
} from '@/services/backend';
|
||||
|
||||
export interface Cell {
|
||||
rowId: string;
|
||||
fieldId: string;
|
||||
fieldType: FieldType;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface TextCell extends Cell {
|
||||
fieldType: FieldType.RichText;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface NumberCell extends Cell {
|
||||
fieldType: FieldType.Number;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface CheckboxCell extends Cell {
|
||||
fieldType: FieldType.Checkbox;
|
||||
data: 'Yes' | 'No';
|
||||
}
|
||||
|
||||
export interface UrlCell extends Cell {
|
||||
fieldType: FieldType.URL;
|
||||
data: UrlCellData;
|
||||
}
|
||||
|
||||
export interface UrlCellData {
|
||||
url: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface SelectCell extends Cell {
|
||||
fieldType: FieldType.SingleSelect | FieldType.MultiSelect;
|
||||
data: SelectCellData;
|
||||
}
|
||||
|
||||
export interface SelectCellData {
|
||||
selectedOptionIds?: string[];
|
||||
}
|
||||
|
||||
export interface DateTimeCell extends Cell {
|
||||
fieldType: FieldType.DateTime;
|
||||
data: DateTimeCellData;
|
||||
}
|
||||
|
||||
export interface DateTimeCellData {
|
||||
date?: string;
|
||||
time?: string;
|
||||
timestamp?: number;
|
||||
includeTime?: boolean;
|
||||
}
|
||||
|
||||
export interface ChecklistCell extends Cell {
|
||||
fieldType: FieldType.Checklist;
|
||||
data: ChecklistCellData;
|
||||
}
|
||||
|
||||
export interface ChecklistCellData {
|
||||
/**
|
||||
* link to [SelectOption's id property]{@link SelectOption#id}.
|
||||
*/
|
||||
selectedOptions?: string[];
|
||||
percentage?: number;
|
||||
}
|
||||
|
||||
export type UndeterminedCell = TextCell | NumberCell | DateTimeCell | SelectCell | CheckboxCell | UrlCell | ChecklistCell;
|
||||
|
||||
const pbToDateCellData = (pb: DateCellDataPB): DateTimeCellData => ({
|
||||
date: pb.date,
|
||||
time: pb.time,
|
||||
timestamp: pb.timestamp,
|
||||
includeTime: pb.include_time,
|
||||
});
|
||||
|
||||
export const pbToSelectCellData = (pb: SelectOptionCellDataPB): SelectCellData => {
|
||||
return {
|
||||
selectedOptionIds: pb.select_options.map(option => option.id),
|
||||
};
|
||||
};
|
||||
|
||||
const pbToURLCellData = (pb: URLCellDataPB): UrlCellData => ({
|
||||
url: pb.url,
|
||||
content: pb.content,
|
||||
});
|
||||
|
||||
export const pbToChecklistCellData = (pb: ChecklistCellDataPB): ChecklistCellData => ({
|
||||
selectedOptions: pb.selected_options.map(({ id }) => id),
|
||||
percentage: pb.percentage,
|
||||
});
|
||||
|
||||
function bytesToCellData(bytes: Uint8Array, fieldType: FieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
case FieldType.Number:
|
||||
case FieldType.Checkbox:
|
||||
return new TextDecoder().decode(bytes);
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return pbToDateCellData(DateCellDataPB.deserialize(bytes));
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return pbToSelectCellData(SelectOptionCellDataPB.deserialize(bytes));
|
||||
case FieldType.URL:
|
||||
return pbToURLCellData(URLCellDataPB.deserialize(bytes));
|
||||
case FieldType.Checklist:
|
||||
return pbToChecklistCellData(ChecklistCellDataPB.deserialize(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
export const pbToCell = (pb: CellPB, fieldType: FieldType = pb.field_type): Cell => {
|
||||
return {
|
||||
rowId: pb.row_id,
|
||||
fieldId: pb.field_id,
|
||||
fieldType: fieldType,
|
||||
data: bytesToCellData(pb.data, fieldType),
|
||||
};
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
export * from './cell_types';
|
||||
export * as cellService from './cell_service';
|
@ -0,0 +1,87 @@
|
||||
import { DatabaseViewIdPB } from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventGetDatabase,
|
||||
DatabaseEventGetDatabaseId,
|
||||
DatabaseEventGetDatabaseSetting,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { fieldService } from '../field';
|
||||
import { pbToFilter } from '../filter';
|
||||
import { groupService, pbToGroupSetting } from '../group';
|
||||
import { pbToRowMeta } from '../row';
|
||||
import { pbToSort } from '../sort';
|
||||
import { Database } from './database_types';
|
||||
|
||||
export async function getDatabaseId(viewId: string): Promise<string> {
|
||||
const payload = DatabaseViewIdPB.fromObject({ value: viewId });
|
||||
|
||||
const result = await DatabaseEventGetDatabaseId(payload);
|
||||
|
||||
return result.map(value => value.value).unwrap();
|
||||
}
|
||||
|
||||
|
||||
export async function getDatabase(viewId: string) {
|
||||
const payload = DatabaseViewIdPB.fromObject({
|
||||
value: viewId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventGetDatabase(payload);
|
||||
|
||||
return result.map(value => {
|
||||
return {
|
||||
id: value.id,
|
||||
isLinked: value.is_linked,
|
||||
layoutType: value.layout_type,
|
||||
fieldIds: value.fields.map(field => field.field_id),
|
||||
rowMetas: value.rows.map(pbToRowMeta),
|
||||
};
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
export async function getDatabaseSetting(viewId: string) {
|
||||
const payload = DatabaseViewIdPB.fromObject({
|
||||
value: viewId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventGetDatabaseSetting(payload);
|
||||
|
||||
return result.map(value => {
|
||||
return {
|
||||
filters: value.filters.items.map(pbToFilter),
|
||||
sorts: value.sorts.items.map(pbToSort),
|
||||
groupSettings: value.group_settings.items.map(pbToGroupSetting),
|
||||
};
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
export async function openDatabase(viewId: string): Promise<Database> {
|
||||
const {
|
||||
id,
|
||||
isLinked,
|
||||
layoutType,
|
||||
fieldIds,
|
||||
rowMetas,
|
||||
} = await getDatabase(viewId);
|
||||
|
||||
const {
|
||||
filters,
|
||||
sorts,
|
||||
groupSettings,
|
||||
} = await getDatabaseSetting(viewId);
|
||||
|
||||
const fields = await fieldService.getFields(viewId, fieldIds);
|
||||
|
||||
const groups = await groupService.getGroups(viewId);
|
||||
|
||||
return {
|
||||
id,
|
||||
isLinked,
|
||||
layoutType,
|
||||
fields,
|
||||
rowMetas,
|
||||
filters,
|
||||
sorts,
|
||||
groups,
|
||||
groupSettings,
|
||||
};
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
import { DatabaseLayoutPB } from '@/services/backend';
|
||||
import { Field } from '../field';
|
||||
import { Filter } from '../filter';
|
||||
import { GroupSetting, Group } from '../group';
|
||||
import { RowMeta } from '../row';
|
||||
import { Sort } from '../sort';
|
||||
|
||||
export interface Database {
|
||||
id: string;
|
||||
isLinked: boolean;
|
||||
layoutType: DatabaseLayoutPB,
|
||||
fields: Field[];
|
||||
rowMetas: RowMeta[];
|
||||
filters: Filter[];
|
||||
sorts: Sort[];
|
||||
groupSettings: GroupSetting[];
|
||||
groups: Group[];
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './database_types';
|
||||
export * as databaseService from './database_service';
|
@ -0,0 +1,70 @@
|
||||
import {
|
||||
CreateViewPayloadPB,
|
||||
RepeatedViewIdPB,
|
||||
UpdateViewPayloadPB,
|
||||
ViewIdPB,
|
||||
ViewLayoutPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
FolderEventCreateView,
|
||||
FolderEventDeleteView,
|
||||
FolderEventReadView,
|
||||
FolderEventUpdateView,
|
||||
} from '@/services/backend/events/flowy-folder2';
|
||||
import { databaseService } from '../database';
|
||||
import { DatabaseView, DatabaseViewLayout, pbToDatabaseView } from './database_view_types';
|
||||
|
||||
export async function getDatabaseViews(viewId: string): Promise<DatabaseView[]> {
|
||||
const payload = ViewIdPB.fromObject({ value: viewId });
|
||||
|
||||
const result = await FolderEventReadView(payload);
|
||||
|
||||
return result.map(value => {
|
||||
return [
|
||||
pbToDatabaseView(value),
|
||||
...value.child_views.map(pbToDatabaseView),
|
||||
];
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
export async function createDatabaseView(
|
||||
viewId: string,
|
||||
layout: DatabaseViewLayout,
|
||||
name: string,
|
||||
databaseId?: string,
|
||||
): Promise<DatabaseView> {
|
||||
const payload = CreateViewPayloadPB.fromObject({
|
||||
parent_view_id: viewId,
|
||||
name,
|
||||
layout,
|
||||
meta: {
|
||||
'database_id': databaseId || await databaseService.getDatabaseId(viewId),
|
||||
},
|
||||
});
|
||||
|
||||
const result = await FolderEventCreateView(payload);
|
||||
|
||||
return result.map(pbToDatabaseView).unwrap();
|
||||
}
|
||||
|
||||
export async function updateView(viewId: string, view: { name?: string; layout?: ViewLayoutPB }): Promise<DatabaseView> {
|
||||
const payload = UpdateViewPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
name: view.name,
|
||||
layout: view.layout,
|
||||
});
|
||||
|
||||
const result = await FolderEventUpdateView(payload);
|
||||
|
||||
return result.map(pbToDatabaseView).unwrap();
|
||||
}
|
||||
|
||||
export async function deleteView(viewId: string): Promise<void> {
|
||||
const payload = RepeatedViewIdPB.fromObject({
|
||||
items: [viewId],
|
||||
});
|
||||
|
||||
const result = await FolderEventDeleteView(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { CalendarLayoutPB, ViewLayoutPB, ViewPB } from '@/services/backend';
|
||||
|
||||
export type DatabaseViewLayout = ViewLayoutPB.Grid | ViewLayoutPB.Board | ViewLayoutPB.Calendar;
|
||||
|
||||
export interface DatabaseView {
|
||||
id: string;
|
||||
name: string;
|
||||
layout: DatabaseViewLayout;
|
||||
}
|
||||
|
||||
export interface CalendarLayoutSetting {
|
||||
fieldId?: string;
|
||||
layoutTy?: CalendarLayoutPB;
|
||||
firstDayOfWeek?: number;
|
||||
showWeekends?: boolean;
|
||||
showWeekNumbers?: boolean;
|
||||
}
|
||||
|
||||
export function pbToDatabaseView(viewPB: ViewPB): DatabaseView {
|
||||
return {
|
||||
id: viewPB.id,
|
||||
layout: viewPB.layout as DatabaseViewLayout,
|
||||
name: viewPB.name,
|
||||
};
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './database_view_types';
|
||||
export * as databaseViewService from './database_view_service';
|
@ -0,0 +1,126 @@
|
||||
import {
|
||||
CreateFieldPayloadPB,
|
||||
DeleteFieldPayloadPB,
|
||||
DuplicateFieldPayloadPB,
|
||||
FieldChangesetPB,
|
||||
FieldType,
|
||||
GetFieldPayloadPB,
|
||||
MoveFieldPayloadPB,
|
||||
RepeatedFieldIdPB,
|
||||
UpdateFieldTypePayloadPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventDuplicateField,
|
||||
DatabaseEventUpdateField,
|
||||
DatabaseEventUpdateFieldType,
|
||||
DatabaseEventMoveField,
|
||||
DatabaseEventGetFields,
|
||||
DatabaseEventDeleteField,
|
||||
DatabaseEventCreateTypeOption,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { Field, pbToField } from './field_types';
|
||||
import { bytesToTypeOption, getTypeOption } from './type_option';
|
||||
|
||||
export async function getFields(viewId: string, fieldIds?: string[]): Promise<Field[]> {
|
||||
const payload = GetFieldPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_ids: fieldIds ? RepeatedFieldIdPB.fromObject({
|
||||
items: fieldIds.map(fieldId => ({ field_id: fieldId })),
|
||||
}) : undefined,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventGetFields(payload);
|
||||
|
||||
const fields = result.map((value) => value.items.map(pbToField)).unwrap();
|
||||
|
||||
await Promise.all(fields.map(async field => {
|
||||
const typeOption = await getTypeOption(viewId, field.id, field.type);
|
||||
|
||||
field.typeOption = typeOption;
|
||||
}));
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
export async function createField(viewId: string, fieldType?: FieldType, data?: Uint8Array): Promise<Field> {
|
||||
const payload = CreateFieldPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_type: fieldType,
|
||||
type_option_data: data,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventCreateTypeOption(payload);
|
||||
|
||||
return result.map(value => {
|
||||
const field = pbToField(value.field);
|
||||
|
||||
field.typeOption = bytesToTypeOption(value.type_option_data, field.type);
|
||||
|
||||
return field;
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
export async function duplicateField(viewId: string, fieldId: string): Promise<void> {
|
||||
const payload = DuplicateFieldPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventDuplicateField(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function updateField(viewId: string, fieldId: string, data: {
|
||||
name?: string;
|
||||
desc?: string;
|
||||
frozen?: boolean;
|
||||
visibility?: boolean;
|
||||
width?: number;
|
||||
}): Promise<void> {
|
||||
const payload = FieldChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
...data,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateField(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function updateFieldType(viewId: string, fieldId: string, fieldType: FieldType): Promise<void> {
|
||||
const payload = UpdateFieldTypePayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateFieldType(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function moveField(viewId: string, fieldId: string, fromIndex: number, toIndex: number): Promise<void> {
|
||||
const payload = MoveFieldPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
from_index: fromIndex,
|
||||
to_index: toIndex,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventMoveField(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function deleteField(viewId: string, fieldId: string): Promise<void> {
|
||||
const payload = DeleteFieldPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventDeleteField(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import {
|
||||
FieldPB,
|
||||
FieldType,
|
||||
} from '@/services/backend';
|
||||
import { DateTimeTypeOption, NumberTypeOption, SelectTypeOption } from './type_option/type_option_types';
|
||||
|
||||
export interface Field {
|
||||
id: string;
|
||||
name: string;
|
||||
type: FieldType;
|
||||
typeOption?: unknown;
|
||||
visibility: boolean;
|
||||
width: number;
|
||||
isPrimary: boolean;
|
||||
}
|
||||
|
||||
export interface NumberField extends Field {
|
||||
type: FieldType.Number;
|
||||
typeOption: NumberTypeOption;
|
||||
}
|
||||
|
||||
export interface DateTimeField extends Field {
|
||||
type: FieldType.DateTime;
|
||||
typeOption: DateTimeTypeOption;
|
||||
}
|
||||
|
||||
export interface SelectField extends Field {
|
||||
type: FieldType.SingleSelect | FieldType.MultiSelect;
|
||||
typeOption: SelectTypeOption;
|
||||
}
|
||||
|
||||
export type UndeterminedField = NumberField | DateTimeField | SelectField | Field;
|
||||
|
||||
export const pbToField = (pb: FieldPB): Field => ({
|
||||
id: pb.id,
|
||||
name: pb.name,
|
||||
type: pb.field_type,
|
||||
visibility: pb.visibility,
|
||||
width: pb.width,
|
||||
isPrimary: pb.is_primary,
|
||||
});
|
@ -0,0 +1,4 @@
|
||||
export * from './select_option';
|
||||
export * from './type_option';
|
||||
export * from './field_types';
|
||||
export * as fieldService from './field_service';
|
@ -0,0 +1,2 @@
|
||||
export * from './select_option_types';
|
||||
export * as selectOptionService from './select_option_service';
|
@ -0,0 +1,61 @@
|
||||
import {
|
||||
CreateSelectOptionPayloadPB,
|
||||
RepeatedSelectOptionPayload,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventCreateSelectOption,
|
||||
DatabaseEventInsertOrUpdateSelectOption,
|
||||
DatabaseEventDeleteSelectOption,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { pbToSelectOption, SelectOption } from './select_option_types';
|
||||
|
||||
export async function createSelectOption(viewId: string, fieldId: string, optionName: string): Promise<SelectOption> {
|
||||
const payload = CreateSelectOptionPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
option_name: optionName,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventCreateSelectOption(payload);
|
||||
|
||||
return result.map(pbToSelectOption).unwrap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [rowId] If pass the rowId, the cell will select this option after insert or update.
|
||||
*/
|
||||
export async function insertOrUpdateSelectOption(
|
||||
viewId: string,
|
||||
fieldId: string,
|
||||
items: Partial<SelectOption>[],
|
||||
rowId?: string,
|
||||
): Promise<void> {
|
||||
const payload = RepeatedSelectOptionPayload.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
row_id: rowId,
|
||||
items: items,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventInsertOrUpdateSelectOption(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function deleteSelectOption(
|
||||
viewId: string,
|
||||
fieldId: string,
|
||||
items: Partial<SelectOption>[],
|
||||
rowId?: string,
|
||||
): Promise<void> {
|
||||
const payload = RepeatedSelectOptionPayload.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
row_id: rowId,
|
||||
items: items,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventDeleteSelectOption(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
import { SelectOptionColorPB, SelectOptionPB } from '@/services/backend';
|
||||
|
||||
export interface SelectOption {
|
||||
id: string;
|
||||
name: string;
|
||||
color: SelectOptionColorPB;
|
||||
}
|
||||
|
||||
export function pbToSelectOption(pb: SelectOptionPB): SelectOption {
|
||||
return {
|
||||
id: pb.id,
|
||||
name: pb.name,
|
||||
color: pb.color,
|
||||
};
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './type_option_types';
|
||||
export * from './type_option_service';
|
@ -0,0 +1,18 @@
|
||||
import {
|
||||
FieldType,
|
||||
TypeOptionPathPB,
|
||||
} from '@/services/backend';
|
||||
import { DatabaseEventGetTypeOption } from '@/services/backend/events/flowy-database2';
|
||||
import { bytesToTypeOption } from './type_option_types';
|
||||
|
||||
export async function getTypeOption(viewId: string, fieldId: string, fieldType: FieldType) {
|
||||
const payload = TypeOptionPathPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventGetTypeOption(payload);
|
||||
|
||||
return result.map(value => bytesToTypeOption(value.type_option_data, fieldType)).unwrap();
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
import {
|
||||
CheckboxTypeOptionPB,
|
||||
DateFormatPB,
|
||||
FieldType,
|
||||
MultiSelectTypeOptionPB,
|
||||
NumberFormatPB,
|
||||
NumberTypeOptionPB,
|
||||
RichTextTypeOptionPB,
|
||||
SingleSelectTypeOptionPB,
|
||||
TimeFormatPB,
|
||||
} from '@/services/backend';
|
||||
import { pbToSelectOption, SelectOption } from '../select_option';
|
||||
|
||||
export interface TextTypeOption {
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface NumberTypeOption {
|
||||
format?: NumberFormatPB;
|
||||
scale?: number;
|
||||
symbol?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface DateTimeTypeOption {
|
||||
dateFormat?: DateFormatPB;
|
||||
timeFormat?: TimeFormatPB;
|
||||
timezoneId?: string;
|
||||
fieldType?: FieldType;
|
||||
}
|
||||
|
||||
export interface SelectTypeOption {
|
||||
options?: SelectOption[];
|
||||
disableColor?: boolean;
|
||||
}
|
||||
|
||||
export interface CheckboxTypeOption {
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
|
||||
function pbToSelectTypeOption(pb: SingleSelectTypeOptionPB | MultiSelectTypeOptionPB): SelectTypeOption {
|
||||
return {
|
||||
options: pb.options?.map(pbToSelectOption),
|
||||
disableColor: pb.disable_color,
|
||||
};
|
||||
}
|
||||
|
||||
function pbToCheckboxTypeOption(pb: CheckboxTypeOptionPB): CheckboxTypeOption {
|
||||
return {
|
||||
isSelected: pb.is_selected,
|
||||
};
|
||||
}
|
||||
|
||||
export function bytesToTypeOption(data: Uint8Array, fieldType: FieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
return RichTextTypeOptionPB.deserialize(data).toObject() as TextTypeOption;
|
||||
case FieldType.Number:
|
||||
return NumberTypeOptionPB.deserialize(data).toObject() as NumberTypeOption;
|
||||
case FieldType.SingleSelect:
|
||||
return pbToSelectTypeOption(SingleSelectTypeOptionPB.deserialize(data));
|
||||
case FieldType.MultiSelect:
|
||||
return pbToSelectTypeOption(MultiSelectTypeOptionPB.deserialize(data));
|
||||
case FieldType.Checkbox:
|
||||
return pbToCheckboxTypeOption(CheckboxTypeOptionPB.deserialize(data));
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import {
|
||||
DatabaseEventGetAllFilters,
|
||||
DatabaseEventUpdateDatabaseSetting,
|
||||
DatabaseSettingChangesetPB,
|
||||
DatabaseViewIdPB,
|
||||
DeleteFilterPayloadPB,
|
||||
FieldType,
|
||||
FilterPB,
|
||||
UpdateFilterPayloadPB,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { Filter, filterDataToPB, UndeterminedFilter } from './filter_types';
|
||||
|
||||
export async function getAllFilters(viewId: string): Promise<FilterPB[]> {
|
||||
const payload = DatabaseViewIdPB.fromObject({ value: viewId });
|
||||
|
||||
const result = await DatabaseEventGetAllFilters(payload);
|
||||
|
||||
return result.map(value => value.items).unwrap();
|
||||
}
|
||||
|
||||
export async function insertFilter(
|
||||
viewId: string,
|
||||
fieldId: string,
|
||||
fieldType: FieldType,
|
||||
data: UndeterminedFilter['data'],
|
||||
): Promise<void> {
|
||||
const payload = DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
update_filter: UpdateFilterPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
data: filterDataToPB(data, fieldType)?.serialize(),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateDatabaseSetting(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function updateFilter(viewId: string, filter: UndeterminedFilter): Promise<void> {
|
||||
const payload = DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
update_filter: UpdateFilterPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
filter_id: filter.id,
|
||||
field_id: filter.fieldId,
|
||||
field_type: filter.fieldType,
|
||||
data: filterDataToPB(filter.data, filter.fieldType)?.serialize(),
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateDatabaseSetting(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function deleteFilter(viewId: string, filter: Omit<Filter, 'data'>): Promise<void> {
|
||||
const payload = DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
delete_filter: DeleteFilterPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
filter_id: filter.id,
|
||||
field_id: filter.fieldId,
|
||||
field_type: filter.fieldType,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateDatabaseSetting(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import {
|
||||
FieldType,
|
||||
TextFilterConditionPB,
|
||||
SelectOptionConditionPB,
|
||||
TextFilterPB,
|
||||
SelectOptionFilterPB,
|
||||
FilterPB,
|
||||
} from '@/services/backend';
|
||||
|
||||
export interface Filter {
|
||||
id: string;
|
||||
fieldId: string;
|
||||
fieldType: FieldType;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface TextFilter extends Filter {
|
||||
fieldType: FieldType.RichText;
|
||||
data: TextFilterData;
|
||||
}
|
||||
|
||||
export interface TextFilterData {
|
||||
condition: TextFilterConditionPB;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface SelectFilter extends Filter {
|
||||
fieldType: FieldType.SingleSelect | FieldType.MultiSelect;
|
||||
data: SelectFilterData;
|
||||
}
|
||||
|
||||
export interface SelectFilterData {
|
||||
condition?: SelectOptionConditionPB;
|
||||
optionIds?: string[];
|
||||
}
|
||||
|
||||
export type UndeterminedFilter = TextFilter | SelectFilter;
|
||||
|
||||
export function filterDataToPB(data: UndeterminedFilter['data'], fieldType: FieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
return TextFilterPB.fromObject({
|
||||
condition: (data as TextFilterData).condition,
|
||||
content: (data as TextFilterData).content,
|
||||
});
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return SelectOptionFilterPB.fromObject({
|
||||
condition: (data as SelectFilterData).condition,
|
||||
option_ids: (data as SelectFilterData).optionIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function pbToTextFilterData(pb: TextFilterPB): TextFilterData {
|
||||
return {
|
||||
condition: pb.condition,
|
||||
content: pb.content,
|
||||
};
|
||||
}
|
||||
|
||||
export function pbToSelectFilterData(pb: SelectOptionFilterPB): SelectFilterData {
|
||||
return {
|
||||
condition: pb.condition,
|
||||
optionIds: pb.option_ids,
|
||||
};
|
||||
}
|
||||
|
||||
export function bytesToFilterData(bytes: Uint8Array, fieldType: FieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
return pbToTextFilterData(TextFilterPB.deserialize(bytes));
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return pbToSelectFilterData(SelectOptionFilterPB.deserialize(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
export function pbToFilter(pb: FilterPB): Filter {
|
||||
return {
|
||||
id: pb.id,
|
||||
fieldId: pb.field_id,
|
||||
fieldType: pb.field_type,
|
||||
data: bytesToFilterData(pb.data, pb.field_type),
|
||||
};
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './filter_types';
|
||||
export * as filterService from './filter_service';
|
@ -0,0 +1,64 @@
|
||||
import {
|
||||
DatabaseViewIdPB,
|
||||
GroupByFieldPayloadPB,
|
||||
MoveGroupPayloadPB,
|
||||
UpdateGroupPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventGetGroups,
|
||||
DatabaseEventMoveGroup,
|
||||
DatabaseEventSetGroupByField,
|
||||
DatabaseEventUpdateGroup,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { Group, pbToGroup } from './group_types';
|
||||
|
||||
export async function getGroups(viewId: string): Promise<Group[]> {
|
||||
const payload = DatabaseViewIdPB.fromObject({ value: viewId });
|
||||
|
||||
const result = await DatabaseEventGetGroups(payload);
|
||||
|
||||
return result.map(value => value.items.map(pbToGroup)).unwrap();
|
||||
}
|
||||
|
||||
export async function setGroupByField(viewId: string, fieldId: string): Promise<void> {
|
||||
const payload = GroupByFieldPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventSetGroupByField(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function updateGroup(
|
||||
viewId: string,
|
||||
group: {
|
||||
id: string,
|
||||
name?: string,
|
||||
visible?: boolean,
|
||||
},
|
||||
): Promise<void> {
|
||||
const payload = UpdateGroupPB.fromObject({
|
||||
view_id: viewId,
|
||||
group_id: group.id,
|
||||
name: group.name,
|
||||
visible: group.visible,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateGroup(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function moveGroup(viewId: string, fromGroupId: string, toGroupId: string): Promise<void> {
|
||||
const payload = MoveGroupPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
from_group_id: fromGroupId,
|
||||
to_group_id: toGroupId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventMoveGroup(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
import { GroupPB, GroupSettingPB } from '@/services/backend';
|
||||
import { pbToRowMeta, RowMeta } from '../row';
|
||||
|
||||
export interface GroupSetting {
|
||||
id: string;
|
||||
fieldId: string;
|
||||
}
|
||||
|
||||
export interface Group {
|
||||
id: string;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
isVisible: boolean;
|
||||
fieldId: string;
|
||||
rows: RowMeta[];
|
||||
}
|
||||
|
||||
export function pbToGroup(pb: GroupPB): Group {
|
||||
return {
|
||||
id: pb.group_id,
|
||||
name: pb.group_name,
|
||||
isDefault: pb.is_default,
|
||||
isVisible: pb.is_visible,
|
||||
fieldId: pb.field_id,
|
||||
rows: pb.rows.map(pbToRowMeta),
|
||||
};
|
||||
}
|
||||
|
||||
export function pbToGroupSetting(pb: GroupSettingPB): GroupSetting {
|
||||
return {
|
||||
id: pb.id,
|
||||
fieldId: pb.field_id,
|
||||
};
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
export * from './group_types';
|
||||
export * as groupService from './group_service';
|
@ -0,0 +1,8 @@
|
||||
export * from './cell';
|
||||
export * from './database';
|
||||
export * from './database_view';
|
||||
export * from './field';
|
||||
export * from './filter';
|
||||
export * from './group';
|
||||
export * from './row';
|
||||
export * from './sort';
|
@ -0,0 +1,3 @@
|
||||
export * from './row_types';
|
||||
export * as rowService from './row_service';
|
||||
export * as rowListeners from './row_listeners';
|
@ -0,0 +1,61 @@
|
||||
import {
|
||||
ReorderAllRowsPB,
|
||||
ReorderSingleRowPB,
|
||||
RowsChangePB,
|
||||
} from '@/services/backend';
|
||||
import { Database } from '../database';
|
||||
import { pbToRowMeta, RowMeta } from './row_types';
|
||||
|
||||
const deleteRowsFromChangeset = (database: Database, changeset: RowsChangePB) => {
|
||||
changeset.deleted_rows.forEach(rowId => {
|
||||
const index = database.rowMetas.findIndex(row => row.id === rowId);
|
||||
|
||||
if (index !== -1) {
|
||||
database.rowMetas.splice(index, 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const insertRowsFromChangeset = (database: Database, changeset: RowsChangePB) => {
|
||||
changeset.inserted_rows.forEach(({ index, row_meta: rowMetaPB }) => {
|
||||
database.rowMetas.splice(index, 0, pbToRowMeta(rowMetaPB));
|
||||
});
|
||||
};
|
||||
|
||||
const updateRowsFromChangeset = (database: Database, changeset: RowsChangePB) => {
|
||||
changeset.updated_rows.forEach(({ row_id: rowId, row_meta: rowMetaPB }) => {
|
||||
const found = database.rowMetas.find(rowMeta => rowMeta.id === rowId);
|
||||
|
||||
if (found) {
|
||||
Object.assign(found, pbToRowMeta(rowMetaPB));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const didUpdateViewRows = (database: Database, changeset: RowsChangePB) => {
|
||||
deleteRowsFromChangeset(database, changeset);
|
||||
insertRowsFromChangeset(database, changeset);
|
||||
updateRowsFromChangeset(database, changeset);
|
||||
};
|
||||
|
||||
export const didReorderRows = (database: Database, changeset: ReorderAllRowsPB) => {
|
||||
const rowById = database.rowMetas.reduce<Record<string, RowMeta>>((prev, cur) => {
|
||||
prev[cur.id] = cur;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
database.rowMetas = changeset.row_orders.map(rowId => rowById[rowId]);
|
||||
};
|
||||
|
||||
export const didReorderSingleRow = (database: Database, changeset: ReorderSingleRowPB) => {
|
||||
const {
|
||||
row_id: rowId,
|
||||
new_index: newIndex,
|
||||
} = changeset;
|
||||
|
||||
const oldIndex = database.rowMetas.findIndex(rowMeta => rowMeta.id === rowId);
|
||||
|
||||
if (oldIndex !== -1) {
|
||||
database.rowMetas.splice(newIndex, 0, database.rowMetas.splice(oldIndex, 1)[0]);
|
||||
}
|
||||
};
|
@ -0,0 +1,124 @@
|
||||
import {
|
||||
CreateRowPayloadPB,
|
||||
MoveGroupRowPayloadPB,
|
||||
MoveRowPayloadPB,
|
||||
RowIdPB,
|
||||
UpdateRowMetaChangesetPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventCreateRow,
|
||||
DatabaseEventDeleteRow,
|
||||
DatabaseEventDuplicateRow,
|
||||
DatabaseEventGetRowMeta,
|
||||
DatabaseEventMoveGroupRow,
|
||||
DatabaseEventMoveRow,
|
||||
DatabaseEventUpdateRowMeta,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { pbToRowMeta, RowMeta } from './row_types';
|
||||
|
||||
export async function createRow(viewId: string, params?: {
|
||||
startRowId?: string;
|
||||
groupId?: string;
|
||||
data?: Record<string, string>;
|
||||
}): Promise<RowMeta> {
|
||||
const payload = CreateRowPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
start_row_id: params?.startRowId,
|
||||
group_id: params?.groupId,
|
||||
data: params?.data ? { cell_data_by_field_id: params.data } : undefined,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventCreateRow(payload);
|
||||
|
||||
return result.map(pbToRowMeta).unwrap();
|
||||
}
|
||||
|
||||
export async function duplicateRow(viewId: string, rowId: string, groupId?: string): Promise<void> {
|
||||
const payload = RowIdPB.fromObject({
|
||||
view_id: viewId,
|
||||
row_id: rowId,
|
||||
group_id: groupId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventDuplicateRow(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function deleteRow(viewId: string, rowId: string, groupId?: string): Promise<void> {
|
||||
const payload = RowIdPB.fromObject({
|
||||
view_id: viewId,
|
||||
row_id: rowId,
|
||||
group_id: groupId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventDeleteRow(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function moveRow(viewId: string, fromRowId: string, toRowId: string): Promise<void> {
|
||||
const payload = MoveRowPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
from_row_id: fromRowId,
|
||||
to_row_id: toRowId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventMoveRow(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the row from one group to another group
|
||||
*
|
||||
* @param fromRowId
|
||||
* @param toGroupId
|
||||
* @param toRowId used to locate the moving row location.
|
||||
* @returns
|
||||
*/
|
||||
export async function moveGroupRow(viewId: string, fromRowId: string, toGroupId: string, toRowId?: string): Promise<void> {
|
||||
const payload = MoveGroupRowPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
from_row_id: fromRowId,
|
||||
to_group_id: toGroupId,
|
||||
to_row_id: toRowId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventMoveGroupRow(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
|
||||
export async function getRowMeta(viewId: string, rowId: string, groupId?: string): Promise<RowMeta> {
|
||||
const payload = RowIdPB.fromObject({
|
||||
view_id: viewId,
|
||||
row_id: rowId,
|
||||
group_id: groupId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventGetRowMeta(payload);
|
||||
|
||||
return result.map(pbToRowMeta).unwrap();
|
||||
}
|
||||
|
||||
export async function updateRowMeta(
|
||||
viewId: string,
|
||||
rowId: string,
|
||||
meta: {
|
||||
iconUrl?: string;
|
||||
coverUrl?: string;
|
||||
},
|
||||
): Promise<void> {
|
||||
const payload = UpdateRowMetaChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
id: rowId,
|
||||
icon_url: meta.iconUrl,
|
||||
cover_url: meta.coverUrl,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateRowMeta(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { RowMetaPB } from '@/services/backend';
|
||||
|
||||
export interface RowMeta {
|
||||
id: string;
|
||||
documentId?: string;
|
||||
icon?: string;
|
||||
cover?: string;
|
||||
}
|
||||
|
||||
export function pbToRowMeta(pb: RowMetaPB): RowMeta {
|
||||
const rowMeta: RowMeta = {
|
||||
id: pb.id,
|
||||
};
|
||||
|
||||
if (pb.document_id) {
|
||||
rowMeta.documentId = pb.document_id;
|
||||
}
|
||||
|
||||
if (pb.icon) {
|
||||
rowMeta.icon = pb.icon;
|
||||
}
|
||||
|
||||
if (pb.cover) {
|
||||
rowMeta.cover = pb.cover;
|
||||
}
|
||||
|
||||
return rowMeta;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export * from './sort_types';
|
||||
export * as sortService from './sort_service';
|
||||
export * as sortListeners from './sort_listeners';
|
@ -0,0 +1,35 @@
|
||||
import { SortChangesetNotificationPB } from '@/services/backend';
|
||||
import { Database } from '../database';
|
||||
import { pbToSort } from './sort_types';
|
||||
|
||||
const deleteSortsFromChange = (database: Database, changeset: SortChangesetNotificationPB) => {
|
||||
const deleteIds = changeset.delete_sorts.map(sort => sort.id);
|
||||
|
||||
if (deleteIds.length) {
|
||||
database.sorts = database.sorts.filter(sort => !deleteIds.includes(sort.id));
|
||||
}
|
||||
};
|
||||
|
||||
const insertSortsFromChange = (database: Database, changeset: SortChangesetNotificationPB) => {
|
||||
changeset.insert_sorts.forEach(sortPB => {
|
||||
database.sorts.push(pbToSort(sortPB));
|
||||
});
|
||||
};
|
||||
|
||||
const updateSortsFromChange = (database: Database, changeset: SortChangesetNotificationPB) => {
|
||||
changeset.update_sorts.forEach(sortPB => {
|
||||
const found = database.sorts.find(sort => sort.id === sortPB.id);
|
||||
|
||||
if (found) {
|
||||
const newSort = pbToSort(sortPB);
|
||||
|
||||
Object.assign(found, newSort);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const didUpdateSort = (database: Database, changeset: SortChangesetNotificationPB) => {
|
||||
deleteSortsFromChange(database, changeset);
|
||||
insertSortsFromChange(database, changeset);
|
||||
updateSortsFromChange(database, changeset);
|
||||
};
|
@ -0,0 +1,77 @@
|
||||
import {
|
||||
DatabaseViewIdPB,
|
||||
DatabaseSettingChangesetPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventDeleteAllSorts,
|
||||
DatabaseEventGetAllSorts, DatabaseEventUpdateDatabaseSetting,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { pbToSort, Sort } from './sort_types';
|
||||
|
||||
export async function getAllSorts(viewId: string): Promise<Sort[]> {
|
||||
const payload = DatabaseViewIdPB.fromObject({
|
||||
value: viewId,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventGetAllSorts(payload);
|
||||
|
||||
return result.map(value => value.items.map(pbToSort)).unwrap();
|
||||
}
|
||||
|
||||
export async function insertSort(viewId: string, sort: Omit<Sort, 'id'>): Promise<void> {
|
||||
const payload = DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
update_sort: {
|
||||
view_id: viewId,
|
||||
field_id: sort.fieldId,
|
||||
field_type: sort.fieldType,
|
||||
condition: sort.condition,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateDatabaseSetting(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function updateSort(viewId: string, sort: Sort): Promise<void> {
|
||||
const payload = DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
update_sort: {
|
||||
view_id: viewId,
|
||||
sort_id: sort.id,
|
||||
field_id: sort.fieldId,
|
||||
field_type: sort.fieldType,
|
||||
condition: sort.condition,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateDatabaseSetting(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function deleteSort(viewId: string, sort: Sort): Promise<void> {
|
||||
const payload = DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
delete_sort: {
|
||||
view_id: viewId,
|
||||
sort_id: sort.id,
|
||||
field_id: sort.fieldId,
|
||||
field_type: sort.fieldType,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateDatabaseSetting(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
||||
|
||||
export async function deleteAllSorts(viewId: string): Promise<void> {
|
||||
const payload = DatabaseViewIdPB.fromObject({
|
||||
value: viewId,
|
||||
});
|
||||
const result = await DatabaseEventDeleteAllSorts(payload);
|
||||
|
||||
return result.unwrap();
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { FieldType, SortConditionPB, SortPB } from '@/services/backend';
|
||||
|
||||
export interface Sort {
|
||||
id: string;
|
||||
fieldId: string;
|
||||
fieldType: FieldType;
|
||||
condition: SortConditionPB;
|
||||
}
|
||||
|
||||
export function pbToSort(pb: SortPB): Sort {
|
||||
return {
|
||||
id: pb.id,
|
||||
fieldId: pb.field_id,
|
||||
fieldType: pb.field_type,
|
||||
condition: pb.condition,
|
||||
};
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
export const Board: FC = () => {
|
||||
return null;
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './Board';
|
@ -0,0 +1,5 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
export const Calendar: FC = () => {
|
||||
return null;
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './Calendar';
|
@ -0,0 +1,24 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { DatabaseNotification, FieldType } from '@/services/backend';
|
||||
import { useNotification, useViewId } from '$app/hooks';
|
||||
import { cellService, Cell } from '../../application';
|
||||
|
||||
export const useCell = (rowId: string, fieldId: string, fieldType: FieldType) => {
|
||||
const viewId = useViewId();
|
||||
const [cell, setCell] = useState<Cell | null>(null);
|
||||
|
||||
const fetchCell = useCallback(() => {
|
||||
void cellService.getCell(viewId, rowId, fieldId, fieldType)
|
||||
.then(data => {
|
||||
setCell(data);
|
||||
});
|
||||
}, [viewId, rowId, fieldId, fieldType]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchCell();
|
||||
}, [fetchCell]);
|
||||
|
||||
useNotification(DatabaseNotification.DidUpdateCell, fetchCell, { id: `${rowId}:${fieldId}` });
|
||||
|
||||
return cell;
|
||||
};
|
@ -0,0 +1,42 @@
|
||||
import { FC } from 'react';
|
||||
import { FieldType } from '@/services/backend';
|
||||
|
||||
import { Cell as CellType, Field } from '../../application';
|
||||
import { useCell } from './Cell.hooks';
|
||||
import { TextCell } from './TextCell';
|
||||
import { SelectCell } from './SelectCell';
|
||||
import { CheckboxCell } from './CheckboxCell';
|
||||
|
||||
export interface CellProps {
|
||||
rowId: string;
|
||||
field: Field;
|
||||
}
|
||||
|
||||
const getCellComponent = (fieldType: FieldType) => {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
return TextCell as FC<{ field: Field, cell: CellType }>;
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return SelectCell as FC<{ field: Field, cell: CellType }>;
|
||||
case FieldType.Checkbox:
|
||||
return CheckboxCell as FC<{ field: Field, cell: CellType }>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Cell: FC<CellProps> = ({
|
||||
rowId,
|
||||
field,
|
||||
}) => {
|
||||
const cell = useCell(rowId, field.id, field.type);
|
||||
|
||||
const Component = getCellComponent(field.type);
|
||||
|
||||
if (!cell || !Component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Component field={field} cell={cell} />;
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
import { Checkbox } from '@mui/material';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { ReactComponent as CheckboxCheckSvg } from '$app/assets/database/checkbox-check.svg';
|
||||
import { ReactComponent as CheckboxUncheckSvg } from '$app/assets/database/checkbox-uncheck.svg';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { cellService, CheckboxCell as CheckboxCellType, Field } from '../../application';
|
||||
|
||||
export const CheckboxCell: FC<{
|
||||
field: Field,
|
||||
cell: CheckboxCellType,
|
||||
}> = ({ field, cell }) => {
|
||||
const viewId = useViewId();
|
||||
const checked = cell.data === 'Yes';
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
void cellService.updateCell(
|
||||
viewId,
|
||||
cell.rowId,
|
||||
field.id,
|
||||
!checked ? 'Yes' : 'No',
|
||||
);
|
||||
}, [viewId, cell.rowId, field.id, checked ]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center w-full px-2 cursor-pointer" onClick={handleClick}>
|
||||
<Checkbox
|
||||
disableRipple
|
||||
style={{ padding: 0 }}
|
||||
checked={checked}
|
||||
icon={<CheckboxUncheckSvg />}
|
||||
checkedIcon={<CheckboxCheckSvg />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -10,9 +10,8 @@ import {
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { Database } from '$app/interfaces/database';
|
||||
import * as service from '../../../database_bd_svc';
|
||||
import { useViewId } from '../../../database.hooks';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { cellService, SelectField, SelectCell as SelectCellType } from '../../../application';
|
||||
import { Tag } from './Tag';
|
||||
import { CreateOption } from './CreateOption';
|
||||
import { SelectOptionItem } from './SelectOptionItem';
|
||||
@ -31,14 +30,14 @@ const menuProps: Partial<MenuProps> = {
|
||||
},
|
||||
};
|
||||
|
||||
export const GridSelectCell: FC<{
|
||||
rowId: string;
|
||||
field: Database.Field;
|
||||
cell: Database.SelectCell | null;
|
||||
}> = ({ rowId, field, cell }) => {
|
||||
export const SelectCell: FC<{
|
||||
field: SelectField;
|
||||
cell: SelectCellType;
|
||||
}> = ({ field, cell }) => {
|
||||
const rowId = cell.rowId;
|
||||
const viewId = useViewId();
|
||||
const options = useMemo(() => cell?.data?.options ?? [], [cell?.data.options]);
|
||||
const selectedIds = useMemo(() => cell?.data.selectOptions?.map(({ id }) => id) ?? [], [cell?.data.selectOptions]);
|
||||
const options = useMemo(() => field.typeOption.options ?? [], [field.typeOption.options]);
|
||||
const selectedIds = useMemo(() => cell.data.selectedOptionIds ?? [], [cell.data.selectedOptionIds]);
|
||||
const [newOptionName, setNewOptionName] = useState('');
|
||||
const filteredOptions = useMemo(() => options.filter(option => {
|
||||
return option.name.toLowerCase().includes(newOptionName.toLowerCase());
|
||||
@ -60,10 +59,10 @@ export const GridSelectCell: FC<{
|
||||
const { target: { value } } = event;
|
||||
|
||||
const current = Array.isArray(value) ? value : [value];
|
||||
const prev = cell?.data.selectOptions?.map(({ id }) => id);
|
||||
const prev = cell.data.selectedOptionIds;
|
||||
const deleteOptionIds = prev?.filter(id => current.find(cur => cur === id) === undefined);
|
||||
|
||||
void service.updateSelectOptionCell(viewId, rowId, field.id, {
|
||||
void cellService.updateSelectCell(viewId, rowId, field.id, {
|
||||
insertOptionIds: current,
|
||||
deleteOptionIds,
|
||||
});
|
||||
@ -73,14 +72,14 @@ export const GridSelectCell: FC<{
|
||||
const exist = options.find(option => option.name.toLowerCase() === newOptionName.toLowerCase());
|
||||
|
||||
if (exist) {
|
||||
return service.updateSelectOptionCell(viewId, rowId, field.id, {
|
||||
return cellService.updateSelectCell(viewId, rowId, field.id, {
|
||||
insertOptionIds: [exist.id],
|
||||
});
|
||||
}
|
||||
|
||||
const option = await service.createSelectOption(viewId, field.id, newOptionName);
|
||||
// const option = await cellService.createSelectOption(viewId, field.id, newOptionName);
|
||||
|
||||
await service.insertOrUpdateSelectOption(viewId, field.id, [option], rowId);
|
||||
// await cellService.insertOrUpdateSelectOption(viewId, field.id, [option], rowId);
|
||||
};
|
||||
|
||||
const searchInput = (
|
@ -1,12 +1,12 @@
|
||||
import { FC, MouseEventHandler, useCallback, useRef, useState } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { Database } from '$app/interfaces/database';
|
||||
import { ReactComponent as DetailsSvg } from '$app/assets/details.svg';
|
||||
import { SelectOption } from '../../../application';
|
||||
import { SelectOptionMenu } from './SelectOptionMenu';
|
||||
import { Tag } from './Tag';
|
||||
|
||||
export interface SelectOptionItemProps {
|
||||
option: Database.SelectOption;
|
||||
option: SelectOption;
|
||||
}
|
||||
|
||||
export const SelectOptionItem: FC<SelectOptionItemProps> = ({
|
||||
@ -46,4 +46,4 @@ export const SelectOptionItem: FC<SelectOptionItemProps> = ({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
};
|
@ -1,14 +1,21 @@
|
||||
import { FC } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import { Divider, ListSubheader, Menu, MenuItem, MenuProps, OutlinedInput } from '@mui/material'
|
||||
import {
|
||||
Divider,
|
||||
ListSubheader,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuProps,
|
||||
OutlinedInput,
|
||||
} from '@mui/material';
|
||||
import { SelectOptionColorPB } from '@/services/backend';
|
||||
import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
|
||||
import { Database } from '$app/interfaces/database';
|
||||
import { SelectOptionColorMap, SelectOptionColorTextMap } from './constants';
|
||||
import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg';
|
||||
import { SelectOption } from '../../../application';
|
||||
import { SelectOptionColorMap, SelectOptionColorTextMap } from './constants';
|
||||
|
||||
interface SelectOptionMenuProps {
|
||||
option: Database.SelectOption;
|
||||
option: SelectOption;
|
||||
open: boolean;
|
||||
MenuProps?: Partial<MenuProps>;
|
||||
}
|
||||
@ -67,5 +74,5 @@ export const SelectOptionMenu: FC<SelectOptionMenuProps> = ({
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { SelectOptionColorPB } from "@/services/backend";
|
||||
import { SelectOptionColorPB } from '@/services/backend';
|
||||
|
||||
export const SelectOptionColorMap = {
|
||||
[SelectOptionColorPB.Purple]: 'bg-tint-purple',
|
@ -0,0 +1 @@
|
||||
export * from './SelectCell';
|
@ -1,41 +1,39 @@
|
||||
import { Popover, TextareaAutosize } from '@mui/material';
|
||||
import { FC, FormEventHandler, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Database } from '$app/interfaces/database';
|
||||
import * as service from '$app/components/database/database_bd_svc';
|
||||
import { useViewId } from '../../database.hooks';
|
||||
import { FC, FormEventHandler, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { cellService, Field, TextCell as TextCellType } from '../../application';
|
||||
import { CellText } from '../../_shared';
|
||||
|
||||
export const GridTextCell: FC<{
|
||||
rowId: string;
|
||||
field: Database.Field,
|
||||
cell: Database.TextCell | null;
|
||||
}> = ({ rowId, field, cell }) => {
|
||||
export const TextCell: FC<{
|
||||
field: Field,
|
||||
cell: TextCellType;
|
||||
}> = ({ field, cell }) => {
|
||||
const viewId = useViewId();
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
const [ editing, setEditing ] = useState(false);
|
||||
const [ text, setText ] = useState('');
|
||||
const [ width, setWidth ] = useState<number | undefined>(undefined);
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClose = () => {
|
||||
if (editing) {
|
||||
if (text !== cell?.data) {
|
||||
void service.updateCell(viewId, rowId, field.id, text);
|
||||
if (text !== cell.data) {
|
||||
void cellService.updateCell(viewId, cell.rowId, field.id, text);
|
||||
}
|
||||
|
||||
setEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubleClick = useCallback(() => {
|
||||
setText(cell?.data ?? '');
|
||||
const handleClick = useCallback(() => {
|
||||
setText(cell.data);
|
||||
setEditing(true);
|
||||
}, [cell?.data]);
|
||||
}, [cell.data]);
|
||||
|
||||
const handleInput = useCallback<FormEventHandler<HTMLTextAreaElement>>((event) => {
|
||||
setText((event.target as HTMLTextAreaElement).value);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (cellRef.current) {
|
||||
setWidth(cellRef.current.clientWidth);
|
||||
}
|
||||
@ -46,9 +44,9 @@ export const GridTextCell: FC<{
|
||||
<CellText
|
||||
ref={cellRef}
|
||||
className="w-full"
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{cell?.data}
|
||||
{cell.data}
|
||||
</CellText>
|
||||
{editing && (
|
||||
<Popover
|
@ -0,0 +1 @@
|
||||
export * from './Cell';
|
@ -0,0 +1,13 @@
|
||||
import { Sort } from '../../application';
|
||||
import { useDatabase } from '../../Database.hooks';
|
||||
import { Sorts } from '../sort';
|
||||
|
||||
export const DatabaseSettings = () => {
|
||||
const { sorts } = useDatabase();
|
||||
|
||||
return (
|
||||
<div className="flex items-center border-t">
|
||||
<Sorts sorts={sorts as Sort[]} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from './DatabaseSettings';
|
@ -0,0 +1,20 @@
|
||||
import { FC } from 'react';
|
||||
import { Field as FieldType } from '../../application';
|
||||
import { FieldTypeSvg } from './FieldTypeSvg';
|
||||
|
||||
export interface FieldProps {
|
||||
field: FieldType;
|
||||
}
|
||||
|
||||
export const Field: FC<FieldProps> = ({
|
||||
field,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center px-2 w-full">
|
||||
<FieldTypeSvg className="text-base mr-1" type={field.type} />
|
||||
<span className="flex-1 text-left text-xs truncate">
|
||||
{field.name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
import { MenuItem, Select, SelectChangeEvent, SelectProps } from '@mui/material';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { Field as FieldType } from '../../application';
|
||||
import { useDatabase } from '../../Database.hooks';
|
||||
import { Field } from './Field';
|
||||
|
||||
export interface FieldSelectProps extends Omit<SelectProps, 'onChange'> {
|
||||
onChange?: (event: SelectChangeEvent<unknown>, field: FieldType | undefined) => void;
|
||||
}
|
||||
|
||||
export const FieldSelect: FC<FieldSelectProps> = ({
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const { fields } = useDatabase();
|
||||
|
||||
const handleChange = useCallback((event: SelectChangeEvent<unknown>) => {
|
||||
const selectedId = event.target.value;
|
||||
|
||||
onChange?.(event, fields.find(field => field.id === selectedId));
|
||||
}, [onChange, fields]);
|
||||
|
||||
return (
|
||||
<Select onChange={handleChange} {...props}>
|
||||
{fields.map(field => (
|
||||
<MenuItem key={field.id} value={field.id}>
|
||||
<Field field={field} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
import { FC, memo } from 'react';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { ReactComponent as TextSvg } from '$app/assets/database/field-type-text.svg';
|
||||
import { ReactComponent as NumberSvg } from '$app/assets/database/field-type-number.svg';
|
||||
import { ReactComponent as DateSvg } from '$app/assets/database/field-type-date.svg';
|
||||
import { ReactComponent as SingleSelectSvg } from '$app/assets/database/field-type-single-select.svg';
|
||||
import { ReactComponent as MultiSelectSvg } from '$app/assets/database/field-type-multi-select.svg';
|
||||
import { ReactComponent as ChecklistSvg } from '$app/assets/database/field-type-checklist.svg';
|
||||
import { ReactComponent as CheckboxSvg } from '$app/assets/database/field-type-checkbox.svg';
|
||||
import { ReactComponent as URLSvg } from '$app/assets/database/field-type-url.svg';
|
||||
import { ReactComponent as LastEditedTimeSvg } from '$app/assets/database/field-type-last-edited-time.svg';
|
||||
|
||||
export const FieldTypeSvgMap: Record<FieldType, FC<React.SVGProps<SVGSVGElement>>> = {
|
||||
[FieldType.RichText]: TextSvg,
|
||||
[FieldType.Number]: NumberSvg,
|
||||
[FieldType.DateTime]: DateSvg,
|
||||
[FieldType.SingleSelect]: SingleSelectSvg,
|
||||
[FieldType.MultiSelect]: MultiSelectSvg,
|
||||
[FieldType.Checkbox]: CheckboxSvg,
|
||||
[FieldType.URL]: URLSvg,
|
||||
[FieldType.Checklist]: ChecklistSvg,
|
||||
[FieldType.LastEditedTime]: LastEditedTimeSvg,
|
||||
[FieldType.CreatedTime]: LastEditedTimeSvg,
|
||||
};
|
||||
|
||||
export const FieldTypeSvg: FC<{ type: FieldType, className?: string }> = memo(({ type, ...props }) => {
|
||||
const Svg = FieldTypeSvgMap[type];
|
||||
|
||||
return <Svg {...props} />;
|
||||
});
|
@ -0,0 +1,19 @@
|
||||
import { t } from 'i18next';
|
||||
import { FieldType } from '@/services/backend';
|
||||
|
||||
export const FieldTypeTextMap = {
|
||||
[FieldType.RichText]: 'textFieldName',
|
||||
[FieldType.Number]: 'numberFieldName',
|
||||
[FieldType.DateTime]: 'dateFieldName',
|
||||
[FieldType.SingleSelect]: 'singleSelectFieldName',
|
||||
[FieldType.MultiSelect]: 'multiSelectFieldName',
|
||||
[FieldType.Checkbox]: 'checkboxFieldName',
|
||||
[FieldType.URL]: 'urlFieldName',
|
||||
[FieldType.Checklist]: 'checklistFieldName',
|
||||
[FieldType.LastEditedTime]: 'updatedAtFieldName',
|
||||
[FieldType.CreatedTime]: 'createdAtFieldName',
|
||||
} as const;
|
||||
|
||||
export const FieldTypeText = (type: FieldType) => {
|
||||
return t(`grid.field.${FieldTypeTextMap[type]}`);
|
||||
};
|
@ -0,0 +1,30 @@
|
||||
import { Menu, MenuItem, MenuProps } from '@mui/material';
|
||||
import { FC, MouseEvent } from 'react';
|
||||
import { Field as FieldType } from '../../application';
|
||||
import { useDatabase } from '../../Database.hooks';
|
||||
import { Field } from './Field';
|
||||
|
||||
export interface FieldsMenuProps extends MenuProps {
|
||||
onMenuItemClick?: (event: MouseEvent<HTMLLIElement>, field: FieldType) => void;
|
||||
}
|
||||
|
||||
export const FieldsMenu: FC<FieldsMenuProps> = ({ onMenuItemClick, ...props }) => {
|
||||
const { fields } = useDatabase();
|
||||
|
||||
return (
|
||||
<Menu {...props}>
|
||||
{fields.map((field) => (
|
||||
<MenuItem
|
||||
key={field.id}
|
||||
value={field.id}
|
||||
onClick={(event) => {
|
||||
onMenuItemClick?.(event, field);
|
||||
props.onClose?.({}, 'backdropClick');
|
||||
}}
|
||||
>
|
||||
<Field field={field} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export * from './Field';
|
||||
export * from './FieldSelect';
|
||||
export * from './FieldsMenu';
|
@ -0,0 +1,2 @@
|
||||
export * from './tab_bar';
|
||||
export * from './cell';
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user