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:
@ -3,7 +3,7 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import {
|
||||
beautifyExactDate,
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
|
||||
@ -64,7 +64,7 @@ const StyledTooltip = styled(Tooltip)`
|
||||
|
||||
export function CommentHeader({ comment, actionBar }: OwnProps) {
|
||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(comment.createdAt);
|
||||
const exactCreatedAt = beautifyExactDate(comment.createdAt);
|
||||
const exactCreatedAt = beautifyExactDateTime(comment.createdAt);
|
||||
const showDate = beautifiedCreatedAt !== '';
|
||||
|
||||
const author = comment.author;
|
||||
|
||||
@ -33,7 +33,7 @@ export function ActivityAssigneePicker({
|
||||
);
|
||||
const [updateActivity] = useUpdateActivityMutation();
|
||||
|
||||
const companies = useFilteredSearchEntityQuery({
|
||||
const users = useFilteredSearchEntityQuery({
|
||||
queryHook: useSearchUserQuery,
|
||||
selectedIds: activity?.accountOwner?.id ? [activity?.accountOwner?.id] : [],
|
||||
searchFilter: searchFilter,
|
||||
@ -70,9 +70,9 @@ export function ActivityAssigneePicker({
|
||||
onEntitySelected={handleEntitySelected}
|
||||
onCancel={onCancel}
|
||||
entities={{
|
||||
loading: companies.loading,
|
||||
entitiesToSelect: companies.entitiesToSelect,
|
||||
selectedEntity: companies.selectedEntities[0],
|
||||
loading: users.loading,
|
||||
entitiesToSelect: users.entitiesToSelect,
|
||||
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 styled from '@emotion/styled';
|
||||
|
||||
import { useCompleteTask } from '@/activities/hooks/useCompleteTask';
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { useCompleteTask } from '@/tasks/hooks/useCompleteTask';
|
||||
import { IconNotes } from '@/ui/icon';
|
||||
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
|
||||
import { Activity, User } from '~/generated/graphql';
|
||||
import {
|
||||
beautifyExactDate,
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
|
||||
@ -126,7 +126,7 @@ type OwnProps = {
|
||||
|
||||
export function TimelineActivity({ activity }: OwnProps) {
|
||||
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 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 { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||
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';
|
||||
import { MultipleFiltersDropdownButton } from './MultipleFiltersDropdownButton';
|
||||
import { SingleEntityFilterDropdownButton } from './SingleEntityFilterDropdownButton';
|
||||
|
||||
export function FilterDropdownButton({
|
||||
context,
|
||||
@ -28,93 +15,20 @@ export function FilterDropdownButton({
|
||||
context: Context<string | null>;
|
||||
HotkeyScope: FiltersHotkeyScope;
|
||||
}) {
|
||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||
|
||||
const [
|
||||
isFilterDropdownOperandSelectUnfolded,
|
||||
setIsFilterDropdownOperandSelectUnfolded,
|
||||
] = useRecoilScopedState(
|
||||
isFilterDropdownOperandSelectUnfoldedScopedState,
|
||||
const [availableFilters] = useRecoilScopedState(
|
||||
availableFiltersScopedState,
|
||||
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}
|
||||
return availableFilters.length === 1 &&
|
||||
availableFilters[0].type === 'entity' ? (
|
||||
<SingleEntityFilterDropdownButton
|
||||
context={context}
|
||||
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>
|
||||
/>
|
||||
) : (
|
||||
<MultipleFiltersDropdownButton
|
||||
context={context}
|
||||
HotkeyScope={HotkeyScope}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
displayAvatarUrl?: string;
|
||||
operand: FilterOperand;
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import { v4 as uuidV4 } from 'uuid';
|
||||
|
||||
import { Avatar } from '@/users/components/Avatar';
|
||||
import {
|
||||
beautifyExactDate,
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
|
||||
@ -71,7 +71,7 @@ export function ShowPageSummaryCard({
|
||||
}: OwnProps) {
|
||||
const beautifiedCreatedAt =
|
||||
date !== '' ? beautifyPastDateRelativeToNow(date) : '';
|
||||
const exactCreatedAt = date !== '' ? beautifyExactDate(date) : '';
|
||||
const exactCreatedAt = date !== '' ? beautifyExactDateTime(date) : '';
|
||||
const dateElementId = `date-id-${uuidV4()}`;
|
||||
|
||||
return (
|
||||
|
||||
@ -1,22 +1,27 @@
|
||||
import { Context } from 'react';
|
||||
|
||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||
import { FilterDropdownEntitySearchSelect } from '@/ui/filter-n-sort/components/FilterDropdownEntitySearchSelect';
|
||||
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
|
||||
import { filterDropdownSelectedEntityIdScopedState } from '@/ui/filter-n-sort/states/filterDropdownSelectedEntityIdScopedState';
|
||||
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 { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useSearchUserQuery } from '~/generated/graphql';
|
||||
|
||||
export function FilterDropdownUserSearchSelect() {
|
||||
export function FilterDropdownUserSearchSelect({
|
||||
context,
|
||||
}: {
|
||||
context: Context<string | null>;
|
||||
}) {
|
||||
const filterDropdownSearchInput = useRecoilScopedValue(
|
||||
filterDropdownSearchInputScopedState,
|
||||
TableContext,
|
||||
context,
|
||||
);
|
||||
|
||||
const [filterDropdownSelectedEntityId] = useRecoilScopedState(
|
||||
filterDropdownSelectedEntityIdScopedState,
|
||||
TableContext,
|
||||
context,
|
||||
);
|
||||
|
||||
const usersForSelect = useFilteredSearchEntityQuery({
|
||||
@ -38,7 +43,7 @@ export function FilterDropdownUserSearchSelect() {
|
||||
return (
|
||||
<FilterDropdownEntitySearchSelect
|
||||
entitiesForSelect={usersForSelect}
|
||||
context={TableContext}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user