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
46 changed files with 875 additions and 205 deletions

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>