diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 9f6b063f6a..a98084a53a 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -2236,14 +2236,14 @@ 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, 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 }> | 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 } | 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 }> | 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, 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 }> | null }> }; +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 } | 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 }> | null }> }; export type AddActivityTargetsOnActivityMutationVariables = Exact<{ activityId: Scalars['String']; @@ -2269,15 +2269,12 @@ export type DeleteActivityMutationVariables = Exact<{ export type DeleteActivityMutation = { __typename?: 'Mutation', deleteManyActivities: { __typename?: 'AffectedRows', count: number } }; export type UpdateActivityMutationVariables = Exact<{ - id: Scalars['String']; - body?: InputMaybe; - title?: InputMaybe; - type?: InputMaybe; - completedAt?: InputMaybe; + where: ActivityWhereUniqueInput; + data: ActivityUpdateInput; }>; -export type UpdateActivityMutation = { __typename?: 'Mutation', updateOneActivity: { __typename?: 'Activity', id: string, body?: string | null, title?: string | null, type: ActivityType, completedAt?: string | null } }; +export type UpdateActivityMutation = { __typename?: 'Mutation', updateOneActivity: { __typename?: 'Activity', id: 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 } | null } }; export type UploadAttachmentMutationVariables = Exact<{ file: Scalars['Upload']; @@ -2775,6 +2772,13 @@ export const GetActivitiesByTargetsDocument = gql` body type completedAt + dueAt + assignee { + id + firstName + lastName + displayName + } author { id firstName @@ -2840,6 +2844,13 @@ export const GetActivityDocument = gql` title type completedAt + dueAt + assignee { + id + firstName + lastName + displayName + } author { id firstName @@ -3021,16 +3032,20 @@ export type DeleteActivityMutationHookResult = ReturnType; export type DeleteActivityMutationOptions = Apollo.BaseMutationOptions; export const UpdateActivityDocument = gql` - mutation UpdateActivity($id: String!, $body: String, $title: String, $type: ActivityType, $completedAt: DateTime) { - updateOneActivity( - where: {id: $id} - data: {body: $body, title: $title, type: $type, completedAt: $completedAt} - ) { + mutation UpdateActivity($where: ActivityWhereUniqueInput!, $data: ActivityUpdateInput!) { + updateOneActivity(where: $where, data: $data) { id body title type completedAt + dueAt + assignee { + id + firstName + lastName + displayName + } } } `; @@ -3049,11 +3064,8 @@ export type UpdateActivityMutationFn = Apollo.MutationFunction & { + accountOwner?: Pick | null; + }; + onSubmit?: () => void; + onCancel?: () => void; +}; + +type UserForSelect = EntityForSelect & { + entityType: Entity.User; +}; + +export function ActivityAssigneePicker({ + activity, + onSubmit, + onCancel, +}: OwnProps) { + const [searchFilter] = useRecoilScopedState( + relationPickerSearchFilterScopedState, + ); + const [updateActivity] = useUpdateActivityMutation(); + + const companies = useFilteredSearchEntityQuery({ + queryHook: useSearchUserQuery, + selectedIds: activity?.accountOwner?.id ? [activity?.accountOwner?.id] : [], + searchFilter: searchFilter, + mappingFunction: (user) => ({ + entityType: Entity.User, + id: user.id, + name: user.displayName, + avatarType: 'rounded', + avatarUrl: user.avatarUrl ?? '', + }), + orderByField: 'firstName', + searchOnFields: ['firstName', 'lastName'], + }); + + async function handleEntitySelected( + selectedUser: UserForSelect | null | undefined, + ) { + if (selectedUser) { + await updateActivity({ + variables: { + where: { id: activity.id }, + data: { + assignee: { connect: { id: selectedUser.id } }, + }, + }, + }); + } + + onSubmit?.(); + } + + return ( + + ); +} diff --git a/front/src/modules/activities/components/ActivityBodyEditor.tsx b/front/src/modules/activities/components/ActivityBodyEditor.tsx index 721ef3830c..61f7ee0a0c 100644 --- a/front/src/modules/activities/components/ActivityBodyEditor.tsx +++ b/front/src/modules/activities/components/ActivityBodyEditor.tsx @@ -35,8 +35,12 @@ export function ActivityBodyEditor({ activity, onChange }: OwnProps) { setBody(activityBody); updateActivityMutation({ variables: { - id: activity.id, - body: activityBody, + where: { + id: activity.id, + }, + data: { + body: activityBody, + }, }, refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], }); diff --git a/front/src/modules/activities/components/ActivityEditor.tsx b/front/src/modules/activities/components/ActivityEditor.tsx index cda03ab939..b1ad7cae41 100644 --- a/front/src/modules/activities/components/ActivityEditor.tsx +++ b/front/src/modules/activities/components/ActivityEditor.tsx @@ -4,20 +4,23 @@ import styled from '@emotion/styled'; import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor'; import { ActivityComments } from '@/activities/components/ActivityComments'; -import { ActivityRelationPicker } from '@/activities/components/ActivityRelationPicker'; import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown'; import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries'; import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox'; -import { PropertyBoxItem } from '@/ui/editable-field/property-box/components/PropertyBoxItem'; +import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField'; import { useIsMobile } from '@/ui/hooks/useIsMobile'; -import { IconArrowUpRight } from '@/ui/icon/index'; +import { IconCalendar } from '@/ui/icon/index'; import { Activity, ActivityTarget, + ActivityType, + User, useUpdateActivityMutation, } from '~/generated/graphql'; import { debounce } from '~/utils/debounce'; +import { ActivityAssigneeEditableField } from '../editable-fields/components/ActivityAssigneeEditableField'; +import { ActivityRelationEditableField } from '../editable-fields/components/ActivityRelationEditableField'; import { ActivityActionBar } from '../right-drawer/components/ActivityActionBar'; import { CommentForDrawer } from '../types/CommentForDrawer'; @@ -65,8 +68,16 @@ const StyledTopActionsContainer = styled.div` `; type OwnProps = { - activity: Pick & { + activity: Pick< + Activity, + 'id' | 'title' | 'body' | 'type' | 'completedAt' | 'dueAt' + > & { comments?: Array | null; + } & { + assignee?: Pick< + User, + 'id' | 'firstName' | 'lastName' | 'displayName' + > | null; } & { activityTargets?: Array< Pick @@ -95,8 +106,12 @@ export function ActivityEditor({ (newTitle: string) => { updateActivityMutation({ variables: { - id: activity.id, - title: newTitle ?? '', + where: { + id: activity.id, + }, + data: { + title: newTitle ?? '', + }, }, refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], }); @@ -108,8 +123,12 @@ export function ActivityEditor({ (value: boolean) => { updateActivityMutation({ variables: { - id: activity.id, - completedAt: value ? new Date().toISOString() : null, + where: { + id: activity.id, + }, + data: { + completedAt: value ? new Date().toISOString() : null, + }, }, refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], }); @@ -152,18 +171,29 @@ export function ActivityEditor({ onCompletionChange={handleActivityCompletionChange} /> - } - value={ - + } + label="Due date" + onSubmit={(newDate) => { + updateActivityMutation({ + variables: { + where: { + id: activity.id, + }, + data: { + dueAt: newDate, + }, + }, + }); }} /> - } - label="Relations" - /> + + + )} + & { + assignee?: Pick | null; + }; +}; + +export function ActivityAssigneeEditableField({ activity }: OwnProps) { + return ( + + + } + editModeContent={ + + } + displayModeContent={ + activity.assignee?.displayName ? ( + + ) : ( + <> + ) + } + isDisplayModeContentEmpty={!activity.assignee} + isDisplayModeFixHeight={true} + /> + + + ); +} diff --git a/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableFieldEditMode.tsx b/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableFieldEditMode.tsx new file mode 100644 index 0000000000..5f16bd120c --- /dev/null +++ b/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableFieldEditMode.tsx @@ -0,0 +1,47 @@ +import styled from '@emotion/styled'; + +import { ActivityAssigneePicker } from '@/activities/components/ActivityAssigneePicker'; +import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; +import { Activity, User } from '~/generated/graphql'; + +const StyledContainer = styled.div` + left: 0px; + position: absolute; + top: -8px; +`; + +export type OwnProps = { + activity: Pick & { + assignee?: Pick | null; + }; + onSubmit?: () => void; + onCancel?: () => void; +}; + +export function ActivityAssigneeEditableFieldEditMode({ + activity, + onSubmit, + onCancel, +}: OwnProps) { + const { closeEditableField } = useEditableField(); + + function handleSubmit() { + closeEditableField(); + onSubmit?.(); + } + + function handleCancel() { + closeEditableField(); + onCancel?.(); + } + + return ( + + + + ); +} diff --git a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx new file mode 100644 index 0000000000..24b75f4839 --- /dev/null +++ b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableField.tsx @@ -0,0 +1,102 @@ +import styled from '@emotion/styled'; + +import { CompanyChip } from '@/companies/components/CompanyChip'; +import { PersonChip } from '@/people/components/PersonChip'; +import { EditableField } from '@/ui/editable-field/components/EditableField'; +import { FieldContext } from '@/ui/editable-field/states/FieldContext'; +import { IconArrowUpRight } from '@/ui/icon'; +import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; +import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope'; +import { + Activity, + ActivityTarget, + useGetCompaniesQuery, + useGetPeopleQuery, +} from '~/generated/graphql'; +import { getLogoUrlFromDomainName } from '~/utils'; + +import { ActivityRelationEditableFieldEditMode } from './ActivityRelationEditableFieldEditMode'; + +const StyledDisplayModeContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: ${({ theme }) => theme.spacing(1)}; +`; + +type OwnProps = { + activity?: Pick & { + activityTargets?: Array< + 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.commentableId ?? '') + : [], + }, + }, + }, + }); + + const { data: targetCompanies } = useGetCompaniesQuery({ + variables: { + where: { + id: { + in: activity?.activityTargets + ? activity?.activityTargets + .filter((target) => target.commentableType === 'Company') + .map((target) => target.commentableId ?? '') + : [], + }, + }, + }, + }); + + return ( + + + } + editModeContent={ + + } + label="Relations" + displayModeContent={ + + {targetCompanies?.companies && + targetCompanies.companies.map((company) => ( + + ))} + {targetPeople?.people && + targetPeople.people.map((person) => ( + + ))} + + } + /> + + + ); +} diff --git a/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx new file mode 100644 index 0000000000..5655b6578e --- /dev/null +++ b/front/src/modules/activities/editable-fields/components/ActivityRelationEditableFieldEditMode.tsx @@ -0,0 +1,123 @@ +import { useCallback, useMemo, useState } from 'react'; +import styled from '@emotion/styled'; + +import { useHandleCheckableActivityTargetChange } from '@/activities/hooks/useHandleCheckableActivityTargetChange'; +import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/activities/utils/flatMapAndSortEntityForSelectArrayByName'; +import { useFilteredSearchCompanyQuery } from '@/companies/queries'; +import { useFilteredSearchPeopleQuery } from '@/people/queries'; +import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; +import { MultipleEntitySelect } from '@/ui/relation-picker/components/MultipleEntitySelect'; +import { Activity, ActivityTarget } from '~/generated/graphql'; +import { assertNotNull } from '~/utils/assert'; + +type OwnProps = { + activity?: Pick & { + activityTargets?: Array< + Pick + > | null; + }; +}; + +const StyledSelectContainer = styled.div` + left: 0px; + position: absolute; + top: -8px; +`; + +export function ActivityRelationEditableFieldEditMode({ activity }: OwnProps) { + const [searchFilter, setSearchFilter] = useState(''); + + const initialPeopleIds = useMemo( + () => + activity?.activityTargets + ?.filter((relation) => relation.commentableType === 'Person') + .map((relation) => relation.commentableId) + .filter(assertNotNull) ?? [], + [activity?.activityTargets], + ); + + const initialCompanyIds = useMemo( + () => + activity?.activityTargets + ?.filter((relation) => relation.commentableType === 'Company') + .map((relation) => relation.commentableId) + .filter(assertNotNull) ?? [], + [activity?.activityTargets], + ); + + const initialSelectedEntityIds = useMemo( + () => + [...initialPeopleIds, ...initialCompanyIds].reduce< + Record + >((result, entityId) => ({ ...result, [entityId]: true }), {}), + [initialPeopleIds, initialCompanyIds], + ); + + const [selectedEntityIds, setSelectedEntityIds] = useState< + Record + >(initialSelectedEntityIds); + + 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 { closeEditableField } = useEditableField(); + + const handleSubmit = useCallback(() => { + handleCheckItemsChange(selectedEntityIds, entitiesToSelect); + closeEditableField(); + }, [ + handleCheckItemsChange, + selectedEntityIds, + entitiesToSelect, + closeEditableField, + ]); + + function handleCancel() { + closeEditableField(); + } + + return ( + + + + ); +} diff --git a/front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts b/front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts index 6c348fc630..5814ee2a49 100644 --- a/front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts +++ b/front/src/modules/activities/hooks/useHandleCheckableActivityTargetChange.ts @@ -10,16 +10,16 @@ import { useRemoveActivityTargetsOnActivityMutation, } from '~/generated/graphql'; -import { GET_ACTIVITIES_BY_TARGETS } from '../queries'; +import { GET_ACTIVITY } from '../queries'; import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect'; export function useHandleCheckableActivityTargetChange({ activity, }: { activity?: Pick & { - activityTargets: Array< + activityTargets?: Array< Pick - >; + > | null; }; }) { const [addActivityTargetsOnActivity] = @@ -27,7 +27,7 @@ export function useHandleCheckableActivityTargetChange({ refetchQueries: [ getOperationName(GET_COMPANIES) ?? '', getOperationName(GET_PEOPLE) ?? '', - getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '', + getOperationName(GET_ACTIVITY) ?? '', ], }); @@ -36,7 +36,7 @@ export function useHandleCheckableActivityTargetChange({ refetchQueries: [ getOperationName(GET_COMPANIES) ?? '', getOperationName(GET_PEOPLE) ?? '', - getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '', + getOperationName(GET_ACTIVITY) ?? '', ], }); @@ -48,9 +48,9 @@ export function useHandleCheckableActivityTargetChange({ return; } - const currentEntityIds = activity.activityTargets.map( - ({ commentableId }) => commentableId, - ); + const currentEntityIds = activity.activityTargets + ? activity.activityTargets.map(({ commentableId }) => commentableId) + : []; const entitiesToAdd = entities.filter( ({ id }) => entityValues[id] && !currentEntityIds.includes(id), @@ -70,10 +70,13 @@ export function useHandleCheckableActivityTargetChange({ }); const activityTargetIdsToDelete = activity.activityTargets - .filter( - ({ commentableId }) => commentableId && !entityValues[commentableId], - ) - .map(({ id }) => id); + ? activity.activityTargets + .filter( + ({ commentableId }) => + commentableId && !entityValues[commentableId], + ) + .map(({ id }) => id) + : []; if (activityTargetIdsToDelete.length) await removeActivityTargetsOnActivity({ diff --git a/front/src/modules/activities/queries/select.ts b/front/src/modules/activities/queries/select.ts index 6b9ec15c32..1b6771d11a 100644 --- a/front/src/modules/activities/queries/select.ts +++ b/front/src/modules/activities/queries/select.ts @@ -17,6 +17,13 @@ export const GET_ACTIVITIES_BY_TARGETS = gql` body type completedAt + dueAt + assignee { + id + firstName + lastName + displayName + } author { id firstName @@ -54,6 +61,13 @@ export const GET_ACTIVITY = gql` title type completedAt + dueAt + assignee { + id + firstName + lastName + displayName + } author { id firstName diff --git a/front/src/modules/activities/queries/update.ts b/front/src/modules/activities/queries/update.ts index 627ba1a3fd..50900d3bd8 100644 --- a/front/src/modules/activities/queries/update.ts +++ b/front/src/modules/activities/queries/update.ts @@ -58,26 +58,22 @@ export const DELETE_ACTIVITY = gql` export const UPDATE_ACTIVITY = gql` mutation UpdateActivity( - $id: String! - $body: String - $title: String - $type: ActivityType - $completedAt: DateTime + $where: ActivityWhereUniqueInput! + $data: ActivityUpdateInput! ) { - updateOneActivity( - where: { id: $id } - data: { - body: $body - title: $title - type: $type - completedAt: $completedAt - } - ) { + updateOneActivity(where: $where, data: $data) { id body title type completedAt + dueAt + assignee { + id + firstName + lastName + displayName + } } } `; diff --git a/front/src/modules/activities/right-drawer/components/Activity.tsx b/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx similarity index 96% rename from front/src/modules/activities/right-drawer/components/Activity.tsx rename to front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx index dd8df0ecf3..3ee556fd37 100644 --- a/front/src/modules/activities/right-drawer/components/Activity.tsx +++ b/front/src/modules/activities/right-drawer/components/RightDrawerActivity.tsx @@ -21,7 +21,7 @@ type OwnProps = { autoFillTitle?: boolean; }; -export function Activity({ +export function RightDrawerActivity({ activityId, showComment = true, autoFillTitle = false, diff --git a/front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx b/front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx index 86f4aafc36..30718201f5 100644 --- a/front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx +++ b/front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx @@ -5,7 +5,7 @@ import { RightDrawerBody } from '@/ui/right-drawer/components/RightDrawerBody'; import { RightDrawerPage } from '@/ui/right-drawer/components/RightDrawerPage'; import { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar'; -import { Activity } from '../Activity'; +import { RightDrawerActivity } from '../RightDrawerActivity'; export function RightDrawerCreateActivity() { const activityId = useRecoilValue(viewableActivityIdState); @@ -15,7 +15,7 @@ export function RightDrawerCreateActivity() { {activityId && ( - - {activityId && } + {activityId && } ); diff --git a/front/src/modules/activities/timeline/components/TimelineActivity.tsx b/front/src/modules/activities/timeline/components/TimelineActivity.tsx index 07c85f0861..d041ee38e8 100644 --- a/front/src/modules/activities/timeline/components/TimelineActivity.tsx +++ b/front/src/modules/activities/timeline/components/TimelineActivity.tsx @@ -7,12 +7,13 @@ import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRi import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries'; import { IconNotes } from '@/ui/icon'; import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; -import { Activity, useUpdateActivityMutation } from '~/generated/graphql'; +import { Activity, User, useUpdateActivityMutation } from '~/generated/graphql'; import { beautifyExactDate, beautifyPastDateRelativeToNow, } from '~/utils/date-utils'; +import { TimelineActivityCardFooter } from './TimelineActivityCardFooter'; import { TimelineActivityTitle } from './TimelineActivityTitle'; const StyledIconContainer = styled.div` @@ -73,19 +74,19 @@ const StyledCard = styled.div` align-items: flex-start; background: ${({ theme }) => theme.background.secondary}; border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.sm}; + border-radius: ${({ theme }) => theme.border.radius.md}; display: flex; flex-direction: column; gap: ${({ theme }) => theme.spacing(3)}; - max-width: 400px; - padding: ${({ theme }) => theme.spacing(3)}; + max-width: 100%; position: relative; + width: 400px; `; const StyledCardContent = styled.div` align-self: stretch; color: ${({ theme }) => theme.font.color.secondary}; - + margin-top: ${({ theme }) => theme.spacing(2)}; width: 100%; `; @@ -104,6 +105,11 @@ const StyledTooltip = styled(Tooltip)` padding: ${({ theme }) => theme.spacing(2)}; `; +const StyledCardDetailsContainer = styled.div` + padding: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + const StyledTimelineItemContainer = styled.div` align-items: center; align-self: stretch; @@ -115,7 +121,9 @@ type OwnProps = { activity: Pick< Activity, 'id' | 'title' | 'body' | 'createdAt' | 'completedAt' | 'type' - > & { author: Pick }; + > & { author: Pick } & { + assignee?: Pick | null; + }; }; export function TimelineActivity({ activity }: OwnProps) { @@ -130,8 +138,10 @@ export function TimelineActivity({ activity }: OwnProps) { (value: boolean) => { updateActivityMutation({ variables: { - id: activity.id, - completedAt: value ? new Date().toISOString() : null, + where: { id: activity.id }, + data: { + completedAt: value ? new Date().toISOString() : null, + }, }, refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], }); @@ -165,15 +175,20 @@ export function TimelineActivity({ activity }: OwnProps) { openActivityRightDrawer(activity.id)}> - - - - + + + + + + + diff --git a/front/src/modules/activities/timeline/components/TimelineActivityCardFooter.tsx b/front/src/modules/activities/timeline/components/TimelineActivityCardFooter.tsx new file mode 100644 index 0000000000..0677d7248c --- /dev/null +++ b/front/src/modules/activities/timeline/components/TimelineActivityCardFooter.tsx @@ -0,0 +1,50 @@ +import styled from '@emotion/styled'; + +import { UserChip } from '@/users/components/UserChip'; +import { Activity, User } from '~/generated/graphql'; +import { beautifyExactDate } from '~/utils/date-utils'; + +type OwnProps = { + activity: Pick & { + assignee?: Pick | null; + }; +}; + +const StyledContainer = styled.div` + align-items: center; + border-top: 1px solid ${({ theme }) => theme.border.color.medium}; + display: flex; + flex-direction: row; + gap: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(2)}; + width: calc(100% - ${({ theme }) => theme.spacing(4)}); +`; + +const StyledVerticalSeparator = styled.div` + border-left: 1px solid ${({ theme }) => theme.border.color.medium}; + height: 24px; +`; + +export function TimelineActivityCardFooter({ activity }: OwnProps) { + return ( + <> + {(activity.assignee || activity.dueAt) && ( + + {activity.assignee && ( + + )} + {activity.dueAt && ( + <> + {activity.assignee && } + {beautifyExactDate(activity.dueAt)} + + )} + + )} + + ); +} diff --git a/front/src/modules/activities/timeline/components/TimelineActivityTitle.tsx b/front/src/modules/activities/timeline/components/TimelineActivityTitle.tsx index 317823f5e2..855a75a346 100644 --- a/front/src/modules/activities/timeline/components/TimelineActivityTitle.tsx +++ b/front/src/modules/activities/timeline/components/TimelineActivityTitle.tsx @@ -12,15 +12,19 @@ const StyledTitleContainer = styled.div` gap: ${({ theme }) => theme.spacing(2)}; line-height: ${({ theme }) => theme.text.lineHeight.lg}; - width: calc(100% - ${({ theme }) => theme.spacing(6)}); -`; - -const StyledTitleText = styled.div<{ completed?: boolean }>` - text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')}; width: 100%; `; -const StyledCheckboxContainer = styled.div` +const StyledTitleText = styled.div<{ + completed?: boolean; + hasCheckbox?: boolean; +}>` + text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')}; + width: ${({ hasCheckbox, theme }) => + !hasCheckbox ? '100%;' : `calc(100% - ${theme.spacing(5)});`}; +`; + +const StyledCheckboxContainer = styled.div<{ hasCheckbox?: boolean }>` align-items: center; display: flex; justify-content: center; @@ -55,7 +59,10 @@ export function TimelineActivityTitle({ /> )} - + diff --git a/front/src/modules/companies/components/CompanyBoardCard.tsx b/front/src/modules/companies/components/CompanyBoardCard.tsx index c81d9a5c11..ac80868b8f 100644 --- a/front/src/modules/companies/components/CompanyBoardCard.tsx +++ b/front/src/modules/companies/components/CompanyBoardCard.tsx @@ -99,7 +99,9 @@ const StyledCheckboxContainer = styled.div` `; const StyledFieldContainer = styled.div` - width: max-content; + display: flex; + flex-direction: row; + width: 100%; `; export function CompanyBoardCard() { diff --git a/front/src/modules/companies/constants/companyViewFields.tsx b/front/src/modules/companies/constants/companyViewFields.tsx index 01f77057ed..4df0ed88b9 100644 --- a/front/src/modules/companies/constants/companyViewFields.tsx +++ b/front/src/modules/companies/constants/companyViewFields.tsx @@ -1,4 +1,5 @@ import { + IconBrandLinkedin, IconBuildingSkyscraper, IconCalendarEvent, IconLink, @@ -81,7 +82,7 @@ export const companyViewFields: ViewFieldDefinition[] = [ { id: 'linkedin', columnLabel: 'LinkedIn', - columnIcon: , + columnIcon: , columnSize: 170, columnOrder: 6, metadata: { diff --git a/front/src/modules/companies/editable-field/components/CompanyAccountOwnerEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyAccountOwnerEditableField.tsx index 5c46a7b1b2..dfe5f21b9f 100644 --- a/front/src/modules/companies/editable-field/components/CompanyAccountOwnerEditableField.tsx +++ b/front/src/modules/companies/editable-field/components/CompanyAccountOwnerEditableField.tsx @@ -38,6 +38,7 @@ export function CompanyAccountOwnerEditableField({ company }: OwnProps) { ) } 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 index a0d4c9e171..0b63a3a066 100644 --- a/front/src/modules/companies/editable-field/components/CompanyAccountOwnerPickerFieldEditMode.tsx +++ b/front/src/modules/companies/editable-field/components/CompanyAccountOwnerPickerFieldEditMode.tsx @@ -4,8 +4,8 @@ import { CompanyAccountOwnerPicker } from '@/companies/components/CompanyAccount import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; import { Company, User } from '~/generated/graphql'; -const CompanyAccountOwnerPickerContainer = styled.div` - left: 24px; +const StyledContainer = styled.div` + left: 0px; position: absolute; top: -8px; `; @@ -36,12 +36,12 @@ export function CompanyAccountOwnerPickerFieldEditMode({ } return ( - + - + ); } diff --git a/front/src/modules/companies/editable-field/components/CompanyNameEditableField.tsx b/front/src/modules/companies/editable-field/components/CompanyNameEditableField.tsx index e32cdb53a4..19c6f408e9 100644 --- a/front/src/modules/companies/editable-field/components/CompanyNameEditableField.tsx +++ b/front/src/modules/companies/editable-field/components/CompanyNameEditableField.tsx @@ -1,8 +1,7 @@ import { useEffect, useState } from 'react'; +import styled from '@emotion/styled'; -import { EditableField } from '@/ui/editable-field/components/EditableField'; import { FieldContext } from '@/ui/editable-field/states/FieldContext'; -import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText'; import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql'; @@ -10,6 +9,30 @@ type OwnProps = { company: Pick; }; +const StyledEditableTitleInput = styled.input<{ + value: string; +}>` + background: transparent; + + border: none; + color: ${({ theme, value }) => + value ? theme.font.color.primary : theme.font.color.light}; + display: flex; + flex-direction: column; + + font-size: ${({ theme }) => theme.font.size.xl}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + justify-content: center; + + line-height: ${({ theme }) => theme.text.lineHeight.md}; + outline: none; + &::placeholder { + color: ${({ theme }) => theme.font.color.light}; + } + text-align: center; + width: calc(100% - ${({ theme }) => theme.spacing(2)}); +`; + export function CompanyNameEditableField({ company }: OwnProps) { const [internalValue, setInternalValue] = useState(company.name); @@ -36,27 +59,14 @@ export function CompanyNameEditableField({ company }: OwnProps) { }); } - async function handleCancel() { - setInternalValue(company.name); - } - return ( - { - handleChange(newValue); - }} - /> - } - displayModeContent={internalValue ?? ''} - isDisplayModeContentEmpty={!(internalValue !== '')} + handleChange(event.target.value)} + onBlur={handleSubmit} + value={internalValue} /> ); diff --git a/front/src/modules/people/constants/peopleViewFields.tsx b/front/src/modules/people/constants/peopleViewFields.tsx index 5f78faad8f..db317e6f5d 100644 --- a/front/src/modules/people/constants/peopleViewFields.tsx +++ b/front/src/modules/people/constants/peopleViewFields.tsx @@ -33,6 +33,7 @@ export const peopleViewFields: ViewFieldDefinition[] = [ secondValueFieldName: 'lastName', firstValuePlaceholder: 'First name', secondValuePlaceholder: 'Last name', + avatarUrlFieldName: 'avatarUrl', entityType: Entity.Person, }, } satisfies ViewFieldDefinition, diff --git a/front/src/modules/people/editable-field/components/PeopleCompanyEditableField.tsx b/front/src/modules/people/editable-field/components/PeopleCompanyEditableField.tsx index 416f3e4af1..6c3cf08ae8 100644 --- a/front/src/modules/people/editable-field/components/PeopleCompanyEditableField.tsx +++ b/front/src/modules/people/editable-field/components/PeopleCompanyEditableField.tsx @@ -41,6 +41,7 @@ export function PeopleCompanyEditableField({ people }: OwnProps) { ) } 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 index 7e944cb41d..a07a49e12f 100644 --- a/front/src/modules/people/editable-field/components/PeopleCompanyEditableFieldEditMode.tsx +++ b/front/src/modules/people/editable-field/components/PeopleCompanyEditableFieldEditMode.tsx @@ -1,3 +1,5 @@ +import styled from '@emotion/styled'; + import { useFilteredSearchCompanyQuery } from '@/companies/queries'; import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState'; @@ -14,6 +16,12 @@ 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(); @@ -51,14 +59,16 @@ export function PeopleCompanyEditableFieldEditMode({ people }: OwnProps) { } return ( - + + + ); } diff --git a/front/src/modules/people/editable-field/components/PeopleFullNameEditableField.tsx b/front/src/modules/people/editable-field/components/PeopleFullNameEditableField.tsx index ee77077329..8852e77111 100644 --- a/front/src/modules/people/editable-field/components/PeopleFullNameEditableField.tsx +++ b/front/src/modules/people/editable-field/components/PeopleFullNameEditableField.tsx @@ -1,6 +1,5 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; -import { EditableField } from '@/ui/editable-field/components/EditableField'; import { FieldContext } from '@/ui/editable-field/states/FieldContext'; import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; import { Person, useUpdateOnePersonMutation } from '~/generated/graphql'; @@ -21,56 +20,40 @@ export function PeopleFullNameEditableField({ people }: OwnProps) { const [updatePeople] = useUpdateOnePersonMutation(); - useEffect(() => { - setInternalValueFirstName(people.firstName); - setInternalValueLastName(people.lastName); - }, [people.firstName, people.lastName]); - async function handleChange( newValueFirstName: string, newValueLastName: string, ) { setInternalValueFirstName(newValueFirstName); setInternalValueLastName(newValueLastName); + handleSubmit(newValueFirstName, newValueLastName); } - async function handleSubmit() { + async function handleSubmit( + newValueFirstName: string, + newValueLastName: string, + ) { await updatePeople({ variables: { where: { id: people.id, }, data: { - firstName: internalValueFirstName ?? '', - lastName: internalValueLastName ?? '', + firstName: newValueFirstName ?? '', + lastName: newValueLastName ?? '', }, }, }); } - async function handleCancel() { - setInternalValueFirstName(people.firstName); - setInternalValueLastName(people.lastName); - } - return ( - - } - displayModeContent={`${internalValueFirstName} ${internalValueLastName}`} - isDisplayModeContentEmpty={ - !(internalValueFirstName !== '') && !(internalValueLastName !== '') - } + ); diff --git a/front/src/modules/pipeline/editable-field/components/PipelineProgressPointOfContactEditableField.tsx b/front/src/modules/pipeline/editable-field/components/PipelineProgressPointOfContactEditableField.tsx index 2eb1000f84..5cc7844fb0 100644 --- a/front/src/modules/pipeline/editable-field/components/PipelineProgressPointOfContactEditableField.tsx +++ b/front/src/modules/pipeline/editable-field/components/PipelineProgressPointOfContactEditableField.tsx @@ -45,6 +45,7 @@ export function PipelineProgressPointOfContactEditableField({ ) } isDisplayModeContentEmpty={!pipelineProgress.pointOfContact} + isDisplayModeFixHeight /> diff --git a/front/src/modules/pipeline/editable-field/components/PipelineProgressPointOfContactPickerFieldEditMode.tsx b/front/src/modules/pipeline/editable-field/components/PipelineProgressPointOfContactPickerFieldEditMode.tsx index 03a3ff16bb..5e6023359b 100644 --- a/front/src/modules/pipeline/editable-field/components/PipelineProgressPointOfContactPickerFieldEditMode.tsx +++ b/front/src/modules/pipeline/editable-field/components/PipelineProgressPointOfContactPickerFieldEditMode.tsx @@ -5,7 +5,7 @@ import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; import { Person, PipelineProgress } from '~/generated/graphql'; const PipelineProgressPointOfContactPickerContainer = styled.div` - left: 24px; + left: 0px; position: absolute; top: -8px; `; diff --git a/front/src/modules/settings/profile/components/DeleteModal.tsx b/front/src/modules/settings/profile/components/DeleteModal.tsx index 8cc37fcb62..a92acc16f8 100644 --- a/front/src/modules/settings/profile/components/DeleteModal.tsx +++ b/front/src/modules/settings/profile/components/DeleteModal.tsx @@ -77,7 +77,14 @@ export function DeleteModal({ return ( - setIsOpen(!isOpen)}> + { + if (isOpen) { + setIsOpen(false); + } + }} + > {title} {subtitle} ) => { diff --git a/front/src/modules/ui/editable-field/components/EditableField.tsx b/front/src/modules/ui/editable-field/components/EditableField.tsx index f05eab573f..6d896646c6 100644 --- a/front/src/modules/ui/editable-field/components/EditableField.tsx +++ b/front/src/modules/ui/editable-field/components/EditableField.tsx @@ -15,6 +15,7 @@ const StyledIconContainer = styled.div` align-items: center; color: ${({ theme }) => theme.font.color.tertiary}; display: flex; + width: 16px; svg { align-items: center; @@ -32,6 +33,12 @@ const StyledLabelAndIconContainer = styled.div` gap: ${({ theme }) => theme.spacing(1)}; `; +const StyledValueContainer = styled.div` + display: flex; + flex: 1; + max-width: calc(100% - ${({ theme }) => theme.spacing(4)}); +`; + const StyledLabel = styled.div>` align-items: center; @@ -39,18 +46,22 @@ const StyledLabel = styled.div>` labelFixedWidth ? `${labelFixedWidth}px` : 'fit-content'}; `; +const StyledEditButtonContainer = styled(motion.div)` + position: absolute; + right: 0; +`; + export const EditableFieldBaseContainer = styled.div` align-items: center; box-sizing: border-box; display: flex; - gap: ${({ theme }) => theme.spacing(1)}; - height: 24px; - justify-content: flex-start; - position: relative; - user-select: none; + justify-content: space-between; + position: relative; + + user-select: none; width: 100%; `; @@ -66,6 +77,7 @@ type OwnProps = { parentHotkeyScope?: HotkeyScope; customEditHotkeyScope?: HotkeyScope; isDisplayModeContentEmpty?: boolean; + isDisplayModeFixHeight?: boolean; onSubmit?: () => void; onCancel?: () => void; }; @@ -82,6 +94,7 @@ export function EditableField({ disableHoverEffect, isDisplayModeContentEmpty, displayModeContentOnly, + isDisplayModeFixHeight, onSubmit, onCancel, }: OwnProps) { @@ -119,29 +132,32 @@ export function EditableField({ {label} )} - {isFieldInEditMode && !displayModeContentOnly ? ( - - {editModeContent} - - ) : ( - - {displayModeContent} - - )} + + {isFieldInEditMode && !displayModeContentOnly ? ( + + {editModeContent} + + ) : ( + + {displayModeContent} + + )} + {showEditButton && ( - - + )} ); diff --git a/front/src/modules/ui/editable-field/components/EditableFieldDisplayMode.tsx b/front/src/modules/ui/editable-field/components/EditableFieldDisplayMode.tsx index 041a585ae4..abaa092fce 100644 --- a/front/src/modules/ui/editable-field/components/EditableFieldDisplayMode.tsx +++ b/front/src/modules/ui/editable-field/components/EditableFieldDisplayMode.tsx @@ -4,15 +4,18 @@ import styled from '@emotion/styled'; export const EditableFieldNormalModeOuterContainer = styled.div< Pick< OwnProps, - 'disableClick' | 'isDisplayModeContentEmpty' | 'disableHoverEffect' + | 'disableClick' + | 'isDisplayModeContentEmpty' + | 'disableHoverEffect' + | 'isDisplayModeFixHeight' > >` align-items: center; border-radius: ${({ theme }) => theme.border.radius.sm}; display: flex; - height: 100%; - - height: 16px; + height: ${({ isDisplayModeFixHeight }) => + isDisplayModeFixHeight ? '16px' : 'auto'}; + min-height: 16px; overflow: hidden; @@ -67,6 +70,7 @@ type OwnProps = { onClick?: () => void; isDisplayModeContentEmpty?: boolean; disableHoverEffect?: boolean; + isDisplayModeFixHeight?: boolean; }; export function EditableFieldDisplayMode({ @@ -75,6 +79,7 @@ export function EditableFieldDisplayMode({ onClick, isDisplayModeContentEmpty, disableHoverEffect, + isDisplayModeFixHeight, }: React.PropsWithChildren) { return ( {children} diff --git a/front/src/modules/ui/editable-field/components/EditableFieldEditMode.tsx b/front/src/modules/ui/editable-field/components/EditableFieldEditMode.tsx index e40e2dede8..fb1b599664 100644 --- a/front/src/modules/ui/editable-field/components/EditableFieldEditMode.tsx +++ b/front/src/modules/ui/editable-field/components/EditableFieldEditMode.tsx @@ -7,10 +7,12 @@ export const EditableFieldEditModeContainer = styled.div` align-items: center; display: flex; + height: 24px; margin-left: -${({ theme }) => theme.spacing(1)}; + position: relative; - width: inherit; + width: 100%; z-index: 10; `; diff --git a/front/src/modules/ui/editable-field/property-box/components/PropertyBox.tsx b/front/src/modules/ui/editable-field/property-box/components/PropertyBox.tsx index c46da9a25f..44fdd1b8e3 100644 --- a/front/src/modules/ui/editable-field/property-box/components/PropertyBox.tsx +++ b/front/src/modules/ui/editable-field/property-box/components/PropertyBox.tsx @@ -7,7 +7,7 @@ const StyledPropertyBoxContainer = styled.div` border-radius: ${({ theme }) => theme.border.radius.sm}; display: flex; flex-direction: column; - gap: ${({ theme }) => theme.spacing(3)}; + gap: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(3)}; `; diff --git a/front/src/modules/ui/editable-field/variants/components/DateEditableField.tsx b/front/src/modules/ui/editable-field/variants/components/DateEditableField.tsx index 24ca0636d8..a528a33aec 100644 --- a/front/src/modules/ui/editable-field/variants/components/DateEditableField.tsx +++ b/front/src/modules/ui/editable-field/variants/components/DateEditableField.tsx @@ -10,11 +10,12 @@ import { EditableFieldEditModeDate } from './EditableFieldEditModeDate'; type OwnProps = { icon?: React.ReactNode; + label?: string; value: string | null | undefined; onSubmit?: (newValue: string) => void; }; -export function DateEditableField({ icon, value, onSubmit }: OwnProps) { +export function DateEditableField({ icon, value, label, onSubmit }: OwnProps) { const [internalValue, setInternalValue] = useState(value); useEffect(() => { @@ -47,6 +48,7 @@ export function DateEditableField({ icon, value, onSubmit }: OwnProps) { onSubmit={handleSubmit} onCancel={handleCancel} iconLabel={icon} + label={label} editModeContent={ input:last-child { - border-left: 1px solid ${({ theme }) => theme.border.color.medium}; - padding-left: ${({ theme }) => theme.spacing(2)}; - } +const StyledNameInput = styled(StyledInput)` + padding: 0; + text-align: center; + width: auto; `; export function InplaceInputDoubleText({ @@ -31,7 +33,8 @@ export function InplaceInputDoubleText({ }: OwnProps) { return ( - - theme.font.color.primary}; + display: flex; + flex-direction: row; font-size: ${({ theme }) => theme.font.size.xl}; font-weight: ${({ theme }) => theme.font.weight.semiBold}; - max-width: 100%; + justify-content: center; + width: 100%; `; const StyledTooltip = styled(Tooltip)` diff --git a/front/src/modules/ui/relation-picker/components/MultipleEntitySelect.tsx b/front/src/modules/ui/relation-picker/components/MultipleEntitySelect.tsx index c7fb611ee1..717e434944 100644 --- a/front/src/modules/ui/relation-picker/components/MultipleEntitySelect.tsx +++ b/front/src/modules/ui/relation-picker/components/MultipleEntitySelect.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react'; import debounce from 'lodash.debounce'; import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu'; @@ -6,6 +7,7 @@ import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch'; import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; +import { useListenClickOutside } from '@/ui/hooks/useListenClickOutside'; import { Avatar } from '@/users/components/Avatar'; import { isNonEmptyString } from '~/utils/isNonEmptyString'; @@ -25,6 +27,7 @@ export function MultipleEntitySelect< >({ entities, onChange, + onSubmit, onSearchFilterChange, searchFilter, value, @@ -33,6 +36,8 @@ export function MultipleEntitySelect< searchFilter: string; onSearchFilterChange: (newSearchFilter: string) => void; onChange: (value: Record) => void; + onCancel?: () => void; + onSubmit?: () => void; value: Record; }) { const debouncedSetSearchFilter = debounce(onSearchFilterChange, 100, { @@ -53,8 +58,21 @@ export function MultipleEntitySelect< isNonEmptyString(entity.name), ); + const containerRef = useRef(null); + + useListenClickOutside({ + refs: [containerRef], + callback: (event) => { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + + onSubmit?.(); + }, + }); + return ( - + ( + tableEntityFieldFamilySelector({ + entityId: currentRowEntityId ?? '', + fieldName: viewField.metadata.avatarUrlFieldName, + }), + ); + const displayName = `${firstValue} ${secondValue}`; switch (viewField.metadata.entityType) { @@ -40,7 +47,13 @@ export function GenericEditableDoubleTextChipCellDisplayMode({ return ; } case Entity.Person: { - return ; + return ( + + ); } default: console.warn( diff --git a/front/src/modules/ui/table/components/GenericEditableRelationCellDisplayMode.tsx b/front/src/modules/ui/table/components/GenericEditableRelationCellDisplayMode.tsx index be5f1dcdd1..fe2ec45cd9 100644 --- a/front/src/modules/ui/table/components/GenericEditableRelationCellDisplayMode.tsx +++ b/front/src/modules/ui/table/components/GenericEditableRelationCellDisplayMode.tsx @@ -45,6 +45,7 @@ export function GenericEditableRelationCellDisplayMode({ ); } diff --git a/front/src/modules/ui/table/types/ViewField.ts b/front/src/modules/ui/table/types/ViewField.ts index 2cf0d197cf..6ae71e5d13 100644 --- a/front/src/modules/ui/table/types/ViewField.ts +++ b/front/src/modules/ui/table/types/ViewField.ts @@ -68,6 +68,7 @@ export type ViewFieldDoubleTextChipMetadata = { firstValuePlaceholder: string; secondValueFieldName: string; secondValuePlaceholder: string; + avatarUrlFieldName: string; entityType: Entity; }; diff --git a/front/src/pages/companies/__stories__/Company.stories.tsx b/front/src/pages/companies/__stories__/Company.stories.tsx index c5fa6306de..f1dc79c436 100644 --- a/front/src/pages/companies/__stories__/Company.stories.tsx +++ b/front/src/pages/companies/__stories__/Company.stories.tsx @@ -6,7 +6,7 @@ import { graphql } from 'msw'; import { GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY } from '@/activities/queries'; import { CREATE_ACTIVITY_WITH_COMMENT } from '@/activities/queries/create'; -import { GET_COMPANY } from '@/companies/queries'; +import { GET_COMPANY, UPDATE_ONE_COMPANY } from '@/companies/queries'; import { PageDecorator, type PageDecoratorArgs, @@ -95,6 +95,16 @@ export const EditNote: Story = { }), ); }), + graphql.mutation( + getOperationName(UPDATE_ONE_COMPANY) ?? '', + (req, res, ctx) => { + return res( + ctx.data({ + updateOneCompany: [mockedCompaniesData[0]], + }), + ); + }, + ), ], }, }; diff --git a/front/src/testing/graphqlMocks.ts b/front/src/testing/graphqlMocks.ts index 46f5912533..c10376906b 100644 --- a/front/src/testing/graphqlMocks.ts +++ b/front/src/testing/graphqlMocks.ts @@ -7,6 +7,7 @@ import { GET_COMPANIES } from '@/companies/queries'; import { GET_PEOPLE, GET_PERSON, UPDATE_ONE_PERSON } from '@/people/queries'; import { GET_PIPELINE_PROGRESS, GET_PIPELINES } from '@/pipeline/queries'; import { + SEARCH_ACTIVITY_QUERY, SEARCH_COMPANY_QUERY, SEARCH_PEOPLE_QUERY, SEARCH_USER_QUERY, @@ -16,11 +17,13 @@ import { GetCompaniesQuery, GetPeopleQuery, GetPersonQuery, + SearchActivityQuery, SearchCompanyQuery, SearchPeopleQuery, SearchUserQuery, } from '~/generated/graphql'; +import { mockedActivities } from './mock-data/activities'; import { mockedCompaniesData } from './mock-data/companies'; import { mockedPeopleData } from './mock-data/people'; import { mockedPipelineProgressData } from './mock-data/pipeline-progress'; @@ -103,6 +106,26 @@ export const graphqlMocks = [ }), ); }), + graphql.query( + getOperationName(SEARCH_ACTIVITY_QUERY) ?? '', + (req, res, ctx) => { + const returnedMockedData = filterAndSortData< + SearchActivityQuery['searchResults'][0] + >( + mockedActivities, + req.variables.where, + Array.isArray(req.variables.orderBy) + ? req.variables.orderBy + : [req.variables.orderBy], + req.variables.limit, + ); + return res( + ctx.data({ + searchResults: returnedMockedData, + }), + ); + }, + ), graphql.query(getOperationName(GET_CURRENT_USER) ?? '', (req, res, ctx) => { return res( ctx.data({ diff --git a/front/src/testing/mock-data/activities.ts b/front/src/testing/mock-data/activities.ts index b2e71919d7..dd2275fc60 100644 --- a/front/src/testing/mock-data/activities.ts +++ b/front/src/testing/mock-data/activities.ts @@ -16,6 +16,8 @@ type MockedActivity = Pick< | 'body' | 'title' | 'authorId' + | 'dueAt' + | 'completedAt' > & { author: { __typename?: 'User' | undefined; @@ -24,6 +26,13 @@ type MockedActivity = Pick< lastName: string; displayName: string; }; + assignee: { + __typename?: 'User' | undefined; + id: string; + firstName: string; + lastName: string; + displayName: string; + }; comments: Array>; activityTargets: Array< Pick< @@ -47,12 +56,20 @@ export const mockedActivities: Array = [ title: 'My very first note', type: ActivityType.Note, body: null, + dueAt: null, + completedAt: null, author: { id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', firstName: 'Charles', lastName: 'Test', displayName: 'Charles Test', }, + assignee: { + id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', + firstName: 'Charles', + lastName: 'Test', + displayName: 'Charles Test', + }, authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', comments: [], activityTargets: [ @@ -94,12 +111,20 @@ export const mockedActivities: Array = [ title: 'Another note', body: null, type: ActivityType.Note, + completedAt: null, + dueAt: null, author: { id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', firstName: 'Charles', lastName: 'Test', displayName: 'Charles Test', }, + assignee: { + id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', + firstName: 'Charles', + lastName: 'Test', + displayName: 'Charles Test', + }, authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', comments: [], activityTargets: [ diff --git a/front/src/utils/__tests__/date-utils.test.ts b/front/src/utils/__tests__/date-utils.test.ts index 22e6a9185f..abcc40d569 100644 --- a/front/src/utils/__tests__/date-utils.test.ts +++ b/front/src/utils/__tests__/date-utils.test.ts @@ -18,7 +18,7 @@ describe('beautifyExactDate', () => { const actualDate = new Date(mockDate); const expected = DateTime.fromJSDate(actualDate) .setLocale(DEFAULT_DATE_LOCALE) - .toFormat('DD · TT'); + .toFormat('DD · T'); const result = beautifyExactDate(mockDate); expect(result).toEqual(expected); diff --git a/front/src/utils/date-utils.ts b/front/src/utils/date-utils.ts index cde50ebb0b..27ad6ba1a8 100644 --- a/front/src/utils/date-utils.ts +++ b/front/src/utils/date-utils.ts @@ -33,7 +33,7 @@ export function beautifyExactDate(dateToBeautify: Date | string | number) { try { const parsedDate = parseDate(dateToBeautify); - return parsedDate.toFormat('DD · TT'); + return parsedDate.toFormat('DD · T'); } catch (error) { logError(error); return '';