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:
Kilu.He 2023-11-03 15:13:49 +08:00 committed by GitHub
parent 73ea1a0685
commit 5f49c1748f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
274 changed files with 2969 additions and 2051 deletions

View File

@ -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: "*" },

View File

@ -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",

View File

@ -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

View File

@ -66,7 +66,7 @@ function BlockDragDropContext({ children }: { children: React.ReactNode }) {
};
const onDragEnd = () => {
dispatch(onDragEndThunk());
void dispatch(onDragEndThunk());
unlisten();
};

View File

@ -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';

View File

@ -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;

View File

@ -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

View File

@ -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) => {

View File

@ -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';

View File

@ -19,6 +19,7 @@ export const NewCheckListOption = ({
const updateNewOption = (value: string) => {
const newOptionsCopy = [...newOptions];
newOptionsCopy[index] = value;
setNewOptions(newOptionsCopy);
};

View File

@ -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) => {

View File

@ -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);
}
};

View File

@ -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;
});
};

View File

@ -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={

View File

@ -13,6 +13,7 @@ export const EditCellDate = ({
const onClick: MouseEventHandler = () => {
if (!ref.current) return;
const { left, top } = ref.current.getBoundingClientRect();
onEditClick(left, top);
};

View File

@ -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);
};

View File

@ -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) => {

View File

@ -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);

View File

@ -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));

View File

@ -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);

View File

@ -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')}

View File

@ -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 () => {

View File

@ -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);

View File

@ -37,6 +37,7 @@ export const LanguageSelectPopup = ({ onClose }: { onClose: () => void }) => {
title: item.title,
icon: <></>,
}));
return (
<PopupSelect
items={items}

View File

@ -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,

View File

@ -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 {

View File

@ -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) => {

View File

@ -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);

View File

@ -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 () => {

View File

@ -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,

View File

@ -21,6 +21,7 @@ export const ProtectedRoutes = () => {
throw new Error(result.val.msg);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (isLoading) {

View File

@ -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,

View File

@ -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>}

View File

@ -14,5 +14,6 @@ export const BoardDateCell = ({
fieldController: FieldController;
}) => {
const { data } = useCell(cellIdentifier, cellCache, fieldController);
return <div>{(data as DateCellDataPB | undefined)?.date ?? ''}&nbsp;</div>;
};

View File

@ -37,6 +37,7 @@ export const BoardSettingsPopup = ({
onClick: onGroupClick,
},
]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [t]);
return (

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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>
);
};

View File

@ -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('');

View File

@ -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 />;
};

View File

@ -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,

View File

@ -162,7 +162,6 @@ export const autoScrollOnEdge = ({
};
const cleanup = () => {
console.log('document drag end');
keepScroll.cancel();
element.removeEventListener('dragover', onDragOver);

View File

@ -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();
}

View File

@ -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),
};
};

View File

@ -0,0 +1,2 @@
export * from './cell_types';
export * as cellService from './cell_service';

View File

@ -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,
};
}

View File

@ -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[];
}

View File

@ -0,0 +1,2 @@
export * from './database_types';
export * as databaseService from './database_service';

View File

@ -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();
}

View File

@ -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,
};
}

View File

@ -0,0 +1,2 @@
export * from './database_view_types';
export * as databaseViewService from './database_view_service';

View File

@ -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();
}

View File

@ -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,
});

View File

@ -0,0 +1,4 @@
export * from './select_option';
export * from './type_option';
export * from './field_types';
export * as fieldService from './field_service';

View File

@ -0,0 +1,2 @@
export * from './select_option_types';
export * as selectOptionService from './select_option_service';

View File

@ -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();
}

View File

@ -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,
};
}

View File

@ -0,0 +1,2 @@
export * from './type_option_types';
export * from './type_option_service';

View File

@ -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();
}

View File

@ -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));
}
}

View File

@ -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();
}

View File

@ -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),
};
}

View File

@ -0,0 +1,2 @@
export * from './filter_types';
export * as filterService from './filter_service';

View File

@ -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();
}

View File

@ -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,
};
}

View File

@ -0,0 +1,2 @@
export * from './group_types';
export * as groupService from './group_service';

View File

@ -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';

View File

@ -0,0 +1,3 @@
export * from './row_types';
export * as rowService from './row_service';
export * as rowListeners from './row_listeners';

View File

@ -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]);
}
};

View File

@ -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();
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export * from './sort_types';
export * as sortService from './sort_service';
export * as sortListeners from './sort_listeners';

View File

@ -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);
};

View File

@ -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();
}

View File

@ -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,
};
}

View File

@ -0,0 +1,5 @@
import { FC } from 'react';
export const Board: FC = () => {
return null;
};

View File

@ -0,0 +1 @@
export * from './Board';

View File

@ -0,0 +1,5 @@
import { FC } from 'react';
export const Calendar: FC = () => {
return null;
};

View File

@ -0,0 +1 @@
export * from './Calendar';

View File

@ -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;
};

View File

@ -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} />;
};

View File

@ -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>
);
};

View File

@ -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 = (

View File

@ -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> = ({
)}
</>
);
};
};

View File

@ -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>
)
}
);
};

View File

@ -1,4 +1,4 @@
import { SelectOptionColorPB } from "@/services/backend";
import { SelectOptionColorPB } from '@/services/backend';
export const SelectOptionColorMap = {
[SelectOptionColorPB.Purple]: 'bg-tint-purple',

View File

@ -0,0 +1 @@
export * from './SelectCell';

View File

@ -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

View File

@ -0,0 +1 @@
export * from './Cell';

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export * from './DatabaseSettings';

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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} />;
});

View File

@ -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]}`);
};

View File

@ -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>
);
};

View File

@ -0,0 +1,3 @@
export * from './Field';
export * from './FieldSelect';
export * from './FieldsMenu';

View File

@ -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