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:
114
front/src/modules/ui/view-bar/components/DropdownButton.tsx
Normal file
114
front/src/modules/ui/view-bar/components/DropdownButton.tsx
Normal 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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
108
front/src/modules/ui/view-bar/components/SortDropdownButton.tsx
Normal file
108
front/src/modules/ui/view-bar/components/SortDropdownButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
220
front/src/modules/ui/view-bar/components/ViewBarDetails.tsx
Normal file
220
front/src/modules/ui/view-bar/components/ViewBarDetails.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user