Refactor/display input part 2 (#1555)

* Email - Money - Number

* Date
This commit is contained in:
Lucas Bordeau
2023-09-12 20:04:26 +02:00
committed by GitHub
parent 9b495ae2e8
commit 9b5e24105b
33 changed files with 348 additions and 295 deletions

View File

@ -8,6 +8,7 @@ import { ActivityComments } from '@/activities/components/ActivityComments';
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown'; import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities'; import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox'; import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { EditableFieldHotkeyScope } from '@/ui/editable-field/types/EditableFieldHotkeyScope';
import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField'; import { DateEditableField } from '@/ui/editable-field/variants/components/DateEditableField';
import { IconCalendar } from '@/ui/icon/index'; import { IconCalendar } from '@/ui/icon/index';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@ -201,6 +202,7 @@ export function ActivityEditor({
refetchQueries: [getOperationName(GET_ACTIVITIES) ?? ''], refetchQueries: [getOperationName(GET_ACTIVITIES) ?? ''],
}); });
}} }}
hotkeyScope={EditableFieldHotkeyScope.EditableField}
/> />
<ActivityAssigneeEditableField activity={activity} /> <ActivityAssigneeEditableField activity={activity} />
</> </>

View File

@ -1,9 +1,9 @@
import { formatToHumanReadableDate } from '~/utils'; import { formatToHumanReadableDate } from '~/utils';
type OwnProps = { type OwnProps = {
value: Date | string | null; value: Date | string | null | undefined;
}; };
export function DateInputDisplay({ value }: OwnProps) { export function DateDisplay({ value }: OwnProps) {
return <div>{value && formatToHumanReadableDate(value)}</div>; return <div>{value && formatToHumanReadableDate(value)}</div>;
} }

View File

