diff --git a/front/src/modules/activities/components/ActivityEditor.tsx b/front/src/modules/activities/components/ActivityEditor.tsx index 326233c72..ffc17e2b6 100644 --- a/front/src/modules/activities/components/ActivityEditor.tsx +++ b/front/src/modules/activities/components/ActivityEditor.tsx @@ -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} /> diff --git a/front/src/modules/ui/input/components/DateInputDisplay.tsx b/front/src/modules/ui/content-display/components/DateDisplay.tsx similarity index 60% rename from front/src/modules/ui/input/components/DateInputDisplay.tsx rename to front/src/modules/ui/content-display/components/DateDisplay.tsx index 0f200a439..fb83ccdf2 100644 --- a/front/src/modules/ui/input/components/DateInputDisplay.tsx +++ b/front/src/modules/ui/content-display/components/DateDisplay.tsx @@ -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
{value && formatToHumanReadableDate(value)}
; } diff --git a/front/src/modules/ui/input/components/EmailInputDisplay.tsx b/front/src/modules/ui/content-display/components/EmailDisplay.tsx similarity index 90% rename from front/src/modules/ui/input/components/EmailInputDisplay.tsx rename to front/src/modules/ui/content-display/components/EmailDisplay.tsx index be508ca1a..dbf7b455a 100644 --- a/front/src/modules/ui/input/components/EmailInputDisplay.tsx +++ b/front/src/modules/ui/content-display/components/EmailDisplay.tsx @@ -11,7 +11,7 @@ type OwnProps = { value: string | null; }; -export function EmailInputDisplay({ value }: OwnProps) { +export function EmailDisplay({ value }: OwnProps) { return value && validateEmail(value) ? ( + {value ? `$${formatNumber(value)}` : ''} + + ); +} diff --git a/front/src/modules/ui/dropdown/hooks/useDropdownButton.ts b/front/src/modules/ui/dropdown/hooks/useDropdownButton.ts index c921c3dbd..cecb886f5 100644 --- a/front/src/modules/ui/dropdown/hooks/useDropdownButton.ts +++ b/front/src/modules/ui/dropdown/hooks/useDropdownButton.ts @@ -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, diff --git a/front/src/modules/ui/editable-field/components/GenericEditableDateField.tsx b/front/src/modules/ui/editable-field/components/GenericEditableDateField.tsx index 727a9faf0..460db14e6 100644 --- a/front/src/modules/ui/editable-field/components/GenericEditableDateField.tsx +++ b/front/src/modules/ui/editable-field/components/GenericEditableDateField.tsx @@ -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() { } - displayModeContent={} + displayModeContent={} isDisplayModeContentEmpty={!fieldValue} /> diff --git a/front/src/modules/ui/editable-field/components/GenericEditableDateFieldDisplayMode.tsx b/front/src/modules/ui/editable-field/components/GenericEditableDateFieldDisplayMode.tsx deleted file mode 100644 index 1e1cb3f96..000000000 --- a/front/src/modules/ui/editable-field/components/GenericEditableDateFieldDisplayMode.tsx +++ /dev/null @@ -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; - - const fieldValue = useRecoilValue( - genericEntityFieldFamilySelector({ - entityId: currentEditableFieldEntityId ?? '', - fieldName: currentEditableFieldDefinition - ? currentEditableFieldDefinition.metadata.fieldName - : '', - }), - ); - - const internalDateValue = fieldValue - ? parseDate(fieldValue).toJSDate() - : null; - - return ; -} diff --git a/front/src/modules/ui/editable-field/components/GenericEditableDateFieldEditMode.tsx b/front/src/modules/ui/editable-field/components/GenericEditableDateFieldEditMode.tsx index 65dd1b38f..886d05892 100644 --- a/front/src/modules/ui/editable-field/components/GenericEditableDateFieldEditMode.tsx +++ b/front/src/modules/ui/editable-field/components/GenericEditableDateFieldEditMode.tsx @@ -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) { + 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 ( - + ); } diff --git a/front/src/modules/ui/editable-field/components/GenericEditableNumberField.tsx b/front/src/modules/ui/editable-field/components/GenericEditableNumberField.tsx index 4ac725cf2..f74cfdf05 100644 --- a/front/src/modules/ui/editable-field/components/GenericEditableNumberField.tsx +++ b/front/src/modules/ui/editable-field/components/GenericEditableNumberField.tsx @@ -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() { } - displayModeContent={fieldValue} + displayModeContent={} isDisplayModeContentEmpty={!fieldValue} /> diff --git a/front/src/modules/ui/editable-field/components/GenericEditableNumberFieldEditMode.tsx b/front/src/modules/ui/editable-field/components/GenericEditableNumberFieldEditMode.tsx index 44f6079be..fd97324f3 100644 --- a/front/src/modules/ui/editable-field/components/GenericEditableNumberFieldEditMode.tsx +++ b/front/src/modules/ui/editable-field/components/GenericEditableNumberFieldEditMode.tsx @@ -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 ( -
- { - handleChange(newValue); - }} - /> -
+ ); } diff --git a/front/src/modules/ui/editable-field/variants/components/DateEditableField.tsx b/front/src/modules/ui/editable-field/variants/components/DateEditableField.tsx index 8842c9a32..fbbf94362 100644 --- a/front/src/modules/ui/editable-field/variants/components/DateEditableField.tsx +++ b/front/src/modules/ui/editable-field/variants/components/DateEditableField.tsx @@ -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 ( { handleChange(newValue); }} + parentHotkeyScope={hotkeyScope} /> } - displayModeContent={} + displayModeContent={} isDisplayModeContentEmpty={!value} /> diff --git a/front/src/modules/ui/editable-field/variants/components/EditableFieldEditModeDate.tsx b/front/src/modules/ui/editable-field/variants/components/EditableFieldEditModeDate.tsx index ade7fa343..ff396720e 100644 --- a/front/src/modules/ui/editable-field/variants/components/EditableFieldEditModeDate.tsx +++ b/front/src/modules/ui/editable-field/variants/components/EditableFieldEditModeDate.tsx @@ -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) { + onChange?.(newValue?.toISOString() ?? ''); + closeEditableField(); + } + + function handleEscape() { closeEditableField(); } return ( - { - handleChange(newDate.toISOString()); - }} + hotkeyScope={parentHotkeyScope} + onClickOutside={handleClickOutside} + onEnter={handleEnter} + onEscape={handleEscape} /> ); } diff --git a/front/src/modules/ui/input/components/DateInput.tsx b/front/src/modules/ui/input/components/DateInput.tsx new file mode 100644 index 000000000..a9f68e8e0 --- /dev/null +++ b/front/src/modules/ui/input/components/DateInput.tsx @@ -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; + onEnter: (newDate: Nullable) => void; + onEscape: (newDate: Nullable) => void; + onClickOutside: ( + event: MouseEvent | TouchEvent, + newDate: Nullable, + ) => 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 ( +
+
+ + + +
+
+ + { + onEnter(newDate); + }} + /> + +
+
+ ); +} diff --git a/front/src/modules/ui/input/components/DateInputEdit.tsx b/front/src/modules/ui/input/components/DateInputEdit.tsx deleted file mode 100644 index dacf8eb3e..000000000 --- a/front/src/modules/ui/input/components/DateInputEdit.tsx +++ /dev/null @@ -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` - 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; - -const DateDisplay = forwardRef( - ({ value, onClick }, ref) => ( - - {value && formatToHumanReadableDate(new Date(value as string))} - - ), -); - -type DatePickerContainerProps = { - children: React.ReactNode; -}; - -const DatePickerContainer = ({ children }: DatePickerContainerProps) => { - return {children}; -}; - -export type DateInputEditProps = { - value: Date | null | undefined; - onChange: (newDate: Date) => void; -}; - -export function DateInputEdit({ onChange, value }: DateInputEditProps) { - return ( - } - customCalendarContainer={DatePickerContainer} - /> - ); -} diff --git a/front/src/modules/ui/input/components/DatePicker.tsx b/front/src/modules/ui/input/components/DatePicker.tsx index 14e826e40..5683555ad 100644 --- a/front/src/modules/ui/input/components/DatePicker.tsx +++ b/front/src/modules/ui/input/components/DatePicker.tsx @@ -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; - - const DefaultDateDisplay = forwardRef( - ({ value, onClick }, ref) => ( -
- {value && - new Intl.DateTimeFormat(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric', - }).format(new Date(value as string))} -
- ), - ); +export type DatePickerProps = { + date: Date; + onMouseSelect?: (date: Date) => void; + onChange?: (date: Date) => void; +}; +export function DatePicker({ date, onChange, onMouseSelect }: DatePickerProps) { return ( { - 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 : } - calendarContainer={ - customCalendarContainer ? customCalendarContainer : undefined - } /> ); } - -export default DatePicker; diff --git a/front/src/modules/ui/input/components/__stories__/DatePicker.stories.tsx b/front/src/modules/ui/input/components/__stories__/DatePicker.stories.tsx index 83d506dd9..3beed7fd6 100644 --- a/front/src/modules/ui/input/components/__stories__/DatePicker.stories.tsx +++ b/front/src/modules/ui/input/components/__stories__/DatePicker.stories.tsx @@ -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 = { 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') }, diff --git a/front/src/modules/ui/input/components/__stories__/EmailInputDisplay.stories.tsx b/front/src/modules/ui/input/components/__stories__/EmailInputDisplay.stories.tsx index a84153d2f..fe2e26339 100644 --- a/front/src/modules/ui/input/components/__stories__/EmailInputDisplay.stories.tsx +++ b/front/src/modules/ui/input/components/__stories__/EmailInputDisplay.stories.tsx @@ -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; +type Story = StoryObj; export const Default: Story = {}; diff --git a/front/src/modules/ui/table/editable-cell/type/components/DateCellEdit.tsx b/front/src/modules/ui/table/editable-cell/type/components/DateCellEdit.tsx deleted file mode 100644 index 80d43e16b..000000000 --- a/front/src/modules/ui/table/editable-cell/type/components/DateCellEdit.tsx +++ /dev/null @@ -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 ( - - - - ); -} diff --git a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableDateCell.tsx b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableDateCell.tsx index 33dc1ee51..b30be9d39 100644 --- a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableDateCell.tsx +++ b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableDateCell.tsx @@ -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={ } - nonEditModeContent={} + nonEditModeContent={} > ); } diff --git a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableDateCellEditMode.tsx b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableDateCellEditMode.tsx index c914da6a0..f5cc5b089 100644 --- a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableDateCellEditMode.tsx +++ b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableDateCellEditMode.tsx @@ -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; }; @@ -29,12 +31,13 @@ export function GenericEditableDateCellEditMode({ const updateField = useUpdateEntityField(); - function handleSubmit(newDate: Date) { + // Wrap this into a hook + function handleSubmit(newDate: Nullable) { 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 ( - ); } diff --git a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableEmailCell.tsx b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableEmailCell.tsx index 7d2ce4541..2a9dc5ff0 100644 --- a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableEmailCell.tsx +++ b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableEmailCell.tsx @@ -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={ } - nonEditModeContent={} + nonEditModeContent={} > ); } diff --git a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableMoneyCell.tsx b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableMoneyCell.tsx index cc9c4c1c6..06cf57c5b 100644 --- a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableMoneyCell.tsx +++ b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableMoneyCell.tsx @@ -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={ } - nonEditModeContent={ - <>{fieldValue ? `$${formatNumber(fieldValue)}` : ''} - } + nonEditModeContent={} > ); } diff --git a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableMoneyCellEditMode.tsx b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableMoneyCellEditMode.tsx index e523bcbfe..c24b134e6 100644 --- a/front/src/modules/ui/table/editable-cell/type/components/GenericEditableMoneyCellEditMode.tsx +++ b/front/src/modules/ui/table/editable-cell/type/components/GenericEditableMoneyCellEditMode.tsx @@ -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 ( } - nonEditModeContent={<>{fieldValue}} + nonEditModeContent={} > ); } diff --git a/front/src/modules/ui/theme/constants/theme.ts b/front/src/modules/ui/theme/constants/theme.ts index 4867e524c..2b67f1e20 100644 --- a/front/src/modules/ui/theme/constants/theme.ts +++ b/front/src/modules/ui/theme/constants/theme.ts @@ -33,6 +33,7 @@ const common = { color: grayScale.gray0, }, }, + spacingMultiplicator: 4, spacing: (multiplicator: number) => `${multiplicator * 4}px`, betweenSiblingsGap: `2px`, table: { diff --git a/front/src/modules/ui/view-bar/components/DropdownButton.tsx b/front/src/modules/ui/view-bar/components/DropdownButton.tsx index 102df877f..5ede64927 100644 --- a/front/src/modules/ui/view-bar/components/DropdownButton.tsx +++ b/front/src/modules/ui/view-bar/components/DropdownButton.tsx @@ -59,6 +59,10 @@ const StyledDropdownButton = styled.div` } `; +/** + * + * @deprecated use ui/dropdown/components/DropdownButton.tsx instead + */ function DropdownButton({ anchor, label, diff --git a/front/src/modules/ui/view-bar/components/FilterDropdownDateSearchInput.tsx b/front/src/modules/ui/view-bar/components/FilterDropdownDateSearchInput.tsx index 88682ffd2..a023560a1 100644 --- a/front/src/modules/ui/view-bar/components/FilterDropdownDateSearchInput.tsx +++ b/front/src/modules/ui/view-bar/components/FilterDropdownDateSearchInput.tsx @@ -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 ( } - customCalendarContainer={styled.div` - top: -10px; - `} + onChange={handleChange} + onMouseSelect={handleChange} /> ); } diff --git a/front/src/modules/ui/view-bar/components/MultipleFiltersDropdownButton.tsx b/front/src/modules/ui/view-bar/components/MultipleFiltersDropdownButton.tsx index f66b1bafc..e1d1ff34f 100644 --- a/front/src/modules/ui/view-bar/components/MultipleFiltersDropdownButton.tsx +++ b/front/src/modules/ui/view-bar/components/MultipleFiltersDropdownButton.tsx @@ -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({ handleIsUnfoldedChange(!isUnfolded)} + isUnfolded={isFilterDropdownUnfolded} + onClick={() => handleIsUnfoldedChange(!isFilterDropdownUnfolded)} > {filters[0] ? ( @@ -96,7 +102,7 @@ export function SingleEntityFilterDropdownButton({ )} - {isUnfolded && ( + {isFilterDropdownUnfolded && ( handleIsUnfoldedChange(false)}> diff --git a/front/src/modules/ui/view-bar/states/isFilterDropdownUnfoldedScopedState.ts b/front/src/modules/ui/view-bar/states/isFilterDropdownUnfoldedScopedState.ts new file mode 100644 index 000000000..477cad02e --- /dev/null +++ b/front/src/modules/ui/view-bar/states/isFilterDropdownUnfoldedScopedState.ts @@ -0,0 +1,6 @@ +import { atomFamily } from 'recoil'; + +export const isFilterDropdownUnfoldedScopedState = atomFamily({ + key: 'isFilterDropdownUnfoldedScopedState', + default: false, +}); diff --git a/front/src/types/Nullable.ts b/front/src/types/Nullable.ts new file mode 100644 index 000000000..164a7056d --- /dev/null +++ b/front/src/types/Nullable.ts @@ -0,0 +1 @@ +export type Nullable = T | null | undefined; diff --git a/front/src/utils/cast-as-integer-or-null.ts b/front/src/utils/cast-as-integer-or-null.ts index ee706f257..0f95db43b 100644 --- a/front/src/utils/cast-as-integer-or-null.ts +++ b/front/src/utils/cast-as-integer-or-null.ts @@ -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; } } diff --git a/front/src/utils/formatNumber.ts b/front/src/utils/formatNumber.ts new file mode 100644 index 000000000..dc318a437 --- /dev/null +++ b/front/src/utils/formatNumber.ts @@ -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, ','); +}