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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 353 additions and 466 deletions

View File

@ -3128,13 +3128,6 @@ export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }
export type DeleteUserAccountMutation = { __typename?: 'Mutation', deleteUserAccount: { __typename?: 'User', id: string } }; export type DeleteUserAccountMutation = { __typename?: 'Mutation', deleteUserAccount: { __typename?: 'User', id: string } };
export type CreateViewFieldMutationVariables = Exact<{
data: ViewFieldCreateInput;
}>;
export type CreateViewFieldMutation = { __typename?: 'Mutation', createOneViewField: { __typename?: 'ViewField', id: string, fieldName: string, isVisible: boolean, sizeInPx: number, index: number } };
export type CreateViewFieldsMutationVariables = Exact<{ export type CreateViewFieldsMutationVariables = Exact<{
data: Array<ViewFieldCreateManyInput> | ViewFieldCreateManyInput; data: Array<ViewFieldCreateManyInput> | ViewFieldCreateManyInput;
}>; }>;
@ -5848,43 +5841,6 @@ export function useDeleteUserAccountMutation(baseOptions?: Apollo.MutationHookOp
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>; export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
export type DeleteUserAccountMutationResult = Apollo.MutationResult<DeleteUserAccountMutation>; export type DeleteUserAccountMutationResult = Apollo.MutationResult<DeleteUserAccountMutation>;
export type DeleteUserAccountMutationOptions = Apollo.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>; export type DeleteUserAccountMutationOptions = Apollo.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
export const CreateViewFieldDocument = gql`
mutation CreateViewField($data: ViewFieldCreateInput!) {
createOneViewField(data: $data) {
id
fieldName
isVisible
sizeInPx
index
}
}
`;
export type CreateViewFieldMutationFn = Apollo.MutationFunction<CreateViewFieldMutation, CreateViewFieldMutationVariables>;
/**
* __useCreateViewFieldMutation__
*
* To run a mutation, you first call `useCreateViewFieldMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateViewFieldMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createViewFieldMutation, { data, loading, error }] = useCreateViewFieldMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useCreateViewFieldMutation(baseOptions?: Apollo.MutationHookOptions<CreateViewFieldMutation, CreateViewFieldMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateViewFieldMutation, CreateViewFieldMutationVariables>(CreateViewFieldDocument, options);
}
export type CreateViewFieldMutationHookResult = ReturnType<typeof useCreateViewFieldMutation>;
export type CreateViewFieldMutationResult = Apollo.MutationResult<CreateViewFieldMutation>;
export type CreateViewFieldMutationOptions = Apollo.BaseMutationOptions<CreateViewFieldMutation, CreateViewFieldMutationVariables>;
export const CreateViewFieldsDocument = gql` export const CreateViewFieldsDocument = gql`
mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) { mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) {
createManyViewField(data: $data) { createManyViewField(data: $data) {

View File

@ -10,6 +10,7 @@ import { EntityTable } from '@/ui/table/components/EntityTable';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData'; import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { TableContext } from '@/ui/table/states/TableContext'; import { TableContext } from '@/ui/table/states/TableContext';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableViewFields } from '@/views/hooks/useTableViewFields';
import { useViewSorts } from '@/views/hooks/useViewSorts'; import { useViewSorts } from '@/views/hooks/useViewSorts';
import { currentViewIdState } from '@/views/states/currentViewIdState'; import { currentViewIdState } from '@/views/states/currentViewIdState';
import { import {
@ -24,6 +25,11 @@ import { defaultOrderBy } from '../../queries';
export function CompanyTable() { export function CompanyTable() {
const currentViewId = useRecoilValue(currentViewIdState); const currentViewId = useRecoilValue(currentViewIdState);
const orderBy = useRecoilScopedValue(sortsOrderByScopedState, TableContext); const orderBy = useRecoilScopedValue(sortsOrderByScopedState, TableContext);
const { handleColumnsChange } = useTableViewFields({
objectName: 'company',
viewFieldDefinitions: companyViewFields,
});
const { updateSorts } = useViewSorts({ const { updateSorts } = useViewSorts({
availableSorts, availableSorts,
Context: TableContext, Context: TableContext,
@ -38,18 +44,17 @@ export function CompanyTable() {
return ( return (
<> <>
<GenericEntityTableData <GenericEntityTableData
objectName="company"
getRequestResultKey="companies" getRequestResultKey="companies"
useGetRequest={useGetCompaniesQuery} useGetRequest={useGetCompaniesQuery}
orderBy={orderBy.length ? orderBy : defaultOrderBy} orderBy={orderBy.length ? orderBy : defaultOrderBy}
whereFilters={whereFilters} whereFilters={whereFilters}
viewFieldDefinitions={companyViewFields}
filterDefinitionArray={companiesFilters} filterDefinitionArray={companiesFilters}
/> />
<EntityTable <EntityTable
viewName="All Companies" viewName="All Companies"
viewIcon={<IconList size={16} />} viewIcon={<IconList size={16} />}
availableSorts={availableSorts} availableSorts={availableSorts}
onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? updateSorts : undefined} onSortsUpdate={currentViewId ? updateSorts : undefined}
useUpdateEntityMutation={useUpdateOneCompanyMutation} useUpdateEntityMutation={useUpdateOneCompanyMutation}
/> />

View File

@ -2,32 +2,21 @@ import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData'; import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
import { entityTableDimensionsState } from '@/ui/table/states/entityTableDimensionsState'; import { tableColumnsState } from '@/ui/table/states/tableColumnsState';
import { viewFieldsState } from '@/ui/table/states/viewFieldsState';
import { companyViewFields } from '../../constants/companyViewFields'; import { companyViewFields } from '../../constants/companyViewFields';
import { mockedCompaniesData } from './companies-mock-data'; import { mockedCompaniesData } from './companies-mock-data';
export function CompanyTableMockData() { export function CompanyTableMockData() {
const setEntityTableDimensions = useSetRecoilState( const setColumns = useSetRecoilState(tableColumnsState);
entityTableDimensionsState,
);
const setViewFieldsState = useSetRecoilState(viewFieldsState);
const setEntityTableData = useSetEntityTableData(); const setEntityTableData = useSetEntityTableData();
setEntityTableData(mockedCompaniesData, []); setEntityTableData(mockedCompaniesData, []);
useEffect(() => { useEffect(() => {
setViewFieldsState({ setColumns(companyViewFields);
objectName: 'company', }, [setColumns]);
viewFields: companyViewFields,
});
setEntityTableDimensions((prevState) => ({
...prevState,
numberOfColumns: companyViewFields.length,
}));
}, [setEntityTableDimensions, setViewFieldsState]);
return <></>; return <></>;
} }

View File

@ -1,17 +1,17 @@
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState';
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEntityTableDataState';
import { numberOfTableRowsState } from '@/ui/table/states/numberOfTableRowsState';
import { TableContext } from '@/ui/table/states/TableContext';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { currentPageLocationState } from '@/ui/utilities/loading-state/states/currentPageLocationState'; import { currentPageLocationState } from '@/ui/utilities/loading-state/states/currentPageLocationState';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { GetPeopleQuery } from '~/generated/graphql'; import { GetPeopleQuery } from '~/generated/graphql';
import { peopleFilters } from '~/pages/people/people-filters';
import { peopleFilters } from '../../../pages/people/people-filters';
import { availableFiltersScopedState } from '../../ui/filter-n-sort/states/availableFiltersScopedState';
import { useResetTableRowSelection } from '../../ui/table/hooks/useResetTableRowSelection';
import { entityTableDimensionsState } from '../../ui/table/states/entityTableDimensionsState';
import { isFetchingEntityTableDataState } from '../../ui/table/states/isFetchingEntityTableDataState';
import { TableContext } from '../../ui/table/states/TableContext';
import { tableRowIdsState } from '../../ui/table/states/tableRowIdsState';
import { peopleCityFamilyState } from '../states/peopleCityFamilyState'; import { peopleCityFamilyState } from '../states/peopleCityFamilyState';
import { peopleCompanyFamilyState } from '../states/peopleCompanyFamilyState'; import { peopleCompanyFamilyState } from '../states/peopleCompanyFamilyState';
import { peopleCreatedAtFamilyState } from '../states/peopleCreatedAtFamilyState'; import { peopleCreatedAtFamilyState } from '../states/peopleCreatedAtFamilyState';
@ -124,10 +124,7 @@ export function useSetPeopleEntityTable() {
resetTableRowSelection(); resetTableRowSelection();
set(entityTableDimensionsState, { set(numberOfTableRowsState, peopleIds.length);
numberOfColumns: 10,
numberOfRows: peopleIds.length,
});
set(availableFiltersScopedState(tableContextScopeId), peopleFilters); set(availableFiltersScopedState(tableContextScopeId), peopleFilters);

View File

@ -10,6 +10,7 @@ import { EntityTable } from '@/ui/table/components/EntityTable';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData'; import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { TableContext } from '@/ui/table/states/TableContext'; import { TableContext } from '@/ui/table/states/TableContext';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableViewFields } from '@/views/hooks/useTableViewFields';
import { useViewSorts } from '@/views/hooks/useViewSorts'; import { useViewSorts } from '@/views/hooks/useViewSorts';
import { currentViewIdState } from '@/views/states/currentViewIdState'; import { currentViewIdState } from '@/views/states/currentViewIdState';
import { import {
@ -24,6 +25,11 @@ import { defaultOrderBy } from '../../queries';
export function PeopleTable() { export function PeopleTable() {
const currentViewId = useRecoilValue(currentViewIdState); const currentViewId = useRecoilValue(currentViewIdState);
const orderBy = useRecoilScopedValue(sortsOrderByScopedState, TableContext); const orderBy = useRecoilScopedValue(sortsOrderByScopedState, TableContext);
const { handleColumnsChange } = useTableViewFields({
objectName: 'person',
viewFieldDefinitions: peopleViewFields,
});
const { updateSorts } = useViewSorts({ const { updateSorts } = useViewSorts({
availableSorts, availableSorts,
Context: TableContext, Context: TableContext,
@ -38,18 +44,17 @@ export function PeopleTable() {
return ( return (
<> <>
<GenericEntityTableData <GenericEntityTableData
objectName="person"
getRequestResultKey="people" getRequestResultKey="people"
useGetRequest={useGetPeopleQuery} useGetRequest={useGetPeopleQuery}
orderBy={orderBy.length ? orderBy : defaultOrderBy} orderBy={orderBy.length ? orderBy : defaultOrderBy}
whereFilters={whereFilters} whereFilters={whereFilters}
viewFieldDefinitions={peopleViewFields}
filterDefinitionArray={peopleFilters} filterDefinitionArray={peopleFilters}
/> />
<EntityTable <EntityTable
viewName="All People" viewName="All People"
viewIcon={<IconList size={16} />} viewIcon={<IconList size={16} />}
availableSorts={availableSorts} availableSorts={availableSorts}
onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? updateSorts : undefined} onSortsUpdate={currentViewId ? updateSorts : undefined}
useUpdateEntityMutation={useUpdateOnePersonMutation} useUpdateEntityMutation={useUpdateOnePersonMutation}
/> />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,11 +2,6 @@ import { useContext } from 'react';
import { RowIdContext } from '../states/RowIdContext'; import { RowIdContext } from '../states/RowIdContext';
export type TableDimensions = {
numberOfColumns: number;
numberOfRows: number;
};
export function useCurrentRowEntityId() { export function useCurrentRowEntityId() {
const currentEntityId = useContext(RowIdContext); const currentEntityId = useContext(RowIdContext);

View File

@ -1,21 +1,12 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState'; import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
import { tableRowIdsState } from '../states/tableRowIdsState'; import { tableRowIdsState } from '../states/tableRowIdsState';
import { useResetTableRowSelection } from './useResetTableRowSelection'; import { useResetTableRowSelection } from './useResetTableRowSelection';
export type TableDimensions = { export function useInitializeEntityTable() {
numberOfColumns: number;
numberOfRows: number;
};
export function useInitializeEntityTable({
numberOfColumns,
}: {
numberOfColumns: number;
}) {
const resetTableRowSelection = useResetTableRowSelection(); const resetTableRowSelection = useResetTableRowSelection();
const tableRowIds = useRecoilValue(tableRowIdsState); const tableRowIds = useRecoilValue(tableRowIdsState);
@ -24,12 +15,9 @@ export function useInitializeEntityTable({
resetTableRowSelection(); resetTableRowSelection();
}, [resetTableRowSelection]); }, [resetTableRowSelection]);
const [, setTableDimensions] = useRecoilState(entityTableDimensionsState); const setNumberOfTableRows = useSetRecoilState(numberOfTableRowsState);
useEffect(() => { useEffect(() => {
setTableDimensions({ setNumberOfTableRows(tableRowIds?.length);
numberOfColumns, }, [tableRowIds, setNumberOfTableRows]);
numberOfRows: tableRowIds?.length,
});
}, [tableRowIds, numberOfColumns, setTableDimensions]);
} }

View File

@ -1,96 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '@/ui/editable-field/types/ViewField';
import { GET_VIEW_FIELDS } from '@/views/queries/select';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
SortOrder,
useCreateViewFieldsMutation,
useGetViewFieldsQuery,
} from '~/generated/graphql';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { viewFieldsState } from '../states/viewFieldsState';
const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = {
type: 'text',
placeHolder: '',
fieldName: '',
};
export const toViewFieldInput = (
objectName: 'company' | 'person',
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => ({
fieldName: viewFieldDefinition.columnLabel,
index: viewFieldDefinition.columnOrder,
isVisible: viewFieldDefinition.isVisible ?? true,
objectName,
sizeInPx: viewFieldDefinition.columnSize,
});
export const useLoadViewFields = ({
objectName,
viewFieldDefinitions,
}: {
objectName: 'company' | 'person';
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
}) => {
const currentViewId = useRecoilValue(currentViewIdState);
const setEntityTableDimensions = useSetRecoilState(
entityTableDimensionsState,
);
const setViewFieldsState = useSetRecoilState(viewFieldsState);
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
useGetViewFieldsQuery({
variables: {
orderBy: { index: SortOrder.Asc },
where: {
objectName: { equals: objectName },
viewId: { equals: currentViewId ?? null },
},
},
onCompleted: (data) => {
if (data.viewFields.length) {
const viewFields = data.viewFields.map<
ViewFieldDefinition<ViewFieldMetadata>
>((viewField) => ({
...(viewFieldDefinitions.find(
({ columnLabel }) => viewField.fieldName === columnLabel,
) || { metadata: DEFAULT_VIEW_FIELD_METADATA }),
id: viewField.id,
columnLabel: viewField.fieldName,
columnOrder: viewField.index,
columnSize: viewField.sizeInPx,
isVisible: viewField.isVisible,
}));
setViewFieldsState({ objectName, viewFields });
setEntityTableDimensions((prevState) => ({
...prevState,
numberOfColumns: data.viewFields.length,
}));
return;
}
// Populate if empty
createViewFieldsMutation({
variables: {
data: viewFieldDefinitions.map((viewFieldDefinition) => ({
...toViewFieldInput(objectName, viewFieldDefinition),
viewId: currentViewId,
})),
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
},
});
};

View File

@ -1,8 +1,8 @@
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { numberOfTableColumnsSelectorState } from '../states/numberOfTableColumnsSelectorState'; import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
import { numberOfTableRowsSelectorState } from '../states/numberOfTableRowsSelectorState';
import { softFocusPositionState } from '../states/softFocusPositionState'; import { softFocusPositionState } from '../states/softFocusPositionState';
import { numberOfTableColumnsState } from '../states/tableColumnsState';
import { useSetSoftFocusPosition } from './useSetSoftFocusPosition'; import { useSetSoftFocusPosition } from './useSetSoftFocusPosition';
@ -39,7 +39,7 @@ export function useMoveSoftFocus() {
.valueOrThrow(); .valueOrThrow();
const numberOfTableRows = snapshot const numberOfTableRows = snapshot
.getLoadable(numberOfTableRowsSelectorState) .getLoadable(numberOfTableRowsState)
.valueOrThrow(); .valueOrThrow();
let newRowNumber = softFocusPosition.row + 1; let newRowNumber = softFocusPosition.row + 1;
@ -64,11 +64,11 @@ export function useMoveSoftFocus() {
.valueOrThrow(); .valueOrThrow();
const numberOfTableColumns = snapshot const numberOfTableColumns = snapshot
.getLoadable(numberOfTableColumnsSelectorState) .getLoadable(numberOfTableColumnsState)
.valueOrThrow(); .valueOrThrow();
const numberOfTableRows = snapshot const numberOfTableRows = snapshot
.getLoadable(numberOfTableRowsSelectorState) .getLoadable(numberOfTableRowsState)
.valueOrThrow(); .valueOrThrow();
const currentColumnNumber = softFocusPosition.column; const currentColumnNumber = softFocusPosition.column;
@ -112,7 +112,7 @@ export function useMoveSoftFocus() {
.valueOrThrow(); .valueOrThrow();
const numberOfTableColumns = snapshot const numberOfTableColumns = snapshot
.getLoadable(numberOfTableColumnsSelectorState) .getLoadable(numberOfTableColumnsState)
.valueOrThrow(); .valueOrThrow();
const currentColumnNumber = softFocusPosition.column; const currentColumnNumber = softFocusPosition.column;

View File

@ -3,13 +3,14 @@ import { useRecoilCallback } from 'recoil';
import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState'; import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState';
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition'; import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
import { entityTableDimensionsState } from '@/ui/table/states/entityTableDimensionsState';
import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEntityTableDataState';
import { TableContext } from '@/ui/table/states/TableContext'; import { TableContext } from '@/ui/table/states/TableContext';
import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState'; import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { isFetchingEntityTableDataState } from '../states/isFetchingEntityTableDataState';
import { numberOfTableRowsState } from '../states/numberOfTableRowsState';
export function useSetEntityTableData() { export function useSetEntityTableData() {
const resetTableRowSelection = useResetTableRowSelection(); const resetTableRowSelection = useResetTableRowSelection();
@ -43,10 +44,7 @@ export function useSetEntityTableData() {
resetTableRowSelection(); resetTableRowSelection();
set(entityTableDimensionsState, (prevState) => ({ set(numberOfTableRowsState, entityIds.length);
...prevState,
numberOfRows: entityIds.length,
}));
set(availableFiltersScopedState(tableContextScopeId), filters); set(availableFiltersScopedState(tableContextScopeId), filters);

View File

@ -1,7 +1,6 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton'; import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
@ -16,16 +15,15 @@ import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope'; import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon'; import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon';
import { import {
hiddenViewFieldsState, hiddenTableColumnsState,
visibleViewFieldsState, tableColumnsState,
} from '@/ui/table/states/viewFieldsState'; visibleTableColumnsState,
import { useUpdateViewFieldMutation } from '~/generated/graphql'; } from '@/ui/table/states/tableColumnsState';
import { GET_VIEW_FIELDS } from '../queries/select'; import { TableOptionsDropdownSection } from './TableOptionsDropdownSection';
import { OptionsDropdownSection } from './OptionsDropdownSection'; type TableOptionsDropdownButtonProps = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
type OptionsDropdownButtonProps = {
HotkeyScope: FiltersHotkeyScope; HotkeyScope: FiltersHotkeyScope;
}; };
@ -33,51 +31,52 @@ enum Option {
Properties = 'Properties', Properties = 'Properties',
} }
export const OptionsDropdownButton = ({ export const TableOptionsDropdownButton = ({
onColumnsChange,
HotkeyScope, HotkeyScope,
}: OptionsDropdownButtonProps) => { }: TableOptionsDropdownButtonProps) => {
const theme = useTheme(); const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false); const [isUnfolded, setIsUnfolded] = useState(false);
const [selectedOption, setSelectedOption] = useState<Option | undefined>( const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined, undefined,
); );
const visibleFields = useRecoilValue(visibleViewFieldsState); const [columns, setColumns] = useRecoilState(tableColumnsState);
const hiddenFields = useRecoilValue(hiddenViewFieldsState); const visibleColumns = useRecoilValue(visibleTableColumnsState);
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
const [updateViewFieldMutation] = useUpdateViewFieldMutation(); const handleColumnVisibilityChange = useCallback(
(columnId: string, nextIsVisible: boolean) => {
const nextColumns = columns.map((column) =>
column.id === columnId
? { ...column, isVisible: nextIsVisible }
: column,
);
const handleViewFieldVisibilityChange = useCallback( setColumns(nextColumns);
(viewFieldId: string, nextIsVisible: boolean) => { onColumnsChange?.(nextColumns);
updateViewFieldMutation({
variables: {
data: { isVisible: nextIsVisible },
where: { id: viewFieldId },
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
}, },
[updateViewFieldMutation], [columns, onColumnsChange, setColumns],
); );
const renderFieldActions = useCallback( const renderFieldActions = useCallback(
(viewField: ViewFieldDefinition<ViewFieldMetadata>) => (column: ViewFieldDefinition<ViewFieldMetadata>) =>
// Do not allow hiding last visible column // Do not allow hiding last visible column
!viewField.isVisible || visibleFields.length > 1 ? ( !column.isVisible || visibleColumns.length > 1 ? (
<IconButton <IconButton
icon={ icon={
viewField.isVisible ? ( column.isVisible ? (
<IconMinus size={theme.icon.size.sm} /> <IconMinus size={theme.icon.size.sm} />
) : ( ) : (
<IconPlus size={theme.icon.size.sm} /> <IconPlus size={theme.icon.size.sm} />
) )
} }
onClick={() => onClick={() =>
handleViewFieldVisibilityChange(viewField.id, !viewField.isVisible) handleColumnVisibilityChange(column.id, !column.isVisible)
} }
/> />
) : undefined, ) : undefined,
[handleViewFieldVisibilityChange, theme.icon.size.sm, visibleFields.length], [handleColumnVisibilityChange, theme.icon.size.sm, visibleColumns.length],
); );
const resetSelectedOption = useCallback(() => { const resetSelectedOption = useCallback(() => {
@ -115,18 +114,18 @@ export const OptionsDropdownButton = ({
Properties Properties
</DropdownMenuHeader> </DropdownMenuHeader>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<OptionsDropdownSection <TableOptionsDropdownSection
renderActions={renderFieldActions} renderActions={renderFieldActions}
title="Visible" title="Visible"
viewFields={visibleFields} columns={visibleColumns}
/> />
{hiddenFields.length > 0 && ( {hiddenColumns.length > 0 && (
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<OptionsDropdownSection <TableOptionsDropdownSection
renderActions={renderFieldActions} renderActions={renderFieldActions}
title="Hidden" title="Hidden"
viewFields={hiddenFields} columns={hiddenColumns}
/> />
</> </>
)} )}

View File

@ -12,32 +12,32 @@ import {
ViewFieldMetadata, ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField'; } from '@/ui/editable-field/types/ViewField';
type OptionsDropdownSectionProps = { type TableOptionsDropdownSectionProps = {
renderActions: ( renderActions: (
viewField: ViewFieldDefinition<ViewFieldMetadata>, column: ViewFieldDefinition<ViewFieldMetadata>,
) => DropdownMenuItemProps['actions']; ) => DropdownMenuItemProps['actions'];
title: string; title: string;
viewFields: ViewFieldDefinition<ViewFieldMetadata>[]; columns: ViewFieldDefinition<ViewFieldMetadata>[];
}; };
export const OptionsDropdownSection = ({ export const TableOptionsDropdownSection = ({
renderActions, renderActions,
title, title,
viewFields, columns,
}: OptionsDropdownSectionProps) => { }: TableOptionsDropdownSectionProps) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<> <>
<DropdownMenuSubheader>{title}</DropdownMenuSubheader> <DropdownMenuSubheader>{title}</DropdownMenuSubheader>
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{viewFields.map((viewField) => ( {columns.map((column) => (
<DropdownMenuItem actions={renderActions(viewField)}> <DropdownMenuItem key={column.id} actions={renderActions(column)}>
{viewField.columnIcon && {column.columnIcon &&
cloneElement(viewField.columnIcon, { cloneElement(column.columnIcon, {
size: theme.icon.size.md, size: theme.icon.size.md,
})} })}
{viewField.columnLabel} {column.columnLabel}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>

View File

@ -2,13 +2,13 @@ import { selector } from 'recoil';
import { AllRowsSelectedStatus } from '../types/AllRowSelectedStatus'; import { AllRowsSelectedStatus } from '../types/AllRowSelectedStatus';
import { numberOfTableRowsSelectorState } from './numberOfTableRowsSelectorState'; import { numberOfTableRowsState } from './numberOfTableRowsState';
import { selectedRowIdsSelector } from './selectedRowIdsSelector'; import { selectedRowIdsSelector } from './selectedRowIdsSelector';
export const allRowsSelectedStatusSelector = selector<AllRowsSelectedStatus>({ export const allRowsSelectedStatusSelector = selector<AllRowsSelectedStatus>({
key: 'allRowsSelectedStatusSelector', key: 'allRowsSelectedStatusSelector',
get: ({ get }) => { get: ({ get }) => {
const numberOfRows = get(numberOfTableRowsSelectorState); const numberOfRows = get(numberOfTableRowsState);
const selectedRowIds = get(selectedRowIdsSelector); const selectedRowIds = get(selectedRowIdsSelector);

View File

@ -1,11 +0,0 @@
import { atom } from 'recoil';
import { TableDimensions } from '../hooks/useInitializeEntityTable';
export const entityTableDimensionsState = atom<TableDimensions>({
key: 'entityTableDimensionsState',
default: {
numberOfRows: 0,
numberOfColumns: 0,
},
});

View File

@ -1,12 +0,0 @@
import { selector } from 'recoil';
import { entityTableDimensionsState } from './entityTableDimensionsState';
export const numberOfTableColumnsSelectorState = selector<number>({
key: 'numberOfTableColumnsState',
get: ({ get }) => {
const { numberOfColumns } = get(entityTableDimensionsState);
return numberOfColumns;
},
});

View File

@ -1,12 +0,0 @@
import { selector } from 'recoil';
import { entityTableDimensionsState } from './entityTableDimensionsState';
export const numberOfTableRowsSelectorState = selector<number>({
key: 'numberOfTableRowsState',
get: ({ get }) => {
const { numberOfRows } = get(entityTableDimensionsState);
return numberOfRows;
},
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const numberOfTableRowsState = atom<number>({
key: 'numberOfTableRowsState',
default: 0,
});

View File

@ -0,0 +1,37 @@
import { atom, selector } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
export const tableColumnsState = atom<ViewFieldDefinition<ViewFieldMetadata>[]>(
{
key: 'tableColumnsState',
default: [],
},
);
export const tableColumnsByIdState = selector({
key: 'tableColumnsByIdState',
get: ({ get }) =>
get(tableColumnsState).reduce<
Record<string, ViewFieldDefinition<ViewFieldMetadata>>
>((result, column) => ({ ...result, [column.id]: column }), {}),
});
export const numberOfTableColumnsState = selector<number>({
key: 'numberOfTableColumnsState',
get: ({ get }) => get(tableColumnsState).length,
});
export const visibleTableColumnsState = selector({
key: 'visibleTableColumnsState',
get: ({ get }) => get(tableColumnsState).filter((column) => column.isVisible),
});
export const hiddenTableColumnsState = selector({
key: 'hiddenTableColumnsState',
get: ({ get }) =>
get(tableColumnsState).filter((column) => !column.isVisible),
});

View File

@ -1,61 +0,0 @@
import { atom, selector } from 'recoil';
import { companyViewFields } from '@/companies/constants/companyViewFields';
import { peopleViewFields } from '@/people/constants/peopleViewFields';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
export const viewFieldsState = atom<{
objectName: 'company' | 'person' | '';
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
}>({
key: 'viewFieldsState',
default: { objectName: '', viewFields: [] },
});
export const columnWidthByViewFieldIdState = selector({
key: 'columnWidthByViewFieldIdState',
get: ({ get }) =>
get(viewFieldsState).viewFields.reduce<Record<string, number>>(
(result, viewField) => ({
...result,
[viewField.id]: viewField.columnSize,
}),
{},
),
});
export const addableViewFieldDefinitionsState = selector({
key: 'addableViewFieldDefinitionsState',
get: ({ get }) => {
const { objectName, viewFields } = get(viewFieldsState);
if (!objectName) return [];
const existingColumnLabels = viewFields.map(
(viewField) => viewField.columnLabel,
);
const viewFieldDefinitions =
objectName === 'company' ? companyViewFields : peopleViewFields;
return viewFieldDefinitions.filter(
(viewFieldDefinition) =>
!existingColumnLabels.includes(viewFieldDefinition.columnLabel),
);
},
});
export const visibleViewFieldsState = selector({
key: 'visibleViewFieldsState',
get: ({ get }) =>
get(viewFieldsState).viewFields.filter((viewField) => viewField.isVisible),
});
export const hiddenViewFieldsState = selector({
key: 'hiddenViewFieldsState',
get: ({ get }) =>
get(viewFieldsState).viewFields.filter((viewField) => !viewField.isVisible),
});

View File

@ -1,15 +1,19 @@
import { ReactNode, useCallback } from 'react'; import { ReactNode, useCallback } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton'; import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar'; import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton'; import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
import { sortScopedState } from '@/ui/filter-n-sort/states/sortScopedState'; import { sortScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope'; import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface'; import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { TableOptionsDropdownButton } from '@/ui/table/options/components/TableOptionsDropdownButton';
import { TopBar } from '@/ui/top-bar/TopBar'; import { TopBar } from '@/ui/top-bar/TopBar';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { OptionsDropdownButton } from '@/views/components/OptionsDropdownButton';
import { TableContext } from '../../states/TableContext'; import { TableContext } from '../../states/TableContext';
@ -17,6 +21,7 @@ type OwnProps<SortField> = {
viewName: string; viewName: string;
viewIcon?: ReactNode; viewIcon?: ReactNode;
availableSorts?: Array<SortType<SortField>>; availableSorts?: Array<SortType<SortField>>;
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void; onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
}; };
@ -34,6 +39,7 @@ export function TableHeader<SortField>({
viewName, viewName,
viewIcon, viewIcon,
availableSorts, availableSorts,
onColumnsChange,
onSortsUpdate, onSortsUpdate,
}: OwnProps<SortField>) { }: OwnProps<SortField>) {
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>( const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
@ -79,7 +85,8 @@ export function TableHeader<SortField>({
onSortSelect={sortSelect} onSortSelect={sortSelect}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton} HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
/> />
<OptionsDropdownButton <TableOptionsDropdownButton
onColumnsChange={onColumnsChange}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton} HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
/> />
</> </>

View File

@ -0,0 +1,146 @@
import { useCallback } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '@/ui/editable-field/types/ViewField';
import {
tableColumnsByIdState,
tableColumnsState,
} from '@/ui/table/states/tableColumnsState';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
SortOrder,
useCreateViewFieldsMutation,
useGetViewFieldsQuery,
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import { GET_VIEW_FIELDS } from '../queries/select';
const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = {
type: 'text',
placeHolder: '',
fieldName: '',
};
export const toViewFieldInput = (
objectName: 'company' | 'person',
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => ({
fieldName: viewFieldDefinition.columnLabel,
index: viewFieldDefinition.columnOrder,
isVisible: viewFieldDefinition.isVisible ?? true,
objectName,
sizeInPx: viewFieldDefinition.columnSize,
});
export const useTableViewFields = ({
objectName,
viewFieldDefinitions,
}: {
objectName: 'company' | 'person';
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
}) => {
const currentViewId = useRecoilValue(currentViewIdState);
const setColumns = useSetRecoilState(tableColumnsState);
const columnsById = useRecoilValue(tableColumnsByIdState);
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const createViewFields = useCallback(
(columns: ViewFieldDefinition<ViewFieldMetadata>[]) => {
if (!columns.length) return;
return createViewFieldsMutation({
variables: {
data: columns.map((column) => ({
...toViewFieldInput(objectName, column),
viewId: currentViewId,
})),
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
},
[createViewFieldsMutation, currentViewId, objectName],
);
const updateViewFields = useCallback(
(columns: ViewFieldDefinition<ViewFieldMetadata>[]) => {
if (!columns.length) return;
return Promise.all(
columns.map((column) =>
updateViewFieldMutation({
variables: {
data: {
isVisible: column.isVisible,
sizeInPx: column.columnSize,
},
where: { id: column.id },
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
}),
),
);
},
[updateViewFieldMutation],
);
useGetViewFieldsQuery({
variables: {
orderBy: { index: SortOrder.Asc },
where: {
objectName: { equals: objectName },
viewId: { equals: currentViewId ?? null },
},
},
onCompleted: (data) => {
if (data.viewFields.length) {
setColumns(
data.viewFields.map<ViewFieldDefinition<ViewFieldMetadata>>(
(viewField) => ({
...(viewFieldDefinitions.find(
({ columnLabel }) => viewField.fieldName === columnLabel,
) || { metadata: DEFAULT_VIEW_FIELD_METADATA }),
id: viewField.id,
columnLabel: viewField.fieldName,
columnOrder: viewField.index,
columnSize: viewField.sizeInPx,
isVisible: viewField.isVisible,
}),
),
);
return;
}
// Populate if empty
createViewFields(viewFieldDefinitions);
},
});
const handleColumnsChange = useCallback(
async (nextColumns: ViewFieldDefinition<ViewFieldMetadata>[]) => {
const viewFieldsToCreate = nextColumns.filter(
(nextColumn) => !columnsById[nextColumn.id],
);
await createViewFields(viewFieldsToCreate);
const viewFieldsToUpdate = nextColumns.filter(
(nextColumn) =>
columnsById[nextColumn.id] &&
(columnsById[nextColumn.id].columnSize !== nextColumn.columnSize ||
columnsById[nextColumn.id].isVisible !== nextColumn.isVisible),
);
await updateViewFields(viewFieldsToUpdate);
},
[columnsById, createViewFields, updateViewFields],
);
return { handleColumnsChange };
};

View File

@ -1,17 +1,5 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const CREATE_VIEW_FIELD = gql`
mutation CreateViewField($data: ViewFieldCreateInput!) {
createOneViewField(data: $data) {
id
fieldName
isVisible
sizeInPx
index
}
}
`;
export const CREATE_VIEW_FIELDS = gql` export const CREATE_VIEW_FIELDS = gql`
mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) { mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) {
createManyViewField(data: $data) { createManyViewField(data: $data) {