fix: handle duplicate tab name in tab widget (#12411)
This commit is contained in:
parent
a210d67e55
commit
d22ae809cb
253
app/client/cypress/fixtures/tabsWidgetDsl.json
Normal file
253
app/client/cypress/fixtures/tabsWidgetDsl.json
Normal file
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -128,12 +128,13 @@ export function DraggableListCard(props: RenderComponentProps) {
|
|||
const showDelete = !!item.isDerived || isDelete;
|
||||
|
||||
return (
|
||||
<ItemWrapper
|
||||
className={props.item.isDuplicateLabel ? "has-duplicate-label" : ""}
|
||||
>
|
||||
<ItemWrapper className={item.isDuplicateLabel ? "has-duplicate-label" : ""}>
|
||||
<StyledDragIcon height={20} width={20} />
|
||||
<StyledOptionControlInputGroup
|
||||
autoFocus={index === focusedIndex}
|
||||
className={
|
||||
props.item.isDuplicateLabel ? `t--has-duplicate-label-${index}` : ""
|
||||
}
|
||||
dataType="text"
|
||||
onBlur={onBlur}
|
||||
onChange={(value: string) => {
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<StyledPropertyPaneButtonWrapper>
|
||||
<StyledPropertyPaneButton
|
||||
category={Category.tertiary}
|
||||
className="t--add-tab-btn"
|
||||
icon="plus"
|
||||
onClick={addOption}
|
||||
size={Size.medium}
|
||||
|
|
@ -65,6 +67,7 @@ function TabControlComponent(props: RenderComponentProps<DroppableItem>) {
|
|||
type: ReduxActionTypes.WIDGET_DELETE_TAB_CHILD,
|
||||
payload: { ...item, index },
|
||||
});
|
||||
if (props.deleteOption) props.deleteOption(index);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -79,6 +82,7 @@ function TabControlComponent(props: RenderComponentProps<DroppableItem>) {
|
|||
|
||||
type State = {
|
||||
focusedIndex: number | null;
|
||||
duplicateTabIds: string[];
|
||||
};
|
||||
|
||||
class TabControl extends BaseControl<ControlProps, State> {
|
||||
|
|
@ -87,8 +91,27 @@ class TabControl extends BaseControl<ControlProps, State> {
|
|||
|
||||
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<ControlProps, State> {
|
|||
}
|
||||
|
||||
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<Record<string, any>>) => {
|
||||
|
|
@ -164,12 +192,11 @@ class TabControl extends BaseControl<ControlProps, State> {
|
|||
propPaneId: this.props.widgetProperties.widgetId,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TabsWrapper>
|
||||
<DroppableComponent
|
||||
deleteOption={noop}
|
||||
deleteOption={this.deleteOption}
|
||||
fixedHeight={370}
|
||||
focusedIndex={this.state.focusedIndex}
|
||||
itemHeight={45}
|
||||
|
|
@ -203,6 +230,15 @@ class TabControl extends BaseControl<ControlProps, State> {
|
|||
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<ControlProps, State> {
|
|||
`${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) => {
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ function TabsComponent(props: TabsComponentProps) {
|
|||
<TabsContainer isScrollable={isScrollable} ref={tabsRef}>
|
||||
{props.tabs.map((tab, index) => (
|
||||
<StyledText
|
||||
className={`t--tab-${tab.label}`}
|
||||
className={`t--tab-${tab.label} t--tabid-${tab.id}`}
|
||||
key={index}
|
||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
onTabChange(tab.widgetId);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user