diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index a98084a53a95..da56f744b84c 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -884,6 +884,7 @@ export type Mutation = { __typename?: 'Mutation'; challenge: LoginToken; createEvent: Analytics; + createManyViewField: AffectedRows; createOneActivity: Activity; createOneComment: Comment; createOneCompany: Company; @@ -927,6 +928,12 @@ export type MutationCreateEventArgs = { }; +export type MutationCreateManyViewFieldArgs = { + data: Array; + skipDuplicates?: InputMaybe; +}; + + export type MutationCreateOneActivityArgs = { data: ActivityCreateInput; }; @@ -2059,6 +2066,15 @@ export type ViewField = { sizeInPx: Scalars['Int']; }; +export type ViewFieldCreateManyInput = { + fieldName: Scalars['String']; + id?: InputMaybe; + index: Scalars['Int']; + isVisible: Scalars['Boolean']; + objectName: Scalars['String']; + sizeInPx: Scalars['Int']; +}; + export type ViewFieldOrderByWithRelationInput = { fieldName?: InputMaybe; id?: InputMaybe; @@ -2597,8 +2613,16 @@ export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; } export type DeleteUserAccountMutation = { __typename?: 'Mutation', deleteUserAccount: { __typename?: 'User', id: string } }; +export type CreateViewFieldsMutationVariables = Exact<{ + data: Array | ViewFieldCreateManyInput; +}>; + + +export type CreateViewFieldsMutation = { __typename?: 'Mutation', createManyViewField: { __typename?: 'AffectedRows', count: number } }; + export type GetViewFieldsQueryVariables = Exact<{ where?: InputMaybe; + orderBy?: InputMaybe | ViewFieldOrderByWithRelationInput>; }>; @@ -4840,9 +4864,42 @@ export function useDeleteUserAccountMutation(baseOptions?: Apollo.MutationHookOp export type DeleteUserAccountMutationHookResult = ReturnType; export type DeleteUserAccountMutationResult = Apollo.MutationResult; export type DeleteUserAccountMutationOptions = Apollo.BaseMutationOptions; +export const CreateViewFieldsDocument = gql` + mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) { + createManyViewField(data: $data) { + count + } +} + `; +export type CreateViewFieldsMutationFn = Apollo.MutationFunction; + +/** + * __useCreateViewFieldsMutation__ + * + * To run a mutation, you first call `useCreateViewFieldsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateViewFieldsMutation` 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 [createViewFieldsMutation, { data, loading, error }] = useCreateViewFieldsMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useCreateViewFieldsMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateViewFieldsDocument, options); + } +export type CreateViewFieldsMutationHookResult = ReturnType; +export type CreateViewFieldsMutationResult = Apollo.MutationResult; +export type CreateViewFieldsMutationOptions = Apollo.BaseMutationOptions; export const GetViewFieldsDocument = gql` - query GetViewFields($where: ViewFieldWhereInput) { - viewFields: findManyViewField(where: $where) { + query GetViewFields($where: ViewFieldWhereInput, $orderBy: [ViewFieldOrderByWithRelationInput!]) { + viewFields: findManyViewField(where: $where, orderBy: $orderBy) { id fieldName isVisible @@ -4865,6 +4922,7 @@ export const GetViewFieldsDocument = gql` * const { data, loading, error } = useGetViewFieldsQuery({ * variables: { * where: // value for 'where' + * orderBy: // value for 'orderBy' * }, * }); */ diff --git a/front/src/modules/companies/table/components/CompanyTable.tsx b/front/src/modules/companies/table/components/CompanyTable.tsx index 8a38101cfdd4..df38b6e2f082 100644 --- a/front/src/modules/companies/table/components/CompanyTable.tsx +++ b/front/src/modules/companies/table/components/CompanyTable.tsx @@ -35,11 +35,12 @@ export function CompanyTable() { return ( <> { + setViewFields(companyViewFields); + setEntityTableDimensions((prevState) => ({ + ...prevState, + numberOfColumns: companyViewFields.length, + })); + }, [setEntityTableDimensions, setViewFields]); return <>; } diff --git a/front/src/modules/people/table/components/PeopleTable.tsx b/front/src/modules/people/table/components/PeopleTable.tsx index 5b0cfca63ec4..ab80fabd435c 100644 --- a/front/src/modules/people/table/components/PeopleTable.tsx +++ b/front/src/modules/people/table/components/PeopleTable.tsx @@ -36,11 +36,12 @@ export function PeopleTable() { return ( <> ({ useUpdateEntityMutation, }: OwnProps) { const viewFields = useRecoilValue(viewFieldsFamilyState); + const setViewFields = useSetRecoilState(viewFieldsFamilyState); - const tableBodyRef = React.useRef(null); + const [updateViewFieldMutation] = useUpdateViewFieldMutation(); + + const tableBodyRef = useRef(null); useMapKeyboardToSoftFocus(); @@ -116,6 +120,25 @@ export function EntityTable({ }, }); + 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], + ); + return ( @@ -129,7 +152,10 @@ export function EntityTable({ {viewFields.length > 0 && ( - + )} diff --git a/front/src/modules/ui/table/components/EntityTableHeader.tsx b/front/src/modules/ui/table/components/EntityTableHeader.tsx index 53ebf7dff2aa..1f14827d5998 100644 --- a/front/src/modules/ui/table/components/EntityTableHeader.tsx +++ b/front/src/modules/ui/table/components/EntityTableHeader.tsx @@ -1,7 +1,10 @@ -import { PointerEvent, useCallback, useState } from 'react'; +import { PointerEvent, useCallback, useMemo, useState } from 'react'; import styled from '@emotion/styled'; -import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField'; +import type { + ViewFieldDefinition, + ViewFieldMetadata, +} from '../types/ViewField'; import { ColumnHead } from './ColumnHead'; import { SelectAllCheckbox } from './SelectAllCheckbox'; @@ -40,18 +43,22 @@ const StyledResizeHandler = styled.div` `; type OwnProps = { + onColumnResize: (resizedFieldId: string, width: number) => void; viewFields: ViewFieldDefinition[]; }; -export function EntityTableHeader({ viewFields }: OwnProps) { - const initialColumnWidths = viewFields.reduce>( - (result, viewField) => ({ - ...result, - [viewField.id]: viewField.columnSize, - }), - {}, +export function EntityTableHeader({ onColumnResize, viewFields }: OwnProps) { + const columnWidths = useMemo( + () => + viewFields.reduce>( + (result, viewField) => ({ + ...result, + [viewField.id]: viewField.columnSize, + }), + {}, + ), + [viewFields], ); - const [columnWidths, setColumnWidths] = useState(initialColumnWidths); const [isResizing, setIsResizing] = useState(false); const [initialPointerPositionX, setInitialPointerPositionX] = useState< number | null @@ -82,16 +89,16 @@ export function EntityTableHeader({ viewFields }: OwnProps) { setIsResizing(false); if (!resizedFieldId) return; - const newColumnWidths = { - ...columnWidths, - [resizedFieldId]: Math.max( - columnWidths[resizedFieldId] + offset, - COLUMN_MIN_WIDTH, - ), - }; - setColumnWidths(newColumnWidths); + const nextWidth = Math.round( + Math.max(columnWidths[resizedFieldId] + offset, COLUMN_MIN_WIDTH), + ); + + if (nextWidth !== columnWidths[resizedFieldId]) { + onColumnResize(resizedFieldId, nextWidth); + } + setOffset(0); - }, [offset, setIsResizing, columnWidths, resizedFieldId]); + }, [resizedFieldId, columnWidths, offset, onColumnResize]); return ( diff --git a/front/src/modules/ui/table/components/GenericEntityTableData.tsx b/front/src/modules/ui/table/components/GenericEntityTableData.tsx index af81edf6176c..2c4be00ab56e 100644 --- a/front/src/modules/ui/table/components/GenericEntityTableData.tsx +++ b/front/src/modules/ui/table/components/GenericEntityTableData.tsx @@ -1,3 +1,4 @@ +import { defaultOrderBy } from '@/people/queries'; import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition'; import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData'; import { @@ -5,31 +6,35 @@ import { ViewFieldMetadata, } from '@/ui/table/types/ViewField'; -import { defaultOrderBy } from '../../../people/queries'; +import { useLoadView } from '../hooks/useLoadView'; export function GenericEntityTableData({ + objectName, useGetRequest, getRequestResultKey, orderBy = defaultOrderBy, whereFilters, - viewFields, + viewFieldDefinitions, filterDefinitionArray, }: { + objectName: 'company' | 'person'; useGetRequest: any; getRequestResultKey: string; orderBy?: any; whereFilters?: any; - viewFields: ViewFieldDefinition[]; + viewFieldDefinitions: ViewFieldDefinition[]; filterDefinitionArray: FilterDefinition[]; }) { const setEntityTableData = useSetEntityTableData(); + useLoadView({ objectName, viewFieldDefinitions }); + useGetRequest({ variables: { orderBy, where: whereFilters }, onCompleted: (data: any) => { const entities = data[getRequestResultKey] ?? []; - setEntityTableData(entities, viewFields, filterDefinitionArray); + setEntityTableData(entities, filterDefinitionArray); }, }); diff --git a/front/src/modules/ui/table/hooks/useLoadView.ts b/front/src/modules/ui/table/hooks/useLoadView.ts new file mode 100644 index 000000000000..6d1b38e60505 --- /dev/null +++ b/front/src/modules/ui/table/hooks/useLoadView.ts @@ -0,0 +1,81 @@ +import { getOperationName } from '@apollo/client/utilities'; +import { useSetRecoilState } from 'recoil'; + +import { GET_VIEW_FIELDS } from '@/views/queries/select'; +import { + SortOrder, + useCreateViewFieldsMutation, + useGetViewFieldsQuery, +} from '~/generated/graphql'; + +import { entityTableDimensionsState } from '../states/entityTableDimensionsState'; +import { viewFieldsFamilyState } from '../states/viewFieldsState'; +import { + ViewFieldDefinition, + ViewFieldMetadata, + ViewFieldTextMetadata, +} from '../types/ViewField'; + +const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = { + type: 'text', + placeHolder: '', + fieldName: '', +}; + +export const useLoadView = ({ + objectName, + viewFieldDefinitions, +}: { + objectName: 'company' | 'person'; + viewFieldDefinitions: ViewFieldDefinition[]; +}) => { + const setEntityTableDimensions = useSetRecoilState( + entityTableDimensionsState, + ); + const setViewFields = useSetRecoilState(viewFieldsFamilyState); + + const [createViewFieldsMutation] = useCreateViewFieldsMutation(); + + useGetViewFieldsQuery({ + variables: { + orderBy: { index: SortOrder.Asc }, + where: { objectName: { equals: objectName } }, + }, + 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, + }), + ), + ); + 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, + })), + }, + refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''], + }); + }, + }); +}; diff --git a/front/src/modules/ui/table/hooks/useSetEntityTableData.ts b/front/src/modules/ui/table/hooks/useSetEntityTableData.ts index 70f75806ba9a..69765571c3c4 100644 --- a/front/src/modules/ui/table/hooks/useSetEntityTableData.ts +++ b/front/src/modules/ui/table/hooks/useSetEntityTableData.ts @@ -8,11 +8,6 @@ import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEnti import { TableContext } from '@/ui/table/states/TableContext'; import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState'; import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; -import { viewFieldsFamilyState } from '@/ui/table/states/viewFieldsState'; -import { - ViewFieldDefinition, - ViewFieldMetadata, -} from '@/ui/table/types/ViewField'; import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; export function useSetEntityTableData() { @@ -24,7 +19,6 @@ export function useSetEntityTableData() { ({ set, snapshot }) => ( newEntityArray: T[], - viewFields: ViewFieldDefinition[], filters: FilterDefinition[], ) => { for (const entity of newEntityArray) { @@ -49,15 +43,13 @@ export function useSetEntityTableData() { resetTableRowSelection(); - set(entityTableDimensionsState, { - numberOfColumns: viewFields.length, + set(entityTableDimensionsState, (prevState) => ({ + ...prevState, numberOfRows: entityIds.length, - }); + })); set(availableFiltersScopedState(tableContextScopeId), filters); - set(viewFieldsFamilyState, viewFields); - set(isFetchingEntityTableDataState, false); }, [], diff --git a/front/src/modules/views/queries/create.ts b/front/src/modules/views/queries/create.ts new file mode 100644 index 000000000000..a91b4264d84a --- /dev/null +++ b/front/src/modules/views/queries/create.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const CREATE_VIEW_FIELDS = gql` + mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) { + createManyViewField(data: $data) { + count + } + } +`; diff --git a/front/src/modules/views/queries/select.ts b/front/src/modules/views/queries/select.ts index 2341c0722cd0..735405c48233 100644 --- a/front/src/modules/views/queries/select.ts +++ b/front/src/modules/views/queries/select.ts @@ -1,8 +1,11 @@ import { gql } from '@apollo/client'; export const GET_VIEW_FIELDS = gql` - query GetViewFields($where: ViewFieldWhereInput) { - viewFields: findManyViewField(where: $where) { + query GetViewFields( + $where: ViewFieldWhereInput + $orderBy: [ViewFieldOrderByWithRelationInput!] + ) { + viewFields: findManyViewField(where: $where, orderBy: $orderBy) { id fieldName isVisible diff --git a/server/src/ability/ability.factory.ts b/server/src/ability/ability.factory.ts index 32fa03f127f4..3d275d804e33 100644 --- a/server/src/ability/ability.factory.ts +++ b/server/src/ability/ability.factory.ts @@ -132,6 +132,7 @@ export class AbilityFactory { // ViewField can(AbilityAction.Read, 'ViewField', { workspaceId: workspace.id }); + can(AbilityAction.Create, 'ViewField', { workspaceId: workspace.id }); can(AbilityAction.Update, 'ViewField', { workspaceId: workspace.id }); return build(); diff --git a/server/src/ability/ability.module.ts b/server/src/ability/ability.module.ts index 7ed68238cdfc..3c5e3a82ce0b 100644 --- a/server/src/ability/ability.module.ts +++ b/server/src/ability/ability.module.ts @@ -95,6 +95,7 @@ import { UpdateAttachmentAbilityHandler, } from './handlers/attachment.ability-handler'; import { + CreateViewFieldAbilityHandler, ReadViewFieldAbilityHandler, UpdateViewFieldAbilityHandler, } from './handlers/view-field.ability-handler'; @@ -184,6 +185,7 @@ import { DeletePipelineProgressAbilityHandler, // ViewField ReadViewFieldAbilityHandler, + CreateViewFieldAbilityHandler, UpdateViewFieldAbilityHandler, ], exports: [ @@ -268,6 +270,7 @@ import { DeletePipelineProgressAbilityHandler, // ViewField ReadViewFieldAbilityHandler, + CreateViewFieldAbilityHandler, UpdateViewFieldAbilityHandler, ], }) diff --git a/server/src/ability/handlers/view-field.ability-handler.ts b/server/src/ability/handlers/view-field.ability-handler.ts index 4a4380cbbea9..219e910b3b66 100644 --- a/server/src/ability/handlers/view-field.ability-handler.ts +++ b/server/src/ability/handlers/view-field.ability-handler.ts @@ -28,6 +28,29 @@ export class ReadViewFieldAbilityHandler implements IAbilityHandler { } } +@Injectable() +export class CreateViewFieldAbilityHandler implements IAbilityHandler { + constructor(private readonly prismaService: PrismaService) {} + + async handle(ability: AppAbility, context: ExecutionContext) { + const gqlContext = GqlExecutionContext.create(context); + const args = gqlContext.getArgs(); + + const allowed = await relationAbilityChecker( + 'ViewField', + ability, + this.prismaService.client, + args, + ); + + if (!allowed) { + return false; + } + + return ability.can(AbilityAction.Create, 'ViewField'); + } +} + @Injectable() export class UpdateViewFieldAbilityHandler implements IAbilityHandler { constructor(private readonly prismaService: PrismaService) {} diff --git a/server/src/core/view/resolvers/view-field.resolver.ts b/server/src/core/view/resolvers/view-field.resolver.ts index 2ac1be3e7751..5d0022037801 100644 --- a/server/src/core/view/resolvers/view-field.resolver.ts +++ b/server/src/core/view/resolvers/view-field.resolver.ts @@ -2,17 +2,21 @@ import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { accessibleBy } from '@casl/prisma'; -import { Prisma } from '@prisma/client'; +import { Prisma, Workspace } from '@prisma/client'; import { AppAbility } from 'src/ability/ability.factory'; import { + CreateViewFieldAbilityHandler, ReadViewFieldAbilityHandler, UpdateViewFieldAbilityHandler, } from 'src/ability/handlers/view-field.ability-handler'; +import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output'; +import { CreateManyViewFieldArgs } from 'src/core/@generated/view-field/create-many-view-field.args'; import { FindManyViewFieldArgs } from 'src/core/@generated/view-field/find-many-view-field.args'; import { UpdateOneViewFieldArgs } from 'src/core/@generated/view-field/update-one-view-field.args'; import { ViewField } from 'src/core/@generated/view-field/view-field.model'; import { ViewFieldService } from 'src/core/view/services/view-field.service'; +import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; import { CheckAbilities } from 'src/decorators/check-abilities.decorator'; import { PrismaSelect, @@ -27,6 +31,21 @@ import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; export class ViewFieldResolver { constructor(private readonly viewFieldService: ViewFieldService) {} + @Mutation(() => AffectedRows) + @UseGuards(AbilityGuard) + @CheckAbilities(CreateViewFieldAbilityHandler) + async createManyViewField( + @Args() args: CreateManyViewFieldArgs, + @AuthWorkspace() workspace: Workspace, + ): Promise { + return this.viewFieldService.createMany({ + data: args.data.map((dataItem) => ({ + ...dataItem, + workspaceId: workspace.id, + })), + }); + } + @Query(() => [ViewField]) @UseGuards(AbilityGuard) @CheckAbilities(ReadViewFieldAbilityHandler)