feat: support the operations of field in the grid of tauri (#3906)

* feat: support the operations of field in the grid of tauri

* fix: performance optimizate
This commit is contained in:
Kilu.He 2023-11-13 14:16:32 +08:00 committed by GitHub
parent 251c6d22b2
commit 7867f0366e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 512 additions and 151 deletions

View File

@ -0,0 +1,16 @@
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M12.8 2H3.2C2.53726 2 2 2.55964 2 3.25V5.75C2 6.44036 2.53726 7 3.2 7H12.8C13.4627 7 14 6.44036 14 5.75V3.25C14 2.55964 13.4627 2 12.8 2Z'
stroke='currentColor'
stroke-linecap='round'
stroke-linejoin='round'
/>
<path
d='M12.8 9H3.2C2.53726 9 2 9.55964 2 10.25V12.75C2 13.4404 2.53726 14 3.2 14H12.8C13.4627 14 14 13.4404 14 12.75V10.25C14 9.55964 13.4627 9 12.8 9Z'
stroke='currentColor'
stroke-linecap='round'
stroke-linejoin='round'
/>
<circle cx='4.5' cy='4.5' r='0.5' fill='currentColor' />
<circle cx='4.5' cy='11.5' r='0.5' fill='currentColor' />
</svg>

After

Width:  |  Height:  |  Size: 788 B

View File

@ -0,0 +1,14 @@
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
<path
d='M10.5 3H11.5C11.8315 3 12.1495 3.12877 12.3839 3.35798C12.6183 3.58719 12.75 3.89807 12.75 4.22222V12.7778C12.75 13.1019 12.6183 13.4128 12.3839 13.642C12.1495 13.8712 11.8315 14 11.5 14H4.5C4.16848 14 3.85054 13.8712 3.61612 13.642C3.3817 13.4128 3.25 13.1019 3.25 12.7778V4.22222C3.25 3.89807 3.3817 3.58719 3.61612 3.35798C3.85054 3.12877 4.16848 3 4.5 3H5.5'
stroke='currentColor'
stroke-linecap='round'
stroke-linejoin='round'
/>
<path
d='M9.5 2H6.5C6.22386 2 6 2.22386 6 2.5V3.5C6 3.77614 6.22386 4 6.5 4H9.5C9.77614 4 10 3.77614 10 3.5V2.5C10 2.22386 9.77614 2 9.5 2Z'
stroke='currentColor'
stroke-linecap='round'
stroke-linejoin='round'
/>
</svg>

After

Width:  |  Height:  |  Size: 875 B

View File

@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.25 1.75H2.625C2.14175 1.75 1.75 2.14175 1.75 2.625V5.25C1.75 5.73325 2.14175 6.125 2.625 6.125H5.25C5.73325 6.125 6.125 5.73325 6.125 5.25V2.625C6.125 2.14175 5.73325 1.75 5.25 1.75Z" stroke="#333333" stroke-width="0.875" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.375 1.75H8.75C8.26675 1.75 7.875 2.14175 7.875 2.625V5.25C7.875 5.73325 8.26675 6.125 8.75 6.125H11.375C11.8582 6.125 12.25 5.73325 12.25 5.25V2.625C12.25 2.14175 11.8582 1.75 11.375 1.75Z" stroke="#333333" stroke-width="0.875" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.375 7.875H8.75C8.26675 7.875 7.875 8.26675 7.875 8.75V11.375C7.875 11.8582 8.26675 12.25 8.75 12.25H11.375C11.8582 12.25 12.25 11.8582 12.25 11.375V8.75C12.25 8.26675 11.8582 7.875 11.375 7.875Z" stroke="#333333" stroke-width="0.875" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.25 7.875H2.625C2.14175 7.875 1.75 8.26675 1.75 8.75V11.375C1.75 11.8582 2.14175 12.25 2.625 12.25H5.25C5.73325 12.25 6.125 11.8582 6.125 11.375V8.75C6.125 8.26675 5.73325 7.875 5.25 7.875Z" stroke="#333333" stroke-width="0.875" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,9 +1,9 @@
import { createContext, useContext, useCallback, useMemo, useEffect, useState, useRef } from 'react'; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { proxy, useSnapshot } from 'valtio'; import { proxy, useSnapshot } from 'valtio';
import { DatabaseLayoutPB, DatabaseNotification } from '@/services/backend'; import { DatabaseLayoutPB, DatabaseNotification, FieldVisibility } from '@/services/backend';
import { subscribeNotifications } from '$app/hooks'; import { subscribeNotifications } from '$app/hooks';
import { Database, databaseService, fieldService, rowListeners, sortListeners } from './application'; import { Database, databaseService, fieldListeners, fieldService, rowListeners, sortListeners } from './application';
export function useSelectDatabaseView({ viewId }: { viewId?: string }) { export function useSelectDatabaseView({ viewId }: { viewId?: string }) {
const key = 'v'; const key = 'v';
@ -40,6 +40,15 @@ export const DatabaseProvider = DatabaseContext.Provider;
export const useDatabase = () => useSnapshot(useContext(DatabaseContext)); export const useDatabase = () => useSnapshot(useContext(DatabaseContext));
export const useDatabaseVisibilityFields = () => {
const database = useDatabase();
return useMemo(
() => database.fields.filter((field) => field.visibility !== FieldVisibility.AlwaysHidden),
[database.fields]
);
};
export const useConnectDatabase = (viewId: string) => { export const useConnectDatabase = (viewId: string) => {
const database = useMemo(() => { const database = useMemo(() => {
const proxyDatabase = proxy<Database>({ const proxyDatabase = proxy<Database>({
@ -65,6 +74,9 @@ export const useConnectDatabase = (viewId: string) => {
[DatabaseNotification.DidUpdateFields]: async () => { [DatabaseNotification.DidUpdateFields]: async () => {
database.fields = await fieldService.getFields(viewId); database.fields = await fieldService.getFields(viewId);
}, },
[DatabaseNotification.DidUpdateFieldSettings]: async (changeset) => {
fieldListeners.didUpdateFieldSettings(database, changeset);
},
[DatabaseNotification.DidUpdateViewRows]: (changeset) => { [DatabaseNotification.DidUpdateViewRows]: (changeset) => {
rowListeners.didUpdateViewRows(database, changeset); rowListeners.didUpdateViewRows(database, changeset);
}, },

View File

@ -52,7 +52,14 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => {
selectedViewId={selectedViewId} selectedViewId={selectedViewId}
childViewIds={childViewIds} childViewIds={childViewIds}
/> />
<SwipeableViews className={'flex-1 overflow-hidden'} axis={'x'} index={index}> <SwipeableViews
slideStyle={{
overflow: 'hidden',
}}
className={'flex-1 overflow-hidden'}
axis={'x'}
index={index}
>
{childViewIds.map((id) => ( {childViewIds.map((id) => (
<TabPanel key={id} index={index} value={index}> <TabPanel key={id} index={index} value={index}>
<DatabaseLoader viewId={id}> <DatabaseLoader viewId={id}>

View File

@ -0,0 +1,11 @@
import { FieldSettingsPB } from '@/services/backend';
import { Database } from '$app/components/database/application';
export function didUpdateFieldSettings(database: Database, settings: FieldSettingsPB) {
const { field_id: fieldId, visibility, width } = settings;
const field = database.fields.find((field) => field.id === fieldId);
if (!field) return;
field.visibility = visibility;
field.width = width;
}

View File

@ -8,6 +8,9 @@ import {
MoveFieldPayloadPB, MoveFieldPayloadPB,
RepeatedFieldIdPB, RepeatedFieldIdPB,
UpdateFieldTypePayloadPB, UpdateFieldTypePayloadPB,
FieldSettingsChangesetPB,
FieldVisibility,
DatabaseViewIdPB,
} from '@/services/backend'; } from '@/services/backend';
import { import {
DatabaseEventDuplicateField, DatabaseEventDuplicateField,
@ -17,6 +20,8 @@ import {
DatabaseEventGetFields, DatabaseEventGetFields,
DatabaseEventDeleteField, DatabaseEventDeleteField,
DatabaseEventCreateTypeOption, DatabaseEventCreateTypeOption,
DatabaseEventUpdateFieldSettings,
DatabaseEventGetAllFieldSettings,
} from '@/services/backend/events/flowy-database2'; } from '@/services/backend/events/flowy-database2';
import { Field, pbToField } from './field_types'; import { Field, pbToField } from './field_types';
import { bytesToTypeOption, getTypeOption } from './type_option'; import { bytesToTypeOption, getTypeOption } from './type_option';
@ -24,20 +29,39 @@ import { bytesToTypeOption, getTypeOption } from './type_option';
export async function getFields(viewId: string, fieldIds?: string[]): Promise<Field[]> { export async function getFields(viewId: string, fieldIds?: string[]): Promise<Field[]> {
const payload = GetFieldPayloadPB.fromObject({ const payload = GetFieldPayloadPB.fromObject({
view_id: viewId, view_id: viewId,
field_ids: fieldIds ? RepeatedFieldIdPB.fromObject({ field_ids: fieldIds
items: fieldIds.map(fieldId => ({ field_id: fieldId })), ? RepeatedFieldIdPB.fromObject({
}) : undefined, items: fieldIds.map((fieldId) => ({ field_id: fieldId })),
})
: undefined,
}); });
const result = await DatabaseEventGetFields(payload); const result = await DatabaseEventGetFields(payload);
const fields = result.map((value) => value.items.map(pbToField)).unwrap(); const getSettingsPayload = DatabaseViewIdPB.fromObject({
value: viewId,
});
await Promise.all(fields.map(async field => { const settings = await DatabaseEventGetAllFieldSettings(getSettingsPayload);
if (settings.ok === false || result.ok === false) {
return Promise.reject('Failed to get fields');
}
const fields = await Promise.all(
result.val.items.map(async (item) => {
const setting = settings.val.items.find((setting) => setting.field_id === item.id);
const field = pbToField(item);
const typeOption = await getTypeOption(viewId, field.id, field.type); const typeOption = await getTypeOption(viewId, field.id, field.type);
field.typeOption = typeOption; return {
})); ...field,
visibility: setting?.visibility,
width: setting?.width,
typeOption,
};
})
);
return fields; return fields;
} }
@ -51,13 +75,14 @@ export async function createField(viewId: string, fieldType?: FieldType, data?:
const result = await DatabaseEventCreateTypeOption(payload); const result = await DatabaseEventCreateTypeOption(payload);
return result.map(value => { if (result.ok === false) {
const field = pbToField(value.field); return Promise.reject('Failed to create field');
}
field.typeOption = bytesToTypeOption(value.type_option_data, field.type); const field = pbToField(result.val.field);
field.typeOption = bytesToTypeOption(result.val.type_option_data, field.type);
return field; return field;
}).unwrap();
} }
export async function duplicateField(viewId: string, fieldId: string): Promise<void> { export async function duplicateField(viewId: string, fieldId: string): Promise<void> {
@ -68,16 +93,21 @@ export async function duplicateField(viewId: string, fieldId: string): Promise<v
const result = await DatabaseEventDuplicateField(payload); const result = await DatabaseEventDuplicateField(payload);
return result.unwrap(); if (result.ok === false) {
return Promise.reject('Failed to duplicate field');
} }
export async function updateField(viewId: string, fieldId: string, data: { return result.val;
}
export async function updateField(
viewId: string,
fieldId: string,
data: {
name?: string; name?: string;
desc?: string; desc?: string;
frozen?: boolean; }
visibility?: boolean; ): Promise<void> {
width?: number;
}): Promise<void> {
const payload = FieldChangesetPB.fromObject({ const payload = FieldChangesetPB.fromObject({
view_id: viewId, view_id: viewId,
field_id: fieldId, field_id: fieldId,
@ -124,3 +154,26 @@ export async function deleteField(viewId: string, fieldId: string): Promise<void
return result.unwrap(); return result.unwrap();
} }
export async function updateFieldSetting(
viewId: string,
fieldId: string,
settings: {
visibility?: FieldVisibility;
width?: number;
}
): Promise<void> {
const payload = FieldSettingsChangesetPB.fromObject({
view_id: viewId,
field_id: fieldId,
...settings,
});
const result = await DatabaseEventUpdateFieldSettings(payload);
if (result.ok === false) {
return Promise.reject('Failed to update field settings');
}
return result.val;
}

View File

@ -1,7 +1,4 @@
import { import { FieldPB, FieldType, FieldVisibility } from '@/services/backend';
FieldPB,
FieldType,
} from '@/services/backend';
import { DateTimeTypeOption, NumberTypeOption, SelectTypeOption } from './type_option/type_option_types'; import { DateTimeTypeOption, NumberTypeOption, SelectTypeOption } from './type_option/type_option_types';
export interface Field { export interface Field {
@ -9,8 +6,8 @@ export interface Field {
name: string; name: string;
type: FieldType; type: FieldType;
typeOption?: unknown; typeOption?: unknown;
visibility: boolean; visibility?: FieldVisibility;
width: number; width?: number;
isPrimary: boolean; isPrimary: boolean;
} }
@ -35,7 +32,5 @@ export const pbToField = (pb: FieldPB): Field => ({
id: pb.id, id: pb.id,
name: pb.name, name: pb.name,
type: pb.field_type, type: pb.field_type,
visibility: pb.visibility,
width: pb.width,
isPrimary: pb.is_primary, isPrimary: pb.is_primary,
}); });

View File

@ -2,3 +2,4 @@ export * from './select_option';
export * from './type_option'; export * from './type_option';
export * from './field_types'; export * from './field_types';
export * as fieldService from './field_service'; export * as fieldService from './field_service';
export * as fieldListeners from './field_listeners';

View File

@ -1,8 +1,9 @@
import { Popover, TextareaAutosize } from '@mui/material'; import { Popover, TextareaAutosize } from '@mui/material';
import { FC, FormEventHandler, useCallback, useLayoutEffect, useRef, useState } from 'react'; import { FC, FormEventHandler, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useViewId } from '$app/hooks'; import { useViewId } from '$app/hooks';
import { cellService, Field, TextCell as TextCellType } from '../../application'; import { cellService, Field, TextCell as TextCellType } from '../../application';
import { CellText } from '../../_shared'; import { CellText } from '../../_shared';
import { useGridUIStateDispatcher } from '$app/components/database/proxy/grid/ui_state/actions';
export const TextCell: FC<{ export const TextCell: FC<{
field: Field; field: Field;
@ -13,7 +14,7 @@ export const TextCell: FC<{
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [text, setText] = useState(''); const [text, setText] = useState('');
const [width, setWidth] = useState<number | undefined>(undefined); const [width, setWidth] = useState<number | undefined>(undefined);
const { setRowHover } = useGridUIStateDispatcher();
const handleClose = () => { const handleClose = () => {
if (!cell) return; if (!cell) return;
if (editing) { if (editing) {
@ -41,6 +42,12 @@ export const TextCell: FC<{
} }
}, [editing]); }, [editing]);
useEffect(() => {
if (editing) {
setRowHover(null);
}
}, [editing, setRowHover]);
return ( return (
<> <>
<CellText ref={cellRef} className='w-full' onClick={handleClick}> <CellText ref={cellRef} className='w-full' onClick={handleClick}>

View File

@ -1,28 +1,31 @@
import { MenuItem, Select, SelectChangeEvent, SelectProps } from '@mui/material'; import { MenuItem, Select, SelectChangeEvent, SelectProps } from '@mui/material';
import { FC, useCallback } from 'react'; import { FC, useCallback } from 'react';
import { Field as FieldType } from '../../application'; import { Field as FieldType } from '../../application';
import { useDatabase } from '../../Database.hooks'; import { useDatabaseVisibilityFields } from '../../Database.hooks';
import { Field } from './Field'; import { Field } from './Field';
export interface FieldSelectProps extends Omit<SelectProps, 'onChange'> { export interface FieldSelectProps extends Omit<SelectProps, 'onChange'> {
onChange?: (event: SelectChangeEvent<unknown>, field: FieldType | undefined) => void; onChange?: (event: SelectChangeEvent<unknown>, field: FieldType | undefined) => void;
} }
export const FieldSelect: FC<FieldSelectProps> = ({ export const FieldSelect: FC<FieldSelectProps> = ({ onChange, ...props }) => {
onChange, const fields = useDatabaseVisibilityFields();
...props
}) => {
const { fields } = useDatabase();
const handleChange = useCallback((event: SelectChangeEvent<unknown>) => { const handleChange = useCallback(
(event: SelectChangeEvent<unknown>) => {
const selectedId = event.target.value; const selectedId = event.target.value;
onChange?.(event, fields.find(field => field.id === selectedId)); onChange?.(
}, [onChange, fields]); event,
fields.find((field) => field.id === selectedId)
);
},
[onChange, fields]
);
return ( return (
<Select onChange={handleChange} {...props}> <Select onChange={handleChange} {...props}>
{fields.map(field => ( {fields.map((field) => (
<MenuItem key={field.id} value={field.id}> <MenuItem key={field.id} value={field.id}>
<Field field={field} /> <Field field={field} />
</MenuItem> </MenuItem>

View File

@ -1,7 +1,7 @@
import { Menu, MenuItem, MenuProps } from '@mui/material'; import { Menu, MenuItem, MenuProps } from '@mui/material';
import { FC, MouseEvent } from 'react'; import { FC, MouseEvent } from 'react';
import { Field as FieldType } from '../../application'; import { Field as FieldType } from '../../application';
import { useDatabase } from '../../Database.hooks'; import { useDatabaseVisibilityFields } from '../../Database.hooks';
import { Field } from './Field'; import { Field } from './Field';
export interface FieldsMenuProps extends MenuProps { export interface FieldsMenuProps extends MenuProps {
@ -9,7 +9,7 @@ export interface FieldsMenuProps extends MenuProps {
} }
export const FieldsMenu: FC<FieldsMenuProps> = ({ onMenuItemClick, ...props }) => { export const FieldsMenu: FC<FieldsMenuProps> = ({ onMenuItemClick, ...props }) => {
const { fields } = useDatabase(); const fields = useDatabaseVisibilityFields();
return ( return (
<Menu {...props}> <Menu {...props}>

View File

@ -1,8 +1,12 @@
import { FC, useEffect } from 'react'; import { FC, FunctionComponent, SVGProps, useEffect } from 'react';
import { ViewTabs, ViewTab } from './ViewTabs'; import { ViewTabs, ViewTab } from './ViewTabs';
import { useAppSelector } from '$app/stores/store'; import { useAppSelector } from '$app/stores/store';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import AddViewBtn from '$app/components/database/components/tab_bar/AddViewBtn'; import AddViewBtn from '$app/components/database/components/tab_bar/AddViewBtn';
import { ViewLayoutPB } from '@/services/backend';
import { ReactComponent as GridSvg } from '$app/assets/grid.svg';
import { ReactComponent as BoardSvg } from '$app/assets/board.svg';
import { ReactComponent as DocumentSvg } from '$app/assets/document.svg';
export interface DatabaseTabBarProps { export interface DatabaseTabBarProps {
childViewIds: string[]; childViewIds: string[];
@ -11,6 +15,15 @@ export interface DatabaseTabBarProps {
pageId: string; pageId: string;
} }
const DatabaseIcons: {
[key in ViewLayoutPB]: FunctionComponent<SVGProps<SVGSVGElement> & { title?: string | undefined }>;
} = {
[ViewLayoutPB.Document]: DocumentSvg,
[ViewLayoutPB.Grid]: GridSvg,
[ViewLayoutPB.Board]: BoardSvg,
[ViewLayoutPB.Calendar]: GridSvg,
};
export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViewIds, selectedViewId, setSelectedViewId }) => { export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViewIds, selectedViewId, setSelectedViewId }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const views = useAppSelector((state) => { const views = useAppSelector((state) => {
@ -33,16 +46,20 @@ export const DatabaseTabBar: FC<DatabaseTabBarProps> = ({ pageId, childViewIds,
<div className='-mb-px flex items-center border-b border-line-divider'> <div className='-mb-px flex items-center border-b border-line-divider'>
<div className='flex flex-1 items-center'> <div className='flex flex-1 items-center'>
<ViewTabs value={selectedViewId} onChange={handleChange}> <ViewTabs value={selectedViewId} onChange={handleChange}>
{views.map((view) => ( {views.map((view) => {
const Icon = DatabaseIcons[view.layout];
return (
<ViewTab <ViewTab
key={view.id} key={view.id}
icon={undefined} icon={<Icon />}
iconPosition='start' iconPosition='start'
color='inherit' color='inherit'
label={view.name || t('grid.title.placeholder')} label={view.name || t('grid.title.placeholder')}
value={view.id} value={view.id}
/> />
))} );
})}
</ViewTabs> </ViewTabs>
<AddViewBtn pageId={pageId} /> <AddViewBtn pageId={pageId} />
</div> </div>

View File

@ -0,0 +1,30 @@
import React from 'react';
import { useDatabase } from '$app/components/database';
import { Field } from '$app/components/database/application';
import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow';
interface Props {
field: Field;
index: number;
}
function GridCalculate({ field, index }: Props) {
const { rowMetas } = useDatabase();
const count = rowMetas.length;
const width = field.width ?? DEFAULT_FIELD_WIDTH;
return (
<div
style={{
width,
visibility: index === 0 ? 'visible' : 'hidden',
}}
className={'flex justify-end'}
>
<span className={'mr-2 text-text-caption'}>Count</span>
<span>{count}</span>
</div>
);
}
export default GridCalculate;

View File

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

View File

@ -7,6 +7,8 @@ import { fieldService, Field } from '../../application';
import { useDatabase } from '../../Database.hooks'; import { useDatabase } from '../../Database.hooks';
import { FieldTypeSvg } from './FieldTypeSvg'; import { FieldTypeSvg } from './FieldTypeSvg';
import { GridFieldMenu } from './GridFieldMenu'; import { GridFieldMenu } from './GridFieldMenu';
import GridResizer from '$app/components/database/grid/GridField/GridResizer';
import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow';
export interface GridFieldProps { export interface GridFieldProps {
field: Field; field: Field;
@ -18,6 +20,7 @@ export const GridField: FC<GridFieldProps> = ({ field }) => {
const [openMenu, setOpenMenu] = useState(false); const [openMenu, setOpenMenu] = useState(false);
const [openTooltip, setOpenTooltip] = useState(false); const [openTooltip, setOpenTooltip] = useState(false);
const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before); const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before);
const [fieldWidth, setFieldWidth] = useState(field.width || DEFAULT_FIELD_WIDTH);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setOpenMenu(true); setOpenMenu(true);
@ -89,7 +92,12 @@ export const GridField: FC<GridFieldProps> = ({ field }) => {
}); });
return ( return (
<> <div
className={'flex border-r border-line-divider'}
style={{
width: fieldWidth,
}}
>
<Tooltip <Tooltip
open={openTooltip && !isDragging} open={openTooltip && !isDragging}
title={field.name} title={field.name}
@ -104,6 +112,11 @@ export const GridField: FC<GridFieldProps> = ({ field }) => {
ref={setPreviewRef} ref={setPreviewRef}
className='relative flex w-full items-center px-2' className='relative flex w-full items-center px-2'
disableRipple disableRipple
onContextMenu={(event) => {
event.stopPropagation();
event.preventDefault();
handleClick();
}}
onClick={handleClick} onClick={handleClick}
{...attributes} {...attributes}
{...listeners} {...listeners}
@ -118,11 +131,12 @@ export const GridField: FC<GridFieldProps> = ({ field }) => {
}`} }`}
/> />
)} )}
<GridResizer field={field} onWidthChange={(width) => setFieldWidth(width)} />
</Button> </Button>
</Tooltip> </Tooltip>
{openMenu && ( {openMenu && (
<GridFieldMenu field={field} open={openMenu} anchorEl={previewRef.current} onClose={handleMenuClose} /> <GridFieldMenu field={field} open={openMenu} anchorEl={previewRef.current} onClose={handleMenuClose} />
)} )}
</> </div>
); );
}; };

View File

@ -7,20 +7,14 @@ import { FieldTypeSvg } from './FieldTypeSvg';
import { FieldTypeText } from './FieldTypeText'; import { FieldTypeText } from './FieldTypeText';
import { GridFieldMenuActions } from './GridFieldMenuActions'; import { GridFieldMenuActions } from './GridFieldMenuActions';
export interface GridFieldMenuProps { export interface GridFieldMenuProps {
field: Field; field: Field;
anchorEl: MenuProps['anchorEl']; anchorEl: MenuProps['anchorEl'];
open: boolean; open: boolean;
onClose: MenuProps['onClose']; onClose: () => void;
} }
export const GridFieldMenu: FC<GridFieldMenuProps> = ({ export const GridFieldMenu: FC<GridFieldMenuProps> = ({ field, anchorEl, open, onClose }) => {
field,
anchorEl,
open,
onClose,
}) => {
const viewId = useViewId(); const viewId = useViewId();
const [inputtingName, setInputtingName] = useState(field.name); const [inputtingName, setInputtingName] = useState(field.name);
@ -43,8 +37,8 @@ export const GridFieldMenu: FC<GridFieldMenuProps> = ({
const fieldNameInput = ( const fieldNameInput = (
<OutlinedInput <OutlinedInput
className="mx-3 mt-1 mb-5 !rounded-[10px]" className='mx-3 mb-5 mt-1 !rounded-[10px]'
size="small" size='small'
value={inputtingName} value={inputtingName}
onChange={handleInput} onChange={handleInput}
onBlur={handleBlur} onBlur={handleBlur}
@ -53,24 +47,27 @@ export const GridFieldMenu: FC<GridFieldMenuProps> = ({
const fieldTypeSelect = ( const fieldTypeSelect = (
<MenuItem dense> <MenuItem dense>
<FieldTypeSvg type={field.type} className="text-base mr-2" /> <FieldTypeSvg type={field.type} className='mr-2 text-base' />
<span className="flex-1 text-xs font-medium"> <span className='flex-1 text-xs font-medium'>{FieldTypeText(field.type)}</span>
{FieldTypeText(field.type)} <MoreSvg className='text-base' />
</span>
<MoreSvg className="text-base" />
</MenuItem> </MenuItem>
); );
const isPrimary = field.isPrimary;
return ( return (
<Menu <>
anchorEl={anchorEl} <Menu anchorEl={anchorEl} open={open} onClose={onClose}>
open={open}
onClose={onClose}
>
{fieldNameInput} {fieldNameInput}
{!isPrimary && (
<>
{fieldTypeSelect} {fieldTypeSelect}
<Divider /> <Divider />
<GridFieldMenuActions /> </>
)}
<GridFieldMenuActions isPrimary={isPrimary} onMenuItemClick={() => onClose()} fieldId={field.id} />
</Menu> </Menu>
</>
); );
}; };

View File

@ -6,6 +6,11 @@ import { ReactComponent as CopySvg } from '$app/assets/copy.svg';
import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
import { ReactComponent as LeftSvg } from '$app/assets/left.svg'; import { ReactComponent as LeftSvg } from '$app/assets/left.svg';
import { ReactComponent as RightSvg } from '$app/assets/right.svg'; import { ReactComponent as RightSvg } from '$app/assets/right.svg';
import { fieldService } from '$app/components/database/application';
import { FieldVisibility } from '@/services/backend';
import { useViewId } from '$app/hooks';
import ConfirmDialog from '$app/components/_shared/app-dialog/ConfirmDialog';
import { useState } from 'react';
enum FieldAction { enum FieldAction {
Hide = 'hide', Hide = 'hide',
@ -25,26 +30,79 @@ const FieldActionSvgMap = {
const TwoColumnActions: FieldAction[][] = [ const TwoColumnActions: FieldAction[][] = [
[FieldAction.Hide, FieldAction.Duplicate, FieldAction.Delete], [FieldAction.Hide, FieldAction.Duplicate, FieldAction.Delete],
[FieldAction.InsertLeft, FieldAction.InsertRight], // [FieldAction.InsertLeft, FieldAction.InsertRight],
]; ];
export const GridFieldMenuActions = () => { // prevent default actions for primary fields
return ( const primaryPreventDefaultActions = [FieldAction.Delete, FieldAction.Duplicate];
<Grid container spacing={2}>
{TwoColumnActions.map((column, index) => ( interface GridFieldMenuActionsProps {
<Grid key={index} item xs={6}> fieldId: string;
{column.map(action => { isPrimary?: boolean;
const ActionSvg = FieldActionSvgMap[action]; onMenuItemClick?: (action: FieldAction) => void;
}
export const GridFieldMenuActions = ({ fieldId, onMenuItemClick, isPrimary }: GridFieldMenuActionsProps) => {
const viewId = useViewId();
const [openConfirm, setOpenConfirm] = useState(false);
const handleOpenConfirm = () => {
setOpenConfirm(true);
};
const handleMenuItemClick = async (action: FieldAction) => {
const preventDefault = isPrimary && primaryPreventDefaultActions.includes(action);
if (preventDefault) {
return;
}
switch (action) {
case FieldAction.Hide:
await fieldService.updateFieldSetting(viewId, fieldId, {
visibility: FieldVisibility.AlwaysHidden,
});
break;
case FieldAction.Duplicate:
await fieldService.duplicateField(viewId, fieldId);
break;
case FieldAction.Delete:
handleOpenConfirm();
return;
}
onMenuItemClick?.(action);
};
return ( return (
<MenuItem key={action} dense> <Grid container columns={TwoColumnActions.length} spacing={2}>
<ActionSvg className="mr-2 text-base" /> {TwoColumnActions.map((column, index) => (
<Grid key={index} item xs={6}>
{column.map((action) => {
const ActionSvg = FieldActionSvgMap[action];
const disabled = isPrimary && primaryPreventDefaultActions.includes(action);
return (
<MenuItem disabled={disabled} onClick={() => handleMenuItemClick(action)} key={action} dense>
<ActionSvg className='mr-2 text-base' />
{t(`grid.field.${action}`)} {t(`grid.field.${action}`)}
</MenuItem> </MenuItem>
); );
})} })}
</Grid> </Grid>
))} ))}
<ConfirmDialog
open={openConfirm}
subtitle={''}
title={t('grid.field.deleteFieldPromptMessage')}
onOk={async () => {
await fieldService.deleteField(viewId, fieldId);
}}
onClose={() => {
setOpenConfirm(false);
onMenuItemClick?.(FieldAction.Delete);
}}
/>
</Grid> </Grid>
); );
}; };

View File

@ -0,0 +1,92 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Field, fieldService } from '$app/components/database/application';
import { useViewId } from '$app/hooks';
interface GridResizerProps {
field: Field;
onWidthChange?: (width: number) => void;
}
const minWidth = 100;
function GridResizer({ field, onWidthChange }: GridResizerProps) {
const viewId = useViewId();
const fieldId = field.id;
const width = field.width || 0;
const [isResizing, setIsResizing] = useState(false);
const [newWidth, setNewWidth] = useState(width);
const [hover, setHover] = useState(false);
const startX = useRef(0);
const onResize = useCallback(
(e: MouseEvent) => {
const diff = e.clientX - startX.current;
const newWidth = width + diff;
if (newWidth < minWidth) {
return;
}
setNewWidth(newWidth);
},
[width]
);
useEffect(() => {
onWidthChange?.(newWidth);
}, [newWidth, onWidthChange]);
useEffect(() => {
if (!isResizing && width !== newWidth) {
void fieldService.updateFieldSetting(viewId, fieldId, {
width: newWidth,
});
}
}, [fieldId, isResizing, newWidth, viewId, width]);
const onResizeEnd = useCallback(() => {
setIsResizing(false);
document.removeEventListener('mousemove', onResize);
document.removeEventListener('mouseup', onResizeEnd);
}, [onResize]);
const onResizeStart = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
startX.current = e.clientX;
setIsResizing(true);
document.addEventListener('mousemove', onResize);
document.addEventListener('mouseup', onResizeEnd);
},
[onResize, onResizeEnd]
);
return (
<div
onMouseDown={onResizeStart}
onClick={(e) => {
e.stopPropagation();
}}
onMouseEnter={() => {
setHover(true);
}}
onMouseLeave={() => {
setHover(false);
}}
style={{
right: `-3px`,
}}
className={'absolute top-0 z-10 h-full cursor-col-resize'}
>
<div
className={'h-full w-[6px] select-none bg-transparent'}
style={{
backgroundColor: hover || isResizing ? 'var(--content-on-fill-hover)' : 'transparent',
}}
></div>
</div>
);
}
export default GridResizer;

View File

@ -1,3 +1,17 @@
export const GridCalculateRow = () => { import React from 'react';
return null; import { useDatabaseVisibilityFields } from '$app/components/database';
}; import GridCalculate from '$app/components/database/grid/GridCalculate/GridCalculate';
function GridCalculateRow() {
const fields = useDatabaseVisibilityFields();
return (
<div className='flex grow items-center'>
{fields.map((field, index) => {
return <GridCalculate index={index} key={field.id} field={field} />;
})}
</div>
);
}
export default GridCalculateRow;

View File

@ -14,6 +14,12 @@ export function useGridRowActionsDisplay(rowId: string, ref: React.RefObject<HTM
setRowHover(rowId); setRowHover(rowId);
}, [setRowHover, rowId]); }, [setRowHover, rowId]);
const onMouseLeave = useCallback(() => {
if (hover) {
setRowHover(null);
}
}, [setRowHover, hover]);
useEffect(() => { useEffect(() => {
// Next frame to avoid layout thrashing // Next frame to avoid layout thrashing
requestAnimationFrame(() => { requestAnimationFrame(() => {
@ -37,6 +43,7 @@ export function useGridRowActionsDisplay(rowId: string, ref: React.RefObject<HTM
return { return {
actionsStyle, actionsStyle,
onMouseEnter, onMouseEnter,
onMouseLeave,
hover, hover,
}; };
} }

View File

@ -3,7 +3,7 @@ import { Portal } from '@mui/material';
import { DragEventHandler, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DragEventHandler, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { throttle } from '$app/utils/tool'; import { throttle } from '$app/utils/tool';
import { useViewId } from '$app/hooks'; import { useViewId } from '$app/hooks';
import { useDatabase } from '../../../Database.hooks'; import { useDatabaseVisibilityFields } from '../../../Database.hooks';
import { rowService, RowMeta } from '../../../application'; import { rowService, RowMeta } from '../../../application';
import { import {
DragItem, DragItem,
@ -21,6 +21,7 @@ import {
useGridRowContextMenu, useGridRowContextMenu,
} from '$app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks'; } from '$app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks';
import GridCellRowContextMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu'; import GridCellRowContextMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu';
import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow';
export interface GridCellRowProps { export interface GridCellRowProps {
rowMeta: RowMeta; rowMeta: RowMeta;
@ -32,14 +33,14 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre
const rowId = rowMeta.id; const rowId = rowMeta.id;
const viewId = useViewId(); const viewId = useViewId();
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const { onMouseEnter, actionsStyle, hover } = useGridRowActionsDisplay(rowId, ref); const { onMouseLeave, onMouseEnter, actionsStyle, hover } = useGridRowActionsDisplay(rowId, ref);
const { const {
isContextMenuOpen, isContextMenuOpen,
closeContextMenu, closeContextMenu,
openContextMenu, openContextMenu,
position: contextMenuPosition, position: contextMenuPosition,
} = useGridRowContextMenu(); } = useGridRowContextMenu();
const { fields } = useDatabase(); const fields = useDatabaseVisibilityFields();
const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before); const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before);
const dragData = useMemo( const dragData = useMemo(
@ -106,7 +107,7 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre
}, [openContextMenu]); }, [openContextMenu]);
return ( return (
<div ref={ref} className='flex grow' onMouseEnter={onMouseEnter} {...dropListeners}> <div ref={ref} className='flex grow' onMouseLeave={onMouseLeave} onMouseEnter={onMouseEnter} {...dropListeners}>
<div <div
ref={setPreviewRef} ref={setPreviewRef}
className={`relative flex grow border-b border-line-divider ${isDragging ? 'bg-blue-50' : ''}`} className={`relative flex grow border-b border-line-divider ${isDragging ? 'bg-blue-50' : ''}`}
@ -117,7 +118,7 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre
virtualizer={virtualizer} virtualizer={virtualizer}
renderItem={(index) => <GridCell rowId={rowMeta.id} field={fields[index]} />} renderItem={(index) => <GridCell rowId={rowMeta.id} field={fields[index]} />}
/> />
<div className='min-w-20 grow' /> <div className={`w-[${DEFAULT_FIELD_WIDTH}px]`} />
{isOver && ( {isOver && (
<div <div
className={`absolute left-0 right-0 z-10 h-0.5 bg-blue-500 ${ className={`absolute left-0 right-0 z-10 h-0.5 bg-blue-500 ${
@ -137,6 +138,7 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre
rowId={rowMeta.id} rowId={rowMeta.id}
getPrevRowId={getPrevRowId} getPrevRowId={getPrevRowId}
/> />
{isContextMenuOpen && (
<GridCellRowContextMenu <GridCellRowContextMenu
open={isContextMenuOpen} open={isContextMenuOpen}
onClose={closeContextMenu} onClose={closeContextMenu}
@ -144,6 +146,7 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre
rowId={rowId} rowId={rowId}
getPrevRowId={getPrevRowId} getPrevRowId={getPrevRowId}
/> />
)}
</Portal> </Portal>
</div> </div>
); );

View File

@ -1,36 +1,31 @@
import { Virtualizer } from '@tanstack/react-virtual';
import { FC } from 'react';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { FieldType } from '@/services/backend'; import { FieldType } from '@/services/backend';
import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import { ReactComponent as AddSvg } from '$app/assets/add.svg';
import { fieldService } from '../../application'; import { fieldService } from '../../application';
import { useDatabase } from '../../Database.hooks'; import { useDatabaseVisibilityFields } from '../../Database.hooks';
import { VirtualizedList } from '../../_shared';
import { GridField } from '../GridField'; import { GridField } from '../GridField';
import { useViewId } from '@/appflowy_app/hooks'; import { useViewId } from '@/appflowy_app/hooks';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow/constants';
export interface GridFieldRowProps { export const GridFieldRow = () => {
virtualizer: Virtualizer<HTMLDivElement, HTMLDivElement>;
}
export const GridFieldRow: FC<GridFieldRowProps> = ({ virtualizer }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const viewId = useViewId(); const viewId = useViewId();
const { fields } = useDatabase(); const fields = useDatabaseVisibilityFields();
const handleClick = async () => { const handleClick = async () => {
await fieldService.createField(viewId, FieldType.RichText); await fieldService.createField(viewId, FieldType.RichText);
}; };
return ( return (
<div className='z-10 flex border-b border-line-divider'> <div className='z-10 flex border-b border-line-divider'>
<VirtualizedList <div className={'flex'}>
className='flex' {fields.map((field) => {
virtualizer={virtualizer} return <GridField key={field.id} field={field} />;
itemClassName='flex border-r border-line-divider' })}
renderItem={(index) => <GridField field={fields[index]} />} </div>
/>
<div className='min-w-20 grow'> <div className={`w-[${DEFAULT_FIELD_WIDTH}px]`}>
<Button <Button
color={'inherit'} color={'inherit'}
className='flex h-full w-full items-center justify-start whitespace-nowrap text-left' className='flex h-full w-full items-center justify-start whitespace-nowrap text-left'

View File

@ -2,9 +2,9 @@ import { Virtualizer } from '@tanstack/react-virtual';
import { FC } from 'react'; import { FC } from 'react';
import { RenderRow, RenderRowType } from './constants'; import { RenderRow, RenderRowType } from './constants';
import { GridCellRow } from './GridCellRow'; import { GridCellRow } from './GridCellRow';
import { GridFieldRow } from './GridFieldRow';
import { GridNewRow } from './GridNewRow'; import { GridNewRow } from './GridNewRow';
import { GridCalculateRow } from './GridCalculateRow'; import { GridFieldRow } from '$app/components/database/grid/GridRow/GridFieldRow';
import GridCalculateRow from '$app/components/database/grid/GridRow/GridCalculateRow';
export interface GridRowProps { export interface GridRowProps {
row: RenderRow; row: RenderRow;
@ -14,13 +14,13 @@ export interface GridRowProps {
export const GridRow: FC<GridRowProps> = ({ row, virtualizer, getPrevRowId }) => { export const GridRow: FC<GridRowProps> = ({ row, virtualizer, getPrevRowId }) => {
switch (row.type) { switch (row.type) {
case RenderRowType.Fields:
return <GridFieldRow />;
case RenderRowType.Row: case RenderRowType.Row:
return <GridCellRow rowMeta={row.data.meta} virtualizer={virtualizer} getPrevRowId={getPrevRowId} />; return <GridCellRow rowMeta={row.data.meta} virtualizer={virtualizer} getPrevRowId={getPrevRowId} />;
case RenderRowType.Fields:
return <GridFieldRow virtualizer={virtualizer} />;
case RenderRowType.NewRow: case RenderRowType.NewRow:
return <GridNewRow startRowId={row.data.startRowId} groupId={row.data.groupId} />; return <GridNewRow startRowId={row.data.startRowId} groupId={row.data.groupId} />;
case RenderRowType.Calculate: case RenderRowType.CalculateRow:
return <GridCalculateRow />; return <GridCalculateRow />;
default: default:
return null; return null;

View File

@ -1,10 +1,18 @@
import { RowMeta } from '../../application'; import { RowMeta } from '../../application';
export const GridCalculateCountHeight = 40;
export const DEFAULT_FIELD_WIDTH = 150;
export enum RenderRowType { export enum RenderRowType {
Fields = 'fields', Fields = 'fields',
Row = 'row', Row = 'row',
NewRow = 'new-row', NewRow = 'new-row',
Calculate = 'calculate', CalculateRow = 'calculate-row',
}
export interface CalculateRenderRow {
type: RenderRowType.CalculateRow;
} }
export interface FieldRenderRow { export interface FieldRenderRow {
@ -26,10 +34,6 @@ export interface NewRenderRow {
}; };
} }
export interface CalculateRenderRow {
type: RenderRowType.Calculate;
}
export type RenderRow = FieldRenderRow | CellRenderRow | NewRenderRow | CalculateRenderRow; export type RenderRow = FieldRenderRow | CellRenderRow | NewRenderRow | CalculateRenderRow;
export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => { export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => {
@ -50,7 +54,7 @@ export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => {
}, },
}, },
{ {
type: RenderRowType.Calculate, type: RenderRowType.CalculateRow,
}, },
]; ];
}; };

View File

@ -1,9 +1,9 @@
import { useVirtualizer } from '@tanstack/react-virtual'; import { useVirtualizer } from '@tanstack/react-virtual';
import { FC, useMemo, useRef } from 'react'; import { FC, useMemo, useRef } from 'react';
import { RowMeta } from '../../application'; import { RowMeta } from '../../application';
import { useDatabase } from '../../Database.hooks'; import { useDatabase, useDatabaseVisibilityFields } from '../../Database.hooks';
import { VirtualizedList } from '../../_shared'; import { VirtualizedList } from '../../_shared';
import { GridRow, RenderRow, RenderRowType, rowMetasToRenderRow } from '../GridRow'; import { DEFAULT_FIELD_WIDTH, GridRow, RenderRow, RenderRowType, rowMetasToRenderRow } from '../GridRow';
const getRenderRowKey = (row: RenderRow) => { const getRenderRowKey = (row: RenderRow) => {
if (row.type === RenderRowType.Row) { if (row.type === RenderRowType.Row) {
@ -16,9 +16,9 @@ const getRenderRowKey = (row: RenderRow) => {
export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => { export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => {
const verticalScrollElementRef = useRef<HTMLDivElement | null>(null); const verticalScrollElementRef = useRef<HTMLDivElement | null>(null);
const horizontalScrollElementRef = useRef<HTMLDivElement | null>(null); const horizontalScrollElementRef = useRef<HTMLDivElement | null>(null);
const { rowMetas, fields } = useDatabase(); const { rowMetas } = useDatabase();
const renderRows = useMemo<RenderRow[]>(() => rowMetasToRenderRow(rowMetas as RowMeta[]), [rowMetas]); const renderRows = useMemo<RenderRow[]>(() => rowMetasToRenderRow(rowMetas as RowMeta[]), [rowMetas]);
const fields = useDatabaseVisibilityFields();
const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLDivElement>({ const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLDivElement>({
count: renderRows.length, count: renderRows.length,
overscan: 20, overscan: 20,
@ -33,7 +33,9 @@ export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => {
overscan: 5, overscan: 5,
getItemKey: (i) => fields[i].id, getItemKey: (i) => fields[i].id,
getScrollElement: () => horizontalScrollElementRef.current, getScrollElement: () => horizontalScrollElementRef.current,
estimateSize: (i) => fields[i].width ?? 201, estimateSize: (i) => {
return fields[i].width ?? DEFAULT_FIELD_WIDTH;
},
}); });
const getPrevRowId = (id: string) => { const getPrevRowId = (id: string) => {

View File

@ -8,7 +8,7 @@ import { useSelection } from '$app/components/document/_shared/EditorHooks/useSe
import { useAppSelector } from '$app/stores/store'; import { useAppSelector } from '$app/stores/store';
import { ThemeMode } from '$app/interfaces'; import { ThemeMode } from '$app/interfaces';
export default function CodeBlock({ export default React.memo(function CodeBlock({
node, node,
placeholder, placeholder,
...props ...props
@ -40,4 +40,4 @@ export default function CodeBlock({
/> />
</div> </div>
); );
} });

View File

@ -84,4 +84,4 @@ function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> })
); );
} }
export default EquationBlock; export default React.memo(EquationBlock);

View File

@ -33,4 +33,4 @@ function GridBlock({ node }: { node: NestedBlock<BlockType.GridBlock> }) {
); );
} }
export default GridBlock; export default React.memo(GridBlock);

View File

@ -77,4 +77,4 @@ function ImageBlock({ node }: { node: NestedBlock<BlockType.ImageBlock> }) {
); );
} }
export default ImageBlock; export default React.memo(ImageBlock);

View File

@ -11,4 +11,4 @@ function NodeChildren({ childIds, ...props }: { childIds?: string[] } & React.HT
) : null; ) : null;
} }
export default NodeChildren; export default React.memo(NodeChildren);

View File

@ -96,7 +96,7 @@ function NodeComponent({ id, ...props }: { id: string } & React.HTMLAttributes<H
); );
} }
const NodeWithErrorBoundary = withErrorBoundary(NodeComponent, { const NodeWithErrorBoundary = withErrorBoundary(React.memo(NodeComponent), {
FallbackComponent: ErrorBoundaryFallbackComponent, FallbackComponent: ErrorBoundaryFallbackComponent,
}); });
@ -110,4 +110,4 @@ const UnSupportedBlock = () => {
); );
}; };
export default React.memo(NodeWithErrorBoundary); export default NodeWithErrorBoundary;

View File

@ -27,8 +27,8 @@ function Root({ documentData }: { documentData: DocumentData }) {
); );
} }
const RootWithErrorBoundary = withErrorBoundary(Root, { const RootWithErrorBoundary = withErrorBoundary(React.memo(Root), {
FallbackComponent: ErrorBoundaryFallbackComponent, FallbackComponent: ErrorBoundaryFallbackComponent,
}); });
export default React.memo(RootWithErrorBoundary); export default RootWithErrorBoundary;

View File

@ -14,6 +14,7 @@ import {
RowsChangePB, RowsChangePB,
RowsVisibilityChangePB, RowsVisibilityChangePB,
SortChangesetNotificationPB, SortChangesetNotificationPB,
FieldSettingsPB,
} from '@/services/backend'; } from '@/services/backend';
const NotificationPBMap = { const NotificationPBMap = {
@ -28,6 +29,7 @@ const NotificationPBMap = {
[DatabaseNotification.DidUpdateField]: FieldPB, [DatabaseNotification.DidUpdateField]: FieldPB,
[DatabaseNotification.DidUpdateCell]: null, [DatabaseNotification.DidUpdateCell]: null,
[DatabaseNotification.DidUpdateSort]: SortChangesetNotificationPB, [DatabaseNotification.DidUpdateSort]: SortChangesetNotificationPB,
[DatabaseNotification.DidUpdateFieldSettings]: FieldSettingsPB,
}; };
type NotificationMap = typeof NotificationPBMap; type NotificationMap = typeof NotificationPBMap;