feat: persist view filters and sorts on Update View button click (#1290)

* feat: add viewFilters table

Closes #1121

* feat: add Update View button + Create View dropdown

Closes #1124, #1289

* feat: add View Filter resolvers

* feat: persist view filters and sorts on Update View button click

Closes #1123

* refactor: code review

- Rename recoil selectors
- Rename filters `field` property to `key`
This commit is contained in:
Thaïs
2023-08-23 18:20:43 +02:00
committed by GitHub
parent 76246ec880
commit 74ab0142c7
54 changed files with 1331 additions and 277 deletions

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const CREATE_VIEW_FILTERS = gql`
mutation CreateViewFilters($data: [ViewFilterCreateManyInput!]!) {
createManyViewFilter(data: $data) {
count
}
}
`;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const DELETE_VIEW_FILTERS = gql`
mutation DeleteViewFilters($where: ViewFilterWhereInput!) {
deleteManyViewFilter(where: $where) {
count
}
}
`;

View File

@ -0,0 +1,16 @@
import { gql } from '@apollo/client';
export const UPDATE_VIEW_FILTER = gql`
mutation UpdateViewFilter(
$data: ViewFilterUpdateInput!
$where: ViewFilterWhereUniqueInput!
) {
viewFilter: updateOneViewFilter(data: $data, where: $where) {
displayValue
key
name
operand
value
}
}
`;

View File

@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const GET_VIEW_FILTERS = gql`
query GetViewFilters($where: ViewFilterWhereInput) {
viewFilters: findManyViewFilter(where: $where) {
displayValue
key
name
operand
value
}
}
`;

View File

@ -0,0 +1,172 @@
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { savedFiltersByKeyScopedSelector } from '@/ui/filter-n-sort/states/savedFiltersByKeyScopedSelector';
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
import type { Filter } from '@/ui/filter-n-sort/types/Filter';
import type { FilterDefinitionByEntity } from '@/ui/filter-n-sort/types/FilterDefinitionByEntity';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import {
useCreateViewFiltersMutation,
useDeleteViewFiltersMutation,
useGetViewFiltersQuery,
useUpdateViewFilterMutation,
} from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useViewFilters = <Entity>({
availableFilters,
}: {
availableFilters: FilterDefinitionByEntity<Entity>[];
}) => {
const currentViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const [filters, setFilters] = useRecoilScopedState(
filtersScopedState,
TableRecoilScopeContext,
);
const [, setSavedFilters] = useRecoilState(
savedFiltersScopedState(currentViewId),
);
const savedFiltersByKey = useRecoilValue(
savedFiltersByKeyScopedSelector(currentViewId),
);
const { refetch } = useGetViewFiltersQuery({
skip: !currentViewId,
variables: {
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: (data) => {
const nextFilters = data.viewFilters
.map(({ __typename, name: _name, ...viewFilter }) => {
const availableFilter = availableFilters.find(
(filter) => filter.key === viewFilter.key,
);
return availableFilter
? {
...viewFilter,
displayValue: viewFilter.displayValue ?? viewFilter.value,
type: availableFilter.type,
}
: undefined;
})
.filter((filter): filter is Filter => !!filter);
if (!isDeeplyEqual(filters, nextFilters)) {
setSavedFilters(nextFilters);
setFilters(nextFilters);
}
},
});
const [createViewFiltersMutation] = useCreateViewFiltersMutation();
const [updateViewFilterMutation] = useUpdateViewFilterMutation();
const [deleteViewFiltersMutation] = useDeleteViewFiltersMutation();
const createViewFilters = useCallback(
(filters: Filter[]) => {
if (!currentViewId || !filters.length) return;
return createViewFiltersMutation({
variables: {
data: filters.map((filter) => ({
displayValue: filter.displayValue ?? filter.value,
key: filter.key,
name:
availableFilters.find(({ key }) => key === filter.key)?.label ??
'',
operand: filter.operand,
value: filter.value,
viewId: currentViewId,
})),
},
});
},
[availableFilters, createViewFiltersMutation, currentViewId],
);
const updateViewFilters = useCallback(
(filters: Filter[]) => {
if (!currentViewId || !filters.length) return;
return Promise.all(
filters.map((filter) =>
updateViewFilterMutation({
variables: {
data: {
displayValue: filter.displayValue ?? filter.value,
operand: filter.operand,
value: filter.value,
},
where: {
viewId_key: { key: filter.key, viewId: currentViewId },
},
},
}),
),
);
},
[currentViewId, updateViewFilterMutation],
);
const deleteViewFilters = useCallback(
(filterKeys: string[]) => {
if (!currentViewId || !filterKeys.length) return;
return deleteViewFiltersMutation({
variables: {
where: {
key: { in: filterKeys },
viewId: { equals: currentViewId },
},
},
});
},
[currentViewId, deleteViewFiltersMutation],
);
const persistFilters = useCallback(async () => {
if (!currentViewId) return;
const filtersToCreate = filters.filter(
(filter) => !savedFiltersByKey[filter.key],
);
await createViewFilters(filtersToCreate);
const filtersToUpdate = filters.filter(
(filter) =>
savedFiltersByKey[filter.key] &&
(savedFiltersByKey[filter.key].operand !== filter.operand ||
savedFiltersByKey[filter.key].value !== filter.value),
);
await updateViewFilters(filtersToUpdate);
const filterKeys = filters.map((filter) => filter.key);
const filterKeysToDelete = Object.keys(savedFiltersByKey).filter(
(previousFilterKey) => !filterKeys.includes(previousFilterKey),
);
await deleteViewFilters(filterKeysToDelete);
return refetch();
}, [
currentViewId,
filters,
createViewFilters,
updateViewFilters,
savedFiltersByKey,
deleteViewFilters,
refetch,
]);
return { persistFilters };
};

View File

@ -1,10 +1,9 @@
import { useCallback, useEffect } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import {
sortsByKeyScopedState,
sortScopedState,
} from '@/ui/filter-n-sort/states/sortScopedState';
import { savedSortsByKeyScopedSelector } from '@/ui/filter-n-sort/states/savedSortsByKeyScopedSelector';
import { savedSortsScopedState } from '@/ui/filter-n-sort/states/savedSortsScopedState';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import type {
SelectedSortType,
SortType,
@ -20,8 +19,7 @@ import {
useUpdateViewSortMutation,
ViewSortDirection,
} from '~/generated/graphql';
import { GET_VIEW_SORTS } from '../graphql/queries/getViewSorts';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useViewSorts = <SortField>({
availableSorts,
@ -32,20 +30,18 @@ export const useViewSorts = <SortField>({
currentTableViewIdState,
TableRecoilScopeContext,
);
const [, setSorts] = useRecoilScopedState(
sortScopedState,
const [sorts, setSorts] = useRecoilScopedState(
sortsScopedState,
TableRecoilScopeContext,
);
const sortsByKey = useRecoilScopedValue(
sortsByKeyScopedState,
TableRecoilScopeContext,
const [, setSavedSorts] = useRecoilState(
savedSortsScopedState(currentViewId),
);
const savedSortsByKey = useRecoilValue(
savedSortsByKeyScopedSelector(currentViewId),
);
useEffect(() => {
if (!currentViewId) setSorts([]);
}, [currentViewId, setSorts]);
useGetViewSortsQuery({
const { refetch } = useGetViewSortsQuery({
skip: !currentViewId,
variables: {
where: {
@ -53,23 +49,26 @@ export const useViewSorts = <SortField>({
},
},
onCompleted: (data) => {
setSorts(
data.viewSorts
.map((viewSort) => {
const availableSort = availableSorts.find(
(sort) => sort.key === viewSort.key,
);
const nextSorts = data.viewSorts
.map((viewSort) => {
const availableSort = availableSorts.find(
(sort) => sort.key === viewSort.key,
);
return availableSort
? {
...availableSort,
label: viewSort.name,
order: viewSort.direction.toLowerCase(),
}
: undefined;
})
.filter((sort): sort is SelectedSortType<SortField> => !!sort),
);
return availableSort
? {
...availableSort,
label: viewSort.name,
order: viewSort.direction.toLowerCase(),
}
: undefined;
})
.filter((sort): sort is SelectedSortType<SortField> => !!sort);
if (!isDeeplyEqual(sorts, nextSorts)) {
setSavedSorts(nextSorts);
setSorts(nextSorts);
}
},
});
@ -90,7 +89,6 @@ export const useViewSorts = <SortField>({
viewId: currentViewId,
})),
},
refetchQueries: [getOperationName(GET_VIEW_SORTS) ?? ''],
});
},
[createViewSortsMutation, currentViewId],
@ -111,7 +109,6 @@ export const useViewSorts = <SortField>({
viewId_key: { key: sort.key, viewId: currentViewId },
},
},
refetchQueries: [getOperationName(GET_VIEW_SORTS) ?? ''],
}),
),
);
@ -130,45 +127,40 @@ export const useViewSorts = <SortField>({
viewId: { equals: currentViewId },
},
},
refetchQueries: [getOperationName(GET_VIEW_SORTS) ?? ''],
});
},
[currentViewId, deleteViewSortsMutation],
);
const handleSortsChange = useCallback(
async (nextSorts: SelectedSortType<SortField>[]) => {
if (!currentViewId) return;
const persistSorts = useCallback(async () => {
if (!currentViewId) return;
setSorts(nextSorts);
const sortsToCreate = sorts.filter((sort) => !savedSortsByKey[sort.key]);
await createViewSorts(sortsToCreate);
const sortsToCreate = nextSorts.filter(
(nextSort) => !sortsByKey[nextSort.key],
);
await createViewSorts(sortsToCreate);
const sortsToUpdate = sorts.filter(
(sort) =>
savedSortsByKey[sort.key] &&
savedSortsByKey[sort.key].order !== sort.order,
);
await updateViewSorts(sortsToUpdate);
const sortsToUpdate = nextSorts.filter(
(nextSort) =>
sortsByKey[nextSort.key] &&
sortsByKey[nextSort.key].order !== nextSort.order,
);
await updateViewSorts(sortsToUpdate);
const sortKeys = sorts.map((sort) => sort.key);
const sortKeysToDelete = Object.keys(savedSortsByKey).filter(
(previousSortKey) => !sortKeys.includes(previousSortKey),
);
await deleteViewSorts(sortKeysToDelete);
const nextSortKeys = nextSorts.map((nextSort) => nextSort.key);
const sortKeysToDelete = Object.keys(sortsByKey).filter(
(previousSortKey) => !nextSortKeys.includes(previousSortKey),
);
return deleteViewSorts(sortKeysToDelete);
},
[
createViewSorts,
currentViewId,
deleteViewSorts,
setSorts,
sortsByKey,
updateViewSorts,
],
);
return refetch();
}, [
currentViewId,
sorts,
createViewSorts,
updateViewSorts,
savedSortsByKey,
deleteViewSorts,
refetch,
]);
return { handleSortsChange };
return { persistSorts };
};