Refactor/display input part 2 (#1555)

* Email - Money - Number

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

View File

@ -8,6 +8,7 @@ import { ActivityComments } from '@/activities/components/ActivityComments';
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { 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} />
</>

View File

@ -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>;
}

View File

@ -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}`}

View File

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

View File

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

View File

@ -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>

View File

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

View File

@ -1,13 +1,17 @@
import { useContext } from 'react';
import { 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}
/>
);
}

View File

@ -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>

View File

@ -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}
/>
);
}

View File

@ -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>

View File

@ -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}
/>
);
}

View File

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

View File

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

View File

@ -1,18 +1,11 @@
import React, { forwardRef, ReactElement, useState } from 'react';
import 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;

View File

@ -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') },

View File

@ -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 = {};

View File

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

View File

@ -1,7 +1,7 @@
import { useRecoilValue } from 'recoil';
import { 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>
);
}

View File

@ -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}
/>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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

View File

@ -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>
);
}

View File

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

View File

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

View File

@ -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}
/>
);
}

View File

@ -1,6 +1,7 @@
import { Context, useCallback, useState } from 'react';
import { Context, useCallback } from 'react';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { 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}

View File

@ -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} />

View File

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

View File

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

View File

@ -1,19 +1,29 @@
const DEBUG_MODE = false;
export function canBeCastAsIntegerOrNull(
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;
}
}

View File

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