mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Fix/tauri board events (#2678)
* chore: focus name input on field edit * fix: remove random id for duplicated view * fix: use alias for imports * fix: folder, grid, board bugs * chore: remove log * fix: update options list on add * chore: close on delete option * chore: show and hide field * chore: add field with specific type * chore: small cleanup * fix: create view on another folder and views notifier reorganize --------- Co-authored-by: qinluhe <qinluhe.twodog@gmail.com>
This commit is contained in:
parent
4f2585baed
commit
6a43dd871d
@ -53,6 +53,7 @@ export const EditCheckListPopup = ({
|
||||
const onDeleteOptionClick = async () => {
|
||||
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||
await svc.deleteOption([editingSelectOption]);
|
||||
onOutsideClick();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { FocusEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -36,12 +36,24 @@ export const EditFieldPopup = ({
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const { t } = useTranslation();
|
||||
const changeTypeButtonRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(databaseStore.fields[cellIdentifier.fieldId].title);
|
||||
}, [databaseStore, cellIdentifier]);
|
||||
|
||||
// focus input on mount
|
||||
useEffect(() => {
|
||||
if (!inputRef.current || !name) return;
|
||||
inputRef.current.focus();
|
||||
}, [inputRef, name]);
|
||||
|
||||
const selectAll: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
e.target.selectionStart = 0;
|
||||
e.target.selectionEnd = e.target.value.length;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!fieldInfo) return;
|
||||
const controller = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
@ -80,6 +92,8 @@ export const EditFieldPopup = ({
|
||||
>
|
||||
<div className={'flex flex-col gap-2'}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
onFocus={selectAll}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={() => save()}
|
||||
|
@ -22,6 +22,7 @@ import { EditCheckListPopup } from '$app/components/_shared/EditRow/CheckList/Ed
|
||||
import { PropertiesPanel } from '$app/components/_shared/EditRow/PropertiesPanel';
|
||||
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
|
||||
import { PromptWindow } from '$app/components/_shared/PromptWindow';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const EditRow = ({
|
||||
onClose,
|
||||
@ -34,6 +35,7 @@ export const EditRow = ({
|
||||
controller: DatabaseController;
|
||||
rowInfo: RowInfo;
|
||||
}) => {
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const { cells, onNewColumnClick } = useRow(viewId, controller, rowInfo);
|
||||
const { t } = useTranslation();
|
||||
const [unveil, setUnveil] = useState(false);
|
||||
@ -226,19 +228,21 @@ export const EditRow = ({
|
||||
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={onEditFieldClick}
|
||||
onEditOptionsClick={onEditOptionsClick}
|
||||
onEditDateClick={onEditDateClick}
|
||||
onEditCheckListClick={onEditCheckListClick}
|
||||
></EditCellWrapper>
|
||||
))}
|
||||
{cells
|
||||
.filter((cell) => databaseStore.fields[cell.cellIdentifier.fieldId].visible)
|
||||
.map((cell, cellIndex) => (
|
||||
<EditCellWrapper
|
||||
index={cellIndex}
|
||||
key={cellIndex}
|
||||
cellIdentifier={cell.cellIdentifier}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
onEditFieldClick={onEditFieldClick}
|
||||
onEditOptionsClick={onEditOptionsClick}
|
||||
onEditDateClick={onEditDateClick}
|
||||
onEditCheckListClick={onEditCheckListClick}
|
||||
></EditCellWrapper>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
@ -261,6 +265,7 @@ export const EditRow = ({
|
||||
controller={controller}
|
||||
rowInfo={rowInfo}
|
||||
onDeletePropertyClick={onDeletePropertyClick}
|
||||
onNewColumnClick={onNewColumnClick}
|
||||
></PropertiesPanel>
|
||||
</div>
|
||||
|
||||
@ -292,7 +297,7 @@ export const EditRow = ({
|
||||
cellIdentifier={editingCell}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
onOutsideClick={() => setShowChangeOptionsPopup(false)}
|
||||
onOutsideClick={() => !showEditCellOption && setShowChangeOptionsPopup(false)}
|
||||
openOptionDetail={onOpenOptionDetailClick}
|
||||
></CellOptionsPopup>
|
||||
)}
|
||||
@ -335,7 +340,7 @@ export const EditRow = ({
|
||||
cellIdentifier={editingCell}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
onOutsideClick={() => setShowCheckListPopup(false)}
|
||||
onOutsideClick={() => !showEditCheckList && setShowCheckListPopup(false)}
|
||||
openCheckListDetail={onOpenCheckListDetailClick}
|
||||
></CheckListPopup>
|
||||
)}
|
||||
|
@ -67,6 +67,7 @@ export const EditCellOptionPopup = ({
|
||||
const onDeleteOptionClick = async () => {
|
||||
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||
await svc.deleteOption([editingSelectOption]);
|
||||
onOutsideClick();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
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';
|
||||
@ -12,6 +12,9 @@ import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { MultiSelectTypeSvg } from '$app/components/_shared/svg/MultiSelectTypeSvg';
|
||||
import { DocumentSvg } from '$app/components/_shared/svg/DocumentSvg';
|
||||
import { SingleSelectTypeSvg } from '$app/components/_shared/svg/SingleSelectTypeSvg';
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { Some } from 'ts-results';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const typesOrder: FieldType[] = [
|
||||
FieldType.RichText,
|
||||
@ -29,28 +32,67 @@ export const PropertiesPanel = ({
|
||||
controller,
|
||||
rowInfo,
|
||||
onDeletePropertyClick,
|
||||
onNewColumnClick,
|
||||
}: {
|
||||
viewId: string;
|
||||
controller: DatabaseController;
|
||||
rowInfo: RowInfo;
|
||||
onDeletePropertyClick: (fieldId: string) => void;
|
||||
onNewColumnClick: (initialFieldType: FieldType, name?: string) => Promise<void>;
|
||||
}) => {
|
||||
const { cells } = useRow(viewId, controller, rowInfo);
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showAddedProperties, setShowAddedProperties] = useState(true);
|
||||
const [showBasicProperties, setShowBasicProperties] = useState(false);
|
||||
const [showAdvancedProperties, setShowAdvancedProperties] = useState(false);
|
||||
|
||||
const [hoveredPropertyIndex, setHoveredPropertyIndex] = useState(-1);
|
||||
const [hiddenProperties, setHiddenProperties] = useState<boolean[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setHiddenProperties(cells.map(() => false));
|
||||
}, [cells]);
|
||||
const toggleHideProperty = async (v: boolean, index: number) => {
|
||||
const fieldInfo = controller.fieldController.getField(cells[index].fieldId);
|
||||
if (fieldInfo) {
|
||||
const typeController = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
await typeController.initialize();
|
||||
if (fieldInfo.field.visibility) {
|
||||
await typeController.hideField();
|
||||
} else {
|
||||
await typeController.showField();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleHideProperty = (v: boolean, index: number) => {
|
||||
setHiddenProperties(hiddenProperties.map((h, i) => (i === index ? !v : h)));
|
||||
const addSelectedFieldType = async (fieldType: FieldType) => {
|
||||
let name = 'New Field';
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
name = t('grid.field.textFieldName');
|
||||
break;
|
||||
case FieldType.Number:
|
||||
name = t('grid.field.numberFieldName');
|
||||
break;
|
||||
case FieldType.DateTime:
|
||||
name = t('grid.field.dateFieldName');
|
||||
break;
|
||||
case FieldType.SingleSelect:
|
||||
name = t('grid.field.singleSelectFieldName');
|
||||
break;
|
||||
case FieldType.MultiSelect:
|
||||
name = t('grid.field.multiSelectFieldName');
|
||||
break;
|
||||
case FieldType.Checklist:
|
||||
name = t('grid.field.checklistFieldName');
|
||||
break;
|
||||
case FieldType.URL:
|
||||
name = t('grid.field.urlFieldName');
|
||||
break;
|
||||
case FieldType.Checkbox:
|
||||
name = t('grid.field.checkboxFieldName');
|
||||
break;
|
||||
}
|
||||
|
||||
await onNewColumnClick(fieldType, name);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -91,7 +133,10 @@ export const PropertiesPanel = ({
|
||||
>
|
||||
<TrashSvg></TrashSvg>
|
||||
</i>
|
||||
<Switch value={!hiddenProperties[cellIndex]} setValue={(v) => toggleHideProperty(v, cellIndex)}></Switch>
|
||||
<Switch
|
||||
value={!!databaseStore.fields[cell.cellIdentifier.fieldId]?.visible}
|
||||
setValue={(v) => toggleHideProperty(v, cellIndex)}
|
||||
></Switch>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@ -108,17 +153,17 @@ export const PropertiesPanel = ({
|
||||
<div className={'flex flex-col gap-2 text-xs'}>
|
||||
{showBasicProperties && (
|
||||
<div className={'flex flex-col'}>
|
||||
{typesOrder.map((t, i) => (
|
||||
{typesOrder.map((type, i) => (
|
||||
<button
|
||||
onClick={() => console.log('type clicked')}
|
||||
onClick={() => addSelectedFieldType(type)}
|
||||
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>
|
||||
<FieldTypeIcon fieldType={type}></FieldTypeIcon>
|
||||
</i>
|
||||
<span>
|
||||
<FieldTypeName fieldType={t}></FieldTypeName>
|
||||
<FieldTypeName fieldType={type}></FieldTypeName>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { TypeOptionController } from '../../../stores/effects/database/field/type_option/type_option_controller';
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { Some } from 'ts-results';
|
||||
import { IDatabaseField, ISelectOption } from '../../../stores/reducers/database/slice';
|
||||
import { IDatabaseField, ISelectOption } from '$app_reducers/database/slice';
|
||||
import { ChecklistTypeOptionPB, FieldType, MultiSelectTypeOptionPB, SingleSelectTypeOptionPB } from '@/services/backend';
|
||||
import {
|
||||
makeChecklistTypeOptionContext,
|
||||
@ -8,10 +8,10 @@ import {
|
||||
makeMultiSelectTypeOptionContext,
|
||||
makeNumberTypeOptionContext,
|
||||
makeSingleSelectTypeOptionContext,
|
||||
} from '../../../stores/effects/database/field/type_option/type_option_context';
|
||||
import { boardActions } from '../../../stores/reducers/board/slice';
|
||||
import { FieldInfo } from '../../../stores/effects/database/field/field_controller';
|
||||
import { AppDispatch } from '../../../stores/store';
|
||||
} from '$app/stores/effects/database/field/type_option/type_option_context';
|
||||
import { boardActions } from '$app_reducers/board/slice';
|
||||
import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
|
||||
import { AppDispatch } from '$app/stores/store';
|
||||
|
||||
export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?: AppDispatch): Promise<IDatabaseField> {
|
||||
const field = fieldInfo.field;
|
||||
@ -53,6 +53,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
fieldId: field.id,
|
||||
title: field.name,
|
||||
fieldType: field.field_type,
|
||||
visible: field.visibility,
|
||||
fieldOptions: {
|
||||
selectOptions,
|
||||
},
|
||||
@ -64,6 +65,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
return {
|
||||
fieldId: field.id,
|
||||
title: field.name,
|
||||
visible: field.visibility,
|
||||
fieldType: field.field_type,
|
||||
fieldOptions: {
|
||||
numberFormat: typeOption.format,
|
||||
@ -76,6 +78,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
return {
|
||||
fieldId: field.id,
|
||||
title: field.name,
|
||||
visible: field.visibility,
|
||||
fieldType: field.field_type,
|
||||
fieldOptions: {
|
||||
dateFormat: typeOption.date_format,
|
||||
@ -88,6 +91,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
return {
|
||||
fieldId: field.id,
|
||||
title: field.name,
|
||||
visible: field.visibility,
|
||||
fieldType: field.field_type,
|
||||
};
|
||||
}
|
||||
|
@ -2,13 +2,17 @@ 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 { DateCellDataPB, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
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>();
|
||||
const [cellController, setCellController] = useState<CellController<any, any>>();
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (!cellIdentifier || !cellCache || !fieldController) return;
|
||||
@ -19,7 +23,34 @@ export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fi
|
||||
c.subscribeChanged({
|
||||
onCellChanged: (cellData) => {
|
||||
if (cellData.some) {
|
||||
setData(cellData.val);
|
||||
const value = cellData.val;
|
||||
setData(value);
|
||||
|
||||
// update redux store for database field if there are new select options
|
||||
if (
|
||||
value instanceof SelectOptionCellDataPB &&
|
||||
(databaseStore.fields[cellIdentifier.fieldId].fieldOptions as ISelectOptionType).selectOptions.length !==
|
||||
value.options.length
|
||||
) {
|
||||
const field = { ...databaseStore.fields[cellIdentifier.fieldId] };
|
||||
const selectOptions = value.options.map((option) => ({
|
||||
selectOptionId: option.id,
|
||||
title: option.name,
|
||||
color: option.color,
|
||||
}));
|
||||
|
||||
dispatch(
|
||||
databaseActions.updateField({
|
||||
field: {
|
||||
...field,
|
||||
fieldOptions: {
|
||||
...field.fieldOptions,
|
||||
selectOptions: selectOptions,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -45,12 +45,12 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!controller) return;
|
||||
|
||||
void (async () => {
|
||||
if (!controller) return;
|
||||
controller.subscribe({
|
||||
onRowsChanged: (rowInfos) => {
|
||||
setRows(rowInfos);
|
||||
// TODO: this is a hack to make sure that the row cache is updated
|
||||
setRows([...rowInfos]);
|
||||
},
|
||||
onFieldsChanged: (fieldInfos) => {
|
||||
void loadFields(fieldInfos);
|
||||
@ -72,6 +72,10 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
setGroups(controller.groups.value);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
void controller?.dispose();
|
||||
};
|
||||
}, [controller]);
|
||||
|
||||
const onNewRowClick = async (index: number) => {
|
||||
|
@ -4,8 +4,9 @@ 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';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { None } from 'ts-results';
|
||||
|
||||
export const useRow = (viewId: string, databaseController: DatabaseController, rowInfo: RowInfo) => {
|
||||
const [cells, setCells] = useState<{ fieldId: string; cellIdentifier: CellIdentifier }[]>([]);
|
||||
@ -42,10 +43,13 @@ export const useRow = (viewId: string, databaseController: DatabaseController, r
|
||||
})();
|
||||
}, [rowController, databaseStore.columns]);
|
||||
|
||||
const onNewColumnClick = async () => {
|
||||
const onNewColumnClick = async (initialFieldType: FieldType = FieldType.RichText, name?: string) => {
|
||||
if (!databaseController) return;
|
||||
const controller = new TypeOptionController(viewId, None);
|
||||
const controller = new TypeOptionController(viewId, None, initialFieldType);
|
||||
await controller.initialize();
|
||||
if (name) {
|
||||
await controller.setFieldName(name);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -9,7 +9,7 @@ import { EditRow } from '$app/components/_shared/EditRow/EditRow';
|
||||
import { BoardToolbar } from '$app/components/board/BoardToolbar';
|
||||
|
||||
export const Board = ({ viewId, title }: { viewId: string; title: string }) => {
|
||||
const { controller, rows, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(viewId, ViewLayoutPB.Board);
|
||||
const { controller, groups, groupByFieldId, onNewRowClick, onDragEnd } = useDatabase(viewId, ViewLayoutPB.Board);
|
||||
const [showBoardRow, setShowBoardRow] = useState(false);
|
||||
const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
|
||||
|
||||
@ -38,7 +38,6 @@ export const Board = ({ viewId, title }: { viewId: string; title: string }) => {
|
||||
viewId={viewId}
|
||||
controller={controller}
|
||||
group={group}
|
||||
allRows={rows}
|
||||
groupByFieldId={groupByFieldId}
|
||||
onNewRowClick={() => onNewRowClick(index)}
|
||||
onOpenRow={onOpenRow}
|
||||
|
@ -9,6 +9,7 @@ import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { RowBackendService } from '$app/stores/effects/database/row/row_bd_svc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const BoardCard = ({
|
||||
index,
|
||||
@ -25,6 +26,7 @@ export const BoardCard = ({
|
||||
groupByFieldId: string;
|
||||
onOpenRow: (rowId: RowInfo) => void;
|
||||
}) => {
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { cells } = useRow(viewId, controller, rowInfo);
|
||||
@ -70,7 +72,9 @@ export const BoardCard = ({
|
||||
</button>
|
||||
<div className={'flex flex-col gap-3'}>
|
||||
{cells
|
||||
.filter((cell) => cell.fieldId !== groupByFieldId)
|
||||
.filter(
|
||||
(cell) => cell.fieldId !== groupByFieldId && databaseStore.fields[cell.cellIdentifier.fieldId].visible
|
||||
)
|
||||
.map((cell, cellIndex) => (
|
||||
<BoardCell
|
||||
key={cellIndex}
|
||||
|
@ -6,11 +6,11 @@ import { DatabaseController } from '$app/stores/effects/database/database_contro
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const BoardGroup = ({
|
||||
viewId,
|
||||
controller,
|
||||
allRows,
|
||||
groupByFieldId,
|
||||
onNewRowClick,
|
||||
onOpenRow,
|
||||
@ -18,7 +18,6 @@ export const BoardGroup = ({
|
||||
}: {
|
||||
viewId: string;
|
||||
controller: DatabaseController;
|
||||
allRows: readonly RowInfo[];
|
||||
groupByFieldId: string;
|
||||
onNewRowClick: () => void;
|
||||
onOpenRow: (rowId: RowInfo) => void;
|
||||
@ -26,6 +25,23 @@ export const BoardGroup = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [rows, setRows] = useState<RowInfo[]>([]);
|
||||
useEffect(() => {
|
||||
const reloadRows = () => {
|
||||
setRows(group.rows.map((rowPB) => new RowInfo(viewId, controller.fieldController.fieldInfos, rowPB)));
|
||||
};
|
||||
reloadRows();
|
||||
group.subscribe({
|
||||
onRemoveRow: reloadRows,
|
||||
onInsertRow: reloadRows,
|
||||
onUpdateRow: reloadRows,
|
||||
onCreateRow: reloadRows,
|
||||
});
|
||||
return () => {
|
||||
group.unsubscribe();
|
||||
};
|
||||
}, [controller, group, viewId]);
|
||||
|
||||
return (
|
||||
<div className={'flex h-full w-[250px] flex-col rounded-lg bg-surface-1'}>
|
||||
<div className={'flex items-center justify-between p-4'}>
|
||||
@ -49,9 +65,8 @@ export const BoardGroup = ({
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{group.rows.map((row_pb, index) => {
|
||||
const row = allRows.find((r) => r.row.id === row_pb.id);
|
||||
return row ? (
|
||||
{rows.map((row, index) => {
|
||||
return (
|
||||
<BoardCard
|
||||
viewId={viewId}
|
||||
controller={controller}
|
||||
@ -61,8 +76,6 @@ export const BoardGroup = ({
|
||||
groupByFieldId={groupByFieldId}
|
||||
onOpenRow={onOpenRow}
|
||||
></BoardCard>
|
||||
) : (
|
||||
<span key={index}></span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
@ -32,21 +32,20 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
|
||||
// Backend services
|
||||
const appBackendService = new AppBackendService(folder.id);
|
||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
|
||||
|
||||
useEffect(() => {
|
||||
void appObserver.subscribe({
|
||||
onAppChanged: (change) => {
|
||||
if (change.ok) {
|
||||
const views = change.val;
|
||||
const updatedPages: IPage[] = views.items.map((view) => ({
|
||||
id: view.id,
|
||||
folderId: view.parent_view_id,
|
||||
pageType: view.layout,
|
||||
title: view.name,
|
||||
}));
|
||||
appDispatch(pagesActions.didReceivePages(updatedPages));
|
||||
}
|
||||
onViewsChanged: async () => {
|
||||
const result = await appBackendService.getAllViews();
|
||||
if (!result.ok) return;
|
||||
const views = result.val;
|
||||
const updatedPages: IPage[] = views.map((view) => ({
|
||||
id: view.id,
|
||||
folderId: view.parent_view_id,
|
||||
pageType: view.layout,
|
||||
title: view.name,
|
||||
}));
|
||||
appDispatch(pagesActions.didReceivePages({ pages: updatedPages, folderId: folder.id }));
|
||||
},
|
||||
});
|
||||
return () => {
|
||||
@ -100,6 +99,7 @@ export const useFolderEvents = (folder: IFolder, pages: IPage[]) => {
|
||||
|
||||
const duplicateFolder = async () => {
|
||||
closePopup();
|
||||
const workspaceBackendService = new WorkspaceBackendService(workspace.id ?? '');
|
||||
const newApp = await workspaceBackendService.createApp({
|
||||
name: folder.title,
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useAppSelector } from '../../../stores/store';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { IPage } from '../../../stores/reducers/pages/slice';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { useState } from 'react';
|
||||
|
||||
@ -21,11 +21,10 @@ export const useNavigationPanelHooks = function () {
|
||||
};
|
||||
|
||||
const onPageClick = (page: IPage) => {
|
||||
let pageTypeRoute = (() => {
|
||||
const pageTypeRoute = (() => {
|
||||
switch (page.pageType) {
|
||||
case ViewLayoutPB.Document:
|
||||
return 'document';
|
||||
break;
|
||||
case ViewLayoutPB.Grid:
|
||||
return 'grid';
|
||||
case ViewLayoutPB.Board:
|
||||
|
@ -4,11 +4,11 @@ import { FolderItem } from './FolderItem';
|
||||
import { TrashButton } from './TrashButton';
|
||||
import { NewFolderButton } from './NewFolderButton';
|
||||
import { NavigationResizer } from './NavigationResizer';
|
||||
import { IFolder } from '../../../stores/reducers/folders/slice';
|
||||
import { IPage } from '../../../stores/reducers/pages/slice';
|
||||
import { IFolder } from '$app_reducers/folders/slice';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useAppSelector } from '../../../stores/store';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import {
|
||||
ANIMATION_DURATION,
|
||||
FOLDER_MARGIN,
|
||||
@ -59,7 +59,7 @@ export const NavigationPanel = ({
|
||||
let height = 0;
|
||||
for (let i = 0; i < folderIndex; i++) {
|
||||
height += INITIAL_FOLDER_HEIGHT + FOLDER_MARGIN;
|
||||
if (foldersStore[i].showPages === true) {
|
||||
if (foldersStore[i].showPages) {
|
||||
height += pagesStore.filter((p) => p.folderId === foldersStore[i].id).length * PAGE_ITEM_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useResizer } from '../../_shared/useResizer';
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { useEffect } from 'react';
|
||||
import { navigationWidthActions } from '../../../stores/reducers/navigation-width/slice';
|
||||
import { navigationWidthActions } from '$app_reducers/navigation-width/slice';
|
||||
|
||||
export const NavigationResizer = ({ minWidth }: { minWidth: number }) => {
|
||||
const width = useAppSelector((state) => state.navigationWidth);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useAppDispatch, useAppSelector } from '../../../stores/store';
|
||||
import { foldersActions } from '../../../stores/reducers/folders/slice';
|
||||
import { WorkspaceBackendService } from '../../../stores/effects/folder/workspace/workspace_bd_svc';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { foldersActions } from '$app_reducers/folders/slice';
|
||||
import { WorkspaceBackendService } from '$app/stores/effects/folder/workspace/workspace_bd_svc';
|
||||
|
||||
export const useNewFolder = () => {
|
||||
const appDispatch = useAppDispatch();
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { IPage, pagesActions } from '$app_reducers/pages/slice';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { ViewBackendService } from '$app/stores/effects/folder/view/view_bd_svc';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ViewPB } from '@/services/backend';
|
||||
@ -43,10 +42,7 @@ export const usePageEvents = (page: IPage) => {
|
||||
|
||||
const duplicatePage = async () => {
|
||||
closePopup();
|
||||
await viewBackendService.duplicate(ViewPB.fromObject({}));
|
||||
appDispatch(
|
||||
pagesActions.addPage({ id: nanoid(8), pageType: page.pageType, title: page.title, folderId: page.folderId })
|
||||
);
|
||||
await viewBackendService.duplicate(ViewPB.fromObject(page));
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
|
@ -3,7 +3,7 @@ import { BoardSvg } from '../../_shared/svg/BoardSvg';
|
||||
import { GridSvg } from '../../_shared/svg/GridSvg';
|
||||
import { Details2Svg } from '../../_shared/svg/Details2Svg';
|
||||
import { NavItemOptionsPopup } from './NavItemOptionsPopup';
|
||||
import { IPage } from '../../../stores/reducers/pages/slice';
|
||||
import { IPage } from '$app_reducers/pages/slice';
|
||||
import { Button } from '../../_shared/Button';
|
||||
import { usePageEvents } from './PageItem.hooks';
|
||||
import { RenamePopup } from './RenamePopup';
|
||||
|
@ -3,7 +3,7 @@ import { NavigationPanel } from './NavigationPanel/NavigationPanel';
|
||||
import { MainPanel } from './MainPanel';
|
||||
import { useNavigationPanelHooks } from './NavigationPanel/NavigationPanel.hooks';
|
||||
import { useWorkspace } from './Workspace.hooks';
|
||||
import { useAppSelector } from '../../stores/store';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const Screen = ({ children }: { children: ReactNode }) => {
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { foldersActions } from '../../stores/reducers/folders/slice';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/store';
|
||||
import { pagesActions } from '../../stores/reducers/pages/slice';
|
||||
import { workspaceActions } from '../../stores/reducers/workspace/slice';
|
||||
import { UserBackendService } from '../../stores/effects/user/user_bd_svc';
|
||||
import { foldersActions } from '$app_reducers/folders/slice';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { pagesActions } from '$app_reducers/pages/slice';
|
||||
import { workspaceActions } from '$app_reducers/workspace/slice';
|
||||
import { UserBackendService } from '$app/stores/effects/user/user_bd_svc';
|
||||
import { AppBackendService } from '$app/stores/effects/folder/app/app_bd_svc';
|
||||
import { Log } from '$app/utils/log';
|
||||
|
||||
export const useWorkspace = () => {
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
@ -22,10 +24,16 @@ export const useWorkspace = () => {
|
||||
const apps = workspace.views;
|
||||
for (const app of apps) {
|
||||
appDispatch(foldersActions.addFolder({ id: app.id, title: app.name }));
|
||||
|
||||
const views = app.child_views;
|
||||
for (const view of views) {
|
||||
appDispatch(pagesActions.addPage({ folderId: app.id, id: view.id, pageType: view.layout, title: view.name }));
|
||||
const service = new AppBackendService(app.id);
|
||||
const result = await service.getAllViews();
|
||||
if (result.ok) {
|
||||
for (const view of result.val) {
|
||||
appDispatch(
|
||||
pagesActions.addPage({ folderId: app.id, id: view.id, pageType: view.layout, title: view.name })
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Log.error('Failed to get views, folderId: ' + app.id);
|
||||
}
|
||||
}
|
||||
} catch (e1) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useAppSelector } from '../../stores/store';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const WorkspaceUser = () => {
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
|
@ -73,6 +73,22 @@ export class TypeOptionController {
|
||||
}
|
||||
};
|
||||
|
||||
hideField = async () => {
|
||||
if (this.fieldBackendSvc) {
|
||||
void this.fieldBackendSvc.updateField({ visibility: false });
|
||||
} else {
|
||||
throw Error('Unexpected empty field backend service');
|
||||
}
|
||||
};
|
||||
|
||||
showField = async () => {
|
||||
if (this.fieldBackendSvc) {
|
||||
void this.fieldBackendSvc.updateField({ visibility: true });
|
||||
} else {
|
||||
throw Error('Unexpected empty field backend service');
|
||||
}
|
||||
};
|
||||
|
||||
saveTypeOption = async (data: Uint8Array) => {
|
||||
if (this.typeOptionData.some) {
|
||||
this.typeOptionData.val.type_option_data = data;
|
||||
|
@ -1,40 +1,34 @@
|
||||
import { Ok, Result } from "ts-results";
|
||||
import { FlowyError, FolderNotification, RepeatedViewPB } from "@/services/backend";
|
||||
import { ChangeNotifier } from "$app/utils/change_notifier";
|
||||
import { FolderNotificationObserver } from "../notifications/observer";
|
||||
|
||||
export type AppUpdateNotifyCallback = (value: Result<RepeatedViewPB, FlowyError>) => void;
|
||||
import { FolderNotification } from '@/services/backend';
|
||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||
import { FolderNotificationObserver } from '../notifications/observer';
|
||||
|
||||
export class AppObserver {
|
||||
_appNotifier = new ChangeNotifier<Result<RepeatedViewPB, FlowyError>>();
|
||||
_viewsNotifier = new ChangeNotifier<void>();
|
||||
_listener?: FolderNotificationObserver;
|
||||
|
||||
constructor(public readonly appId: string) {
|
||||
}
|
||||
constructor(public readonly appId: string) {}
|
||||
|
||||
subscribe = async (callbacks: { onAppChanged: AppUpdateNotifyCallback }) => {
|
||||
this._appNotifier?.observer?.subscribe(callbacks.onAppChanged);
|
||||
subscribe = async (callbacks: { onViewsChanged: () => void }) => {
|
||||
this._viewsNotifier?.observer?.subscribe(callbacks.onViewsChanged);
|
||||
this._listener = new FolderNotificationObserver({
|
||||
viewId: this.appId,
|
||||
parserHandler: (notification, result) => {
|
||||
switch (notification) {
|
||||
case FolderNotification.DidUpdateWorkspaceViews:
|
||||
case FolderNotification.DidUpdateChildViews:
|
||||
if (result.ok) {
|
||||
this._appNotifier?.notify(Ok(RepeatedViewPB.deserializeBinary(result.val)));
|
||||
} else {
|
||||
this._appNotifier?.notify(result);
|
||||
this._viewsNotifier?.notify();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
await this._listener.start();
|
||||
};
|
||||
|
||||
unsubscribe = async () => {
|
||||
this._appNotifier.unsubscribe();
|
||||
this._viewsNotifier.unsubscribe();
|
||||
await this._listener?.stop();
|
||||
};
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ export interface INumberType {
|
||||
export interface IDatabaseField {
|
||||
fieldId: string;
|
||||
title: string;
|
||||
visible: boolean;
|
||||
fieldType: FieldType;
|
||||
fieldOptions?: ISelectOptionType | IDateType | INumberType;
|
||||
}
|
||||
@ -112,11 +113,10 @@ export const databaseSlice = createSlice({
|
||||
});
|
||||
},*/
|
||||
|
||||
/*updateField: (state, action: PayloadAction<{ field: IDatabaseField }>) => {
|
||||
updateField: (state, action: PayloadAction<{ field: IDatabaseField }>) => {
|
||||
const { field } = action.payload;
|
||||
|
||||
state.fields[field.fieldId] = field;
|
||||
},*/
|
||||
},
|
||||
|
||||
/*addFieldSelectOption: (state, action: PayloadAction<{ fieldId: string; option: ISelectOption }>) => {
|
||||
const { fieldId, option } = action.payload;
|
||||
|
@ -14,8 +14,8 @@ export const pagesSlice = createSlice({
|
||||
name: 'pages',
|
||||
initialState: initialState,
|
||||
reducers: {
|
||||
didReceivePages(state, action: PayloadAction<IPage[]>) {
|
||||
return action.payload;
|
||||
didReceivePages(state, action: PayloadAction<{ pages: IPage[]; folderId: string }>) {
|
||||
return state.filter((page) => page.folderId !== action.payload.folderId).concat(action.payload.pages);
|
||||
},
|
||||
addPage(state, action: PayloadAction<IPage>) {
|
||||
state.push(action.payload);
|
||||
|
Loading…
Reference in New Issue
Block a user