Uniformize folder structure (#693)

* Uniformize folder structure

* Fix icons

* Fix icons

* Fix tests

* Fix tests
This commit is contained in:
Charles Bochet
2023-07-16 14:29:28 -07:00
committed by GitHub
parent 900ec5572f
commit 6ced8434bd
462 changed files with 931 additions and 960 deletions

View File

@ -2,8 +2,9 @@ import React, { useEffect } from 'react';
import styled from '@emotion/styled';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { contextMenuPositionState } from '@/ui/tables/states/contextMenuPositionState';
import { PositionType } from '@/ui/types/PositionType';
import { contextMenuPositionState } from '@/ui/table/states/contextMenuPositionState';
import { PositionType } from '../types/PositionType';
type OwnProps = {
children: React.ReactNode | React.ReactNode[];

View File

@ -1,7 +1,7 @@
import { ReactElement } from 'react';
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { BoardCardFieldContext } from '../states/BoardCardFieldContext';

View File

@ -1,9 +1,9 @@
import { useMemo, useState } from 'react';
import { BoardCardEditableField } from '@/ui/board-card-field/components/BoardCardEditableField';
import { InplaceInputDateDisplayMode } from '@/ui/inplace-inputs/components/InplaceInputDateDisplayMode';
import { debounce } from '@/utils/debounce';
import { InplaceInputDateDisplayMode } from '@/ui/display/component/InplaceInputDateDisplayMode';
import { debounce } from '~/utils/debounce';
import { BoardCardEditableField } from './BoardCardEditableField';
import { BoardCardEditableFieldDateEditMode } from './BoardCardEditableFieldDateEditMode';
type OwnProps = {

View File

@ -1,4 +1,4 @@
import { InplaceInputDate } from '@/ui/inplace-inputs/components/InplaceInputDate';
import { InplaceInputDate } from '@/ui/inplace-input/components/InplaceInputDate';
type OwnProps = {
value: Date;

View File

@ -1,8 +1,8 @@
import { ReactElement, useRef } from 'react';
import styled from '@emotion/styled';
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { overlayBackground } from '@/ui/themes/effects';
import { BoardCardFieldHotkeyScope } from '../types/BoardCardFieldHotkeyScope';

View File

@ -1,8 +1,8 @@
import { ReactElement } from 'react';
import styled from '@emotion/styled';
import { usePreviousHotkeyScope } from '@/lib/hotkeys/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { useBoardCardField } from '../hooks/useBoardCardField';
import { BoardCardFieldHotkeyScope } from '../types/BoardCardFieldHotkeyScope';

View File

@ -1,9 +1,10 @@
import { ChangeEvent, useMemo, useState } from 'react';
import { BoardCardEditableField } from '@/ui/board-card-field/components/BoardCardEditableField';
import { InplaceInputTextDisplayMode } from '@/ui/inplace-inputs/components/InplaceInputTextDisplayMode';
import { InplaceInputTextEditMode } from '@/ui/inplace-inputs/components/InplaceInputTextEditMode';
import { debounce } from '@/utils/debounce';
import { InplaceInputTextDisplayMode } from '@/ui/display/component/InplaceInputTextDisplayMode';
import { InplaceInputTextEditMode } from '@/ui/inplace-input/components/InplaceInputTextEditMode';
import { debounce } from '~/utils/debounce';
import { BoardCardEditableField } from './BoardCardEditableField';
type OwnProps = {
placeholder?: string;

View File

@ -1,4 +1,4 @@
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { BoardCardFieldContext } from '../states/BoardCardFieldContext';
import { isBoardCardFieldInEditModeScopedState } from '../states/isBoardCardFieldInEditModeScopedState';

View File

@ -1,7 +1,7 @@
import React, { ChangeEvent } from 'react';
import styled from '@emotion/styled';
import { debounce } from '@/utils/debounce';
import { debounce } from '~/utils/debounce';
import { EditColumnTitleInput } from './EditColumnTitleInput';

View File

@ -1,9 +1,9 @@
import React from 'react';
import styled from '@emotion/styled';
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { ColumnHotkeyScope } from './ColumnHotkeyScope';

View File

@ -1,7 +1,7 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconPlus } from '@/ui/icons/index';
import { IconPlus } from '@/ui/icon/index';
const StyledButton = styled.button`
align-items: center;

View File

@ -1,10 +1,9 @@
import React from 'react';
import styled from '@emotion/styled';
import { SoonPill } from '@/ui/pill/components/SoonPill';
import { rgba } from '@/ui/themes/colors';
import { SoonPill } from '../accessories/SoonPill';
export type ButtonVariant =
| 'primary'
| 'secondary'

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { IconChevronDown } from '@/ui/icons/index';
import { IconChevronDown } from '@/ui/icon/index';
type ButtonProps = React.ComponentProps<'button'>;

View File

@ -5,7 +5,7 @@ import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconSearch } from '@/ui/icons';
import { IconSearch } from '@/ui/icon';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { Button } from '../Button';

View File

@ -5,7 +5,7 @@ import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconUser } from '@/ui/icons';
import { IconUser } from '@/ui/icon';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { IconButton } from '../IconButton';

View File

@ -2,7 +2,7 @@ import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconBrandGoogle } from '@/ui/icons';
import { IconBrandGoogle } from '@/ui/icon';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { MainButton } from '../MainButton';

View File

@ -2,7 +2,7 @@ import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconArrowRight } from '@/ui/icons';
import { IconArrowRight } from '@/ui/icon';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { RoundedIconButton } from '../RoundedIconButton';

View File

@ -1,24 +0,0 @@
import { FilterDefinition } from '@/lib/filters-and-sorts/types/FilterDefinition';
import { useInitializeEntityTable } from '@/ui/tables/hooks/useInitializeEntityTable';
import { useInitializeEntityTableFilters } from '@/ui/tables/hooks/useInitializeEntityTableFilters';
import { useMapKeyboardToSoftFocus } from '@/ui/tables/hooks/useMapKeyboardToSoftFocus';
export function HooksEntityTable({
numberOfColumns,
availableFilters,
}: {
numberOfColumns: number;
availableFilters: FilterDefinition[];
}) {
useMapKeyboardToSoftFocus();
useInitializeEntityTable({
numberOfColumns,
});
useInitializeEntityTableFilters({
availableFilters,
});
return <></>;
}

View File

@ -1,4 +1,4 @@
import { formatToHumanReadableDate } from '@/utils/utils';
import { formatToHumanReadableDate } from '~/utils';
type OwnProps = {
value: Date;

View File

@ -2,7 +2,7 @@ import { MouseEvent } from 'react';
import styled from '@emotion/styled';
import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js';
import { RawLink } from '@/ui/components/links/RawLink';
import { RawLink } from '@/ui/link/components/RawLink';
const StyledRawLink = styled(RawLink)`
overflow: hidden;

View File

@ -1,7 +1,7 @@
import React from 'react';
import styled from '@emotion/styled';
import { Checkbox } from '../form/Checkbox';
import { Checkbox } from '@/ui/input/components/Checkbox';
import { DropdownMenuButton } from './DropdownMenuButton';

View File

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCheck } from '@/ui/icons/index';
import { IconCheck } from '@/ui/icon/index';
import { hoverBackground } from '@/ui/themes/effects';
import { DropdownMenuButton } from './DropdownMenuButton';

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import styled from '@emotion/styled';
import type { Meta, StoryObj } from '@storybook/react';
import { IconPlus } from '@/ui/icons/index';
import { IconPlus } from '@/ui/icon/index';
import { Avatar } from '@/users/components/Avatar';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';

View File

@ -2,7 +2,7 @@ import { useState } from 'react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { useEditableField } from '../hooks/useEditableField';

View File

@ -1,8 +1,8 @@
import styled from '@emotion/styled';
import { IconPencil } from '@tabler/icons-react';
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
import { IconButton } from '@/ui/components/buttons/IconButton';
import { IconButton } from '@/ui/button/components/IconButton';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { IconPencil } from '@/ui/icon';
import { overlayBackground } from '@/ui/themes/effects';
import { useEditableField } from '../hooks/useEditableField';

View File

@ -1,10 +1,10 @@
import { useEffect, useState } from 'react';
import { IconMap } from '@tabler/icons-react';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { EditableField } from '@/ui/editable-fields/components/EditableField';
import { FieldContext } from '@/ui/editable-fields/states/FieldContext';
import { InplaceInputText } from '@/ui/inplace-inputs/components/InplaceInputText';
import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldContext } from '@/ui/editable-field/states/FieldContext';
import { IconMap } from '@/ui/icon';
import { InplaceInputText } from '@/ui/inplace-input/components/InplaceInputText';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { Company, useUpdateCompanyMutation } from '~/generated/graphql';
type OwnProps = {

View File

@ -1,4 +1,4 @@
import { RawLink } from '@/ui/components/links/RawLink';
import { RawLink } from '@/ui/link/components/RawLink';
export function FieldDisplayURL({ URL }: { URL: string | undefined }) {
return <RawLink href={URL ? 'https://' + URL : ''}>{URL}</RawLink>;

View File

@ -1,6 +1,6 @@
import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { FieldContext } from '../states/FieldContext';
import { isFieldInEditModeScopedState } from '../states/isFieldInEditModeScopedState';

View File

@ -1,5 +1,5 @@
import { useScopedHotkeys } from '@/lib/hotkeys/hooks/useScopedHotkeys';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';

View File

@ -1,6 +1,6 @@
import { HotkeyScope } from '@/lib/hotkeys/types/HotkeyScope';
import { InplaceInputDate } from '@/ui/inplace-inputs/components/InplaceInputDate';
import { parseDate } from '@/utils/datetime/date-utils';
import { HotkeyScope } from '@/ui/hotkey/types/HotkeyScope';
import { InplaceInputDate } from '@/ui/inplace-input/components/InplaceInputDate';
import { parseDate } from '~/utils/date-utils';
import { useEditableField } from '../../hooks/useEditableField';

View File

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

View File

@ -0,0 +1,132 @@
import { Context, useCallback, useState } from 'react';
import { Key } from 'ts-key-enum';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/filter-n-sort/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/selectedOperandInDropdownScopedState';
import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
import DropdownButton from './DropdownButton';
import { FilterDropdownDateSearchInput } from './FilterDropdownDateSearchInput';
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
import { FilterDropdownFilterSelect } from './FilterDropdownFilterSelect';
import { FilterDropdownNumberSearchInput } from './FilterDropdownNumberSearchInput';
import { FilterDropdownOperandButton } from './FilterDropdownOperandButton';
import { FilterDropdownOperandSelect } from './FilterDropdownOperandSelect';
import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
export function FilterDropdownButton({
context,
HotkeyScope,
}: {
context: Context<string | null>;
HotkeyScope: FiltersHotkeyScope;
}) {
const [isUnfolded, setIsUnfolded] = useState(false);
const [
isFilterDropdownOperandSelectUnfolded,
setIsFilterDropdownOperandSelectUnfolded,
] = useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
context,
);
const [filterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown] =
useRecoilScopedState(filterDefinitionUsedInDropdownScopedState, context);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
context,
);
const [filters] = useRecoilScopedState(filtersScopedState, context);
const [selectedOperandInDropdown, setSelectedOperandInDropdown] =
useRecoilScopedState(selectedOperandInDropdownScopedState, context);
const resetState = useCallback(() => {
setIsFilterDropdownOperandSelectUnfolded(false);
setFilterDefinitionUsedInDropdown(null);
setSelectedOperandInDropdown(null);
setFilterDropdownSearchInput('');
}, [
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setFilterDropdownSearchInput,
setIsFilterDropdownOperandSelectUnfolded,
]);
const isFilterSelected = (filters?.length ?? 0) > 0;
const setHotkeyScope = useSetHotkeyScope();
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) {
setHotkeyScope(HotkeyScope);
setIsUnfolded(true);
} else {
if (filterDefinitionUsedInDropdown?.type === 'entity') {
setHotkeyScope(HotkeyScope);
}
setIsUnfolded(false);
resetState();
}
}
useScopedHotkeys(
[Key.Escape],
() => {
handleIsUnfoldedChange(false);
},
RelationPickerHotkeyScope.RelationPicker,
[handleIsUnfoldedChange],
);
return (
<DropdownButton
label="Filter"
isActive={isFilterSelected}
isUnfolded={isUnfolded}
onIsUnfoldedChange={handleIsUnfoldedChange}
HotkeyScope={HotkeyScope}
>
{!filterDefinitionUsedInDropdown ? (
<FilterDropdownFilterSelect context={context} />
) : isFilterDropdownOperandSelectUnfolded ? (
<FilterDropdownOperandSelect context={context} />
) : (
selectedOperandInDropdown && (
<>
<FilterDropdownOperandButton context={context} />
<DropdownButton.StyledSearchField autoFocus key={'search-filter'}>
{filterDefinitionUsedInDropdown.type === 'text' && (
<FilterDropdownTextSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'number' && (
<FilterDropdownNumberSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'date' && (
<FilterDropdownDateSearchInput context={context} />
)}
{filterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySearchInput context={context} />
)}
</DropdownButton.StyledSearchField>
{filterDefinitionUsedInDropdown.type === 'entity' && (
<FilterDropdownEntitySelect context={context} />
)}
</>
)
)}
</DropdownButton>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,72 @@
import { Context } from 'react';
import { DropdownMenuItemContainer } from '@/ui/dropdown/components/DropdownMenuItemContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/recoil-scope/hooks/useRecoilScopedValue';
import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope';
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';
import DropdownButton from './DropdownButton';
export function FilterDropdownFilterSelect({
context,
}: {
context: Context<string | null>;
}) {
const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
filterDropdownSearchInputScopedState,
context,
);
const availableFilters = useRecoilScopedValue(
availableFiltersScopedState,
context,
);
const setHotkeyScope = useSetHotkeyScope();
return (
<DropdownMenuItemContainer style={{ maxHeight: '300px' }}>
{availableFilters.map((availableFilter, index) => (
<DropdownMenuSelectableItem
key={`select-filter-${index}`}
onClick={() => {
setFilterDefinitionUsedInDropdown(availableFilter);
if (availableFilter.type === 'entity') {
setHotkeyScope(RelationPickerHotkeyScope.RelationPicker);
}
setSelectedOperandInDropdown(
getOperandsForFilterType(availableFilter.type)?.[0],
);
setFilterDropdownSearchInput('');
}}
>
<DropdownButton.StyledIcon>
{availableFilter.icon}
</DropdownButton.StyledIcon>
{availableFilter.label}
</DropdownMenuSelectableItem>
))}
</DropdownMenuItemContainer>
);
}

View File

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

View File

@ -0,0 +1,40 @@
import { Context } from 'react';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
import { getOperandLabel } from '../utils/getOperandLabel';
import DropdownButton from './DropdownButton';
export function FilterDropdownOperandButton({
context,
}: {
context: Context<string | null>;
}) {
const [selectedOperandInDropdown] = useRecoilScopedState(
selectedOperandInDropdownScopedState,
context,
);
const [isOperandSelectionUnfolded, setIsOperandSelectionUnfolded] =
useRecoilScopedState(
isFilterDropdownOperandSelectUnfoldedScopedState,
context,
);
if (isOperandSelectionUnfolded) {
return null;
}
return (
<DropdownButton.StyledDropdownTopOption
key={'selected-filter-operand'}
onClick={() => setIsOperandSelectionUnfolded(true)}
>
{getOperandLabel(selectedOperandInDropdown)}
<DropdownButton.StyledDropdownTopOptionAngleDown />
</DropdownButton.StyledDropdownTopOption>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,28 @@
import { SortOrder as Order_By } from '~/generated/graphql';
import { SelectedSortType } from './types/interface';
const mapOrderToOrder_By = (order: string) => {
if (order === 'asc') return Order_By.Asc;
return Order_By.Desc;
};
export const defaultOrderByTemplateFactory =
(key: string) => (order: string) => ({
[key]: order,
});
export const reduceSortsToOrderBy = <OrderByTemplate>(
sorts: Array<SelectedSortType<OrderByTemplate>>,
): OrderByTemplate[] => {
const mappedSorts = sorts.map((sort) => {
if (sort.orderByTemplates) {
return sort.orderByTemplates?.map((orderByTemplate) =>
orderByTemplate(mapOrderToOrder_By(sort.order)),
);
}
return defaultOrderByTemplateFactory(sort.key as string)(sort.order);
});
return mappedSorts.flat() as OrderByTemplate[];
};

View File

@ -0,0 +1,21 @@
import { Context, useMemo } from 'react';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
import { filtersScopedState } from '../states/filtersScopedState';
export function useFilterCurrentlyEdited(context: Context<string | null>) {
const [filters] = useRecoilScopedState(filtersScopedState, context);
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
filterDefinitionUsedInDropdownScopedState,
context,
);
return useMemo(() => {
return filters.find(
(filter) => filter.field === filterDefinitionUsedInDropdown?.field,
);
}, [filterDefinitionUsedInDropdown, filters]);
}

View File

@ -0,0 +1,17 @@
import { Context } from 'react';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { filtersScopedState } from '../states/filtersScopedState';
export function useRemoveFilter(context: Context<string | null>) {
const [, setFilters] = useRecoilScopedState(filtersScopedState, context);
return function removeFilter(filterField: string) {
setFilters((filters) => {
return filters.filter((filter) => {
return filter.field !== filterField;
});
});
};
}

View File

@ -0,0 +1,27 @@
import { Context } from 'react';
import { produce } from 'immer';
import { useRecoilScopedState } from '@/ui/recoil-scope/hooks/useRecoilScopedState';
import { filtersScopedState } from '../states/filtersScopedState';
import { Filter } from '../types/Filter';
export function useUpsertFilter(context: Context<string | null>) {
const [, setFilters] = useRecoilScopedState(filtersScopedState, context);
return function upsertFilter(filterToUpsert: Filter) {
setFilters((filters) => {
return produce(filters, (filtersDraft) => {
const index = filtersDraft.findIndex(
(filter) => filter.field === filterToUpsert.field,
);
if (index === -1) {
filtersDraft.push(filterToUpsert);
} else {
filtersDraft[index] = filterToUpsert;
}
});
});
};
}

View File

@ -0,0 +1,11 @@
import { atomFamily } from 'recoil';
import { FilterDefinition } from '../types/FilterDefinition';
export const availableFiltersScopedState = atomFamily<
FilterDefinition[],
string
>({
key: 'availableFiltersScopedState',
default: [],
});

View File

@ -0,0 +1,11 @@
import { atomFamily } from 'recoil';
import { FilterDefinition } from '../types/FilterDefinition';
export const filterDefinitionUsedInDropdownScopedState = atomFamily<
FilterDefinition | null,
string
>({
key: 'filterDefinitionUsedInDropdownScopedState',
default: null,
});

View File

@ -0,0 +1,6 @@
import { atomFamily } from 'recoil';
export const filterDropdownSearchInputScopedState = atomFamily<string, string>({
key: 'filterDropdownSearchInputScopedState',
default: '',
});

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const filterDropdownSelectedEntityIdScopedState = atomFamily<
string | null,
string
>({
key: 'filterDropdownSelectedEntityIdScopedState',
default: null,
});

View File

@ -0,0 +1,8 @@
import { atomFamily } from 'recoil';
import { Filter } from '../types/Filter';
export const filtersScopedState = atomFamily<Filter[], string>({
key: 'filtersScopedState',
default: [],
});

View File

@ -0,0 +1,9 @@
import { atomFamily } from 'recoil';
export const isFilterDropdownOperandSelectUnfoldedScopedState = atomFamily<
boolean,
string
>({
key: 'isFilterDropdownOperandSelectUnfoldedScopedState',
default: false,
});

View File

@ -0,0 +1,11 @@
import { atomFamily } from 'recoil';
import { FilterOperand } from '../types/FilterOperand';
export const selectedOperandInDropdownScopedState = atomFamily<
FilterOperand | null,
string
>({
key: 'selectedOperandInDropdownScopedState',
default: null,
});

View File

@ -0,0 +1,10 @@
import { FilterOperand } from './FilterOperand';
import { FilterType } from './FilterType';
export type Filter = {
field: string;
type: FilterType;
value: string;
displayValue: string;
operand: FilterOperand;
};

View File

@ -0,0 +1,9 @@
import { FilterType } from './FilterType';
export type FilterDefinition = {
field: string;
label: string;
icon: JSX.Element;
type: FilterType;
entitySelectComponent?: JSX.Element;
};

View File

@ -0,0 +1,5 @@
import { FilterDefinition } from './FilterDefinition';
export type FilterDefinitionByEntity<T> = FilterDefinition & {
field: keyof T;
};

View File

@ -0,0 +1,7 @@
export type FilterOperand =
| 'contains'
| 'does-not-contain'
| 'greater-than'
| 'less-than'
| 'is'
| 'is-not';

View File

@ -0,0 +1,4 @@
export type FilterSearchResult = {
id: string;
label: string;
};

View File

@ -0,0 +1 @@
export type FilterType = 'text' | 'date' | 'entity' | 'number';

View File

@ -0,0 +1,3 @@
export enum FiltersHotkeyScope {
FilterDropdownButton = 'filter-dropdown-button',
}

View File

@ -0,0 +1,14 @@
import { ReactNode } from 'react';
import { SortOrder as Order_By } from '~/generated/graphql';
export type SortType<OrderByTemplate> = {
label: string;
key: string;
icon?: ReactNode;
orderByTemplates?: Array<(order: Order_By) => OrderByTemplate>;
};
export type SelectedSortType<OrderByTemplate> = SortType<OrderByTemplate> & {
order: 'asc' | 'desc';
};

View File

@ -0,0 +1,20 @@
import { FilterOperand } from '../types/FilterOperand';
export function getOperandLabel(operand: FilterOperand | null | undefined) {
switch (operand) {
case 'contains':
return 'Contains';
case 'does-not-contain':
return "Does'nt contain";
case 'greater-than':
return 'Greater than';
case 'less-than':
return 'Less than';
case 'is':
return 'Is';
case 'is-not':
return 'Is not';
default:
return '';
}
}

View File

@ -0,0 +1,18 @@
import { FilterOperand } from '../types/FilterOperand';
import { FilterType } from '../types/FilterType';
export function getOperandsForFilterType(
filterType: FilterType | null | undefined,
): FilterOperand[] {
switch (filterType) {
case 'text':
return ['contains', 'does-not-contain'];
case 'number':
case 'date':
return ['greater-than', 'less-than'];
case 'entity':
return ['is', 'is-not'];
default:
return [];
}
}

View File

@ -0,0 +1,90 @@
import { QueryMode } from '~/generated/graphql';
import { Filter } from '../types/Filter';
export function turnFilterIntoWhereClause(filter: Filter) {
switch (filter.type) {
case 'text':
switch (filter.operand) {
case 'contains':
return {
[filter.field]: {
contains: filter.value,
mode: QueryMode.Insensitive,
},
};
case 'does-not-contain':
return {
[filter.field]: {
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 'greater-than':
return {
[filter.field]: {
gte: parseFloat(filter.value),
},
};
case 'less-than':
return {
[filter.field]: {
lte: parseFloat(filter.value),
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
case 'date':
switch (filter.operand) {
case 'greater-than':
return {
[filter.field]: {
gte: filter.value,
},
};
case 'less-than':
return {
[filter.field]: {
lte: filter.value,
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
case 'entity':
switch (filter.operand) {
case 'is':
return {
[filter.field]: {
equals: filter.value,
},
};
case 'is-not':
return {
[filter.field]: {
not: { equals: filter.value },
},
};
default:
throw new Error(
`Unknown operand ${filter.operand} for ${filter.type} filter`,
);
}
default:
throw new Error('Unknown filter type');
}
}

View File

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { isDefined } from '@/utils/type-guards/isDefined';
import { isDefined } from '~/utils/isDefined';
export function useListenClickOutsideArrayOfRef<T extends Element>(
arrayOfRef: Array<React.RefObject<T>>,

View File

@ -0,0 +1,23 @@
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
export const INITIAL_HOTKEYS_SCOPES: string[] = [AppHotkeyScope.App];
export const ALWAYS_ON_HOTKEYS_SCOPES: string[] = [
AppHotkeyScope.CommandMenu,
AppHotkeyScope.App,
];
export const DEFAULT_HOTKEYS_SCOPE_CUSTOM_SCOPES: CustomHotkeyScopes = {
commandMenu: true,
goto: false,
};
export const INITIAL_HOTKEYS_SCOPE: HotkeyScope = {
scope: AppHotkeyScope.App,
customScopes: {
commandMenu: true,
goto: true,
},
};

View File

@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { currentHotkeyScopeState } from '@/ui/hotkey/states/internal/currentHotkeyScopeState';
import { AppHotkeyScope } from '../../types/AppHotkeyScope';
import { useHotkeyScopes } from './useHotkeyScopes';
export function useHotkeyScopeAutoSync() {
const { setHotkeyScopes } = useHotkeyScopes();
const currentHotkeyScope = useRecoilValue(currentHotkeyScopeState);
useEffect(() => {
const scopesToSet: string[] = [];
if (currentHotkeyScope.customScopes?.commandMenu) {
scopesToSet.push(AppHotkeyScope.CommandMenu);
}
if (currentHotkeyScope?.customScopes?.goto) {
scopesToSet.push(AppHotkeyScope.Goto);
}
scopesToSet.push(currentHotkeyScope.scope);
setHotkeyScopes(scopesToSet);
}, [setHotkeyScopes, currentHotkeyScope]);
}

View File

@ -0,0 +1,103 @@
import { useHotkeysContext } from 'react-hotkeys-hook';
import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '../../states/internal/internalHotkeysEnabledScopesState';
export function useHotkeyScopes() {
const { disableScope, enableScope } = useHotkeysContext();
const disableAllHotkeyScopes = useRecoilCallback(
({ set, snapshot }) => {
return async () => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
for (const enabledScope of enabledScopes) {
disableScope(enabledScope);
}
set(internalHotkeysEnabledScopesState, []);
};
},
[disableScope],
);
const enableHotkeyScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToEnable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
if (!enabledScopes.includes(scopeToEnable)) {
enableScope(scopeToEnable);
set(internalHotkeysEnabledScopesState, [
...enabledScopes,
scopeToEnable,
]);
}
};
},
[enableScope],
);
const disableHotkeyScope = useRecoilCallback(
({ set, snapshot }) => {
return async (scopeToDisable: string) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopeToRemoveIndex = enabledScopes.findIndex(
(scope) => scope === scopeToDisable,
);
if (scopeToRemoveIndex > -1) {
disableScope(scopeToDisable);
enabledScopes.splice(scopeToRemoveIndex);
set(internalHotkeysEnabledScopesState, enabledScopes);
}
};
},
[disableScope],
);
const setHotkeyScopes = useRecoilCallback(
({ set, snapshot }) => {
return async (scopesToSet: string[]) => {
const enabledScopes = await snapshot.getPromise(
internalHotkeysEnabledScopesState,
);
const scopesToDisable = enabledScopes.filter(
(enabledScope) => !scopesToSet.includes(enabledScope),
);
const scopesToEnable = scopesToSet.filter(
(scopeToSet) => !enabledScopes.includes(scopeToSet),
);
for (const scopeToDisable of scopesToDisable) {
disableScope(scopeToDisable);
}
for (const scopeToEnable of scopesToEnable) {
enableScope(scopeToEnable);
}
set(internalHotkeysEnabledScopesState, scopesToSet);
};
},
[disableScope, enableScope],
);
return {
disableAllHotkeyScopes,
enableHotkeyScope,
disableHotkeyScope,
setHotkeyScopes,
};
}

View File

@ -0,0 +1,25 @@
import { Keys } from 'react-hotkeys-hook/dist/types';
import { useNavigate } from 'react-router-dom';
import { AppHotkeyScope } from '../types/AppHotkeyScope';
import { useSequenceHotkeys } from './useSequenceScopedHotkeys';
export function useGoToHotkeys(key: Keys, location: string) {
const navigate = useNavigate();
useSequenceHotkeys(
'g',
key,
() => {
navigate(location);
},
AppHotkeyScope.Goto,
{
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
[navigate],
);
}

View File

@ -0,0 +1,39 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { currentHotkeyScopeState } from '../states/internal/currentHotkeyScopeState';
import { CustomHotkeyScopes } from '../types/CustomHotkeyScope';
import { HotkeyScope } from '../types/HotkeyScope';
import { useSetHotkeyScope } from './useSetHotkeyScope';
export function usePreviousHotkeyScope() {
const [previousHotkeyScope, setPreviousHotkeyScope] =
useState<HotkeyScope | null>();
const setHotkeyScope = useSetHotkeyScope();
const currentHotkeyScope = useRecoilValue(currentHotkeyScopeState);
function goBackToPreviousHotkeyScope() {
if (previousHotkeyScope) {
setHotkeyScope(
previousHotkeyScope.scope,
previousHotkeyScope.customScopes,
);
}
}
function setHotkeyScopeAndMemorizePreviousScope(
scope: string,
customScopes?: CustomHotkeyScopes,
) {
setPreviousHotkeyScope(currentHotkeyScope);
setHotkeyScope(scope, customScopes);
}
return {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
};
}

View File

@ -0,0 +1,43 @@
import { useHotkeys } from 'react-hotkeys-hook';
import {
Hotkey,
HotkeyCallback,
Keys,
Options,
OptionsOrDependencyArray,
} from 'react-hotkeys-hook/dist/types';
import { useRecoilState } from 'recoil';
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
export function useScopedHotkeys(
keys: Keys,
callback: HotkeyCallback,
scope: string,
dependencies?: OptionsOrDependencyArray,
options: Options = {
enableOnContentEditable: true,
enableOnFormTags: true,
preventDefault: true,
},
) {
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
function callbackIfDirectKey(
keyboardEvent: KeyboardEvent,
hotkeysEvent: Hotkey,
) {
if (!pendingHotkey) {
callback(keyboardEvent, hotkeysEvent);
return;
}
setPendingHotkey(null);
}
return useHotkeys(
keys,
callbackIfDirectKey,
{ ...options, scopes: [scope] },
dependencies,
);
}

Some files were not shown because too many files have changed in this diff Show More