mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
251c6d22b2
commit
7867f0366e
16
frontend/appflowy_tauri/src/appflowy_app/assets/board.svg
Normal file
16
frontend/appflowy_tauri/src/appflowy_app/assets/board.svg
Normal 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 |
14
frontend/appflowy_tauri/src/appflowy_app/assets/document.svg
Normal file
14
frontend/appflowy_tauri/src/appflowy_app/assets/document.svg
Normal 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 |
6
frontend/appflowy_tauri/src/appflowy_app/assets/grid.svg
Normal file
6
frontend/appflowy_tauri/src/appflowy_app/assets/grid.svg
Normal 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 |
@ -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 { proxy, useSnapshot } from 'valtio';
|
||||
import { DatabaseLayoutPB, DatabaseNotification } from '@/services/backend';
|
||||
import { DatabaseLayoutPB, DatabaseNotification, FieldVisibility } from '@/services/backend';
|
||||
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 }) {
|
||||
const key = 'v';
|
||||
@ -40,6 +40,15 @@ export const DatabaseProvider = DatabaseContext.Provider;
|
||||
|
||||
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) => {
|
||||
const database = useMemo(() => {
|
||||
const proxyDatabase = proxy<Database>({
|
||||
@ -65,6 +74,9 @@ export const useConnectDatabase = (viewId: string) => {
|
||||
[DatabaseNotification.DidUpdateFields]: async () => {
|
||||
database.fields = await fieldService.getFields(viewId);
|
||||
},
|
||||
[DatabaseNotification.DidUpdateFieldSettings]: async (changeset) => {
|
||||
fieldListeners.didUpdateFieldSettings(database, changeset);
|
||||
},
|
||||
[DatabaseNotification.DidUpdateViewRows]: (changeset) => {
|
||||
rowListeners.didUpdateViewRows(database, changeset);
|
||||
},
|
||||
|
@ -52,7 +52,14 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => {
|
||||
selectedViewId={selectedViewId}
|
||||
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) => (
|
||||
<TabPanel key={id} index={index} value={index}>
|
||||
<DatabaseLoader viewId={id}>
|
||||
|
@ -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;
|
||||
}
|
@ -8,6 +8,9 @@ import {
|
||||
MoveFieldPayloadPB,
|
||||
RepeatedFieldIdPB,
|
||||
UpdateFieldTypePayloadPB,
|
||||
FieldSettingsChangesetPB,
|
||||
FieldVisibility,
|
||||
DatabaseViewIdPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventDuplicateField,
|
||||
@ -17,6 +20,8 @@ import {
|
||||
DatabaseEventGetFields,
|
||||
DatabaseEventDeleteField,
|
||||
DatabaseEventCreateTypeOption,
|
||||
DatabaseEventUpdateFieldSettings,
|
||||
DatabaseEventGetAllFieldSettings,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { Field, pbToField } from './field_types';
|
||||
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[]> {
|
||||
const payload = GetFieldPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_ids: fieldIds ? RepeatedFieldIdPB.fromObject({
|
||||
items: fieldIds.map(fieldId => ({ field_id: fieldId })),
|
||||
}) : undefined,
|
||||
field_ids: fieldIds
|
||||
? RepeatedFieldIdPB.fromObject({
|
||||
items: fieldIds.map((fieldId) => ({ field_id: fieldId })),
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventGetFields(payload);
|
||||
|
||||
const fields = result.map((value) => value.items.map(pbToField)).unwrap();
|
||||
const getSettingsPayload = DatabaseViewIdPB.fromObject({
|
||||
value: viewId,
|
||||
});
|
||||
|
||||
await Promise.all(fields.map(async field => {
|
||||
const typeOption = await getTypeOption(viewId, field.id, field.type);
|
||||
const settings = await DatabaseEventGetAllFieldSettings(getSettingsPayload);
|
||||
|
||||
field.typeOption = typeOption;
|
||||
}));
|
||||
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);
|
||||
|
||||
return {
|
||||
...field,
|
||||
visibility: setting?.visibility,
|
||||
width: setting?.width,
|
||||
typeOption,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return fields;
|
||||
}
|
||||
@ -51,13 +75,14 @@ export async function createField(viewId: string, fieldType?: FieldType, data?:
|
||||
|
||||
const result = await DatabaseEventCreateTypeOption(payload);
|
||||
|
||||
return result.map(value => {
|
||||
const field = pbToField(value.field);
|
||||
if (result.ok === false) {
|
||||
return Promise.reject('Failed to create field');
|
||||
}
|
||||
|
||||
field.typeOption = bytesToTypeOption(value.type_option_data, field.type);
|
||||
const field = pbToField(result.val.field);
|
||||
|
||||
return field;
|
||||
}).unwrap();
|
||||
field.typeOption = bytesToTypeOption(result.val.type_option_data, field.type);
|
||||
return field;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return result.unwrap();
|
||||
if (result.ok === false) {
|
||||
return Promise.reject('Failed to duplicate field');
|
||||
}
|
||||
|
||||
return result.val;
|
||||
}
|
||||
|
||||
export async function updateField(viewId: string, fieldId: string, data: {
|
||||
name?: string;
|
||||
desc?: string;
|
||||
frozen?: boolean;
|
||||
visibility?: boolean;
|
||||
width?: number;
|
||||
}): Promise<void> {
|
||||
export async function updateField(
|
||||
viewId: string,
|
||||
fieldId: string,
|
||||
data: {
|
||||
name?: string;
|
||||
desc?: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const payload = FieldChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
@ -124,3 +154,26 @@ export async function deleteField(viewId: string, fieldId: string): Promise<void
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
import {
|
||||
FieldPB,
|
||||
FieldType,
|
||||
} from '@/services/backend';
|
||||
import { FieldPB, FieldType, FieldVisibility } from '@/services/backend';
|
||||
import { DateTimeTypeOption, NumberTypeOption, SelectTypeOption } from './type_option/type_option_types';
|
||||
|
||||
export interface Field {
|
||||
@ -9,8 +6,8 @@ export interface Field {
|
||||
name: string;
|
||||
type: FieldType;
|
||||
typeOption?: unknown;
|
||||
visibility: boolean;
|
||||
width: number;
|
||||
visibility?: FieldVisibility;
|
||||
width?: number;
|
||||
isPrimary: boolean;
|
||||
}
|
||||
|
||||
@ -35,7 +32,5 @@ export const pbToField = (pb: FieldPB): Field => ({
|
||||
id: pb.id,
|
||||
name: pb.name,
|
||||
type: pb.field_type,
|
||||
visibility: pb.visibility,
|
||||
width: pb.width,
|
||||
isPrimary: pb.is_primary,
|
||||
});
|
||||
|
@ -2,3 +2,4 @@ export * from './select_option';
|
||||
export * from './type_option';
|
||||
export * from './field_types';
|
||||
export * as fieldService from './field_service';
|
||||
export * as fieldListeners from './field_listeners';
|
||||
|
@ -1,8 +1,9 @@
|
||||
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 { cellService, Field, TextCell as TextCellType } from '../../application';
|
||||
import { CellText } from '../../_shared';
|
||||
import { useGridUIStateDispatcher } from '$app/components/database/proxy/grid/ui_state/actions';
|
||||
|
||||
export const TextCell: FC<{
|
||||
field: Field;
|
||||
@ -13,7 +14,7 @@ export const TextCell: FC<{
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [text, setText] = useState('');
|
||||
const [width, setWidth] = useState<number | undefined>(undefined);
|
||||
|
||||
const { setRowHover } = useGridUIStateDispatcher();
|
||||
const handleClose = () => {
|
||||
if (!cell) return;
|
||||
if (editing) {
|
||||
@ -41,6 +42,12 @@ export const TextCell: FC<{
|
||||
}
|
||||
}, [editing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) {
|
||||
setRowHover(null);
|
||||
}
|
||||
}, [editing, setRowHover]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CellText ref={cellRef} className='w-full' onClick={handleClick}>
|
||||
|
@ -1,28 +1,31 @@
|
||||
import { MenuItem, Select, SelectChangeEvent, SelectProps } from '@mui/material';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { Field as FieldType } from '../../application';
|
||||
import { useDatabase } from '../../Database.hooks';
|
||||
import { useDatabaseVisibilityFields } from '../../Database.hooks';
|
||||
import { Field } from './Field';
|
||||
|
||||
export interface FieldSelectProps extends Omit<SelectProps, 'onChange'> {
|
||||
onChange?: (event: SelectChangeEvent<unknown>, field: FieldType | undefined) => void;
|
||||
}
|
||||
|
||||
export const FieldSelect: FC<FieldSelectProps> = ({
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const { fields } = useDatabase();
|
||||
export const FieldSelect: FC<FieldSelectProps> = ({ onChange, ...props }) => {
|
||||
const fields = useDatabaseVisibilityFields();
|
||||
|
||||
const handleChange = useCallback((event: SelectChangeEvent<unknown>) => {
|
||||
const selectedId = event.target.value;
|
||||
const handleChange = useCallback(
|
||||
(event: SelectChangeEvent<unknown>) => {
|
||||
const selectedId = event.target.value;
|
||||
|
||||
onChange?.(event, fields.find(field => field.id === selectedId));
|
||||
}, [onChange, fields]);
|
||||
onChange?.(
|
||||
event,
|
||||
fields.find((field) => field.id === selectedId)
|
||||
);
|
||||
},
|
||||
[onChange, fields]
|
||||
);
|
||||
|
||||
return (
|
||||
<Select onChange={handleChange} {...props}>
|
||||
{fields.map(field => (
|
||||
{fields.map((field) => (
|
||||
<MenuItem key={field.id} value={field.id}>
|
||||
<Field field={field} />
|
||||
</MenuItem>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Menu, MenuItem, MenuProps } from '@mui/material';
|
||||
import { FC, MouseEvent } from 'react';
|
||||
import { Field as FieldType } from '../../application';
|
||||
import { useDatabase } from '../../Database.hooks';
|
||||
import { useDatabaseVisibilityFields } from '../../Database.hooks';
|
||||
import { Field } from './Field';
|
||||
|
||||
export interface FieldsMenuProps extends MenuProps {
|
||||
@ -9,7 +9,7 @@ export interface FieldsMenuProps extends MenuProps {
|
||||
}
|
||||
|
||||
export const FieldsMenu: FC<FieldsMenuProps> = ({ onMenuItemClick, ...props }) => {
|
||||
const { fields } = useDatabase();
|
||||
const fields = useDatabaseVisibilityFields();
|
||||
|
||||
return (
|
||||
<Menu {...props}>
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { FC, useEffect } from 'react';
|
||||
import { FC, FunctionComponent, SVGProps, useEffect } from 'react';
|
||||
import { ViewTabs, ViewTab } from './ViewTabs';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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 {
|
||||
childViewIds: string[];
|
||||
@ -11,6 +15,15 @@ export interface DatabaseTabBarProps {
|
||||
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 }) => {
|
||||
const { t } = useTranslation();
|
||||
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='flex flex-1 items-center'>
|
||||
<ViewTabs value={selectedViewId} onChange={handleChange}>
|
||||
{views.map((view) => (
|
||||
<ViewTab
|
||||
key={view.id}
|
||||
icon={undefined}
|
||||
iconPosition='start'
|
||||
color='inherit'
|
||||
label={view.name || t('grid.title.placeholder')}
|
||||
value={view.id}
|
||||
/>
|
||||
))}
|
||||
{views.map((view) => {
|
||||
const Icon = DatabaseIcons[view.layout];
|
||||
|
||||
return (
|
||||
<ViewTab
|
||||
key={view.id}
|
||||
icon={<Icon />}
|
||||
iconPosition='start'
|
||||
color='inherit'
|
||||
label={view.name || t('grid.title.placeholder')}
|
||||
value={view.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ViewTabs>
|
||||
<AddViewBtn pageId={pageId} />
|
||||
</div>
|
||||
|
@ -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;
|
@ -0,0 +1 @@
|
||||
export * from './GridCalculate';
|
@ -7,6 +7,8 @@ import { fieldService, Field } from '../../application';
|
||||
import { useDatabase } from '../../Database.hooks';
|
||||
import { FieldTypeSvg } from './FieldTypeSvg';
|
||||
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 {
|
||||
field: Field;
|
||||
@ -18,6 +20,7 @@ export const GridField: FC<GridFieldProps> = ({ field }) => {
|
||||
const [openMenu, setOpenMenu] = useState(false);
|
||||
const [openTooltip, setOpenTooltip] = useState(false);
|
||||
const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before);
|
||||
const [fieldWidth, setFieldWidth] = useState(field.width || DEFAULT_FIELD_WIDTH);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setOpenMenu(true);
|
||||
@ -89,7 +92,12 @@ export const GridField: FC<GridFieldProps> = ({ field }) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={'flex border-r border-line-divider'}
|
||||
style={{
|
||||
width: fieldWidth,
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
open={openTooltip && !isDragging}
|
||||
title={field.name}
|
||||
@ -104,6 +112,11 @@ export const GridField: FC<GridFieldProps> = ({ field }) => {
|
||||
ref={setPreviewRef}
|
||||
className='relative flex w-full items-center px-2'
|
||||
disableRipple
|
||||
onContextMenu={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handleClick();
|
||||
}}
|
||||
onClick={handleClick}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
@ -118,11 +131,12 @@ export const GridField: FC<GridFieldProps> = ({ field }) => {
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
<GridResizer field={field} onWidthChange={(width) => setFieldWidth(width)} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{openMenu && (
|
||||
<GridFieldMenu field={field} open={openMenu} anchorEl={previewRef.current} onClose={handleMenuClose} />
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -7,20 +7,14 @@ import { FieldTypeSvg } from './FieldTypeSvg';
|
||||
import { FieldTypeText } from './FieldTypeText';
|
||||
import { GridFieldMenuActions } from './GridFieldMenuActions';
|
||||
|
||||
|
||||
export interface GridFieldMenuProps {
|
||||
field: Field;
|
||||
anchorEl: MenuProps['anchorEl'];
|
||||
open: boolean;
|
||||
onClose: MenuProps['onClose'];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const GridFieldMenu: FC<GridFieldMenuProps> = ({
|
||||
field,
|
||||
anchorEl,
|
||||
open,
|
||||
onClose,
|
||||
}) => {
|
||||
export const GridFieldMenu: FC<GridFieldMenuProps> = ({ field, anchorEl, open, onClose }) => {
|
||||
const viewId = useViewId();
|
||||
const [inputtingName, setInputtingName] = useState(field.name);
|
||||
|
||||
@ -43,8 +37,8 @@ export const GridFieldMenu: FC<GridFieldMenuProps> = ({
|
||||
|
||||
const fieldNameInput = (
|
||||
<OutlinedInput
|
||||
className="mx-3 mt-1 mb-5 !rounded-[10px]"
|
||||
size="small"
|
||||
className='mx-3 mb-5 mt-1 !rounded-[10px]'
|
||||
size='small'
|
||||
value={inputtingName}
|
||||
onChange={handleInput}
|
||||
onBlur={handleBlur}
|
||||
@ -53,24 +47,27 @@ export const GridFieldMenu: FC<GridFieldMenuProps> = ({
|
||||
|
||||
const fieldTypeSelect = (
|
||||
<MenuItem dense>
|
||||
<FieldTypeSvg type={field.type} className="text-base mr-2" />
|
||||
<span className="flex-1 text-xs font-medium">
|
||||
{FieldTypeText(field.type)}
|
||||
</span>
|
||||
<MoreSvg className="text-base" />
|
||||
<FieldTypeSvg type={field.type} className='mr-2 text-base' />
|
||||
<span className='flex-1 text-xs font-medium'>{FieldTypeText(field.type)}</span>
|
||||
<MoreSvg className='text-base' />
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const isPrimary = field.isPrimary;
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
{fieldNameInput}
|
||||
{fieldTypeSelect}
|
||||
<Divider />
|
||||
<GridFieldMenuActions />
|
||||
</Menu>
|
||||
<>
|
||||
<Menu anchorEl={anchorEl} open={open} onClose={onClose}>
|
||||
{fieldNameInput}
|
||||
{!isPrimary && (
|
||||
<>
|
||||
{fieldTypeSelect}
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<GridFieldMenuActions isPrimary={isPrimary} onMenuItemClick={() => onClose()} fieldId={field.id} />
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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 LeftSvg } from '$app/assets/left.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 {
|
||||
Hide = 'hide',
|
||||
@ -25,26 +30,79 @@ const FieldActionSvgMap = {
|
||||
|
||||
const TwoColumnActions: FieldAction[][] = [
|
||||
[FieldAction.Hide, FieldAction.Duplicate, FieldAction.Delete],
|
||||
[FieldAction.InsertLeft, FieldAction.InsertRight],
|
||||
// [FieldAction.InsertLeft, FieldAction.InsertRight],
|
||||
];
|
||||
|
||||
export const GridFieldMenuActions = () => {
|
||||
// prevent default actions for primary fields
|
||||
const primaryPreventDefaultActions = [FieldAction.Delete, FieldAction.Duplicate];
|
||||
|
||||
interface GridFieldMenuActionsProps {
|
||||
fieldId: string;
|
||||
isPrimary?: boolean;
|
||||
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 (
|
||||
<Grid container spacing={2}>
|
||||
<Grid container columns={TwoColumnActions.length} spacing={2}>
|
||||
{TwoColumnActions.map((column, index) => (
|
||||
<Grid key={index} item xs={6}>
|
||||
{column.map(action => {
|
||||
{column.map((action) => {
|
||||
const ActionSvg = FieldActionSvgMap[action];
|
||||
const disabled = isPrimary && primaryPreventDefaultActions.includes(action);
|
||||
|
||||
return (
|
||||
<MenuItem key={action} dense>
|
||||
<ActionSvg className="mr-2 text-base" />
|
||||
<MenuItem disabled={disabled} onClick={() => handleMenuItemClick(action)} key={action} dense>
|
||||
<ActionSvg className='mr-2 text-base' />
|
||||
{t(`grid.field.${action}`)}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
))}
|
||||
<ConfirmDialog
|
||||
open={openConfirm}
|
||||
subtitle={''}
|
||||
title={t('grid.field.deleteFieldPromptMessage')}
|
||||
onOk={async () => {
|
||||
await fieldService.deleteField(viewId, fieldId);
|
||||
}}
|
||||
onClose={() => {
|
||||
setOpenConfirm(false);
|
||||
onMenuItemClick?.(FieldAction.Delete);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
@ -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;
|
@ -1,3 +1,17 @@
|
||||
export const GridCalculateRow = () => {
|
||||
return null;
|
||||
};
|
||||
import React from 'react';
|
||||
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;
|
||||
|
@ -14,6 +14,12 @@ export function useGridRowActionsDisplay(rowId: string, ref: React.RefObject<HTM
|
||||
setRowHover(rowId);
|
||||
}, [setRowHover, rowId]);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
if (hover) {
|
||||
setRowHover(null);
|
||||
}
|
||||
}, [setRowHover, hover]);
|
||||
|
||||
useEffect(() => {
|
||||
// Next frame to avoid layout thrashing
|
||||
requestAnimationFrame(() => {
|
||||
@ -37,6 +43,7 @@ export function useGridRowActionsDisplay(rowId: string, ref: React.RefObject<HTM
|
||||
return {
|
||||
actionsStyle,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
hover,
|
||||
};
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { Portal } from '@mui/material';
|
||||
import { DragEventHandler, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { throttle } from '$app/utils/tool';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { useDatabase } from '../../../Database.hooks';
|
||||
import { useDatabaseVisibilityFields } from '../../../Database.hooks';
|
||||
import { rowService, RowMeta } from '../../../application';
|
||||
import {
|
||||
DragItem,
|
||||
@ -21,6 +21,7 @@ import {
|
||||
useGridRowContextMenu,
|
||||
} from '$app/components/database/grid/GridRow/GridCellRow/GridCellRow.hooks';
|
||||
import GridCellRowContextMenu from '$app/components/database/grid/GridRow/GridCellRow/GridCellRowContextMenu';
|
||||
import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow';
|
||||
|
||||
export interface GridCellRowProps {
|
||||
rowMeta: RowMeta;
|
||||
@ -32,14 +33,14 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre
|
||||
const rowId = rowMeta.id;
|
||||
const viewId = useViewId();
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const { onMouseEnter, actionsStyle, hover } = useGridRowActionsDisplay(rowId, ref);
|
||||
const { onMouseLeave, onMouseEnter, actionsStyle, hover } = useGridRowActionsDisplay(rowId, ref);
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
closeContextMenu,
|
||||
openContextMenu,
|
||||
position: contextMenuPosition,
|
||||
} = useGridRowContextMenu();
|
||||
const { fields } = useDatabase();
|
||||
const fields = useDatabaseVisibilityFields();
|
||||
|
||||
const [dropPosition, setDropPosition] = useState<DropPosition>(DropPosition.Before);
|
||||
const dragData = useMemo(
|
||||
@ -106,7 +107,7 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre
|
||||
}, [openContextMenu]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className='flex grow' onMouseEnter={onMouseEnter} {...dropListeners}>
|
||||
<div ref={ref} className='flex grow' onMouseLeave={onMouseLeave} onMouseEnter={onMouseEnter} {...dropListeners}>
|
||||
<div
|
||||
ref={setPreviewRef}
|
||||
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}
|
||||
renderItem={(index) => <GridCell rowId={rowMeta.id} field={fields[index]} />}
|
||||
/>
|
||||
<div className='min-w-20 grow' />
|
||||
<div className={`w-[${DEFAULT_FIELD_WIDTH}px]`} />
|
||||
{isOver && (
|
||||
<div
|
||||
className={`absolute left-0 right-0 z-10 h-0.5 bg-blue-500 ${
|
||||
@ -137,13 +138,15 @@ export const GridCellRow: FC<GridCellRowProps> = ({ rowMeta, virtualizer, getPre
|
||||
rowId={rowMeta.id}
|
||||
getPrevRowId={getPrevRowId}
|
||||
/>
|
||||
<GridCellRowContextMenu
|
||||
open={isContextMenuOpen}
|
||||
onClose={closeContextMenu}
|
||||
anchorPosition={contextMenuPosition}
|
||||
rowId={rowId}
|
||||
getPrevRowId={getPrevRowId}
|
||||
/>
|
||||
{isContextMenuOpen && (
|
||||
<GridCellRowContextMenu
|
||||
open={isContextMenuOpen}
|
||||
onClose={closeContextMenu}
|
||||
anchorPosition={contextMenuPosition}
|
||||
rowId={rowId}
|
||||
getPrevRowId={getPrevRowId}
|
||||
/>
|
||||
)}
|
||||
</Portal>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,36 +1,31 @@
|
||||
import { Virtualizer } from '@tanstack/react-virtual';
|
||||
import { FC } from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
|
||||
import { fieldService } from '../../application';
|
||||
import { useDatabase } from '../../Database.hooks';
|
||||
import { VirtualizedList } from '../../_shared';
|
||||
import { useDatabaseVisibilityFields } from '../../Database.hooks';
|
||||
import { GridField } from '../GridField';
|
||||
import { useViewId } from '@/appflowy_app/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DEFAULT_FIELD_WIDTH } from '$app/components/database/grid/GridRow/constants';
|
||||
|
||||
export interface GridFieldRowProps {
|
||||
virtualizer: Virtualizer<HTMLDivElement, HTMLDivElement>;
|
||||
}
|
||||
|
||||
export const GridFieldRow: FC<GridFieldRowProps> = ({ virtualizer }) => {
|
||||
export const GridFieldRow = () => {
|
||||
const { t } = useTranslation();
|
||||
const viewId = useViewId();
|
||||
const { fields } = useDatabase();
|
||||
const fields = useDatabaseVisibilityFields();
|
||||
|
||||
const handleClick = async () => {
|
||||
await fieldService.createField(viewId, FieldType.RichText);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='z-10 flex border-b border-line-divider'>
|
||||
<VirtualizedList
|
||||
className='flex'
|
||||
virtualizer={virtualizer}
|
||||
itemClassName='flex border-r border-line-divider'
|
||||
renderItem={(index) => <GridField field={fields[index]} />}
|
||||
/>
|
||||
<div className='min-w-20 grow'>
|
||||
<div className={'flex'}>
|
||||
{fields.map((field) => {
|
||||
return <GridField key={field.id} field={field} />;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className={`w-[${DEFAULT_FIELD_WIDTH}px]`}>
|
||||
<Button
|
||||
color={'inherit'}
|
||||
className='flex h-full w-full items-center justify-start whitespace-nowrap text-left'
|
||||
|
@ -2,9 +2,9 @@ import { Virtualizer } from '@tanstack/react-virtual';
|
||||
import { FC } from 'react';
|
||||
import { RenderRow, RenderRowType } from './constants';
|
||||
import { GridCellRow } from './GridCellRow';
|
||||
import { GridFieldRow } from './GridFieldRow';
|
||||
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 {
|
||||
row: RenderRow;
|
||||
@ -14,13 +14,13 @@ export interface GridRowProps {
|
||||
|
||||
export const GridRow: FC<GridRowProps> = ({ row, virtualizer, getPrevRowId }) => {
|
||||
switch (row.type) {
|
||||
case RenderRowType.Fields:
|
||||
return <GridFieldRow />;
|
||||
case RenderRowType.Row:
|
||||
return <GridCellRow rowMeta={row.data.meta} virtualizer={virtualizer} getPrevRowId={getPrevRowId} />;
|
||||
case RenderRowType.Fields:
|
||||
return <GridFieldRow virtualizer={virtualizer} />;
|
||||
case RenderRowType.NewRow:
|
||||
return <GridNewRow startRowId={row.data.startRowId} groupId={row.data.groupId} />;
|
||||
case RenderRowType.Calculate:
|
||||
case RenderRowType.CalculateRow:
|
||||
return <GridCalculateRow />;
|
||||
default:
|
||||
return null;
|
||||
|
@ -1,10 +1,18 @@
|
||||
import { RowMeta } from '../../application';
|
||||
|
||||
export const GridCalculateCountHeight = 40;
|
||||
|
||||
export const DEFAULT_FIELD_WIDTH = 150;
|
||||
|
||||
export enum RenderRowType {
|
||||
Fields = 'fields',
|
||||
Row = 'row',
|
||||
NewRow = 'new-row',
|
||||
Calculate = 'calculate',
|
||||
CalculateRow = 'calculate-row',
|
||||
}
|
||||
|
||||
export interface CalculateRenderRow {
|
||||
type: RenderRowType.CalculateRow;
|
||||
}
|
||||
|
||||
export interface FieldRenderRow {
|
||||
@ -26,10 +34,6 @@ export interface NewRenderRow {
|
||||
};
|
||||
}
|
||||
|
||||
export interface CalculateRenderRow {
|
||||
type: RenderRowType.Calculate;
|
||||
}
|
||||
|
||||
export type RenderRow = FieldRenderRow | CellRenderRow | NewRenderRow | CalculateRenderRow;
|
||||
|
||||
export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => {
|
||||
@ -50,7 +54,7 @@ export const rowMetasToRenderRow = (rowMetas: RowMeta[]): RenderRow[] => {
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RenderRowType.Calculate,
|
||||
type: RenderRowType.CalculateRow,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { FC, useMemo, useRef } from 'react';
|
||||
import { RowMeta } from '../../application';
|
||||
import { useDatabase } from '../../Database.hooks';
|
||||
import { useDatabase, useDatabaseVisibilityFields } from '../../Database.hooks';
|
||||
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) => {
|
||||
if (row.type === RenderRowType.Row) {
|
||||
@ -16,9 +16,9 @@ const getRenderRowKey = (row: RenderRow) => {
|
||||
export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => {
|
||||
const verticalScrollElementRef = 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 fields = useDatabaseVisibilityFields();
|
||||
const rowVirtualizer = useVirtualizer<HTMLDivElement, HTMLDivElement>({
|
||||
count: renderRows.length,
|
||||
overscan: 20,
|
||||
@ -33,7 +33,9 @@ export const GridTable: FC<{ tableHeight: number }> = ({ tableHeight }) => {
|
||||
overscan: 5,
|
||||
getItemKey: (i) => fields[i].id,
|
||||
getScrollElement: () => horizontalScrollElementRef.current,
|
||||
estimateSize: (i) => fields[i].width ?? 201,
|
||||
estimateSize: (i) => {
|
||||
return fields[i].width ?? DEFAULT_FIELD_WIDTH;
|
||||
},
|
||||
});
|
||||
|
||||
const getPrevRowId = (id: string) => {
|
||||
|
@ -8,7 +8,7 @@ import { useSelection } from '$app/components/document/_shared/EditorHooks/useSe
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { ThemeMode } from '$app/interfaces';
|
||||
|
||||
export default function CodeBlock({
|
||||
export default React.memo(function CodeBlock({
|
||||
node,
|
||||
placeholder,
|
||||
...props
|
||||
@ -40,4 +40,4 @@ export default function CodeBlock({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -84,4 +84,4 @@ function EquationBlock({ node }: { node: NestedBlock<BlockType.EquationBlock> })
|
||||
);
|
||||
}
|
||||
|
||||
export default EquationBlock;
|
||||
export default React.memo(EquationBlock);
|
||||
|
@ -33,4 +33,4 @@ function GridBlock({ node }: { node: NestedBlock<BlockType.GridBlock> }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default GridBlock;
|
||||
export default React.memo(GridBlock);
|
||||
|
@ -77,4 +77,4 @@ function ImageBlock({ node }: { node: NestedBlock<BlockType.ImageBlock> }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageBlock;
|
||||
export default React.memo(ImageBlock);
|
||||
|
@ -11,4 +11,4 @@ function NodeChildren({ childIds, ...props }: { childIds?: string[] } & React.HT
|
||||
) : null;
|
||||
}
|
||||
|
||||
export default NodeChildren;
|
||||
export default React.memo(NodeChildren);
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
@ -110,4 +110,4 @@ const UnSupportedBlock = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(NodeWithErrorBoundary);
|
||||
export default NodeWithErrorBoundary;
|
||||
|
@ -27,8 +27,8 @@ function Root({ documentData }: { documentData: DocumentData }) {
|
||||
);
|
||||
}
|
||||
|
||||
const RootWithErrorBoundary = withErrorBoundary(Root, {
|
||||
const RootWithErrorBoundary = withErrorBoundary(React.memo(Root), {
|
||||
FallbackComponent: ErrorBoundaryFallbackComponent,
|
||||
});
|
||||
|
||||
export default React.memo(RootWithErrorBoundary);
|
||||
export default RootWithErrorBoundary;
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
RowsChangePB,
|
||||
RowsVisibilityChangePB,
|
||||
SortChangesetNotificationPB,
|
||||
FieldSettingsPB,
|
||||
} from '@/services/backend';
|
||||
|
||||
const NotificationPBMap = {
|
||||
@ -28,6 +29,7 @@ const NotificationPBMap = {
|
||||
[DatabaseNotification.DidUpdateField]: FieldPB,
|
||||
[DatabaseNotification.DidUpdateCell]: null,
|
||||
[DatabaseNotification.DidUpdateSort]: SortChangesetNotificationPB,
|
||||
[DatabaseNotification.DidUpdateFieldSettings]: FieldSettingsPB,
|
||||
};
|
||||
|
||||
type NotificationMap = typeof NotificationPBMap;
|
||||
|
Loading…
Reference in New Issue
Block a user