Skip to content

Commit

Permalink
feat(FiltersPanel): support sort filter
Browse files Browse the repository at this point in the history
  • Loading branch information
awinogradov committed May 25, 2023
1 parent 3f61db1 commit 6e0dd45
Show file tree
Hide file tree
Showing 15 changed files with 284 additions and 35 deletions.
3 changes: 2 additions & 1 deletion src/components/FiltersPanel/FiltersPanel.i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"Tags": "Tags",
"Estimate": "Estimate",
"Limit": "Limit",
"Preset": "Preset"
"Preset": "Preset",
"Sort": "Sort"
}
3 changes: 2 additions & 1 deletion src/components/FiltersPanel/FiltersPanel.i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
"Tags": "Теги",
"Estimate": "Срок",
"Limit": "Лимит",
"Preset": "Пресет"
"Preset": "Пресет",
"Sort": "Сортировка"
}
6 changes: 6 additions & 0 deletions src/components/FiltersPanel/FiltersPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { StateFilter } from '../StateFilter';
import { FiltersPanelApplied } from '../FiltersPanelApplied/FiltersPanelApplied';
import { Priority } from '../../types/priority';
import { FilterById } from '../../../trpc/inferredTypes';
import { SortFilter } from '../SortFilter/SortFilter';

import { tr } from './FiltersPanel.i18n';

