diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx index 8e22ee7bf..3b3e637c5 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateComp.tsx @@ -1,5 +1,8 @@ import _, { noop } from "lodash"; import dayjs from "dayjs"; +import utc from 'dayjs/plugin/utc'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; +import timezone from 'dayjs/plugin/timezone'; import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"; import { BoolCodeControl, @@ -48,6 +51,14 @@ import { RefControl } from "comps/controls/refControl"; // import { CommonPickerMethods } from "antd/es/date-picker/generatePicker/interface"; import { DateRangeUIView } from "comps/comps/dateComp/dateRangeUIView"; import { EditorContext } from "comps/editorState"; +import { dropdownControl } from "comps/controls/dropdownControl"; +import { timeZoneOptions } from "./timeZone"; + + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(customParseFormat); + const EventOptions = [changeEvent, focusEvent, blurEvent] as const; @@ -80,6 +91,7 @@ const commonChildren = { ...validationChildren, viewRef: RefControl, inputFieldStyle: styleControl(DateTimeStyle, 'inputFieldStyle'), + timeZone: dropdownControl(timeZoneOptions, "Etc/UTC"), }; type CommonChildrenType = RecordConstructorToComp; @@ -137,6 +149,7 @@ function validate( const childrenMap = { value: stringExposingStateControl("value"), + userTimeZone: stringExposingStateControl("userTimeZone", 'Etc/UTC'), ...commonChildren, ...formDataChildren, }; @@ -153,6 +166,7 @@ export type DateCompViewProps = Pick< | "minuteStep" | "secondStep" | "viewRef" + | "timeZone" > & { onFocus: () => void; onBlur: () => void; @@ -175,6 +189,10 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { setTempValue(value); }, [props.value.value]) + const handleDateZoneChange = (newTimeZone: any) => { + props.userTimeZone.onChange(newTimeZone) + } + return props.label({ required: props.required, style: props.style, @@ -184,6 +202,8 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { onMouseDown: (e) => e.stopPropagation(), children: ( disabledTime(props.minTime, props.maxTime)} $style={props.inputFieldStyle} @@ -224,6 +244,9 @@ export const datePickerControl = new UICompBuilder(childrenMap, (props) => { placeholder: "2022-04-07 21:39:59", tooltip: trans("date.formatTip") })} + {children.timeZone.propertyView({ + label: trans("prop.timeZone") + })} @@ -287,6 +310,7 @@ export const dateRangeControl = (function () { const childrenMap = { start: stringExposingStateControl("start"), end: stringExposingStateControl("end"), + userRangeTimeZone: stringExposingStateControl("userRangeTimeZone" , 'Etc/UTC'), ...formDataChildren, ...commonChildren, }; @@ -315,8 +339,15 @@ export const dateRangeControl = (function () { setTempEndValue(value); }, [props.end.value]) + + const handleDateRangeZoneChange = (newTimeZone: any) => { + props.userRangeTimeZone.onChange(newTimeZone) + } + const children = ( props.onEvent("focus")} onBlur={() => props.onEvent("blur")} - suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} - /> + suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} /> ); const startResult = validate({ ...props, value: props.start }); @@ -380,6 +410,9 @@ export const dateRangeControl = (function () { placeholder: "2022-04-07 21:39:59", tooltip: trans("date.formatTip"), })} + {children.timeZone.propertyView({ + label: trans("prop.timeZone") + })} @@ -440,21 +473,57 @@ export const DatePickerComp = withExposingConfigs(datePickerControl, [ depsConfig({ name: "value", desc: trans("export.datePickerValueDesc"), - depKeys: ["value", "showTime"], + depKeys: ["value", "showTime", "timeZone", "userTimeZone"], func: (input) => { - const mom = Boolean(input.value) ? dayjs(input.value, DateParser) : null; - return mom?.isValid() ? mom.format(input.showTime ? DATE_TIME_FORMAT : DATE_FORMAT) : null; + + let mom = null; + for (const format of DateParser) { + if (dayjs.utc(input.value, format).isValid()) { + mom = dayjs.utc(input.value, format); + break; + } + } + + if (!input.showTime && mom?.hour() === 0 && mom?.minute() === 0 && mom?.second() === 0) { + mom = mom?.hour(12); // Default to noon to avoid day shift + } + + if (mom?.isValid()) { + const tz = input.timeZone === 'UserChoice' ? input.userTimeZone : input.timeZone || 'UTC'; + const formattedDate = mom.tz(tz).format(input.showTime ? DATE_TIME_FORMAT : DATE_FORMAT); + return formattedDate; + } + + return null; }, }), + depsConfig({ name: "formattedValue", desc: trans("export.datePickerFormattedValueDesc"), - depKeys: ["value", "format"], + depKeys: ["value", "format", "timeZone", "userTimeZone"], + func: (input) => { - const mom = Boolean(input.value) ? dayjs(input.value, DateParser) : null; - return mom?.isValid() ? mom.format(input.format) : ""; + let mom = null; + for (const format of DateParser) { + if (dayjs.utc(input.value, format).isValid()) { + mom = dayjs.utc(input.value, format); + break; + } + } + if (!input.showTime && mom?.hour() === 0 && mom?.minute() === 0 && mom?.second() === 0) { + mom = mom?.hour(12); // Default to noon to avoid timezone-related day shifts + } + if (mom?.isValid()) { + const tz = input.timeZone === 'UserChoice' ? input.userTimeZone : input.timeZone || 'UTC'; + const formattedTime = mom.tz(tz).format(input.format); + return formattedTime; + } + return ''; }, }), + + depsConfig({ name: "timestamp", desc: trans("export.datePickerTimestampDesc"), @@ -474,6 +543,16 @@ export const DatePickerComp = withExposingConfigs(datePickerControl, [ value: { value: input.value }, } as any).validateStatus !== "success", }), + depsConfig({ + name: "timeZone", + desc: trans("export.timeZoneDesc"), + depKeys: ["timeZone", "userTimeZone"], + func: (input) => { + console.log("input.timeZone", input.timeZone) + return input.timeZone === 'UserChoice' ? input.userTimeZone : input.timeZone || 'UTC'; + + }, + }), ...CommonNameConfig, ]); @@ -481,89 +560,235 @@ export let DateRangeComp = withExposingConfigs(dateRangeControl, [ depsConfig({ name: "start", desc: trans("export.dateRangeStartDesc"), - depKeys: ["start", "showTime"], + depKeys: ["start", "showTime", "timeZone", "userRangeTimeZone"], func: (input) => { - const mom = Boolean(input.start) ? dayjs(input.start, DateParser): null; - return mom?.isValid() ? mom.format(input.showTime ? DATE_TIME_FORMAT : DATE_FORMAT) : null; + let mom = null; + for (const format of DateParser) { + if (dayjs.utc(input.start, format).isValid()) { + mom = dayjs.utc(input.start, format); + break; + } + } + if (!input.showTime && mom?.hour() === 0 && mom?.minute() === 0 && mom?.second() === 0) { + mom = mom?.hour(12); + } + + if (mom?.isValid()) { + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + const formattedStart = mom.tz(tz).format(input.showTime ? DATE_TIME_FORMAT : DATE_FORMAT); + return formattedStart; + } + return null; }, }), + depsConfig({ name: "end", desc: trans("export.dateRangeEndDesc"), - depKeys: ["end", "showTime"], + depKeys: ["end", "showTime", "timeZone", "userRangeTimeZone"], + func: (input) => { - const mom = Boolean(input.end) ? dayjs(input.end, DateParser): null; - return mom?.isValid() ? mom.format(input.showTime ? DATE_TIME_FORMAT : DATE_FORMAT) : null; + let mom = null; + for (const format of DateParser) { + if (dayjs.utc(input.end, format).isValid()) { + mom = dayjs.utc(input.end, format); + break; + } + } + if (!input.showTime && mom?.hour() === 0 && mom?.minute() === 0 && mom?.second() === 0) { + mom = mom?.hour(12); + } + + if (mom?.isValid()) { + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + const formattedEnd = mom.tz(tz).format(input.showTime ? DATE_TIME_FORMAT : DATE_FORMAT); + return formattedEnd; + } + return null; }, }), + depsConfig({ name: "startTimestamp", desc: trans("export.dateRangeStartTimestampDesc"), - depKeys: ["start"], + depKeys: ["start", "timeZone", "userRangeTimeZone"], func: (input) => { - const mom = Boolean(input.start) ? dayjs(input.start, DateParser) : null; - return mom?.isValid() ? mom.unix() : ""; + + let mom = null; + for (const format of DateParser) { + if (dayjs.utc(input.start, format).isValid()) { + mom = dayjs.utc(input.start, format); + break; + } + } + if (mom?.isValid()) { + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + return mom.tz(tz).unix(); + } + return ""; }, }), + depsConfig({ name: "endTimestamp", desc: trans("export.dateRangeEndTimestampDesc"), - depKeys: ["end"], + depKeys: ["end", "timeZone", "userRangeTimeZone"], func: (input) => { - const mom = Boolean(input.end) ? dayjs(input.end, DateParser) : null; - return mom?.isValid() ? mom.unix() : ""; + + let mom = null; + for (const format of DateParser) { + if (dayjs.utc(input.end, format).isValid()) { + mom = dayjs.utc(input.end, format); + break; + } + } + if (mom?.isValid()) { + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + return mom.tz(tz).unix(); + } + return ""; }, }), + depsConfig({ name: "formattedValue", desc: trans("export.dateRangeFormattedValueDesc"), - depKeys: ["start", "end", "format"], + depKeys: ["start", "end", "format", "timeZone", "userRangeTimeZone"], func: (input) => { - const start = Boolean(input.start) ? dayjs(input.start, DateParser): null; - const end = Boolean(input.end) ? dayjs(input.end, DateParser): null; - return [ - start?.isValid() && start.format(input.format), - end?.isValid() && end.format(input.format), - ] - .filter((item) => item) - .join(" - "); + let start = null; + let end = null; + + for (const format of DateParser) { + if (dayjs.utc(input.start, format).isValid()) { + start = dayjs.utc(input.start, format); + break; + } + } + for (const format of DateParser) { + if (dayjs.utc(input.end, format).isValid()) { + end = dayjs.utc(input.end, format); + break; + } + } + + //When the time is 00:00:00 and you convert it to a timezone behind UTC (e.g., UTC-5), the date can shift to the previous day + if (!input.showTime && start?.hour() === 0 && start?.minute() === 0 && start?.second() === 0) { + start = start?.hour(12); + } + + if (!input.showTime && end?.hour() === 0 && end?.minute() === 0 && end?.second() === 0) { + end = end?.hour(12); + } + + if (start?.isValid() || end?.isValid()) { + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + + const formattedStart = start?.isValid() ? start.tz(tz).format(input.format) : ''; + const formattedEnd = end?.isValid() ? end.tz(tz).format(input.format) : ''; + const formattedValue = [formattedStart, formattedEnd].filter(Boolean).join(" - "); + return formattedValue; + } + return ''; }, }), + + depsConfig({ name: "formattedStartValue", desc: trans("export.dateRangeFormattedStartValueDesc"), - depKeys: ["start", "format"], + depKeys: ["start", "format", "timeZone", "userRangeTimeZone"], func: (input) => { - const start = Boolean(input.start) ? dayjs(input.start, DateParser): null; - return start?.isValid() && start.format(input.format); + let start = null; + // Loop through DateParser to find a valid format + for (const format of DateParser) { + if (dayjs.utc(input.start, format).isValid()) { + start = dayjs.utc(input.start, format); + break; + } + } + + if (!input.showTime && start?.hour() === 0 && start?.minute() === 0 && start?.second() === 0) { + start = start?.hour(12); + } + + if (start?.isValid()) { + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + const formattedStart = start.tz(tz).format(input.format); + return formattedStart; + } + return ''; }, }), + depsConfig({ name: "formattedEndValue", desc: trans("export.dateRangeFormattedEndValueDesc"), - depKeys: ["end", "format"], + depKeys: ["end", "format", "timeZone", "userRangeTimeZone"], func: (input) => { - const end = Boolean(input.end) ? dayjs(input.end, DateParser): null; - return end?.isValid() && end.format(input.format); + let end = null; + // Loop through DateParser to find a valid format + for (const format of DateParser) { + if (dayjs.utc(input.end, format).isValid()) { + end = dayjs.utc(input.end, format); + break; + } + } + + if (!input.showTime && end?.hour() === 0 && end?.minute() === 0 && end?.second() === 0) { + end = end?.hour(12); + } + + if (end?.isValid()) { + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + const formattedEnd = end.tz(tz).format(input.format); + return formattedEnd; + } + return ''; }, }), + + depsConfig({ name: "invalid", desc: trans("export.invalidDesc"), - depKeys: ["start", "end", "required", "minTime", "maxTime", "minDate", "maxDate", "customRule"], - func: (input) => - validate({ - ...input, - value: { value: input.start }, - }).validateStatus !== "success" || - validate({ - ...input, - value: { value: input.end }, - }).validateStatus !== "success", + depKeys: ["start", "end", "required", "minTime", "maxTime", "minDate", "maxDate", "customRule", "timeZone", "userRangeTimeZone"], + func: (input) => { + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + let startDate = null; + let endDate = null; + + for (const format of DateParser) { + if (dayjs.utc(input.start, format).isValid()) { + startDate = dayjs.utc(input.start, format).tz(tz); + break; + } + } + for (const format of DateParser) { + if (dayjs.utc(input.end, format).isValid()) { + endDate = dayjs.utc(input.end, format).tz(tz); + break; + } + } + + const startInvalid = startDate && (!startDate.isValid() || (input.minDate && startDate.isBefore(dayjs(input.minDate).tz(tz))) || (input.maxDate && startDate.isAfter(dayjs(input.maxDate).tz(tz)))); + const endInvalid = endDate && (!endDate.isValid() || (input.minDate && endDate.isBefore(dayjs(input.minDate).tz(tz))) || (input.maxDate && endDate.isAfter(dayjs(input.maxDate).tz(tz)))); + + return startInvalid || endInvalid; + }, + }), + + depsConfig({ + name: "timeZone", + desc: trans("export.timeZoneDesc"), + depKeys: ["timeZone", "userRangeTimeZone"], + func: (input) => { + return input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + }, }), ...CommonNameConfig, ]); + DateRangeComp = withMethodExposing(DateRangeComp, [ ...dateRefMethods, { diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateRangeUIView.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateRangeUIView.tsx index d31a7980a..1c925fa95 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateRangeUIView.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateRangeUIView.tsx @@ -11,6 +11,8 @@ import { default as DatePicker } from "antd/es/date-picker"; import { hasIcon } from "comps/utils"; import { omit } from "lodash"; import { DateParser } from "@lowcoder-ee/util/dateTimeUtils"; +import { default as AntdSelect } from "antd/es/select"; +import { timeZoneOptions } from "./timeZone"; const { RangePicker } = DatePicker; @@ -21,6 +23,17 @@ const RangePickerStyled = styled(RangePicker)<{$style: DateTimeStyleType}>` ${(props) => props.$style && getStyle(props.$style)} `; +const StyledAntdSelect = styled(AntdSelect)` + width: 400px; + margin: 10px 0px; + .ant-select-selector { + font-size: 14px; + line-height: 1.5; + } +`; +const StyledDiv = styled.div` + text-align: center; +`; const DateRangeMobileUIView = React.lazy(() => import("./dateMobileUIView").then((m) => ({ default: m.DateRangeMobileUIView })) ); @@ -31,6 +44,7 @@ export interface DateRangeUIViewProps extends DateCompViewProps { placeholder?: string | [string, string]; onChange: (start?: dayjs.Dayjs | null, end?: dayjs.Dayjs | null) => void; onPanelChange: (value: any, mode: [string, string]) => void; + onClickDateRangeTimeZone:(value:any)=>void } export const DateRangeUIView = (props: DateRangeUIViewProps) => { @@ -44,7 +58,6 @@ export const DateRangeUIView = (props: DateRangeUIViewProps) => { // Use the same placeholder for both start and end if it's a single string placeholders = [props.placeholder || 'Start Date', props.placeholder || 'End Date']; } - return useUIView( , { hourStep={props.hourStep as any} minuteStep={props.minuteStep as any} secondStep={props.secondStep as any} + renderExtraFooter={() => ( + props.timeZone === "UserChoice" && ( + + option.value !== 'UserChoice')} + placeholder="Select Time Zone" + defaultValue={'Etc/UTC'} + onChange={props?.onClickDateRangeTimeZone} + /> + + ) + )} /> ); }; diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx index 58310258e..80ab630f6 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/dateUIView.tsx @@ -11,6 +11,8 @@ import { default as DatePicker } from "antd/es/date-picker"; import type { DatePickerProps } from "antd/es/date-picker"; import type { Dayjs } from 'dayjs'; import { DateParser } from "@lowcoder-ee/util/dateTimeUtils"; +import { timeZoneOptions } from "./timeZone"; +import { default as AntdSelect } from "antd/es/select"; const DatePickerStyled = styled(DatePicker)<{ $style: DateTimeStyleType }>` width: 100%; @@ -18,11 +20,25 @@ const DatePickerStyled = styled(DatePicker)<{ $style: DateTimeStyleType } ${(props) => props.$style && getStyle(props.$style)} `; +const StyledDiv = styled.div` + width: 100%; + margin: 10px 0px; +`; + +const StyledAntdSelect = styled(AntdSelect)` + width: 100%; + .ant-select-selector { + font-size: 14px; + line-height: 1.5; + } +`; export interface DataUIViewProps extends DateCompViewProps { value?: DatePickerProps['value']; onChange: DatePickerProps['onChange']; onPanelChange: () => void; + onClickDateTimeZone:(value:any)=>void; + } const DateMobileUIView = React.lazy(() => @@ -48,6 +64,18 @@ export const DateUIView = (props: DataUIViewProps) => { picker={"date"} inputReadOnly={checkIsMobile(editorState?.getAppSettings().maxWidth)} placeholder={placeholder} + renderExtraFooter={()=>( + props.timeZone === "UserChoice" && ( + + option.value !== 'UserChoice')} + placeholder="Select Time Zone" + defaultValue={'Etc/UTC'} + onChange={props.onClickDateTimeZone} + /> + + ) + )} /> ); }; diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx index c362e01b7..5b8809e9a 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/timeComp.tsx @@ -1,5 +1,8 @@ import _ from "lodash"; import dayjs from "dayjs"; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; import { RecordConstructorToComp, RecordConstructorToView } from "lowcoder-core"; import { BoolCodeControl, @@ -52,6 +55,12 @@ import { RefControl } from "comps/controls/refControl"; import { TimePickerProps } from "antd/es/time-picker"; import { EditorContext } from "comps/editorState"; +import { dropdownControl } from "comps/controls/dropdownControl"; +import { timeZoneOptions } from "./timeZone"; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(customParseFormat); const EventOptions = [changeEvent, focusEvent, blurEvent] as const; @@ -81,6 +90,7 @@ const commonChildren = { ), inputFieldStyle: styleControl(DateTimeStyle, 'inputFieldStyle'), suffixIcon: withDefault(IconControl, "/icon:regular/clock"), + timeZone: dropdownControl(timeZoneOptions, "Etc/UTC"), viewRef: RefControl, ...validationChildren, }; @@ -120,6 +130,7 @@ function validate( const childrenMap = { value: stringExposingStateControl("value"), + userTimeZone: stringExposingStateControl("userTimeZone" , 'Etc/UTC'), ...commonChildren, ...formDataChildren, }; @@ -140,6 +151,7 @@ export type TimeCompViewProps = Pick< disabledTime: () => ReturnType; suffixIcon?: ReactNode | false; placeholder?: string | [string, string]; + timeZone:string }; export const timePickerControl = new UICompBuilder(childrenMap, (props) => { @@ -155,6 +167,10 @@ export const timePickerControl = new UICompBuilder(childrenMap, (props) => { setTempValue(value); }, [props.value.value]) + const handleTimeZoneChange = (newTimeZone: any) => { + props.userTimeZone.onChange(newTimeZone) + } + return props.label({ required: props.required, style: props.style, @@ -164,6 +180,8 @@ export const timePickerControl = new UICompBuilder(childrenMap, (props) => { onMouseDown: (e) => e.stopPropagation(), children: ( { }} onFocus={() => props.onEvent("focus")} onBlur={() => props.onEvent("blur")} - suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} - /> + suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} /> ), ...validate(props), }); @@ -196,7 +213,9 @@ export const timePickerControl = new UICompBuilder(childrenMap, (props) => { label: trans("prop.defaultValue"), tooltip: trans("time.formatTip"), })} - + {children.timeZone.propertyView({ + label: trans("prop.timeZone") + })} @@ -256,6 +275,7 @@ export const timeRangeControl = (function () { const childrenMap = { start: stringExposingStateControl("start"), end: stringExposingStateControl("end"), + userRangeTimeZone: stringExposingStateControl("userRangeTimeZone" , 'Etc/UTC'), ...formDataChildren, ...commonChildren, }; @@ -282,9 +302,15 @@ export const timeRangeControl = (function () { const value = props.end.value ? dayjs(props.end.value, TimeParser) : null; setTempEndValue(value); }, [props.end.value]) + + const handleTimeRangeZoneChange = (newTimeZone: any) => { + props.userRangeTimeZone.onChange(newTimeZone) + } const children = ( @@ -393,15 +422,36 @@ export const timeRangeControl = (function () { export const TimePickerComp = withExposingConfigs(timePickerControl, [ new NameConfig("value", trans("export.timePickerValueDesc")), + depsConfig({ name: "formattedValue", desc: trans("export.timePickerFormattedValueDesc"), - depKeys: ["value", "format"], + depKeys: ["value", "format", "timeZone", "userTimeZone"], func: (input) => { - const mom = Boolean(input.value) ? dayjs(input.value, TimeParser) : null; - return mom?.isValid() ? mom.format(input.format) : ''; + let mom = null; + + // Loop through TimeParser to find a valid format + for (const format of TimeParser) { + if (dayjs.utc(input.value, format).isValid()) { + mom = dayjs.utc(input.value, format); + break; + } + } + + const tz = input.timeZone === 'UserChoice' ? input.userTimeZone : input.timeZone || 'UTC'; + return mom?.isValid() ? mom.tz(tz).format(input.format) : ''; }, }), + + depsConfig({ + name: "timeZone", + desc: trans("export.timeZoneDesc"), + depKeys: ["timeZone", "userTimeZone"], + func: (input) => { + return input.timeZone === 'UserChoice' ? input.userTimeZone : input.timeZone || 'UTC'; + }, + }), + depsConfig({ name: "invalid", desc: trans("export.invalidDesc"), @@ -410,64 +460,158 @@ export const TimePickerComp = withExposingConfigs(timePickerControl, [ validate({ ...input, value: { value: input.value }, - } as any).validateStatus !== "success", + }).validateStatus !== "success", }), + ...CommonNameConfig, ]); + export let TimeRangeComp = withExposingConfigs(timeRangeControl, [ - new NameConfig("start", trans("export.timeRangeStartDesc")), - new NameConfig("end", trans("export.timeRangeEndDesc")), + // new NameConfig("start", trans("export.timeRangeStartDesc")), + // new NameConfig("end", trans("export.timeRangeEndDesc")), + depsConfig({ + name: "start", + desc: trans("export.timeRangeStartDesc"), + depKeys: ["start", "timeZone", "userRangeTimeZone"], + func: (input) => { + let start = null; + + // Loop through TimeParser to find a valid format for start + for (const format of TimeParser) { + if (dayjs.utc(input.start, format).isValid()) { + start = dayjs.utc(input.start, format); + break; + } + } + + if (start?.hour() === 0 && start?.minute() === 0 && start?.second() === 0) { + start = start?.hour(12); + } + + // Apply timezone conversion if valid + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + return start?.isValid() ? start.tz(tz).format(input.format || "HH:mm:ss") : null; + }, + }), + + depsConfig({ + name: "end", + desc: trans("export.timeRangeEndDesc"), + depKeys: ["end", "timeZone", "userRangeTimeZone"], + func: (input) => { + let end = null; + + // Loop through TimeParser to find a valid format for end + for (const format of TimeParser) { + if (dayjs.utc(input.end, format).isValid()) { + end = dayjs.utc(input.end, format); + break; + } + } + + // Apply timezone conversion if valid + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + return end?.isValid() ? end.tz(tz).format(input.format || "HH:mm:ss") : null; + }, + }), + depsConfig({ name: "formattedValue", desc: trans("export.timeRangeFormattedValueDesc"), - depKeys: ["start", "end", "format"], + depKeys: ["start", "end", "format", "timeZone", "userRangeTimeZone"], func: (input) => { - const start = Boolean(input.start) ? dayjs(input.start, TimeParser) : null; - const end = Boolean(input.end) ? dayjs(input.end, TimeParser) : null; - return [ - start?.isValid() && start.format(input.format), - end?.isValid() && end.format(input.format), - ] - .filter((item) => item) - .join(" - "); + let start = null; + let end = null; + for (const format of TimeParser) { + if (dayjs.utc(input.start, format).isValid()) { + start = dayjs.utc(input.start, format); + break; + } + } + for (const format of TimeParser) { + if (dayjs.utc(input.end, format).isValid()) { + end = dayjs.utc(input.end, format); + break; + } + } + + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + const formattedStart = start?.isValid() ? start.tz(tz).format(input.format) : ''; + const formattedEnd = end?.isValid() ? end.tz(tz).format(input.format) : ''; + + return [formattedStart, formattedEnd].filter(Boolean).join(" - "); }, }), + depsConfig({ name: "formattedStartValue", desc: trans("export.timeRangeFormattedStartValueDesc"), - depKeys: ["start", "format"], + depKeys: ["start", "format", "timeZone", "userRangeTimeZone"], func: (input) => { - const start = Boolean(input.start) ? dayjs(input.start, TimeParser) : null; - return start?.isValid() && start.format(input.format); + let start = null; + for (const format of TimeParser) { + if (dayjs.utc(input.start, format).isValid()) { + start = dayjs.utc(input.start, format); + break; + } + } + + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + return start?.isValid() ? start.tz(tz).format(input.format) : ''; }, }), + depsConfig({ name: "formattedEndValue", desc: trans("export.timeRangeFormattedEndValueDesc"), - depKeys: ["end", "format"], + depKeys: ["end", "format", "timeZone", "userRangeTimeZone"], func: (input) => { - const end = Boolean(input.end) ? dayjs(input.end, TimeParser) : null; - return end?.isValid() && end.format(input.format); + let end = null; + for (const format of TimeParser) { + if (dayjs.utc(input.end, format).isValid()) { + end = dayjs.utc(input.end, format); + break; + } + } + + const tz = input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + return end?.isValid() ? end.tz(tz).format(input.format) : ''; }, }), + + depsConfig({ + name: "timeZone", + desc: trans("export.timeZoneDesc"), + depKeys: ["timeZone", "userRangeTimeZone"], + func: (input) => { + return input.timeZone === 'UserChoice' ? input.userRangeTimeZone : input.timeZone || 'UTC'; + }, + }), + depsConfig({ name: "invalid", desc: trans("export.invalidDesc"), depKeys: ["start", "end", "required", "minTime", "maxTime", "customRule"], - func: (input) => - validate({ + func: (input) => { + const startInvalid = validate({ ...input, value: { value: input.start }, - }).validateStatus !== "success" || - validate({ + }).validateStatus !== "success"; + + const endInvalid = validate({ ...input, value: { value: input.end }, - }).validateStatus !== "success", + }).validateStatus !== "success"; + + return startInvalid || endInvalid; + }, }), + ...CommonNameConfig, ]); + TimeRangeComp = withMethodExposing(TimeRangeComp, [ ...dateRefMethods, { diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/timeRangeUIView.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/timeRangeUIView.tsx index a4a36ae68..4d837b94a 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/timeRangeUIView.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/timeRangeUIView.tsx @@ -10,6 +10,8 @@ import { EditorContext } from "../../editorState"; import dayjs from "dayjs"; import { hasIcon } from "comps/utils"; import { omit } from "lodash"; +import { timeZoneOptions } from "./timeZone"; +import { default as AntdSelect } from "antd/es/select"; const { RangePicker } = TimePicker; @@ -18,6 +20,15 @@ const RangePickerStyled = styled((props: any) => )<{ $ ${(props) => props.$style && getStyle(props.$style)} `; +const StyledAntdSelect = styled(AntdSelect)` + width: 100%; + margin: 10px 0px; + .ant-select-selector { + font-size: 14px; + line-height: 1.5; + } +`; + const TimeRangeMobileUIView = React.lazy(() => import("./timeMobileUIView").then((m) => ({ default: m.TimeRangeMobileUIView })) ); @@ -27,6 +38,7 @@ export interface TimeRangeUIViewProps extends TimeCompViewProps { end: dayjs.Dayjs | null; placeholder?: string | [string, string]; onChange: (start?: dayjs.Dayjs | null, end?: dayjs.Dayjs | null) => void; + handleTimeRangeZoneChange: (value:any) => void; } export const TimeRangeUIView = (props: TimeRangeUIViewProps) => { @@ -54,6 +66,16 @@ export const TimeRangeUIView = (props: TimeRangeUIViewProps) => { inputReadOnly={checkIsMobile(editorState?.getAppSettings().maxWidth)} suffixIcon={hasIcon(props.suffixIcon) && props.suffixIcon} placeholder={placeholders} + renderExtraFooter={() => ( + props.timeZone === "UserChoice" && ( + option.value !== 'UserChoice')} // Filter out 'userChoice' + defaultValue={'Etc/UTC'} + onChange={props.handleTimeRangeZoneChange} + /> + ) + )} /> ); }; diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/timeUIView.tsx b/client/packages/lowcoder/src/comps/comps/dateComp/timeUIView.tsx index 5c51d12ab..942061e68 100644 --- a/client/packages/lowcoder/src/comps/comps/dateComp/timeUIView.tsx +++ b/client/packages/lowcoder/src/comps/comps/dateComp/timeUIView.tsx @@ -8,6 +8,8 @@ import React, { useContext } from "react"; import type { TimeCompViewProps } from "./timeComp"; import { EditorContext } from "../../editorState"; import dayjs from "dayjs" +import { default as AntdSelect } from "antd/es/select"; +import { timeZoneOptions } from "./timeZone"; const TimePickerStyled = styled(TimePicker)<{ $style: DateTimeStyleType }>` width: 100%; @@ -17,10 +19,20 @@ const TimePickerStyled = styled(TimePicker)<{ $style: DateTimeStyleType }>` const TimeMobileUIView = React.lazy(() => import("./timeMobileUIView").then((m) => ({ default: m.TimeMobileUIView })) ); + +const StyledAntdSelect = styled(AntdSelect)` + width: 100%; + margin: 10px 0; + .ant-select-selector { + font-size: 14px; + padding: 8px; + } +`; export interface TimeUIViewProps extends TimeCompViewProps { value: dayjs.Dayjs | null; onChange: (value: dayjs.Dayjs | null) => void; + handleTimeZoneChange: (value:any) => void; } export const TimeUIView = (props: TimeUIViewProps) => { @@ -36,6 +48,16 @@ export const TimeUIView = (props: TimeUIViewProps) => { hideDisabledOptions inputReadOnly={checkIsMobile(editorState?.getAppSettings().maxWidth)} placeholder={placeholder} - /> + renderExtraFooter={()=>( + props.timeZone === "UserChoice" && ( + option.value !== 'UserChoice')} // Filter out 'userChoice' + onChange={props?.handleTimeZoneChange} + defaultValue={'Etc/UTC'} + /> + ) + )} + /> ); }; diff --git a/client/packages/lowcoder/src/comps/comps/dateComp/timeZone.ts b/client/packages/lowcoder/src/comps/comps/dateComp/timeZone.ts new file mode 100644 index 000000000..33108bcad --- /dev/null +++ b/client/packages/lowcoder/src/comps/comps/dateComp/timeZone.ts @@ -0,0 +1,29 @@ +import { trans } from "i18n"; + +export const timeZoneOptions = [ + { label: trans("timeZone.UTC-12:00"), value: "Etc/GMT+12" }, + { label: trans("timeZone.UTC-11:00"), value: "Etc/GMT+11" }, + { label: trans("timeZone.UTC-10:00"), value: "Pacific/Honolulu" }, + { label: trans("timeZone.UTC-09:00"), value: "America/Anchorage" }, + { label: trans("timeZone.UTC-08:00"), value: "America/Tijuana" }, + { label: trans("timeZone.UTC-07:00"), value: "America/Los_Angeles" }, + { label: trans("timeZone.UTC-06:00"), value: "America/Chicago" }, + { label: trans("timeZone.UTC-05:00"), value: "America/New_York" }, + { label: trans("timeZone.UTC-04:00"), value: "America/Halifax" }, + { label: trans("timeZone.UTC-03:00"), value: "America/Argentina/Buenos_Aires" }, + { label: trans("timeZone.UTC-02:00"), value: "Etc/GMT+2" }, + { label: trans("timeZone.UTC-01:00"), value: "Atlantic/Cape_Verde" }, + { label: trans("timeZone.UTC+00:00"), value: "Etc/UTC" }, + { label: trans("timeZone.UTC+01:00"), value: "Europe/Berlin" }, + { label: trans("timeZone.UTC+02:00"), value: "Europe/Bucharest" }, + { label: trans("timeZone.UTC+03:00"), value: "Europe/Moscow" }, + { label: trans("timeZone.UTC+04:00"), value: "Asia/Dubai" }, + { label: trans("timeZone.UTC+05:00"), value: "Asia/Karachi" }, + { label: trans("timeZone.UTC+05:30"), value: "Asia/Kolkata" }, + { label: trans("timeZone.UTC+06:00"), value: "Asia/Dhaka" }, + { label: trans("timeZone.UTC+07:00"), value: "Asia/Bangkok" }, + { label: trans("timeZone.UTC+08:00"), value: "Asia/Shanghai" }, + { label: trans("timeZone.UTC+09:00"), value: "Asia/Tokyo" }, + { label: trans("timeZone.UTC+10:00"), value: "Australia/Sydney" }, + { label: trans("timeZone.UserChoice"), value: "UserChoice" }, +]; diff --git a/client/packages/lowcoder/src/i18n/locales/en.ts b/client/packages/lowcoder/src/i18n/locales/en.ts index 41fffef0e..61d0ae8a6 100644 --- a/client/packages/lowcoder/src/i18n/locales/en.ts +++ b/client/packages/lowcoder/src/i18n/locales/en.ts @@ -222,6 +222,7 @@ export const en = { "horizontalGridCells": "Horizontal Grid Cells", "showHorizontalScrollbar": "Show Horizontal Scrollbar", "showVerticalScrollbar": "Show Vertical Scrollbar", + "timeZone": "TimeZone", }, "autoHeightProp": { "auto": "Auto", @@ -597,7 +598,11 @@ export const en = { "timeRangeEndDesc": "End time of the range", "timeRangeFormattedValueDesc": "Formatted time range", "timeRangeFormattedStartValueDesc": "Formatted start time", - "timeRangeFormattedEndValueDesc": "Formatted end time" + "timeRangeFormattedEndValueDesc": "Formatted end time", + "timeZone": "Time Zone", + "timeZoneDesc": "Timezone of the selected date", + + }, "validationDesc": { "email": "Please enter a valid email address", @@ -3608,6 +3613,34 @@ export const en = { "mobileNavIconSize": "Icon Size", }, +"timeZone": { + "UTC-12:00": "(UTC-12:00) Int'l Date Line W", + "UTC-11:00": "(UTC-11:00) UTC-11", + "UTC-10:00": "(UTC-10:00) Hawaii", + "UTC-09:00": "(UTC-09:00) Alaska", + "UTC-08:00": "(UTC-08:00) Baja CA", + "UTC-07:00": "(UTC-07:00) Pacific Time (US)", + "UTC-06:00": "(UTC-06:00) Central Time (US)", + "UTC-05:00": "(UTC-05:00) Eastern Time (US)", + "UTC-04:00": "(UTC-04:00) Atlantic Time", + "UTC-03:00": "(UTC-03:00) Buenos Aires", + "UTC-02:00": "(UTC-02:00) UTC-02", + "UTC-01:00": "(UTC-01:00) Cape Verde", + "UTC+00:00": "(UTC 00:00) UTC", + "UTC+01:00": "(UTC+01:00) Berlin, Rome", + "UTC+02:00": "(UTC+02:00) Athens, Bucharest", + "UTC+03:00": "(UTC+03:00) Moscow", + "UTC+04:00": "(UTC+04:00) Dubai, Muscat", + "UTC+05:00": "(UTC+05:00) Karachi", + "UTC+05:30": "(UTC+05:30) New Delhi", + "UTC+06:00": "(UTC+06:00) Dhaka", + "UTC+07:00": "(UTC+07:00) Bangkok", + "UTC+08:00": "(UTC+08:00) Beijing, HK", + "UTC+09:00": "(UTC+09:00) Tokyo, Seoul", + "UTC+10:00": "(UTC+10:00) Sydney", + "UserChoice": "User Choice" +}, + tour: { section1Title: "Steps", section1Subtitle: "Steps",