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:
@ -4,6 +4,7 @@ import { useRecoilState } from 'recoil';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { FilterOperand } from '@/ui/filter-n-sort/types/FilterOperand';
|
||||
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';
|
||||
@ -37,10 +38,10 @@ export function useTasks() {
|
||||
if (currentUser && !filters.length) {
|
||||
setFilters([
|
||||
{
|
||||
field: 'assigneeId',
|
||||
key: 'assigneeId',
|
||||
type: 'entity',
|
||||
value: currentUser.id,
|
||||
operand: 'is',
|
||||
operand: FilterOperand.Is,
|
||||
displayValue: currentUser.displayName,
|
||||
displayAvatarUrl: currentUser.avatarUrl ?? undefined,
|
||||
},
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { companyViewFields } from '@/companies/constants/companyViewFields';
|
||||
import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries';
|
||||
import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyTableContextMenuEntries';
|
||||
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
|
||||
import { sortsOrderByScopedSelector } from '@/ui/filter-n-sort/states/sortsOrderByScopedSelector';
|
||||
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
|
||||
import { EntityTable } from '@/ui/table/components/EntityTable';
|
||||
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
|
||||
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
|
||||
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useTableViewFields } from '@/views/hooks/useTableViewFields';
|
||||
import { useTableViews } from '@/views/hooks/useTableViews';
|
||||
import { useViewFilters } from '@/views/hooks/useViewFilters';
|
||||
import { useViewSorts } from '@/views/hooks/useViewSorts';
|
||||
import {
|
||||
SortOrder,
|
||||
@ -26,12 +26,8 @@ import { companiesFilters } from '~/pages/companies/companies-filters';
|
||||
import { availableSorts } from '~/pages/companies/companies-sorts';
|
||||
|
||||
export function CompanyTable() {
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentTableViewIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const orderBy = useRecoilScopedValue(
|
||||
sortsOrderByScopedState,
|
||||
sortsOrderByScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const [updateEntityMutation] = useUpdateOneCompanyMutation();
|
||||
@ -44,7 +40,10 @@ export function CompanyTable() {
|
||||
viewFieldDefinitions: companyViewFields,
|
||||
});
|
||||
|
||||
const { handleSortsChange } = useViewSorts({ availableSorts });
|
||||
const { persistFilters } = useViewFilters({
|
||||
availableFilters: companiesFilters,
|
||||
});
|
||||
const { persistSorts } = useViewSorts({ availableSorts });
|
||||
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
|
||||
|
||||
const filters = useRecoilScopedValue(
|
||||
@ -59,6 +58,11 @@ export function CompanyTable() {
|
||||
const { setContextMenuEntries } = useCompanyTableContextMenuEntries();
|
||||
const { setActionBarEntries } = useCompanyTableActionBarEntries();
|
||||
|
||||
const handleViewSubmit = useCallback(async () => {
|
||||
await persistFilters();
|
||||
await persistSorts();
|
||||
}, [persistFilters, persistSorts]);
|
||||
|
||||
function handleImport() {
|
||||
openCompanySpreadsheetImport();
|
||||
}
|
||||
@ -68,15 +72,7 @@ export function CompanyTable() {
|
||||
<GenericEntityTableData
|
||||
getRequestResultKey="companies"
|
||||
useGetRequest={useGetCompaniesQuery}
|
||||
orderBy={
|
||||
orderBy.length
|
||||
? orderBy
|
||||
: [
|
||||
{
|
||||
createdAt: SortOrder.Desc,
|
||||
},
|
||||
]
|
||||
}
|
||||
orderBy={orderBy.length ? orderBy : [{ createdAt: SortOrder.Desc }]}
|
||||
whereFilters={whereFilters}
|
||||
filterDefinitionArray={companiesFilters}
|
||||
setContextMenuEntries={setContextMenuEntries}
|
||||
@ -86,8 +82,8 @@ export function CompanyTable() {
|
||||
viewName="All Companies"
|
||||
availableSorts={availableSorts}
|
||||
onColumnsChange={handleColumnsChange}
|
||||
onSortsUpdate={currentViewId ? handleSortsChange : undefined}
|
||||
onViewsChange={handleViewsChange}
|
||||
onViewSubmit={handleViewSubmit}
|
||||
onImport={handleImport}
|
||||
updateEntityMutation={({
|
||||
variables,
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { peopleViewFields } from '@/people/constants/peopleViewFields';
|
||||
import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries';
|
||||
import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableActionBarEntries';
|
||||
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
|
||||
import { sortsOrderByScopedSelector } from '@/ui/filter-n-sort/states/sortsOrderByScopedSelector';
|
||||
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
|
||||
import { EntityTable } from '@/ui/table/components/EntityTable';
|
||||
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
|
||||
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
|
||||
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useTableViewFields } from '@/views/hooks/useTableViewFields';
|
||||
import { useTableViews } from '@/views/hooks/useTableViews';
|
||||
import { useViewFilters } from '@/views/hooks/useViewFilters';
|
||||
import { useViewSorts } from '@/views/hooks/useViewSorts';
|
||||
import {
|
||||
SortOrder,
|
||||
@ -26,12 +26,8 @@ import { peopleFilters } from '~/pages/people/people-filters';
|
||||
import { availableSorts } from '~/pages/people/people-sorts';
|
||||
|
||||
export function PeopleTable() {
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentTableViewIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const orderBy = useRecoilScopedValue(
|
||||
sortsOrderByScopedState,
|
||||
sortsOrderByScopedSelector,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const [updateEntityMutation] = useUpdateOnePersonMutation();
|
||||
@ -44,7 +40,10 @@ export function PeopleTable() {
|
||||
objectName: objectId,
|
||||
viewFieldDefinitions: peopleViewFields,
|
||||
});
|
||||
const { handleSortsChange } = useViewSorts({ availableSorts });
|
||||
const { persistFilters } = useViewFilters({
|
||||
availableFilters: peopleFilters,
|
||||
});
|
||||
const { persistSorts } = useViewSorts({ availableSorts });
|
||||
|
||||
const filters = useRecoilScopedValue(
|
||||
filtersScopedState,
|
||||
@ -58,6 +57,11 @@ export function PeopleTable() {
|
||||
const { setContextMenuEntries } = usePersonTableContextMenuEntries();
|
||||
const { setActionBarEntries } = usePersonTableActionBarEntries();
|
||||
|
||||
const handleViewSubmit = useCallback(async () => {
|
||||
await persistFilters();
|
||||
await persistSorts();
|
||||
}, [persistFilters, persistSorts]);
|
||||
|
||||
function handleImport() {
|
||||
openPersonSpreadsheetImport();
|
||||
}
|
||||
@ -67,15 +71,7 @@ export function PeopleTable() {
|
||||
<GenericEntityTableData
|
||||
getRequestResultKey="people"
|
||||
useGetRequest={useGetPeopleQuery}
|
||||
orderBy={
|
||||
orderBy.length
|
||||
? orderBy
|
||||
: [
|
||||
{
|
||||
createdAt: SortOrder.Desc,
|
||||
},
|
||||
]
|
||||
}
|
||||
orderBy={orderBy.length ? orderBy : [{ createdAt: SortOrder.Desc }]}
|
||||
whereFilters={whereFilters}
|
||||
filterDefinitionArray={peopleFilters}
|
||||
setContextMenuEntries={setContextMenuEntries}
|
||||
@ -85,8 +81,8 @@ export function PeopleTable() {
|
||||
viewName="All People"
|
||||
availableSorts={availableSorts}
|
||||
onColumnsChange={handleColumnsChange}
|
||||
onSortsUpdate={currentViewId ? handleSortsChange : undefined}
|
||||
onViewsChange={handleViewsChange}
|
||||
onViewSubmit={handleViewSubmit}
|
||||
onImport={handleImport}
|
||||
updateEntityMutation={({
|
||||
variables,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ButtonPosition, ButtonProps } from './Button';
|
||||
@ -9,13 +9,15 @@ const StyledButtonGroupContainer = styled.div`
|
||||
`;
|
||||
|
||||
type ButtonGroupProps = Pick<ButtonProps, 'variant' | 'size'> & {
|
||||
children: React.ReactElement[];
|
||||
children: ReactNode[];
|
||||
};
|
||||
|
||||
export function ButtonGroup({ children, variant, size }: ButtonGroupProps) {
|
||||
return (
|
||||
<StyledButtonGroupContainer>
|
||||
{React.Children.map(children, (child, index) => {
|
||||
if (!React.isValidElement(child)) return null;
|
||||
|
||||
let position: ButtonPosition;
|
||||
|
||||
if (index === 0) {
|
||||
|
||||
@ -28,7 +28,7 @@ export function FilterDropdownDateSearchInput({
|
||||
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
|
||||
|
||||
upsertFilter({
|
||||
field: filterDefinitionUsedInDropdown.field,
|
||||
key: filterDefinitionUsedInDropdown.key,
|
||||
type: filterDefinitionUsedInDropdown.type,
|
||||
value: date.toISOString(),
|
||||
operand: selectedOperandInDropdown,
|
||||
|
||||
@ -51,14 +51,14 @@ export function FilterDropdownEntitySearchSelect({
|
||||
selectedEntity.id === filterDropdownSelectedEntityId;
|
||||
|
||||
if (clickedOnAlreadySelectedEntity) {
|
||||
removeFilter(filterDefinitionUsedInDropdown.field);
|
||||
removeFilter(filterDefinitionUsedInDropdown.key);
|
||||
setFilterDropdownSelectedEntityId(null);
|
||||
} else {
|
||||
setFilterDropdownSelectedEntityId(selectedEntity.id);
|
||||
|
||||
upsertFilter({
|
||||
displayValue: selectedEntity.name,
|
||||
field: filterDefinitionUsedInDropdown.field,
|
||||
key: filterDefinitionUsedInDropdown.key,
|
||||
operand: selectedOperandInDropdown,
|
||||
type: filterDefinitionUsedInDropdown.type,
|
||||
value: selectedEntity.id,
|
||||
|
||||
@ -34,10 +34,10 @@ export function FilterDropdownNumberSearchInput({
|
||||
placeholder={filterDefinitionUsedInDropdown.label}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.value === '') {
|
||||
removeFilter(filterDefinitionUsedInDropdown.field);
|
||||
removeFilter(filterDefinitionUsedInDropdown.key);
|
||||
} else {
|
||||
upsertFilter({
|
||||
field: filterDefinitionUsedInDropdown.field,
|
||||
key: filterDefinitionUsedInDropdown.key,
|
||||
type: filterDefinitionUsedInDropdown.type,
|
||||
value: event.target.value,
|
||||
operand: selectedOperandInDropdown,
|
||||
|
||||
@ -48,7 +48,7 @@ export function FilterDropdownOperandSelect({
|
||||
|
||||
if (filterDefinitionUsedInDropdown && filterCurrentlyEdited) {
|
||||
upsertFilter({
|
||||
field: filterCurrentlyEdited.field,
|
||||
key: filterCurrentlyEdited.key,
|
||||
displayValue: filterCurrentlyEdited.displayValue,
|
||||
operand: newOperand,
|
||||
type: filterCurrentlyEdited.type,
|
||||
|
||||
@ -44,10 +44,10 @@ export function FilterDropdownTextSearchInput({
|
||||
setFilterDropdownSearchInput(event.target.value);
|
||||
|
||||
if (event.target.value === '') {
|
||||
removeFilter(filterDefinitionUsedInDropdown.field);
|
||||
removeFilter(filterDefinitionUsedInDropdown.key);
|
||||
} else {
|
||||
upsertFilter({
|
||||
field: filterDefinitionUsedInDropdown.field,
|
||||
key: filterDefinitionUsedInDropdown.key,
|
||||
type: filterDefinitionUsedInDropdown.type,
|
||||
value: event.target.value,
|
||||
operand: selectedOperandInDropdown,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Context } from 'react';
|
||||
import type { Context, ReactNode } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
@ -26,6 +26,7 @@ type OwnProps<SortField> = {
|
||||
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
|
||||
onCancelClick: () => void;
|
||||
hasFilterButton?: boolean;
|
||||
rightComponent?: ReactNode;
|
||||
};
|
||||
|
||||
const StyledBar = styled.div`
|
||||
@ -97,6 +98,7 @@ function SortAndFilterBar<SortField>({
|
||||
onRemoveSort,
|
||||
onCancelClick,
|
||||
hasFilterButton = false,
|
||||
rightComponent,
|
||||
}: OwnProps<SortField>) {
|
||||
const theme = useTheme();
|
||||
|
||||
@ -117,7 +119,7 @@ function SortAndFilterBar<SortField>({
|
||||
|
||||
const filtersWithDefinition = filters.map((filter) => {
|
||||
const filterDefinition = availableFilters.find((availableFilter) => {
|
||||
return availableFilter.field === filter.field;
|
||||
return availableFilter.key === filter.key;
|
||||
});
|
||||
|
||||
return {
|
||||
@ -170,15 +172,15 @@ function SortAndFilterBar<SortField>({
|
||||
{filtersWithDefinition.map((filter) => {
|
||||
return (
|
||||
<SortOrFilterChip
|
||||
key={filter.field}
|
||||
key={filter.key}
|
||||
labelKey={filter.label}
|
||||
labelValue={`${getOperandLabelShort(filter.operand)} ${
|
||||
filter.displayValue
|
||||
}`}
|
||||
id={filter.field}
|
||||
id={filter.key}
|
||||
icon={filter.icon}
|
||||
onRemove={() => {
|
||||
removeFilter(filter.field);
|
||||
removeFilter(filter.key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -190,18 +192,19 @@ function SortAndFilterBar<SortField>({
|
||||
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
color={theme.font.color.tertiary}
|
||||
icon={<IconPlus size={theme.icon.size.md} />}
|
||||
label={`Add filter`}
|
||||
label="Add filter"
|
||||
/>
|
||||
)}
|
||||
</StyledFilterContainer>
|
||||
{filters.length + sorts.length > 0 && (
|
||||
<StyledCancelButton
|
||||
data-testid={'cancel-button'}
|
||||
data-testid="cancel-button"
|
||||
onClick={handleCancelClick}
|
||||
>
|
||||
Cancel
|
||||
</StyledCancelButton>
|
||||
)}
|
||||
{rightComponent}
|
||||
</StyledBar>
|
||||
);
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export function useFilterCurrentlyEdited(context: Context<string | null>) {
|
||||
|
||||
return useMemo(() => {
|
||||
return filters.find(
|
||||
(filter) => filter.field === filterDefinitionUsedInDropdown?.field,
|
||||
(filter) => filter.key === filterDefinitionUsedInDropdown?.key,
|
||||
);
|
||||
}, [filterDefinitionUsedInDropdown, filters]);
|
||||
}
|
||||
|
||||
@ -7,10 +7,10 @@ import { filtersScopedState } from '../states/filtersScopedState';
|
||||
export function useRemoveFilter(context: Context<string | null>) {
|
||||
const [, setFilters] = useRecoilScopedState(filtersScopedState, context);
|
||||
|
||||
return function removeFilter(filterField: string) {
|
||||
return function removeFilter(filterKey: string) {
|
||||
setFilters((filters) => {
|
||||
return filters.filter((filter) => {
|
||||
return filter.field !== filterField;
|
||||
return filter.key !== filterKey;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@ -13,7 +13,7 @@ export function useUpsertFilter(context: Context<string | null>) {
|
||||
setFilters((filters) => {
|
||||
return produce(filters, (filtersDraft) => {
|
||||
const index = filtersDraft.findIndex(
|
||||
(filter) => filter.field === filterToUpsert.field,
|
||||
(filter) => filter.key === filterToUpsert.key,
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { Filter } from '../types/Filter';
|
||||
import type { Filter } from '../types/Filter';
|
||||
|
||||
export const filtersScopedState = atomFamily<Filter[], string>({
|
||||
key: 'filtersScopedState',
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import type { Filter } from '../types/Filter';
|
||||
|
||||
import { savedFiltersScopedState } from './savedFiltersScopedState';
|
||||
|
||||
export const savedFiltersByKeyScopedSelector = selectorFamily({
|
||||
key: 'savedFiltersByKeyScopedSelector',
|
||||
get:
|
||||
(param: string | undefined) =>
|
||||
({ get }) =>
|
||||
get(savedFiltersScopedState(param)).reduce<Record<string, Filter>>(
|
||||
(result, filter) => ({ ...result, [filter.key]: filter }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import type { Filter } from '../types/Filter';
|
||||
|
||||
export const savedFiltersScopedState = atomFamily<Filter[], string | undefined>(
|
||||
{
|
||||
key: 'savedFiltersScopedState',
|
||||
default: [],
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,15 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import type { SelectedSortType } from '../types/interface';
|
||||
|
||||
import { savedSortsScopedState } from './savedSortsScopedState';
|
||||
|
||||
export const savedSortsByKeyScopedSelector = selectorFamily({
|
||||
key: 'savedSortsByKeyScopedSelector',
|
||||
get:
|
||||
(viewId: string | undefined) =>
|
||||
({ get }) =>
|
||||
get(savedSortsScopedState(viewId)).reduce<
|
||||
Record<string, SelectedSortType<any>>
|
||||
>((result, sort) => ({ ...result, [sort.key]: sort }), {}),
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import type { SelectedSortType } from '../types/interface';
|
||||
|
||||
export const savedSortsScopedState = atomFamily<
|
||||
SelectedSortType<any>[],
|
||||
string | undefined
|
||||
>({
|
||||
key: 'savedSortsScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -1,28 +0,0 @@
|
||||
import { atomFamily, selectorFamily } from 'recoil';
|
||||
|
||||
import { reduceSortsToOrderBy } from '../helpers';
|
||||
import { SelectedSortType } from '../types/interface';
|
||||
|
||||
export const sortScopedState = atomFamily<SelectedSortType<any>[], string>({
|
||||
key: 'sortScopedState',
|
||||
default: [],
|
||||
});
|
||||
|
||||
export const sortsByKeyScopedState = selectorFamily({
|
||||
key: 'sortsByKeyScopedState',
|
||||
get:
|
||||
(param: string) =>
|
||||
({ get }) =>
|
||||
get(sortScopedState(param)).reduce<Record<string, SelectedSortType<any>>>(
|
||||
(result, sort) => ({ ...result, [sort.key]: sort }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
|
||||
export const sortsOrderByScopedState = selectorFamily({
|
||||
key: 'sortsOrderByScopedState',
|
||||
get:
|
||||
(param: string) =>
|
||||
({ get }) =>
|
||||
reduceSortsToOrderBy(get(sortScopedState(param))),
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { reduceSortsToOrderBy } from '../helpers';
|
||||
|
||||
import { sortsScopedState } from './sortsScopedState';
|
||||
|
||||
export const sortsOrderByScopedSelector = selectorFamily({
|
||||
key: 'sortsOrderByScopedSelector',
|
||||
get:
|
||||
(param: string) =>
|
||||
({ get }) =>
|
||||
reduceSortsToOrderBy(get(sortsScopedState(param))),
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import type { SelectedSortType } from '../types/interface';
|
||||
|
||||
export const sortsScopedState = atomFamily<SelectedSortType<any>[], string>({
|
||||
key: 'sortsScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -2,7 +2,7 @@ import { FilterOperand } from './FilterOperand';
|
||||
import { FilterType } from './FilterType';
|
||||
|
||||
export type Filter = {
|
||||
field: string;
|
||||
key: string;
|
||||
type: FilterType;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { FilterType } from './FilterType';
|
||||
|
||||
export type FilterDefinition = {
|
||||
field: string;
|
||||
key: string;
|
||||
label: string;
|
||||
icon: JSX.Element;
|
||||
type: FilterType;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { FilterDefinition } from './FilterDefinition';
|
||||
|
||||
export type FilterDefinitionByEntity<T> = FilterDefinition & {
|
||||
field: keyof T;
|
||||
key: keyof T;
|
||||
};
|
||||
|
||||
@ -1,7 +1 @@
|
||||
export type FilterOperand =
|
||||
| 'contains'
|
||||
| 'does-not-contain'
|
||||
| 'greater-than'
|
||||
| 'less-than'
|
||||
| 'is'
|
||||
| 'is-not';
|
||||
export { ViewFilterOperand as FilterOperand } from '~/generated/graphql';
|
||||
|
||||
@ -2,17 +2,17 @@ import { FilterOperand } from '../types/FilterOperand';
|
||||
|
||||
export function getOperandLabel(operand: FilterOperand | null | undefined) {
|
||||
switch (operand) {
|
||||
case 'contains':
|
||||
case FilterOperand.Contains:
|
||||
return 'Contains';
|
||||
case 'does-not-contain':
|
||||
case FilterOperand.DoesNotContain:
|
||||
return "Doesn't contain";
|
||||
case 'greater-than':
|
||||
case FilterOperand.GreaterThan:
|
||||
return 'Greater than';
|
||||
case 'less-than':
|
||||
case FilterOperand.LessThan:
|
||||
return 'Less than';
|
||||
case 'is':
|
||||
case FilterOperand.Is:
|
||||
return 'Is';
|
||||
case 'is-not':
|
||||
case FilterOperand.IsNot:
|
||||
return 'Is not';
|
||||
default:
|
||||
return '';
|
||||
@ -22,15 +22,15 @@ export function getOperandLabelShort(
|
||||
operand: FilterOperand | null | undefined,
|
||||
) {
|
||||
switch (operand) {
|
||||
case 'is':
|
||||
case 'contains':
|
||||
case FilterOperand.Is:
|
||||
case FilterOperand.Contains:
|
||||
return ': ';
|
||||
case 'is-not':
|
||||
case 'does-not-contain':
|
||||
case FilterOperand.IsNot:
|
||||
case FilterOperand.DoesNotContain:
|
||||
return ': Not';
|
||||
case 'greater-than':
|
||||
case FilterOperand.GreaterThan:
|
||||
return '\u00A0> ';
|
||||
case 'less-than':
|
||||
case FilterOperand.LessThan:
|
||||
return '\u00A0< ';
|
||||
default:
|
||||
return ': ';
|
||||
|
||||
@ -6,12 +6,12 @@ export function getOperandsForFilterType(
|
||||
): FilterOperand[] {
|
||||
switch (filterType) {
|
||||
case 'text':
|
||||
return ['contains', 'does-not-contain'];
|
||||
return [FilterOperand.Contains, FilterOperand.DoesNotContain];
|
||||
case 'number':
|
||||
case 'date':
|
||||
return ['greater-than', 'less-than'];
|
||||
return [FilterOperand.GreaterThan, FilterOperand.LessThan];
|
||||
case 'entity':
|
||||
return ['is', 'is-not'];
|
||||
return [FilterOperand.Is, FilterOperand.IsNot];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
import { QueryMode } from '~/generated/graphql';
|
||||
|
||||
import { Filter } from '../types/Filter';
|
||||
import { FilterOperand } from '../types/FilterOperand';
|
||||
|
||||
export function turnFilterIntoWhereClause(filter: Filter) {
|
||||
switch (filter.type) {
|
||||
case 'text':
|
||||
switch (filter.operand) {
|
||||
case 'contains':
|
||||
case FilterOperand.Contains:
|
||||
return {
|
||||
[filter.field]: {
|
||||
[filter.key]: {
|
||||
contains: filter.value,
|
||||
mode: QueryMode.Insensitive,
|
||||
},
|
||||
};
|
||||
case 'does-not-contain':
|
||||
case FilterOperand.DoesNotContain:
|
||||
return {
|
||||
[filter.field]: {
|
||||
[filter.key]: {
|
||||
not: {
|
||||
contains: filter.value,
|
||||
mode: QueryMode.Insensitive,
|
||||
@ -29,15 +30,15 @@ export function turnFilterIntoWhereClause(filter: Filter) {
|
||||
}
|
||||
case 'number':
|
||||
switch (filter.operand) {
|
||||
case 'greater-than':
|
||||
case FilterOperand.GreaterThan:
|
||||
return {
|
||||
[filter.field]: {
|
||||
[filter.key]: {
|
||||
gte: parseFloat(filter.value),
|
||||
},
|
||||
};
|
||||
case 'less-than':
|
||||
case FilterOperand.LessThan:
|
||||
return {
|
||||
[filter.field]: {
|
||||
[filter.key]: {
|
||||
lte: parseFloat(filter.value),
|
||||
},
|
||||
};
|
||||
@ -48,15 +49,15 @@ export function turnFilterIntoWhereClause(filter: Filter) {
|
||||
}
|
||||
case 'date':
|
||||
switch (filter.operand) {
|
||||
case 'greater-than':
|
||||
case FilterOperand.GreaterThan:
|
||||
return {
|
||||
[filter.field]: {
|
||||
[filter.key]: {
|
||||
gte: filter.value,
|
||||
},
|
||||
};
|
||||
case 'less-than':
|
||||
case FilterOperand.LessThan:
|
||||
return {
|
||||
[filter.field]: {
|
||||
[filter.key]: {
|
||||
lte: filter.value,
|
||||
},
|
||||
};
|
||||
@ -67,15 +68,15 @@ export function turnFilterIntoWhereClause(filter: Filter) {
|
||||
}
|
||||
case 'entity':
|
||||
switch (filter.operand) {
|
||||
case 'is':
|
||||
case FilterOperand.Is:
|
||||
return {
|
||||
[filter.field]: {
|
||||
[filter.key]: {
|
||||
equals: filter.value,
|
||||
},
|
||||
};
|
||||
case 'is-not':
|
||||
case FilterOperand.IsNot:
|
||||
return {
|
||||
[filter.field]: {
|
||||
[filter.key]: {
|
||||
not: { equals: filter.value },
|
||||
},
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@ import type {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
@ -97,8 +97,8 @@ type OwnProps<SortField> = {
|
||||
viewIcon?: React.ReactNode;
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
onViewsChange?: (views: TableView[]) => void;
|
||||
onViewSubmit?: () => void;
|
||||
onImport?: () => void;
|
||||
updateEntityMutation: any;
|
||||
};
|
||||
@ -107,8 +107,8 @@ export function EntityTable<SortField>({
|
||||
viewName,
|
||||
availableSorts,
|
||||
onColumnsChange,
|
||||
onSortsUpdate,
|
||||
onViewsChange,
|
||||
onViewSubmit,
|
||||
onImport,
|
||||
updateEntityMutation,
|
||||
}: OwnProps<SortField>) {
|
||||
@ -136,8 +136,8 @@ export function EntityTable<SortField>({
|
||||
viewName={viewName}
|
||||
availableSorts={availableSorts}
|
||||
onColumnsChange={onColumnsChange}
|
||||
onSortsUpdate={onSortsUpdate}
|
||||
onViewsChange={onViewsChange}
|
||||
onViewSubmit={onViewSubmit}
|
||||
onImport={onImport}
|
||||
/>
|
||||
<StyledTableWrapper>
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { Button, ButtonSize } from '@/ui/button/components/Button';
|
||||
import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
|
||||
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuContainer } from '@/ui/filter-n-sort/components/DropdownMenuContainer';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
|
||||
import { savedSortsScopedState } from '@/ui/filter-n-sort/states/savedSortsScopedState';
|
||||
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
|
||||
import { IconChevronDown, IconPlus } from '@/ui/icon';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
|
||||
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import {
|
||||
currentTableViewIdState,
|
||||
tableViewEditModeState,
|
||||
} from '../../states/tableViewsState';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: inline-flex;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledDropdownMenuContainer = styled(DropdownMenuContainer)`
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
type TableUpdateViewButtonGroupProps = {
|
||||
onViewSubmit?: () => void;
|
||||
HotkeyScope: string;
|
||||
};
|
||||
|
||||
export const TableUpdateViewButtonGroup = ({
|
||||
onViewSubmit,
|
||||
HotkeyScope,
|
||||
}: TableUpdateViewButtonGroupProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
|
||||
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentTableViewIdState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const setViewEditMode = useSetRecoilState(tableViewEditModeState);
|
||||
|
||||
const handleArrowDownButtonClick = useCallback(() => {
|
||||
setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
|
||||
}, []);
|
||||
|
||||
const handleCreateViewButtonClick = useCallback(() => {
|
||||
setViewEditMode({ mode: 'create', viewId: undefined });
|
||||
setIsDropdownOpen(false);
|
||||
}, [setViewEditMode]);
|
||||
|
||||
const handleDropdownClose = useCallback(() => {
|
||||
setIsDropdownOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleViewSubmit = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async () => {
|
||||
await Promise.resolve(onViewSubmit?.());
|
||||
|
||||
const selectedFilters = await snapshot.getPromise(
|
||||
filtersScopedState(tableScopeId),
|
||||
);
|
||||
set(savedFiltersScopedState(currentViewId), selectedFilters);
|
||||
|
||||
const selectedSorts = await snapshot.getPromise(
|
||||
sortsScopedState(tableScopeId),
|
||||
);
|
||||
set(savedSortsScopedState(currentViewId), selectedSorts);
|
||||
},
|
||||
[currentViewId, onViewSubmit],
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Enter, Key.Escape],
|
||||
handleDropdownClose,
|
||||
HotkeyScope,
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ButtonGroup size={ButtonSize.Small}>
|
||||
<Button
|
||||
title="Update view"
|
||||
disabled={!currentViewId}
|
||||
onClick={handleViewSubmit}
|
||||
/>
|
||||
<Button
|
||||
size={ButtonSize.Small}
|
||||
icon={<IconChevronDown />}
|
||||
onClick={handleArrowDownButtonClick}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<StyledDropdownMenuContainer onClose={handleDropdownClose}>
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItem onClick={handleCreateViewButtonClick}>
|
||||
<IconPlus size={theme.icon.size.md} />
|
||||
Create view
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItemsContainer>
|
||||
</StyledDropdownMenuContainer>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,13 +1,17 @@
|
||||
import { type MouseEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
||||
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
|
||||
import { savedSortsScopedState } from '@/ui/filter-n-sort/states/savedSortsScopedState';
|
||||
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconList,
|
||||
@ -23,6 +27,7 @@ import {
|
||||
tableViewsState,
|
||||
} from '@/ui/table/states/tableViewsState';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
|
||||
@ -59,6 +64,8 @@ export const TableViewsDropdownButton = ({
|
||||
const theme = useTheme();
|
||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||
|
||||
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
|
||||
|
||||
const currentView = useRecoilScopedValue(
|
||||
currentTableViewState,
|
||||
TableRecoilScopeContext,
|
||||
@ -78,11 +85,21 @@ export const TableViewsDropdownButton = ({
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const handleViewSelect = useCallback(
|
||||
(viewId?: string) => {
|
||||
setCurrentViewId(viewId);
|
||||
setIsUnfolded(false);
|
||||
},
|
||||
const handleViewSelect = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async (viewId?: string) => {
|
||||
const savedFilters = await snapshot.getPromise(
|
||||
savedFiltersScopedState(viewId),
|
||||
);
|
||||
const savedSorts = await snapshot.getPromise(
|
||||
savedSortsScopedState(viewId),
|
||||
);
|
||||
|
||||
set(filtersScopedState(tableScopeId), savedFilters);
|
||||
set(sortsScopedState(tableScopeId), savedSorts);
|
||||
setCurrentViewId(viewId);
|
||||
setIsUnfolded(false);
|
||||
},
|
||||
[setCurrentViewId],
|
||||
);
|
||||
|
||||
|
||||
@ -7,13 +7,14 @@ import type {
|
||||
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
|
||||
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
|
||||
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
|
||||
import { sortScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
|
||||
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
|
||||
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { TableOptionsDropdownButton } from '@/ui/table/options/components/TableOptionsDropdownButton';
|
||||
import { TopBar } from '@/ui/top-bar/TopBar';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { TableUpdateViewButtonGroup } from '../../options/components/TableUpdateViewButtonGroup';
|
||||
import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton';
|
||||
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
|
||||
import type { TableView } from '../../states/tableViewsState';
|
||||
@ -24,8 +25,8 @@ type OwnProps<SortField> = {
|
||||
viewName: string;
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
onViewsChange?: (views: TableView[]) => void;
|
||||
onViewSubmit?: () => void;
|
||||
onImport?: () => void;
|
||||
};
|
||||
|
||||
@ -33,30 +34,29 @@ export function TableHeader<SortField>({
|
||||
viewName,
|
||||
availableSorts,
|
||||
onColumnsChange,
|
||||
onSortsUpdate,
|
||||
onViewsChange,
|
||||
onViewSubmit,
|
||||
onImport,
|
||||
}: OwnProps<SortField>) {
|
||||
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
|
||||
sortScopedState,
|
||||
sortsScopedState,
|
||||
TableRecoilScopeContext,
|
||||
);
|
||||
const handleSortsUpdate = onSortsUpdate ?? setSorts;
|
||||
|
||||
const sortSelect = useCallback(
|
||||
(newSort: SelectedSortType<SortField>) => {
|
||||
const newSorts = updateSortOrFilterByKey(sorts, newSort);
|
||||
handleSortsUpdate(newSorts);
|
||||
setSorts(newSorts);
|
||||
},
|
||||
[handleSortsUpdate, sorts],
|
||||
[setSorts, sorts],
|
||||
);
|
||||
|
||||
const sortUnselect = useCallback(
|
||||
(sortKey: string) => {
|
||||
const newSorts = sorts.filter((sort) => sort.key !== sortKey);
|
||||
handleSortsUpdate(newSorts);
|
||||
setSorts(newSorts);
|
||||
},
|
||||
[handleSortsUpdate, sorts],
|
||||
[setSorts, sorts],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -65,7 +65,7 @@ export function TableHeader<SortField>({
|
||||
<TableViewsDropdownButton
|
||||
defaultViewName={viewName}
|
||||
onViewsChange={onViewsChange}
|
||||
HotkeyScope={TableViewsHotkeyScope.Dropdown}
|
||||
HotkeyScope={TableViewsHotkeyScope.ListDropdown}
|
||||
/>
|
||||
}
|
||||
displayBottomBorder={false}
|
||||
@ -97,10 +97,14 @@ export function TableHeader<SortField>({
|
||||
context={TableRecoilScopeContext}
|
||||
sorts={sorts}
|
||||
onRemoveSort={sortUnselect}
|
||||
onCancelClick={() => {
|
||||
handleSortsUpdate([]);
|
||||
}}
|
||||
onCancelClick={() => setSorts([])}
|
||||
hasFilterButton
|
||||
rightComponent={
|
||||
<TableUpdateViewButtonGroup
|
||||
onViewSubmit={onViewSubmit}
|
||||
HotkeyScope={TableViewsHotkeyScope.CreateDropdown}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export enum TableViewsHotkeyScope {
|
||||
Dropdown = 'table-views-dropdown',
|
||||
ListDropdown = 'table-views-list-dropdown',
|
||||
CreateDropdown = 'table-views-create-dropdown',
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_VIEW_FILTERS = gql`
|
||||
mutation CreateViewFilters($data: [ViewFilterCreateManyInput!]!) {
|
||||
createManyViewFilter(data: $data) {
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_VIEW_FILTERS = gql`
|
||||
mutation DeleteViewFilters($where: ViewFilterWhereInput!) {
|
||||
deleteManyViewFilter(where: $where) {
|
||||
count
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
13
front/src/modules/views/graphql/queries/getViewFilters.ts
Normal file
13
front/src/modules/views/graphql/queries/getViewFilters.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
||||
172
front/src/modules/views/hooks/useViewFilters.ts
Normal file
172
front/src/modules/views/hooks/useViewFilters.ts
Normal 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 };
|
||||
};
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user