Move filter and sort compoenets in a separate lib (#612)

* Move filter and sort compoenets in a separate lib

* Add SortAndFilterBar to the filter lib

* Abstract hotkeys scopes

* Fix hotkeys on filters

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Emilien Chauvet
2023-07-11 20:42:15 -07:00
committed by GitHub
parent e8d77833a7
commit b5de2abd48
17 changed files with 213 additions and 174 deletions

View File

@ -1,223 +0,0 @@
import { ReactNode, useRef } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { IconChevronDown } from '@/ui/icons/index';
import { overlayBackground, textInputStyle } from '@/ui/themes/effects';
import { useOutsideAlerter } from '../../../hooks/useOutsideAlerter';
type OwnProps = {
label: string;
isActive: boolean;
children?: ReactNode;
isUnfolded?: boolean;
onIsUnfoldedChange?: (newIsUnfolded: boolean) => void;
resetState?: () => void;
};
const StyledDropdownButtonContainer = styled.div`
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
`;
type StyledDropdownButtonProps = {
isUnfolded: boolean;
isActive: boolean;
};
const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
background: ${({ theme }) => theme.background.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${(props) => (props.isActive ? props.theme.color.blue : 'none')};
cursor: pointer;
display: flex;
filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')};
padding: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
user-select: none;
&:hover {
filter: brightness(0.95);
}
`;
const StyledDropdown = styled.ul`
--outer-border-radius: calc(var(--wraper-border-radius) - 2px);
--wraper-border: 1px;
--wraper-border-radius: ${({ theme }) => theme.border.radius.md};
border: var(--wraper-border) solid ${({ theme }) => theme.border.color.light};
border-radius: var(--wraper-border-radius);
display: flex;
flex-direction: column;
min-width: 160px;
padding: 0px;
position: absolute;
right: 0;
top: 14px;
${overlayBackground}
li {
&:first-of-type {
border-top-left-radius: var(--outer-border-radius);
border-top-right-radius: var(--outer-border-radius);
}
&:last-of-type {
border-bottom: 0;
border-bottom-left-radius: var(--outer-border-radius);
border-bottom-right-radius: var(--outer-border-radius);
}
}
`;
const StyledDropdownItem = styled.li`
align-items: center;
border-radius: ${({ theme }) => theme.border.radius.xs};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
margin: 2px;
padding: ${({ theme }) => theme.spacing(2)}
calc(${({ theme }) => theme.spacing(2)} - 2px);
user-select: none;
width: calc(160px - ${({ theme }) => theme.spacing(4)});
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
`;
const StyledDropdownItemClipped = styled.span`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledDropdownTopOption = styled.li`
align-items: center;
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.primary};
cursor: pointer;
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
justify-content: space-between;
padding: calc(${({ theme }) => theme.spacing(2)})
calc(${({ theme }) => theme.spacing(2)});
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
user-select: none;
`;
const StyledIcon = styled.div`
display: flex;
justify-content: center;
margin-right: ${({ theme }) => theme.spacing(1)};
min-width: ${({ theme }) => theme.spacing(4)};
`;
const StyledSearchField = styled.li`
align-items: center;
border-bottom: var(--wraper-border) solid
${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
font-weight: ${({ theme }) => theme.font.weight.medium};
justify-content: space-between;
overflow: hidden;
user-select: none;
input {
border-radius: ${({ theme }) => theme.border.radius.md};
box-sizing: border-box;
font-family: ${({ theme }) => theme.font.family};
height: 36px;
padding: 8px;
width: 100%;
${textInputStyle}
&:focus {
outline: 0 none;
}
}
`;
function DropdownButton({
label,
isActive,
children,
isUnfolded = false,
onIsUnfoldedChange,
}: OwnProps) {
useScopedHotkeys(
[Key.Enter, Key.Escape],
() => {
onIsUnfoldedChange?.(false);
},
InternalHotkeysScope.TableHeaderDropdownButton,
[onIsUnfoldedChange],
);
const onButtonClick = () => {
onIsUnfoldedChange?.(!isUnfolded);
};
const onOutsideClick = () => {
onIsUnfoldedChange?.(false);
};
const dropdownRef = useRef(null);
useOutsideAlerter({ ref: dropdownRef, callback: onOutsideClick });
return (
<StyledDropdownButtonContainer>
<StyledDropdownButton
isUnfolded={isUnfolded}
onClick={onButtonClick}
isActive={isActive}
aria-selected={isActive}
>
{label}
</StyledDropdownButton>
{isUnfolded && (
<StyledDropdown ref={dropdownRef}>{children}</StyledDropdown>
)}
</StyledDropdownButtonContainer>
);
}
const StyleAngleDownContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
height: 100%;
justify-content: center;
margin-left: auto;
`;
function DropdownTopOptionAngleDown() {
return (
<StyleAngleDownContainer>
<IconChevronDown size={16} />
</StyleAngleDownContainer>
);
}
DropdownButton.StyledDropdownItem = StyledDropdownItem;
DropdownButton.StyledDropdownItemClipped = StyledDropdownItemClipped;
DropdownButton.StyledSearchField = StyledSearchField;
DropdownButton.StyledDropdownTopOption = StyledDropdownTopOption;
DropdownButton.StyledDropdownTopOptionAngleDown = DropdownTopOptionAngleDown;
DropdownButton.StyledIcon = StyledIcon;
export default DropdownButton;

View File

@ -1,131 +0,0 @@
import { useCallback, useState } from 'react';
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { useSetHotkeysScope } from '@/hotkeys/hooks/useSetHotkeysScope';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { filterDefinitionUsedInDropdownScopedState } from '@/lib/filters-and-sorts/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/lib/filters-and-sorts/states/filterDropdownSearchInputScopedState';
import { filtersScopedState } from '@/lib/filters-and-sorts/states/filtersScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/lib/filters-and-sorts/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/lib/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
import DropdownButton from './DropdownButton';
import { FilterDropdownDateSearchInput } from './FilterDropdownDateSearchInput';
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
import { FilterDropdownFilterSelect } from './FilterDropdownFilterSelect';
import { FilterDropdownNumberSearchInput } from './FilterDropdownNumberSearchInput';
import { FilterDropdownOperandButton } from './FilterDropdownOperandButton';
import { FilterDropdownOperandSelect } from './FilterDropdownOperandSelect';
import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
export function FilterDropdownButton() {
const [isUnfolded, setIsUnfolded] = useState(false);
const [
isFilterDropdownOperandSelectUnfolded,
setIsFilterDropdownOperandSelectUnfolded,
] = useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
TableContext,
);
const [
tableFilterDefinitionUsedInDropdown,
setTableFilterDefinitionUsedInDropdown,
] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
TableContext,
);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
TableContext,
);
const [activeTableFilters] = useRecoilScopedState(
filtersScopedState,
TableContext,
);
const [selectedOperandInDropdown, setSelectedOperandInDropdown] =
useRecoilScopedState(selectedOperandInDropdownScopedState, TableContext);
const resetState = useCallback(() => {
setIsFilterDropdownOperandSelectUnfolded(false);
setTableFilterDefinitionUsedInDropdown(null);
setSelectedOperandInDropdown(null);
setFilterDropdownSearchInput('');
}, [
setTableFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setFilterDropdownSearchInput,
setIsFilterDropdownOperandSelectUnfolded,
]);
const isFilterSelected = (activeTableFilters?.length ?? 0) > 0;
const setHotkeysScope = useSetHotkeysScope();
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setIsUnfolded(true);
} else {
if (tableFilterDefinitionUsedInDropdown?.type === 'entity') {
setHotkeysScope(InternalHotkeysScope.Table);
}
setIsUnfolded(false);
resetState();
}
}
useScopedHotkeys(
[Key.Escape],
() => {
handleIsUnfoldedChange(false);
},
InternalHotkeysScope.RelationPicker,
[handleIsUnfoldedChange],
);
return (
<DropdownButton
label="Filter"
isActive={isFilterSelected}
isUnfolded={isUnfolded}
onIsUnfoldedChange={handleIsUnfoldedChange}
>
{!tableFilterDefinitionUsedInDropdown ? (
<FilterDropdownFilterSelect />
) : isFilterDropdownOperandSelectUnfolded ? (
<FilterDropdownOperandSelect />
) : (
selectedOperandInDropdown && (
<>
<FilterDropdownOperandButton />
<DropdownButton.StyledSearchField autoFocus key={'search-filter'}>
{tableFilterDefinitionUsedInDropdown.type === 'text' && (
<FilterDropdownTextSearchInput />
)}
{tableFilterDefinitionUsedInDropdown.type === 'number' && (
<FilterDropdownNumberSearchInput />
)}
{tableFilterDefinitionUsedInDropdown.type === 'date' && (
<FilterDropdownDateSearchInput />
)}
{tableFilterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySearchInput />
)}
</DropdownButton.StyledSearchField>
{tableFilterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySelect />
)}
</>
)
)}
</DropdownButton>
);
}

View File

@ -1,47 +0,0 @@
import styled from '@emotion/styled';
import { useUpsertFilter } from '@/lib/filters-and-sorts/hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '@/lib/filters-and-sorts/states/filterDefinitionUsedInDropdownScopedState';
import { selectedOperandInDropdownScopedState } from '@/lib/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
import DatePicker from '../../form/DatePicker';
export function FilterDropdownDateSearchInput() {
const [tableFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
TableContext,
);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
TableContext,
);
const upsertActiveTableFilter = useUpsertFilter(TableContext);
function handleChange(date: Date) {
if (!tableFilterDefinitionUsedInDropdown || !selectedOperandInDropdown)
return;
upsertActiveTableFilter({
field: tableFilterDefinitionUsedInDropdown.field,
type: tableFilterDefinitionUsedInDropdown.type,
value: date.toISOString(),
operand: selectedOperandInDropdown,
displayValue: date.toLocaleDateString(),
});
}
return (
<DatePicker
date={new Date()}
onChangeHandler={handleChange}
customInput={<></>}
customCalendarContainer={styled.div`
top: -10px;
`}
/>
);
}

View File

@ -1,36 +0,0 @@
import { ChangeEvent } from 'react';
import { filterDefinitionUsedInDropdownScopedState } from '@/lib/filters-and-sorts/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/lib/filters-and-sorts/states/filterDropdownSearchInputScopedState';
import { selectedOperandInDropdownScopedState } from '@/lib/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
export function FilterDropdownEntitySearchInput() {
const [tableFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
TableContext,
);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
TableContext,
);
const [filterDropdownSearchInput, setFilterDropdownSearchInput] =
useRecoilScopedState(filterDropdownSearchInputScopedState, TableContext);
return (
tableFilterDefinitionUsedInDropdown &&
selectedOperandInDropdown && (
<input
type="text"
value={filterDropdownSearchInput}
placeholder={tableFilterDefinitionUsedInDropdown.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterDropdownSearchInput(event.target.value);
}}
/>
)
);
}

View File

@ -1,83 +0,0 @@
import { useEffect } from 'react';
import { useFilterCurrentlyEdited } from '@/lib/filters-and-sorts/hooks/useFilterCurrentlyEdited';
import { useRemoveFilter } from '@/lib/filters-and-sorts/hooks/useRemoveFilter';
import { useUpsertFilter } from '@/lib/filters-and-sorts/hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '@/lib/filters-and-sorts/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSelectedEntityIdScopedState } from '@/lib/filters-and-sorts/states/filterDropdownSelectedEntityIdScopedState';
import { selectedOperandInDropdownScopedState } from '@/lib/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { EntitiesForMultipleEntitySelect } from '@/relation-picker/components/MultipleEntitySelect';
import { SingleEntitySelectBase } from '@/relation-picker/components/SingleEntitySelectBase';
import { EntityForSelect } from '@/relation-picker/types/EntityForSelect';
import { TableContext } from '@/ui/tables/states/TableContext';
export function FilterDropdownEntitySearchSelect({
entitiesForSelect,
}: {
entitiesForSelect: EntitiesForMultipleEntitySelect<EntityForSelect>;
}) {
const [filterDropdownSelectedEntityId, setFilterDropdownSelectedEntityId] =
useRecoilScopedState(
filterDropdownSelectedEntityIdScopedState,
TableContext,
);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
TableContext,
);
const [tableFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
TableContext,
);
const upsertActiveTableFilter = useUpsertFilter(TableContext);
const removeActiveTableFilter = useRemoveFilter(TableContext);
const filterCurrentlyEdited = useFilterCurrentlyEdited(TableContext);
function handleUserSelected(selectedEntity: EntityForSelect) {
if (!tableFilterDefinitionUsedInDropdown || !selectedOperandInDropdown) {
return;
}
const clickedOnAlreadySelectedEntity =
selectedEntity.id === filterDropdownSelectedEntityId;
if (clickedOnAlreadySelectedEntity) {
removeActiveTableFilter(tableFilterDefinitionUsedInDropdown.field);
setFilterDropdownSelectedEntityId(null);
} else {
setFilterDropdownSelectedEntityId(selectedEntity.id);
upsertActiveTableFilter({
displayValue: selectedEntity.name,
field: tableFilterDefinitionUsedInDropdown.field,
operand: selectedOperandInDropdown,
type: tableFilterDefinitionUsedInDropdown.type,
value: selectedEntity.id,
});
}
}
useEffect(() => {
if (!filterCurrentlyEdited) {
setFilterDropdownSelectedEntityId(null);
}
}, [filterCurrentlyEdited, setFilterDropdownSelectedEntityId]);
return (
<>
<SingleEntitySelectBase
entities={{
entitiesToSelect: entitiesForSelect.entitiesToSelect,
selectedEntity: entitiesForSelect.selectedEntities[0],
loading: entitiesForSelect.loading,
}}
onEntitySelected={handleUserSelected}
/>
</>
);
}

View File

@ -1,26 +0,0 @@
import { filterDefinitionUsedInDropdownScopedState } from '@/lib/filters-and-sorts/states/filterDefinitionUsedInDropdownScopedState';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
import { DropdownMenuSeparator } from '../../menu/DropdownMenuSeparator';
export function FilterDropdownEntitySelect() {
const [tableFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
TableContext,
);
if (tableFilterDefinitionUsedInDropdown?.type !== 'entity') {
return null;
}
return (
<>
<DropdownMenuSeparator />
<RecoilScope>
{tableFilterDefinitionUsedInDropdown.entitySelectComponent}
</RecoilScope>
</>
);
}

View File

@ -1,67 +0,0 @@
import { useSetHotkeysScope } from '@/hotkeys/hooks/useSetHotkeysScope';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { availableFiltersScopedState } from '@/lib/filters-and-sorts/states/availableFiltersScopedState';
import { filterDefinitionUsedInDropdownScopedState } from '@/lib/filters-and-sorts/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/lib/filters-and-sorts/states/filterDropdownSearchInputScopedState';
import { selectedOperandInDropdownScopedState } from '@/lib/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { getOperandsForFilterType } from '@/lib/filters-and-sorts/utils/getOperandsForFilterType';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/recoil-scope/hooks/useRecoilScopedValue';
import { TableContext } from '@/ui/tables/states/TableContext';
import { DropdownMenuItemContainer } from '../../menu/DropdownMenuItemContainer';
import { DropdownMenuSelectableItem } from '../../menu/DropdownMenuSelectableItem';
import DropdownButton from './DropdownButton';
export function FilterDropdownFilterSelect() {
const [, setTableFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
TableContext,
);
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
TableContext,
);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
TableContext,
);
const availableTableFilters = useRecoilScopedValue(
availableFiltersScopedState,
TableContext,
);
const setHotkeysScope = useSetHotkeysScope();
return (
<DropdownMenuItemContainer style={{ maxHeight: '300px' }}>
{availableTableFilters.map((availableTableFilter, index) => (
<DropdownMenuSelectableItem
key={`select-filter-${index}`}
onClick={() => {
setTableFilterDefinitionUsedInDropdown(availableTableFilter);
if (availableTableFilter.type === 'entity') {
setHotkeysScope(InternalHotkeysScope.RelationPicker);
}
setSelectedOperandInDropdown(
getOperandsForFilterType(availableTableFilter.type)?.[0],
);
setFilterDropdownSearchInput('');
}}
>
<DropdownButton.StyledIcon>
{availableTableFilter.icon}
</DropdownButton.StyledIcon>
{availableTableFilter.label}
</DropdownMenuSelectableItem>
))}
</DropdownMenuItemContainer>
);
}

View File

@ -1,46 +0,0 @@
import { ChangeEvent } from 'react';
import { useRemoveFilter } from '@/lib/filters-and-sorts/hooks/useRemoveFilter';
import { useUpsertFilter } from '@/lib/filters-and-sorts/hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '@/lib/filters-and-sorts/states/filterDefinitionUsedInDropdownScopedState';
import { selectedOperandInDropdownScopedState } from '@/lib/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
export function FilterDropdownNumberSearchInput() {
const [tableFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
TableContext,
);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
TableContext,
);
const upsertActiveTableFilter = useUpsertFilter(TableContext);
const removeActiveTableFilter = useRemoveFilter(TableContext);
return (
tableFilterDefinitionUsedInDropdown &&
selectedOperandInDropdown && (
<input
type="number"
placeholder={tableFilterDefinitionUsedInDropdown.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
if (event.target.value === '') {
removeActiveTableFilter(tableFilterDefinitionUsedInDropdown.field);
} else {
upsertActiveTableFilter({
field: tableFilterDefinitionUsedInDropdown.field,
type: tableFilterDefinitionUsedInDropdown.type,
value: event.target.value,
operand: selectedOperandInDropdown,
displayValue: event.target.value,
});
}
}}
/>
)
);
}

View File

@ -1,34 +0,0 @@
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/lib/filters-and-sorts/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/lib/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { getOperandLabel } from '@/lib/filters-and-sorts/utils/getOperandLabel';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
import DropdownButton from './DropdownButton';
export function FilterDropdownOperandButton() {
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
TableContext,
);
const [isOperandSelectionUnfolded, setIsOperandSelectionUnfolded] =
useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
TableContext,
);
if (isOperandSelectionUnfolded) {
return null;
}
return (
<DropdownButton.StyledDropdownTopOption
key={'selected-filter-operand'}
onClick={() => setIsOperandSelectionUnfolded(true)}
>
{getOperandLabel(selectedOperandInDropdown)}
<DropdownButton.StyledDropdownTopOptionAngleDown />
</DropdownButton.StyledDropdownTopOption>
);
}

View File

@ -1,78 +0,0 @@
import { useFilterCurrentlyEdited } from '@/lib/filters-and-sorts/hooks/useFilterCurrentlyEdited';
import { useUpsertFilter } from '@/lib/filters-and-sorts/hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '@/lib/filters-and-sorts/states/filterDefinitionUsedInDropdownScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/lib/filters-and-sorts/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/lib/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { FilterOperand } from '@/lib/filters-and-sorts/types/FilterOperand';
import { getOperandLabel } from '@/lib/filters-and-sorts/utils/getOperandLabel';
import { getOperandsForFilterType } from '@/lib/filters-and-sorts/utils/getOperandsForFilterType';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
import { DropdownMenuItemContainer } from '../../menu/DropdownMenuItemContainer';
import DropdownButton from './DropdownButton';
export function FilterDropdownOperandSelect() {
const [tableFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
TableContext,
);
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
TableContext,
);
const operandsForFilterType = getOperandsForFilterType(
tableFilterDefinitionUsedInDropdown?.type,
);
const [isOperandSelectionUnfolded, setIsOperandSelectionUnfolded] =
useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
TableContext,
);
const activeTableFilterCurrentlyEdited =
useFilterCurrentlyEdited(TableContext);
const upsertActiveTableFilter = useUpsertFilter(TableContext);
function handleOperangeChange(newOperand: FilterOperand) {
setSelectedOperandInDropdown(newOperand);
setIsOperandSelectionUnfolded(false);
if (
tableFilterDefinitionUsedInDropdown &&
activeTableFilterCurrentlyEdited
) {
upsertActiveTableFilter({
field: activeTableFilterCurrentlyEdited.field,
displayValue: activeTableFilterCurrentlyEdited.displayValue,
operand: newOperand,
type: activeTableFilterCurrentlyEdited.type,
value: activeTableFilterCurrentlyEdited.value,
});
}
}
if (!isOperandSelectionUnfolded) {
return <></>;
}
return (
<DropdownMenuItemContainer>
{operandsForFilterType.map((filterOperand, index) => (
<DropdownButton.StyledDropdownItem
key={`select-filter-operand-${index}`}
onClick={() => {
handleOperangeChange(filterOperand);
}}
>
{getOperandLabel(filterOperand)}
</DropdownButton.StyledDropdownItem>
))}
</DropdownMenuItemContainer>
);
}

View File

@ -1,56 +0,0 @@
import { ChangeEvent } from 'react';
import { useFilterCurrentlyEdited } from '@/lib/filters-and-sorts/hooks/useFilterCurrentlyEdited';
import { useRemoveFilter } from '@/lib/filters-and-sorts/hooks/useRemoveFilter';
import { useUpsertFilter } from '@/lib/filters-and-sorts/hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '@/lib/filters-and-sorts/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/lib/filters-and-sorts/states/filterDropdownSearchInputScopedState';
import { selectedOperandInDropdownScopedState } from '@/lib/filters-and-sorts/states/selectedOperandInDropdownScopedState';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { TableContext } from '@/ui/tables/states/TableContext';
export function FilterDropdownTextSearchInput() {
const [tableFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
TableContext,
);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
TableContext,
);
const [filterDropdownSearchInput, setFilterDropdownSearchInput] =
useRecoilScopedState(filterDropdownSearchInputScopedState, TableContext);
const upsertActiveTableFilter = useUpsertFilter(TableContext);
const removeActiveTableFilter = useRemoveFilter(TableContext);
const filterCurrentlyEdited = useFilterCurrentlyEdited(TableContext);
return (
tableFilterDefinitionUsedInDropdown &&
selectedOperandInDropdown && (
<input
type="text"
placeholder={tableFilterDefinitionUsedInDropdown.label}
value={filterCurrentlyEdited?.value ?? filterDropdownSearchInput}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterDropdownSearchInput(event.target.value);
if (event.target.value === '') {
removeActiveTableFilter(tableFilterDefinitionUsedInDropdown.field);
} else {
upsertActiveTableFilter({
field: tableFilterDefinitionUsedInDropdown.field,
type: tableFilterDefinitionUsedInDropdown.type,
value: event.target.value,
operand: selectedOperandInDropdown,
displayValue: event.target.value,
});
}
}}
/>
)
);
}

View File

@ -1,150 +0,0 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRemoveFilter } from '@/lib/filters-and-sorts/hooks/useRemoveFilter';
import { SelectedSortType } from '@/lib/filters-and-sorts/interfaces/sorts/interface';
import { availableFiltersScopedState } from '@/lib/filters-and-sorts/states/availableFiltersScopedState';
import { filtersScopedState } from '@/lib/filters-and-sorts/states/filtersScopedState';
import { getOperandLabel } from '@/lib/filters-and-sorts/utils/getOperandLabel';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { IconArrowNarrowDown, IconArrowNarrowUp } from '@/ui/icons/index';
import { TableContext } from '@/ui/tables/states/TableContext';
import SortOrFilterChip from './SortOrFilterChip';
type OwnProps<SortField> = {
sorts: Array<SelectedSortType<SortField>>;
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
onCancelClick: () => void;
};
const StyledBar = styled.div`
align-items: center;
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
flex-direction: row;
height: 40px;
justify-content: space-between;
`;
const StyledChipcontainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(1)};
height: 40px;
justify-content: space-between;
margin-left: ${({ theme }) => theme.spacing(2)};
overflow-x: auto;
`;
const StyledCancelButton = styled.button`
background-color: inherit;
border: none;
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: auto;
margin-right: ${({ theme }) => theme.spacing(2)};
padding: ${(props) => {
const horiz = props.theme.spacing(2);
const vert = props.theme.spacing(1);
return `${vert} ${horiz} ${vert} ${horiz}`;
}};
user-select: none;
&:hover {
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.spacing(1)};
}
`;
function SortAndFilterBar<SortField>({
sorts,
onRemoveSort,
onCancelClick,
}: OwnProps<SortField>) {
const theme = useTheme();
const [filters, setFilters] = useRecoilScopedState(
filtersScopedState,
TableContext,
);
const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState,
TableContext,
);
const filtersWithDefinition = filters.map((filter) => {
const tableFilterDefinition = availableFilters.find((availableFilter) => {
return availableFilter.field === filter.field;
});
return {
...filter,
...tableFilterDefinition,
};
});
const removeFilter = useRemoveFilter(TableContext);
function handleCancelClick() {
setFilters([]);
onCancelClick();
}
if (!filtersWithDefinition.length && !sorts.length) {
return null;
}
return (
<StyledBar>
<StyledChipcontainer>
{sorts.map((sort) => {
return (
<SortOrFilterChip
key={sort.key}
labelValue={sort.label}
id={sort.key}
icon={
sort.order === 'desc' ? (
<IconArrowNarrowDown size={theme.icon.size.md} />
) : (
<IconArrowNarrowUp size={theme.icon.size.md} />
)
}
onRemove={() => onRemoveSort(sort.key)}
/>
);
})}
{filtersWithDefinition.map((filter) => {
return (
<SortOrFilterChip
key={filter.field}
labelKey={filter.label}
labelValue={`${getOperandLabel(filter.operand)} ${
filter.displayValue
}`}
id={filter.field}
icon={filter.icon}
onRemove={() => {
removeFilter(filter.field);
}}
/>
);
})}
</StyledChipcontainer>
{filters.length + sorts.length > 0 && (
<StyledCancelButton
data-testid={'cancel-button'}
onClick={handleCancelClick}
>
Cancel
</StyledCancelButton>
)}
</StyledBar>
);
}
export default SortAndFilterBar;

View File

@ -1,96 +0,0 @@
import { useCallback, useState } from 'react';
import {
SelectedSortType,
SortType,
} from '@/lib/filters-and-sorts/interfaces/sorts/interface';
import DropdownButton from './DropdownButton';
type OwnProps<SortField> = {
isSortSelected: boolean;
onSortSelect: (sort: SelectedSortType<SortField>) => void;
availableSorts: SortType<SortField>[];
};
const options: Array<SelectedSortType<any>['order']> = ['asc', 'desc'];
export function SortDropdownButton<SortField>({
isSortSelected,
availableSorts,
onSortSelect,
}: OwnProps<SortField>) {
const [isUnfolded, setIsUnfolded] = useState(false);
const [isOptionUnfolded, setIsOptionUnfolded] = useState(false);
const [selectedSortDirection, setSelectedSortDirection] =
useState<SelectedSortType<SortField>['order']>('asc');
const onSortItemSelect = useCallback(
(sort: SortType<SortField>) => {
onSortSelect({ ...sort, order: selectedSortDirection });
},
[onSortSelect, selectedSortDirection],
);
const resetState = useCallback(() => {
setIsOptionUnfolded(false);
setSelectedSortDirection('asc');
}, []);
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setIsUnfolded(true);
} else {
setIsUnfolded(false);
resetState();
}
}
return (
<DropdownButton
label="Sort"
isActive={isSortSelected}
isUnfolded={isUnfolded}
onIsUnfoldedChange={handleIsUnfoldedChange}
>
{isOptionUnfolded
? options.map((option, index) => (
<DropdownButton.StyledDropdownItem
key={index}
onClick={() => {
setSelectedSortDirection(option);
setIsOptionUnfolded(false);
}}
>
{option === 'asc' ? 'Ascending' : 'Descending'}
</DropdownButton.StyledDropdownItem>
))
: [
<DropdownButton.StyledDropdownTopOption
key={0}
onClick={() => setIsOptionUnfolded(true)}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
<DropdownButton.StyledDropdownTopOptionAngleDown />
</DropdownButton.StyledDropdownTopOption>,
...availableSorts.map((sort, index) => (
<DropdownButton.StyledDropdownItem
key={index + 1}
onClick={() => {
setIsUnfolded(false);
onSortItemSelect(sort);
}}
>
<DropdownButton.StyledIcon>
{sort.icon}
</DropdownButton.StyledIcon>
{sort.label}
</DropdownButton.StyledDropdownItem>
)),
]}
</DropdownButton>
);
}

