feat: change column visibility on add (#1174)

* feat: change column visibility on add

* refactor: extract views business logic from table
This commit is contained in:
Thaïs
2023-08-11 21:38:20 +02:00
committed by GitHub
parent e61c263b1a
commit 3978ef4edb
27 changed files with 353 additions and 466 deletions

View File

@ -2,6 +2,10 @@ import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -90,6 +94,7 @@ type OwnProps<SortField> = {
viewName: string;
viewIcon?: React.ReactNode;
availableSorts?: Array<SortType<SortField>>;
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onRowSelectionChange?: (rowSelection: string[]) => void;
useUpdateEntityMutation: any;
@ -99,6 +104,7 @@ export function EntityTable<SortField>({
viewName,
viewIcon,
availableSorts,
onColumnsChange,
onSortsUpdate,
useUpdateEntityMutation,
}: OwnProps<SortField>) {
@ -132,11 +138,12 @@ export function EntityTable<SortField>({
viewName={viewName}
viewIcon={viewIcon}
availableSorts={availableSorts}
onColumnsChange={onColumnsChange}
onSortsUpdate={onSortsUpdate}
/>
<StyledTableWrapper>
<StyledTable>
<EntityTableHeader />
<EntityTableHeader onColumnsChange={onColumnsChange} />
<EntityTableBody />
</StyledTable>
</StyledTableWrapper>

View File

@ -1,6 +1,7 @@
import { cloneElement, ComponentProps, useRef } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
@ -9,32 +10,27 @@ import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMen
import { IconPlus } from '@/ui/icon';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
import { hiddenTableColumnsState } from '../states/tableColumnsState';
const StyledColumnMenu = styled(DropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
type EntityTableColumnMenuProps = {
onAddViewField: (
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => void;
onAddColumn: (columnId: string) => void;
onClickOutside?: () => void;
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
} & ComponentProps<'div'>;
export const EntityTableColumnMenu = ({
onAddViewField,
onAddColumn,
onClickOutside = () => undefined,
viewFieldDefinitions,
...props
}: EntityTableColumnMenuProps) => {
const ref = useRef<HTMLDivElement>(null);
const theme = useTheme();
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
useListenClickOutside({
refs: [ref],
callback: onClickOutside,
@ -43,21 +39,21 @@ export const EntityTableColumnMenu = ({
return (
<StyledColumnMenu {...props} ref={ref}>
<DropdownMenuItemsContainer>
{viewFieldDefinitions.map((viewFieldDefinition) => (
{hiddenColumns.map((column) => (
<DropdownMenuItem
key={viewFieldDefinition.id}
key={column.id}
actions={
<IconButton
icon={<IconPlus size={theme.icon.size.sm} />}
onClick={() => onAddViewField(viewFieldDefinition)}
onClick={() => onAddColumn(column.id)}
/>
}
>
{viewFieldDefinition.columnIcon &&
cloneElement(viewFieldDefinition.columnIcon, {
{column.columnIcon &&
cloneElement(column.columnIcon, {
size: theme.icon.size.md,
})}
{viewFieldDefinition.columnLabel}
{column.columnLabel}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>

View File

@ -1,5 +1,4 @@
import { useCallback, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
@ -11,21 +10,14 @@ import type {
} from '@/ui/editable-field/types/ViewField';
import { IconPlus } from '@/ui/icon';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { GET_VIEW_FIELDS } from '@/views/queries/select';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
useCreateViewFieldMutation,
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import { toViewFieldInput } from '../hooks/useLoadViewFields';
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
import {
addableViewFieldDefinitionsState,
columnWidthByViewFieldIdState,
viewFieldsState,
visibleViewFieldsState,
} from '../states/viewFieldsState';
hiddenTableColumnsState,
tableColumnsByIdState,
tableColumnsState,
visibleTableColumnsState,
} from '../states/tableColumnsState';
import { ColumnHead } from './ColumnHead';
import { EntityTableColumnMenu } from './EntityTableColumnMenu';
@ -86,17 +78,18 @@ const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)`
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
export function EntityTableHeader() {
export type EntityTableHeaderProps = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
};
export function EntityTableHeader({ onColumnsChange }: EntityTableHeaderProps) {
const theme = useTheme();
const [{ objectName }, setViewFieldsState] = useRecoilState(viewFieldsState);
const currentViewId = useRecoilValue(currentViewIdState);
const viewFields = useRecoilValue(visibleViewFieldsState);
const columnWidths = useRecoilValue(columnWidthByViewFieldIdState);
const addableViewFieldDefinitions = useRecoilValue(
addableViewFieldDefinitionsState,
);
const [columns, setColumns] = useRecoilState(tableColumnsState);
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
const columnsById = useRecoilValue(tableColumnsByIdState);
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
const visibleColumns = useRecoilValue(visibleTableColumnsState);
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
number | null
@ -104,9 +97,6 @@ export function EntityTableHeader() {
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
const [createViewFieldMutation] = useCreateViewFieldMutation();
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const handleResizeHandlerStart = useCallback((positionX: number) => {
setInitialPointerPositionX(positionX);
}, []);
@ -126,37 +116,28 @@ export function EntityTableHeader() {
const nextWidth = Math.round(
Math.max(
columnWidths[resizedFieldId] +
columnsById[resizedFieldId].columnSize +
snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(),
COLUMN_MIN_WIDTH,
),
);
if (nextWidth !== columnWidths[resizedFieldId]) {
// Optimistic update to avoid "bouncing width" visual effect on resize.
setViewFieldsState((previousState) => ({
...previousState,
viewFields: previousState.viewFields.map((viewField) =>
viewField.id === resizedFieldId
? { ...viewField, columnSize: nextWidth }
: viewField,
),
}));
if (nextWidth !== columnsById[resizedFieldId].columnSize) {
const nextColumns = columns.map((column) =>
column.id === resizedFieldId
? { ...column, columnSize: nextWidth }
: column,
);
updateViewFieldMutation({
variables: {
data: { sizeInPx: nextWidth },
where: { id: resizedFieldId },
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
setColumns(nextColumns);
onColumnsChange?.(nextColumns);
}
set(resizeFieldOffsetState, 0);
setInitialPointerPositionX(null);
setResizedFieldId(null);
},
[resizedFieldId, columnWidths, setResizedFieldId],
[resizedFieldId, columnsById, setResizedFieldId],
);
useTrackPointer({
@ -170,26 +151,18 @@ export function EntityTableHeader() {
setIsColumnMenuOpen((previousValue) => !previousValue);
}, []);
const handleAddViewField = useCallback(
(viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>) => {
const handleAddColumn = useCallback(
(columnId: string) => {
setIsColumnMenuOpen(false);
if (!objectName) return;
const nextColumns = columns.map((column) =>
column.id === columnId ? { ...column, isVisible: true } : column,
);
createViewFieldMutation({
variables: {
data: {
...toViewFieldInput(objectName, {
...viewFieldDefinition,
columnOrder: viewFields.length + 1,
}),
view: { connect: { id: currentViewId } },
},
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
setColumns(nextColumns);
onColumnsChange?.(nextColumns);
},
[createViewFieldMutation, currentViewId, objectName, viewFields.length],
[columns, onColumnsChange, setColumns],
);
return (
@ -205,31 +178,31 @@ export function EntityTableHeader() {
<SelectAllCheckbox />
</th>
{viewFields.map((viewField) => (
{visibleColumns.map((column) => (
<StyledColumnHeaderCell
key={viewField.id}
isResizing={resizedFieldId === viewField.id}
key={column.id}
isResizing={resizedFieldId === column.id}
columnWidth={Math.max(
columnWidths[viewField.id] +
(resizedFieldId === viewField.id ? offset : 0),
columnsById[column.id].columnSize +
(resizedFieldId === column.id ? offset : 0),
COLUMN_MIN_WIDTH,
)}
>
<ColumnHead
viewName={viewField.columnLabel}
viewIcon={viewField.columnIcon}
viewName={column.columnLabel}
viewIcon={column.columnIcon}
/>
<StyledResizeHandler
className="cursor-col-resize"
role="separator"
onPointerDown={() => {
setResizedFieldId(viewField.id);
setResizedFieldId(column.id);
}}
/>
</StyledColumnHeaderCell>
))}
<th>
{addableViewFieldDefinitions.length > 0 && (
{hiddenColumns.length > 0 && (
<StyledAddIconButtonWrapper>
<StyledAddIconButton
size="large"
@ -238,9 +211,8 @@ export function EntityTableHeader() {
/>
{isColumnMenuOpen && (
<StyledEntityTableColumnMenu
onAddViewField={handleAddViewField}
onAddColumn={handleAddColumn}
onClickOutside={toggleColumnMenu}
viewFieldDefinitions={addableViewFieldDefinitions}
/>
)}
</StyledAddIconButtonWrapper>

View File

@ -1,8 +1,8 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { visibleTableColumnsState } from '../states/tableColumnsState';
import { ViewFieldContext } from '../states/ViewFieldContext';
import { visibleViewFieldsState } from '../states/viewFieldsState';
import { CheckboxCell } from './CheckboxCell';
import { EntityTableCell } from './EntityTableCell';
@ -13,7 +13,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
`;
export function EntityTableRow({ rowId }: { rowId: string }) {
const viewFields = useRecoilValue(visibleViewFieldsState);
const columns = useRecoilValue(visibleTableColumnsState);
return (
<StyledRow
@ -24,9 +24,9 @@ export function EntityTableRow({ rowId }: { rowId: string }) {
<td>
<CheckboxCell />
</td>
{viewFields.map((viewField, columnIndex) => {
{columns.map((column, columnIndex) => {
return (
<ViewFieldContext.Provider value={viewField} key={viewField.id}>
<ViewFieldContext.Provider value={column} key={column.id}>
<EntityTableCell cellIndex={columnIndex} />
</ViewFieldContext.Provider>
);

View File

@ -1,34 +1,22 @@
import { defaultOrderBy } from '@/people/queries';
import {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
import { useLoadViewFields } from '../hooks/useLoadViewFields';
export function GenericEntityTableData({
objectName,
useGetRequest,
getRequestResultKey,
orderBy = defaultOrderBy,
whereFilters,
viewFieldDefinitions,
filterDefinitionArray,
}: {
objectName: 'company' | 'person';
useGetRequest: any;
getRequestResultKey: string;
orderBy?: any;
whereFilters?: any;
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
filterDefinitionArray: FilterDefinition[];
}) {
const setEntityTableData = useSetEntityTableData();
useLoadViewFields({ objectName, viewFieldDefinitions });
useGetRequest({
variables: { orderBy, where: whereFilters },
onCompleted: (data: any) => {