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:
@ -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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user