mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-18 17:12:53 +03:00
Feat/filter activity inbox (#1032)
* Move files * Add filtering for tasks inbox * Add filter dropdown for single entity * Minor * Fill empty button * Refine logic for filter dropdown * remove log * Fix unwanted change * Set current user as default filter * Add avatar on filter * Improve initialization of assignee filter * Add story for Tasks page * Add more stories * Add sotry with no tasks * Improve dates * Enh tests --------- Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
This commit is contained in:
parent
2128d44212
commit
4252a0a2c3
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { Avatar } from '@/users/components/Avatar';
|
import { Avatar } from '@/users/components/Avatar';
|
||||||
import {
|
import {
|
||||||
beautifyExactDate,
|
beautifyExactDateTime,
|
||||||
beautifyPastDateRelativeToNow,
|
beautifyPastDateRelativeToNow,
|
||||||
} from '~/utils/date-utils';
|
} from '~/utils/date-utils';
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ const StyledTooltip = styled(Tooltip)`
|
|||||||
|
|
||||||
export function CommentHeader({ comment, actionBar }: OwnProps) {
|
export function CommentHeader({ comment, actionBar }: OwnProps) {
|
||||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(comment.createdAt);
|
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(comment.createdAt);
|
||||||
const exactCreatedAt = beautifyExactDate(comment.createdAt);
|
const exactCreatedAt = beautifyExactDateTime(comment.createdAt);
|
||||||
const showDate = beautifiedCreatedAt !== '';
|
const showDate = beautifiedCreatedAt !== '';
|
||||||
|
|
||||||
const author = comment.author;
|
const author = comment.author;
|
||||||
|
@ -33,7 +33,7 @@ export function ActivityAssigneePicker({
|
|||||||
);
|
);
|
||||||
const [updateActivity] = useUpdateActivityMutation();
|
const [updateActivity] = useUpdateActivityMutation();
|
||||||
|
|
||||||
const companies = useFilteredSearchEntityQuery({
|
const users = useFilteredSearchEntityQuery({
|
||||||
queryHook: useSearchUserQuery,
|
queryHook: useSearchUserQuery,
|
||||||
selectedIds: activity?.accountOwner?.id ? [activity?.accountOwner?.id] : [],
|
selectedIds: activity?.accountOwner?.id ? [activity?.accountOwner?.id] : [],
|
||||||
searchFilter: searchFilter,
|
searchFilter: searchFilter,
|
||||||
@ -70,9 +70,9 @@ export function ActivityAssigneePicker({
|
|||||||
onEntitySelected={handleEntitySelected}
|
onEntitySelected={handleEntitySelected}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
entities={{
|
entities={{
|
||||||
loading: companies.loading,
|
loading: users.loading,
|
||||||
entitiesToSelect: companies.entitiesToSelect,
|
entitiesToSelect: users.entitiesToSelect,
|
||||||
selectedEntity: companies.selectedEntities[0],
|
selectedEntity: users.selectedEntities[0],
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||||
|
|
||||||
|
import { TaskList } from '../TaskList';
|
||||||
|
|
||||||
|
const meta: Meta<typeof TaskList> = {
|
||||||
|
title: 'Modules/Activity/TaskList',
|
||||||
|
component: TaskList,
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<MemoryRouter>
|
||||||
|
<Story />
|
||||||
|
</MemoryRouter>
|
||||||
|
),
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
|
args: {
|
||||||
|
title: 'Tasks',
|
||||||
|
tasks: mockedActivities,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof TaskList>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'Tasks',
|
||||||
|
tasks: mockedActivities,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Empty: Story = {
|
||||||
|
args: {
|
||||||
|
title: 'No tasks',
|
||||||
|
tasks: [],
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/availableFiltersScopedState';
|
||||||
|
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||||
|
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||||
|
|
||||||
|
import { TasksContext } from '../states/TasksContext';
|
||||||
|
|
||||||
|
export function useInitializeTasksFilters({
|
||||||
|
availableFilters,
|
||||||
|
}: {
|
||||||
|
availableFilters: FilterDefinition[];
|
||||||
|
}) {
|
||||||
|
const [, setAvailableFilters] = useRecoilScopedState(
|
||||||
|
availableFiltersScopedState,
|
||||||
|
TasksContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAvailableFilters(availableFilters);
|
||||||
|
}, [setAvailableFilters, availableFilters]);
|
||||||
|
}
|
103
front/src/modules/activities/hooks/useTasks.ts
Normal file
103
front/src/modules/activities/hooks/useTasks.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
|
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||||
|
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
|
||||||
|
import { activeTabIdScopedState } from '@/ui/tab/states/activeTabIdScopedState';
|
||||||
|
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||||
|
import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql';
|
||||||
|
import { tasksFilters } from '~/pages/tasks/tasks-filters';
|
||||||
|
import { parseDate } from '~/utils/date-utils';
|
||||||
|
|
||||||
|
import { TasksContext } from '../states/TasksContext';
|
||||||
|
|
||||||
|
import { useInitializeTasksFilters } from './useInitializeTasksFilters';
|
||||||
|
|
||||||
|
export function useTasks() {
|
||||||
|
useInitializeTasksFilters({
|
||||||
|
availableFilters: tasksFilters,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [activeTabId] = useRecoilScopedState(
|
||||||
|
activeTabIdScopedState,
|
||||||
|
TasksContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [filters, setFilters] = useRecoilScopedState(
|
||||||
|
filtersScopedState,
|
||||||
|
TasksContext,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there is no filter, we set the default filter to the current user
|
||||||
|
const [currentUser] = useRecoilState(currentUserState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUser && !filters.length) {
|
||||||
|
setFilters([
|
||||||
|
{
|
||||||
|
field: 'assigneeId',
|
||||||
|
type: 'entity',
|
||||||
|
value: currentUser.id,
|
||||||
|
operand: 'is',
|
||||||
|
displayValue: currentUser.displayName,
|
||||||
|
displayAvatarUrl: currentUser.avatarUrl ?? undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}, [currentUser, filters, setFilters]);
|
||||||
|
|
||||||
|
const whereFilters = Object.assign(
|
||||||
|
{},
|
||||||
|
...filters.map((filter) => {
|
||||||
|
return turnFilterIntoWhereClause(filter);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: completeTasksData } = useGetActivitiesQuery({
|
||||||
|
variables: {
|
||||||
|
where: {
|
||||||
|
type: { equals: ActivityType.Task },
|
||||||
|
completedAt: { not: { equals: null } },
|
||||||
|
...whereFilters,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: incompleteTaskData } = useGetActivitiesQuery({
|
||||||
|
variables: {
|
||||||
|
where: {
|
||||||
|
type: { equals: ActivityType.Task },
|
||||||
|
completedAt: { equals: null },
|
||||||
|
...whereFilters,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tasksData =
|
||||||
|
activeTabId === 'done' ? completeTasksData : incompleteTaskData;
|
||||||
|
|
||||||
|
const todayOrPreviousTasks = tasksData?.findManyActivities.filter((task) => {
|
||||||
|
if (!task.dueAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const dueDate = parseDate(task.dueAt).toJSDate();
|
||||||
|
const today = DateTime.now().endOf('day').toJSDate();
|
||||||
|
return dueDate <= today;
|
||||||
|
});
|
||||||
|
|
||||||
|
const upcomingTasks = tasksData?.findManyActivities.filter((task) => {
|
||||||
|
if (!task.dueAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const dueDate = parseDate(task.dueAt).toJSDate();
|
||||||
|
const today = DateTime.now().endOf('day').toJSDate();
|
||||||
|
return dueDate > today;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
todayOrPreviousTasks,
|
||||||
|
upcomingTasks,
|
||||||
|
};
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import { Tooltip } from 'react-tooltip';
|
import { Tooltip } from 'react-tooltip';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { useCompleteTask } from '@/activities/hooks/useCompleteTask';
|
||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||||
import { useCompleteTask } from '@/tasks/hooks/useCompleteTask';
|
|
||||||
import { IconNotes } from '@/ui/icon';
|
import { IconNotes } from '@/ui/icon';
|
||||||
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
|
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
|
||||||
import { Activity, User } from '~/generated/graphql';
|
import { Activity, User } from '~/generated/graphql';
|
||||||
import {
|
import {
|
||||||
beautifyExactDate,
|
beautifyExactDateTime,
|
||||||
beautifyPastDateRelativeToNow,
|
beautifyPastDateRelativeToNow,
|
||||||
} from '~/utils/date-utils';
|
} from '~/utils/date-utils';
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ type OwnProps = {
|
|||||||
|
|
||||||
export function TimelineActivity({ activity }: OwnProps) {
|
export function TimelineActivity({ activity }: OwnProps) {
|
||||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(activity.createdAt);
|
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(activity.createdAt);
|
||||||
const exactCreatedAt = beautifyExactDate(activity.createdAt);
|
const exactCreatedAt = beautifyExactDateTime(activity.createdAt);
|
||||||
const body = JSON.parse(activity.body ?? '{}')[0]?.content[0]?.text;
|
const body = JSON.parse(activity.body ?? '{}')[0]?.content[0]?.text;
|
||||||
|
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
import { activeTabIdScopedState } from '@/ui/tab/states/activeTabIdScopedState';
|
|
||||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
|
||||||
import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql';
|
|
||||||
import { parseDate } from '~/utils/date-utils';
|
|
||||||
|
|
||||||
import { TasksContext } from '../states/TasksContext';
|
|
||||||
|
|
||||||
export function useTasks() {
|
|
||||||
const [activeTabId] = useRecoilScopedState(
|
|
||||||
activeTabIdScopedState,
|
|
||||||
TasksContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: completeTasksData } = useGetActivitiesQuery({
|
|
||||||
variables: {
|
|
||||||
where: {
|
|
||||||
type: { equals: ActivityType.Task },
|
|
||||||
completedAt: { not: { equals: null } },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: incompleteTaskData } = useGetActivitiesQuery({
|
|
||||||
variables: {
|
|
||||||
where: {
|
|
||||||
type: { equals: ActivityType.Task },
|
|
||||||
completedAt:
|
|
||||||
activeTabId === 'done' ? { not: { equals: null } } : { equals: null },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = activeTabId === 'done' ? completeTasksData : incompleteTaskData;
|
|
||||||
|
|
||||||
const todayOrPreviousTasks = data?.findManyActivities.filter((task) => {
|
|
||||||
if (!task.dueAt) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const dueDate = parseDate(task.dueAt).toJSDate();
|
|
||||||
const today = DateTime.now().endOf('day').toJSDate();
|
|
||||||
return dueDate <= today;
|
|
||||||
});
|
|
||||||
|
|
||||||
const upcomingTasks = data?.findManyActivities.filter((task) => {
|
|
||||||
if (!task.dueAt) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const dueDate = parseDate(task.dueAt).toJSDate();
|
|
||||||
const today = DateTime.now().endOf('day').toJSDate();
|
|
||||||
return dueDate > today;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
todayOrPreviousTasks,
|
|
||||||
upcomingTasks,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,25 +1,12 @@
|
|||||||
import { Context, useCallback, useState } from 'react';
|
import { Context } from 'react';
|
||||||
|
|
||||||
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
|
||||||
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
|
|
||||||
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
|
|
||||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
|
||||||
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/filter-n-sort/states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
|
||||||
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
|
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
|
||||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||||
|
|
||||||
|
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||||
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
|
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
|
||||||
|
|
||||||
import DropdownButton from './DropdownButton';
|
import { MultipleFiltersDropdownButton } from './MultipleFiltersDropdownButton';
|
||||||
import { FilterDropdownDateSearchInput } from './FilterDropdownDateSearchInput';
|
import { SingleEntityFilterDropdownButton } from './SingleEntityFilterDropdownButton';
|
||||||
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
|
|
||||||
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
|
|
||||||
import { FilterDropdownFilterSelect } from './FilterDropdownFilterSelect';
|
|
||||||
import { FilterDropdownNumberSearchInput } from './FilterDropdownNumberSearchInput';
|
|
||||||
import { FilterDropdownOperandButton } from './FilterDropdownOperandButton';
|
|
||||||
import { FilterDropdownOperandSelect } from './FilterDropdownOperandSelect';
|
|
||||||
import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
|
|
||||||
|
|
||||||
export function FilterDropdownButton({
|
export function FilterDropdownButton({
|
||||||
context,
|
context,
|
||||||
@ -28,93 +15,20 @@ export function FilterDropdownButton({
|
|||||||
context: Context<string | null>;
|
context: Context<string | null>;
|
||||||
HotkeyScope: FiltersHotkeyScope;
|
HotkeyScope: FiltersHotkeyScope;
|
||||||
}) {
|
}) {
|
||||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
const [availableFilters] = useRecoilScopedState(
|
||||||
|
availableFiltersScopedState,
|
||||||
const [
|
|
||||||
isFilterDropdownOperandSelectUnfolded,
|
|
||||||
setIsFilterDropdownOperandSelectUnfolded,
|
|
||||||
] = useRecoilScopedState(
|
|
||||||
isFilterDropdownOperandSelectUnfoldedScopedState,
|
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
return availableFilters.length === 1 &&
|
||||||
const [filterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown] =
|
availableFilters[0].type === 'entity' ? (
|
||||||
useRecoilScopedState(filterDefinitionUsedInDropdownScopedState, context);
|
<SingleEntityFilterDropdownButton
|
||||||
|
context={context}
|
||||||
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
|
|
||||||
filterDropdownSearchInputScopedState,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [filters] = useRecoilScopedState(filtersScopedState, context);
|
|
||||||
|
|
||||||
const [selectedOperandInDropdown, setSelectedOperandInDropdown] =
|
|
||||||
useRecoilScopedState(selectedOperandInDropdownScopedState, context);
|
|
||||||
|
|
||||||
const resetState = useCallback(() => {
|
|
||||||
setIsFilterDropdownOperandSelectUnfolded(false);
|
|
||||||
setFilterDefinitionUsedInDropdown(null);
|
|
||||||
setSelectedOperandInDropdown(null);
|
|
||||||
setFilterDropdownSearchInput('');
|
|
||||||
}, [
|
|
||||||
setFilterDefinitionUsedInDropdown,
|
|
||||||
setSelectedOperandInDropdown,
|
|
||||||
setFilterDropdownSearchInput,
|
|
||||||
setIsFilterDropdownOperandSelectUnfolded,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isFilterSelected = (filters?.length ?? 0) > 0;
|
|
||||||
|
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
|
||||||
|
|
||||||
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
|
|
||||||
if (newIsUnfolded) {
|
|
||||||
setHotkeyScope(HotkeyScope);
|
|
||||||
setIsUnfolded(true);
|
|
||||||
} else {
|
|
||||||
if (filterDefinitionUsedInDropdown?.type === 'entity') {
|
|
||||||
setHotkeyScope(HotkeyScope);
|
|
||||||
}
|
|
||||||
setIsUnfolded(false);
|
|
||||||
resetState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownButton
|
|
||||||
label="Filter"
|
|
||||||
isActive={isFilterSelected}
|
|
||||||
isUnfolded={isUnfolded}
|
|
||||||
onIsUnfoldedChange={handleIsUnfoldedChange}
|
|
||||||
HotkeyScope={HotkeyScope}
|
HotkeyScope={HotkeyScope}
|
||||||
>
|
/>
|
||||||
{!filterDefinitionUsedInDropdown ? (
|
) : (
|
||||||
<FilterDropdownFilterSelect context={context} />
|
<MultipleFiltersDropdownButton
|
||||||
) : isFilterDropdownOperandSelectUnfolded ? (
|
context={context}
|
||||||
<FilterDropdownOperandSelect context={context} />
|
HotkeyScope={HotkeyScope}
|
||||||
) : (
|
/>
|
||||||
selectedOperandInDropdown && (
|
|
||||||
<>
|
|
||||||
<FilterDropdownOperandButton context={context} />
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
{filterDefinitionUsedInDropdown.type === 'text' && (
|
|
||||||
<FilterDropdownTextSearchInput context={context} />
|
|
||||||
)}
|
|
||||||
{filterDefinitionUsedInDropdown.type === 'number' && (
|
|
||||||
<FilterDropdownNumberSearchInput context={context} />
|
|
||||||
)}
|
|
||||||
{filterDefinitionUsedInDropdown.type === 'date' && (
|
|
||||||
<FilterDropdownDateSearchInput context={context} />
|
|
||||||
)}
|
|
||||||
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
|
||||||
<FilterDropdownEntitySearchInput context={context} />
|
|
||||||
)}
|
|
||||||
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
|
||||||
<FilterDropdownEntitySelect context={context} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</DropdownButton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { EntityChip } from '@/ui/chip/components/EntityChip';
|
||||||
|
|
||||||
|
import { Filter } from '../types/Filter';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
filter: Filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GenericEntityFilterChip({ filter }: OwnProps) {
|
||||||
|
return (
|
||||||
|
<EntityChip
|
||||||
|
entityId={filter.value}
|
||||||
|
name={filter.displayValue}
|
||||||
|
avatarType="rounded"
|
||||||
|
pictureUrl={filter.displayAvatarUrl}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
import { Context, useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
||||||
|
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
|
||||||
|
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
|
||||||
|
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||||
|
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/filter-n-sort/states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
||||||
|
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
|
||||||
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||||
|
|
||||||
|
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
|
||||||
|
|
||||||
|
import DropdownButton from './DropdownButton';
|
||||||
|
import { FilterDropdownDateSearchInput } from './FilterDropdownDateSearchInput';
|
||||||
|
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
|
||||||
|
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
|
||||||
|
import { FilterDropdownFilterSelect } from './FilterDropdownFilterSelect';
|
||||||
|
import { FilterDropdownNumberSearchInput } from './FilterDropdownNumberSearchInput';
|
||||||
|
import { FilterDropdownOperandButton } from './FilterDropdownOperandButton';
|
||||||
|
import { FilterDropdownOperandSelect } from './FilterDropdownOperandSelect';
|
||||||
|
import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
|
||||||
|
|
||||||
|
export function MultipleFiltersDropdownButton({
|
||||||
|
context,
|
||||||
|
HotkeyScope,
|
||||||
|
}: {
|
||||||
|
context: Context<string | null>;
|
||||||
|
HotkeyScope: FiltersHotkeyScope;
|
||||||
|
}) {
|
||||||
|
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||||
|
|
||||||
|
const [
|
||||||
|
isFilterDropdownOperandSelectUnfolded,
|
||||||
|
setIsFilterDropdownOperandSelectUnfolded,
|
||||||
|
] = useRecoilScopedState(
|
||||||
|
isFilterDropdownOperandSelectUnfoldedScopedState,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [filterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown] =
|
||||||
|
useRecoilScopedState(filterDefinitionUsedInDropdownScopedState, context);
|
||||||
|
|
||||||
|
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
|
||||||
|
filterDropdownSearchInputScopedState,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [filters] = useRecoilScopedState(filtersScopedState, context);
|
||||||
|
|
||||||
|
const [selectedOperandInDropdown, setSelectedOperandInDropdown] =
|
||||||
|
useRecoilScopedState(selectedOperandInDropdownScopedState, context);
|
||||||
|
|
||||||
|
const resetState = useCallback(() => {
|
||||||
|
setIsFilterDropdownOperandSelectUnfolded(false);
|
||||||
|
setFilterDefinitionUsedInDropdown(null);
|
||||||
|
setSelectedOperandInDropdown(null);
|
||||||
|
setFilterDropdownSearchInput('');
|
||||||
|
}, [
|
||||||
|
setFilterDefinitionUsedInDropdown,
|
||||||
|
setSelectedOperandInDropdown,
|
||||||
|
setFilterDropdownSearchInput,
|
||||||
|
setIsFilterDropdownOperandSelectUnfolded,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isFilterSelected = (filters?.length ?? 0) > 0;
|
||||||
|
|
||||||
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
|
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
|
||||||
|
if (newIsUnfolded) {
|
||||||
|
setHotkeyScope(HotkeyScope);
|
||||||
|
setIsUnfolded(true);
|
||||||
|
} else {
|
||||||
|
if (filterDefinitionUsedInDropdown?.type === 'entity') {
|
||||||
|
setHotkeyScope(HotkeyScope);
|
||||||
|
}
|
||||||
|
setIsUnfolded(false);
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownButton
|
||||||
|
label="Filter"
|
||||||
|
isActive={isFilterSelected}
|
||||||
|
isUnfolded={isUnfolded}
|
||||||
|
onIsUnfoldedChange={handleIsUnfoldedChange}
|
||||||
|
HotkeyScope={HotkeyScope}
|
||||||
|
>
|
||||||
|
{!filterDefinitionUsedInDropdown ? (
|
||||||
|
<FilterDropdownFilterSelect context={context} />
|
||||||
|
) : isFilterDropdownOperandSelectUnfolded ? (
|
||||||
|
<FilterDropdownOperandSelect context={context} />
|
||||||
|
) : (
|
||||||
|
selectedOperandInDropdown && (
|
||||||
|
<>
|
||||||
|
<FilterDropdownOperandButton context={context} />
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
{filterDefinitionUsedInDropdown.type === 'text' && (
|
||||||
|
<FilterDropdownTextSearchInput context={context} />
|
||||||
|
)}
|
||||||
|
{filterDefinitionUsedInDropdown.type === 'number' && (
|
||||||
|
<FilterDropdownNumberSearchInput context={context} />
|
||||||
|
)}
|
||||||
|
{filterDefinitionUsedInDropdown.type === 'date' && (
|
||||||
|
<FilterDropdownDateSearchInput context={context} />
|
||||||
|
)}
|
||||||
|
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
||||||
|
<FilterDropdownEntitySearchInput context={context} />
|
||||||
|
)}
|
||||||
|
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
||||||
|
<FilterDropdownEntitySelect context={context} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</DropdownButton>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
import { Context, useState } from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { IconChevronDown } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
|
||||||
|
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
|
||||||
|
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
|
||||||
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||||
|
|
||||||
|
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||||
|
import { filtersScopedState } from '../states/filtersScopedState';
|
||||||
|
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
|
||||||
|
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
|
||||||
|
|
||||||
|
import { DropdownMenuContainer } from './DropdownMenuContainer';
|
||||||
|
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
|
||||||
|
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
|
||||||
|
import { GenericEntityFilterChip } from './GenericEntityFilterChip';
|
||||||
|
|
||||||
|
const StyledDropdownButtonContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
type StyledDropdownButtonProps = {
|
||||||
|
isUnfolded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
|
||||||
|
align-items: center;
|
||||||
|
background: ${({ theme }) => theme.background.primary};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')};
|
||||||
|
padding: ${({ theme }) => theme.spacing(1)};
|
||||||
|
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.95);
|
||||||
|
}
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function SingleEntityFilterDropdownButton({
|
||||||
|
context,
|
||||||
|
HotkeyScope,
|
||||||
|
}: {
|
||||||
|
context: Context<string | null>;
|
||||||
|
HotkeyScope: FiltersHotkeyScope;
|
||||||
|
}) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [availableFilters] = useRecoilScopedState(
|
||||||
|
availableFiltersScopedState,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
const availableFilter = availableFilters[0];
|
||||||
|
|
||||||
|
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||||
|
|
||||||
|
const [filters] = useRecoilScopedState(filtersScopedState, context);
|
||||||
|
|
||||||
|
const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||||
|
filterDefinitionUsedInDropdownScopedState,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
|
||||||
|
filterDropdownSearchInputScopedState,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
|
||||||
|
selectedOperandInDropdownScopedState,
|
||||||
|
context,
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setFilterDefinitionUsedInDropdown(availableFilter);
|
||||||
|
const defaultOperand = getOperandsForFilterType(availableFilter?.type)[0];
|
||||||
|
setSelectedOperandInDropdown(defaultOperand);
|
||||||
|
}, [
|
||||||
|
availableFilter,
|
||||||
|
setFilterDefinitionUsedInDropdown,
|
||||||
|
setSelectedOperandInDropdown,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
|
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
|
||||||
|
if (newIsUnfolded) {
|
||||||
|
setHotkeyScope(HotkeyScope);
|
||||||
|
setIsUnfolded(true);
|
||||||
|
} else {
|
||||||
|
setHotkeyScope(HotkeyScope);
|
||||||
|
setIsUnfolded(false);
|
||||||
|
setFilterDropdownSearchInput('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledDropdownButtonContainer>
|
||||||
|
<StyledDropdownButton
|
||||||
|
isUnfolded={isUnfolded}
|
||||||
|
onClick={() => handleIsUnfoldedChange(!isUnfolded)}
|
||||||
|
>
|
||||||
|
{filters[0] ? (
|
||||||
|
<GenericEntityFilterChip filter={filters[0]} />
|
||||||
|
) : (
|
||||||
|
'Filter'
|
||||||
|
)}
|
||||||
|
<IconChevronDown size={theme.icon.size.md} />
|
||||||
|
</StyledDropdownButton>
|
||||||
|
{isUnfolded && (
|
||||||
|
<DropdownMenuContainer onClose={() => handleIsUnfoldedChange(false)}>
|
||||||
|
<FilterDropdownEntitySearchInput context={context} />
|
||||||
|
<FilterDropdownEntitySelect context={context} />
|
||||||
|
</DropdownMenuContainer>
|
||||||
|
)}
|
||||||
|
</StyledDropdownButtonContainer>
|
||||||
|
);
|
||||||
|
}
|
@ -6,5 +6,6 @@ export type Filter = {
|
|||||||
type: FilterType;
|
type: FilterType;
|
||||||
value: string;
|
value: string;
|
||||||
displayValue: string;
|
displayValue: string;
|
||||||
|
displayAvatarUrl?: string;
|
||||||
operand: FilterOperand;
|
operand: FilterOperand;
|
||||||
};
|
};
|
||||||
|
@ -4,7 +4,7 @@ import { v4 as uuidV4 } from 'uuid';
|
|||||||
|
|
||||||
import { Avatar } from '@/users/components/Avatar';
|
import { Avatar } from '@/users/components/Avatar';
|
||||||
import {
|
import {
|
||||||
beautifyExactDate,
|
beautifyExactDateTime,
|
||||||
beautifyPastDateRelativeToNow,
|
beautifyPastDateRelativeToNow,
|
||||||
} from '~/utils/date-utils';
|
} from '~/utils/date-utils';
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ export function ShowPageSummaryCard({
|
|||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const beautifiedCreatedAt =
|
const beautifiedCreatedAt =
|
||||||
date !== '' ? beautifyPastDateRelativeToNow(date) : '';
|
date !== '' ? beautifyPastDateRelativeToNow(date) : '';
|
||||||
const exactCreatedAt = date !== '' ? beautifyExactDate(date) : '';
|
const exactCreatedAt = date !== '' ? beautifyExactDateTime(date) : '';
|
||||||
const dateElementId = `date-id-${uuidV4()}`;
|
const dateElementId = `date-id-${uuidV4()}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,22 +1,27 @@
|
|||||||
|
import { Context } from 'react';
|
||||||
|
|
||||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||||
import { FilterDropdownEntitySearchSelect } from '@/ui/filter-n-sort/components/FilterDropdownEntitySearchSelect';
|
import { FilterDropdownEntitySearchSelect } from '@/ui/filter-n-sort/components/FilterDropdownEntitySearchSelect';
|
||||||
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
|
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
|
||||||
import { filterDropdownSelectedEntityIdScopedState } from '@/ui/filter-n-sort/states/filterDropdownSelectedEntityIdScopedState';
|
import { filterDropdownSelectedEntityIdScopedState } from '@/ui/filter-n-sort/states/filterDropdownSelectedEntityIdScopedState';
|
||||||
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
import { Entity } from '@/ui/input/relation-picker/types/EntityTypeForSelect';
|
||||||
import { TableContext } from '@/ui/table/states/TableContext';
|
|
||||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||||
import { useSearchUserQuery } from '~/generated/graphql';
|
import { useSearchUserQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
export function FilterDropdownUserSearchSelect() {
|
export function FilterDropdownUserSearchSelect({
|
||||||
|
context,
|
||||||
|
}: {
|
||||||
|
context: Context<string | null>;
|
||||||
|
}) {
|
||||||
const filterDropdownSearchInput = useRecoilScopedValue(
|
const filterDropdownSearchInput = useRecoilScopedValue(
|
||||||
filterDropdownSearchInputScopedState,
|
filterDropdownSearchInputScopedState,
|
||||||
TableContext,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [filterDropdownSelectedEntityId] = useRecoilScopedState(
|
const [filterDropdownSelectedEntityId] = useRecoilScopedState(
|
||||||
filterDropdownSelectedEntityIdScopedState,
|
filterDropdownSelectedEntityIdScopedState,
|
||||||
TableContext,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
const usersForSelect = useFilteredSearchEntityQuery({
|
const usersForSelect = useFilteredSearchEntityQuery({
|
||||||
@ -38,7 +43,7 @@ export function FilterDropdownUserSearchSelect() {
|
|||||||
return (
|
return (
|
||||||
<FilterDropdownEntitySearchSelect
|
<FilterDropdownEntitySearchSelect
|
||||||
entitiesForSelect={usersForSelect}
|
entitiesForSelect={usersForSelect}
|
||||||
context={TableContext}
|
context={context}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
IconUser,
|
IconUser,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
} from '@/ui/icon/index';
|
} from '@/ui/icon/index';
|
||||||
|
import { TableContext } from '@/ui/table/states/TableContext';
|
||||||
import { icon } from '@/ui/theme/constants/icon';
|
import { icon } from '@/ui/theme/constants/icon';
|
||||||
import { FilterDropdownUserSearchSelect } from '@/users/components/FilterDropdownUserSearchSelect';
|
import { FilterDropdownUserSearchSelect } from '@/users/components/FilterDropdownUserSearchSelect';
|
||||||
import { Company } from '~/generated/graphql';
|
import { Company } from '~/generated/graphql';
|
||||||
@ -49,6 +50,8 @@ export const companiesFilters: FilterDefinitionByEntity<Company>[] = [
|
|||||||
label: 'Account owner',
|
label: 'Account owner',
|
||||||
icon: <IconUser size={icon.size.md} stroke={icon.stroke.sm} />,
|
icon: <IconUser size={icon.size.md} stroke={icon.stroke.sm} />,
|
||||||
type: 'entity',
|
type: 'entity',
|
||||||
entitySelectComponent: <FilterDropdownUserSearchSelect />,
|
entitySelectComponent: (
|
||||||
|
<FilterDropdownUserSearchSelect context={TableContext} />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { TaskGroups } from '@/tasks/components/TaskGroups';
|
import { TaskGroups } from '@/activities/components/TaskGroups';
|
||||||
import { TasksContext } from '@/tasks/states/TasksContext';
|
import { TasksContext } from '@/activities/states/TasksContext';
|
||||||
|
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
|
||||||
|
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||||
import { IconArchive, IconCheck, IconCheckbox } from '@/ui/icon/index';
|
import { IconArchive, IconCheck, IconCheckbox } from '@/ui/icon/index';
|
||||||
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
|
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
|
||||||
import { TabList } from '@/ui/tab/components/TabList';
|
import { TabList } from '@/ui/tab/components/TabList';
|
||||||
@ -52,6 +54,13 @@ export function Tasks() {
|
|||||||
<TabList context={TasksContext} tabs={TASK_TABS} />
|
<TabList context={TasksContext} tabs={TASK_TABS} />
|
||||||
</StyledTabListContainer>
|
</StyledTabListContainer>
|
||||||
}
|
}
|
||||||
|
rightComponents={[
|
||||||
|
<FilterDropdownButton
|
||||||
|
key="tasks-filter-dropdown-button"
|
||||||
|
context={TasksContext}
|
||||||
|
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<TaskGroups />
|
<TaskGroups />
|
||||||
</RecoilScope>
|
</RecoilScope>
|
||||||
|
26
front/src/pages/tasks/__stories__/Tasks.stories.tsx
Normal file
26
front/src/pages/tasks/__stories__/Tasks.stories.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PageDecorator,
|
||||||
|
type PageDecoratorArgs,
|
||||||
|
} from '~/testing/decorators/PageDecorator';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
|
||||||
|
import { Tasks } from '../Tasks';
|
||||||
|
|
||||||
|
const meta: Meta<PageDecoratorArgs> = {
|
||||||
|
title: 'Pages/Tasks/Default',
|
||||||
|
component: Tasks,
|
||||||
|
decorators: [PageDecorator],
|
||||||
|
args: { currentPath: '/tasks' },
|
||||||
|
parameters: {
|
||||||
|
docs: { story: 'inline', iframeHeight: '500px' },
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export type Story = StoryObj<typeof Tasks>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
18
front/src/pages/tasks/tasks-filters.tsx
Normal file
18
front/src/pages/tasks/tasks-filters.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { IconUser } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
import { TasksContext } from '@/activities/states/TasksContext';
|
||||||
|
import { FilterDefinitionByEntity } from '@/ui/filter-n-sort/types/FilterDefinitionByEntity';
|
||||||
|
import { FilterDropdownUserSearchSelect } from '@/users/components/FilterDropdownUserSearchSelect';
|
||||||
|
import { Activity } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export const tasksFilters: FilterDefinitionByEntity<Activity>[] = [
|
||||||
|
{
|
||||||
|
field: 'assigneeId',
|
||||||
|
label: 'Assignee',
|
||||||
|
icon: <IconUser />,
|
||||||
|
type: 'entity',
|
||||||
|
entitySelectComponent: (
|
||||||
|
<FilterDropdownUserSearchSelect context={TasksContext} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
@ -1,6 +1,7 @@
|
|||||||
import { getOperationName } from '@apollo/client/utilities';
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
import { graphql } from 'msw';
|
import { graphql } from 'msw';
|
||||||
|
|
||||||
|
import { GET_ACTIVITIES } from '@/activities/queries';
|
||||||
import { CREATE_EVENT } from '@/analytics/queries';
|
import { CREATE_EVENT } from '@/analytics/queries';
|
||||||
import { GET_CLIENT_CONFIG } from '@/client-config/queries';
|
import { GET_CLIENT_CONFIG } from '@/client-config/queries';
|
||||||
import { GET_COMPANIES } from '@/companies/queries';
|
import { GET_COMPANIES } from '@/companies/queries';
|
||||||
@ -210,6 +211,13 @@ export const graphqlMocks = [
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
graphql.query(getOperationName(GET_ACTIVITIES) ?? '', (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.data({
|
||||||
|
findManyActivities: mockedActivities,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}),
|
||||||
graphql.query(getOperationName(GET_VIEW_FIELDS) ?? '', (req, res, ctx) => {
|
graphql.query(getOperationName(GET_VIEW_FIELDS) ?? '', (req, res, ctx) => {
|
||||||
const {
|
const {
|
||||||
where: {
|
where: {
|
||||||
|
@ -33,7 +33,7 @@ type MockedActivity = Pick<
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
};
|
};
|
||||||
comments: Array<Pick<Comment, 'body'>>;
|
comments: Array<Pick<Comment, 'body' | 'id'>>;
|
||||||
activityTargets: Array<
|
activityTargets: Array<
|
||||||
Pick<
|
Pick<
|
||||||
ActivityTarget,
|
ActivityTarget,
|
||||||
@ -56,7 +56,7 @@ export const mockedActivities: Array<MockedActivity> = [
|
|||||||
title: 'My very first note',
|
title: 'My very first note',
|
||||||
type: ActivityType.Note,
|
type: ActivityType.Note,
|
||||||
body: null,
|
body: null,
|
||||||
dueAt: null,
|
dueAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
completedAt: null,
|
completedAt: null,
|
||||||
author: {
|
author: {
|
||||||
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
|
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
|
||||||
@ -112,7 +112,7 @@ export const mockedActivities: Array<MockedActivity> = [
|
|||||||
body: null,
|
body: null,
|
||||||
type: ActivityType.Note,
|
type: ActivityType.Note,
|
||||||
completedAt: null,
|
completedAt: null,
|
||||||
dueAt: null,
|
dueAt: '2029-08-26T10:12:42.33625+00:00',
|
||||||
author: {
|
author: {
|
||||||
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
|
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
|
||||||
firstName: 'Charles',
|
firstName: 'Charles',
|
||||||
|
@ -3,6 +3,7 @@ import { DateTime } from 'luxon';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
beautifyExactDate,
|
beautifyExactDate,
|
||||||
|
beautifyExactDateTime,
|
||||||
beautifyPastDateAbsolute,
|
beautifyPastDateAbsolute,
|
||||||
beautifyPastDateRelativeToNow,
|
beautifyPastDateRelativeToNow,
|
||||||
DEFAULT_DATE_LOCALE,
|
DEFAULT_DATE_LOCALE,
|
||||||
@ -12,14 +13,46 @@ import { logError } from '../logError';
|
|||||||
|
|
||||||
jest.mock('~/utils/logError');
|
jest.mock('~/utils/logError');
|
||||||
|
|
||||||
describe('beautifyExactDate', () => {
|
describe('beautifyExactDateTime', () => {
|
||||||
it('should return the correct relative date', () => {
|
it('should return the date in the correct format with time', () => {
|
||||||
const mockDate = '2023-01-01T12:13:24';
|
const mockDate = '2023-01-01T12:13:24';
|
||||||
const actualDate = new Date(mockDate);
|
const actualDate = new Date(mockDate);
|
||||||
const expected = DateTime.fromJSDate(actualDate)
|
const expected = DateTime.fromJSDate(actualDate)
|
||||||
.setLocale(DEFAULT_DATE_LOCALE)
|
.setLocale(DEFAULT_DATE_LOCALE)
|
||||||
.toFormat('DD · T');
|
.toFormat('DD · T');
|
||||||
|
|
||||||
|
const result = beautifyExactDateTime(mockDate);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
it('should return the time in the correct format for a datetime that is today', () => {
|
||||||
|
const todayString = DateTime.local().toISODate();
|
||||||
|
const mockDate = `${todayString}T12:13:24`;
|
||||||
|
const actualDate = new Date(mockDate);
|
||||||
|
const expected = DateTime.fromJSDate(actualDate)
|
||||||
|
.setLocale(DEFAULT_DATE_LOCALE)
|
||||||
|
.toFormat('T');
|
||||||
|
|
||||||
|
const result = beautifyExactDateTime(mockDate);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('beautifyExactDate', () => {
|
||||||
|
it('should return the past date in the correct format without time', () => {
|
||||||
|
const mockDate = '2023-01-01T12:13:24';
|
||||||
|
const actualDate = new Date(mockDate);
|
||||||
|
const expected = DateTime.fromJSDate(actualDate)
|
||||||
|
.setLocale(DEFAULT_DATE_LOCALE)
|
||||||
|
.toFormat('DD');
|
||||||
|
|
||||||
|
const result = beautifyExactDate(mockDate);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
it('should return "Today" if the date is today', () => {
|
||||||
|
const todayString = DateTime.local().toISODate();
|
||||||
|
const mockDate = `${todayString}T12:13:24`;
|
||||||
|
const expected = 'Today';
|
||||||
|
|
||||||
const result = beautifyExactDate(mockDate);
|
const result = beautifyExactDate(mockDate);
|
||||||
expect(result).toEqual(expected);
|
expect(result).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
@ -29,17 +29,32 @@ export function parseDate(dateToParse: Date | string | number) {
|
|||||||
return formattedDate.setLocale(DEFAULT_DATE_LOCALE);
|
return formattedDate.setLocale(DEFAULT_DATE_LOCALE);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function beautifyExactDate(dateToBeautify: Date | string | number) {
|
function isSameDay(a: DateTime, b: DateTime): boolean {
|
||||||
try {
|
return a.hasSame(b, 'day') && a.hasSame(b, 'month') && a.hasSame(b, 'year');
|
||||||
const parsedDate = parseDate(dateToBeautify);
|
}
|
||||||
|
|
||||||
return parsedDate.toFormat('DD · T');
|
function formatDate(dateToFormat: Date | string | number, format: string) {
|
||||||
|
try {
|
||||||
|
const parsedDate = parseDate(dateToFormat);
|
||||||
|
return parsedDate.toFormat(format);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error);
|
logError(error);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function beautifyExactDateTime(dateToBeautify: Date | string | number) {
|
||||||
|
const isToday = isSameDay(parseDate(dateToBeautify), DateTime.local());
|
||||||
|
const dateFormat = isToday ? 'T' : 'DD · T';
|
||||||
|
return formatDate(dateToBeautify, dateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function beautifyExactDate(dateToBeautify: Date | string | number) {
|
||||||
|
const isToday = isSameDay(parseDate(dateToBeautify), DateTime.local());
|
||||||
|
const dateFormat = isToday ? "'Today'" : 'DD';
|
||||||
|
return formatDate(dateToBeautify, dateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
export function beautifyPastDateRelativeToNow(
|
export function beautifyPastDateRelativeToNow(
|
||||||
pastDate: Date | string | number,
|
pastDate: Date | string | number,
|
||||||
) {
|
) {
|
||||||
|
Loading…
Reference in New Issue
Block a user