View File

@ -1,71 +0,0 @@
import { ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconX } from '@/ui/icons/index';
type OwnProps = {
id: string;
labelKey?: string;
labelValue: string;
icon: ReactNode;
onRemove: () => void;
};
const StyledChip = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 50px;
color: ${({ theme }) => theme.color.blue};
display: flex;
flex-direction: row;
flex-shrink: 0;
font-size: ${({ theme }) => theme.font.size.sm};
padding: ${({ theme }) => theme.spacing(1) + ' ' + theme.spacing(2)};
`;
const StyledIcon = styled.div`
align-items: center;
display: flex;
margin-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledDelete = styled.div`
align-items: center;
cursor: pointer;
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
margin-left: ${({ theme }) => theme.spacing(2)};
margin-top: 1px;
user-select: none;
&:hover {
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.border.radius.sm};
}
`;
const StyledLabelKey = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
function SortOrFilterChip({
id,
labelKey,
labelValue,
icon,
onRemove,
}: OwnProps) {
const theme = useTheme();
return (
<StyledChip>
<StyledIcon>{icon}</StyledIcon>
{labelKey && <StyledLabelKey>{labelKey}:&nbsp;</StyledLabelKey>}
{labelValue}
<StyledDelete onClick={onRemove} data-testid={'remove-icon-' + id}>
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
</StyledDelete>
</StyledChip>
);
}
export default SortOrFilterChip;

