feat: add table columns (#1056)

* feat: add table columns

Closes #879

* refactor: ComponentProps first
This commit is contained in:
Thaïs
2023-08-04 19:07:31 +02:00
committed by GitHub
parent a8856516bd
commit 417ca3d131
11 changed files with 406 additions and 107 deletions

View File

@ -905,6 +905,7 @@ export type Mutation = {
createOneCompany: Company; createOneCompany: Company;
createOnePerson: Person; createOnePerson: Person;
createOnePipelineProgress: PipelineProgress; createOnePipelineProgress: PipelineProgress;
createOneViewField: ViewField;
deleteCurrentWorkspace: Workspace; deleteCurrentWorkspace: Workspace;
deleteManyActivities: AffectedRows; deleteManyActivities: AffectedRows;
deleteManyCompany: AffectedRows; deleteManyCompany: AffectedRows;
@ -980,6 +981,11 @@ export type MutationCreateOnePipelineProgressArgs = {
}; };
export type MutationCreateOneViewFieldArgs = {
data: ViewFieldCreateInput;
};
export type MutationDeleteManyActivitiesArgs = { export type MutationDeleteManyActivitiesArgs = {
where?: InputMaybe<ActivityWhereInput>; where?: InputMaybe<ActivityWhereInput>;
}; };
@ -2137,6 +2143,15 @@ export type ViewField = {
sizeInPx: Scalars['Int']; sizeInPx: Scalars['Int'];
}; };
export type ViewFieldCreateInput = {
fieldName: Scalars['String'];
id?: InputMaybe<Scalars['String']>;
index: Scalars['Int'];
isVisible: Scalars['Boolean'];
objectName: Scalars['String'];
sizeInPx: Scalars['Int'];
};
export type ViewFieldCreateManyInput = { export type ViewFieldCreateManyInput = {
fieldName: Scalars['String']; fieldName: Scalars['String'];
id?: InputMaybe<Scalars['String']>; id?: InputMaybe<Scalars['String']>;
@ -2709,6 +2724,13 @@ 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;
}>; }>;
@ -5128,6 +5150,43 @@ 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

@ -13,18 +13,21 @@ export function CompanyTableMockData() {
const setEntityTableDimensions = useSetRecoilState( const setEntityTableDimensions = useSetRecoilState(
entityTableDimensionsState, entityTableDimensionsState,
); );
const setViewFields = useSetRecoilState(viewFieldsState); const setViewFieldsState = useSetRecoilState(viewFieldsState);
const setEntityTableData = useSetEntityTableData(); const setEntityTableData = useSetEntityTableData();
setEntityTableData(mockedCompaniesData, []); setEntityTableData(mockedCompaniesData, []);
useEffect(() => { useEffect(() => {
setViewFields(companyViewFields); setViewFieldsState({
objectName: 'company',
viewFields: companyViewFields,
});
setEntityTableDimensions((prevState) => ({ setEntityTableDimensions((prevState) => ({
...prevState, ...prevState,
numberOfColumns: companyViewFields.length, numberOfColumns: companyViewFields.length,
})); }));
}, [setEntityTableDimensions, setViewFields]); }, [setEntityTableDimensions, setViewFieldsState]);
return <></>; return <></>;
} }

View File

