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<{
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<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
type?: InputMaybe<ActivityType>;
completedAt?: InputMaybe<Scalars['DateTime']>;
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<typeof useDeleteActivi
export type DeleteActivityMutationResult = Apollo.MutationResult<DeleteActivityMutation>;
export type DeleteActivityMutationOptions = Apollo.BaseMutationOptions<DeleteActivityMutation, DeleteActivityMutationVariables>;
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<UpdateActivityMut
* @example
* const [updateActivityMutation, { data, loading, error }] = useUpdateActivityMutation({
* variables: {
* id: // value for 'id'
* body: // value for 'body'
* title: // value for 'title'
* type: // value for 'type'
* completedAt: // value for 'completedAt'
* where: // value for 'where'
* data: // value for 'data'
* },
* });
*/

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);
updateActivityMutation({
variables: {
id: activity.id,
body: activityBody,
where: {
id: activity.id,
},
data: {
body: activityBody,
},
},
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
});

View File

@ -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, 'id' | 'title' | 'body' | 'type' | 'completedAt'> & {
activity: Pick<
Activity,
'id' | 'title' | 'body' | 'type' | 'completedAt' | 'dueAt'
> & {
comments?: Array<CommentForDrawer> | null;
} & {
assignee?: Pick<
User,
'id' | 'firstName' | 'lastName' | 'displayName'
> | null;
} & {
activityTargets?: Array<
Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
@ -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}
/>
<PropertyBox>
<PropertyBoxItem
icon={<IconArrowUpRight />}
value={
<ActivityRelationPicker
activity={{
id: activity.id,
activityTargets: activity.activityTargets ?? [],
{activity.type === ActivityType.Task && (
<>
<DateEditableField
value={activity.dueAt}
icon={<IconCalendar />}
label="Due date"
onSubmit={(newDate) => {
updateActivityMutation({
variables: {
where: {
id: activity.id,
},
data: {
dueAt: newDate,
},
},
});
}}
/>
}
label="Relations"
/>
<ActivityAssigneeEditableField activity={activity} />
</>
)}
<ActivityRelationEditableField activity={activity} />
</PropertyBox>
</StyledTopContainer>
<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,
} 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<Activity, 'id'> & {
activityTargets: Array<
activityTargets?: Array<
Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
>;
> | 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({

View File

@ -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

View File

@ -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
}
}
}
`;

View File

@ -21,7 +21,7 @@ type OwnProps = {
autoFillTitle?: boolean;
};
export function Activity({
export function RightDrawerActivity({
activityId,
showComment = true,
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 { 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() {
<RightDrawerTopBar />
<RightDrawerBody>
{activityId && (
<Activity
<RightDrawerActivity
activityId={activityId}
showComment={false}
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 { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar';
import { Activity } from '../Activity';
import { RightDrawerActivity } from '../RightDrawerActivity';
export function RightDrawerEditActivity() {
const activityId = useRecoilValue(viewableActivityIdState);
@ -14,7 +14,7 @@ export function RightDrawerEditActivity() {
<RightDrawerPage>
<RightDrawerTopBar />
<RightDrawerBody>
{activityId && <Activity activityId={activityId} />}
{activityId && <RightDrawerActivity activityId={activityId} />}
</RightDrawerBody>
</RightDrawerPage>
);

View File

@ -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<Activity['author'], 'displayName'> };
> & { author: Pick<Activity['author'], 'displayName'> } & {
assignee?: Pick<User, 'id' | 'displayName'> | 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) {
</StyledVerticalLineContainer>
<StyledCardContainer>
<StyledCard onClick={() => openActivityRightDrawer(activity.id)}>
<TimelineActivityTitle
title={activity.title ?? ''}
completed={!!activity.completedAt}
type={activity.type}
onCompletionChange={handleActivityCompletionChange}
/>
<StyledCardContent>
<OverflowingTextWithTooltip text={body ? body : '(No content)'} />
</StyledCardContent>
<StyledCardDetailsContainer>
<TimelineActivityTitle
title={activity.title ?? ''}
completed={!!activity.completedAt}
type={activity.type}
onCompletionChange={handleActivityCompletionChange}
/>
<StyledCardContent>
<OverflowingTextWithTooltip
text={body ? body : '(No content)'}
/>
</StyledCardContent>
</StyledCardDetailsContainer>
<TimelineActivityCardFooter activity={activity} />
</StyledCard>
</StyledCardContainer>
</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)};
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({
/>
</StyledCheckboxContainer>
)}
<StyledTitleText completed={completed}>
<StyledTitleText
completed={completed}
hasCheckbox={type === ActivityType.Task}
>
<OverflowingTextWithTooltip text={title ? title : '(No title)'} />
</StyledTitleText>
</StyledTitleContainer>

View File

@ -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() {

View File

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

View File

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

View File

@ -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 (
<CompanyAccountOwnerPickerContainer>
<StyledContainer>
<CompanyAccountOwnerPicker
company={company}
onCancel={handleCancel}
onSubmit={handleSubmit}
/>
</CompanyAccountOwnerPickerContainer>
</StyledContainer>
);
}

View File

@ -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<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) {
const [internalValue, setInternalValue] = useState(company.name);
@ -36,27 +59,14 @@ export function CompanyNameEditableField({ company }: OwnProps) {
});
}
async function handleCancel() {
setInternalValue(company.name);
}
return (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
editModeContent={
<InplaceInputText
placeholder={'Name'}
autoFocus
value={internalValue}
onChange={(newValue: string) => {
handleChange(newValue);
}}
/>
}
displayModeContent={internalValue ?? ''}
isDisplayModeContentEmpty={!(internalValue !== '')}
<StyledEditableTitleInput
autoComplete="off"
autoFocus
onChange={(event) => handleChange(event.target.value)}
onBlur={handleSubmit}
value={internalValue}
/>
</RecoilScope>
);

View File

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

View File

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

View File

@ -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<Person, 'id'> & { company?: Pick<Company, 'id'> | 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 (
<SingleEntitySelect
onEntitySelected={handleEntitySelected}
entities={{
entitiesToSelect: companies.entitiesToSelect,
selectedEntity: companies.selectedEntities[0],
loading: companies.loading,
}}
onCancel={handleCancel}
/>
<StyledContainer>
<SingleEntitySelect
onEntitySelected={handleEntitySelected}
entities={{
entitiesToSelect: companies.entitiesToSelect,
selectedEntity: companies.selectedEntities[0],
loading: companies.loading,
}}
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 { 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 (
<RecoilScope SpecificContext={FieldContext}>
<EditableField
onSubmit={handleSubmit}
onCancel={handleCancel}
editModeContent={
<InplaceInputDoubleText
firstValuePlaceholder={'First name'}
secondValuePlaceholder={'Last name'}
firstValue={internalValueFirstName ?? ''}
secondValue={internalValueLastName ?? ''}
onChange={handleChange}
/>
}
displayModeContent={`${internalValueFirstName} ${internalValueLastName}`}
isDisplayModeContentEmpty={
!(internalValueFirstName !== '') && !(internalValueLastName !== '')
}
<InplaceInputDoubleText
firstValuePlaceholder={'First name'}
secondValuePlaceholder={'Last name'}
firstValue={internalValueFirstName ?? ''}
secondValue={internalValueLastName ?? ''}
onChange={handleChange}
/>
</RecoilScope>
);

View File

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

View File

@ -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;
`;

View File

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

View File

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

View File

@ -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<Pick<OwnProps, 'labelFixedWidth'>>`
align-items: center;
@ -39,18 +46,22 @@ const StyledLabel = styled.div<Pick<OwnProps, 'labelFixedWidth'>>`
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({
<StyledLabel labelFixedWidth={labelFixedWidth}>{label}</StyledLabel>
)}
</StyledLabelAndIconContainer>
{isFieldInEditMode && !displayModeContentOnly ? (
<EditableFieldEditMode onSubmit={onSubmit} onCancel={onCancel}>
{editModeContent}
</EditableFieldEditMode>
) : (
<EditableFieldDisplayMode
disableHoverEffect={disableHoverEffect}
disableClick={useEditButton}
onClick={handleDisplayModeClick}
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
>
{displayModeContent}
</EditableFieldDisplayMode>
)}
<StyledValueContainer>
{isFieldInEditMode && !displayModeContentOnly ? (
<EditableFieldEditMode onSubmit={onSubmit} onCancel={onCancel}>
{editModeContent}
</EditableFieldEditMode>
) : (
<EditableFieldDisplayMode
disableHoverEffect={disableHoverEffect}
disableClick={useEditButton}
onClick={handleDisplayModeClick}
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
isDisplayModeFixHeight={isDisplayModeFixHeight}
>
{displayModeContent}
</EditableFieldDisplayMode>
)}
</StyledValueContainer>
{showEditButton && (
<motion.div
<StyledEditButtonContainer
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.1 }}
whileHover={{ scale: 1.04 }}
>
<EditableFieldEditButton />
</motion.div>
</StyledEditButtonContainer>
)}
</EditableFieldBaseContainer>
);

