diff --git a/docs/docs/developer/additional/windows-wsl-setup.mdx b/docs/docs/developer/additional/windows-wsl-setup.mdx index 84e4cdbc1..1d297da31 100644 --- a/docs/docs/developer/additional/windows-wsl-setup.mdx +++ b/docs/docs/developer/additional/windows-wsl-setup.mdx @@ -55,8 +55,5 @@ Close and reopen your terminal to start using nvm or run the following to use it ### Install Twenty project -You are ready to install Twenty project. Follow the [Yarn install guide](/developer/local-setup#yarn-install) instructions. +You are ready to install Twenty project. Follow the [Yarn install guide](/developer/local-setup#yarn-install-recommended) instructions. We don't recommend to use Docker on WSL as it adds an extra layer of complexity. - - - diff --git a/docs/docs/developer/local-setup.mdx b/docs/docs/developer/local-setup.mdx index 1d303b7ac..467661c06 100644 --- a/docs/docs/developer/local-setup.mdx +++ b/docs/docs/developer/local-setup.mdx @@ -83,7 +83,8 @@ CREATE DATABASE "test"; Create a `twenty` user with password `twenty`: ```sql -CREATE USER twenty PASSWORD ‘twenty’ +CREATE USER twenty PASSWORD 'twenty'; +ALTER USER twenty CREATEDB; ``` diff --git a/docs/docs/start/getting-started.mdx b/docs/docs/start/getting-started.mdx index 00415fcc3..e930bebe3 100644 --- a/docs/docs/start/getting-started.mdx +++ b/docs/docs/start/getting-started.mdx @@ -12,8 +12,8 @@ The easiest way to quickly try the app is to signup on [app.twenty.com](https:// The signup is free. - + ## Developer documentation -If you are looking to install the project locally, either to try it or to contribute, check out our [developer guide](../developer/local-setup). +If you are looking to install the project locally, either to try it or to contribute, check out our [developer guide](/developer/local-setup). diff --git a/front/.eslintrc.js b/front/.eslintrc.js index 51c37fcc1..154da81b2 100644 --- a/front/.eslintrc.js +++ b/front/.eslintrc.js @@ -21,6 +21,7 @@ module.exports = { { files: ['*.js', '*.jsx', '*.ts', '*.tsx'], rules: { + 'no-control-regex': 0, 'simple-import-sort/imports': [ 'error', { diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 78c7f8364..7f6964ea0 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -117,8 +117,6 @@ export type ActivityTarget = { __typename?: 'ActivityTarget'; activity: Activity; activityId: Scalars['String']; - commentableId?: Maybe; - commentableType?: Maybe; company?: Maybe; companyId?: Maybe; createdAt: Scalars['DateTime']; @@ -129,8 +127,6 @@ export type ActivityTarget = { }; export type ActivityTargetCreateManyActivityInput = { - commentableId?: InputMaybe; - commentableType?: InputMaybe; companyId?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; @@ -145,8 +141,6 @@ export type ActivityTargetCreateManyActivityInputEnvelope = { export type ActivityTargetCreateManyCompanyInput = { activityId: Scalars['String']; - commentableId?: InputMaybe; - commentableType?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; personId?: InputMaybe; @@ -160,8 +154,6 @@ export type ActivityTargetCreateManyCompanyInputEnvelope = { export type ActivityTargetCreateManyPersonInput = { activityId: Scalars['String']; - commentableId?: InputMaybe; - commentableType?: InputMaybe; companyId?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; @@ -175,8 +167,6 @@ export type ActivityTargetCreateManyPersonInputEnvelope = { export type ActivityTargetCreateManyWorkspaceInput = { activityId: Scalars['String']; - commentableId?: InputMaybe; - commentableType?: InputMaybe; companyId?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; @@ -231,8 +221,6 @@ export type ActivityTargetCreateOrConnectWithoutWorkspaceInput = { }; export type ActivityTargetCreateWithoutActivityInput = { - commentableId?: InputMaybe; - commentableType?: InputMaybe; company?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; @@ -242,8 +230,6 @@ export type ActivityTargetCreateWithoutActivityInput = { export type ActivityTargetCreateWithoutCompanyInput = { activity: ActivityCreateNestedOneWithoutActivityTargetsInput; - commentableId?: InputMaybe; - commentableType?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; person?: InputMaybe; @@ -252,8 +238,6 @@ export type ActivityTargetCreateWithoutCompanyInput = { export type ActivityTargetCreateWithoutPersonInput = { activity: ActivityCreateNestedOneWithoutActivityTargetsInput; - commentableId?: InputMaybe; - commentableType?: InputMaybe; company?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; @@ -262,8 +246,6 @@ export type ActivityTargetCreateWithoutPersonInput = { export type ActivityTargetCreateWithoutWorkspaceInput = { activity: ActivityCreateNestedOneWithoutActivityTargetsInput; - commentableId?: InputMaybe; - commentableType?: InputMaybe; company?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; @@ -286,8 +268,6 @@ export type ActivityTargetScalarWhereInput = { NOT?: InputMaybe>; OR?: InputMaybe>; activityId?: InputMaybe; - commentableId?: InputMaybe; - commentableType?: InputMaybe; companyId?: InputMaybe; createdAt?: InputMaybe; id?: InputMaybe; @@ -345,8 +325,6 @@ export type ActivityTargetWhereInput = { OR?: InputMaybe>; activity?: InputMaybe; activityId?: InputMaybe; - commentableId?: InputMaybe; - commentableType?: InputMaybe; company?: InputMaybe; companyId?: InputMaybe; createdAt?: InputMaybe; @@ -632,11 +610,6 @@ export type CommentWhereUniqueInput = { id?: InputMaybe; }; -export enum CommentableType { - Company = 'Company', - Person = 'Person' -} - export type Company = { __typename?: 'Company'; ActivityTarget?: Maybe>; @@ -837,13 +810,6 @@ export type EnumColorSchemeFilter = { notIn?: InputMaybe>; }; -export type EnumCommentableTypeNullableFilter = { - equals?: InputMaybe; - in?: InputMaybe>; - not?: InputMaybe; - notIn?: InputMaybe>; -}; - export type EnumPipelineProgressableTypeFilter = { equals?: InputMaybe; in?: InputMaybe>; @@ -1286,13 +1252,6 @@ export type NestedEnumColorSchemeFilter = { notIn?: InputMaybe>; }; -export type NestedEnumCommentableTypeNullableFilter = { - equals?: InputMaybe; - in?: InputMaybe>; - not?: InputMaybe; - notIn?: InputMaybe>; -}; - export type NestedEnumPipelineProgressableTypeFilter = { equals?: InputMaybe; in?: InputMaybe>; @@ -2694,7 +2653,9 @@ export type CreateActivityMutationVariables = Exact<{ }>; -export type CreateActivityMutation = { __typename?: 'Mutation', createOneActivity: { __typename?: 'Activity', id: string, createdAt: string, updatedAt: string, authorId: string, type: ActivityType, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, createdAt: string, updatedAt: string, activityId: string, commentableType?: CommentableType | null, commentableId?: string | null, companyId?: string | null, personId?: string | null }> | null, comments?: Array<{ __typename?: 'Comment', id: string, createdAt: string, updatedAt: string, body: string, author: { __typename?: 'User', id: string } }> | null } }; +export type CreateActivityMutation = { __typename?: 'Mutation', createOneActivity: { __typename?: 'Activity', id: string, createdAt: string, updatedAt: string, authorId: string, type: ActivityType, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, createdAt: string, updatedAt: string, activityId: string, companyId?: string | null, personId?: string | null }> | null, comments?: Array<{ __typename?: 'Comment', id: string, createdAt: string, updatedAt: string, body: string, author: { __typename?: 'User', id: string } }> | null } }; + +export type ActivityQueryFragmentFragment = { __typename?: 'Activity', id: string, createdAt: string, title?: string | null, body?: string | null, type: ActivityType, completedAt?: string | null, dueAt?: string | null, assignee?: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string, avatarUrl?: string | null } | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string }, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, companyId?: string | null, personId?: string | null, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null, person?: { __typename?: 'Person', id: string, displayName: string, avatarUrl?: string | null } | null }> | null }; export type GetActivitiesByTargetsQueryVariables = Exact<{ activityTargetIds: Array | Scalars['String']; @@ -2702,7 +2663,7 @@ export type GetActivitiesByTargetsQueryVariables = Exact<{ }>; -export type GetActivitiesByTargetsQuery = { __typename?: 'Query', findManyActivities: Array<{ __typename?: 'Activity', id: string, createdAt: string, title?: string | null, body?: string | null, type: ActivityType, completedAt?: string | null, dueAt?: string | null, assignee?: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string, avatarUrl?: string | null } | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string }, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, commentableType?: CommentableType | null, commentableId?: string | null, companyId?: string | null, personId?: string | null }> | null }> }; +export type GetActivitiesByTargetsQuery = { __typename?: 'Query', findManyActivities: Array<{ __typename?: 'Activity', id: string, createdAt: string, title?: string | null, body?: string | null, type: ActivityType, completedAt?: string | null, dueAt?: string | null, assignee?: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string, avatarUrl?: string | null } | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string }, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, companyId?: string | null, personId?: string | null, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null, person?: { __typename?: 'Person', id: string, displayName: string, avatarUrl?: string | null } | null }> | null }> }; export type GetActivitiesQueryVariables = Exact<{ where: ActivityWhereInput; @@ -2710,14 +2671,14 @@ export type GetActivitiesQueryVariables = Exact<{ }>; -export type GetActivitiesQuery = { __typename?: 'Query', findManyActivities: Array<{ __typename?: 'Activity', id: string, createdAt: string, title?: string | null, body?: string | null, type: ActivityType, completedAt?: string | null, dueAt?: string | null, assignee?: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string, avatarUrl?: string | null } | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string }, comments?: Array<{ __typename?: 'Comment', id: string }> | null, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, commentableType?: CommentableType | null, commentableId?: string | null, companyId?: string | null, personId?: string | null }> | null }> }; +export type GetActivitiesQuery = { __typename?: 'Query', findManyActivities: Array<{ __typename?: 'Activity', id: string, createdAt: string, title?: string | null, body?: string | null, type: ActivityType, completedAt?: string | null, dueAt?: string | null, assignee?: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string, avatarUrl?: string | null } | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string }, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, companyId?: string | null, personId?: string | null, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null, person?: { __typename?: 'Person', id: string, displayName: string, avatarUrl?: string | null } | null }> | null }> }; export type GetActivityQueryVariables = Exact<{ activityId: Scalars['String']; }>; -export type GetActivityQuery = { __typename?: 'Query', findManyActivities: Array<{ __typename?: 'Activity', id: string, createdAt: string, body?: string | null, title?: string | null, type: ActivityType, completedAt?: string | null, dueAt?: string | null, assignee?: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string, avatarUrl?: string | null } | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string }, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, commentableType?: CommentableType | null, commentableId?: string | null, companyId?: string | null, personId?: string | null }> | null }> }; +export type GetActivityQuery = { __typename?: 'Query', findManyActivities: Array<{ __typename?: 'Activity', id: string, createdAt: string, title?: string | null, body?: string | null, type: ActivityType, completedAt?: string | null, dueAt?: string | null, assignee?: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string, avatarUrl?: string | null } | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: string }, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, companyId?: string | null, personId?: string | null, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null, person?: { __typename?: 'Person', id: string, displayName: string, avatarUrl?: string | null } | null }> | null }> }; export type AddActivityTargetsOnActivityMutationVariables = Exact<{ activityId: Scalars['String']; @@ -2725,7 +2686,7 @@ export type AddActivityTargetsOnActivityMutationVariables = Exact<{ }>; -export type AddActivityTargetsOnActivityMutation = { __typename?: 'Mutation', updateOneActivity: { __typename?: 'Activity', id: string, createdAt: string, updatedAt: string, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, createdAt: string, updatedAt: string, commentableType?: CommentableType | null, commentableId?: string | null, companyId?: string | null, personId?: string | null }> | null } }; +export type AddActivityTargetsOnActivityMutation = { __typename?: 'Mutation', updateOneActivity: { __typename?: 'Activity', id: string, createdAt: string, updatedAt: string, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, createdAt: string, updatedAt: string, companyId?: string | null, personId?: string | null }> | null } }; export type RemoveActivityTargetsOnActivityMutationVariables = Exact<{ activityId: Scalars['String']; @@ -2733,7 +2694,7 @@ export type RemoveActivityTargetsOnActivityMutationVariables = Exact<{ }>; -export type RemoveActivityTargetsOnActivityMutation = { __typename?: 'Mutation', updateOneActivity: { __typename?: 'Activity', id: string, createdAt: string, updatedAt: string, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, createdAt: string, updatedAt: string, commentableType?: CommentableType | null, commentableId?: string | null, companyId?: string | null, personId?: string | null }> | null } }; +export type RemoveActivityTargetsOnActivityMutation = { __typename?: 'Mutation', updateOneActivity: { __typename?: 'Activity', id: string, createdAt: string, updatedAt: string, activityTargets?: Array<{ __typename?: 'ActivityTarget', id: string, createdAt: string, updatedAt: string, companyId?: string | null, personId?: string | null }> | null } }; export type DeleteActivityMutationVariables = Exact<{ activityId: Scalars['String']; @@ -2833,22 +2794,22 @@ export type GetCompanyQueryVariables = Exact<{ export type GetCompanyQuery = { __typename?: 'Query', findUniqueCompany: { __typename?: 'Company', id: string, domainName: string, name: string, createdAt: string, address: string, linkedinUrl?: string | null, employees?: number | null, _activityCount: number, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, avatarUrl?: string | null } | null, Favorite?: Array<{ __typename?: 'Favorite', id: string, person?: { __typename?: 'Person', id: string } | null, company?: { __typename?: 'Company', id: string } | null }> | null } }; +export type CompanyFieldsFragmentFragment = { __typename?: 'Company', address: string, createdAt: string, domainName: string, employees?: number | null, linkedinUrl?: string | null, id: string, name: string, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, avatarUrl?: string | null } | null }; + export type UpdateOneCompanyMutationVariables = Exact<{ where: CompanyWhereUniqueInput; data: CompanyUpdateInput; }>; -export type UpdateOneCompanyMutation = { __typename?: 'Mutation', updateOneCompany?: { __typename?: 'Company', address: string, createdAt: string, domainName: string, employees?: number | null, linkedinUrl?: string | null, id: string, name: string, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null } | null } | null }; - -export type InsertCompanyFragmentFragment = { __typename?: 'Company', domainName: string, address: string, id: string, name: string, createdAt: string }; +export type UpdateOneCompanyMutation = { __typename?: 'Mutation', updateOneCompany?: { __typename?: 'Company', address: string, createdAt: string, domainName: string, employees?: number | null, linkedinUrl?: string | null, id: string, name: string, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, avatarUrl?: string | null } | null } | null }; export type InsertOneCompanyMutationVariables = Exact<{ data: CompanyCreateInput; }>; -export type InsertOneCompanyMutation = { __typename?: 'Mutation', createOneCompany: { __typename?: 'Company', domainName: string, address: string, id: string, name: string, createdAt: string } }; +export type InsertOneCompanyMutation = { __typename?: 'Mutation', createOneCompany: { __typename?: 'Company', address: string, createdAt: string, domainName: string, employees?: number | null, linkedinUrl?: string | null, id: string, name: string, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, avatarUrl?: string | null } | null } }; export type DeleteManyCompaniesMutationVariables = Exact<{ ids?: InputMaybe | Scalars['String']>; @@ -3073,7 +3034,7 @@ export type SearchCompanyQueryVariables = Exact<{ }>; -export type SearchCompanyQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'Company', id: string, name: string, domainName: string }> }; +export type SearchCompanyQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'Company', address: string, createdAt: string, domainName: string, employees?: number | null, linkedinUrl?: string | null, id: string, name: string, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, avatarUrl?: string | null } | null }> }; export type SearchActivityQueryVariables = Exact<{ where?: InputMaybe; @@ -3128,13 +3089,6 @@ 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; }>; @@ -3230,6 +3184,58 @@ export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: nev export type DeleteCurrentWorkspaceMutation = { __typename?: 'Mutation', deleteCurrentWorkspace: { __typename?: 'Workspace', id: string } }; +export const ActivityQueryFragmentFragmentDoc = gql` + fragment ActivityQueryFragment on Activity { + id + createdAt + title + body + type + completedAt + dueAt + assignee { + id + firstName + lastName + displayName + avatarUrl + } + author { + id + firstName + lastName + displayName + } + comments { + id + body + createdAt + updatedAt + author { + id + displayName + firstName + lastName + avatarUrl + } + } + activityTargets { + id + companyId + personId + company { + id + name + domainName + } + person { + id + displayName + avatarUrl + } + } +} + `; export const ActivityUpdatePartsFragmentDoc = gql` fragment ActivityUpdateParts on Activity { id @@ -3246,13 +3252,21 @@ export const ActivityUpdatePartsFragmentDoc = gql` } } `; -export const InsertCompanyFragmentFragmentDoc = gql` - fragment InsertCompanyFragment on Company { - domainName +export const CompanyFieldsFragmentFragmentDoc = gql` + fragment CompanyFieldsFragment on Company { + accountOwner { + id + email + displayName + avatarUrl + } address + createdAt + domainName + employees + linkedinUrl id name - createdAt } `; export const InsertPersonFragmentFragmentDoc = gql` @@ -3326,8 +3340,6 @@ export const CreateActivityDocument = gql` createdAt updatedAt activityId - commentableType - commentableId companyId personId } @@ -3373,51 +3385,12 @@ export const GetActivitiesByTargetsDocument = gql` query GetActivitiesByTargets($activityTargetIds: [String!]!, $orderBy: [ActivityOrderByWithRelationInput!]) { findManyActivities( orderBy: $orderBy - where: {activityTargets: {some: {commentableId: {in: $activityTargetIds}}}} + where: {activityTargets: {some: {OR: [{personId: {in: $activityTargetIds}}, {companyId: {in: $activityTargetIds}}]}}} ) { - id - createdAt - title - body - type - completedAt - dueAt - assignee { - id - firstName - lastName - displayName - avatarUrl - } - author { - id - firstName - lastName - displayName - } - comments { - id - body - createdAt - updatedAt - author { - id - displayName - firstName - lastName - avatarUrl - } - } - activityTargets { - id - commentableType - commentableId - companyId - personId - } + ...ActivityQueryFragment } } - `; + ${ActivityQueryFragmentFragmentDoc}`; /** * __useGetActivitiesByTargetsQuery__ @@ -3450,39 +3423,10 @@ export type GetActivitiesByTargetsQueryResult = Apollo.QueryResult; /** @@ -4310,10 +4198,10 @@ export type UpdateOneCompanyMutationOptions = Apollo.BaseMutationOptions; /** @@ -5489,12 +5377,10 @@ export type EmptyQueryQueryResult = Apollo.QueryResult; 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/activities/components/ActivityEditor.tsx b/front/src/modules/activities/components/ActivityEditor.tsx index 36435d7f5..52ee861d2 100644 --- a/front/src/modules/activities/components/ActivityEditor.tsx +++ b/front/src/modules/activities/components/ActivityEditor.tsx @@ -72,9 +72,7 @@ type OwnProps = { 'id' | 'firstName' | 'lastName' | 'displayName' > | null; } & { - activityTargets?: Array< - Pick - > | null; + activityTargets?: Array> | null; }; showComment?: boolean; autoFillTitle?: boolean; diff --git a/front/src/modules/activities/components/ActivityRelationPicker.tsx b/front/src/modules/activities/components/ActivityRelationPicker.tsx deleted file mode 100644 index d3234e278..000000000 --- a/front/src/modules/activities/components/ActivityRelationPicker.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import { useCallback, useMemo, useState } from 'react'; -import styled from '@emotion/styled'; -import { - autoUpdate, - flip, - offset, - size, - useFloating, -} from '@floating-ui/react'; - -import { CompanyChip } from '@/companies/components/CompanyChip'; -import { useFilteredSearchCompanyQuery } from '@/companies/queries'; -import { PersonChip } from '@/people/components/PersonChip'; -import { useFilteredSearchPeopleQuery } from '@/people/queries'; -import { MultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect'; -import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; -import { Activity, ActivityTarget, CommentableType } from '~/generated/graphql'; -import { assertNotNull } from '~/utils/assert'; - -import { useHandleCheckableActivityTargetChange } from '../hooks/useHandleCheckableActivityTargetChange'; -import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '../utils/flatMapAndSortEntityForSelectArrayByName'; - -type OwnProps = { - activity?: Pick & { - activityTargets: Array< - Pick< - ActivityTarget, - 'id' | 'commentableId' | 'commentableType' | 'companyId' | 'personId' - > - >; - }; -}; - -const StyledContainer = styled.div` - align-items: flex-start; - display: flex; - flex-direction: row; - gap: ${({ theme }) => theme.spacing(2)}; - justify-content: flex-start; - - width: 100%; -`; - -const StyledRelationContainer = styled.div` - --horizontal-padding: ${({ theme }) => theme.spacing(1)}; - --vertical-padding: ${({ theme }) => theme.spacing(1.5)}; - - border: 1px solid transparent; - - cursor: pointer; - - display: flex; - flex-wrap: wrap; - - gap: ${({ theme }) => theme.spacing(2)}; - - &:hover { - background-color: ${({ theme }) => theme.background.secondary}; - border: 1px solid ${({ theme }) => theme.border.color.light}; - } - - min-height: calc(32px - 2 * var(--vertical-padding)); - - overflow: hidden; - - padding: var(--vertical-padding) var(--horizontal-padding); - width: calc(100% - 2 * var(--horizontal-padding)); -`; - -const StyledMenuWrapper = styled.div` - z-index: ${({ theme }) => theme.lastLayerZIndex}; -`; - -export function ActivityRelationPicker({ activity }: OwnProps) { - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [searchFilter, setSearchFilter] = useState(''); - const [selectedEntityIds, setSelectedEntityIds] = useState< - Record - >({}); - const { - setHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope, - } = usePreviousHotkeyScope(); - - const initialPeopleIds = useMemo( - () => - activity?.activityTargets - ?.filter((relation) => relation.commentableType === 'Person') - .map((relation) => relation.personId || relation.commentableId) - .filter(assertNotNull) ?? [], - [activity?.activityTargets], - ); - - const initialCompanyIds = useMemo( - () => - activity?.activityTargets - ?.filter((relation) => relation.commentableType === 'Company') - .map((relation) => relation.companyId || relation.commentableId) - .filter(assertNotNull) ?? [], - [activity?.activityTargets], - ); - - const initialSelectedEntityIds = useMemo( - () => - [...initialPeopleIds, ...initialCompanyIds].reduce< - Record - >((result, entityId) => ({ ...result, [entityId]: true }), {}), - [initialPeopleIds, initialCompanyIds], - ); - - const personsForMultiSelect = useFilteredSearchPeopleQuery({ - searchFilter, - selectedIds: initialPeopleIds, - }); - - const companiesForMultiSelect = useFilteredSearchCompanyQuery({ - searchFilter, - selectedIds: initialCompanyIds, - }); - - const selectedEntities = flatMapAndSortEntityForSelectArrayOfArrayByName([ - personsForMultiSelect.selectedEntities, - companiesForMultiSelect.selectedEntities, - ]); - - const filteredSelectedEntities = - flatMapAndSortEntityForSelectArrayOfArrayByName([ - personsForMultiSelect.filteredSelectedEntities, - companiesForMultiSelect.filteredSelectedEntities, - ]); - - const entitiesToSelect = flatMapAndSortEntityForSelectArrayOfArrayByName([ - personsForMultiSelect.entitiesToSelect, - companiesForMultiSelect.entitiesToSelect, - ]); - - const handleCheckItemsChange = useHandleCheckableActivityTargetChange({ - activity, - }); - - const exitEditMode = useCallback(() => { - goBackToPreviousHotkeyScope(); - setIsMenuOpen(false); - setSearchFilter(''); - - if (Object.values(selectedEntityIds).some((value) => !!value)) { - handleCheckItemsChange(selectedEntityIds, entitiesToSelect); - } - }, [ - entitiesToSelect, - selectedEntityIds, - goBackToPreviousHotkeyScope, - handleCheckItemsChange, - ]); - - const handleRelationContainerClick = useCallback(() => { - if (isMenuOpen) { - exitEditMode(); - } else { - setIsMenuOpen(true); - setSelectedEntityIds(initialSelectedEntityIds); - setHotkeyScopeAndMemorizePreviousScope( - RelationPickerHotkeyScope.RelationPicker, - ); - } - }, [ - initialSelectedEntityIds, - exitEditMode, - isMenuOpen, - setHotkeyScopeAndMemorizePreviousScope, - ]); - - useScopedHotkeys( - ['esc', 'enter'], - () => { - exitEditMode(); - }, - RelationPickerHotkeyScope.RelationPicker, - [exitEditMode], - ); - - const { refs, floatingStyles } = useFloating({ - strategy: 'absolute', - middleware: [ - offset(({ rects }) => { - return -rects.reference.height; - }), - flip(), - size(), - ], - whileElementsMounted: autoUpdate, - open: isMenuOpen, - placement: 'bottom-start', - }); - - useListenClickOutside({ - refs: [refs.floating, refs.domReference], - callback: () => { - exitEditMode(); - }, - }); - - return ( - - - {selectedEntities?.map((entity) => - entity.entityType === CommentableType.Company ? ( - - ) : ( - - ), - )} - - {isMenuOpen && ( - - - - - - )} - - ); -} diff --git a/front/src/modules/activities/components/ActivityTargetChips.tsx b/front/src/modules/activities/components/ActivityTargetChips.tsx index 5fd3b4ce0..22e5659a9 100644 --- a/front/src/modules/activities/components/ActivityTargetChips.tsx +++ b/front/src/modules/activities/components/ActivityTargetChips.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import { CompanyChip } from '@/companies/components/CompanyChip'; import { PersonChip } from '@/people/components/PersonChip'; -import { GetCompaniesQuery, GetPeopleQuery } from '~/generated/graphql'; +import { ActivityTarget, Company, Person } from '~/generated/graphql'; import { getLogoUrlFromDomainName } from '~/utils'; const StyledContainer = styled.div` @@ -12,32 +12,44 @@ const StyledContainer = styled.div` `; export function ActivityTargetChips({ - targetCompanies, - targetPeople, + targets, }: { - targetCompanies?: GetCompaniesQuery; - targetPeople?: GetPeopleQuery; + targets?: Array< + Pick & { + person?: Pick | null; + company?: Pick | null; + } + > | null; }) { + if (!targets) { + return null; + } + return ( - {targetCompanies?.companies && - targetCompanies.companies.map((company) => ( - - ))} - {targetPeople?.people && - targetPeople.people.map((person) => ( - - ))} + {targets.map(({ company, person }) => { + if (company) { + return ( + + ); + } + if (person) { + return ( + + ); + } + return <>; + })} ); } diff --git a/front/src/modules/activities/components/TaskRow.tsx b/front/src/modules/activities/components/TaskRow.tsx index b26facd1c..30e4bc1b1 100644 --- a/front/src/modules/activities/components/TaskRow.tsx +++ b/front/src/modules/activities/components/TaskRow.tsx @@ -9,7 +9,6 @@ import { CheckboxShape, } from '@/ui/input/checkbox/components/Checkbox'; import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; -import { useGetCompaniesQuery, useGetPeopleQuery } from '~/generated/graphql'; import { beautifyExactDate } from '~/utils/date-utils'; import { useCompleteTask } from '../hooks/useCompleteTask'; @@ -34,10 +33,13 @@ const StyledTaskBody = styled.div` width: 1px; `; -const StyledTaskTitle = styled.div` +const StyledTaskTitle = styled.div<{ + completed: boolean; +}>` color: ${({ theme }) => theme.font.color.primary}; font-weight: ${({ theme }) => theme.font.weight.medium}; padding: 0 ${({ theme }) => theme.spacing(2)}; + text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')}; `; const StyledCommentIcon = styled.div` @@ -62,37 +64,7 @@ const StyledFieldsContainer = styled.div` export function TaskRow({ task }: { task: TaskForList }) { const theme = useTheme(); const openActivityRightDrawer = useOpenActivityRightDrawer(); - const { data: targetPeople } = useGetPeopleQuery({ - variables: { - where: { - id: { - in: task?.activityTargets - ? task?.activityTargets - .filter((target) => target.commentableType === 'Person') - .map( - (target) => (target.personId || target.commentableId) ?? '', - ) - : [], - }, - }, - }, - }); - const { data: targetCompanies } = useGetCompaniesQuery({ - variables: { - where: { - id: { - in: task?.activityTargets - ? task?.activityTargets - .filter((target) => target.commentableType === 'Company') - .map( - (target) => (target.companyId || target.commentableId) ?? '', - ) - : [], - }, - }, - }, - }); const body = JSON.parse(task.body ?? '{}')[0]?.content[0]?.text; const { completeTask } = useCompleteTask(task); @@ -113,7 +85,9 @@ export function TaskRow({ task }: { task: TaskForList }) { onChange={completeTask} /> - {task.title ?? '(No title)'} + + {task.title ?? '(No title)'} + {task.comments && task.comments.length > 0 && ( @@ -123,10 +97,7 @@ export function TaskRow({ task }: { task: TaskForList }) { )} - + {task.dueAt && beautifyExactDate(task.dueAt)} diff --git a/front/src/modules/activities/components/__stories__/ActivityRelationPicker.stories.tsx b/front/src/modules/activities/components/__stories__/ActivityRelationPicker.stories.tsx deleted file mode 100644 index e8ca2dcd0..000000000 --- a/front/src/modules/activities/components/__stories__/ActivityRelationPicker.stories.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { MemoryRouter } from 'react-router-dom'; -import styled from '@emotion/styled'; -import type { Meta, StoryObj } from '@storybook/react'; - -import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; -import { graphqlMocks } from '~/testing/graphqlMocks'; -import { mockedActivities } from '~/testing/mock-data/activities'; - -import { ActivityRelationPicker } from '../ActivityRelationPicker'; - -const StyledContainer = styled.div` - width: 400px; -`; - -const meta: Meta = { - title: 'Modules/Comments/ActivityRelationPicker', - component: ActivityRelationPicker, - decorators: [ - (Story) => ( - - - - - - ), - ComponentDecorator, - ], - args: { activity: mockedActivities[0] }, - parameters: { - msw: graphqlMocks, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx index f99d71263..42f17bd7d 100644 --- a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx +++ b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx @@ -4,59 +4,22 @@ import { FieldContext } from '@/ui/editable-field/states/FieldContext'; import { IconArrowUpRight } from '@/ui/icon'; import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; -import { - Activity, - ActivityTarget, - useGetCompaniesQuery, - useGetPeopleQuery, -} from '~/generated/graphql'; +import { Activity, ActivityTarget, Company, Person } from '~/generated/graphql'; import { ActivityRelationEditableFieldEditMode } from './ActivityRelationEditableFieldEditMode'; type OwnProps = { activity?: Pick & { activityTargets?: Array< - Pick< - ActivityTarget, - 'id' | 'commentableId' | 'commentableType' | 'personId' | 'companyId' - > + Pick & { + person?: Pick; + company?: Pick; + } > | null; }; }; export function ActivityRelationEditableField({ activity }: OwnProps) { - const { data: targetPeople } = useGetPeopleQuery({ - variables: { - where: { - id: { - in: activity?.activityTargets - ? activity?.activityTargets - .filter((target) => target.commentableType === 'Person') - .map( - (target) => (target.personId || target.commentableId) ?? '', - ) - : [], - }, - }, - }, - }); - - const { data: targetCompanies } = useGetCompaniesQuery({ - variables: { - where: { - id: { - in: activity?.activityTargets - ? activity?.activityTargets - .filter((target) => target.commentableType === 'Company') - .map( - (target) => (target.companyId || target.commentableId) ?? '', - ) - : [], - }, - }, - }, - }); - return ( @@ -71,10 +34,7 @@ export function ActivityRelationEditableField({ activity }: OwnProps) { } label="Relations" displayModeContent={ - + } /> diff --git a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx index 2a3c97501..26e80677c 100644 --- a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx +++ b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx @@ -13,10 +13,7 @@ import { assertNotNull } from '~/utils/assert'; type OwnProps = { activity?: Pick & { activityTargets?: Array< - Pick< - ActivityTarget, - 'id' | 'commentableId' | 'commentableType' | 'personId' | 'companyId' - > + Pick > | null; }; }; @@ -33,8 +30,8 @@ export function ActivityRelationEditableFieldEditMode({ activity }: OwnProps) { const initialPeopleIds = useMemo( () => activity?.activityTargets - ?.filter((relation) => relation.commentableType === 'Person') - .map((relation) => relation.personId || relation.commentableId) + ?.filter((relation) => relation.personId !== null) + .map((relation) => relation.personId) .filter(assertNotNull) ?? [], [activity?.activityTargets], ); @@ -42,8 +39,8 @@ export function ActivityRelationEditableFieldEditMode({ activity }: OwnProps) { const initialCompanyIds = useMemo( () => activity?.activityTargets - ?.filter((relation) => relation.commentableType === 'Company') - .map((relation) => relation.companyId || relation.commentableId) + ?.filter((relation) => relation.companyId !== null) + .map((relation) => relation.companyId) .filter(assertNotNull) ?? [], [activity?.activityTargets], ); diff --git a/front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts b/front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts index 8c10b39be..4352e4b3b 100644 --- a/front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts +++ b/front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts @@ -6,20 +6,20 @@ import { GET_PEOPLE } from '@/people/queries'; import { Activity, ActivityTarget, - CommentableType, useAddActivityTargetsOnActivityMutation, useRemoveActivityTargetsOnActivityMutation, } from '~/generated/graphql'; import { GET_ACTIVITY } from '../queries'; -import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect'; +import { ActivityTargetableEntityType } from '../types/ActivityTargetableEntity'; +import { ActivityTargetableEntityForSelect } from '../types/ActivityTargetableEntityForSelect'; export function useHandleCheckableActivityTargetChange({ activity, }: { activity?: Pick & { activityTargets?: Array< - Pick + Pick > | null; }; }) { @@ -43,14 +43,16 @@ export function useHandleCheckableActivityTargetChange({ return async function handleCheckItemsChange( entityValues: Record, - entities: CommentableEntityForSelect[], + entities: ActivityTargetableEntityForSelect[], ) { if (!activity) { return; } const currentEntityIds = activity.activityTargets - ? activity.activityTargets.map(({ commentableId }) => commentableId) + ? activity.activityTargets.map( + ({ personId, companyId }) => personId ?? companyId, + ) : []; const entitiesToAdd = entities.filter( @@ -64,12 +66,14 @@ export function useHandleCheckableActivityTargetChange({ activityTargetInputs: entitiesToAdd.map((entity) => ({ id: v4(), createdAt: new Date().toISOString(), - commentableType: entity.entityType, - commentableId: entity.id, companyId: - entity.entityType === CommentableType.Company ? entity.id : null, + entity.entityType === ActivityTargetableEntityType.Company + ? entity.id + : null, personId: - entity.entityType === CommentableType.Person ? entity.id : null, + entity.entityType === ActivityTargetableEntityType.Person + ? entity.id + : null, })), }, }); @@ -77,8 +81,9 @@ export function useHandleCheckableActivityTargetChange({ const activityTargetIdsToDelete = activity.activityTargets ? activity.activityTargets .filter( - ({ commentableId }) => - commentableId && !entityValues[commentableId], + ({ personId, companyId }) => + (personId ?? companyId) && + !entityValues[personId ?? companyId ?? ''], ) .map(({ id }) => id) : []; diff --git a/front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index 535f73580..a0d695f3c 100644 --- a/front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -9,20 +9,19 @@ import { useRightDrawer } from '@/ui/right-drawer/hooks/useRightDrawer'; import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { - ActivityType, - CommentableType, - useCreateActivityMutation, -} from '~/generated/graphql'; +import { ActivityType, useCreateActivityMutation } from '~/generated/graphql'; import { GET_ACTIVITIES, GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY, } from '../queries'; -import { commentableEntityArrayState } from '../states/commentableEntityArrayState'; +import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState'; import { viewableActivityIdState } from '../states/viewableActivityIdState'; -import { CommentableEntity } from '../types/CommentableEntity'; +import { + ActivityTargetableEntity, + ActivityTargetableEntityType, +} from '../types/ActivityTargetableEntity'; export function useOpenCreateActivityDrawer() { const { openRightDrawer } = useRightDrawer(); @@ -30,14 +29,14 @@ export function useOpenCreateActivityDrawer() { const currentUser = useRecoilValue(currentUserState); const setHotkeyScope = useSetHotkeyScope(); - const [, setCommentableEntityArray] = useRecoilState( - commentableEntityArrayState, + const [, setActivityTargetableEntityArray] = useRecoilState( + activityTargetableEntityArrayState, ); const [, setViewableActivityId] = useRecoilState(viewableActivityIdState); return function openCreateActivityDrawer( type: ActivityType, - entities?: CommentableEntity[], + entities?: ActivityTargetableEntity[], ) { const now = new Date().toISOString(); @@ -54,14 +53,14 @@ export function useOpenCreateActivityDrawer() { createMany: { data: entities ? entities.map((entity) => ({ - commentableId: entity.id, - commentableType: entity.type, companyId: - entity.type === CommentableType.Company + entity.type === ActivityTargetableEntityType.Company ? entity.id : null, personId: - entity.type === CommentableType.Person ? entity.id : null, + entity.type === ActivityTargetableEntityType.Person + ? entity.id + : null, id: v4(), createdAt: now, })) @@ -81,7 +80,7 @@ export function useOpenCreateActivityDrawer() { onCompleted(data) { setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); setViewableActivityId(data.createOneActivity.id); - setCommentableEntityArray(entities ?? []); + setActivityTargetableEntityArray(entities ?? []); openRightDrawer(RightDrawerPages.CreateActivity); }, }); diff --git a/front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts b/front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts index 554218d25..5f6cda7b8 100644 --- a/front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts +++ b/front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts @@ -1,9 +1,12 @@ import { useRecoilValue } from 'recoil'; import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector'; -import { ActivityType, CommentableType } from '~/generated/graphql'; +import { ActivityType } from '~/generated/graphql'; -import { CommentableEntity } from '../types/CommentableEntity'; +import { + ActivityTargetableEntity, + ActivityTargetableEntityType, +} from '../types/ActivityTargetableEntity'; import { useOpenCreateActivityDrawer } from './useOpenCreateActivityDrawer'; @@ -14,14 +17,13 @@ export function useOpenCreateActivityDrawerForSelectedRowIds() { return function openCreateCommentDrawerForSelectedRowIds( type: ActivityType, - entityType: CommentableType, + entityType: ActivityTargetableEntityType, ) { - const commentableEntityArray: CommentableEntity[] = selectedEntityIds.map( - (id) => ({ + const activityTargetableEntityArray: ActivityTargetableEntity[] = + selectedEntityIds.map((id) => ({ type: entityType, id, - }), - ); - openCreateActivityDrawer(type, commentableEntityArray); + })); + openCreateActivityDrawer(type, activityTargetableEntityArray); }; } diff --git a/front/src/modules/activities/hooks/useOpenTimelineRightDrawer.ts b/front/src/modules/activities/hooks/useOpenTimelineRightDrawer.ts index f36e5ef86..f6cf662b8 100644 --- a/front/src/modules/activities/hooks/useOpenTimelineRightDrawer.ts +++ b/front/src/modules/activities/hooks/useOpenTimelineRightDrawer.ts @@ -5,22 +5,22 @@ import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotke import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { commentableEntityArrayState } from '../states/commentableEntityArrayState'; -import { CommentableEntity } from '../types/CommentableEntity'; +import { activityTargetableEntityArrayState } from '../states/activityTargetableEntityArrayState'; +import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity'; // TODO: refactor with recoil callback to avoid rerender export function useOpenTimelineRightDrawer() { const { openRightDrawer } = useRightDrawer(); - const [, setCommentableEntityArray] = useRecoilState( - commentableEntityArrayState, + const [, setActivityTargetableEntityArray] = useRecoilState( + activityTargetableEntityArrayState, ); const setHotkeyScope = useSetHotkeyScope(); return function openTimelineRightDrawer( - commentableEntityArray: CommentableEntity[], + activityTargetableEntityArray: ActivityTargetableEntity[], ) { setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); - setCommentableEntityArray(commentableEntityArray); + setActivityTargetableEntityArray(activityTargetableEntityArray); openRightDrawer(RightDrawerPages.Timeline); }; } diff --git a/front/src/modules/activities/queries/create.ts b/front/src/modules/activities/queries/create.ts index f79548631..1fb75bac5 100644 --- a/front/src/modules/activities/queries/create.ts +++ b/front/src/modules/activities/queries/create.ts @@ -45,8 +45,6 @@ export const CREATE_ACTIVITY_WITH_COMMENT = gql` createdAt updatedAt activityId - commentableType - commentableId companyId personId } diff --git a/front/src/modules/activities/queries/select.ts b/front/src/modules/activities/queries/select.ts index c7acc033d..eee362d94 100644 --- a/front/src/modules/activities/queries/select.ts +++ b/front/src/modules/activities/queries/select.ts @@ -1,5 +1,58 @@ import { gql } from '@apollo/client'; +export const ACTIVITY_QUERY_FRAGMENT = gql` + fragment ActivityQueryFragment on Activity { + id + createdAt + title + body + type + completedAt + dueAt + assignee { + id + firstName + lastName + displayName + avatarUrl + } + author { + id + firstName + lastName + displayName + } + comments { + id + body + createdAt + updatedAt + author { + id + displayName + firstName + lastName + avatarUrl + } + } + activityTargets { + id + companyId + personId + company { + id + name + domainName + } + person { + id + displayName + avatarUrl + } + } + } +`; + export const GET_ACTIVITIES_BY_TARGETS = gql` query GetActivitiesByTargets( $activityTargetIds: [String!]! @@ -8,49 +61,17 @@ export const GET_ACTIVITIES_BY_TARGETS = gql` findManyActivities( orderBy: $orderBy where: { - activityTargets: { some: { commentableId: { in: $activityTargetIds } } } - } - ) { - id - createdAt - title - body - type - completedAt - dueAt - assignee { - id - firstName - lastName - displayName - avatarUrl - } - author { - id - firstName - lastName - displayName - } - comments { - id - body - createdAt - updatedAt - author { - id - displayName - firstName - lastName - avatarUrl + activityTargets: { + some: { + OR: [ + { personId: { in: $activityTargetIds } } + { companyId: { in: $activityTargetIds } } + ] + } } } - activityTargets { - id - commentableType - commentableId - companyId - personId - } + ) { + ...ActivityQueryFragment } } `; @@ -61,36 +82,7 @@ export const GET_ACTIVITIES = gql` $orderBy: [ActivityOrderByWithRelationInput!] ) { findManyActivities(orderBy: $orderBy, where: $where) { - id - createdAt - title - body - type - completedAt - dueAt - assignee { - id - firstName - lastName - displayName - avatarUrl - } - author { - id - firstName - lastName - displayName - } - comments { - id - } - activityTargets { - id - commentableType - commentableId - companyId - personId - } + ...ActivityQueryFragment } } `; @@ -98,46 +90,7 @@ export const GET_ACTIVITIES = gql` export const GET_ACTIVITY = gql` query GetActivity($activityId: String!) { findManyActivities(where: { id: { equals: $activityId } }) { - id - createdAt - body - title - type - completedAt - dueAt - assignee { - id - firstName - lastName - displayName - avatarUrl - } - author { - id - firstName - lastName - displayName - } - comments { - id - body - createdAt - updatedAt - author { - id - displayName - firstName - lastName - avatarUrl - } - } - activityTargets { - id - commentableType - commentableId - companyId - personId - } + ...ActivityQueryFragment } } `; diff --git a/front/src/modules/activities/queries/update.ts b/front/src/modules/activities/queries/update.ts index be716efac..02aa5d782 100644 --- a/front/src/modules/activities/queries/update.ts +++ b/front/src/modules/activities/queries/update.ts @@ -16,8 +16,6 @@ export const ADD_ACTIVITY_TARGETS = gql` id createdAt updatedAt - commentableType - commentableId companyId personId } @@ -43,8 +41,6 @@ export const REMOVE_ACTIVITY_TARGETS = gql` id createdAt updatedAt - commentableType - commentableId companyId personId } diff --git a/front/src/modules/activities/right-drawer/components/RightDrawerTimeline.tsx b/front/src/modules/activities/right-drawer/components/RightDrawerTimeline.tsx index d8787605b..bbbcdeb25 100644 --- a/front/src/modules/activities/right-drawer/components/RightDrawerTimeline.tsx +++ b/front/src/modules/activities/right-drawer/components/RightDrawerTimeline.tsx @@ -1,19 +1,21 @@ -import { useRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; -import { commentableEntityArrayState } from '@/activities/states/commentableEntityArrayState'; +import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState'; import { Timeline } from '@/activities/timeline/components/Timeline'; export function RightDrawerTimeline() { - const [commentableEntityArray] = useRecoilState(commentableEntityArrayState); + const activityTargetableEntityArray = useRecoilValue( + activityTargetableEntityArrayState, + ); return ( <> - {commentableEntityArray.map((commentableEntity) => ( + {activityTargetableEntityArray.map((targetableEntity) => ( ))} diff --git a/front/src/modules/activities/states/activityTargetableEntityArrayState.ts b/front/src/modules/activities/states/activityTargetableEntityArrayState.ts new file mode 100644 index 000000000..25e2da755 --- /dev/null +++ b/front/src/modules/activities/states/activityTargetableEntityArrayState.ts @@ -0,0 +1,10 @@ +import { atom } from 'recoil'; + +import { ActivityTargetableEntity } from '../types/ActivityTargetableEntity'; + +export const activityTargetableEntityArrayState = atom< + ActivityTargetableEntity[] +>({ + key: 'activities/targetable-entity-array', + default: [], +}); diff --git a/front/src/modules/activities/states/commentableEntityArrayState.ts b/front/src/modules/activities/states/commentableEntityArrayState.ts deleted file mode 100644 index 987bb9a3d..000000000 --- a/front/src/modules/activities/states/commentableEntityArrayState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { atom } from 'recoil'; - -import { CommentableEntity } from '../types/CommentableEntity'; - -export const commentableEntityArrayState = atom({ - key: 'activities/commentable-entity-array', - default: [], -}); diff --git a/front/src/modules/activities/timeline/components/Timeline.tsx b/front/src/modules/activities/timeline/components/Timeline.tsx index a5dd649da..62efcf24c 100644 --- a/front/src/modules/activities/timeline/components/Timeline.tsx +++ b/front/src/modules/activities/timeline/components/Timeline.tsx @@ -5,7 +5,7 @@ import styled from '@emotion/styled'; import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer'; -import { CommentableEntity } from '@/activities/types/CommentableEntity'; +import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity'; import { IconCircleDot } from '@/ui/icon'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { @@ -93,7 +93,7 @@ const StyledStartIcon = styled.div` width: 20px; `; -export function Timeline({ entity }: { entity: CommentableEntity }) { +export function Timeline({ entity }: { entity: ActivityTargetableEntity }) { const theme = useTheme(); const { data: queryResult, loading } = useGetActivitiesByTargetsQuery({ diff --git a/front/src/modules/activities/types/ActivityTargetableEntity.ts b/front/src/modules/activities/types/ActivityTargetableEntity.ts new file mode 100644 index 000000000..b9455968d --- /dev/null +++ b/front/src/modules/activities/types/ActivityTargetableEntity.ts @@ -0,0 +1,9 @@ +export enum ActivityTargetableEntityType { + Person = 'Person', + Company = 'Company', +} + +export type ActivityTargetableEntity = { + id: string; + type: ActivityTargetableEntityType; +}; diff --git a/front/src/modules/activities/types/ActivityTargetableEntityForSelect.ts b/front/src/modules/activities/types/ActivityTargetableEntityForSelect.ts new file mode 100644 index 000000000..aa6ca8f92 --- /dev/null +++ b/front/src/modules/activities/types/ActivityTargetableEntityForSelect.ts @@ -0,0 +1,7 @@ +import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; + +import { ActivityTargetableEntityType } from './ActivityTargetableEntity'; + +export type ActivityTargetableEntityForSelect = EntityForSelect & { + entityType: ActivityTargetableEntityType; +}; diff --git a/front/src/modules/activities/types/CommentableEntity.ts b/front/src/modules/activities/types/CommentableEntity.ts deleted file mode 100644 index 481e513c8..000000000 --- a/front/src/modules/activities/types/CommentableEntity.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { CommentableType } from '~/generated/graphql'; - -export type CommentableEntity = { - id: string; - type: CommentableType; -}; diff --git a/front/src/modules/activities/types/CommentableEntityForSelect.ts b/front/src/modules/activities/types/CommentableEntityForSelect.ts deleted file mode 100644 index 07bfdfc27..000000000 --- a/front/src/modules/activities/types/CommentableEntityForSelect.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; -import { CommentableType } from '~/generated/graphql'; - -export type CommentableEntityForSelect = EntityForSelect & { - entityType: CommentableType; -}; diff --git a/front/src/modules/command-menu/components/CommandMenu.tsx b/front/src/modules/command-menu/components/CommandMenu.tsx index d4d3ed5ea..feaeb2de7 100644 --- a/front/src/modules/command-menu/components/CommandMenu.tsx +++ b/front/src/modules/command-menu/components/CommandMenu.tsx @@ -92,43 +92,6 @@ export function CommandMenu() { cmd.type === CommandType.Create, ); - /* - TODO: Allow performing actions on page through CommandBar - - import { useMatch, useResolvedPath } from 'react-router-dom'; - import { IconBuildingSkyscraper, IconUser } from '@/ui/icon'; - - const createSection = ( - - } - shortcuts={ - !!useMatch({ - path: useResolvedPath('/people').pathname, - end: true, - }) - ? ['C'] - : [] - } - /> - } - shortcuts={ - !!useMatch({ - path: useResolvedPath('/companies').pathname, - end: true, - }) - ? ['C'] - : [] - } - /> - - );*/ - return ( )} @@ -245,7 +209,7 @@ export function CommandMenu() { ) .map((cmd) => ( void; icon?: ReactNode; shortcuts?: Array; diff --git a/front/src/modules/companies/components/CompanyBoardCard.tsx b/front/src/modules/companies/components/CompanyBoardCard.tsx index 1974fa1a8..08bd59aed 100644 --- a/front/src/modules/companies/components/CompanyBoardCard.tsx +++ b/front/src/modules/companies/components/CompanyBoardCard.tsx @@ -1,9 +1,9 @@ import { ReactNode, useContext } from 'react'; import styled from '@emotion/styled'; -import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { useCurrentCardSelected } from '@/ui/board/hooks/useCurrentCardSelected'; import { BoardCardIdContext } from '@/ui/board/states/BoardCardIdContext'; -import { selectedBoardCardIdsState } from '@/ui/board/states/selectedBoardCardIdsState'; import { viewFieldsDefinitionsState } from '@/ui/board/states/viewFieldsDefinitionsState'; import { EntityChipVariant } from '@/ui/chip/components/EntityChip'; import { GenericEditableField } from '@/ui/editable-field/components/GenericEditableField'; @@ -14,7 +14,6 @@ import { Checkbox, CheckboxVariant, } from '@/ui/input/checkbox/components/Checkbox'; -import { actionBarOpenState } from '@/ui/table/states/ActionBarIsOpenState'; import { useUpdateOnePipelineProgressMutation } from '~/generated/graphql'; import { getLogoUrlFromDomainName } from '~/utils'; @@ -41,7 +40,7 @@ const StyledBoardCard = styled.div<{ selected: boolean }>` cursor: pointer; .checkbox-container { - opacity: 0; + opacity: ${({ selected }) => (selected ? 1 : 0)}; } &:hover .checkbox-container { @@ -103,6 +102,8 @@ const StyledFieldContainer = styled.div` `; export function CompanyBoardCard() { + const { currentCardSelected, setCurrentCardSelected } = + useCurrentCardSelected(); const boardCardId = useContext(BoardCardIdContext); const [companyProgress] = useRecoilState( @@ -110,25 +111,8 @@ export function CompanyBoardCard() { ); const { pipelineProgress, company } = companyProgress ?? {}; - const [selectedBoardCards, setSelectedBoardCards] = useRecoilState( - selectedBoardCardIdsState, - ); const viewFieldsDefinitions = useRecoilValue(viewFieldsDefinitionsState); - const selected = selectedBoardCards.includes(boardCardId ?? ''); - const setActionBarOpenState = useSetRecoilState(actionBarOpenState); - - function setSelected(isSelected: boolean) { - if (isSelected) { - setSelectedBoardCards([...selectedBoardCards, boardCardId ?? '']); - setActionBarOpenState(true); - } else { - setSelectedBoardCards( - selectedBoardCards.filter((id) => id !== boardCardId), - ); - } - } - // boardCardId check can be moved to a wrapper to avoid unnecessary logic above if (!company || !pipelineProgress || !boardCardId) { return null; @@ -153,8 +137,8 @@ export function CompanyBoardCard() { return ( setSelected(!selected)} + selected={currentCardSelected} + onClick={() => setCurrentCardSelected(!currentCardSelected)} > setSelected(!selected)} + checked={currentCardSelected} + onChange={() => setCurrentCardSelected(!currentCardSelected)} variant={CheckboxVariant.Secondary} /> diff --git a/front/src/modules/companies/components/CompanyPicker.tsx b/front/src/modules/companies/components/CompanyPicker.tsx new file mode 100644 index 000000000..102e71f9b --- /dev/null +++ b/front/src/modules/companies/components/CompanyPicker.tsx @@ -0,0 +1,41 @@ +import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; +import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; +import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; + +import { useFilteredSearchCompanyQuery } from '../queries'; + +export type OwnProps = { + companyId: string | null; + onSubmit: (newCompanyId: EntityForSelect | null) => void; + onCancel?: () => void; +}; + +export function CompanyPicker({ companyId, onSubmit, onCancel }: OwnProps) { + const [searchFilter] = useRecoilScopedState( + relationPickerSearchFilterScopedState, + ); + + const companies = useFilteredSearchCompanyQuery({ + searchFilter, + selectedIds: companyId ? [companyId] : [], + }); + + async function handleEntitySelected( + selectedCompany: EntityForSelect | null | undefined, + ) { + onSubmit(selectedCompany ?? null); + } + + return ( + + ); +} diff --git a/front/src/modules/companies/components/HooksCompanyBoard.tsx b/front/src/modules/companies/components/HooksCompanyBoard.tsx index ffda2cf6e..5c86847f3 100644 --- a/front/src/modules/companies/components/HooksCompanyBoard.tsx +++ b/front/src/modules/companies/components/HooksCompanyBoard.tsx @@ -4,8 +4,10 @@ import { useRecoilState, useSetRecoilState } from 'recoil'; import { pipelineViewFields } from '@/pipeline/constants/pipelineViewFields'; import { isBoardLoadedState } from '@/ui/board/states/isBoardLoadedState'; import { viewFieldsDefinitionsState } from '@/ui/board/states/viewFieldsDefinitionsState'; +import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause'; +import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { PipelineProgressableType, @@ -17,6 +19,7 @@ import { useGetPipelineProgressQuery, useGetPipelinesQuery, } from '~/generated/graphql'; +import { opportunitiesBoardOptions } from '~/pages/opportunities/opportunitiesBoardOptions'; import { useUpdateCompanyBoardCardIds } from '../hooks/useUpdateBoardCardIds'; import { useUpdateCompanyBoard } from '../hooks/useUpdateCompanyBoardColumns'; @@ -30,8 +33,13 @@ export function HooksCompanyBoard({ const setFieldsDefinitionsState = useSetRecoilState( viewFieldsDefinitionsState, ); + const [, setAvailableFilters] = useRecoilScopedState( + availableFiltersScopedState, + CompanyBoardContext, + ); useEffect(() => { + setAvailableFilters(opportunitiesBoardOptions.filters); setFieldsDefinitionsState(pipelineViewFields); }); diff --git a/front/src/modules/companies/editable-field/components/CompanyAccountOwnerEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyAccountOwnerEditableField.tsx deleted file mode 100644 index b5ed11bb8..000000000 --- a/front/src/modules/companies/editable-field/components/CompanyAccountOwnerEditableField.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { EditableField } from '@/ui/editable-field/components/EditableField'; -import { FieldContext } from '@/ui/editable-field/states/FieldContext'; -import { IconUserCircle } from '@/ui/icon'; -import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; -import { UserChip } from '@/users/components/UserChip'; -import { Company, User } from '~/generated/graphql'; - -import { CompanyAccountOwnerPickerFieldEditMode } from './CompanyAccountOwnerPickerFieldEditMode'; - -type OwnProps = { - company: Pick & { - accountOwner?: Pick | null; - }; -}; - -export function CompanyAccountOwnerEditableField({ company }: OwnProps) { - return ( - - - } - editModeContent={ - - } - displayModeContent={ - company.accountOwner?.displayName ? ( - - ) : ( - <> - ) - } - isDisplayModeContentEmpty={!company.accountOwner} - isDisplayModeFixHeight={true} - /> - - - ); -} diff --git a/front/src/modules/companies/editable-field/components/CompanyAccountOwnerPickerFieldEditMode.tsx b/front/src/modules/companies/editable-field/components/CompanyAccountOwnerPickerFieldEditMode.tsx deleted file mode 100644 index 0b63a3a06..000000000 --- a/front/src/modules/companies/editable-field/components/CompanyAccountOwnerPickerFieldEditMode.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import styled from '@emotion/styled'; - -import { CompanyAccountOwnerPicker } from '@/companies/components/CompanyAccountOwnerPicker'; -import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; -import { Company, User } from '~/generated/graphql'; - -const StyledContainer = styled.div` - left: 0px; - position: absolute; - top: -8px; -`; - -export type OwnProps = { - company: Pick & { - accountOwner?: Pick | null; - }; - onSubmit?: () => void; - onCancel?: () => void; -}; - -export function CompanyAccountOwnerPickerFieldEditMode({ - company, - onSubmit, - onCancel, -}: OwnProps) { - const { closeEditableField } = useEditableField(); - - function handleSubmit() { - closeEditableField(); - onSubmit?.(); - } - - function handleCancel() { - closeEditableField(); - onCancel?.(); - } - - return ( - - - - ); -} diff --git a/front/src/modules/companies/editable-field/components/CompanyAddressEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyAddressEditableField.tsx deleted file mode 100644 index fe7430359..000000000 --- a/front/src/modules/companies/editable-field/components/CompanyAddressEditableField.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { EditableField } from '@/ui/editable-field/components/EditableField'; -import { FieldContext } from '@/ui/editable-field/states/FieldContext'; -import { IconMap } from '@/ui/icon'; -import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; -import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql'; - -type OwnProps = { - company: Pick; -}; - -export function CompanyAddressEditableField({ company }: OwnProps) { - const [internalValue, setInternalValue] = useState(company.address); - - const [updateCompany] = useUpdateOneCompanyMutation(); - - useEffect(() => { - setInternalValue(company.address); - }, [company.address]); - - async function handleChange(newValue: string) { - setInternalValue(newValue); - } - - async function handleSubmit() { - await updateCompany({ - variables: { - where: { - id: company.id, - }, - data: { - address: internalValue ?? '', - }, - }, - }); - } - - async function handleCancel() { - setInternalValue(company.address); - } - - return ( - - } - editModeContent={ - { - handleChange(newValue); - }} - /> - } - displayModeContent={internalValue ?? ''} - isDisplayModeContentEmpty={!(internalValue !== '')} - /> - - ); -} diff --git a/front/src/modules/companies/editable-field/components/CompanyCreatedAtEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyCreatedAtEditableField.tsx deleted file mode 100644 index fd614ba03..000000000 --- a/front/src/modules/companies/editable-field/components/CompanyCreatedAtEditableField.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { EditableField } from '@/ui/editable-field/components/EditableField'; -import { FieldContext } from '@/ui/editable-field/states/FieldContext'; -import { EditableFieldEditModeDate } from '@/ui/editable-field/variants/components/EditableFieldEditModeDate'; -import { IconCalendar } from '@/ui/icon'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; -import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql'; -import { formatToHumanReadableDate } from '~/utils'; -import { parseDate } from '~/utils/date-utils'; - -type OwnProps = { - company: Pick; -}; - -export function CompanyCreatedAtEditableField({ company }: OwnProps) { - const [internalValue, setInternalValue] = useState(company.createdAt); - - const [updateCompany] = useUpdateOneCompanyMutation(); - - useEffect(() => { - setInternalValue(company.createdAt); - }, [company.createdAt]); - - // TODO: refactor change and submit - async function handleChange(newValue: string) { - setInternalValue(newValue); - - await updateCompany({ - variables: { - where: { - id: company.id, - }, - data: { - createdAt: newValue ?? '', - }, - }, - }); - } - - async function handleSubmit() { - await updateCompany({ - variables: { - where: { - id: company.id, - }, - data: { - createdAt: internalValue ?? '', - }, - }, - }); - } - - async function handleCancel() { - setInternalValue(company.createdAt); - } - - return ( - - } - editModeContent={ - - } - displayModeContent={ - internalValue !== '' - ? formatToHumanReadableDate(parseDate(internalValue).toJSDate()) - : 'No date' - } - isDisplayModeContentEmpty={!(internalValue !== '')} - /> - - ); -} diff --git a/front/src/modules/companies/editable-field/components/CompanyDomainNameEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyDomainNameEditableField.tsx deleted file mode 100644 index 00f13d372..000000000 --- a/front/src/modules/companies/editable-field/components/CompanyDomainNameEditableField.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { EditableField } from '@/ui/editable-field/components/EditableField'; -import { FieldDisplayURL } from '@/ui/editable-field/components/FieldDisplayURL'; -import { FieldContext } from '@/ui/editable-field/states/FieldContext'; -import { IconLink } from '@/ui/icon'; -import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; -import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql'; - -type OwnProps = { - company: Pick; -}; - -export function CompanyDomainNameEditableField({ company }: OwnProps) { - const [internalValue, setInternalValue] = useState(company.domainName); - - const [updateCompany] = useUpdateOneCompanyMutation(); - - useEffect(() => { - setInternalValue(company.domainName); - }, [company.domainName]); - - async function handleChange(newValue: string) { - setInternalValue(newValue); - } - - async function handleSubmit() { - await updateCompany({ - variables: { - where: { - id: company.id, - }, - data: { - domainName: internalValue ?? '', - }, - }, - }); - } - - async function handleCancel() { - setInternalValue(company.domainName); - } - - return ( - - } - onCancel={handleCancel} - onSubmit={handleSubmit} - editModeContent={ - { - handleChange(newValue); - }} - /> - } - displayModeContent={} - useEditButton - isDisplayModeContentEmpty={!(internalValue !== '')} - /> - - ); -} diff --git a/front/src/modules/companies/editable-field/components/CompanyEmployeesEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyEmployeesEditableField.tsx deleted file mode 100644 index c4cb9d009..000000000 --- a/front/src/modules/companies/editable-field/components/CompanyEmployeesEditableField.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { EditableField } from '@/ui/editable-field/components/EditableField'; -import { FieldContext } from '@/ui/editable-field/states/FieldContext'; -import { IconUsers } from '@/ui/icon'; -import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; -import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql'; -import { - canBeCastAsIntegerOrNull, - castAsIntegerOrNull, -} from '~/utils/cast-as-integer-or-null'; - -type OwnProps = { - company: Pick; -}; - -export function CompanyEmployeesEditableField({ company }: OwnProps) { - const [internalValue, setInternalValue] = useState( - company.employees?.toString(), - ); - - const [updateCompany] = useUpdateOneCompanyMutation(); - - useEffect(() => { - setInternalValue(company.employees?.toString()); - }, [company.employees]); - - async function handleChange(newValue: string) { - setInternalValue(newValue); - } - - async function handleSubmit() { - if (!canBeCastAsIntegerOrNull(internalValue)) { - handleCancel(); - return; - } - - const valueCastedAsNumberOrNull = castAsIntegerOrNull(internalValue); - - await updateCompany({ - variables: { - where: { - id: company.id, - }, - data: { - employees: valueCastedAsNumberOrNull, - }, - }, - }); - - setInternalValue(valueCastedAsNumberOrNull?.toString()); - } - - async function handleCancel() { - setInternalValue(company.employees?.toString()); - } - - return ( - - } - editModeContent={ - { - handleChange(newValue); - }} - /> - } - displayModeContent={internalValue} - isDisplayModeContentEmpty={!(internalValue && internalValue !== '0')} - /> - - ); -} diff --git a/front/src/modules/companies/hooks/useOpenActionBar.tsx b/front/src/modules/companies/hooks/useOpenActionBar.tsx index 290a980bc..3a623698a 100644 --- a/front/src/modules/companies/hooks/useOpenActionBar.tsx +++ b/front/src/modules/companies/hooks/useOpenActionBar.tsx @@ -2,6 +2,7 @@ import { getOperationName } from '@apollo/client/utilities'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; +import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity'; import { GET_PIPELINES } from '@/pipeline/queries'; import { ActionBarEntry } from '@/ui/action-bar/components/ActionBarEntry'; import { IconCheckbox, IconNotes, IconTrash } from '@/ui/icon'; @@ -11,7 +12,6 @@ import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; import { ActivityType, - CommentableType, useDeleteManyCompaniesMutation, } from '~/generated/graphql'; @@ -22,7 +22,7 @@ export function useOpenActionBar() { useOpenCreateActivityDrawerForSelectedRowIds(); async function handleActivityClick(type: ActivityType) { - openCreateActivityRightDrawer(type, CommentableType.Company); + openCreateActivityRightDrawer(type, ActivityTargetableEntityType.Company); } const selectedRowIds = useRecoilValue(selectedRowIdsSelector); diff --git a/front/src/modules/companies/hooks/useOpenContextMenu.tsx b/front/src/modules/companies/hooks/useOpenContextMenu.tsx index ed8765a48..376c19928 100644 --- a/front/src/modules/companies/hooks/useOpenContextMenu.tsx +++ b/front/src/modules/companies/hooks/useOpenContextMenu.tsx @@ -3,6 +3,7 @@ import { IconCheckbox, IconNotes, IconTrash } from '@tabler/icons-react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; +import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity'; import { GET_PIPELINES } from '@/pipeline/queries'; import { ContextMenuEntry } from '@/ui/context-menu/components/ContextMenuEntry'; import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; @@ -11,7 +12,6 @@ import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; import { ActivityType, - CommentableType, useDeleteManyCompaniesMutation, } from '~/generated/graphql'; @@ -22,7 +22,7 @@ export function useOpenContextMenu() { useOpenCreateActivityDrawerForSelectedRowIds(); async function handleButtonClick(type: ActivityType) { - openCreateActivityRightDrawer(type, CommentableType.Company); + openCreateActivityRightDrawer(type, ActivityTargetableEntityType.Company); } const selectedRowIds = useRecoilValue(selectedRowIdsSelector); diff --git a/front/src/modules/companies/queries/select.ts b/front/src/modules/companies/queries/select.ts index fdecd5e07..9929f7d4f 100644 --- a/front/src/modules/companies/queries/select.ts +++ b/front/src/modules/companies/queries/select.ts @@ -1,10 +1,10 @@ import { gql } from '@apollo/client'; -import { CommentableEntityForSelect } from '@/activities/types/CommentableEntityForSelect'; +import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity'; +import { ActivityTargetableEntityForSelect } from '@/activities/types/ActivityTargetableEntityForSelect'; import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; import { SelectedSortType } from '@/ui/filter-n-sort/types/interface'; import { - CommentableType, CompanyOrderByWithRelationInput as Companies_Order_By, CompanyWhereInput as Companies_Bool_Exp, SortOrder as Order_By, @@ -65,11 +65,11 @@ export function useFilteredSearchCompanyQuery({ mappingFunction: (company) => ({ id: company.id, - entityType: CommentableType.Company, + entityType: ActivityTargetableEntityType.Company, name: company.name, avatarUrl: getLogoUrlFromDomainName(company.domainName), avatarType: 'squared', - } as CommentableEntityForSelect), + } as ActivityTargetableEntityForSelect), searchFilter, limit, }); diff --git a/front/src/modules/companies/queries/show.ts b/front/src/modules/companies/queries/show.ts index ebc4d63ab..a372632a5 100644 --- a/front/src/modules/companies/queries/show.ts +++ b/front/src/modules/companies/queries/show.ts @@ -1,5 +1,7 @@ import { gql } from '@apollo/client'; +import { useSetRecoilState } from 'recoil'; +import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState'; import { useGetCompanyQuery } from '~/generated/graphql'; export const GET_COMPANY = gql` @@ -33,5 +35,13 @@ export const GET_COMPANY = gql` `; export function useCompanyQuery(id: string) { - return useGetCompanyQuery({ variables: { where: { id } } }); + const updateCompanyShowPage = useSetRecoilState( + genericEntitiesFamilyState(id), + ); + return useGetCompanyQuery({ + variables: { where: { id } }, + onCompleted: (data) => { + updateCompanyShowPage(data?.findUniqueCompany); + }, + }); } diff --git a/front/src/modules/companies/queries/update.ts b/front/src/modules/companies/queries/update.ts index fe9c2c7e7..b8f31cf73 100644 --- a/front/src/modules/companies/queries/update.ts +++ b/front/src/modules/companies/queries/update.ts @@ -1,43 +1,38 @@ import { gql } from '@apollo/client'; +export const COMPANY_FIELDS_FRAGMENT = gql` + fragment CompanyFieldsFragment on Company { + accountOwner { + id + email + displayName + avatarUrl + } + address + createdAt + domainName + employees + linkedinUrl + id + name + } +`; + export const UPDATE_ONE_COMPANY = gql` mutation UpdateOneCompany( $where: CompanyWhereUniqueInput! $data: CompanyUpdateInput! ) { updateOneCompany(data: $data, where: $where) { - accountOwner { - id - email - displayName - firstName - lastName - } - address - createdAt - domainName - employees - linkedinUrl - id - name + ...CompanyFieldsFragment } } `; -export const INSERT_COMPANY_FRAGMENT = gql` - fragment InsertCompanyFragment on Company { - domainName - address - id - name - createdAt - } -`; - export const INSERT_ONE_COMPANY = gql` mutation InsertOneCompany($data: CompanyCreateInput!) { createOneCompany(data: $data) { - ...InsertCompanyFragment + ...CompanyFieldsFragment } } `; diff --git a/front/src/modules/companies/table/components/CompanyTable.tsx b/front/src/modules/companies/table/components/CompanyTable.tsx index cd5ba3d7d..2574ae4d5 100644 --- a/front/src/modules/companies/table/components/CompanyTable.tsx +++ b/front/src/modules/companies/table/components/CompanyTable.tsx @@ -8,11 +8,14 @@ import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIn import { IconList } from '@/ui/icon'; import { EntityTable } from '@/ui/table/components/EntityTable'; import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData'; +import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem'; import { TableContext } from '@/ui/table/states/TableContext'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { useTableViewFields } from '@/views/hooks/useTableViewFields'; import { useViewSorts } from '@/views/hooks/useViewSorts'; import { currentViewIdState } from '@/views/states/currentViewIdState'; import { + UpdateOneCompanyMutationVariables, useGetCompaniesQuery, useUpdateOneCompanyMutation, } from '~/generated/graphql'; @@ -24,6 +27,13 @@ import { defaultOrderBy } from '../../queries'; export function CompanyTable() { const currentViewId = useRecoilValue(currentViewIdState); const orderBy = useRecoilScopedValue(sortsOrderByScopedState, TableContext); + const [updateEntityMutation] = useUpdateOneCompanyMutation(); + const upsertEntityTableItem = useUpsertEntityTableItem(); + + const { handleColumnsChange } = useTableViewFields({ + objectName: 'company', + viewFieldDefinitions: companyViewFields, + }); const { updateSorts } = useViewSorts({ availableSorts, Context: TableContext, @@ -38,20 +48,33 @@ export function CompanyTable() { return ( <> } availableSorts={availableSorts} + onColumnsChange={handleColumnsChange} onSortsUpdate={currentViewId ? updateSorts : undefined} - useUpdateEntityMutation={useUpdateOneCompanyMutation} + updateEntityMutation={({ + variables, + }: { + variables: UpdateOneCompanyMutationVariables; + }) => + updateEntityMutation({ + variables, + onCompleted: (data) => { + if (!data.updateOneCompany) { + return; + } + upsertEntityTableItem(data.updateOneCompany); + }, + }) + } /> ); diff --git a/front/src/modules/companies/table/components/CompanyTableMockData.tsx b/front/src/modules/companies/table/components/CompanyTableMockData.tsx index f28c26c43..c1cc8b2bb 100644 --- a/front/src/modules/companies/table/components/CompanyTableMockData.tsx +++ b/front/src/modules/companies/table/components/CompanyTableMockData.tsx @@ -2,32 +2,21 @@ import { useEffect } from 'react'; import { useSetRecoilState } from 'recoil'; import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData'; -import { entityTableDimensionsState } from '@/ui/table/states/entityTableDimensionsState'; -import { viewFieldsState } from '@/ui/table/states/viewFieldsState'; +import { tableColumnsState } from '@/ui/table/states/tableColumnsState'; import { companyViewFields } from '../../constants/companyViewFields'; import { mockedCompaniesData } from './companies-mock-data'; export function CompanyTableMockData() { - const setEntityTableDimensions = useSetRecoilState( - entityTableDimensionsState, - ); - const setViewFieldsState = useSetRecoilState(viewFieldsState); + const setColumns = useSetRecoilState(tableColumnsState); const setEntityTableData = useSetEntityTableData(); setEntityTableData(mockedCompaniesData, []); useEffect(() => { - setViewFieldsState({ - objectName: 'company', - viewFields: companyViewFields, - }); - setEntityTableDimensions((prevState) => ({ - ...prevState, - numberOfColumns: companyViewFields.length, - })); - }, [setEntityTableDimensions, setViewFieldsState]); + setColumns(companyViewFields); + }, [setColumns]); return <>; } diff --git a/front/src/modules/companies/table/components/CompanyTableMockMode.tsx b/front/src/modules/companies/table/components/CompanyTableMockMode.tsx index a7de979c8..83a1128bf 100644 --- a/front/src/modules/companies/table/components/CompanyTableMockMode.tsx +++ b/front/src/modules/companies/table/components/CompanyTableMockMode.tsx @@ -13,7 +13,7 @@ export function CompanyTableMockMode() { viewName="All Companies" viewIcon={} availableSorts={availableSorts} - useUpdateEntityMutation={useUpdateOneCompanyMutation} + updateEntityMutation={[useUpdateOneCompanyMutation()]} /> ); diff --git a/front/src/modules/people/components/PeopleCompanyPicker.tsx b/front/src/modules/people/components/PeopleCompanyPicker.tsx deleted file mode 100644 index b61c88405..000000000 --- a/front/src/modules/people/components/PeopleCompanyPicker.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useFilteredSearchCompanyQuery } from '@/companies/queries'; -import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; -import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; -import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; -import { useEditableCell } from '@/ui/table/editable-cell/hooks/useEditableCell'; -import { isCreateModeScopedState } from '@/ui/table/editable-cell/states/isCreateModeScopedState'; -import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope'; -import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; -import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; -import { - Company, - Person, - useUpdateOnePersonMutation, -} from '~/generated/graphql'; - -export type OwnProps = { - people: Pick & { company?: Pick | null }; -}; - -export function PeopleCompanyPicker({ people }: OwnProps) { - const [, setIsCreating] = useRecoilScopedState(isCreateModeScopedState); - - const [searchFilter] = useRecoilScopedState( - relationPickerSearchFilterScopedState, - ); - const [updatePerson] = useUpdateOnePersonMutation(); - - const { closeEditableCell } = useEditableCell(); - - const addToScopeStack = useSetHotkeyScope(); - - const companies = useFilteredSearchCompanyQuery({ - searchFilter, - selectedIds: people.company?.id ? [people.company.id] : [], - }); - - async function handleEntitySelected( - entity: EntityForSelect | null | undefined, - ) { - if (entity) { - await updatePerson({ - variables: { - where: { - id: people.id, - }, - data: { - company: { connect: { id: entity.id } }, - }, - }, - }); - } - - closeEditableCell(); - } - - function handleCreate() { - setIsCreating(true); - addToScopeStack(TableHotkeyScope.CellDoubleTextInput); - } - - return ( - closeEditableCell()} - onEntitySelected={handleEntitySelected} - entities={{ - entitiesToSelect: companies.entitiesToSelect, - selectedEntity: companies.selectedEntities[0], - loading: companies.loading, - }} - /> - ); -} diff --git a/front/src/modules/people/components/PersonPropertyBox.tsx b/front/src/modules/people/components/PersonPropertyBox.tsx deleted file mode 100644 index 841758b38..000000000 --- a/front/src/modules/people/components/PersonPropertyBox.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { - IconCalendar, - IconMail, - IconMap, - IconPhone, -} from '@tabler/icons-react'; - -import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox'; -import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField'; -import { PhoneEditableField } from '@/ui/editable-field/variants/components/PhoneEditableField'; -import { TextEditableField } from '@/ui/editable-field/variants/components/TextEditableField'; -import { - Company, - Person, - useUpdateOnePersonMutation, -} from '~/generated/graphql'; - -import { PeopleCompanyEditableField } from '../editable-field/components/PeopleCompanyEditableField'; - -type OwnProps = { - person: Pick< - Person, - 'id' | 'city' | 'email' | 'displayName' | 'phone' | 'createdAt' - > & { - company?: Pick | null; - }; -}; - -export function PersonPropertyBox({ person }: OwnProps) { - const [updatePerson] = useUpdateOnePersonMutation(); - - return ( - - } - placeholder={'Email'} - onSubmit={(newEmail) => { - updatePerson({ - variables: { - where: { - id: person.id, - }, - data: { - email: newEmail, - }, - }, - }); - }} - /> - } - placeholder={'Phone'} - onSubmit={(newPhone) => { - updatePerson({ - variables: { - where: { - id: person.id, - }, - data: { - phone: newPhone, - }, - }, - }); - }} - /> - } - onSubmit={(newDate) => { - updatePerson({ - variables: { - where: { - id: person.id, - }, - data: { - createdAt: newDate, - }, - }, - }); - }} - /> - - } - placeholder={'City'} - onSubmit={(newCity) => { - updatePerson({ - variables: { - where: { - id: person.id, - }, - data: { - city: newCity, - }, - }, - }); - }} - /> - - ); -} diff --git a/front/src/modules/people/components/__stories__/PeopleCompanyEditableField.stories.tsx b/front/src/modules/people/components/__stories__/PeopleCompanyEditableField.stories.tsx deleted file mode 100644 index 6ecc78aae..000000000 --- a/front/src/modules/people/components/__stories__/PeopleCompanyEditableField.stories.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { BrowserRouter } from 'react-router-dom'; -import type { Meta, StoryObj } from '@storybook/react'; - -import { mockedPeopleData } from '~/testing/mock-data/people'; - -import { PeopleCompanyEditableField } from '../../editable-field/components/PeopleCompanyEditableField'; - -const meta: Meta = { - title: 'Modules/People/EditableFields/PeopleCompanyEditableField', - component: PeopleCompanyEditableField, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: () => ( - - - - ), -}; diff --git a/front/src/modules/people/editable-field/components/PeopleCompanyEditableField.tsx b/front/src/modules/people/editable-field/components/PeopleCompanyEditableField.tsx deleted file mode 100644 index 70d3eb257..000000000 --- a/front/src/modules/people/editable-field/components/PeopleCompanyEditableField.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { IconBuildingSkyscraper } from '@tabler/icons-react'; - -import { CompanyChip } from '@/companies/components/CompanyChip'; -import { EditableField } from '@/ui/editable-field/components/EditableField'; -import { FieldContext } from '@/ui/editable-field/states/FieldContext'; -import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; -import { Company, Person } from '~/generated/graphql'; -import { getLogoUrlFromDomainName } from '~/utils'; - -import { PeopleCompanyEditableFieldEditMode } from './PeopleCompanyEditableFieldEditMode'; - -export type OwnProps = { - people: Pick & { - company?: Pick | null; - }; -}; - -export function PeopleCompanyEditableField({ people }: OwnProps) { - return ( - - - } - editModeContent={ - - } - displayModeContent={ - people.company ? ( - - ) : ( - <> - ) - } - isDisplayModeContentEmpty={!people.company} - isDisplayModeFixHeight - /> - - - ); -} diff --git a/front/src/modules/people/editable-field/components/PeopleCompanyEditableFieldEditMode.tsx b/front/src/modules/people/editable-field/components/PeopleCompanyEditableFieldEditMode.tsx deleted file mode 100644 index 01f790a4c..000000000 --- a/front/src/modules/people/editable-field/components/PeopleCompanyEditableFieldEditMode.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import styled from '@emotion/styled'; - -import { useFilteredSearchCompanyQuery } from '@/companies/queries'; -import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; -import { SingleEntitySelect } from '@/ui/input/relation-picker/components/SingleEntitySelect'; -import { relationPickerSearchFilterScopedState } from '@/ui/input/relation-picker/states/relationPickerSearchFilterScopedState'; -import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; -import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; -import { - Company, - Person, - useUpdateOnePersonMutation, -} from '~/generated/graphql'; - -export type OwnProps = { - people: Pick & { company?: Pick | null }; -}; - -const StyledContainer = styled.div` - left: 0px; - position: absolute; - top: -8px; -`; - -export function PeopleCompanyEditableFieldEditMode({ people }: OwnProps) { - const { closeEditableField } = useEditableField(); - - const [searchFilter] = useRecoilScopedState( - relationPickerSearchFilterScopedState, - ); - const [updatePerson] = useUpdateOnePersonMutation(); - - const companies = useFilteredSearchCompanyQuery({ - searchFilter, - selectedIds: people.company?.id ? [people.company.id] : [], - }); - - async function handleEntitySelected( - entity: EntityForSelect | null | undefined, - ) { - if (entity) { - await updatePerson({ - variables: { - where: { - id: people.id, - }, - data: { - company: { connect: { id: entity.id } }, - }, - }, - }); - } - - closeEditableField(); - } - - function handleCancel() { - closeEditableField(); - } - - return ( - - - - ); -} diff --git a/front/src/modules/people/hooks/useOpenActionBar.tsx b/front/src/modules/people/hooks/useOpenActionBar.tsx index 8f425ad8d..2742e6b1e 100644 --- a/front/src/modules/people/hooks/useOpenActionBar.tsx +++ b/front/src/modules/people/hooks/useOpenActionBar.tsx @@ -2,17 +2,14 @@ import { getOperationName } from '@apollo/client/utilities'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; +import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity'; import { ActionBarEntry } from '@/ui/action-bar/components/ActionBarEntry'; import { IconCheckbox, IconNotes, IconTrash } from '@/ui/icon'; import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; import { actionBarEntriesState } from '@/ui/table/states/ActionBarEntriesState'; import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector'; import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; -import { - ActivityType, - CommentableType, - useDeleteManyPersonMutation, -} from '~/generated/graphql'; +import { ActivityType, useDeleteManyPersonMutation } from '~/generated/graphql'; import { GET_PEOPLE } from '../queries'; @@ -23,7 +20,7 @@ export function useOpenActionBar() { useOpenCreateActivityDrawerForSelectedRowIds(); async function handleActivityClick(type: ActivityType) { - openCreateActivityRightDrawer(type, CommentableType.Person); + openCreateActivityRightDrawer(type, ActivityTargetableEntityType.Person); } const selectedRowIds = useRecoilValue(selectedRowIdsSelector); diff --git a/front/src/modules/people/hooks/useOpenContextMenu.tsx b/front/src/modules/people/hooks/useOpenContextMenu.tsx index 3792bdfac..559970177 100644 --- a/front/src/modules/people/hooks/useOpenContextMenu.tsx +++ b/front/src/modules/people/hooks/useOpenContextMenu.tsx @@ -3,16 +3,13 @@ import { IconCheckbox, IconNotes, IconTrash } from '@tabler/icons-react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; +import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity'; import { ContextMenuEntry } from '@/ui/context-menu/components/ContextMenuEntry'; import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; import { contextMenuEntriesState } from '@/ui/table/states/ContextMenuEntriesState'; import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector'; import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; -import { - ActivityType, - CommentableType, - useDeleteManyPersonMutation, -} from '~/generated/graphql'; +import { ActivityType, useDeleteManyPersonMutation } from '~/generated/graphql'; import { GET_PEOPLE } from '../queries'; @@ -23,7 +20,7 @@ export function useOpenContextMenu() { useOpenCreateActivityDrawerForSelectedRowIds(); async function handleActivityClick(type: ActivityType) { - openCreateActivityRightDrawer(type, CommentableType.Person); + openCreateActivityRightDrawer(type, ActivityTargetableEntityType.Person); } const selectedRowIds = useRecoilValue(selectedRowIdsSelector); diff --git a/front/src/modules/people/hooks/useSetPeopleEntityTable.ts b/front/src/modules/people/hooks/useSetPeopleEntityTable.ts index aa7513e15..9ae902b14 100644 --- a/front/src/modules/people/hooks/useSetPeopleEntityTable.ts +++ b/front/src/modules/people/hooks/useSetPeopleEntityTable.ts @@ -1,17 +1,17 @@ import { useLocation } from 'react-router-dom'; import { useRecoilCallback } from 'recoil'; +import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState'; +import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; +import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEntityTableDataState'; +import { numberOfTableRowsState } from '@/ui/table/states/numberOfTableRowsState'; +import { TableContext } from '@/ui/table/states/TableContext'; +import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; import { currentPageLocationState } from '@/ui/utilities/loading-state/states/currentPageLocationState'; import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { GetPeopleQuery } from '~/generated/graphql'; +import { peopleFilters } from '~/pages/people/people-filters'; -import { peopleFilters } from '../../../pages/people/people-filters'; -import { availableFiltersScopedState } from '../../ui/filter-n-sort/states/availableFiltersScopedState'; -import { useResetTableRowSelection } from '../../ui/table/hooks/useResetTableRowSelection'; -import { entityTableDimensionsState } from '../../ui/table/states/entityTableDimensionsState'; -import { isFetchingEntityTableDataState } from '../../ui/table/states/isFetchingEntityTableDataState'; -import { TableContext } from '../../ui/table/states/TableContext'; -import { tableRowIdsState } from '../../ui/table/states/tableRowIdsState'; import { peopleCityFamilyState } from '../states/peopleCityFamilyState'; import { peopleCompanyFamilyState } from '../states/peopleCompanyFamilyState'; import { peopleCreatedAtFamilyState } from '../states/peopleCreatedAtFamilyState'; @@ -124,10 +124,7 @@ export function useSetPeopleEntityTable() { resetTableRowSelection(); - set(entityTableDimensionsState, { - numberOfColumns: 10, - numberOfRows: peopleIds.length, - }); + set(numberOfTableRowsState, peopleIds.length); set(availableFiltersScopedState(tableContextScopeId), peopleFilters); diff --git a/front/src/modules/people/queries/select.ts b/front/src/modules/people/queries/select.ts index 3c532687a..0ef7b3774 100644 --- a/front/src/modules/people/queries/select.ts +++ b/front/src/modules/people/queries/select.ts @@ -1,10 +1,10 @@ import { gql } from '@apollo/client'; -import { CommentableEntityForSelect } from '@/activities/types/CommentableEntityForSelect'; +import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity'; +import { ActivityTargetableEntityForSelect } from '@/activities/types/ActivityTargetableEntityForSelect'; import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery'; import { SelectedSortType } from '@/ui/filter-n-sort/types/interface'; import { - CommentableType, PersonOrderByWithRelationInput as People_Order_By, PersonWhereInput as People_Bool_Exp, SortOrder, @@ -69,11 +69,11 @@ export function useFilteredSearchPeopleQuery({ mappingFunction: (entity) => ({ id: entity.id, - entityType: CommentableType.Person, + entityType: ActivityTargetableEntityType.Person, name: `${entity.firstName} ${entity.lastName}`, avatarUrl: entity.avatarUrl, avatarType: 'rounded', - } as CommentableEntityForSelect), + } as ActivityTargetableEntityForSelect), searchFilter, limit, }); diff --git a/front/src/modules/people/queries/show.ts b/front/src/modules/people/queries/show.ts index 26c1ae3c4..07096f96d 100644 --- a/front/src/modules/people/queries/show.ts +++ b/front/src/modules/people/queries/show.ts @@ -1,5 +1,7 @@ import { gql } from '@apollo/client'; +import { useSetRecoilState } from 'recoil'; +import { genericEntitiesFamilyState } from '@/ui/editable-field/states/genericEntitiesFamilyState'; import { useGetPersonQuery } from '~/generated/graphql'; export const GET_PERSON = gql` @@ -37,5 +39,13 @@ export const GET_PERSON = gql` `; export function usePersonQuery(id: string) { - return useGetPersonQuery({ variables: { id } }); + const updatePersonShowPage = useSetRecoilState( + genericEntitiesFamilyState(id), + ); + return useGetPersonQuery({ + variables: { id }, + onCompleted: (data) => { + updatePersonShowPage(data?.findUniquePerson); + }, + }); } diff --git a/front/src/modules/people/table/components/PeopleTable.tsx b/front/src/modules/people/table/components/PeopleTable.tsx index 0e4bd2ba7..4f081e9ab 100644 --- a/front/src/modules/people/table/components/PeopleTable.tsx +++ b/front/src/modules/people/table/components/PeopleTable.tsx @@ -8,11 +8,14 @@ import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIn import { IconList } from '@/ui/icon'; import { EntityTable } from '@/ui/table/components/EntityTable'; import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData'; +import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem'; import { TableContext } from '@/ui/table/states/TableContext'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; +import { useTableViewFields } from '@/views/hooks/useTableViewFields'; import { useViewSorts } from '@/views/hooks/useViewSorts'; import { currentViewIdState } from '@/views/states/currentViewIdState'; import { + UpdateOnePersonMutationVariables, useGetPeopleQuery, useUpdateOnePersonMutation, } from '~/generated/graphql'; @@ -24,6 +27,13 @@ import { defaultOrderBy } from '../../queries'; export function PeopleTable() { const currentViewId = useRecoilValue(currentViewIdState); const orderBy = useRecoilScopedValue(sortsOrderByScopedState, TableContext); + const [updateEntityMutation] = useUpdateOnePersonMutation(); + const upsertEntityTableItem = useUpsertEntityTableItem(); + + const { handleColumnsChange } = useTableViewFields({ + objectName: 'person', + viewFieldDefinitions: peopleViewFields, + }); const { updateSorts } = useViewSorts({ availableSorts, Context: TableContext, @@ -38,20 +48,33 @@ export function PeopleTable() { return ( <> } availableSorts={availableSorts} + onColumnsChange={handleColumnsChange} onSortsUpdate={currentViewId ? updateSorts : undefined} - useUpdateEntityMutation={useUpdateOnePersonMutation} + updateEntityMutation={({ + variables, + }: { + variables: UpdateOnePersonMutationVariables; + }) => + updateEntityMutation({ + variables, + onCompleted: (data) => { + if (!data.updateOnePerson) { + return; + } + upsertEntityTableItem(data.updateOnePerson); + }, + }) + } /> ); diff --git a/front/src/modules/pipeline/constants/pipelineViewFields.tsx b/front/src/modules/pipeline/constants/pipelineViewFields.tsx index 59d7df110..4bce73430 100644 --- a/front/src/modules/pipeline/constants/pipelineViewFields.tsx +++ b/front/src/modules/pipeline/constants/pipelineViewFields.tsx @@ -61,6 +61,7 @@ export const pipelineViewFields: ViewFieldDefinition[] = [ type: 'relation', fieldName: 'pointOfContact', relationType: Entity.Person, + useEditButton: true, }, isVisible: true, } satisfies ViewFieldDefinition, diff --git a/front/src/modules/search/queries/search.ts b/front/src/modules/search/queries/search.ts index 50157768f..9eb90ea8e 100644 --- a/front/src/modules/search/queries/search.ts +++ b/front/src/modules/search/queries/search.ts @@ -64,9 +64,7 @@ export const SEARCH_COMPANY_QUERY = gql` take: $limit orderBy: $orderBy ) { - id - name - domainName + ...CompanyFieldsFragment } } `; diff --git a/front/src/modules/ui/board/card-field/components/BoardCardEditableField.tsx b/front/src/modules/ui/board/card-field/components/BoardCardEditableField.tsx deleted file mode 100644 index 77f5d6374..000000000 --- a/front/src/modules/ui/board/card-field/components/BoardCardEditableField.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { ReactElement } from 'react'; - -import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; - -import { BoardCardFieldContext } from '../states/BoardCardFieldContext'; - -import { BoardCardEditableFieldInternal } from './BoardCardEditableFieldInternal'; - -type OwnProps = { - editModeContent: ReactElement; - nonEditModeContent: ReactElement; - editModeHorizontalAlign?: 'left' | 'right'; - editModeVerticalPosition?: 'over' | 'below'; - editHotkeyScope?: HotkeyScope; -}; - -export function BoardCardEditableField(props: OwnProps) { - return ( - - - - ); -} diff --git a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldDate.tsx b/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldDate.tsx deleted file mode 100644 index 42757dae7..000000000 --- a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldDate.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useMemo, useState } from 'react'; - -import { DateInputDisplay } from '@/ui/input/date/components/DateInputDisplay'; -import { debounce } from '~/utils/debounce'; - -import { BoardCardEditableField } from './BoardCardEditableField'; -import { BoardCardEditableFieldDateEditMode } from './BoardCardEditableFieldDateEditMode'; - -type OwnProps = { - value: Date; - onChange: (newValue: Date) => void; - editModeHorizontalAlign?: 'left' | 'right'; -}; - -export function BoardCardEditableFieldDate({ - value, - onChange, - editModeHorizontalAlign, -}: OwnProps) { - const [internalValue, setInternalValue] = useState(value); - const debouncedOnChange = useMemo(() => { - return debounce(onChange, 200); - }, [onChange]); - return ( - { - setInternalValue(date); - debouncedOnChange(date); - }} - /> - } - nonEditModeContent={} - > - ); -} diff --git a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldDateEditMode.tsx b/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldDateEditMode.tsx deleted file mode 100644 index bb08f7904..000000000 --- a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldDateEditMode.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { DateInputEdit } from '@/ui/input/date/components/DateInputEdit'; - -type OwnProps = { - value: Date; - onChange: (newValue: Date) => void; -}; - -export function BoardCardEditableFieldDateEditMode({ - value, - onChange, -}: OwnProps) { - function handleDateChange(newDate: Date) { - onChange(newDate); - } - - return ; -} diff --git a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldDisplayMode.tsx b/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldDisplayMode.tsx deleted file mode 100644 index a3dfce28d..000000000 --- a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldDisplayMode.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import styled from '@emotion/styled'; - -export const BoardCardFieldDisplayModeOuterContainer = styled.div` - align-items: center; - display: flex; - height: 100%; - overflow: hidden; - - padding-left: ${({ theme }) => theme.spacing(2)}; - padding-right: ${({ theme }) => theme.spacing(1)}; - width: 100%; -`; - -export const BoardCardFieldDisplayModeInnerContainer = styled.div` - align-items: center; - display: flex; - height: 100%; - overflow: hidden; - width: 100%; -`; - -export function BoardCardEditableFieldDisplayMode({ - children, -}: React.PropsWithChildren) { - return ( - - - {children} - - - ); -} diff --git a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldEditMode.tsx b/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldEditMode.tsx deleted file mode 100644 index 180091a13..000000000 --- a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldEditMode.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { ReactElement, useRef } from 'react'; -import styled from '@emotion/styled'; - -import { overlayBackground } from '@/ui/theme/constants/effects'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; - -import { BoardCardFieldHotkeyScope } from '../types/BoardCardFieldHotkeyScope'; - -export const BoardCardFieldEditModeContainer = styled.div< - Omit ->` - align-items: center; - border: 1px solid ${({ theme }) => theme.border.color.light}; - border-radius: ${({ theme }) => theme.border.radius.sm}; - display: flex; - left: ${(props) => - props.editModeHorizontalAlign === 'right' ? 'auto' : '0'}; - margin-left: -2px; - min-height: 100%; - min-width: calc(100% + 20px); - position: absolute; - - right: ${(props) => - props.editModeHorizontalAlign === 'right' ? '0' : 'auto'}; - top: ${(props) => (props.editModeVerticalPosition === 'over' ? '0' : '100%')}; - z-index: 1; - ${overlayBackground} -`; - -type OwnProps = { - children: ReactElement; - editModeHorizontalAlign?: 'left' | 'right'; - editModeVerticalPosition?: 'over' | 'below'; - onExit: () => void; -}; - -export function BoardCardEditableFieldEditMode({ - editModeHorizontalAlign, - editModeVerticalPosition, - children, - onExit, -}: OwnProps) { - const wrapperRef = useRef(null); - - useListenClickOutside({ - refs: [wrapperRef], - callback: () => { - onExit(); - }, - }); - - useScopedHotkeys( - 'enter', - () => { - onExit(); - }, - BoardCardFieldHotkeyScope.BoardCardFieldEditMode, - [onExit], - ); - - useScopedHotkeys( - 'esc', - () => { - onExit(); - }, - BoardCardFieldHotkeyScope.BoardCardFieldEditMode, - [onExit], - ); - - return ( - - {children} - - ); -} diff --git a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldInternal.tsx b/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldInternal.tsx deleted file mode 100644 index 43fcb0ebc..000000000 --- a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldInternal.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { ReactElement } from 'react'; -import styled from '@emotion/styled'; - -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; -import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; - -import { useBoardCardField } from '../hooks/useBoardCardField'; -import { BoardCardFieldHotkeyScope } from '../types/BoardCardFieldHotkeyScope'; - -import { BoardCardEditableFieldDisplayMode } from './BoardCardEditableFieldDisplayMode'; -import { BoardCardEditableFieldEditMode } from './BoardCardEditableFieldEditMode'; - -export const BoardCardFieldContainer = styled.div` - align-items: center; - box-sizing: border-box; - cursor: pointer; - display: flex; - height: 32px; - position: relative; - user-select: none; - width: 100%; -`; - -type OwnProps = { - editModeContent: ReactElement; - nonEditModeContent: ReactElement; - editModeHorizontalAlign?: 'left' | 'right'; - editModeVerticalPosition?: 'over' | 'below'; - editHotkeyScope?: HotkeyScope; -}; - -export function BoardCardEditableFieldInternal({ - editModeHorizontalAlign = 'left', - editModeVerticalPosition = 'over', - editModeContent, - nonEditModeContent, - editHotkeyScope, -}: OwnProps) { - const { openBoardCardField, isBoardCardFieldInEditMode } = - useBoardCardField(); - - const { closeBoardCardField } = useBoardCardField(); - - const { - goBackToPreviousHotkeyScope, - setHotkeyScopeAndMemorizePreviousScope, - } = usePreviousHotkeyScope(); - - function handleOnClick() { - if (!isBoardCardFieldInEditMode) { - openBoardCardField(); - setHotkeyScopeAndMemorizePreviousScope( - editHotkeyScope?.scope ?? - BoardCardFieldHotkeyScope.BoardCardFieldEditMode, - editHotkeyScope?.customScopes ?? {}, - ); - } - } - - function handleEditModeExit() { - goBackToPreviousHotkeyScope(); - closeBoardCardField(); - } - - return ( - - {isBoardCardFieldInEditMode ? ( - - {editModeContent} - - ) : ( - - {nonEditModeContent} - - )} - - ); -} diff --git a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldText.tsx b/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldText.tsx deleted file mode 100644 index eb8d8caf9..000000000 --- a/front/src/modules/ui/board/card-field/components/BoardCardEditableFieldText.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { ChangeEvent, useMemo, useState } from 'react'; - -import { TextInputDisplay } from '@/ui/input/text/components/TextInputDisplay'; -import { StyledInput } from '@/ui/table/editable-cell/type/components/TextCellEdit'; -import { debounce } from '~/utils/debounce'; - -import { BoardCardEditableField } from './BoardCardEditableField'; - -type OwnProps = { - placeholder?: string; - value: string; - onChange: (newValue: string) => void; - editModeHorizontalAlign?: 'left' | 'right'; -}; - -export function BoardCardEditableFieldText({ - value, - placeholder, - onChange, - editModeHorizontalAlign, -}: OwnProps) { - const [internalValue, setInternalValue] = useState(value); - - const debouncedOnChange = useMemo(() => { - return debounce(onChange, 200); - }, [onChange]); - - return ( - ) => { - setInternalValue(event.target.value); - debouncedOnChange(event.target.value); - }} - /> - } - nonEditModeContent={{value}} - > - ); -} diff --git a/front/src/modules/ui/board/card-field/hooks/useBoardCardField.ts b/front/src/modules/ui/board/card-field/hooks/useBoardCardField.ts deleted file mode 100644 index 0545e4909..000000000 --- a/front/src/modules/ui/board/card-field/hooks/useBoardCardField.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; - -import { BoardCardFieldContext } from '../states/BoardCardFieldContext'; -import { isBoardCardFieldInEditModeScopedState } from '../states/isBoardCardFieldInEditModeScopedState'; - -export function useBoardCardField() { - const [isBoardCardFieldInEditMode, setIsBoardCardFieldInEditMode] = - useRecoilScopedState( - isBoardCardFieldInEditModeScopedState, - BoardCardFieldContext, - ); - - function openBoardCardField() { - setIsBoardCardFieldInEditMode(true); - } - - function closeBoardCardField() { - setIsBoardCardFieldInEditMode(false); - } - - return { - isBoardCardFieldInEditMode, - openBoardCardField, - closeBoardCardField, - }; -} diff --git a/front/src/modules/ui/board/card-field/states/BoardCardFieldContext.ts b/front/src/modules/ui/board/card-field/states/BoardCardFieldContext.ts deleted file mode 100644 index 6d8beeb4e..000000000 --- a/front/src/modules/ui/board/card-field/states/BoardCardFieldContext.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export const BoardCardFieldContext = createContext(null); diff --git a/front/src/modules/ui/board/card-field/states/isBoardCardFieldInEditModeScopedState.ts b/front/src/modules/ui/board/card-field/states/isBoardCardFieldInEditModeScopedState.ts deleted file mode 100644 index 2fda71a0c..000000000 --- a/front/src/modules/ui/board/card-field/states/isBoardCardFieldInEditModeScopedState.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { atomFamily } from 'recoil'; - -export const isBoardCardFieldInEditModeScopedState = atomFamily< - boolean, - string ->({ - key: 'isBoardCardFieldInEditModeScopedState', - default: false, -}); diff --git a/front/src/modules/ui/board/card-field/types/BoardCardFieldHotkeyScope.ts b/front/src/modules/ui/board/card-field/types/BoardCardFieldHotkeyScope.ts deleted file mode 100644 index 98b6ade87..000000000 --- a/front/src/modules/ui/board/card-field/types/BoardCardFieldHotkeyScope.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum BoardCardFieldHotkeyScope { - BoardCardFieldEditMode = 'board-card-field-edit-mode', -} diff --git a/front/src/modules/ui/board/components/BoardActionBarButtonDeleteBoardCard.tsx b/front/src/modules/ui/board/components/BoardActionBarButtonDeleteBoardCard.tsx index 3048b03f4..6baeb5057 100644 --- a/front/src/modules/ui/board/components/BoardActionBarButtonDeleteBoardCard.tsx +++ b/front/src/modules/ui/board/components/BoardActionBarButtonDeleteBoardCard.tsx @@ -1,53 +1,40 @@ -import { useRecoilCallback } from 'recoil'; +import { getOperationName } from '@apollo/client/utilities'; +import { useRecoilValue } from 'recoil'; +import { GET_PIPELINES } from '@/pipeline/queries'; import { ActionBarEntry } from '@/ui/action-bar/components/ActionBarEntry'; -import { boardCardIdsByColumnIdFamilyState } from '@/ui/board/states/boardCardIdsByColumnIdFamilyState'; -import { boardColumnsState } from '@/ui/board/states/boardColumnsState'; -import { selectedBoardCardIdsState } from '@/ui/board/states/selectedBoardCardIdsState'; import { IconTrash } from '@/ui/icon/index'; +import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql'; -export function BoardActionBarButtonDeleteBoardCard({ - onDelete, -}: { - onDelete: (deletedCardIds: string[]) => void; -}) { - const deleteBoardCardIds = useRecoilCallback( - ({ set, snapshot }) => - () => { - const boardCardIdsToDelete = snapshot - .getLoadable(selectedBoardCardIdsState) - .getValue(); +import { useRemoveCardIds } from '../hooks/useRemoveCardIds'; +import { selectedCardIdsSelector } from '../states/selectedCardIdsSelector'; - const boardColumns = snapshot.getLoadable(boardColumnsState).getValue(); +export function BoardActionBarButtonDeleteBoardCard() { + const selectedCardIds = useRecoilValue(selectedCardIdsSelector); + const removeCardIds = useRemoveCardIds(); - for (const boardColumn of boardColumns) { - const boardColumnCardIds = snapshot - .getLoadable(boardCardIdsByColumnIdFamilyState(boardColumn.id)) - .getValue(); + const [deletePipelineProgress] = useDeleteManyPipelineProgressMutation({ + refetchQueries: [getOperationName(GET_PIPELINES) ?? ''], + }); - const newBoardColumnCardIds = boardColumnCardIds.filter( - (cardId) => !boardCardIdsToDelete.includes(cardId), - ); - - if (newBoardColumnCardIds.length !== boardColumnCardIds.length) { - set( - boardCardIdsByColumnIdFamilyState(boardColumn.id), - newBoardColumnCardIds, - ); - } - } - - set(selectedBoardCardIdsState, []); - - return boardCardIdsToDelete; + async function handleDelete() { + await deletePipelineProgress({ + variables: { + ids: selectedCardIds, }, - [], - ); - - async function handleDeleteClick() { - const deletedCardIds = deleteBoardCardIds(); - - onDelete(deletedCardIds); + optimisticResponse: { + __typename: 'Mutation', + deleteManyPipelineProgress: { + count: selectedCardIds.length, + }, + }, + update: (cache) => { + removeCardIds(selectedCardIds); + selectedCardIds.forEach((id) => { + cache.evict({ id: `PipelineProgress:${id}` }); + }); + }, + }); } return ( @@ -55,7 +42,7 @@ export function BoardActionBarButtonDeleteBoardCard({ label="Delete" icon={} type="danger" - onClick={handleDeleteClick} + onClick={handleDelete} /> ); } diff --git a/front/src/modules/ui/board/components/EntityBoard.tsx b/front/src/modules/ui/board/components/EntityBoard.tsx index e725f1431..6bc6e33b6 100644 --- a/front/src/modules/ui/board/components/EntityBoard.tsx +++ b/front/src/modules/ui/board/components/EntityBoard.tsx @@ -4,16 +4,14 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 import { IconList } from '@tabler/icons-react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; +import { useRecoilState } from 'recoil'; import { CompanyBoardContext } from '@/companies/states/CompanyBoardContext'; import { GET_PIPELINE_PROGRESS } from '@/pipeline/queries'; import { BoardHeader } from '@/ui/board/components/BoardHeader'; import { StyledBoard } from '@/ui/board/components/StyledBoard'; -import { useUpdateBoardCardIds } from '@/ui/board/hooks/useUpdateBoardCardIds'; import { BoardColumnIdContext } from '@/ui/board/states/BoardColumnIdContext'; import { SelectedSortType } from '@/ui/filter-n-sort/types/interface'; -import { actionBarOpenState } from '@/ui/table/states/ActionBarIsOpenState'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { @@ -23,9 +21,10 @@ import { useUpdateOnePipelineProgressStageMutation, } from '~/generated/graphql'; +import { useSetCardSelected } from '../hooks/useSetCardSelected'; +import { useUpdateBoardCardIds } from '../hooks/useUpdateBoardCardIds'; import { BoardColumnContext } from '../states/BoardColumnContext'; import { boardColumnsState } from '../states/boardColumnsState'; -import { selectedBoardCardIdsState } from '../states/selectedBoardCardIdsState'; import { BoardOptions } from '../types/BoardOptions'; import { EntityBoardColumn } from './EntityBoardColumn'; @@ -49,6 +48,7 @@ export function EntityBoard({ onEditColumnTitle: (columnId: string, title: string, color: string) => void; }) { const [boardColumns] = useRecoilState(boardColumnsState); + const setCardSelected = useSetCardSelected(); const theme = useTheme(); const [updatePipelineProgressStage] = @@ -105,21 +105,6 @@ export function EntityBoard({ }); const boardRef = useRef(null); - const [selectedBoardCards, setSelectedBoardCards] = useRecoilState( - selectedBoardCardIdsState, - ); - const setActionBarOpenState = useSetRecoilState(actionBarOpenState); - - function setRowSelectedState(boardCardId: string, selected: boolean) { - if (selected && !selectedBoardCards.includes(boardCardId)) { - setSelectedBoardCards([...selectedBoardCards, boardCardId ?? '']); - setActionBarOpenState(true); - } else if (!selected && selectedBoardCards.includes(boardCardId)) { - setSelectedBoardCards( - selectedBoardCards.filter((id) => id !== boardCardId), - ); - } - } return (boardColumns?.length ?? 0) > 0 ? ( @@ -147,7 +132,7 @@ export function EntityBoard({ ) : ( diff --git a/front/src/modules/ui/board/components/EntityBoardActionBar.tsx b/front/src/modules/ui/board/components/EntityBoardActionBar.tsx index bccd1ff0b..4df6dddb9 100644 --- a/front/src/modules/ui/board/components/EntityBoardActionBar.tsx +++ b/front/src/modules/ui/board/components/EntityBoardActionBar.tsx @@ -3,9 +3,9 @@ import { useRecoilValue } from 'recoil'; import { ActionBar } from '@/ui/action-bar/components/ActionBar'; -import { selectedBoardCardIdsState } from '../states/selectedBoardCardIdsState'; +import { selectedCardIdsSelector } from '../states/selectedCardIdsSelector'; export function EntityBoardActionBar() { - const selectedBoardCards = useRecoilValue(selectedBoardCardIdsState); + const selectedBoardCards = useRecoilValue(selectedCardIdsSelector); return ; } diff --git a/front/src/modules/ui/board/components/__tests__/Board.test.ts b/front/src/modules/ui/board/components/__tests__/Board.test.ts deleted file mode 100644 index 8701b3c9f..000000000 --- a/front/src/modules/ui/board/components/__tests__/Board.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -// TODO: refactor this test with Recoil -describe('getOptimisticlyUpdatedBoard', () => { - it('should return a new board with the updated cell', () => { - // const initialColumn1: string[] = ['item-1', 'item-2', 'item-3']; - // const initialColumn2: string[] = ['item-4', 'item-5']; - // const finalColumn1: string[] = ['item-2', 'item-3']; - // const finalColumn2: string[] = ['item-4', 'item-1', 'item-5']; - // const dropResult = { - // source: { - // droppableId: 'column-1', - // index: 0, - // }, - // destination: { - // droppableId: 'column-2', - // index: 1, - // }, - // } as DropResult; - // const initialBoard = [ - // { - // id: 'column-1', - // title: 'My Column', - // pipelineStageId: 'column-1', - // pipelineProgressIds: initialColumn1, - // }, - // { - // id: 'column-2', - // title: 'My Column', - // pipelineStageId: 'column-2', - // pipelineProgressIds: initialColumn2, - // }, - // ]; - // const updatedBoard = u( - // initialBoard, - // dropResult, - // ); - // const finalBoard = [ - // { - // id: 'column-1', - // title: 'My Column', - // pipelineStageId: 'column-1', - // pipelineProgressIds: finalColumn1, - // }, - // { - // id: 'column-2', - // title: 'My Column', - // pipelineStageId: 'column-2', - // pipelineProgressIds: finalColumn2, - // }, - // ]; - // expect(updatedBoard).toEqual(finalBoard); - // expect(updatedBoard).not.toBe(initialBoard); - }); -}); diff --git a/front/src/modules/ui/board/hooks/useActionBar.tsx b/front/src/modules/ui/board/hooks/useActionBar.tsx index 2b8aea3bf..0eee81fff 100644 --- a/front/src/modules/ui/board/hooks/useActionBar.tsx +++ b/front/src/modules/ui/board/hooks/useActionBar.tsx @@ -1,30 +1,13 @@ -import { getOperationName } from '@apollo/client/utilities'; import { useSetRecoilState } from 'recoil'; -import { GET_PIPELINES } from '@/pipeline/queries'; import { actionBarEntriesState } from '@/ui/table/states/ActionBarEntriesState'; -import { useDeleteManyPipelineProgressMutation } from '~/generated/graphql'; import { BoardActionBarButtonDeleteBoardCard } from '../components/BoardActionBarButtonDeleteBoardCard'; export function useOpenActionBar() { const setActionBarEntries = useSetRecoilState(actionBarEntriesState); - const [deletePipelineProgress] = useDeleteManyPipelineProgressMutation({ - refetchQueries: [getOperationName(GET_PIPELINES) ?? ''], - }); - - async function handleDelete(cardIdsToDelete: string[]) { - await deletePipelineProgress({ - variables: { - ids: cardIdsToDelete, - }, - }); - } - return () => { - setActionBarEntries([ - , - ]); + setActionBarEntries([]); }; } diff --git a/front/src/modules/ui/board/hooks/useCurrentCardSelected.ts b/front/src/modules/ui/board/hooks/useCurrentCardSelected.ts new file mode 100644 index 000000000..78a17f38d --- /dev/null +++ b/front/src/modules/ui/board/hooks/useCurrentCardSelected.ts @@ -0,0 +1,28 @@ +import { useContext } from 'react'; +import { useRecoilCallback, useRecoilState } from 'recoil'; + +import { BoardCardIdContext } from '../states/BoardCardIdContext'; +import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState'; + +export function useCurrentCardSelected() { + const currentCardId = useContext(BoardCardIdContext); + + const [isCardSelected] = useRecoilState( + isCardSelectedFamilyState(currentCardId ?? ''), + ); + + const setCurrentCardSelected = useRecoilCallback( + ({ set }) => + (selected: boolean) => { + if (!currentCardId) return; + + set(isCardSelectedFamilyState(currentCardId), selected); + }, + [], + ); + + return { + currentCardSelected: isCardSelected, + setCurrentCardSelected, + }; +} diff --git a/front/src/modules/ui/board/hooks/useRemoveCardIds.ts b/front/src/modules/ui/board/hooks/useRemoveCardIds.ts new file mode 100644 index 000000000..871cc887f --- /dev/null +++ b/front/src/modules/ui/board/hooks/useRemoveCardIds.ts @@ -0,0 +1,27 @@ +// Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350 +import { useRecoilCallback } from 'recoil'; + +import { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState'; +import { boardColumnsState } from '../states/boardColumnsState'; + +export function useRemoveCardIds() { + return useRecoilCallback( + ({ snapshot, set }) => + (cardIdToRemove: string[]) => { + const boardColumns = snapshot + .getLoadable(boardColumnsState) + .valueOrThrow(); + + boardColumns.forEach((boardColumn) => { + const columnCardIds = snapshot + .getLoadable(boardCardIdsByColumnIdFamilyState(boardColumn.id)) + .valueOrThrow(); + set( + boardCardIdsByColumnIdFamilyState(boardColumn.id), + columnCardIds.filter((cardId) => !cardIdToRemove.includes(cardId)), + ); + }); + }, + [], + ); +} diff --git a/front/src/modules/ui/board/hooks/useResetCardSelection.ts b/front/src/modules/ui/board/hooks/useResetCardSelection.ts new file mode 100644 index 000000000..7bb92cbd6 --- /dev/null +++ b/front/src/modules/ui/board/hooks/useResetCardSelection.ts @@ -0,0 +1,27 @@ +import { useRecoilCallback } from 'recoil'; + +import { boardCardIdsByColumnIdFamilyState } from '../states/boardCardIdsByColumnIdFamilyState'; +import { boardColumnsState } from '../states/boardColumnsState'; +import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState'; + +export function useResetCardSelection() { + return useRecoilCallback( + ({ snapshot, set }) => + () => { + const boardColumns = snapshot + .getLoadable(boardColumnsState) + .valueOrThrow(); + + const cardIds = boardColumns.flatMap((boardColumn) => + snapshot + .getLoadable(boardCardIdsByColumnIdFamilyState(boardColumn.id)) + .valueOrThrow(), + ); + + for (const cardId of cardIds) { + set(isCardSelectedFamilyState(cardId), false); + } + }, + [], + ); +} diff --git a/front/src/modules/ui/board/hooks/useSetCardSelected.ts b/front/src/modules/ui/board/hooks/useSetCardSelected.ts new file mode 100644 index 000000000..2bb3763f2 --- /dev/null +++ b/front/src/modules/ui/board/hooks/useSetCardSelected.ts @@ -0,0 +1,9 @@ +import { useRecoilCallback } from 'recoil'; + +import { isCardSelectedFamilyState } from '../states/isCardSelectedFamilyState'; + +export function useSetCardSelected() { + return useRecoilCallback(({ set }) => (cardId: string, selected: boolean) => { + set(isCardSelectedFamilyState(cardId), selected); + }); +} diff --git a/front/src/modules/ui/board/states/FieldDefinitionContext.ts b/front/src/modules/ui/board/states/FieldDefinitionContext.ts index 0a7fb8b2a..1b165791d 100644 --- a/front/src/modules/ui/board/states/FieldDefinitionContext.ts +++ b/front/src/modules/ui/board/states/FieldDefinitionContext.ts @@ -1,7 +1,10 @@ import { createContext } from 'react'; import { FieldDefinition } from '@/ui/editable-field/types/FieldDefinition'; -import { FieldMetadata } from '@/ui/editable-field/types/FieldMetadata'; +import { + FieldMetadata, + FieldType, +} from '@/ui/editable-field/types/FieldMetadata'; export const FieldDefinitionContext = createContext< FieldDefinition @@ -9,6 +12,6 @@ export const FieldDefinitionContext = createContext< id: '', label: '', icon: undefined, - type: '', + type: 'unknown' satisfies FieldType, metadata: {} as FieldMetadata, }); diff --git a/front/src/modules/ui/board/states/isCardSelectedFamilyState.ts b/front/src/modules/ui/board/states/isCardSelectedFamilyState.ts new file mode 100644 index 000000000..a6402a88e --- /dev/null +++ b/front/src/modules/ui/board/states/isCardSelectedFamilyState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const isCardSelectedFamilyState = atomFamily({ + key: 'isCardSelectedFamilyState', + default: false, +}); diff --git a/front/src/modules/ui/board/states/selectedBoardCardIdsState.ts b/front/src/modules/ui/board/states/selectedBoardCardIdsState.ts deleted file mode 100644 index bb2beee46..000000000 --- a/front/src/modules/ui/board/states/selectedBoardCardIdsState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { atom } from 'recoil'; - -export const selectedBoardCardIdsState = atom({ - key: 'selectedBoardCardIdsState', - default: [], -}); diff --git a/front/src/modules/ui/board/states/selectedCardIdsSelector.ts b/front/src/modules/ui/board/states/selectedCardIdsSelector.ts new file mode 100644 index 000000000..ff604d6ef --- /dev/null +++ b/front/src/modules/ui/board/states/selectedCardIdsSelector.ts @@ -0,0 +1,22 @@ +import { selector } from 'recoil'; + +import { boardCardIdsByColumnIdFamilyState } from './boardCardIdsByColumnIdFamilyState'; +import { boardColumnsState } from './boardColumnsState'; +import { isCardSelectedFamilyState } from './isCardSelectedFamilyState'; + +export const selectedCardIdsSelector = selector({ + key: 'selectedCardIdsSelector', + get: ({ get }) => { + const boardColumns = get(boardColumnsState); + + const cardIds = boardColumns.flatMap((boardColumn) => + get(boardCardIdsByColumnIdFamilyState(boardColumn.id)), + ); + + const selectedCardIds = cardIds.filter( + (cardId) => get(isCardSelectedFamilyState(cardId)) === true, + ); + + return selectedCardIds; + }, +}); diff --git a/front/src/modules/ui/dropdown/components/DropdownMenu.tsx b/front/src/modules/ui/dropdown/components/DropdownMenu.tsx index 12e09ff91..6b8958706 100644 --- a/front/src/modules/ui/dropdown/components/DropdownMenu.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownMenu.tsx @@ -18,5 +18,5 @@ export const DropdownMenu = styled.div<{ overflow: hidden; - width: ${({ width }) => width ?? 160}px; + width: ${({ width }) => (width && width > 160 ? width : 160)}px; `; diff --git a/front/src/modules/ui/editable-field/components/EditableField.tsx b/front/src/modules/ui/editable-field/components/EditableField.tsx index e7e4bc4b6..538aabc74 100644 --- a/front/src/modules/ui/editable-field/components/EditableField.tsx +++ b/front/src/modules/ui/editable-field/components/EditableField.tsx @@ -4,7 +4,6 @@ import { motion } from 'framer-motion'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { useBindFieldHotkeyScope } from '../hooks/useBindFieldHotkeyScope'; import { useEditableField } from '../hooks/useEditableField'; import { EditableFieldDisplayMode } from './EditableFieldDisplayMode'; @@ -74,7 +73,6 @@ type OwnProps = { displayModeContentOnly?: boolean; disableHoverEffect?: boolean; displayModeContent: React.ReactNode; - parentHotkeyScope?: HotkeyScope; customEditHotkeyScope?: HotkeyScope; isDisplayModeContentEmpty?: boolean; isDisplayModeFixHeight?: boolean; @@ -89,7 +87,6 @@ export function EditableField({ useEditButton, editModeContent, displayModeContent, - parentHotkeyScope, customEditHotkeyScope, disableHoverEffect, isDisplayModeContentEmpty, @@ -100,11 +97,6 @@ export function EditableField({ }: OwnProps) { const [isHovered, setIsHovered] = useState(false); - useBindFieldHotkeyScope({ - customEditHotkeyScope, - parentHotkeyScope, - }); - function handleContainerMouseEnter() { setIsHovered(true); } @@ -116,7 +108,7 @@ export function EditableField({ const { isFieldInEditMode, openEditableField } = useEditableField(); function handleDisplayModeClick() { - openEditableField(); + openEditableField(customEditHotkeyScope); } const showEditButton = !isFieldInEditMode && isHovered && useEditButton; diff --git a/front/src/modules/ui/editable-field/components/EditableFieldEntityText.tsx b/front/src/modules/ui/editable-field/components/EditableFieldEntityText.tsx deleted file mode 100644 index 13b20c2f8..000000000 --- a/front/src/modules/ui/editable-field/components/EditableFieldEntityText.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { EditableField } from '@/ui/editable-field/components/EditableField'; -import { FieldContext } from '@/ui/editable-field/states/FieldContext'; -import { IconMap } from '@/ui/icon'; -import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; -import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql'; - -type OwnProps = { - company: Pick; -}; - -export function CompanyEditableFieldAddress({ company }: OwnProps) { - const [internalValue, setInternalValue] = useState(company.address); - - const [updateCompany] = useUpdateOneCompanyMutation(); - - useEffect(() => { - setInternalValue(company.address); - }, [company.address]); - - async function handleChange(newValue: string) { - setInternalValue(newValue); - } - - async function handleSubmit() { - await updateCompany({ - variables: { - where: { - id: company.id, - }, - data: { - address: internalValue ?? '', - }, - }, - }); - } - - async function handleCancel() { - setInternalValue(company.address); - } - - return ( - - } - editModeContent={ - { - handleChange(newValue); - }} - /> - } - displayModeContent={internalValue !== '' ? internalValue : 'No address'} - /> - - ); -} diff --git a/front/src/modules/ui/editable-field/components/GenericEditableField.tsx b/front/src/modules/ui/editable-field/components/GenericEditableField.tsx index c70881722..501b1ddf7 100644 --- a/front/src/modules/ui/editable-field/components/GenericEditableField.tsx +++ b/front/src/modules/ui/editable-field/components/GenericEditableField.tsx @@ -3,12 +3,18 @@ import { useContext } from 'react'; import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext'; import { isFieldDate } from '../types/guards/isFieldDate'; import { isFieldNumber } from '../types/guards/isFieldNumber'; +import { isFieldPhone } from '../types/guards/isFieldPhone'; import { isFieldProbability } from '../types/guards/isFieldProbability'; import { isFieldRelation } from '../types/guards/isFieldRelation'; +import { isFieldText } from '../types/guards/isFieldText'; +import { isFieldURL } from '../types/guards/isFieldURL'; import { GenericEditableDateField } from './GenericEditableDateField'; import { GenericEditableNumberField } from './GenericEditableNumberField'; +import { GenericEditablePhoneField } from './GenericEditablePhoneField'; import { GenericEditableRelationField } from './GenericEditableRelationField'; +import { GenericEditableTextField } from './GenericEditableTextField'; +import { GenericEditableURLField } from './GenericEditableURLField'; import { ProbabilityEditableField } from './ProbabilityEditableField'; export function GenericEditableField() { @@ -22,9 +28,15 @@ export function GenericEditableField() { return ; } else if (isFieldProbability(fieldDefinition)) { return ; + } else if (isFieldURL(fieldDefinition)) { + return ; + } else if (isFieldText(fieldDefinition)) { + return ; + } else if (isFieldPhone(fieldDefinition)) { + return ; } else { console.warn( - `Unknown field metadata type: ${fieldDefinition.metadata.type} in GenericEditableCell`, + `Unknown field metadata type: ${fieldDefinition.type} in GenericEditableField`, ); return <>; } diff --git a/front/src/modules/ui/editable-field/components/GenericEditableNumberFieldEditMode.tsx b/front/src/modules/ui/editable-field/components/GenericEditableNumberFieldEditMode.tsx index 4ce2ec326..ebf5dec19 100644 --- a/front/src/modules/ui/editable-field/components/GenericEditableNumberFieldEditMode.tsx +++ b/front/src/modules/ui/editable-field/components/GenericEditableNumberFieldEditMode.tsx @@ -69,6 +69,7 @@ export function GenericEditableNumberFieldEditMode() {
{ handleChange(newValue); diff --git a/front/src/modules/ui/editable-field/components/GenericEditablePhoneField.tsx b/front/src/modules/ui/editable-field/components/GenericEditablePhoneField.tsx new file mode 100644 index 000000000..69a252f9f --- /dev/null +++ b/front/src/modules/ui/editable-field/components/GenericEditablePhoneField.tsx @@ -0,0 +1,43 @@ +import { useContext } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { PhoneInputDisplay } from '@/ui/input/phone/components/PhoneInputDisplay'; +import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; + +import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext'; +import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext'; +import { FieldContext } from '../states/FieldContext'; +import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector'; +import { FieldDefinition } from '../types/FieldDefinition'; +import { FieldPhoneMetadata } from '../types/FieldMetadata'; + +import { EditableField } from './EditableField'; +import { GenericEditablePhoneFieldEditMode } from './GenericEditablePhoneFieldEditMode'; + +export function GenericEditablePhoneField() { + const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext); + const currentEditableFieldDefinition = useContext( + EditableFieldDefinitionContext, + ) as FieldDefinition; + + const fieldValue = useRecoilValue( + genericEntityFieldFamilySelector({ + entityId: currentEditableFieldEntityId ?? '', + fieldName: currentEditableFieldDefinition + ? currentEditableFieldDefinition.metadata.fieldName + : '', + }), + ); + + return ( + + } + displayModeContent={} + isDisplayModeContentEmpty={!fieldValue} + /> + + ); +} diff --git a/front/src/modules/ui/editable-field/components/GenericEditablePhoneFieldEditMode.tsx b/front/src/modules/ui/editable-field/components/GenericEditablePhoneFieldEditMode.tsx new file mode 100644 index 000000000..23a59b25f --- /dev/null +++ b/front/src/modules/ui/editable-field/components/GenericEditablePhoneFieldEditMode.tsx @@ -0,0 +1,72 @@ +import { useContext, useRef, useState } from 'react'; +import { useRecoilState } from 'recoil'; + +import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit'; + +import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers'; +import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField'; +import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext'; +import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext'; +import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector'; +import { FieldDefinition } from '../types/FieldDefinition'; +import { FieldPhoneMetadata } from '../types/FieldMetadata'; + +export function GenericEditablePhoneFieldEditMode() { + const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext); + const currentEditableFieldDefinition = useContext( + EditableFieldDefinitionContext, + ) as FieldDefinition; + + // TODO: we could use a hook that would return the field value with the right type + const [fieldValue, setFieldValue] = useRecoilState( + genericEntityFieldFamilySelector({ + entityId: currentEditableFieldEntityId ?? '', + fieldName: currentEditableFieldDefinition + ? currentEditableFieldDefinition.metadata.fieldName + : '', + }), + ); + + const [internalValue, setInternalValue] = useState(fieldValue); + + const updateField = useUpdateGenericEntityField(); + + const wrapperRef = useRef(null); + + useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel); + + function handleSubmit() { + if (internalValue === fieldValue) return; + + setFieldValue(internalValue); + + if (currentEditableFieldEntityId && updateField) { + updateField( + currentEditableFieldEntityId, + currentEditableFieldDefinition, + internalValue, + ); + } + } + + function onCancel() { + setFieldValue(fieldValue); + } + + function handleChange(newValue: string) { + setInternalValue(newValue); + } + + return ( +
+ { + handleChange(newValue); + }} + /> +
+ ); +} diff --git a/front/src/modules/ui/editable-field/components/GenericEditableRelationField.tsx b/front/src/modules/ui/editable-field/components/GenericEditableRelationField.tsx index ae2c42ec2..596b60dea 100644 --- a/front/src/modules/ui/editable-field/components/GenericEditableRelationField.tsx +++ b/front/src/modules/ui/editable-field/components/GenericEditableRelationField.tsx @@ -34,7 +34,7 @@ export function GenericEditableRelationField() { ); } + case Entity.User: { + return ( + + ); + } + case Entity.Company: { + return ( + + ); + } default: console.warn( `Unknown relation type: "${currentEditableFieldDefinition.metadata.relationType}" diff --git a/front/src/modules/ui/editable-field/components/GenericEditableRelationFieldEditMode.tsx b/front/src/modules/ui/editable-field/components/GenericEditableRelationFieldEditMode.tsx index 9b4c08c4f..dc3650e5d 100644 --- a/front/src/modules/ui/editable-field/components/GenericEditableRelationFieldEditMode.tsx +++ b/front/src/modules/ui/editable-field/components/GenericEditableRelationFieldEditMode.tsx @@ -2,10 +2,11 @@ import { useContext } from 'react'; import styled from '@emotion/styled'; import { useRecoilState } from 'recoil'; +import { CompanyPicker } from '@/companies/components/CompanyPicker'; import { PeoplePicker } from '@/people/components/PeoplePicker'; -import { ViewFieldRelationValue } from '@/ui/editable-field/types/ViewField'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; +import { UserPicker } from '@/users/components/UserPicker'; import { useEditableField } from '../hooks/useEditableField'; import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField'; @@ -13,7 +14,10 @@ import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitio import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext'; import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector'; import { FieldDefinition } from '../types/FieldDefinition'; -import { FieldRelationMetadata } from '../types/FieldMetadata'; +import { + FieldRelationMetadata, + FieldRelationValue, +} from '../types/FieldMetadata'; const RelationPickerContainer = styled.div` left: 0px; @@ -28,7 +32,7 @@ function RelationPicker({ handleCancel, }: { fieldDefinition: FieldDefinition; - fieldValue: ViewFieldRelationValue; + fieldValue: FieldRelationValue; handleEntitySubmit: (newRelationId: EntityForSelect | null) => void; handleCancel: () => void; }) { @@ -36,7 +40,25 @@ function RelationPicker({ case Entity.Person: { return ( + ); + } + case Entity.User: { + return ( + + ); + } + case Entity.Company: { + return ( + @@ -46,7 +68,7 @@ function RelationPicker({ console.warn( `Unknown relation type: "${fieldDefinition.metadata.relationType}" in GenericEditableRelationField`, ); - return <> ; + return <>; } } diff --git a/front/src/modules/ui/editable-field/components/GenericEditableTextField.tsx b/front/src/modules/ui/editable-field/components/GenericEditableTextField.tsx new file mode 100644 index 000000000..038d1484d --- /dev/null +++ b/front/src/modules/ui/editable-field/components/GenericEditableTextField.tsx @@ -0,0 +1,41 @@ +import { useContext } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; + +import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext'; +import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext'; +import { FieldContext } from '../states/FieldContext'; +import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector'; +import { FieldDefinition } from '../types/FieldDefinition'; +import { FieldNumberMetadata } from '../types/FieldMetadata'; + +import { EditableField } from './EditableField'; +import { GenericEditableTextFieldEditMode } from './GenericEditableTextFieldEditMode'; + +export function GenericEditableTextField() { + const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext); + const currentEditableFieldDefinition = useContext( + EditableFieldDefinitionContext, + ) as FieldDefinition; + + const fieldValue = useRecoilValue( + genericEntityFieldFamilySelector({ + entityId: currentEditableFieldEntityId ?? '', + fieldName: currentEditableFieldDefinition + ? currentEditableFieldDefinition.metadata.fieldName + : '', + }), + ); + + return ( + + } + displayModeContent={fieldValue} + isDisplayModeContentEmpty={!fieldValue} + /> + + ); +} diff --git a/front/src/modules/ui/editable-field/components/GenericEditableTextFieldEditMode.tsx b/front/src/modules/ui/editable-field/components/GenericEditableTextFieldEditMode.tsx new file mode 100644 index 000000000..815fb2497 --- /dev/null +++ b/front/src/modules/ui/editable-field/components/GenericEditableTextFieldEditMode.tsx @@ -0,0 +1,72 @@ +import { useContext, useRef, useState } from 'react'; +import { useRecoilState } from 'recoil'; + +import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit'; + +import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers'; +import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField'; +import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext'; +import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext'; +import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector'; +import { FieldDefinition } from '../types/FieldDefinition'; +import { FieldTextMetadata } from '../types/FieldMetadata'; + +export function GenericEditableTextFieldEditMode() { + const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext); + const currentEditableFieldDefinition = useContext( + EditableFieldDefinitionContext, + ) as FieldDefinition; + + // TODO: we could use a hook that would return the field value with the right type + const [fieldValue, setFieldValue] = useRecoilState( + genericEntityFieldFamilySelector({ + entityId: currentEditableFieldEntityId ?? '', + fieldName: currentEditableFieldDefinition + ? currentEditableFieldDefinition.metadata.fieldName + : '', + }), + ); + + const [internalValue, setInternalValue] = useState(fieldValue); + + const updateField = useUpdateGenericEntityField(); + + const wrapperRef = useRef(null); + + useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel); + + function handleSubmit() { + if (internalValue === fieldValue) return; + + setFieldValue(internalValue); + + if (currentEditableFieldEntityId && updateField) { + updateField( + currentEditableFieldEntityId, + currentEditableFieldDefinition, + internalValue, + ); + } + } + + function onCancel() { + setFieldValue(fieldValue); + } + + function handleChange(newValue: string) { + setInternalValue(newValue); + } + + return ( +
+ { + handleChange(newValue); + }} + /> +
+ ); +} diff --git a/front/src/modules/ui/editable-field/components/GenericEditableURLField.tsx b/front/src/modules/ui/editable-field/components/GenericEditableURLField.tsx new file mode 100644 index 000000000..322290d8f --- /dev/null +++ b/front/src/modules/ui/editable-field/components/GenericEditableURLField.tsx @@ -0,0 +1,43 @@ +import { useContext } from 'react'; +import { useRecoilValue } from 'recoil'; + +import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; + +import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext'; +import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext'; +import { FieldContext } from '../states/FieldContext'; +import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector'; +import { FieldDefinition } from '../types/FieldDefinition'; +import { FieldNumberMetadata } from '../types/FieldMetadata'; + +import { EditableField } from './EditableField'; +import { FieldDisplayURL } from './FieldDisplayURL'; +import { GenericEditableURLFieldEditMode } from './GenericEditableURLFieldEditMode'; + +export function GenericEditableURLField() { + const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext); + const currentEditableFieldDefinition = useContext( + EditableFieldDefinitionContext, + ) as FieldDefinition; + + const fieldValue = useRecoilValue( + genericEntityFieldFamilySelector({ + entityId: currentEditableFieldEntityId ?? '', + fieldName: currentEditableFieldDefinition + ? currentEditableFieldDefinition.metadata.fieldName + : '', + }), + ); + + return ( + + } + displayModeContent={} + isDisplayModeContentEmpty={!fieldValue} + /> + + ); +} diff --git a/front/src/modules/ui/editable-field/components/GenericEditableURLFieldEditMode.tsx b/front/src/modules/ui/editable-field/components/GenericEditableURLFieldEditMode.tsx new file mode 100644 index 000000000..a0e3b229f --- /dev/null +++ b/front/src/modules/ui/editable-field/components/GenericEditableURLFieldEditMode.tsx @@ -0,0 +1,74 @@ +import { useContext, useRef, useState } from 'react'; +import { useRecoilState } from 'recoil'; + +import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit'; + +import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers'; +import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField'; +import { EditableFieldDefinitionContext } from '../states/EditableFieldDefinitionContext'; +import { EditableFieldEntityIdContext } from '../states/EditableFieldEntityIdContext'; +import { genericEntityFieldFamilySelector } from '../states/genericEntityFieldFamilySelector'; +import { FieldDefinition } from '../types/FieldDefinition'; +import { FieldURLMetadata } from '../types/FieldMetadata'; + +// This one is very similar to GenericEditableTextFieldEditMode +// We could probably merge them since FieldURLMetadata is basically a FieldTextMetadata +export function GenericEditableURLFieldEditMode() { + const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext); + const currentEditableFieldDefinition = useContext( + EditableFieldDefinitionContext, + ) as FieldDefinition; + + // TODO: we could use a hook that would return the field value with the right type + const [fieldValue, setFieldValue] = useRecoilState( + genericEntityFieldFamilySelector({ + entityId: currentEditableFieldEntityId ?? '', + fieldName: currentEditableFieldDefinition + ? currentEditableFieldDefinition.metadata.fieldName + : '', + }), + ); + + const [internalValue, setInternalValue] = useState(fieldValue); + + const updateField = useUpdateGenericEntityField(); + + const wrapperRef = useRef(null); + + useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel); + + function handleSubmit() { + if (internalValue === fieldValue) return; + + setFieldValue(internalValue); + + if (currentEditableFieldEntityId && updateField) { + updateField( + currentEditableFieldEntityId, + currentEditableFieldDefinition, + internalValue, + ); + } + } + + function onCancel() { + setFieldValue(fieldValue); + } + + function handleChange(newValue: string) { + setInternalValue(newValue); + } + + return ( +
+ { + handleChange(newValue); + }} + /> +
+ ); +} diff --git a/front/src/modules/ui/editable-field/hooks/useBindFieldHotkeyScope.ts b/front/src/modules/ui/editable-field/hooks/useBindFieldHotkeyScope.ts deleted file mode 100644 index 2552fa7ba..000000000 --- a/front/src/modules/ui/editable-field/hooks/useBindFieldHotkeyScope.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useEffect } from 'react'; -import { useRecoilCallback } from 'recoil'; - -import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState'; -import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { isSameHotkeyScope } from '@/ui/utilities/hotkey/utils/isSameHotkeyScope'; -import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; -import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; -import { getSnapshotScopedState } from '@/ui/utilities/recoil-scope/utils/getSnapshotScopedState'; -import { getSnapshotState } from '@/ui/utilities/recoil-scope/utils/getSnapshotState'; - -import { customEditHotkeyScopeForFieldScopedState } from '../states/customEditHotkeyScopeForFieldScopedState'; -import { FieldContext } from '../states/FieldContext'; -import { parentHotkeyScopeForFieldScopedState } from '../states/parentHotkeyScopeForFieldScopedState'; - -export function useBindFieldHotkeyScope({ - customEditHotkeyScope, - parentHotkeyScope, -}: { - customEditHotkeyScope?: HotkeyScope; - parentHotkeyScope?: HotkeyScope; -}) { - const [customEditHotkeyScopeForField, setCustomEditHotkeyScopeForField] = - useRecoilScopedState( - customEditHotkeyScopeForFieldScopedState, - FieldContext, - ); - - const fieldContextScopeId = useContextScopeId(FieldContext); - - useEffect(() => { - if ( - customEditHotkeyScope && - !isSameHotkeyScope(customEditHotkeyScope, customEditHotkeyScopeForField) - ) { - setCustomEditHotkeyScopeForField(customEditHotkeyScope); - } - }, [ - customEditHotkeyScope, - customEditHotkeyScopeForField, - setCustomEditHotkeyScopeForField, - ]); - - const setParentHotkeyScopeForField = useRecoilCallback( - ({ snapshot, set }) => - (parentHotkeyScopeToSet: HotkeyScope | null | undefined) => { - const currentHotkeyScope = getSnapshotState({ - snapshot, - state: currentHotkeyScopeState, - }); - - const parentHotkeyScopeForField = getSnapshotScopedState({ - snapshot, - state: parentHotkeyScopeForFieldScopedState, - contextScopeId: fieldContextScopeId, - }); - - if (!parentHotkeyScopeToSet) { - set( - parentHotkeyScopeForFieldScopedState(fieldContextScopeId), - currentHotkeyScope, - ); - } else if ( - !isSameHotkeyScope(parentHotkeyScopeToSet, parentHotkeyScopeForField) - ) { - setParentHotkeyScopeForField(parentHotkeyScopeToSet); - } - }, - [fieldContextScopeId], - ); - - useEffect(() => { - setParentHotkeyScopeForField(parentHotkeyScope); - }, [parentHotkeyScope, setParentHotkeyScopeForField]); -} diff --git a/front/src/modules/ui/editable-field/hooks/useEditableField.ts b/front/src/modules/ui/editable-field/hooks/useEditableField.ts index b676c1b43..4db05c696 100644 --- a/front/src/modules/ui/editable-field/hooks/useEditableField.ts +++ b/front/src/modules/ui/editable-field/hooks/useEditableField.ts @@ -1,10 +1,9 @@ -import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; -import { customEditHotkeyScopeForFieldScopedState } from '../states/customEditHotkeyScopeForFieldScopedState'; import { FieldContext } from '../states/FieldContext'; import { isFieldInEditModeScopedState } from '../states/isFieldInEditModeScopedState'; -import { parentHotkeyScopeForFieldScopedState } from '../states/parentHotkeyScopeForFieldScopedState'; import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope'; export function useEditableField() { @@ -13,39 +12,29 @@ export function useEditableField() { FieldContext, ); - const [customEditHotkeyScopeForField] = useRecoilScopedState( - customEditHotkeyScopeForFieldScopedState, - FieldContext, - ); - - const [parentHotkeyScopeForField] = useRecoilScopedState( - parentHotkeyScopeForFieldScopedState, - FieldContext, - ); - - const setHotkeyScope = useSetHotkeyScope(); + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); function closeEditableField() { setIsFieldInEditMode(false); - if (parentHotkeyScopeForField) { - setHotkeyScope( - parentHotkeyScopeForField.scope, - parentHotkeyScopeForField.customScopes, - ); - } + goBackToPreviousHotkeyScope(); } - function openEditableField() { + function openEditableField(customEditHotkeyScopeForField?: HotkeyScope) { setIsFieldInEditMode(true); if (customEditHotkeyScopeForField) { - setHotkeyScope( + setHotkeyScopeAndMemorizePreviousScope( customEditHotkeyScopeForField.scope, customEditHotkeyScopeForField.customScopes, ); } else { - setHotkeyScope(EditableFieldHotkeyScope.EditableField); + setHotkeyScopeAndMemorizePreviousScope( + EditableFieldHotkeyScope.EditableField, + ); } } diff --git a/front/src/modules/ui/editable-field/hooks/useUpdateGenericEntityField.ts b/front/src/modules/ui/editable-field/hooks/useUpdateGenericEntityField.ts index 545bc86d7..1e6b2e1aa 100644 --- a/front/src/modules/ui/editable-field/hooks/useUpdateGenericEntityField.ts +++ b/front/src/modules/ui/editable-field/hooks/useUpdateGenericEntityField.ts @@ -1,7 +1,5 @@ import { useContext } from 'react'; -import { isFieldChip } from '@/ui/editable-field/types/guards/isFieldChip'; - import { EditableFieldMutationContext } from '../states/EditableFieldMutationContext'; import { FieldDefinition } from '../types/FieldDefinition'; import { @@ -27,6 +25,7 @@ import { FieldURLMetadata, FieldURLValue, } from '../types/FieldMetadata'; +import { isFieldChip } from '../types/guards/isFieldChip'; import { isFieldChipValue } from '../types/guards/isFieldChipValue'; import { isFieldDate } from '../types/guards/isFieldDate'; import { isFieldDateValue } from '../types/guards/isFieldDateValue'; @@ -53,31 +52,30 @@ export function useUpdateGenericEntityField() { const [updateEntity] = useUpdateEntityMutation(); return function updateEntityField< - MetadataType extends FieldMetadata, - ValueType extends MetadataType extends FieldDoubleTextMetadata + ValueType extends FieldMetadata extends FieldDoubleTextMetadata ? FieldDoubleTextValue - : MetadataType extends FieldTextMetadata + : FieldMetadata extends FieldTextMetadata ? FieldTextValue - : MetadataType extends FieldPhoneMetadata + : FieldMetadata extends FieldPhoneMetadata ? FieldPhoneValue - : MetadataType extends FieldURLMetadata + : FieldMetadata extends FieldURLMetadata ? FieldURLValue - : MetadataType extends FieldNumberMetadata + : FieldMetadata extends FieldNumberMetadata ? FieldNumberValue - : MetadataType extends FieldDateMetadata + : FieldMetadata extends FieldDateMetadata ? FieldDateValue - : MetadataType extends FieldChipMetadata + : FieldMetadata extends FieldChipMetadata ? FieldChipValue - : MetadataType extends FieldDoubleTextChipMetadata + : FieldMetadata extends FieldDoubleTextChipMetadata ? FieldDoubleTextChipValue - : MetadataType extends FieldRelationMetadata + : FieldMetadata extends FieldRelationMetadata ? FieldRelationValue - : MetadataType extends FieldProbabilityMetadata + : FieldMetadata extends FieldProbabilityMetadata ? FieldProbabilityValue : unknown, >( currentEntityId: string, - field: FieldDefinition, + field: FieldDefinition, newFieldValue: ValueType, ) { const newFieldValueUnknown = newFieldValue as unknown; diff --git a/front/src/modules/ui/editable-field/property-box/components/PropertyBoxItem.tsx b/front/src/modules/ui/editable-field/property-box/components/PropertyBoxItem.tsx deleted file mode 100644 index 13cc1ce13..000000000 --- a/front/src/modules/ui/editable-field/property-box/components/PropertyBoxItem.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { ReactNode } from 'react'; -import styled from '@emotion/styled'; - -const StyledPropertyBoxItem = styled.div` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - padding: ${({ theme }) => theme.spacing(1)}; -`; - -const StyledIconContainer = styled.div` - align-items: center; - display: flex; - - svg { - align-items: center; - display: flex; - height: 16px; - justify-content: center; - width: 16px; - } -`; - -const StyledValueContainer = styled.div` - align-content: flex-start; - align-items: center; - color: ${({ theme }) => theme.font.color.primary}; - display: flex; - flex: 1 0 0; - flex-wrap: wrap; -`; - -const StyledLabelAndIconContainer = styled.div` - align-items: center; - color: ${({ theme }) => theme.font.color.tertiary}; - display: flex; - gap: ${({ theme }) => theme.spacing(1)}; -`; - -export function PropertyBoxItem({ - icon, - label, - value, -}: { - icon: ReactNode; - label?: string; - value: ReactNode; -}) { - return ( - - - {icon} - {label} - - {value} - - ); -} diff --git a/front/src/modules/ui/editable-field/states/EditableFieldDefinitionContext.ts b/front/src/modules/ui/editable-field/states/EditableFieldDefinitionContext.ts index 2ce04ca7b..6a804aeb7 100644 --- a/front/src/modules/ui/editable-field/states/EditableFieldDefinitionContext.ts +++ b/front/src/modules/ui/editable-field/states/EditableFieldDefinitionContext.ts @@ -1,8 +1,8 @@ import { createContext } from 'react'; import { FieldDefinition } from '../types/FieldDefinition'; -import { ViewFieldMetadata } from '../types/ViewField'; +import { FieldMetadata } from '../types/FieldMetadata'; export const EditableFieldDefinitionContext = createContext< - FieldDefinition ->({} as FieldDefinition); + FieldDefinition +>({} as FieldDefinition); diff --git a/front/src/modules/ui/editable-field/types/FieldDefinition.ts b/front/src/modules/ui/editable-field/types/FieldDefinition.ts index 0cb858308..41cf30e36 100644 --- a/front/src/modules/ui/editable-field/types/FieldDefinition.ts +++ b/front/src/modules/ui/editable-field/types/FieldDefinition.ts @@ -1,9 +1,9 @@ -import { FieldMetadata } from './FieldMetadata'; +import { FieldMetadata, FieldType } from './FieldMetadata'; export type FieldDefinition = { id: string; label: string; icon?: JSX.Element; - type: string; + type: FieldType; metadata: T; }; diff --git a/front/src/modules/ui/editable-field/types/FieldMetadata.ts b/front/src/modules/ui/editable-field/types/FieldMetadata.ts index 0b2e598b3..b7afa92f9 100644 --- a/front/src/modules/ui/editable-field/types/FieldMetadata.ts +++ b/front/src/modules/ui/editable-field/types/FieldMetadata.ts @@ -2,6 +2,7 @@ import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelec import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect'; export type FieldType = + | 'unknown' | 'text' | 'relation' | 'chip' @@ -14,41 +15,36 @@ export type FieldType = | 'probability'; export type FieldTextMetadata = { - type: 'text'; placeHolder: string; fieldName: string; }; export type FieldPhoneMetadata = { - type: 'phone'; placeHolder: string; fieldName: string; }; export type FieldURLMetadata = { - type: 'url'; placeHolder: string; fieldName: string; }; export type FieldDateMetadata = { - type: 'date'; fieldName: string; }; export type FieldNumberMetadata = { - type: 'number'; fieldName: string; + placeHolder: string; }; export type FieldRelationMetadata = { - type: 'relation'; relationType: Entity; fieldName: string; + useEditButton?: boolean; }; export type FieldChipMetadata = { - type: 'chip'; relationType: Entity; contentFieldName: string; urlFieldName: string; @@ -56,7 +52,6 @@ export type FieldChipMetadata = { }; export type FieldDoubleTextMetadata = { - type: 'double-text'; firstValueFieldName: string; firstValuePlaceholder: string; secondValueFieldName: string; @@ -64,7 +59,6 @@ export type FieldDoubleTextMetadata = { }; export type FieldDoubleTextChipMetadata = { - type: 'double-text-chip'; firstValueFieldName: string; firstValuePlaceholder: string; secondValueFieldName: string; @@ -74,11 +68,10 @@ export type FieldDoubleTextChipMetadata = { }; export type FieldProbabilityMetadata = { - type: 'probability'; fieldName: string; }; -export type FieldMetadata = { type: FieldType } & ( +export type FieldMetadata = | FieldTextMetadata | FieldRelationMetadata | FieldChipMetadata @@ -88,8 +81,7 @@ export type FieldMetadata = { type: FieldType } & ( | FieldURLMetadata | FieldNumberMetadata | FieldDateMetadata - | FieldProbabilityMetadata -); + | FieldProbabilityMetadata; export type FieldTextValue = string; diff --git a/front/src/modules/ui/editable-field/types/ViewField.ts b/front/src/modules/ui/editable-field/types/ViewField.ts index 24b04bdce..f1386e15c 100644 --- a/front/src/modules/ui/editable-field/types/ViewField.ts +++ b/front/src/modules/ui/editable-field/types/ViewField.ts @@ -46,6 +46,7 @@ export type ViewFieldRelationMetadata = { type: 'relation'; relationType: Entity; fieldName: string; + useEditButton?: boolean; }; export type ViewFieldChipMetadata = { diff --git a/front/src/modules/ui/editable-field/variants/components/TextEditableField.tsx b/front/src/modules/ui/editable-field/variants/components/TextEditableField.tsx deleted file mode 100644 index 7bc3f2ecb..000000000 --- a/front/src/modules/ui/editable-field/variants/components/TextEditableField.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { EditableField } from '@/ui/editable-field/components/EditableField'; -import { FieldContext } from '@/ui/editable-field/states/FieldContext'; -import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; - -import { OverflowingTextWithTooltip } from '../../../tooltip/OverflowingTextWithTooltip'; - -type OwnProps = { - icon?: React.ReactNode; - placeholder?: string; - value: string | null | undefined; - onSubmit?: (newValue: string) => void; -}; - -export function TextEditableField({ - icon, - placeholder, - value, - onSubmit, -}: OwnProps) { - const [internalValue, setInternalValue] = useState(value); - - useEffect(() => { - setInternalValue(value); - }, [value]); - - async function handleChange(newValue: string) { - setInternalValue(newValue); - } - - async function handleSubmit() { - if (!internalValue) return; - - onSubmit?.(internalValue); - } - - async function handleCancel() { - setInternalValue(value); - } - - return ( - - { - handleChange(newValue); - }} - /> - } - displayModeContent={} - isDisplayModeContentEmpty={!(internalValue !== '')} - /> - - ); -} diff --git a/front/src/modules/ui/editable-field/variants/components/__stories__/TextEditableField.stories.tsx b/front/src/modules/ui/editable-field/variants/components/__stories__/TextEditableField.stories.tsx deleted file mode 100644 index fbffa6855..000000000 --- a/front/src/modules/ui/editable-field/variants/components/__stories__/TextEditableField.stories.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { IconUser } from '@tabler/icons-react'; - -import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; - -import { TextEditableField } from '../TextEditableField'; - -const meta: Meta = { - title: 'UI/EditableField/TextEditableField', - component: TextEditableField, - decorators: [ComponentDecorator], - argTypes: { - icon: { - type: 'boolean', - mapping: { - true: , - false: undefined, - }, - }, - }, - args: { - value: 'John Doe', - icon: true, - placeholder: 'Name', - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/front/src/modules/ui/filter-n-sort/components/DropdownMenuContainer.tsx b/front/src/modules/ui/filter-n-sort/components/DropdownMenuContainer.tsx index 430c73959..b1f01e1f6 100644 --- a/front/src/modules/ui/filter-n-sort/components/DropdownMenuContainer.tsx +++ b/front/src/modules/ui/filter-n-sort/components/DropdownMenuContainer.tsx @@ -28,7 +28,7 @@ export function DropdownMenuContainer({ }); return ( - + {children} ); diff --git a/front/src/modules/ui/input/relation-picker/types/EntityTypeForSelect.ts b/front/src/modules/ui/input/relation-picker/types/EntityTypeForSelect.ts index e27c21f6f..80d845d6b 100644 --- a/front/src/modules/ui/input/relation-picker/types/EntityTypeForSelect.ts +++ b/front/src/modules/ui/input/relation-picker/types/EntityTypeForSelect.ts @@ -1,4 +1,5 @@ -import { CommentableType, PipelineProgressableType } from '~/generated/graphql'; +import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity'; +import { PipelineProgressableType } from '~/generated/graphql'; export enum Entity { Company = 'Company', @@ -7,6 +8,6 @@ export enum Entity { } export type EntityTypeForSelect = - | CommentableType + | ActivityTargetableEntityType | PipelineProgressableType | Entity; diff --git a/front/src/modules/ui/link/components/SocialLink.tsx b/front/src/modules/ui/link/components/SocialLink.tsx index dd4acb392..04603b314 100644 --- a/front/src/modules/ui/link/components/SocialLink.tsx +++ b/front/src/modules/ui/link/components/SocialLink.tsx @@ -30,16 +30,25 @@ export function SocialLink({ children, href, onClick, type }: OwnProps) { let displayValue = children; if (type === 'linkedin') { - const splitUrl = href.split('/'); - const splitName = splitUrl[4].split('-'); - displayValue = splitName[2] - ? `${splitName[0]}-${splitName[1]}` - : splitName[0]; + const matches = href.match( + /(?:https?:\/\/)?(?:www.)?linkedin.com\/(?:in|company)\/([-a-zA-Z0-9@:%_+.~#?&//=]*)/, + ); + if (matches && matches[1]) { + displayValue = matches[1]; + } else { + displayValue = 'LinkedIn'; + } } if (type === 'twitter') { - const splitUrl = href.split('/'); - displayValue = `@${splitUrl[3]}`; + const matches = href.match( + /(?:https?:\/\/)?(?:www.)?twitter.com\/([-a-zA-Z0-9@:%_+.~#?&//=]*)/, + ); + if (matches && matches[1]) { + displayValue = `@${matches[1]}`; + } else { + displayValue = '@twitter'; + } } return ( diff --git a/front/src/modules/ui/table/components/EntityTable.tsx b/front/src/modules/ui/table/components/EntityTable.tsx index d4e4d3af3..d3a07d393 100644 --- a/front/src/modules/ui/table/components/EntityTable.tsx +++ b/front/src/modules/ui/table/components/EntityTable.tsx @@ -1,16 +1,19 @@ import { useRef } from 'react'; import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; +import type { + ViewFieldDefinition, + ViewFieldMetadata, +} from '@/ui/editable-field/types/ViewField'; import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus'; import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus'; +import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection'; import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState'; -import { EntityUpdateMutationHookContext } from '../states/EntityUpdateMutationHookContext'; -import { tableRowIdsState } from '../states/tableRowIdsState'; +import { EntityUpdateMutationContext } from '../states/EntityUpdateMutationHookContext'; import { TableHeader } from '../table-header/components/TableHeader'; import { EntityTableBody } from './EntityTableBody'; @@ -25,6 +28,8 @@ const StyledTable = styled.table` 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}; border-collapse: collapse; @@ -90,28 +95,24 @@ type OwnProps = { viewName: string; viewIcon?: React.ReactNode; availableSorts?: Array>; + onColumnsChange?: (columns: ViewFieldDefinition[]) => void; onSortsUpdate?: (sorts: Array>) => void; onRowSelectionChange?: (rowSelection: string[]) => void; - useUpdateEntityMutation: any; + updateEntityMutation: any; }; export function EntityTable({ viewName, viewIcon, availableSorts, + onColumnsChange, onSortsUpdate, - useUpdateEntityMutation, + updateEntityMutation, }: OwnProps) { const tableBodyRef = useRef(null); - const rowIds = useRecoilValue(tableRowIdsState); const setRowSelectedState = useSetRowSelectedState(); - - function resetSelections() { - for (const rowId of rowIds) { - setRowSelectedState(rowId, false); - } - } + const resetTableRowSelection = useResetTableRowSelection(); useMapKeyboardToSoftFocus(); @@ -125,28 +126,29 @@ export function EntityTable({ }); return ( - + - + - + ); } diff --git a/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx b/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx index 3afc80d99..770789b65 100644 --- a/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx +++ b/front/src/modules/ui/table/components/EntityTableColumnMenu.tsx @@ -1,6 +1,7 @@ import { cloneElement, ComponentProps, useRef } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import { IconButton } from '@/ui/button/components/IconButton'; import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu'; @@ -9,32 +10,27 @@ import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMen import { IconPlus } from '@/ui/icon'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; -import type { - ViewFieldDefinition, - ViewFieldMetadata, -} from '../../editable-field/types/ViewField'; +import { hiddenTableColumnsState } from '../states/tableColumnsState'; const StyledColumnMenu = styled(DropdownMenu)` font-weight: ${({ theme }) => theme.font.weight.regular}; `; type EntityTableColumnMenuProps = { - onAddViewField: ( - viewFieldDefinition: ViewFieldDefinition, - ) => void; + onAddColumn: (columnId: string) => void; onClickOutside?: () => void; - viewFieldDefinitions: ViewFieldDefinition[]; } & ComponentProps<'div'>; export const EntityTableColumnMenu = ({ - onAddViewField, + onAddColumn, onClickOutside = () => undefined, - viewFieldDefinitions, ...props }: EntityTableColumnMenuProps) => { const ref = useRef(null); const theme = useTheme(); + const hiddenColumns = useRecoilValue(hiddenTableColumnsState); + useListenClickOutside({ refs: [ref], callback: onClickOutside, @@ -43,21 +39,21 @@ export const EntityTableColumnMenu = ({ return ( - {viewFieldDefinitions.map((viewFieldDefinition) => ( + {hiddenColumns.map((column) => ( } - onClick={() => onAddViewField(viewFieldDefinition)} + onClick={() => onAddColumn(column.id)} /> } > - {viewFieldDefinition.columnIcon && - cloneElement(viewFieldDefinition.columnIcon, { + {column.columnIcon && + cloneElement(column.columnIcon, { size: theme.icon.size.md, })} - {viewFieldDefinition.columnLabel} + {column.columnLabel} ))} diff --git a/front/src/modules/ui/table/components/EntityTableHeader.tsx b/front/src/modules/ui/table/components/EntityTableHeader.tsx index e999449cc..89b4db82f 100644 --- a/front/src/modules/ui/table/components/EntityTableHeader.tsx +++ b/front/src/modules/ui/table/components/EntityTableHeader.tsx @@ -1,5 +1,4 @@ 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 } from 'recoil'; @@ -11,21 +10,14 @@ import type { } from '@/ui/editable-field/types/ViewField'; import { IconPlus } from '@/ui/icon'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; -import { GET_VIEW_FIELDS } from '@/views/queries/select'; -import { currentViewIdState } from '@/views/states/currentViewIdState'; -import { - useCreateViewFieldMutation, - useUpdateViewFieldMutation, -} from '~/generated/graphql'; -import { toViewFieldInput } from '../hooks/useLoadViewFields'; import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState'; import { - addableViewFieldDefinitionsState, - columnWidthByViewFieldIdState, - viewFieldsState, - visibleViewFieldsState, -} from '../states/viewFieldsState'; + hiddenTableColumnsState, + tableColumnsByIdState, + tableColumnsState, + visibleTableColumnsState, +} from '../states/tableColumnsState'; import { ColumnHead } from './ColumnHead'; import { EntityTableColumnMenu } from './EntityTableColumnMenu'; @@ -86,17 +78,18 @@ const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)` z-index: ${({ theme }) => theme.lastLayerZIndex}; `; -export function EntityTableHeader() { +export type EntityTableHeaderProps = { + onColumnsChange?: (columns: ViewFieldDefinition[]) => void; +}; + +export function EntityTableHeader({ onColumnsChange }: EntityTableHeaderProps) { const theme = useTheme(); - const [{ objectName }, setViewFieldsState] = useRecoilState(viewFieldsState); - const currentViewId = useRecoilValue(currentViewIdState); - const viewFields = useRecoilValue(visibleViewFieldsState); - const columnWidths = useRecoilValue(columnWidthByViewFieldIdState); - const addableViewFieldDefinitions = useRecoilValue( - addableViewFieldDefinitionsState, - ); + const [columns, setColumns] = useRecoilState(tableColumnsState); const [offset, setOffset] = useRecoilState(resizeFieldOffsetState); + const columnsById = useRecoilValue(tableColumnsByIdState); + const hiddenColumns = useRecoilValue(hiddenTableColumnsState); + const visibleColumns = useRecoilValue(visibleTableColumnsState); const [initialPointerPositionX, setInitialPointerPositionX] = useState< number | null @@ -104,9 +97,6 @@ export function EntityTableHeader() { const [resizedFieldId, setResizedFieldId] = useState(null); const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false); - const [createViewFieldMutation] = useCreateViewFieldMutation(); - const [updateViewFieldMutation] = useUpdateViewFieldMutation(); - const handleResizeHandlerStart = useCallback((positionX: number) => { setInitialPointerPositionX(positionX); }, []); @@ -126,37 +116,28 @@ export function EntityTableHeader() { const nextWidth = Math.round( Math.max( - columnWidths[resizedFieldId] + + columnsById[resizedFieldId].columnSize + snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(), COLUMN_MIN_WIDTH, ), ); - if (nextWidth !== columnWidths[resizedFieldId]) { - // 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, - ), - })); + if (nextWidth !== columnsById[resizedFieldId].columnSize) { + const nextColumns = columns.map((column) => + column.id === resizedFieldId + ? { ...column, columnSize: nextWidth } + : column, + ); - updateViewFieldMutation({ - variables: { - data: { sizeInPx: nextWidth }, - where: { id: resizedFieldId }, - }, - refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''], - }); + setColumns(nextColumns); + onColumnsChange?.(nextColumns); } set(resizeFieldOffsetState, 0); setInitialPointerPositionX(null); setResizedFieldId(null); }, - [resizedFieldId, columnWidths, setResizedFieldId], + [resizedFieldId, columnsById, setResizedFieldId], ); useTrackPointer({ @@ -170,26 +151,18 @@ export function EntityTableHeader() { setIsColumnMenuOpen((previousValue) => !previousValue); }, []); - const handleAddViewField = useCallback( - (viewFieldDefinition: ViewFieldDefinition) => { + const handleAddColumn = useCallback( + (columnId: string) => { setIsColumnMenuOpen(false); - if (!objectName) return; + const nextColumns = columns.map((column) => + column.id === columnId ? { ...column, isVisible: true } : column, + ); - createViewFieldMutation({ - variables: { - data: { - ...toViewFieldInput(objectName, { - ...viewFieldDefinition, - columnOrder: viewFields.length + 1, - }), - view: { connect: { id: currentViewId } }, - }, - }, - refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''], - }); + setColumns(nextColumns); + onColumnsChange?.(nextColumns); }, - [createViewFieldMutation, currentViewId, objectName, viewFields.length], + [columns, onColumnsChange, setColumns], ); return ( @@ -205,31 +178,31 @@ export function EntityTableHeader() { - {viewFields.map((viewField) => ( + {visibleColumns.map((column) => ( { - setResizedFieldId(viewField.id); + setResizedFieldId(column.id); }} /> ))} - {addableViewFieldDefinitions.length > 0 && ( + {hiddenColumns.length > 0 && ( {isColumnMenuOpen && ( )} diff --git a/front/src/modules/ui/table/components/EntityTableRow.tsx b/front/src/modules/ui/table/components/EntityTableRow.tsx index c39b7f190..b71306a9f 100644 --- a/front/src/modules/ui/table/components/EntityTableRow.tsx +++ b/front/src/modules/ui/table/components/EntityTableRow.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; +import { visibleTableColumnsState } from '../states/tableColumnsState'; import { ViewFieldContext } from '../states/ViewFieldContext'; -import { visibleViewFieldsState } from '../states/viewFieldsState'; import { CheckboxCell } from './CheckboxCell'; import { EntityTableCell } from './EntityTableCell'; @@ -13,7 +13,7 @@ const StyledRow = styled.tr<{ selected: boolean }>` `; export function EntityTableRow({ rowId }: { rowId: string }) { - const viewFields = useRecoilValue(visibleViewFieldsState); + const columns = useRecoilValue(visibleTableColumnsState); return ( - {viewFields.map((viewField, columnIndex) => { + {columns.map((column, columnIndex) => { return ( - + ); diff --git a/front/src/modules/ui/table/components/GenericEntityTableData.tsx b/front/src/modules/ui/table/components/GenericEntityTableData.tsx index f28457147..58aae7899 100644 --- a/front/src/modules/ui/table/components/GenericEntityTableData.tsx +++ b/front/src/modules/ui/table/components/GenericEntityTableData.tsx @@ -1,34 +1,21 @@ import { defaultOrderBy } from '@/people/queries'; -import { - ViewFieldDefinition, - ViewFieldMetadata, -} from '@/ui/editable-field/types/ViewField'; import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition'; import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData'; -import { useLoadViewFields } from '../hooks/useLoadViewFields'; - export function GenericEntityTableData({ - objectName, useGetRequest, getRequestResultKey, orderBy = defaultOrderBy, whereFilters, - viewFieldDefinitions, filterDefinitionArray, }: { - objectName: 'company' | 'person'; useGetRequest: any; getRequestResultKey: string; orderBy?: any; whereFilters?: any; - viewFieldDefinitions: ViewFieldDefinition[]; filterDefinitionArray: FilterDefinition[]; }) { const setEntityTableData = useSetEntityTableData(); - - useLoadViewFields({ objectName, viewFieldDefinitions }); - useGetRequest({ variables: { orderBy, where: whereFilters }, onCompleted: (data: any) => { diff --git a/front/src/modules/ui/table/editable-cell/components/HoverableMenuItem.tsx b/front/src/modules/ui/table/editable-cell/components/HoverableMenuItem.tsx deleted file mode 100644 index 33888c329..000000000 --- a/front/src/modules/ui/table/editable-cell/components/HoverableMenuItem.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import styled from '@emotion/styled'; - -export const HoverableMenuItem = styled.div` - align-items: center; - background: ${({ theme }) => theme.background.primary}; - border-radius: ${({ theme }) => theme.border.radius.sm}; - box-sizing: border-box; - cursor: pointer; - display: flex; - height: 100%; - position: relative; - transition: background 0.1s ease; - user-select: none; - width: 100%; - - &:hover { - background: ${({ theme }) => theme.background.transparent.light}; - } -`; diff --git a/front/src/modules/ui/table/editable-cell/hooks/useEditableCell.ts b/front/src/modules/ui/table/editable-cell/hooks/useEditableCell.ts index 8a514f1a8..8da2ee7a5 100644 --- a/front/src/modules/ui/table/editable-cell/hooks/useEditableCell.ts +++ b/front/src/modules/ui/table/editable-cell/hooks/useEditableCell.ts @@ -1,5 +1,6 @@ import { useContext } from 'react'; +import { useDragSelect } from '@/ui/utilities/drag-select/hooks/useDragSelect'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; @@ -17,17 +18,20 @@ export function useEditableCell() { const { setCurrentCellInEditMode } = useCurrentCellEditMode(); const setHotkeyScope = useSetHotkeyScope(); + const { setDragSelectionStartEnabled } = useDragSelect(); const closeCurrentCellInEditMode = useCloseCurrentCellInEditMode(); const customCellHotkeyScope = useContext(CellHotkeyScopeContext); function closeEditableCell() { + setDragSelectionStartEnabled(true); closeCurrentCellInEditMode(); setHotkeyScope(TableHotkeyScope.TableSoftFocus); } function openEditableCell() { + setDragSelectionStartEnabled(false); setCurrentCellInEditMode(); if (customCellHotkeyScope) { diff --git a/front/src/modules/ui/table/hooks/useCurrentEntityId.ts b/front/src/modules/ui/table/hooks/useCurrentEntityId.ts index 05040206d..ae7940735 100644 --- a/front/src/modules/ui/table/hooks/useCurrentEntityId.ts +++ b/front/src/modules/ui/table/hooks/useCurrentEntityId.ts @@ -2,11 +2,6 @@ import { useContext } from 'react'; import { RowIdContext } from '../states/RowIdContext'; -export type TableDimensions = { - numberOfColumns: number; - numberOfRows: number; -}; - export function useCurrentRowEntityId() { const currentEntityId = useContext(RowIdContext); diff --git a/front/src/modules/ui/table/hooks/useInitializeEntityTable.ts b/front/src/modules/ui/table/hooks/useInitializeEntityTable.ts deleted file mode 100644 index 1e7de1c14..000000000 --- a/front/src/modules/ui/table/hooks/useInitializeEntityTable.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; - -import { entityTableDimensionsState } from '../states/entityTableDimensionsState'; -import { tableRowIdsState } from '../states/tableRowIdsState'; - -import { useResetTableRowSelection } from './useResetTableRowSelection'; - -export type TableDimensions = { - numberOfColumns: number; - numberOfRows: number; -}; - -export function useInitializeEntityTable({ - numberOfColumns, -}: { - numberOfColumns: number; -}) { - const resetTableRowSelection = useResetTableRowSelection(); - - const tableRowIds = useRecoilValue(tableRowIdsState); - - useEffect(() => { - resetTableRowSelection(); - }, [resetTableRowSelection]); - - const [, setTableDimensions] = useRecoilState(entityTableDimensionsState); - - useEffect(() => { - setTableDimensions({ - numberOfColumns, - numberOfRows: tableRowIds?.length, - }); - }, [tableRowIds, numberOfColumns, setTableDimensions]); -} diff --git a/front/src/modules/ui/table/hooks/useInitializeEntityTableFilters.ts b/front/src/modules/ui/table/hooks/useInitializeEntityTableFilters.ts deleted file mode 100644 index 130e5e6fb..000000000 --- a/front/src/modules/ui/table/hooks/useInitializeEntityTableFilters.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { useEffect } from 'react'; - -import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState'; -import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition'; -import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; - -import { TableContext } from '../states/TableContext'; - -export function useInitializeEntityTableFilters({ - availableFilters, -}: { - availableFilters: FilterDefinition[]; -}) { - const [, setAvailableFilters] = useRecoilScopedState( - availableFiltersScopedState, - TableContext, - ); - - useEffect(() => { - setAvailableFilters(availableFilters); - }, [setAvailableFilters, availableFilters]); -} diff --git a/front/src/modules/ui/table/hooks/useLoadViewFields.ts b/front/src/modules/ui/table/hooks/useLoadViewFields.ts deleted file mode 100644 index f7cbb5640..000000000 --- a/front/src/modules/ui/table/hooks/useLoadViewFields.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { getOperationName } from '@apollo/client/utilities'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import type { - ViewFieldDefinition, - ViewFieldMetadata, - ViewFieldTextMetadata, -} from '@/ui/editable-field/types/ViewField'; -import { GET_VIEW_FIELDS } from '@/views/queries/select'; -import { currentViewIdState } from '@/views/states/currentViewIdState'; -import { - SortOrder, - useCreateViewFieldsMutation, - useGetViewFieldsQuery, -} from '~/generated/graphql'; - -import { entityTableDimensionsState } from '../states/entityTableDimensionsState'; -import { viewFieldsState } from '../states/viewFieldsState'; - -const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = { - type: 'text', - placeHolder: '', - fieldName: '', -}; - -export const toViewFieldInput = ( - objectName: 'company' | 'person', - viewFieldDefinition: ViewFieldDefinition, -) => ({ - fieldName: viewFieldDefinition.columnLabel, - index: viewFieldDefinition.columnOrder, - isVisible: viewFieldDefinition.isVisible ?? true, - objectName, - sizeInPx: viewFieldDefinition.columnSize, -}); - -export const useLoadViewFields = ({ - objectName, - viewFieldDefinitions, -}: { - objectName: 'company' | 'person'; - viewFieldDefinitions: ViewFieldDefinition[]; -}) => { - const currentViewId = useRecoilValue(currentViewIdState); - const setEntityTableDimensions = useSetRecoilState( - entityTableDimensionsState, - ); - const setViewFieldsState = useSetRecoilState(viewFieldsState); - - const [createViewFieldsMutation] = useCreateViewFieldsMutation(); - - useGetViewFieldsQuery({ - variables: { - orderBy: { index: SortOrder.Asc }, - where: { - objectName: { equals: objectName }, - viewId: { equals: currentViewId ?? null }, - }, - }, - onCompleted: (data) => { - if (data.viewFields.length) { - const viewFields = data.viewFields.map< - ViewFieldDefinition - >((viewField) => ({ - ...(viewFieldDefinitions.find( - ({ columnLabel }) => viewField.fieldName === columnLabel, - ) || { metadata: DEFAULT_VIEW_FIELD_METADATA }), - id: viewField.id, - columnLabel: viewField.fieldName, - columnOrder: viewField.index, - columnSize: viewField.sizeInPx, - isVisible: viewField.isVisible, - })); - - setViewFieldsState({ objectName, viewFields }); - setEntityTableDimensions((prevState) => ({ - ...prevState, - numberOfColumns: data.viewFields.length, - })); - - return; - } - - // Populate if empty - createViewFieldsMutation({ - variables: { - data: viewFieldDefinitions.map((viewFieldDefinition) => ({ - ...toViewFieldInput(objectName, viewFieldDefinition), - viewId: currentViewId, - })), - }, - refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''], - }); - }, - }); -}; diff --git a/front/src/modules/ui/table/hooks/useMoveSoftFocus.ts b/front/src/modules/ui/table/hooks/useMoveSoftFocus.ts index 800bda596..9903a1f87 100644 --- a/front/src/modules/ui/table/hooks/useMoveSoftFocus.ts +++ b/front/src/modules/ui/table/hooks/useMoveSoftFocus.ts @@ -1,8 +1,8 @@ import { useRecoilCallback } from 'recoil'; -import { numberOfTableColumnsSelectorState } from '../states/numberOfTableColumnsSelectorState'; -import { numberOfTableRowsSelectorState } from '../states/numberOfTableRowsSelectorState'; +import { numberOfTableRowsState } from '../states/numberOfTableRowsState'; import { softFocusPositionState } from '../states/softFocusPositionState'; +import { numberOfTableColumnsState } from '../states/tableColumnsState'; import { useSetSoftFocusPosition } from './useSetSoftFocusPosition'; @@ -39,7 +39,7 @@ export function useMoveSoftFocus() { .valueOrThrow(); const numberOfTableRows = snapshot - .getLoadable(numberOfTableRowsSelectorState) + .getLoadable(numberOfTableRowsState) .valueOrThrow(); let newRowNumber = softFocusPosition.row + 1; @@ -64,11 +64,11 @@ export function useMoveSoftFocus() { .valueOrThrow(); const numberOfTableColumns = snapshot - .getLoadable(numberOfTableColumnsSelectorState) + .getLoadable(numberOfTableColumnsState) .valueOrThrow(); const numberOfTableRows = snapshot - .getLoadable(numberOfTableRowsSelectorState) + .getLoadable(numberOfTableRowsState) .valueOrThrow(); const currentColumnNumber = softFocusPosition.column; @@ -112,7 +112,7 @@ export function useMoveSoftFocus() { .valueOrThrow(); const numberOfTableColumns = snapshot - .getLoadable(numberOfTableColumnsSelectorState) + .getLoadable(numberOfTableColumnsState) .valueOrThrow(); const currentColumnNumber = softFocusPosition.column; diff --git a/front/src/modules/ui/table/hooks/useSetEntityTableData.ts b/front/src/modules/ui/table/hooks/useSetEntityTableData.ts index 69765571c..983f16837 100644 --- a/front/src/modules/ui/table/hooks/useSetEntityTableData.ts +++ b/front/src/modules/ui/table/hooks/useSetEntityTableData.ts @@ -3,13 +3,14 @@ import { useRecoilCallback } from 'recoil'; import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState'; import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition'; import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; -import { entityTableDimensionsState } from '@/ui/table/states/entityTableDimensionsState'; -import { isFetchingEntityTableDataState } from '@/ui/table/states/isFetchingEntityTableDataState'; import { TableContext } from '@/ui/table/states/TableContext'; import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState'; import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState'; import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; +import { isFetchingEntityTableDataState } from '../states/isFetchingEntityTableDataState'; +import { numberOfTableRowsState } from '../states/numberOfTableRowsState'; + export function useSetEntityTableData() { const resetTableRowSelection = useResetTableRowSelection(); @@ -43,10 +44,7 @@ export function useSetEntityTableData() { resetTableRowSelection(); - set(entityTableDimensionsState, (prevState) => ({ - ...prevState, - numberOfRows: entityIds.length, - })); + set(numberOfTableRowsState, entityIds.length); set(availableFiltersScopedState(tableContextScopeId), filters); diff --git a/front/src/modules/ui/table/hooks/useSetTableRowIds.ts b/front/src/modules/ui/table/hooks/useSetTableRowIds.ts new file mode 100644 index 000000000..9751b56e7 --- /dev/null +++ b/front/src/modules/ui/table/hooks/useSetTableRowIds.ts @@ -0,0 +1,19 @@ +import { useRecoilCallback } from 'recoil'; + +import { tableRowIdsState } from '../states/tableRowIdsState'; + +export function useSetTableRowIds() { + return useRecoilCallback( + ({ set, snapshot }) => + (rowIds: string[]) => { + const currentRowIds = snapshot + .getLoadable(tableRowIdsState) + .valueOrThrow(); + + if (JSON.stringify(rowIds) !== JSON.stringify(currentRowIds)) { + set(tableRowIdsState, rowIds); + } + }, + [], + ); +} diff --git a/front/src/modules/ui/table/hooks/useUpdateEntityField.ts b/front/src/modules/ui/table/hooks/useUpdateEntityField.ts index 5dcd1fbdc..4fbe45c75 100644 --- a/front/src/modules/ui/table/hooks/useUpdateEntityField.ts +++ b/front/src/modules/ui/table/hooks/useUpdateEntityField.ts @@ -17,7 +17,6 @@ import { isViewFieldText } from '@/ui/editable-field/types/guards/isViewFieldTex import { isViewFieldTextValue } from '@/ui/editable-field/types/guards/isViewFieldTextValue'; import { isViewFieldURL } from '@/ui/editable-field/types/guards/isViewFieldURL'; import { isViewFieldURLValue } from '@/ui/editable-field/types/guards/isViewFieldURLValue'; -import { EntityUpdateMutationHookContext } from '@/ui/table/states/EntityUpdateMutationHookContext'; import { isViewFieldChipValue } from '../../editable-field/types/guards/isViewFieldChipValue'; import { @@ -42,11 +41,10 @@ import { ViewFieldURLMetadata, ViewFieldURLValue, } from '../../editable-field/types/ViewField'; +import { EntityUpdateMutationContext } from '../states/EntityUpdateMutationHookContext'; export function useUpdateEntityField() { - const useUpdateEntityMutation = useContext(EntityUpdateMutationHookContext); - - const [updateEntity] = useUpdateEntityMutation(); + const updateEntity = useContext(EntityUpdateMutationContext); return function updatePeopleField< MetadataType extends ViewFieldMetadata, diff --git a/front/src/modules/ui/table/hooks/useUpsertEntityTableItem.ts b/front/src/modules/ui/table/hooks/useUpsertEntityTableItem.ts new file mode 100644 index 000000000..bbf8c3146 --- /dev/null +++ b/front/src/modules/ui/table/hooks/useUpsertEntityTableItem.ts @@ -0,0 +1,19 @@ +import { useRecoilCallback } from 'recoil'; + +import { tableEntitiesFamilyState } from '@/ui/table/states/tableEntitiesFamilyState'; + +export function useUpsertEntityTableItem() { + return useRecoilCallback( + ({ set, snapshot }) => + (entity: T) => { + const currentEntity = snapshot + .getLoadable(tableEntitiesFamilyState(entity.id)) + .valueOrThrow(); + + if (JSON.stringify(currentEntity) !== JSON.stringify(entity)) { + set(tableEntitiesFamilyState(entity.id), entity); + } + }, + [], + ); +} diff --git a/front/src/modules/ui/table/hooks/useUpsertTableRowId.ts b/front/src/modules/ui/table/hooks/useUpsertTableRowId.ts new file mode 100644 index 000000000..0a8d519af --- /dev/null +++ b/front/src/modules/ui/table/hooks/useUpsertTableRowId.ts @@ -0,0 +1,17 @@ +import { useRecoilCallback } from 'recoil'; + +import { tableRowIdsState } from '../states/tableRowIdsState'; + +export function useUpsertTableRowId() { + return useRecoilCallback( + ({ set, snapshot }) => + (rowId: string) => { + const currentRowIds = snapshot + .getLoadable(tableRowIdsState) + .valueOrThrow(); + + set(tableRowIdsState, Array.from(new Set([rowId, ...currentRowIds]))); + }, + [], + ); +} diff --git a/front/src/modules/views/components/OptionsDropdownButton.tsx b/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx similarity index 64% rename from front/src/modules/views/components/OptionsDropdownButton.tsx rename to front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx index d574f13a6..c5ebb98be 100644 --- a/front/src/modules/views/components/OptionsDropdownButton.tsx +++ b/front/src/modules/ui/table/options/components/TableOptionsDropdownButton.tsx @@ -1,7 +1,6 @@ import { useCallback, useState } from 'react'; -import { getOperationName } from '@apollo/client/utilities'; import { useTheme } from '@emotion/react'; -import { useRecoilValue } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { IconButton } from '@/ui/button/components/IconButton'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; @@ -16,16 +15,15 @@ import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton'; import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope'; import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon'; import { - hiddenViewFieldsState, - visibleViewFieldsState, -} from '@/ui/table/states/viewFieldsState'; -import { useUpdateViewFieldMutation } from '~/generated/graphql'; + hiddenTableColumnsState, + tableColumnsState, + visibleTableColumnsState, +} from '@/ui/table/states/tableColumnsState'; -import { GET_VIEW_FIELDS } from '../queries/select'; +import { TableOptionsDropdownSection } from './TableOptionsDropdownSection'; -import { OptionsDropdownSection } from './OptionsDropdownSection'; - -type OptionsDropdownButtonProps = { +type TableOptionsDropdownButtonProps = { + onColumnsChange?: (columns: ViewFieldDefinition[]) => void; HotkeyScope: FiltersHotkeyScope; }; @@ -33,51 +31,52 @@ enum Option { Properties = 'Properties', } -export const OptionsDropdownButton = ({ +export const TableOptionsDropdownButton = ({ + onColumnsChange, HotkeyScope, -}: OptionsDropdownButtonProps) => { +}: TableOptionsDropdownButtonProps) => { const theme = useTheme(); const [isUnfolded, setIsUnfolded] = useState(false); const [selectedOption, setSelectedOption] = useState