feat: move kanban blocks (#2022)

* chore: add edit / create field test

* chore: add delete field test

* chore: change log class arguments

* chore: delete/create row

* chore: set tracing log to debug level

* fix: filter notification with id

* chore: add get single select type option data

* fix: high cpu usage

* chore: format code

* chore: update tokio version

* chore: config tokio runtime subscriber

* chore: add profiling feature

* chore: setup auto login

* chore: fix tauri build

* chore: (unstable) using controllers

* fix: initially authenticated and serializable fix

* fix: ci warning

* ci: compile error

* fix: new folder trash overflow

* fix: min width for nav panel

* fix: nav panel and main panel animation on hide menu

* fix: highlight active page

* fix: post merge fixes

* fix: post merge fix

* fix: remove warnings

* fix: change IDatabaseField fix eslint errors

* chore: create cell component for each field type

* chore: move cell hook into custom cell component

* chore: refactor row hook

* chore: add tauri clean

* chore: add tauri clean

* chore: save offset top of nav items

* chore: move constants

* fix: nav item popup overflow

* fix: page rename position

* chore: remove offset top

* chore: remove floating menu functions

* chore: scroll down to new page

* chore: smooth scroll and scroll to new folder

* fix: breadcrumbs

* chore: back and forward buttons nav scroll fix

* chore: get board groups and rows

* chore: set log level & remove empty line

* fix: create kanban board row

* fix: appflowy session name

* chore: import beautiful dnd

* bug: kanban new row

* chore: update refs

* fix: dispose group controller

* fix: dispose cell controller

* chore: move rows in group

* chore: move row into other block

* fix: groups observer dispose

* chore: dnd reordering

* chore: fix import references

* chore: initial edit board modal

* fix: kanban board rendering

* chore: add column and edit text cell

* chore: column rename

* chore: edit row components reorganize

* chore: don't show group by field

* wip: edit cell type

* chore: fade in, out

* chore: change field type

* chore: update editing cell

* chore: fade in change

* chore: cell options layout

* fix: padding fixes for cell wrapper

* fix: cell options positions

* chore: cell options write to backend

* fix: select options for new row

* chore: edit url cell

* chore: language button

* fix: close popup on lang select

* fix: save url cell

* chore: date picker

* chore: small code cleanups

* chore: options in board

* chore: move fields dnd

---------

Co-authored-by: nathan <nathan@appflowy.io>
Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
Co-authored-by: appflowy <annie@appflowy.io>
This commit is contained in:
Askarbek Zadauly 2023-04-02 18:54:39 +06:00 committed by GitHub
parent 9fff00f731
commit fe524dbc78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 1660 additions and 196 deletions

View File

@ -23,6 +23,7 @@
"@slate-yjs/core": "^0.3.1",
"@tanstack/react-virtual": "3.0.0-beta.54",
"@tauri-apps/api": "^1.2.0",
"dayjs": "^1.11.7",
"events": "^3.3.0",
"google-protobuf": "^3.21.2",
"i18next": "^22.4.10",
@ -32,6 +33,8 @@
"nanoid": "^4.0.0",
"protoc-gen-ts": "^0.8.5",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-calendar": "^4.1.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
"react-i18next": "^12.2.0",
@ -44,8 +47,8 @@
"slate-react": "^0.91.9",
"ts-results": "^3.3.0",
"utf8": "^3.0.0",
"yjs": "^13.5.51",
"y-indexeddb": "^9.0.9"
"y-indexeddb": "^9.0.9",
"yjs": "^13.5.51"
},
"devDependencies": {
"@tauri-apps/cli": "^1.2.2",
@ -53,6 +56,7 @@
"@types/is-hotkey": "^0.1.7",
"@types/node": "^18.7.10",
"@types/react": "^18.0.15",
"@types/react-beautiful-dnd": "^13.1.3",
"@types/react-dom": "^18.0.6",
"@types/utf8": "^3.0.1",
"@types/uuid": "^9.0.1",

View File

@ -0,0 +1,34 @@
import { SelectOptionCellDataPB } from '@/services/backend';
import { getBgColor } from '$app/components/_shared/getColor';
import { useRef } from 'react';
export const CellOptions = ({
data,
onEditClick,
}: {
data: SelectOptionCellDataPB | undefined;
onEditClick: (left: number, top: number) => void;
}) => {
const ref = useRef<HTMLDivElement>(null);
const onClick = () => {
if (!ref.current) return;
const { left, top } = ref.current.getBoundingClientRect();
onEditClick(left, top);
};
return (
<div
ref={ref}
onClick={() => onClick()}
className={'flex flex-wrap items-center gap-2 px-4 py-2 text-xs text-black'}
>
{data?.select_options?.map((option, index) => (
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
{option?.name || ''}
</div>
)) || ''}
&nbsp;
</div>
);
};

View File

@ -0,0 +1,152 @@
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { SelectOptionCellDataPB, SelectOptionColorPB, SelectOptionPB } from '@/services/backend';
import { getBgColor } from '$app/components/_shared/getColor';
import { useTranslation } from 'react-i18next';
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
import useOutsideClick from '$app/components/_shared/useOutsideClick';
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
import { useAppSelector } from '$app/stores/store';
import { ISelectOptionType } from '$app/stores/reducers/database/slice';
export const CellOptionsPopup = ({
top,
left,
cellIdentifier,
cellCache,
fieldController,
onOutsideClick,
}: {
top: number;
left: number;
cellIdentifier: CellIdentifier;
cellCache: CellCache;
fieldController: FieldController;
onOutsideClick: () => void;
}) => {
const ref = useRef<HTMLDivElement>(null);
const { t } = useTranslation('');
const [adjustedTop, setAdjustedTop] = useState(-100);
const [value, setValue] = useState('');
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
const databaseStore = useAppSelector((state) => state.database);
useEffect(() => {
if (!ref.current) return;
const { height } = ref.current.getBoundingClientRect();
if (top + height + 40 > window.innerHeight) {
setAdjustedTop(window.innerHeight - height - 40);
} else {
setAdjustedTop(top);
}
}, [ref, window, top, left]);
useOutsideClick(ref, async () => {
onOutsideClick();
});
const onKeyDown: KeyboardEventHandler = async (e) => {
if (e.key === 'Enter' && value.length > 0) {
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value });
setValue('');
}
};
const onUnselectOptionClick = async (option: SelectOptionPB) => {
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
setValue('');
};
const onToggleOptionClick = async (option: SelectOptionPB) => {
if (
(data as SelectOptionCellDataPB | undefined)?.select_options?.find(
(selectedOption) => selectedOption.id === option.id
)
) {
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
} else {
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]);
}
setValue('');
};
useEffect(() => {
console.log('loaded data: ', data);
console.log('have stored ', databaseStore.fields[cellIdentifier.fieldId]);
}, [data]);
return (
<div
ref={ref}
className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
}`}
style={{ top: `${adjustedTop + 40}px`, left: `${left}px` }}
>
<div className={'flex flex-col gap-2 p-2'}>
<div className={'border-shades-3 flex flex-1 items-center gap-2 rounded border bg-main-selector px-2 '}>
<div className={'flex flex-wrap items-center gap-2 text-black'}>
{(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => (
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5`} key={index}>
<span>{option?.name || ''}</span>
<button onClick={() => onUnselectOptionClick(option)} className={'h-5 w-5 cursor-pointer'}>
<CloseSvg></CloseSvg>{' '}
</button>
</div>
)) || ''}
</div>
<input
className={'py-2'}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={t('grid.selectOption.searchOption') || ''}
onKeyDown={onKeyDown}
/>
<div className={'font-mono text-shade-3'}>{value.length}/30</div>
</div>
<div className={'-mx-4 h-[1px] bg-shade-6'}></div>
<div className={'font-semibold text-shade-3'}>{t('grid.selectOption.panelTitle') || ''}</div>
<div className={'flex flex-col gap-1'}>
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
(option, index) => (
<div
key={index}
onClick={() =>
onToggleOptionClick(
new SelectOptionPB({
id: option.selectOptionId,
name: option.title,
color: option.color || SelectOptionColorPB.Purple,
})
)
}
className={
'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-main-secondary'
}
>
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`}>{option.title}</div>
<div className={'flex items-center'}>
{(data as SelectOptionCellDataPB | undefined)?.select_options?.find(
(selectedOption) => selectedOption.id === option.selectOptionId
) && (
<button className={'h-5 w-5 p-1'}>
<CheckmarkSvg></CheckmarkSvg>
</button>
)}
<button className={'h-6 w-6 p-1'}>
<Details2Svg></Details2Svg>
</button>
</div>
</div>
)
)}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,71 @@
import { FieldType } from '@/services/backend';
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
import { useEffect, useMemo, useRef, useState } from 'react';
import useOutsideClick from '$app/components/_shared/useOutsideClick';
const typesOrder: FieldType[] = [
FieldType.RichText,
FieldType.Number,
FieldType.DateTime,
FieldType.SingleSelect,
FieldType.MultiSelect,
FieldType.Checkbox,
FieldType.URL,
FieldType.Checklist,
];
export const ChangeFieldTypePopup = ({
top,
right,
onClick,
onOutsideClick,
}: {
top: number;
right: number;
onClick: (newType: FieldType) => void;
onOutsideClick: () => void;
}) => {
const ref = useRef<HTMLDivElement>(null);
const [adjustedTop, setAdjustedTop] = useState(-100);
useOutsideClick(ref, async () => {
onOutsideClick();
});
useEffect(() => {
if (!ref.current) return;
const { height } = ref.current.getBoundingClientRect();
if (top + height > window.innerHeight) {
setAdjustedTop(window.innerHeight - height);
} else {
setAdjustedTop(top);
}
}, [ref, window, top, right]);
return (
<div
ref={ref}
className={`fixed z-10 rounded-lg bg-white p-2 text-xs shadow-md transition-opacity duration-300 ${
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
}`}
style={{ top: `${adjustedTop}px`, left: `${right + 30}px` }}
>
<div className={'flex flex-col'}>
{typesOrder.map((t, i) => (
<button
onClick={() => onClick(t)}
key={i}
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-main-secondary'}
>
<i className={'h-5 w-5'}>
<FieldTypeIcon fieldType={t}></FieldTypeIcon>
</i>
<span>
<FieldTypeName fieldType={t}></FieldTypeName>
</span>
</button>
))}
</div>
</div>
);
};

View File

@ -0,0 +1,97 @@
import { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import useOutsideClick from '$app/components/_shared/useOutsideClick';
import Calendar from 'react-calendar';
import dayjs from 'dayjs';
import { ClockSvg } from '$app/components/_shared/svg/ClockSvg';
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
import { useCell } from '$app/components/_shared/database-hooks/useCell';
export const DatePickerPopup = ({
left,
top,
cellIdentifier,
cellCache,
fieldController,
onOutsideClick,
}: {
left: number;
top: number;
cellIdentifier: CellIdentifier;
cellCache: CellCache;
fieldController: FieldController;
onOutsideClick: () => void;
}) => {
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
const ref = useRef<HTMLDivElement>(null);
const [adjustedTop, setAdjustedTop] = useState(-100);
// const [value, setValue] = useState();
const { t } = useTranslation('');
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
useEffect(() => {
if (!ref.current) return;
const { height } = ref.current.getBoundingClientRect();
if (top + height + 40 > window.innerHeight) {
setAdjustedTop(top - height - 40);
} else {
setAdjustedTop(top);
}
}, [ref, window, top, left]);
useOutsideClick(ref, async () => {
onOutsideClick();
});
useEffect(() => {
// console.log((data as DateCellDataPB).date);
// setSelectedDate(new Date((data as DateCellDataPB).date));
}, [data]);
const onChange = (v: Date | null | (Date | null)[]) => {
if (v instanceof Date) {
console.log(dayjs(v).format('YYYY-MM-DD'));
setSelectedDate(v);
// void cellController?.saveCellData(new DateCellDataPB({ date: dayjs(v).format('YYYY-MM-DD') }));
}
};
return (
<div
ref={ref}
className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
}`}
style={{ top: `${adjustedTop + 40}px`, left: `${left}px` }}
>
<div className={'px-2'}>
<Calendar onChange={(d) => onChange(d)} value={selectedDate} />
</div>
<hr className={'-mx-2 my-4 border-shade-6'} />
<div className={'flex items-center justify-between px-4'}>
<div className={'flex items-center gap-2'}>
<i className={'h-4 w-4'}>
<ClockSvg></ClockSvg>
</i>
<span>{t('grid.field.includeTime')}</span>
</div>
<i className={'h-5 w-5'}>
<EditorUncheckSvg></EditorUncheckSvg>
</i>
</div>
<hr className={'-mx-2 my-4 border-shade-6'} />
<div className={'flex items-center justify-between px-4 pb-2'}>
<span>
{t('grid.field.dateFormat')} & {t('grid.field.timeFormat')}
</span>
<i className={'h-5 w-5'}>
<MoreSvg></MoreSvg>
</i>
</div>
</div>
);
};

