Added ability to edit filter and sort chip directly (#2968)

* - Added EditableSortChip
- Fixed EditableFilterChip onRemove not closing

* Added missing script in dependencies

* Linted files

* Finished fixing lint
This commit is contained in:
Lucas Bordeau 2023-12-13 15:24:06 +01:00 committed by GitHub
parent e3e42be723
commit bded46444d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 232 additions and 55 deletions

View File

@ -32,6 +32,7 @@
"jest": "^28.1.3",
"postcss": "^8.4.29",
"prettier": "^3.0.3",
"rimraf": "^5.0.5",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"

View File

@ -140,10 +140,9 @@ export const useFindManyRecords = <
if (isNonEmptyArray(previousEdges) && isNonEmptyArray(nextEdges)) {
newEdges = filterUniqueRecordEdgesByCursor([
// eslint-disable-next-line no-unsafe-optional-chaining
...prev?.[objectMetadataItem.namePlural]?.edges,
// eslint-disable-next-line no-unsafe-optional-chaining
...fetchMoreResult?.[objectMetadataItem.namePlural]?.edges,
...(prev?.[objectMetadataItem.namePlural]?.edges ?? []),
...(fetchMoreResult?.[objectMetadataItem.namePlural]?.edges ??
[]),
]);
}

View File

@ -11,12 +11,18 @@ import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperand
import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect';
import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput';
export const MultipleFiltersDropdownContent = () => {
type MultipleFiltersDropdownContentProps = {
filterDropdownId?: string;
};
export const MultipleFiltersDropdownContent = ({
filterDropdownId,
}: MultipleFiltersDropdownContentProps) => {
const {
isObjectFilterDropdownOperandSelectUnfolded,
filterDefinitionUsedInDropdown,
selectedOperandInDropdown,
} = useFilterDropdown();
} = useFilterDropdown({ filterDropdownId });
return (
<>

View File

@ -1,7 +1,7 @@
import { ScopedStateKey } from '@/ui/utilities/recoil-scope/scopes-internal/types/ScopedStateKey';
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
type RecordBoardScopeInternalContextType = ScopedStateKey;
type RecordBoardScopeInternalContextProps = ScopedStateKey;
export const RecordBoardScopeInternalContext =
createScopeInternalContext<RecordBoardScopeInternalContextType>();
createScopeInternalContext<RecordBoardScopeInternalContextProps>();

View File

@ -106,7 +106,7 @@ export const turnFiltersIntoObjectRecordFilters = (
);
}
break;
case 'RELATION':
case 'RELATION': {
try {
JSON.parse(rawUIFilter.value);
} catch (e) {
@ -143,6 +143,7 @@ export const turnFiltersIntoObjectRecordFilters = (
}
}
break;
}
case 'CURRENCY':
switch (rawUIFilter.operand) {
case ViewFilterOperand.GreaterThan:

View File

@ -1,4 +1,4 @@
import { Meta, StoryObj } from '@storybook/react';
import { Meta, Story, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';

View File

@ -56,6 +56,7 @@ export const Dropdown = ({
useDropdown();
const offsetMiddlewares = [];
if (dropdownOffset.x) {
offsetMiddlewares.push(offset({ crossAxis: dropdownOffset.x }));
}

View File

@ -0,0 +1,28 @@
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
import { ViewFilter } from '@/views/types/ViewFilter';
type EditableFilterChipProps = {
viewFilter: ViewFilter;
onRemove: () => void;
};
export const EditableFilterChip = ({
viewFilter,
onRemove,
}: EditableFilterChipProps) => {
const { icons } = useLazyLoadIcons();
return (
<SortOrFilterChip
key={viewFilter.fieldMetadataId}
testId={viewFilter.fieldMetadataId}
labelKey={viewFilter.definition.label}
labelValue={`${getOperandLabelShort(viewFilter.operand)} ${
viewFilter.displayValue
}`}
Icon={icons[viewFilter.definition.iconName]}
onRemove={onRemove}
/>
);
};

View File

@ -0,0 +1,79 @@
import { useEffect } from 'react';
import { MultipleFiltersDropdownContent } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { EditableFilterChip } from '@/views/components/EditableFilterChip';
import { useViewBar } from '@/views/hooks/useViewBar';
import { ViewFilter } from '@/views/types/ViewFilter';
type EditableFilterDropdownButtonProps = {
viewFilterDropdownId: string;
viewFilter: ViewFilter;
hotkeyScope: HotkeyScope;
};
export const EditableFilterDropdownButton = ({
viewFilterDropdownId,
viewFilter,
hotkeyScope,
}: EditableFilterDropdownButtonProps) => {
const {
availableFilterDefinitions,
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setSelectedFilter,
} = useFilterDropdown({
filterDropdownId: viewFilterDropdownId,
});
const { closeDropdown } = useDropdown({
dropdownScopeId: viewFilterDropdownId,
});
const { removeViewFilter } = useViewBar();
useEffect(() => {
const filterDefinition = availableFilterDefinitions.find(
(filterDefinition) =>
filterDefinition.fieldMetadataId === viewFilter.fieldMetadataId,
);
if (filterDefinition) {
setFilterDefinitionUsedInDropdown(filterDefinition);
setSelectedOperandInDropdown(viewFilter.operand);
setSelectedFilter(viewFilter);
}
}, [
availableFilterDefinitions,
setFilterDefinitionUsedInDropdown,
viewFilter,
setSelectedOperandInDropdown,
setSelectedFilter,
viewFilterDropdownId,
]);
const handleRemove = () => {
closeDropdown();
removeViewFilter(viewFilter.fieldMetadataId);
};
return (
<Dropdown
clickableComponent={
<EditableFilterChip viewFilter={viewFilter} onRemove={handleRemove} />
}
dropdownComponents={
<MultipleFiltersDropdownContent
filterDropdownId={viewFilterDropdownId}
/>
}
dropdownHotkeyScope={hotkeyScope}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>
);
};

View File

@ -0,0 +1,35 @@
import { IconArrowDown, IconArrowUp } from '@/ui/display/icon/index';
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
import { useViewBar } from '@/views/hooks/useViewBar';
import { ViewSort } from '@/views/types/ViewSort';
type EditableSortChipProps = {
viewSort: ViewSort;
};
export const EditableSortChip = ({ viewSort }: EditableSortChipProps) => {
const { removeViewSort, upsertViewSort } = useViewBar();
const handleRemoveClick = () => {
removeViewSort(viewSort.fieldMetadataId);
};
const handleClick = () => {
upsertViewSort({
...viewSort,
direction: viewSort.direction === 'asc' ? 'desc' : 'asc',
});
};
return (
<SortOrFilterChip
key={viewSort.fieldMetadataId}
testId={viewSort.fieldMetadataId}
labelValue={viewSort.definition.label}
Icon={viewSort.direction === 'desc' ? IconArrowDown : IconArrowUp}
isSort
onRemove={handleRemoveClick}
onClick={handleClick}
/>
);
};

View File

@ -4,15 +4,6 @@ import styled from '@emotion/styled';
import { IconX } from '@/ui/display/icon/index';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
type SortOrFilterChipProps = {
labelKey?: string;
labelValue: string;
Icon?: IconComponent;
onRemove: () => void;
isSort?: boolean;
testId?: string;
};
type StyledChipProps = {
isSort?: boolean;
};
@ -23,13 +14,16 @@ const StyledChip = styled.div<StyledChipProps>`
border: 1px solid ${({ theme }) => theme.accent.tertiary};
border-radius: 4px;
color: ${({ theme }) => theme.color.blue};
cursor: pointer;
display: flex;
flex-direction: row;
flex-shrink: 0;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ isSort }) => (isSort ? 'bold' : 'normal')};
padding: ${({ theme }) => theme.spacing(1) + ' ' + theme.spacing(2)};
user-select: none;
`;
const StyledIcon = styled.div`
align-items: center;
display: flex;
@ -54,17 +48,34 @@ const StyledLabelKey = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const SortOrFilterChip = ({
type SortOrFilterChipProps = {
labelKey?: string;
labelValue: string;
Icon?: IconComponent;
onRemove: () => void;
onClick?: () => void;
isSort?: boolean;
testId?: string;
};
export const SortOrFilterChip = ({
labelKey,
labelValue,
Icon,
onRemove,
isSort,
testId,
onClick,
}: SortOrFilterChipProps) => {
const theme = useTheme();
const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation();
onRemove();
};
return (
<StyledChip isSort={isSort}>
<StyledChip isSort={isSort} onClick={onClick}>
{Icon && (
<StyledIcon>
<Icon size={theme.icon.size.sm} />
@ -72,11 +83,12 @@ const SortOrFilterChip = ({
)}
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
{labelValue}
<StyledDelete onClick={onRemove} data-testid={'remove-icon-' + testId}>
<StyledDelete
onClick={handleDeleteClick}
data-testid={'remove-icon-' + testId}
>
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
</StyledDelete>
</StyledChip>
);
};
export default SortOrFilterChip;

View File

@ -97,6 +97,7 @@ export const ViewBar = ({
<ViewBarDetails
filterDropdownId={filterDropdownId}
hasFilterButton
viewBarId={viewBarId}
rightComponent={
<UpdateViewButtonGroup
onViewEditModeChange={openOptionsDropdownButton}

View File

@ -3,19 +3,21 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { IconArrowDown, IconArrowUp } from '@/ui/display/icon/index';
import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
import { EditableSortChip } from '@/views/components/EditableSortChip';
import { ViewBarFilterEffect } from '@/views/components/ViewBarFilterEffect';
import { useViewBar } from '@/views/hooks/useViewBar';
import { useViewScopedStates } from '../hooks/internal/useViewScopedStates';
import SortOrFilterChip from './SortOrFilterChip';
export type ViewBarDetailsProps = {
hasFilterButton?: boolean;
rightComponent?: ReactNode;
filterDropdownId?: string;
viewBarId: string;
};
const StyledBar = styled.div`
@ -90,6 +92,7 @@ export const ViewBarDetails = ({
hasFilterButton = false,
rightComponent,
filterDropdownId,
viewBarId,
}: ViewBarDetailsProps) => {
const {
currentViewSortsState,
@ -98,7 +101,6 @@ export const ViewBarDetails = ({
canPersistSortsSelector,
isViewBarExpandedState,
} = useViewScopedStates();
const { icons } = useLazyLoadIcons();
const currentViewSorts = useRecoilValue(currentViewSortsState);
const currentViewFilters = useRecoilValue(currentViewFiltersState);
@ -106,7 +108,7 @@ export const ViewBarDetails = ({
const canPersistSorts = useRecoilValue(canPersistSortsSelector);
const isViewBarExpanded = useRecoilValue(isViewBarExpandedState);
const { resetViewBar, removeViewSort, removeViewFilter } = useViewBar();
const { resetViewBar } = useViewBar();
const canPersistView = canPersistFilters || canPersistSorts;
@ -114,6 +116,10 @@ export const ViewBarDetails = ({
resetViewBar();
};
const { upsertViewFilter } = useViewBar({
viewBarId: viewBarId,
});
const shouldExpandViewBar =
canPersistView ||
((currentViewSorts?.length || currentViewFilters?.length) &&
@ -128,36 +134,32 @@ export const ViewBarDetails = ({
<StyledFilterContainer>
<StyledChipcontainer>
{currentViewSorts?.map((sort) => {
return (
<SortOrFilterChip
key={sort.fieldMetadataId}
testId={sort.fieldMetadataId}
labelValue={sort.definition.label}
Icon={sort.direction === 'desc' ? IconArrowDown : IconArrowUp}
isSort
onRemove={() => removeViewSort(sort.fieldMetadataId)}
/>
);
return <EditableSortChip viewSort={sort} />;
})}
{!!currentViewSorts?.length && !!currentViewFilters?.length && (
<StyledSeperatorContainer>
<StyledSeperator />
</StyledSeperatorContainer>
)}
{currentViewFilters?.map((filter) => {
{currentViewFilters?.map((viewFilter) => {
return (
<SortOrFilterChip
key={filter.fieldMetadataId}
testId={filter.fieldMetadataId}
labelKey={filter.definition.label}
labelValue={`${getOperandLabelShort(filter.operand)} ${
filter.displayValue
}`}
Icon={icons[filter.definition.iconName]}
onRemove={() => {
removeViewFilter(filter.fieldMetadataId);
}}
/>
<ObjectFilterDropdownScope
filterScopeId={viewFilter.fieldMetadataId}
>
<DropdownScope dropdownScopeId={viewFilter.fieldMetadataId}>
<ViewBarFilterEffect
filterDropdownId={viewFilter.fieldMetadataId}
onFilterSelect={upsertViewFilter}
/>
<EditableFilterDropdownButton
viewFilter={viewFilter}
hotkeyScope={{
scope: FiltersHotkeyScope.ObjectFilterDropdownButton,
}}
viewFilterDropdownId={viewFilter.fieldMetadataId}
/>
</DropdownScope>
</ObjectFilterDropdownScope>
);
})}
</StyledChipcontainer>

View File

@ -18991,6 +18991,7 @@ __metadata:
jest: "npm:^28.1.3"
postcss: "npm:^8.4.29"
prettier: "npm:^3.0.3"
rimraf: "npm:^5.0.5"
ts-jest: "npm:^29.1.1"
ts-node: "npm:^10.9.1"
typescript: "npm:^5.2.2"
@ -20527,7 +20528,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10":
"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7":
version: 10.3.10
resolution: "glob@npm:10.3.10"
dependencies:
@ -31483,6 +31484,17 @@ __metadata:
languageName: node
linkType: hard
"rimraf@npm:^5.0.5":
version: 5.0.5
resolution: "rimraf@npm:5.0.5"
dependencies:
glob: "npm:^10.3.7"
bin:
rimraf: dist/esm/bin.mjs
checksum: d50dbe724f33835decd88395b25ed35995077c60a50ae78ded06e0185418914e555817aad1b4243edbff2254548c2f6ad6f70cc850040bebb4da9e8cc016f586
languageName: node
linkType: hard
"rimraf@npm:~2.6.2":
version: 2.6.3
resolution: "rimraf@npm:2.6.3"