Add dueDate and assignee on notes (#988)

* Add dueDate and assignee on notes

* Fix tests

* Fix tests
This commit is contained in:
Charles Bochet 2023-07-29 15:36:21 -07:00 committed by GitHub
parent d9f6ae8663
commit 8601ed04ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 875 additions and 205 deletions

View File

@ -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<{ export type GetActivityQueryVariables = Exact<{
activityId: Scalars['String']; 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<{ export type AddActivityTargetsOnActivityMutationVariables = Exact<{
activityId: Scalars['String']; activityId: Scalars['String'];
@ -2269,15 +2269,12 @@ export type DeleteActivityMutationVariables = Exact<{
export type DeleteActivityMutation = { __typename?: 'Mutation', deleteManyActivities: { __typename?: 'AffectedRows', count: number } }; export type DeleteActivityMutation = { __typename?: 'Mutation', deleteManyActivities: { __typename?: 'AffectedRows', count: number } };
export type UpdateActivityMutationVariables = Exact<{ export type UpdateActivityMutationVariables = Exact<{
id: Scalars['String']; where: ActivityWhereUniqueInput;
body?: InputMaybe<Scalars['String']>; data: ActivityUpdateInput;
title?: InputMaybe<Scalars['String']>;
type?: InputMaybe<ActivityType>;
completedAt?: InputMaybe<Scalars['DateTime']>;
}>; }>;
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<{ export type UploadAttachmentMutationVariables = Exact<{
file: Scalars['Upload']; file: Scalars['Upload'];
@ -2775,6 +2772,13 @@ export const GetActivitiesByTargetsDocument = gql`
body body
type type
completedAt completedAt
dueAt
assignee {
id
firstName
lastName
displayName
}
author { author {
id id
firstName firstName
@ -2840,6 +2844,13 @@ export const GetActivityDocument = gql`
title title
type type
completedAt completedAt
dueAt
assignee {
id
firstName
lastName
displayName
}
author { author {
id id
firstName firstName
@ -3021,16 +3032,20 @@ export type DeleteActivityMutationHookResult = ReturnType<typeof useDeleteActivi
export type DeleteActivityMutationResult = Apollo.MutationResult<DeleteActivityMutation>; export type DeleteActivityMutationResult = Apollo.MutationResult<DeleteActivityMutation>;
export type DeleteActivityMutationOptions = Apollo.BaseMutationOptions<DeleteActivityMutation, DeleteActivityMutationVariables>; export type DeleteActivityMutationOptions = Apollo.BaseMutationOptions<DeleteActivityMutation, DeleteActivityMutationVariables>;
export const UpdateActivityDocument = gql` export const UpdateActivityDocument = gql`
mutation UpdateActivity($id: String!, $body: String, $title: String, $type: ActivityType, $completedAt: DateTime) { mutation UpdateActivity($where: ActivityWhereUniqueInput!, $data: ActivityUpdateInput!) {
updateOneActivity( updateOneActivity(where: $where, data: $data) {
where: {id: $id}
data: {body: $body, title: $title, type: $type, completedAt: $completedAt}
) {
id id
body body
title title
type type
completedAt completedAt
dueAt
assignee {
id
firstName
lastName
displayName
}
} }
} }
`; `;
@ -3049,11 +3064,8 @@ export type UpdateActivityMutationFn = Apollo.MutationFunction<UpdateActivityMut
* @example * @example
* const [updateActivityMutation, { data, loading, error }] = useUpdateActivityMutation({ * const [updateActivityMutation, { data, loading, error }] = useUpdateActivityMutation({
* variables: { * variables: {
* id: // value for 'id' * where: // value for 'where'
* body: // value for 'body' * data: // value for 'data'
* title: // value for 'title'
* type: // value for 'type'
* completedAt: // value for 'completedAt'
* }, * },
* }); * });
*/ */

View File

@ -0,0 +1,79 @@
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { SingleEntitySelect } from '@/ui/relation-picker/components/SingleEntitySelect';
import { relationPickerSearchFilterScopedState } from '@/ui/relation-picker/states/relationPickerSearchFilterScopedState';
import { EntityForSelect } from '@/ui/relation-picker/types/EntityForSelect';
import { Entity } from '@/ui/relation-picker/types/EntityTypeForSelect';
import {
Activity,
User,
useSearchUserQuery,
useUpdateActivityMutation,
} from '~/generated/graphql';
export type OwnProps = {
activity: Pick<Activity, 'id'> & {
accountOwner?: Pick<User, 'id' | 'displayName'> | 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 (
<SingleEntitySelect
onEntitySelected={handleEntitySelected}
onCancel={onCancel}
entities={{
loading: companies.loading,
entitiesToSelect: companies.entitiesToSelect,
selectedEntity: companies.selectedEntities[0],
}}
/>
);
}

View File

@ -35,8 +35,12 @@ export function ActivityBodyEditor({ activity, onChange }: OwnProps) {
setBody(activityBody); setBody(activityBody);
updateActivityMutation({ updateActivityMutation({
variables: { variables: {
id: activity.id, where: {
body: activityBody, id: activity.id,
},
data: {
body: activityBody,
},
}, },
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
}); });

View File

@ -4,20 +4,23 @@ import styled from '@emotion/styled';
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor'; import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
import { ActivityComments } from '@/activities/components/ActivityComments'; import { ActivityComments } from '@/activities/components/ActivityComments';
import { ActivityRelationPicker } from '@/activities/components/ActivityRelationPicker';
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown'; import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries'; import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox'; 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 { useIsMobile } from '@/ui/hooks/useIsMobile';
import { IconArrowUpRight } from '@/ui/icon/index'; import { IconCalendar } from '@/ui/icon/index';
import { import {
Activity, Activity,
ActivityTarget, ActivityTarget,
ActivityType,
User,
useUpdateActivityMutation, useUpdateActivityMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { debounce } from '~/utils/debounce'; 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 { ActivityActionBar } from '../right-drawer/components/ActivityActionBar';
import { CommentForDrawer } from '../types/CommentForDrawer'; import { CommentForDrawer } from '../types/CommentForDrawer';
@ -65,8 +68,16 @@ const StyledTopActionsContainer = styled.div`
`; `;
type OwnProps = { type OwnProps = {
activity: Pick<Activity, 'id' | 'title' | 'body' | 'type' | 'completedAt'> & { activity: Pick<
Activity,
'id' | 'title' | 'body' | 'type' | 'completedAt' | 'dueAt'
> & {
comments?: Array<CommentForDrawer> | null; comments?: Array<CommentForDrawer> | null;
} & {
assignee?: Pick<
User,
'id' | 'firstName' | 'lastName' | 'displayName'
> | null;
} & { } & {
activityTargets?: Array< activityTargets?: Array<
Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'> Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
@ -95,8 +106,12 @@ export function ActivityEditor({
(newTitle: string) => { (newTitle: string) => {
updateActivityMutation({ updateActivityMutation({
variables: { variables: {
id: activity.id, where: {
title: newTitle ?? '', id: activity.id,
},
data: {
title: newTitle ?? '',
},
}, },
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
}); });
@ -108,8 +123,12 @@ export function ActivityEditor({
(value: boolean) => { (value: boolean) => {
updateActivityMutation({ updateActivityMutation({
variables: { variables: {
id: activity.id, where: {
completedAt: value ? new Date().toISOString() : null, id: activity.id,
},
data: {
completedAt: value ? new Date().toISOString() : null,
},
}, },
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
}); });
@ -152,18 +171,29 @@ export function ActivityEditor({
onCompletionChange={handleActivityCompletionChange} onCompletionChange={handleActivityCompletionChange}
/> />
<PropertyBox> <PropertyBox>
<PropertyBoxItem {activity.type === ActivityType.Task && (
icon={<IconArrowUpRight />} <>
value={ <DateEditableField
<ActivityRelationPicker value={activity.dueAt}
activity={{ icon={<IconCalendar />}
id: activity.id, label="Due date"
activityTargets: activity.activityTargets ?? [], onSubmit={(newDate) => {
updateActivityMutation({
variables: {
where: {
id: activity.id,
},
data: {
dueAt: newDate,
},
},
});
}} }}
/> />
} <ActivityAssigneeEditableField activity={activity} />
label="Relations" </>
/> )}
<ActivityRelationEditableField activity={activity} />
</PropertyBox> </PropertyBox>
</StyledTopContainer> </StyledTopContainer>
<ActivityBodyEditor <ActivityBodyEditor

View File

@ -0,0 +1,47 @@
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { IconUserCircle } from '@/ui/icon';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope';
import { UserChip } from '@/users/components/UserChip';
import { Company, User } from '~/generated/graphql';
import { ActivityAssigneeEditableFieldEditMode } from './ActivityAssigneeEditableFieldEditMode';
type OwnProps = {
activity: Pick<Company, 'id' | 'accountOwnerId'> & {
assignee?: Pick<User, 'id' | 'displayName' | 'avatarUrl'> | null;
};
};
export function ActivityAssigneeEditableField({ activity }: OwnProps) {
return (
<RecoilScope SpecificContext={FieldContext}>
<RecoilScope>
<EditableField
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
label="Assignee"
iconLabel={<IconUserCircle />}
editModeContent={
<ActivityAssigneeEditableFieldEditMode activity={activity} />
}
displayModeContent={
activity.assignee?.displayName ? (
<UserChip
id={activity.assignee.id}
name={activity.assignee?.displayName ?? ''}
pictureUrl={activity.assignee?.avatarUrl ?? ''}
/>
) : (
<></>
)
}
isDisplayModeContentEmpty={!activity.assignee}
isDisplayModeFixHeight={true}
/>
</RecoilScope>
</RecoilScope>
);
}

View File

@ -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<Activity, 'id'> & {
assignee?: Pick<User, 'id' | 'displayName'> | null;
};
onSubmit?: () => void;
onCancel?: () => void;
};
export function ActivityAssigneeEditableFieldEditMode({
activity,
onSubmit,
onCancel,
}: OwnProps) {
const { closeEditableField } = useEditableField();
function handleSubmit() {
closeEditableField();
onSubmit?.();
}
function handleCancel() {
closeEditableField();
onCancel?.();
}
return (
<StyledContainer>
<ActivityAssigneePicker
activity={activity}
onCancel={handleCancel}
onSubmit={handleSubmit}
/>
</StyledContainer>
);
}

View File

@ -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<Activity, 'id'> & {
activityTargets?: Array<
Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
> | 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 (
<RecoilScope SpecificContext={FieldContext}>
<RecoilScope>
<EditableField
useEditButton
customEditHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker,
}}
iconLabel={<IconArrowUpRight />}
editModeContent={
<ActivityRelationEditableFieldEditMode activity={activity} />
}
label="Relations"
displayModeContent={
<StyledDisplayModeContainer>
{targetCompanies?.companies &&
targetCompanies.companies.map((company) => (
<CompanyChip
key={company.id}
id={company.id}
name={company.name}
pictureUrl={getLogoUrlFromDomainName(company.domainName)}
/>
))}
{targetPeople?.people &&
targetPeople.people.map((person) => (
<PersonChip
key={person.id}
id={person.id}
name={person.displayName}
pictureUrl={person.avatarUrl ?? ''}
/>
))}
</StyledDisplayModeContainer>
}
/>
</RecoilScope>
</RecoilScope>
);
}

View File

@ -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<Activity, 'id'> & {
activityTargets?: Array<
Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
> | 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<string, boolean>
>((result, entityId) => ({ ...result, [entityId]: true }), {}),
[initialPeopleIds, initialCompanyIds],
);
const [selectedEntityIds, setSelectedEntityIds] = useState<
Record<string, boolean>
>(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 (
<StyledSelectContainer>
<MultipleEntitySelect
entities={{
entitiesToSelect,
filteredSelectedEntities,
selectedEntities,
loading: false,
}}
onChange={setSelectedEntityIds}
onSearchFilterChange={setSearchFilter}
searchFilter={searchFilter}
value={selectedEntityIds}
onCancel={handleCancel}
onSubmit={handleSubmit}
/>
</StyledSelectContainer>
);
}

View File

@ -10,16 +10,16 @@ import {
useRemoveActivityTargetsOnActivityMutation, useRemoveActivityTargetsOnActivityMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { GET_ACTIVITIES_BY_TARGETS } from '../queries'; import { GET_ACTIVITY } from '../queries';
import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect'; import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect';
export function useHandleCheckableActivityTargetChange({ export function useHandleCheckableActivityTargetChange({
activity, activity,
}: { }: {
activity?: Pick<Activity, 'id'> & { activity?: Pick<Activity, 'id'> & {
activityTargets: Array< activityTargets?: Array<
Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'> Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
>; > | null;
}; };
}) { }) {
const [addActivityTargetsOnActivity] = const [addActivityTargetsOnActivity] =
@ -27,7 +27,7 @@ export function useHandleCheckableActivityTargetChange({
refetchQueries: [ refetchQueries: [
getOperationName(GET_COMPANIES) ?? '', getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '', getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '', getOperationName(GET_ACTIVITY) ?? '',
], ],
}); });
@ -36,7 +36,7 @@ export function useHandleCheckableActivityTargetChange({
refetchQueries: [ refetchQueries: [
getOperationName(GET_COMPANIES) ?? '', getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '', getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '', getOperationName(GET_ACTIVITY) ?? '',
], ],
}); });
@ -48,9 +48,9 @@ export function useHandleCheckableActivityTargetChange({
return; return;
} }
const currentEntityIds = activity.activityTargets.map( const currentEntityIds = activity.activityTargets
({ commentableId }) => commentableId, ? activity.activityTargets.map(({ commentableId }) => commentableId)
); : [];
const entitiesToAdd = entities.filter( const entitiesToAdd = entities.filter(
({ id }) => entityValues[id] && !currentEntityIds.includes(id), ({ id }) => entityValues[id] && !currentEntityIds.includes(id),
@ -70,10 +70,13 @@ export function useHandleCheckableActivityTargetChange({
}); });
const activityTargetIdsToDelete = activity.activityTargets const activityTargetIdsToDelete = activity.activityTargets
.filter( ? activity.activityTargets
({ commentableId }) => commentableId && !entityValues[commentableId], .filter(
) ({ commentableId }) =>
.map(({ id }) => id); commentableId && !entityValues[commentableId],
)
.map(({ id }) => id)
: [];
if (activityTargetIdsToDelete.length) if (activityTargetIdsToDelete.length)
await removeActivityTargetsOnActivity({ await removeActivityTargetsOnActivity({

View File

@ -17,6 +17,13 @@ export const GET_ACTIVITIES_BY_TARGETS = gql`
body body
type type
completedAt completedAt
dueAt
assignee {
id
firstName
lastName
displayName
}
author { author {
id id
firstName firstName
@ -54,6 +61,13 @@ export const GET_ACTIVITY = gql`
title title
type type
completedAt completedAt
dueAt
assignee {
id
firstName
lastName
displayName
}
author { author {
id id
firstName firstName

View File

@ -58,26 +58,22 @@ export const DELETE_ACTIVITY = gql`
export const UPDATE_ACTIVITY = gql` export const UPDATE_ACTIVITY = gql`
mutation UpdateActivity( mutation UpdateActivity(
$id: String! $where: ActivityWhereUniqueInput!
$body: String $data: ActivityUpdateInput!
$title: String
$type: ActivityType
$completedAt: DateTime
) { ) {
updateOneActivity( updateOneActivity(where: $where, data: $data) {
where: { id: $id }
data: {
body: $body
title: $title
type: $type
completedAt: $completedAt
}
) {
id id
body body
title title
type type
completedAt completedAt
dueAt
assignee {
id
firstName
lastName
displayName
}
} }
} }
`; `;

View File

@ -21,7 +21,7 @@ type OwnProps = {
autoFillTitle?: boolean; autoFillTitle?: boolean;
}; };
export function Activity({ export function RightDrawerActivity({
activityId, activityId,
showComment = true, showComment = true,
autoFillTitle = false, autoFillTitle = false,

View File

@ -5,7 +5,7 @@ import { RightDrawerBody } from '@/ui/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/right-drawer/components/RightDrawerPage'; import { RightDrawerPage } from '@/ui/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar'; import { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar';
import { Activity } from '../Activity'; import { RightDrawerActivity } from '../RightDrawerActivity';
export function RightDrawerCreateActivity() { export function RightDrawerCreateActivity() {
const activityId = useRecoilValue(viewableActivityIdState); const activityId = useRecoilValue(viewableActivityIdState);
@ -15,7 +15,7 @@ export function RightDrawerCreateActivity() {
<RightDrawerTopBar /> <RightDrawerTopBar />
<RightDrawerBody> <RightDrawerBody>
{activityId && ( {activityId && (
<Activity <RightDrawerActivity
activityId={activityId} activityId={activityId}
showComment={false} showComment={false}
autoFillTitle={true} autoFillTitle={true}

View File

@ -5,7 +5,7 @@ import { RightDrawerBody } from '@/ui/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/right-drawer/components/RightDrawerPage'; import { RightDrawerPage } from '@/ui/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar'; import { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar';
import { Activity } from '../Activity'; import { RightDrawerActivity } from '../RightDrawerActivity';
export function RightDrawerEditActivity() { export function RightDrawerEditActivity() {
const activityId = useRecoilValue(viewableActivityIdState); const activityId = useRecoilValue(viewableActivityIdState);
@ -14,7 +14,7 @@ export function RightDrawerEditActivity() {
<RightDrawerPage> <RightDrawerPage>
<RightDrawerTopBar /> <RightDrawerTopBar />
<RightDrawerBody> <RightDrawerBody>
{activityId && <Activity activityId={activityId} />} {activityId && <RightDrawerActivity activityId={activityId} />}
</RightDrawerBody> </RightDrawerBody>
</RightDrawerPage> </RightDrawerPage>
); );

View File

@ -7,12 +7,13 @@ import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRi
import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries'; import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries';
import { IconNotes } from '@/ui/icon'; import { IconNotes } from '@/ui/icon';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import { Activity, useUpdateActivityMutation } from '~/generated/graphql'; import { Activity, User, useUpdateActivityMutation } from '~/generated/graphql';
import { import {
beautifyExactDate, beautifyExactDate,
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
} from '~/utils/date-utils'; } from '~/utils/date-utils';
import { TimelineActivityCardFooter } from './TimelineActivityCardFooter';
import { TimelineActivityTitle } from './TimelineActivityTitle'; import { TimelineActivityTitle } from './TimelineActivityTitle';
const StyledIconContainer = styled.div` const StyledIconContainer = styled.div`
@ -73,19 +74,19 @@ const StyledCard = styled.div`
align-items: flex-start; align-items: flex-start;
background: ${({ theme }) => theme.background.secondary}; background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.md};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: ${({ theme }) => theme.spacing(3)}; gap: ${({ theme }) => theme.spacing(3)};
max-width: 400px; max-width: 100%;
padding: ${({ theme }) => theme.spacing(3)};
position: relative; position: relative;
width: 400px;
`; `;
const StyledCardContent = styled.div` const StyledCardContent = styled.div`
align-self: stretch; align-self: stretch;
color: ${({ theme }) => theme.font.color.secondary}; color: ${({ theme }) => theme.font.color.secondary};
margin-top: ${({ theme }) => theme.spacing(2)};
width: 100%; width: 100%;
`; `;
@ -104,6 +105,11 @@ const StyledTooltip = styled(Tooltip)`
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledCardDetailsContainer = styled.div`
padding: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledTimelineItemContainer = styled.div` const StyledTimelineItemContainer = styled.div`
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;
@ -115,7 +121,9 @@ type OwnProps = {
activity: Pick< activity: Pick<
Activity, Activity,
'id' | 'title' | 'body' | 'createdAt' | 'completedAt' | 'type' 'id' | 'title' | 'body' | 'createdAt' | 'completedAt' | 'type'
> & { author: Pick<Activity['author'], 'displayName'> }; > & { author: Pick<Activity['author'], 'displayName'> } & {
assignee?: Pick<User, 'id' | 'displayName'> | null;
};
}; };
export function TimelineActivity({ activity }: OwnProps) { export function TimelineActivity({ activity }: OwnProps) {
@ -130,8 +138,10 @@ export function TimelineActivity({ activity }: OwnProps) {
(value: boolean) => { (value: boolean) => {
updateActivityMutation({ updateActivityMutation({
variables: { variables: {
id: activity.id, where: { id: activity.id },
completedAt: value ? new Date().toISOString() : null, data: {
completedAt: value ? new Date().toISOString() : null,
},
}, },
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''], refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
}); });
@ -165,15 +175,20 @@ export function TimelineActivity({ activity }: OwnProps) {
</StyledVerticalLineContainer> </StyledVerticalLineContainer>
<StyledCardContainer> <StyledCardContainer>
<StyledCard onClick={() => openActivityRightDrawer(activity.id)}> <StyledCard onClick={() => openActivityRightDrawer(activity.id)}>
<TimelineActivityTitle <StyledCardDetailsContainer>
title={activity.title ?? ''} <TimelineActivityTitle
completed={!!activity.completedAt} title={activity.title ?? ''}
type={activity.type} completed={!!activity.completedAt}
onCompletionChange={handleActivityCompletionChange} type={activity.type}
/> onCompletionChange={handleActivityCompletionChange}
<StyledCardContent> />
<OverflowingTextWithTooltip text={body ? body : '(No content)'} /> <StyledCardContent>
</StyledCardContent> <OverflowingTextWithTooltip
text={body ? body : '(No content)'}
/>
</StyledCardContent>
</StyledCardDetailsContainer>
<TimelineActivityCardFooter activity={activity} />
</StyledCard> </StyledCard>
</StyledCardContainer> </StyledCardContainer>
</StyledTimelineItemContainer> </StyledTimelineItemContainer>

View File

@ -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<Activity, 'id' | 'dueAt'> & {
assignee?: Pick<User, 'id' | 'displayName' | 'avatarUrl'> | 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) && (
<StyledContainer>
{activity.assignee && (
<UserChip
id={activity.assignee.id}
name={activity.assignee.displayName ?? ''}
pictureUrl={activity.assignee.avatarUrl ?? ''}
/>
)}
{activity.dueAt && (
<>
{activity.assignee && <StyledVerticalSeparator />}
{beautifyExactDate(activity.dueAt)}
</>
)}
</StyledContainer>
)}
</>
);
}

View File

@ -12,15 +12,19 @@ const StyledTitleContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
line-height: ${({ theme }) => theme.text.lineHeight.lg}; 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%; 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; align-items: center;
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -55,7 +59,10 @@ export function TimelineActivityTitle({
/> />
</StyledCheckboxContainer> </StyledCheckboxContainer>
)} )}
<StyledTitleText completed={completed}> <StyledTitleText
completed={completed}
hasCheckbox={type === ActivityType.Task}
>
<OverflowingTextWithTooltip text={title ? title : '(No title)'} /> <OverflowingTextWithTooltip text={title ? title : '(No title)'} />
</StyledTitleText> </StyledTitleText>
</StyledTitleContainer> </StyledTitleContainer>

View File

@ -99,7 +99,9 @@ const StyledCheckboxContainer = styled.div`
`; `;
const StyledFieldContainer = styled.div` const StyledFieldContainer = styled.div`
width: max-content; display: flex;
flex-direction: row;
width: 100%;
`; `;
export function CompanyBoardCard() { export function CompanyBoardCard() {

View File

@ -1,4 +1,5 @@
import { import {
IconBrandLinkedin,
IconBuildingSkyscraper, IconBuildingSkyscraper,
IconCalendarEvent, IconCalendarEvent,
IconLink, IconLink,
@ -81,7 +82,7 @@ export const companyViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
{ {
id: 'linkedin', id: 'linkedin',
columnLabel: 'LinkedIn', columnLabel: 'LinkedIn',
columnIcon: <IconMap />, columnIcon: <IconBrandLinkedin />,
columnSize: 170, columnSize: 170,
columnOrder: 6, columnOrder: 6,
metadata: { metadata: {

View File

@ -38,6 +38,7 @@ export function CompanyAccountOwnerEditableField({ company }: OwnProps) {
) )
} }
isDisplayModeContentEmpty={!company.accountOwner} isDisplayModeContentEmpty={!company.accountOwner}
isDisplayModeFixHeight={true}
/> />
</RecoilScope> </RecoilScope>
</RecoilScope> </RecoilScope>

View File

@ -4,8 +4,8 @@ import { CompanyAccountOwnerPicker } from '@/companies/components/CompanyAccount
import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; import { useEditableField } from '@/ui/editable-field/hooks/useEditableField';
import { Company, User } from '~/generated/graphql'; import { Company, User } from '~/generated/graphql';
const CompanyAccountOwnerPickerContainer = styled.div` const StyledContainer = styled.div`
left: 24px; left: 0px;
position: absolute; position: absolute;
top: -8px; top: -8px;
`; `;
@ -36,12 +36,12 @@ export function CompanyAccountOwnerPickerFieldEditMode({
} }
return ( return (
<CompanyAccountOwnerPickerContainer> <StyledContainer>
<CompanyAccountOwnerPicker <CompanyAccountOwnerPicker
company={company} company={company}
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={handleSubmit} onSubmit={handleSubmit}
/> />
</CompanyAccountOwnerPickerContainer> </StyledContainer>
); );
} }

View File

@ -1,8 +1,7 @@
import { useEffect, useState } from 'react'; 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 { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql'; import { Company, useUpdateOneCompanyMutation } from '~/generated/graphql';
@ -10,6 +9,30 @@ type OwnProps = {
company: Pick<Company, 'id' | 'name'>; company: Pick<Company, 'id' | 'name'>;
}; };
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) { export function CompanyNameEditableField({ company }: OwnProps) {
const [internalValue, setInternalValue] = useState(company.name); const [internalValue, setInternalValue] = useState(company.name);
@ -36,27 +59,14 @@ export function CompanyNameEditableField({ company }: OwnProps) {
}); });
} }
async function handleCancel() {
setInternalValue(company.name);
}
return ( return (
<RecoilScope SpecificContext={FieldContext}> <RecoilScope SpecificContext={FieldContext}>
<EditableField <StyledEditableTitleInput
onSubmit={handleSubmit} autoComplete="off"
onCancel={handleCancel} autoFocus
editModeContent={ onChange={(event) => handleChange(event.target.value)}
<InplaceInputText onBlur={handleSubmit}
placeholder={'Name'} value={internalValue}
autoFocus
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={internalValue ?? ''}
isDisplayModeContentEmpty={!(internalValue !== '')}
/> />
</RecoilScope> </RecoilScope>
); );

View File

@ -33,6 +33,7 @@ export const peopleViewFields: ViewFieldDefinition<ViewFieldMetadata>[] = [
secondValueFieldName: 'lastName', secondValueFieldName: 'lastName',
firstValuePlaceholder: 'First name', firstValuePlaceholder: 'First name',
secondValuePlaceholder: 'Last name', secondValuePlaceholder: 'Last name',
avatarUrlFieldName: 'avatarUrl',
entityType: Entity.Person, entityType: Entity.Person,
}, },
} satisfies ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>, } satisfies ViewFieldDefinition<ViewFieldDoubleTextChipMetadata>,

View File

@ -41,6 +41,7 @@ export function PeopleCompanyEditableField({ people }: OwnProps) {
) )
} }
isDisplayModeContentEmpty={!people.company} isDisplayModeContentEmpty={!people.company}
isDisplayModeFixHeight
/> />
</RecoilScope> </RecoilScope>
</RecoilScope> </RecoilScope>

View File

@ -1,3 +1,5 @@
import styled from '@emotion/styled';
import { useFilteredSearchCompanyQuery } from '@/companies/queries'; import { useFilteredSearchCompanyQuery } from '@/companies/queries';
import { useEditableField } from '@/ui/editable-field/hooks/useEditableField'; import { useEditableField } from '@/ui/editable-field/hooks/useEditableField';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
@ -14,6 +16,12 @@ export type OwnProps = {
people: Pick<Person, 'id'> & { company?: Pick<Company, 'id'> | null }; people: Pick<Person, 'id'> & { company?: Pick<Company, 'id'> | null };
}; };
const StyledContainer = styled.div`
left: 0px;
position: absolute;
top: -8px;
`;
export function PeopleCompanyEditableFieldEditMode({ people }: OwnProps) { export function PeopleCompanyEditableFieldEditMode({ people }: OwnProps) {
const { closeEditableField } = useEditableField(); const { closeEditableField } = useEditableField();
@ -51,14 +59,16 @@ export function PeopleCompanyEditableFieldEditMode({ people }: OwnProps) {
} }
return ( return (
<SingleEntitySelect <StyledContainer>
onEntitySelected={handleEntitySelected} <SingleEntitySelect
entities={{ onEntitySelected={handleEntitySelected}
entitiesToSelect: companies.entitiesToSelect, entities={{
selectedEntity: companies.selectedEntities[0], entitiesToSelect: companies.entitiesToSelect,
loading: companies.loading, selectedEntity: companies.selectedEntities[0],
}} loading: companies.loading,
onCancel={handleCancel} }}
/> onCancel={handleCancel}
/>
</StyledContainer>
); );
} }

View File

@ -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 { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { Person, useUpdateOnePersonMutation } from '~/generated/graphql'; import { Person, useUpdateOnePersonMutation } from '~/generated/graphql';
@ -21,56 +20,40 @@ export function PeopleFullNameEditableField({ people }: OwnProps) {
const [updatePeople] = useUpdateOnePersonMutation(); const [updatePeople] = useUpdateOnePersonMutation();
useEffect(() => {
setInternalValueFirstName(people.firstName);
setInternalValueLastName(people.lastName);
}, [people.firstName, people.lastName]);
async function handleChange( async function handleChange(
newValueFirstName: string, newValueFirstName: string,
newValueLastName: string, newValueLastName: string,
) { ) {
setInternalValueFirstName(newValueFirstName); setInternalValueFirstName(newValueFirstName);
setInternalValueLastName(newValueLastName); setInternalValueLastName(newValueLastName);
handleSubmit(newValueFirstName, newValueLastName);
} }
async function handleSubmit() { async function handleSubmit(
newValueFirstName: string,
newValueLastName: string,
) {
await updatePeople({ await updatePeople({
variables: { variables: {
where: { where: {
id: people.id, id: people.id,
}, },
data: { data: {
firstName: internalValueFirstName ?? '', firstName: newValueFirstName ?? '',
lastName: internalValueLastName ?? '', lastName: newValueLastName ?? '',
}, },
}, },
}); });
} }
async function handleCancel() {
setInternalValueFirstName(people.firstName);
setInternalValueLastName(people.lastName);
}
return ( return (
<RecoilScope SpecificContext={FieldContext}> <RecoilScope SpecificContext={FieldContext}>
<EditableField <InplaceInputDoubleText
onSubmit={handleSubmit} firstValuePlaceholder={'First name'}
onCancel={handleCancel} secondValuePlaceholder={'Last name'}
editModeContent={ firstValue={internalValueFirstName ?? ''}
<InplaceInputDoubleText secondValue={internalValueLastName ?? ''}
firstValuePlaceholder={'First name'} onChange={handleChange}
secondValuePlaceholder={'Last name'}
firstValue={internalValueFirstName ?? ''}
secondValue={internalValueLastName ?? ''}
onChange={handleChange}
/>
}
displayModeContent={`${internalValueFirstName} ${internalValueLastName}`}
isDisplayModeContentEmpty={
!(internalValueFirstName !== '') && !(internalValueLastName !== '')
}
/> />
</RecoilScope> </RecoilScope>
); );

View File

@ -45,6 +45,7 @@ export function PipelineProgressPointOfContactEditableField({
) )
} }
isDisplayModeContentEmpty={!pipelineProgress.pointOfContact} isDisplayModeContentEmpty={!pipelineProgress.pointOfContact}
isDisplayModeFixHeight
/> />
</RecoilScope> </RecoilScope>
</RecoilScope> </RecoilScope>

View File

@ -5,7 +5,7 @@ import { useEditableField } from '@/ui/editable-field/hooks/useEditableField';
import { Person, PipelineProgress } from '~/generated/graphql'; import { Person, PipelineProgress } from '~/generated/graphql';
const PipelineProgressPointOfContactPickerContainer = styled.div` const PipelineProgressPointOfContactPickerContainer = styled.div`
left: 24px; left: 0px;
position: absolute; position: absolute;
top: -8px; top: -8px;
`; `;

View File

@ -77,7 +77,14 @@ export function DeleteModal({
return ( return (
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
<LayoutGroup> <LayoutGroup>
<StyledModal isOpen={isOpen} onOutsideClick={() => setIsOpen(!isOpen)}> <StyledModal
isOpen={isOpen}
onOutsideClick={() => {
if (isOpen) {
setIsOpen(false);
}
}}
>
<StyledTitle>{title}</StyledTitle> <StyledTitle>{title}</StyledTitle>
<StyledSubtitle>{subtitle}</StyledSubtitle> <StyledSubtitle>{subtitle}</StyledSubtitle>
<TextInput <TextInput

View File

@ -15,11 +15,11 @@ const StyledRawLink = styled(RawLink)`
`; `;
type OwnProps = { type OwnProps = {
value: string; value: string | null;
}; };
export function InplaceInputPhoneDisplayMode({ value }: OwnProps) { export function InplaceInputPhoneDisplayMode({ value }: OwnProps) {
return isValidPhoneNumber(value) ? ( return value && isValidPhoneNumber(value) ? (
<StyledRawLink <StyledRawLink
href={parsePhoneNumber(value, 'FR')?.getURI()} href={parsePhoneNumber(value, 'FR')?.getURI()}
onClick={(event: MouseEvent<HTMLElement>) => { onClick={(event: MouseEvent<HTMLElement>) => {

View File

@ -15,6 +15,7 @@ const StyledIconContainer = styled.div`
align-items: center; align-items: center;
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
display: flex; display: flex;
width: 16px;
svg { svg {
align-items: center; align-items: center;
@ -32,6 +33,12 @@ const StyledLabelAndIconContainer = styled.div`
gap: ${({ theme }) => theme.spacing(1)}; 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<Pick<OwnProps, 'labelFixedWidth'>>` const StyledLabel = styled.div<Pick<OwnProps, 'labelFixedWidth'>>`
align-items: center; align-items: center;
@ -39,18 +46,22 @@ const StyledLabel = styled.div<Pick<OwnProps, 'labelFixedWidth'>>`
labelFixedWidth ? `${labelFixedWidth}px` : 'fit-content'}; labelFixedWidth ? `${labelFixedWidth}px` : 'fit-content'};
`; `;
const StyledEditButtonContainer = styled(motion.div)`
position: absolute;
right: 0;
`;
export const EditableFieldBaseContainer = styled.div` export const EditableFieldBaseContainer = styled.div`
align-items: center; align-items: center;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)}; 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%; width: 100%;
`; `;
@ -66,6 +77,7 @@ type OwnProps = {
parentHotkeyScope?: HotkeyScope; parentHotkeyScope?: HotkeyScope;
customEditHotkeyScope?: HotkeyScope; customEditHotkeyScope?: HotkeyScope;
isDisplayModeContentEmpty?: boolean; isDisplayModeContentEmpty?: boolean;
isDisplayModeFixHeight?: boolean;
onSubmit?: () => void; onSubmit?: () => void;
onCancel?: () => void; onCancel?: () => void;
}; };
@ -82,6 +94,7 @@ export function EditableField({
disableHoverEffect, disableHoverEffect,
isDisplayModeContentEmpty, isDisplayModeContentEmpty,
displayModeContentOnly, displayModeContentOnly,
isDisplayModeFixHeight,
onSubmit, onSubmit,
onCancel, onCancel,
}: OwnProps) { }: OwnProps) {
@ -119,29 +132,32 @@ export function EditableField({
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel> <StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
)} )}
</StyledLabelAndIconContainer> </StyledLabelAndIconContainer>
{isFieldInEditMode && !displayModeContentOnly ? ( <StyledValueContainer>
<EditableFieldEditMode onSubmit={onSubmit} onCancel={onCancel}> {isFieldInEditMode && !displayModeContentOnly ? (
{editModeContent} <EditableFieldEditMode onSubmit={onSubmit} onCancel={onCancel}>
</EditableFieldEditMode> {editModeContent}
) : ( </EditableFieldEditMode>
<EditableFieldDisplayMode ) : (
disableHoverEffect={disableHoverEffect} <EditableFieldDisplayMode
disableClick={useEditButton} disableHoverEffect={disableHoverEffect}
onClick={handleDisplayModeClick} disableClick={useEditButton}
isDisplayModeContentEmpty={isDisplayModeContentEmpty} onClick={handleDisplayModeClick}
> isDisplayModeContentEmpty={isDisplayModeContentEmpty}
{displayModeContent} isDisplayModeFixHeight={isDisplayModeFixHeight}
</EditableFieldDisplayMode> >
)} {displayModeContent}
</EditableFieldDisplayMode>
)}
</StyledValueContainer>
{showEditButton && ( {showEditButton && (
<motion.div <StyledEditButtonContainer
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.1 }} transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }} whileHover={{ scale: 1.04 }}
> >
<EditableFieldEditButton /> <EditableFieldEditButton />
</motion.div> </StyledEditButtonContainer>
)} )}
</EditableFieldBaseContainer> </EditableFieldBaseContainer>
); );

View File

@ -4,15 +4,18 @@ import styled from '@emotion/styled';
export const EditableFieldNormalModeOuterContainer = styled.div< export const EditableFieldNormalModeOuterContainer = styled.div<
Pick< Pick<
OwnProps, OwnProps,
'disableClick' | 'isDisplayModeContentEmpty' | 'disableHoverEffect' | 'disableClick'
| 'isDisplayModeContentEmpty'
| 'disableHoverEffect'
| 'isDisplayModeFixHeight'
> >
>` >`
align-items: center; align-items: center;
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex; display: flex;
height: 100%; height: ${({ isDisplayModeFixHeight }) =>
isDisplayModeFixHeight ? '16px' : 'auto'};
height: 16px; min-height: 16px;
overflow: hidden; overflow: hidden;
@ -67,6 +70,7 @@ type OwnProps = {
onClick?: () => void; onClick?: () => void;
isDisplayModeContentEmpty?: boolean; isDisplayModeContentEmpty?: boolean;
disableHoverEffect?: boolean; disableHoverEffect?: boolean;
isDisplayModeFixHeight?: boolean;
}; };
export function EditableFieldDisplayMode({ export function EditableFieldDisplayMode({
@ -75,6 +79,7 @@ export function EditableFieldDisplayMode({
onClick, onClick,
isDisplayModeContentEmpty, isDisplayModeContentEmpty,
disableHoverEffect, disableHoverEffect,
isDisplayModeFixHeight,
}: React.PropsWithChildren<OwnProps>) { }: React.PropsWithChildren<OwnProps>) {
return ( return (
<EditableFieldNormalModeOuterContainer <EditableFieldNormalModeOuterContainer
@ -82,6 +87,7 @@ export function EditableFieldDisplayMode({
disableClick={disableClick} disableClick={disableClick}
isDisplayModeContentEmpty={isDisplayModeContentEmpty} isDisplayModeContentEmpty={isDisplayModeContentEmpty}
disableHoverEffect={disableHoverEffect} disableHoverEffect={disableHoverEffect}
isDisplayModeFixHeight={isDisplayModeFixHeight}
> >
<EditableFieldNormalModeInnerContainer> <EditableFieldNormalModeInnerContainer>
{children} {children}

View File

@ -7,10 +7,12 @@ export const EditableFieldEditModeContainer = styled.div<OwnProps>`
align-items: center; align-items: center;
display: flex; display: flex;
height: 24px;
margin-left: -${({ theme }) => theme.spacing(1)}; margin-left: -${({ theme }) => theme.spacing(1)};
position: relative;
width: inherit; width: 100%;
z-index: 10; z-index: 10;
`; `;

View File

@ -7,7 +7,7 @@ const StyledPropertyBoxContainer = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: ${({ theme }) => theme.spacing(3)}; gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(3)}; padding: ${({ theme }) => theme.spacing(3)};
`; `;

View File

@ -10,11 +10,12 @@ import { EditableFieldEditModeDate } from './EditableFieldEditModeDate';
type OwnProps = { type OwnProps = {
icon?: React.ReactNode; icon?: React.ReactNode;
label?: string;
value: string | null | undefined; value: string | null | undefined;
onSubmit?: (newValue: string) => void; onSubmit?: (newValue: string) => void;
}; };
export function DateEditableField({ icon, value, onSubmit }: OwnProps) { export function DateEditableField({ icon, value, label, onSubmit }: OwnProps) {
const [internalValue, setInternalValue] = useState(value); const [internalValue, setInternalValue] = useState(value);
useEffect(() => { useEffect(() => {
@ -47,6 +48,7 @@ export function DateEditableField({ icon, value, onSubmit }: OwnProps) {
onSubmit={handleSubmit} onSubmit={handleSubmit}
onCancel={handleCancel} onCancel={handleCancel}
iconLabel={icon} iconLabel={icon}
label={label}
editModeContent={ editModeContent={
<EditableFieldEditModeDate <EditableFieldEditModeDate
value={internalValue || new Date().toISOString()} value={internalValue || new Date().toISOString()}

View File

@ -14,12 +14,14 @@ type OwnProps = {
export const StyledDoubleTextContainer = styled.div` export const StyledDoubleTextContainer = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: space-between; justify-content: center;
text-align: center;
`;
& > input:last-child { const StyledNameInput = styled(StyledInput)`
border-left: 1px solid ${({ theme }) => theme.border.color.medium}; padding: 0;
padding-left: ${({ theme }) => theme.spacing(2)}; text-align: center;
} width: auto;
`; `;
export function InplaceInputDoubleText({ export function InplaceInputDoubleText({
@ -31,7 +33,8 @@ export function InplaceInputDoubleText({
}: OwnProps) { }: OwnProps) {
return ( return (
<StyledDoubleTextContainer> <StyledDoubleTextContainer>
<StyledInput <StyledNameInput
size={firstValue.length}
autoFocus autoFocus
placeholder={firstValuePlaceholder} placeholder={firstValuePlaceholder}
value={firstValue} value={firstValue}
@ -39,7 +42,8 @@ export function InplaceInputDoubleText({
onChange(event.target.value, secondValue); onChange(event.target.value, secondValue);
}} }}
/> />
<StyledInput <StyledNameInput
size={secondValue.length}
autoComplete="off" autoComplete="off"
placeholder={secondValuePlaceholder} placeholder={secondValuePlaceholder}
value={secondValue} value={secondValue}

View File

@ -43,9 +43,12 @@ const StyledDate = styled.div`
const StyledTitle = styled.div` const StyledTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.xl}; font-size: ${({ theme }) => theme.font.size.xl};
font-weight: ${({ theme }) => theme.font.weight.semiBold}; font-weight: ${({ theme }) => theme.font.weight.semiBold};
max-width: 100%; justify-content: center;
width: 100%;
`; `;
const StyledTooltip = styled(Tooltip)` const StyledTooltip = styled(Tooltip)`

View File

@ -1,3 +1,4 @@
import { useRef } from 'react';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu'; 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 { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch'; import { DropdownMenuSearch } from '@/ui/dropdown/components/DropdownMenuSearch';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import { useListenClickOutside } from '@/ui/hooks/useListenClickOutside';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
import { isNonEmptyString } from '~/utils/isNonEmptyString'; import { isNonEmptyString } from '~/utils/isNonEmptyString';
@ -25,6 +27,7 @@ export function MultipleEntitySelect<
>({ >({
entities, entities,
onChange, onChange,
onSubmit,
onSearchFilterChange, onSearchFilterChange,
searchFilter, searchFilter,
value, value,
@ -33,6 +36,8 @@ export function MultipleEntitySelect<
searchFilter: string; searchFilter: string;
onSearchFilterChange: (newSearchFilter: string) => void; onSearchFilterChange: (newSearchFilter: string) => void;
onChange: (value: Record<string, boolean>) => void; onChange: (value: Record<string, boolean>) => void;
onCancel?: () => void;
onSubmit?: () => void;
value: Record<string, boolean>; value: Record<string, boolean>;
}) { }) {
const debouncedSetSearchFilter = debounce(onSearchFilterChange, 100, { const debouncedSetSearchFilter = debounce(onSearchFilterChange, 100, {
@ -53,8 +58,21 @@ export function MultipleEntitySelect<
isNonEmptyString(entity.name), isNonEmptyString(entity.name),
); );
const containerRef = useRef<HTMLDivElement>(null);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
onSubmit?.();
},
});
return ( return (
<DropdownMenu> <DropdownMenu ref={containerRef}>
<DropdownMenuSearch <DropdownMenuSearch
value={searchFilter} value={searchFilter}
onChange={handleFilterChange} onChange={handleFilterChange}

View File

@ -33,6 +33,13 @@ export function GenericEditableDoubleTextChipCellDisplayMode({
}), }),
); );
const [avatarUrlValue] = useRecoilState<string>(
tableEntityFieldFamilySelector({
entityId: currentRowEntityId ?? '',
fieldName: viewField.metadata.avatarUrlFieldName,
}),
);
const displayName = `${firstValue} ${secondValue}`; const displayName = `${firstValue} ${secondValue}`;
switch (viewField.metadata.entityType) { switch (viewField.metadata.entityType) {
@ -40,7 +47,13 @@ export function GenericEditableDoubleTextChipCellDisplayMode({
return <CompanyChip id={currentRowEntityId ?? ''} name={displayName} />; return <CompanyChip id={currentRowEntityId ?? ''} name={displayName} />;
} }
case Entity.Person: { case Entity.Person: {
return <PersonChip id={currentRowEntityId ?? ''} name={displayName} />; return (
<PersonChip
id={currentRowEntityId ?? ''}
name={displayName}
pictureUrl={avatarUrlValue}
/>
);
} }
default: default:
console.warn( console.warn(

View File

@ -45,6 +45,7 @@ export function GenericEditableRelationCellDisplayMode({
<UserChip <UserChip
id={fieldValue?.id ?? ''} id={fieldValue?.id ?? ''}
name={fieldValue?.displayName ?? ''} name={fieldValue?.displayName ?? ''}
pictureUrl={fieldValue?.avatarUrl ?? ''}
/> />
); );
} }

View File

@ -68,6 +68,7 @@ export type ViewFieldDoubleTextChipMetadata = {
firstValuePlaceholder: string; firstValuePlaceholder: string;
secondValueFieldName: string; secondValueFieldName: string;
secondValuePlaceholder: string; secondValuePlaceholder: string;
avatarUrlFieldName: string;
entityType: Entity; entityType: Entity;
}; };

View File

@ -6,7 +6,7 @@ import { graphql } from 'msw';
import { GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY } from '@/activities/queries'; import { GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY } from '@/activities/queries';
import { CREATE_ACTIVITY_WITH_COMMENT } from '@/activities/queries/create'; 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 { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, 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]],
}),
);
},
),
], ],
}, },
}; };

View File

@ -7,6 +7,7 @@ import { GET_COMPANIES } from '@/companies/queries';
import { GET_PEOPLE, GET_PERSON, UPDATE_ONE_PERSON } from '@/people/queries'; import { GET_PEOPLE, GET_PERSON, UPDATE_ONE_PERSON } from '@/people/queries';
import { GET_PIPELINE_PROGRESS, GET_PIPELINES } from '@/pipeline/queries'; import { GET_PIPELINE_PROGRESS, GET_PIPELINES } from '@/pipeline/queries';
import { import {
SEARCH_ACTIVITY_QUERY,
SEARCH_COMPANY_QUERY, SEARCH_COMPANY_QUERY,
SEARCH_PEOPLE_QUERY, SEARCH_PEOPLE_QUERY,
SEARCH_USER_QUERY, SEARCH_USER_QUERY,
@ -16,11 +17,13 @@ import {
GetCompaniesQuery, GetCompaniesQuery,
GetPeopleQuery, GetPeopleQuery,
GetPersonQuery, GetPersonQuery,
SearchActivityQuery,
SearchCompanyQuery, SearchCompanyQuery,
SearchPeopleQuery, SearchPeopleQuery,
SearchUserQuery, SearchUserQuery,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { mockedActivities } from './mock-data/activities';
import { mockedCompaniesData } from './mock-data/companies'; import { mockedCompaniesData } from './mock-data/companies';
import { mockedPeopleData } from './mock-data/people'; import { mockedPeopleData } from './mock-data/people';
import { mockedPipelineProgressData } from './mock-data/pipeline-progress'; 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) => { graphql.query(getOperationName(GET_CURRENT_USER) ?? '', (req, res, ctx) => {
return res( return res(
ctx.data({ ctx.data({

View File

@ -16,6 +16,8 @@ type MockedActivity = Pick<
| 'body' | 'body'
| 'title' | 'title'
| 'authorId' | 'authorId'
| 'dueAt'
| 'completedAt'
> & { > & {
author: { author: {
__typename?: 'User' | undefined; __typename?: 'User' | undefined;
@ -24,6 +26,13 @@ type MockedActivity = Pick<
lastName: string; lastName: string;
displayName: string; displayName: string;
}; };
assignee: {
__typename?: 'User' | undefined;
id: string;
firstName: string;
lastName: string;
displayName: string;
};
comments: Array<Pick<Comment, 'body'>>; comments: Array<Pick<Comment, 'body'>>;
activityTargets: Array< activityTargets: Array<
Pick< Pick<
@ -47,12 +56,20 @@ export const mockedActivities: Array<MockedActivity> = [
title: 'My very first note', title: 'My very first note',
type: ActivityType.Note, type: ActivityType.Note,
body: null, body: null,
dueAt: null,
completedAt: null,
author: { author: {
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
firstName: 'Charles', firstName: 'Charles',
lastName: 'Test', lastName: 'Test',
displayName: 'Charles Test', displayName: 'Charles Test',
}, },
assignee: {
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
firstName: 'Charles',
lastName: 'Test',
displayName: 'Charles Test',
},
authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
comments: [], comments: [],
activityTargets: [ activityTargets: [
@ -94,12 +111,20 @@ export const mockedActivities: Array<MockedActivity> = [
title: 'Another note', title: 'Another note',
body: null, body: null,
type: ActivityType.Note, type: ActivityType.Note,
completedAt: null,
dueAt: null,
author: { author: {
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
firstName: 'Charles', firstName: 'Charles',
lastName: 'Test', lastName: 'Test',
displayName: 'Charles Test', displayName: 'Charles Test',
}, },
assignee: {
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
firstName: 'Charles',
lastName: 'Test',
displayName: 'Charles Test',
},
authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
comments: [], comments: [],
activityTargets: [ activityTargets: [

View File

@ -18,7 +18,7 @@ describe('beautifyExactDate', () => {
const actualDate = new Date(mockDate); const actualDate = new Date(mockDate);
const expected = DateTime.fromJSDate(actualDate) const expected = DateTime.fromJSDate(actualDate)
.setLocale(DEFAULT_DATE_LOCALE) .setLocale(DEFAULT_DATE_LOCALE)
.toFormat('DD · TT'); .toFormat('DD · T');
const result = beautifyExactDate(mockDate); const result = beautifyExactDate(mockDate);
expect(result).toEqual(expected); expect(result).toEqual(expected);

View File

@ -33,7 +33,7 @@ export function beautifyExactDate(dateToBeautify: Date | string | number) {
try { try {
const parsedDate = parseDate(dateToBeautify); const parsedDate = parseDate(dateToBeautify);
return parsedDate.toFormat('DD · TT'); return parsedDate.toFormat('DD · T');
} catch (error) { } catch (error) {
logError(error); logError(error);
return ''; return '';