mirror of
https://github.com/twentyhq/twenty.git
synced 2024-09-11 15:35:36 +03:00
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:
parent
e3e42be723
commit
bded46444d
@ -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"
|
||||
|
@ -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 ??
|
||||
[]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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>();
|
||||
|
@ -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:
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { Meta, Story, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
|
@ -56,6 +56,7 @@ export const Dropdown = ({
|
||||
useDropdown();
|
||||
|
||||
const offsetMiddlewares = [];
|
||||
|
||||
if (dropdownOffset.x) {
|
||||
offsetMiddlewares.push(offset({ crossAxis: dropdownOffset.x }));
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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;
|
||||
|
@ -97,6 +97,7 @@ export const ViewBar = ({
|
||||
<ViewBarDetails
|
||||
filterDropdownId={filterDropdownId}
|
||||
hasFilterButton
|
||||
viewBarId={viewBarId}
|
||||
rightComponent={
|
||||
<UpdateViewButtonGroup
|
||||
onViewEditModeChange={openOptionsDropdownButton}
|
||||
|
@ -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>
|
||||
|
14
yarn.lock
14
yarn.lock
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user