View File

@ -1,14 +1,15 @@
import { ReactNode, useCallback, useState } from 'react';
import styled from '@emotion/styled';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { FilterDropdownButton } from '@/lib/filters-and-sorts/components/FilterDropdownButton';
import SortAndFilterBar from '@/lib/filters-and-sorts/components/SortAndFilterBar';
import { SortDropdownButton } from '@/lib/filters-and-sorts/components/SortDropdownButton';
import {
SelectedSortType,
SortType,
} from '@/lib/filters-and-sorts/interfaces/sorts/interface';
import { FilterDropdownButton } from './FilterDropdownButton';
import SortAndFilterBar from './SortAndFilterBar';
import { SortDropdownButton } from './SortDropdownButton';
import { TableContext } from '@/ui/tables/states/TableContext';
type OwnProps<SortField> = {
viewName: string;
@ -89,15 +90,20 @@ export function TableHeader<SortField>({
{viewName}
</StyledViewSection>
<StyledFilters>
<FilterDropdownButton />
<FilterDropdownButton
context={TableContext}
hotkeysScope={InternalHotkeysScope.TableHeaderDropdownButton}
/>
<SortDropdownButton<SortField>
isSortSelected={sorts.length > 0}
availableSorts={availableSorts || []}
onSortSelect={sortSelect}
hotkeysScope={InternalHotkeysScope.TableHeaderDropdownButton}
/>
</StyledFilters>
</StyledTableHeader>
<SortAndFilterBar
context={TableContext}
sorts={sorts}
onRemoveSort={sortUnselect}
onCancelClick={() => {