Refactor UI folder (#2016)
* Added Overview page * Revised Getting Started page * Minor revision * Edited readme, minor modifications to docs * Removed sweep.yaml, .devcontainer, .ergomake * Moved security.md to .github, added contributing.md * changes as per code review * updated contributing.md * fixed broken links & added missing links in doc, improved structure * fixed link in wsl setup * fixed server link, added https cloning in yarn-setup * removed package-lock.json * added doc card, admonitions * removed underline from nav buttons * refactoring modules/ui * refactoring modules/ui * Change folder case * Fix theme location * Fix case 2 * Fix storybook --------- Co-authored-by: Nimra Ahmed <nimra1408@gmail.com> Co-authored-by: Nimra Ahmed <50912134+nimraahmed@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,24 @@
|
||||
import { IconPlus } from '@/ui/display/icon';
|
||||
import { LightButton } from '@/ui/input/button/components/LightButton';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
|
||||
import { FilterDropdownId } from '../constants/FilterDropdownId';
|
||||
|
||||
export const AddFilterFromDropdownButton = () => {
|
||||
const { toggleDropdown } = useDropdown({
|
||||
dropdownScopeId: FilterDropdownId,
|
||||
});
|
||||
|
||||
const handleClick = () => {
|
||||
toggleDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<LightButton
|
||||
onClick={handleClick}
|
||||
icon={<IconPlus />}
|
||||
title="Add filter"
|
||||
accent="tertiary"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,36 @@
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||
|
||||
import { MultipleFiltersDropdownButton } from './MultipleFiltersDropdownButton';
|
||||
import { SingleEntityFilterDropdownButton } from './SingleEntityFilterDropdownButton';
|
||||
|
||||
type FilterDropdownButtonProps = {
|
||||
hotkeyScope: HotkeyScope;
|
||||
};
|
||||
|
||||
export const FilterDropdownButton = ({
|
||||
hotkeyScope,
|
||||
}: FilterDropdownButtonProps) => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [availableFilters] = useRecoilScopedState(
|
||||
availableFiltersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const hasOnlyOneEntityFilter =
|
||||
availableFilters.length === 1 && availableFilters[0].type === 'entity';
|
||||
|
||||
if (!availableFilters.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return hasOnlyOneEntityFilter ? (
|
||||
<SingleEntityFilterDropdownButton hotkeyScope={hotkeyScope} />
|
||||
) : (
|
||||
<MultipleFiltersDropdownButton hotkeyScope={hotkeyScope} />
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { useUpsertFilter } from '@/ui/data/view-bar/hooks/useUpsertFilter';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/data/view-bar/states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '@/ui/data/view-bar/states/selectedOperandInDropdownScopedState';
|
||||
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState';
|
||||
|
||||
export const FilterDropdownDateSearchInput = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [selectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setIsFilterDropdownUnfolded] = useRecoilScopedState(
|
||||
isFilterDropdownUnfoldedScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const upsertFilter = useUpsertFilter();
|
||||
|
||||
const handleChange = (date: Date) => {
|
||||
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
|
||||
|
||||
upsertFilter({
|
||||
key: filterDefinitionUsedInDropdown.key,
|
||||
type: filterDefinitionUsedInDropdown.type,
|
||||
value: date.toISOString(),
|
||||
operand: selectedOperandInDropdown,
|
||||
displayValue: date.toLocaleDateString(),
|
||||
});
|
||||
|
||||
setIsFilterDropdownUnfolded(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<InternalDatePicker
|
||||
date={new Date()}
|
||||
onChange={handleChange}
|
||||
onMouseSelect={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/data/view-bar/states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { filterDropdownSearchInputScopedState } from '@/ui/data/view-bar/states/filterDropdownSearchInputScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '@/ui/data/view-bar/states/selectedOperandInDropdownScopedState';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
|
||||
export const FilterDropdownEntitySearchInput = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [selectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [filterDropdownSearchInput, setFilterDropdownSearchInput] =
|
||||
useRecoilScopedState(
|
||||
filterDropdownSearchInputScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
return (
|
||||
filterDefinitionUsedInDropdown &&
|
||||
selectedOperandInDropdown && (
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
type="text"
|
||||
value={filterDropdownSearchInput}
|
||||
placeholder={filterDefinitionUsedInDropdown.label}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterDropdownSearchInput(event.target.value);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,153 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useFilterCurrentlyEdited } from '@/ui/data/view-bar/hooks/useFilterCurrentlyEdited';
|
||||
import { useRemoveFilter } from '@/ui/data/view-bar/hooks/useRemoveFilter';
|
||||
import { useUpsertFilter } from '@/ui/data/view-bar/hooks/useUpsertFilter';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/data/view-bar/states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { filterDropdownSelectedEntityIdScopedState } from '@/ui/data/view-bar/states/filterDropdownSelectedEntityIdScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '@/ui/data/view-bar/states/selectedOperandInDropdownScopedState';
|
||||
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 { ViewFilterOperand } from '~/generated/graphql';
|
||||
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState';
|
||||
|
||||
export const FilterDropdownEntitySearchSelect = ({
|
||||
entitiesForSelect,
|
||||
}: {
|
||||
entitiesForSelect: EntitiesForMultipleEntitySelect<EntityForSelect>;
|
||||
}) => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [isAllEntitySelected, setIsAllEntitySelected] = useState(false);
|
||||
|
||||
const [filterDropdownSelectedEntityId, setFilterDropdownSelectedEntityId] =
|
||||
useRecoilScopedState(
|
||||
filterDropdownSelectedEntityIdScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [selectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const upsertFilter = useUpsertFilter();
|
||||
const removeFilter = useRemoveFilter();
|
||||
|
||||
const filterCurrentlyEdited = useFilterCurrentlyEdited();
|
||||
|
||||
const handleUserSelected = (
|
||||
selectedEntity: EntityForSelect | null | undefined,
|
||||
) => {
|
||||
if (
|
||||
!filterDefinitionUsedInDropdown ||
|
||||
!selectedOperandInDropdown ||
|
||||
!selectedEntity
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAllEntitySelected) {
|
||||
setIsAllEntitySelected(false);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [filterDropdownSearchInput] = useRecoilScopedState(
|
||||
filterDropdownSearchInputScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const isAllEntitySelectShown =
|
||||
!!filterDefinitionUsedInDropdown?.selectAllLabel &&
|
||||
!!filterDefinitionUsedInDropdown?.SelectAllIcon &&
|
||||
(isAllEntitySelected ||
|
||||
filterDefinitionUsedInDropdown?.selectAllLabel
|
||||
.toLocaleLowerCase()
|
||||
.includes(filterDropdownSearchInput.toLocaleLowerCase()));
|
||||
|
||||
const handleAllEntitySelectClick = () => {
|
||||
if (
|
||||
!filterDefinitionUsedInDropdown ||
|
||||
!selectedOperandInDropdown ||
|
||||
!filterDefinitionUsedInDropdown.selectAllLabel
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (isAllEntitySelected) {
|
||||
setIsAllEntitySelected(false);
|
||||
|
||||
removeFilter(filterDefinitionUsedInDropdown.key);
|
||||
} else {
|
||||
setIsAllEntitySelected(true);
|
||||
|
||||
setFilterDropdownSelectedEntityId(null);
|
||||
|
||||
upsertFilter({
|
||||
displayValue: filterDefinitionUsedInDropdown.selectAllLabel,
|
||||
key: filterDefinitionUsedInDropdown.key,
|
||||
operand: ViewFilterOperand.IsNotNull,
|
||||
type: filterDefinitionUsedInDropdown.type,
|
||||
value: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!filterCurrentlyEdited) {
|
||||
setFilterDropdownSelectedEntityId(null);
|
||||
} else {
|
||||
setFilterDropdownSelectedEntityId(filterCurrentlyEdited.value);
|
||||
setIsAllEntitySelected(
|
||||
filterCurrentlyEdited.operand === ViewFilterOperand.IsNotNull,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
filterCurrentlyEdited,
|
||||
setFilterDropdownSelectedEntityId,
|
||||
entitiesForSelect.selectedEntities,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SingleEntitySelectBase
|
||||
entitiesToSelect={entitiesForSelect.entitiesToSelect}
|
||||
selectedEntity={entitiesForSelect.selectedEntities[0]}
|
||||
loading={entitiesForSelect.loading}
|
||||
onEntitySelected={handleUserSelected}
|
||||
SelectAllIcon={filterDefinitionUsedInDropdown?.SelectAllIcon}
|
||||
selectAllLabel={filterDefinitionUsedInDropdown?.selectAllLabel}
|
||||
isAllEntitySelected={isAllEntitySelected}
|
||||
isAllEntitySelectShown={isAllEntitySelectShown}
|
||||
onAllEntitySelected={handleAllEntitySelectClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
|
||||
export const FilterDropdownEntitySelect = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
if (filterDefinitionUsedInDropdown?.type !== 'entity') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<RecoilScope>
|
||||
{filterDefinitionUsedInDropdown.entitySelectComponent}
|
||||
</RecoilScope>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,65 @@
|
||||
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { MenuItem } from '@/ui/navigation/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 { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
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 const FilterDropdownFilterSelect = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
|
||||
filterDropdownSearchInputScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const availableFilters = useRecoilScopedValue(
|
||||
availableFiltersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer>
|
||||
{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}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useRemoveFilter } from '../hooks/useRemoveFilter';
|
||||
import { useUpsertFilter } from '../hooks/useUpsertFilter';
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
|
||||
|
||||
export const FilterDropdownNumberSearchInput = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [selectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const upsertFilter = useUpsertFilter();
|
||||
const removeFilter = useRemoveFilter();
|
||||
|
||||
return (
|
||||
filterDefinitionUsedInDropdown &&
|
||||
selectedOperandInDropdown && (
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
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,39 @@
|
||||
import { IconChevronDown } from '@/ui/display/icon';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
|
||||
import { getOperandLabel } from '../utils/getOperandLabel';
|
||||
|
||||
export const FilterDropdownOperandButton = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [selectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [
|
||||
isFilterDropdownOperandSelectUnfolded,
|
||||
setIsFilterDropdownOperandSelectUnfolded,
|
||||
] = useRecoilScopedState(
|
||||
isFilterDropdownOperandSelectUnfoldedScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
if (isFilterDropdownOperandSelectUnfolded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenuHeader
|
||||
key={'selected-filter-operand'}
|
||||
EndIcon={IconChevronDown}
|
||||
onClick={() => setIsFilterDropdownOperandSelectUnfolded(true)}
|
||||
>
|
||||
{getOperandLabel(selectedOperandInDropdown)}
|
||||
</DropdownMenuHeader>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,76 @@
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { ViewFilterOperand } from '~/generated/graphql';
|
||||
|
||||
import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited';
|
||||
import { useUpsertFilter } from '../hooks/useUpsertFilter';
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
|
||||
import { getOperandLabel } from '../utils/getOperandLabel';
|
||||
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
|
||||
|
||||
export const FilterDropdownOperandSelect = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const operandsForFilterType = getOperandsForFilterType(
|
||||
filterDefinitionUsedInDropdown?.type,
|
||||
);
|
||||
|
||||
const [
|
||||
isFilterDropdownOperandSelectUnfolded,
|
||||
setIsFilterDropdownOperandSelectUnfolded,
|
||||
] = useRecoilScopedState(
|
||||
isFilterDropdownOperandSelectUnfoldedScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const filterCurrentlyEdited = useFilterCurrentlyEdited();
|
||||
|
||||
const upsertFilter = useUpsertFilter();
|
||||
|
||||
const handleOperangeChange = (newOperand: ViewFilterOperand) => {
|
||||
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 (
|
||||
<DropdownMenuItemsContainer>
|
||||
{operandsForFilterType.map((filterOperand, index) => (
|
||||
<MenuItem
|
||||
key={`select-filter-operand-${index}`}
|
||||
onClick={() => {
|
||||
handleOperangeChange(filterOperand);
|
||||
}}
|
||||
text={getOperandLabel(filterOperand)}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,64 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
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 { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
|
||||
|
||||
export const FilterDropdownTextSearchInput = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [selectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [filterDropdownSearchInput, setFilterDropdownSearchInput] =
|
||||
useRecoilScopedState(
|
||||
filterDropdownSearchInputScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const upsertFilter = useUpsertFilter();
|
||||
const removeFilter = useRemoveFilter();
|
||||
|
||||
const filterCurrentlyEdited = useFilterCurrentlyEdited();
|
||||
|
||||
return (
|
||||
filterDefinitionUsedInDropdown &&
|
||||
selectedOperandInDropdown && (
|
||||
<DropdownMenuSearchInput
|
||||
autoFocus
|
||||
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,22 @@
|
||||
import { EntityChip } from '@/ui/display/chip/components/EntityChip';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
|
||||
import { Filter } from '../types/Filter';
|
||||
|
||||
type GenericEntityFilterChipProps = {
|
||||
filter: Filter;
|
||||
Icon?: IconComponent;
|
||||
};
|
||||
|
||||
export const GenericEntityFilterChip = ({
|
||||
filter,
|
||||
Icon,
|
||||
}: GenericEntityFilterChipProps) => (
|
||||
<EntityChip
|
||||
entityId={filter.value}
|
||||
name={filter.displayValue}
|
||||
avatarType="rounded"
|
||||
pictureUrl={filter.displayAvatarUrl}
|
||||
LeftIcon={Icon}
|
||||
/>
|
||||
);
|
||||
@ -0,0 +1,59 @@
|
||||
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { FilterDropdownId } from '../constants/FilterDropdownId';
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState';
|
||||
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
|
||||
|
||||
export const MultipleFiltersButton = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const { isDropdownOpen, toggleDropdown } = useDropdown({
|
||||
dropdownScopeId: FilterDropdownId,
|
||||
});
|
||||
|
||||
const [, setIsFilterDropdownOperandSelectUnfolded] = useRecoilScopedState(
|
||||
isFilterDropdownOperandSelectUnfoldedScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
|
||||
filterDropdownSearchInputScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const resetState = () => {
|
||||
setIsFilterDropdownOperandSelectUnfolded(false);
|
||||
setFilterDefinitionUsedInDropdown(null);
|
||||
setSelectedOperandInDropdown(null);
|
||||
setFilterDropdownSearchInput('');
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
toggleDropdown();
|
||||
resetState();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHeaderDropdownButton
|
||||
isUnfolded={isDropdownOpen}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Filter
|
||||
</StyledHeaderDropdownButton>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,24 @@
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
import { FilterDropdownId } from '../constants/FilterDropdownId';
|
||||
|
||||
import { MultipleFiltersButton } from './MultipleFiltersButton';
|
||||
import { MultipleFiltersDropdownContent } from './MultipleFiltersDropdownContent';
|
||||
import { ViewBarDropdownButton } from './ViewBarDropdownButton';
|
||||
|
||||
type MultipleFiltersDropdownButtonProps = {
|
||||
hotkeyScope: HotkeyScope;
|
||||
};
|
||||
|
||||
export const MultipleFiltersDropdownButton = ({
|
||||
hotkeyScope,
|
||||
}: MultipleFiltersDropdownButtonProps) => {
|
||||
return (
|
||||
<ViewBarDropdownButton
|
||||
dropdownId={FilterDropdownId}
|
||||
buttonComponent={<MultipleFiltersButton />}
|
||||
dropdownComponents={<MultipleFiltersDropdownContent />}
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu';
|
||||
import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
|
||||
|
||||
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 const MultipleFiltersDropdownContent = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [isFilterDropdownOperandSelectUnfolded] = useRecoilScopedState(
|
||||
isFilterDropdownOperandSelectUnfoldedScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [selectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDropdownMenu>
|
||||
<>
|
||||
{!filterDefinitionUsedInDropdown ? (
|
||||
<FilterDropdownFilterSelect />
|
||||
) : isFilterDropdownOperandSelectUnfolded ? (
|
||||
<FilterDropdownOperandSelect />
|
||||
) : (
|
||||
selectedOperandInDropdown && (
|
||||
<>
|
||||
<FilterDropdownOperandButton />
|
||||
<StyledDropdownMenuSeparator />
|
||||
{filterDefinitionUsedInDropdown.type === 'text' && (
|
||||
<FilterDropdownTextSearchInput />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'number' && (
|
||||
<FilterDropdownNumberSearchInput />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'date' && (
|
||||
<FilterDropdownDateSearchInput />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
||||
<FilterDropdownEntitySearchInput />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
||||
<FilterDropdownEntitySelect />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
</StyledDropdownMenu>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { IconChevronDown } from '@/ui/display/icon/index';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuContainer } from '@/ui/layout/dropdown/components/DropdownMenuContainer';
|
||||
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { ViewFilterOperand } from '~/generated/graphql';
|
||||
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { filtersScopedState } from '../states/filtersScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
|
||||
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
|
||||
|
||||
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
|
||||
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
|
||||
import { GenericEntityFilterChip } from './GenericEntityFilterChip';
|
||||
|
||||
export const SingleEntityFilterDropdownButton = ({
|
||||
hotkeyScope,
|
||||
}: {
|
||||
hotkeyScope: HotkeyScope;
|
||||
}) => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [availableFilters] = useRecoilScopedState(
|
||||
availableFiltersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
const availableFilter = availableFilters[0];
|
||||
|
||||
const [filters] = useRecoilScopedState(
|
||||
filtersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFilterDefinitionUsedInDropdown(availableFilter);
|
||||
const defaultOperand = getOperandsForFilterType(availableFilter?.type)[0];
|
||||
setSelectedOperandInDropdown(defaultOperand);
|
||||
}, [
|
||||
availableFilter,
|
||||
setFilterDefinitionUsedInDropdown,
|
||||
setSelectedOperandInDropdown,
|
||||
]);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownScope dropdownScopeId="single-entity-filter-dropdown">
|
||||
<DropdownMenu
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
dropdownOffset={{ x: 0, y: -28 }}
|
||||
clickableComponent={
|
||||
<StyledHeaderDropdownButton>
|
||||
{filters[0] ? (
|
||||
<GenericEntityFilterChip
|
||||
filter={filters[0]}
|
||||
Icon={
|
||||
filters[0].operand === ViewFilterOperand.IsNotNull
|
||||
? availableFilter.SelectAllIcon
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
'Filter'
|
||||
)}
|
||||
<IconChevronDown size={theme.icon.size.md} />
|
||||
</StyledHeaderDropdownButton>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenuContainer>
|
||||
<FilterDropdownEntitySearchInput />
|
||||
<FilterDropdownEntitySelect />
|
||||
</DropdownMenuContainer>
|
||||
}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,146 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { IconChevronDown } from '@/ui/display/icon';
|
||||
import { LightButton } from '@/ui/input/button/components/LightButton';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu';
|
||||
import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { SortDropdownId } from '../constants/SortDropdownId';
|
||||
import { useViewBarContext } from '../hooks/useViewBarContext';
|
||||
import { availableSortsScopedState } from '../states/availableSortsScopedState';
|
||||
import { sortsScopedState } from '../states/sortsScopedState';
|
||||
import { SortDefinition } from '../types/SortDefinition';
|
||||
import { SORT_DIRECTIONS, SortDirection } from '../types/SortDirection';
|
||||
|
||||
import { ViewBarDropdownButton } from './ViewBarDropdownButton';
|
||||
|
||||
export type SortDropdownButtonProps = {
|
||||
hotkeyScope: HotkeyScope;
|
||||
isPrimaryButton?: boolean;
|
||||
};
|
||||
|
||||
export const SortDropdownButton = ({
|
||||
hotkeyScope,
|
||||
}: SortDropdownButtonProps) => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [isSortDirectionMenuUnfolded, setIsSortDirectionMenuUnfolded] =
|
||||
useState(false);
|
||||
|
||||
const [selectedSortDirection, setSelectedSortDirection] =
|
||||
useState<SortDirection>('asc');
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setIsSortDirectionMenuUnfolded(false);
|
||||
setSelectedSortDirection('asc');
|
||||
}, []);
|
||||
|
||||
const [availableSorts] = useRecoilScopedState(
|
||||
availableSortsScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [sorts, setSorts] = useRecoilScopedState(
|
||||
sortsScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const isSortSelected = sorts.length > 0;
|
||||
|
||||
const { toggleDropdown } = useDropdown({
|
||||
dropdownScopeId: SortDropdownId,
|
||||
});
|
||||
|
||||
const handleButtonClick = () => {
|
||||
toggleDropdown();
|
||||
resetState();
|
||||
};
|
||||
|
||||
const handleAddSort = (selectedSortDefinition: SortDefinition) => {
|
||||
toggleDropdown();
|
||||
|
||||
setSorts(
|
||||
produce(sorts, (existingSortsDraft) => {
|
||||
const foundExistingSortIndex = existingSortsDraft.findIndex(
|
||||
(existingSort) => existingSort.key === selectedSortDefinition.key,
|
||||
);
|
||||
|
||||
if (foundExistingSortIndex !== -1) {
|
||||
existingSortsDraft[foundExistingSortIndex].direction =
|
||||
selectedSortDirection;
|
||||
} else {
|
||||
existingSortsDraft.push({
|
||||
key: selectedSortDefinition.key,
|
||||
direction: selectedSortDirection,
|
||||
definition: selectedSortDefinition,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleDropdownButtonClose = () => {
|
||||
resetState();
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewBarDropdownButton
|
||||
dropdownId={SortDropdownId}
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
buttonComponent={
|
||||
<LightButton
|
||||
title="Sort"
|
||||
active={isSortSelected}
|
||||
onClick={handleButtonClick}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<StyledDropdownMenu>
|
||||
{isSortDirectionMenuUnfolded ? (
|
||||
<DropdownMenuItemsContainer>
|
||||
{SORT_DIRECTIONS.map((sortOrder, index) => (
|
||||
<MenuItem
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setSelectedSortDirection(sortOrder);
|
||||
setIsSortDirectionMenuUnfolded(false);
|
||||
}}
|
||||
text={sortOrder === 'asc' ? 'Ascending' : 'Descending'}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
EndIcon={IconChevronDown}
|
||||
onClick={() => setIsSortDirectionMenuUnfolded(true)}
|
||||
>
|
||||
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
|
||||
</DropdownMenuHeader>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
{availableSorts.map((availableSort, index) => (
|
||||
<MenuItem
|
||||
testId={`select-sort-${index}`}
|
||||
key={index}
|
||||
onClick={() => handleAddSort(availableSort)}
|
||||
LeftIcon={availableSort.Icon}
|
||||
text={availableSort.label}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
</StyledDropdownMenu>
|
||||
}
|
||||
onClose={handleDropdownButtonClose}
|
||||
></ViewBarDropdownButton>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,82 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconX } from '@/ui/display/icon/index';
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
|
||||
type SortOrFilterChipProps = {
|
||||
labelKey?: string;
|
||||
labelValue: string;
|
||||
Icon?: IconComponent;
|
||||
onRemove: () => void;
|
||||
isSort?: boolean;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
type StyledChipProps = {
|
||||
isSort?: boolean;
|
||||
};
|
||||
|
||||
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};
|
||||
`;
|
||||
|
||||
const SortOrFilterChip = ({
|
||||
labelKey,
|
||||
labelValue,
|
||||
Icon,
|
||||
onRemove,
|
||||
isSort,
|
||||
testId,
|
||||
}: SortOrFilterChipProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledChip isSort={isSort}>
|
||||
{Icon && (
|
||||
<StyledIcon>
|
||||
<Icon size={theme.icon.size.sm} />
|
||||
</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;
|
||||
@ -0,0 +1,141 @@
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
|
||||
import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState';
|
||||
import { savedFiltersFamilyState } from '@/ui/data/view-bar/states/savedFiltersFamilyState';
|
||||
import { savedSortsFamilyState } from '@/ui/data/view-bar/states/savedSortsFamilyState';
|
||||
import { canPersistFiltersScopedFamilySelector } from '@/ui/data/view-bar/states/selectors/canPersistFiltersScopedFamilySelector';
|
||||
import { canPersistSortsScopedFamilySelector } from '@/ui/data/view-bar/states/selectors/canPersistSortsScopedFamilySelector';
|
||||
import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState';
|
||||
import { viewEditModeState } from '@/ui/data/view-bar/states/viewEditModeState';
|
||||
import { IconChevronDown, IconPlus } from '@/ui/display/icon';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
|
||||
import { DropdownMenuContainer } from '@/ui/layout/dropdown/components/DropdownMenuContainer';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
|
||||
|
||||
import { ViewBarContext } from '../contexts/ViewBarContext';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: inline-flex;
|
||||
margin-right: ${({ theme }) => theme.spacing(2)};
|
||||
position: relative;
|
||||
`;
|
||||
export type UpdateViewButtonGroupProps = {
|
||||
hotkeyScope: string;
|
||||
onViewEditModeChange?: () => void;
|
||||
};
|
||||
|
||||
export const UpdateViewButtonGroup = ({
|
||||
hotkeyScope,
|
||||
onViewEditModeChange,
|
||||
}: UpdateViewButtonGroupProps) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const {
|
||||
canPersistViewFields,
|
||||
onCurrentViewSubmit,
|
||||
ViewBarRecoilScopeContext,
|
||||
} = useContext(ViewBarContext);
|
||||
|
||||
const recoilScopeId = useRecoilScopeId(ViewBarRecoilScopeContext);
|
||||
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentViewIdScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const filters = useRecoilScopedValue(
|
||||
filtersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
const setSavedFilters = useSetRecoilState(
|
||||
savedFiltersFamilyState(currentViewId),
|
||||
);
|
||||
const canPersistFilters = useRecoilValue(
|
||||
canPersistFiltersScopedFamilySelector({
|
||||
recoilScopeId,
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
);
|
||||
|
||||
const sorts = useRecoilScopedValue(
|
||||
sortsScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
const setSavedSorts = useSetRecoilState(savedSortsFamilyState(currentViewId));
|
||||
const canPersistSorts = useRecoilValue(
|
||||
canPersistSortsScopedFamilySelector({
|
||||
recoilScopeId,
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
);
|
||||
|
||||
const setViewEditMode = useSetRecoilState(viewEditModeState);
|
||||
|
||||
const canPersistView =
|
||||
currentViewId &&
|
||||
(canPersistViewFields || canPersistFilters || canPersistSorts);
|
||||
|
||||
const handleArrowDownButtonClick = useCallback(() => {
|
||||
setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
|
||||
}, []);
|
||||
|
||||
const handleCreateViewButtonClick = useCallback(() => {
|
||||
setViewEditMode({ mode: 'create', viewId: undefined });
|
||||
onViewEditModeChange?.();
|
||||
setIsDropdownOpen(false);
|
||||
}, [setViewEditMode, onViewEditModeChange]);
|
||||
|
||||
const handleDropdownClose = useCallback(() => {
|
||||
setIsDropdownOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleViewSubmit = async () => {
|
||||
if (canPersistFilters) setSavedFilters(filters);
|
||||
if (canPersistSorts) setSavedSorts(sorts);
|
||||
|
||||
await onCurrentViewSubmit?.();
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Enter, Key.Escape],
|
||||
handleDropdownClose,
|
||||
hotkeyScope,
|
||||
[],
|
||||
);
|
||||
|
||||
if (!canPersistView) return null;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ButtonGroup size="small" accent="blue">
|
||||
<Button title="Update view" onClick={handleViewSubmit} />
|
||||
<Button
|
||||
size="small"
|
||||
Icon={IconChevronDown}
|
||||
onClick={handleArrowDownButtonClick}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
||||
{isDropdownOpen && (
|
||||
<DropdownMenuContainer onClose={handleDropdownClose}>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleCreateViewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Create view"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenuContainer>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
65
front/src/modules/ui/data/view-bar/components/ViewBar.tsx
Normal file
65
front/src/modules/ui/data/view-bar/components/ViewBar.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { TopBar } from '@/ui/layout/top-bar/TopBar';
|
||||
|
||||
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
|
||||
import { ViewsHotkeyScope } from '../types/ViewsHotkeyScope';
|
||||
|
||||
import { FilterDropdownButton } from './FilterDropdownButton';
|
||||
import { SortDropdownButton } from './SortDropdownButton';
|
||||
import { UpdateViewButtonGroup } from './UpdateViewButtonGroup';
|
||||
import { ViewBarDetails } from './ViewBarDetails';
|
||||
import { ViewsDropdownButton } from './ViewsDropdownButton';
|
||||
|
||||
export type ViewBarProps = {
|
||||
className?: string;
|
||||
optionsDropdownButton: ReactNode;
|
||||
optionsDropdownScopeId: string;
|
||||
};
|
||||
|
||||
export const ViewBar = ({
|
||||
className,
|
||||
optionsDropdownButton,
|
||||
optionsDropdownScopeId,
|
||||
}: ViewBarProps) => {
|
||||
const { openDropdown: openOptionsDropdownButton } = useDropdown({
|
||||
dropdownScopeId: optionsDropdownScopeId,
|
||||
});
|
||||
|
||||
return (
|
||||
<TopBar
|
||||
className={className}
|
||||
leftComponent={
|
||||
<ViewsDropdownButton
|
||||
onViewEditModeChange={openOptionsDropdownButton}
|
||||
hotkeyScope={{ scope: ViewsHotkeyScope.ListDropdown }}
|
||||
/>
|
||||
}
|
||||
displayBottomBorder={false}
|
||||
rightComponent={
|
||||
<>
|
||||
<FilterDropdownButton
|
||||
hotkeyScope={{ scope: FiltersHotkeyScope.FilterDropdownButton }}
|
||||
/>
|
||||
<SortDropdownButton
|
||||
hotkeyScope={{ scope: FiltersHotkeyScope.SortDropdownButton }}
|
||||
isPrimaryButton
|
||||
/>
|
||||
{optionsDropdownButton}
|
||||
</>
|
||||
}
|
||||
bottomComponent={
|
||||
<ViewBarDetails
|
||||
hasFilterButton
|
||||
rightComponent={
|
||||
<UpdateViewButtonGroup
|
||||
onViewEditModeChange={openOptionsDropdownButton}
|
||||
hotkeyScope={ViewsHotkeyScope.CreateDropdown}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
242
front/src/modules/ui/data/view-bar/components/ViewBarDetails.tsx
Normal file
242
front/src/modules/ui/data/view-bar/components/ViewBarDetails.tsx
Normal file
@ -0,0 +1,242 @@
|
||||
import { ReactNode, useContext } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { IconArrowDown, IconArrowUp } from '@/ui/display/icon/index';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
|
||||
|
||||
import { ViewBarContext } from '../contexts/ViewBarContext';
|
||||
import { useRemoveFilter } from '../hooks/useRemoveFilter';
|
||||
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
|
||||
import { filtersScopedState } from '../states/filtersScopedState';
|
||||
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
|
||||
import { canPersistFiltersScopedFamilySelector } from '../states/selectors/canPersistFiltersScopedFamilySelector';
|
||||
import { canPersistSortsScopedFamilySelector } from '../states/selectors/canPersistSortsScopedFamilySelector';
|
||||
import { savedFiltersFamilySelector } from '../states/selectors/savedFiltersFamilySelector';
|
||||
import { savedSortsFamilySelector } from '../states/selectors/savedSortsFamilySelector';
|
||||
import { sortsScopedState } from '../states/sortsScopedState';
|
||||
import { getOperandLabelShort } from '../utils/getOperandLabel';
|
||||
|
||||
import { AddFilterFromDropdownButton } from './AddFilterFromDetailsButton';
|
||||
import SortOrFilterChip from './SortOrFilterChip';
|
||||
|
||||
export type ViewBarDetailsProps = {
|
||||
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)};
|
||||
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`
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
z-index: 5;
|
||||
`;
|
||||
|
||||
export const ViewBarDetails = ({
|
||||
hasFilterButton = false,
|
||||
rightComponent,
|
||||
}: ViewBarDetailsProps) => {
|
||||
const { canPersistViewFields, onViewBarReset, ViewBarRecoilScopeContext } =
|
||||
useContext(ViewBarContext);
|
||||
|
||||
const recoilScopeId = useRecoilScopeId(ViewBarRecoilScopeContext);
|
||||
|
||||
const currentViewId = useRecoilScopedValue(
|
||||
currentViewIdScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [filters, setFilters] = useRecoilScopedState(
|
||||
filtersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const savedFilters = useRecoilValue(
|
||||
savedFiltersFamilySelector(currentViewId),
|
||||
);
|
||||
|
||||
const savedSorts = useRecoilValue(savedSortsFamilySelector(currentViewId));
|
||||
|
||||
const [availableFilters] = useRecoilScopedState(
|
||||
availableFiltersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
const canPersistFilters = useRecoilValue(
|
||||
canPersistFiltersScopedFamilySelector({
|
||||
recoilScopeId,
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
);
|
||||
|
||||
const [sorts, setSorts] = useRecoilScopedState(
|
||||
sortsScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const canPersistSorts = useRecoilValue(
|
||||
canPersistSortsScopedFamilySelector({
|
||||
recoilScopeId,
|
||||
viewId: currentViewId,
|
||||
}),
|
||||
);
|
||||
|
||||
const canPersistView =
|
||||
canPersistViewFields || canPersistFilters || canPersistSorts;
|
||||
|
||||
const [isViewBarExpanded] = useRecoilScopedState(
|
||||
isViewBarExpandedScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const filtersWithDefinition = filters.map((filter) => {
|
||||
const filterDefinition = availableFilters.find((availableFilter) => {
|
||||
return availableFilter.key === filter.key;
|
||||
});
|
||||
|
||||
return {
|
||||
...filter,
|
||||
...filterDefinition,
|
||||
};
|
||||
});
|
||||
|
||||
const removeFilter = useRemoveFilter();
|
||||
|
||||
const handleCancelClick = () => {
|
||||
onViewBarReset?.();
|
||||
setFilters(savedFilters);
|
||||
setSorts(savedSorts);
|
||||
};
|
||||
|
||||
const handleSortRemove = (sortKey: string) =>
|
||||
setSorts((previousSorts) =>
|
||||
previousSorts.filter((sort) => sort.key !== sortKey),
|
||||
);
|
||||
|
||||
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.definition.label}
|
||||
Icon={sort.direction === 'desc' ? IconArrowDown : IconArrowUp}
|
||||
isSort
|
||||
onRemove={() => handleSortRemove(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>
|
||||
<AddFilterFromDropdownButton />
|
||||
</StyledAddFilterContainer>
|
||||
)}
|
||||
</StyledFilterContainer>
|
||||
{canPersistView && (
|
||||
<StyledCancelButton
|
||||
data-testid="cancel-button"
|
||||
onClick={handleCancelClick}
|
||||
>
|
||||
Reset
|
||||
</StyledCancelButton>
|
||||
)}
|
||||
{rightComponent}
|
||||
</StyledBar>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,49 @@
|
||||
import { Keys } from 'react-hotkeys-hook';
|
||||
import { Placement } from '@floating-ui/react';
|
||||
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
|
||||
type ViewBarDropdownButtonProps = {
|
||||
buttonComponent: JSX.Element | JSX.Element[];
|
||||
dropdownComponents: JSX.Element | JSX.Element[];
|
||||
dropdownId: string;
|
||||
hotkey?: {
|
||||
key: Keys;
|
||||
scope: string;
|
||||
};
|
||||
dropdownHotkeyScope: HotkeyScope;
|
||||
dropdownPlacement?: Placement;
|
||||
onClickOutside?: () => void;
|
||||
onClose?: () => void;
|
||||
onOpen?: () => void;
|
||||
};
|
||||
|
||||
export const ViewBarDropdownButton = ({
|
||||
buttonComponent,
|
||||
dropdownComponents,
|
||||
dropdownId,
|
||||
hotkey,
|
||||
dropdownHotkeyScope,
|
||||
dropdownPlacement = 'bottom-end',
|
||||
onClickOutside,
|
||||
onClose,
|
||||
onOpen,
|
||||
}: ViewBarDropdownButtonProps) => {
|
||||
return (
|
||||
<DropdownScope dropdownScopeId={dropdownId}>
|
||||
<DropdownMenu
|
||||
clickableComponent={buttonComponent}
|
||||
dropdownComponents={dropdownComponents}
|
||||
hotkey={hotkey}
|
||||
dropdownHotkeyScope={dropdownHotkeyScope}
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
dropdownPlacement={dropdownPlacement}
|
||||
onClickOutside={onClickOutside}
|
||||
onClose={onClose}
|
||||
onOpen={onOpen}
|
||||
/>
|
||||
</DropdownScope>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,142 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
DropResult,
|
||||
OnDragEndResponder,
|
||||
ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
|
||||
import { IconMinus, IconPlus } from '@/ui/display/icon';
|
||||
import { AppTooltip } from '@/ui/display/tooltip/AppTooltip';
|
||||
import { IconInfoCircle } from '@/ui/input/constants/icons';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { ViewFieldForVisibility } from '../types/ViewFieldForVisibility';
|
||||
|
||||
type ViewFieldsVisibilityDropdownSectionProps = {
|
||||
fields: ViewFieldForVisibility[];
|
||||
onVisibilityChange: (field: ViewFieldForVisibility) => void;
|
||||
title: string;
|
||||
isDraggable: boolean;
|
||||
onDragEnd?: OnDragEndResponder;
|
||||
};
|
||||
|
||||
export const ViewFieldsVisibilityDropdownSection = ({
|
||||
fields,
|
||||
onVisibilityChange,
|
||||
title,
|
||||
isDraggable,
|
||||
onDragEnd,
|
||||
}: ViewFieldsVisibilityDropdownSectionProps) => {
|
||||
const handleOnDrag = (result: DropResult, provided: ResponderProvided) => {
|
||||
onDragEnd?.(result, provided);
|
||||
};
|
||||
|
||||
const [openToolTipIndex, setOpenToolTipIndex] = useState<number>();
|
||||
|
||||
const handleInfoButtonClick = (index: number) => {
|
||||
if (index === openToolTipIndex) setOpenToolTipIndex(undefined);
|
||||
else setOpenToolTipIndex(index);
|
||||
};
|
||||
|
||||
const getIconButtons = (index: number, field: ViewFieldForVisibility) => {
|
||||
if (field.infoTooltipContent) {
|
||||
return [
|
||||
{
|
||||
Icon: IconInfoCircle,
|
||||
onClick: () => handleInfoButtonClick(index),
|
||||
isActive: openToolTipIndex === index,
|
||||
},
|
||||
{
|
||||
Icon: field.isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (!field.infoTooltipContent) {
|
||||
return [
|
||||
{
|
||||
Icon: field.isVisible ? IconMinus : IconPlus,
|
||||
onClick: () => onVisibilityChange(field),
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [ref],
|
||||
callback: () => {
|
||||
setOpenToolTipIndex(undefined);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{isDraggable ? (
|
||||
<DraggableList
|
||||
onDragEnd={handleOnDrag}
|
||||
draggableItems={
|
||||
<>
|
||||
{fields
|
||||
.filter(({ index, size }) => index !== 0 || !size)
|
||||
.map((field, index) => (
|
||||
<DraggableItem
|
||||
key={field.key}
|
||||
draggableId={field.key}
|
||||
index={index + 1}
|
||||
itemComponent={
|
||||
<MenuItemDraggable
|
||||
key={field.key}
|
||||
LeftIcon={field.Icon}
|
||||
iconButtons={getIconButtons(index + 1, field)}
|
||||
isTooltipOpen={openToolTipIndex === index + 1}
|
||||
text={field.name}
|
||||
className={`${title}-draggable-item-tooltip-anchor-${
|
||||
index + 1
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
fields.map((field, index) => (
|
||||
<MenuItem
|
||||
key={field.key}
|
||||
LeftIcon={field.Icon}
|
||||
iconButtons={getIconButtons(index, field)}
|
||||
isTooltipOpen={openToolTipIndex === index}
|
||||
text={field.name}
|
||||
className={`${title}-fixed-item-tooltip-anchor-${index}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
{isDefined(openToolTipIndex) &&
|
||||
createPortal(
|
||||
<AppTooltip
|
||||
anchorSelect={`.${title}-${
|
||||
isDraggable ? 'draggable' : 'fixed'
|
||||
}-item-tooltip-anchor-${openToolTipIndex}`}
|
||||
place="left"
|
||||
content={fields[openToolTipIndex].infoTooltipContent}
|
||||
isOpen={true}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,214 @@
|
||||
import { MouseEvent, useContext } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
RecoilValueReadOnly,
|
||||
useRecoilCallback,
|
||||
useRecoilValue,
|
||||
useSetRecoilState,
|
||||
} from 'recoil';
|
||||
|
||||
import { currentViewIdScopedState } from '@/ui/data/view-bar/states/currentViewIdScopedState';
|
||||
import { entityCountInCurrentViewState } from '@/ui/data/view-bar/states/entityCountInCurrentViewState';
|
||||
import { filtersScopedState } from '@/ui/data/view-bar/states/filtersScopedState';
|
||||
import { savedFiltersFamilyState } from '@/ui/data/view-bar/states/savedFiltersFamilyState';
|
||||
import { savedSortsFamilyState } from '@/ui/data/view-bar/states/savedSortsFamilyState';
|
||||
import { currentViewScopedSelector } from '@/ui/data/view-bar/states/selectors/currentViewScopedSelector';
|
||||
import { sortsScopedState } from '@/ui/data/view-bar/states/sortsScopedState';
|
||||
import { viewEditModeState } from '@/ui/data/view-bar/states/viewEditModeState';
|
||||
import { viewsScopedState } from '@/ui/data/view-bar/states/viewsScopedState';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconList,
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
} from '@/ui/display/icon';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
|
||||
import { StyledDropdownMenu } from '@/ui/layout/dropdown/components/StyledDropdownMenu';
|
||||
import { StyledDropdownMenuSeparator } from '@/ui/layout/dropdown/components/StyledDropdownMenuSeparator';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import { MOBILE_VIEWPORT } from '@/ui/theme/constants/theme';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
|
||||
import { assertNotNull } from '~/utils/assert';
|
||||
|
||||
import { ViewsDropdownId } from '../constants/ViewsDropdownId';
|
||||
import { ViewBarContext } from '../contexts/ViewBarContext';
|
||||
import { useRemoveView } from '../hooks/useRemoveView';
|
||||
|
||||
import { ViewBarDropdownButton } from './ViewBarDropdownButton';
|
||||
|
||||
const StyledBoldDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)`
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
`;
|
||||
|
||||
const StyledDropdownLabelAdornments = styled.span`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.grayScale.gray35};
|
||||
display: inline-flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledViewIcon = styled(IconList)`
|
||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledViewName = styled.span`
|
||||
display: inline-block;
|
||||
max-width: 130px;
|
||||
@media (max-width: 375px) {
|
||||
max-width: 90px;
|
||||
}
|
||||
@media (min-width: 376px) and (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
max-width: 110px;
|
||||
}
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export type ViewsDropdownButtonProps = {
|
||||
hotkeyScope: HotkeyScope;
|
||||
onViewEditModeChange?: () => void;
|
||||
};
|
||||
|
||||
export const ViewsDropdownButton = ({
|
||||
hotkeyScope,
|
||||
onViewEditModeChange,
|
||||
}: ViewsDropdownButtonProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { defaultViewName, onViewSelect, ViewBarRecoilScopeContext } =
|
||||
useContext(ViewBarContext);
|
||||
|
||||
const recoilScopeId = useRecoilScopeId(ViewBarRecoilScopeContext);
|
||||
|
||||
const currentView = useRecoilScopedValue(
|
||||
currentViewScopedSelector,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [views] = useRecoilScopedState(
|
||||
viewsScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const entityCount = useRecoilValue(
|
||||
entityCountInCurrentViewState as RecoilValueReadOnly<number>,
|
||||
);
|
||||
|
||||
const { isDropdownOpen, closeDropdown } = useDropdown({
|
||||
dropdownScopeId: ViewsDropdownId,
|
||||
});
|
||||
|
||||
const setViewEditMode = useSetRecoilState(viewEditModeState);
|
||||
|
||||
const handleViewSelect = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async (viewId: string) => {
|
||||
await onViewSelect?.(viewId);
|
||||
|
||||
const savedFilters = await snapshot.getPromise(
|
||||
savedFiltersFamilyState(viewId),
|
||||
);
|
||||
const savedSorts = await snapshot.getPromise(
|
||||
savedSortsFamilyState(viewId),
|
||||
);
|
||||
|
||||
set(filtersScopedState(recoilScopeId), savedFilters);
|
||||
set(sortsScopedState(recoilScopeId), savedSorts);
|
||||
set(currentViewIdScopedState(recoilScopeId), viewId);
|
||||
closeDropdown();
|
||||
},
|
||||
[onViewSelect, recoilScopeId, closeDropdown],
|
||||
);
|
||||
|
||||
const handleAddViewButtonClick = () => {
|
||||
setViewEditMode({ mode: 'create', viewId: undefined });
|
||||
onViewEditModeChange?.();
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const handleEditViewButtonClick = (
|
||||
event: MouseEvent<HTMLButtonElement>,
|
||||
viewId: string,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
setViewEditMode({ mode: 'edit', viewId });
|
||||
onViewEditModeChange?.();
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
const { removeView } = useRemoveView();
|
||||
|
||||
const handleDeleteViewButtonClick = async (
|
||||
event: MouseEvent<HTMLButtonElement>,
|
||||
viewId: string,
|
||||
) => {
|
||||
event.stopPropagation();
|
||||
|
||||
await removeView(viewId);
|
||||
closeDropdown();
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewBarDropdownButton
|
||||
dropdownId={ViewsDropdownId}
|
||||
dropdownHotkeyScope={hotkeyScope}
|
||||
buttonComponent={
|
||||
<StyledDropdownButtonContainer isUnfolded={isDropdownOpen}>
|
||||
<StyledViewIcon size={theme.icon.size.md} />
|
||||
<StyledViewName>
|
||||
{currentView?.name || defaultViewName}
|
||||
</StyledViewName>
|
||||
<StyledDropdownLabelAdornments>
|
||||
· {entityCount} <IconChevronDown size={theme.icon.size.sm} />
|
||||
</StyledDropdownLabelAdornments>
|
||||
</StyledDropdownButtonContainer>
|
||||
}
|
||||
dropdownComponents={
|
||||
<StyledDropdownMenu width={200}>
|
||||
<DropdownMenuItemsContainer>
|
||||
{views.map((view) => (
|
||||
<MenuItem
|
||||
key={view.id}
|
||||
iconButtons={[
|
||||
{
|
||||
Icon: IconPencil,
|
||||
onClick: (event: MouseEvent<HTMLButtonElement>) =>
|
||||
handleEditViewButtonClick(event, view.id),
|
||||
},
|
||||
views.length > 1
|
||||
? {
|
||||
Icon: IconTrash,
|
||||
onClick: (event: MouseEvent<HTMLButtonElement>) =>
|
||||
handleDeleteViewButtonClick(event, view.id),
|
||||
}
|
||||
: null,
|
||||
].filter(assertNotNull)}
|
||||
onClick={() => handleViewSelect(view.id)}
|
||||
LeftIcon={IconList}
|
||||
text={view.name}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
<StyledDropdownMenuSeparator />
|
||||
<StyledBoldDropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleAddViewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Add view"
|
||||
/>
|
||||
</StyledBoldDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export const FilterDropdownId = 'filter';
|
||||
@ -0,0 +1 @@
|
||||
export const SortDropdownId = 'sort-dropdown';
|
||||
@ -0,0 +1 @@
|
||||
export const ViewsDropdownId = 'views';
|
||||
@ -0,0 +1,20 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { RecoilScopeContext } from '@/types/RecoilScopeContext';
|
||||
|
||||
import { View } from '../types/View';
|
||||
|
||||
export const ViewBarContext = createContext<{
|
||||
canPersistViewFields?: boolean;
|
||||
defaultViewName?: string;
|
||||
onCurrentViewSubmit?: () => void | Promise<void>;
|
||||
onViewBarReset?: () => void;
|
||||
onViewCreate?: (view: View) => void | Promise<void>;
|
||||
onViewEdit?: (view: View) => void | Promise<void>;
|
||||
onViewRemove?: (viewId: string) => void | Promise<void>;
|
||||
onViewSelect?: (viewId: string) => void | Promise<void>;
|
||||
onImport?: () => void | Promise<void>;
|
||||
ViewBarRecoilScopeContext: RecoilScopeContext;
|
||||
}>({
|
||||
ViewBarRecoilScopeContext: createContext<string | null>(null),
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { filtersScopedState } from '../states/filtersScopedState';
|
||||
|
||||
import { useViewBarContext } from './useViewBarContext';
|
||||
|
||||
export const useFilterCurrentlyEdited = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [filters] = useRecoilScopedState(
|
||||
filtersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return filters.find(
|
||||
(filter) => filter.key === filterDefinitionUsedInDropdown?.key,
|
||||
);
|
||||
}, [filterDefinitionUsedInDropdown, filters]);
|
||||
};
|
||||
25
front/src/modules/ui/data/view-bar/hooks/useRemoveFilter.ts
Normal file
25
front/src/modules/ui/data/view-bar/hooks/useRemoveFilter.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { ViewBarContext } from '../contexts/ViewBarContext';
|
||||
import { filtersScopedState } from '../states/filtersScopedState';
|
||||
|
||||
export const useRemoveFilter = () => {
|
||||
const { ViewBarRecoilScopeContext } = useContext(ViewBarContext);
|
||||
|
||||
const [, setFilters] = useRecoilScopedState(
|
||||
filtersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const removeFilter = (filterKey: string) => {
|
||||
setFilters((filters) => {
|
||||
return filters.filter((filter) => {
|
||||
return filter.key !== filterKey;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return removeFilter;
|
||||
};
|
||||
35
front/src/modules/ui/data/view-bar/hooks/useRemoveView.ts
Normal file
35
front/src/modules/ui/data/view-bar/hooks/useRemoveView.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
|
||||
|
||||
import { ViewBarContext } from '../contexts/ViewBarContext';
|
||||
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
|
||||
import { viewsScopedState } from '../states/viewsScopedState';
|
||||
|
||||
export const useRemoveView = () => {
|
||||
const { onViewRemove, ViewBarRecoilScopeContext } =
|
||||
useContext(ViewBarContext);
|
||||
|
||||
const recoilScopeId = useRecoilScopeId(ViewBarRecoilScopeContext);
|
||||
|
||||
const removeView = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async (viewId: string) => {
|
||||
const currentViewId = await snapshot.getPromise(
|
||||
currentViewIdScopedState(recoilScopeId),
|
||||
);
|
||||
|
||||
if (currentViewId === viewId)
|
||||
set(currentViewIdScopedState(recoilScopeId), undefined);
|
||||
|
||||
set(viewsScopedState(recoilScopeId), (previousViews) =>
|
||||
previousViews.filter((view) => view.id !== viewId),
|
||||
);
|
||||
await onViewRemove?.(viewId);
|
||||
},
|
||||
[onViewRemove, recoilScopeId],
|
||||
);
|
||||
|
||||
return { removeView };
|
||||
};
|
||||
35
front/src/modules/ui/data/view-bar/hooks/useUpsertFilter.ts
Normal file
35
front/src/modules/ui/data/view-bar/hooks/useUpsertFilter.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { filtersScopedState } from '../states/filtersScopedState';
|
||||
import { Filter } from '../types/Filter';
|
||||
|
||||
import { useViewBarContext } from './useViewBarContext';
|
||||
|
||||
export const useUpsertFilter = () => {
|
||||
const { ViewBarRecoilScopeContext } = useViewBarContext();
|
||||
|
||||
const [, setFilters] = useRecoilScopedState(
|
||||
filtersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
|
||||
const upsertFilter = (filterToUpsert: Filter) => {
|
||||
setFilters((filters) => {
|
||||
return produce(filters, (filtersDraft) => {
|
||||
const index = filtersDraft.findIndex(
|
||||
(filter) => filter.key === filterToUpsert.key,
|
||||
);
|
||||
|
||||
if (index === -1) {
|
||||
filtersDraft.push(filterToUpsert);
|
||||
} else {
|
||||
filtersDraft[index] = filterToUpsert;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return upsertFilter;
|
||||
};
|
||||
109
front/src/modules/ui/data/view-bar/hooks/useUpsertView.ts
Normal file
109
front/src/modules/ui/data/view-bar/hooks/useUpsertView.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilCallback, useRecoilValue, useResetRecoilState } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId';
|
||||
|
||||
import { ViewBarContext } from '../contexts/ViewBarContext';
|
||||
import { currentViewIdScopedState } from '../states/currentViewIdScopedState';
|
||||
import { filtersScopedState } from '../states/filtersScopedState';
|
||||
import { savedFiltersFamilyState } from '../states/savedFiltersFamilyState';
|
||||
import { savedSortsFamilyState } from '../states/savedSortsFamilyState';
|
||||
import { currentViewScopedSelector } from '../states/selectors/currentViewScopedSelector';
|
||||
import { viewsByIdScopedSelector } from '../states/selectors/viewsByIdScopedSelector';
|
||||
import { sortsScopedState } from '../states/sortsScopedState';
|
||||
import { viewEditModeState } from '../states/viewEditModeState';
|
||||
import { viewsScopedState } from '../states/viewsScopedState';
|
||||
|
||||
export const useUpsertView = () => {
|
||||
const { onViewCreate, onViewEdit, ViewBarRecoilScopeContext } =
|
||||
useContext(ViewBarContext);
|
||||
const recoilScopeId = useRecoilScopeId(ViewBarRecoilScopeContext);
|
||||
|
||||
const filters = useRecoilScopedValue(
|
||||
filtersScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
const sorts = useRecoilScopedValue(
|
||||
sortsScopedState,
|
||||
ViewBarRecoilScopeContext,
|
||||
);
|
||||
const viewEditMode = useRecoilValue(viewEditModeState);
|
||||
const resetViewEditMode = useResetRecoilState(viewEditModeState);
|
||||
|
||||
const upsertView = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
async (name?: string) => {
|
||||
if (!viewEditMode.mode || !name) {
|
||||
resetViewEditMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewEditMode.mode === 'create') {
|
||||
const createdView = { id: v4(), name };
|
||||
|
||||
set(savedFiltersFamilyState(createdView.id), filters);
|
||||
set(savedSortsFamilyState(createdView.id), sorts);
|
||||
|
||||
set(viewsScopedState(recoilScopeId), (previousViews) => [
|
||||
...previousViews,
|
||||
createdView,
|
||||
]);
|
||||
|
||||
await onViewCreate?.(createdView);
|
||||
|
||||
resetViewEditMode();
|
||||
|
||||
set(currentViewIdScopedState(recoilScopeId), createdView.id);
|
||||
|
||||
return createdView;
|
||||
}
|
||||
|
||||
const viewsById = await snapshot.getPromise(
|
||||
viewsByIdScopedSelector(recoilScopeId),
|
||||
);
|
||||
const currentView = await snapshot.getPromise(
|
||||
currentViewScopedSelector(recoilScopeId),
|
||||
);
|
||||
|
||||
const viewToEdit = viewEditMode.viewId
|
||||
? viewsById[viewEditMode.viewId]
|
||||
: currentView;
|
||||
|
||||
if (!viewToEdit) {
|
||||
resetViewEditMode();
|
||||
return;
|
||||
}
|
||||
|
||||
const editedView = {
|
||||
...viewToEdit,
|
||||
name,
|
||||
};
|
||||
|
||||
set(viewsScopedState(recoilScopeId), (previousViews) =>
|
||||
previousViews.map((previousView) =>
|
||||
previousView.id === editedView.id ? editedView : previousView,
|
||||
),
|
||||
);
|
||||
|
||||
await onViewEdit?.(editedView);
|
||||
|
||||
resetViewEditMode();
|
||||
|
||||
return editedView;
|
||||
},
|
||||
[
|
||||
filters,
|
||||
onViewCreate,
|
||||
onViewEdit,
|
||||
recoilScopeId,
|
||||
resetViewEditMode,
|
||||
sorts,
|
||||
viewEditMode.mode,
|
||||
viewEditMode.viewId,
|
||||
],
|
||||
);
|
||||
|
||||
return { upsertView };
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { ViewBarContext } from '../contexts/ViewBarContext';
|
||||
|
||||
export const useViewBarContext = () => {
|
||||
return useContext(ViewBarContext);
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { FilterDefinition } from '../types/FilterDefinition';
|
||||
|
||||
export const availableFiltersScopedState = atomFamily<
|
||||
FilterDefinition[],
|
||||
string
|
||||
>({
|
||||
key: 'availableFiltersScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { SortDefinition } from '../types/SortDefinition';
|
||||
|
||||
export const availableSortsScopedState = atomFamily<SortDefinition[], string>({
|
||||
key: 'availableSortsScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const currentViewIdScopedState = atomFamily<string | undefined, string>({
|
||||
key: 'currentViewIdScopedState',
|
||||
default: undefined,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const entityCountInCurrentViewState = atom<number>({
|
||||
key: 'entityCountInCurrentViewState',
|
||||
default: 0,
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { FilterDefinition } from '../types/FilterDefinition';
|
||||
|
||||
export const filterDefinitionUsedInDropdownScopedState = atomFamily<
|
||||
FilterDefinition | null,
|
||||
string
|
||||
>({
|
||||
key: 'filterDefinitionUsedInDropdownScopedState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const filterDropdownSearchInputScopedState = atomFamily<string, string>({
|
||||
key: 'filterDropdownSearchInputScopedState',
|
||||
default: '',
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const filterDropdownSelectedEntityIdScopedState = atomFamily<
|
||||
string | null,
|
||||
string
|
||||
>({
|
||||
key: 'filterDropdownSelectedEntityIdScopedState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { Filter } from '../types/Filter';
|
||||
|
||||
export const filtersScopedState = atomFamily<Filter[], string>({
|
||||
key: 'filtersScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isFilterDropdownOperandSelectUnfoldedScopedState = atomFamily<
|
||||
boolean,
|
||||
string
|
||||
>({
|
||||
key: 'isFilterDropdownOperandSelectUnfoldedScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isFilterDropdownUnfoldedScopedState = atomFamily<boolean, string>({
|
||||
key: 'isFilterDropdownUnfoldedScopedState',
|
||||
default: false,
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isViewBarExpandedScopedState = atomFamily<boolean, string>({
|
||||
key: 'isViewBarExpandedScopedState',
|
||||
default: true,
|
||||
});
|
||||
@ -0,0 +1,10 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { Filter } from '../types/Filter';
|
||||
|
||||
export const savedFiltersFamilyState = atomFamily<Filter[], string | undefined>(
|
||||
{
|
||||
key: 'savedFiltersFamilyState',
|
||||
default: [],
|
||||
},
|
||||
);
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { Sort } from '../types/Sort';
|
||||
|
||||
export const savedSortsFamilyState = atomFamily<Sort[], string | undefined>({
|
||||
key: 'savedSortsFamilyState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { ViewFilterOperand } from '~/generated/graphql';
|
||||
|
||||
export const selectedOperandInDropdownScopedState = atomFamily<
|
||||
ViewFilterOperand | null,
|
||||
string
|
||||
>({
|
||||
key: 'selectedOperandInDropdownScopedState',
|
||||
default: null,
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { filtersScopedState } from '../filtersScopedState';
|
||||
import { savedFiltersFamilyState } from '../savedFiltersFamilyState';
|
||||
|
||||
export const canPersistFiltersScopedFamilySelector = selectorFamily({
|
||||
key: 'canPersistFiltersScopedFamilySelector',
|
||||
get:
|
||||
({
|
||||
recoilScopeId,
|
||||
viewId,
|
||||
}: {
|
||||
recoilScopeId: string;
|
||||
viewId: string | undefined;
|
||||
}) =>
|
||||
({ get }) =>
|
||||
!isDeeplyEqual(
|
||||
get(savedFiltersFamilyState(viewId)),
|
||||
get(filtersScopedState(recoilScopeId)),
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { savedSortsFamilyState } from '../savedSortsFamilyState';
|
||||
import { sortsScopedState } from '../sortsScopedState';
|
||||
|
||||
export const canPersistSortsScopedFamilySelector = selectorFamily({
|
||||
key: 'canPersistSortsScopedFamilySelector',
|
||||
get:
|
||||
({
|
||||
recoilScopeId,
|
||||
viewId,
|
||||
}: {
|
||||
recoilScopeId: string;
|
||||
viewId: string | undefined;
|
||||
}) =>
|
||||
({ get }) =>
|
||||
!isDeeplyEqual(
|
||||
get(savedSortsFamilyState(viewId)),
|
||||
get(sortsScopedState(recoilScopeId)),
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { View } from '../../types/View';
|
||||
import { currentViewIdScopedState } from '../currentViewIdScopedState';
|
||||
|
||||
import { viewsByIdScopedSelector } from './viewsByIdScopedSelector';
|
||||
|
||||
export const currentViewScopedSelector = selectorFamily<
|
||||
View | undefined,
|
||||
string
|
||||
>({
|
||||
key: 'currentViewScopedSelector',
|
||||
get:
|
||||
(scopeId) =>
|
||||
({ get }) => {
|
||||
const currentViewId = get(currentViewIdScopedState(scopeId));
|
||||
return currentViewId
|
||||
? get(viewsByIdScopedSelector(scopeId))[currentViewId]
|
||||
: undefined;
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,13 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { turnFilterIntoWhereClause } from '../../utils/turnFilterIntoWhereClause';
|
||||
import { filtersScopedState } from '../filtersScopedState';
|
||||
|
||||
export const filtersWhereScopedSelector = selectorFamily({
|
||||
key: 'filtersWhereScopedSelector',
|
||||
get:
|
||||
(param: string) =>
|
||||
({ get }) => ({
|
||||
AND: get(filtersScopedState(param)).map(turnFilterIntoWhereClause),
|
||||
}),
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { Filter } from '../../types/Filter';
|
||||
import { savedFiltersFamilyState } from '../savedFiltersFamilyState';
|
||||
|
||||
export const savedFiltersByKeyFamilySelector = selectorFamily({
|
||||
key: 'savedFiltersByKeyFamilySelector',
|
||||
get:
|
||||
(viewId: string | undefined) =>
|
||||
({ get }) =>
|
||||
get(savedFiltersFamilyState(viewId)).reduce<Record<string, Filter>>(
|
||||
(result, filter) => ({ ...result, [filter.key]: filter }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { savedFiltersFamilyState } from '../savedFiltersFamilyState';
|
||||
|
||||
export const savedFiltersFamilySelector = selectorFamily({
|
||||
key: 'savedFiltersFamilySelector',
|
||||
get:
|
||||
(viewId: string | undefined) =>
|
||||
({ get }) =>
|
||||
get(savedFiltersFamilyState(viewId)),
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { Sort } from '../../types/Sort';
|
||||
import { savedSortsFamilyState } from '../savedSortsFamilyState';
|
||||
|
||||
export const savedSortsByKeyFamilySelector = selectorFamily({
|
||||
key: 'savedSortsByKeyFamilySelector',
|
||||
get:
|
||||
(viewId: string | undefined) =>
|
||||
({ get }) =>
|
||||
get(savedSortsFamilyState(viewId)).reduce<Record<string, Sort>>(
|
||||
(result, sort) => ({ ...result, [sort.key]: sort }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { savedSortsFamilyState } from '../savedSortsFamilyState';
|
||||
|
||||
export const savedSortsFamilySelector = selectorFamily({
|
||||
key: 'savedSortsFamilySelector',
|
||||
get:
|
||||
(viewId: string | undefined) =>
|
||||
({ get }) =>
|
||||
get(savedSortsFamilyState(viewId)),
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { SortOrder } from '~/generated/graphql';
|
||||
|
||||
import { reduceSortsToOrderBy } from '../../utils/helpers';
|
||||
import { sortsScopedState } from '../sortsScopedState';
|
||||
|
||||
export const sortsOrderByScopedSelector = selectorFamily({
|
||||
key: 'sortsOrderByScopedSelector',
|
||||
get:
|
||||
(scopeId: string) =>
|
||||
({ get }) => {
|
||||
const orderBy = reduceSortsToOrderBy(get(sortsScopedState(scopeId)));
|
||||
return orderBy.length ? orderBy : [{ createdAt: SortOrder.Desc }];
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,18 @@
|
||||
import { selectorFamily } from 'recoil';
|
||||
|
||||
import { View } from '../../types/View';
|
||||
import { viewsScopedState } from '../viewsScopedState';
|
||||
|
||||
export const viewsByIdScopedSelector = selectorFamily<
|
||||
Record<string, View>,
|
||||
string
|
||||
>({
|
||||
key: 'viewsByIdScopedSelector',
|
||||
get:
|
||||
(scopeId) =>
|
||||
({ get }) =>
|
||||
get(viewsScopedState(scopeId)).reduce<Record<string, View>>(
|
||||
(result, view) => ({ ...result, [view.id]: view }),
|
||||
{},
|
||||
),
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { Sort } from '../types/Sort';
|
||||
|
||||
export const sortsScopedState = atomFamily<Sort[], string>({
|
||||
key: 'sortsScopedState',
|
||||
default: [],
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
import { atom } from 'recoil';
|
||||
|
||||
export const viewEditModeState = atom<{
|
||||
mode: 'create' | 'edit';
|
||||
viewId: string | undefined;
|
||||
}>({
|
||||
key: 'viewEditModeState',
|
||||
default: { mode: 'edit', viewId: undefined },
|
||||
});
|
||||
@ -0,0 +1,8 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
import { View } from '../types/View';
|
||||
|
||||
export const viewsScopedState = atomFamily<View[], string>({
|
||||
key: 'viewsScopedState',
|
||||
default: [],
|
||||
});
|
||||
12
front/src/modules/ui/data/view-bar/types/Filter.ts
Normal file
12
front/src/modules/ui/data/view-bar/types/Filter.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ViewFilterOperand } from '~/generated/graphql';
|
||||
|
||||
import { FilterType } from './FilterType';
|
||||
|
||||
export type Filter = {
|
||||
key: string;
|
||||
type: FilterType;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
displayAvatarUrl?: string;
|
||||
operand: ViewFilterOperand;
|
||||
};
|
||||
13
front/src/modules/ui/data/view-bar/types/FilterDefinition.ts
Normal file
13
front/src/modules/ui/data/view-bar/types/FilterDefinition.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
|
||||
import { FilterType } from './FilterType';
|
||||
|
||||
export type FilterDefinition = {
|
||||
key: string;
|
||||
label: string;
|
||||
Icon: IconComponent;
|
||||
type: FilterType;
|
||||
entitySelectComponent?: JSX.Element;
|
||||
selectAllLabel?: string;
|
||||
SelectAllIcon?: IconComponent;
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
import { FilterDefinition } from './FilterDefinition';
|
||||
|
||||
export type FilterDefinitionByEntity<T> = FilterDefinition & {
|
||||
key: keyof T;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export { ViewFilterOperand as FilterOperand } from '~/generated/graphql';
|
||||
1
front/src/modules/ui/data/view-bar/types/FilterType.ts
Normal file
1
front/src/modules/ui/data/view-bar/types/FilterType.ts
Normal file
@ -0,0 +1 @@
|
||||
export type FilterType = 'text' | 'date' | 'entity' | 'number';
|
||||
@ -0,0 +1,4 @@
|
||||
export enum FiltersHotkeyScope {
|
||||
FilterDropdownButton = 'filter-dropdown-button',
|
||||
SortDropdownButton = 'sort-dropdown-button',
|
||||
}
|
||||
8
front/src/modules/ui/data/view-bar/types/Sort.ts
Normal file
8
front/src/modules/ui/data/view-bar/types/Sort.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { SortDefinition } from './SortDefinition';
|
||||
import { SortDirection } from './SortDirection';
|
||||
|
||||
export type Sort = {
|
||||
key: string;
|
||||
direction: SortDirection;
|
||||
definition: SortDefinition;
|
||||
};
|
||||
10
front/src/modules/ui/data/view-bar/types/SortDefinition.ts
Normal file
10
front/src/modules/ui/data/view-bar/types/SortDefinition.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
|
||||
import { SortDirection } from './SortDirection';
|
||||
|
||||
export type SortDefinition = {
|
||||
key: string;
|
||||
label: string;
|
||||
Icon?: IconComponent;
|
||||
getOrderByTemplate?: (direction: SortDirection) => any[];
|
||||
};
|
||||
@ -0,0 +1,3 @@
|
||||
export const SORT_DIRECTIONS = ['asc', 'desc'] as const;
|
||||
|
||||
export type SortDirection = (typeof SORT_DIRECTIONS)[number];
|
||||
1
front/src/modules/ui/data/view-bar/types/View.ts
Normal file
1
front/src/modules/ui/data/view-bar/types/View.ts
Normal file
@ -0,0 +1 @@
|
||||
export type View = { id: string; name: string };
|
||||
@ -0,0 +1,11 @@
|
||||
import { FieldDefinition } from '@/ui/data/field/types/FieldDefinition';
|
||||
import { FieldMetadata } from '@/ui/data/field/types/FieldMetadata';
|
||||
|
||||
export type ViewFieldForVisibility = Pick<
|
||||
FieldDefinition<FieldMetadata>,
|
||||
'key' | 'name' | 'Icon' | 'infoTooltipContent'
|
||||
> & {
|
||||
isVisible?: boolean;
|
||||
index: number;
|
||||
size?: number | undefined;
|
||||
};
|
||||
@ -0,0 +1,4 @@
|
||||
export enum ViewsHotkeyScope {
|
||||
ListDropdown = 'views-list-dropdown',
|
||||
CreateDropdown = 'views-create-dropdown',
|
||||
}
|
||||
13
front/src/modules/ui/data/view-bar/types/interface.ts
Normal file
13
front/src/modules/ui/data/view-bar/types/interface.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
|
||||
import { SortOrder as Order_By } from '~/generated/graphql';
|
||||
|
||||
export type SortType<OrderByTemplate> = {
|
||||
label: string;
|
||||
key: string;
|
||||
Icon?: IconComponent;
|
||||
orderByTemplate?: (order: Order_By) => OrderByTemplate[];
|
||||
};
|
||||
|
||||
export type SelectedSortType<OrderByTemplate> = SortType<OrderByTemplate> & {
|
||||
order: 'asc' | 'desc';
|
||||
};
|
||||
45
front/src/modules/ui/data/view-bar/utils/getOperandLabel.ts
Normal file
45
front/src/modules/ui/data/view-bar/utils/getOperandLabel.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { ViewFilterOperand } from '~/generated/graphql';
|
||||
|
||||
export const getOperandLabel = (
|
||||
operand: ViewFilterOperand | null | undefined,
|
||||
) => {
|
||||
switch (operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
return 'Contains';
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return "Doesn't contain";
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
return 'Greater than';
|
||||
case ViewFilterOperand.LessThan:
|
||||
return 'Less than';
|
||||
case ViewFilterOperand.Is:
|
||||
return 'Is';
|
||||
case ViewFilterOperand.IsNot:
|
||||
return 'Is not';
|
||||
case ViewFilterOperand.IsNotNull:
|
||||
return 'Is not null';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const getOperandLabelShort = (
|
||||
operand: ViewFilterOperand | null | undefined,
|
||||
) => {
|
||||
switch (operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
case ViewFilterOperand.Contains:
|
||||
return ': ';
|
||||
case ViewFilterOperand.IsNot:
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return ': Not';
|
||||
case ViewFilterOperand.IsNotNull:
|
||||
return ': NotNull';
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
return '\u00A0> ';
|
||||
case ViewFilterOperand.LessThan:
|
||||
return '\u00A0< ';
|
||||
default:
|
||||
return ': ';
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,19 @@
|
||||
import { ViewFilterOperand } from '~/generated/graphql';
|
||||
|
||||
import { FilterType } from '../types/FilterType';
|
||||
|
||||
export const getOperandsForFilterType = (
|
||||
filterType: FilterType | null | undefined,
|
||||
): ViewFilterOperand[] => {
|
||||
switch (filterType) {
|
||||
case 'text':
|
||||
return [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain];
|
||||
case 'number':
|
||||
case 'date':
|
||||
return [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan];
|
||||
case 'entity':
|
||||
return [ViewFilterOperand.Is, ViewFilterOperand.IsNot];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
16
front/src/modules/ui/data/view-bar/utils/helpers.ts
Normal file
16
front/src/modules/ui/data/view-bar/utils/helpers.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { SortOrder as Order_By } from '~/generated/graphql';
|
||||
|
||||
import { Sort } from '../types/Sort';
|
||||
|
||||
export const reduceSortsToOrderBy = (sorts: Sort[]): any[] =>
|
||||
sorts
|
||||
.map((sort) => {
|
||||
const direction = sort.direction === 'asc' ? Order_By.Asc : Order_By.Desc;
|
||||
|
||||
if (sort.definition.getOrderByTemplate) {
|
||||
return sort.definition.getOrderByTemplate(direction);
|
||||
} else {
|
||||
return [{ [sort.definition.key]: direction }];
|
||||
}
|
||||
})
|
||||
.flat();
|
||||
@ -0,0 +1,99 @@
|
||||
import { QueryMode, ViewFilterOperand } from '~/generated/graphql';
|
||||
|
||||
import { Filter } from '../types/Filter';
|
||||
|
||||
export const turnFilterIntoWhereClause = (filter: Filter) => {
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.IsNotNull:
|
||||
return {
|
||||
[filter.key]: {
|
||||
not: null,
|
||||
},
|
||||
};
|
||||
default:
|
||||
switch (filter.type) {
|
||||
case 'text':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Contains:
|
||||
return {
|
||||
[filter.key]: {
|
||||
contains: filter.value,
|
||||
mode: QueryMode.Insensitive,
|
||||
},
|
||||
};
|
||||
case ViewFilterOperand.DoesNotContain:
|
||||
return {
|
||||
[filter.key]: {
|
||||
not: {
|
||||
contains: filter.value,
|
||||
mode: QueryMode.Insensitive,
|
||||
},
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'number':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
return {
|
||||
[filter.key]: {
|
||||
gte: parseFloat(filter.value),
|
||||
},
|
||||
};
|
||||
case ViewFilterOperand.LessThan:
|
||||
return {
|
||||
[filter.key]: {
|
||||
lte: parseFloat(filter.value),
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'date':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.GreaterThan:
|
||||
return {
|
||||
[filter.key]: {
|
||||
gte: filter.value,
|
||||
},
|
||||
};
|
||||
case ViewFilterOperand.LessThan:
|
||||
return {
|
||||
[filter.key]: {
|
||||
lte: filter.value,
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.type} filter`,
|
||||
);
|
||||
}
|
||||
case 'entity':
|
||||
switch (filter.operand) {
|
||||
case ViewFilterOperand.Is:
|
||||
return {
|
||||
[filter.key]: {
|
||||
equals: filter.value,
|
||||
},
|
||||
};
|
||||
case ViewFilterOperand.IsNot:
|
||||
return {
|
||||
[filter.key]: {
|
||||
not: { equals: filter.value },
|
||||
},
|
||||
};
|
||||
default:
|
||||
throw new Error(
|
||||
`Unknown operand ${filter.operand} for ${filter.type} filter`,
|
||||
);
|
||||
}
|
||||
default:
|
||||
throw new Error('Unknown filter type');
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user