diff --git a/src/components/SpruceForm/customFormats.ts b/src/components/SpruceForm/customFormats.ts index 3edd4956ad..38e92f383e 100644 --- a/src/components/SpruceForm/customFormats.ts +++ b/src/components/SpruceForm/customFormats.ts @@ -5,6 +5,7 @@ const { validateEmail, validateJira, validateJiraURL, + validateNoSpecialCharacters, validatePercentage, validateRegexp, validateSlack, @@ -13,16 +14,16 @@ const { } = validators; export const customFormats = (jiraHost: string) => ({ + noSpecialCharacters: validateNoSpecialCharacters, // Permit empty string but disallow whitespace noSpaces: /^$|^\S+$/, - // Permit url - validURL: validateURL, - validURLTemplate: validateURLTemplate, validDuration: validateDuration, - validPercentage: validatePercentage, + validEmail: validateEmail, validJiraTicket: validateJira, validJiraURL: (url: string) => validateJiraURL(jiraHost, url), + validPercentage: validatePercentage, validRegex: validateRegexp, validSlack: validateSlack, - validEmail: validateEmail, + validURL: validateURL, + validURLTemplate: validateURLTemplate, }); diff --git a/src/components/SpruceForm/errors.ts b/src/components/SpruceForm/errors.ts index 3eea1a1a14..3776466e6c 100644 --- a/src/components/SpruceForm/errors.ts +++ b/src/components/SpruceForm/errors.ts @@ -1,4 +1,5 @@ import { AjvError } from "@rjsf/core"; +import { allowedSymbols } from "utils/validators"; export enum Errors { Invisible = "invisible", @@ -51,6 +52,11 @@ export const transformErrors = (errors: AjvError[]) => }; case "format": switch (error.params.format) { + case "noSpecialCharacters": + return { + ...error, + message: `Value can only contain numbers, letters and these symbols: ${allowedSymbols}.`, + }; case "noSpaces": return { ...error, diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 257538b0b7..6f8305ea0e 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -227,11 +227,12 @@ export const getProjectSettingsRoute = ( projectId: string, tab?: ProjectSettingsTabRoutes ) => { - if (!tab) { - return `${paths.project}/${projectId}/${PageNames.Settings}`; - } - - return `${paths.project}/${projectId}/${PageNames.Settings}/${tab}`; + // Encode projectId for backwards compatibilty. + // Encoding can be removed when all projectIDs + // are URL friendly withou encoding + const encodedProjectId = encodeURIComponent(projectId); + const root = `${paths.project}/${encodedProjectId}/${PageNames.Settings}`; + return tab ? `${root}/${tab}` : root; }; export const getDistroSettingsRoute = ( diff --git a/src/pages/projectSettings/sharedFormSchema.ts b/src/pages/projectSettings/sharedFormSchema.ts index 3ffe662eec..f58e6d270d 100644 --- a/src/pages/projectSettings/sharedFormSchema.ts +++ b/src/pages/projectSettings/sharedFormSchema.ts @@ -3,7 +3,7 @@ export const projectName = { type: "string" as "string", title: "Project Name", minLength: 1, - format: "noSpaces", + format: "noSpecialCharacters", }, uiSchema: { "ui:data-cy": "project-name-input", @@ -14,7 +14,7 @@ export const projectId = { schema: { type: "string" as "string", title: "Project ID", - format: "noSpaces", + format: "noSpecialCharacters", }, uiSchema: { "ui:data-cy": "project-id-input", diff --git a/src/pages/projectSettings/tabs/GeneralTab/getFormSchema.tsx b/src/pages/projectSettings/tabs/GeneralTab/getFormSchema.tsx index c21b3a230c..76d97f8e80 100644 --- a/src/pages/projectSettings/tabs/GeneralTab/getFormSchema.tsx +++ b/src/pages/projectSettings/tabs/GeneralTab/getFormSchema.tsx @@ -82,6 +82,7 @@ export const getFormSchema = ( title: "Identifier", default: "", minLength: 1, + format: "noSpecialCharacters", }, }), batchTime: { diff --git a/src/utils/validators/index.ts b/src/utils/validators/index.ts index a786fa90b6..9def5f64c9 100644 --- a/src/utils/validators/index.ts +++ b/src/utils/validators/index.ts @@ -132,16 +132,30 @@ const validateRegexp = (regexp: string): boolean => { } }; +const allowedSymbols = "-._~()"; + +/** + * `validateNoSpecialCharacters` tests if a provided string contains no special characters + * @param str - The string to test. + * @returns - true if the string has no special characters and false otherwise + */ +const validateNoSpecialCharacters = (str: string): boolean => { + const noSpecialCharacters = new RegExp(`^[0-9a-zA-Z${allowedSymbols}]*$`); + return noSpecialCharacters.test(str); +}; + export { + allowedSymbols, validateDuration, validateEmail, validateJira, validateJiraURL, + validateNoSpecialCharacters, validateObjectId, validatePercentage, validateRegexp, - validateSSHPublicKey, validateSlack, + validateSSHPublicKey, validateURL, validateURLTemplate, }; diff --git a/src/utils/validators/validators.test.ts b/src/utils/validators/validators.test.ts index 061ca8a63f..45ad6277e7 100644 --- a/src/utils/validators/validators.test.ts +++ b/src/utils/validators/validators.test.ts @@ -1,13 +1,29 @@ import { - validateRegexp, + validateJira, + validateJiraURL, + validateNoSpecialCharacters, validateObjectId, + validateRegexp, + validateSlack, validateSSHPublicKey, - validateJiraURL, - validateJira, validateURL, - validateSlack, } from "."; +describe("validateNoSpecialCharacters", () => { + it("returns true if string has no special characters", () => { + expect(validateNoSpecialCharacters("")).toBe(true); + expect(validateNoSpecialCharacters("helloworld-_~)(")).toBe(true); + expect(validateNoSpecialCharacters("hello-world123")).toBe(true); + expect(validateNoSpecialCharacters("helloworld.123")).toBe(true); + expect(validateNoSpecialCharacters("hellowo~rld.123")).toBe(true); + expect(validateNoSpecialCharacters("helloWorld123")).toBe(true); + + expect(validateNoSpecialCharacters(" ")).toBe(false); + expect(validateNoSpecialCharacters("he/lloworld")).toBe(false); + expect(validateNoSpecialCharacters("hello%world")).toBe(false); + }); +}); + describe("validateObjectId", () => { it("validates object ids", () => { expect(validateObjectId("5f74d99ab2373627c047c5e5")).toBeTruthy();