diff --git a/app/client/cypress/fixtures/tabsWidgetDsl.json b/app/client/cypress/fixtures/tabsWidgetDsl.json new file mode 100644 index 0000000000..b642c167a1 --- /dev/null +++ b/app/client/cypress/fixtures/tabsWidgetDsl.json @@ -0,0 +1,253 @@ +{ + "dsl": { + "widgetName": "MainContainer", + "backgroundColor": "none", + "rightColumn": 816, + "snapColumns": 64, + "detachFromLayout": true, + "widgetId": "0", + "topRow": 0, + "bottomRow": 1290, + "containerStyle": "none", + "snapRows": 128, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "version": 53, + "minHeight": 1292, + "parentColumnSpace": 1, + "dynamicBindingPathList": [], + "leftColumn": 0, + "children": [ + { + "widgetName": "Tabs1", + "isCanvas": true, + "displayName": "Tabs", + "iconSVG": "/static/media/icon.74a6d653.svg", + "topRow": 4, + "bottomRow": 44, + "parentRowSpace": 10, + "type": "TABS_WIDGET", + "hideCard": false, + "shouldScrollContents": false, + "animateLoading": true, + "parentColumnSpace": 12.5625, + "leftColumn": 6, + "children": [ + { + "tabId": "tab1", + "widgetName": "Canvas1", + "displayName": "Canvas", + "topRow": 0, + "bottomRow": 400, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "hideCard": true, + "shouldScrollContents": false, + "minHeight": 400, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [], + "isDisabled": false, + "key": "ea37knju6p", + "tabName": "Tab 1", + "rightColumn": 301.5, + "detachFromLayout": true, + "widgetId": "j10ncmda0q", + "isVisible": true, + "version": 1, + "parentId": "s7xdcvqsg4", + "renderMode": "CANVAS", + "isLoading": false + }, + { + "tabId": "tab2", + "widgetName": "Canvas2", + "displayName": "Canvas", + "topRow": 0, + "bottomRow": 400, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": true, + "hideCard": true, + "shouldScrollContents": false, + "minHeight": 400, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [], + "isDisabled": false, + "key": "ea37knju6p", + "tabName": "Tab 2", + "rightColumn": 301.5, + "detachFromLayout": true, + "widgetId": "zkjnfy3aa5", + "isVisible": true, + "version": 1, + "parentId": "s7xdcvqsg4", + "renderMode": "CANVAS", + "isLoading": false + }, + { + "tabId": "tab3", + "widgetName": "Canvas3", + "displayName": "Canvas", + "topRow": 1, + "bottomRow": 401, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": false, + "hideCard": true, + "minHeight": 400, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [], + "key": "ea37knju6p", + "tabName": "Tab 3", + "rightColumn": 301.5, + "detachFromLayout": true, + "widgetId": "ipxdvnqaoq", + "containerStyle": "none", + "isVisible": true, + "version": 1, + "parentId": "s7xdcvqsg4", + "renderMode": "CANVAS", + "isLoading": false + }, + { + "tabId": "tab4", + "widgetName": "Canvas4", + "displayName": "Canvas", + "topRow": 1, + "bottomRow": 401, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": false, + "hideCard": true, + "minHeight": 400, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [], + "key": "ea37knju6p", + "tabName": "Tab 4", + "rightColumn": 301.5, + "detachFromLayout": true, + "widgetId": "h1y3838ss4", + "containerStyle": "none", + "isVisible": true, + "version": 1, + "parentId": "s7xdcvqsg4", + "renderMode": "CANVAS", + "isLoading": false + }, + { + "tabId": "tab5", + "widgetName": "Canvas5", + "displayName": "Canvas", + "topRow": 1, + "bottomRow": 401, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": false, + "hideCard": true, + "minHeight": 400, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [], + "key": "ea37knju6p", + "tabName": "Tab 5", + "rightColumn": 301.5, + "detachFromLayout": true, + "widgetId": "z5fzrjxc8s", + "containerStyle": "none", + "isVisible": true, + "version": 1, + "parentId": "s7xdcvqsg4", + "renderMode": "CANVAS", + "isLoading": false + }, + { + "tabId": "tab6", + "widgetName": "Canvas6", + "displayName": "Canvas", + "topRow": 1, + "bottomRow": 401, + "parentRowSpace": 1, + "type": "CANVAS_WIDGET", + "canExtend": false, + "hideCard": true, + "minHeight": 400, + "parentColumnSpace": 1, + "leftColumn": 0, + "children": [], + "key": "ea37knju6p", + "tabName": "Tab 6", + "rightColumn": 301.5, + "detachFromLayout": true, + "widgetId": "ya7f4u2w2f", + "containerStyle": "none", + "isVisible": true, + "version": 1, + "parentId": "s7xdcvqsg4", + "renderMode": "CANVAS", + "isLoading": false + } + ], + "key": "qm3k5dd41e", + "rightColumn": 43, + "widgetId": "s7xdcvqsg4", + "defaultTab": "Tab 1", + "shouldShowTabs": true, + "tabsObj": { + "tab1": { + "label": "Tab 1", + "id": "tab1", + "widgetId": "j10ncmda0q", + "isVisible": true, + "index": 0 + }, + "tab2": { + "label": "Tab 2", + "id": "tab2", + "widgetId": "zkjnfy3aa5", + "isVisible": true, + "index": 1 + }, + "tab3": { + "id": "tab3", + "label": "Tab 3", + "widgetId": "ipxdvnqaoq", + "isVisible": true, + "index": 1 + }, + "tab4": { + "id": "tab4", + "label": "Tab 4", + "widgetId": "h1y3838ss4", + "isVisible": true, + "index": 2 + }, + "tab5": { + "id": "tab5", + "label": "Tab 5", + "widgetId": "z5fzrjxc8s", + "isVisible": true, + "index": 3 + }, + "tab6": { + "id": "tab6", + "label": "Tab 6", + "widgetId": "ya7f4u2w2f", + "isVisible": true, + "index": 4 + } + }, + "isVisible": true, + "version": 3, + "parentId": "0", + "renderMode": "CANVAS", + "isLoading": false + } + ] + } +} \ No newline at end of file diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_Duplicate_TabName_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_Duplicate_TabName_spec.js new file mode 100644 index 0000000000..86cb3a2ae5 --- /dev/null +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/LayoutWidgets/Tab_Duplicate_TabName_spec.js @@ -0,0 +1,25 @@ +const Layoutpage = require("../../../../locators/Layout.json"); +const publish = require("../../../../locators/publishWidgetspage.json"); +const dsl = require("../../../../fixtures/tabsWidgetDsl.json"); + +describe("Tab widget test duplicate tab name validation", function() { + before(() => { + cy.addDsl(dsl); + }); + it("Tab Widget Functionality Test with Modal on change of selected tab", function() { + cy.openPropertyPane("tabswidget"); + // added duplicate tab names + cy.tabPopertyUpdate("tab2", "TestUpdated"); + cy.tabPopertyUpdate("tab4", "TestUpdated"); + cy.get(".t--has-duplicate-label-3").should("exist"); + cy.get(".t--has-duplicate-label-4").should("not.exist"); + + // detele column and re-validate duplicate column + cy.deleteColumn("tab2"); + cy.get(".t--has-duplicate-label-3").should("not.exist"); + }); +}); + +afterEach(() => { + // put your clean up code if any +}); diff --git a/app/client/cypress/support/commands.js b/app/client/cypress/support/commands.js index f85d23ab0f..e05f7b9c3f 100644 --- a/app/client/cypress/support/commands.js +++ b/app/client/cypress/support/commands.js @@ -1688,6 +1688,24 @@ Cypress.Commands.add("tableColumnPopertyUpdate", (colId, newColName) => { .should("be.visible"); }); +Cypress.Commands.add("tabPopertyUpdate", (tabId, newTabName) => { + cy.get("[data-rbd-draggable-id='" + tabId + "'] input") + .scrollIntoView() + .should("be.visible") + .click({ + force: true, + }); + cy.get("[data-rbd-draggable-id='" + tabId + "'] input").clear({ + force: true, + }); + cy.get("[data-rbd-draggable-id='" + tabId + "'] input").type(newTabName, { + force: true, + }); + cy.get(`.t--tabid-${tabId}`) + .contains(newTabName) + .should("be.visible"); +}); + Cypress.Commands.add("hideColumn", (colId) => { cy.get("[data-rbd-draggable-id='" + colId + "'] .t--show-column-btn").click({ force: true, diff --git a/app/client/src/components/ads/DraggableListCard.tsx b/app/client/src/components/ads/DraggableListCard.tsx index e9bd5705b3..0645c8c95d 100644 --- a/app/client/src/components/ads/DraggableListCard.tsx +++ b/app/client/src/components/ads/DraggableListCard.tsx @@ -128,12 +128,13 @@ export function DraggableListCard(props: RenderComponentProps) { const showDelete = !!item.isDerived || isDelete; return ( - + { diff --git a/app/client/src/components/propertyControls/TabControl.tsx b/app/client/src/components/propertyControls/TabControl.tsx index 021bf6c74a..eeeebcb62b 100644 --- a/app/client/src/components/propertyControls/TabControl.tsx +++ b/app/client/src/components/propertyControls/TabControl.tsx @@ -7,10 +7,11 @@ import { DroppableComponent, RenderComponentProps, } from "components/ads/DraggableListComponent"; -import { noop } from "utils/AppsmithUtils"; import orderBy from "lodash/orderBy"; import isString from "lodash/isString"; import isUndefined from "lodash/isUndefined"; +import includes from "lodash/includes"; +import map from "lodash/map"; import * as Sentry from "@sentry/react"; import { Category, Size } from "components/ads/Button"; import { useDispatch } from "react-redux"; @@ -46,6 +47,7 @@ function AddTabButtonComponent({ widgetId }: any) { ) { type: ReduxActionTypes.WIDGET_DELETE_TAB_CHILD, payload: { ...item, index }, }); + if (props.deleteOption) props.deleteOption(index); }; return ( @@ -79,6 +82,7 @@ function TabControlComponent(props: RenderComponentProps) { type State = { focusedIndex: number | null; + duplicateTabIds: string[]; }; class TabControl extends BaseControl { @@ -87,8 +91,27 @@ class TabControl extends BaseControl { this.state = { focusedIndex: null, + duplicateTabIds: this.getDuplicateTabIds(props.propertyValue), }; } + + getDuplicateTabIds = (propertyValue: ControlProps["propertyValue"]) => { + const duplicateTabIds = []; + const tabIds = Object.keys(propertyValue); + const tabNames = map(propertyValue, "label"); + + for (let index = 0; index < tabNames.length; index++) { + const currLabel = tabNames[index] as string; + const duplicateValueIndex = tabNames.indexOf(currLabel); + if (duplicateValueIndex !== index) { + // get tab id from propertyValue index + duplicateTabIds.push(propertyValue[tabIds[index]].id); + } + } + + return duplicateTabIds; + }; + componentDidMount() { this.migrateTabData(this.props.propertyValue); } @@ -131,17 +154,22 @@ class TabControl extends BaseControl { } getTabItems = () => { - const menuItems: Array<{ + let menuItems: Array<{ id: string; label: string; - isVisible: boolean; + isVisible?: boolean; + isDuplicateLabel?: boolean; }> = isString(this.props.propertyValue) || isUndefined(this.props.propertyValue) ? [] : Object.values(this.props.propertyValue); - - return orderBy(menuItems, ["index"], ["asc"]); + menuItems = orderBy(menuItems, ["index"], ["asc"]); + menuItems = menuItems.map((tab: DroppableItem) => ({ + ...tab, + isDuplicateLabel: includes(this.state.duplicateTabIds, tab.id), + })); + return menuItems; }; updateItems = (items: Array>) => { @@ -164,12 +192,11 @@ class TabControl extends BaseControl { propPaneId: this.props.widgetProperties.widgetId, }); }; - render() { return ( { this.updateProperty(this.props.propertyName, updatedTabs); }; + deleteOption = (index: number) => { + const tabIds = Object.keys(this.props.propertyValue); + const newPropertyValue = { ...this.props.propertyValue }; + // detele current item from propertyValue + delete newPropertyValue[tabIds[index]]; + const duplicateTabIds = this.getDuplicateTabIds(newPropertyValue); + this.setState({ duplicateTabIds }); + }; + updateOption = (index: number, updatedLabel: string) => { const tabsArray = this.getTabItems(); const { id: itemId } = tabsArray[index]; @@ -210,6 +246,17 @@ class TabControl extends BaseControl { `${this.props.propertyName}.${itemId}.label`, updatedLabel, ); + // check entered label is unique or duplicate + const tabNames = map(tabsArray, "label"); + let duplicateTabIds = [...this.state.duplicateTabIds]; + // if duplicate, add into array + if (includes(tabNames, updatedLabel)) { + duplicateTabIds.push(itemId); + this.setState({ duplicateTabIds }); + } else { + duplicateTabIds = duplicateTabIds.filter((id) => id !== itemId); + this.setState({ duplicateTabIds }); + } }; updateFocus = (index: number, isFocused: boolean) => { diff --git a/app/client/src/widgets/TabsWidget/component/index.tsx b/app/client/src/widgets/TabsWidget/component/index.tsx index e69e749f2f..bf611814e6 100644 --- a/app/client/src/widgets/TabsWidget/component/index.tsx +++ b/app/client/src/widgets/TabsWidget/component/index.tsx @@ -288,7 +288,7 @@ function TabsComponent(props: TabsComponentProps) { {props.tabs.map((tab, index) => ( ) => { onTabChange(tab.widgetId);