diff --git a/apps/admin/react-router.config.ts b/apps/admin/react-router.config.ts index a4cef08832d..89fb51ac059 100644 --- a/apps/admin/react-router.config.ts +++ b/apps/admin/react-router.config.ts @@ -1,7 +1,7 @@ import type { Config } from "@react-router/dev/config"; -import { joinUrlPath } from "@plane/utils"; +import { normalizeBasePath } from "@plane/utils"; -const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/"; +const basePath = normalizeBasePath(process.env.VITE_ADMIN_BASE_PATH ?? ""); export default { appDirectory: "app", diff --git a/apps/admin/vite.config.ts b/apps/admin/vite.config.ts index f61d9b49eb5..2526ed92a38 100644 --- a/apps/admin/vite.config.ts +++ b/apps/admin/vite.config.ts @@ -3,7 +3,7 @@ import * as dotenv from "dotenv"; import { reactRouter } from "@react-router/dev/vite"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; -import { joinUrlPath } from "@plane/utils"; +import { normalizeBasePath } from "@plane/utils"; dotenv.config({ path: path.resolve(__dirname, ".env") }); @@ -15,7 +15,7 @@ const viteEnv = Object.keys(process.env) return a; }, {}); -const basePath = joinUrlPath(process.env.VITE_ADMIN_BASE_PATH ?? "", "/") ?? "/"; +const basePath = normalizeBasePath(process.env.VITE_ADMIN_BASE_PATH ?? ""); export default defineConfig(() => ({ base: basePath, diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 4d2f112e33e..891da8b915f 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -56,7 +56,7 @@ export const truncateText = (str: string, length: number) => { export const createSimilarString = (str: string) => { const shuffled = str .split("") - .sort(() => Math.random() - 0.5) + .toSorted(() => Math.random() - 0.5) .join(""); return shuffled; @@ -153,7 +153,7 @@ export const checkEmailValidity = (email: string): boolean => { if (!email) return false; const isEmailValid = - /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( email ); @@ -236,7 +236,7 @@ export const isCommentEmpty = (comment: Content | undefined): boolean => { // Handle JSONContent[] (array) if (Array.isArray(comment)) { - return comment.length === 0 || comment.every(isJSONContentEmpty); + return comment.every(isJSONContentEmpty); } // Handle JSONContent (object) @@ -431,3 +431,37 @@ export const joinUrlPath = (...segments: string[]): string => { return pathParts.length > 0 ? `/${pathParts.join("/")}` : ""; } }; + +/** + * @description Normalizes a base path by: + * - converting empty or slash-only values to "/" + * - collapsing duplicate slashes + * - removing trailing slashes except for root + * - ensuring the path starts with a leading slash + * + * @param {string} rawBasePath - Raw base path to normalize + * @returns {string} Normalized base path + * + * @example + * normalizeBasePath("") // returns "/" + * normalizeBasePath("/") // returns "/" + * normalizeBasePath("///api//v1/") // returns "/api/v1" + * normalizeBasePath("api/v1") // returns "/api/v1" + */ +export const normalizeBasePath = (rawBasePath: string): string => { + const trimmed = rawBasePath.trim(); + + // Empty or slash-only paths become root + if (trimmed === "" || /^\/+$/.test(trimmed)) { + return "/"; + } + + // Collapse multiple slashes + const normalized = trimmed.replace(/\/{2,}/g, "/"); + + // Remove trailing slashes except root + const withoutTrailingSlashes = normalized.replace(/\/+$/, ""); + + // Ensure leading slash + return withoutTrailingSlashes.startsWith("/") ? withoutTrailingSlashes : `/${withoutTrailingSlashes}`; +};