diff --git a/app/client/cypress/fixtures/TreeSelectDsl.json b/app/client/cypress/fixtures/TreeSelectDsl.json new file mode 100644 index 0000000000..edc5109c2c --- /dev/null +++ b/app/client/cypress/fixtures/TreeSelectDsl.json @@ -0,0 +1,128 @@ +{ + "dsl":{ + "widgetName":"MainContainer", + "backgroundColor":"none", + "rightColumn":1034.3999999999999, + "snapColumns":64, + "detachFromLayout":true, + "widgetId":"0", + "topRow":0, + "bottomRow":1990, + "containerStyle":"none", + "snapRows":125, + "parentRowSpace":1, + "type":"CANVAS_WIDGET", + "canExtend":true, + "version":38, + "minHeight":2000, + "parentColumnSpace":1, + "dynamicTriggerPathList":[], + "dynamicBindingPathList":[], + "leftColumn":0, + "children":[ + { + "widgetName":"MultiSelectTree1", + "displayName":"Multi Select Tree", + "iconSVG":"/static/media/icon.f264210c.svg", + "labelText":"Label", + "topRow":38, + "bottomRow":44.88, + "parentRowSpace":10, + "type":"MULTI_SELECT_TREE_WIDGET", + "hideCard":false, + "mode":"SHOW_ALL", + "defaultOptionValue":[], + "parentColumnSpace":15.974999999999998, + "leftColumn":18, + "options":[ + { + "label":"Blue", + "value":"BLUE", + "children":[ + { + "label":"Dark Blue", + "value":"DARK BLUE" + }, + { + "label":"Light Blue", + "value":"LIGHT BLUE" + } + ] + }, + { + "label":"Green", + "value":"GREEN" + }, + { + "label":"Red", + "value":"RED" + } + ], + "placeholderText":"select option(s)", + "isDisabled":false, + "key":"1zu067mn51", + "isRequired":false, + "rightColumn":34, + "widgetId":"zvm3vcs5gp", + "isVisible":true, + "version":1, + "expandAll":false, + "parentId":"0", + "renderMode":"CANVAS", + "isLoading":false, + "allowClear":false + }, + { + "widgetName":"SingleSelectTree1", + "displayName":"Single Select Tree", + "iconSVG":"/static/media/icon.f815ebe3.svg", + "labelText":"Label", + "topRow":58, + "bottomRow":64.8, + "parentRowSpace":10, + "type":"SINGLE_SELECT_TREE_WIDGET", + "hideCard":false, + "defaultOptionValue":"BLUE", + "parentColumnSpace":15.974999999999998, + "leftColumn":17, + "options":[ + { + "label":"Blue", + "value":"BLUE", + "children":[ + { + "label":"Dark Blue", + "value":"DARK BLUE" + }, + { + "label":"Light Blue", + "value":"LIGHT BLUE" + } + ] + }, + { + "label":"Green", + "value":"GREEN" + }, + { + "label":"Red", + "value":"RED" + } + ], + "placeholderText":"select option", + "isDisabled":false, + "key":"cul8w70bzs", + "isRequired":false, + "rightColumn":33, + "widgetId":"0zloh94nd4", + "isVisible":true, + "version":1, + "expandAll":false, + "parentId":"0", + "renderMode":"CANVAS", + "isLoading":false, + "allowClear":false + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Tree_Select_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Tree_Select_spec.js new file mode 100644 index 0000000000..1954d220ea --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/DisplayWidgets/Tree_Select_spec.js @@ -0,0 +1,28 @@ +const dsl = require("../../../../fixtures/TreeSelectDsl.json"); + +describe("Tree Select Widget", function() { + beforeEach(() => { + cy.addDsl(dsl); + }); + + it("Open Existing MultiSelectTree from created Widgets list", () => { + cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Widgets)").click({ + multiple: true, + }); + cy.get( + ".bp3-icon-caret-right ~ .t--entity-name:contains(MultiSelectTree1)", + ).click({ + multiple: true, + }); + }); + it("Open Existing SingleSelectTree from created Widgets list", () => { + cy.get(".bp3-icon-caret-right ~ .t--entity-name:contains(Widgets)").click({ + multiple: true, + }); + cy.get( + ".bp3-icon-caret-right ~ .t--entity-name:contains(SingleSelectTree1)", + ).click({ + multiple: true, + }); + }); +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js new file mode 100644 index 0000000000..85bc985397 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Multi_Select_Tree_spec.js @@ -0,0 +1,47 @@ +const dsl = require("../../../../fixtures/TreeSelectDsl.json"); +const formWidgetsPage = require("../../../../locators/FormWidgets.json"); +const publish = require("../../../../locators/publishWidgetspage.json"); +const commonlocators = require("../../../../locators/commonlocators.json"); + +describe("MultiSelectTree Widget Functionality", function() { + before(() => { + cy.addDsl(dsl); + }); + it("Selects value with enter in default value", () => { + cy.openPropertyPane("multiselecttreewidget"); + cy.testJsontext("defaultvalue", "RED\n"); + cy.get(formWidgetsPage.multiselecttreeWidget) + .find(".rc-tree-select-selection-item-content") + .first() + .should("have.text", "Red"); + }); + it(" To Validate Options", function() { + cy.get(formWidgetsPage.treeSelectInput) + .first() + .click({ force: true }); + cy.get(formWidgetsPage.treeSelectInput) + .first() + .type("light"); + cy.treeSelectDropdown("Light Blue"); + }); + it("To Unchecked Visible Widget", function() { + cy.togglebarDisable(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get( + publish.multiselecttreewidget + " " + ".rc-tree-select-multiple", + ).should("not.exist"); + cy.get(publish.backToEditor).click(); + }); + it(" To Check Visible Widget", function() { + cy.openPropertyPane("multiselecttreewidget"); + cy.togglebar(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get( + publish.multiselecttreewidget + " " + ".rc-tree-select-multiple", + ).should("be.visible"); + cy.get(publish.backToEditor).click(); + }); +}); +afterEach(() => { + // put your clean up code if any +}); diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js new file mode 100644 index 0000000000..355c946203 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/Single_Select_Tree_spec.js @@ -0,0 +1,47 @@ +const dsl = require("../../../../fixtures/TreeSelectDsl.json"); +const formWidgetsPage = require("../../../../locators/FormWidgets.json"); +const publish = require("../../../../locators/publishWidgetspage.json"); +const commonlocators = require("../../../../locators/commonlocators.json"); + +describe("MultiSelectTree Widget Functionality", function() { + before(() => { + cy.addDsl(dsl); + }); + it("Selects value with enter in default value", () => { + cy.openPropertyPane("singleselecttreewidget"); + cy.testJsontext("defaultvalue", "RED\n"); + cy.get(formWidgetsPage.singleselecttreeWidget) + .find(".rc-tree-select-selection-item") + .first() + .should("have.text", "Red"); + }); + it(" To Validate Options", function() { + cy.get(formWidgetsPage.treeSelectInput) + .last() + .click({ force: true }); + cy.get(formWidgetsPage.treeSelectInput) + .last() + .type("light"); + cy.treeSelectDropdown("Light Blue"); + }); + it("To Unchecked Visible Widget", function() { + cy.togglebarDisable(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get( + publish.singleselecttreewidget + " " + ".rc-tree-select-single", + ).should("not.exist"); + cy.get(publish.backToEditor).click(); + }); + it(" To Check Visible Widget", function() { + cy.openPropertyPane("singleselecttreewidget"); + cy.togglebar(commonlocators.visibleCheckbox); + cy.PublishtheApp(); + cy.get( + publish.singleselecttreewidget + " " + ".rc-tree-select-single", + ).should("be.visible"); + cy.get(publish.backToEditor).click(); + }); +}); +afterEach(() => { + // put your clean up code if any +}); diff --git a/app/client/cypress/locators/FormWidgets.json b/app/client/cypress/locators/FormWidgets.json index a839156cf7..b7a5f1ae04 100644 --- a/app/client/cypress/locators/FormWidgets.json +++ b/app/client/cypress/locators/FormWidgets.json @@ -2,6 +2,8 @@ "checkboxWidget": ".t--draggable-checkboxwidget", "dropdownWidget": ".t--draggable-dropdownwidget", "multiselectWidget": ".t--draggable-multiselectwidget", + "multiselecttreeWidget": ".t--draggable-multiselecttreewidget", + "singleselecttreeWidget": ".t--draggable-singleselecttreewidget", "dropdownSelectionType": ".t--property-control-selectiontype .bp3-popover-target", "radioWidget": ".t--draggable-radiogroupwidget", "checkboxGroupWidget": ".t--draggable-checkboxgroupwidget", @@ -22,6 +24,7 @@ "labelvalue": ".t--draggable-dropdownwidget label", "dropdownInput": ".bp3-tag-input-values", "mulitiselectInput": ".rc-select-selection-search-input", + "treeSelectInput": ".rc-tree-select-selection-search-input", "labelradio": ".t--draggable-radiogroupwidget label", "labelCheckboxGroup": ".t--draggable-checkboxgroupwidget label", "deleteradiovalue": ".t--property-control-options mask", diff --git a/app/client/cypress/locators/publishWidgetspage.json b/app/client/cypress/locators/publishWidgetspage.json index c57574f84f..35e419820d 100644 --- a/app/client/cypress/locators/publishWidgetspage.json +++ b/app/client/cypress/locators/publishWidgetspage.json @@ -13,6 +13,8 @@ "imageWidget": ".t--widget-imagewidget", "dropdownWidget": ".t--widget-dropdownwidget", "multiselectwidget": ".t--widget-multiselectwidget", + "multiselecttreewidget": ".t--widget-multiselecttreewidget", + "singleselecttreewidget": ".t--widget-singleselecttreewidget", "tabWidget": ".t--widget-tabswidget", "chartWidget": ".t--widget-chartwidget", "horizontalTab": ".t--widget-chartwidget g[class*='-scrollContainer'] rect", diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index dfec7c649f..d9420a1fb9 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -1805,6 +1805,14 @@ Cypress.Commands.add("dropdownMultiSelectDynamic", (text) => { .click({ force: true }) .should("have.text", text); }); +Cypress.Commands.add("treeSelectDropdown", (text) => { + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + cy.get(".tree-select-dropdown") + .contains(text) + .click({ force: true }) + .should("have.text", text); +}); Cypress.Commands.add("dropdownDynamicUpdated", (text) => { // eslint-disable-next-line cypress/no-unnecessary-waiting diff --git a/app/client/package.json b/app/client/package.json index e47c2144de..1e82d2e267 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -103,6 +103,7 @@ "prismjs": "^1.24.0", "rc-pagination": "^3.1.3", "rc-select": "^12.1.10", + "rc-tree-select": "^4.4.0-alpha.2", "re-reselect": "^3.4.0", "react": "^16.12.0", "react-base-table": "^1.9.1", diff --git a/app/client/src/assets/icons/widget/multi-tree-select.svg b/app/client/src/assets/icons/widget/multi-tree-select.svg new file mode 100644 index 0000000000..181e4e55cc --- /dev/null +++ b/app/client/src/assets/icons/widget/multi-tree-select.svg @@ -0,0 +1 @@ + diff --git a/app/client/src/assets/icons/widget/single-tree-select.svg b/app/client/src/assets/icons/widget/single-tree-select.svg new file mode 100644 index 0000000000..dc9ccf2f42 --- /dev/null +++ b/app/client/src/assets/icons/widget/single-tree-select.svg @@ -0,0 +1 @@ + diff --git a/app/client/src/constants/HelpConstants.ts b/app/client/src/constants/HelpConstants.ts index 26a0b0a5b3..46083cb65c 100644 --- a/app/client/src/constants/HelpConstants.ts +++ b/app/client/src/constants/HelpConstants.ts @@ -139,6 +139,14 @@ export const HelpMap: Record = { path: "/widget-reference/menu-button", searchKey: "Menu Button", }, + TREE_MULTI_SELECT_WIDGET: { + path: "/widget-reference/tree-multi-select", + searchKey: "Tree Multi Select", + }, + TREE_SINGLE_SELECT_WIDGET: { + path: "/widget-reference/tree-single-select", + searchKey: "Tree Single Select", + }, ICON_BUTTON_WIDGET: { path: "/widget-reference/icon-button", searchKey: "Icon Button", diff --git a/app/client/src/constants/WidgetValidation.ts b/app/client/src/constants/WidgetValidation.ts index d740138d7c..bbd1dbe231 100644 --- a/app/client/src/constants/WidgetValidation.ts +++ b/app/client/src/constants/WidgetValidation.ts @@ -10,6 +10,7 @@ export enum ValidationTypes { OBJECT = "OBJECT", ARRAY = "ARRAY", OBJECT_ARRAY = "OBJECT_ARRAY", + NESTED_OBJECT_ARRAY = "NESTED_OBJECT_ARRAY", DATE_ISO_STRING = "DATE_ISO_STRING", IMAGE_URL = "IMAGE_URL", FUNCTION = "FUNCTION", diff --git a/app/client/src/icons/WidgetIcons.tsx b/app/client/src/icons/WidgetIcons.tsx index 1b198d5d29..895cf6943b 100644 --- a/app/client/src/icons/WidgetIcons.tsx +++ b/app/client/src/icons/WidgetIcons.tsx @@ -27,6 +27,8 @@ import { ReactComponent as RatingIcon } from "assets/icons/widget/rating.svg"; import { ReactComponent as EmbedIcon } from "assets/icons/widget/embed.svg"; import { ReactComponent as DividerIcon } from "assets/icons/widget/divider.svg"; import { ReactComponent as MenuButtonIcon } from "assets/icons/widget/menu-button.svg"; +import { ReactComponent as MultiTreeSelectIcon } from "assets/icons/widget/multi-tree-select.svg"; +import { ReactComponent as SingleTreeSelectIcon } from "assets/icons/widget/single-tree-select.svg"; import { ReactComponent as IconButtonIcon } from "assets/icons/widget/icon-button.svg"; import { ReactComponent as StatboxIcon } from "assets/icons/widget/statbox.svg"; import { ReactComponent as CheckboxGroupIcon } from "assets/icons/widget/checkbox-group.svg"; @@ -177,6 +179,16 @@ export const WidgetIcons: { ), + TREE_SINGLE_SELECT_WIDGET: (props: IconProps) => ( + + + + ), + TREE_MULTI_SELECT_WIDGET: (props: IconProps) => ( + + + + ), ICON_BUTTON_WIDGET: (props: IconProps) => ( diff --git a/app/client/src/utils/WidgetRegistry.tsx b/app/client/src/utils/WidgetRegistry.tsx index 17ac4be9ae..f830e15467 100644 --- a/app/client/src/utils/WidgetRegistry.tsx +++ b/app/client/src/utils/WidgetRegistry.tsx @@ -96,6 +96,12 @@ import AudioRecorderWidget, { } from "widgets/AudioRecorderWidget"; 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"; export const registerWidgets = () => { const start = performance.now(); @@ -135,5 +141,8 @@ export const registerWidgets = () => { registerWidget(FilePickerWidgetV2, FILEPICKER_WIDGET_V2_CONFIG); registerWidget(StatboxWidget, STATBOX_WIDGET_CONFIG); registerWidget(AudioRecorderWidget, AUDIO_RECORDER_WIDGET_CONFIG); + registerWidget(MultiSelectTreeWidget, MULTI_SELECT_TREE_WIDGET_CONFIG); + registerWidget(SingleSelectTreeWidget, SINGLE_SELECT_TREE_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 fac3797279..0216ee6024 100644 --- a/app/client/src/utils/autocomplete/EntityDefinitions.ts +++ b/app/client/src/utils/autocomplete/EntityDefinitions.ts @@ -314,6 +314,45 @@ export const entityDefinitions: Record = { isVisible: isVisible, label: "string", }, + //TODO: fix this after development + SINGLE_SELECT_TREE_WIDGET: { + "!doc": + "Single Select Tree is used to capture user input from a specified list of permitted inputs/Nested Inputs.", + "!url": "https://docs.appsmith.com/widget-reference/treeselect", + isVisible: isVisible, + selectedOptionValue: { + "!type": "string", + "!doc": "The value selected in a tree select dropdown", + "!url": "https://docs.appsmith.com/widget-reference/treeselect", + }, + selectedOptionLabel: { + "!type": "string", + "!doc": "The selected option label in a tree select dropdown", + "!url": "https://docs.appsmith.com/widget-reference/treeselect", + }, + isDisabled: "bool", + isValid: "bool", + options: "[dropdownOption]", + }, + MULTI_SELECT_TREE_WIDGET: { + "!doc": + "Multi Select Tree is used to capture user inputs from a specified list of permitted inputs/Nested Inputs. A Tree Select can capture a single choice as well as multiple choices", + "!url": "https://docs.appsmith.com/widget-reference/treeselect", + isVisible: isVisible, + selectedOptionValues: { + "!type": "[string]", + "!doc": "The array of values selected in a tree select dropdown", + "!url": "https://docs.appsmith.com/widget-reference/treeselect", + }, + selectedOptionLabels: { + "!type": "[string]", + "!doc": "The array of selected option labels in a tree select dropdown", + "!url": "https://docs.appsmith.com/widget-reference/treeselect", + }, + isDisabled: "bool", + isValid: "bool", + options: "[dropdownOption]", + }, ICON_BUTTON_WIDGET: { "!doc": "Icon button widget is just an icon, along with all other button properties.", diff --git a/app/client/src/utils/validation/common.ts b/app/client/src/utils/validation/common.ts index 30794e98c3..ffad3f8eda 100644 --- a/app/client/src/utils/validation/common.ts +++ b/app/client/src/utils/validation/common.ts @@ -114,6 +114,7 @@ export function getExpectedValue( autocompleteDataType: AutocompleteDataType.OBJECT, }; case ValidationTypes.ARRAY: + case ValidationTypes.NESTED_OBJECT_ARRAY: if (config.params?.allowedValues) { const allowed = config.params?.allowedValues.join("' | '"); return { diff --git a/app/client/src/widgets/MenuButtonWidget/component/index.tsx b/app/client/src/widgets/MenuButtonWidget/component/index.tsx index 4c30371681..5940ca2222 100644 --- a/app/client/src/widgets/MenuButtonWidget/component/index.tsx +++ b/app/client/src/widgets/MenuButtonWidget/component/index.tsx @@ -272,7 +272,6 @@ const BaseButton = styled(Button)` } `} - border-radius: ${({ borderRadius }) => borderRadius === ButtonBorderRadiusTypes.ROUNDED ? "5px" : 0}; diff --git a/app/client/src/widgets/MultiSelectTreeWidget/component/index.styled.tsx b/app/client/src/widgets/MultiSelectTreeWidget/component/index.styled.tsx new file mode 100644 index 0000000000..cb8ec9c21b --- /dev/null +++ b/app/client/src/widgets/MultiSelectTreeWidget/component/index.styled.tsx @@ -0,0 +1,891 @@ +import React from "react"; +import { Checkbox, Classes, Label } from "@blueprintjs/core"; +import styled, { keyframes } from "styled-components"; +import { Colors } from "constants/Colors"; +import { createGlobalStyle } from "constants/DefaultTheme"; +import { + FontStyleTypes, + TextSize, + TEXT_SIZES, +} from "constants/WidgetConstants"; + +export const menuItemSelectedIcon = (props: { isSelected: boolean }) => { + return ; +}; + +export const TextLabelWrapper = styled.div<{ + compactMode: boolean; +}>` + ${(props) => + props.compactMode ? "&&& {margin-right: 5px;}" : "width: 100%;"} + display: flex; +`; + +export const StyledLabel = styled(Label)<{ + $compactMode: boolean; + $labelText?: string; + $labelTextColor?: string; + $labelTextSize?: TextSize; + $labelStyle?: string; +}>` + overflow-y: hidden; + text-overflow: ellipsis; + width: ${(props) => (props.$compactMode ? "auto" : "100%")}; + text-align: left; + color: ${(props) => props.$labelTextColor || "inherit"}; + font-size: ${(props) => + props.$labelTextSize ? TEXT_SIZES[props.$labelTextSize] : "14px"}; + font-weight: ${(props) => + props?.$labelStyle?.includes(FontStyleTypes.BOLD) ? "bold" : "normal"}; + font-style: ${(props) => + props?.$labelStyle?.includes(FontStyleTypes.ITALIC) ? "italic" : ""}; +`; + +const rcSelectDropdownSlideUpIn = keyframes` + 0% { + opacity: 0; + transform-origin: 0% 0%; + } + 100% { + opacity: 1; + transform-origin: 0% 0%; + } +`; + +const rcSelectDropdownSlideUpOut = keyframes` + 0% { + opacity: 1; + transform-origin: 0% 0%; + } +100% { + opacity: 0; + transform-origin: 0% 0%; + } +`; + +export const DropdownStyles = createGlobalStyle` +.rc-tree-select-dropdown-hidden { + display: none; +} +.rc-tree-select-item-group { + color: #999; + font-weight: bold; + font-size: 80%; +} +.rc-tree-select-item-option { + position: relative; + display: flex; + + flex-direction: row-reverse; + .rc-tree-select-item-option-state { + pointer-events: all; + margin-right: 10px; + } +} +.rc-tree-select-item-option-grouped { + padding-left: 24px; +} +.rc-tree-select-item-option-content { + flex: 1 1 0; +} +.rc-tree-select-item-option-active { + background: rgb(233, 250, 243); +} +.rc-tree-select-item-option-selected { + background: rgb(233, 250, 243); +} +.rc-tree-select-item-option-disabled { + color: #999; +} +.rc-tree-select-item-empty { + text-align: center; + color: #999; +} +.rc-tree-select-selection__choice-zoom { + transition: all 0s; +} +.rc-tree-select-selection__choice-zoom-appear { + opacity: 0; +} +.rc-tree-select-selection__choice-zoom-appear.rc-tree-select-selection__choice-zoom-appear-active { + opacity: 1; +} +.rc-tree-select-selection__choice-zoom-leave { + opacity: 1; +} +.rc-tree-select-selection__choice-zoom-leave.rc-tree-select-selection__choice-zoom-leave-active { + opacity: 0; +} +.rc-tree-select-dropdown-slide-up-enter { + animation-duration: 0s; + animation-fill-mode: both; + transform-origin: 0 0; + opacity: 0; + animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + animation-play-state: paused; +} +.rc-tree-select-dropdown-slide-up-appear { + animation-duration: 0s; + animation-fill-mode: both; + transform-origin: 0 0; + opacity: 0; + animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + animation-play-state: paused; +} +.rc-tree-select-dropdown-slide-up-leave { + animation-duration: 0s; + animation-fill-mode: both; + transform-origin: 0 0; + opacity: 1; + animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); + animation-play-state: paused; +} +.rc-tree-select-dropdown-slide-up-enter.rc-tree-select-dropdown-slide-up-enter-active.rc-tree-select-dropdown-placement-bottomLeft { + animation-name: ${rcSelectDropdownSlideUpIn}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-appear.rc-tree-select-dropdown-slide-up-appear-active.rc-tree-select-dropdown-placement-bottomLeft { + animation-name:${rcSelectDropdownSlideUpIn}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-leave.rc-tree-select-dropdown-slide-up-leave-active.rc-tree-select-dropdown-placement-bottomLeft { + animation-name: ${rcSelectDropdownSlideUpOut}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-enter.rc-tree-select-dropdown-slide-up-enter-active.rc-tree-select-dropdown-placement-topLeft { + animation-name: ${rcSelectDropdownSlideUpIn}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-appear.rc-tree-select-dropdown-slide-up-appear-active.rc-tree-select-dropdown-placement-topLeft { + animation-name: ${rcSelectDropdownSlideUpIn}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-leave.rc-tree-select-dropdown-slide-up-leave-active.rc-tree-select-dropdown-placement-topLeft { + animation-name: ${rcSelectDropdownSlideUpOut}; + animation-play-state: running; +} + + + + +.tree-select-dropdown.single-tree-select-dropdown { + .rc-tree-select-tree + .rc-tree-select-tree-treenode.rc-tree-select-tree-treenode-disabled + span.rc-tree-select-tree-iconEle { +cursor: not-allowed; + } + .rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-iconEle { + position: relative; + cursor: pointer; + margin-left: 5px; + top: 0; + left: 0; + display: inline-block; + width: 16px; + height: 16px; + direction: ltr; + background-color: #fff; +border: 1px solid #E8E8E8; + border-radius: 100%; + border-collapse: separate; + transition: all .3s; + :after{ + position: absolute; + top: 50%; + left: 52%; + display: table; + width: 10px; + height: 10px; + border: none; + border-top: 0; + border-left: 0; + transform: rotate( + 45deg + ) scale(0) translate(-50%,-50%); + opacity: 0; + transition: all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s; + content: " "; + } + + } + + .rc-tree-select-tree + .rc-tree-select-tree-treenode + .rc-tree-select-tree-node-selected + span.rc-tree-select-tree-iconEle { + :after{ + width: 10px; + height: 10px; + transform: translate(-50%,-50%) scale(1); + background: rgb(3, 179, 101) !important; + opacity: 1; + content: " "; + border-radius: 100%; + + } + } + +} +.tree-select-dropdown { + min-height: 100px; + min-width: 250px !important; + position: absolute; + background: #fff; + width: 100%; + border-radius: 0px; + margin-top: 10px; + padding: 12px; + background: white; + box-shadow: 0 0 2px rgb(0 0 0 / 20%) !important; + &&&& .${Classes.ALIGN_LEFT} { + font-size: 16px; + padding-bottom: 10px; + margin-left: 16px ; + .${Classes.CONTROL_INDICATOR} { + margin-right: 20px; + } + } + &&&& .${Classes.CONTROL} .${Classes.CONTROL_INDICATOR} { + background: white; + box-shadow: none; + border-width: 2px; + border-style: solid; + border-color: ${Colors.GEYSER}; + &::before { + width: auto; + height: 1em; + } + } + .${Classes.CONTROL} input:checked ~ .${Classes.CONTROL_INDICATOR} { + background: rgb(3, 179, 101) !important; + color: rgb(255, 255, 255); + border-color: rgb(3, 179, 101) !important; + box-shadow: none; + outline: none !important; + } + .rc-tree-select-item { + font-size: 16px; + line-height: 1.5; + padding: 5px 16px; + align-items: center; + cursor: pointer; +} +.rc-tree-select-item-option-state { + .bp3-control.bp3-checkbox { + margin-bottom: 0; + } +} + + + +.rc-tree-select-tree { + margin: 0; + border: 1px solid transparent; +} +.rc-tree-select-tree-focused:not(.rc-tree-select-tree-active-focused) { + border-color: cyan; +} +.rc-tree-select-tree .rc-tree-select-tree-treenode { + margin: 0; + padding: 0; + line-height: 24px; + white-space: nowrap; + list-style: none; + outline: 0; + padding: 0 5px; + height: 34px; + align-items: center; + display: flex !important; +} +.rc-tree-select-tree .rc-tree-select-tree-treenode .draggable { + color: #333; + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + user-select: none; + -khtml-user-drag: element; + -webkit-user-drag: element; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode.drop-container + > .draggable::after { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + box-shadow: inset 0 0 0 2px red; + content: ""; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode.drop-container + ~ .rc-tree-select-tree-treenode { + border-left: 2px solid chocolate; +} +.rc-tree-select-tree .rc-tree-select-tree-treenode.drop-target { + background-color: yellowgreen; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode.drop-target + ~ .rc-tree-select-tree-treenode { + border-left: none; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode.filter-node + > .rc-tree-select-tree-node-content-wrapper { + color: #182026 !important; + font-weight: bold !important; +} +.rc-tree-select-tree .rc-tree-select-tree-treenode ul { + margin: 0; + padding: 0 0 0 18px; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + .rc-tree-select-tree-node-content-wrapper { + position: relative; + display: inline-flex; + align-items: center; + height: 34px; + margin: 0; + padding: 0; + text-decoration: none; + vertical-align: top; + cursor: pointer; + flex: 1 +} + +.rc-tree-select-tree-checkbox-checked .rc-tree-select-tree-checkbox-inner:after { + position: absolute; + display: table; + border: 2px solid #fff; + border-top: 0; + border-left: 0; + transform: rotate( +45deg +) scale(1) translate(-50%,-50%); + opacity: 1; + transition: all .2s cubic-bezier(.12,.4,.29,1.46) .1s; + content: " "; +} +.rc-tree-select-tree-checkbox-inner:after { + position: absolute; + top: 50%; + left: 22%; + display: table; + width: 5.71428571px; + height: 9.14285714px; + border: 2px solid #fff; + border-top: 0; + border-left: 0; + transform: rotate( +45deg +) scale(0) translate(-50%,-50%); + opacity: 0; + transition: all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s; + content: " "; +} + +.rc-tree-select-tree-checkbox-indeterminate .rc-tree-select-tree-checkbox-inner:after { + top: 50%; + left: 50%; + width: 8px; + height: 8px; + background-color: rgb(3, 179, 101) !important; + border: 0; + transform: translate(-50%,-50%) scale(1); + opacity: 1; + content: " "; +} + +.rc-tree-select-tree-checkbox:hover:after, .rc-tree-select-tree-checkbox-wrapper:hover .rc-tree-select-tree-checkbox:after { + visibility: visible; +} + +.rc-tree-select-tree-checkbox { + top: initial; + margin: 4px 8px 0 0; +} +.rc-tree-select-tree-checkbox { + box-sizing: border-box; + margin: 0; + padding: 0; + color: #000000d9; + font-size: 14px; + font-variant: tabular-nums; + line-height: 1.5715; + list-style: none; + font-feature-settings: "tnum"; + position: relative; + top: 0; + line-height: 1; + white-space: nowrap; + outline: none; + cursor: pointer; + margin-left: 3px; +} + + +.rc-tree-select-tree-checkbox-wrapper:hover .rc-tree-select-tree-checkbox-inner, .rc-tree-select-tree-checkbox:hover .rc-tree-select-tree-checkbox-inner, .rc-tree-select-tree-checkbox-input:focus+.rc-tree-select-tree-checkbox-inner { + border-color: rgb(3, 179, 101) !important; +} +.rc-tree-select-tree-checkbox-checked .rc-tree-select-tree-checkbox-inner { + border-color: rgb(3, 179, 101) !important; + background: rgb(3, 179, 101) !important; +} + +.rc-tree-select-tree-checkbox-inner { + position: relative; + top: 0; + left: 0; + display: inline-block; + width: 16px; + height: 16px; + direction: ltr; + background-color: #fff; + border: 1px solid #d9d9d9; + border-radius: 0px; + border-collapse: separate; + transition: all .3s; +} + .rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree.select-tree-checkbox-checked { + .rc-tree-select-tree-checkbox-inner { + border-color: rgb(3, 179, 101) !important; + background: rgb(3, 179, 101) !important; + } + } + .single-tree-select-dropdown + .rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-iconEle { + width: 20px; + } + +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-iconEle { + display: inline-block; + width: 0px; + height: 16px; + margin-right: 2px; + line-height: 16px; + vertical-align: -0.125em; + background-color: transparent; + background-image: none; + background-repeat: no-repeat; + background-attachment: scroll; + border: 0 none; + outline: none; + cursor: pointer; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-switcher.rc-tree-select-tree-icon__customize, +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-checkbox.rc-tree-select-tree-icon__customize, +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-iconEle.rc-tree-select-tree-icon__customize { + background-image: none; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-icon_loading { + margin-right: 2px; + vertical-align: top; + background: none; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-switcher.rc-tree-select-tree-switcher-noop { + cursor: auto; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-switcher.rc-tree-select-tree-switcher_open { + background-position: -93px -56px; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-switcher.rc-tree-select-tree-switcher_close { + background-position: -75px -56px; +} +.rc-tree-select-tree:not(.rc-tree-select-tree-show-line) + .rc-tree-select-tree-treenode + .rc-tree-select-tree-switcher-noop { + background: none; +} +.rc-tree-select-tree.rc-tree-select-tree-show-line + .rc-tree-select-tree-treenode:not(:last-child) + > ul { + background: none; +} +.rc-tree-select-tree.rc-tree-select-tree-show-line + .rc-tree-select-tree-treenode:not(:last-child) + > .rc-tree-select-tree-switcher-noop { + background-position: -56px -18px; +} +.rc-tree-select-tree.rc-tree-select-tree-show-line + .rc-tree-select-tree-treenode:last-child + > .rc-tree-select-tree-switcher-noop { + background-position: -56px -36px; +} +.rc-tree-select-tree-child-tree { + display: none; +} +.rc-tree-select-tree-child-tree-open { + display: block; +} +.rc-tree-select-tree-treenode-disabled + > span:not(.rc-tree-select-tree-switcher), +.rc-tree-select-tree-treenode-disabled > a, +.rc-tree-select-tree-treenode-disabled > a span { + color: #767676; + cursor: not-allowed; +} +.rc-tree-select-tree-treenode-active { + background: rgb(233, 250, 243); +} +.rc-tree-select-tree-treenode:hover { + background: rgb(233, 250, 243); +} +.rc-tree-select-tree-node-selected { + background-color: none; + box-shadow: 0 0 0 0 #ffb951; + opacity: 1; +} +.rc-tree-select-tree-icon__open { + margin-right: 2px; + vertical-align: top; + background-position: -110px -16px; +} +.rc-tree-select-tree-icon__close { + margin-right: 2px; + vertical-align: top; + background-position: -110px 0; +} +.rc-tree-select-tree-icon__docu { + margin-right: 2px; + vertical-align: top; + background-position: -110px -32px; +} +.rc-tree-select-tree-icon__customize { + margin-right: 2px; + vertical-align: top; +} +.rc-tree-select-tree-title { + display: inline-block; + margin-left: 10px; + font-size: 16px !important; +} +.rc-tree-select-tree-indent { + display: inline-block; + vertical-align: bottom; + height: 0; +} +.rc-tree-select-tree-indent-unit { + width: 25px; + display: inline-block; +} + + } +`; + +export const TreeSelectContainer = styled.div<{ + compactMode: boolean; + allowClear: boolean; +}>` + display: flex; + flex-direction: ${(props) => (props.compactMode ? "row" : "column")}; + align-items: ${(props) => (props.compactMode ? "center" : "left")}; + + label.tree-select-label { + margin-bottom: ${(props) => (props.compactMode ? "0px" : "5px")}; + margin-right: ${(props) => (props.compactMode ? "10px" : "0px")}; + } + .rc-tree-select { + display: inline-block; + font-size: 12px; + width: 100%; + height: 100%; + position: relative; + cursor: pointer; + flex: 1 1; + .rc-tree-select-selection-placeholder { + pointer-events: none; + position: absolute; + top: 50%; + right: 11px; + left: 11px; + transform: translateY(-50%); + transition: all 0.3s; + flex: 1; + overflow: hidden; + color: #bfbfbf; + white-space: nowrap; + text-overflow: ellipsis; + pointer-events: none; + font-size: 14px; + } + .rc-tree-select-selection-search-input { + appearance: none; + &::-webkit-search-cancel-button { + display: none; + appearance: none; + } + } + .rc-tree-select-selection-overflow-item-suffix { + position: relative !important; + left: 0px !important; + } + } + .rc-tree-select-disabled { + cursor: not-allowed; + input { + cursor: not-allowed; + } + .rc-tree-select-selector { + opacity: 0.3; + } + } + .rc-tree-select-show-arrow.rc-tree-select-loading { + .rc-tree-select-arrow-icon { + &::after { + box-sizing: border-box; + width: 12px; + height: 12px; + border-radius: 100%; + border: 2px solid #999; + border-top-color: transparent; + border-bottom-color: transparent; + transform: none; + margin-top: 4px; + animation: rcSelectLoadingIcon 0.5s infinite; + } + } + } + .rc-tree-select-single .rc-tree-select-selector { + display: flex; + flex-wrap: wrap; + padding: 1px; + padding-right: 20px; + box-shadow: none; + border: 1px solid rgb(231, 231, 231); + border-radius: 0px; + width: 100%; + transition: border-color 0.15s ease-in-out 0s, + box-shadow 0.15s ease-in-out 0s; + background-color: white; + height: 100%; + .rc-tree-select-selection-search { + width: 100%; + height: 100%; + input { + width: 100%; + appearance: none; + &::-webkit-search-cancel-button { + display: none; + appearance: none; + } + font-family: system-ui; + + height: 100%; + border: none; + } + } + .rc-tree-select-selection-item { + pointer-events: none; + position: absolute; + top: 50%; + right: 11px; + left: 11px; + transform: translateY(-50%); + transition: all 0.3s; + flex: 1; + overflow: hidden; + color: #231f20; + white-space: nowrap; + text-overflow: ellipsis; + pointer-events: none; + font-size: 14px; + } + } + .rc-tree-select-multiple { + .rc-tree-select-selector { + display: flex; + flex-wrap: wrap; + padding: 1px; + box-shadow: none; + border: 1px solid rgb(231, 231, 231); + border-radius: 0px; + width: 100%; + transition: border-color 0.15s ease-in-out 0s, + box-shadow 0.15s ease-in-out 0s; + background-color: white; + .rc-tree-select-selection-item { + background: none; + border: 1px solid rgb(208, 215, 221); + border-radius: 2px; + margin: 3px 2px; + max-width: 273.926px; + height: 24px; + color: #182026; + overflow-wrap: break-word; + display: inline-flex; + flex-direction: row; + align-items: center; + box-shadow: none; + font-size: 12px; + line-height: 16px; + min-height: 20px; + min-width: 20px; + padding: 2px 6px; + position: relative; + } + .rc-tree-select-selection-item-disabled { + cursor: not-allowed; + opacity: 0.5; + } + .rc-tree-select-selection-overflow { + display: flex; + flex-wrap: wrap; + width: 100%; + align-content: center; + } + .rc-tree-select-selection-overflow-item { + flex: none; + max-width: 100%; + } + .rc-tree-select-selection-search { + position: relative; + max-width: 100%; + margin-bottom: 2px; + height: 100%; + display: flex; + align-items: center; + } + .rc-tree-select-selection-search-input { + padding: 1px; + font-family: system-ui; + width: 5px; + margin: 0px; + display: flex; + height: 26px; + flex: 1 1 0%; + border: none; + outline: none; + width: 100%; + } + .rc-tree-select-selection-search-mirror { + padding: 1px; + font-family: system-ui; + width: 5px; + margin: 0px; + display: flex; + height: 26px; + flex: 1 1 0%; + position: absolute; + z-index: 999; + white-space: nowrap; + position: none; + left: 0; + top: 0; + visibility: hidden; + } + } + } + .rc-tree-select-selection-item-content { + flex-grow: 1; + flex-shrink: 1; + margin-right: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: normal; + font-size: 12px; + line-height: 16px; + } + .rc-tree-select-allow-clear { + .rc-tree-select-clear { + position: absolute; + right: 20px; + right: 25px; + top: -1px; + height: 100%; + display: flex; + align-items: center; + z-index: -1; + .rc-tree-select-clear-icon { + font-size: 18px; + font-weight: bold; + } + } + } + .rc-tree-select-allow-clear.rc-tree-select-focused { + .rc-tree-select-clear { + z-index: 1; + } + } + .rc-tree-select-show-arrow.rc-tree-select-multiple { + .rc-tree-select-selector { + padding-right: ${({ allowClear }) => (allowClear ? "40px" : "20px")}; + + box-shadow: none; + border: 1px solid rgb(231, 231, 231); + border-radius: 0px; + height: inherit; + width: 100%; + transition: border-color 0.15s ease-in-out 0s, + box-shadow 0.15s ease-in-out 0s; + } + } + .rc-tree-select-show-arrow { + .rc-tree-select-arrow { + pointer-events: none; + position: absolute; + right: 5px; + top: 0; + height: 100%; + display: flex; + align-items: center; + } + .rc-tree-select-arrow-icon { + &::after { + content: ""; + border: 5px solid transparent; + width: 0; + height: 0; + display: inline-block; + border-top-color: #999; + transform: translateY(5px); + } + } + } + .rc-tree-select-show-arrow.rc-tree-select-focused { + .rc-tree-select-selector { + border: 1px solid rgb(128, 189, 255); + outline: 0px; + box-shadow: rgba(0, 123, 255, 0.25) 0px 0px 0px 0.1rem; + } + } +`; +export const StyledCheckbox = styled(Checkbox)` + &&.${Classes.CHECKBOX}.${Classes.CONTROL} { + margin: 0; + } +`; + +export const inputIcon = (): JSX.Element => ( + + chevron-down + + +); diff --git a/app/client/src/widgets/MultiSelectTreeWidget/component/index.tsx b/app/client/src/widgets/MultiSelectTreeWidget/component/index.tsx new file mode 100644 index 0000000000..7b1316ddf8 --- /dev/null +++ b/app/client/src/widgets/MultiSelectTreeWidget/component/index.tsx @@ -0,0 +1,191 @@ +import React, { + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import TreeSelect, { TreeSelectProps as SelectProps } from "rc-tree-select"; +import { + TreeSelectContainer, + DropdownStyles, + inputIcon, + StyledLabel, + TextLabelWrapper, +} from "./index.styled"; +import "rc-tree-select/assets/index.less"; +import { DefaultValueType } from "rc-tree-select/lib/interface"; +import { TreeNodeProps } from "rc-tree-select/lib/TreeNode"; +import { CheckedStrategy } from "rc-tree-select/lib/utils/strategyUtil"; +import { + CANVAS_CLASSNAME, + MODAL_PORTAL_CLASSNAME, + TextSize, +} from "constants/WidgetConstants"; +import { Classes } from "@blueprintjs/core"; +export interface TreeSelectProps + extends Required< + Pick< + SelectProps, + | "disabled" + | "options" + | "placeholder" + | "loading" + | "dropdownStyle" + | "allowClear" + > + > { + value?: DefaultValueType; + onChange: (value?: DefaultValueType, labelList?: ReactNode[]) => void; + expandAll: boolean; + mode: CheckedStrategy; + labelText?: string; + labelTextColor?: string; + labelTextSize?: TextSize; + labelStyle?: string; + compactMode: boolean; +} + +const getSvg = (style = {}) => ( + + + + + +); + +const switcherIcon = (treeNode: TreeNodeProps) => { + if (treeNode.isLeaf) { + return ( + + ); + } + return getSvg({ transform: `rotate(${treeNode.expanded ? 90 : 0}deg)` }); +}; + +function MultiTreeSelectComponent({ + allowClear, + compactMode, + disabled, + dropdownStyle, + expandAll, + labelStyle, + labelText, + labelTextColor, + labelTextSize, + loading, + mode, + onChange, + options, + placeholder, + value, +}: TreeSelectProps): JSX.Element { + const [key, setKey] = useState(Math.random()); + const _menu = useRef(null); + + // treeDefaultExpandAll is uncontrolled after first render, + // using this to force render to respond to changes in expandAll + useEffect(() => { + setKey(Math.random()); + }, [expandAll]); + + const getDropdownPosition = useCallback(() => { + const node = _menu.current; + if (Boolean(node?.closest(`.${MODAL_PORTAL_CLASSNAME}`))) { + return document.querySelector( + `.${MODAL_PORTAL_CLASSNAME}`, + ) as HTMLElement; + } + return document.querySelector(`.${CANVAS_CLASSNAME}`) as HTMLElement; + }, []); + + const onClear = useCallback(() => onChange([], []), []); + + return ( + } + > + + {labelText && ( + + + {labelText} + + + )} + `+${e.length} more`} + multiple + notFoundContent="No item Found" + onChange={onChange} + onClear={onClear} + placeholder={placeholder} + showArrow + showCheckedStrategy={mode} + showSearch + style={{ width: "100%" }} + switcherIcon={switcherIcon} + transitionName="rc-tree-select-dropdown-slide-up" + treeCheckable={ + + } + treeData={options} + treeDefaultExpandAll={expandAll} + treeIcon + treeNodeFilterProp="label" + value={value} + /> + + ); +} + +export default MultiTreeSelectComponent; diff --git a/app/client/src/widgets/MultiSelectTreeWidget/icon.svg b/app/client/src/widgets/MultiSelectTreeWidget/icon.svg new file mode 100644 index 0000000000..181e4e55cc --- /dev/null +++ b/app/client/src/widgets/MultiSelectTreeWidget/icon.svg @@ -0,0 +1 @@ + diff --git a/app/client/src/widgets/MultiSelectTreeWidget/index.ts b/app/client/src/widgets/MultiSelectTreeWidget/index.ts new file mode 100644 index 0000000000..0b16de9246 --- /dev/null +++ b/app/client/src/widgets/MultiSelectTreeWidget/index.ts @@ -0,0 +1,51 @@ +import Widget from "./widget"; +import IconSVG from "./icon.svg"; +import { GRID_DENSITY_MIGRATION_V1 } from "widgets/constants"; + +export const CONFIG = { + type: Widget.getWidgetType(), + name: "Multi Select Tree", + iconSVG: IconSVG, + needsMeta: true, + defaults: { + rows: 1.72 * GRID_DENSITY_MIGRATION_V1, + columns: 4 * GRID_DENSITY_MIGRATION_V1, + mode: "SHOW_ALL", + options: [ + { + label: "Blue", + value: "BLUE", + children: [ + { + label: "Dark Blue", + value: "DARK BLUE", + }, + { + label: "Light Blue", + value: "LIGHT BLUE", + }, + ], + }, + { label: "Green", value: "GREEN" }, + { label: "Red", value: "RED" }, + ], + widgetName: "MultiSelectTree", + defaultOptionValue: ["GREEN"], + version: 1, + isVisible: true, + isRequired: false, + isDisabled: false, + allowClear: false, + expandAll: false, + placeholderText: "select option(s)", + labelText: "Label", + }, + properties: { + derived: Widget.getDerivedPropertiesMap(), + default: Widget.getDefaultPropertiesMap(), + meta: Widget.getMetaPropertiesMap(), + config: Widget.getPropertyPaneConfig(), + }, +}; + +export default Widget; diff --git a/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx b/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx new file mode 100644 index 0000000000..2cc9b667f5 --- /dev/null +++ b/app/client/src/widgets/MultiSelectTreeWidget/widget/index.tsx @@ -0,0 +1,465 @@ +import React, { ReactNode } from "react"; +import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget"; +import { TextSize, WidgetType } from "constants/WidgetConstants"; +import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; +import { isArray, findIndex } from "lodash"; +import { + ValidationResponse, + ValidationTypes, +} from "constants/WidgetValidation"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; +import { DefaultValueType } from "rc-select/lib/interface/generator"; +import { Layers } from "constants/Layers"; +import { CheckedStrategy } from "rc-tree-select/lib/utils/strategyUtil"; +import { GRID_DENSITY_MIGRATION_V1 } from "widgets/constants"; +import { AutocompleteDataType } from "utils/autocomplete/TernServer"; +import MultiTreeSelectComponent from "../component"; + +function defaultOptionValueValidation(value: unknown): ValidationResponse { + let values: string[] = []; + if (typeof value === "string") { + try { + values = JSON.parse(value); + if (!Array.isArray(values)) { + throw new Error(); + } + } catch { + values = value.length ? value.split(",") : []; + if (values.length > 0) { + values = values.map((_v: string) => _v.trim()); + } + } + } + if (Array.isArray(value)) { + values = Array.from(new Set(value)); + } + + return { + isValid: true, + parsed: values, + }; +} +class MultiSelectTreeWidget extends BaseWidget< + MultiSelectTreeWidgetProps, + WidgetState +> { + static getPropertyPaneConfig() { + return [ + { + sectionName: "General", + children: [ + { + helpText: "Mode to Display options", + propertyName: "mode", + label: "Mode", + controlType: "DROP_DOWN", + options: [ + { + label: "Display only parent items", + value: "SHOW_PARENT", + }, + { + label: "Display only child items", + value: "SHOW_CHILD", + }, + { + label: "Display all items", + value: "SHOW_ALL", + }, + ], + isBindProperty: false, + isTriggerProperty: false, + }, + { + helpText: + "Allows users to select multiple options. Values must be unique", + propertyName: "options", + label: "Options", + controlType: "INPUT_TEXT", + placeholderText: "Enter option value", + isBindProperty: true, + isTriggerProperty: false, + isJSConvertible: false, + validation: { + type: ValidationTypes.NESTED_OBJECT_ARRAY, + params: { + unique: ["value"], + default: [], + children: { + type: ValidationTypes.OBJECT, + params: { + allowedKeys: [ + { + name: "label", + type: ValidationTypes.TEXT, + params: { + default: "", + required: true, + }, + }, + { + name: "value", + type: ValidationTypes.TEXT, + params: { + default: "", + required: true, + }, + }, + { + name: "children", + type: ValidationTypes.ARRAY, + required: false, + params: { + children: { + type: ValidationTypes.OBJECT, + params: { + allowedKeys: [ + { + name: "label", + type: ValidationTypes.TEXT, + params: { + default: "", + required: true, + }, + }, + { + name: "value", + type: ValidationTypes.TEXT, + params: { + default: "", + required: true, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + evaluationSubstitutionType: + EvaluationSubstitutionType.SMART_SUBSTITUTE, + }, + { + helpText: "Selects the option with value by default", + propertyName: "defaultOptionValue", + label: "Default Value", + controlType: "INPUT_TEXT", + placeholderText: "Enter option value", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fn: defaultOptionValueValidation, + expected: { + type: "Array of values", + example: `['value1', 'value2']`, + autocompleteDataType: AutocompleteDataType.ARRAY, + }, + }, + }, + }, + { + helpText: "Label Text", + propertyName: "labelText", + label: "Label Text", + controlType: "INPUT_TEXT", + placeholderText: "Enter Label text", + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + helpText: "Input Place Holder", + propertyName: "placeholderText", + label: "Placeholder", + controlType: "INPUT_TEXT", + placeholderText: "Enter placeholder text", + 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: "isRequired", + label: "Required", + helpText: "Makes input to the widget mandatory", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "allowClear", + label: "Clear all Selections", + helpText: "Enables Icon to clear all Selections", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "expandAll", + label: "Expand all by default", + helpText: "Expand All nested options", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + ], + }, + { + sectionName: "Styles", + children: [ + { + propertyName: "labelTextColor", + label: "Label Text Color", + controlType: "COLOR_PICKER", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + propertyName: "labelTextSize", + label: "Label Text Size", + controlType: "DROP_DOWN", + options: [ + { + label: "Heading 1", + value: "HEADING1", + subText: "24px", + icon: "HEADING_ONE", + }, + { + label: "Heading 2", + value: "HEADING2", + subText: "18px", + icon: "HEADING_TWO", + }, + { + label: "Heading 3", + value: "HEADING3", + subText: "16px", + icon: "HEADING_THREE", + }, + { + label: "Paragraph", + value: "PARAGRAPH", + subText: "14px", + icon: "PARAGRAPH", + }, + { + label: "Paragraph 2", + value: "PARAGRAPH2", + subText: "12px", + icon: "PARAGRAPH_TWO", + }, + ], + isBindProperty: false, + isTriggerProperty: false, + }, + { + propertyName: "labelStyle", + label: "Label Font Style", + controlType: "BUTTON_TABS", + options: [ + { + icon: "BOLD_FONT", + value: "BOLD", + }, + { + icon: "ITALICS_FONT", + value: "ITALIC", + }, + ], + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + ], + }, + { + sectionName: "Actions", + children: [ + { + helpText: "Triggers an action when a user selects an option", + propertyName: "onOptionChange", + label: "onOptionChange", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + }, + ], + }, + ]; + } + + static getDerivedPropertiesMap() { + return { + selectedOptionLabels: `{{ this.selectedLabel }}`, + selectedOptionValues: + '{{ this.selectedOptionValueArr.filter((o) => JSON.stringify(this.options).match(new RegExp(`"value":"${o}"`, "g")) )}}', + isValid: `{{ this.isRequired ? this.selectedOptionValues?.length > 0 : true}}`, + }; + } + + static getDefaultPropertiesMap(): Record { + return { + selectedOptionValueArr: "defaultOptionValue", + selectedLabel: "defaultOptionValue", + }; + } + + static getMetaPropertiesMap(): Record { + return { + selectedOptionValueArr: undefined, + selectedLabel: [], + }; + } + getPageView() { + const options = + isArray(this.props.options) && + !this.props.__evaluation__?.errors.options.length + ? this.props.options + : []; + + const values = isArray(this.props.selectedOptionValueArr) + ? this.props.selectedOptionValueArr + : []; + + const filteredValue = this.filterValues(values); + + return ( + + 1 + ) + } + disabled={this.props.isDisabled ?? false} + dropdownStyle={{ + zIndex: Layers.dropdownModalWidget, + }} + expandAll={this.props.expandAll} + labelStyle={this.props.labelStyle} + labelText={this.props.labelText} + labelTextColor={this.props.labelTextColor} + labelTextSize={this.props.labelTextSize} + loading={this.props.isLoading} + mode={this.props.mode} + onChange={this.onOptionChange} + options={options} + placeholder={this.props.placeholderText as string} + value={filteredValue} + /> + ); + } + + onOptionChange = (value?: DefaultValueType, labelList?: ReactNode[]) => { + this.props.updateWidgetMetaProperty("selectedLabel", labelList, { + triggerPropertyName: "onOptionChange", + dynamicString: this.props.onOptionChange, + event: { + type: EventType.ON_OPTION_CHANGE, + }, + }); + + this.props.updateWidgetMetaProperty("selectedOptionValueArr", value, { + triggerPropertyName: "onOptionChange", + dynamicString: this.props.onOptionChange, + event: { + type: EventType.ON_OPTION_CHANGE, + }, + }); + }; + + flat(array: DropdownOption[]) { + let result: { value: string }[] = []; + array.forEach((a) => { + result.push({ value: a.value }); + if (Array.isArray(a.children)) { + result = result.concat(this.flat(a.children)); + } + }); + return result; + } + + filterValues(values: string[] | undefined) { + const options = this.props.options + ? this.flat(this.props.options as DropdownOption[]) + : []; + if (isArray(values)) { + return values.filter((o) => { + const index = findIndex(options, { value: o }); + return index > -1; + }); + } + } + + static getWidgetType(): WidgetType { + return "MULTI_SELECT_TREE_WIDGET"; + } +} + +export interface DropdownOption { + label: string; + value: string; + disabled?: boolean; + children?: DropdownOption[]; +} + +export interface MultiSelectTreeWidgetProps extends WidgetProps { + placeholderText?: string; + selectedIndexArr?: number[]; + options?: DropdownOption[]; + onOptionChange: string; + defaultOptionValue: string[]; + isRequired: boolean; + isLoading: boolean; + allowClear: boolean; + labelText?: string; + selectedLabel: string[]; + selectedOptionValueArr: string[]; + selectedOptionValues: string[]; + selectedOptionLabels: string[]; + expandAll: boolean; + mode: CheckedStrategy; + labelTextColor?: string; + labelTextSize?: TextSize; + labelStyle?: string; +} + +export default MultiSelectTreeWidget; diff --git a/app/client/src/widgets/MultiSelectWidget/component/index.tsx b/app/client/src/widgets/MultiSelectWidget/component/index.tsx index ed80edbd25..aa4bd94cc2 100644 --- a/app/client/src/widgets/MultiSelectWidget/component/index.tsx +++ b/app/client/src/widgets/MultiSelectWidget/component/index.tsx @@ -48,13 +48,13 @@ function MultiSelectComponent({ const [isSelectAll, setIsSelectAll] = useState(false); const _menu = useRef(null); - const getDropdownPosition = useCallback((node: HTMLElement | null) => { + const getDropdownPosition = useCallback(() => { + const node = _menu.current; if (Boolean(node?.closest(`.${MODAL_PORTAL_CLASSNAME}`))) { return document.querySelector( `.${MODAL_PORTAL_CLASSNAME}`, ) as HTMLElement; } - // TODO: Use generateClassName func. return document.querySelector(`.${CANVAS_CLASSNAME}`) as HTMLElement; }, []); @@ -129,7 +129,7 @@ function MultiSelectComponent({ dropdownRender={dropdownRender} dropdownStyle={dropdownStyle} filterOption={serverSideFiltering ? false : filterOption} - getPopupContainer={() => getDropdownPosition(_menu.current)} + getPopupContainer={getDropdownPosition} inputIcon={inputIcon} loading={loading} maxTagCount={"responsive"} diff --git a/app/client/src/widgets/SingleSelectTreeWidget/component/index.styled.tsx b/app/client/src/widgets/SingleSelectTreeWidget/component/index.styled.tsx new file mode 100644 index 0000000000..35a35478ca --- /dev/null +++ b/app/client/src/widgets/SingleSelectTreeWidget/component/index.styled.tsx @@ -0,0 +1,888 @@ +import React from "react"; +import { Checkbox, Classes, Label } from "@blueprintjs/core"; +import styled, { keyframes } from "styled-components"; +import { Colors } from "constants/Colors"; +import { createGlobalStyle } from "constants/DefaultTheme"; +import { + FontStyleTypes, + TextSize, + TEXT_SIZES, +} from "constants/WidgetConstants"; + +export const menuItemSelectedIcon = (props: { isSelected: boolean }) => { + return ; +}; + +export const TextLabelWrapper = styled.div<{ + compactMode: boolean; +}>` + ${(props) => + props.compactMode ? "&&& {margin-right: 5px;}" : "width: 100%;"} + display: flex; +`; + +export const StyledLabel = styled(Label)<{ + $compactMode: boolean; + $labelText?: string; + $labelTextColor?: string; + $labelTextSize?: TextSize; + $labelStyle?: string; +}>` + overflow-y: hidden; + text-overflow: ellipsis; + width: ${(props) => (props.$compactMode ? "auto" : "100%")}; + text-align: left; + color: ${(props) => props.$labelTextColor || "inherit"}; + font-size: ${(props) => + props.$labelTextSize ? TEXT_SIZES[props.$labelTextSize] : "14px"}; + font-weight: ${(props) => + props?.$labelStyle?.includes(FontStyleTypes.BOLD) ? "bold" : "normal"}; + font-style: ${(props) => + props?.$labelStyle?.includes(FontStyleTypes.ITALIC) ? "italic" : ""}; +`; + +const rcSelectDropdownSlideUpIn = keyframes` + 0% { + opacity: 0; + transform-origin: 0% 0%; + } + 100% { + opacity: 1; + transform-origin: 0% 0%; + } +`; + +const rcSelectDropdownSlideUpOut = keyframes` + 0% { + opacity: 1; + transform-origin: 0% 0%; + } +100% { + opacity: 0; + transform-origin: 0% 0%; + } +`; + +export const DropdownStyles = createGlobalStyle` +.rc-tree-select-dropdown-hidden { + display: none; +} +.rc-tree-select-item-group { + color: #999; + font-weight: bold; + font-size: 80%; +} +.rc-tree-select-item-option { + position: relative; + display: flex; + + flex-direction: row-reverse; + .rc-tree-select-item-option-state { + pointer-events: all; + margin-right: 10px; + } +} +.rc-tree-select-item-option-grouped { + padding-left: 24px; +} +.rc-tree-select-item-option-content { + flex: 1 1 0; +} +.rc-tree-select-item-option-active { + background: rgb(233, 250, 243); +} +.rc-tree-select-item-option-selected { + background: rgb(233, 250, 243); +} +.rc-tree-select-item-option-disabled { + color: #999; +} +.rc-tree-select-item-empty { + text-align: center; + color: #999; +} +.rc-tree-select-selection__choice-zoom { + transition: all 0s; +} +.rc-tree-select-selection__choice-zoom-appear { + opacity: 0; +} +.rc-tree-select-selection__choice-zoom-appear.rc-tree-select-selection__choice-zoom-appear-active { + opacity: 1; +} +.rc-tree-select-selection__choice-zoom-leave { + opacity: 1; +} +.rc-tree-select-selection__choice-zoom-leave.rc-tree-select-selection__choice-zoom-leave-active { + opacity: 0; +} +.rc-tree-select-dropdown-slide-up-enter { + animation-duration: 0s; + animation-fill-mode: both; + transform-origin: 0 0; + opacity: 0; + animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + animation-play-state: paused; +} +.rc-tree-select-dropdown-slide-up-appear { + animation-duration: 0s; + animation-fill-mode: both; + transform-origin: 0 0; + opacity: 0; + animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1); + animation-play-state: paused; +} +.rc-tree-select-dropdown-slide-up-leave { + animation-duration: 0s; + animation-fill-mode: both; + transform-origin: 0 0; + opacity: 1; + animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34); + animation-play-state: paused; +} +.rc-tree-select-dropdown-slide-up-enter.rc-tree-select-dropdown-slide-up-enter-active.rc-tree-select-dropdown-placement-bottomLeft { + animation-name: ${rcSelectDropdownSlideUpIn}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-appear.rc-tree-select-dropdown-slide-up-appear-active.rc-tree-select-dropdown-placement-bottomLeft { + animation-name:${rcSelectDropdownSlideUpIn}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-leave.rc-tree-select-dropdown-slide-up-leave-active.rc-tree-select-dropdown-placement-bottomLeft { + animation-name: ${rcSelectDropdownSlideUpOut}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-enter.rc-tree-select-dropdown-slide-up-enter-active.rc-tree-select-dropdown-placement-topLeft { + animation-name: ${rcSelectDropdownSlideUpIn}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-appear.rc-tree-select-dropdown-slide-up-appear-active.rc-tree-select-dropdown-placement-topLeft { + animation-name: ${rcSelectDropdownSlideUpIn}; + animation-play-state: running; +} +.rc-tree-select-dropdown-slide-up-leave.rc-tree-select-dropdown-slide-up-leave-active.rc-tree-select-dropdown-placement-topLeft { + animation-name: ${rcSelectDropdownSlideUpOut}; + animation-play-state: running; +} + + + + +.tree-select-dropdown.single-tree-select-dropdown { + .rc-tree-select-tree + .rc-tree-select-tree-treenode.rc-tree-select-tree-treenode-disabled + span.rc-tree-select-tree-iconEle { +cursor: not-allowed; + } + .rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-iconEle { + position: relative; + cursor: pointer; + margin-left: 5px; + top: 0; + left: 0; + display: inline-block; + width: 16px; + height: 16px; + direction: ltr; + background-color: #fff; +border: 1px solid #E8E8E8; + border-radius: 100%; + border-collapse: separate; + transition: all .3s; + :after{ + position: absolute; + top: 50%; + left: 52%; + display: table; + width: 10px; + height: 10px; + border: none; + border-top: 0; + border-left: 0; + transform: rotate( + 45deg + ) scale(0) translate(-50%,-50%); + opacity: 0; + transition: all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s; + content: " "; + } + + } + + .rc-tree-select-tree + .rc-tree-select-tree-treenode + .rc-tree-select-tree-node-selected + span.rc-tree-select-tree-iconEle { + :after{ + width: 10px; + height: 10px; + transform: translate(-50%,-50%) scale(1); + background: rgb(3, 179, 101) !important; + opacity: 1; + content: " "; + border-radius: 100%; + + } + } + +} +.tree-select-dropdown { + min-height: 100px; + min-width: 250px !important; + position: absolute; + background: #fff; + width: 100%; + border-radius: 0px; + margin-top: 10px; + padding: 12px; + background: white; + box-shadow: 0 0 2px rgb(0 0 0 / 20%) !important; + &&&& .${Classes.ALIGN_LEFT} { + font-size: 16px; + padding-bottom: 10px; + margin-left: 16px ; + .${Classes.CONTROL_INDICATOR} { + margin-right: 20px; + } + } + &&&& .${Classes.CONTROL} .${Classes.CONTROL_INDICATOR} { + background: white; + box-shadow: none; + border-width: 2px; + border-style: solid; + border-color: ${Colors.GEYSER}; + &::before { + width: auto; + height: 1em; + } + } + .${Classes.CONTROL} input:checked ~ .${Classes.CONTROL_INDICATOR} { + background: rgb(3, 179, 101) !important; + color: rgb(255, 255, 255); + border-color: rgb(3, 179, 101) !important; + box-shadow: none; + outline: none !important; + } + .rc-tree-select-item { + font-size: 16px; + line-height: 1.5; + padding: 5px 16px; + align-items: center; + cursor: pointer; +} +.rc-tree-select-item-option-state { + .bp3-control.bp3-checkbox { + margin-bottom: 0; + } +} + + + +.rc-tree-select-tree { + margin: 0; + border: 1px solid transparent; +} +.rc-tree-select-tree-focused:not(.rc-tree-select-tree-active-focused) { + border-color: cyan; +} +.rc-tree-select-tree .rc-tree-select-tree-treenode { + margin: 0; + padding: 0; + line-height: 24px; + white-space: nowrap; + list-style: none; + outline: 0; + padding: 0 5px; + height: 34px; + align-items: center; + display: flex !important; +} +.rc-tree-select-tree .rc-tree-select-tree-treenode .draggable { + color: #333; + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + user-select: none; + -khtml-user-drag: element; + -webkit-user-drag: element; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode.drop-container + > .draggable::after { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + box-shadow: inset 0 0 0 2px red; + content: ""; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode.drop-container + ~ .rc-tree-select-tree-treenode { + border-left: 2px solid chocolate; +} +.rc-tree-select-tree .rc-tree-select-tree-treenode.drop-target { + background-color: yellowgreen; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode.drop-target + ~ .rc-tree-select-tree-treenode { + border-left: none; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode.filter-node + > .rc-tree-select-tree-node-content-wrapper { + color: #182026 !important; + font-weight: bold !important; +} +.rc-tree-select-tree .rc-tree-select-tree-treenode ul { + margin: 0; + padding: 0 0 0 18px; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + .rc-tree-select-tree-node-content-wrapper { + position: relative; + display: inline-flex; + align-items: center; + height: 34px; + margin: 0; + padding: 0; + text-decoration: none; + vertical-align: top; + cursor: pointer; + flex: 1 +} + +.rc-tree-select-tree-checkbox-checked .rc-tree-select-tree-checkbox-inner:after { + position: absolute; + display: table; + border: 2px solid #fff; + border-top: 0; + border-left: 0; + transform: rotate( +45deg +) scale(1) translate(-50%,-50%); + opacity: 1; + transition: all .2s cubic-bezier(.12,.4,.29,1.46) .1s; + content: " "; +} +.rc-tree-select-tree-checkbox-inner:after { + position: absolute; + top: 50%; + left: 22%; + display: table; + width: 5.71428571px; + height: 9.14285714px; + border: 2px solid #fff; + border-top: 0; + border-left: 0; + transform: rotate( +45deg +) scale(0) translate(-50%,-50%); + opacity: 0; + transition: all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s; + content: " "; +} + +.rc-tree-select-tree-checkbox-indeterminate .rc-tree-select-tree-checkbox-inner:after { + top: 50%; + left: 50%; + width: 8px; + height: 8px; + background-color: rgb(3, 179, 101) !important; + border: 0; + transform: translate(-50%,-50%) scale(1); + opacity: 1; + content: " "; +} + +.rc-tree-select-tree-checkbox:hover:after, .rc-tree-select-tree-checkbox-wrapper:hover .rc-tree-select-tree-checkbox:after { + visibility: visible; +} + +.rc-tree-select-tree-checkbox { + top: initial; + margin: 4px 8px 0 0; +} +.rc-tree-select-tree-checkbox { + box-sizing: border-box; + margin: 0; + padding: 0; + color: #000000d9; + font-size: 14px; + font-variant: tabular-nums; + line-height: 1.5715; + list-style: none; + font-feature-settings: "tnum"; + position: relative; + top: 0; + line-height: 1; + white-space: nowrap; + outline: none; + cursor: pointer; + margin-left: 3px; +} + + +.rc-tree-select-tree-checkbox-wrapper:hover .rc-tree-select-tree-checkbox-inner, .rc-tree-select-tree-checkbox:hover .rc-tree-select-tree-checkbox-inner, .rc-tree-select-tree-checkbox-input:focus+.rc-tree-select-tree-checkbox-inner { + border-color: rgb(3, 179, 101) !important; +} +.rc-tree-select-tree-checkbox-checked .rc-tree-select-tree-checkbox-inner { + border-color: rgb(3, 179, 101) !important; + background: rgb(3, 179, 101) !important; +} + +.rc-tree-select-tree-checkbox-inner { + position: relative; + top: 0; + left: 0; + display: inline-block; + width: 16px; + height: 16px; + direction: ltr; + background-color: #fff; + border: 1px solid #d9d9d9; + border-radius: 0px; + border-collapse: separate; + transition: all .3s; +} + .rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree.select-tree-checkbox-checked { + .rc-tree-select-tree-checkbox-inner { + border-color: rgb(3, 179, 101) !important; + background: rgb(3, 179, 101) !important; + } + } + .single-tree-select-dropdown + .rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-iconEle { + width: 20px; + } + +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-iconEle { + display: inline-block; + width: 0px; + height: 16px; + margin-right: 2px; + line-height: 16px; + vertical-align: -0.125em; + background-color: transparent; + background-image: none; + background-repeat: no-repeat; + background-attachment: scroll; + border: 0 none; + outline: none; + cursor: pointer; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-switcher.rc-tree-select-tree-icon__customize, +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-checkbox.rc-tree-select-tree-icon__customize, +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-iconEle.rc-tree-select-tree-icon__customize { + background-image: none; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-icon_loading { + margin-right: 2px; + vertical-align: top; + background: none; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-switcher.rc-tree-select-tree-switcher-noop { + cursor: auto; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-switcher.rc-tree-select-tree-switcher_open { + background-position: -93px -56px; +} +.rc-tree-select-tree + .rc-tree-select-tree-treenode + span.rc-tree-select-tree-switcher.rc-tree-select-tree-switcher_close { + background-position: -75px -56px; +} +.rc-tree-select-tree:not(.rc-tree-select-tree-show-line) + .rc-tree-select-tree-treenode + .rc-tree-select-tree-switcher-noop { + background: none; +} +.rc-tree-select-tree.rc-tree-select-tree-show-line + .rc-tree-select-tree-treenode:not(:last-child) + > ul { + background: none; +} +.rc-tree-select-tree.rc-tree-select-tree-show-line + .rc-tree-select-tree-treenode:not(:last-child) + > .rc-tree-select-tree-switcher-noop { + background-position: -56px -18px; +} +.rc-tree-select-tree.rc-tree-select-tree-show-line + .rc-tree-select-tree-treenode:last-child + > .rc-tree-select-tree-switcher-noop { + background-position: -56px -36px; +} +.rc-tree-select-tree-child-tree { + display: none; +} +.rc-tree-select-tree-child-tree-open { + display: block; +} +.rc-tree-select-tree-treenode-disabled + > span:not(.rc-tree-select-tree-switcher), +.rc-tree-select-tree-treenode-disabled > a, +.rc-tree-select-tree-treenode-disabled > a span { + color: #767676; + cursor: not-allowed; +} +.rc-tree-select-tree-treenode-active { + background: rgb(233, 250, 243); +} +.rc-tree-select-tree-treenode:hover { + background: rgb(233, 250, 243); +} +.rc-tree-select-tree-node-selected { + background-color: none; + box-shadow: 0 0 0 0 #ffb951; + opacity: 1; +} +.rc-tree-select-tree-icon__open { + margin-right: 2px; + vertical-align: top; + background-position: -110px -16px; +} +.rc-tree-select-tree-icon__close { + margin-right: 2px; + vertical-align: top; + background-position: -110px 0; +} +.rc-tree-select-tree-icon__docu { + margin-right: 2px; + vertical-align: top; + background-position: -110px -32px; +} +.rc-tree-select-tree-icon__customize { + margin-right: 2px; + vertical-align: top; +} +.rc-tree-select-tree-title { + display: inline-block; + margin-left: 10px; + font-size: 16px !important; +} +.rc-tree-select-tree-indent { + display: inline-block; + vertical-align: bottom; + height: 0; +} +.rc-tree-select-tree-indent-unit { + width: 25px; + display: inline-block; +} + + } +`; + +export const TreeSelectContainer = styled.div<{ compactMode: boolean }>` + display: flex; + flex-direction: ${(props) => (props.compactMode ? "row" : "column")}; + align-items: ${(props) => (props.compactMode ? "center" : "left")}; + + label.tree-select-label { + margin-bottom: ${(props) => (props.compactMode ? "0px" : "5px")}; + margin-right: ${(props) => (props.compactMode ? "10px" : "0px")}; + } + .rc-tree-select { + display: inline-block; + font-size: 12px; + width: 100%; + height: 100%; + position: relative; + cursor: pointer; + flex: 1 1; + .rc-tree-select-selection-placeholder { + pointer-events: none; + position: absolute; + top: 50%; + right: 11px; + left: 11px; + transform: translateY(-50%); + transition: all 0.3s; + flex: 1; + overflow: hidden; + color: #bfbfbf; + white-space: nowrap; + text-overflow: ellipsis; + pointer-events: none; + font-size: 14px; + } + .rc-tree-select-selection-search-input { + appearance: none; + &::-webkit-search-cancel-button { + display: none; + appearance: none; + } + } + .rc-tree-select-selection-overflow-item-suffix { + position: relative !important; + left: 0px !important; + } + } + .rc-tree-select-disabled { + cursor: not-allowed; + input { + cursor: not-allowed; + } + .rc-tree-select-selector { + opacity: 0.3; + } + } + .rc-tree-select-show-arrow.rc-tree-select-loading { + .rc-tree-select-arrow-icon { + &::after { + box-sizing: border-box; + width: 12px; + height: 12px; + border-radius: 100%; + border: 2px solid #999; + border-top-color: transparent; + border-bottom-color: transparent; + transform: none; + margin-top: 4px; + animation: rcSelectLoadingIcon 0.5s infinite; + } + } + } + .rc-tree-select-single .rc-tree-select-selector { + display: flex; + flex-wrap: wrap; + padding: 1px; + padding-right: 20px; + box-shadow: none; + border: 1px solid rgb(231, 231, 231); + border-radius: 0px; + width: 100%; + transition: border-color 0.15s ease-in-out 0s, + box-shadow 0.15s ease-in-out 0s; + background-color: white; + height: 100%; + .rc-tree-select-selection-search { + width: 100%; + height: 100%; + input { + width: 100%; + appearance: none; + &::-webkit-search-cancel-button { + display: none; + appearance: none; + } + font-family: system-ui; + + height: 100%; + border: none; + } + } + .rc-tree-select-selection-item { + pointer-events: none; + position: absolute; + top: 50%; + right: 11px; + left: 11px; + transform: translateY(-50%); + transition: all 0.3s; + flex: 1; + overflow: hidden; + color: #231f20; + white-space: nowrap; + text-overflow: ellipsis; + pointer-events: none; + font-size: 14px; + } + } + .rc-tree-select-multiple { + .rc-tree-select-selector { + padding-right: 20px; + display: flex; + flex-wrap: wrap; + padding: 1px; + box-shadow: none; + border: 1px solid rgb(231, 231, 231); + border-radius: 0px; + width: 100%; + transition: border-color 0.15s ease-in-out 0s, + box-shadow 0.15s ease-in-out 0s; + background-color: white; + .rc-tree-select-selection-item { + background: none; + border: 1px solid rgb(208, 215, 221); + border-radius: 2px; + margin: 3px 2px; + max-width: 273.926px; + height: 24px; + color: #182026; + overflow-wrap: break-word; + display: inline-flex; + flex-direction: row; + align-items: center; + box-shadow: none; + font-size: 12px; + line-height: 16px; + min-height: 20px; + min-width: 20px; + padding: 2px 6px; + position: relative; + } + .rc-tree-select-selection-item-disabled { + cursor: not-allowed; + opacity: 0.5; + } + .rc-tree-select-selection-overflow { + display: flex; + flex-wrap: wrap; + width: 100%; + align-content: center; + } + .rc-tree-select-selection-overflow-item { + flex: none; + max-width: 100%; + } + .rc-tree-select-selection-search { + position: relative; + max-width: 100%; + margin-bottom: 2px; + height: 100%; + display: flex; + align-items: center; + } + .rc-tree-select-selection-search-input { + padding: 1px; + font-family: system-ui; + width: 5px; + margin: 0px; + display: flex; + height: 26px; + flex: 1 1 0%; + border: none; + outline: none; + width: 100%; + } + .rc-tree-select-selection-search-mirror { + padding: 1px; + font-family: system-ui; + width: 5px; + margin: 0px; + display: flex; + height: 26px; + flex: 1 1 0%; + position: absolute; + z-index: 999; + white-space: nowrap; + position: none; + left: 0; + top: 0; + visibility: hidden; + } + } + } + .rc-tree-select-selection-item-content { + flex-grow: 1; + flex-shrink: 1; + margin-right: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + word-wrap: normal; + font-size: 12px; + line-height: 16px; + } + .rc-tree-select-allow-clear { + .rc-tree-select-clear { + position: absolute; + right: 20px; + right: 25px; + top: -1px; + height: 100%; + display: flex; + align-items: center; + z-index: -1; + .rc-tree-select-clear-icon { + font-size: 18px; + font-weight: bold; + } + } + } + .rc-tree-select-allow-clear.rc-tree-select-focused { + .rc-tree-select-clear { + z-index: 1; + } + } + .rc-tree-select-show-arrow.rc-tree-select-multiple { + .rc-tree-select-selector { + padding-right: 20px; + box-shadow: none; + border: 1px solid rgb(231, 231, 231); + border-radius: 0px; + height: inherit; + width: 100%; + transition: border-color 0.15s ease-in-out 0s, + box-shadow 0.15s ease-in-out 0s; + } + } + .rc-tree-select-show-arrow { + .rc-tree-select-arrow { + pointer-events: none; + position: absolute; + right: 5px; + top: 0; + height: 100%; + display: flex; + align-items: center; + } + .rc-tree-select-arrow-icon { + &::after { + content: ""; + border: 5px solid transparent; + width: 0; + height: 0; + display: inline-block; + border-top-color: #999; + transform: translateY(5px); + } + } + } + .rc-tree-select-show-arrow.rc-tree-select-focused { + .rc-tree-select-selector { + border: 1px solid rgb(128, 189, 255); + outline: 0px; + box-shadow: rgba(0, 123, 255, 0.25) 0px 0px 0px 0.1rem; + } + } +`; +export const StyledCheckbox = styled(Checkbox)` + &&.${Classes.CHECKBOX}.${Classes.CONTROL} { + margin: 0; + } +`; + +export const inputIcon = (): JSX.Element => ( + + chevron-down + + +); diff --git a/app/client/src/widgets/SingleSelectTreeWidget/component/index.tsx b/app/client/src/widgets/SingleSelectTreeWidget/component/index.tsx new file mode 100644 index 0000000000..98f813f691 --- /dev/null +++ b/app/client/src/widgets/SingleSelectTreeWidget/component/index.tsx @@ -0,0 +1,182 @@ +import React, { + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from "react"; +import TreeSelect, { TreeSelectProps as SelectProps } from "rc-tree-select"; +import { + TreeSelectContainer, + DropdownStyles, + inputIcon, + StyledLabel, + TextLabelWrapper, +} from "./index.styled"; +import "rc-tree-select/assets/index.less"; +import { DefaultValueType } from "rc-tree-select/lib/interface"; +import { TreeNodeProps } from "rc-tree-select/lib/TreeNode"; +import { + CANVAS_CLASSNAME, + MODAL_PORTAL_CLASSNAME, + TextSize, +} from "constants/WidgetConstants"; +import { Classes } from "@blueprintjs/core"; + +export interface TreeSelectProps + extends Required< + Pick< + SelectProps, + | "disabled" + | "options" + | "placeholder" + | "loading" + | "dropdownStyle" + | "allowClear" + > + > { + value?: DefaultValueType; + onChange: (value?: DefaultValueType, labelList?: ReactNode[]) => void; + expandAll: boolean; + labelText?: string; + labelTextColor?: string; + labelTextSize?: TextSize; + labelStyle?: string; + compactMode: boolean; +} + +const getSvg = (style = {}) => ( + + + + + +); + +const switcherIcon = (treeNode: TreeNodeProps) => { + if (treeNode.isLeaf) { + return ( + + ); + } + return getSvg({ transform: `rotate(${treeNode.expanded ? 90 : 0}deg)` }); +}; + +function SingleSelectTreeComponent({ + allowClear, + compactMode, + disabled, + dropdownStyle, + expandAll, + labelStyle, + labelText, + labelTextColor, + labelTextSize, + loading, + onChange, + options, + placeholder, + value, +}: TreeSelectProps): JSX.Element { + const [key, setKey] = useState(Math.random()); + const _menu = useRef(null); + + // treeDefaultExpandAll is uncontrolled after first render, + // using this to force render to respond to changes in expandAll + useEffect(() => { + setKey(Math.random()); + }, [expandAll]); + + const getDropdownPosition = useCallback(() => { + const node = _menu.current; + if (Boolean(node?.closest(`.${MODAL_PORTAL_CLASSNAME}`))) { + return document.querySelector( + `.${MODAL_PORTAL_CLASSNAME}`, + ) as HTMLElement; + } + return document.querySelector(`.${CANVAS_CLASSNAME}`) as HTMLElement; + }, []); + const onClear = useCallback(() => onChange([], []), []); + + return ( + } + > + + {labelText && ( + + + {labelText} + + + )} + `+${e.length} more`} + notFoundContent="No item Found" + onChange={onChange} + onClear={onClear} + placeholder={placeholder} + showArrow + showSearch + style={{ width: "100%" }} + switcherIcon={switcherIcon} + transitionName="rc-tree-select-dropdown-slide-up" + treeData={options} + treeDefaultExpandAll={expandAll} + treeIcon + treeNodeFilterProp="label" + value={value} + /> + + ); +} + +export default SingleSelectTreeComponent; diff --git a/app/client/src/widgets/SingleSelectTreeWidget/icon.svg b/app/client/src/widgets/SingleSelectTreeWidget/icon.svg new file mode 100644 index 0000000000..dc9ccf2f42 --- /dev/null +++ b/app/client/src/widgets/SingleSelectTreeWidget/icon.svg @@ -0,0 +1 @@ + diff --git a/app/client/src/widgets/SingleSelectTreeWidget/index.ts b/app/client/src/widgets/SingleSelectTreeWidget/index.ts new file mode 100644 index 0000000000..d569f973a5 --- /dev/null +++ b/app/client/src/widgets/SingleSelectTreeWidget/index.ts @@ -0,0 +1,50 @@ +import Widget from "./widget"; +import IconSVG from "./icon.svg"; +import { GRID_DENSITY_MIGRATION_V1 } from "widgets/constants"; + +export const CONFIG = { + type: Widget.getWidgetType(), + name: "Single Select Tree", + iconSVG: IconSVG, + needsMeta: true, + defaults: { + rows: 1.7 * GRID_DENSITY_MIGRATION_V1, + columns: 4 * GRID_DENSITY_MIGRATION_V1, + options: [ + { + label: "Blue", + value: "BLUE", + children: [ + { + label: "Dark Blue", + value: "DARK BLUE", + }, + { + label: "Light Blue", + value: "LIGHT BLUE", + }, + ], + }, + { label: "Green", value: "GREEN" }, + { label: "Red", value: "RED" }, + ], + widgetName: "SingleSelectTree", + defaultOptionValue: "BLUE", + version: 1, + isVisible: true, + isRequired: false, + isDisabled: false, + allowClear: false, + expandAll: false, + placeholderText: "select option", + labelText: "Label", + }, + properties: { + derived: Widget.getDerivedPropertiesMap(), + default: Widget.getDefaultPropertiesMap(), + meta: Widget.getMetaPropertiesMap(), + config: Widget.getPropertyPaneConfig(), + }, +}; + +export default Widget; diff --git a/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx b/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx new file mode 100644 index 0000000000..f137cabc5b --- /dev/null +++ b/app/client/src/widgets/SingleSelectTreeWidget/widget/index.tsx @@ -0,0 +1,425 @@ +import React, { ReactNode } from "react"; +import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget"; +import { TextSize, WidgetType } from "constants/WidgetConstants"; +import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; +import { isArray, findIndex } from "lodash"; +import { + ValidationResponse, + ValidationTypes, +} from "constants/WidgetValidation"; +import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; +import { DefaultValueType } from "rc-select/lib/interface/generator"; +import { Layers } from "constants/Layers"; +import { isString } from "../../../utils/helpers"; +import { AutocompleteDataType } from "utils/autocomplete/TernServer"; +import { GRID_DENSITY_MIGRATION_V1 } from "widgets/constants"; +import SingleSelectTreeComponent from "../component"; + +function defaultOptionValueValidation(value: unknown): ValidationResponse { + if (typeof value === "string") return { isValid: true, parsed: value.trim() }; + if (value === undefined || value === null) + return { + isValid: false, + parsed: "", + message: "This value does not evaluate to type: string", + }; + return { isValid: true, parsed: value }; +} +class SingleSelectTreeWidget extends BaseWidget< + SingleSelectTreeWidgetProps, + WidgetState +> { + static getPropertyPaneConfig() { + return [ + { + sectionName: "General", + children: [ + { + helpText: + "Allows users to select multiple options. Values must be unique", + propertyName: "options", + label: "Options", + controlType: "INPUT_TEXT", + placeholderText: "Enter option value", + isBindProperty: true, + isTriggerProperty: false, + isJSConvertible: false, + validation: { + type: ValidationTypes.NESTED_OBJECT_ARRAY, + params: { + unique: ["value"], + default: [], + children: { + type: ValidationTypes.OBJECT, + params: { + allowedKeys: [ + { + name: "label", + type: ValidationTypes.TEXT, + params: { + default: "", + required: true, + }, + }, + { + name: "value", + type: ValidationTypes.TEXT, + params: { + default: "", + required: true, + }, + }, + { + name: "children", + type: ValidationTypes.ARRAY, + required: false, + params: { + children: { + type: ValidationTypes.OBJECT, + params: { + allowedKeys: [ + { + name: "label", + type: ValidationTypes.TEXT, + params: { + default: "", + required: true, + }, + }, + { + name: "value", + type: ValidationTypes.TEXT, + params: { + default: "", + required: true, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }, + evaluationSubstitutionType: + EvaluationSubstitutionType.SMART_SUBSTITUTE, + }, + { + helpText: "Selects the option with value by default", + propertyName: "defaultOptionValue", + label: "Default Value", + controlType: "INPUT_TEXT", + placeholderText: "Enter option value", + isBindProperty: true, + isTriggerProperty: false, + validation: { + type: ValidationTypes.FUNCTION, + params: { + fn: defaultOptionValueValidation, + expected: { + type: "value", + example: `value1`, + autocompleteDataType: AutocompleteDataType.STRING, + }, + }, + }, + }, + { + helpText: "Label Text", + propertyName: "labelText", + label: "Label Text", + controlType: "INPUT_TEXT", + placeholderText: "Enter Label text", + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + helpText: "Input Place Holder", + propertyName: "placeholderText", + label: "Placeholder", + controlType: "INPUT_TEXT", + placeholderText: "Enter placeholder text", + 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: "isRequired", + label: "Required", + helpText: "Makes input to the widget mandatory", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "allowClear", + label: "Clear all Selections", + helpText: "Enables Icon to clear all Selections", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + { + propertyName: "expandAll", + label: "Expand all by default", + helpText: "Expand All nested options", + controlType: "SWITCH", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.BOOLEAN }, + }, + ], + }, + { + sectionName: "Styles", + children: [ + { + propertyName: "labelTextColor", + label: "Label Text Color", + controlType: "COLOR_PICKER", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + { + propertyName: "labelTextSize", + label: "Label Text Size", + controlType: "DROP_DOWN", + options: [ + { + label: "Heading 1", + value: "HEADING1", + subText: "24px", + icon: "HEADING_ONE", + }, + { + label: "Heading 2", + value: "HEADING2", + subText: "18px", + icon: "HEADING_TWO", + }, + { + label: "Heading 3", + value: "HEADING3", + subText: "16px", + icon: "HEADING_THREE", + }, + { + label: "Paragraph", + value: "PARAGRAPH", + subText: "14px", + icon: "PARAGRAPH", + }, + { + label: "Paragraph 2", + value: "PARAGRAPH2", + subText: "12px", + icon: "PARAGRAPH_TWO", + }, + ], + isBindProperty: false, + isTriggerProperty: false, + }, + { + propertyName: "labelStyle", + label: "Label Font Style", + controlType: "BUTTON_TABS", + options: [ + { + icon: "BOLD_FONT", + value: "BOLD", + }, + { + icon: "ITALICS_FONT", + value: "ITALIC", + }, + ], + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: false, + validation: { type: ValidationTypes.TEXT }, + }, + ], + }, + { + sectionName: "Actions", + children: [ + { + helpText: "Triggers an action when a user selects an option", + propertyName: "onOptionChange", + label: "onOptionChange", + controlType: "ACTION_SELECTOR", + isJSConvertible: true, + isBindProperty: true, + isTriggerProperty: true, + }, + ], + }, + ]; + } + + static getDerivedPropertiesMap() { + return { + selectedOptionLabel: `{{ this.selectedLabel[0] }}`, + selectedOptionValue: + '{{ JSON.stringify(this.options).match(new RegExp(`"value":"${this.selectedOption}"`), "g") ? this.selectedOption : undefined }}', + isValid: `{{this.isRequired ? !!this.selectedOptionValue?.length : true}}`, + }; + } + + static getDefaultPropertiesMap(): Record { + return { + selectedOption: "defaultOptionValue", + selectedLabel: "defaultOptionValue", + }; + } + + static getMetaPropertiesMap(): Record { + return { + selectedOption: undefined, + selectedOptionValueArr: undefined, + selectedLabel: [], + }; + } + + getPageView() { + const options = + isArray(this.props.options) && + !this.props.__evaluation__?.errors.options.length + ? this.props.options + : []; + const values: string | undefined = isString(this.props.selectedOption) + ? this.props.selectedOption + : undefined; + + const filteredValue = this.filterValues(values); + + return ( + + 1 + ) + } + disabled={this.props.isDisabled ?? false} + dropdownStyle={{ + zIndex: Layers.dropdownModalWidget, + }} + expandAll={this.props.expandAll} + labelStyle={this.props.labelStyle} + labelText={this.props.labelText} + labelTextColor={this.props.labelTextColor} + labelTextSize={this.props.labelTextSize} + loading={this.props.isLoading} + onChange={this.onOptionChange} + options={options} + placeholder={this.props.placeholderText as string} + value={filteredValue} + /> + ); + } + + onOptionChange = (value?: DefaultValueType, labelList?: ReactNode[]) => { + this.props.updateWidgetMetaProperty("selectedLabel", labelList, { + triggerPropertyName: "onOptionChange", + dynamicString: this.props.onOptionChange, + event: { + type: EventType.ON_OPTION_CHANGE, + }, + }); + + this.props.updateWidgetMetaProperty("selectedOption", value, { + triggerPropertyName: "onOptionChange", + dynamicString: this.props.onOptionChange, + event: { + type: EventType.ON_OPTION_CHANGE, + }, + }); + }; + + flat(array: DropdownOption[]) { + let result: { value: string }[] = []; + array.forEach((a) => { + result.push({ value: a.value }); + if (Array.isArray(a.children)) { + result = result.concat(this.flat(a.children)); + } + }); + return result; + } + + filterValues(values: string | undefined) { + const options = this.props.options ? this.flat(this.props.options) : []; + + if (isString(values)) { + const index = findIndex(options, { value: values as string }); + return index > -1 ? values : undefined; + } + } + + static getWidgetType(): WidgetType { + return "SINGLE_SELECT_TREE_WIDGET"; + } +} + +export interface DropdownOption { + label: string; + value: string; + disabled?: boolean; + children?: DropdownOption[]; +} + +export interface SingleSelectTreeWidgetProps extends WidgetProps { + placeholderText?: string; + selectedIndex?: number; + options?: DropdownOption[]; + onOptionChange: string; + defaultOptionValue: string; + isRequired: boolean; + isLoading: boolean; + allowClear: boolean; + labelText?: string; + selectedLabel: string[]; + selectedOption: string; + selectedOptionValue: string; + selectedOptionLabel: string; + expandAll: boolean; + labelTextColor?: string; + labelTextSize?: TextSize; + labelStyle?: string; +} + +export default SingleSelectTreeWidget; diff --git a/app/client/src/workers/validations.ts b/app/client/src/workers/validations.ts index 92813a3106..134043874a 100644 --- a/app/client/src/workers/validations.ts +++ b/app/client/src/workers/validations.ts @@ -22,6 +22,16 @@ import evaluate from "./evaluate"; import getIsSafeURL from "utils/validation/getIsSafeURL"; export const UNDEFINED_VALIDATION = "UNDEFINED_VALIDATION"; +const flat = (array: Record[], uniqueParam: string) => { + let result: { value: string }[] = []; + array.forEach((a) => { + result.push({ value: a[uniqueParam] }); + if (Array.isArray(a.children)) { + result = result.concat(flat(a.children, uniqueParam)); + } + }); + return result; +}; function validatePlainObject( config: ValidationConfig, value: Record, @@ -242,6 +252,7 @@ export function getExpectedType(config?: ValidationConfig): string | undefined { } return type; case ValidationTypes.ARRAY: + case ValidationTypes.NESTED_OBJECT_ARRAY: if (config.params?.allowedValues) { const allowed = config.params?.allowedValues.join("' | '"); return `Array<'${allowed}'>`; @@ -626,6 +637,42 @@ export const VALIDATORS: Record = { } return invalidResponse; }, + + [ValidationTypes.NESTED_OBJECT_ARRAY]: ( + config: ValidationConfig, + value: unknown, + props: Record, + ): ValidationResponse => { + let response: ValidationResponse = { + isValid: false, + parsed: config.params?.default || [], + message: `${WIDGET_TYPE_VALIDATION_ERROR} ${getExpectedType(config)}`, + }; + response = VALIDATORS.ARRAY(config, value, props); + + if (!response.isValid) { + return response; + } + // Check if all values and children values are unique + if (config.params?.unique && response.parsed.length) { + if (isArray(config.params?.unique)) { + for (const param of config.params?.unique) { + const flattenedArray = flat(response.parsed, param); + const shouldBeUnique = flattenedArray.map((entry) => + get(entry, param, ""), + ); + if (uniq(shouldBeUnique).length !== flattenedArray.length) { + response = { + ...response, + isValid: false, + message: `Array entry path:${param} must be unique. Duplicate values found`, + }; + } + } + } + } + return response; + }, [ValidationTypes.DATE_ISO_STRING]: ( config: ValidationConfig, value: unknown, diff --git a/app/client/test/__mocks__/RealmExecutorMock.ts b/app/client/test/__mocks__/RealmExecutorMock.ts index a152614261..d3469b7644 100644 --- a/app/client/test/__mocks__/RealmExecutorMock.ts +++ b/app/client/test/__mocks__/RealmExecutorMock.ts @@ -2,7 +2,7 @@ // Import this named export into your test file: export const mockExecute = jest.fn().mockImplementation((src, data) => { let finalSource = "let "; - Object.keys(data).forEach(key => { + Object.keys(data).forEach((key) => { finalSource += ` ${key} = ${JSON.stringify(data[key])}, `; }); finalSource = finalSource.substring(0, finalSource.length - 2) + ";"; diff --git a/app/client/test/factories/Widgets/ContainerFactory.ts b/app/client/test/factories/Widgets/ContainerFactory.ts index 8abcdc4024..c5e8f330a0 100644 --- a/app/client/test/factories/Widgets/ContainerFactory.ts +++ b/app/client/test/factories/Widgets/ContainerFactory.ts @@ -4,7 +4,7 @@ import { WidgetProps } from "widgets/BaseWidget"; export const ContainerFactory = Factory.Sync.makeFactory({ backgroundColor: "#FFFFFF", - widgetName: Factory.each((i) => `Container${(i+1)}` ), + widgetName: Factory.each((i) => `Container${i + 1}`), type: "CONTAINER_WIDGET", containerStyle: "card", isVisible: true, diff --git a/app/client/test/factories/Widgets/RadiogroupFactory.ts b/app/client/test/factories/Widgets/RadiogroupFactory.ts index f34c5a3335..873da00241 100644 --- a/app/client/test/factories/Widgets/RadiogroupFactory.ts +++ b/app/client/test/factories/Widgets/RadiogroupFactory.ts @@ -3,37 +3,39 @@ import { generateReactKey } from "utils/generators"; import { WidgetProps } from "widgets/BaseWidget"; export const RadiogroupFactory = Factory.Sync.makeFactory({ - rightColumn: 16, - topRow: 3, - bottomRow: 5, - parentRowSpace: 38, - isVisible: true, - label: "Test Radio", - type: "RADIO_GROUP_WIDGET", - isLoading: false, - defaultOptionValue: "1", - parentColumnSpace: 34.6875, - leftColumn: 12, - dynamicTriggerPathList: [{ - key: "onSelectionChange" - }], - onSelectionChange: "{{navigateTo()}}", - options: [ + rightColumn: 16, + topRow: 3, + bottomRow: 5, + parentRowSpace: 38, + isVisible: true, + label: "Test Radio", + type: "RADIO_GROUP_WIDGET", + isLoading: false, + defaultOptionValue: "1", + parentColumnSpace: 34.6875, + leftColumn: 12, + dynamicTriggerPathList: [ { - id: "1", - label: "jarvis", - value: "1" + key: "onSelectionChange", + }, + ], + onSelectionChange: "{{navigateTo()}}", + options: [ + { + id: "1", + label: "jarvis", + value: "1", }, { - id: "2", - label: "marvel", - value: "2" + id: "2", + label: "marvel", + value: "2", }, { - label: "iron", - value: "4" - } - ], + label: "iron", + value: "4", + }, + ], dynamicBindingPathList: [], widgetName: Factory.each((i) => `RadioGroup${i + 1}`), widgetId: generateReactKey(), diff --git a/app/client/test/factories/Widgets/TextFactory.ts b/app/client/test/factories/Widgets/TextFactory.ts index e0fd5e9bb1..660b8e899b 100644 --- a/app/client/test/factories/Widgets/TextFactory.ts +++ b/app/client/test/factories/Widgets/TextFactory.ts @@ -3,7 +3,7 @@ import { generateReactKey } from "utils/generators"; import { WidgetProps } from "widgets/BaseWidget"; export const TextFactory = Factory.Sync.makeFactory({ - widgetName: Factory.each((i) => `Text${(i+1)}`), + widgetName: Factory.each((i) => `Text${i + 1}`), rightColumn: 12, onClick: "", isDefaultClickDisabled: true, diff --git a/app/client/test/testMockedWidgets.tsx b/app/client/test/testMockedWidgets.tsx index b5dee0b772..0928fac1b4 100644 --- a/app/client/test/testMockedWidgets.tsx +++ b/app/client/test/testMockedWidgets.tsx @@ -4,10 +4,10 @@ import React from "react"; import { useSelector } from "react-redux"; import { mockGetCanvasWidgetDsl, useMockDsl } from "./testCommon"; -export const MockCanvas = () => { +export function MockCanvas() { const dsl = useSelector(mockGetCanvasWidgetDsl); - return ; -}; + return ; +} export function UpdatedMainContainer({ dsl }: any) { useMockDsl(dsl); return ; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index c761ab53f6..19299c6007 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -14789,7 +14789,7 @@ rc-resize-observer@^1.0.0: rc-util "^5.0.0" resize-observer-polyfill "^1.5.1" -rc-select@^12.1.10: +rc-select@^12.0.0, rc-select@^12.1.10: version "12.1.13" resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-12.1.13.tgz#c33560ccb9339d30695b52458f55efc35af35273" integrity sha512-cPI+aesP6dgCAaey4t4upDbEukJe+XN0DK6oO/6flcCX5o28o7KNZD7JAiVtC/6fCwqwI/kSs7S/43dvHmBl+A== @@ -14802,6 +14802,28 @@ rc-select@^12.1.10: rc-util "^5.9.8" rc-virtual-list "^3.2.0" +rc-tree-select@^4.4.0-alpha.2: + version "4.4.0-alpha.2" + resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-4.4.0-alpha.2.tgz#b96019bd401084076bedac4e49ea50321709b273" + integrity sha512-8PdODhXpNK13z5u3P0uWb8+ghDtpkQ+ImhxgTXZxBj1KSSi0fB1Ey/mHFLP/R3r72vmwMRGkfkpbW2G6ZRtipw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-select "^12.0.0" + rc-tree "^4.0.0" + rc-util "^5.0.5" + +rc-tree@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-4.2.2.tgz#4429187cbbfbecbe989714a607e3de8b3ab7763f" + integrity sha512-V1hkJt092VrOVjNyfj5IYbZKRMHxWihZarvA5hPL/eqm7o2+0SNkeidFYm7LVVBrAKBpOpa0l8xt04uiqOd+6w== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.0.1" + rc-util "^5.0.0" + rc-virtual-list "^3.0.1" + rc-trigger@^5.0.4: version "5.2.9" resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.2.9.tgz#795a787d2b038347dcde27b89a4a5cec8fc40f3e" @@ -14813,7 +14835,7 @@ rc-trigger@^5.0.4: rc-motion "^2.0.0" rc-util "^5.5.0" -rc-util@^5.0.0, rc-util@^5.0.7, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.5.0, rc-util@^5.5.1, rc-util@^5.9.8: +rc-util@^5.0.0, rc-util@^5.0.5, rc-util@^5.0.7, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.5.0, rc-util@^5.5.1, rc-util@^5.9.8: version "5.13.2" resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.13.2.tgz#a8a0bb77743351841ba8bed6393e03b8d2f685c8" integrity sha512-eYc71XXGlp96RMzg01Mhq/T3BL6OOVTDSS0urFEuvpi+e7slhJRhaHGCKy2hqJm18m9ff7VoRoptplKu60dYog== @@ -14822,7 +14844,7 @@ rc-util@^5.0.0, rc-util@^5.0.7, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.5.0, react-is "^16.12.0" shallowequal "^1.1.0" -rc-virtual-list@^3.2.0: +rc-virtual-list@^3.0.1, rc-virtual-list@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.3.0.tgz#2f95a6ddbbf63d78b28662b57f1e69f7472762fe" integrity sha512-lVXpGWC6yMdwV2SHo6kc63WlqjCnb3eO72V726KA2/wh9KA6wi/swcdR3zAowuA8hJxG/lRANmY5kpLZ+Pz3iQ==