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:
parent
5efcb47486
commit
ad52842e1c
|
|
@ -6,5 +6,6 @@ describe("Name uniqueness test", function() {
|
||||||
cy.CreateAPI("UniqueName");
|
cy.CreateAPI("UniqueName");
|
||||||
cy.log("Creation of UniqueName Action successful");
|
cy.log("Creation of UniqueName Action successful");
|
||||||
cy.CreationOfUniqueAPIcheck("UniqueName");
|
cy.CreationOfUniqueAPIcheck("UniqueName");
|
||||||
|
cy.CreationOfUniqueAPIcheck("download");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,15 @@ describe("Entity explorer tests related to query and datasource", function() {
|
||||||
|
|
||||||
cy.testSaveDatasource();
|
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").click();
|
||||||
cy.get(".t--edit-datasource-name input")
|
cy.get(".t--edit-datasource-name input")
|
||||||
.clear()
|
.clear()
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ describe("Button Widget Functionality", function() {
|
||||||
widgetsPage.buttonWidget + " " + commonlocators.widgetNameTag,
|
widgetsPage.buttonWidget + " " + commonlocators.widgetNameTag,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// changing button to invalid name
|
||||||
|
cy.invalidWidgetText();
|
||||||
|
|
||||||
//Changing the text on the Button
|
//Changing the text on the Button
|
||||||
cy.testCodeMirror(this.data.ButtonLabel);
|
cy.testCodeMirror(this.data.ButtonLabel);
|
||||||
cy.EvaluateDataType("string");
|
cy.EvaluateDataType("string");
|
||||||
|
|
|
||||||
|
|
@ -451,7 +451,6 @@ Cypress.Commands.add("CreateSubsequentAPI", apiname => {
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add("EditApiName", apiname => {
|
Cypress.Commands.add("EditApiName", apiname => {
|
||||||
//cy.wait("@getUser");
|
|
||||||
cy.get(apiwidget.ApiName).click({ force: true });
|
cy.get(apiwidget.ApiName).click({ force: true });
|
||||||
cy.get(apiwidget.apiTxt)
|
cy.get(apiwidget.apiTxt)
|
||||||
.clear()
|
.clear()
|
||||||
|
|
@ -912,6 +911,7 @@ Cypress.Commands.add(
|
||||||
);
|
);
|
||||||
|
|
||||||
Cypress.Commands.add("widgetText", (text, inputcss, innercss) => {
|
Cypress.Commands.add("widgetText", (text, inputcss, innercss) => {
|
||||||
|
// checking valid widget name
|
||||||
cy.get(commonlocators.editWidgetName)
|
cy.get(commonlocators.editWidgetName)
|
||||||
.click({ force: true })
|
.click({ force: true })
|
||||||
.type(text)
|
.type(text)
|
||||||
|
|
@ -922,6 +922,15 @@ Cypress.Commands.add("widgetText", (text, inputcss, innercss) => {
|
||||||
cy.get(innercss).should("have.text", text);
|
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 => {
|
Cypress.Commands.add("EvaluateDataType", dataType => {
|
||||||
cy.get(commonlocators.evaluatedType)
|
cy.get(commonlocators.evaluatedType)
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ import styled from "styled-components";
|
||||||
import EditableText, {
|
import EditableText, {
|
||||||
EditInteractionKind,
|
EditInteractionKind,
|
||||||
} from "components/editorComponents/EditableText";
|
} from "components/editorComponents/EditableText";
|
||||||
import { removeSpecialChars } from "utils/helpers";
|
import { removeSpecialChars, isNameValid } from "utils/helpers";
|
||||||
import { AppState } from "reducers";
|
import { AppState } from "reducers";
|
||||||
import { RestAction } from "entities/Action";
|
import { RestAction } from "entities/Action";
|
||||||
import { Page } from "constants/ReduxActionConstants";
|
import { Page } from "constants/ReduxActionConstants";
|
||||||
|
import { getDataTree } from "selectors/dataTreeSelectors";
|
||||||
|
import { getExistingPageNames } from "sagas/selectors";
|
||||||
|
|
||||||
import { saveActionName } from "actions/actionActions";
|
import { saveActionName } from "actions/actionActions";
|
||||||
import { Spinner } from "@blueprintjs/core";
|
import { Spinner } from "@blueprintjs/core";
|
||||||
|
|
@ -42,10 +44,6 @@ export const ActionNameEditor = () => {
|
||||||
state.entities.actions.map(action => action.config),
|
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(
|
const currentActionConfig: RestAction | undefined = actions.find(
|
||||||
action => action.id === params.apiId || action.id === params.queryId,
|
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: {
|
const saveStatus: {
|
||||||
isSaving: boolean;
|
isSaving: boolean;
|
||||||
error: boolean;
|
error: boolean;
|
||||||
|
|
@ -68,12 +69,7 @@ export const ActionNameEditor = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasActionNameConflict = useCallback(
|
const hasActionNameConflict = useCallback(
|
||||||
(name: string) =>
|
(name: string) => !isNameValid(name, { ...existingPageNames, ...evalTree }),
|
||||||
!(
|
|
||||||
existingPageNames.indexOf(name) === -1 &&
|
|
||||||
actions.findIndex(action => action.name === name) === -1 &&
|
|
||||||
existingWidgetNames.indexOf(name) === -1
|
|
||||||
),
|
|
||||||
[existingPageNames, actions, existingWidgetNames],
|
[existingPageNames, actions, existingWidgetNames],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,3 +37,52 @@ export type Validator = (
|
||||||
) => ValidationResponse;
|
) => ValidationResponse;
|
||||||
|
|
||||||
export const ISO_DATE_FORMAT = "YYYY-MM-DDTHH:mm:ss.SSSZ";
|
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",
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import { getDatasource } from "selectors/entitiesSelector";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { Datasource } from "api/DatasourcesApi";
|
import { Datasource } from "api/DatasourcesApi";
|
||||||
import { getDataSources } from "selectors/editorSelectors";
|
import { getDataSources } from "selectors/editorSelectors";
|
||||||
|
import { getDataTree } from "selectors/dataTreeSelectors";
|
||||||
|
import { isNameValid } from "utils/helpers";
|
||||||
import { saveDatasourceName } from "actions/datasourceActions";
|
import { saveDatasourceName } from "actions/datasourceActions";
|
||||||
import { Spinner } from "@blueprintjs/core";
|
import { Spinner } from "@blueprintjs/core";
|
||||||
|
|
||||||
|
|
@ -35,6 +37,7 @@ const FormTitle = (props: FormTitleProps) => {
|
||||||
getDatasource(state, params.datasourceId),
|
getDatasource(state, params.datasourceId),
|
||||||
);
|
);
|
||||||
const datasources: Datasource[] = useSelector(getDataSources);
|
const datasources: Datasource[] = useSelector(getDataSources);
|
||||||
|
const evalTree = useSelector(getDataTree);
|
||||||
const [forceUpdate, setForceUpdate] = useState(false);
|
const [forceUpdate, setForceUpdate] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const saveStatus: {
|
const saveStatus: {
|
||||||
|
|
@ -50,11 +53,16 @@ const FormTitle = (props: FormTitleProps) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasNameConflict = React.useCallback(
|
const hasNameConflict = React.useCallback(
|
||||||
(name: string) =>
|
(name: string) => {
|
||||||
datasources.some(
|
const datasourcesNames: Record<string, any> = {};
|
||||||
datasource =>
|
datasources
|
||||||
datasource.name === name && datasource.id !== currentDatasource?.id,
|
.filter(datasource => datasource.id !== currentDatasource?.id)
|
||||||
),
|
.map(datasource => {
|
||||||
|
datasourcesNames[datasource.name] = datasource;
|
||||||
|
});
|
||||||
|
|
||||||
|
return !isNameValid(name, { ...datasourcesNames, ...evalTree });
|
||||||
|
},
|
||||||
[datasources, currentDatasource],
|
[datasources, currentDatasource],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,16 +47,15 @@ import {
|
||||||
} from "redux-saga/effects";
|
} from "redux-saga/effects";
|
||||||
import history from "utils/history";
|
import history from "utils/history";
|
||||||
import { BUILDER_PAGE_URL } from "constants/routes";
|
import { BUILDER_PAGE_URL } from "constants/routes";
|
||||||
|
import { isNameValid } from "utils/helpers";
|
||||||
import { extractCurrentDSL } from "utils/WidgetPropsUtils";
|
import { extractCurrentDSL } from "utils/WidgetPropsUtils";
|
||||||
import {
|
import {
|
||||||
getAllPageIds,
|
getAllPageIds,
|
||||||
getEditorConfigs,
|
getEditorConfigs,
|
||||||
getExistingActionNames,
|
|
||||||
getExistingPageNames,
|
getExistingPageNames,
|
||||||
getExistingWidgetNames,
|
|
||||||
getWidgets,
|
getWidgets,
|
||||||
} from "./selectors";
|
} from "./selectors";
|
||||||
|
import { getDataTree } from "selectors/dataTreeSelectors";
|
||||||
import { validateResponse } from "./ErrorSagas";
|
import { validateResponse } from "./ErrorSagas";
|
||||||
import { executePageLoadActions } from "actions/widgetActions";
|
import { executePageLoadActions } from "actions/widgetActions";
|
||||||
import { ApiResponse } from "api/ApiResponses";
|
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(
|
export function* updateWidgetNameSaga(
|
||||||
action: ReduxAction<{ id: string; newName: string }>,
|
action: ReduxAction<{ id: string; newName: string }>,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { widgetName } = yield select(getWidgetName, action.payload.id);
|
const { widgetName } = yield select(getWidgetName, action.payload.id);
|
||||||
const layoutId = yield select(getCurrentLayoutId);
|
const layoutId = yield select(getCurrentLayoutId);
|
||||||
|
const evalTree = yield select(getDataTree);
|
||||||
const pageId = yield select(getCurrentPageId);
|
const pageId = yield select(getCurrentPageId);
|
||||||
const existingWidgetNames = yield select(getExistingWidgetNames);
|
|
||||||
const existingActionNames = yield select(getExistingActionNames);
|
|
||||||
const existingPageNames = yield select(getExistingPageNames);
|
const existingPageNames = yield select(getExistingPageNames);
|
||||||
const hasWidgetNameConflict =
|
|
||||||
existingWidgetNames.indexOf(action.payload.newName) > -1 ||
|
// check if name is not conflicting with any
|
||||||
existingActionNames.indexOf(action.payload.newName) > -1 ||
|
// existing entity/api/queries/reserved words
|
||||||
existingPageNames.indexOf(action.payload.newName) > -1;
|
if (
|
||||||
if (!hasWidgetNameConflict) {
|
isNameValid(action.payload.newName, {
|
||||||
|
...evalTree,
|
||||||
|
...existingPageNames,
|
||||||
|
})
|
||||||
|
) {
|
||||||
const request: UpdateWidgetNameRequest = {
|
const request: UpdateWidgetNameRequest = {
|
||||||
newName: action.payload.newName,
|
newName: action.payload.newName,
|
||||||
oldName: widgetName,
|
oldName: widgetName,
|
||||||
|
|
|
||||||
|
|
@ -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 = (
|
export const getWidgetByName = (
|
||||||
state: AppState,
|
state: AppState,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ export const getUnevaluatedDataTree = createSelector(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns evaluation tree object
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
*/
|
||||||
export const getDataTree = (state: AppState) => state.evaluations.tree;
|
export const getDataTree = (state: AppState) => state.evaluations.tree;
|
||||||
|
|
||||||
// For autocomplete. Use actions cached responses if
|
// For autocomplete. Use actions cached responses if
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { GridDefaults } from "constants/WidgetConstants";
|
import { GridDefaults } from "constants/WidgetConstants";
|
||||||
|
import { JAVSCRIPT_KEYWORDS } from "constants/WidgetValidation";
|
||||||
export const snapToGrid = (
|
export const snapToGrid = (
|
||||||
columnWidth: number,
|
columnWidth: number,
|
||||||
rowHeight: number,
|
rowHeight: number,
|
||||||
|
|
@ -162,3 +163,26 @@ export const isEllipsisActive = (element: HTMLElement | null) => {
|
||||||
export const convertArrayToSentence = (arr: string[]) => {
|
export const convertArrayToSentence = (arr: string[]) => {
|
||||||
return arr.join(", ").replace(/,\s([^,]+)$/, " and $1");
|
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;
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user