View File

@ -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<OwnProps>) {
return (
<EditableFieldNormalModeOuterContainer
@ -82,6 +87,7 @@ export function EditableFieldDisplayMode({
disableClick={disableClick}
isDisplayModeContentEmpty={isDisplayModeContentEmpty}
disableHoverEffect={disableHoverEffect}
isDisplayModeFixHeight={isDisplayModeFixHeight}
>
<EditableFieldNormalModeInnerContainer>
{children}

View File

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

View File

@ -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)};
`;

View File

@ -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={
<EditableFieldEditModeDate
value={internalValue || new Date().toISOString()}

View File

@ -14,12 +14,14 @@ type OwnProps = {
export const StyledDoubleTextContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
justify-content: center;
text-align: center;
`;
& > 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 (
<StyledDoubleTextContainer>
<StyledInput
<StyledNameInput
size={firstValue.length}
autoFocus
placeholder={firstValuePlaceholder}
value={firstValue}
@ -39,7 +42,8 @@ export function InplaceInputDoubleText({
onChange(event.target.value, secondValue);
}}
/>
<StyledInput
<StyledNameInput
size={secondValue.length}
autoComplete="off"
placeholder={secondValuePlaceholder}
value={secondValue}

View File

@ -43,9 +43,12 @@ const StyledDate = styled.div`
const StyledTitle = styled.div`
color: ${({ theme }) => 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)`

View File

@ -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<string, boolean>) => void;
onCancel?: () => void;
onSubmit?: () => void;
value: Record<string, boolean>;
}) {
const debouncedSetSearchFilter = debounce(onSearchFilterChange, 100, {
@ -53,8 +58,21 @@ export function MultipleEntitySelect<
isNonEmptyString(entity.name),
);
const containerRef = useRef<HTMLDivElement>(null);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
onSubmit?.();
},
});
return (
<DropdownMenu>
<DropdownMenu ref={containerRef}>
<DropdownMenuSearch
value={searchFilter}
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}`;
switch (viewField.metadata.entityType) {
@ -40,7 +47,13 @@ export function GenericEditableDoubleTextChipCellDisplayMode({
return <CompanyChip id={currentRowEntityId ?? ''} name={displayName} />;
}
case Entity.Person: {
return <PersonChip id={currentRowEntityId ?? ''} name={displayName} />;
return (
<PersonChip
id={currentRowEntityId ?? ''}
name={displayName}
pictureUrl={avatarUrlValue}
/>
);
}
default:
console.warn(

View File

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

View File

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

View File

@ -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]],
}),
);
},
),
],
},
};

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_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({

View File

@ -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<Pick<Comment, 'body'>>;
activityTargets: Array<
Pick<
@ -47,12 +56,20 @@ export const mockedActivities: Array<MockedActivity> = [
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<MockedActivity> = [
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: [

View File

@ -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);

View File

@ -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 '';