@ -1,7 +1,8 @@
import React from 'react'; import React, { type ComponentProps } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconButtonSize, IconButtonVariant } from './IconButton'; import type { IconButtonSize, IconButtonVariant } from './IconButton';
const StyledIconButtonGroupContainer = styled.div` const StyledIconButtonGroupContainer = styled.div`
align-items: flex-start; align-items: flex-start;
background: ${({ theme }) => theme.background.transparent.primary}; background: ${({ theme }) => theme.background.transparent.primary};
@ -11,24 +12,27 @@ const StyledIconButtonGroupContainer = styled.div`
padding: ${({ theme }) => theme.spacing(0.5)}; padding: ${({ theme }) => theme.spacing(0.5)};
`; `;
type IconButtonGroupProps = { type IconButtonGroupProps = Omit<ComponentProps<'div'>, 'children'> & {
variant: IconButtonVariant; variant: IconButtonVariant;
size: IconButtonSize; size: IconButtonSize;
children: React.ReactElement[]; children: React.ReactElement | React.ReactElement[];
}; };
export function IconButtonGroup({ export function IconButtonGroup({
children, children,
variant, variant,
size, size,
...props
}: IconButtonGroupProps) { }: IconButtonGroupProps) {
return ( return (
<StyledIconButtonGroupContainer> <StyledIconButtonGroupContainer {...props}>
{React.Children.map(children, (child) => {React.Children.map(
React.cloneElement(child, { Array.isArray(children) ? children : [children],
...(variant ? { variant } : {}), (child) =>
...(size ? { size } : {}), React.cloneElement(child, {
}), ...(variant ? { variant } : {}),
...(size ? { size } : {}),
}),
)} )}
</StyledIconButtonGroupContainer> </StyledIconButtonGroupContainer>
); );

View File

@ -20,7 +20,6 @@ const StyledTable = styled.table`
margin-left: ${({ theme }) => theme.table.horizontalCellMargin}; margin-left: ${({ theme }) => theme.table.horizontalCellMargin};
margin-right: ${({ theme }) => theme.table.horizontalCellMargin}; margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
table-layout: fixed; table-layout: fixed;
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
th { th {
border: 1px solid ${({ theme }) => theme.border.color.light}; border: 1px solid ${({ theme }) => theme.border.color.light};
@ -37,7 +36,7 @@ const StyledTable = styled.table`
border-right-color: transparent; border-right-color: transparent;
} }
:last-of-type { :last-of-type {
min-width: 0; min-width: fit-content;
width: 100%; width: 100%;
} }
} }
@ -58,7 +57,7 @@ const StyledTable = styled.table`
border-right-color: transparent; border-right-color: transparent;
} }
:last-of-type { :last-of-type {
min-width: 0; min-width: fit-content;
width: 100%; width: 100%;
} }
} }

View File

