diff --git a/app/client/cypress/fixtures/SwitchGroupWidgetDsl.json b/app/client/cypress/fixtures/SwitchGroupWidgetDsl.json
new file mode 100644
index 0000000000..acc93d0818
--- /dev/null
+++ b/app/client/cypress/fixtures/SwitchGroupWidgetDsl.json
@@ -0,0 +1,65 @@
+{
+ "dsl": {
+ "widgetName": "MainContainer",
+ "backgroundColor": "none",
+ "rightColumn": 1206.35,
+ "snapColumns": 64,
+ "detachFromLayout": true,
+ "widgetId": "0",
+ "topRow": 0,
+ "bottomRow": 710,
+ "containerStyle": "none",
+ "snapRows": 125,
+ "parentRowSpace": 1,
+ "type": "CANVAS_WIDGET",
+ "canExtend": true,
+ "version": 45,
+ "minHeight": 690,
+ "parentColumnSpace": 1,
+ "dynamicBindingPathList": [],
+ "leftColumn": 0,
+ "children": [
+ {
+ "widgetName": "SwitchGroup1",
+ "isCanvas": false,
+ "displayName": "Switch Group",
+ "iconSVG": "/static/media/icon.086a7201.svg",
+ "topRow": 11,
+ "bottomRow": 17,
+ "parentRowSpace": 10,
+ "type": "SWITCH_GROUP_WIDGET",
+ "hideCard": false,
+ "parentColumnSpace": 18.66171875,
+ "leftColumn": 6,
+ "options": [
+ {
+ "label": "Blue",
+ "value": "BLUE"
+ },
+ {
+ "label": "Green",
+ "value": "GREEN"
+ },
+ {
+ "label": "Red",
+ "value": "RED"
+ }
+ ],
+ "isDisabled": false,
+ "key": "4dddg71qr8",
+ "isRequired": false,
+ "rightColumn": 24,
+ "defaultSelectedValues": [
+ "BLUE"
+ ],
+ "widgetId": "lexxd4wyb1",
+ "isVisible": true,
+ "version": 1,
+ "parentId": "0",
+ "renderMode": "CANVAS",
+ "isLoading": false,
+ "isInline": true
+ }
+ ]
+}
+}
diff --git a/app/client/cypress/fixtures/newFormDsl.json b/app/client/cypress/fixtures/newFormDsl.json
index bf53a552a9..fde4b70297 100644
--- a/app/client/cypress/fixtures/newFormDsl.json
+++ b/app/client/cypress/fixtures/newFormDsl.json
@@ -307,6 +307,45 @@
"bottomRow": 17,
"parentId": "kipfh779n5",
"widgetId": "zbzrlbiose"
+ },
+ {
+ "widgetName": "SwitchGroup1",
+ "isCanvas": false,
+ "displayName": "Switch Group",
+ "iconSVG": "/static/media/icon.086a7201.svg",
+ "topRow": 8,
+ "bottomRow": 14,
+ "parentRowSpace": 10,
+ "type": "SWITCH_GROUP_WIDGET",
+ "hideCard": false,
+ "parentColumnSpace": 6.6856445312499995,
+ "leftColumn": 2,
+ "options": [
+ {
+ "label": "Blue",
+ "value": "BLUE"
+ },
+ {
+ "label": "Green",
+ "value": "GREEN"
+ },
+ {
+ "label": "Red",
+ "value": "RED"
+ }
+ ],
+ "isDisabled": false,
+ "key": "vrhzyvir7s",
+ "isRequired": false,
+ "rightColumn": 20,
+ "defaultSelectedValues": "BLUE",
+ "widgetId": "egtjazr1wh",
+ "isVisible": true,
+ "version": 1,
+ "parentId": "xbbqrz2ncf",
+ "renderMode": "CANVAS",
+ "isLoading": false,
+ "isInline": true
}
],
"widgetId": "e3tq9qwta6",
diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js
new file mode 100644
index 0000000000..497326be87
--- /dev/null
+++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/SwitchGroup_spec.js
@@ -0,0 +1,103 @@
+const commonlocators = require("../../../../locators/commonlocators.json");
+const formWidgetsPage = require("../../../../locators/FormWidgets.json");
+const modalWidgetPage = require("../../../../locators/ModalWidget.json");
+const publish = require("../../../../locators/publishWidgetspage.json");
+const dsl = require("../../../../fixtures/SwitchGroupWidgetDsl.json");
+
+describe("Switch Group Widget Functionality", function() {
+ before(() => {
+ cy.addDsl(dsl);
+ });
+
+ beforeEach(() => {
+ cy.openPropertyPane("switchgroupwidget");
+ });
+
+ afterEach(() => {
+ cy.goToEditFromPublish();
+ });
+
+ it("Widget name changes", function() {
+ /**
+ * @param{Text} Random Text
+ * @param{RadioWidget}Mouseover
+ * @param{RadioPre Css} Assertion
+ */
+
+ cy.widgetText(
+ "switchgrouptest",
+ formWidgetsPage.switchGroupWidget,
+ formWidgetsPage.switchGroupInput,
+ );
+ cy.closePropertyPane();
+ });
+
+ it("Property: options", function() {
+ // Add a new option
+ const optionToAdd = { label: "Yellow", value: "YELLOW" };
+ cy.get(".t--property-control-options .CodeMirror textarea")
+ .first()
+ .focus({ force: true })
+ .type("{ctrl}{end}", { force: true })
+ .type("{ctrl}{uparrow}", { force: true })
+ .type("{end}", { force: true })
+ .type(",{enter}")
+ .type(JSON.stringify(optionToAdd), {
+ parseSpecialCharSequences: false,
+ });
+ // Assert
+ cy.get(formWidgetsPage.labelSwitchGroup)
+ .should("have.length", 4)
+ .eq(3)
+ .contains("Yellow");
+ cy.closePropertyPane();
+ });
+
+ it("Property: defaultSelectedValues", function() {
+ // Add a new option
+ const valueToAdd = "GREEN";
+ cy.get(".t--property-control-defaultselectedvalues .CodeMirror textarea")
+ .first()
+ .focus({ force: true })
+ .type("{ctrl}{end}", { force: true })
+ .type("{ctrl}{uparrow}", { force: true })
+ .type("{end}", { force: true })
+ .type(",{enter}")
+ .type(`"${valueToAdd}"`);
+ // Assert
+ cy.get(`${formWidgetsPage.labelSwitchGroup} input:checked`)
+ .should("have.length", 2)
+ .eq(1)
+ .parent()
+ .contains("Green");
+ cy.closePropertyPane();
+ });
+
+ it("Property: isVisible === FALSE", function() {
+ cy.togglebarDisable(commonlocators.visibleCheckbox);
+ cy.PublishtheApp();
+ cy.get(publish.switchGroupWidget + " " + "input").should("not.exist");
+ });
+
+ it("Property: isVisible === TRUE", function() {
+ cy.togglebar(commonlocators.visibleCheckbox);
+ cy.PublishtheApp();
+ cy.get(publish.switchGroupWidget + " " + "input")
+ .eq(0)
+ .should("exist");
+ });
+
+ it("Property: onSelectionChange", function() {
+ // create an alert modal and verify its name
+ cy.createModal(this.data.ModalName);
+ cy.PublishtheApp();
+ cy.get(publish.switchGroupWidget + " " + "label.bp3-switch")
+ .children()
+ .first()
+ .click({ force: true });
+ cy.get(modalWidgetPage.modelTextField).should(
+ "have.text",
+ this.data.ModalName,
+ );
+ });
+});
diff --git a/app/client/cypress/locators/FormWidgets.json b/app/client/cypress/locators/FormWidgets.json
index 1f2b0bc8fc..b49525cffe 100644
--- a/app/client/cypress/locators/FormWidgets.json
+++ b/app/client/cypress/locators/FormWidgets.json
@@ -8,6 +8,7 @@
"dropdownSelectionType": ".t--property-control-selectiontype .bp3-popover-target",
"radioWidget": ".t--draggable-radiogroupwidget",
"checkboxGroupWidget": ".t--draggable-checkboxgroupwidget",
+ "switchGroupWidget": ".t--draggable-switchgroupwidget",
"radioOnSelectionChangeDropdown": ".t--property-control-onselectionchange",
"nextDayBtn": ".DayPicker-Day[aria-selected='true'] + div.DayPicker-Day",
"datepickerWidget": ".t--draggable-datepickerwidget",
@@ -28,6 +29,7 @@
"treeSelectInput": ".rc-tree-select-selection-search-input",
"labelradio": ".t--draggable-radiogroupwidget label",
"labelCheckboxGroup": ".t--draggable-checkboxgroupwidget label",
+ "labelSwitchGroup": ".t--draggable-switchgroupwidget label",
"deleteradiovalue": ".t--property-control-options svg.remixicon-icon ",
"inputRadio": ".t--draggable-radiogroupwidget input",
"defaultSelect": ".t--property-control-defaultselectedvalue .CodeMirror-code",
@@ -35,6 +37,7 @@
"formInner": ".t--draggable-formwidget span.t--widget-name",
"radioInput": ".t--draggable-radiogroupwidget span.t--widget-name",
"checkboxGroupInput": ".t--draggable-checkboxgroupwidget span.t--widget-name",
+ "switchGroupInput": ".t--draggable-switchgroupwidget span.t--widget-name",
"radioAddButton": ".t--property-control-options button",
"formD": "div[type='FORM_WIDGET']",
"datepickerFooter": ".bp3-datepicker-footer span",
diff --git a/app/client/cypress/locators/publishWidgetspage.json b/app/client/cypress/locators/publishWidgetspage.json
index db8943f523..34c5fb661d 100644
--- a/app/client/cypress/locators/publishWidgetspage.json
+++ b/app/client/cypress/locators/publishWidgetspage.json
@@ -9,6 +9,7 @@
"switchwidget": ".t--widget-switchwidget",
"radioWidget": ".t--widget-radiogroupwidget",
"checkboxGroupWidget": ".t--widget-checkboxgroupwidget",
+ "switchGroupWidget": ".t--widget-switchgroupwidget",
"formWidget": ".t--widget-formwidget",
"imageWidget": ".t--widget-imagewidget",
"dropdownWidget": ".t--widget-dropdownwidget",
diff --git a/app/client/package.json b/app/client/package.json
index 6f4e2113d2..4b0bfec42a 100644
--- a/app/client/package.json
+++ b/app/client/package.json
@@ -146,7 +146,7 @@
"tinycolor2": "^1.4.2",
"toposort": "^2.0.2",
"ts-loader": "^6.0.4",
- "tslib": "^2.1.0",
+ "tslib": "^2.3.1",
"typescript": "^4.1.3",
"unescape-js": "^1.1.4",
"url-search-params-polyfill": "^8.0.0",
diff --git a/app/client/src/assets/icons/widget/switch-group.svg b/app/client/src/assets/icons/widget/switch-group.svg
new file mode 100755
index 0000000000..fcaa891f23
--- /dev/null
+++ b/app/client/src/assets/icons/widget/switch-group.svg
@@ -0,0 +1,9 @@
+
diff --git a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx
index 1191abffad..b7368bdeb5 100644
--- a/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx
+++ b/app/client/src/constants/AppsmithActionConstants/ActionConstants.tsx
@@ -90,6 +90,7 @@ export enum EventType {
ON_LIST_PAGE_CHANGE = "ON_LIST_PAGE_CHANGE",
ON_RECORDING_START = "ON_RECORDING_START",
ON_RECORDING_COMPLETE = "ON_RECORDING_COMPLETE",
+ ON_SWITCH_GROUP_SELECTION_CHANGE = "ON_SWITCH_GROUP_SELECTION_CHANGE",
}
export interface PageAction {
diff --git a/app/client/src/constants/HelpConstants.ts b/app/client/src/constants/HelpConstants.ts
index 25be69f731..ba079d7b7a 100644
--- a/app/client/src/constants/HelpConstants.ts
+++ b/app/client/src/constants/HelpConstants.ts
@@ -167,6 +167,10 @@ export const HelpMap: Record = {
path: "/widget-reference/audio-recorder",
searchKey: "Audio Recorder",
},
+ SWITCH_GROUP_WIDGET: {
+ path: "/widget-reference/switch-group",
+ searchKey: "Switch Group",
+ },
};
export const HelpBaseURL = "https://docs.appsmith.com";
diff --git a/app/client/src/icons/WidgetIcons.tsx b/app/client/src/icons/WidgetIcons.tsx
index f779ef22f7..b0dbbbae45 100644
--- a/app/client/src/icons/WidgetIcons.tsx
+++ b/app/client/src/icons/WidgetIcons.tsx
@@ -34,6 +34,7 @@ import { ReactComponent as StatboxIcon } from "assets/icons/widget/statbox.svg";
import { ReactComponent as CheckboxGroupIcon } from "assets/icons/widget/checkbox-group.svg";
import { ReactComponent as AudioRecorderIcon } from "assets/icons/widget/audio-recorder.svg";
import { ReactComponent as ButtonGroupIcon } from "assets/icons/widget/button-group.svg";
+import { ReactComponent as SwitchGroupIcon } from "assets/icons/widget/switch-group.svg";
import styled from "styled-components";
import { Colors } from "constants/Colors";
@@ -225,6 +226,11 @@ export const WidgetIcons: {
),
+ SWITCH_GROUP_WIDGET: (props: IconProps) => (
+
+
+
+ ),
};
export type WidgetIcon = typeof WidgetIcons[keyof typeof WidgetIcons];
diff --git a/app/client/src/utils/AppsmithUtils.test.ts b/app/client/src/utils/AppsmithUtils.test.ts
new file mode 100644
index 0000000000..57fe7efb7b
--- /dev/null
+++ b/app/client/src/utils/AppsmithUtils.test.ts
@@ -0,0 +1,13 @@
+import { getCamelCaseString } from "utils/AppsmithUtils";
+
+describe("getCamelCaseString", () => {
+ it("Should return a string in camelCase", () => {
+ const inputs = ["abcd", "ab12cd", "开关", "😃 😃 😃"];
+ const expected = ["abcd", "ab12Cd", "", ""];
+
+ inputs.forEach((input, index) => {
+ const result = getCamelCaseString(input);
+ expect(result).toStrictEqual(expected[index]);
+ });
+ });
+});
diff --git a/app/client/src/utils/AppsmithUtils.tsx b/app/client/src/utils/AppsmithUtils.tsx
index 9d1d5a2ad4..83b110785c 100644
--- a/app/client/src/utils/AppsmithUtils.tsx
+++ b/app/client/src/utils/AppsmithUtils.tsx
@@ -372,6 +372,26 @@ export const parseBlobUrl = (blobId: string) => {
};
/**
+ * Convert a string into camelCase
+ * @param sourceString input string
+ * @returns camelCase string
+ */
+export const getCamelCaseString = (sourceString: string) => {
+ let out = "";
+ // Split the input string to separate words using RegEx
+ const regEx = /[A-Z\xC0-\xD6\xD8-\xDE]?[a-z\xDF-\xF6\xF8-\xFF]+|[A-Z\xC0-\xD6\xD8-\xDE]+(?![a-z\xDF-\xF6\xF8-\xFF])|\d+/g;
+ const words = sourceString.match(regEx);
+ if (words) {
+ words.forEach(function(el, idx) {
+ const add = el.toLowerCase();
+ out += idx === 0 ? add : add[0].toUpperCase() + add.slice(1);
+ });
+ }
+
+ return out;
+};
+
+/*
* gets the page url
*
* Note: for edit mode, the page will have different url ( contains '/edit' at the end )
diff --git a/app/client/src/utils/WidgetRegistry.tsx b/app/client/src/utils/WidgetRegistry.tsx
index ba1fabdbee..bd3975dfe4 100644
--- a/app/client/src/utils/WidgetRegistry.tsx
+++ b/app/client/src/utils/WidgetRegistry.tsx
@@ -93,7 +93,6 @@ import FilePickerWidgetV2, {
import AudioWidget, {
CONFIG as AUDIO_WIDGET_CONFIG,
} from "widgets/AudioWidget";
-
import AudioRecorderWidget, {
CONFIG as AUDIO_RECORDER_WIDGET_CONFIG,
} from "widgets/AudioRecorderWidget";
@@ -103,14 +102,17 @@ import DocumentViewerWidget, {
import ButtonGroupWidget, {
CONFIG as BUTTON_GROUP_CONFIG,
} from "widgets/ButtonGroupWidget";
-
-import log from "loglevel";
import SingleSelectTreeWidget, {
CONFIG as SINGLE_SELECT_TREE_WIDGET_CONFIG,
} from "widgets/SingleSelectTreeWidget";
import MultiSelectTreeWidget, {
CONFIG as MULTI_SELECT_TREE_WIDGET_CONFIG,
} from "widgets/MultiSelectTreeWidget";
+import SwitchGroupWidget, {
+ CONFIG as SWITCH_GROUP_WIDGET_CONFIG,
+} from "widgets/SwitchGroupWidget";
+
+import log from "loglevel";
export const registerWidgets = () => {
const start = performance.now();
@@ -154,6 +156,7 @@ export const registerWidgets = () => {
registerWidget(ButtonGroupWidget, BUTTON_GROUP_CONFIG);
registerWidget(MultiSelectTreeWidget, MULTI_SELECT_TREE_WIDGET_CONFIG);
registerWidget(SingleSelectTreeWidget, SINGLE_SELECT_TREE_WIDGET_CONFIG);
+ registerWidget(SwitchGroupWidget, SWITCH_GROUP_WIDGET_CONFIG);
registerWidget(AudioWidget, AUDIO_WIDGET_CONFIG);
log.debug("Widget registration took: ", performance.now() - start, "ms");
diff --git a/app/client/src/utils/autocomplete/EntityDefinitions.ts b/app/client/src/utils/autocomplete/EntityDefinitions.ts
index 2263e985a6..89c116e1e8 100644
--- a/app/client/src/utils/autocomplete/EntityDefinitions.ts
+++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts
@@ -422,6 +422,12 @@ export const entityDefinitions: Record = {
dataURL: "string",
rawBinary: "string",
},
+ SWITCH_GROUP_WIDGET: {
+ "!doc":
+ "Switch group widget allows users to create many switch components which can easily by used in a form",
+ "!url": "https://docs.appsmith.com/widget-reference/switch-group",
+ selectedValues: "[string]",
+ },
};
export const GLOBAL_DEFS = {
diff --git a/app/client/src/widgets/SwitchGroupWidget/component/index.tsx b/app/client/src/widgets/SwitchGroupWidget/component/index.tsx
new file mode 100644
index 0000000000..4bf8ee01b4
--- /dev/null
+++ b/app/client/src/widgets/SwitchGroupWidget/component/index.tsx
@@ -0,0 +1,143 @@
+import React from "react";
+import styled from "styled-components";
+import { Alignment, Switch } from "@blueprintjs/core";
+
+import { ThemeProp } from "components/ads/common";
+import { BlueprintControlTransform } from "constants/DefaultTheme";
+import { Colors } from "constants/Colors";
+
+export interface OptionProps {
+ label?: string;
+ value: string;
+}
+
+export interface SwitchGroupContainerProps {
+ alignment: Alignment;
+ inline?: boolean;
+ optionCount: number;
+ valid?: boolean;
+}
+
+export const SwitchGroupContainer = styled.div<
+ ThemeProp & SwitchGroupContainerProps
+>`
+ display: ${({ inline }) => (inline ? "inline-flex" : "flex")};
+ ${({ alignment, inline }) => `
+ flex-direction: ${inline ? "row" : "column"};
+ align-items: ${
+ inline
+ ? "center"
+ : alignment === Alignment.LEFT
+ ? "flex-start"
+ : "flex-end"
+ };
+ ${inline && "flex-wrap: wrap"};
+ `}
+ justify-content: ${({ alignment, inline, optionCount }) =>
+ optionCount > 1
+ ? `space-between`
+ : inline
+ ? alignment === Alignment.LEFT
+ ? `flex-start`
+ : `flex-end`
+ : `center`};
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ border: 1px solid transparent;
+ ${({ theme, valid }) =>
+ !valid &&
+ `
+ border: 1px solid ${theme.colors.error};
+ `}
+ padding: 2px 4px;
+
+ ${BlueprintControlTransform}
+`;
+
+export interface StyledSwitchProps {
+ disabled?: boolean;
+ inline?: boolean;
+ optionCount: number;
+ rowSpace: number;
+}
+
+const StyledSwitch = styled(Switch)`
+ height: ${({ rowSpace }) => rowSpace}px;
+
+ &.bp3-control.bp3-switch {
+ margin-top: ${({ inline, optionCount }) =>
+ (inline || optionCount === 1) && `4px`};
+ ${({ alignIndicator }) =>
+ alignIndicator === Alignment.RIGHT && `margin-right: 0`};
+ input:checked ~ .bp3-control-indicator,
+ &:hover input:checked ~ .bp3-control-indicator {
+ background-color: ${Colors.GREEN};
+ }
+ }
+`;
+
+function SwitchGroupComponent(props: SwitchGroupComponentProps) {
+ const {
+ alignment,
+ disabled,
+ inline,
+ onChange,
+ options,
+ rowSpace,
+ selected,
+ valid,
+ } = props;
+
+ return (
+
+ {Array.isArray(options) &&
+ options.length > 0 &&
+ options.map((option: OptionProps) => (
+
+ ))}
+
+ );
+}
+
+export interface Item {
+ widgetId: string;
+ id: string;
+ index: number;
+ isVisible?: boolean;
+ isDisabled?: boolean;
+ label?: string;
+ value: string;
+ alignIndicator?: Alignment;
+ defaultChecked?: boolean;
+ checked?: boolean;
+}
+
+export interface SwitchGroupComponentProps {
+ alignment: Alignment;
+ disabled?: boolean;
+ inline?: boolean;
+ options: OptionProps[];
+ onChange: (value: string) => React.FormEventHandler;
+ required?: boolean;
+ rowSpace: number;
+ selected: string[];
+ valid?: boolean;
+}
+
+export default SwitchGroupComponent;
diff --git a/app/client/src/widgets/SwitchGroupWidget/icon.svg b/app/client/src/widgets/SwitchGroupWidget/icon.svg
new file mode 100755
index 0000000000..fcaa891f23
--- /dev/null
+++ b/app/client/src/widgets/SwitchGroupWidget/icon.svg
@@ -0,0 +1,9 @@
+
diff --git a/app/client/src/widgets/SwitchGroupWidget/index.ts b/app/client/src/widgets/SwitchGroupWidget/index.ts
new file mode 100644
index 0000000000..964f38f26f
--- /dev/null
+++ b/app/client/src/widgets/SwitchGroupWidget/index.ts
@@ -0,0 +1,38 @@
+import { Alignment } from "@blueprintjs/core";
+
+import Widget from "./widget";
+import IconSVG from "./icon.svg";
+import { GRID_DENSITY_MIGRATION_V1 } from "widgets/constants";
+
+export const CONFIG = {
+ type: Widget.getWidgetType(),
+ name: "Switch Group", // The display name which will be made in uppercase and show in the widgets panel ( can have spaces )
+ iconSVG: IconSVG,
+ needsMeta: true, // Defines if this widget adds any meta properties
+ isCanvas: false, // Defines if this widget has a canvas within in which we can drop other widgets
+ defaults: {
+ widgetName: "SwitchGroup",
+ rows: 1.5 * GRID_DENSITY_MIGRATION_V1,
+ columns: 4.5 * GRID_DENSITY_MIGRATION_V1,
+ options: [
+ { label: "Blue", value: "BLUE" },
+ { label: "Green", value: "GREEN" },
+ { label: "Red", value: "RED" },
+ ],
+ defaultSelectedValues: ["BLUE"],
+ isDisabled: false,
+ isRequired: false,
+ isInline: true,
+ isVisible: true,
+ alignment: Alignment.LEFT,
+ version: 1,
+ },
+ properties: {
+ derived: Widget.getDerivedPropertiesMap(),
+ default: Widget.getDefaultPropertiesMap(),
+ meta: Widget.getMetaPropertiesMap(),
+ config: Widget.getPropertyPaneConfig(),
+ },
+};
+
+export default Widget;
diff --git a/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx b/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx
new file mode 100644
index 0000000000..e8617fbc8f
--- /dev/null
+++ b/app/client/src/widgets/SwitchGroupWidget/widget/index.tsx
@@ -0,0 +1,254 @@
+import React from "react";
+import { Alignment } from "@blueprintjs/core";
+
+import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget";
+import { DerivedPropertiesMap } from "utils/WidgetFactory";
+import { ValidationTypes } from "constants/WidgetValidation";
+import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
+import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
+
+import SwitchGroupComponent, { OptionProps } from "../component";
+
+class SwitchGroupWidget extends BaseWidget<
+ SwitchGroupWidgetProps,
+ WidgetState
+> {
+ static getPropertyPaneConfig() {
+ return [
+ {
+ sectionName: "General",
+ children: [
+ {
+ helpText:
+ "Displays a list of options for a user to select. Values must be unique",
+ propertyName: "options",
+ label: "Options",
+ controlType: "INPUT_TEXT",
+ placeholderText: '[{ "label": "Option1", "value": "Option2" }]',
+ isBindProperty: true,
+ isTriggerProperty: false,
+ validation: {
+ type: ValidationTypes.ARRAY,
+ params: {
+ children: {
+ type: ValidationTypes.OBJECT,
+ params: {
+ allowedKeys: [
+ {
+ name: "label",
+ type: ValidationTypes.TEXT,
+ params: {
+ default: "",
+ required: true,
+ unique: true,
+ },
+ },
+ {
+ name: "value",
+ type: ValidationTypes.TEXT,
+ params: {
+ default: "",
+ required: true,
+ unique: true,
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ evaluationSubstitutionType:
+ EvaluationSubstitutionType.SMART_SUBSTITUTE,
+ },
+ {
+ helpText:
+ "Selects values of the options checked by default. Enter comma separated values for multiple selected",
+ propertyName: "defaultSelectedValues",
+ label: "Default Selected Values",
+ placeholderText: "Enter option values",
+ controlType: "INPUT_TEXT",
+ isBindProperty: true,
+ isTriggerProperty: false,
+ validation: {
+ type: ValidationTypes.ARRAY,
+ params: {
+ default: [],
+ children: {
+ type: ValidationTypes.TEXT,
+ },
+ strict: true,
+ },
+ },
+ },
+ {
+ propertyName: "isInline",
+ helpText:
+ "Whether switches are to be displayed inline horizontally",
+ label: "Inline",
+ controlType: "SWITCH",
+ isJSConvertible: true,
+ isBindProperty: true,
+ isTriggerProperty: false,
+ validation: { type: ValidationTypes.BOOLEAN },
+ },
+ {
+ propertyName: "isRequired",
+ label: "Required",
+ helpText: "Makes input to the widget mandatory",
+ controlType: "SWITCH",
+ isJSConvertible: true,
+ isBindProperty: true,
+ isTriggerProperty: false,
+ validation: { type: ValidationTypes.BOOLEAN },
+ },
+ {
+ propertyName: "isVisible",
+ helpText: "Controls the visibility of the widget",
+ label: "Visible",
+ controlType: "SWITCH",
+ isJSConvertible: true,
+ isBindProperty: true,
+ isTriggerProperty: false,
+ validation: { type: ValidationTypes.BOOLEAN },
+ },
+ {
+ propertyName: "isDisabled",
+ helpText: "Disables input to the widget",
+ label: "Disabled",
+ controlType: "SWITCH",
+ isJSConvertible: true,
+ isBindProperty: true,
+ isTriggerProperty: false,
+ validation: { type: ValidationTypes.BOOLEAN },
+ },
+ {
+ propertyName: "alignment",
+ helpText: "Sets the alignment of the widget",
+ label: "Alignment",
+ controlType: "DROP_DOWN",
+ isBindProperty: true,
+ isTriggerProperty: false,
+ options: [
+ {
+ label: "Left",
+ value: Alignment.LEFT,
+ },
+ {
+ label: "Right",
+ value: Alignment.RIGHT,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ sectionName: "Actions",
+ children: [
+ {
+ helpText:
+ "Triggers an action when a switch state inside the group is changed",
+ propertyName: "onSelectionChange",
+ label: "onSelectionChange",
+ controlType: "ACTION_SELECTOR",
+ isJSConvertible: true,
+ isBindProperty: true,
+ isTriggerProperty: true,
+ },
+ ],
+ },
+ ];
+ }
+
+ static getDefaultPropertiesMap(): Record {
+ return {
+ selectedValuesArray: "defaultSelectedValues",
+ };
+ }
+
+ static getMetaPropertiesMap(): Record {
+ return {
+ selectedValuesArray: undefined,
+ };
+ }
+
+ static getDerivedPropertiesMap(): DerivedPropertiesMap {
+ return {
+ isValid: `{{ this.isRequired ? !!this.selectedValues.length : true }}`,
+ selectedValues: `{{
+ this.selectedValuesArray.filter(
+ selectedValue => this.options.map(option => option.value).includes(selectedValue)
+ )
+ }}`,
+ };
+ }
+
+ static getWidgetType(): string {
+ return "SWITCH_GROUP_WIDGET";
+ }
+
+ getPageView() {
+ const {
+ alignment,
+ isDisabled,
+ isInline,
+ isRequired,
+ isValid,
+ options,
+ parentRowSpace,
+ selectedValues,
+ } = this.props;
+
+ return (
+
+ );
+ }
+
+ private handleSwitchStateChange = (value: string) => {
+ return (event: React.FormEvent) => {
+ let { selectedValuesArray } = this.props;
+ const isChecked = (event.target as HTMLInputElement).checked;
+ if (isChecked) {
+ selectedValuesArray = [...selectedValuesArray, value];
+ } else {
+ selectedValuesArray = selectedValuesArray.filter(
+ (item: string) => item !== value,
+ );
+ }
+
+ this.props.updateWidgetMetaProperty(
+ "selectedValuesArray",
+ selectedValuesArray,
+ {
+ triggerPropertyName: "onSelectionChange",
+ dynamicString: this.props.onSelectionChange,
+ event: {
+ type: EventType.ON_SWITCH_GROUP_SELECTION_CHANGE,
+ },
+ },
+ );
+ };
+ };
+}
+
+export interface SwitchGroupWidgetProps extends WidgetProps {
+ options: OptionProps[];
+ defaultSelectedValues: string[];
+ isInline: boolean;
+ isRequired?: boolean;
+ isValid?: boolean;
+ isDisabled?: boolean;
+ alignment: Alignment;
+ onSelectionChange?: boolean;
+}
+
+export default SwitchGroupWidget;
diff --git a/app/client/yarn.lock b/app/client/yarn.lock
index 4b1dae5729..5d8da2320a 100644
--- a/app/client/yarn.lock
+++ b/app/client/yarn.lock
@@ -16019,7 +16019,7 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.14.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
-tslib@^2.1.0:
+tslib@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==