Fix/entity name coincide issue 1396 (#1781)

* Fixing the telemetry on self-hosted instances (#1714)

* Fix issue where creating a new application twice does not work (#1713)

* add check if name is conflicting with internal fn

* use already existing selector

* add conflicting names checks on api/queries/widget names

* add test cases for invalid widget/api/query names

* refactor

* change array to map

* remove getDataTreeKeys selector

* refactor

Co-authored-by: Arpit Mohan <mohanarpit@users.noreply.github.com>
Co-authored-by: Abhinav Jha <abhinav@appsmith.com>
Co-authored-by: Pawan Kumar <pawankumar@Pawans-MacBook-Pro.local>
This commit is contained in:
Pawan Kumar 2020-11-23 14:57:00 +05:30 committed by GitHub
parent 5efcb47486
commit ad52842e1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 155 additions and 29 deletions

View File

@ -6,5 +6,6 @@ describe("Name uniqueness test", function() {
cy.CreateAPI("UniqueName");
cy.log("Creation of UniqueName Action successful");
cy.CreationOfUniqueAPIcheck("UniqueName");
cy.CreationOfUniqueAPIcheck("download");
});
});

View File

@ -23,6 +23,15 @@ describe("Entity explorer tests related to query and datasource", function() {
cy.testSaveDatasource();
// checking that conflicting names are not allowed
cy.get(".t--edit-datasource-name").click();
cy.get(".t--edit-datasource-name input")
.clear()
.type("download", { force: true })
.blur();
cy.get(".Toastify").should("contain", "Invalid name");
// checking a valid name
cy.get(".t--edit-datasource-name").click();
cy.get(".t--edit-datasource-name input")
.clear()

View File

@ -23,6 +23,9 @@ describe("Button Widget Functionality", function() {
widgetsPage.buttonWidget + " " + commonlocators.widgetNameTag,
);
// changing button to invalid name
cy.invalidWidgetText();
//Changing the text on the Button
cy.testCodeMirror(this.data.ButtonLabel);
cy.EvaluateDataType("string");

View File

@ -451,7 +451,6 @@ Cypress.Commands.add("CreateSubsequentAPI", apiname => {
});
Cypress.Commands.add("EditApiName", apiname => {
//cy.wait("@getUser");
cy.get(apiwidget.ApiName).click({ force: true });
cy.get(apiwidget.apiTxt)
.clear()
@ -912,6 +911,7 @@ Cypress.Commands.add(
);
Cypress.Commands.add("widgetText", (text, inputcss, innercss) => {
// checking valid widget name
cy.get(commonlocators.editWidgetName)
.click({ force: true })
.type(text)
@ -922,6 +922,15 @@ Cypress.Commands.add("widgetText", (text, inputcss, innercss) => {
cy.get(innercss).should("have.text", text);
});
Cypress.Commands.add("invalidWidgetText", () => {
// checking invalid widget name
cy.get(commonlocators.editWidgetName)
.click({ force: true })
.type("download")
.type("{enter}");
cy.get(commonlocators.toastmsg).contains("download is already being used.");
});
Cypress.Commands.add("EvaluateDataType", dataType => {
cy.get(commonlocators.evaluatedType)
.should("be.visible")

View File

@ -6,10 +6,12 @@ import styled from "styled-components";
import EditableText, {
EditInteractionKind,
} from "components/editorComponents/EditableText";
import { removeSpecialChars } from "utils/helpers";
import { removeSpecialChars, isNameValid } from "utils/helpers";
import { AppState } from "reducers";
import { RestAction } from "entities/Action";
import { Page } from "constants/ReduxActionConstants";
import { getDataTree } from "selectors/dataTreeSelectors";
import { getExistingPageNames } from "sagas/selectors";
import { saveActionName } from "actions/actionActions";
import { Spinner } from "@blueprintjs/core";
@ -42,10 +44,6 @@ export const ActionNameEditor = () => {
state.entities.actions.map(action => action.config),
);
const existingPageNames: string[] = useSelector((state: AppState) =>
state.entities.pageList.pages.map((page: Page) => page.pageName),
);
const currentActionConfig: RestAction | undefined = actions.find(
action => action.id === params.apiId || action.id === params.queryId,
);
@ -56,6 +54,9 @@ export const ActionNameEditor = () => {
),
);
const evalTree = useSelector(getDataTree);
const existingPageNames = useSelector(getExistingPageNames);
const saveStatus: {
isSaving: boolean;
error: boolean;
@ -68,12 +69,7 @@ export const ActionNameEditor = () => {
});
const hasActionNameConflict = useCallback(
(name: string) =>
!(
existingPageNames.indexOf(name) === -1 &&
actions.findIndex(action => action.name === name) === -1 &&
existingWidgetNames.indexOf(name) === -1
),
(name: string) => !isNameValid(name, { ...existingPageNames, ...evalTree }),
[existingPageNames, actions, existingWidgetNames],
);

View File

@ -37,3 +37,52 @@ export type Validator = (
) => ValidationResponse;
export const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ss.SSSZ";
export const JAVSCRIPT_KEYWORDS = {
true: "true",
await: "await",
break: "break",
case: "case",
catch: "catch",
class: "class",
const: "const",
continue: "continue",
debugger: "debugger",
default: "default",
delete: "delete",
do: "do",
else: "else",
enum: "enum",
export: "export",
extends: "extends",
false: "false",
finally: "finally",
for: "for",
function: "function",
if: "if",
implements: "implements",
import: "import",
in: "in",
instanceof: "instanceof",
interface: "interface",
let: "let",
new: "new",
null: "null",
package: "package",
private: "private",
protected: "protected",
public: "public",
return: "return",
static: "static",
super: "super",
switch: "switch",
this: "this",
throw: "throw",
try: "try",
typeof: "typeof",
var: "var",
void: "void",
while: "while",
with: "with",
yield: "yield",
};

View File

@ -10,6 +10,8 @@ import { getDatasource } from "selectors/entitiesSelector";
import { useSelector, useDispatch } from "react-redux";
import { Datasource } from "api/DatasourcesApi";
import { getDataSources } from "selectors/editorSelectors";
import { getDataTree } from "selectors/dataTreeSelectors";
import { isNameValid } from "utils/helpers";
import { saveDatasourceName } from "actions/datasourceActions";
import { Spinner } from "@blueprintjs/core";
@ -35,6 +37,7 @@ const FormTitle = (props: FormTitleProps) => {
getDatasource(state, params.datasourceId),
);
const datasources: Datasource[] = useSelector(getDataSources);
const evalTree = useSelector(getDataTree);
const [forceUpdate, setForceUpdate] = useState(false);
const dispatch = useDispatch();
const saveStatus: {
@ -50,11 +53,16 @@ const FormTitle = (props: FormTitleProps) => {
});
const hasNameConflict = React.useCallback(
(name: string) =>
datasources.some(
datasource =>
datasource.name === name && datasource.id !== currentDatasource?.id,
),
(name: string) => {
const datasourcesNames: Record<string, any> = {};
datasources
.filter(datasource => datasource.id !== currentDatasource?.id)
.map(datasource => {
datasourcesNames[datasource.name] = datasource;
});
return !isNameValid(name, { ...datasourcesNames, ...evalTree });
},
[datasources, currentDatasource],
);

View File

@ -47,16 +47,15 @@ import {
} from "redux-saga/effects";
import history from "utils/history";
import { BUILDER_PAGE_URL } from "constants/routes";
import { isNameValid } from "utils/helpers";
import { extractCurrentDSL } from "utils/WidgetPropsUtils";
import {
getAllPageIds,
getEditorConfigs,
getExistingActionNames,
getExistingPageNames,
getExistingWidgetNames,
getWidgets,
} from "./selectors";
import { getDataTree } from "selectors/dataTreeSelectors";
import { validateResponse } from "./ErrorSagas";
import { executePageLoadActions } from "actions/widgetActions";
import { ApiResponse } from "api/ApiResponses";
@ -524,21 +523,32 @@ export function* clonePageSaga(clonePageAction: ReduxAction<ClonePageRequest>) {
}
}
/**
* this saga do two things
*
* 1. Checks if the name of page is conflicting with any used name
* 2. dispatches a action which triggers a request to update the name
*
* @param action
*/
export function* updateWidgetNameSaga(
action: ReduxAction<{ id: string; newName: string }>,
) {
try {
const { widgetName } = yield select(getWidgetName, action.payload.id);
const layoutId = yield select(getCurrentLayoutId);
const evalTree = yield select(getDataTree);
const pageId = yield select(getCurrentPageId);
const existingWidgetNames = yield select(getExistingWidgetNames);
const existingActionNames = yield select(getExistingActionNames);
const existingPageNames = yield select(getExistingPageNames);
const hasWidgetNameConflict =
existingWidgetNames.indexOf(action.payload.newName) > -1 ||
existingActionNames.indexOf(action.payload.newName) > -1 ||
existingPageNames.indexOf(action.payload.newName) > -1;
if (!hasWidgetNameConflict) {
// check if name is not conflicting with any
// existing entity/api/queries/reserved words
if (
isNameValid(action.payload.newName, {
...evalTree,
...existingPageNames,
})
) {
const request: UpdateWidgetNameRequest = {
newName: action.payload.newName,
oldName: widgetName,

View File

@ -84,8 +84,20 @@ export const getExistingActionNames = createSelector(
},
);
export const getExistingPageNames = (state: AppState) =>
state.entities.pageList.pages.map((page: Page) => page.pageName);
/**
* returns a objects of existing page name in data tree
*
* @param state
*/
export const getExistingPageNames = (state: AppState) => {
const map: Record<string, any> = {};
state.entities.pageList.pages.map((page: Page) => {
map[page.pageName] = page.pageName;
});
return map;
};
export const getWidgetByName = (
state: AppState,

View File

@ -33,6 +33,11 @@ export const getUnevaluatedDataTree = createSelector(
},
);
/**
* returns evaluation tree object
*
* @param state
*/
export const getDataTree = (state: AppState) => state.evaluations.tree;
// For autocomplete. Use actions cached responses if

View File

@ -1,4 +1,5 @@
import { GridDefaults } from "constants/WidgetConstants";
import { JAVSCRIPT_KEYWORDS } from "constants/WidgetValidation";
export const snapToGrid = (
columnWidth: number,
rowHeight: number,
@ -162,3 +163,26 @@ export const isEllipsisActive = (element: HTMLElement | null) => {
export const convertArrayToSentence = (arr: string[]) => {
return arr.join(", ").replace(/,\s([^,]+)$/, " and $1");
};
/**
* checks if the name is conflciting with
* 1. API names,
* 2. Queries name
* 3. Javascript reserved names
* 4. Few internal function names that are in the evaluation tree
*
* return if false name conflicts with anything from the above list
*
* @param name
* @param invalidNames
*/
export const isNameValid = (
name: string,
invalidNames: Record<string, any>,
) => {
if (name in JAVSCRIPT_KEYWORDS || name in invalidNames) {
return false;
}
return true;
};