diff --git a/graph/client-e2e/src/e2e/dev-project-graph.cy.ts b/graph/client-e2e/src/e2e/dev-project-graph.cy.ts index 8e8e9f4ea2338..86ab967acc7a6 100644 --- a/graph/client-e2e/src/e2e/dev-project-graph.cy.ts +++ b/graph/client-e2e/src/e2e/dev-project-graph.cy.ts @@ -269,6 +269,11 @@ describe('dev mode - project graph', () => { ['cart', ...dependencies, ...dependents].length ); }); + + it('should url encode projects with special chars', () => { + getFocusButtonForProject('@scoped/project-a').click({ force: true }); + cy.url().should('include', '%40scoped%2Fproject-a'); + }); }); describe('unfocus button', () => { diff --git a/graph/client-e2e/src/fixtures/nx-examples-project-graph.json b/graph/client-e2e/src/fixtures/nx-examples-project-graph.json index 7692323db9e03..165b6af9e06b2 100644 --- a/graph/client-e2e/src/fixtures/nx-examples-project-graph.json +++ b/graph/client-e2e/src/fixtures/nx-examples-project-graph.json @@ -1,6 +1,26 @@ { "hash": "081624f3bbc67c126e9dc313133c5a0138ae383da39f8793b26609698aea957b", "projects": [ + { + "name": "@scoped/project-a", + "type": "lib", + "data": { + "tags": [], + "root": "libs/project-a", + "files": [], + "targets": {} + } + }, + { + "name": "@scoped/project-b", + "type": "lib", + "data": { + "tags": [], + "root": "libs/project-a", + "files": [], + "targets": {} + } + }, { "name": "products-product-detail-page", "type": "lib", @@ -1629,6 +1649,13 @@ } ], "dependencies": { + "@scoped/project-a": [ + { + "source": "@scoped/project-a", + "target": "@scoped/project-b" + } + ], + "@scoped/project-b": [], "products-product-detail-page": [ { "source": "products-product-detail-page", diff --git a/graph/client-e2e/src/support/routing-tests.ts b/graph/client-e2e/src/support/routing-tests.ts index a0e9bc4335b4b..e38fd8256907a 100644 --- a/graph/client-e2e/src/support/routing-tests.ts +++ b/graph/client-e2e/src/support/routing-tests.ts @@ -68,6 +68,27 @@ export function testProjectsRoutes( ); }); + it('should focus projects with special characters', () => { + cy.visit( + resolveProjectsRoute(router, `${route}/%40scoped%2Fproject-a`, '') + ); + + // wait for first graph to finish loading + waitForProjectGraph(router); + + const dependencies = nxExamplesJson.dependencies['@scoped/project-a']; + const dependents = Object.keys(nxExamplesJson.dependencies).filter( + (key) => + nxExamplesJson.dependencies[key] + .map((dependencies) => dependencies.target) + .includes('@scoped/project-a') + ); + getCheckedProjectItems().should( + 'have.length', + ['@scoped/project-a', ...dependencies, ...dependents].length + ); + }); + it('should focus projects with search depth', () => { cy.visit( resolveProjectsRoute(router, `${route}/cart`, `searchDepth=2`) diff --git a/graph/client/src/app/external-api.ts b/graph/client/src/app/external-api.ts index 457e2e34b6d25..c6fa8f7f51451 100644 --- a/graph/client/src/app/external-api.ts +++ b/graph/client/src/app/external-api.ts @@ -25,7 +25,7 @@ export class ExternalApi { } focusProject(projectName: string) { - this.router.navigate(`/projects/${projectName}`); + this.router.navigate(`/projects/${encodeURIComponent(projectName)}`); } selectAllProjects() { diff --git a/graph/client/src/app/feature-projects/project-list.tsx b/graph/client/src/app/feature-projects/project-list.tsx index 07bfbae886f63..ddb41da31922f 100644 --- a/graph/client/src/app/feature-projects/project-list.tsx +++ b/graph/client/src/app/feature-projects/project-list.tsx @@ -103,7 +103,7 @@ function ProjectListItem({ className="mr-1 flex items-center rounded-md border-slate-300 bg-white p-1 font-medium text-slate-500 shadow-sm ring-1 ring-slate-200 transition hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-400 dark:ring-slate-600 hover:dark:bg-slate-700" title="Focus on this library" to={routeConstructor( - `/projects/${project.projectGraphNode.name}`, + `/projects/${encodeURIComponent(project.projectGraphNode.name)}`, true )} > diff --git a/graph/client/src/app/feature-tasks/tasks-sidebar.tsx b/graph/client/src/app/feature-tasks/tasks-sidebar.tsx index 03adcbe926e02..69731ec16e64b 100644 --- a/graph/client/src/app/feature-tasks/tasks-sidebar.tsx +++ b/graph/client/src/app/feature-tasks/tasks-sidebar.tsx @@ -58,9 +58,15 @@ export function TasksSidebar() { hideAllProjects(); if (params['selectedTarget']) { - navigate({ pathname: `../${target}`, search: searchParams.toString() }); + navigate({ + pathname: `../${encodeURIComponent(target)}`, + search: searchParams.toString(), + }); } else { - navigate({ pathname: `${target}`, search: searchParams.toString() }); + navigate({ + pathname: `${encodeURIComponent(target)}`, + search: searchParams.toString(), + }); } } diff --git a/graph/client/src/app/shell.tsx b/graph/client/src/app/shell.tsx index 646cd75f2547b..b61ce729bb696 100644 --- a/graph/client/src/app/shell.tsx +++ b/graph/client/src/app/shell.tsx @@ -53,7 +53,7 @@ export function Shell(): JSX.Element { ]; function projectChange(projectGraphId: string) { - navigate(`/${projectGraphId}${topLevelRoute}`); + navigate(`/${encodeURIComponent(projectGraphId)}${topLevelRoute}`); } function downloadImage() { @@ -107,7 +107,9 @@ export function Shell(): JSX.Element { projectGraphService.send('deselectAll'); if (environment.environment === 'dev') { navigate( - `/${currentPath.workspace}${event.currentTarget.value}` + `/${encodeURIComponent(currentPath.workspace)}${ + event.currentTarget.value + }` ); } else { navigate(`${event.currentTarget.value}`); diff --git a/graph/client/src/app/ui-tooltips/project-node-tooltip.tsx b/graph/client/src/app/ui-tooltips/project-node-tooltip.tsx index 24ac14a4b0666..4d7146434da9d 100644 --- a/graph/client/src/app/ui-tooltips/project-node-tooltip.tsx +++ b/graph/client/src/app/ui-tooltips/project-node-tooltip.tsx @@ -31,11 +31,20 @@ export function ProjectNodeToolTip({ } function onStartTrace() { - navigate(routeConstructor(`/projects/trace/${id}`, true)); + navigate( + routeConstructor(`/projects/trace/${encodeURIComponent(id)}`, true) + ); } function onEndTrace() { - navigate(routeConstructor(`/projects/trace/${start}/${id}`, true)); + navigate( + routeConstructor( + `/projects/trace/${encodeURIComponent(start)}/${encodeURIComponent( + id + )}`, + true + ) + ); } return ( diff --git a/graph/client/src/assets/project-graphs/e2e.json b/graph/client/src/assets/project-graphs/e2e.json index 9123d954158df..7915163997254 100644 --- a/graph/client/src/assets/project-graphs/e2e.json +++ b/graph/client/src/assets/project-graphs/e2e.json @@ -1,6 +1,24 @@ { "hash": "081624f3bbc67c126e9dc313133c5a0138ae383da39f8793b26609698aea957b", "projects": [ + { + "name": "@scoped/project-a", + "type": "lib", + "data": { + "tags": [], + "root": "libs/project-a", + "files": [] + } + }, + { + "name": "@scoped/project-b", + "type": "lib", + "data": { + "tags": [], + "root": "libs/project-a", + "files": [] + } + }, { "name": "products-product-detail-page", "type": "lib", @@ -1629,6 +1647,13 @@ } ], "dependencies": { + "@scoped/project-a": [ + { + "source": "@scoped/project-a", + "target": "@scoped/project-b" + } + ], + "@scoped/project-b": [], "products-product-detail-page": [ { "source": "products-product-detail-page", diff --git a/graph/client/src/assets/release-static/environment.js b/graph/client/src/assets/release-static/environment.js index 9e982485d85ad..ac29df466a287 100644 --- a/graph/client/src/assets/release-static/environment.js +++ b/graph/client/src/assets/release-static/environment.js @@ -21,6 +21,24 @@ window.appConfig = { window.projectGraphResponse = { hash: '081624f3bbc67c126e9dc313133c5a0138ae383da39f8793b26609698aea957b', projects: [ + { + name: '@scoped/project-a', + type: 'lib', + data: { + tags: [], + root: 'libs/project-a', + files: [], + }, + }, + { + name: '@scoped/project-b', + type: 'lib', + data: { + tags: [], + root: 'libs/project-a', + files: [], + }, + }, { name: 'products-product-detail-page', type: 'lib', @@ -1651,6 +1669,13 @@ window.projectGraphResponse = { }, ], dependencies: { + '@scoped/project-a': [ + { + source: '@scoped/project-a', + target: '@scoped/project-b', + }, + ], + '@scoped/project-b': [], 'products-product-detail-page': [ { source: 'products-product-detail-page',