@ -0,0 +1,86 @@
import React, { ComponentProps, useRef } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconButton } from '@/ui/button/components/IconButton';
import { IconButtonGroup } from '@/ui/button/components/IconButtonGroup';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { IconPlus } from '@/ui/icon';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../types/ViewField';
const StyledColumnMenu = styled(DropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledIconButtonGroup = styled(IconButtonGroup)`
display: none;
position: absolute;
right: ${({ theme }) => theme.spacing(1)};
`;
const styledIconButtonGroupClassName = 'column-menu-item-icon-button-group';
const StyledColumnMenuItem = styled(DropdownMenuItem)`
position: relative;
&:hover {
.${styledIconButtonGroupClassName} {
display: flex;
}
}
`;
type EntityTableColumnMenuProps = {
onAddViewField: (
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => void;
onClickOutside?: () => void;
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
} & ComponentProps<'div'>;
export const EntityTableColumnMenu = ({
onAddViewField,
onClickOutside = () => undefined,
viewFieldDefinitions,
...props
}: EntityTableColumnMenuProps) => {
const ref = useRef<HTMLDivElement>(null);
const theme = useTheme();
useListenClickOutside({
refs: [ref],
callback: onClickOutside,
});
return (
<StyledColumnMenu {...props} ref={ref}>
<DropdownMenuItemsContainer>
{viewFieldDefinitions.map((viewFieldDefinition) => (
<StyledColumnMenuItem key={viewFieldDefinition.id}>
{viewFieldDefinition.columnIcon &&
React.cloneElement(viewFieldDefinition.columnIcon, {
size: theme.icon.size.md,
})}
{viewFieldDefinition.columnLabel}
<StyledIconButtonGroup
className={styledIconButtonGroupClassName}
variant="transparent"
size="small"
>
<IconButton
icon={<IconPlus size={theme.icon.size.sm} />}
onClick={() => onAddViewField(viewFieldDefinition)}
/>
</StyledIconButtonGroup>
</StyledColumnMenuItem>
))}
</DropdownMenuItemsContainer>
</StyledColumnMenu>
);
};

View File

@ -1,25 +1,44 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
useRecoilCallback,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { IconPlus } from '@/ui/icon';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { useUpdateViewFieldMutation } from '~/generated/graphql'; import { GET_VIEW_FIELDS } from '@/views/queries/select';
import {
useCreateViewFieldMutation,
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import { toViewFieldInput } from '../hooks/useLoadView';
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState'; import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
import { viewFieldsState } from '../states/viewFieldsState'; import {
addableViewFieldDefinitionsState,
columnWidthByViewFieldIdState,
viewFieldsState,
} from '../states/viewFieldsState';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../types/ViewField';
import { ColumnHead } from './ColumnHead'; import { ColumnHead } from './ColumnHead';
import { EntityTableColumnMenu } from './EntityTableColumnMenu';
import { SelectAllCheckbox } from './SelectAllCheckbox'; import { SelectAllCheckbox } from './SelectAllCheckbox';
const COLUMN_MIN_WIDTH = 75; const COLUMN_MIN_WIDTH = 75;
const StyledColumnHeaderCell = styled.th<{ isResizing?: boolean }>` const StyledColumnHeaderCell = styled.th<{
min-width: ${COLUMN_MIN_WIDTH}px; columnWidth: number;
isResizing?: boolean;
}>`
${({ columnWidth }) => `
min-width: ${columnWidth}px;
width: ${columnWidth}px;
`}
position: relative; position: relative;
user-select: none; user-select: none;
${({ isResizing, theme }) => { ${({ isResizing, theme }) => {
@ -49,57 +68,48 @@ const StyledResizeHandler = styled.div`
z-index: 1; z-index: 1;
`; `;
export function EntityTableHeader() { const StyledAddIconButtonWrapper = styled.div`
const viewFields = useRecoilValue(viewFieldsState); display: inline-flex;
const setViewFields = useSetRecoilState(viewFieldsState); position: relative;
`;
const [updateViewFieldMutation] = useUpdateViewFieldMutation(); const StyledAddIconButton = styled(IconButton)`
const columnWidths = useMemo( border-radius: 0;
() => `;
viewFields.reduce<Record<string, number>>(
(result, viewField) => ({ const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)`
...result, position: absolute;
[viewField.id]: viewField.columnSize, right: 0;
}), top: 100%;
{}, z-index: ${({ theme }) => theme.lastLayerZIndex};
), `;
[viewFields],
export function EntityTableHeader() {
const theme = useTheme();
const [{ objectName, viewFields }, setViewFieldsState] =
useRecoilState(viewFieldsState);
const columnWidths = useRecoilValue(columnWidthByViewFieldIdState);
const addableViewFieldDefinitions = useRecoilValue(
addableViewFieldDefinitionsState,
); );
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
const [initialPointerPositionX, setInitialPointerPositionX] = useState< const [initialPointerPositionX, setInitialPointerPositionX] = useState<
number | null number | null
>(null); >(null);
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null); const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState); const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
const handleColumnResize = useCallback( const [createViewFieldMutation] = useCreateViewFieldMutation();
(resizedFieldId: string, width: number) => { const [updateViewFieldMutation] = useUpdateViewFieldMutation();
setViewFields((previousViewFields) =>
previousViewFields.map((viewField) =>
viewField.id === resizedFieldId
? { ...viewField, columnSize: width }
: viewField,
),
);
updateViewFieldMutation({
variables: {
data: { sizeInPx: width },
where: { id: resizedFieldId },
},
});
},
[setViewFields, updateViewFieldMutation],
);
const handleResizeHandlerStart = useCallback( const handleResizeHandlerStart = useCallback((positionX: number) => {
(positionX: number, _: number) => { setInitialPointerPositionX(positionX);
setInitialPointerPositionX(positionX); }, []);
},
[],
);
const handleResizeHandlerMove = useCallback( const handleResizeHandlerMove = useCallback(
(positionX: number, _positionY: number) => { (positionX: number) => {
if (!initialPointerPositionX) return; if (!initialPointerPositionX) return;
setOffset(positionX - initialPointerPositionX); setOffset(positionX - initialPointerPositionX);
}, },
@ -108,8 +118,9 @@ export function EntityTableHeader() {
const handleResizeHandlerEnd = useRecoilCallback( const handleResizeHandlerEnd = useRecoilCallback(
({ snapshot, set }) => ({ snapshot, set }) =>
(_positionX: number, _positionY: number) => { () => {
if (!resizedFieldId) return; if (!resizedFieldId) return;
const nextWidth = Math.round( const nextWidth = Math.round(
Math.max( Math.max(
columnWidths[resizedFieldId] + columnWidths[resizedFieldId] +
@ -119,13 +130,30 @@ export function EntityTableHeader() {
); );
if (nextWidth !== columnWidths[resizedFieldId]) { if (nextWidth !== columnWidths[resizedFieldId]) {
handleColumnResize(resizedFieldId, nextWidth); // 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,
),
}));
updateViewFieldMutation({
variables: {
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, handleColumnResize], [resizedFieldId, columnWidths, setResizedFieldId],
); );
useTrackPointer({ useTrackPointer({
@ -135,6 +163,29 @@ export function EntityTableHeader() {
onMouseUp: handleResizeHandlerEnd, onMouseUp: handleResizeHandlerEnd,
}); });
const toggleColumnMenu = useCallback(() => {
setIsColumnMenuOpen((previousValue) => !previousValue);
}, []);
const handleAddViewField = useCallback(
(viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>) => {
setIsColumnMenuOpen(false);
if (!objectName) return;
createViewFieldMutation({
variables: {
data: toViewFieldInput(objectName, {
...viewFieldDefinition,
columnOrder: viewFields.length + 1,
}),
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
},
[createViewFieldMutation, objectName, viewFields.length],
);
return ( return (
<thead> <thead>
<tr> <tr>
@ -150,15 +201,13 @@ export function EntityTableHeader() {
{viewFields.map((viewField) => ( {viewFields.map((viewField) => (
<StyledColumnHeaderCell <StyledColumnHeaderCell
key={viewField.columnOrder.toString()} key={viewField.id}
isResizing={resizedFieldId === viewField.id} isResizing={resizedFieldId === viewField.id}
style={{ columnWidth={Math.max(
width: Math.max( columnWidths[viewField.id] +
columnWidths[viewField.id] + (resizedFieldId === viewField.id ? offset : 0),
(resizedFieldId === viewField.id ? offset : 0), COLUMN_MIN_WIDTH,
COLUMN_MIN_WIDTH, )}
),
}}
> >
<ColumnHead <ColumnHead
viewName={viewField.columnLabel} viewName={viewField.columnLabel}
@ -173,7 +222,24 @@ export function EntityTableHeader() {
/> />
</StyledColumnHeaderCell> </StyledColumnHeaderCell>
))} ))}
<th></th> <th>
{addableViewFieldDefinitions.length > 0 && (
<StyledAddIconButtonWrapper>
<StyledAddIconButton
size="large"
icon={<IconPlus size={theme.icon.size.md} />}
onClick={toggleColumnMenu}
/>
{isColumnMenuOpen && (
<StyledEntityTableColumnMenu
onAddViewField={handleAddViewField}
onClickOutside={toggleColumnMenu}
viewFieldDefinitions={addableViewFieldDefinitions}
/>
)}
</StyledAddIconButtonWrapper>
)}
</th>
</tr> </tr>
</thead> </thead>
); );

View File

@ -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(viewFieldsState); const { viewFields } = useRecoilValue(viewFieldsState);
return ( return (
<StyledRow data-testid={`row-id-${rowId}`} selected={false}> <StyledRow data-testid={`row-id-${rowId}`} selected={false}>

View File

@ -10,7 +10,7 @@ import {
import { entityTableDimensionsState } from '../states/entityTableDimensionsState'; import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { viewFieldsState } from '../states/viewFieldsState'; import { viewFieldsState } from '../states/viewFieldsState';
import { import type {
ViewFieldDefinition, ViewFieldDefinition,
ViewFieldMetadata, ViewFieldMetadata,
ViewFieldTextMetadata, ViewFieldTextMetadata,
@ -22,6 +22,17 @@ const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = {
fieldName: '', fieldName: '',
}; };
export const toViewFieldInput = (
objectName: 'company' | 'person',
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => ({
fieldName: viewFieldDefinition.columnLabel,
index: viewFieldDefinition.columnOrder,
isVisible: true,
objectName,
sizeInPx: viewFieldDefinition.columnSize,
});
export const useLoadView = ({ export const useLoadView = ({
objectName, objectName,
viewFieldDefinitions, viewFieldDefinitions,
@ -32,7 +43,7 @@ export const useLoadView = ({
const setEntityTableDimensions = useSetRecoilState( const setEntityTableDimensions = useSetRecoilState(
entityTableDimensionsState, entityTableDimensionsState,
); );
const setViewFields = useSetRecoilState(viewFieldsState); const setViewFieldsState = useSetRecoilState(viewFieldsState);
const [createViewFieldsMutation] = useCreateViewFieldsMutation(); const [createViewFieldsMutation] = useCreateViewFieldsMutation();
@ -43,36 +54,33 @@ export const useLoadView = ({
}, },
onCompleted: (data) => { onCompleted: (data) => {
if (data.viewFields.length) { if (data.viewFields.length) {
setViewFields( const viewFields = data.viewFields.map<
data.viewFields.map<ViewFieldDefinition<ViewFieldMetadata>>( ViewFieldDefinition<ViewFieldMetadata>
(viewField) => ({ >((viewField) => ({
...(viewFieldDefinitions.find( ...(viewFieldDefinitions.find(
({ columnLabel }) => viewField.fieldName === columnLabel, ({ columnLabel }) => viewField.fieldName === columnLabel,
) || { metadata: DEFAULT_VIEW_FIELD_METADATA }), ) || { metadata: DEFAULT_VIEW_FIELD_METADATA }),
id: viewField.id, id: viewField.id,
columnLabel: viewField.fieldName, columnLabel: viewField.fieldName,
columnOrder: viewField.index, columnOrder: viewField.index,
columnSize: viewField.sizeInPx, columnSize: viewField.sizeInPx,
}), }));
),
); setViewFieldsState({ objectName, viewFields });
setEntityTableDimensions((prevState) => ({ setEntityTableDimensions((prevState) => ({
...prevState, ...prevState,
numberOfColumns: data.viewFields.length, numberOfColumns: data.viewFields.length,
})); }));
return; return;
} }
// Populate if empty // Populate if empty
createViewFieldsMutation({ createViewFieldsMutation({
variables: { variables: {
data: viewFieldDefinitions.map((viewFieldDefinition) => ({ data: viewFieldDefinitions.map((viewFieldDefinition) =>
fieldName: viewFieldDefinition.columnLabel, toViewFieldInput(objectName, viewFieldDefinition),
index: viewFieldDefinition.columnOrder, ),
isVisible: true,
objectName,
sizeInPx: viewFieldDefinition.columnSize,
})),
}, },
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''], refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
}); });

View File

@ -1,8 +1,49 @@
import { atom } from 'recoil'; import { atom, selector } from 'recoil';
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField'; import { companyViewFields } from '@/companies/constants/companyViewFields';
import { peopleViewFields } from '@/people/constants/peopleViewFields';
export const viewFieldsState = atom<ViewFieldDefinition<ViewFieldMetadata>[]>({ import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../types/ViewField';
export const viewFieldsState = atom<{
objectName: 'company' | 'person' | '';
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
}>({
key: 'viewFieldsState', key: 'viewFieldsState',
default: [], 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),
);
},
}); });

View File

@ -1,5 +1,17 @@
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) {

View File

@ -25,12 +25,33 @@ import {
import { UserAbility } from 'src/decorators/user-ability.decorator'; import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AbilityGuard } from 'src/guards/ability.guard'; import { AbilityGuard } from 'src/guards/ability.guard';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { CreateOneViewFieldArgs } from 'src/core/@generated/view-field/create-one-view-field.args';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Resolver(() => ViewField) @Resolver(() => ViewField)
export class ViewFieldResolver { export class ViewFieldResolver {
constructor(private readonly viewFieldService: ViewFieldService) {} constructor(private readonly viewFieldService: ViewFieldService) {}
@Mutation(() => ViewField, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateViewFieldAbilityHandler)
async createOneViewField(
@Args() args: CreateOneViewFieldArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'ViewField' })
prismaSelect: PrismaSelect<'ViewField'>,
): Promise<Partial<ViewField>> {
return this.viewFieldService.create({
data: {
...args.data,
workspace: { connect: { id: workspace.id } },
},
select: prismaSelect.value,
});
}
@Mutation(() => AffectedRows) @Mutation(() => AffectedRows)
@UseGuards(AbilityGuard) @UseGuards(AbilityGuard)
@CheckAbilities(CreateViewFieldAbilityHandler) @CheckAbilities(CreateViewFieldAbilityHandler)