From 417ca3d131be47d47d831464e250faef0ae00a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Fri, 4 Aug 2023 19:07:31 +0200 Subject: [PATCH] feat: add table columns (#1056) * feat: add table columns Closes #879 * refactor: ComponentProps first --- front/src/generated/graphql.tsx | 59 ++++++ .../table/components/CompanyTableMockData.tsx | 9 +- .../ui/button/components/IconButtonGroup.tsx | 24 ++- .../ui/table/components/EntityTable.tsx | 5 +- .../components/EntityTableColumnMenu.tsx | 86 ++++++++ .../ui/table/components/EntityTableHeader.tsx | 194 ++++++++++++------ .../ui/table/components/EntityTableRow.tsx | 2 +- .../src/modules/ui/table/hooks/useLoadView.ts | 52 +++-- .../ui/table/states/viewFieldsState.ts | 49 ++++- front/src/modules/views/queries/create.ts | 12 ++ .../view/resolvers/view-field.resolver.ts | 21 ++ 11 files changed, 406 insertions(+), 107 deletions(-) create mode 100644 front/src/modules/ui/table/components/EntityTableColumnMenu.tsx diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index a34a84150..1c837b0b2 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -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; }; @@ -2137,6 +2143,15 @@ export type ViewField = { sizeInPx: Scalars['Int']; }; +export type ViewFieldCreateInput = { + fieldName: Scalars['String']; + id?: InputMaybe; + index: Scalars['Int']; + isVisible: Scalars['Boolean']; + objectName: Scalars['String']; + sizeInPx: Scalars['Int']; +}; + export type ViewFieldCreateManyInput = { fieldName: Scalars['String']; id?: InputMaybe; @@ -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; }>; @@ -5128,6 +5150,43 @@ export function useDeleteUserAccountMutation(baseOptions?: Apollo.MutationHookOp export type DeleteUserAccountMutationHookResult = ReturnType; export type DeleteUserAccountMutationResult = Apollo.MutationResult; export type DeleteUserAccountMutationOptions = Apollo.BaseMutationOptions; +export const CreateViewFieldDocument = gql` + mutation CreateViewField($data: ViewFieldCreateInput!) { + createOneViewField(data: $data) { + id + fieldName + isVisible + sizeInPx + index + } +} + `; +export type CreateViewFieldMutationFn = Apollo.MutationFunction; + +/** + * __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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateViewFieldDocument, options); + } +export type CreateViewFieldMutationHookResult = ReturnType; +export type CreateViewFieldMutationResult = Apollo.MutationResult; +export type CreateViewFieldMutationOptions = Apollo.BaseMutationOptions; export const CreateViewFieldsDocument = gql` mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) { createManyViewField(data: $data) { diff --git a/front/src/modules/companies/table/components/CompanyTableMockData.tsx b/front/src/modules/companies/table/components/CompanyTableMockData.tsx index 5cd168665..f28c26c43 100644 --- a/front/src/modules/companies/table/components/CompanyTableMockData.tsx +++ b/front/src/modules/companies/table/components/CompanyTableMockData.tsx @@ -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 <>; } diff --git a/front/src/modules/ui/button/components/IconButtonGroup.tsx b/front/src/modules/ui/button/components/IconButtonGroup.tsx index d5fd8d533..5fe4cbc32 100644 --- a/front/src/modules/ui/button/components/IconButtonGroup.tsx +++ b/front/src/modules/ui/button/components/IconButtonGroup.tsx @@ -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, 'children'> & { variant: IconButtonVariant; size: IconButtonSize; - children: React.ReactElement[]; + children: React.ReactElement | React.ReactElement[]; }; export function IconButtonGroup({ children, variant, size, + ...props }: IconButtonGroupProps) { return ( - - {React.Children.map(children, (child) => - React.cloneElement(child, { - ...(variant ? { variant } : {}), - ...(size ? { size } : {}), - }), + + {React.Children.map( + Array.isArray(children) ? children : [children], + (child) => + React.cloneElement(child, { + ...(variant ? { variant } : {}), + ...(size ? { size } : {}), + }), )} ); diff --git a/front/src/modules/ui/table/components/EntityTable.tsx b/front/src/modules/ui/table/components/EntityTable.tsx index c0fb0051c..e7b6da1aa 100644 --- a/front/src/modules/ui/table/components/EntityTable.tsx +++ b/front/src/modules/ui/table/components/EntityTable.tsx @@ -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%; } } diff --git a/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx b/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx new file mode 100644 index 000000000..244506674 --- /dev/null +++ b/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx @@ -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, + ) => void; + onClickOutside?: () => void; + viewFieldDefinitions: ViewFieldDefinition[]; +} & ComponentProps<'div'>; + +export const EntityTableColumnMenu = ({ + onAddViewField, + onClickOutside = () => undefined, + viewFieldDefinitions, + ...props +}: EntityTableColumnMenuProps) => { + const ref = useRef(null); + const theme = useTheme(); + + useListenClickOutside({ + refs: [ref], + callback: onClickOutside, + }); + + return ( + + + {viewFieldDefinitions.map((viewFieldDefinition) => ( + + {viewFieldDefinition.columnIcon && + React.cloneElement(viewFieldDefinition.columnIcon, { + size: theme.icon.size.md, + })} + {viewFieldDefinition.columnLabel} + + } + onClick={() => onAddViewField(viewFieldDefinition)} + /> + + + ))} + + + ); +}; diff --git a/front/src/modules/ui/table/components/EntityTableHeader.tsx b/front/src/modules/ui/table/components/EntityTableHeader.tsx index 58d30d8f7..02f212d08 100644 --- a/front/src/modules/ui/table/components/EntityTableHeader.tsx +++ b/front/src/modules/ui/table/components/EntityTableHeader.tsx @@ -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>( - (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(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) => { + 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 ( @@ -150,15 +201,13 @@ export function EntityTableHeader() { {viewFields.map((viewField) => ( ))} - + + {addableViewFieldDefinitions.length > 0 && ( + + } + onClick={toggleColumnMenu} + /> + {isColumnMenuOpen && ( + + )} + + )} + ); diff --git a/front/src/modules/ui/table/components/EntityTableRow.tsx b/front/src/modules/ui/table/components/EntityTableRow.tsx index 8a566221c..fdccf9379 100644 --- a/front/src/modules/ui/table/components/EntityTableRow.tsx +++ b/front/src/modules/ui/table/components/EntityTableRow.tsx @@ -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 ( diff --git a/front/src/modules/ui/table/hooks/useLoadView.ts b/front/src/modules/ui/table/hooks/useLoadView.ts index df8381ef0..900908c88 100644 --- a/front/src/modules/ui/table/hooks/useLoadView.ts +++ b/front/src/modules/ui/table/hooks/useLoadView.ts @@ -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, +) => ({ + 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>( - (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 + >((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) ?? ''], }); diff --git a/front/src/modules/ui/table/states/viewFieldsState.ts b/front/src/modules/ui/table/states/viewFieldsState.ts index d78ddc5ee..327857372 100644 --- a/front/src/modules/ui/table/states/viewFieldsState.ts +++ b/front/src/modules/ui/table/states/viewFieldsState.ts @@ -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[]>({ +import type { + ViewFieldDefinition, + ViewFieldMetadata, +} from '../types/ViewField'; + +export const viewFieldsState = atom<{ + objectName: 'company' | 'person' | ''; + viewFields: ViewFieldDefinition[]; +}>({ key: 'viewFieldsState', - default: [], + default: { objectName: '', viewFields: [] }, +}); + +export const columnWidthByViewFieldIdState = selector({ + key: 'columnWidthByViewFieldIdState', + get: ({ get }) => + get(viewFieldsState).viewFields.reduce>( + (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), + ); + }, }); diff --git a/front/src/modules/views/queries/create.ts b/front/src/modules/views/queries/create.ts index a91b4264d..a99ada2fd 100644 --- a/front/src/modules/views/queries/create.ts +++ b/front/src/modules/views/queries/create.ts @@ -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) { diff --git a/server/src/core/view/resolvers/view-field.resolver.ts b/server/src/core/view/resolvers/view-field.resolver.ts index 5d0022037..ea9a94cdc 100644 --- a/server/src/core/view/resolvers/view-field.resolver.ts +++ b/server/src/core/view/resolvers/view-field.resolver.ts @@ -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> { + return this.viewFieldService.create({ + data: { + ...args.data, + workspace: { connect: { id: workspace.id } }, + }, + select: prismaSelect.value, + }); + } + @Mutation(() => AffectedRows) @UseGuards(AbilityGuard) @CheckAbilities(CreateViewFieldAbilityHandler)