View File

@ -0,0 +1,24 @@
import { useRef } from 'react';
import { DateCellDataPB } from '@/services/backend';
export const EditCellDate = ({
data,
onEditClick,
}: {
data?: DateCellDataPB;
onEditClick: (left: number, top: number) => void;
}) => {
const ref = useRef<HTMLDivElement>(null);
const onClick = () => {
if (!ref.current) return;
const { left, top } = ref.current.getBoundingClientRect();
onEditClick(left, top);
};
return (
<div ref={ref} onClick={() => onClick()} className={'px-4 py-2'}>
{data?.date || <>&nbsp;</>}
</div>
);
};

View File

@ -0,0 +1,29 @@
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
import { useEffect, useState } from 'react';
export const EditCellNumber = ({
data,
cellController,
}: {
data: string | undefined;
cellController: CellController<any, any>;
}) => {
const [value, setValue] = useState('');
useEffect(() => {
setValue(data || '');
}, [data]);
const save = async () => {
await cellController?.saveCellData(value);
};
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={() => save()}
className={'w-full px-4 py-2'}
></input>
);
};

View File

@ -0,0 +1,41 @@
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
import { useEffect, useState, KeyboardEvent, useMemo } from 'react';
export const EditCellText = ({
data,
cellController,
}: {
data: string | undefined;
cellController: CellController<any, any>;
}) => {
const [value, setValue] = useState('');
const [contentRows, setContentRows] = useState(1);
useEffect(() => {
setValue(data || '');
}, [data]);
useEffect(() => {
setContentRows(Math.max(1, (value || '').split('\n').length));
}, [value]);
const onTextFieldChange = async (v: string) => {
setValue(v);
};
const save = async () => {
await cellController?.saveCellData(value);
};
return (
<div className={''}>
<textarea
className={'mt-0.5 h-full w-full resize-none px-4 py-2'}
rows={contentRows}
value={value}
onChange={(e) => onTextFieldChange(e.target.value)}
onBlur={() => save()}
/>
</div>
);
};

View File

@ -0,0 +1,31 @@
import { URLCellDataPB } from '@/services/backend';
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
import { useEffect, useState } from 'react';
import { URLCellController } from '$app/stores/effects/database/cell/controller_builder';
export const EditCellUrl = ({
data,
cellController,
}: {
data: URLCellDataPB | undefined;
cellController: CellController<any, any>;
}) => {
const [value, setValue] = useState('');
useEffect(() => {
setValue((data as URLCellDataPB)?.url || '');
}, [data]);
const save = async () => {
await (cellController as URLCellController)?.saveCellData(value);
};
return (
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onBlur={() => save()}
className={'w-full px-4 py-2'}
></input>
);
};

View File

@ -0,0 +1,102 @@
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { DateCellDataPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend';
import { useAppSelector } from '$app/stores/store';
import { EditCellText } from '$app/components/_shared/EditRow/EditCellText';
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
import { EditCellDate } from '$app/components/_shared/EditRow/EditCellDate';
import { useRef } from 'react';
import { CellOptions } from '$app/components/_shared/EditRow/CellOptions';
import { EditCellNumber } from '$app/components/_shared/EditRow/EditCellNumber';
import { EditCheckboxCell } from '$app/components/_shared/EditRow/EditCheckboxCell';
import { EditCellUrl } from '$app/components/_shared/EditRow/EditCellUrl';
import { Draggable } from 'react-beautiful-dnd';
export const EditCellWrapper = ({
index,
cellIdentifier,
cellCache,
fieldController,
onEditFieldClick,
onEditOptionsClick,
onEditDateClick,
}: {
index: number;
cellIdentifier: CellIdentifier;
cellCache: CellCache;
fieldController: FieldController;
onEditFieldClick: (top: number, right: number) => void;
onEditOptionsClick: (left: number, top: number) => void;
onEditDateClick: (left: number, top: number) => void;
}) => {
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
const databaseStore = useAppSelector((state) => state.database);
const el = useRef<HTMLDivElement>(null);
const onClick = () => {
if (!el.current) return;
const { top, right } = el.current.getBoundingClientRect();
onEditFieldClick(top, right);
};
return (
<Draggable draggableId={cellIdentifier.fieldId} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className={'flex w-full items-center text-xs'}
>
<div
ref={el}
onClick={() => onClick()}
className={
'relative flex w-[180px] cursor-pointer items-center gap-2 rounded-lg px-3 py-1.5 hover:bg-shade-6'
}
>
<div className={'flex h-5 w-5 flex-shrink-0 items-center justify-center'}>
<FieldTypeIcon fieldType={cellIdentifier.fieldType}></FieldTypeIcon>
</div>
<span className={'overflow-hidden text-ellipsis whitespace-nowrap'}>
{databaseStore.fields[cellIdentifier.fieldId].title}
</span>
</div>
<div className={'flex-1 cursor-pointer rounded-lg hover:bg-shade-6'}>
{(cellIdentifier.fieldType === FieldType.SingleSelect ||
cellIdentifier.fieldType === FieldType.MultiSelect ||
cellIdentifier.fieldType === FieldType.Checklist) &&
cellController && (
<CellOptions
data={data as SelectOptionCellDataPB | undefined}
onEditClick={onEditOptionsClick}
></CellOptions>
)}
{cellIdentifier.fieldType === FieldType.Checkbox && cellController && (
<EditCheckboxCell data={data as boolean | undefined} cellController={cellController}></EditCheckboxCell>
)}
{cellIdentifier.fieldType === FieldType.DateTime && (
<EditCellDate data={data as DateCellDataPB | undefined} onEditClick={onEditDateClick}></EditCellDate>
)}
{cellIdentifier.fieldType === FieldType.Number && cellController && (
<EditCellNumber data={data as string | undefined} cellController={cellController}></EditCellNumber>
)}
{cellIdentifier.fieldType === FieldType.URL && cellController && (
<EditCellUrl data={data as URLCellDataPB | undefined} cellController={cellController}></EditCellUrl>
)}
{cellIdentifier.fieldType === FieldType.RichText && cellController && (
<EditCellText data={data as string | undefined} cellController={cellController}></EditCellText>
)}
</div>
</div>
)}
</Draggable>
);
};

View File

