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 { 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);
|
||||||
},
|
},
|
||||||
|
@ -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}>
|
||||||
|
@ -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,
|
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;
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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';
|
||||||
|
@ -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}>
|
||||||
|
@ -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>
|
||||||
|
@ -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}>
|
||||||
|
@ -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>
|
||||||
|
@ -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 { 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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 = () => {
|
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;
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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'
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
@ -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;
|
) : 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,
|
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,
|
FallbackComponent: ErrorBoundaryFallbackComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default React.memo(RootWithErrorBoundary);
|
export default RootWithErrorBoundary;
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user