feat: add table columns (#1056)
* feat: add table columns Closes #879 * refactor: ComponentProps first
This commit is contained in:
@ -905,6 +905,7 @@ export type Mutation = {
|
||||
createOneCompany: Company;
|
||||
createOnePerson: Person;
|
||||
createOnePipelineProgress: PipelineProgress;
|
||||
createOneViewField: ViewField;
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteManyActivities: AffectedRows;
|
||||
deleteManyCompany: AffectedRows;
|
||||
@ -980,6 +981,11 @@ export type MutationCreateOnePipelineProgressArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneViewFieldArgs = {
|
||||
data: ViewFieldCreateInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteManyActivitiesArgs = {
|
||||
where?: InputMaybe<ActivityWhereInput>;
|
||||
};
|
||||
@ -2137,6 +2143,15 @@ export type ViewField = {
|
||||
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 = {
|
||||
fieldName: 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 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<{
|
||||
data: Array<ViewFieldCreateManyInput> | ViewFieldCreateManyInput;
|
||||
}>;
|
||||
@ -5128,6 +5150,43 @@ export function useDeleteUserAccountMutation(baseOptions?: Apollo.MutationHookOp
|
||||
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
|
||||
export type DeleteUserAccountMutationResult = Apollo.MutationResult<DeleteUserAccountMutation>;
|
||||
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`
|
||||
mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) {
|
||||
createManyViewField(data: $data) {
|
||||
|
||||
@ -13,18 +13,21 @@ export function CompanyTableMockData() {
|
||||
const setEntityTableDimensions = useSetRecoilState(
|
||||
entityTableDimensionsState,
|
||||
);
|
||||
const setViewFields = useSetRecoilState(viewFieldsState);
|
||||
const setViewFieldsState = useSetRecoilState(viewFieldsState);
|
||||
const setEntityTableData = useSetEntityTableData();
|
||||
|
||||
setEntityTableData(mockedCompaniesData, []);
|
||||
|
||||
useEffect(() => {
|
||||
setViewFields(companyViewFields);
|
||||
setViewFieldsState({
|
||||
objectName: 'company',
|
||||
viewFields: companyViewFields,
|
||||
});
|
||||
setEntityTableDimensions((prevState) => ({
|
||||
...prevState,
|
||||
numberOfColumns: companyViewFields.length,
|
||||
}));
|
||||
}, [setEntityTableDimensions, setViewFields]);
|
||||
}, [setEntityTableDimensions, setViewFieldsState]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import React, { type ComponentProps } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconButtonSize, IconButtonVariant } from './IconButton';
|
||||
import type { IconButtonSize, IconButtonVariant } from './IconButton';
|
||||
|
||||
const StyledIconButtonGroupContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
background: ${({ theme }) => theme.background.transparent.primary};
|
||||
@ -11,24 +12,27 @@ const StyledIconButtonGroupContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(0.5)};
|
||||
`;
|
||||
|
||||
type IconButtonGroupProps = {
|
||||
type IconButtonGroupProps = Omit<ComponentProps<'div'>, 'children'> & {
|
||||
variant: IconButtonVariant;
|
||||
size: IconButtonSize;
|
||||
children: React.ReactElement[];
|
||||
children: React.ReactElement | React.ReactElement[];
|
||||
};
|
||||
|
||||
export function IconButtonGroup({
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: IconButtonGroupProps) {
|
||||
return (
|
||||
<StyledIconButtonGroupContainer>
|
||||
{React.Children.map(children, (child) =>
|
||||
React.cloneElement(child, {
|
||||
...(variant ? { variant } : {}),
|
||||
...(size ? { size } : {}),
|
||||
}),
|
||||
<StyledIconButtonGroupContainer {...props}>
|
||||
{React.Children.map(
|
||||
Array.isArray(children) ? children : [children],
|
||||
(child) =>
|
||||
React.cloneElement(child, {
|
||||
...(variant ? { variant } : {}),
|
||||
...(size ? { size } : {}),
|
||||
}),
|
||||
)}
|
||||
</StyledIconButtonGroupContainer>
|
||||
);
|
||||
|
||||
@ -20,7 +20,6 @@ const StyledTable = styled.table`
|
||||
margin-left: ${({ theme }) => theme.table.horizontalCellMargin};
|
||||
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
|
||||
table-layout: fixed;
|
||||
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
|
||||
|
||||
th {
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
@ -37,7 +36,7 @@ const StyledTable = styled.table`
|
||||
border-right-color: transparent;
|
||||
}
|
||||
:last-of-type {
|
||||
min-width: 0;
|
||||
min-width: fit-content;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@ -58,7 +57,7 @@ const StyledTable = styled.table`
|
||||
border-right-color: transparent;
|
||||
}
|
||||
:last-of-type {
|
||||
min-width: 0;
|
||||
min-width: fit-content;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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 {
|
||||
useRecoilCallback,
|
||||
useRecoilState,
|
||||
useRecoilValue,
|
||||
useSetRecoilState,
|
||||
} from 'recoil';
|
||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { IconPlus } from '@/ui/icon';
|
||||
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 { viewFieldsState } from '../states/viewFieldsState';
|
||||
import {
|
||||
addableViewFieldDefinitionsState,
|
||||
columnWidthByViewFieldIdState,
|
||||
viewFieldsState,
|
||||
} from '../states/viewFieldsState';
|
||||
import type {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '../types/ViewField';
|
||||
|
||||
import { ColumnHead } from './ColumnHead';
|
||||
import { EntityTableColumnMenu } from './EntityTableColumnMenu';
|
||||
import { SelectAllCheckbox } from './SelectAllCheckbox';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 75;
|
||||
|
||||
const StyledColumnHeaderCell = styled.th<{ isResizing?: boolean }>`
|
||||
min-width: ${COLUMN_MIN_WIDTH}px;
|
||||
const StyledColumnHeaderCell = styled.th<{
|
||||
columnWidth: number;
|
||||
isResizing?: boolean;
|
||||
}>`
|
||||
${({ columnWidth }) => `
|
||||
min-width: ${columnWidth}px;
|
||||
width: ${columnWidth}px;
|
||||
`}
|
||||
position: relative;
|
||||
user-select: none;
|
||||
${({ isResizing, theme }) => {
|
||||
@ -49,57 +68,48 @@ const StyledResizeHandler = styled.div`
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export function EntityTableHeader() {
|
||||
const viewFields = useRecoilValue(viewFieldsState);
|
||||
const setViewFields = useSetRecoilState(viewFieldsState);
|
||||
const StyledAddIconButtonWrapper = styled.div`
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
const columnWidths = useMemo(
|
||||
() =>
|
||||
viewFields.reduce<Record<string, number>>(
|
||||
(result, viewField) => ({
|
||||
...result,
|
||||
[viewField.id]: viewField.columnSize,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
[viewFields],
|
||||
const StyledAddIconButton = styled(IconButton)`
|
||||
border-radius: 0;
|
||||
`;
|
||||
|
||||
const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)`
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
||||
`;
|
||||
|
||||
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<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
|
||||
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
|
||||
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
|
||||
|
||||
const handleColumnResize = useCallback(
|
||||
(resizedFieldId: string, width: number) => {
|
||||
setViewFields((previousViewFields) =>
|
||||
previousViewFields.map((viewField) =>
|
||||
viewField.id === resizedFieldId
|
||||
? { ...viewField, columnSize: width }
|
||||
: viewField,
|
||||
),
|
||||
);
|
||||
updateViewFieldMutation({
|
||||
variables: {
|
||||
data: { sizeInPx: width },
|
||||
where: { id: resizedFieldId },
|
||||
},
|
||||
});
|
||||
},
|
||||
[setViewFields, updateViewFieldMutation],
|
||||
);
|
||||
const [createViewFieldMutation] = useCreateViewFieldMutation();
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
|
||||
const handleResizeHandlerStart = useCallback(
|
||||
(positionX: number, _: number) => {
|
||||
setInitialPointerPositionX(positionX);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const handleResizeHandlerStart = useCallback((positionX: number) => {
|
||||
setInitialPointerPositionX(positionX);
|
||||
}, []);
|
||||
|
||||
const handleResizeHandlerMove = useCallback(
|
||||
(positionX: number, _positionY: number) => {
|
||||
(positionX: number) => {
|
||||
if (!initialPointerPositionX) return;
|
||||
setOffset(positionX - initialPointerPositionX);
|
||||
},
|
||||
@ -108,8 +118,9 @@ export function EntityTableHeader() {
|
||||
|
||||
const handleResizeHandlerEnd = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(_positionX: number, _positionY: number) => {
|
||||
() => {
|
||||
if (!resizedFieldId) return;
|
||||
|
||||
const nextWidth = Math.round(
|
||||
Math.max(
|
||||
columnWidths[resizedFieldId] +
|
||||
@ -119,13 +130,30 @@ export function EntityTableHeader() {
|
||||
);
|
||||
|
||||
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);
|
||||
setInitialPointerPositionX(null);
|
||||
setResizedFieldId(null);
|
||||
},
|
||||
[resizedFieldId, columnWidths, setResizedFieldId, handleColumnResize],
|
||||
[resizedFieldId, columnWidths, setResizedFieldId],
|
||||
);
|
||||
|
||||
useTrackPointer({
|
||||
@ -135,6 +163,29 @@ export function EntityTableHeader() {
|
||||
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 (
|
||||
<thead>
|
||||
<tr>
|
||||
@ -150,15 +201,13 @@ export function EntityTableHeader() {
|
||||
|
||||
{viewFields.map((viewField) => (
|
||||
<StyledColumnHeaderCell
|
||||
key={viewField.columnOrder.toString()}
|
||||
key={viewField.id}
|
||||
isResizing={resizedFieldId === viewField.id}
|
||||
style={{
|
||||
width: Math.max(
|
||||
columnWidths[viewField.id] +
|
||||
(resizedFieldId === viewField.id ? offset : 0),
|
||||
COLUMN_MIN_WIDTH,
|
||||
),
|
||||
}}
|
||||
columnWidth={Math.max(
|
||||
columnWidths[viewField.id] +
|
||||
(resizedFieldId === viewField.id ? offset : 0),
|
||||
COLUMN_MIN_WIDTH,
|
||||
)}
|
||||
>
|
||||
<ColumnHead
|
||||
viewName={viewField.columnLabel}
|
||||
@ -173,7 +222,24 @@ export function EntityTableHeader() {
|
||||
/>
|
||||
</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>
|
||||
</thead>
|
||||
);
|
||||
|
||||
@ -13,7 +13,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
|
||||
`;
|
||||
|
||||
export function EntityTableRow({ rowId }: { rowId: string }) {
|
||||
const viewFields = useRecoilValue(viewFieldsState);
|
||||
const { viewFields } = useRecoilValue(viewFieldsState);
|
||||
|
||||
return (
|
||||
<StyledRow data-testid={`row-id-${rowId}`} selected={false}>
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
|
||||
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
|
||||
import { viewFieldsState } from '../states/viewFieldsState';
|
||||
import {
|
||||
import type {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldTextMetadata,
|
||||
@ -22,6 +22,17 @@ const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = {
|
||||
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 = ({
|
||||
objectName,
|
||||
viewFieldDefinitions,
|
||||
@ -32,7 +43,7 @@ export const useLoadView = ({
|
||||
const setEntityTableDimensions = useSetRecoilState(
|
||||
entityTableDimensionsState,
|
||||
);
|
||||
const setViewFields = useSetRecoilState(viewFieldsState);
|
||||
const setViewFieldsState = useSetRecoilState(viewFieldsState);
|
||||
|
||||
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
|
||||
|
||||
@ -43,36 +54,33 @@ export const useLoadView = ({
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data.viewFields.length) {
|
||||
setViewFields(
|
||||
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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
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,
|
||||
}));
|
||||
|
||||
setViewFieldsState({ objectName, viewFields });
|
||||
setEntityTableDimensions((prevState) => ({
|
||||
...prevState,
|
||||
numberOfColumns: data.viewFields.length,
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate if empty
|
||||
createViewFieldsMutation({
|
||||
variables: {
|
||||
data: viewFieldDefinitions.map((viewFieldDefinition) => ({
|
||||
fieldName: viewFieldDefinition.columnLabel,
|
||||
index: viewFieldDefinition.columnOrder,
|
||||
isVisible: true,
|
||||
objectName,
|
||||
sizeInPx: viewFieldDefinition.columnSize,
|
||||
})),
|
||||
data: viewFieldDefinitions.map((viewFieldDefinition) =>
|
||||
toViewFieldInput(objectName, viewFieldDefinition),
|
||||
),
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
|
||||
});
|
||||
|
||||
@ -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',
|
||||
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),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
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`
|
||||
mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) {
|
||||
createManyViewField(data: $data) {
|
||||
|
||||
@ -25,12 +25,33 @@ import {
|
||||
import { UserAbility } from 'src/decorators/user-ability.decorator';
|
||||
import { AbilityGuard } from 'src/guards/ability.guard';
|
||||
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
|
||||
import { CreateOneViewFieldArgs } from 'src/core/@generated/view-field/create-one-view-field.args';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Resolver(() => ViewField)
|
||||
export class ViewFieldResolver {
|
||||
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)
|
||||
@UseGuards(AbilityGuard)
|
||||
@CheckAbilities(CreateViewFieldAbilityHandler)
|
||||
|
||||
Reference in New Issue
Block a user