Added a clear/reset button in InternalDateInput to reset/unschedule events (#3154)
* Added a clear/reset button in InternalDateInput to reset/unschedule events * Added clearable prop to <InternalDateInput /> and fixed some design mistakes * Removed unnecessary code that was used during debugging * Replaced button with <MenuItem /> component * Fixed null date in ObjectFilterDropdownDateSearchInput * Moved clear context call from DateInput to DateFieldInput * Removed useless props --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -90,6 +90,7 @@ export const ActivityEditor = ({
|
|||||||
objectRecordId: activity.id,
|
objectRecordId: activity.id,
|
||||||
fieldMetadataName: 'dueAt',
|
fieldMetadataName: 'dueAt',
|
||||||
fieldPosition: 0,
|
fieldPosition: 0,
|
||||||
|
clearable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { FieldContextProvider: AssigneeFieldContextProvider } =
|
const { FieldContextProvider: AssigneeFieldContextProvider } =
|
||||||
@ -98,6 +99,7 @@ export const ActivityEditor = ({
|
|||||||
objectRecordId: activity.id,
|
objectRecordId: activity.id,
|
||||||
fieldMetadataName: 'assignee',
|
fieldMetadataName: 'assignee',
|
||||||
fieldPosition: 1,
|
fieldPosition: 1,
|
||||||
|
clearable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateTitle = useCallback(
|
const updateTitle = useCallback(
|
||||||
|
|||||||
@ -91,8 +91,6 @@ export const FieldInput = ({
|
|||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
onEscape={onEscape}
|
onEscape={onEscape}
|
||||||
onClickOutside={onClickOutside}
|
onClickOutside={onClickOutside}
|
||||||
onTab={onTab}
|
|
||||||
onShiftTab={onShiftTab}
|
|
||||||
/>
|
/>
|
||||||
) : isFieldNumber(fieldDefinition) ? (
|
) : isFieldNumber(fieldDefinition) ? (
|
||||||
<NumberFieldInput
|
<NumberFieldInput
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export type GenericFieldContextType = {
|
|||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
isLabelIdentifier: boolean;
|
isLabelIdentifier: boolean;
|
||||||
basePathToShowPage?: string;
|
basePathToShowPage?: string;
|
||||||
|
clearable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FieldContext = createContext<GenericFieldContextType>(
|
export const FieldContext = createContext<GenericFieldContextType>(
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import { assertFieldMetadata } from '../../types/guards/assertFieldMetadata';
|
|||||||
import { isFieldDateTime } from '../../types/guards/isFieldDateTime';
|
import { isFieldDateTime } from '../../types/guards/isFieldDateTime';
|
||||||
|
|
||||||
export const useDateTimeField = () => {
|
export const useDateTimeField = () => {
|
||||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
const { entityId, fieldDefinition, hotkeyScope, clearable } =
|
||||||
|
useContext(FieldContext);
|
||||||
|
|
||||||
assertFieldMetadata('DATE_TIME', isFieldDateTime, fieldDefinition);
|
assertFieldMetadata('DATE_TIME', isFieldDateTime, fieldDefinition);
|
||||||
|
|
||||||
@ -25,5 +26,6 @@ export const useDateTimeField = () => {
|
|||||||
fieldValue,
|
fieldValue,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
hotkeyScope,
|
hotkeyScope,
|
||||||
|
clearable,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,8 +10,6 @@ export type DateFieldInputProps = {
|
|||||||
onClickOutside?: FieldInputEvent;
|
onClickOutside?: FieldInputEvent;
|
||||||
onEnter?: FieldInputEvent;
|
onEnter?: FieldInputEvent;
|
||||||
onEscape?: FieldInputEvent;
|
onEscape?: FieldInputEvent;
|
||||||
onTab?: FieldInputEvent;
|
|
||||||
onShiftTab?: FieldInputEvent;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateFieldInput = ({
|
export const DateFieldInput = ({
|
||||||
@ -19,13 +17,13 @@ export const DateFieldInput = ({
|
|||||||
onEscape,
|
onEscape,
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
}: DateFieldInputProps) => {
|
}: DateFieldInputProps) => {
|
||||||
const { fieldValue, hotkeyScope } = useDateTimeField();
|
const { fieldValue, hotkeyScope, clearable } = useDateTimeField();
|
||||||
|
|
||||||
const persistField = usePersistField();
|
const persistField = usePersistField();
|
||||||
|
|
||||||
const persistDate = (newDate: Nullable<Date>) => {
|
const persistDate = (newDate: Nullable<Date>) => {
|
||||||
if (!newDate) {
|
if (!newDate) {
|
||||||
persistField('');
|
persistField(null);
|
||||||
} else {
|
} else {
|
||||||
const newDateISO = newDate?.toISOString();
|
const newDateISO = newDate?.toISOString();
|
||||||
|
|
||||||
@ -57,6 +55,7 @@ export const DateFieldInput = ({
|
|||||||
onEnter={handleEnter}
|
onEnter={handleEnter}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
value={dateValue}
|
value={dateValue}
|
||||||
|
clearable={clearable}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { isString } from '@sniptt/guards';
|
import { isNull, isString } from '@sniptt/guards';
|
||||||
|
|
||||||
import { FieldDateTimeValue } from '../FieldMetadata';
|
import { FieldDateTimeValue } from '../FieldMetadata';
|
||||||
|
|
||||||
@ -6,4 +6,5 @@ import { FieldDateTimeValue } from '../FieldMetadata';
|
|||||||
export const isFieldDateTimeValue = (
|
export const isFieldDateTimeValue = (
|
||||||
fieldValue: unknown,
|
fieldValue: unknown,
|
||||||
): fieldValue is FieldDateTimeValue =>
|
): fieldValue is FieldDateTimeValue =>
|
||||||
isString(fieldValue) && !isNaN(Date.parse(fieldValue));
|
(isString(fieldValue) && !isNaN(Date.parse(fieldValue))) ||
|
||||||
|
isNull(fieldValue);
|
||||||
|
|||||||
@ -15,11 +15,13 @@ export const useFieldContext = ({
|
|||||||
fieldMetadataName,
|
fieldMetadataName,
|
||||||
objectRecordId,
|
objectRecordId,
|
||||||
fieldPosition,
|
fieldPosition,
|
||||||
|
clearable,
|
||||||
}: {
|
}: {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
objectRecordId: string;
|
objectRecordId: string;
|
||||||
fieldMetadataName: string;
|
fieldMetadataName: string;
|
||||||
fieldPosition: number;
|
fieldPosition: number;
|
||||||
|
clearable?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
@ -60,6 +62,7 @@ export const useFieldContext = ({
|
|||||||
}),
|
}),
|
||||||
useUpdateRecord: useUpdateOneObjectMutation,
|
useUpdateRecord: useUpdateOneObjectMutation,
|
||||||
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
hotkeyScope: InlineCellHotkeyScope.InlineCell,
|
||||||
|
clearable,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||||
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const ObjectFilterDropdownDateSearchInput = () => {
|
export const ObjectFilterDropdownDateSearchInput = () => {
|
||||||
const {
|
const {
|
||||||
@ -9,14 +10,14 @@ export const ObjectFilterDropdownDateSearchInput = () => {
|
|||||||
selectFilter,
|
selectFilter,
|
||||||
} = useFilterDropdown();
|
} = useFilterDropdown();
|
||||||
|
|
||||||
const handleChange = (date: Date) => {
|
const handleChange = (date: Date | null) => {
|
||||||
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
|
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
|
||||||
|
|
||||||
selectFilter?.({
|
selectFilter?.({
|
||||||
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId,
|
||||||
value: date.toISOString(),
|
value: isDefined(date) ? date.toISOString() : '',
|
||||||
operand: selectedOperandInDropdown,
|
operand: selectedOperandInDropdown,
|
||||||
displayValue: date.toLocaleDateString(),
|
displayValue: isDefined(date) ? date.toLocaleString() : '',
|
||||||
definition: filterDefinitionUsedInDropdown,
|
definition: filterDefinitionUsedInDropdown,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export {
|
|||||||
IconBuildingSkyscraper,
|
IconBuildingSkyscraper,
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconCalendarEvent,
|
IconCalendarEvent,
|
||||||
|
IconCalendarX,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconCheckbox,
|
IconCheckbox,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export type DateInputProps = {
|
|||||||
newDate: Nullable<Date>,
|
newDate: Nullable<Date>,
|
||||||
) => void;
|
) => void;
|
||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
|
clearable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateInput = ({
|
export const DateInput = ({
|
||||||
@ -45,6 +46,7 @@ export const DateInput = ({
|
|||||||
onEnter,
|
onEnter,
|
||||||
onEscape,
|
onEscape,
|
||||||
onClickOutside,
|
onClickOutside,
|
||||||
|
clearable,
|
||||||
}: DateInputProps) => {
|
}: DateInputProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -91,9 +93,10 @@ export const DateInput = ({
|
|||||||
<InternalDatePicker
|
<InternalDatePicker
|
||||||
date={internalValue ?? new Date()}
|
date={internalValue ?? new Date()}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onMouseSelect={(newDate: Date) => {
|
onMouseSelect={(newDate: Date | null) => {
|
||||||
onEnter(newDate);
|
onEnter(newDate);
|
||||||
}}
|
}}
|
||||||
|
clearable={clearable ? clearable : false}
|
||||||
/>
|
/>
|
||||||
</StyledCalendarContainer>
|
</StyledCalendarContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import React from 'react';
|
|||||||
import ReactDatePicker from 'react-datepicker';
|
import ReactDatePicker from 'react-datepicker';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
import { IconCalendarX } from '@/ui/display/icon';
|
||||||
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
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';
|
||||||
@ -218,36 +220,76 @@ const StyledContainer = styled.div`
|
|||||||
& .react-datepicker__day:hover {
|
& .react-datepicker__day:hover {
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .clearable {
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledButtonContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||||
|
|
||||||
|
& .menu-item {
|
||||||
|
height: ${({ theme }) => theme.spacing(8)};
|
||||||
|
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export type InternalDatePickerProps = {
|
export type InternalDatePickerProps = {
|
||||||
date: Date;
|
date: Date | null;
|
||||||
onMouseSelect?: (date: Date) => void;
|
onMouseSelect?: (date: Date | null) => void;
|
||||||
onChange?: (date: Date) => void;
|
onChange?: (date: Date) => void;
|
||||||
|
clearable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InternalDatePicker = ({
|
export const InternalDatePicker = ({
|
||||||
date,
|
date,
|
||||||
onChange,
|
onChange,
|
||||||
onMouseSelect,
|
onMouseSelect,
|
||||||
}: InternalDatePickerProps) => (
|
clearable = true,
|
||||||
<StyledContainer>
|
}: InternalDatePickerProps) => {
|
||||||
<ReactDatePicker
|
const handleClear = () => {
|
||||||
open={true}
|
onMouseSelect?.(null);
|
||||||
selected={date}
|
};
|
||||||
showMonthDropdown
|
|
||||||
showYearDropdown
|
return (
|
||||||
onChange={() => {
|
<StyledContainer>
|
||||||
// We need to use onSelect here but onChange is almost redundant with onSelect but is required
|
<div className={clearable ? 'clearable ' : ''}>
|
||||||
}}
|
<ReactDatePicker
|
||||||
customInput={<></>}
|
open={true}
|
||||||
onSelect={(date: Date, event) => {
|
selected={date}
|
||||||
if (event?.type === 'click') {
|
showMonthDropdown
|
||||||
onMouseSelect?.(date);
|
showYearDropdown
|
||||||
} else {
|
onChange={() => {
|
||||||
onChange?.(date);
|
// We need to use onSelect here but onChange is almost redundant with onSelect but is require
|
||||||
}
|
}}
|
||||||
}}
|
customInput={<></>}
|
||||||
/>
|
onSelect={(date: Date, event) => {
|
||||||
</StyledContainer>
|
if (event?.type === 'click') {
|
||||||
);
|
onMouseSelect?.(date);
|
||||||
|
} else {
|
||||||
|
onChange?.(date);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></ReactDatePicker>
|
||||||
|
</div>
|
||||||
|
{clearable && (
|
||||||
|
<StyledButtonContainer>
|
||||||
|
<MenuItem
|
||||||
|
text="Clear"
|
||||||
|
LeftIcon={IconCalendarX}
|
||||||
|
onClick={handleClear}
|
||||||
|
className="menu-item"
|
||||||
|
/>
|
||||||
|
</StyledButtonContainer>
|
||||||
|
)}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user