fix: handle duplicate tab name in tab widget (#12411)

This commit is contained in:
Bhavin K 2022-04-07 21:49:12 +05:30 committed by GitHub
parent a210d67e55
commit d22ae809cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 355 additions and 11 deletions

View 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
}
]
}
}

View File

@ -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
});

View File

@ -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,

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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);