@ -11,7 +11,7 @@ type OwnProps = {
value: string | null; value: string | null;
}; };
export function EmailInputDisplay({ value }: OwnProps) { export function EmailDisplay({ value }: OwnProps) {
return value && validateEmail(value) ? ( return value && validateEmail(value) ? (
<ContactLink <ContactLink
href={`mailto:${value}`} href={`mailto:${value}`}

View File

@ -0,0 +1,22 @@
import styled from '@emotion/styled';
import { formatNumber } from '~/utils/formatNumber';
const StyledTextInputDisplay = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
`;
type OwnProps = {
value: number | null;
};
export function MoneyDisplay({ value }: OwnProps) {
return (
<StyledTextInputDisplay>
{value ? `$${formatNumber(value)}` : ''}
</StyledTextInputDisplay>
);
}

View File

@ -5,6 +5,7 @@ import { dropdownButtonCustomHotkeyScopeScopedFamilyState } from '../states/drop
import { isDropdownButtonOpenScopedFamilyState } from '../states/isDropdownButtonOpenScopedFamilyState'; import { isDropdownButtonOpenScopedFamilyState } from '../states/isDropdownButtonOpenScopedFamilyState';
import { DropdownRecoilScopeContext } from '../states/recoil-scope-contexts/DropdownRecoilScopeContext'; import { DropdownRecoilScopeContext } from '../states/recoil-scope-contexts/DropdownRecoilScopeContext';
// TODO: have a more explicit name than key
export function useDropdownButton({ key }: { key: string }) { export function useDropdownButton({ key }: { key: string }) {
const { const {
setHotkeyScopeAndMemorizePreviousScope, setHotkeyScopeAndMemorizePreviousScope,

View File

@ -1,6 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext'; import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
@ -11,7 +12,6 @@ import { FieldDefinition } from '../types/FieldDefinition';
import { FieldDateMetadata } from '../types/FieldMetadata'; import { FieldDateMetadata } from '../types/FieldMetadata';
import { EditableField } from './EditableField'; import { EditableField } from './EditableField';
import { GenericEditableDateFieldDisplayMode } from './GenericEditableDateFieldDisplayMode';
import { GenericEditableDateFieldEditMode } from './GenericEditableDateFieldEditMode'; import { GenericEditableDateFieldEditMode } from './GenericEditableDateFieldEditMode';
export function GenericEditableDateField() { export function GenericEditableDateField() {
@ -34,7 +34,7 @@ export function GenericEditableDateField() {
<EditableField <EditableField
IconLabel={currentEditableFieldDefinition.Icon} IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableDateFieldEditMode />} editModeContent={<GenericEditableDateFieldEditMode />}
displayModeContent={<GenericEditableDateFieldDisplayMode />} displayModeContent={<DateDisplay value={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue} isDisplayModeContentEmpty={!fieldValue}
/> />
</RecoilScope> </RecoilScope>

View File

@ -1,33 +0,0 @@
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { DateInputDisplay } from '@/ui/input/components/DateInputDisplay';
import { parseDate } from '~/utils/date-utils';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { FieldDefinition } from '../types/FieldDefinition';
import { FieldDateMetadata } from '../types/FieldMetadata';
export function GenericEditableDateFieldDisplayMode() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
const currentEditableFieldDefinition = useContext(
EditableFieldDefinitionContext,
) as FieldDefinition<FieldDateMetadata>;
const fieldValue = useRecoilValue<string>(
genericEntityFieldFamilySelector({
entityId: currentEditableFieldEntityId ?? '',
fieldName: currentEditableFieldDefinition
? currentEditableFieldDefinition.metadata.fieldName
: '',
}),
);
const internalDateValue = fieldValue
? parseDate(fieldValue).toJSDate()
: null;
return <DateInputDisplay value={internalDateValue} />;
}

View File

@ -1,13 +1,17 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { DateInput } from '@/ui/input/components/DateInput';
import { Nullable } from '~/types/Nullable';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext'; import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext'; import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useFieldInputEventHandlers } from '../hooks/useFieldInputEventHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField'; import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector'; import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
import { FieldDefinition } from '../types/FieldDefinition'; import { FieldDefinition } from '../types/FieldDefinition';
import { FieldDateMetadata } from '../types/FieldMetadata'; import { FieldDateMetadata } from '../types/FieldMetadata';
import { EditableFieldEditModeDate } from '../variants/components/EditableFieldEditModeDate';
export function GenericEditableDateFieldEditMode() { export function GenericEditableDateFieldEditMode() {
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext); const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
@ -27,7 +31,21 @@ export function GenericEditableDateFieldEditMode() {
const updateField = useUpdateGenericEntityField(); const updateField = useUpdateGenericEntityField();
function handleSubmit(newDateISO: string) { function handleSubmit(newDate: Nullable<Date>) {
if (!newDate) {
setFieldValue('');
if (currentEditableFieldEntityId && updateField) {
updateField(
currentEditableFieldEntityId,
currentEditableFieldDefinition,
'',
);
}
}
const newDateISO = newDate?.toISOString();
if (newDateISO === fieldValue || !newDateISO) return; if (newDateISO === fieldValue || !newDateISO) return;
setFieldValue(newDateISO); setFieldValue(newDateISO);
@ -41,7 +59,18 @@ export function GenericEditableDateFieldEditMode() {
} }
} }
const { handleEnter, handleEscape, handleClickOutside } =
useFieldInputEventHandlers({
onSubmit: handleSubmit,
});
return ( return (
<EditableFieldEditModeDate value={fieldValue} onChange={handleSubmit} /> <DateInput
hotkeyScope={EditableFieldHotkeyScope.EditableField}
onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
value={fieldValue ? new Date(fieldValue) : null}
/>
); );
} }

View File

@ -1,6 +1,7 @@
import { useContext } from 'react'; import { useContext } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { TextDisplay } from '@/ui/content-display/components/TextDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext'; import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
@ -33,7 +34,7 @@ export function GenericEditableNumberField() {
<EditableField <EditableField
IconLabel={currentEditableFieldDefinition.Icon} IconLabel={currentEditableFieldDefinition.Icon}
editModeContent={<GenericEditableNumberFieldEditMode />} editModeContent={<GenericEditableNumberFieldEditMode />}
displayModeContent={fieldValue} displayModeContent={<TextDisplay text={fieldValue} />}
isDisplayModeContentEmpty={!fieldValue} isDisplayModeContentEmpty={!fieldValue}
/> />
</RecoilScope> </RecoilScope>

View File

@ -1,7 +1,7 @@
import { useContext, useRef, useState } from 'react'; import { useContext } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit'; import { TextInput } from '@/ui/input/components/TextInput';
import { import {
canBeCastAsIntegerOrNull, canBeCastAsIntegerOrNull,
castAsIntegerOrNull, castAsIntegerOrNull,
@ -9,9 +9,10 @@ import {
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext'; import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext'; import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers'; import { useFieldInputEventHandlers } from '../hooks/useFieldInputEventHandlers';
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField'; import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector'; import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
import { FieldDefinition } from '../types/FieldDefinition'; import { FieldDefinition } from '../types/FieldDefinition';
import { FieldNumberMetadata } from '../types/FieldMetadata'; import { FieldNumberMetadata } from '../types/FieldMetadata';
@ -30,51 +31,43 @@ export function GenericEditableNumberFieldEditMode() {
: '', : '',
}), }),
); );
const [internalValue, setInternalValue] = useState(
fieldValue ? fieldValue.toString() : '',
);
const updateField = useUpdateGenericEntityField(); const updateField = useUpdateGenericEntityField();
const wrapperRef = useRef(null); function handleSubmit(newValue: string) {
if (!canBeCastAsIntegerOrNull(newValue)) {
useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel);
function handleSubmit() {
if (!canBeCastAsIntegerOrNull(internalValue)) {
return; return;
} }
if (internalValue === fieldValue) return;
setFieldValue(castAsIntegerOrNull(internalValue)); if (newValue === fieldValue) return;
const castedValue = castAsIntegerOrNull(newValue);
setFieldValue(castedValue);
if (currentEditableFieldEntityId && updateField) { if (currentEditableFieldEntityId && updateField) {
updateField( updateField(
currentEditableFieldEntityId, currentEditableFieldEntityId,
currentEditableFieldDefinition, currentEditableFieldDefinition,
castAsIntegerOrNull(internalValue), castedValue,
); );
} }
} }
function onCancel() { const { handleEnter, handleEscape, handleClickOutside } =
setFieldValue(fieldValue); useFieldInputEventHandlers({
} onSubmit: handleSubmit,
});
function handleChange(newValue: string) {
setInternalValue(newValue);
}
return ( return (
<div ref={wrapperRef}> <TextInput
<TextInputEdit autoFocus
autoFocus placeholder={currentEditableFieldDefinition.metadata.placeHolder}
placeholder={currentEditableFieldDefinition.metadata.placeHolder} hotkeyScope={EditableFieldHotkeyScope.EditableField}
value={internalValue ? internalValue.toString() : ''} value={fieldValue ? fieldValue.toString() : ''}
onChange={(newValue: string) => { onClickOutside={handleClickOutside}
handleChange(newValue); onEnter={handleEnter}
}} onEscape={handleEscape}
/> />
</div>
); );
} }

View File

@ -1,7 +1,7 @@
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
import { EditableField } from '@/ui/editable-field/components/EditableField'; import { EditableField } from '@/ui/editable-field/components/EditableField';
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext'; import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
import { IconComponent } from '@/ui/icon/types/IconComponent'; import { IconComponent } from '@/ui/icon/types/IconComponent';
import { DateInputDisplay } from '@/ui/input/components/DateInputDisplay';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { parseDate } from '~/utils/date-utils'; import { parseDate } from '~/utils/date-utils';
@ -12,9 +12,16 @@ type OwnProps = {
label?: string; label?: string;
value: string | null | undefined; value: string | null | undefined;
onSubmit?: (newValue: string) => void; onSubmit?: (newValue: string) => void;
hotkeyScope: string;
}; };
export function DateEditableField({ Icon, value, label, onSubmit }: OwnProps) { export function DateEditableField({
Icon,
value,
label,
onSubmit,
hotkeyScope,
}: OwnProps) {
async function handleChange(newValue: string) { async function handleChange(newValue: string) {
onSubmit?.(newValue); onSubmit?.(newValue);
} }
@ -24,8 +31,6 @@ export function DateEditableField({ Icon, value, label, onSubmit }: OwnProps) {
return ( return (
<RecoilScope SpecificContext={FieldRecoilScopeContext}> <RecoilScope SpecificContext={FieldRecoilScopeContext}>
<EditableField <EditableField
// onSubmit={handleSubmit}
// onCancel={handleCancel}
IconLabel={Icon} IconLabel={Icon}
label={label} label={label}
editModeContent={ editModeContent={
@ -34,9 +39,10 @@ export function DateEditableField({ Icon, value, label, onSubmit }: OwnProps) {
onChange={(newValue: string) => { onChange={(newValue: string) => {
handleChange(newValue); handleChange(newValue);
}} }}
parentHotkeyScope={hotkeyScope}
/> />
} }
displayModeContent={<DateInputDisplay value={internalDateValue} />} displayModeContent={<DateDisplay value={internalDateValue} />}
isDisplayModeContentEmpty={!value} isDisplayModeContentEmpty={!value}
/> />
</RecoilScope> </RecoilScope>

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { DateInputEdit } from '@/ui/input/components/DateInputEdit'; import { DateInput } from '@/ui/input/components/DateInput';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { Nullable } from '~/types/Nullable';
import { parseDate } from '~/utils/date-utils'; import { parseDate } from '~/utils/date-utils';
import { useEditableField } from '../../hooks/useEditableField'; import { useEditableField } from '../../hooks/useEditableField';
@ -9,10 +9,15 @@ import { useEditableField } from '../../hooks/useEditableField';
type OwnProps = { type OwnProps = {
value: string; value: string;
onChange?: (newValue: string) => void; onChange?: (newValue: string) => void;
parentHotkeyScope?: HotkeyScope; parentHotkeyScope: string;
}; };
export function EditableFieldEditModeDate({ value, onChange }: OwnProps) { // TODO: refactor this component to use the same logic as the GenericDateField component
export function EditableFieldEditModeDate({
value,
onChange,
parentHotkeyScope,
}: OwnProps) {
const [internalValue, setInternalValue] = useState(value); const [internalValue, setInternalValue] = useState(value);
useEffect(() => { useEffect(() => {
@ -21,17 +26,26 @@ export function EditableFieldEditModeDate({ value, onChange }: OwnProps) {
const { closeEditableField } = useEditableField(); const { closeEditableField } = useEditableField();
function handleChange(newValue: string) { function handleClickOutside() {
onChange?.(newValue); closeEditableField();
}
function handleEnter(newValue: Nullable<Date>) {
onChange?.(newValue?.toISOString() ?? '');
closeEditableField();
}
function handleEscape() {
closeEditableField(); closeEditableField();
} }
return ( return (
<DateInputEdit <DateInput
value={internalValue ? parseDate(internalValue).toJSDate() : new Date()} value={internalValue ? parseDate(internalValue).toJSDate() : new Date()}
onChange={(newDate: Date) => { hotkeyScope={parentHotkeyScope}
handleChange(newDate.toISOString()); onClickOutside={handleClickOutside}
}} onEnter={handleEnter}
onEscape={handleEscape}
/> />
); );
} }

View File

@ -0,0 +1,103 @@
import { useEffect, useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react';
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
import { Nullable } from '~/types/Nullable';
import { useRegisterInputEvents } from '../hooks/useRegisterInputEvents';
import { DatePicker } from './DatePicker';
const StyledCalendarContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
margin-top: 1px;
position: absolute;
z-index: 1;
`;
const StyledInputContainer = styled.div`
padding: ${({ theme }) => theme.spacing(0)} ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
export type DateInputEditProps = {
value: Nullable<Date>;
onEnter: (newDate: Nullable<Date>) => void;
onEscape: (newDate: Nullable<Date>) => void;
onClickOutside: (
event: MouseEvent | TouchEvent,
newDate: Nullable<Date>,
) => void;
hotkeyScope: string;
};
export function DateInput({
value,
hotkeyScope,
onEnter,
onEscape,
onClickOutside,
}: DateInputEditProps) {
const theme = useTheme();
const [internalValue, setInternalValue] = useState(value);
const wrapperRef = useRef(null);
const { refs, floatingStyles } = useFloating({
placement: 'bottom-start',
middleware: [
flip(),
offset({
mainAxis: theme.spacingMultiplicator * 2,
}),
],
});
function handleChange(newDate: Date) {
setInternalValue(newDate);
}
useEffect(() => {
setInternalValue(value);
}, [value]);
useRegisterInputEvents({
inputRef: wrapperRef,
inputValue: internalValue,
onEnter,
onEscape,
onClickOutside,
hotkeyScope,
});
return (
<div ref={wrapperRef}>
<div ref={refs.setReference}>
<StyledInputContainer>
<DateDisplay value={internalValue} />
</StyledInputContainer>
</div>
<div ref={refs.setFloating} style={floatingStyles}>
<StyledCalendarContainer>
<DatePicker
date={internalValue ?? new Date()}
onChange={handleChange}
onMouseSelect={(newDate: Date) => {
onEnter(newDate);
}}
/>
</StyledCalendarContainer>
</div>
</div>
);
}

View File

@ -1,64 +0,0 @@
import { forwardRef } from 'react';
import styled from '@emotion/styled';
import { formatToHumanReadableDate } from '~/utils';
import DatePicker from './DatePicker';
type StyledCalendarContainerProps = {
editModeHorizontalAlign?: 'left' | 'right';
};
const StyledInputContainer = styled.div`
color: ${({ theme }) => theme.font.color.primary};
display: flex;
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledCalendarContainer = styled.div<StyledCalendarContainerProps>`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.light};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
margin-top: 1px;
position: absolute;
z-index: 1;
`;
type DivProps = React.HTMLProps<HTMLDivElement>;
const DateDisplay = forwardRef<HTMLDivElement, DivProps>(
({ value, onClick }, ref) => (
<StyledInputContainer onClick={onClick} ref={ref}>
{value && formatToHumanReadableDate(new Date(value as string))}
</StyledInputContainer>
),
);
type DatePickerContainerProps = {
children: React.ReactNode;
};
const DatePickerContainer = ({ children }: DatePickerContainerProps) => {
return <StyledCalendarContainer>{children}</StyledCalendarContainer>;
};
export type DateInputEditProps = {
value: Date | null | undefined;
onChange: (newDate: Date) => void;
};
export function DateInputEdit({ onChange, value }: DateInputEditProps) {
return (
<DatePicker
date={value ?? new Date()}
onChangeHandler={onChange}
customInput={<DateDisplay />}
customCalendarContainer={DatePickerContainer}
/>
);
}

View File

@ -1,18 +1,11 @@
import React, { forwardRef, ReactElement, useState } from 'react'; import React from 'react';
import ReactDatePicker, { CalendarContainerProps } from 'react-datepicker'; import ReactDatePicker from 'react-datepicker';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { overlayBackground } from '@/ui/theme/constants/effects'; import { overlayBackground } from '@/ui/theme/constants/effects';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
export type DatePickerProps = {
date: Date;
onChangeHandler: (date: Date) => void;
customInput?: ReactElement;
customCalendarContainer?(props: CalendarContainerProps): React.ReactNode;
};
const StyledContainer = styled.div` const StyledContainer = styled.div`
& .react-datepicker { & .react-datepicker {
border-color: ${({ theme }) => theme.border.color.light}; border-color: ${({ theme }) => theme.border.color.light};
@ -39,6 +32,10 @@ const StyledContainer = styled.div`
display: none; display: none;
} }
& .react-datepicker-wrapper {
display: none;
}
// Header // Header
& .react-datepicker__header { & .react-datepicker__header {
@ -223,47 +220,32 @@ const StyledContainer = styled.div`
} }
`; `;
function DatePicker({ export type DatePickerProps = {
date, date: Date;
onChangeHandler, onMouseSelect?: (date: Date) => void;
customInput, onChange?: (date: Date) => void;
customCalendarContainer, };
}: DatePickerProps) {
const [startDate, setStartDate] = useState(date);
type DivProps = React.HTMLProps<HTMLDivElement>;
const DefaultDateDisplay = forwardRef<HTMLDivElement, DivProps>(
({ value, onClick }, ref) => (
<div onClick={onClick} ref={ref}>
{value &&
new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(new Date(value as string))}
</div>
),
);
export function DatePicker({ date, onChange, onMouseSelect }: DatePickerProps) {
return ( return (
<StyledContainer> <StyledContainer>
<ReactDatePicker <ReactDatePicker
open={true} open={true}
selected={startDate} selected={date}
showMonthDropdown showMonthDropdown
showYearDropdown showYearDropdown
onChange={(date: Date) => { onChange={() => {
setStartDate(date); // We need to use onSelect here but onChange is almost redundant with onSelect but is required
onChangeHandler(date); }}
customInput={<></>}
onSelect={(date: Date, event) => {
if (event?.type === 'click') {
onMouseSelect?.(date);
} else {
onChange?.(date);
}
}} }}
customInput={customInput ? customInput : <DefaultDateDisplay />}
calendarContainer={
customCalendarContainer ? customCalendarContainer : undefined
}
/> />
</StyledContainer> </StyledContainer>
); );
} }
export default DatePicker;

View File

@ -4,14 +4,13 @@ import { userEvent, within } from '@storybook/testing-library';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import DatePicker from '../DatePicker'; import { DatePicker } from '../DatePicker';
const meta: Meta<typeof DatePicker> = { const meta: Meta<typeof DatePicker> = {
title: 'UI/Input/DatePicker', title: 'UI/Input/DatePicker',
component: DatePicker, component: DatePicker,
decorators: [ComponentDecorator], decorators: [ComponentDecorator],
argTypes: { argTypes: {
customInput: { control: false },
date: { control: 'date' }, date: { control: 'date' },
}, },
args: { date: new Date('January 1, 2023 00:00:00') }, args: { date: new Date('January 1, 2023 00:00:00') },

View File

@ -2,11 +2,11 @@ import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { EmailInputDisplay } from '../EmailInputDisplay'; import { EmailDisplay } from '../../../content-display/components/EmailDisplay';
const meta: Meta = { const meta: Meta = {
title: 'UI/Input/EmailInputDisplay', title: 'UI/Input/EmailInputDisplay',
component: EmailInputDisplay, component: EmailDisplay,
decorators: [ComponentWithRouterDecorator], decorators: [ComponentWithRouterDecorator],
args: { args: {
value: 'mustajab.ikram@google.com', value: 'mustajab.ikram@google.com',
@ -15,6 +15,6 @@ const meta: Meta = {
export default meta; export default meta;
type Story = StoryObj<typeof EmailInputDisplay>; type Story = StoryObj<typeof EmailDisplay>;
export const Default: Story = {}; export const Default: Story = {};

View File

@ -1,58 +0,0 @@
import { useRef } from 'react';
import styled from '@emotion/styled';
import { Key } from 'ts-key-enum';
import { DateInputEdit } from '@/ui/input/components/DateInputEdit';
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useEditableCell } from '../../hooks/useEditableCell';
const StyledEditableCellDateEditModeContainer = styled.div`
margin-top: -1px;
width: 100%;
`;
export type DateCellEditProps = {
value: Date;
onSubmit: (date: Date) => void;
};
export function DateCellEdit({ value, onSubmit }: DateCellEditProps) {
const { closeEditableCell } = useEditableCell();
function handleDateChange(newDate: Date) {
onSubmit(newDate);
closeEditableCell();
}
useScopedHotkeys(
Key.Escape,
() => {
closeEditableCell();
},
TableHotkeyScope.CellDateEditMode,
[closeEditableCell],
);
const containerRef = useRef(null);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
closeEditableCell();
},
});
return (
<StyledEditableCellDateEditModeContainer ref={containerRef}>
<DateInputEdit onChange={handleDateChange} value={value} />
</StyledEditableCellDateEditModeContainer>
);
}

View File

@ -1,7 +1,7 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
import type { ViewFieldDateMetadata } from '@/ui/editable-field/types/ViewField'; import type { ViewFieldDateMetadata } from '@/ui/editable-field/types/ViewField';
import { DateInputDisplay } from '@/ui/input/components/DateInputDisplay';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector'; import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector';
@ -34,7 +34,7 @@ export function GenericEditableDateCell({
editModeContent={ editModeContent={
<GenericEditableDateCellEditMode columnDefinition={columnDefinition} /> <GenericEditableDateCellEditMode columnDefinition={columnDefinition} />
} }
nonEditModeContent={<DateInputDisplay value={fieldValue} />} nonEditModeContent={<DateDisplay value={fieldValue} />}
></EditableCell> ></EditableCell>
); );
} }

View File

@ -2,14 +2,16 @@ import { DateTime } from 'luxon';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import type { ViewFieldDateMetadata } from '@/ui/editable-field/types/ViewField'; import type { ViewFieldDateMetadata } from '@/ui/editable-field/types/ViewField';
import { DateInput } from '@/ui/input/components/DateInput';
import { useCellInputEventHandlers } from '@/ui/table/hooks/useCellInputEventHandlers';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField'; import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector'; import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector';
import { TableHotkeyScope } from '@/ui/table/types/TableHotkeyScope';
import { Nullable } from '~/types/Nullable';
import type { ColumnDefinition } from '../../../types/ColumnDefinition'; import type { ColumnDefinition } from '../../../types/ColumnDefinition';
import { DateCellEdit } from './DateCellEdit';
type OwnProps = { type OwnProps = {
columnDefinition: ColumnDefinition<ViewFieldDateMetadata>; columnDefinition: ColumnDefinition<ViewFieldDateMetadata>;
}; };
@ -29,12 +31,13 @@ export function GenericEditableDateCellEditMode({
const updateField = useUpdateEntityField(); const updateField = useUpdateEntityField();
function handleSubmit(newDate: Date) { // Wrap this into a hook
function handleSubmit(newDate: Nullable<Date>) {
const fieldValueDate = fieldValue const fieldValueDate = fieldValue
? DateTime.fromISO(fieldValue).toJSDate() ? DateTime.fromISO(fieldValue).toJSDate()
: null; : null;
const newDateISO = DateTime.fromJSDate(newDate).toISO(); const newDateISO = newDate ? DateTime.fromJSDate(newDate).toISO() : null;
if (newDate === fieldValueDate || !newDateISO) return; if (newDate === fieldValueDate || !newDateISO) return;
@ -45,10 +48,18 @@ export function GenericEditableDateCellEditMode({
} }
} }
const { handleEnter, handleEscape, handleClickOutside } =
useCellInputEventHandlers({
onSubmit: handleSubmit,
});
return ( return (
<DateCellEdit <DateInput
value={DateTime.fromISO(fieldValue).toJSDate()} value={DateTime.fromISO(fieldValue).toJSDate()}
onSubmit={handleSubmit} onClickOutside={handleClickOutside}
onEnter={handleEnter}
onEscape={handleEscape}
hotkeyScope={TableHotkeyScope.CellEditMode}
/> />
); );
} }

View File

@ -1,7 +1,7 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { EmailDisplay } from '@/ui/content-display/components/EmailDisplay';
import type { ViewFieldEmailMetadata } from '@/ui/editable-field/types/ViewField'; import type { ViewFieldEmailMetadata } from '@/ui/editable-field/types/ViewField';
import { EmailInputDisplay } from '@/ui/input/components/EmailInputDisplay';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector'; import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector';
@ -34,7 +34,7 @@ export function GenericEditableEmailCell({
editModeContent={ editModeContent={
<GenericEditableEmailCellEditMode columnDefinition={columnDefinition} /> <GenericEditableEmailCellEditMode columnDefinition={columnDefinition} />
} }
nonEditModeContent={<EmailInputDisplay value={fieldValue} />} nonEditModeContent={<EmailDisplay value={fieldValue} />}
></EditableCell> ></EditableCell>
); );
} }

View File

@ -1,5 +1,6 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { MoneyDisplay } from '@/ui/content-display/components/MoneyDisplay';
import type { ViewFieldMoneyMetadata } from '@/ui/editable-field/types/ViewField'; import type { ViewFieldMoneyMetadata } from '@/ui/editable-field/types/ViewField';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
@ -14,11 +15,6 @@ type OwnProps = {
editModeHorizontalAlign?: 'left' | 'right'; editModeHorizontalAlign?: 'left' | 'right';
}; };
function formatNumber(value: number) {
// Formats the value to a string and add commas to it ex: 50,000 | 500,000
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
export function GenericEditableMoneyCell({ export function GenericEditableMoneyCell({
columnDefinition, columnDefinition,
editModeHorizontalAlign, editModeHorizontalAlign,
@ -38,9 +34,7 @@ export function GenericEditableMoneyCell({
editModeContent={ editModeContent={
<GenericEditableMoneyCellEditMode columnDefinition={columnDefinition} /> <GenericEditableMoneyCellEditMode columnDefinition={columnDefinition} />
} }
nonEditModeContent={ nonEditModeContent={<MoneyDisplay value={fieldValue} />}
<>{fieldValue ? `$${formatNumber(fieldValue)}` : ''}</>
}
></EditableCell> ></EditableCell>
); );
} }

View File

@ -28,6 +28,7 @@ export function GenericEditableMoneyCellEditMode({
const updateField = useUpdateEntityField(); const updateField = useUpdateEntityField();
// TODO: handle this logic in a number input
function handleSubmit(newText: string) { function handleSubmit(newText: string) {
if (newText === fieldValue) return; if (newText === fieldValue) return;
@ -64,6 +65,7 @@ export function GenericEditableMoneyCellEditMode({
onSubmit: handleSubmit, onSubmit: handleSubmit,
}); });
// TODO: use a number input
return ( return (
<TextInput <TextInput
autoFocus autoFocus

View File

@ -1,5 +1,6 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { TextDisplay } from '@/ui/content-display/components/TextDisplay';
import type { ViewFieldNumberMetadata } from '@/ui/editable-field/types/ViewField'; import type { ViewFieldNumberMetadata } from '@/ui/editable-field/types/ViewField';
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell'; import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId'; import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
@ -35,7 +36,7 @@ export function GenericEditableNumberCell({
columnDefinition={columnDefinition} columnDefinition={columnDefinition}
/> />
} }
nonEditModeContent={<>{fieldValue}</>} nonEditModeContent={<TextDisplay text={fieldValue} />}
></EditableCell> ></EditableCell>
); );
} }

View File

@ -33,6 +33,7 @@ const common = {
color: grayScale.gray0, color: grayScale.gray0,
}, },
}, },
spacingMultiplicator: 4,
spacing: (multiplicator: number) => `${multiplicator * 4}px`, spacing: (multiplicator: number) => `${multiplicator * 4}px`,
betweenSiblingsGap: `2px`, betweenSiblingsGap: `2px`,
table: { table: {

View File

@ -59,6 +59,10 @@ const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
} }
`; `;
/**
*
* @deprecated use ui/dropdown/components/DropdownButton.tsx instead
*/
function DropdownButton({ function DropdownButton({
anchor, anchor,
label, label,

View File

@ -1,12 +1,14 @@
import { Context } from 'react'; import { Context } from 'react';
import styled from '@emotion/styled';
import DatePicker from '@/ui/input/components/DatePicker'; import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { DatePicker } from '@/ui/input/components/DatePicker';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useUpsertFilter } from '@/ui/view-bar/hooks/useUpsertFilter'; import { useUpsertFilter } from '@/ui/view-bar/hooks/useUpsertFilter';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState'; import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState'; import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState';
export function FilterDropdownDateSearchInput({ export function FilterDropdownDateSearchInput({
context, context,
}: { }: {
@ -22,6 +24,11 @@ export function FilterDropdownDateSearchInput({
context, context,
); );
const [, setIsFilterDropdownUnfolded] = useRecoilScopedState(
isFilterDropdownUnfoldedScopedState,
DropdownRecoilScopeContext,
);
const upsertFilter = useUpsertFilter(context); const upsertFilter = useUpsertFilter(context);
function handleChange(date: Date) { function handleChange(date: Date) {
@ -34,16 +41,15 @@ export function FilterDropdownDateSearchInput({
operand: selectedOperandInDropdown, operand: selectedOperandInDropdown,
displayValue: date.toLocaleDateString(), displayValue: date.toLocaleDateString(),
}); });
setIsFilterDropdownUnfolded(false);
} }
return ( return (
<DatePicker <DatePicker
date={new Date()} date={new Date()}
onChangeHandler={handleChange} onChange={handleChange}
customInput={<></>} onMouseSelect={handleChange}
customCalendarContainer={styled.div`
top: -10px;
`}
/> />
); );
} }

View File

@ -1,6 +1,7 @@
import { Context, useCallback, useState } from 'react'; import { Context, useCallback } from 'react';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { IconComponent } from '@/ui/icon/types/IconComponent'; import { IconComponent } from '@/ui/icon/types/IconComponent';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
@ -10,6 +11,7 @@ import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState';
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/view-bar/states/isFilterDropdownOperandSelectUnfoldedScopedState'; import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/view-bar/states/isFilterDropdownOperandSelectUnfoldedScopedState';
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState'; import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState';
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState'; import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
import DropdownButton from './DropdownButton'; import DropdownButton from './DropdownButton';
@ -39,7 +41,11 @@ export function MultipleFiltersDropdownButton({
Icon, Icon,
label, label,
}: MultipleFiltersDropdownButtonProps) { }: MultipleFiltersDropdownButtonProps) {
const [isUnfolded, setIsUnfolded] = useState(false); const [isFilterDropdownUnfolded, setIsFilterDropdownUnfolded] =
useRecoilScopedState(
isFilterDropdownUnfoldedScopedState,
DropdownRecoilScopeContext,
);
const [ const [
isFilterDropdownOperandSelectUnfolded, isFilterDropdownOperandSelectUnfolded,
@ -93,7 +99,7 @@ export function MultipleFiltersDropdownButton({
((isPrimaryButton && !isFilterSelected) || !isPrimaryButton) ((isPrimaryButton && !isFilterSelected) || !isPrimaryButton)
) { ) {
setHotkeyScope(hotkeyScope); setHotkeyScope(hotkeyScope);
setIsUnfolded(true); setIsFilterDropdownUnfolded(true);
return; return;
} }
@ -101,7 +107,7 @@ export function MultipleFiltersDropdownButton({
setHotkeyScope(hotkeyScope); setHotkeyScope(hotkeyScope);
} }
setIsUnfolded(false); setIsFilterDropdownUnfolded(false);
resetState(); resetState();
} }
@ -109,7 +115,7 @@ export function MultipleFiltersDropdownButton({
<DropdownButton <DropdownButton
label={label ?? 'Filter'} label={label ?? 'Filter'}
isActive={isFilterSelected} isActive={isFilterSelected}
isUnfolded={isUnfolded} isUnfolded={isFilterDropdownUnfolded}
Icon={Icon} Icon={Icon}
onIsUnfoldedChange={handleIsUnfoldedChange} onIsUnfoldedChange={handleIsUnfoldedChange}
hotkeyScope={hotkeyScope} hotkeyScope={hotkeyScope}

View File

@ -1,8 +1,9 @@
import { Context, useState } from 'react'; import { Context } from 'react';
import React from 'react'; import React from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { IconChevronDown } from '@/ui/icon'; import { IconChevronDown } from '@/ui/icon';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
@ -13,6 +14,7 @@ import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selec
import { StyledHeaderDropdownButton } from '../../dropdown/components/StyledHeaderDropdownButton'; import { StyledHeaderDropdownButton } from '../../dropdown/components/StyledHeaderDropdownButton';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState'; import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { filtersScopedState } from '../states/filtersScopedState'; import { filtersScopedState } from '../states/filtersScopedState';
import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState';
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
import { DropdownMenuContainer } from './DropdownMenuContainer'; import { DropdownMenuContainer } from './DropdownMenuContainer';
@ -41,7 +43,11 @@ export function SingleEntityFilterDropdownButton({
); );
const availableFilter = availableFilters[0]; const availableFilter = availableFilters[0];
const [isUnfolded, setIsUnfolded] = useState(false); const [isFilterDropdownUnfolded, setIsFilterDropdownUnfolded] =
useRecoilScopedState(
isFilterDropdownUnfoldedScopedState,
DropdownRecoilScopeContext,
);
const [filters] = useRecoilScopedState(filtersScopedState, context); const [filters] = useRecoilScopedState(filtersScopedState, context);
@ -75,10 +81,10 @@ export function SingleEntityFilterDropdownButton({
function handleIsUnfoldedChange(newIsUnfolded: boolean) { function handleIsUnfoldedChange(newIsUnfolded: boolean) {
if (newIsUnfolded) { if (newIsUnfolded) {
setHotkeyScope(hotkeyScope); setHotkeyScope(hotkeyScope);
setIsUnfolded(true); setIsFilterDropdownUnfolded(true);
} else { } else {
setHotkeyScope(hotkeyScope); setHotkeyScope(hotkeyScope);
setIsUnfolded(false); setIsFilterDropdownUnfolded(false);
setFilterDropdownSearchInput(''); setFilterDropdownSearchInput('');
} }
} }
@ -86,8 +92,8 @@ export function SingleEntityFilterDropdownButton({
return ( return (
<StyledDropdownButtonContainer> <StyledDropdownButtonContainer>
<StyledHeaderDropdownButton <StyledHeaderDropdownButton
isUnfolded={isUnfolded} isUnfolded={isFilterDropdownUnfolded}
onClick={() => handleIsUnfoldedChange(!isUnfolded)} onClick={() => handleIsUnfoldedChange(!isFilterDropdownUnfolded)}
> >
{filters[0] ? ( {filters[0] ? (
<GenericEntityFilterChip filter={filters[0]} /> <GenericEntityFilterChip filter={filters[0]} />
@ -96,7 +102,7 @@ export function SingleEntityFilterDropdownButton({
)} )}
<IconChevronDown size={theme.icon.size.md} /> <IconChevronDown size={theme.icon.size.md} />
</StyledHeaderDropdownButton> </StyledHeaderDropdownButton>
{isUnfolded && ( {isFilterDropdownUnfolded && (
<DropdownMenuContainer onClose={() => handleIsUnfoldedChange(false)}> <DropdownMenuContainer onClose={() => handleIsUnfoldedChange(false)}>
<FilterDropdownEntitySearchInput context={context} /> <FilterDropdownEntitySearchInput context={context} />
<FilterDropdownEntitySelect context={context} /> <FilterDropdownEntitySelect context={context} />

View File

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

View File

@ -0,0 +1 @@
export type Nullable<T> = T | null | undefined;

View File

@ -1,19 +1,29 @@
const DEBUG_MODE = false;
export function canBeCastAsIntegerOrNull( export function canBeCastAsIntegerOrNull(
probableNumberOrNull: string | undefined | number | null, probableNumberOrNull: string | undefined | number | null,
): probableNumberOrNull is number | null { ): probableNumberOrNull is number | null {
if (probableNumberOrNull === undefined) { if (probableNumberOrNull === undefined) {
if (DEBUG_MODE) console.log('probableNumberOrNull === undefined');
return false; return false;
} }
if (typeof probableNumberOrNull === 'number') { if (typeof probableNumberOrNull === 'number') {
if (DEBUG_MODE) console.log('typeof probableNumberOrNull === "number"');
return Number.isInteger(probableNumberOrNull); return Number.isInteger(probableNumberOrNull);
} }
if (probableNumberOrNull === null) { if (probableNumberOrNull === null) {
if (DEBUG_MODE) console.log('probableNumberOrNull === null');
return true; return true;
} }
if (probableNumberOrNull === '') { if (probableNumberOrNull === '') {
if (DEBUG_MODE) console.log('probableNumberOrNull === ""');
return true; return true;
} }
@ -21,9 +31,13 @@ export function canBeCastAsIntegerOrNull(
const stringAsNumber = +probableNumberOrNull; const stringAsNumber = +probableNumberOrNull;
if (isNaN(stringAsNumber)) { if (isNaN(stringAsNumber)) {
if (DEBUG_MODE) console.log('isNaN(stringAsNumber)');
return false; return false;
} }
if (Number.isInteger(stringAsNumber)) { if (Number.isInteger(stringAsNumber)) {
if (DEBUG_MODE) console.log('Number.isInteger(stringAsNumber)');
return true; return true;
} }
} }

View File

@ -0,0 +1,4 @@
export function formatNumber(value: number) {
// Formats the value to a string and add commas to it ex: 50,000 | 500,000
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}