From a931eedae6b05ce57f7d254fca43aab42b60c0c5 Mon Sep 17 00:00:00 2001 From: Pawan Kumar Date: Fri, 29 Nov 2024 16:20:58 +0530 Subject: [PATCH] chore: add wds datepicker widget (#37711) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![CleanShot 2024-11-26 at 15 56 15](https://github.com/user-attachments/assets/d812f475-11e1-4750-9018-bdd39d5a5de3) /ok-to-test tags="@tag.Anvil" ## Summary by CodeRabbit ## Release Notes - **New Features** - Introduced the WDS Date Picker Widget, enhancing date selection capabilities within the UI. - Added configuration options for widget size, visibility, and autocomplete behavior. - Implemented comprehensive validation for date input, ensuring accurate user selections. - Expanded widget collection to include the new WDS Date Picker Widget. - Introduced new constants for date format options, facilitating diverse formatting choices. - Added support for a new "Date" input type, enhancing input widget configurability. - **Documentation** - Updated property pane configurations to include detailed settings for date format, validation, and event handling. - **Bug Fixes** - Improved handling of derived properties to ensure proper context during widget interactions. These updates collectively improve user experience and flexibility in date selection within the application. > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: > Commit: b17348e03db911501970d2c8a59c4fea30a175e1 > Cypress dashboard. > Tags: `@tag.Anvil` > Spec: >
Fri, 29 Nov 2024 10:44:39 UTC --------- Co-authored-by: Vadim Vaitenko --- .../components/Calendar/src/styles.module.css | 8 +- .../ui/wds/WDSBaseInputWidget/constants.ts | 2 + .../config/defaultsConfig.ts | 6 +- .../WDSDatePickerWidget/config/anvilConfig.ts | 11 + .../config/autocompleteConfig.ts | 11 + .../config/defaultsConfig.ts | 20 ++ .../wds/WDSDatePickerWidget/config/index.ts | 7 + .../WDSDatePickerWidget/config/metaConfig.ts | 21 ++ .../config/methodsConfig.ts | 6 + .../propertyPaneConfig/contentConfig.ts | 191 ++++++++++++++++++ .../config/propertyPaneConfig/index.ts | 1 + .../config/settersConfig.ts | 12 ++ .../ui/wds/WDSDatePickerWidget/constants.ts | 88 ++++++++ .../ui/wds/WDSDatePickerWidget/index.ts | 3 + .../wds/WDSDatePickerWidget/widget/derived.js | 36 ++++ .../widget/derived.test.ts | 64 ++++++ .../wds/WDSDatePickerWidget/widget/helpers.ts | 15 ++ .../wds/WDSDatePickerWidget/widget/index.tsx | 149 ++++++++++++++ .../wds/WDSDatePickerWidget/widget/types.ts | 11 + .../propertyPaneConfig/contentConfig.ts | 4 + .../config/defaultsConfig.ts | 6 +- src/modules/ui-builder/ui/wds/constants.ts | 1 + src/widgets/WidgetUtils.ts | 45 +++++ src/widgets/index.ts | 2 + 24 files changed, 716 insertions(+), 4 deletions(-) create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/anvilConfig.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/autocompleteConfig.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/defaultsConfig.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/index.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/metaConfig.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/methodsConfig.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/propertyPaneConfig/contentConfig.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/propertyPaneConfig/index.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/settersConfig.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/constants.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/index.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/derived.js create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/derived.test.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/helpers.ts create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/index.tsx create mode 100644 src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/types.ts diff --git a/packages/design-system/widgets/src/components/Calendar/src/styles.module.css b/packages/design-system/widgets/src/components/Calendar/src/styles.module.css index 2a51ca6840a9..216afe61510f 100644 --- a/packages/design-system/widgets/src/components/Calendar/src/styles.module.css +++ b/packages/design-system/widgets/src/components/Calendar/src/styles.module.css @@ -61,6 +61,10 @@ } .calendar tbody [role="button"][data-focus-visible] { - outline: var(--border-width-2) solid var(--color-bd-accent); - outline-offset: var(--border-width-2); + --box-shadow-offset: 2px; + + box-shadow: + 0 0 0 var(--box-shadow-offset) var(--color-bg), + 0 0 0 calc(var(--box-shadow-offset) + var(--border-width-2)) + var(--color-bd-focus); } diff --git a/src/modules/ui-builder/ui/wds/WDSBaseInputWidget/constants.ts b/src/modules/ui-builder/ui/wds/WDSBaseInputWidget/constants.ts index 945c8e0b646e..c06a033f2778 100644 --- a/src/modules/ui-builder/ui/wds/WDSBaseInputWidget/constants.ts +++ b/src/modules/ui-builder/ui/wds/WDSBaseInputWidget/constants.ts @@ -6,6 +6,7 @@ export const INPUT_TYPES = { PASSWORD: "PASSWORD", PHONE_NUMBER: "PHONE_NUMBER", MULTI_LINE_TEXT: "MULTI_LINE_TEXT", + DATE: "DATE", } as const; export const INPUT_TYPE_TO_WIDGET_TYPE_MAP = { @@ -16,4 +17,5 @@ export const INPUT_TYPE_TO_WIDGET_TYPE_MAP = { [INPUT_TYPES.MULTI_LINE_TEXT]: "WDS_MULTILINE_INPUT_WIDGET", [INPUT_TYPES.CURRENCY]: "WDS_CURRENCY_INPUT_WIDGET", [INPUT_TYPES.PHONE_NUMBER]: "WDS_PHONE_INPUT_WIDGET", + [INPUT_TYPES.DATE]: "WDS_DATEPICKER_WIDGET", }; diff --git a/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/config/defaultsConfig.ts b/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/config/defaultsConfig.ts index 1efd682612b6..c7309a059207 100644 --- a/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/config/defaultsConfig.ts +++ b/src/modules/ui-builder/ui/wds/WDSCurrencyInputWidget/config/defaultsConfig.ts @@ -1,4 +1,7 @@ -import { WDSBaseInputWidget } from "modules/ui-builder/ui/wds/WDSBaseInputWidget"; +import { + INPUT_TYPES, + WDSBaseInputWidget, +} from "modules/ui-builder/ui/wds/WDSBaseInputWidget"; import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; import type { WidgetDefaultProps } from "WidgetProvider/constants"; @@ -14,4 +17,5 @@ export const defaultsConfig = { showStepArrows: false, label: "Current Price", responsiveBehavior: ResponsiveBehavior.Fill, + inputType: INPUT_TYPES.CURRENCY, } as WidgetDefaultProps; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/anvilConfig.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/anvilConfig.ts new file mode 100644 index 000000000000..dc7fe21e103c --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/anvilConfig.ts @@ -0,0 +1,11 @@ +import type { AnvilConfig } from "WidgetProvider/constants"; + +export const anvilConfig: AnvilConfig = { + isLargeWidget: false, + widgetSize: { + minWidth: { + base: "100%", + "180px": "sizing-30", + }, + }, +}; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/autocompleteConfig.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/autocompleteConfig.ts new file mode 100644 index 000000000000..456e9ec25652 --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/autocompleteConfig.ts @@ -0,0 +1,11 @@ +import { DefaultAutocompleteDefinitions } from "widgets/WidgetUtils"; + +export const autocompleteConfig = { + "!doc": + "Datepicker is used to capture the date and time from a user. It can be used to filter data base on the input date range as well as to capture personal information such as date of birth", + "!url": "https://docs.appsmith.com/widget-reference/datepicker", + isVisible: DefaultAutocompleteDefinitions.isVisible, + selectedDate: "string", + formattedDate: "string", + isDisabled: "bool", +}; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/defaultsConfig.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/defaultsConfig.ts new file mode 100644 index 000000000000..d16c0029829d --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/defaultsConfig.ts @@ -0,0 +1,20 @@ +import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; +import type { WidgetDefaultProps } from "WidgetProvider/constants"; +import { INPUT_TYPES } from "modules/ui-builder/ui/wds/WDSBaseInputWidget"; + +export const defaultsConfig = { + animateLoading: true, + label: "Label", + dateFormat: "YYYY-MM-DD HH:mm", + defaultOptionValue: "", + isRequired: false, + isDisabled: false, + isVisible: true, + isInline: false, + widgetName: "DatePicker", + widgetType: "WDS_DATE_PICKER", + version: 1, + timePrecision: "day", + responsiveBehavior: ResponsiveBehavior.Fill, + inputType: INPUT_TYPES.DATE, +} as unknown as WidgetDefaultProps; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/index.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/index.ts new file mode 100644 index 000000000000..995925903b3f --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/index.ts @@ -0,0 +1,7 @@ +export * from "./propertyPaneConfig"; +export { metaConfig } from "./metaConfig"; +export { anvilConfig } from "./anvilConfig"; +export { defaultsConfig } from "./defaultsConfig"; +export { settersConfig } from "./settersConfig"; +export { methodsConfig } from "./methodsConfig"; +export { autocompleteConfig } from "./autocompleteConfig"; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/metaConfig.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/metaConfig.ts new file mode 100644 index 000000000000..23147fb5b53a --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/metaConfig.ts @@ -0,0 +1,21 @@ +import { WIDGET_TAGS } from "constants/WidgetConstants"; + +export const metaConfig = { + name: "DatePicker", + tags: [WIDGET_TAGS.INPUTS], + needsMeta: true, + searchTags: [ + "datepicker", + "appointment", + "calendar", + "date", + "day", + "hour", + "meeting", + "moment", + "schedule", + "time", + "week", + "year", + ], +}; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/methodsConfig.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/methodsConfig.ts new file mode 100644 index 000000000000..f2434d189307 --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/methodsConfig.ts @@ -0,0 +1,6 @@ +import { DatePickerIcon, DatePickerThumbnail } from "appsmith-icons"; + +export const methodsConfig = { + IconCmp: DatePickerIcon, + ThumbnailCmp: DatePickerThumbnail, +}; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/propertyPaneConfig/contentConfig.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/propertyPaneConfig/contentConfig.ts new file mode 100644 index 000000000000..77a22697323a --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/propertyPaneConfig/contentConfig.ts @@ -0,0 +1,191 @@ +import { ValidationTypes } from "constants/WidgetValidation"; +import { DATE_FORMAT_OPTIONS } from "../../constants"; + +import { propertyPaneContentConfig as WdsInputWidgetPropertyPaneContentConfig } from "modules/ui-builder/ui/wds/WDSInputWidget/config/propertyPaneConfig/contentConfig"; +import type { PropertyPaneConfig } from "constants/PropertyControlConstants"; + +const inputTypeSectionConfig = WdsInputWidgetPropertyPaneContentConfig.find( + (config) => config.sectionName === "Type", +); + +export const propertyPaneContentConfig = [ + inputTypeSectionConfig, + { + sectionName: "Data", + children: [ + { + helpText: "Sets the format of the selected date", + propertyName: "dateFormat", + label: "Date format", + controlType: "DROP_DOWN", + isJSConvertible: true, + optionWidth: "340px", + options: DATE_FORMAT_OPTIONS, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + hideSubText: true, + }, + { + propertyName: "defaultDate", + label: "Default Date", + helpText: + "Sets the default date of the widget. The date is updated if the default date changes", + controlType: "DATE_PICKER", + placeholderText: "Enter Default Date", + useValidationMessage: true, + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.DATE_ISO_STRING }, + }, + { + propertyName: "timePrecision", + label: "Time Precision", + controlType: "DROP_DOWN", + helpText: "Sets the time precision or hides the time picker.", + defaultValue: "day", + options: [ + { + label: "Day", + value: "day", + }, + { + label: "Hour", + value: "hour", + }, + { + label: "Minute", + value: "minute", + }, + { + label: "Second", + value: "second", + }, + ], + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.TEXT, + params: { + allowedValues: ["day", "hour", "minute", "second"], + default: "day", + }, + }, + }, + ], + }, + { + sectionName: "Label", + children: [ + { + helpText: "Sets the label text of the date picker widget", + propertyName: "label", + label: "Text", + controlType: "INPUT_TEXT", + placeholderText: "Label", + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + ], + }, + { + sectionName: "Validations", + children: [ + { + propertyName: "isRequired", + label: "Required", + helpText: "Makes input to the widget mandatory", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "minDate", + label: "Minimum Date", + helpText: "Sets the minimum date that can be selected", + controlType: "DATE_PICKER", + placeholderText: "Enter Minimum Date", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.DATE_ISO_STRING }, + }, + { + propertyName: "maxDate", + label: "Maximum Date", + helpText: "Sets the maximum date that can be selected", + controlType: "DATE_PICKER", + placeholderText: "Enter Maximum Date", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.DATE_ISO_STRING }, + }, + ], + }, + { + sectionName: "General", + children: [ + { + helpText: "Shows help text or details about the current input", + propertyName: "labelTooltip", + label: "Tooltip", + controlType: "INPUT_TEXT", + placeholderText: "", + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + helpText: "Controls the visibility of the widget", + propertyName: "isVisible", + label: "Visible", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "isDisabled", + label: "Disabled", + helpText: "Disables input to this widget", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "animateLoading", + label: "Animate loading", + controlType: "SWITCH", + helpText: "Controls the loading of the widget", + defaultValue: true, + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + ], + }, + { + sectionName: "Events", + children: [ + { + propertyName: "onDateSelected", + label: "onDateSelected", + helpText: "when a date is selected in the calendar", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + }, + ], + }, +] as PropertyPaneConfig[]; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/propertyPaneConfig/index.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/propertyPaneConfig/index.ts new file mode 100644 index 000000000000..7f43d3bde57a --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/propertyPaneConfig/index.ts @@ -0,0 +1 @@ +export { propertyPaneContentConfig } from "./contentConfig"; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/settersConfig.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/settersConfig.ts new file mode 100644 index 000000000000..888f2f9bcb70 --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/config/settersConfig.ts @@ -0,0 +1,12 @@ +export const settersConfig = { + __setters: { + setVisibility: { + path: "isVisible", + type: "boolean", + }, + setDisabled: { + path: "isDisabled", + type: "boolean", + }, + }, +}; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/constants.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/constants.ts new file mode 100644 index 000000000000..3b53c462e713 --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/constants.ts @@ -0,0 +1,88 @@ +import moment from "moment"; +import { SubTextPosition } from "components/constants"; + +export const DATE_FORMAT_OPTIONS = [ + { + label: moment().format("YYYY-MM-DDTHH:mm:ss.sssZ"), + subText: "ISO 8601", + value: "YYYY-MM-DDTHH:mm:ss.sssZ", + }, + { + label: moment().format("LLL"), + subText: "LLL", + value: "LLL", + }, + { + label: moment().format("LL"), + subText: "LL", + value: "LL", + }, + { + label: moment().format("YYYY-MM-DD HH:mm"), + subText: "YYYY-MM-DD HH:mm", + value: "YYYY-MM-DD HH:mm", + }, + { + label: moment().format("YYYY-MM-DDTHH:mm:ss"), + subText: "YYYY-MM-DDTHH:mm:ss", + value: "YYYY-MM-DDTHH:mm:ss", + }, + { + label: moment().format("YYYY-MM-DD hh:mm:ss A"), + subText: "YYYY-MM-DD hh:mm:ss A", + value: "YYYY-MM-DD hh:mm:ss A", + }, + { + label: moment().format("DD/MM/YYYY HH:mm"), + subText: "DD/MM/YYYY HH:mm", + value: "DD/MM/YYYY HH:mm", + }, + { + label: moment().format("D MMMM, YYYY"), + subText: "D MMMM, YYYY", + value: "D MMMM, YYYY", + }, + { + label: moment().format("H:mm A D MMMM, YYYY"), + subText: "H:mm A D MMMM, YYYY", + value: "H:mm A D MMMM, YYYY", + }, + { + label: moment().format("YYYY-MM-DD"), + subText: "YYYY-MM-DD", + value: "YYYY-MM-DD", + }, + { + label: moment().format("MM-DD-YYYY"), + subText: "MM-DD-YYYY", + value: "MM-DD-YYYY", + }, + { + label: moment().format("DD-MM-YYYY"), + subText: "DD-MM-YYYY", + value: "DD-MM-YYYY", + }, + { + label: moment().format("MM/DD/YYYY"), + subText: "MM/DD/YYYY", + value: "MM/DD/YYYY", + }, + { + label: moment().format("DD/MM/YYYY"), + subText: "DD/MM/YYYY", + value: "DD/MM/YYYY", + }, + { + label: moment().format("DD/MM/YY"), + subText: "DD/MM/YY", + value: "DD/MM/YY", + }, + { + label: moment().format("MM/DD/YY"), + subText: "MM/DD/YY", + value: "MM/DD/YY", + }, +].map((x) => ({ + ...x, + subTextPosition: SubTextPosition.BOTTOM, +})); diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/index.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/index.ts new file mode 100644 index 000000000000..8c35d8ef1789 --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/index.ts @@ -0,0 +1,3 @@ +import { WDSDatePickerWidget } from "./widget"; + +export { WDSDatePickerWidget }; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/derived.js b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/derived.js new file mode 100644 index 000000000000..7e4be5805db2 --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/derived.js @@ -0,0 +1,36 @@ +export default { + isValid: (props, moment) => { + const parsedMinDate = new Date(props.minDate); + const parsedMaxDate = new Date(props.maxDate); + const parsedSelectedDate = props.selectedDate + ? moment(new Date(props.selectedDate)) + : null; + + // only do validation when the date is dirty + if (!props.isDirty) { + return true; + } + + if (!parsedSelectedDate && !props.isRequired) { + return true; + } + + if (!parsedSelectedDate && props.isRequired) { + return false; + } + + if (props.minDate && props.maxDate) { + return parsedSelectedDate.isBetween(parsedMinDate, parsedMaxDate); + } + + if (props.minDate) { + return parsedSelectedDate.isAfter(parsedMinDate); + } + + if (props.maxDate) { + return parsedSelectedDate.isBefore(parsedMaxDate); + } + + return true; + }, +}; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/derived.test.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/derived.test.ts new file mode 100644 index 000000000000..48be8c2041ba --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/derived.test.ts @@ -0,0 +1,64 @@ +import moment from "moment"; +import derived from "./derived"; + +describe("isValid function", () => { + const mockMoment = (date: string) => moment(date); + + it("should return true when isDirty is false", () => { + const props = { isDirty: false }; + + expect(derived.isValid(props, mockMoment)).toBe(true); + }); + + it("should return true when selectedDate is null and not required", () => { + const props = { isDirty: true, isRequired: false, selectedDate: null }; + + expect(derived.isValid(props, mockMoment)).toBe(true); + }); + + it("should return false when selectedDate is null and required", () => { + const props = { isDirty: true, isRequired: true, selectedDate: null }; + + expect(derived.isValid(props, mockMoment)).toBe(false); + }); + + it("should return true when selectedDate is between minDate and maxDate", () => { + const props = { + isDirty: true, + minDate: "2023-01-01", + maxDate: "2023-12-31", + selectedDate: "2023-06-15", + }; + + expect(derived.isValid(props, mockMoment)).toBe(true); + }); + + it("should return false when selectedDate is before minDate", () => { + const props = { + isDirty: true, + minDate: "2023-01-01", + selectedDate: "2022-12-31", + }; + + expect(derived.isValid(props, mockMoment)).toBe(false); + }); + + it("should return false when selectedDate is after maxDate", () => { + const props = { + isDirty: true, + maxDate: "2023-12-31", + selectedDate: "2024-01-01", + }; + + expect(derived.isValid(props, mockMoment)).toBe(false); + }); + + it("should return true when selectedDate is valid and no min/max dates are set", () => { + const props = { + isDirty: true, + selectedDate: "2023-06-15", + }; + + expect(derived.isValid(props, mockMoment)).toBe(true); + }); +}); diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/helpers.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/helpers.ts new file mode 100644 index 000000000000..a08a462ac33c --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/helpers.ts @@ -0,0 +1,15 @@ +import type { WDSDatePickerWidgetProps } from "./types"; + +export function validateInput(props: WDSDatePickerWidgetProps) { + if (props.isValid === false) { + return { + validationStatus: "invalid", + errorMessage: "Please select a valid date", + }; + } + + return { + validationStatus: "valid", + errorMessage: "", + }; +} diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/index.tsx b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/index.tsx new file mode 100644 index 000000000000..e36e2a587039 --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/index.tsx @@ -0,0 +1,149 @@ +import React from "react"; +import moment from "moment"; +import BaseWidget from "widgets/BaseWidget"; +import type { WidgetState } from "widgets/BaseWidget"; +import type { + AnvilConfig, + AutocompletionDefinitions, +} from "WidgetProvider/constants"; +import { parseDateTime } from "@internationalized/date"; +import { DatePicker, type DateValue } from "@appsmith/wds"; +import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; + +import * as config from "../config"; +import { validateInput } from "./helpers"; +import derivedPropertyFns from "./derived"; +import type { WDSDatePickerWidgetProps } from "./types"; +import { parseDerivedProperties } from "widgets/WidgetUtils"; + +class WDSDatePickerWidget extends BaseWidget< + WDSDatePickerWidgetProps, + WidgetState +> { + static type = "WDS_DATEPICKER_WIDGET"; + + static getConfig() { + return config.metaConfig; + } + + static getDefaults() { + return config.defaultsConfig; + } + + static getMethods() { + return config.methodsConfig; + } + + static getAnvilConfig(): AnvilConfig | null { + return config.anvilConfig; + } + + static getAutocompleteDefinitions(): AutocompletionDefinitions { + return config.autocompleteConfig; + } + + static getPropertyPaneContentConfig() { + return config.propertyPaneContentConfig; + } + + static getPropertyPaneStyleConfig() { + return []; + } + + static getDerivedPropertiesMap() { + const parsedDerivedProperties = parseDerivedProperties(derivedPropertyFns); + + return { + isValid: `{{(() => {${parsedDerivedProperties.isValid}})()}}`, + selectedDate: `{{ this.value ? moment(this.value).toISOString() : "" }}`, + formattedDate: `{{ this.value ? moment(this.value).format(this.dateFormat) : "" }}`, + }; + } + + static getDefaultPropertiesMap(): Record { + return { + value: "defaultDate", + }; + } + + static getMetaPropertiesMap() { + return { + value: undefined, + isDirty: false, + }; + } + + static getStylesheetConfig() { + return {}; + } + + static getSetterConfig() { + return config.settersConfig; + } + + static getDependencyMap() { + return {}; + } + + componentDidUpdate(prevProps: WDSDatePickerWidgetProps): void { + if (!this.shouldResetDirtyState(prevProps)) { + return; + } + + this.resetDirtyState(); + } + + handleDateChange = (date: DateValue) => { + if (!this.props.isDirty) { + this.props.updateWidgetMetaProperty("isDirty", true); + } + + this.props.updateWidgetMetaProperty("value", date.toString(), { + triggerPropertyName: "onDateSelected", + dynamicString: this.props.onDateSelected, + event: { + type: EventType.ON_DATE_SELECTED, + }, + }); + }; + + private shouldResetDirtyState(prevProps: WDSDatePickerWidgetProps): boolean { + const { defaultDate, isDirty } = this.props; + const hasDefaultDateChanged = defaultDate !== prevProps.defaultDate; + + return hasDefaultDateChanged && isDirty; + } + + private resetDirtyState() { + this.props.updateWidgetMetaProperty("isDirty", false); + } + + private parseDate(date: string | undefined) { + return date + ? parseDateTime(moment(date).format("YYYY-MM-DDTHH:mm:ss")) + : undefined; + } + + getWidgetView() { + const { label, labelTooltip, maxDate, minDate, value, ...rest } = + this.props; + const { errorMessage, validationStatus } = validateInput(this.props); + + return ( + + ); + } +} + +export { WDSDatePickerWidget }; diff --git a/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/types.ts b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/types.ts new file mode 100644 index 000000000000..1fa58c814588 --- /dev/null +++ b/src/modules/ui-builder/ui/wds/WDSDatePickerWidget/widget/types.ts @@ -0,0 +1,11 @@ +import type { WidgetProps } from "widgets/BaseWidget"; + +export interface WDSDatePickerWidgetProps extends WidgetProps { + selectedDate: string; + defaultDate: string; + onDateSelected: string; + isRequired?: boolean; + isDisabled?: boolean; + label: string; + labelTooltip?: string; +} diff --git a/src/modules/ui-builder/ui/wds/WDSInputWidget/config/propertyPaneConfig/contentConfig.ts b/src/modules/ui-builder/ui/wds/WDSInputWidget/config/propertyPaneConfig/contentConfig.ts index 175acb771c0a..e40aa0007b0e 100644 --- a/src/modules/ui-builder/ui/wds/WDSInputWidget/config/propertyPaneConfig/contentConfig.ts +++ b/src/modules/ui-builder/ui/wds/WDSInputWidget/config/propertyPaneConfig/contentConfig.ts @@ -48,6 +48,10 @@ export const propertyPaneContentConfig = [ label: "Currency", value: "CURRENCY", }, + { + label: "Date", + value: "DATE", + }, ], isBindProperty: false, isTriggerProperty: false, diff --git a/src/modules/ui-builder/ui/wds/WDSPhoneInputWidget/config/defaultsConfig.ts b/src/modules/ui-builder/ui/wds/WDSPhoneInputWidget/config/defaultsConfig.ts index 1d85047574a5..9edf9bee89b1 100644 --- a/src/modules/ui-builder/ui/wds/WDSPhoneInputWidget/config/defaultsConfig.ts +++ b/src/modules/ui-builder/ui/wds/WDSPhoneInputWidget/config/defaultsConfig.ts @@ -1,4 +1,7 @@ -import { WDSBaseInputWidget } from "modules/ui-builder/ui/wds/WDSBaseInputWidget"; +import { + INPUT_TYPES, + WDSBaseInputWidget, +} from "modules/ui-builder/ui/wds/WDSBaseInputWidget"; import { ResponsiveBehavior } from "layoutSystems/common/utils/constants"; import type { WidgetDefaultProps } from "WidgetProvider/constants"; @@ -13,4 +16,5 @@ export const defaultsConfig = { allowFormatting: true, responsiveBehavior: ResponsiveBehavior.Fill, label: "Phone number", + inputType: INPUT_TYPES.PHONE_NUMBER, } as WidgetDefaultProps; diff --git a/src/modules/ui-builder/ui/wds/constants.ts b/src/modules/ui-builder/ui/wds/constants.ts index 8f897732e6b0..9452b5af8e30 100644 --- a/src/modules/ui-builder/ui/wds/constants.ts +++ b/src/modules/ui-builder/ui/wds/constants.ts @@ -61,6 +61,7 @@ export const WDS_V2_WIDGET_MAP = { MULTILINE_INPUT_WIDGET: "WDS_MULTILINE_INPUT_WIDGET", WDS_SELECT_WIDGET: "WDS_SELECT_WIDGET", WDS_COMBOBOX_WIDGET: "WDS_COMBOBOX_WIDGET", + WDS_DATEPICKER_WIDGET: "WDS_DATEPICKER_WIDGET", // Anvil layout widgets ZONE_WIDGET: anvilWidgets.ZONE_WIDGET, diff --git a/src/widgets/WidgetUtils.ts b/src/widgets/WidgetUtils.ts index 5184b0c73d2a..286d5eeaafa1 100644 --- a/src/widgets/WidgetUtils.ts +++ b/src/widgets/WidgetUtils.ts @@ -989,3 +989,48 @@ export const checkForOnClick = (e: React.MouseEvent) => { return false; }; + +/** + * Parses the derived properties from the given property functions. Used in getDerivedPropertiesMap + * + * @example + * ```js + * { + * isValidDate: (props, moment, _) => { + * return props.value === 1; + * } + * ``` + * + * It will return + * ```js + * { + * isValidDate: "{{ this.value === 1 }}" + * } + * ``` + * + * Main rule to remember is don't deconstruct the props like `const { value } = props;` in the derived property function. + * Directly access props like `props.value` + */ +export function parseDerivedProperties(propertyFns: Record) { + const derivedProperties: Record = {}; + + for (const [key, value] of Object.entries(propertyFns)) { + if (typeof value === "function") { + const functionString = value.toString(); + const functionBody = functionString.match(/(?<=\{)(.|\n)*(?=\})/)?.[0]; + + if (functionBody) { + const paramMatch = functionString.match(/\((.*?),/); + const propsParam = paramMatch ? paramMatch[1].trim() : "props"; + + const modifiedBody = functionBody + .trim() + .replace(new RegExp(`${propsParam}\\.`, "g"), "this."); + + derivedProperties[key] = modifiedBody; + } + } + } + + return derivedProperties; +} diff --git a/src/widgets/index.ts b/src/widgets/index.ts index c1e4cb2e52ca..c9e25d732745 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -88,6 +88,7 @@ import { WDSNumberInputWidget } from "modules/ui-builder/ui/wds/WDSNumberInputWi import { WDSMultilineInputWidget } from "modules/ui-builder/ui/wds/WDSMultilineInputWidget"; import { WDSSelectWidget } from "modules/ui-builder/ui/wds/WDSSelectWidget"; import { EEWDSWidgets } from "ee/modules/ui-builder/ui/wds"; +import { WDSDatePickerWidget } from "modules/ui-builder/ui/wds/WDSDatePickerWidget"; const LegacyWidgets = [ CanvasWidget, @@ -185,6 +186,7 @@ const WDSWidgets = [ WDSNumberInputWidget, WDSMultilineInputWidget, WDSSelectWidget, + WDSDatePickerWidget, ]; const Widgets = [