@ -8,6 +8,7 @@ import { ActivityComments } from '@/activities/components/ActivityComments';
|
||||
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
|
||||
import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities';
|
||||
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 { IconCalendar } from '@/ui/icon/index';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
@ -201,6 +202,7 @@ export function ActivityEditor({
|
||||
refetchQueries: [getOperationName(GET_ACTIVITIES) ?? ''],
|
||||
});
|
||||
}}
|
||||
hotkeyScope={EditableFieldHotkeyScope.EditableField}
|
||||
/>
|
||||
<ActivityAssigneeEditableField activity={activity} />
|
||||
</>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { formatToHumanReadableDate } from '~/utils';
|
||||
|
||||
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>;
|
||||
}
|
||||
@ -11,7 +11,7 @@ type OwnProps = {
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
export function EmailInputDisplay({ value }: OwnProps) {
|
||||
export function EmailDisplay({ value }: OwnProps) {
|
||||
return value && validateEmail(value) ? (
|
||||
<ContactLink
|
||||
href={`mailto:${value}`}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -5,6 +5,7 @@ import { dropdownButtonCustomHotkeyScopeScopedFamilyState } from '../states/drop
|
||||
import { isDropdownButtonOpenScopedFamilyState } from '../states/isDropdownButtonOpenScopedFamilyState';
|
||||
import { DropdownRecoilScopeContext } from '../states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||
|
||||
// TODO: have a more explicit name than key
|
||||
export function useDropdownButton({ key }: { key: string }) {
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
|
||||
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
|
||||
@ -11,7 +12,6 @@ import { FieldDefinition } from '../types/FieldDefinition';
|
||||
import { FieldDateMetadata } from '../types/FieldMetadata';
|
||||
|
||||
import { EditableField } from './EditableField';
|
||||
import { GenericEditableDateFieldDisplayMode } from './GenericEditableDateFieldDisplayMode';
|
||||
import { GenericEditableDateFieldEditMode } from './GenericEditableDateFieldEditMode';
|
||||
|
||||
export function GenericEditableDateField() {
|
||||
@ -34,7 +34,7 @@ export function GenericEditableDateField() {
|
||||
<EditableField
|
||||
IconLabel={currentEditableFieldDefinition.Icon}
|
||||
editModeContent={<GenericEditableDateFieldEditMode />}
|
||||
displayModeContent={<GenericEditableDateFieldDisplayMode />}
|
||||
displayModeContent={<DateDisplay value={fieldValue} />}
|
||||
isDisplayModeContentEmpty={!fieldValue}
|
||||
/>
|
||||
</RecoilScope>
|
||||
|
||||
@ -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} />;
|
||||
}
|
||||
@ -1,13 +1,17 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { DateInput } from '@/ui/input/components/DateInput';
|
||||
import { Nullable } from '~/types/Nullable';
|
||||
|
||||
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
|
||||
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
|
||||
import { useFieldInputEventHandlers } from '../hooks/useFieldInputEventHandlers';
|
||||
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
|
||||
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
|
||||
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
|
||||
import { FieldDefinition } from '../types/FieldDefinition';
|
||||
import { FieldDateMetadata } from '../types/FieldMetadata';
|
||||
import { EditableFieldEditModeDate } from '../variants/components/EditableFieldEditModeDate';
|
||||
|
||||
export function GenericEditableDateFieldEditMode() {
|
||||
const currentEditableFieldEntityId = useContext(EditableFieldEntityIdContext);
|
||||
@ -27,7 +31,21 @@ export function GenericEditableDateFieldEditMode() {
|
||||
|
||||
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;
|
||||
|
||||
setFieldValue(newDateISO);
|
||||
@ -41,7 +59,18 @@ export function GenericEditableDateFieldEditMode() {
|
||||
}
|
||||
}
|
||||
|
||||
const { handleEnter, handleEscape, handleClickOutside } =
|
||||
useFieldInputEventHandlers({
|
||||
onSubmit: handleSubmit,
|
||||
});
|
||||
|
||||
return (
|
||||
<EditableFieldEditModeDate value={fieldValue} onChange={handleSubmit} />
|
||||
<DateInput
|
||||
hotkeyScope={EditableFieldHotkeyScope.EditableField}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
value={fieldValue ? new Date(fieldValue) : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { TextDisplay } from '@/ui/content-display/components/TextDisplay';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
|
||||
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
|
||||
@ -33,7 +34,7 @@ export function GenericEditableNumberField() {
|
||||
<EditableField
|
||||
IconLabel={currentEditableFieldDefinition.Icon}
|
||||
editModeContent={<GenericEditableNumberFieldEditMode />}
|
||||
displayModeContent={fieldValue}
|
||||
displayModeContent={<TextDisplay text={fieldValue} />}
|
||||
isDisplayModeContentEmpty={!fieldValue}
|
||||
/>
|
||||
</RecoilScope>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useContext, useRef, useState } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { TextInputEdit } from '@/ui/input/text/components/TextInputEdit';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import {
|
||||
canBeCastAsIntegerOrNull,
|
||||
castAsIntegerOrNull,
|
||||
@ -9,9 +9,10 @@ import {
|
||||
|
||||
import { EditableFieldDefinitionContext } from '../contexts/EditableFieldDefinitionContext';
|
||||
import { EditableFieldEntityIdContext } from '../contexts/EditableFieldEntityIdContext';
|
||||
import { useRegisterCloseFieldHandlers } from '../hooks/useRegisterCloseFieldHandlers';
|
||||
import { useFieldInputEventHandlers } from '../hooks/useFieldInputEventHandlers';
|
||||
import { useUpdateGenericEntityField } from '../hooks/useUpdateGenericEntityField';
|
||||
import { genericEntityFieldFamilySelector } from '../states/selectors/genericEntityFieldFamilySelector';
|
||||
import { EditableFieldHotkeyScope } from '../types/EditableFieldHotkeyScope';
|
||||
import { FieldDefinition } from '../types/FieldDefinition';
|
||||
import { FieldNumberMetadata } from '../types/FieldMetadata';
|
||||
|
||||
@ -30,51 +31,43 @@ export function GenericEditableNumberFieldEditMode() {
|
||||
: '',
|
||||
}),
|
||||
);
|
||||
const [internalValue, setInternalValue] = useState(
|
||||
fieldValue ? fieldValue.toString() : '',
|
||||
);
|
||||
|
||||
const updateField = useUpdateGenericEntityField();
|
||||
|
||||
const wrapperRef = useRef(null);
|
||||
|
||||
useRegisterCloseFieldHandlers(wrapperRef, handleSubmit, onCancel);
|
||||
|
||||
function handleSubmit() {
|
||||
if (!canBeCastAsIntegerOrNull(internalValue)) {
|
||||
function handleSubmit(newValue: string) {
|
||||
if (!canBeCastAsIntegerOrNull(newValue)) {
|
||||
return;
|
||||
}
|
||||
if (internalValue === fieldValue) return;
|
||||
|
||||
setFieldValue(castAsIntegerOrNull(internalValue));
|
||||
if (newValue === fieldValue) return;
|
||||
|
||||
const castedValue = castAsIntegerOrNull(newValue);
|
||||
|
||||
setFieldValue(castedValue);
|
||||
|
||||
if (currentEditableFieldEntityId && updateField) {
|
||||
updateField(
|
||||
currentEditableFieldEntityId,
|
||||
currentEditableFieldDefinition,
|
||||
castAsIntegerOrNull(internalValue),
|
||||
castedValue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
setFieldValue(fieldValue);
|
||||
}
|
||||
|
||||
function handleChange(newValue: string) {
|
||||
setInternalValue(newValue);
|
||||
}
|
||||
const { handleEnter, handleEscape, handleClickOutside } =
|
||||
useFieldInputEventHandlers({
|
||||
onSubmit: handleSubmit,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
<TextInputEdit
|
||||
autoFocus
|
||||
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
|
||||
value={internalValue ? internalValue.toString() : ''}
|
||||
onChange={(newValue: string) => {
|
||||
handleChange(newValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
autoFocus
|
||||
placeholder={currentEditableFieldDefinition.metadata.placeHolder}
|
||||
hotkeyScope={EditableFieldHotkeyScope.EditableField}
|
||||
value={fieldValue ? fieldValue.toString() : ''}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
|
||||
import { EditableField } from '@/ui/editable-field/components/EditableField';
|
||||
import { FieldRecoilScopeContext } from '@/ui/editable-field/states/recoil-scope-contexts/FieldRecoilScopeContext';
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
import { DateInputDisplay } from '@/ui/input/components/DateInputDisplay';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
|
||||
@ -12,9 +12,16 @@ type OwnProps = {
|
||||
label?: string;
|
||||
value: string | null | undefined;
|
||||
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) {
|
||||
onSubmit?.(newValue);
|
||||
}
|
||||
@ -24,8 +31,6 @@ export function DateEditableField({ Icon, value, label, onSubmit }: OwnProps) {
|
||||
return (
|
||||
<RecoilScope SpecificContext={FieldRecoilScopeContext}>
|
||||
<EditableField
|
||||
// onSubmit={handleSubmit}
|
||||
// onCancel={handleCancel}
|
||||
IconLabel={Icon}
|
||||
label={label}
|
||||
editModeContent={
|
||||
@ -34,9 +39,10 @@ export function DateEditableField({ Icon, value, label, onSubmit }: OwnProps) {
|
||||
onChange={(newValue: string) => {
|
||||
handleChange(newValue);
|
||||
}}
|
||||
parentHotkeyScope={hotkeyScope}
|
||||
/>
|
||||
}
|
||||
displayModeContent={<DateInputDisplay value={internalDateValue} />}
|
||||
displayModeContent={<DateDisplay value={internalDateValue} />}
|
||||
isDisplayModeContentEmpty={!value}
|
||||
/>
|
||||
</RecoilScope>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { DateInputEdit } from '@/ui/input/components/DateInputEdit';
|
||||
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
|
||||
import { DateInput } from '@/ui/input/components/DateInput';
|
||||
import { Nullable } from '~/types/Nullable';
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
|
||||
import { useEditableField } from '../../hooks/useEditableField';
|
||||
@ -9,10 +9,15 @@ import { useEditableField } from '../../hooks/useEditableField';
|
||||
type OwnProps = {
|
||||
value: string;
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@ -21,17 +26,26 @@ export function EditableFieldEditModeDate({ value, onChange }: OwnProps) {
|
||||
|
||||
const { closeEditableField } = useEditableField();
|
||||
|
||||
function handleChange(newValue: string) {
|
||||
onChange?.(newValue);
|
||||
function handleClickOutside() {
|
||||
closeEditableField();
|
||||
}
|
||||
|
||||
function handleEnter(newValue: Nullable<Date>) {
|
||||
onChange?.(newValue?.toISOString() ?? '');
|
||||
closeEditableField();
|
||||
}
|
||||
|
||||
function handleEscape() {
|
||||
closeEditableField();
|
||||
}
|
||||
|
||||
return (
|
||||
<DateInputEdit
|
||||
<DateInput
|
||||
value={internalValue ? parseDate(internalValue).toJSDate() : new Date()}
|
||||
onChange={(newDate: Date) => {
|
||||
handleChange(newDate.toISOString());
|
||||
}}
|
||||
hotkeyScope={parentHotkeyScope}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
103
front/src/modules/ui/input/components/DateInput.tsx
Normal file
103
front/src/modules/ui/input/components/DateInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,18 +1,11 @@
|
||||
import React, { forwardRef, ReactElement, useState } from 'react';
|
||||
import ReactDatePicker, { CalendarContainerProps } from 'react-datepicker';
|
||||
import React from 'react';
|
||||
import ReactDatePicker from 'react-datepicker';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { overlayBackground } from '@/ui/theme/constants/effects';
|
||||
|
||||
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`
|
||||
& .react-datepicker {
|
||||
border-color: ${({ theme }) => theme.border.color.light};
|
||||
@ -39,6 +32,10 @@ const StyledContainer = styled.div`
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .react-datepicker-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Header
|
||||
|
||||
& .react-datepicker__header {
|
||||
@ -223,47 +220,32 @@ const StyledContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
function DatePicker({
|
||||
date,
|
||||
onChangeHandler,
|
||||
customInput,
|
||||
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 type DatePickerProps = {
|
||||
date: Date;
|
||||
onMouseSelect?: (date: Date) => void;
|
||||
onChange?: (date: Date) => void;
|
||||
};
|
||||
|
||||
export function DatePicker({ date, onChange, onMouseSelect }: DatePickerProps) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<ReactDatePicker
|
||||
open={true}
|
||||
selected={startDate}
|
||||
selected={date}
|
||||
showMonthDropdown
|
||||
showYearDropdown
|
||||
onChange={(date: Date) => {
|
||||
setStartDate(date);
|
||||
onChangeHandler(date);
|
||||
onChange={() => {
|
||||
// We need to use onSelect here but onChange is almost redundant with onSelect but is required
|
||||
}}
|
||||
customInput={<></>}
|
||||
onSelect={(date: Date, event) => {
|
||||
if (event?.type === 'click') {
|
||||
onMouseSelect?.(date);
|
||||
} else {
|
||||
onChange?.(date);
|
||||
}
|
||||
}}
|
||||
customInput={customInput ? customInput : <DefaultDateDisplay />}
|
||||
calendarContainer={
|
||||
customCalendarContainer ? customCalendarContainer : undefined
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default DatePicker;
|
||||
|
||||
@ -4,14 +4,13 @@ import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import DatePicker from '../DatePicker';
|
||||
import { DatePicker } from '../DatePicker';
|
||||
|
||||
const meta: Meta<typeof DatePicker> = {
|
||||
title: 'UI/Input/DatePicker',
|
||||
component: DatePicker,
|
||||
decorators: [ComponentDecorator],
|
||||
argTypes: {
|
||||
customInput: { control: false },
|
||||
date: { control: 'date' },
|
||||
},
|
||||
args: { date: new Date('January 1, 2023 00:00:00') },
|
||||
|
||||
@ -2,11 +2,11 @@ import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
|
||||
import { EmailInputDisplay } from '../EmailInputDisplay';
|
||||
import { EmailDisplay } from '../../../content-display/components/EmailDisplay';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'UI/Input/EmailInputDisplay',
|
||||
component: EmailInputDisplay,
|
||||
component: EmailDisplay,
|
||||
decorators: [ComponentWithRouterDecorator],
|
||||
args: {
|
||||
value: 'mustajab.ikram@google.com',
|
||||
@ -15,6 +15,6 @@ const meta: Meta = {
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof EmailInputDisplay>;
|
||||
type Story = StoryObj<typeof EmailDisplay>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { DateDisplay } from '@/ui/content-display/components/DateDisplay';
|
||||
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 { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector';
|
||||
@ -34,7 +34,7 @@ export function GenericEditableDateCell({
|
||||
editModeContent={
|
||||
<GenericEditableDateCellEditMode columnDefinition={columnDefinition} />
|
||||
}
|
||||
nonEditModeContent={<DateInputDisplay value={fieldValue} />}
|
||||
nonEditModeContent={<DateDisplay value={fieldValue} />}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,14 +2,16 @@ import { DateTime } from 'luxon';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
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 { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
|
||||
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 { DateCellEdit } from './DateCellEdit';
|
||||
|
||||
type OwnProps = {
|
||||
columnDefinition: ColumnDefinition<ViewFieldDateMetadata>;
|
||||
};
|
||||
@ -29,12 +31,13 @@ export function GenericEditableDateCellEditMode({
|
||||
|
||||
const updateField = useUpdateEntityField();
|
||||
|
||||
function handleSubmit(newDate: Date) {
|
||||
// Wrap this into a hook
|
||||
function handleSubmit(newDate: Nullable<Date>) {
|
||||
const fieldValueDate = fieldValue
|
||||
? DateTime.fromISO(fieldValue).toJSDate()
|
||||
: null;
|
||||
|
||||
const newDateISO = DateTime.fromJSDate(newDate).toISO();
|
||||
const newDateISO = newDate ? DateTime.fromJSDate(newDate).toISO() : null;
|
||||
|
||||
if (newDate === fieldValueDate || !newDateISO) return;
|
||||
|
||||
@ -45,10 +48,18 @@ export function GenericEditableDateCellEditMode({
|
||||
}
|
||||
}
|
||||
|
||||
const { handleEnter, handleEscape, handleClickOutside } =
|
||||
useCellInputEventHandlers({
|
||||
onSubmit: handleSubmit,
|
||||
});
|
||||
|
||||
return (
|
||||
<DateCellEdit
|
||||
<DateInput
|
||||
value={DateTime.fromISO(fieldValue).toJSDate()}
|
||||
onSubmit={handleSubmit}
|
||||
onClickOutside={handleClickOutside}
|
||||
onEnter={handleEnter}
|
||||
onEscape={handleEscape}
|
||||
hotkeyScope={TableHotkeyScope.CellEditMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { EmailDisplay } from '@/ui/content-display/components/EmailDisplay';
|
||||
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 { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
import { tableEntityFieldFamilySelector } from '@/ui/table/states/selectors/tableEntityFieldFamilySelector';
|
||||
@ -34,7 +34,7 @@ export function GenericEditableEmailCell({
|
||||
editModeContent={
|
||||
<GenericEditableEmailCellEditMode columnDefinition={columnDefinition} />
|
||||
}
|
||||
nonEditModeContent={<EmailInputDisplay value={fieldValue} />}
|
||||
nonEditModeContent={<EmailDisplay value={fieldValue} />}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { MoneyDisplay } from '@/ui/content-display/components/MoneyDisplay';
|
||||
import type { ViewFieldMoneyMetadata } from '@/ui/editable-field/types/ViewField';
|
||||
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
@ -14,11 +15,6 @@ type OwnProps = {
|
||||
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({
|
||||
columnDefinition,
|
||||
editModeHorizontalAlign,
|
||||
@ -38,9 +34,7 @@ export function GenericEditableMoneyCell({
|
||||
editModeContent={
|
||||
<GenericEditableMoneyCellEditMode columnDefinition={columnDefinition} />
|
||||
}
|
||||
nonEditModeContent={
|
||||
<>{fieldValue ? `$${formatNumber(fieldValue)}` : ''}</>
|
||||
}
|
||||
nonEditModeContent={<MoneyDisplay value={fieldValue} />}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ export function GenericEditableMoneyCellEditMode({
|
||||
|
||||
const updateField = useUpdateEntityField();
|
||||
|
||||
// TODO: handle this logic in a number input
|
||||
function handleSubmit(newText: string) {
|
||||
if (newText === fieldValue) return;
|
||||
|
||||
@ -64,6 +65,7 @@ export function GenericEditableMoneyCellEditMode({
|
||||
onSubmit: handleSubmit,
|
||||
});
|
||||
|
||||
// TODO: use a number input
|
||||
return (
|
||||
<TextInput
|
||||
autoFocus
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { TextDisplay } from '@/ui/content-display/components/TextDisplay';
|
||||
import type { ViewFieldNumberMetadata } from '@/ui/editable-field/types/ViewField';
|
||||
import { EditableCell } from '@/ui/table/editable-cell/components/EditableCell';
|
||||
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
|
||||
@ -35,7 +36,7 @@ export function GenericEditableNumberCell({
|
||||
columnDefinition={columnDefinition}
|
||||
/>
|
||||
}
|
||||
nonEditModeContent={<>{fieldValue}</>}
|
||||
nonEditModeContent={<TextDisplay text={fieldValue} />}
|
||||
></EditableCell>
|
||||
);
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ const common = {
|
||||
color: grayScale.gray0,
|
||||
},
|
||||
},
|
||||
spacingMultiplicator: 4,
|
||||
spacing: (multiplicator: number) => `${multiplicator * 4}px`,
|
||||
betweenSiblingsGap: `2px`,
|
||||
table: {
|
||||
|
||||
@ -59,6 +59,10 @@ const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
*
|
||||
* @deprecated use ui/dropdown/components/DropdownButton.tsx instead
|
||||
*/
|
||||
function DropdownButton({
|
||||
anchor,
|
||||
label,
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
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 { useUpsertFilter } from '@/ui/view-bar/hooks/useUpsertFilter';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
|
||||
|
||||
import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState';
|
||||
|
||||
export function FilterDropdownDateSearchInput({
|
||||
context,
|
||||
}: {
|
||||
@ -22,6 +24,11 @@ export function FilterDropdownDateSearchInput({
|
||||
context,
|
||||
);
|
||||
|
||||
const [, setIsFilterDropdownUnfolded] = useRecoilScopedState(
|
||||
isFilterDropdownUnfoldedScopedState,
|
||||
DropdownRecoilScopeContext,
|
||||
);
|
||||
|
||||
const upsertFilter = useUpsertFilter(context);
|
||||
|
||||
function handleChange(date: Date) {
|
||||
@ -34,16 +41,15 @@ export function FilterDropdownDateSearchInput({
|
||||
operand: selectedOperandInDropdown,
|
||||
displayValue: date.toLocaleDateString(),
|
||||
});
|
||||
|
||||
setIsFilterDropdownUnfolded(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<DatePicker
|
||||
date={new Date()}
|
||||
onChangeHandler={handleChange}
|
||||
customInput={<></>}
|
||||
customCalendarContainer={styled.div`
|
||||
top: -10px;
|
||||
`}
|
||||
onChange={handleChange}
|
||||
onMouseSelect={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Context, useCallback, useState } from 'react';
|
||||
import { Context, useCallback } from 'react';
|
||||
|
||||
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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
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 { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
|
||||
|
||||
import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState';
|
||||
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
|
||||
|
||||
import DropdownButton from './DropdownButton';
|
||||
@ -39,7 +41,11 @@ export function MultipleFiltersDropdownButton({
|
||||
Icon,
|
||||
label,
|
||||
}: MultipleFiltersDropdownButtonProps) {
|
||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||
const [isFilterDropdownUnfolded, setIsFilterDropdownUnfolded] =
|
||||
useRecoilScopedState(
|
||||
isFilterDropdownUnfoldedScopedState,
|
||||
DropdownRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [
|
||||
isFilterDropdownOperandSelectUnfolded,
|
||||
@ -93,7 +99,7 @@ export function MultipleFiltersDropdownButton({
|
||||
((isPrimaryButton && !isFilterSelected) || !isPrimaryButton)
|
||||
) {
|
||||
setHotkeyScope(hotkeyScope);
|
||||
setIsUnfolded(true);
|
||||
setIsFilterDropdownUnfolded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -101,7 +107,7 @@ export function MultipleFiltersDropdownButton({
|
||||
setHotkeyScope(hotkeyScope);
|
||||
}
|
||||
|
||||
setIsUnfolded(false);
|
||||
setIsFilterDropdownUnfolded(false);
|
||||
resetState();
|
||||
}
|
||||
|
||||
@ -109,7 +115,7 @@ export function MultipleFiltersDropdownButton({
|
||||
<DropdownButton
|
||||
label={label ?? 'Filter'}
|
||||
isActive={isFilterSelected}
|
||||
isUnfolded={isUnfolded}
|
||||
isUnfolded={isFilterDropdownUnfolded}
|
||||
Icon={Icon}
|
||||
onIsUnfoldedChange={handleIsUnfoldedChange}
|
||||
hotkeyScope={hotkeyScope}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Context, useState } from 'react';
|
||||
import { Context } from 'react';
|
||||
import React from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||
import { IconChevronDown } from '@/ui/icon';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
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 { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||
import { filtersScopedState } from '../states/filtersScopedState';
|
||||
import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState';
|
||||
import { getOperandsForFilterType } from '../utils/getOperandsForFilterType';
|
||||
|
||||
import { DropdownMenuContainer } from './DropdownMenuContainer';
|
||||
@ -41,7 +43,11 @@ export function SingleEntityFilterDropdownButton({
|
||||
);
|
||||
const availableFilter = availableFilters[0];
|
||||
|
||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||
const [isFilterDropdownUnfolded, setIsFilterDropdownUnfolded] =
|
||||
useRecoilScopedState(
|
||||
isFilterDropdownUnfoldedScopedState,
|
||||
DropdownRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [filters] = useRecoilScopedState(filtersScopedState, context);
|
||||
|
||||
@ -75,10 +81,10 @@ export function SingleEntityFilterDropdownButton({
|
||||
function handleIsUnfoldedChange(newIsUnfolded: boolean) {
|
||||
if (newIsUnfolded) {
|
||||
setHotkeyScope(hotkeyScope);
|
||||
setIsUnfolded(true);
|
||||
setIsFilterDropdownUnfolded(true);
|
||||
} else {
|
||||
setHotkeyScope(hotkeyScope);
|
||||
setIsUnfolded(false);
|
||||
setIsFilterDropdownUnfolded(false);
|
||||
setFilterDropdownSearchInput('');
|
||||
}
|
||||
}
|
||||
@ -86,8 +92,8 @@ export function SingleEntityFilterDropdownButton({
|
||||
return (
|
||||
<StyledDropdownButtonContainer>
|
||||
<StyledHeaderDropdownButton
|
||||
isUnfolded={isUnfolded}
|
||||
onClick={() => handleIsUnfoldedChange(!isUnfolded)}
|
||||
isUnfolded={isFilterDropdownUnfolded}
|
||||
onClick={() => handleIsUnfoldedChange(!isFilterDropdownUnfolded)}
|
||||
>
|
||||
{filters[0] ? (
|
||||
<GenericEntityFilterChip filter={filters[0]} />
|
||||
@ -96,7 +102,7 @@ export function SingleEntityFilterDropdownButton({
|
||||
)}
|
||||
<IconChevronDown size={theme.icon.size.md} />
|
||||
</StyledHeaderDropdownButton>
|
||||
{isUnfolded && (
|
||||
{isFilterDropdownUnfolded && (
|
||||
<DropdownMenuContainer onClose={() => handleIsUnfoldedChange(false)}>
|
||||
<FilterDropdownEntitySearchInput context={context} />
|
||||
<FilterDropdownEntitySelect context={context} />
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import { atomFamily } from 'recoil';
|
||||
|
||||
export const isFilterDropdownUnfoldedScopedState = atomFamily<boolean, string>({
|
||||
key: 'isFilterDropdownUnfoldedScopedState',
|
||||
default: false,
|
||||
});
|
||||
1
front/src/types/Nullable.ts
Normal file
1
front/src/types/Nullable.ts
Normal file
@ -0,0 +1 @@
|
||||
export type Nullable<T> = T | null | undefined;
|
||||
@ -1,19 +1,29 @@
|
||||
const DEBUG_MODE = false;
|
||||
|
||||
export function canBeCastAsIntegerOrNull(
|
||||
probableNumberOrNull: string | undefined | number | null,
|
||||
): probableNumberOrNull is number | null {
|
||||
if (probableNumberOrNull === undefined) {
|
||||
if (DEBUG_MODE) console.log('probableNumberOrNull === undefined');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof probableNumberOrNull === 'number') {
|
||||
if (DEBUG_MODE) console.log('typeof probableNumberOrNull === "number"');
|
||||
|
||||
return Number.isInteger(probableNumberOrNull);
|
||||
}
|
||||
|
||||
if (probableNumberOrNull === null) {
|
||||
if (DEBUG_MODE) console.log('probableNumberOrNull === null');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (probableNumberOrNull === '') {
|
||||
if (DEBUG_MODE) console.log('probableNumberOrNull === ""');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -21,9 +31,13 @@ export function canBeCastAsIntegerOrNull(
|
||||
const stringAsNumber = +probableNumberOrNull;
|
||||
|
||||
if (isNaN(stringAsNumber)) {
|
||||
if (DEBUG_MODE) console.log('isNaN(stringAsNumber)');
|
||||
|
||||
return false;
|
||||
}
|
||||
if (Number.isInteger(stringAsNumber)) {
|
||||
if (DEBUG_MODE) console.log('Number.isInteger(stringAsNumber)');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
4
front/src/utils/formatNumber.ts
Normal file
4
front/src/utils/formatNumber.ts
Normal 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, ',');
|
||||
}
|
||||
Reference in New Issue
Block a user