@ -0,0 +1,23 @@
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
export const EditCheckboxCell = ({
data,
cellController,
}: {
data: boolean | undefined;
cellController: CellController<any, any>;
}) => {
const toggleValue = async () => {
await cellController?.saveCellData(!data);
};
return (
<div onClick={() => toggleValue()} className={'block px-4 py-2'}>
<button className={'h-5 w-5'}>
{data ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
</button>
</div>
);
};

View File

@ -0,0 +1,130 @@
import { useEffect, useRef, useState } from 'react';
import useOutsideClick from '$app/components/_shared/useOutsideClick';
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
import { useTranslation } from 'react-i18next';
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
import { Some } from 'ts-results';
import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
import { useAppSelector } from '$app/stores/store';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
export const EditFieldPopup = ({
top,
right,
cellIdentifier,
viewId,
onOutsideClick,
fieldInfo,
changeFieldTypeClick,
}: {
top: number;
right: number;
cellIdentifier: CellIdentifier;
viewId: string;
onOutsideClick: () => void;
fieldInfo: FieldInfo | undefined;
changeFieldTypeClick: (buttonTop: number, buttonRight: number) => void;
}) => {
const databaseStore = useAppSelector((state) => state.database);
const { t } = useTranslation('');
const ref = useRef<HTMLDivElement>(null);
const changeTypeButtonRef = useRef<HTMLDivElement>(null);
const [name, setName] = useState('');
const [adjustedTop, setAdjustedTop] = useState(-100);
useOutsideClick(ref, async () => {
await save();
onOutsideClick();
});
useEffect(() => {
setName(databaseStore.fields[cellIdentifier.fieldId].title);
}, [databaseStore, cellIdentifier]);
useEffect(() => {
if (!ref.current) return;
const { height } = ref.current.getBoundingClientRect();
if (top + height > window.innerHeight) {
setAdjustedTop(window.innerHeight - height);
} else {
setAdjustedTop(top);
}
}, [ref, window, top, right]);
const save = async () => {
if (!fieldInfo) return;
const controller = new TypeOptionController(viewId, Some(fieldInfo));
await controller.initialize();
await controller.setFieldName(name);
};
const onChangeFieldTypeClick = () => {
if (!changeTypeButtonRef.current) return;
const { top: buttonTop, right: buttonRight } = changeTypeButtonRef.current.getBoundingClientRect();
changeFieldTypeClick(buttonTop, buttonRight);
};
// this is causing an error right now
const onDeleteFieldClick = async () => {
if (!fieldInfo) return;
const controller = new TypeOptionController(viewId, Some(fieldInfo));
await controller.initialize();
await controller.deleteField();
onOutsideClick();
};
return (
<div
ref={ref}
className={`fixed z-10 rounded-lg bg-white px-2 py-2 text-xs shadow-md transition-opacity duration-300 ${
adjustedTop === -100 ? 'opacity-0' : 'opacity-100'
}`}
style={{ top: `${adjustedTop}px`, left: `${right + 10}px` }}
>
<div className={'flex flex-col gap-2 p-2'}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
onBlur={() => save()}
className={'border-shades-3 flex-1 rounded border bg-main-selector px-2 py-2'}
/>
<button
onClick={() => onDeleteFieldClick()}
className={
'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-main-alert hover:bg-main-secondary'
}
>
<i className={'h-5 w-5'}>
<TrashSvg></TrashSvg>
</i>
<span>{t('grid.field.delete')}</span>
</button>
<div
ref={changeTypeButtonRef}
onClick={() => onChangeFieldTypeClick()}
className={
'relative flex cursor-pointer items-center justify-between rounded-lg text-black hover:bg-main-secondary'
}
>
<button className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2'}>
<i className={'h-5 w-5'}>
<FieldTypeIcon fieldType={cellIdentifier.fieldType}></FieldTypeIcon>
</i>
<span>
<FieldTypeName fieldType={cellIdentifier.fieldType}></FieldTypeName>
</span>
</button>
<i className={'h-5 w-5'}>
<MoreSvg></MoreSvg>
</i>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,210 @@
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
import { useRow } from '$app/components/_shared/database-hooks/useRow';
import { DatabaseController } from '$app/stores/effects/database/database_controller';
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { EditCellWrapper } from '$app/components/_shared/EditRow/EditCellWrapper';
import AddSvg from '$app/components/_shared/svg/AddSvg';
import { useTranslation } from 'react-i18next';
import { EditFieldPopup } from '$app/components/_shared/EditRow/EditFieldPopup';
import { useEffect, useState } from 'react';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { ChangeFieldTypePopup } from '$app/components/_shared/EditRow/ChangeFieldTypePopup';
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
import { Some } from 'ts-results';
import { FieldType } from '@/services/backend';
import { CellOptionsPopup } from '$app/components/_shared/EditRow/CellOptionsPopup';
import { DatePickerPopup } from '$app/components/_shared/EditRow/DatePickerPopup';
import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd';
export const EditRow = ({
onClose,
viewId,
controller,
rowInfo,
}: {
onClose: () => void;
viewId: string;
controller: DatabaseController;
rowInfo: RowInfo;
}) => {
const { cells, onNewColumnClick } = useRow(viewId, controller, rowInfo);
const { t } = useTranslation('');
const [unveil, setUnveil] = useState(false);
const [editingCell, setEditingCell] = useState<CellIdentifier | null>(null);
const [showFieldEditor, setShowFieldEditor] = useState(false);
const [editFieldTop, setEditFieldTop] = useState(0);
const [editFieldRight, setEditFieldRight] = useState(0);
const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0);
const [changeFieldTypeRight, setChangeFieldTypeRight] = useState(0);
const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false);
const [changeOptionsTop, setChangeOptionsTop] = useState(0);
const [changeOptionsLeft, setChangeOptionsLeft] = useState(0);
const [showDatePicker, setShowDatePicker] = useState(false);
const [datePickerTop, setDatePickerTop] = useState(0);
const [datePickerLeft, setDatePickerLeft] = useState(0);
useEffect(() => {
setUnveil(true);
}, []);
const onCloseClick = () => {
setUnveil(false);
setTimeout(() => {
onClose();
}, 300);
};
const onEditFieldClick = (cellIdentifier: CellIdentifier, top: number, right: number) => {
setEditingCell(cellIdentifier);
setEditFieldTop(top);
setEditFieldRight(right);
setShowFieldEditor(true);
};
const onOutsideEditFieldClick = () => {
if (!showChangeFieldTypePopup) {
setShowFieldEditor(false);
}
};
const onChangeFieldTypeClick = (buttonTop: number, buttonRight: number) => {
setChangeFieldTypeTop(buttonTop);
setChangeFieldTypeRight(buttonRight);
setShowChangeFieldTypePopup(true);
};
const changeFieldType = async (newType: FieldType) => {
if (!editingCell) return;
const currentField = controller.fieldController.getField(editingCell.fieldId);
if (!currentField) return;
const typeOptionController = new TypeOptionController(viewId, Some(currentField));
await typeOptionController.switchToField(newType);
setEditingCell(new CellIdentifier(viewId, rowInfo.row.id, editingCell.fieldId, newType));
setShowChangeFieldTypePopup(false);
};
const onEditOptionsClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
setEditingCell(cellIdentifier);
setChangeOptionsLeft(left);
setChangeOptionsTop(top);
setShowChangeOptionsPopup(true);
};
const onEditDateClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
setEditingCell(cellIdentifier);
setDatePickerLeft(left);
setDatePickerTop(top);
setShowDatePicker(true);
};
const onDragEnd: OnDragEndResponder = (result) => {
if (!result.destination?.index) return;
void controller.moveField(result.source.droppableId, result.source.index, result.destination.index);
};
return (
<div
className={`fixed inset-0 z-10 flex items-center justify-center bg-black/30 backdrop-blur-sm transition-opacity duration-300 ${
unveil ? 'opacity-100' : 'opacity-0'
}`}
>
<div className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-white px-8 pb-4 pt-12`}>
<div onClick={() => onCloseClick()} className={'absolute top-4 right-4'}>
<button className={'block h-8 w-8 rounded-lg text-shade-2 hover:bg-main-secondary'}>
<CloseSvg></CloseSvg>
</button>
</div>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId={'field-list'}>
{(provided) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
className={`flex flex-1 flex-col gap-2 ${
showFieldEditor || showChangeOptionsPopup || showDatePicker ? 'overflow-hidden' : 'overflow-auto'
}`}
>
{cells.map((cell, cellIndex) => (
<EditCellWrapper
index={cellIndex}
key={cellIndex}
cellIdentifier={cell.cellIdentifier}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onEditFieldClick={(top: number, right: number) => onEditFieldClick(cell.cellIdentifier, top, right)}
onEditOptionsClick={(left: number, top: number) =>
onEditOptionsClick(cell.cellIdentifier, left, top)
}
onEditDateClick={(left: number, top: number) => onEditDateClick(cell.cellIdentifier, left, top)}
></EditCellWrapper>
))}
</div>
)}
</Droppable>
</DragDropContext>
<div className={'border-t border-shade-6 pt-2'}>
<button
onClick={() => onNewColumnClick()}
className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-shade-6'}
>
<i className={'h-5 w-5'}>
<AddSvg></AddSvg>
</i>
<span>{t('grid.field.newColumn')}</span>
</button>
</div>
{showFieldEditor && editingCell && (
<EditFieldPopup
top={editFieldTop}
right={editFieldRight}
cellIdentifier={editingCell}
viewId={viewId}
onOutsideClick={onOutsideEditFieldClick}
fieldInfo={controller.fieldController.getField(editingCell.fieldId)}
changeFieldTypeClick={onChangeFieldTypeClick}
></EditFieldPopup>
)}
{showChangeFieldTypePopup && (
<ChangeFieldTypePopup
top={changeFieldTypeTop}
right={changeFieldTypeRight}
onClick={(newType) => changeFieldType(newType)}
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
></ChangeFieldTypePopup>
)}
{showChangeOptionsPopup && editingCell && (
<CellOptionsPopup
top={changeOptionsTop}
left={changeOptionsLeft}
cellIdentifier={editingCell}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onOutsideClick={() => setShowChangeOptionsPopup(false)}
></CellOptionsPopup>
)}
{showDatePicker && editingCell && (
<DatePickerPopup
top={datePickerTop}
left={datePickerLeft}
cellIdentifier={editingCell}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
onOutsideClick={() => setShowDatePicker(false)}
></DatePickerPopup>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,24 @@
import { FieldType } from '@/services/backend';
import { TextTypeSvg } from '$app/components/_shared/svg/TextTypeSvg';
import { NumberTypeSvg } from '$app/components/_shared/svg/NumberTypeSvg';
import { DateTypeSvg } from '$app/components/_shared/svg/DateTypeSvg';
import { SingleSelectTypeSvg } from '$app/components/_shared/svg/SingleSelectTypeSvg';
import { MultiSelectTypeSvg } from '$app/components/_shared/svg/MultiSelectTypeSvg';
import { ChecklistTypeSvg } from '$app/components/_shared/svg/ChecklistTypeSvg';
import { UrlTypeSvg } from '$app/components/_shared/svg/UrlTypeSvg';
import { CheckboxSvg } from '$app/components/_shared/svg/CheckboxSvg';
export const FieldTypeIcon = ({ fieldType }: { fieldType: FieldType }) => {
return (
<>
{fieldType === FieldType.RichText && <TextTypeSvg></TextTypeSvg>}
{fieldType === FieldType.Number && <NumberTypeSvg></NumberTypeSvg>}
{fieldType === FieldType.DateTime && <DateTypeSvg></DateTypeSvg>}
{fieldType === FieldType.SingleSelect && <SingleSelectTypeSvg></SingleSelectTypeSvg>}
{fieldType === FieldType.MultiSelect && <MultiSelectTypeSvg></MultiSelectTypeSvg>}
{fieldType === FieldType.Checklist && <ChecklistTypeSvg></ChecklistTypeSvg>}
{fieldType === FieldType.URL && <UrlTypeSvg></UrlTypeSvg>}
{fieldType === FieldType.Checkbox && <CheckboxSvg></CheckboxSvg>}
</>
);
};

View File

@ -0,0 +1,18 @@
import { FieldType } from '@/services/backend';
import { useTranslation } from 'react-i18next';
export const FieldTypeName = ({ fieldType }: { fieldType: FieldType }) => {
const { t } = useTranslation('');
return (
<>
{fieldType === FieldType.RichText && t('grid.field.textFieldName')}
{fieldType === FieldType.Number && t('grid.field.numberFieldName')}
{fieldType === FieldType.DateTime && t('grid.field.dateFieldName')}
{fieldType === FieldType.SingleSelect && t('grid.field.singleSelectFieldName')}
{fieldType === FieldType.MultiSelect && t('grid.field.multiSelectFieldName')}
{fieldType === FieldType.Checklist && t('grid.field.checklistFieldName')}
{fieldType === FieldType.URL && t('grid.field.urlFieldName')}
{fieldType === FieldType.Checkbox && t('grid.field.checkboxFieldName')}
</>
);
};

View File

@ -29,7 +29,10 @@ const supportedLanguages: { key: string; title: string }[] = [
export const LanguageSelectPopup = ({ onClose }: { onClose: () => void }) => {
const items: IPopupItem[] = supportedLanguages.map<IPopupItem>((item) => ({
onClick: () => void i18n.changeLanguage(item.key),
onClick: () => {
void i18n.changeLanguage(item.key);
onClose();
},
title: item.title,
icon: <></>,
}));

View File

@ -9,7 +9,7 @@ import {
NumberFormat,
SingleSelectTypeOptionPB,
TimeFormat,
} from '../../../../services/backend';
} from '@/services/backend';
import {
makeChecklistTypeOptionContext,
makeDateTypeOptionContext,

View File

@ -1,32 +1,43 @@
import { CellIdentifier } from '../../../stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '../../../stores/effects/database/cell/cell_cache';
import { FieldController } from '../../../stores/effects/database/field/field_controller';
import { CellControllerBuilder } from '../../../stores/effects/database/cell/controller_builder';
import { DateCellDataPB, SelectOptionCellDataPB, URLCellDataPB } from '../../../../services/backend';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { CellControllerBuilder } from '$app/stores/effects/database/cell/controller_builder';
import { DateCellDataPB, SelectOptionCellDataPB, URLCellDataPB } from '$app/../services/backend';
import { useEffect, useState } from 'react';
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fieldController: FieldController) => {
const [data, setData] = useState<DateCellDataPB | URLCellDataPB | SelectOptionCellDataPB | string | undefined>();
const [cellController, setCellController] = useState<CellController<any, any>>();
useEffect(() => {
if (!cellIdentifier || !cellCache || !fieldController) return;
const builder = new CellControllerBuilder(cellIdentifier, cellCache, fieldController);
const cellController = builder.build();
cellController.subscribeChanged({
onCellChanged: (value) => {
setData(value.unwrap());
const c = builder.build();
setCellController(c);
c.subscribeChanged({
onCellChanged: (cellData) => {
if (cellData.some) {
setData(cellData.val);
}
},
});
// ignore the return value, because we are using the subscription
void cellController.getCellData();
void (async () => {
const cellData = await c.getCellData();
if (cellData.some) {
setData(cellData.unwrap());
}
})();
return () => {
// dispose is causing an error
// void cellController.dispose();
void c.dispose();
};
}, []);
}, [cellIdentifier, cellCache, fieldController]);
return {
cellController,
data,
};
};

View File

@ -1,26 +1,27 @@
import { useEffect, useState } from 'react';
import { DatabaseController } from '../../../stores/effects/database/database_controller';
import { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '../../../stores/reducers/database/slice';
import { useAppDispatch } from '../../../stores/store';
import { DatabaseController } from '$app/stores/effects/database/database_controller';
import { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '$app/stores/reducers/database/slice';
import { useAppDispatch } from '$app/stores/store';
import loadField from './loadField';
import { FieldInfo } from '../../../stores/effects/database/field/field_controller';
import { RowInfo } from '../../../stores/effects/database/row/row_cache';
import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { ViewLayoutTypePB } from '@/services/backend';
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
import { OnDragEndResponder } from 'react-beautiful-dnd';
export const useDatabase = (viewId: string, type?: ViewLayoutTypePB) => {
const dispatch = useAppDispatch();
const [controller, setController] = useState<DatabaseController>();
const [rows, setRows] = useState<readonly RowInfo[]>([]);
const [groups, setGroups] = useState<readonly DatabaseGroupController[]>([]);
const [groupByFieldId, setGroupByFieldId] = useState('');
useEffect(() => {
if (!viewId.length) return;
const c = new DatabaseController(viewId);
setController(c);
// dispose is causing an error
// return () => void c.dispose();
return () => void c.dispose();
}, [viewId]);
const loadFields = async (fieldInfos: readonly FieldInfo[]) => {
@ -58,10 +59,37 @@ export const useDatabase = (viewId: string, type?: ViewLayoutTypePB) => {
await controller.open();
if (type === ViewLayoutTypePB.Board) {
const fieldId = await controller.getGroupByFieldId();
setGroupByFieldId(fieldId.unwrap());
setGroups(controller.groups.value);
}
})();
}, [controller]);
return { loadFields, controller, rows, groups };
const onNewRowClick = async (index: number) => {
if (!groups) return;
if (!controller?.groups) return;
const group = groups[index];
await group.createRow();
setGroups([...controller.groups.value]);
};
const onDragEnd: OnDragEndResponder = async (result) => {
if (!controller) return;
const { source, destination } = result;
const group = groups.find((g) => g.groupId === source.droppableId);
if (!group) return;
if (source.droppableId === destination?.droppableId) {
// move inside the block (group)
await controller.exchangeRow(group.rows[source.index].id, group.rows[destination.index].id);
} else {
// move to different block (group)
if (!destination?.droppableId) return;
await controller.moveRow(group.rows[source.index].id, destination.droppableId);
}
};
return { loadFields, controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd };
};

View File

@ -1,14 +1,19 @@
import { DatabaseController } from '../../../stores/effects/database/database_controller';
import { RowController } from '../../../stores/effects/database/row/row_controller';
import { RowInfo } from '../../../stores/effects/database/row/row_cache';
import { CellIdentifier } from '../../../stores/effects/database/cell/cell_bd_svc';
import { DatabaseController } from '$app/stores/effects/database/database_controller';
import { RowController } from '$app/stores/effects/database/row/row_controller';
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { useEffect, useState } from 'react';
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
import { None } from 'ts-results';
import { useAppSelector } from '$app/stores/store';
export const useRow = (viewId: string, databaseController: DatabaseController, rowInfo: RowInfo) => {
const [cells, setCells] = useState<{ fieldId: string; cellIdentifier: CellIdentifier }[]>([]);
const [rowController, setRowController] = useState<RowController>();
const databaseStore = useAppSelector((state) => state.database);
useEffect(() => {
if (!databaseController || !rowInfo) return;
const rowCache = databaseController.databaseViewCache.getRowCache();
const fieldController = databaseController.fieldController;
const c = new RowController(rowInfo, fieldController, rowCache);
@ -17,7 +22,7 @@ export const useRow = (viewId: string, databaseController: DatabaseController, r
return () => {
// dispose row controller in future
};
}, []);
}, [databaseController, rowInfo]);
useEffect(() => {
if (!rowController) return;
@ -35,9 +40,16 @@ export const useRow = (viewId: string, databaseController: DatabaseController, r
setCells(loadingCells);
})();
}, [rowController]);
}, [rowController, databaseStore.columns]);
const onNewColumnClick = async () => {
if (!databaseController) return;
const controller = new TypeOptionController(viewId, None);
await controller.initialize();
};
return {
cells: cells,
cells,
onNewColumnClick,
};
};

View File

@ -0,0 +1,7 @@
export const ArrowLeftSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M10 4L6 8L10 12' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
</svg>
);
};

View File

@ -0,0 +1,7 @@
export const ArrowRightSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M6 4L10 8L6 12' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
</svg>
);
};

View File

@ -0,0 +1,13 @@
export const CheckboxSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M6.5 8L8.11538 9.5L13.5 4.5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path
d='M13 8.5V11.8889C13 12.1836 12.8829 12.4662 12.6746 12.6746C12.4662 12.8829 12.1836 13 11.8889 13H4.11111C3.81643 13 3.53381 12.8829 3.32544 12.6746C3.11706 12.4662 3 12.1836 3 11.8889V4.11111C3 3.81643 3.11706 3.53381 3.32544 3.32544C3.53381 3.11706 3.81643 3 4.11111 3H10.2222'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
);
};

View File

@ -0,0 +1,7 @@
export const CheckmarkSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 10 8' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M1 5.2L2.84615 7L9 1' stroke='#00BCF0' strokeLinecap='round' strokeLinejoin='round' />
</svg>
);
};

View File

@ -0,0 +1,15 @@
export const ClockSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z'
stroke='currentColor'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path d='M8 5V8L10 9' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path d='M11.5 2.5L13.5 4.5' stroke='currentColor' strokeLinecap='round' />
<path d='M4.5 2.5L2.5 4.5' stroke='currentColor' strokeLinecap='round' />
</svg>
);
};

View File

@ -0,0 +1,8 @@
export const EditorCheckSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect x='2' y='2' width='12' height='12' rx='4' fill='#00BCF0' />
<path d='M6 8L7.61538 9.5L10.5 6.5' stroke='white' strokeLinecap='round' strokeLinejoin='round' />
</svg>
);
};

View File

@ -0,0 +1,7 @@
export const EditorUncheckSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<rect x='2.5' y='2.5' width='11' height='11' rx='3.5' stroke='#BDBDBD' />
</svg>
);
};

View File

@ -0,0 +1,10 @@
export const MoreSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M9.39568 7.6963L6.91032 5.56599C6.65085 5.34358 6.25 5.52795 6.25 5.86969L6.25 10.1303C6.25 10.4721 6.65085 10.6564 6.91032 10.434L9.39568 8.3037C9.58192 8.14406 9.58192 7.85594 9.39568 7.6963Z'
fill='currentColor'
/>
</svg>
);
};

View File

@ -0,0 +1,9 @@
export const SkipLeftSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M3 11.7778L3 4' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path d='M9.5 4.5L6 8L9.5 11.5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path d='M6 8L13 8' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
</svg>
);
};

View File

@ -0,0 +1,9 @@
export const SkipRightSvg = () => {
return (
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path d='M13 11.7778L13 4' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path d='M6.5 4.5L10 8L6.5 11.5' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
<path d='M10 8L3 8' stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' />
</svg>
);
};

View File

@ -4,9 +4,23 @@ import { BoardBlock } from './BoardBlock';
import { NewBoardBlock } from './NewBoardBlock';
import { useDatabase } from '../_shared/database-hooks/useDatabase';
import { ViewLayoutTypePB } from '@/services/backend';
import { DragDropContext } from 'react-beautiful-dnd';
import { useState } from 'react';
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
import { EditRow } from '$app/components/_shared/EditRow/EditRow';
export const Board = ({ viewId }: { viewId: string }) => {
const { controller, rows, groups } = useDatabase(viewId, ViewLayoutTypePB.Board);
const { controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(
viewId,
ViewLayoutTypePB.Board
);
const [showBoardRow, setShowBoardRow] = useState(false);
const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
const onOpenRow = (rowInfo: RowInfo) => {
setBoardRowInfo(rowInfo);
setShowBoardRow(true);
};
return (
<>
@ -22,24 +36,35 @@ export const Board = ({ viewId }: { viewId: string }) => {
<SearchInput />
</div>
</div>
<div className={'relative w-full flex-1 overflow-auto'}>
<div className={'absolute flex h-full flex-shrink-0 items-start justify-start gap-4'}>
{controller &&
groups &&
groups.map((group, index) => (
<BoardBlock
key={index}
viewId={viewId}
controller={controller}
rows={group.rows}
title={group.name}
allRows={rows}
/>
))}
<NewBoardBlock onClick={() => console.log('new block')}></NewBoardBlock>
<DragDropContext onDragEnd={onDragEnd}>
<div className={'relative w-full flex-1 overflow-auto'}>
<div className={'absolute flex h-full flex-shrink-0 items-start justify-start gap-4'}>
{controller &&
groups &&
groups.map((group, index) => (
<BoardBlock
key={group.groupId}
viewId={viewId}
controller={controller}
group={group}
allRows={rows}
groupByFieldId={groupByFieldId}
onNewRowClick={() => onNewRowClick(index)}
onOpenRow={onOpenRow}
/>
))}
<NewBoardBlock onClick={() => console.log('new block')}></NewBoardBlock>
</div>
</div>
</div>
</DragDropContext>
{controller && showBoardRow && boardRowInfo && (
<EditRow
onClose={() => setShowBoardRow(false)}
viewId={viewId}
controller={controller}
rowInfo={boardRowInfo}
></EditRow>
)}
</>
);
};

View File

@ -3,26 +3,31 @@ import AddSvg from '../_shared/svg/AddSvg';
import { BoardCard } from './BoardCard';
import { RowInfo } from '../../stores/effects/database/row/row_cache';
import { DatabaseController } from '../../stores/effects/database/database_controller';
import { RowPB } from '@/services/backend';
import { Droppable } from 'react-beautiful-dnd';
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
export const BoardBlock = ({
viewId,
controller,
title,
rows,
allRows,
groupByFieldId,
onNewRowClick,
onOpenRow,
group,
}: {
viewId: string;
controller: DatabaseController;
title: string;
rows: RowPB[];
allRows: readonly RowInfo[];
groupByFieldId: string;
onNewRowClick: () => void;
onOpenRow: (rowId: RowInfo) => void;
group: DatabaseGroupController;
}) => {
return (
<div className={'flex h-full w-[250px] flex-col rounded-lg bg-surface-1'}>
<div className={'flex items-center justify-between p-4'}>
<div className={'flex items-center gap-2'}>
<span>{title}</span>
<span>{group.name}</span>
<span className={'text-shade-4'}>()</span>
</div>
<div className={'flex items-center gap-2'}>
@ -34,18 +39,37 @@ export const BoardBlock = ({
</button>
</div>
</div>
<div className={'flex flex-1 flex-col gap-1 overflow-auto px-2'}>
{rows.map((row_pb, index) => {
const row = allRows.find((r) => r.row.id === row_pb.id);
return row ? (
<BoardCard viewId={viewId} controller={controller} key={index} rowInfo={row}></BoardCard>
) : (
<span key={index}></span>
);
})}
</div>
<Droppable droppableId={group.groupId}>
{(provided) => (
<div
className={'flex flex-1 flex-col gap-1 overflow-auto px-2'}
{...provided.droppableProps}
ref={provided.innerRef}
>
{group.rows.map((row_pb, index) => {
const row = allRows.find((r) => r.row.id === row_pb.id);
return row ? (
<BoardCard
viewId={viewId}
controller={controller}
index={index}
key={row.row.id}
rowInfo={row}
groupByFieldId={groupByFieldId}
onOpenRow={onOpenRow}
></BoardCard>
) : (
<span key={index}></span>
);
})}
</div>
)}
</Droppable>
<div className={'p-2'}>
<button className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-surface-2'}>
<button
onClick={onNewRowClick}
className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-surface-2'}
>
<span className={'h-5 w-5'}>
<AddSvg></AddSvg>
</span>

View File

@ -3,36 +3,52 @@ import { RowInfo } from '../../stores/effects/database/row/row_cache';
import { useRow } from '../_shared/database-hooks/useRow';
import { DatabaseController } from '../../stores/effects/database/database_controller';
import { BoardCell } from './BoardCell';
import { Draggable } from 'react-beautiful-dnd';
export const BoardCard = ({
index,
viewId,
controller,
rowInfo,
groupByFieldId,
onOpenRow,
}: {
index: number;
viewId: string;
controller: DatabaseController;
rowInfo: RowInfo;
groupByFieldId: string;
onOpenRow: (rowId: RowInfo) => void;
}) => {
const { cells } = useRow(viewId, controller, rowInfo);
return (
<div
onClick={() => console.log('on click')}
className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `}
>
<button className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
<Details2Svg></Details2Svg>
</button>
<div className={'flex flex-col gap-3'}>
{cells.map((cell, index) => (
<BoardCell
key={index}
cellIdentifier={cell.cellIdentifier}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
></BoardCell>
))}
</div>
</div>
<Draggable draggableId={rowInfo.row.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
onClick={() => onOpenRow(rowInfo)}
className={`relative cursor-pointer select-none rounded-lg border border-shade-6 bg-white px-3 py-2 transition-transform duration-100 hover:bg-main-selector `}
>
<button className={'absolute right-4 top-2.5 h-5 w-5 rounded hover:bg-surface-2'}>
<Details2Svg></Details2Svg>
</button>
<div className={'flex flex-col gap-3'}>
{cells
.filter((cell) => cell.fieldId !== groupByFieldId)
.map((cell, cellIndex) => (
<BoardCell
key={cellIndex}
cellIdentifier={cell.cellIdentifier}
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
fieldController={controller.fieldController}
></BoardCell>
))}
</div>
</div>
)}
</Draggable>
);
};

