Skip to content
Snippets Groups Projects
Unverified Commit 89927882 authored by Anton Kulyk's avatar Anton Kulyk Committed by GitHub
Browse files

Add anchor time setting to Admin > Caching (#22839)

* Update `SegmentedControl` variant style getters

* Add `fill-all` variant to `SegmentedControl`

* Fix `SegmentedControl` borders

* Make `TimeInput's` clear button optional

* Extract `useTimeInput` hook

* Add `CompactTimeInput`

* Add anchor time setting

* Only show anchor time if refresh interval >= 6 hours

* Show refresh time hints under the anchor input
parent b63b0d28
No related branches found
No related tags found
No related merge requests found
Showing
with 528 additions and 102 deletions
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
export const RelativeTimeLabel = styled.span`
display: block;
color: ${color("text-medium")};
font-size: 0.875rem;
font-weight: 400;
margin-top: 1.5rem;
`;
import React, { useCallback, useMemo } from "react";
import _ from "underscore";
import moment, { Moment } from "moment";
import { t } from "ttag";
import TimeInput from "metabase/core/components/TimeInput";
import { has24HourModeSetting } from "metabase/lib/time";
import { formatTime, getNextExpectedRefreshTime } from "./utils";
import { RelativeTimeLabel } from "./PersistedModelAnchorTimeWidget.styled";
const DEFAULT_REFRESH_INTERVAL = 6;
type Props = {
setting: {
key: string;
value: string | null;
default: string;
};
settingValues: {
"persisted-model-refresh-interval-hours": number | null;
};
onChange: (anchor: string) => void;
};
function PersistedModelAnchorTimeWidget({
setting,
onChange,
settingValues,
}: Props) {
const anchor = setting.value || setting.default;
const [hours, minutes] = anchor.split(":").map(value => parseInt(value, 10));
const value = moment({ hours, minutes });
const onChangeDebounced = useMemo(() => _.debounce(onChange, 300), [
onChange,
]);
const handleChange = useCallback(
(value: Moment) => {
const hours = value.hours();
const minutes = value.minutes();
const nextAnchor = `${formatTime(hours)}:${formatTime(minutes)}`;
onChangeDebounced(nextAnchor);
},
[onChangeDebounced],
);
const nextRefreshDates = useMemo(() => {
const refreshInterval =
settingValues["persisted-model-refresh-interval-hours"] ||
DEFAULT_REFRESH_INTERVAL;
const firstRefresh = getNextExpectedRefreshTime(
moment(),
refreshInterval,
anchor,
);
const secondRefresh = getNextExpectedRefreshTime(
firstRefresh,
refreshInterval,
anchor,
);
const thirdRefresh = getNextExpectedRefreshTime(
secondRefresh,
refreshInterval,
anchor,
);
return [firstRefresh, secondRefresh, thirdRefresh];
}, [anchor, settingValues]);
const renderRefreshTimeHintText = useCallback(() => {
const [first, second, third] = nextRefreshDates.map(time =>
time.fromNow(true),
);
return (
<RelativeTimeLabel>{t`The next three refresh jobs will run in ${first}, ${second}, and ${third}.`}</RelativeTimeLabel>
);
}, [nextRefreshDates]);
return (
<div>
<TimeInput.Compact
value={value}
is24HourMode={has24HourModeSetting()}
onChange={handleChange}
/>
{renderRefreshTimeHintText()}
</div>
);
}
export default PersistedModelAnchorTimeWidget;
export { default } from "./PersistedModelAnchorTimeWidget";
import { Moment } from "moment";
export function formatTime(value: number) {
return value < 10 ? `0${value}` : value;
}
export function getNextExpectedRefreshTime(
fromTime: Moment,
refreshInterval: number,
anchorTime: string,
) {
const nextRefresh = fromTime.clone();
nextRefresh.add(refreshInterval, "hours");
const isNextDay = nextRefresh.date() !== fromTime.date();
if (isNextDay) {
const [hours, minutes] = anchorTime
.split(":")
.map(number => parseInt(number, 10));
nextRefresh.hours(hours);
nextRefresh.minutes(minutes);
}
return nextRefresh;
}
import moment, { Moment } from "moment";
import { getNextExpectedRefreshTime } from "./utils";
describe("getNextExpectedRefreshTime", () => {
it("should jump forward by refresh interval (hours)", () => {
const fromTime = moment({ hours: 12, minutes: 0 });
const result = getNextExpectedRefreshTime(fromTime, 8, "10:00");
expect(result.hours()).toBe(20);
expect(result.minutes()).toBe(0);
});
it("should respect minutes", () => {
const fromTime = moment({ hours: 12, minutes: 15 });
const result = getNextExpectedRefreshTime(fromTime, 6, "10:00");
expect(result.hours()).toBe(18);
expect(result.minutes()).toBe(15);
});
it("should stick to anchor time for refreshes tomorrow", () => {
const fromTime = moment({ hours: 19, minutes: 0 });
const result = getNextExpectedRefreshTime(fromTime, 6, "10:00");
expect(result.hours()).toBe(10);
expect(result.minutes()).toBe(0);
});
});
...@@ -20,6 +20,7 @@ import SecretKeyWidget from "./components/widgets/SecretKeyWidget"; ...@@ -20,6 +20,7 @@ import SecretKeyWidget from "./components/widgets/SecretKeyWidget";
import EmbeddingLegalese from "./components/widgets/EmbeddingLegalese"; import EmbeddingLegalese from "./components/widgets/EmbeddingLegalese";
import FormattingWidget from "./components/widgets/FormattingWidget"; import FormattingWidget from "./components/widgets/FormattingWidget";
import { PremiumEmbeddingLinkWidget } from "./components/widgets/PremiumEmbeddingLinkWidget"; import { PremiumEmbeddingLinkWidget } from "./components/widgets/PremiumEmbeddingLinkWidget";
import PersistedModelAnchorTimeWidget from "./components/widgets/PersistedModelAnchorTimeWidget";
import PersistedModelRefreshIntervalWidget from "./components/widgets/PersistedModelRefreshIntervalWidget"; import PersistedModelRefreshIntervalWidget from "./components/widgets/PersistedModelRefreshIntervalWidget";
import SectionDivider from "./components/widgets/SectionDivider"; import SectionDivider from "./components/widgets/SectionDivider";
import SettingsUpdatesForm from "./components/SettingsUpdatesForm/SettingsUpdatesForm"; import SettingsUpdatesForm from "./components/SettingsUpdatesForm/SettingsUpdatesForm";
...@@ -54,6 +55,8 @@ function updateSectionsWithPlugins(sections) { ...@@ -54,6 +55,8 @@ function updateSectionsWithPlugins(sections) {
} }
} }
const CACHING_MIN_REFRESH_HOURS_FOR_ANCHOR_TIME_SETTING = 6;
const SECTIONS = updateSectionsWithPlugins({ const SECTIONS = updateSectionsWithPlugins({
setup: { setup: {
name: t`Setup`, name: t`Setup`,
...@@ -449,8 +452,28 @@ const SECTIONS = updateSectionsWithPlugins({ ...@@ -449,8 +452,28 @@ const SECTIONS = updateSectionsWithPlugins({
disableDefaultUpdate: true, disableDefaultUpdate: true,
widget: PersistedModelRefreshIntervalWidget, widget: PersistedModelRefreshIntervalWidget,
getHidden: settings => !settings["persisted-models-enabled"], getHidden: settings => !settings["persisted-models-enabled"],
onChanged: async (oldValue, newValue) => onChanged: (oldHours, hours) =>
PersistedModelsApi.setRefreshInterval({ hours: newValue }), PersistedModelsApi.setRefreshInterval({ hours }),
},
{
key: "persisted-model-refresh-anchor-time",
display_name: t`Anchoring time`,
disableDefaultUpdate: true,
widget: PersistedModelAnchorTimeWidget,
getHidden: settings => {
if (!settings["persisted-models-enabled"]) {
return true;
}
const DEFAULT_REFRESH_INTERVAL = 6;
const refreshInterval =
settings["persisted-model-refresh-interval-hours"] ||
DEFAULT_REFRESH_INTERVAL;
return (
refreshInterval < CACHING_MIN_REFRESH_HOURS_FOR_ANCHOR_TIME_SETTING
);
},
onChanged: (oldAnchor, anchor) =>
PersistedModelsApi.setRefreshInterval({ anchor }),
}, },
], ],
}, },
......
...@@ -57,6 +57,7 @@ export const examples = { ...@@ -57,6 +57,7 @@ export const examples = {
options={SIMPLE_OPTIONS} options={SIMPLE_OPTIONS}
variant="fill-background" variant="fill-background"
/> />
<SegmentedControlDemo options={SIMPLE_OPTIONS} variant="fill-all" />
</React.Fragment> </React.Fragment>
), ),
icons: <SegmentedControlDemo options={OPTIONS_WITH_ICONS} />, icons: <SegmentedControlDemo options={OPTIONS_WITH_ICONS} />,
......
...@@ -25,7 +25,7 @@ const propTypes = { ...@@ -25,7 +25,7 @@ const propTypes = {
name: PropTypes.string, name: PropTypes.string,
value: PropTypes.any, value: PropTypes.any,
options: PropTypes.arrayOf(optionShape).isRequired, options: PropTypes.arrayOf(optionShape).isRequired,
variant: PropTypes.oneOf(["fill-text", "fill-background"]), variant: PropTypes.oneOf(["fill-text", "fill-background", "fill-all"]),
inactiveColor: PropTypes.string, inactiveColor: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
fullWidth: PropTypes.bool, fullWidth: PropTypes.bool,
...@@ -45,12 +45,13 @@ export function SegmentedControl({ ...@@ -45,12 +45,13 @@ export function SegmentedControl({
}) { }) {
const id = useMemo(() => _.uniqueId("radio-"), []); const id = useMemo(() => _.uniqueId("radio-"), []);
const name = nameFromProps || id; const name = nameFromProps || id;
const selectedOptionIndex = options.findIndex(
option => option.value === value,
);
return ( return (
<SegmentedList {...props} fullWidth={fullWidth}> <SegmentedList {...props} fullWidth={fullWidth}>
{options.map((option, index) => { {options.map((option, index) => {
const isSelected = option.value === value; const isSelected = index === selectedOptionIndex;
const isFirst = index === 0;
const isLast = index === options.length - 1;
const id = `${name}-${option.value}`; const id = `${name}-${option.value}`;
const labelId = `${name}-${option.value}`; const labelId = `${name}-${option.value}`;
const iconOnly = !option.name; const iconOnly = !option.name;
...@@ -59,8 +60,9 @@ export function SegmentedControl({ ...@@ -59,8 +60,9 @@ export function SegmentedControl({
<SegmentedItem <SegmentedItem
key={option.value} key={option.value}
isSelected={isSelected} isSelected={isSelected}
isFirst={isFirst} index={index}
isLast={isLast} total={options.length}
selectedOptionIndex={selectedOptionIndex}
fullWidth={fullWidth} fullWidth={fullWidth}
variant={variant} variant={variant}
selectedColor={selectedColor} selectedColor={selectedColor}
......
import React from "react"; import React from "react";
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { css } from "@emotion/react";
import _ from "underscore"; import _ from "underscore";
import Icon from "metabase/components/Icon"; import Icon from "metabase/components/Icon";
import { color, darken } from "metabase/lib/colors"; import { color, darken, alpha } from "metabase/lib/colors";
const BORDER_RADIUS = "8px"; function getDefaultBorderColor() {
return darken(color("border"), 0.1);
}
export const SegmentedList = styled.ul` const COLORS = {
display: flex; "fill-text": {
width: ${props => (props.fullWidth ? 1 : 0)}; background: () => "transparent",
`; border: () => getDefaultBorderColor(),
text: ({ isSelected, selectedColor, inactiveColor }) =>
color(isSelected ? selectedColor : inactiveColor),
},
"fill-background": {
background: ({ isSelected, selectedColor }) =>
isSelected ? color(selectedColor) : "transparent",
border: ({ isSelected, selectedColor }) =>
isSelected ? color(selectedColor) : getDefaultBorderColor(),
text: ({ isSelected, inactiveColor }) =>
color(isSelected ? "text-white" : inactiveColor),
},
"fill-all": {
background: ({ isSelected, selectedColor }) =>
isSelected ? alpha(color(selectedColor), 0.2) : "transparent",
border: ({ isSelected, selectedColor }) =>
isSelected ? color(selectedColor) : getDefaultBorderColor(),
text: ({ isSelected, selectedColor, inactiveColor }) =>
color(isSelected ? selectedColor : inactiveColor),
},
};
function getSpecialBorderStyles({
index,
isSelected,
total,
selectedOptionIndex,
}) {
if (isSelected) {
return css`
border-right-width: 1px;
border-left-width: 1px;
`;
}
const isBeforeSelected = index === selectedOptionIndex - 1;
if (isBeforeSelected) {
return css`
border-right-width: 0;
`;
}
const isAfterSelected = index === selectedOptionIndex + 1;
if (isAfterSelected) {
return css`
border-left-width: 0;
`;
}
function getSegmentedItemColor(props, fallbackColor) { const isFirst = index === 0;
if (props.variant === "fill-text") { if (isFirst) {
return fallbackColor; return css`
border-left-width: 1px;
border-right-width: 0;
`;
}
const isLast = index === total - 1;
if (isLast) {
return css`
border-right-width: 1px;
border-left-width: 0;
`;
} }
return props.isSelected ? color(props.selectedColor) : fallbackColor;
} }
export const SegmentedItem = styled.li` export const SegmentedItem = styled.li`
display: flex; display: flex;
flex-grow: ${props => (props.fullWidth ? 1 : 0)}; flex-grow: ${props => (props.fullWidth ? 1 : 0)};
background-color: ${props => getSegmentedItemColor(props, "transparent")}; background-color: ${props => COLORS[props.variant].background(props)};
border: 1px solid ${props => COLORS[props.variant].border(props)};
border: 1px solid
${props => getSegmentedItemColor(props, darken(color("border"), 0.1))};
border-right-width: ${props => (props.isLast ? "1px" : 0)}; ${props => getSpecialBorderStyles(props)};
border-top-left-radius: ${props => (props.isFirst ? BORDER_RADIUS : 0)};
border-bottom-left-radius: ${props => (props.isFirst ? BORDER_RADIUS : 0)};
border-top-right-radius: ${props => (props.isLast ? BORDER_RADIUS : 0)};
border-bottom-right-radius: ${props => (props.isLast ? BORDER_RADIUS : 0)};
`; `;
export const SegmentedItemLabel = styled.label` export const SegmentedItemLabel = styled.label`
...@@ -41,12 +94,7 @@ export const SegmentedItemLabel = styled.label` ...@@ -41,12 +94,7 @@ export const SegmentedItemLabel = styled.label`
justify-content: center; justify-content: center;
position: relative; position: relative;
font-weight: bold; font-weight: bold;
color: ${props => { color: ${props => COLORS[props.variant].text(props)};
const selectedColor = color(
props.variant === "fill-text" ? props.selectedColor : "white",
);
return props.isSelected ? selectedColor : color(props.inactiveColor);
}};
padding: ${props => (props.compact ? "8px" : "8px 12px")}; padding: ${props => (props.compact ? "8px" : "8px 12px")};
cursor: pointer; cursor: pointer;
...@@ -77,3 +125,21 @@ function IconWrapper(props) { ...@@ -77,3 +125,21 @@ function IconWrapper(props) {
export const ItemIcon = styled(IconWrapper)` export const ItemIcon = styled(IconWrapper)`
margin-right: ${props => (props.iconOnly ? 0 : "4px")}; margin-right: ${props => (props.iconOnly ? 0 : "4px")};
`; `;
const BORDER_RADIUS = "8px";
export const SegmentedList = styled.ul`
display: flex;
${SegmentedItem} {
&:first-of-type {
border-top-left-radius: ${BORDER_RADIUS};
border-bottom-left-radius: ${BORDER_RADIUS};
}
&:last-of-type {
border-top-right-radius: ${BORDER_RADIUS};
border-bottom-right-radius: ${BORDER_RADIUS};
}
}
`;
...@@ -84,4 +84,7 @@ const Input = forwardRef(function Input( ...@@ -84,4 +84,7 @@ const Input = forwardRef(function Input(
); );
}); });
export default Input; export default Object.assign(Input, {
Root: InputRoot,
Field: InputField,
});
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
import NumericInput from "metabase/core/components/NumericInput";
import Input from "metabase/core/components/Input";
export const CompactInputContainer = styled.div`
display: flex;
align-items: center;
padding: 0.55rem 1rem;
border-radius: 8px;
border: 1px solid ${color("border")};
background-color: ${color("white")};
`;
export const CompactInput = styled(NumericInput)`
width: 1rem;
text-align: center;
${Input.Root}, ${Input.Field} {
border: none;
padding: 0;
margin: 0;
font-size: 0.875rem;
}
`;
import React, { forwardRef, Ref, useCallback } from "react";
import { t } from "ttag";
import { SegmentedControl } from "metabase/components/SegmentedControl";
import useTimeInput, { BaseTimeInputProps } from "./useTimeInput";
import {
InputDivider,
InputRoot,
InputMeridiemContainer,
} from "./TimeInput.styled";
import { CompactInputContainer, CompactInput } from "./CompactTimeInput.styled";
export type CompactTimeInputProps = BaseTimeInputProps;
const CompactTimeInput = forwardRef(function TimeInput(
{ value, is24HourMode, autoFocus, onChange }: CompactTimeInputProps,
ref: Ref<HTMLDivElement>,
): JSX.Element {
const {
isAm,
hoursText,
minutesText,
amText,
pmText,
handleHoursChange,
handleMinutesChange,
handleAM,
handlePM,
} = useTimeInput({ value, is24HourMode, onChange });
const onAmPmChange = useCallback(
value => {
if (value === "am") {
handleAM();
} else {
handlePM();
}
},
[handleAM, handlePM],
);
return (
<InputRoot ref={ref}>
<CompactInputContainer>
<CompactInput
value={hoursText}
placeholder="00"
autoFocus={autoFocus}
fullWidth
aria-label={t`Hours`}
onChange={handleHoursChange}
/>
<InputDivider>:</InputDivider>
<CompactInput
value={minutesText}
placeholder="00"
fullWidth
aria-label={t`Minutes`}
onChange={handleMinutesChange}
/>
</CompactInputContainer>
{!is24HourMode && (
<InputMeridiemContainer>
<SegmentedControl
name="am-pm"
value={isAm ? "am" : "pm"}
options={[
{ name: amText, value: "am" },
{ name: pmText, value: "pm" },
]}
variant="fill-all"
onChange={onAmPmChange}
/>
</InputMeridiemContainer>
)}
</InputRoot>
);
});
export default CompactTimeInput;
...@@ -17,3 +17,11 @@ const Template: ComponentStory<typeof TimeInput> = args => { ...@@ -17,3 +17,11 @@ const Template: ComponentStory<typeof TimeInput> = args => {
}; };
export const Default = Template.bind({}); export const Default = Template.bind({});
const CompactTemplate: ComponentStory<typeof TimeInput> = args => {
const [value, setValue] = useState(moment("2020-01-01T10:20"));
return <TimeInput.Compact {...args} value={value} onChange={setValue} />;
};
export const Compact = CompactTemplate.bind({});
import React, { forwardRef, Ref, useCallback } from "react"; import React, { forwardRef, Ref } from "react";
import { t } from "ttag"; import { t } from "ttag";
import moment, { Moment } from "moment"; import { Moment } from "moment";
import Tooltip from "metabase/components/Tooltip"; import Tooltip from "metabase/components/Tooltip";
import useTimeInput, { BaseTimeInputProps } from "./useTimeInput";
import CompactTimeInput from "./CompactTimeInput";
import { import {
InputClearButton, InputClearButton,
InputClearIcon, InputClearIcon,
...@@ -12,69 +16,35 @@ import { ...@@ -12,69 +16,35 @@ import {
InputRoot, InputRoot,
} from "./TimeInput.styled"; } from "./TimeInput.styled";
export interface TimeInputProps { export interface TimeInputProps extends BaseTimeInputProps {
value: Moment; hasClearButton?: boolean;
is24HourMode?: boolean;
autoFocus?: boolean;
onChange?: (value: Moment) => void;
onClear?: (value: Moment) => void; onClear?: (value: Moment) => void;
} }
const TimeInput = forwardRef(function TimeInput( const TimeInput = forwardRef(function TimeInput(
{ value, is24HourMode, autoFocus, onChange, onClear }: TimeInputProps, {
value,
is24HourMode,
autoFocus,
hasClearButton = true,
onChange,
onClear,
}: TimeInputProps,
ref: Ref<HTMLDivElement>, ref: Ref<HTMLDivElement>,
): JSX.Element { ): JSX.Element {
const hoursText = value.format(is24HourMode ? "HH" : "hh"); const {
const minutesText = value.format("mm"); isAm,
const isAm = value.hours() < 12; isPm,
const isPm = !isAm; hoursText,
const amText = moment.localeData().meridiem(0, 0, false); minutesText,
const pmText = moment.localeData().meridiem(12, 0, false); amText,
pmText,
const handleHoursChange = useCallback( handleHoursChange,
(hours = 0) => { handleMinutesChange,
const newValue = value.clone(); handleAM: handleAmClick,
if (is24HourMode) { handlePM: handlePmClick,
newValue.hours(hours % 24); handleClear: handleClearClick,
} else { } = useTimeInput({ value, is24HourMode, onChange, onClear });
newValue.hours((hours % 12) + (isAm ? 0 : 12));
}
onChange?.(newValue);
},
[value, isAm, is24HourMode, onChange],
);
const handleMinutesChange = useCallback(
(minutes = 0) => {
const newValue = value.clone();
newValue.minutes(minutes % 60);
onChange?.(newValue);
},
[value, onChange],
);
const handleAmClick = useCallback(() => {
if (isPm) {
const newValue = value.clone();
newValue.hours(newValue.hours() - 12);
onChange?.(newValue);
}
}, [value, isPm, onChange]);
const handlePmClick = useCallback(() => {
if (isAm) {
const newValue = value.clone();
newValue.hours(newValue.hours() + 12);
onChange?.(newValue);
}
}, [value, isAm, onChange]);
const handleClearClick = useCallback(() => {
const newValue = value.clone();
newValue.hours(0);
newValue.minutes(0);
onClear?.(newValue);
}, [value, onClear]);
return ( return (
<InputRoot ref={ref}> <InputRoot ref={ref}>
...@@ -104,16 +74,20 @@ const TimeInput = forwardRef(function TimeInput( ...@@ -104,16 +74,20 @@ const TimeInput = forwardRef(function TimeInput(
</InputMeridiemButton> </InputMeridiemButton>
</InputMeridiemContainer> </InputMeridiemContainer>
)} )}
<Tooltip tooltip={t`Remove time`}> {hasClearButton && (
<InputClearButton <Tooltip tooltip={t`Remove time`}>
aria-label={t`Remove time`} <InputClearButton
onClick={handleClearClick} aria-label={t`Remove time`}
> onClick={handleClearClick}
<InputClearIcon name="close" /> >
</InputClearButton> <InputClearIcon name="close" />
</Tooltip> </InputClearButton>
</Tooltip>
)}
</InputRoot> </InputRoot>
); );
}); });
export default TimeInput; export default Object.assign(TimeInput, {
Compact: CompactTimeInput,
});
import { useCallback } from "react";
import moment, { Moment } from "moment";
export interface BaseTimeInputProps {
value: Moment;
is24HourMode?: boolean;
autoFocus?: boolean;
onChange?: (value: Moment) => void;
onClear?: (value: Moment) => void;
}
function useTimeInput({
value,
is24HourMode,
onChange,
onClear,
}: BaseTimeInputProps) {
const hoursText = value.format(is24HourMode ? "HH" : "hh");
const minutesText = value.format("mm");
const isAm = value.hours() < 12;
const isPm = !isAm;
const amText = moment.localeData().meridiem(0, 0, false);
const pmText = moment.localeData().meridiem(12, 0, false);
const handleHoursChange = useCallback(
(hours = 0) => {
const newValue = value.clone();
if (is24HourMode) {
newValue.hours(hours % 24);
} else {
newValue.hours((hours % 12) + (isAm ? 0 : 12));
}
onChange?.(newValue);
},
[value, isAm, is24HourMode, onChange],
);
const handleMinutesChange = useCallback(
(minutes = 0) => {
const newValue = value.clone();
newValue.minutes(minutes % 60);
onChange?.(newValue);
},
[value, onChange],
);
const handleAM = useCallback(() => {
if (isPm) {
const newValue = value.clone();
newValue.hours(newValue.hours() - 12);
onChange?.(newValue);
}
}, [value, isPm, onChange]);
const handlePM = useCallback(() => {
if (isAm) {
const newValue = value.clone();
newValue.hours(newValue.hours() + 12);
onChange?.(newValue);
}
}, [value, isAm, onChange]);
const handleClear = useCallback(() => {
const newValue = value.clone();
newValue.hours(0);
newValue.minutes(0);
onClear?.(newValue);
}, [value, onClear]);
return {
isAm,
isPm,
hoursText,
minutesText,
amText,
pmText,
handleHoursChange,
handleMinutesChange,
handleAM,
handlePM,
handleClear,
};
}
export default useTimeInput;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment