DateRange.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /*
  2. * Copyright 2021-2024 Avaiga Private Limited
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  5. * the License. You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  10. * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  11. * specific language governing permissions and limitations under the License.
  12. */
  13. import React, { useState, useEffect, useCallback } from "react";
  14. import Box from "@mui/material/Box";
  15. import Tooltip from "@mui/material/Tooltip";
  16. import { DatePicker, DatePickerProps } from "@mui/x-date-pickers/DatePicker";
  17. import { BaseDateTimePickerSlotProps } from "@mui/x-date-pickers/DateTimePicker/shared";
  18. import { DateTimePicker, DateTimePickerProps } from "@mui/x-date-pickers/DateTimePicker";
  19. import { isValid } from "date-fns";
  20. import { ErrorBoundary } from "react-error-boundary";
  21. import { createSendUpdateAction } from "../../context/taipyReducers";
  22. import { getSuffixedClassNames, TaipyActiveProps, TaipyChangeProps } from "./utils";
  23. import { dateToString, getDateTime, getTimeZonedDate } from "../../utils";
  24. import { useClassNames, useDispatch, useDynamicProperty, useFormatConfig, useModule } from "../../utils/hooks";
  25. import Field from "./Field";
  26. import ErrorFallback from "../../utils/ErrorBoundary";
  27. interface DateRangeProps extends TaipyActiveProps, TaipyChangeProps {
  28. withTime?: boolean;
  29. format?: string;
  30. dates: string[];
  31. defaultDates?: string;
  32. defaultEditable?: boolean;
  33. editable?: boolean;
  34. labelStart?: string;
  35. labelEnd?: string;
  36. }
  37. const boxSx = { display: "inline-flex", alignItems: "center", gap: "0.5em" };
  38. const textFieldProps = { textField: { margin: "dense" } } as BaseDateTimePickerSlotProps<Date>;
  39. const getRangeDateTime = (
  40. json: string | string[] | undefined,
  41. tz: string,
  42. withTime: boolean
  43. ): [Date | null, Date | null] => {
  44. let dates: string[] = [];
  45. if (typeof json == "string") {
  46. try {
  47. dates = JSON.parse(json);
  48. } catch (e) {}
  49. } else {
  50. dates = json as string[];
  51. }
  52. if (Array.isArray(dates) && dates.length) {
  53. return dates.map((d) => getDateTime(d, tz, withTime)) as [Date, Date];
  54. }
  55. return [null, null];
  56. };
  57. interface DateProps {
  58. maxDate?: unknown;
  59. maxDateTime?: unknown;
  60. maxTime?: unknown;
  61. minDate?: unknown;
  62. minDateTime?: unknown;
  63. minTime?: unknown;
  64. }
  65. const getProps = (p: DateProps, start: boolean, val: Date | null, withTime: boolean): DateProps => {
  66. if (!val) {
  67. return {};
  68. }
  69. const propName: keyof DateProps = withTime
  70. ? start
  71. ? "minDateTime"
  72. : "maxDateTime"
  73. : start
  74. ? "minDate"
  75. : "maxDate";
  76. if (p[propName] == val) {
  77. return p;
  78. }
  79. return { ...p, [propName]: val };
  80. };
  81. const DateRange = (props: DateRangeProps) => {
  82. const { updateVarName, withTime = false, id, propagate = true } = props;
  83. const dispatch = useDispatch();
  84. const formatConfig = useFormatConfig();
  85. const tz = formatConfig.timeZone;
  86. const [value, setValue] = useState<[Date | null, Date | null]>([null, null]);
  87. const [startProps, setStartProps] = useState<DateProps>({});
  88. const [endProps, setEndProps] = useState<DateProps>({});
  89. const module = useModule();
  90. const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
  91. const active = useDynamicProperty(props.active, props.defaultActive, true);
  92. const editable = useDynamicProperty(props.editable, props.defaultEditable, true);
  93. const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
  94. const handleChange = useCallback(
  95. (v: Date | null, start: boolean) => {
  96. setValue((dates) => {
  97. if (v !== null && isValid(v)) {
  98. const newDate = getTimeZonedDate(v, tz, withTime);
  99. const otherDate = start
  100. ? dates[1] && getTimeZonedDate(dates[1], tz, withTime)
  101. : dates[0] && getTimeZonedDate(dates[0], tz, withTime);
  102. dispatch(
  103. createSendUpdateAction(
  104. updateVarName,
  105. [
  106. start
  107. ? dateToString(newDate, withTime)
  108. : otherDate && dateToString(otherDate, withTime),
  109. start
  110. ? otherDate && dateToString(otherDate, withTime)
  111. : dateToString(newDate, withTime),
  112. ],
  113. module,
  114. props.onChange,
  115. propagate
  116. )
  117. );
  118. (start ? setEndProps : setStartProps)((p) => getProps(p, start, v, withTime));
  119. }
  120. return [start ? v : dates[0], start ? dates[1] : v];
  121. });
  122. },
  123. [updateVarName, dispatch, withTime, propagate, tz, props.onChange, module]
  124. );
  125. const handleChangeStart = useCallback((v: Date | null) => handleChange(v, true), [handleChange]);
  126. const handleChangeEnd = useCallback((v: Date | null) => handleChange(v, false), [handleChange]);
  127. // Run every time props.value get updated
  128. useEffect(() => {
  129. if (props.dates !== undefined || props.defaultDates) {
  130. const dates = getRangeDateTime(props.dates === undefined ? props.defaultDates : props.dates, tz, withTime);
  131. setEndProps((p) => getProps(p, true, dates[0], withTime));
  132. setStartProps((p) => getProps(p, false, dates[1], withTime));
  133. setValue(dates);
  134. }
  135. }, [props.dates, props.defaultDates, tz, withTime]);
  136. return (
  137. <ErrorBoundary FallbackComponent={ErrorFallback}>
  138. <Tooltip title={hover || ""}>
  139. <Box id={id} className={className} sx={boxSx}>
  140. {editable ? (
  141. withTime ? (
  142. <>
  143. <DateTimePicker
  144. {...(startProps as DateTimePickerProps<Date>)}
  145. value={value[0]}
  146. onChange={handleChangeStart}
  147. className={
  148. getSuffixedClassNames(className, "-picker") +
  149. " " +
  150. getSuffixedClassNames(className, "-picker-start")
  151. }
  152. disabled={!active}
  153. slotProps={textFieldProps}
  154. label={props.labelStart}
  155. />
  156. -
  157. <DateTimePicker
  158. {...(endProps as DateTimePickerProps<Date>)}
  159. value={value[1]}
  160. onChange={handleChangeEnd}
  161. className={
  162. getSuffixedClassNames(className, "-picker") +
  163. " " +
  164. getSuffixedClassNames(className, "-picker-end")
  165. }
  166. disabled={!active}
  167. slotProps={textFieldProps}
  168. label={props.labelEnd}
  169. />
  170. </>
  171. ) : (
  172. <>
  173. <DatePicker
  174. {...(startProps as DatePickerProps<Date>)}
  175. value={value[0]}
  176. onChange={handleChangeStart}
  177. className={
  178. getSuffixedClassNames(className, "-picker") +
  179. " " +
  180. getSuffixedClassNames(className, "-picker-start")
  181. }
  182. disabled={!active}
  183. slotProps={textFieldProps}
  184. label={props.labelStart}
  185. />
  186. -
  187. <DatePicker
  188. {...(endProps as DatePickerProps<Date>)}
  189. value={value[1]}
  190. onChange={handleChangeEnd}
  191. className={
  192. getSuffixedClassNames(className, "-picker") +
  193. " " +
  194. getSuffixedClassNames(className, "-picker-end")
  195. }
  196. disabled={!active}
  197. slotProps={textFieldProps}
  198. label={props.labelEnd}
  199. />
  200. </>
  201. )
  202. ) : (
  203. <>
  204. <Field
  205. dataType="datetime"
  206. value={props.dates[0]}
  207. format={props.format}
  208. id={id && id + "-field"}
  209. className={getSuffixedClassNames(className, "-text")}
  210. />
  211. -
  212. <Field
  213. dataType="datetime"
  214. value={props.dates[1]}
  215. format={props.format}
  216. id={id && id + "-field"}
  217. className={getSuffixedClassNames(className, "-text")}
  218. />
  219. </>
  220. )}
  221. </Box>
  222. </Tooltip>
  223. </ErrorBoundary>
  224. );
  225. };
  226. export default DateRange;