refactor: rename ui/filter-n-sort to ui/view-bar (#1475)

Closes #1473

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Thaïs
2023-09-06 16:46:02 +02:00
committed by GitHub
parent 28ca9a9e49
commit d6b89359f5
81 changed files with 108 additions and 111 deletions

View File

@ -0,0 +1,114 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { DropdownMenuContainer } from './DropdownMenuContainer';
type OwnProps = {
anchor?: 'left' | 'right';
label: ReactNode;
isActive: boolean;
children?: ReactNode;
isUnfolded?: boolean;
icon?: ReactNode;
onIsUnfoldedChange?: (newIsUnfolded: boolean) => void;
resetState?: () => void;
HotkeyScope: string;
color?: string;
menuWidth?: `${string}px` | 'auto' | number;
};
const StyledDropdownButtonContainer = styled.div`
display: flex;
flex-direction: column;
`;
const StyledDropdownButtonIcon = styled.div`
display: flex;
justify-content: center;
margin-right: ${({ theme }) => theme.spacing(1)};
`;
type StyledDropdownButtonProps = {
isUnfolded: boolean;
isActive: boolean;
};
const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
align-items: center;
background: ${({ theme }) => theme.background.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ isActive, theme, color }) =>
color ?? (isActive ? 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);
}
`;
function DropdownButton({
anchor,
label,
isActive,
children,
isUnfolded = false,
onIsUnfoldedChange,
HotkeyScope,
icon,
color,
menuWidth,
}: OwnProps) {
useScopedHotkeys(
[Key.Enter, Key.Escape],
() => {
onIsUnfoldedChange?.(false);
},
HotkeyScope,
[onIsUnfoldedChange],
);
const onButtonClick = () => {
onIsUnfoldedChange?.(!isUnfolded);
};
const onOutsideClick = () => {
onIsUnfoldedChange?.(false);
};
return (
<StyledDropdownButtonContainer>
<StyledDropdownButton
isUnfolded={isUnfolded}
onClick={onButtonClick}
isActive={isActive}
aria-selected={isActive}
color={color}
>
{icon && <StyledDropdownButtonIcon>{icon}</StyledDropdownButtonIcon>}
{label}
</StyledDropdownButton>
{isUnfolded && (
<DropdownMenuContainer
width={menuWidth}
anchor={anchor}
onClose={onOutsideClick}
>
{children}
</DropdownMenuContainer>
)}
</StyledDropdownButtonContainer>
);
}
export default DropdownButton;

View File

@ -0,0 +1,48 @@
import { type HTMLAttributes, useRef } from 'react';
import styled from '@emotion/styled';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
const StyledDropdownMenuContainer = styled.ul<{
anchor: 'left' | 'right';
}>`
padding: 0;
position: absolute;
${({ anchor }) => {
if (anchor === 'right') return 'right: 0';
}};
top: 14px;
`;
export type DropdownMenuContainerProps = {
anchor?: 'left' | 'right';
children: React.ReactNode;
onClose?: () => void;
width?: `${string}px` | 'auto' | number;
} & HTMLAttributes<HTMLUListElement>;
export function DropdownMenuContainer({
anchor = 'right',
children,
onClose,
width,
...props
}: DropdownMenuContainerProps) {
const dropdownRef = useRef(null);
useListenClickOutside({
refs: [dropdownRef],
callback: () => {
onClose?.();
},
});
return (
<StyledDropdownMenuContainer data-select-disable {...props} anchor={anchor}>
<StyledDropdownMenu ref={dropdownRef} width={width}>
{children}
</StyledDropdownMenu>
</StyledDropdownMenuContainer>
);
}

View File

@ -0,0 +1,49 @@
import { Context } from 'react';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { MultipleFiltersDropdownButton } from './MultipleFiltersDropdownButton';
import { SingleEntityFilterDropdownButton } from './SingleEntityFilterDropdownButton';
export function FilterDropdownButton({
context,
HotkeyScope,
isPrimaryButton = false,
color,
icon,
label,
}: {
context: Context<string | null>;
HotkeyScope: FiltersHotkeyScope;
isPrimaryButton?: boolean;
icon?: React.ReactNode;
color?: string;
label?: string;
}) {
const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState,
context,
);
const hasOnlyOneEntityFilter =
availableFilters.length === 1 && availableFilters[0].type === 'entity';
return hasOnlyOneEntityFilter ? (
<SingleEntityFilterDropdownButton
context={context}
HotkeyScope={HotkeyScope}
/>
) : (
<MultipleFiltersDropdownButton
context={context}
HotkeyScope={HotkeyScope}
icon={icon}
isPrimaryButton={isPrimaryButton}
color={color}
label={label}
/>
);
}

View File

@ -0,0 +1,49 @@
import { Context } from 'react';
import styled from '@emotion/styled';
import DatePicker from '@/ui/input/date/components/DatePicker';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useUpsertFilter } from '@/ui/view-bar/hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
export function FilterDropdownDateSearchInput({
context,
}: {
context: Context<string | null>;
}) {
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const upsertFilter = useUpsertFilter(context);
function handleChange(date: Date) {
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
upsertFilter({
key: filterDefinitionUsedInDropdown.key,
type: filterDefinitionUsedInDropdown.type,
value: date.toISOString(),
operand: selectedOperandInDropdown,
displayValue: date.toLocaleDateString(),
});
}
return (
<DatePicker
date={new Date()}
onChangeHandler={handleChange}
customInput={<></>}
customCalendarContainer={styled.div`
top: -10px;
`}
/>
);
}

View File

@ -0,0 +1,40 @@
import { ChangeEvent, Context } from 'react';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/view-bar/states/filterDropdownSearchInputScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
export function FilterDropdownEntitySearchInput({
context,
}: {
context: Context<string | null>;
}) {
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const [filterDropdownSearchInput, setFilterDropdownSearchInput] =
useRecoilScopedState(filterDropdownSearchInputScopedState, context);
return (
filterDefinitionUsedInDropdown &&
selectedOperandInDropdown && (
<DropdownMenuInput
type="text"
value={filterDropdownSearchInput}
placeholder={filterDefinitionUsedInDropdown.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterDropdownSearchInput(event.target.value);
}}
/>
)
);
}

View File

@ -0,0 +1,88 @@
import { useEffect } from 'react';
import { EntitiesForMultipleEntitySelect } from '@/ui/input/relation-picker/components/MultipleEntitySelect';
import { SingleEntitySelectBase } from '@/ui/input/relation-picker/components/SingleEntitySelectBase';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useFilterCurrentlyEdited } from '@/ui/view-bar/hooks/useFilterCurrentlyEdited';
import { useRemoveFilter } from '@/ui/view-bar/hooks/useRemoveFilter';
import { useUpsertFilter } from '@/ui/view-bar/hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSelectedEntityIdScopedState } from '@/ui/view-bar/states/filterDropdownSelectedEntityIdScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
export function FilterDropdownEntitySearchSelect({
entitiesForSelect,
context,
}: {
entitiesForSelect: EntitiesForMultipleEntitySelect<EntityForSelect>;
context: React.Context<string | null>;
}) {
const [filterDropdownSelectedEntityId, setFilterDropdownSelectedEntityId] =
useRecoilScopedState(filterDropdownSelectedEntityIdScopedState, context);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const upsertFilter = useUpsertFilter(context);
const removeFilter = useRemoveFilter(context);
const filterCurrentlyEdited = useFilterCurrentlyEdited(context);
function handleUserSelected(
selectedEntity: EntityForSelect | null | undefined,
) {
if (
!filterDefinitionUsedInDropdown ||
!selectedOperandInDropdown ||
!selectedEntity
) {
return;
}
const clickedOnAlreadySelectedEntity =
selectedEntity.id === filterDropdownSelectedEntityId;
if (clickedOnAlreadySelectedEntity) {
removeFilter(filterDefinitionUsedInDropdown.key);
setFilterDropdownSelectedEntityId(null);
} else {
setFilterDropdownSelectedEntityId(selectedEntity.id);
upsertFilter({
displayValue: selectedEntity.name,
key: filterDefinitionUsedInDropdown.key,
operand: selectedOperandInDropdown,
type: filterDefinitionUsedInDropdown.type,
value: selectedEntity.id,
displayAvatarUrl: selectedEntity.avatarUrl,
});
}
}
useEffect(() => {
if (!filterCurrentlyEdited) {
setFilterDropdownSelectedEntityId(null);
}
}, [filterCurrentlyEdited, setFilterDropdownSelectedEntityId]);
return (
<>
<SingleEntitySelectBase
entities={{
entitiesToSelect: entitiesForSelect.entitiesToSelect,
selectedEntity: entitiesForSelect.selectedEntities[0],
loading: entitiesForSelect.loading,
}}
onEntitySelected={handleUserSelected}
/>
</>
);
}

View File

@ -0,0 +1,31 @@
import { Context } from 'react';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
export function FilterDropdownEntitySelect({
context,
}: {
context: Context<string | null>;
}) {
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
if (filterDefinitionUsedInDropdown?.type !== 'entity') {
return null;
}
return (
<>
<StyledDropdownMenuSeparator />
<RecoilScope>
{filterDefinitionUsedInDropdown.entitySelectComponent}
</RecoilScope>
</>
);
}

View File

@ -0,0 +1,68 @@
import { Context } from 'react';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState';
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
export function FilterDropdownFilterSelect({
context,
}: {
context: Context<string | null>;
}) {
const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
context,
);
const availableFilters = useRecoilScopedValue(
availableFiltersScopedState,
context,
);
const setHotkeyScope = useSetHotkeyScope();
return (
<StyledDropdownMenuItemsContainer>
{availableFilters.map((availableFilter, index) => (
<MenuItem
key={`select-filter-${index}`}
testId={`select-filter-${index}`}
onClick={() => {
setFilterDefinitionUsedInDropdown(availableFilter);
if (availableFilter.type === 'entity') {
setHotkeyScope(RelationPickerHotkeyScope.RelationPicker);
}
setSelectedOperandInDropdown(
getOperandsForFilterType(availableFilter.type)?.[0],
);
setFilterDropdownSearchInput('');
}}
LeftIcon={availableFilter.Icon}
text={availableFilter.label}
/>
))}
</StyledDropdownMenuItemsContainer>
);
}

View File

@ -0,0 +1,51 @@
import { ChangeEvent, Context } from 'react';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRemoveFilter } from '../hooks/useRemoveFilter';
import { useUpsertFilter } from '../hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
export function FilterDropdownNumberSearchInput({
context,
}: {
context: Context<string | null>;
}) {
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const upsertFilter = useUpsertFilter(context);
const removeFilter = useRemoveFilter(context);
return (
filterDefinitionUsedInDropdown &&
selectedOperandInDropdown && (
<DropdownMenuInput
type="number"
placeholder={filterDefinitionUsedInDropdown.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
if (event.target.value === '') {
removeFilter(filterDefinitionUsedInDropdown.key);
} else {
upsertFilter({
key: filterDefinitionUsedInDropdown.key,
type: filterDefinitionUsedInDropdown.type,
value: event.target.value,
operand: selectedOperandInDropdown,
displayValue: event.target.value,
});
}
}}
/>
)
);
}

View File

@ -0,0 +1,42 @@
import { Context } from 'react';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { IconChevronDown } from '@/ui/icon';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
import { getOperandLabel } from '../utils/getOperandLabel';
export function FilterDropdownOperandButton({
context,
}: {
context: Context<string | null>;
}) {
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const [
isFilterDropdownOperandSelectUnfolded,
setIsFilterDropdownOperandSelectUnfolded,
] = useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
context,
);
if (isFilterDropdownOperandSelectUnfolded) {
return null;
}
return (
<DropdownMenuHeader
key={'selected-filter-operand'}
EndIcon={IconChevronDown}
onClick={() => setIsFilterDropdownOperandSelectUnfolded(true)}
>
{getOperandLabel(selectedOperandInDropdown)}
</DropdownMenuHeader>
);
}

View File

@ -0,0 +1,79 @@
import { Context } from 'react';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited';
import { useUpsertFilter } from '../hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
import { FilterOperand } from '../types/FilterOperand';
import { getOperandLabel } from '../utils/getOperandLabel';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
export function FilterDropdownOperandSelect({
context,
}: {
context: Context<string | null>;
}) {
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const operandsForFilterType = getOperandsForFilterType(
filterDefinitionUsedInDropdown?.type,
);
const [
isFilterDropdownOperandSelectUnfolded,
setIsFilterDropdownOperandSelectUnfolded,
] = useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
context,
);
const filterCurrentlyEdited = useFilterCurrentlyEdited(context);
const upsertFilter = useUpsertFilter(context);
function handleOperangeChange(newOperand: FilterOperand) {
setSelectedOperandInDropdown(newOperand);
setIsFilterDropdownOperandSelectUnfolded(false);
if (filterDefinitionUsedInDropdown && filterCurrentlyEdited) {
upsertFilter({
key: filterCurrentlyEdited.key,
displayValue: filterCurrentlyEdited.displayValue,
operand: newOperand,
type: filterCurrentlyEdited.type,
value: filterCurrentlyEdited.value,
});
}
}
if (!isFilterDropdownOperandSelectUnfolded) {
return <></>;
}
return (
<StyledDropdownMenuItemsContainer>
{operandsForFilterType.map((filterOperand, index) => (
<MenuItem
key={`select-filter-operand-${index}`}
onClick={() => {
handleOperangeChange(filterOperand);
}}
text={getOperandLabel(filterOperand)}
/>
))}
</StyledDropdownMenuItemsContainer>
);
}

View File

@ -0,0 +1,61 @@
import { ChangeEvent, Context } from 'react';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited';
import { useRemoveFilter } from '../hooks/useRemoveFilter';
import { useUpsertFilter } from '../hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState';
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
export function FilterDropdownTextSearchInput({
context,
}: {
context: Context<string | null>;
}) {
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const [filterDropdownSearchInput, setFilterDropdownSearchInput] =
useRecoilScopedState(filterDropdownSearchInputScopedState, context);
const upsertFilter = useUpsertFilter(context);
const removeFilter = useRemoveFilter(context);
const filterCurrentlyEdited = useFilterCurrentlyEdited(context);
return (
filterDefinitionUsedInDropdown &&
selectedOperandInDropdown && (
<DropdownMenuInput
type="text"
placeholder={filterDefinitionUsedInDropdown.label}
value={filterCurrentlyEdited?.value ?? filterDropdownSearchInput}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
setFilterDropdownSearchInput(event.target.value);
if (event.target.value === '') {
removeFilter(filterDefinitionUsedInDropdown.key);
} else {
upsertFilter({
key: filterDefinitionUsedInDropdown.key,
type: filterDefinitionUsedInDropdown.type,
value: event.target.value,
operand: selectedOperandInDropdown,
displayValue: event.target.value,
});
}
}}
/>
)
);
}

View File

@ -0,0 +1,18 @@
import { EntityChip } from '@/ui/chip/components/EntityChip';
import { Filter } from '../types/Filter';
type OwnProps = {
filter: Filter;
};
export function GenericEntityFilterChip({ filter }: OwnProps) {
return (
<EntityChip
entityId={filter.value}
name={filter.displayValue}
avatarType="rounded"
pictureUrl={filter.displayAvatarUrl}
/>
);
}

View File

@ -0,0 +1,151 @@
import { Context, useCallback, useState } from 'react';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/view-bar/states/filterDropdownSearchInputScopedState';
import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/view-bar/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
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 MultipleFiltersDropdownButton({
context,
HotkeyScope,
isPrimaryButton = false,
color,
icon,
label,
}: {
context: Context<string | null>;
HotkeyScope: FiltersHotkeyScope;
isPrimaryButton?: boolean;
icon?: React.ReactNode;
color?: string;
label?: string;
}) {
const [isUnfolded, setIsUnfolded] = useState(false);
const [
isFilterDropdownOperandSelectUnfolded,
setIsFilterDropdownOperandSelectUnfolded,
] = useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
context,
);
const [filterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown] =
useRecoilScopedState(filterDefinitionUsedInDropdownScopedState, context);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
context,
);
const [filters] = useRecoilScopedState(filtersScopedState, context);
const [selectedOperandInDropdown, setSelectedOperandInDropdown] =
useRecoilScopedState(selectedOperandInDropdownScopedState, context);
const resetState = useCallback(() => {
setIsFilterDropdownOperandSelectUnfolded(false);
setFilterDefinitionUsedInDropdown(null);
setSelectedOperandInDropdown(null);
setFilterDropdownSearchInput('');
}, [
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setFilterDropdownSearchInput,
setIsFilterDropdownOperandSelectUnfolded,
]);
const isFilterSelected = (filters?.length ?? 0) > 0;
const setHotkeyScope = useSetHotkeyScope();
const [isViewBarExpanded, setIsViewBarExpanded] = useRecoilScopedState(
isViewBarExpandedScopedState,
context,
);
function handleIsUnfoldedChange(unfolded: boolean) {
if (unfolded && isPrimaryButton) {
setIsViewBarExpanded(!isViewBarExpanded);
}
if (
unfolded &&
((isPrimaryButton && !isFilterSelected) || !isPrimaryButton)
) {
setHotkeyScope(HotkeyScope);
setIsUnfolded(true);
return;
}
if (filterDefinitionUsedInDropdown?.type === 'entity') {
setHotkeyScope(HotkeyScope);
}
setIsUnfolded(false);
resetState();
}
return (
<DropdownButton
label={label ?? 'Filter'}
isActive={isFilterSelected}
isUnfolded={isUnfolded}
icon={icon}
onIsUnfoldedChange={handleIsUnfoldedChange}
HotkeyScope={HotkeyScope}
color={color}
menuWidth={
selectedOperandInDropdown &&
filterDefinitionUsedInDropdown?.type === 'date'
? 'auto'
: undefined
}
>
{!filterDefinitionUsedInDropdown ? (
<FilterDropdownFilterSelect context={context} />
) : isFilterDropdownOperandSelectUnfolded ? (
<FilterDropdownOperandSelect context={context} />
) : (
selectedOperandInDropdown && (
<>
<FilterDropdownOperandButton context={context} />
<StyledDropdownMenuSeparator />
{filterDefinitionUsedInDropdown.type === 'text' && (
<FilterDropdownTextSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'number' && (
<FilterDropdownNumberSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'date' && (
<FilterDropdownDateSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySelect context={context} />
)}
</>
)
)}
</DropdownButton>
);
}

View File

@ -0,0 +1,108 @@
import { Context, useState } from 'react';
import React from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconChevronDown } from '@/ui/icon';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/view-bar/states/filterDropdownSearchInputScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
import { StyledHeaderDropdownButton } from '../../dropdown/components/StyledHeaderDropdownButton';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { filtersScopedState } from '../states/filtersScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
import { DropdownMenuContainer } from './DropdownMenuContainer';
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
import { GenericEntityFilterChip } from './GenericEntityFilterChip';
const StyledDropdownButtonContainer = styled.div`
display: flex;
flex-direction: column;
position: relative;
`;
export function SingleEntityFilterDropdownButton({
context,
HotkeyScope,
}: {
context: Context<string | null>;
HotkeyScope: FiltersHotkeyScope;
}) {
const theme = useTheme();
const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState,
context,
);
const availableFilter = availableFilters[0];
const [isUnfolded, setIsUnfolded] = useState(false);
const [filters] = useRecoilScopedState(filtersScopedState, context);
const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
context,
);
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
React.useEffect(() => {
setFilterDefinitionUsedInDropdown(availableFilter);
const defaultOperand = getOperandsForFilterType(availableFilter?.type)[0];
setSelectedOperandInDropdown(defaultOperand);
}, [
availableFilter,
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
]);
const setHotkeyScope = useSetHotkeyScope();
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setHotkeyScope(HotkeyScope);
setIsUnfolded(true);
} else {
setHotkeyScope(HotkeyScope);
setIsUnfolded(false);
setFilterDropdownSearchInput('');
}
}
return (
<StyledDropdownButtonContainer>
<StyledHeaderDropdownButton
isUnfolded={isUnfolded}
onClick={() => handleIsUnfoldedChange(!isUnfolded)}
>
{filters[0] ? (
<GenericEntityFilterChip filter={filters[0]} />
) : (
'Filter'
)}
<IconChevronDown size={theme.icon.size.md} />
</StyledHeaderDropdownButton>
{isUnfolded && (
<DropdownMenuContainer onClose={() => handleIsUnfoldedChange(false)}>
<FilterDropdownEntitySearchInput context={context} />
<FilterDropdownEntitySelect context={context} />
</DropdownMenuContainer>
)}
</StyledDropdownButtonContainer>
);
}

View File

@ -0,0 +1,108 @@
import { Context, useCallback, useState } from 'react';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconChevronDown } from '@/ui/icon';
import { MenuItem } from '@/ui/menu-item/components/MenuItem';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '../types/interface';
import DropdownButton from './DropdownButton';
type OwnProps<SortField> = {
isSortSelected: boolean;
onSortSelect: (sort: SelectedSortType<SortField>) => void;
availableSorts: SortType<SortField>[];
HotkeyScope: FiltersHotkeyScope;
context: Context<string | null>;
isPrimaryButton?: boolean;
};
const options: Array<SelectedSortType<any>['order']> = ['asc', 'desc'];
export function SortDropdownButton<SortField>({
isSortSelected,
availableSorts,
onSortSelect,
HotkeyScope,
}: 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();
}
}
function handleAddSort(sort: SortType<SortField>) {
setIsUnfolded(false);
onSortItemSelect(sort);
}
return (
<DropdownButton
label="Sort"
isActive={isSortSelected}
isUnfolded={isUnfolded}
onIsUnfoldedChange={handleIsUnfoldedChange}
HotkeyScope={HotkeyScope}
>
{isOptionUnfolded ? (
<StyledDropdownMenuItemsContainer>
{options.map((option, index) => (
<MenuItem
key={index}
onClick={() => {
setSelectedSortDirection(option);
setIsOptionUnfolded(false);
}}
text={option === 'asc' ? 'Ascending' : 'Descending'}
/>
))}
</StyledDropdownMenuItemsContainer>
) : (
<>
<DropdownMenuHeader
EndIcon={IconChevronDown}
onClick={() => setIsOptionUnfolded(true)}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer>
{availableSorts.map((sort, index) => (
<MenuItem
testId={`select-sort-${index}`}
key={index}
onClick={() => handleAddSort(sort)}
LeftIcon={sort.Icon}
text={sort.label}
/>
))}
</StyledDropdownMenuItemsContainer>
</>
)}
</DropdownButton>
);
}

View File

@ -0,0 +1,82 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconX } from '@/ui/icon/index';
import { IconComponent } from '@/ui/icon/types/IconComponent';
type OwnProps = {
labelKey?: string;
labelValue: string;
Icon?: IconComponent;
onRemove: () => void;
isSort?: boolean;
testId?: string;
};
type StyledChipProps = {
isSort?: boolean;
};
const StyledChip = styled.div<StyledChipProps>`
align-items: center;
background-color: ${({ theme }) => theme.accent.quaternary};
border: 1px solid ${({ theme }) => theme.accent.tertiary};
border-radius: 4px;
color: ${({ theme }) => theme.color.blue};
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)};
`;
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.accent.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm};
}
`;
const StyledLabelKey = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
function SortOrFilterChip({
labelKey,
labelValue,
Icon,
onRemove,
isSort,
testId,
}: OwnProps) {
const theme = useTheme();
return (
<StyledChip isSort={isSort}>
{Icon && (
<StyledIcon>
<Icon />
</StyledIcon>
)}
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
{labelValue}
<StyledDelete onClick={onRemove} data-testid={'remove-icon-' + testId}>
<IconX size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
</StyledDelete>
</StyledChip>
);
}
export default SortOrFilterChip;

View File

@ -0,0 +1,220 @@
import type { Context, ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
IconArrowNarrowDown,
IconArrowNarrowUp,
IconPlus,
} from '@/ui/icon/index';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRemoveFilter } from '../hooks/useRemoveFilter';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { filtersScopedState } from '../states/filtersScopedState';
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import { SelectedSortType } from '../types/interface';
import { getOperandLabelShort } from '../utils/getOperandLabel';
import { FilterDropdownButton } from './FilterDropdownButton';
import SortOrFilterChip from './SortOrFilterChip';
type OwnProps<SortField> = {
canPersistView?: boolean;
context: Context<string | null>;
sorts: Array<SelectedSortType<SortField>>;
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
onCancelClick: () => void;
hasFilterButton?: boolean;
rightComponent?: ReactNode;
};
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;
z-index: 4;
`;
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)};
margin-right: ${({ theme }) => theme.spacing(1)};
overflow-x: auto;
`;
const StyledCancelButton = styled.button`
background-color: inherit;
border: none;
color: ${({ theme }) => theme.font.color.tertiary};
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)};
}
`;
const StyledFilterContainer = styled.div`
align-items: center;
display: flex;
`;
const StyledSeperatorContainer = styled.div`
align-items: flex-start;
align-self: stretch;
display: flex;
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(1)};
padding-right: ${({ theme }) => theme.spacing(1)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledSeperator = styled.div`
align-self: stretch;
background: ${({ theme }) => theme.background.quaternary};
width: 1px;
`;
const StyledAddFilterContainer = styled.div`
z-index: 5;
`;
function ViewBarDetails<SortField>({
canPersistView,
context,
sorts,
onRemoveSort,
onCancelClick,
hasFilterButton = false,
rightComponent,
}: OwnProps<SortField>) {
const theme = useTheme();
const [filters, setFilters] = useRecoilScopedState(
filtersScopedState,
context,
);
const [availableFilters] = useRecoilScopedState(
availableFiltersScopedState,
context,
);
const [isViewBarExpanded] = useRecoilScopedState(
isViewBarExpandedScopedState,
context,
);
const filtersWithDefinition = filters.map((filter) => {
const filterDefinition = availableFilters.find((availableFilter) => {
return availableFilter.key === filter.key;
});
return {
...filter,
...filterDefinition,
};
});
const removeFilter = useRemoveFilter(context);
function handleCancelClick() {
setFilters([]);
onCancelClick();
}
const shouldExpandViewBar =
canPersistView ||
((filtersWithDefinition.length || sorts.length) && isViewBarExpanded);
if (!shouldExpandViewBar) {
return null;
}
return (
<StyledBar>
<StyledFilterContainer>
<StyledChipcontainer>
{sorts.map((sort) => {
return (
<SortOrFilterChip
key={sort.key}
testId={sort.key}
labelValue={sort.label}
Icon={
sort.order === 'desc'
? IconArrowNarrowDown
: IconArrowNarrowUp
}
isSort
onRemove={() => onRemoveSort(sort.key)}
/>
);
})}
{!!sorts.length && !!filtersWithDefinition.length && (
<StyledSeperatorContainer>
<StyledSeperator />
</StyledSeperatorContainer>
)}
{filtersWithDefinition.map((filter) => {
return (
<SortOrFilterChip
key={filter.key}
testId={filter.key}
labelKey={filter.label}
labelValue={`${getOperandLabelShort(filter.operand)} ${
filter.displayValue
}`}
Icon={filter.Icon}
onRemove={() => {
removeFilter(filter.key);
}}
/>
);
})}
</StyledChipcontainer>
{hasFilterButton && (
<StyledAddFilterContainer>
<FilterDropdownButton
context={context}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
color={theme.font.color.tertiary}
icon={<IconPlus size={theme.icon.size.md} />}
label="Add filter"
/>
</StyledAddFilterContainer>
)}
</StyledFilterContainer>
{filters.length + sorts.length > 0 && (
<StyledCancelButton
data-testid="cancel-button"
onClick={handleCancelClick}
>
Cancel
</StyledCancelButton>
)}
{rightComponent}
</StyledBar>
);
}
export default ViewBarDetails;