@ -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} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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>;
|
||||||
}
|
}
|
||||||
@ -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}`}
|
||||||
@ -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 { 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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 { 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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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 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;
|
|
||||||
|
|||||||
@ -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') },
|
||||||
|
|||||||
@ -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 = {};
|
||||||
|
|||||||
@ -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 { 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
|
||||||
`}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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(
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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