View File

@ -5,6 +5,7 @@ import { FieldType } from '../../../services/backend';
import { BoardOptionsCell } from './BoardOptionsCell';
import { BoardDateCell } from './BoardDateCell';
import { BoardTextCell } from './BoardTextCell';
import { BoardUrlCell } from '$app/components/board/BoardUrlCell';
export const BoardCell = ({
cellIdentifier,
@ -31,6 +32,12 @@ export const BoardCell = ({
cellCache={cellCache}
fieldController={fieldController}
></BoardDateCell>
) : cellIdentifier.fieldType === FieldType.URL ? (
<BoardUrlCell
cellIdentifier={cellIdentifier}
cellCache={cellCache}
fieldController={fieldController}
></BoardUrlCell>
) : (
<BoardTextCell
cellIdentifier={cellIdentifier}

View File

@ -3,6 +3,7 @@ import { useCell } from '../_shared/database-hooks/useCell';
import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '../../stores/effects/database/cell/cell_cache';
import { FieldController } from '../../stores/effects/database/field/field_controller';
import { getBgColor } from '$app/components/_shared/getColor';
export const BoardOptionsCell = ({
cellIdentifier,
@ -16,10 +17,13 @@ export const BoardOptionsCell = ({
const { data } = useCell(cellIdentifier, cellCache, fieldController);
return (
<>
<div className={'flex flex-wrap items-center gap-2 py-2 text-xs text-black'}>
{(data as SelectOptionCellDataPB | undefined)?.select_options?.map((option, index) => (
<div key={index}>{option?.name || ''}</div>
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5`} key={index}>
{option?.name || ''}
</div>
)) || ''}
</>
&nbsp;
</div>
);
};

View File

@ -1,6 +1,6 @@
import { CellIdentifier } from '../../stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '../../stores/effects/database/cell/cell_cache';
import { FieldController } from '../../stores/effects/database/field/field_controller';
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { useCell } from '../_shared/database-hooks/useCell';
export const BoardTextCell = ({
@ -14,5 +14,11 @@ export const BoardTextCell = ({
}) => {
const { data } = useCell(cellIdentifier, cellCache, fieldController);
return <div>{(data as string | undefined) || ''}</div>;
return (
<div>
{((data as string | undefined) || '').split('\n').map((line, index) => (
<div key={index}>{line}</div>
))}
</div>
);
};

View File

@ -0,0 +1,29 @@
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
import { FieldController } from '$app/stores/effects/database/field/field_controller';
import { useCell } from '$app/components/_shared/database-hooks/useCell';
import { URLCellDataPB } from '@/services/backend';
export const BoardUrlCell = ({
cellIdentifier,
cellCache,
fieldController,
}: {
cellIdentifier: CellIdentifier;
cellCache: CellCache;
fieldController: FieldController;
}) => {
const { data } = useCell(cellIdentifier, cellCache, fieldController);
return (
<>
<a
className={'text-main-accent hover:underline'}
href={(data as URLCellDataPB | undefined)?.url || ''}
target={'_blank'}
>
{(data as URLCellDataPB | undefined)?.content || ''}
</a>
</>
);
};

View File

@ -3,7 +3,7 @@ import { CloseSvg } from '../_shared/svg/CloseSvg';
export const ErrorModal = ({ message, onClose }: { message: string; onClose: () => void }) => {
return (
<div className={'fixed inset-0 z-20 flex items-center justify-center bg-white/30 backdrop-blur-sm'}>
<div className={'fixed inset-0 z-10 flex items-center justify-center bg-white/30 backdrop-blur-sm'}>
<div
className={
'relative flex flex-col items-center gap-8 rounded-xl border border-shade-5 bg-white px-16 py-8 shadow-md'
@ -11,7 +11,7 @@ export const ErrorModal = ({ message, onClose }: { message: string; onClose: ()
>
<button
onClick={() => onClose()}
className={'absolute right-0 top-0 z-20 px-2 py-2 text-shade-5 hover:text-black'}
className={'absolute right-0 top-0 z-10 px-2 py-2 text-shade-5 hover:text-black'}
>
<i className={'block h-8 w-8'}>
<CloseSvg></CloseSvg>

View File

@ -1,5 +1,5 @@
import { nanoid } from 'nanoid';
import { FieldType } from '../../../../services/backend/models/flowy-database/field_entities';
import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
import { gridActions } from '../../../stores/reducers/grid/slice';
import { useAppDispatch, useAppSelector } from '../../../stores/store';

View File

@ -7,7 +7,7 @@ import { SingleSelectTypeSvg } from '../../_shared/svg/SingleSelectTypeSvg';
import { MultiSelectTypeSvg } from '../../_shared/svg/MultiSelectTypeSvg';
import { ChecklistTypeSvg } from '../../_shared/svg/ChecklistTypeSvg';
import { UrlTypeSvg } from '../../_shared/svg/UrlTypeSvg';
import { FieldType } from '../../../../services/backend/models/flowy-database/field_entities';
import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
export const GridTableHeader = () => {
const { fields, onAddField } = useGridTableHeaderHooks();

View File

@ -0,0 +1,15 @@
import { EarthSvg } from '$app/components/_shared/svg/EarthSvg';
import { useState } from 'react';
import { LanguageSelectPopup } from '$app/components/_shared/LanguageSelectPopup';
export const LanguageButton = () => {
const [showPopup, setShowPopup] = useState(false);
return (
<>
<button onClick={() => setShowPopup(!showPopup)} className={'h-5 w-5'}>
<EarthSvg></EarthSvg>
</button>
{showPopup && <LanguageSelectPopup onClose={() => setShowPopup(false)}></LanguageSelectPopup>}
</>
);
};

View File

@ -2,6 +2,7 @@ import { Button } from '../../_shared/Button';
import { Details2Svg } from '../../_shared/svg/Details2Svg';
import { usePageOptions } from './PageOptions.hooks';
import { OptionsPopup } from './OptionsPopup';
import { LanguageButton } from '$app/components/layout/HeaderPanel/LanguageButton';
export const PageOptions = () => {
const { showOptionsPopup, onOptionsClick, onClose, onSignOutClick } = usePageOptions();
@ -13,7 +14,9 @@ export const PageOptions = () => {
Share
</Button>
<button id='option-button' className={'relative h-8 w-8'} onClick={onOptionsClick} >
<LanguageButton></LanguageButton>
<button id='option-button' className={'relative h-8 w-8'} onClick={onOptionsClick}>
<Details2Svg></Details2Svg>
</button>
</div>

View File

@ -2,7 +2,7 @@ import { foldersActions, IFolder } from '../../../stores/reducers/folders/slice'
import { useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from '../../../stores/store';
import { IPage, pagesActions } from '../../../stores/reducers/pages/slice';
import { AppPB, ViewLayoutTypePB } from '../../../../services/backend';
import { AppPB, ViewLayoutTypePB } from '@/services/backend';
import { AppBackendService } from '../../../stores/effects/folder/app/app_bd_svc';
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
import { useError } from '../../error/Error.hooks';

View File

@ -1,7 +1,7 @@
import { useAppSelector } from '../../../stores/store';
import { useNavigate } from 'react-router-dom';
import { IPage } from '../../../stores/reducers/pages/slice';
import { ViewLayoutTypePB } from '../../../../services/backend';
import { ViewLayoutTypePB } from '@/services/backend';
import { useState } from 'react';
export const useNavigationPanelHooks = function () {

View File

@ -7,7 +7,7 @@ import { IPage } from '../../../stores/reducers/pages/slice';
import { Button } from '../../_shared/Button';
import { usePageEvents } from './PageItem.hooks';
import { RenamePopup } from './RenamePopup';
import { ViewLayoutTypePB } from '../../../../services/backend';
import { ViewLayoutTypePB } from '@/services/backend';
import { useEffect, useRef, useState } from 'react';
import { PAGE_ITEM_HEIGHT } from '../../_shared/constants';

View File

@ -1,5 +1,5 @@
import { FlowyError, UserNotification } from '../../../../../services/backend';
import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications';
import { FlowyError, UserNotification } from '@/services/backend';
import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
import { Result } from 'ts-results';
declare type UserNotificationCallback = (ty: UserNotification, payload: Result<Uint8Array, FlowyError>) => void;

View File

@ -1,5 +1,5 @@
import { FlowyError, UserNotification, UserProfilePB } from '../../../../../services/backend';
import { AFNotificationObserver, OnNotificationError } from '../../../../../services/backend/notifications';
import { FlowyError, UserNotification, UserProfilePB } from '@/services/backend';
import { AFNotificationObserver, OnNotificationError } from '@/services/backend/notifications';
import { UserNotificationParser } from './parser';
import { Ok, Result } from 'ts-results';

View File

@ -1,6 +1,5 @@
import { DatabaseEventGetCell, DatabaseEventUpdateCell } from '../../../../../services/backend/events/flowy-database';
import { CellChangesetPB, CellIdPB } from '../../../../../services/backend/models/flowy-database/cell_entities';
import { FieldType } from '../../../../../services/backend/models/flowy-database/field_entities';
import { DatabaseEventGetCell, DatabaseEventUpdateCell } from '@/services/backend/events/flowy-database';
import { CellChangesetPB, CellIdPB, FieldType } from '@/services/backend';
class CellIdentifier {
constructor(

View File

@ -108,6 +108,7 @@ export class CellController<T, D> {
};
dispose = async () => {
this.cellDataNotifier.unsubscribe();
await this.cellObserver.unsubscribe();
await this.fieldNotifier.unsubscribe();
};

View File

@ -1,7 +1,7 @@
import { Ok, Result } from 'ts-results';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { DatabaseNotificationObserver } from '../notifications/observer';
import { DatabaseNotification, FlowyError } from '../../../../../services/backend';
import { DatabaseNotification, FlowyError } from '@/services/backend';
type UpdateCellNotifiedValue = Result<void, FlowyError>;

View File

@ -1,4 +1,4 @@
import { DateCellDataPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '../../../../../services/backend';
import { DateCellDataPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend';
import { CellIdentifier } from './cell_bd_svc';
import { CellController } from './cell_controller';
import {

View File

@ -1,10 +1,8 @@
import utf8 from 'utf8';
import { CellBackendService, CellIdentifier } from './cell_bd_svc';
import { DateCellDataPB } from '../../../../../services/backend/models/flowy-database/date_type_option_entities';
import { SelectOptionCellDataPB } from '../../../../../services/backend/models/flowy-database/select_type_option';
import { URLCellDataPB } from '../../../../../services/backend/models/flowy-database/url_type_option_entities';
import { SelectOptionCellDataPB, URLCellDataPB, DateCellDataPB } from '@/services/backend';
import { Err, None, Ok, Option, Some } from 'ts-results';
import { Log } from '../../../../utils/log';
import { Log } from '$app/utils/log';
abstract class CellDataParser<T> {
abstract parserData(data: Uint8Array): Option<T>;

View File

@ -1,10 +1,8 @@
import { Result } from 'ts-results';
import { FlowyError } from '../../../../../services/backend/models/flowy-error';
import { CellBackendService, CellIdentifier } from './cell_bd_svc';
import { CalendarData } from './controller_builder';
import { DateChangesetPB } from '../../../../../services/backend/models/flowy-database/date_type_option_entities';
import { CellIdPB } from '../../../../../services/backend/models/flowy-database/cell_entities';
import { DatabaseEventUpdateDateCell } from '../../../../../services/backend/events/flowy-database';
import { DateChangesetPB, FlowyError, CellIdPB } from '@/services/backend';
import { DatabaseEventUpdateDateCell } from '@/services/backend/events/flowy-database';
export abstract class CellDataPersistence<D> {
abstract save(data: D): Promise<Result<void, FlowyError>>;

View File

@ -5,13 +5,13 @@ import {
SelectOptionCellChangesetPB,
SelectOptionChangesetPB,
SelectOptionPB,
} from '../../../../../services/backend';
} from '@/services/backend';
import {
DatabaseEventCreateSelectOption,
DatabaseEventGetSelectOptionCellData,
DatabaseEventUpdateSelectOption,
DatabaseEventUpdateSelectOptionCell,
} from '../../../../../services/backend/events/flowy-database';
} from '@/services/backend/events/flowy-database';
export class SelectOptionBackendService {
constructor(public readonly viewId: string, public readonly fieldId: string) {}

View File

@ -1,15 +1,20 @@
import {
DatabaseEventCreateRow,
DatabaseEventGetDatabase,
DatabaseEventGetDatabaseSetting,
DatabaseEventGetFields,
DatabaseEventGetGroup,
DatabaseEventGetGroups,
DatabaseEventMoveField,
DatabaseEventMoveGroup,
DatabaseEventMoveGroupRow,
DatabaseEventMoveRow,
DatabaseGroupIdPB,
MoveFieldPayloadPB,
MoveGroupPayloadPB,
MoveGroupRowPayloadPB,
} from '../../../../services/backend/events/flowy-database';
MoveRowPayloadPB,
} from '@/services/backend/events/flowy-database';
import {
GetFieldPayloadPB,
RepeatedFieldIdPB,
@ -17,9 +22,10 @@ import {
DatabaseViewIdPB,
CreateRowPayloadPB,
ViewIdPB,
} from '../../../../services/backend';
import { FolderEventCloseView } from '../../../../services/backend/events/flowy-folder';
} from '@/services/backend';
import { FolderEventCloseView } from '@/services/backend/events/flowy-folder';
/// A service that wraps the backend service
export class DatabaseBackendService {
viewId: string;
@ -27,6 +33,7 @@ export class DatabaseBackendService {
this.viewId = viewId;
}
/// Open a database
openDatabase = async () => {
const payload = DatabaseViewIdPB.fromObject({
value: this.viewId,
@ -34,6 +41,7 @@ export class DatabaseBackendService {
return DatabaseEventGetDatabase(payload);
};
/// Close a database
closeDatabase = async () => {
const payload = ViewIdPB.fromObject({ value: this.viewId });
return FolderEventCloseView(payload);
@ -72,6 +80,15 @@ export class DatabaseBackendService {
return DatabaseEventMoveGroupRow(payload);
};
exchangeRow = (fromRowId: string, toRowId: string) => {
const payload = MoveRowPayloadPB.fromObject({
view_id: this.viewId,
from_row_id: fromRowId,
to_row_id: toRowId,
});
return DatabaseEventMoveRow(payload);
};
moveGroup = (fromGroupId: string, toGroupId: string) => {
const payload = MoveGroupPayloadPB.fromObject({
view_id: this.viewId,
@ -81,6 +98,17 @@ export class DatabaseBackendService {
return DatabaseEventMoveGroup(payload);
};
moveField = (fieldId: string, fromIndex: number, toIndex: number) => {
const payload = MoveFieldPayloadPB.fromObject({
view_id: this.viewId,
field_id: fieldId,
from_index: fromIndex,
to_index: toIndex,
});
return DatabaseEventMoveField(payload);
};
/// Get all fields in database
getFields = async (fieldIds?: FieldIdPB[]) => {
const payload = GetFieldPayloadPB.fromObject({ view_id: this.viewId });
@ -91,6 +119,7 @@ export class DatabaseBackendService {
return DatabaseEventGetFields(payload).then((result) => result.map((value) => value.items));
};
/// Get a group by id
getGroup = (groupId: string) => {
const payload = DatabaseGroupIdPB.fromObject({ view_id: this.viewId, group_id: groupId });
return DatabaseEventGetGroup(payload);
@ -102,4 +131,9 @@ export class DatabaseBackendService {
const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
return DatabaseEventGetGroups(payload);
};
getSettings = () => {
const payload = DatabaseViewIdPB.fromObject({ value: this.viewId });
return DatabaseEventGetDatabaseSetting(payload);
};
}

View File

@ -1,13 +1,13 @@
import { DatabaseBackendService } from './database_bd_svc';
import { FieldController, FieldInfo } from './field/field_controller';
import { DatabaseViewCache } from './view/database_view_cache';
import { DatabasePB, GroupPB } from '../../../../services/backend';
import { DatabasePB, FlowyError, GroupPB } from '@/services/backend';
import { RowChangedReason, RowInfo } from './row/row_cache';
import { Err } from 'ts-results';
import { Err, Ok } from 'ts-results';
import { DatabaseGroupController } from './group/group_controller';
import { BehaviorSubject } from 'rxjs';
import { DatabaseGroupObserver } from './group/group_observer';
import { Log } from '../../../utils/log';
import { Log } from '$app/utils/log';
export type DatabaseSubscriberCallbacks = {
onViewChanged?: (data: DatabasePB) => void;
@ -71,6 +71,20 @@ export class DatabaseController {
}
};
getGroupByFieldId = async () => {
const settingsResult = await this.backendService.getSettings();
if (settingsResult.ok) {
const settings = settingsResult.val;
const groupConfig = settings.group_configurations.items;
if (groupConfig.length === 0) {
return Err(new FlowyError({ msg: 'this database has no groups' }));
}
return Ok(settings.group_configurations.items[0].field_id);
} else {
return Err(settingsResult.val);
}
};
createRow = () => {
return this.backendService.createRow();
};
@ -79,10 +93,19 @@ export class DatabaseController {
return this.backendService.moveGroupRow(rowId, groupId);
};
exchangeRow = async (fromRowId: string, toRowId: string) => {
await this.backendService.exchangeRow(fromRowId, toRowId);
await this.loadGroup();
};
moveGroup = (fromGroupId: string, toGroupId: string) => {
return this.backendService.moveGroup(fromGroupId, toGroupId);
};
moveField = (fieldId: string, fromIndex: number, toIndex: number) => {
return this.backendService.moveField(fieldId, fromIndex, toIndex);
};
private loadGroup = async () => {
const result = await this.backendService.loadGroups();
if (result.ok) {
@ -146,6 +169,10 @@ export class DatabaseController {
};
dispose = async () => {
this.groups.value.forEach((group) => {
void group.dispose();
});
await this.groupsObserver.unsubscribe();
await this.backendService.closeDatabase();
await this.fieldController.dispose();
await this.databaseViewCache.dispose();

View File

@ -5,14 +5,14 @@ import {
FieldType,
TypeOptionChangesetPB,
TypeOptionPathPB,
} from '../../../../../services/backend/models/flowy-database/field_entities';
} from '@/services/backend';
import {
DatabaseEventDeleteField,
DatabaseEventDuplicateField,
DatabaseEventGetTypeOption,
DatabaseEventUpdateField,
DatabaseEventUpdateFieldTypeOption,
} from '../../../../../services/backend/events/flowy-database';
} from '@/services/backend/events/flowy-database';
export abstract class TypeOptionParser<T> {
abstract fromBuffer(buffer: Uint8Array): T;

View File

@ -1,8 +1,8 @@
import { Log } from '../../../../utils/log';
import { Log } from '$app/utils/log';
import { DatabaseBackendService } from '../database_bd_svc';
import { DatabaseFieldChangesetObserver } from './field_observer';
import { FieldIdPB, FieldPB, IndexFieldPB } from '../../../../../services/backend';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { FieldIdPB, FieldPB, IndexFieldPB } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
export class FieldController {
private backendService: DatabaseBackendService;

View File

@ -1,6 +1,6 @@
import { Ok, Result } from 'ts-results';
import { DatabaseNotification, DatabaseFieldChangesetPB, FlowyError, FieldPB } from '../../../../../services/backend';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { DatabaseNotification, DatabaseFieldChangesetPB, FlowyError, FieldPB } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { DatabaseNotificationObserver } from '../notifications/observer';
export type FieldChangesetSubscribeCallback = (value: Result<DatabaseFieldChangesetPB, FlowyError>) => void;

View File

@ -3,12 +3,12 @@ import {
FieldType,
TypeOptionPathPB,
UpdateFieldTypePayloadPB,
} from '../../../../../../services/backend';
} from '@/services/backend';
import {
DatabaseEventCreateTypeOption,
DatabaseEventGetTypeOption,
DatabaseEventUpdateFieldType,
} from '../../../../../../services/backend/events/flowy-database';
} from '@/services/backend/events/flowy-database';
export class TypeOptionBackendService {
constructor(public readonly viewId: string) {}

View File

@ -9,7 +9,7 @@ import {
NumberTypeOptionPB,
SingleSelectTypeOptionPB,
URLTypeOptionPB,
} from '../../../../../../services/backend';
} from '@/services/backend';
import { utf8Decoder, utf8Encoder } from '../../cell/data_parser';
import { DatabaseFieldObserver } from '../field_observer';

View File

@ -1,7 +1,7 @@
import { FieldPB, FieldType, TypeOptionPB } from '../../../../../../services/backend';
import { ChangeNotifier } from '../../../../../utils/change_notifier';
import { FieldPB, FieldType, TypeOptionPB } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { FieldBackendService } from '../field_bd_svc';
import { Log } from '../../../../../utils/log';
import { Log } from '$app/utils/log';
import { None, Option, Some } from 'ts-results';
import { FieldInfo } from '../field_controller';
import { TypeOptionBackendService } from './type_option_bd_svc';

View File

@ -1,14 +1,8 @@
import {
DatabaseNotification,
FlowyError,
GroupPB,
GroupRowsNotificationPB,
RowPB,
} from '../../../../../services/backend';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { DatabaseNotification, FlowyError, GroupPB, GroupRowsNotificationPB, RowPB } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { None, Ok, Option, Result, Some } from 'ts-results';
import { DatabaseNotificationObserver } from '../notifications/observer';
import { Log } from '../../../../utils/log';
import { Log } from '$app/utils/log';
import { DatabaseBackendService } from '../database_bd_svc';
export type GroupDataCallbacks = {

View File

@ -1,6 +1,6 @@
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { Ok, Result } from 'ts-results';
import { DatabaseNotification, FlowyError, GroupChangesetPB, GroupPB } from '../../../../../services/backend';
import { DatabaseNotification, FlowyError, GroupChangesetPB, GroupPB } from '@/services/backend';
import { DatabaseNotificationObserver } from '../notifications/observer';
export type GroupByFieldCallback = (value: Result<GroupPB[], FlowyError>) => void;

View File

@ -1,5 +1,5 @@
import { DatabaseNotification, FlowyError } from '../../../../../services/backend';
import { AFNotificationObserver } from '../../../../../services/backend/notifications';
import { DatabaseNotification, FlowyError } from '@/services/backend';
import { AFNotificationObserver } from '@/services/backend/notifications';
import { DatabaseNotificationParser } from './parser';
import { Result } from 'ts-results';

View File

@ -1,5 +1,5 @@
import { DatabaseNotification, FlowyError } from '../../../../../services/backend';
import { NotificationParser } from '../../../../../services/backend/notifications';
import { DatabaseNotification, FlowyError } from '@/services/backend';
import { NotificationParser } from '@/services/backend/notifications';
import { Result } from 'ts-results';
declare type DatabaseNotificationCallback = (ty: DatabaseNotification, payload: Result<Uint8Array, FlowyError>) => void;

View File

@ -1,10 +1,10 @@
import { CreateRowPayloadPB, RowIdPB } from '../../../../../services/backend';
import { CreateRowPayloadPB, RowIdPB } from '@/services/backend';
import {
DatabaseEventCreateRow,
DatabaseEventDeleteRow,
DatabaseEventDuplicateRow,
DatabaseEventGetRow,
} from '../../../../../services/backend/events/flowy-database';
} from '@/services/backend/events/flowy-database';
export class RowBackendService {
constructor(public readonly viewId: string) {}

View File

@ -7,14 +7,14 @@ import {
RowsChangesetPB,
RowsVisibilityChangesetPB,
ReorderSingleRowPB,
} from '../../../../../services/backend';
import { ChangeNotifier } from '../../../../utils/change_notifier';
} from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { FieldInfo } from '../field/field_controller';
import { CellCache, CellCacheKey } from '../cell/cell_cache';
import { CellIdentifier } from '../cell/cell_bd_svc';
import { DatabaseEventGetRow } from '../../../../../services/backend/events/flowy-database';
import { DatabaseEventGetRow } from '@/services/backend/events/flowy-database';
import { None, Option, Some } from 'ts-results';
import { Log } from '../../../../utils/log';
import { Log } from '$app/utils/log';
export type CellByFieldId = Map<string, CellIdentifier>;

View File

@ -1,8 +1,7 @@
import { DatabaseViewRowsObserver } from './view_row_observer';
import { RowCache, RowInfo } from '../row/row_cache';
import { FieldController } from '../field/field_controller';
import { RowPB } from '../../../../../services/backend';
import { Subscription } from 'rxjs';
import { RowPB } from '@/services/backend';
export class DatabaseViewCache {
private readonly rowsObserver: DatabaseViewRowsObserver;

View File

@ -6,8 +6,8 @@ import {
ReorderSingleRowPB,
RowsChangesetPB,
RowsVisibilityChangesetPB,
} from '../../../../../services/backend';
import { ChangeNotifier } from '../../../../utils/change_notifier';
} from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { DatabaseNotificationObserver } from '../notifications/observer';
export type RowsVisibilityNotifyValue = Result<RowsVisibilityChangesetPB, FlowyError>;

View File

@ -5,10 +5,10 @@ import {
FlowyError,
OpenDocumentPayloadPB,
ViewIdPB,
} from '../../../../services/backend';
import { DocumentEventApplyEdit, DocumentEventGetDocument } from '../../../../services/backend/events/flowy-document';
} from '@/services/backend';
import { DocumentEventApplyEdit, DocumentEventGetDocument } from '@/services/backend/events/flowy-document';
import { Result } from 'ts-results';
import { FolderEventCloseView } from '../../../../services/backend/events/flowy-folder';
import { FolderEventCloseView } from '@/services/backend/events/flowy-folder';
export class DocumentBackendService {
constructor(public readonly viewId: string) {}

View File

@ -6,7 +6,7 @@ import {
FolderEventReadApp,
FolderEventUpdateApp,
ViewLayoutTypePB,
} from '../../../../../services/backend/events/flowy-folder';
} from '@/services/backend/events/flowy-folder';
import {
AppIdPB,
UpdateAppPayloadPB,
@ -16,7 +16,7 @@ import {
MoveFolderItemPayloadPB,
MoveFolderItemType,
FlowyError,
} from '../../../../../services/backend';
} from '@/services/backend';
import { None, Result, Some } from 'ts-results';
export class AppBackendService {

View File

@ -1,6 +1,6 @@
import { Ok, Result } from 'ts-results';
import { AppPB, FlowyError, FolderNotification } from '../../../../../services/backend';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { AppPB, FlowyError, FolderNotification } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { FolderNotificationObserver } from '../notifications/observer';
export type AppUpdateNotifyCallback = (value: Result<AppPB, FlowyError>) => void;

View File

@ -1,6 +1,6 @@
import { OnNotificationError, AFNotificationObserver } from '../../../../../services/backend/notifications';
import { OnNotificationError, AFNotificationObserver } from '@/services/backend/notifications';
import { FolderNotificationParser } from './parser';
import { FlowyError, FolderNotification } from '../../../../../services/backend';
import { FlowyError, FolderNotification } from '@/services/backend';
import { Result } from 'ts-results';
export type ParserHandler = (notification: FolderNotification, payload: Result<Uint8Array, FlowyError>) => void;

View File

@ -1,5 +1,5 @@
import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications';
import { FlowyError, FolderNotification } from '../../../../../services/backend';
import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
import { FlowyError, FolderNotification } from '@/services/backend';
import { Result } from 'ts-results';
declare type FolderNotificationCallback = (ty: FolderNotification, payload: Result<Uint8Array, FlowyError>) => void;

View File

@ -1,9 +1,9 @@
import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB } from '../../../../../services/backend/models/flowy-folder/view';
import { UpdateViewPayloadPB, RepeatedViewIdPB, ViewPB } from '@/services/backend';
import {
FolderEventDeleteView,
FolderEventDuplicateView,
FolderEventUpdateView,
} from '../../../../../services/backend/events/flowy-folder';
} from '@/services/backend/events/flowy-folder';
export class ViewBackendService {
constructor(public readonly viewId: string) {}

View File

@ -1,7 +1,6 @@
import { Ok, Result } from 'ts-results';
import { FlowyError } from '../../../../../services/backend/models/flowy-error/errors';
import { DeletedViewPB, FolderNotification, ViewPB } from '../../../../../services/backend/models/flowy-folder';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { DeletedViewPB, FolderNotification, ViewPB, FlowyError } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { FolderNotificationObserver } from '../notifications/observer';
type DeleteViewNotifyValue = Result<ViewPB, FlowyError>;

View File

@ -4,8 +4,8 @@ import {
FolderEventMoveItem,
FolderEventReadWorkspaceApps,
FolderEventReadWorkspaces,
} from '../../../../../services/backend/events/flowy-folder';
import { CreateAppPayloadPB, WorkspaceIdPB, FlowyError, MoveFolderItemPayloadPB } from '../../../../../services/backend';
} from '@/services/backend/events/flowy-folder';
import { CreateAppPayloadPB, WorkspaceIdPB, FlowyError, MoveFolderItemPayloadPB } from '@/services/backend';
import assert from 'assert';
export class WorkspaceBackendService {

View File

@ -1,6 +1,6 @@
import { Ok, Result } from 'ts-results';
import { AppPB, FolderNotification, RepeatedAppPB, WorkspacePB, FlowyError } from '../../../../../services/backend';
import { ChangeNotifier } from '../../../../utils/change_notifier';
import { AppPB, FolderNotification, RepeatedAppPB, WorkspacePB, FlowyError } from '@/services/backend';
import { ChangeNotifier } from '$app/utils/change_notifier';
import { FolderNotificationObserver } from '../notifications/observer';
export type AppListNotifyValue = Result<AppPB[], FlowyError>;

View File

@ -6,7 +6,7 @@ import {
UserEventSignOut,
UserEventSignUp,
UserEventUpdateUserProfile,
} from '../../../../services/backend/events/flowy-user';
} from '@/services/backend/events/flowy-user';
import {
SignInPayloadPB,
SignUpPayloadPB,
@ -15,13 +15,13 @@ import {
CreateWorkspacePayloadPB,
WorkspaceSettingPB,
WorkspacePB,
} from '../../../../services/backend';
} from '@/services/backend';
import {
FolderEventCreateWorkspace,
FolderEventOpenWorkspace,
FolderEventReadCurrentWorkspace,
FolderEventReadWorkspaces,
} from '../../../../services/backend/events/flowy-folder';
} from '@/services/backend/events/flowy-folder';
export class UserBackendService {
constructor(public readonly userId: string) {}

View File

@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { nanoid } from 'nanoid';
import { WorkspaceSettingPB } from '../../../../services/backend/models/flowy-folder/workspace';
import { WorkspaceSettingPB } from '@/services/backend/models/flowy-folder/workspace';
export interface ICurrentUser {
id?: string;

View File

@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { FieldType } from '../../../../services/backend/models/flowy-database/field_entities';
import { DateFormat, NumberFormat, SelectOptionColorPB, TimeFormat } from '../../../../services/backend';
import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
import { DateFormat, NumberFormat, SelectOptionColorPB, TimeFormat } from '@/services/backend';
export interface ISelectOption {
selectOptionId: string;

View File

@ -1,5 +1,5 @@
import { FlowyError, FolderNotification } from '../../../../../services/backend';
import { NotificationParser, OnNotificationError } from '../../../../../services/backend/notifications';
import { FlowyError, FolderNotification } from '@/services/backend';
import { NotificationParser, OnNotificationError } from '@/services/backend/notifications';
import { Result } from 'ts-results';
declare type FolderNotificationCallback = (ty: FolderNotification, payload: Result<Uint8Array, FlowyError>) => void;

View File

@ -1,6 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { nanoid } from 'nanoid';
import { FieldType } from '../../../../services/backend/models/flowy-database/field_entities';
import { FieldType } from '@/services/backend/models/flowy-database/field_entities';
const initialState = {
title: 'My plans on the week',

View File

@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { ViewLayoutTypePB } from '../../../../services/backend';
import { ViewLayoutTypePB } from '@/services/backend';
export interface IPage {
id: string;

View File

@ -4,5 +4,6 @@ import App from './appflowy_app/App';
import './styles/tailwind.css';
import './styles/font.css';
import './styles/template.css';
import './styles/Calendar.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />);

View File

@ -0,0 +1,141 @@
.react-calendar {
width: 250px;
max-width: 100%;
background: white;
line-height: 1.125em;
}
.react-calendar--doubleView {
width: 700px;
}
.react-calendar--doubleView .react-calendar__viewContainer {
display: flex;
margin: -0.5em;
}
.react-calendar--doubleView .react-calendar__viewContainer > * {
width: 50%;
margin: 0.5em;
}
.react-calendar,
.react-calendar *,
.react-calendar *:before,
.react-calendar *:after {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.react-calendar button {
margin: 0;
border: 0;
outline: none;
}
.react-calendar button:enabled:hover {
cursor: pointer;
}
.react-calendar__navigation {
display: flex;
height: 44px;
margin-bottom: 1em;
}
.react-calendar__navigation button {
min-width: 44px;
background: none;
}
.react-calendar__navigation button:disabled {
background-color: #f0f0f0;
}
.react-calendar__navigation button:enabled:hover,
.react-calendar__navigation button:enabled:focus {
background-color: #e6e6e6;
}
.react-calendar__month-view__weekdays {
@apply mb-2 text-center text-xs uppercase text-shade-3;
}
.react-calendar__month-view__weekdays abbr {
@apply no-underline;
}
.react-calendar__month-view__weekdays__weekday {
padding: 0.5em;
}
.react-calendar__month-view__weekNumbers .react-calendar__tile {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75em;
font-weight: 400;
}
.react-calendar__month-view__days__day--weekend {
@apply text-main-alert;
}
.react-calendar__month-view__days__day--neighboringMonth {
@apply text-shade-4;
}
.react-calendar__year-view .react-calendar__tile,
.react-calendar__decade-view .react-calendar__tile,
.react-calendar__century-view .react-calendar__tile {
padding: 2em 0.5em;
}
.react-calendar__tile {
max-width: 100%;
background: none;
text-align: center;
line-height: 16px;
@apply rounded py-2;
}
.react-calendar__tile:disabled {
background-color: #f0f0f0;
}
.react-calendar__tile:enabled:hover,
.react-calendar__tile:enabled:focus {
background-color: #e6e6e6;
}
.react-calendar__tile--now {
@apply bg-shade-6;
}
.react-calendar__tile--now:enabled:hover,
.react-calendar__tile--now:enabled:focus {
@apply bg-shade-6;
}
.react-calendar__tile--hasActive {
background: #76baff;
}
.react-calendar__tile--hasActive:enabled:hover,
.react-calendar__tile--hasActive:enabled:focus {
background: #a9d4ff;
}
.react-calendar__tile--active {
@apply bg-main-accent text-white;
}
.react-calendar__tile--active:enabled:hover,
.react-calendar__tile--active:enabled:focus {
@apply bg-main-hovered;
}
.react-calendar--selectRange .react-calendar__tile--hover {
@apply bg-shade-4;
}

View File

@ -1,6 +1,11 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/react-tailwindcss-datepicker/dist/index.esm.js',
],
darkMode: 'class',
theme: {
extend: {
colors: {
@ -41,6 +46,9 @@ module.exports = {
fiol: '#2C144B',
},
},
boxShadow: {
md: '0px 0px 20px rgba(0, 0, 0, 0.1);',
},
},
},
plugins: [],