Expand Down Expand Up @@ -53,6 +54,7 @@ export const FiltersPanel: FC<{
onTagChange: React.ComponentProps<typeof TagFilter>['onChange'];
onEstimateChange: React.ComponentProps<typeof EstimateFilter>['onChange'];
onPresetChange: React.ComponentProps<typeof PresetDropdown>['onChange'];
onSortChange: React.ComponentProps<typeof SortFilter>['onChange'];
onLimitChange?: React.ComponentProps<typeof LimitDropdown>['onChange'];
onFilterStar?: () => void;
}> = ({
Expand Down Expand Up @@ -80,6 +82,7 @@ export const FiltersPanel: FC<{
onTagChange,
onLimitChange,
onFilterStar,
onSortChange,
}) => (
<>
<FiltersPanelContainer loading={loading}>
Expand Down Expand Up @@ -133,6 +136,9 @@ export const FiltersPanel: FC<{
{Boolean(tags.length) && (
<TagFilter text={tr('Tags')} value={queryState.tag} tags={tags} onChange={onTagChange} />
)}

<SortFilter text={tr('Sort')} value={queryState.sort} onChange={onSortChange} />

{Boolean(presets.length) && (
<PresetDropdown
text={tr('Preset')}
Expand Down
2 changes: 2 additions & 0 deletions src/components/GoalsPage/GoalsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const GoalsPage = ({ user, ssrTime, locale }: ExternalPageProps) => {
setEstimateFilter,
setOwnerFilter,
setProjectFilter,
setSortFilter,
setFulltextFilter,
resetQueryState,
setPreset,
Expand Down Expand Up @@ -192,6 +193,7 @@ export const GoalsPage = ({ user, ssrTime, locale }: ExternalPageProps) => {
onPriorityChange={setPriorityFilter}
onPresetChange={setPreset}
onFilterStar={onFilterStar}
onSortChange={setSortFilter}
>
{Boolean(queryString) && <Button text={tr('Reset')} onClick={resetQueryState} />}
</FiltersPanel>
Expand Down
2 changes: 2 additions & 0 deletions src/components/ProjectPage/ProjectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const ProjectPage = ({ user, locale, ssrTime, params: { id } }: ExternalP
setEstimateFilter,
setOwnerFilter,
setProjectFilter,
setSortFilter,
setFulltextFilter,
resetQueryState,
setPreset,
Expand Down Expand Up @@ -230,6 +231,7 @@ export const ProjectPage = ({ user, locale, ssrTime, params: { id } }: ExternalP
onPriorityChange={setPriorityFilter}
onPresetChange={setPreset}
onFilterStar={onFilterStar}
onSortChange={setSortFilter}
>
{Boolean(queryString) && <Button text={tr('Reset')} onClick={resetQueryState} />}
</FiltersPanel>
Expand Down
10 changes: 10 additions & 0 deletions src/components/SortFilter/SortFilter.i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"title": "Title",
"state": "State",
"priority": "Priority",
"project": "Project",
"activity": "Issuer",
"owner": "Owner",
"updatedAt": "Updated",
"createdAt": "Created"
}
17 changes: 17 additions & 0 deletions src/components/SortFilter/SortFilter.i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable */
// Do not edit, use generator to update
import { i18n, fmt, I18nLangSet } from 'easy-typed-intl';
import getLang from '../../../utils/getLang';

import ru from './ru.json';
import en from './en.json';

export type I18nKey = keyof typeof ru & keyof typeof en;
type I18nLang = 'ru' | 'en';

const keyset: I18nLangSet<I18nKey> = {};

keyset['ru'] = ru;
keyset['en'] = en;

export const tr = i18n<I18nLang, I18nKey>(keyset, fmt, getLang);
10 changes: 10 additions & 0 deletions src/components/SortFilter/SortFilter.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"title": "Название",
"state": "Статус",
"priority": "Приоритет",
"project": "Проект",
"activity": "Автор",
"owner": "Ответственный",
"updatedAt": "Обновлено",
"createdAt": "Создано"
}
108 changes: 108 additions & 0 deletions src/components/SortFilter/SortFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { FC, useCallback, useMemo } from 'react';
import { FiltersDropdownBase, MenuItem, Text } from '@taskany/bricks';
import { gray8 } from '@taskany/colors';

import { tr } from './SortFilter.i18n';

export type SortDirection = 'asc' | 'desc' | null;
export type SortableProps =
| 'title'
| 'state'
| 'priority'
| 'project'
| 'activity'
| 'owner'
| 'updatedAt'
| 'createdAt';

const getNextDirection = (currentDirection: SortDirection): SortDirection => {
switch (currentDirection) {
case 'asc':
return 'desc';
case 'desc':
return null;
default:
return 'asc';
}
};

export const SortFilter: FC<{
text: string;
value: Record<SortableProps, NonNullable<SortDirection>> | Record<string, never>;
onChange: (value: Record<SortableProps, NonNullable<SortDirection>>) => void;
}> = ({ text, value, onChange }) => {
const sortableProps = useMemo<Record<SortableProps, string>>(
() => ({
title: tr('title'),
state: tr('state'),
priority: tr('priority'),
project: tr('project'),
activity: tr('activity'),
owner: tr('owner'),
updatedAt: tr('updatedAt'),
createdAt: tr('createdAt'),
}),
[],
);

const items = useMemo(
() =>
Object.entries(sortableProps).map(([id, text]) => {
const direction = value[id as SortableProps];
return {
item: {
id,
data: {
text,
direction,
selected: Boolean(direction),
},
},
};
}),
[value, sortableProps],
);

type SortFilterItems = typeof items;

const onClick = useCallback(
(id: SortableProps, direction: SortDirection) => () => {
let newValue = { ...value } as Record<SortableProps, NonNullable<SortDirection>>;
const nextDirection = getNextDirection(direction);

if (!nextDirection) {
delete newValue[id];
} else {
newValue = {
...newValue,
[id]: nextDirection,
};
}

onChange(newValue);
},
[value, onChange],
);

return (
<FiltersDropdownBase
text={text}
items={items.map((el) => ({ id: el.item.id, data: el.item.data }))}
value={Object.keys(value).length ? [''] : []}
onChange={() => {}}
renderItem={({
item: {
id,
data: { text, direction },
},
}: SortFilterItems[number]) => (
<MenuItem ghost key={id} onClick={onClick(id as SortableProps, direction)}>
{text}{' '}
<Text as="span" weight="bold" color={gray8} size="xs">
{direction}
</Text>
</MenuItem>
)}
/>
);
};
26 changes: 25 additions & 1 deletion src/hooks/useUrlFilterParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { Tag } from '@prisma/client';

import { FilterById } from '../../trpc/inferredTypes';
import { Priority } from '../types/priority';
import { SortDirection, SortableProps } from '../components/SortFilter/SortFilter';

// TODO: replace it with QueryWithFilters from schema/common
export interface QueryState {
priority: Priority[];
state: string[];
Expand All @@ -14,11 +16,26 @@ export interface QueryState {
owner: string[];
project: string[];
query: string;
sort: Record<SortableProps, NonNullable<SortDirection>> | Record<string, never>;
limit?: number;
}

const parseQueryParam = (param = '') => param.split(',').filter(Boolean);

const parseSortQueryParam = (param = '') =>
param.split(',').reduce((acc, curr) => {
if (curr) {
const [id, direction] = curr.split(':');
acc[id as SortableProps] = direction as NonNullable<SortDirection>;
}
return acc;
}, {} as Record<SortableProps, NonNullable<SortDirection>>);

const stringifySortQueryParam = (param: QueryState['sort']) =>
Object.entries(param)
.map(([id, direction]) => `${id}:${direction}`)
.join(',');

export const parseFilterValues = (query: ParsedUrlQuery): QueryState => ({
priority: parseQueryParam(query.priority?.toString()) as Priority[],
state: parseQueryParam(query.state?.toString()),
Expand All @@ -27,6 +44,7 @@ export const parseFilterValues = (query: ParsedUrlQuery): QueryState => ({
owner: parseQueryParam(query.owner?.toString()),
project: parseQueryParam(query.project?.toString()),
query: parseQueryParam(query.query?.toString()).toString(),
sort: parseSortQueryParam(query.sort?.toString()),
limit: query.limit ? Number(query.limit) : undefined,
});

Expand All @@ -44,7 +62,7 @@ export const useUrlFilterParams = ({ preset }: { preset?: FilterById }) => {
}

const pushNewState = useCallback(
({ priority, state, tag, estimate, owner, project, query, limit }: QueryState) => {
({ priority, state, tag, estimate, owner, project, query, sort, limit }: QueryState) => {
const newurl = router.asPath.split('?')[0];
const urlParams = new URLSearchParams();

Expand All @@ -64,6 +82,10 @@ export const useUrlFilterParams = ({ preset }: { preset?: FilterById }) => {

project.length > 0 ? urlParams.set('project', Array.from(project).toString()) : urlParams.delete('project');

Object.keys(sort).length > 0
? urlParams.set('sort', stringifySortQueryParam(sort))
: urlParams.delete('sort');

query.length > 0 ? urlParams.set('query', query.toString()) : urlParams.delete('query');

limit ? urlParams.set('limit', limit.toString()) : urlParams.delete('limit');
Expand Down Expand Up @@ -94,6 +116,7 @@ export const useUrlFilterParams = ({ preset }: { preset?: FilterById }) => {
tag: [],
estimate: [],
query: '',
sort: {},
});
}, [pushNewState]);

Expand Down Expand Up @@ -137,6 +160,7 @@ export const useUrlFilterParams = ({ preset }: { preset?: FilterById }) => {
setEstimateFilter: pushStateProvider('estimate'),
setOwnerFilter: pushStateProvider('owner'),
setProjectFilter: pushStateProvider('project'),
setSortFilter: pushStateProvider('sort'),
setFulltextFilter: pushStateProvider('query'),
setLimitFilter: pushStateProvider('limit'),
}),
Expand Down
26 changes: 26 additions & 0 deletions src/schema/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,29 @@ export const ToggleSubscriptionSchema = z.object({
});

export type ToggleSubscription = z.infer<typeof ToggleSubscriptionSchema>;

export const sortablePropertiesSchema = z
.object({
title: z.string().optional(),
state: z.string().optional(),
priority: z.string().optional(),
project: z.string().optional(),
activity: z.string().optional(),
owner: z.string().optional(),
updatedAt: z.string().optional(),
createdAt: z.string().optional(),
})
.optional();

export const queryWithFiltersSchema = z.object({
priority: z.array(z.string()).optional(),
state: z.array(z.string()).optional(),
tag: z.array(z.string()).optional(),
estimate: z.array(z.string()).optional(),
owner: z.array(z.string()).optional(),
project: z.array(z.string()).optional(),
sort: sortablePropertiesSchema,
query: z.string().optional(),
});

export type QueryWithFilters = z.infer<typeof queryWithFiltersSchema>;
11 changes: 2 additions & 9 deletions src/schema/goal.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import { z } from 'zod';

import { tr } from './schema.i18n';
import { queryWithFiltersSchema } from './common';

export const userGoalsSchema = z.object({
priority: z.array(z.string()).optional(),
state: z.array(z.string()).optional(),
tag: z.array(z.string()).optional(),
estimate: z.array(z.string()).optional(),
owner: z.array(z.string()).optional(),
project: z.array(z.string()).optional(),
query: z.string().optional(),
});
export const userGoalsSchema = queryWithFiltersSchema;

export type UserGoals = z.infer<typeof userGoalsSchema>;

Expand Down
10 changes: 2 additions & 8 deletions src/schema/project.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { z } from 'zod';

import { tr } from './schema.i18n';
import { queryWithFiltersSchema } from './common';

export const projectDeepInfoSchema = z.object({
export const projectDeepInfoSchema = queryWithFiltersSchema.extend({
id: z.string(),
priority: z.array(z.string()).optional(),
state: z.array(z.string()).optional(),
tag: z.array(z.string()).optional(),
estimate: z.array(z.string()).optional(),
owner: z.array(z.string()).optional(),
project: z.array(z.string()).optional(),
query: z.string().optional(),
});

export type ProjectDeepInfo = z.infer<typeof projectDeepInfoSchema>;
Expand Down
Loading

0 comments on commit 6e0dd45

Please sign in to comment.