Add dueDate and assignee on notes (#988)
* Add dueDate and assignee on notes * Fix tests * Fix tests
This commit is contained in:
@ -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],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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) ?? ''],
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -21,7 +21,7 @@ type OwnProps = {
|
||||
autoFillTitle?: boolean;
|
||||
};
|
||||
|
||||
export function Activity({
|
||||
export function RightDrawerActivity({
|
||||
activityId,
|
||||
showComment = true,
|
||||
autoFillTitle = false,
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user