From 01651e9b8751aa11fa7fe6f945ea0f14df8c6075 Mon Sep 17 00:00:00 2001 From: mpabarca Date: Mon, 22 Jul 2024 17:24:13 +0200 Subject: [PATCH 01/76] Add new project button to Sidebar Header --- .env.example | 1 + src/features/sidebar/view/SidebarHeader.tsx | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 42dcfe76..f535aac1 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ SHAPE_DOCS_BASE_URL=http://localhost:3000 SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME=.shape-docs.yml +NEXT_PUBLIC_SHAPE_DOCS_PROJECT_WIKI_URL=https://github.com/shapehq/shape-docs/wiki NEXT_PUBLIC_SHAPE_DOCS_TITLE=Shape Docs NEXT_PUBLIC_SHAPE_DOCS_DESCRIPTION=Documentation for Shape's APIs NEXTAUTH_URL_INTERNAL=http://localhost:3000 diff --git a/src/features/sidebar/view/SidebarHeader.tsx b/src/features/sidebar/view/SidebarHeader.tsx index 5bc88a4d..5f384a6d 100644 --- a/src/features/sidebar/view/SidebarHeader.tsx +++ b/src/features/sidebar/view/SidebarHeader.tsx @@ -1,11 +1,15 @@ import Image from "next/image" -import { Box, Typography } from "@mui/material" +import { Box, Typography, IconButton, Tooltip } from "@mui/material" import Link from "next/link" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faPlus } from "@fortawesome/free-solid-svg-icons" export default function SidebarHeader() { const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE + const projectWikiURL = process.env.NEXT_PUBLIC_SHAPE_DOCS_PROJECT_WIKI_URL || "" + return ( - + {siteName} - + + + + + + + + ) } From 777d747374546fb0f9735f3b90479159c738a8c8 Mon Sep 17 00:00:00 2001 From: mpabarca Date: Tue, 23 Jul 2024 09:45:03 +0200 Subject: [PATCH 02/76] Adapt SecondaryHeader style to SidebarHeader --- src/features/sidebar/view/SidebarHeader.tsx | 2 +- src/features/sidebar/view/base/SecondaryHeader.tsx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/features/sidebar/view/SidebarHeader.tsx b/src/features/sidebar/view/SidebarHeader.tsx index 5f384a6d..c3a55eab 100644 --- a/src/features/sidebar/view/SidebarHeader.tsx +++ b/src/features/sidebar/view/SidebarHeader.tsx @@ -9,7 +9,7 @@ export default function SidebarHeader() { const projectWikiURL = process.env.NEXT_PUBLIC_SHAPE_DOCS_PROJECT_WIKI_URL || "" return ( - + - + {showOpenSidebar && onToggleSidebarOpen(true)} edge="start" > - + } {showCloseSidebar && onToggleSidebarOpen(false)} edge="start" > - + } From 4c477b6db3260482b4de7870190dab902a8e73d2 Mon Sep 17 00:00:00 2001 From: Ulrik Andersen Date: Wed, 24 Jul 2024 09:43:12 +0200 Subject: [PATCH 03/76] Enable persistAuthorization for Swagger UI From the docs: "If set to true, it persists authorization data and it would not be lost on browser close/refresh".https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ --- src/features/docs/view/Swagger.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/docs/view/Swagger.tsx b/src/features/docs/view/Swagger.tsx index 98f9838a..ad06d1da 100644 --- a/src/features/docs/view/Swagger.tsx +++ b/src/features/docs/view/Swagger.tsx @@ -7,7 +7,7 @@ const Swagger = ({ url }: { url: string }) => { const [isLoading, setLoading] = useState(true) return ( - setLoading(false)} deepLinking /> + setLoading(false)} deepLinking persistAuthorization /> ) } From 67e5772e8dd201a82002b39422ec092a2d808d06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:07:55 +0000 Subject: [PATCH 04/76] Bump @types/node from 20.14.11 to 20.14.12 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.14.11 to 20.14.12. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 09bd0dc1..02d49388 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "devDependencies": { "@auth/pg-adapter": "^1.4.1", "@types/jest": "^29.5.12", - "@types/node": "^20.14.11", + "@types/node": "^20.14.12", "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/react": "^18.3.3", @@ -4805,9 +4805,9 @@ "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==" }, "node_modules/@types/node": { - "version": "20.14.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", - "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "version": "20.14.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", + "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", "dependencies": { "undici-types": "~5.26.4" } diff --git a/package.json b/package.json index afa77571..bad96dac 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "devDependencies": { "@auth/pg-adapter": "^1.4.1", "@types/jest": "^29.5.12", - "@types/node": "^20.14.11", + "@types/node": "^20.14.12", "@types/nodemailer": "^6.4.15", "@types/pg": "^8.11.6", "@types/react": "^18.3.3", From d7fe0a22f3be01cb59efdd73f0826917aa9f5bbf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:11:09 +0000 Subject: [PATCH 05/76] Bump yaml from 2.4.5 to 2.5.0 Bumps [yaml](https://github.com/eemeli/yaml) from 2.4.5 to 2.5.0. - [Release notes](https://github.com/eemeli/yaml/releases) - [Commits](https://github.com/eemeli/yaml/compare/v2.4.5...v2.5.0) --- updated-dependencies: - dependency-name: yaml dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf0d253e..479052c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "swagger-ui-react": "^5.17.14", "swr": "^2.2.5", "usehooks-ts": "^3.1.0", - "yaml": "^2.4.5", + "yaml": "^2.5.0", "zod": "^3.23.8" }, "devDependencies": { @@ -20226,9 +20226,9 @@ "peer": true }, "node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "bin": { "yaml": "bin.mjs" }, diff --git a/package.json b/package.json index 50ec2a4a..8250b643 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "swagger-ui-react": "^5.17.14", "swr": "^2.2.5", "usehooks-ts": "^3.1.0", - "yaml": "^2.4.5", + "yaml": "^2.5.0", "zod": "^3.23.8" }, "devDependencies": { From fdbeef65dc0389f2fb825073fd1f6d95d7e0d762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 08:06:01 +0200 Subject: [PATCH 06/76] Hides New Project button --- src/features/sidebar/view/SidebarHeader.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/features/sidebar/view/SidebarHeader.tsx b/src/features/sidebar/view/SidebarHeader.tsx index c3a55eab..de09bd9f 100644 --- a/src/features/sidebar/view/SidebarHeader.tsx +++ b/src/features/sidebar/view/SidebarHeader.tsx @@ -1,13 +1,12 @@ import Image from "next/image" -import { Box, Typography, IconButton, Tooltip } from "@mui/material" +// import { Box, Typography, IconButton, Tooltip } from "@mui/material" +import { Box, Typography } from "@mui/material" import Link from "next/link" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faPlus } from "@fortawesome/free-solid-svg-icons" +// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +// import { faPlus } from "@fortawesome/free-solid-svg-icons" export default function SidebarHeader() { const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE - const projectWikiURL = process.env.NEXT_PUBLIC_SHAPE_DOCS_PROJECT_WIKI_URL || "" - return ( - + {/* - + */} ) } From 444984a7d07f8aa02ebccbabc381b09ab85c7c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 08:06:09 +0200 Subject: [PATCH 07/76] Moves environment variable --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index f535aac1..c2c78954 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ SHAPE_DOCS_BASE_URL=http://localhost:3000 SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME=.shape-docs.yml -NEXT_PUBLIC_SHAPE_DOCS_PROJECT_WIKI_URL=https://github.com/shapehq/shape-docs/wiki NEXT_PUBLIC_SHAPE_DOCS_TITLE=Shape Docs NEXT_PUBLIC_SHAPE_DOCS_DESCRIPTION=Documentation for Shape's APIs +NEXT_PUBLIC_SHAPE_DOCS_PROJECT_WIKI_URL=https://github.com/shapehq/shape-docs/wiki NEXTAUTH_URL_INTERNAL=http://localhost:3000 NEXTAUTH_SECRET=use [openssl rand -base64 32] to generate a 32 bytes value REDIS_URL=localhost From 0f686d324f029ade31df7bca7ac96bd56aae5a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 08:07:29 +0200 Subject: [PATCH 08/76] Renames to NEXT_PUBLIC_SHAPE_DOCS_HELP_URL --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index c2c78954..7327e16b 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ SHAPE_DOCS_BASE_URL=http://localhost:3000 SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME=.shape-docs.yml NEXT_PUBLIC_SHAPE_DOCS_TITLE=Shape Docs NEXT_PUBLIC_SHAPE_DOCS_DESCRIPTION=Documentation for Shape's APIs -NEXT_PUBLIC_SHAPE_DOCS_PROJECT_WIKI_URL=https://github.com/shapehq/shape-docs/wiki +NEXT_PUBLIC_SHAPE_DOCS_HELP_URL=https://github.com/shapehq/shape-docs/wiki NEXTAUTH_URL_INTERNAL=http://localhost:3000 NEXTAUTH_SECRET=use [openssl rand -base64 32] to generate a 32 bytes value REDIS_URL=localhost From 126ebab1ac3479b81eb41893fec354f5239af5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 08:08:40 +0200 Subject: [PATCH 09/76] Adds Help button --- src/features/user/view/SettingsList.tsx | 70 +++++++++++++++++-------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/src/features/user/view/SettingsList.tsx b/src/features/user/view/SettingsList.tsx index 93fe7987..57745900 100644 --- a/src/features/user/view/SettingsList.tsx +++ b/src/features/user/view/SettingsList.tsx @@ -1,35 +1,63 @@ -import { List, Button } from "@mui/material" +import { ReactNode } from "react" +import { signOut } from "next-auth/react" +import Link from "next/link" +import { List, Button, Stack, Typography } from "@mui/material" import ThickDivider from "@/common/ui/ThickDivider" import DocumentationVisualizationPicker from "./DocumentationVisualizationPicker" -import { signOut } from "next-auth/react" - -const SettingsItem = ({ onClick, href, children }: { - onClick?: () => void; - href?: string; - children?: string; -}) => - +import { IconProp } from "@fortawesome/fontawesome-svg-core" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faQuestionCircle, faRightFromBracket } from "@fortawesome/free-solid-svg-icons" + +const SettingsItem = ({ onClick, icon, children }: { + onClick?: () => void + icon?: IconProp + children?: ReactNode +}) => { + return ( + + ) +} const SettingsList = () => { + const helpURL = process.env.NEXT_PUBLIC_SHAPE_DOCS_HELP_URL return ( - + - signOut()}> - Log out + {helpURL && helpURL.length > 0 && + + + + Help + + + + } + signOut()} icon={faRightFromBracket}> + + Log out + ) From 588f4cc42117f4338b3e4f13cbe2393516f47103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 08:11:35 +0200 Subject: [PATCH 10/76] Reduces line length --- src/features/sidebar/view/SidebarHeader.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/features/sidebar/view/SidebarHeader.tsx b/src/features/sidebar/view/SidebarHeader.tsx index de09bd9f..916fdb5e 100644 --- a/src/features/sidebar/view/SidebarHeader.tsx +++ b/src/features/sidebar/view/SidebarHeader.tsx @@ -8,7 +8,14 @@ import Link from "next/link" export default function SidebarHeader() { const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE return ( - + Date: Thu, 25 Jul 2024 08:19:41 +0200 Subject: [PATCH 11/76] Fixes linting --- src/features/sidebar/view/base/SecondaryHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/sidebar/view/base/SecondaryHeader.tsx b/src/features/sidebar/view/base/SecondaryHeader.tsx index 2454a3fd..8b65cbd7 100644 --- a/src/features/sidebar/view/base/SecondaryHeader.tsx +++ b/src/features/sidebar/view/base/SecondaryHeader.tsx @@ -57,8 +57,8 @@ export default function SecondaryHeader({ } {showCloseSidebar && - onToggleSidebarOpen(false)} edge="start" From 5383da0791908d62865d05c9af756011139de2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 08:57:06 +0200 Subject: [PATCH 12/76] Adds FilteringGitHubRepositoryDataSource --- .env.example | 1 + src/composition.ts | 20 ++- .../projects/data/GitHubProjectDataSource.ts | 159 ++---------------- .../data/GitHubRepositoryDataSource.ts | 155 +++++++++++++++++ src/features/projects/data/index.ts | 1 + .../FilteringGitHubRepositoryDataSource.ts | 38 +++++ .../IGitHubRepositoryDataSource.ts} | 20 ++- src/features/projects/domain/index.ts | 3 + 8 files changed, 236 insertions(+), 161 deletions(-) create mode 100644 src/features/projects/data/GitHubRepositoryDataSource.ts create mode 100644 src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts rename src/features/projects/{data/GitHubProjectRepository.ts => domain/IGitHubRepositoryDataSource.ts} (58%) diff --git a/.env.example b/.env.example index 42dcfe76..20dfed57 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ POSTGRESQL_USER=dbuser POSTGRESQL_PASSWORD= POSTGRESQL_DB=shape-docs REPOSITORY_NAME_SUFFIX=-openapi +HIDDEN_REPOSITORIES= GITHUB_WEBHOOK_SECRET=preshared secret also put in app configuration in GitHub GITHUB_WEBHOK_REPOSITORY_ALLOWLIST= GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST= diff --git a/src/composition.ts b/src/composition.ts index 0fdeb7e0..a78d6d82 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -17,10 +17,12 @@ import { } from "@/common" import { GitHubLoginDataSource, - GitHubProjectDataSource + GitHubProjectDataSource, + GitHubRepositoryDataSource } from "@/features/projects/data" import { CachingProjectDataSource, + FilteringGitHubRepositoryDataSource, ProjectRepository } from "@/features/projects/domain" import { @@ -157,12 +159,18 @@ export const projectRepository = new ProjectRepository({ export const projectDataSource = new CachingProjectDataSource({ dataSource: new GitHubProjectDataSource({ - loginsDataSource: new GitHubLoginDataSource({ - graphQlClient: userGitHubClient + repositoryDataSource: new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: listFromCommaSeparatedString(env.getOrThrow("HIDDEN_REPOSITORIES")), + dataSource: new GitHubRepositoryDataSource({ + loginsDataSource: new GitHubLoginDataSource({ + graphQlClient: userGitHubClient + }), + graphQlClient: userGitHubClient, + repositoryNameSuffix: env.getOrThrow("REPOSITORY_NAME_SUFFIX"), + projectConfigurationFilename: env.getOrThrow("SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME") + }) }), - graphQlClient: userGitHubClient, - repositoryNameSuffix: env.getOrThrow("REPOSITORY_NAME_SUFFIX"), - projectConfigurationFilename: env.getOrThrow("SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME") + repositoryNameSuffix: env.getOrThrow("REPOSITORY_NAME_SUFFIX") }), repository: projectRepository }) diff --git a/src/features/projects/data/GitHubProjectDataSource.ts b/src/features/projects/data/GitHubProjectDataSource.ts index 848c4859..4d08991c 100644 --- a/src/features/projects/data/GitHubProjectDataSource.ts +++ b/src/features/projects/data/GitHubProjectDataSource.ts @@ -1,38 +1,29 @@ -import GitHubProjectRepository, { - GitHubProjectRepositoryRef -} from "./GitHubProjectRepository" -import IGitHubLoginDataSource from "./IGitHubLoginDataSource" -import IGitHubGraphQLClient from "./IGitHubGraphQLClient" import { Project, Version, IProjectConfig, IProjectDataSource, ProjectConfigParser, - ProjectConfigRemoteVersion + ProjectConfigRemoteVersion, + IGitHubRepositoryDataSource, + GitHubRepository, + GitHubRepositoryRef } from "../domain" export default class GitHubProjectDataSource implements IProjectDataSource { - private readonly loginsDataSource: IGitHubLoginDataSource - private readonly graphQlClient: IGitHubGraphQLClient + private readonly repositoryDataSource: IGitHubRepositoryDataSource private readonly repositoryNameSuffix: string - private readonly projectConfigurationFilename: string constructor(config: { - loginsDataSource: IGitHubLoginDataSource, - graphQlClient: IGitHubGraphQLClient, - repositoryNameSuffix: string, - projectConfigurationFilename: string + repositoryDataSource: IGitHubRepositoryDataSource + repositoryNameSuffix: string }) { - this.loginsDataSource = config.loginsDataSource - this.graphQlClient = config.graphQlClient + this.repositoryDataSource = config.repositoryDataSource this.repositoryNameSuffix = config.repositoryNameSuffix - this.projectConfigurationFilename = config.projectConfigurationFilename.replace(/\.ya?ml$/, "") } async getProjects(): Promise { - const logins = await this.loginsDataSource.getLogins() - const repositories = await this.getRepositories({ logins }) + const repositories = await this.repositoryDataSource.getRepositories() return repositories.map(repository => { return this.mapProject(repository) }) @@ -44,7 +35,7 @@ export default class GitHubProjectDataSource implements IProjectDataSource { }) } - private mapProject(repository: GitHubProjectRepository): Project { + private mapProject(repository: GitHubRepository): Project { const config = this.getConfig(repository) let imageURL: string | undefined if (config && config.image) { @@ -77,7 +68,7 @@ export default class GitHubProjectDataSource implements IProjectDataSource { } } - private getConfig(repository: GitHubProjectRepository): IProjectConfig | null { + private getConfig(repository: GitHubRepository): IProjectConfig | null { const yml = repository.configYml || repository.configYaml if (!yml || !yml.text || yml.text.length == 0) { return null @@ -86,7 +77,7 @@ export default class GitHubProjectDataSource implements IProjectDataSource { return parser.parse(yml.text) } - private getVersions(repository: GitHubProjectRepository): Version[] { + private getVersions(repository: GitHubRepository): Version[] { const branchVersions = repository.branches.edges.map(edge => { const isDefaultRef = edge.node.name == repository.defaultBranchRef.name return this.mapVersionFromRef({ @@ -114,7 +105,7 @@ export default class GitHubProjectDataSource implements IProjectDataSource { }: { ownerName: string repositoryName: string - ref: GitHubProjectRepositoryRef + ref: GitHubRepositoryRef isDefaultRef?: boolean }): Version { const specifications = ref.target.tree.entries.filter(file => { @@ -221,128 +212,4 @@ export default class GitHubProjectDataSource implements IProjectDataSource { .replace(/ /g, "-") .replace(/[^A-Za-z0-9-]/g, "") } - - private async getRepositories({ logins }: { logins: string[] }): Promise { - let searchQueries: string[] = [] - // Search for all private repositories the user has access to. This is needed to find - // repositories for external collaborators who do not belong to an organization. - searchQueries.push(`"${this.repositoryNameSuffix}" in:name is:private`) - // Search for public repositories belonging to a user or organization. - searchQueries = searchQueries.concat(logins.map(login => { - return `"${this.repositoryNameSuffix}" in:name user:${login} is:public` - })) - return await Promise.all(searchQueries.map(searchQuery => { - return this.getRepositoriesForSearchQuery({ searchQuery }) - })) - .then(e => e.flat()) - .then(repositories => { - // GitHub's search API does not enable searching for repositories whose name ends with "-openapi", - // only repositories whose names include "openapi" so we filter the results ourselves. - return repositories.filter(repository => { - return repository.name.endsWith(this.repositoryNameSuffix) - }) - }) - .then(repositories => { - // Ensure we don't have duplicates in the resulting repositories. - const uniqueIdentifiers = new Set() - return repositories.filter(repository => { - const identifier = `${repository.owner.login}-${repository.name}` - const alreadyAdded = uniqueIdentifiers.has(identifier) - uniqueIdentifiers.add(identifier) - return !alreadyAdded - }) - }) - } - - private async getRepositoriesForSearchQuery(params: { - searchQuery: string, - cursor?: string - }): Promise { - const { searchQuery, cursor } = params - const request = { - query: ` - query Repositories($searchQuery: String!, $cursor: String) { - search(query: $searchQuery, type: REPOSITORY, first: 100, after: $cursor) { - results: nodes { - ... on Repository { - name - owner { - login - } - defaultBranchRef { - name - target { - ...on Commit { - oid - } - } - } - configYml: object(expression: "HEAD:${this.projectConfigurationFilename}.yml") { - ...ConfigParts - } - configYaml: object(expression: "HEAD:${this.projectConfigurationFilename}.yaml") { - ...ConfigParts - } - branches: refs(refPrefix: "refs/heads/", first: 100) { - ...RefConnectionParts - } - tags: refs(refPrefix: "refs/tags/", first: 100) { - ...RefConnectionParts - } - } - } - - pageInfo { - hasNextPage - endCursor - } - } - } - - fragment RefConnectionParts on RefConnection { - edges { - node { - name - ... on Ref { - name - target { - ... on Commit { - oid - tree { - entries { - name - } - } - } - } - } - } - } - } - - fragment ConfigParts on GitObject { - ... on Blob { - text - } - } - `, - variables: { searchQuery, cursor } - } - const response = await this.graphQlClient.graphql(request) - if (!response.search || !response.search.results) { - return [] - } - const pageInfo = response.search.pageInfo - if (!pageInfo) { - return response.search.results - } - if (!pageInfo.hasNextPage || !pageInfo.endCursor) { - return response.search.results - } - const nextResults = await this.getRepositoriesForSearchQuery({ - searchQuery, - cursor: pageInfo.endCursor - }) - return response.search.results.concat(nextResults) - } } diff --git a/src/features/projects/data/GitHubRepositoryDataSource.ts b/src/features/projects/data/GitHubRepositoryDataSource.ts new file mode 100644 index 00000000..f26de9b6 --- /dev/null +++ b/src/features/projects/data/GitHubRepositoryDataSource.ts @@ -0,0 +1,155 @@ +import IGitHubLoginDataSource from "./IGitHubLoginDataSource" +import IGitHubGraphQLClient from "./IGitHubGraphQLClient" +import { GitHubRepository, IGitHubRepositoryDataSource } from "../domain" + +export default class GitHubProjectDataSource implements IGitHubRepositoryDataSource { + private readonly loginsDataSource: IGitHubLoginDataSource + private readonly graphQlClient: IGitHubGraphQLClient + private readonly repositoryNameSuffix: string + private readonly projectConfigurationFilename: string + + constructor(config: { + loginsDataSource: IGitHubLoginDataSource, + graphQlClient: IGitHubGraphQLClient, + repositoryNameSuffix: string, + projectConfigurationFilename: string + }) { + this.loginsDataSource = config.loginsDataSource + this.graphQlClient = config.graphQlClient + this.repositoryNameSuffix = config.repositoryNameSuffix + this.projectConfigurationFilename = config.projectConfigurationFilename.replace(/\.ya?ml$/, "") + } + + async getRepositories(): Promise { + const logins = await this.loginsDataSource.getLogins() + return await this.getRepositoriesForLogins({ logins }) + } + + private async getRepositoriesForLogins({ + logins + }: { + logins: string[] + }): Promise { + let searchQueries: string[] = [] + // Search for all private repositories the user has access to. This is needed to find + // repositories for external collaborators who do not belong to an organization. + searchQueries.push(`"${this.repositoryNameSuffix}" in:name is:private`) + // Search for public repositories belonging to a user or organization. + searchQueries = searchQueries.concat(logins.map(login => { + return `"${this.repositoryNameSuffix}" in:name user:${login} is:public` + })) + return await Promise.all(searchQueries.map(searchQuery => { + return this.getRepositoriesForSearchQuery({ searchQuery }) + })) + .then(e => e.flat()) + .then(repositories => { + // GitHub's search API does not enable searching for repositories whose name ends with "-openapi", + // only repositories whose names include "openapi" so we filter the results ourselves. + return repositories.filter(repository => { + return repository.name.endsWith(this.repositoryNameSuffix) + }) + }) + .then(repositories => { + // Ensure we don't have duplicates in the resulting repositories. + const uniqueIdentifiers = new Set() + return repositories.filter(repository => { + const identifier = `${repository.owner.login}-${repository.name}` + const alreadyAdded = uniqueIdentifiers.has(identifier) + uniqueIdentifiers.add(identifier) + return !alreadyAdded + }) + }) + } + + private async getRepositoriesForSearchQuery(params: { + searchQuery: string, + cursor?: string + }): Promise { + const { searchQuery, cursor } = params + const request = { + query: ` + query Repositories($searchQuery: String!, $cursor: String) { + search(query: $searchQuery, type: REPOSITORY, first: 100, after: $cursor) { + results: nodes { + ... on Repository { + name + owner { + login + } + defaultBranchRef { + name + target { + ...on Commit { + oid + } + } + } + configYml: object(expression: "HEAD:${this.projectConfigurationFilename}.yml") { + ...ConfigParts + } + configYaml: object(expression: "HEAD:${this.projectConfigurationFilename}.yaml") { + ...ConfigParts + } + branches: refs(refPrefix: "refs/heads/", first: 100) { + ...RefConnectionParts + } + tags: refs(refPrefix: "refs/tags/", first: 100) { + ...RefConnectionParts + } + } + } + + pageInfo { + hasNextPage + endCursor + } + } + } + + fragment RefConnectionParts on RefConnection { + edges { + node { + name + ... on Ref { + name + target { + ... on Commit { + oid + tree { + entries { + name + } + } + } + } + } + } + } + } + + fragment ConfigParts on GitObject { + ... on Blob { + text + } + } + `, + variables: { searchQuery, cursor } + } + const response = await this.graphQlClient.graphql(request) + if (!response.search || !response.search.results) { + return [] + } + const pageInfo = response.search.pageInfo + if (!pageInfo) { + return response.search.results + } + if (!pageInfo.hasNextPage || !pageInfo.endCursor) { + return response.search.results + } + const nextResults = await this.getRepositoriesForSearchQuery({ + searchQuery, + cursor: pageInfo.endCursor + }) + return response.search.results.concat(nextResults) + } +} diff --git a/src/features/projects/data/index.ts b/src/features/projects/data/index.ts index cf44de31..86fa88a0 100644 --- a/src/features/projects/data/index.ts +++ b/src/features/projects/data/index.ts @@ -3,4 +3,5 @@ export * from "./GitHubProjectDataSource" export { default as useProjects } from "./useProjects" export type { default as IGitHubLoginDataSource } from "./IGitHubLoginDataSource" export { default as GitHubLoginDataSource } from "./GitHubLoginDataSource" +export { default as GitHubRepositoryDataSource } from "./GitHubRepositoryDataSource" export * from "./IGitHubGraphQLClient" diff --git a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts new file mode 100644 index 00000000..2ec639c4 --- /dev/null +++ b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts @@ -0,0 +1,38 @@ +import IGitHubRepositoryDataSource, { + GitHubRepository +} from "./IGitHubRepositoryDataSource" + +export default class FilteringGitHubRepositoryDataSource implements IGitHubRepositoryDataSource { + private readonly dataSource: IGitHubRepositoryDataSource + private readonly hiddenRepositories: string[] + + constructor(config: { + dataSource: IGitHubRepositoryDataSource, + hiddenRepositories: string[] + }) { + this.dataSource = config.dataSource + this.hiddenRepositories = config.hiddenRepositories + } + + async getRepositories(): Promise { + const repositories = await this.dataSource.getRepositories() + // Split full repository names into owner and repository. + // shapehq/foo becomes { owner: "shapehq", "repository": "foo" } + const hiddenOwnerAndRepoNameList = this.hiddenRepositories.map(str => { + const index = str.indexOf("/") + if (index === -1) { + return { owner: str, repository: "" } + } + const owner = str.substring(0, index) + const repository = str.substring(index + 1) + return { owner, repository } + }) + // Only return repositories that are not on the hidden list. + return repositories.filter(repository => { + const hiddenMatch = hiddenOwnerAndRepoNameList.find(e => + e.owner == repository.owner.login && e.repository == repository.name + ) + return hiddenMatch === undefined + }) + } +} \ No newline at end of file diff --git a/src/features/projects/data/GitHubProjectRepository.ts b/src/features/projects/domain/IGitHubRepositoryDataSource.ts similarity index 58% rename from src/features/projects/data/GitHubProjectRepository.ts rename to src/features/projects/domain/IGitHubRepositoryDataSource.ts index 20b88af6..15dbba05 100644 --- a/src/features/projects/data/GitHubProjectRepository.ts +++ b/src/features/projects/domain/IGitHubRepositoryDataSource.ts @@ -1,4 +1,4 @@ -type GitHubProjectRepository = { +export type GitHubRepository = { readonly name: string readonly owner: { readonly login: string @@ -15,12 +15,10 @@ type GitHubProjectRepository = { readonly configYaml?: { readonly text: string } - readonly branches: EdgesContainer - readonly tags: EdgesContainer + readonly branches: EdgesContainer + readonly tags: EdgesContainer } -export default GitHubProjectRepository - type EdgesContainer = { readonly edges: Edge[] } @@ -29,16 +27,20 @@ type Edge = { readonly node: T } -export type GitHubProjectRepositoryRef = { +export type GitHubRepositoryRef = { readonly name: string readonly target: { readonly oid: string readonly tree: { - readonly entries: GitHubProjectRepositoryFile[] + readonly entries: GitHubRepositoryFile[] } } } -export type GitHubProjectRepositoryFile = { +export type GitHubRepositoryFile = { readonly name: string -} \ No newline at end of file +} + +export default interface IGitHubRepositoryDataSource { + getRepositories(): Promise +} diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts index bfce5ee2..62596d40 100644 --- a/src/features/projects/domain/index.ts +++ b/src/features/projects/domain/index.ts @@ -1,5 +1,8 @@ export { default as CachingProjectDataSource } from "./CachingProjectDataSource" +export { default as FilteringGitHubRepositoryDataSource } from "./FilteringGitHubRepositoryDataSource" export { default as getSelection } from "./getSelection" +export type { default as IGitHubRepositoryDataSource } from "./IGitHubRepositoryDataSource" +export * from "./IGitHubRepositoryDataSource" export type { default as IProjectConfig } from "./IProjectConfig" export * from "./IProjectConfig" export type { default as IProjectDataSource } from "./IProjectDataSource" From 017d9eea4adeba8e9b28121c73a59442576ee375 Mon Sep 17 00:00:00 2001 From: mpabarca Date: Thu, 25 Jul 2024 09:31:30 +0200 Subject: [PATCH 13/76] Adapt conditional to falsy value on string --- src/features/user/view/SettingsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/user/view/SettingsList.tsx b/src/features/user/view/SettingsList.tsx index 57745900..183bad38 100644 --- a/src/features/user/view/SettingsList.tsx +++ b/src/features/user/view/SettingsList.tsx @@ -45,7 +45,7 @@ const SettingsList = () => { paddingRight: 0.5 }} /> - {helpURL && helpURL.length > 0 && + {helpURL && From 471b6f9627c8e7bdab54a9bbc7bc1df6b964fb9b Mon Sep 17 00:00:00 2001 From: mpabarca Date: Thu, 25 Jul 2024 09:31:48 +0200 Subject: [PATCH 14/76] Reduces line length --- src/features/sidebar/view/base/SecondaryHeader.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/features/sidebar/view/base/SecondaryHeader.tsx b/src/features/sidebar/view/base/SecondaryHeader.tsx index 8b65cbd7..0e4a0177 100644 --- a/src/features/sidebar/view/base/SecondaryHeader.tsx +++ b/src/features/sidebar/view/base/SecondaryHeader.tsx @@ -42,7 +42,14 @@ export default function SecondaryHeader({ color: theme.palette.text.primary, }} > - + {showOpenSidebar && Date: Thu, 25 Jul 2024 10:03:18 +0200 Subject: [PATCH 15/76] Adds tests for GitHubRepositoryDataSource --- .../GitHubRepositoryDataSource.test.ts | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 __test__/projects/GitHubRepositoryDataSource.test.ts diff --git a/__test__/projects/GitHubRepositoryDataSource.test.ts b/__test__/projects/GitHubRepositoryDataSource.test.ts new file mode 100644 index 00000000..9095b6b9 --- /dev/null +++ b/__test__/projects/GitHubRepositoryDataSource.test.ts @@ -0,0 +1,242 @@ +import { + GitHubRepositoryDataSource + } from "../../src/features/projects/data" + +test("It loads repositories from data source", async () => { + let didLoadRepositories = false + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs.yml", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql() { + didLoadRepositories = true + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(didLoadRepositories).toBeTruthy() +}) + +test("It maps repositories from GraphQL to the GitHubRepository model", async () => { + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs.yml", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql() { + return { + search: { + results: [{ + name: "foo-openapi", + owner: { + login: "acme" + }, + defaultBranchRef: { + name: "main", + target: { + oid: "12345678" + } + }, + branches: { + edges: [{ + node: { + name: "main", + target: { + oid: "12345678", + tree: { + entries: [{ + name: "openapi.yml" + }] + } + } + } + }] + }, + tags: { + edges: [{ + node: { + name: "1.0", + target: { + oid: "12345678", + tree: { + entries: [{ + name: "openapi.yml" + }] + } + } + } + }] + } + }] + } + } + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories).toEqual([{ + "name": "foo-openapi", + "owner": { + "login": "acme" + }, + "defaultBranchRef": { + "name": "main", + "target": { + "oid": "12345678" + } + }, + "branches": { + "edges": [{ + "node": { + "name": "main", + "target": { + "oid": "12345678", + "tree": { + "entries": [{ + "name": "openapi.yml" + }] + } + } + } + }] + }, + "tags": { + "edges": [{ + "node": { + "name": "1.0", + "target": { + "oid": "12345678", + "tree": { + "entries": [{ + "name": "openapi.yml" + }] + } + } + } + }] + } + }]) +}) + +test("It queries for both .yml and .yaml file extension with specifying .yml extension", async () => { + let query: string | undefined + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs.yml", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql(request) { + query = request.query + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(query).toContain(".demo-docs.yml") + expect(query).toContain(".demo-docs.yaml") +}) + +test("It queries for both .yml and .yaml file extension with specifying .yaml extension", async () => { + let query: string | undefined + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs.yml", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql(request) { + query = request.query + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(query).toContain(".demo-docs.yml") + expect(query).toContain(".demo-docs.yaml") +}) + +test("It queries for both .yml and .yaml file extension with no extension", async () => { + let query: string | undefined + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs", + loginsDataSource: { + async getLogins() { + return ["acme"] + } + }, + graphQlClient: { + async graphql(request) { + query = request.query + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(query).toContain(".demo-docs.yml") + expect(query).toContain(".demo-docs.yaml") +}) + +test("It loads repositories for all logins", async () => { + let searchQueries: string[] = [] + const sut = new GitHubRepositoryDataSource({ + repositoryNameSuffix: "-openapi", + projectConfigurationFilename: ".demo-docs", + loginsDataSource: { + async getLogins() { + return ["acme", "somecorp", "techsystems"] + } + }, + graphQlClient: { + async graphql(request) { + if (request.variables?.searchQuery) { + searchQueries.push(request.variables.searchQuery) + } + return { + search: { + results: [] + } + } + } + } + }) + await sut.getRepositories() + expect(searchQueries.length).toEqual(4) + expect(searchQueries).toContain("\"-openapi\" in:name is:private") + expect(searchQueries).toContain("\"-openapi\" in:name user:acme is:public") + expect(searchQueries).toContain("\"-openapi\" in:name user:somecorp is:public") + expect(searchQueries).toContain("\"-openapi\" in:name user:techsystems is:public") +}) From 57cd6dfcd2f24269e942a22ae73658efcd0d7e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 10:48:27 +0200 Subject: [PATCH 16/76] Simplifies GitHubRepository model --- .../GitHubRepositoryDataSource.test.ts | 54 +++++--------- .../projects/data/GitHubProjectDataSource.ts | 30 ++++---- .../data/GitHubRepositoryDataSource.ts | 72 ++++++++++++++++++- .../FilteringGitHubRepositoryDataSource.ts | 2 +- .../domain/IGitHubRepositoryDataSource.ts | 36 +++------- 5 files changed, 115 insertions(+), 79 deletions(-) diff --git a/__test__/projects/GitHubRepositoryDataSource.test.ts b/__test__/projects/GitHubRepositoryDataSource.test.ts index 9095b6b9..c6763ab2 100644 --- a/__test__/projects/GitHubRepositoryDataSource.test.ts +++ b/__test__/projects/GitHubRepositoryDataSource.test.ts @@ -89,46 +89,26 @@ test("It maps repositories from GraphQL to the GitHubRepository model", async () }) const repositories = await sut.getRepositories() expect(repositories).toEqual([{ - "name": "foo-openapi", - "owner": { - "login": "acme" + name: "foo-openapi", + owner: "acme", + defaultBranchRef: { + id: "12345678", + name: "main" }, - "defaultBranchRef": { - "name": "main", - "target": { - "oid": "12345678" - } - }, - "branches": { - "edges": [{ - "node": { - "name": "main", - "target": { - "oid": "12345678", - "tree": { - "entries": [{ - "name": "openapi.yml" - }] - } - } - } + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml" }] - }, - "tags": { - "edges": [{ - "node": { - "name": "1.0", - "target": { - "oid": "12345678", - "tree": { - "entries": [{ - "name": "openapi.yml" - }] - } - } - } + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" }] - } + }] }]) }) diff --git a/src/features/projects/data/GitHubProjectDataSource.ts b/src/features/projects/data/GitHubProjectDataSource.ts index 4d08991c..bc79fe25 100644 --- a/src/features/projects/data/GitHubProjectDataSource.ts +++ b/src/features/projects/data/GitHubProjectDataSource.ts @@ -40,10 +40,10 @@ export default class GitHubProjectDataSource implements IProjectDataSource { let imageURL: string | undefined if (config && config.image) { imageURL = this.getGitHubBlobURL({ - ownerName: repository.owner.login, + ownerName: repository.owner, repositoryName: repository.name, path: config.image, - ref: repository.defaultBranchRef.target.oid + ref: repository.defaultBranchRef.id }) } const versions = this.sortVersions( @@ -57,14 +57,14 @@ export default class GitHubProjectDataSource implements IProjectDataSource { }) const defaultName = repository.name.replace(new RegExp(this.repositoryNameSuffix + "$"), "") return { - id: `${repository.owner.login}-${defaultName}`, - owner: repository.owner.login, + id: `${repository.owner}-${defaultName}`, + owner: repository.owner, name: defaultName, displayName: config?.name || defaultName, versions, imageURL: imageURL, - ownerUrl: `https://github.com/${repository.owner.login}`, - url: `https://github.com/${repository.owner.login}/${repository.name}` + ownerUrl: `https://github.com/${repository.owner}`, + url: `https://github.com/${repository.owner}/${repository.name}` } } @@ -78,20 +78,20 @@ export default class GitHubProjectDataSource implements IProjectDataSource { } private getVersions(repository: GitHubRepository): Version[] { - const branchVersions = repository.branches.edges.map(edge => { - const isDefaultRef = edge.node.name == repository.defaultBranchRef.name + const branchVersions = repository.branches.map(branch => { + const isDefaultRef = branch.name == repository.defaultBranchRef.name return this.mapVersionFromRef({ - ownerName: repository.owner.login, + ownerName: repository.owner, repositoryName: repository.name, - ref: edge.node, + ref: branch, isDefaultRef }) }) - const tagVersions = repository.tags.edges.map(edge => { + const tagVersions = repository.tags.map(tag => { return this.mapVersionFromRef({ - ownerName: repository.owner.login, + ownerName: repository.owner, repositoryName: repository.name, - ref: edge.node + ref: tag }) }) return branchVersions.concat(tagVersions) @@ -108,7 +108,7 @@ export default class GitHubProjectDataSource implements IProjectDataSource { ref: GitHubRepositoryRef isDefaultRef?: boolean }): Version { - const specifications = ref.target.tree.entries.filter(file => { + const specifications = ref.files.filter(file => { return this.isOpenAPISpecification(file.name) }).map(file => { return { @@ -118,7 +118,7 @@ export default class GitHubProjectDataSource implements IProjectDataSource { ownerName, repositoryName, path: file.name, - ref: ref.target.oid + ref: ref.id }), editURL: `https://github.com/${ownerName}/${repositoryName}/edit/${ref.name}/${file.name}` } diff --git a/src/features/projects/data/GitHubRepositoryDataSource.ts b/src/features/projects/data/GitHubRepositoryDataSource.ts index f26de9b6..66a5423e 100644 --- a/src/features/projects/data/GitHubRepositoryDataSource.ts +++ b/src/features/projects/data/GitHubRepositoryDataSource.ts @@ -2,6 +2,47 @@ import IGitHubLoginDataSource from "./IGitHubLoginDataSource" import IGitHubGraphQLClient from "./IGitHubGraphQLClient" import { GitHubRepository, IGitHubRepositoryDataSource } from "../domain" +type GraphQLGitHubRepository = { + readonly name: string + readonly owner: { + readonly login: string + } + readonly defaultBranchRef: { + readonly name: string + readonly target: { + readonly oid: string + } + } + readonly configYml?: { + readonly text: string + } + readonly configYaml?: { + readonly text: string + } + readonly branches: EdgesContainer + readonly tags: EdgesContainer +} + +type EdgesContainer = { + readonly edges: Edge[] +} + +type Edge = { + readonly node: T +} + +type GraphQLGitHubRepositoryRef = { + readonly name: string + readonly target: { + readonly oid: string + readonly tree: { + readonly entries: { + readonly name: string + }[] + } + } +} + export default class GitHubProjectDataSource implements IGitHubRepositoryDataSource { private readonly loginsDataSource: IGitHubLoginDataSource private readonly graphQlClient: IGitHubGraphQLClient @@ -59,12 +100,41 @@ export default class GitHubProjectDataSource implements IGitHubRepositoryDataSou return !alreadyAdded }) }) + .then(repositories => { + // Map from the internal model to the public model. + return repositories.map(repository => { + return { + name: repository.name, + owner: repository.owner.login, + defaultBranchRef: { + id: repository.defaultBranchRef.target.oid, + name: repository.defaultBranchRef.name + }, + configYml: repository.configYml, + configYaml: repository.configYaml, + branches: repository.branches.edges.map(branch => { + return { + id: branch.node.target.oid, + name: branch.node.name, + files: branch.node.target.tree.entries + } + }), + tags: repository.tags.edges.map(branch => { + return { + id: branch.node.target.oid, + name: branch.node.name, + files: branch.node.target.tree.entries + } + }) + } + }) + }) } private async getRepositoriesForSearchQuery(params: { searchQuery: string, cursor?: string - }): Promise { + }): Promise { const { searchQuery, cursor } = params const request = { query: ` diff --git a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts index 2ec639c4..26ee3be3 100644 --- a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts +++ b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts @@ -30,7 +30,7 @@ export default class FilteringGitHubRepositoryDataSource implements IGitHubRepos // Only return repositories that are not on the hidden list. return repositories.filter(repository => { const hiddenMatch = hiddenOwnerAndRepoNameList.find(e => - e.owner == repository.owner.login && e.repository == repository.name + e.owner == repository.owner && e.repository == repository.name ) return hiddenMatch === undefined }) diff --git a/src/features/projects/domain/IGitHubRepositoryDataSource.ts b/src/features/projects/domain/IGitHubRepositoryDataSource.ts index 15dbba05..b48ff43e 100644 --- a/src/features/projects/domain/IGitHubRepositoryDataSource.ts +++ b/src/features/projects/domain/IGitHubRepositoryDataSource.ts @@ -1,13 +1,9 @@ export type GitHubRepository = { readonly name: string - readonly owner: { - readonly login: string - } + readonly owner: string readonly defaultBranchRef: { + readonly id: string readonly name: string - readonly target: { - readonly oid: string - } } readonly configYml?: { readonly text: string @@ -15,30 +11,20 @@ export type GitHubRepository = { readonly configYaml?: { readonly text: string } - readonly branches: EdgesContainer - readonly tags: EdgesContainer -} - -type EdgesContainer = { - readonly edges: Edge[] -} - -type Edge = { - readonly node: T + readonly branches: GitHubRepositoryRef[] + readonly tags: GitHubRepositoryRef[] } export type GitHubRepositoryRef = { - readonly name: string - readonly target: { - readonly oid: string - readonly tree: { - readonly entries: GitHubRepositoryFile[] - } - } + readonly id: string + readonly name: string + readonly files: { + readonly name: string + }[] } -export type GitHubRepositoryFile = { - readonly name: string +export default interface IGitHubRepositoryDataSource { + getRepositories(): Promise } export default interface IGitHubRepositoryDataSource { From 2a8a7009afc3c88db4cb8d523125ef26556cf661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 10:48:33 +0200 Subject: [PATCH 17/76] Fixes GitHubProjectDataSource tests --- .../projects/GitHubProjectDataSource.test.ts | 1832 +++++------------ 1 file changed, 499 insertions(+), 1333 deletions(-) diff --git a/__test__/projects/GitHubProjectDataSource.test.ts b/__test__/projects/GitHubProjectDataSource.test.ts index b4aeb857..e000a115 100644 --- a/__test__/projects/GitHubProjectDataSource.test.ts +++ b/__test__/projects/GitHubProjectDataSource.test.ts @@ -6,20 +6,10 @@ test("It loads repositories from data source", async () => { let didLoadRepositories = false const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { + repositoryDataSource: { + async getRepositories() { didLoadRepositories = true - return { - search: { - results: [] - } - } + return [] } } }) @@ -30,60 +20,30 @@ test("It loads repositories from data source", async () => { test("It maps projects including branches and tags", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml" }] - } - } + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" + }] + }] + }] } } }) @@ -121,63 +81,33 @@ test("It maps projects including branches and tags", async () => { }]) }) -test("It removes \"-openapi\" suffix from project name", async () => { +test("It removes suffix from project name", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml" }] - } - } + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" + }] + }] + }] } } }) @@ -190,64 +120,34 @@ test("It removes \"-openapi\" suffix from project name", async () => { test("It supports multiple OpenAPI specifications on a branch", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "foo-service.yml" - }, { - name: "bar-service.yml" - }, { - name: "baz-service.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "foo-service.yml", + }, { + name: "bar-service.yml", + }, { + name: "baz-service.yml", }] - } - } + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" + }] + }] + }] } } }) @@ -295,105 +195,21 @@ test("It supports multiple OpenAPI specifications on a branch", async () => { }]) }) -test("It removes \"-openapi\" suffix from project name", async () => { - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } - }] - } - } - } - } - }) - const projects = await sut.getProjects() - expect(projects[0].id).toEqual("acme-foo") - expect(projects[0].name).toEqual("foo") - expect(projects[0].displayName).toEqual("foo") -}) - test("It filters away projects with no versions", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [] - }, - tags: { - edges: [] - } - }] - } - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }] } } }) @@ -404,60 +220,30 @@ test("It filters away projects with no versions", async () => { test("It filters away branches with no specifications", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "bugfix", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "foo.txt" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }, { + id: "12345678", + name: "bugfix", + files: [{ + name: "README.md", + }] + }], + tags: [] + }] } } }) @@ -468,72 +254,36 @@ test("It filters away branches with no specifications", async () => { test("It filters away tags with no specifications", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "1.1", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "foo.txt" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "foo-service.yml", }] - } - } + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml" + }] + }, { + id: "12345678", + name: "0.1", + files: [{ + name: "README.md" + }] + }] + }] } } }) @@ -544,51 +294,27 @@ test("It filters away tags with no specifications", async () => { test("It reads image from configuration file with .yml extension", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYml: { - text: "image: icon.png" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYml: { + text: "image: icon.png" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -596,130 +322,30 @@ test("It reads image from configuration file with .yml extension", async () => { expect(projects[0].imageURL).toEqual("/api/blob/acme/foo-openapi/icon.png?ref=12345678") }) -test("It filters away tags with no specifications", async () => { - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "1.1", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "foo.txt" - }] - } - } - } - }] - } - }] - } - } - } - } - }) - const projects = await sut.getProjects() - expect(projects[0].versions.length).toEqual(2) -}) - test("It reads display name from configuration file with .yml extension", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYml: { - text: "name: Hello World" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -729,54 +355,30 @@ test("It reads display name from configuration file with .yml extension", async expect(projects[0].displayName).toEqual("Hello World") }) -test("It reads image from configuration file with .yml extension", async () => { +test("It reads image from configuration file with .yaml extension", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYml: { - text: "image: icon.png" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "image: icon.png" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -787,51 +389,27 @@ test("It reads image from configuration file with .yml extension", async () => { test("It reads display name from configuration file with .yaml extension", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYaml: { - text: "name: Hello World" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -841,164 +419,66 @@ test("It reads display name from configuration file with .yaml extension", async expect(projects[0].displayName).toEqual("Hello World") }) -test("It reads image from configuration file with .yaml extension", async () => { - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYaml: { - text: "image: icon.png" - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } - }] - } - } - } - } - }) - const projects = await sut.getProjects() - expect(projects[0].imageURL).toEqual("/api/blob/acme/foo-openapi/icon.png?ref=12345678") -}) - test("It sorts projects alphabetically", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "cathrine-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } - }, { - name: "anne-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } - }, { - name: "bobby-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "cathrine-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", + }] + }], + tags: [] + }, { + owner: "acme", + name: "bobby-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", + }] + }], + tags: [] + }, { + owner: "acme", + name: "anne-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", }] - } - } + }], + tags: [] + }] } } }) @@ -1011,84 +491,45 @@ test("It sorts projects alphabetically", async () => { test("It sorts versions alphabetically", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "bobby", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "anne", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "cathrine", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "anne", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "bobby", + files: [{ + name: "openapi.yml", + }] + }], + tags: [{ + id: "12345678", + name: "cathrine", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml", }] - } - } + }] + }] } } }) @@ -1102,108 +543,57 @@ test("It sorts versions alphabetically", async () => { test("It prioritizes main, master, develop, and development branch names when sorting verisons", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "anne", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "develop", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "development", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "master", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [{ - node: { - name: "1.0", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "anne", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "develop", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "development", + files: [{ + name: "openapi.yml", }] - } - } + }, { + id: "12345678", + name: "master", + files: [{ + name: "openapi.yml", + }] + }], + tags: [{ + id: "12345678", + name: "1.0", + files: [{ + name: "openapi.yml", + }] + }] + }] } } }) @@ -1219,72 +609,39 @@ test("It prioritizes main, master, develop, and development branch names when so test("It identifies the default branch in returned versions", async () => { const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "development", - target: { - oid: "12345678" - } - }, - branches: { - edges: [{ - node: { - name: "anne", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "main", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }, { - node: { - name: "development", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "development" + }, + configYaml: { + text: "name: Hello World" + }, + branches: [{ + id: "12345678", + name: "anne", + files: [{ + name: "openapi.yml", }] - } - } + }, { + id: "12345678", + name: "main", + files: [{ + name: "openapi.yml", + }] + }, { + id: "12345678", + name: "development", + files: [{ + name: "openapi.yml", + }] + }], + tags: [] + }] } } }) @@ -1297,54 +654,35 @@ test("It identifies the default branch in returned versions", async () => { }) test("It adds remote versions from the project configuration", async () => { - const rawProjectConfig = ` - remoteVersions: - - name: Anne - specifications: - - name: Huey - url: https://example.com/huey.yml - - name: Dewey - url: https://example.com/dewey.yml - - name: Bobby - specifications: - - name: Louie - url: https://example.com/louie.yml - ` const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "main", - target: { - oid: "12345678" - } - }, - configYml: { - text: rawProjectConfig - }, - branches: { - edges: [] - }, - tags: { - edges: [] - } - }] - } - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + configYaml: { + text: ` + remoteVersions: + - name: Anne + specifications: + - name: Huey + url: https://example.com/huey.yml + - name: Dewey + url: https://example.com/dewey.yml + - name: Bobby + specifications: + - name: Louie + url: https://example.com/louie.yml + ` + }, + branches: [], + tags: [] + }] } } }) @@ -1375,64 +713,39 @@ test("It adds remote versions from the project configuration", async () => { }) test("It modifies ID of remote version if the ID already exists", async () => { - const rawProjectConfig = ` - remoteVersions: - - name: Bar - specifications: - - name: Baz - url: https://example.com/baz.yml - - name: Bar - specifications: - - name: Hello - url: https://example.com/hello.yml - ` const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "bar", - target: { - oid: "12345678" - } - }, - configYml: { - text: rawProjectConfig - }, - branches: { - edges: [{ - node: { - name: "bar", - target: { - oid: "12345678", - tree: { - entries: [{ - name: "openapi.yml" - }] - } - } - } - }] - }, - tags: { - edges: [] - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + configYaml: { + text: ` + remoteVersions: + - name: Bar + specifications: + - name: Baz + url: https://example.com/baz.yml + - name: Bar + specifications: + - name: Hello + url: https://example.com/hello.yml + ` + }, + branches: [{ + id: "12345678", + name: "bar", + files: [{ + name: "openapi.yml" }] - } - } + }], + tags: [] + }] } } }) @@ -1470,49 +783,30 @@ test("It modifies ID of remote version if the ID already exists", async () => { }) test("It lets users specify the ID of a remote version", async () => { - const rawProjectConfig = ` - remoteVersions: - - id: some-version - name: Bar - specifications: - - name: Baz - url: https://example.com/baz.yml - ` const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "bar", - target: { - oid: "12345678" - } - }, - configYml: { - text: rawProjectConfig - }, - branches: { - edges: [] - }, - tags: { - edges: [] - } - }] - } - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + configYaml: { + text: ` + remoteVersions: + - id: some-version + name: Bar + specifications: + - name: Baz + url: https://example.com/baz.yml + ` + }, + branches: [], + tags: [] + }] } } }) @@ -1530,49 +824,30 @@ test("It lets users specify the ID of a remote version", async () => { }) test("It lets users specify the ID of a remote specification", async () => { - const rawProjectConfig = ` - remoteVersions: - - name: Bar - specifications: - - id: some-spec - name: Baz - url: https://example.com/baz.yml - ` const sut = new GitHubProjectDataSource({ repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql() { - return { - search: { - results: [{ - name: "foo-openapi", - owner: { - login: "acme" - }, - defaultBranchRef: { - name: "bar", - target: { - oid: "12345678" - } - }, - configYml: { - text: rawProjectConfig - }, - branches: { - edges: [] - }, - tags: { - edges: [] - } - }] - } - } + repositoryDataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + configYaml: { + text: ` + remoteVersions: + - name: Bar + specifications: + - id: some-spec + name: Baz + url: https://example.com/baz.yml + ` + }, + branches: [], + tags: [] + }] } } }) @@ -1588,112 +863,3 @@ test("It lets users specify the ID of a remote specification", async () => { }] }]) }) - -test("It queries for both .yml and .yaml file extension with specifying .yml extension", async () => { - let query: string | undefined - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql(request) { - query = request.query - return { - search: { - results: [] - } - } - } - } - }) - await sut.getProjects() - expect(query).toContain(".demo-docs.yml") - expect(query).toContain(".demo-docs.yaml") -}) - -test("It queries for both .yml and .yaml file extension with specifying .yaml extension", async () => { - let query: string | undefined - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs.yml", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql(request) { - query = request.query - return { - search: { - results: [] - } - } - } - } - }) - await sut.getProjects() - expect(query).toContain(".demo-docs.yml") - expect(query).toContain(".demo-docs.yaml") -}) - -test("It queries for both .yml and .yaml file extension with no extension", async () => { - let query: string | undefined - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs", - loginsDataSource: { - async getLogins() { - return ["acme"] - } - }, - graphQlClient: { - async graphql(request) { - query = request.query - return { - search: { - results: [] - } - } - } - } - }) - await sut.getProjects() - expect(query).toContain(".demo-docs.yml") - expect(query).toContain(".demo-docs.yaml") -}) - -test("It loads projects for all logins", async () => { - let searchQueries: string[] = [] - const sut = new GitHubProjectDataSource({ - repositoryNameSuffix: "-openapi", - projectConfigurationFilename: ".demo-docs", - loginsDataSource: { - async getLogins() { - return ["acme", "somecorp", "techsystems"] - } - }, - graphQlClient: { - async graphql(request) { - if (request.variables?.searchQuery) { - searchQueries.push(request.variables.searchQuery) - } - return { - search: { - results: [] - } - } - } - } - }) - await sut.getProjects() - expect(searchQueries.length).toEqual(4) - expect(searchQueries).toContain("\"-openapi\" in:name is:private") - expect(searchQueries).toContain("\"-openapi\" in:name user:acme is:public") - expect(searchQueries).toContain("\"-openapi\" in:name user:somecorp is:public") - expect(searchQueries).toContain("\"-openapi\" in:name user:techsystems is:public") -}) From 5ea139d41845060d850449fd354bca4130e7545e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 11:01:57 +0200 Subject: [PATCH 18/76] Adds tests for FilteringGitHubRepositoryDataSource --- ...ilteringGitHubRepositoryDataSource.test.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 __test__/projects/FilteringGitHubRepositoryDataSource.test.ts diff --git a/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts new file mode 100644 index 00000000..1f408320 --- /dev/null +++ b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts @@ -0,0 +1,125 @@ +import { FilteringGitHubRepositoryDataSource } from "../../src/features/projects/domain" + +test("It returns all repositories when no hidden repositories are provided", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: [], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(2) +}) + +test("It removes hidden repository", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: ["acme/foo-openapi"], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(1) +}) + +test("It returns unmodified list when hidden repository was not found", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: ["acme/baz-openapi"], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(2) +}) + +test("It removes multiple hidden repositories", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: ["acme/foo-openapi", "acme/bar-openapi"], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(0) +}) From 660831c68e966e4e4d917d3cc23612fcc8eb4657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 11:19:34 +0200 Subject: [PATCH 19/76] Removes FilteringGitHubRepositoryDataSource --- ...ilteringGitHubRepositoryDataSource.test.ts | 125 ------------------ src/composition.ts | 18 +-- .../FilteringGitHubRepositoryDataSource.ts | 38 ------ src/features/projects/domain/index.ts | 1 - 4 files changed, 7 insertions(+), 175 deletions(-) delete mode 100644 __test__/projects/FilteringGitHubRepositoryDataSource.test.ts delete mode 100644 src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts diff --git a/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts deleted file mode 100644 index 1f408320..00000000 --- a/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { FilteringGitHubRepositoryDataSource } from "../../src/features/projects/domain" - -test("It returns all repositories when no hidden repositories are provided", async () => { - const sut = new FilteringGitHubRepositoryDataSource({ - hiddenRepositories: [], - dataSource: { - async getRepositories() { - return [{ - owner: "acme", - name: "foo-openapi", - defaultBranchRef: { - id: "12345678", - name: "main" - }, - branches: [], - tags: [] - }, { - owner: "acme", - name: "bar-openapi", - defaultBranchRef: { - id: "12345678", - name: "bar" - }, - branches: [], - tags: [] - }] - } - } - }) - const repositories = await sut.getRepositories() - expect(repositories.length).toEqual(2) -}) - -test("It removes hidden repository", async () => { - const sut = new FilteringGitHubRepositoryDataSource({ - hiddenRepositories: ["acme/foo-openapi"], - dataSource: { - async getRepositories() { - return [{ - owner: "acme", - name: "foo-openapi", - defaultBranchRef: { - id: "12345678", - name: "main" - }, - branches: [], - tags: [] - }, { - owner: "acme", - name: "bar-openapi", - defaultBranchRef: { - id: "12345678", - name: "bar" - }, - branches: [], - tags: [] - }] - } - } - }) - const repositories = await sut.getRepositories() - expect(repositories.length).toEqual(1) -}) - -test("It returns unmodified list when hidden repository was not found", async () => { - const sut = new FilteringGitHubRepositoryDataSource({ - hiddenRepositories: ["acme/baz-openapi"], - dataSource: { - async getRepositories() { - return [{ - owner: "acme", - name: "foo-openapi", - defaultBranchRef: { - id: "12345678", - name: "main" - }, - branches: [], - tags: [] - }, { - owner: "acme", - name: "bar-openapi", - defaultBranchRef: { - id: "12345678", - name: "bar" - }, - branches: [], - tags: [] - }] - } - } - }) - const repositories = await sut.getRepositories() - expect(repositories.length).toEqual(2) -}) - -test("It removes multiple hidden repositories", async () => { - const sut = new FilteringGitHubRepositoryDataSource({ - hiddenRepositories: ["acme/foo-openapi", "acme/bar-openapi"], - dataSource: { - async getRepositories() { - return [{ - owner: "acme", - name: "foo-openapi", - defaultBranchRef: { - id: "12345678", - name: "main" - }, - branches: [], - tags: [] - }, { - owner: "acme", - name: "bar-openapi", - defaultBranchRef: { - id: "12345678", - name: "bar" - }, - branches: [], - tags: [] - }] - } - } - }) - const repositories = await sut.getRepositories() - expect(repositories.length).toEqual(0) -}) diff --git a/src/composition.ts b/src/composition.ts index a78d6d82..69806563 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -22,7 +22,6 @@ import { } from "@/features/projects/data" import { CachingProjectDataSource, - FilteringGitHubRepositoryDataSource, ProjectRepository } from "@/features/projects/domain" import { @@ -159,16 +158,13 @@ export const projectRepository = new ProjectRepository({ export const projectDataSource = new CachingProjectDataSource({ dataSource: new GitHubProjectDataSource({ - repositoryDataSource: new FilteringGitHubRepositoryDataSource({ - hiddenRepositories: listFromCommaSeparatedString(env.getOrThrow("HIDDEN_REPOSITORIES")), - dataSource: new GitHubRepositoryDataSource({ - loginsDataSource: new GitHubLoginDataSource({ - graphQlClient: userGitHubClient - }), - graphQlClient: userGitHubClient, - repositoryNameSuffix: env.getOrThrow("REPOSITORY_NAME_SUFFIX"), - projectConfigurationFilename: env.getOrThrow("SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME") - }) + repositoryDataSource: new GitHubRepositoryDataSource({ + loginsDataSource: new GitHubLoginDataSource({ + graphQlClient: userGitHubClient + }), + graphQlClient: userGitHubClient, + repositoryNameSuffix: env.getOrThrow("REPOSITORY_NAME_SUFFIX"), + projectConfigurationFilename: env.getOrThrow("SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME") }), repositoryNameSuffix: env.getOrThrow("REPOSITORY_NAME_SUFFIX") }), diff --git a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts deleted file mode 100644 index 26ee3be3..00000000 --- a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts +++ /dev/null @@ -1,38 +0,0 @@ -import IGitHubRepositoryDataSource, { - GitHubRepository -} from "./IGitHubRepositoryDataSource" - -export default class FilteringGitHubRepositoryDataSource implements IGitHubRepositoryDataSource { - private readonly dataSource: IGitHubRepositoryDataSource - private readonly hiddenRepositories: string[] - - constructor(config: { - dataSource: IGitHubRepositoryDataSource, - hiddenRepositories: string[] - }) { - this.dataSource = config.dataSource - this.hiddenRepositories = config.hiddenRepositories - } - - async getRepositories(): Promise { - const repositories = await this.dataSource.getRepositories() - // Split full repository names into owner and repository. - // shapehq/foo becomes { owner: "shapehq", "repository": "foo" } - const hiddenOwnerAndRepoNameList = this.hiddenRepositories.map(str => { - const index = str.indexOf("/") - if (index === -1) { - return { owner: str, repository: "" } - } - const owner = str.substring(0, index) - const repository = str.substring(index + 1) - return { owner, repository } - }) - // Only return repositories that are not on the hidden list. - return repositories.filter(repository => { - const hiddenMatch = hiddenOwnerAndRepoNameList.find(e => - e.owner == repository.owner && e.repository == repository.name - ) - return hiddenMatch === undefined - }) - } -} \ No newline at end of file diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts index 62596d40..ad17f135 100644 --- a/src/features/projects/domain/index.ts +++ b/src/features/projects/domain/index.ts @@ -1,5 +1,4 @@ export { default as CachingProjectDataSource } from "./CachingProjectDataSource" -export { default as FilteringGitHubRepositoryDataSource } from "./FilteringGitHubRepositoryDataSource" export { default as getSelection } from "./getSelection" export type { default as IGitHubRepositoryDataSource } from "./IGitHubRepositoryDataSource" export * from "./IGitHubRepositoryDataSource" From 9d934625c586d7e9565c143bf62f85f0a53ba088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 11:22:02 +0200 Subject: [PATCH 20/76] Removes HIDDEN_REPOSITORIES --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index 0b7926ac..7327e16b 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,6 @@ POSTGRESQL_USER=dbuser POSTGRESQL_PASSWORD= POSTGRESQL_DB=shape-docs REPOSITORY_NAME_SUFFIX=-openapi -HIDDEN_REPOSITORIES= GITHUB_WEBHOOK_SECRET=preshared secret also put in app configuration in GitHub GITHUB_WEBHOK_REPOSITORY_ALLOWLIST= GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST= From 35f72dcc83b8dc2b152d3478d852d8cda7f2b07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 11:51:43 +0200 Subject: [PATCH 21/76] Revert "Removes FilteringGitHubRepositoryDataSource" This reverts commit 660831c68e966e4e4d917d3cc23612fcc8eb4657. --- ...ilteringGitHubRepositoryDataSource.test.ts | 125 ++++++++++++++++++ src/composition.ts | 18 ++- .../FilteringGitHubRepositoryDataSource.ts | 38 ++++++ src/features/projects/domain/index.ts | 1 + 4 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 __test__/projects/FilteringGitHubRepositoryDataSource.test.ts create mode 100644 src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts diff --git a/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts new file mode 100644 index 00000000..1f408320 --- /dev/null +++ b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts @@ -0,0 +1,125 @@ +import { FilteringGitHubRepositoryDataSource } from "../../src/features/projects/domain" + +test("It returns all repositories when no hidden repositories are provided", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: [], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(2) +}) + +test("It removes hidden repository", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: ["acme/foo-openapi"], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(1) +}) + +test("It returns unmodified list when hidden repository was not found", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: ["acme/baz-openapi"], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(2) +}) + +test("It removes multiple hidden repositories", async () => { + const sut = new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: ["acme/foo-openapi", "acme/bar-openapi"], + dataSource: { + async getRepositories() { + return [{ + owner: "acme", + name: "foo-openapi", + defaultBranchRef: { + id: "12345678", + name: "main" + }, + branches: [], + tags: [] + }, { + owner: "acme", + name: "bar-openapi", + defaultBranchRef: { + id: "12345678", + name: "bar" + }, + branches: [], + tags: [] + }] + } + } + }) + const repositories = await sut.getRepositories() + expect(repositories.length).toEqual(0) +}) diff --git a/src/composition.ts b/src/composition.ts index 69806563..a78d6d82 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -22,6 +22,7 @@ import { } from "@/features/projects/data" import { CachingProjectDataSource, + FilteringGitHubRepositoryDataSource, ProjectRepository } from "@/features/projects/domain" import { @@ -158,13 +159,16 @@ export const projectRepository = new ProjectRepository({ export const projectDataSource = new CachingProjectDataSource({ dataSource: new GitHubProjectDataSource({ - repositoryDataSource: new GitHubRepositoryDataSource({ - loginsDataSource: new GitHubLoginDataSource({ - graphQlClient: userGitHubClient - }), - graphQlClient: userGitHubClient, - repositoryNameSuffix: env.getOrThrow("REPOSITORY_NAME_SUFFIX"), - projectConfigurationFilename: env.getOrThrow("SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME") + repositoryDataSource: new FilteringGitHubRepositoryDataSource({ + hiddenRepositories: listFromCommaSeparatedString(env.getOrThrow("HIDDEN_REPOSITORIES")), + dataSource: new GitHubRepositoryDataSource({ + loginsDataSource: new GitHubLoginDataSource({ + graphQlClient: userGitHubClient + }), + graphQlClient: userGitHubClient, + repositoryNameSuffix: env.getOrThrow("REPOSITORY_NAME_SUFFIX"), + projectConfigurationFilename: env.getOrThrow("SHAPE_DOCS_PROJECT_CONFIGURATION_FILENAME") + }) }), repositoryNameSuffix: env.getOrThrow("REPOSITORY_NAME_SUFFIX") }), diff --git a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts new file mode 100644 index 00000000..26ee3be3 --- /dev/null +++ b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts @@ -0,0 +1,38 @@ +import IGitHubRepositoryDataSource, { + GitHubRepository +} from "./IGitHubRepositoryDataSource" + +export default class FilteringGitHubRepositoryDataSource implements IGitHubRepositoryDataSource { + private readonly dataSource: IGitHubRepositoryDataSource + private readonly hiddenRepositories: string[] + + constructor(config: { + dataSource: IGitHubRepositoryDataSource, + hiddenRepositories: string[] + }) { + this.dataSource = config.dataSource + this.hiddenRepositories = config.hiddenRepositories + } + + async getRepositories(): Promise { + const repositories = await this.dataSource.getRepositories() + // Split full repository names into owner and repository. + // shapehq/foo becomes { owner: "shapehq", "repository": "foo" } + const hiddenOwnerAndRepoNameList = this.hiddenRepositories.map(str => { + const index = str.indexOf("/") + if (index === -1) { + return { owner: str, repository: "" } + } + const owner = str.substring(0, index) + const repository = str.substring(index + 1) + return { owner, repository } + }) + // Only return repositories that are not on the hidden list. + return repositories.filter(repository => { + const hiddenMatch = hiddenOwnerAndRepoNameList.find(e => + e.owner == repository.owner && e.repository == repository.name + ) + return hiddenMatch === undefined + }) + } +} \ No newline at end of file diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts index ad17f135..62596d40 100644 --- a/src/features/projects/domain/index.ts +++ b/src/features/projects/domain/index.ts @@ -1,4 +1,5 @@ export { default as CachingProjectDataSource } from "./CachingProjectDataSource" +export { default as FilteringGitHubRepositoryDataSource } from "./FilteringGitHubRepositoryDataSource" export { default as getSelection } from "./getSelection" export type { default as IGitHubRepositoryDataSource } from "./IGitHubRepositoryDataSource" export * from "./IGitHubRepositoryDataSource" From 62bb5320bd0e4eaa910a867b7ef80e5c9042cf14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 11:51:50 +0200 Subject: [PATCH 22/76] Revert "Removes HIDDEN_REPOSITORIES" This reverts commit 9d934625c586d7e9565c143bf62f85f0a53ba088. --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 7327e16b..0b7926ac 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,7 @@ POSTGRESQL_USER=dbuser POSTGRESQL_PASSWORD= POSTGRESQL_DB=shape-docs REPOSITORY_NAME_SUFFIX=-openapi +HIDDEN_REPOSITORIES= GITHUB_WEBHOOK_SECRET=preshared secret also put in app configuration in GitHub GITHUB_WEBHOK_REPOSITORY_ALLOWLIST= GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST= From 7d2c38505e22ecf69e7ea99b62c988e68572b3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 12:19:51 +0200 Subject: [PATCH 23/76] Allows empty HIDDEN_REPOSITORIES --- src/composition.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/composition.ts b/src/composition.ts index a78d6d82..88c7316c 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -160,7 +160,7 @@ export const projectRepository = new ProjectRepository({ export const projectDataSource = new CachingProjectDataSource({ dataSource: new GitHubProjectDataSource({ repositoryDataSource: new FilteringGitHubRepositoryDataSource({ - hiddenRepositories: listFromCommaSeparatedString(env.getOrThrow("HIDDEN_REPOSITORIES")), + hiddenRepositories: listFromCommaSeparatedString(env.get("HIDDEN_REPOSITORIES")), dataSource: new GitHubRepositoryDataSource({ loginsDataSource: new GitHubLoginDataSource({ graphQlClient: userGitHubClient From c3f9e637b602f5fc5d57bff0c6d496120bcf2d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 12:40:17 +0200 Subject: [PATCH 24/76] GitHubLoginDataSource returns logins with avatars --- .../GitHubRepositoryDataSource.test.ts | 36 +++++++++++++++---- .../projects/data/GitHubLoginDataSource.ts | 21 ++++++++--- .../data/GitHubRepositoryDataSource.ts | 4 +-- .../projects/data/IGitHubLoginDataSource.ts | 7 +++- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/__test__/projects/GitHubRepositoryDataSource.test.ts b/__test__/projects/GitHubRepositoryDataSource.test.ts index c6763ab2..38d78778 100644 --- a/__test__/projects/GitHubRepositoryDataSource.test.ts +++ b/__test__/projects/GitHubRepositoryDataSource.test.ts @@ -9,7 +9,10 @@ test("It loads repositories from data source", async () => { projectConfigurationFilename: ".demo-docs.yml", loginsDataSource: { async getLogins() { - return ["acme"] + return [{ + name: "acme", + avatarUrl: "https://example.com/avatar.png" + }] } }, graphQlClient: { @@ -33,7 +36,10 @@ test("It maps repositories from GraphQL to the GitHubRepository model", async () projectConfigurationFilename: ".demo-docs.yml", loginsDataSource: { async getLogins() { - return ["acme"] + return [{ + name: "acme", + avatarUrl: "https://example.com/avatar.png" + }] } }, graphQlClient: { @@ -119,7 +125,10 @@ test("It queries for both .yml and .yaml file extension with specifying .yml ext projectConfigurationFilename: ".demo-docs.yml", loginsDataSource: { async getLogins() { - return ["acme"] + return [{ + name: "acme", + avatarUrl: "https://example.com/avatar.png" + }] } }, graphQlClient: { @@ -145,7 +154,10 @@ test("It queries for both .yml and .yaml file extension with specifying .yaml ex projectConfigurationFilename: ".demo-docs.yml", loginsDataSource: { async getLogins() { - return ["acme"] + return [{ + name: "acme", + avatarUrl: "https://example.com/avatar.png" + }] } }, graphQlClient: { @@ -171,7 +183,10 @@ test("It queries for both .yml and .yaml file extension with no extension", asyn projectConfigurationFilename: ".demo-docs", loginsDataSource: { async getLogins() { - return ["acme"] + return [{ + name: "acme", + avatarUrl: "https://example.com/avatar.png" + }] } }, graphQlClient: { @@ -197,7 +212,16 @@ test("It loads repositories for all logins", async () => { projectConfigurationFilename: ".demo-docs", loginsDataSource: { async getLogins() { - return ["acme", "somecorp", "techsystems"] + return [{ + name: "acme", + avatarUrl: "https://example.com/avatar.png" + }, { + name: "somecorp", + avatarUrl: "https://example.com/avatar.png" + }, { + name: "techsystems", + avatarUrl: "https://example.com/avatar.png" + }] } }, graphQlClient: { diff --git a/src/features/projects/data/GitHubLoginDataSource.ts b/src/features/projects/data/GitHubLoginDataSource.ts index 3d60d1bf..7f75e7b2 100644 --- a/src/features/projects/data/GitHubLoginDataSource.ts +++ b/src/features/projects/data/GitHubLoginDataSource.ts @@ -1,4 +1,4 @@ -import IGitHubLoginDataSource from "./IGitHubLoginDataSource" +import IGitHubLoginDataSource, { GitHubLogin } from "./IGitHubLoginDataSource" import IGitHubGraphQLClient from "./IGitHubGraphQLClient" export default class GitHubLoginDataSource implements IGitHubLoginDataSource { @@ -8,15 +8,17 @@ export default class GitHubLoginDataSource implements IGitHubLoginDataSource { this.graphQlClient = config.graphQlClient } - async getLogins(): Promise { + async getLogins(): Promise { const request = { query: ` query { viewer { login + avatarUrl organizations(first: 100) { nodes { login + avatarUrl } } } @@ -33,7 +35,18 @@ export default class GitHubLoginDataSource implements IGitHubLoginDataSource { throw new Error("organizations property not found on viewer in response") } const viewer = response.viewer - const organizations = viewer.organizations.nodes.map((e: { login: string }) => e.login) - return [viewer.login].concat(organizations) + const personalLogin: GitHubLogin = { + name: viewer.login, + avatarUrl: viewer.avatarUrl + } + const organizationLogins: GitHubLogin[] = viewer + .organizations + .nodes + .map((e: { login: string, avatarUrl: string }) => { + const name = e.login + const avatarUrl = e.avatarUrl + return { name, avatarUrl } + }) + return [personalLogin].concat(organizationLogins) } } diff --git a/src/features/projects/data/GitHubRepositoryDataSource.ts b/src/features/projects/data/GitHubRepositoryDataSource.ts index 66a5423e..b0dd3f1f 100644 --- a/src/features/projects/data/GitHubRepositoryDataSource.ts +++ b/src/features/projects/data/GitHubRepositoryDataSource.ts @@ -69,7 +69,7 @@ export default class GitHubProjectDataSource implements IGitHubRepositoryDataSou private async getRepositoriesForLogins({ logins }: { - logins: string[] + logins: { name: string }[] }): Promise { let searchQueries: string[] = [] // Search for all private repositories the user has access to. This is needed to find @@ -77,7 +77,7 @@ export default class GitHubProjectDataSource implements IGitHubRepositoryDataSou searchQueries.push(`"${this.repositoryNameSuffix}" in:name is:private`) // Search for public repositories belonging to a user or organization. searchQueries = searchQueries.concat(logins.map(login => { - return `"${this.repositoryNameSuffix}" in:name user:${login} is:public` + return `"${this.repositoryNameSuffix}" in:name user:${login.name} is:public` })) return await Promise.all(searchQueries.map(searchQuery => { return this.getRepositoriesForSearchQuery({ searchQuery }) diff --git a/src/features/projects/data/IGitHubLoginDataSource.ts b/src/features/projects/data/IGitHubLoginDataSource.ts index fa880775..83711dbd 100644 --- a/src/features/projects/data/IGitHubLoginDataSource.ts +++ b/src/features/projects/data/IGitHubLoginDataSource.ts @@ -1,3 +1,8 @@ +export type GitHubLogin = { + readonly name: string + readonly avatarUrl: string +} + export default interface IGitHubLoginDataSource { - getLogins(): Promise + getLogins(): Promise } From 4a10be13755422f17fb279a9596490607346ec48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 12:59:44 +0200 Subject: [PATCH 25/76] Adds missing space --- src/features/sidebar/view/base/SecondaryHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/sidebar/view/base/SecondaryHeader.tsx b/src/features/sidebar/view/base/SecondaryHeader.tsx index 0e4a0177..ea60cb94 100644 --- a/src/features/sidebar/view/base/SecondaryHeader.tsx +++ b/src/features/sidebar/view/base/SecondaryHeader.tsx @@ -32,7 +32,7 @@ export default function SecondaryHeader({ } } }, [showOpenSidebar, showCloseSidebar, onToggleSidebarOpen]) - const openCloseShortcutString = isMac() ? " (⌘ + .)" : "(^ + .)" + const openCloseShortcutString = isMac() ? " (⌘ + .)" : " (^ + .)" const theme = useTheme() return ( Date: Thu, 25 Jul 2024 13:03:11 +0200 Subject: [PATCH 26/76] Adds back New Project button --- src/features/sidebar/view/SidebarHeader.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/features/sidebar/view/SidebarHeader.tsx b/src/features/sidebar/view/SidebarHeader.tsx index 916fdb5e..fbe92a7b 100644 --- a/src/features/sidebar/view/SidebarHeader.tsx +++ b/src/features/sidebar/view/SidebarHeader.tsx @@ -1,9 +1,8 @@ import Image from "next/image" -// import { Box, Typography, IconButton, Tooltip } from "@mui/material" -import { Box, Typography } from "@mui/material" import Link from "next/link" -// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -// import { faPlus } from "@fortawesome/free-solid-svg-icons" +import { Box, Typography, IconButton, Tooltip } from "@mui/material" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faPlus } from "@fortawesome/free-solid-svg-icons" export default function SidebarHeader() { const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE @@ -42,13 +41,13 @@ export default function SidebarHeader() { {siteName} - {/* - - + + + - */} + ) } From 2229a4529f36f0a4c503e10d2a89e53050012b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 13:09:11 +0200 Subject: [PATCH 27/76] Moves interfaces --- src/features/projects/data/GitHubLoginDataSource.ts | 3 +-- src/features/projects/data/GitHubRepositoryDataSource.ts | 9 ++++++--- src/features/projects/data/index.ts | 2 -- .../projects/{data => domain}/IGitHubGraphQLClient.ts | 0 .../projects/{data => domain}/IGitHubLoginDataSource.ts | 0 src/features/projects/domain/index.ts | 3 +++ 6 files changed, 10 insertions(+), 7 deletions(-) rename src/features/projects/{data => domain}/IGitHubGraphQLClient.ts (100%) rename src/features/projects/{data => domain}/IGitHubLoginDataSource.ts (100%) diff --git a/src/features/projects/data/GitHubLoginDataSource.ts b/src/features/projects/data/GitHubLoginDataSource.ts index 3d60d1bf..f0198b39 100644 --- a/src/features/projects/data/GitHubLoginDataSource.ts +++ b/src/features/projects/data/GitHubLoginDataSource.ts @@ -1,5 +1,4 @@ -import IGitHubLoginDataSource from "./IGitHubLoginDataSource" -import IGitHubGraphQLClient from "./IGitHubGraphQLClient" +import { IGitHubLoginDataSource, IGitHubGraphQLClient } from "../domain" export default class GitHubLoginDataSource implements IGitHubLoginDataSource { private readonly graphQlClient: IGitHubGraphQLClient diff --git a/src/features/projects/data/GitHubRepositoryDataSource.ts b/src/features/projects/data/GitHubRepositoryDataSource.ts index 66a5423e..e94e228a 100644 --- a/src/features/projects/data/GitHubRepositoryDataSource.ts +++ b/src/features/projects/data/GitHubRepositoryDataSource.ts @@ -1,6 +1,9 @@ -import IGitHubLoginDataSource from "./IGitHubLoginDataSource" -import IGitHubGraphQLClient from "./IGitHubGraphQLClient" -import { GitHubRepository, IGitHubRepositoryDataSource } from "../domain" +import { + GitHubRepository, + IGitHubRepositoryDataSource, + IGitHubLoginDataSource, + IGitHubGraphQLClient +} from "../domain" type GraphQLGitHubRepository = { readonly name: string diff --git a/src/features/projects/data/index.ts b/src/features/projects/data/index.ts index 86fa88a0..8155c986 100644 --- a/src/features/projects/data/index.ts +++ b/src/features/projects/data/index.ts @@ -1,7 +1,5 @@ export { default as GitHubProjectDataSource } from "./GitHubProjectDataSource" export * from "./GitHubProjectDataSource" export { default as useProjects } from "./useProjects" -export type { default as IGitHubLoginDataSource } from "./IGitHubLoginDataSource" export { default as GitHubLoginDataSource } from "./GitHubLoginDataSource" export { default as GitHubRepositoryDataSource } from "./GitHubRepositoryDataSource" -export * from "./IGitHubGraphQLClient" diff --git a/src/features/projects/data/IGitHubGraphQLClient.ts b/src/features/projects/domain/IGitHubGraphQLClient.ts similarity index 100% rename from src/features/projects/data/IGitHubGraphQLClient.ts rename to src/features/projects/domain/IGitHubGraphQLClient.ts diff --git a/src/features/projects/data/IGitHubLoginDataSource.ts b/src/features/projects/domain/IGitHubLoginDataSource.ts similarity index 100% rename from src/features/projects/data/IGitHubLoginDataSource.ts rename to src/features/projects/domain/IGitHubLoginDataSource.ts diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts index ad17f135..8b493c0f 100644 --- a/src/features/projects/domain/index.ts +++ b/src/features/projects/domain/index.ts @@ -1,7 +1,10 @@ export { default as CachingProjectDataSource } from "./CachingProjectDataSource" export { default as getSelection } from "./getSelection" +export type { default as IGitHubLoginDataSource } from "./IGitHubLoginDataSource" export type { default as IGitHubRepositoryDataSource } from "./IGitHubRepositoryDataSource" export * from "./IGitHubRepositoryDataSource" +export type { default as IGitHubGraphQLClient } from "./IGitHubGraphQLClient" +export * from "./IGitHubGraphQLClient" export type { default as IProjectConfig } from "./IProjectConfig" export * from "./IProjectConfig" export type { default as IProjectDataSource } from "./IProjectDataSource" From 00910cc9b489b61fbb6950744650e2cdbd92f8e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 13:23:43 +0200 Subject: [PATCH 28/76] Adds splitOwnerAndRepository --- .../utils/splitOwnerAndRepository.test.ts | 26 +++++++++++++++++++ src/common/utils/index.ts | 1 + src/common/utils/splitOwnerAndRepository.ts | 14 ++++++++++ .../FilteringGitHubRepositoryDataSource.ts | 16 +++--------- 4 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 __test__/utils/splitOwnerAndRepository.test.ts create mode 100644 src/common/utils/splitOwnerAndRepository.ts diff --git a/__test__/utils/splitOwnerAndRepository.test.ts b/__test__/utils/splitOwnerAndRepository.test.ts new file mode 100644 index 00000000..d4c162a6 --- /dev/null +++ b/__test__/utils/splitOwnerAndRepository.test.ts @@ -0,0 +1,26 @@ +import { splitOwnerAndRepository } from "@/common" + +test("It returns undefined when string includes no slash", async () => { + const result = splitOwnerAndRepository("foo") + expect(result).toBeUndefined() +}) + +test("It returns undefined when repository is empty", async () => { + const result = splitOwnerAndRepository("foo/") + expect(result).toBeUndefined() +}) + +test("It returns undefined when owner is empty", async () => { + const result = splitOwnerAndRepository("/foo") + expect(result).toBeUndefined() +}) + +test("It splits owner and repository", async () => { + const result = splitOwnerAndRepository("acme/foo") + expect(result).toEqual({ owner: "acme", repository: "foo" }) +}) + +test("It splits owner and repository for repository name containing a slash", async () => { + const result = splitOwnerAndRepository("acme/foo/bar") + expect(result).toEqual({ owner: "acme", repository: "foo/bar" }) +}) diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 9d51c208..76bd7c88 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -6,3 +6,4 @@ export { default as env } from "./env" export { default as getBaseFilename } from "./getBaseFilename" export { default as isMac } from "./isMac" export { default as useKeyboardShortcut } from "./useKeyboardShortcut" +export { default as splitOwnerAndRepository } from "./splitOwnerAndRepository" diff --git a/src/common/utils/splitOwnerAndRepository.ts b/src/common/utils/splitOwnerAndRepository.ts new file mode 100644 index 00000000..3a4f80f4 --- /dev/null +++ b/src/common/utils/splitOwnerAndRepository.ts @@ -0,0 +1,14 @@ +// Split full repository names into owner and repository. +// shapehq/foo becomes { owner: "shapehq", "repository": "foo" } +export default (str: string) => { + const index = str.indexOf("/") + if (index === -1) { + return undefined + } + const owner = str.substring(0, index) + const repository = str.substring(index + 1) + if (owner.length == 0 || repository.length == 0) { + return undefined + } + return { owner, repository } +} diff --git a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts index 26ee3be3..e4018b3b 100644 --- a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts +++ b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts @@ -1,6 +1,7 @@ import IGitHubRepositoryDataSource, { GitHubRepository } from "./IGitHubRepositoryDataSource" +import { splitOwnerAndRepository } from "@/common" export default class FilteringGitHubRepositoryDataSource implements IGitHubRepositoryDataSource { private readonly dataSource: IGitHubRepositoryDataSource @@ -16,18 +17,9 @@ export default class FilteringGitHubRepositoryDataSource implements IGitHubRepos async getRepositories(): Promise { const repositories = await this.dataSource.getRepositories() - // Split full repository names into owner and repository. - // shapehq/foo becomes { owner: "shapehq", "repository": "foo" } - const hiddenOwnerAndRepoNameList = this.hiddenRepositories.map(str => { - const index = str.indexOf("/") - if (index === -1) { - return { owner: str, repository: "" } - } - const owner = str.substring(0, index) - const repository = str.substring(index + 1) - return { owner, repository } - }) - // Only return repositories that are not on the hidden list. + const hiddenOwnerAndRepoNameList = this.hiddenRepositories + .map(splitOwnerAndRepository) + .filter(e => e !== undefined) return repositories.filter(repository => { const hiddenMatch = hiddenOwnerAndRepoNameList.find(e => e.owner == repository.owner && e.repository == repository.name From dcb2fba2702a6a853307972079c30c98f168d3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 13:25:58 +0200 Subject: [PATCH 29/76] Improves naming --- .../domain/FilteringGitHubRepositoryDataSource.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts index e4018b3b..647770c6 100644 --- a/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts +++ b/src/features/projects/domain/FilteringGitHubRepositoryDataSource.ts @@ -5,23 +5,23 @@ import { splitOwnerAndRepository } from "@/common" export default class FilteringGitHubRepositoryDataSource implements IGitHubRepositoryDataSource { private readonly dataSource: IGitHubRepositoryDataSource - private readonly hiddenRepositories: string[] + private readonly rawHiddenRepositories: string[] constructor(config: { dataSource: IGitHubRepositoryDataSource, hiddenRepositories: string[] }) { this.dataSource = config.dataSource - this.hiddenRepositories = config.hiddenRepositories + this.rawHiddenRepositories = config.hiddenRepositories } async getRepositories(): Promise { const repositories = await this.dataSource.getRepositories() - const hiddenOwnerAndRepoNameList = this.hiddenRepositories + const hiddenRepositories = this.rawHiddenRepositories .map(splitOwnerAndRepository) .filter(e => e !== undefined) return repositories.filter(repository => { - const hiddenMatch = hiddenOwnerAndRepoNameList.find(e => + const hiddenMatch = hiddenRepositories.find(e => e.owner == repository.owner && e.repository == repository.name ) return hiddenMatch === undefined From d47f091dc00e5bee15acf3692ad4dd913232adc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 15:33:03 +0200 Subject: [PATCH 30/76] Fixes lint warning --- src/common/utils/splitOwnerAndRepository.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/utils/splitOwnerAndRepository.ts b/src/common/utils/splitOwnerAndRepository.ts index 3a4f80f4..4bd3a96a 100644 --- a/src/common/utils/splitOwnerAndRepository.ts +++ b/src/common/utils/splitOwnerAndRepository.ts @@ -1,6 +1,6 @@ // Split full repository names into owner and repository. // shapehq/foo becomes { owner: "shapehq", "repository": "foo" } -export default (str: string) => { +const splitOwnerAndRepository = (str: string) => { const index = str.indexOf("/") if (index === -1) { return undefined @@ -12,3 +12,5 @@ export default (str: string) => { } return { owner, repository } } + +export default splitOwnerAndRepository From 90134be183ef1735348fd501d60af09724021842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 15:42:06 +0200 Subject: [PATCH 31/76] Reverts GitHubLogin type --- .../GitHubRepositoryDataSource.test.ts | 36 ++++--------------- .../projects/data/GitHubLoginDataSource.ts | 20 +++-------- .../data/GitHubRepositoryDataSource.ts | 8 ++--- .../projects/domain/IGitHubLoginDataSource.ts | 7 +--- 4 files changed, 13 insertions(+), 58 deletions(-) diff --git a/__test__/projects/GitHubRepositoryDataSource.test.ts b/__test__/projects/GitHubRepositoryDataSource.test.ts index 38d78778..c6763ab2 100644 --- a/__test__/projects/GitHubRepositoryDataSource.test.ts +++ b/__test__/projects/GitHubRepositoryDataSource.test.ts @@ -9,10 +9,7 @@ test("It loads repositories from data source", async () => { projectConfigurationFilename: ".demo-docs.yml", loginsDataSource: { async getLogins() { - return [{ - name: "acme", - avatarUrl: "https://example.com/avatar.png" - }] + return ["acme"] } }, graphQlClient: { @@ -36,10 +33,7 @@ test("It maps repositories from GraphQL to the GitHubRepository model", async () projectConfigurationFilename: ".demo-docs.yml", loginsDataSource: { async getLogins() { - return [{ - name: "acme", - avatarUrl: "https://example.com/avatar.png" - }] + return ["acme"] } }, graphQlClient: { @@ -125,10 +119,7 @@ test("It queries for both .yml and .yaml file extension with specifying .yml ext projectConfigurationFilename: ".demo-docs.yml", loginsDataSource: { async getLogins() { - return [{ - name: "acme", - avatarUrl: "https://example.com/avatar.png" - }] + return ["acme"] } }, graphQlClient: { @@ -154,10 +145,7 @@ test("It queries for both .yml and .yaml file extension with specifying .yaml ex projectConfigurationFilename: ".demo-docs.yml", loginsDataSource: { async getLogins() { - return [{ - name: "acme", - avatarUrl: "https://example.com/avatar.png" - }] + return ["acme"] } }, graphQlClient: { @@ -183,10 +171,7 @@ test("It queries for both .yml and .yaml file extension with no extension", asyn projectConfigurationFilename: ".demo-docs", loginsDataSource: { async getLogins() { - return [{ - name: "acme", - avatarUrl: "https://example.com/avatar.png" - }] + return ["acme"] } }, graphQlClient: { @@ -212,16 +197,7 @@ test("It loads repositories for all logins", async () => { projectConfigurationFilename: ".demo-docs", loginsDataSource: { async getLogins() { - return [{ - name: "acme", - avatarUrl: "https://example.com/avatar.png" - }, { - name: "somecorp", - avatarUrl: "https://example.com/avatar.png" - }, { - name: "techsystems", - avatarUrl: "https://example.com/avatar.png" - }] + return ["acme", "somecorp", "techsystems"] } }, graphQlClient: { diff --git a/src/features/projects/data/GitHubLoginDataSource.ts b/src/features/projects/data/GitHubLoginDataSource.ts index d944bf0d..b71c254a 100644 --- a/src/features/projects/data/GitHubLoginDataSource.ts +++ b/src/features/projects/data/GitHubLoginDataSource.ts @@ -7,17 +7,15 @@ export default class GitHubLoginDataSource implements IGitHubLoginDataSource { this.graphQlClient = config.graphQlClient } - async getLogins(): Promise { + async getLogins(): Promise { const request = { query: ` query { viewer { login - avatarUrl organizations(first: 100) { nodes { login - avatarUrl } } } @@ -34,18 +32,8 @@ export default class GitHubLoginDataSource implements IGitHubLoginDataSource { throw new Error("organizations property not found on viewer in response") } const viewer = response.viewer - const personalLogin: GitHubLogin = { - name: viewer.login, - avatarUrl: viewer.avatarUrl - } - const organizationLogins: GitHubLogin[] = viewer - .organizations - .nodes - .map((e: { login: string, avatarUrl: string }) => { - const name = e.login - const avatarUrl = e.avatarUrl - return { name, avatarUrl } - }) - return [personalLogin].concat(organizationLogins) + const organizationLogins = viewer.organizations.nodes + .map((e: { login: string }) => e.login) + return [viewer.login].concat(organizationLogins) } } diff --git a/src/features/projects/data/GitHubRepositoryDataSource.ts b/src/features/projects/data/GitHubRepositoryDataSource.ts index 3512903e..569a4bd1 100644 --- a/src/features/projects/data/GitHubRepositoryDataSource.ts +++ b/src/features/projects/data/GitHubRepositoryDataSource.ts @@ -69,18 +69,14 @@ export default class GitHubProjectDataSource implements IGitHubRepositoryDataSou return await this.getRepositoriesForLogins({ logins }) } - private async getRepositoriesForLogins({ - logins - }: { - logins: { name: string }[] - }): Promise { + private async getRepositoriesForLogins({ logins }: { logins: string[] }): Promise { let searchQueries: string[] = [] // Search for all private repositories the user has access to. This is needed to find // repositories for external collaborators who do not belong to an organization. searchQueries.push(`"${this.repositoryNameSuffix}" in:name is:private`) // Search for public repositories belonging to a user or organization. searchQueries = searchQueries.concat(logins.map(login => { - return `"${this.repositoryNameSuffix}" in:name user:${login.name} is:public` + return `"${this.repositoryNameSuffix}" in:name user:${login} is:public` })) return await Promise.all(searchQueries.map(searchQuery => { return this.getRepositoriesForSearchQuery({ searchQuery }) diff --git a/src/features/projects/domain/IGitHubLoginDataSource.ts b/src/features/projects/domain/IGitHubLoginDataSource.ts index 83711dbd..fa880775 100644 --- a/src/features/projects/domain/IGitHubLoginDataSource.ts +++ b/src/features/projects/domain/IGitHubLoginDataSource.ts @@ -1,8 +1,3 @@ -export type GitHubLogin = { - readonly name: string - readonly avatarUrl: string -} - export default interface IGitHubLoginDataSource { - getLogins(): Promise + getLogins(): Promise } From a8a4af4bfc9eafedfe5efb07ac0cbc585b90a031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Thu, 25 Jul 2024 16:03:11 +0200 Subject: [PATCH 32/76] Adds NewProjectPage --- .env.example | 1 + src/app/new/page.tsx | 26 +++++++++ src/features/projects/view/NewProjectPage.tsx | 58 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 src/app/new/page.tsx create mode 100644 src/features/projects/view/NewProjectPage.tsx diff --git a/.env.example b/.env.example index 0b7926ac..ad45d3e5 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,7 @@ POSTGRESQL_PASSWORD= POSTGRESQL_DB=shape-docs REPOSITORY_NAME_SUFFIX=-openapi HIDDEN_REPOSITORIES= +NEW_PROJECT_TEMPLATE_REPOSITORY=shapehq/starter-openapi GITHUB_WEBHOOK_SECRET=preshared secret also put in app configuration in GitHub GITHUB_WEBHOK_REPOSITORY_ALLOWLIST= GITHUB_WEBHOK_REPOSITORY_DISALLOWLIST= diff --git a/src/app/new/page.tsx b/src/app/new/page.tsx new file mode 100644 index 00000000..2444ccc0 --- /dev/null +++ b/src/app/new/page.tsx @@ -0,0 +1,26 @@ +import { redirect } from "next/navigation" +import { SessionProvider } from "next-auth/react" +import { session } from "@/composition" +import ErrorHandler from "@/common/errors/client/ErrorHandler" +import SessionBarrier from "@/features/auth/view/SessionBarrier" +import NewProjectPage from "@/features/projects/view/NewProjectPage" +import { env } from "@/common" + +export default async function Page() { + const isAuthenticated = await session.getIsAuthenticated() + if (!isAuthenticated) { + return redirect("/api/auth/signin") + } + return ( + + + + + + + + ) +} diff --git a/src/features/projects/view/NewProjectPage.tsx b/src/features/projects/view/NewProjectPage.tsx new file mode 100644 index 00000000..67ba4fd2 --- /dev/null +++ b/src/features/projects/view/NewProjectPage.tsx @@ -0,0 +1,58 @@ +import Link from "next/link" +import { splitOwnerAndRepository } from "@/common" + +export default async function NewProjectPage({ + repositoryNameSuffix, + templateName +}: { + repositoryNameSuffix: string + templateName?: string +}) { + const projectName = "Nordisk Film" + const suffixedRepositoryName = makeFullRepositoryName({ + name: projectName, + suffix: repositoryNameSuffix + }) + const newGitHubRepositoryLink = makeNewGitHubRepositoryLink({ + templateName, + repositoryName: suffixedRepositoryName, + description: `Contains OpenAPI specifications for ${projectName}` + }) + return ( + + {newGitHubRepositoryLink} + + ) +} + +function makeFullRepositoryName({ name, suffix }: { name: string, suffix: string }) { + const safeRepositoryName = name + .trim() + .toLowerCase() + .replace(/[^a-z0-9-]+/g, "") + .replace(/\s+/g, "-") + return `${safeRepositoryName}${suffix}` +} + +function makeNewGitHubRepositoryLink({ + templateName, + repositoryName, + description +}: { + templateName?: string, + repositoryName: string, + description: string +}) { + let url = `https://github.com/new` + + `?name=${encodeURIComponent(repositoryName)}` + + `&description=${encodeURIComponent(description)}` + + `&visibility=private` + if (templateName) { + const templateRepository = splitOwnerAndRepository(templateName) + if (templateRepository) { + url += `&template_owner=${encodeURIComponent(templateRepository.owner)}` + url += `&template_name=${encodeURIComponent(templateRepository.repository)}` + } + } + return url +} From af18ca6dae59e28081ca43a0c18a8b9910050c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 12:36:45 +0200 Subject: [PATCH 33/76] Streamlines imports in tests --- __test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts | 2 +- __test__/auth/CompositeLogOutHandler.test.ts | 2 +- __test__/auth/ErrorIgnoringLogOutHandler.test.ts | 2 +- __test__/auth/LockingAccessTokenRefresher.test.ts | 2 +- __test__/auth/LogInHandler.test.ts | 2 +- __test__/auth/OAuthTokenDataSource.test.ts | 2 +- __test__/auth/OAuthTokenRepository.test.ts | 2 +- __test__/auth/OAuthTokenSessionValidator.test.ts | 2 +- __test__/auth/PersistingOAuthTokenRefresher.test.ts | 2 +- __test__/auth/UserDataCleanUpLogOutHandler.test.ts | 2 +- .../common/github/OAuthTokenRefreshingGitHubClient.test.ts | 4 ++-- __test__/common/utils/listFromCommaSeparatedString.test.ts | 2 +- __test__/common/utils/saneParseInt.test.ts | 2 +- __test__/hooks/FilteringPullRequestEventHandler.test.ts | 2 +- __test__/hooks/PostCommentPullRequestEventHandler.test.ts | 2 +- __test__/hooks/PullRequestCommenter.test.ts | 2 +- __test__/hooks/RepositoryNameEventFilter.test.ts | 2 +- __test__/projects/CachingProjectDataSource.test.ts | 2 +- __test__/projects/FilteringGitHubRepositoryDataSource.test.ts | 2 +- __test__/projects/GitHubProjectDataSource.test.ts | 4 +--- __test__/projects/GitHubRepositoryDataSource.test.ts | 4 +--- __test__/projects/ProjectConfigParser.test.ts | 2 +- __test__/projects/projectNavigator.test.ts | 2 +- __test__/projects/updateWindowTitle.test.ts | 2 +- 24 files changed, 25 insertions(+), 29 deletions(-) diff --git a/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts b/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts index 685c2c02..f31b7e7f 100644 --- a/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts +++ b/__test__/auth/AuthjsAccountsOAuthTokenRepository.test.ts @@ -1,4 +1,4 @@ -import { AuthjsAccountsOAuthTokenRepository } from "../../src/features/auth/domain" +import { AuthjsAccountsOAuthTokenRepository } from "@/features/auth/domain" test("It gets token for user ID and provider", async () => { let queryUserId: string | undefined diff --git a/__test__/auth/CompositeLogOutHandler.test.ts b/__test__/auth/CompositeLogOutHandler.test.ts index f95757bf..83c00fe1 100644 --- a/__test__/auth/CompositeLogOutHandler.test.ts +++ b/__test__/auth/CompositeLogOutHandler.test.ts @@ -1,4 +1,4 @@ -import { CompositeLogOutHandler } from "../../src/features/auth/domain" +import { CompositeLogOutHandler } from "@/features/auth/domain" test("It invokes all log out handlers", async () => { let didCallLogOutHandler1 = false diff --git a/__test__/auth/ErrorIgnoringLogOutHandler.test.ts b/__test__/auth/ErrorIgnoringLogOutHandler.test.ts index a7bacdd0..d6375f3f 100644 --- a/__test__/auth/ErrorIgnoringLogOutHandler.test.ts +++ b/__test__/auth/ErrorIgnoringLogOutHandler.test.ts @@ -1,4 +1,4 @@ -import { ErrorIgnoringLogOutHandler } from "../../src/features/auth/domain" +import { ErrorIgnoringLogOutHandler } from "@/features/auth/domain" test("It ignores errors", async () => { const sut = new ErrorIgnoringLogOutHandler({ diff --git a/__test__/auth/LockingAccessTokenRefresher.test.ts b/__test__/auth/LockingAccessTokenRefresher.test.ts index 00ee777f..736f519c 100644 --- a/__test__/auth/LockingAccessTokenRefresher.test.ts +++ b/__test__/auth/LockingAccessTokenRefresher.test.ts @@ -1,4 +1,4 @@ -import { LockingOAuthTokenRefresher } from "../../src/features/auth/domain" +import { LockingOAuthTokenRefresher } from "@/features/auth/domain" test("It acquires a lock", async () => { let didAcquireLock = false diff --git a/__test__/auth/LogInHandler.test.ts b/__test__/auth/LogInHandler.test.ts index d1dd0c0f..05f87227 100644 --- a/__test__/auth/LogInHandler.test.ts +++ b/__test__/auth/LogInHandler.test.ts @@ -1,4 +1,4 @@ -import { LogInHandler } from "../../src/features/auth/domain" +import { LogInHandler } from "@/features/auth/domain" test("It disallows logging in when account is undefined", async () => { const sut = new LogInHandler({ diff --git a/__test__/auth/OAuthTokenDataSource.test.ts b/__test__/auth/OAuthTokenDataSource.test.ts index 28390b61..857b4e8e 100644 --- a/__test__/auth/OAuthTokenDataSource.test.ts +++ b/__test__/auth/OAuthTokenDataSource.test.ts @@ -1,4 +1,4 @@ -import { OAuthTokenDataSource } from "../../src/features/auth/domain" +import { OAuthTokenDataSource } from "@/features/auth/domain" test("It reads OAuth token for user's ID", async () => { let readUserId: string | undefined diff --git a/__test__/auth/OAuthTokenRepository.test.ts b/__test__/auth/OAuthTokenRepository.test.ts index e7f1a124..bf2c964f 100644 --- a/__test__/auth/OAuthTokenRepository.test.ts +++ b/__test__/auth/OAuthTokenRepository.test.ts @@ -1,4 +1,4 @@ -import { OAuthTokenRepository } from "../../src/features/auth/domain" +import { OAuthTokenRepository } from "@/features/auth/domain" test("It reads the auth token for the specified user", async () => { let readProvider: string | undefined diff --git a/__test__/auth/OAuthTokenSessionValidator.test.ts b/__test__/auth/OAuthTokenSessionValidator.test.ts index 2abadbbd..60f75e3f 100644 --- a/__test__/auth/OAuthTokenSessionValidator.test.ts +++ b/__test__/auth/OAuthTokenSessionValidator.test.ts @@ -1,4 +1,4 @@ -import { OAuthTokenSessionValidator, SessionValidity } from "../../src/features/auth/domain" +import { OAuthTokenSessionValidator, SessionValidity } from "@/features/auth/domain" test("It reads the access token", async () => { let didReadOAuthToken = false diff --git a/__test__/auth/PersistingOAuthTokenRefresher.test.ts b/__test__/auth/PersistingOAuthTokenRefresher.test.ts index a2537939..791c1b0c 100644 --- a/__test__/auth/PersistingOAuthTokenRefresher.test.ts +++ b/__test__/auth/PersistingOAuthTokenRefresher.test.ts @@ -1,4 +1,4 @@ -import { PersistingOAuthTokenRefresher, OAuthToken } from "../../src/features/auth/domain" +import { PersistingOAuthTokenRefresher, OAuthToken } from "@/features/auth/domain" test("It refreshes OAuth token using provided refresh token", async () => { let usedRefreshToken: string | undefined diff --git a/__test__/auth/UserDataCleanUpLogOutHandler.test.ts b/__test__/auth/UserDataCleanUpLogOutHandler.test.ts index 6cbfc03d..9b7ec310 100644 --- a/__test__/auth/UserDataCleanUpLogOutHandler.test.ts +++ b/__test__/auth/UserDataCleanUpLogOutHandler.test.ts @@ -1,4 +1,4 @@ -import { UserDataCleanUpLogOutHandler } from "../../src/features/auth/domain" +import { UserDataCleanUpLogOutHandler } from "@/features/auth/domain" test("It deletes data for the read user ID", async () => { let deletedUserId: string | undefined diff --git a/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts b/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts index 7e353354..548d85a3 100644 --- a/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts +++ b/__test__/common/github/OAuthTokenRefreshingGitHubClient.test.ts @@ -1,10 +1,10 @@ -import { OAuthTokenRefreshingGitHubClient } from "@/common" import { + OAuthTokenRefreshingGitHubClient, GraphQLQueryRequest, GetRepositoryContentRequest, GetPullRequestCommentsRequest, AddCommentToPullRequestRequest -} from "@/common/github/IGitHubClient" +} from "@/common" test("It forwards a GraphQL request", async () => { let forwardedRequest: GraphQLQueryRequest | undefined diff --git a/__test__/common/utils/listFromCommaSeparatedString.test.ts b/__test__/common/utils/listFromCommaSeparatedString.test.ts index e3f75d43..08fad85b 100644 --- a/__test__/common/utils/listFromCommaSeparatedString.test.ts +++ b/__test__/common/utils/listFromCommaSeparatedString.test.ts @@ -1,4 +1,4 @@ -import listFromCommaSeparatedString from "@/common/utils/listFromCommaSeparatedString" +import { listFromCommaSeparatedString } from "@/common" test("It returns an empty list given undefined", async () => { const result = listFromCommaSeparatedString(undefined) diff --git a/__test__/common/utils/saneParseInt.test.ts b/__test__/common/utils/saneParseInt.test.ts index ebd217d4..5acc1e28 100644 --- a/__test__/common/utils/saneParseInt.test.ts +++ b/__test__/common/utils/saneParseInt.test.ts @@ -1,4 +1,4 @@ -import saneParseInt from "@/common/utils/saneParseInt" +import { saneParseInt } from "@/common" test("It parses an integer", async () => { // @ts-ignore diff --git a/__test__/hooks/FilteringPullRequestEventHandler.test.ts b/__test__/hooks/FilteringPullRequestEventHandler.test.ts index 2c1e8045..65b5836f 100644 --- a/__test__/hooks/FilteringPullRequestEventHandler.test.ts +++ b/__test__/hooks/FilteringPullRequestEventHandler.test.ts @@ -1,4 +1,4 @@ -import { FilteringPullRequestEventHandler } from "../../src/features/hooks/domain" +import { FilteringPullRequestEventHandler } from "@/features/hooks/domain" test("It calls pullRequestOpened(_:) when event is included", async () => { let didCall = false diff --git a/__test__/hooks/PostCommentPullRequestEventHandler.test.ts b/__test__/hooks/PostCommentPullRequestEventHandler.test.ts index 46f7ee55..153c3fa7 100644 --- a/__test__/hooks/PostCommentPullRequestEventHandler.test.ts +++ b/__test__/hooks/PostCommentPullRequestEventHandler.test.ts @@ -1,4 +1,4 @@ -import { PostCommentPullRequestEventHandler } from "../../src/features/hooks/domain" +import { PostCommentPullRequestEventHandler } from "@/features/hooks/domain" test("It comments when opening a pull request", async () => { let didComment = false diff --git a/__test__/hooks/PullRequestCommenter.test.ts b/__test__/hooks/PullRequestCommenter.test.ts index 358c5af5..ff4cadde 100644 --- a/__test__/hooks/PullRequestCommenter.test.ts +++ b/__test__/hooks/PullRequestCommenter.test.ts @@ -1,4 +1,4 @@ -import { PullRequestCommenter } from "../../src/features/hooks/domain" +import { PullRequestCommenter } from "@/features/hooks/domain" test("It adds comment when none exist", async () => { let didAddComment = false diff --git a/__test__/hooks/RepositoryNameEventFilter.test.ts b/__test__/hooks/RepositoryNameEventFilter.test.ts index 473ce34b..2b06ca1d 100644 --- a/__test__/hooks/RepositoryNameEventFilter.test.ts +++ b/__test__/hooks/RepositoryNameEventFilter.test.ts @@ -1,4 +1,4 @@ -import { RepositoryNameEventFilter } from "../../src/features/hooks/domain" +import { RepositoryNameEventFilter } from "@/features/hooks/domain" test("It does not include repositories that do not have the \"-openapi\" suffix", async () => { const sut = new RepositoryNameEventFilter({ diff --git a/__test__/projects/CachingProjectDataSource.test.ts b/__test__/projects/CachingProjectDataSource.test.ts index 0a9fc418..9365f53a 100644 --- a/__test__/projects/CachingProjectDataSource.test.ts +++ b/__test__/projects/CachingProjectDataSource.test.ts @@ -1,4 +1,4 @@ -import { Project, CachingProjectDataSource } from "../../src/features/projects/domain" +import { Project, CachingProjectDataSource } from "@/features/projects/domain" test("It caches projects read from the data source", async () => { const projects: Project[] = [{ diff --git a/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts index 1f408320..e834d7b0 100644 --- a/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts +++ b/__test__/projects/FilteringGitHubRepositoryDataSource.test.ts @@ -1,4 +1,4 @@ -import { FilteringGitHubRepositoryDataSource } from "../../src/features/projects/domain" +import { FilteringGitHubRepositoryDataSource } from "@/features/projects/domain" test("It returns all repositories when no hidden repositories are provided", async () => { const sut = new FilteringGitHubRepositoryDataSource({ diff --git a/__test__/projects/GitHubProjectDataSource.test.ts b/__test__/projects/GitHubProjectDataSource.test.ts index e000a115..ac01eaa1 100644 --- a/__test__/projects/GitHubProjectDataSource.test.ts +++ b/__test__/projects/GitHubProjectDataSource.test.ts @@ -1,6 +1,4 @@ -import { - GitHubProjectDataSource - } from "../../src/features/projects/data" +import { GitHubProjectDataSource } from "@/features/projects/data" test("It loads repositories from data source", async () => { let didLoadRepositories = false diff --git a/__test__/projects/GitHubRepositoryDataSource.test.ts b/__test__/projects/GitHubRepositoryDataSource.test.ts index c6763ab2..9235a28b 100644 --- a/__test__/projects/GitHubRepositoryDataSource.test.ts +++ b/__test__/projects/GitHubRepositoryDataSource.test.ts @@ -1,6 +1,4 @@ -import { - GitHubRepositoryDataSource - } from "../../src/features/projects/data" +import { GitHubRepositoryDataSource } from "@/features/projects/data" test("It loads repositories from data source", async () => { let didLoadRepositories = false diff --git a/__test__/projects/ProjectConfigParser.test.ts b/__test__/projects/ProjectConfigParser.test.ts index 4cb0ee0e..4b2256fb 100644 --- a/__test__/projects/ProjectConfigParser.test.ts +++ b/__test__/projects/ProjectConfigParser.test.ts @@ -1,4 +1,4 @@ -import { ProjectConfigParser } from "../../src/features/projects/domain" +import { ProjectConfigParser } from "@/features/projects/domain" test("It parses an empty string", async () => { const sut = new ProjectConfigParser() diff --git a/__test__/projects/projectNavigator.test.ts b/__test__/projects/projectNavigator.test.ts index 4cc0cb6a..d1d71825 100644 --- a/__test__/projects/projectNavigator.test.ts +++ b/__test__/projects/projectNavigator.test.ts @@ -1,4 +1,4 @@ -import { ProjectNavigator } from "../../src/features/projects/domain" +import { ProjectNavigator } from "@/features/projects/domain" test("It navigates to the correct path", async () => { let pushedPath: string | undefined diff --git a/__test__/projects/updateWindowTitle.test.ts b/__test__/projects/updateWindowTitle.test.ts index 5521f406..86509b38 100644 --- a/__test__/projects/updateWindowTitle.test.ts +++ b/__test__/projects/updateWindowTitle.test.ts @@ -1,4 +1,4 @@ -import { updateWindowTitle } from "../../src/features/projects/domain" +import { updateWindowTitle } from "@/features/projects/domain" test("It uses default title when there is no selection", async () => { const store: { title: string } = { title: "" } From e41031cee8619663bd234bf9ac9a52d2468d8af7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 12:36:58 +0200 Subject: [PATCH 34/76] Removes unneeded file --- test | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test diff --git a/test b/test deleted file mode 100644 index db13c084..00000000 --- a/test +++ /dev/null @@ -1 +0,0 @@ -testing automatic deployment From beca0537991b5d81a2bc31b5c3eabada0911a16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 12:37:41 +0200 Subject: [PATCH 35/76] Removes unused types --- .../auth/view/client/SessionBarrier.tsx | 24 ------------------- .../auth/view/client/SessionProvider.tsx | 15 ------------ 2 files changed, 39 deletions(-) delete mode 100644 src/features/auth/view/client/SessionBarrier.tsx delete mode 100644 src/features/auth/view/client/SessionProvider.tsx diff --git a/src/features/auth/view/client/SessionBarrier.tsx b/src/features/auth/view/client/SessionBarrier.tsx deleted file mode 100644 index 55408cfd..00000000 --- a/src/features/auth/view/client/SessionBarrier.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import { ReactNode } from "react" -import { SessionValidity } from "../../domain" -import InvalidSessionPage from "./InvalidSessionPage" - -export default function SessionBarrier({ - sessionValidity, - children -}: { - sessionValidity: SessionValidity - children: ReactNode -}) { - switch (sessionValidity) { - case SessionValidity.VALID: - return <>{children} - case SessionValidity.INVALID_ACCESS_TOKEN: - return ( - - It was not possible to obtain access to the repositories on GitHub. - - ) - } -} diff --git a/src/features/auth/view/client/SessionProvider.tsx b/src/features/auth/view/client/SessionProvider.tsx deleted file mode 100644 index 37c3aee7..00000000 --- a/src/features/auth/view/client/SessionProvider.tsx +++ /dev/null @@ -1,15 +0,0 @@ -"use client" - -import { SessionProvider as NextAuthSessionProvider } from "next-auth/react" - -export default function SessionProvider({ - children -}: { - children: React.ReactNode -}) { - return ( - - {children} - - ) -} From fb700c039b7ec906aae0df595bfe61bf2a366211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 12:37:48 +0200 Subject: [PATCH 36/76] Removes InvalidSessionPage --- src/features/auth/view/SessionBarrier.tsx | 14 +++--- .../auth/view/client/InvalidSessionPage.tsx | 45 ------------------- 2 files changed, 8 insertions(+), 51 deletions(-) delete mode 100644 src/features/auth/view/client/InvalidSessionPage.tsx diff --git a/src/features/auth/view/SessionBarrier.tsx b/src/features/auth/view/SessionBarrier.tsx index 8f1fdf0d..cf4b41f4 100644 --- a/src/features/auth/view/SessionBarrier.tsx +++ b/src/features/auth/view/SessionBarrier.tsx @@ -1,6 +1,7 @@ import { ReactNode } from "react" +import { redirect } from "next/navigation" import { blockingSessionValidator } from "@/composition" -import ClientSessionBarrier from "./client/SessionBarrier" +import { SessionValidity } from "../domain" export default async function SessionBarrier({ children @@ -8,9 +9,10 @@ export default async function SessionBarrier({ children: ReactNode }) { const sessionValidity = await blockingSessionValidator.validateSession() - return ( - - {children} - - ) + switch (sessionValidity) { + case SessionValidity.VALID: + return <>{children} + case SessionValidity.INVALID_ACCESS_TOKEN: + return redirect("/api/auth/signout") + } } diff --git a/src/features/auth/view/client/InvalidSessionPage.tsx b/src/features/auth/view/client/InvalidSessionPage.tsx deleted file mode 100644 index 8a796a46..00000000 --- a/src/features/auth/view/client/InvalidSessionPage.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client" - -import { ReactNode } from "react" -import { Stack, Typography } from "@mui/material" -import SidebarContainer from "@/features/sidebar/view/client/SidebarContainer" -import useSidebarOpen from "@/common/state/useSidebarOpen" - -export default function InvalidSessionPage({ - title, - children -}: { - title?: ReactNode - children?: ReactNode -}) { - const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() - return ( - - - - {title && - - {title} - - } - - {children} - - - - - ) -} From b2694457f74de8a03842e8a5893a9742c633280e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 12:39:01 +0200 Subject: [PATCH 37/76] Rearchitects app to share sidebar in layout --- ...ts => getProjectSelectionFromPath.test.ts} | 20 +-- src/app/(authed)/(home)/[[...slug]]/page.tsx | 43 ++++++ src/app/(authed)/(home)/layout.tsx | 31 +++++ .../(authed)/(home)/new/page.tsx} | 13 +- src/app/{new/page.tsx => (authed)/layout.tsx} | 18 +-- src/app/[[...slug]]/page.tsx | 37 ------ src/common/contexts.ts | 17 +++ src/common/index.ts | 1 + src/common/state/sidebarOpen.tsx | 5 - .../DelayedLoadingIndicator.tsx | 8 +- .../{errors/client => ui}/ErrorHandler.tsx | 6 +- .../view => common/ui}/ErrorMessage.tsx | 0 .../{loading => ui}/LoadingIndicator.tsx | 2 + src/common/ui/ThickDivider.tsx | 6 +- src/common/utils/index.ts | 1 + src/features/_old_sidebar/view/Sidebar.tsx | 18 +++ .../view/SidebarHeader.tsx | 0 .../view/TrailingToolbar.tsx | 0 .../view/base/Drawer.tsx | 0 .../view/base/SecondaryHeader.tsx | 0 .../view/base/SecondaryWrapper.tsx | 0 .../view/base/responsive/Drawer.tsx | 0 .../view/base/responsive/SecondaryHeader.tsx | 0 .../view/base/responsive/SecondaryWrapper.tsx | 0 .../view/base/responsive/SidebarContainer.tsx | 0 .../view/client/SidebarContainer.tsx | 0 src/features/docs/view/LoadingWrapper.tsx | 2 +- src/features/projects/data/index.ts | 1 + .../projects/data/useProjectSelection.ts | 71 ++++++++++ ...tion.ts => getProjectSelectionFromPath.ts} | 2 +- src/features/projects/domain/index.ts | 3 +- .../projects/domain/useProjectNavigator.ts | 16 --- .../projects/view/DocumentationIframe.tsx | 2 +- src/features/projects/view/MainContent.tsx | 34 ----- src/features/projects/view/ProjectsPage.tsx | 18 --- .../view/ServerSideCachedProjectsProvider.tsx | 20 +++ .../projects/view/client/ProjectsPage.tsx | 125 ------------------ src/features/sidebar/data/index.ts | 1 + .../sidebar/data/useSidebarOpen.ts} | 2 +- src/features/sidebar/view/Sidebar.tsx | 27 ++-- src/features/sidebar/view/SplitView.tsx | 48 +++++++ src/features/sidebar/view/index.ts | 2 + .../view/internal/PrimaryContainer.tsx | 82 ++++++++++++ .../view/internal/SecondaryContainer.tsx | 82 ++++++++++++ .../view/internal/sidebar-content/Header.tsx | 55 ++++++++ .../projects}/ProjectAvatar.tsx | 11 +- .../projects/ProjectAvatarSquircle.tsx} | 7 +- .../sidebar-content/projects}/ProjectList.tsx | 26 ++-- .../projects}/ProjectListItem.tsx | 7 +- .../projects}/ProjectListItemPlaceholder.tsx | 18 +-- .../DocumentationVisualizationPicker.tsx | 0 .../settings}/SettingsList.tsx | 0 .../sidebar-content/user}/UserButton.tsx | 40 +++++- .../sidebar-content/user}/UserSkeleton.tsx | 0 src/features/user/view/UserFooter.tsx | 19 --- 55 files changed, 589 insertions(+), 358 deletions(-) rename __test__/projects/{getSelection.test.ts => getProjectSelectionFromPath.test.ts} (93%) create mode 100644 src/app/(authed)/(home)/[[...slug]]/page.tsx create mode 100644 src/app/(authed)/(home)/layout.tsx rename src/{features/projects/view/NewProjectPage.tsx => app/(authed)/(home)/new/page.tsx} (87%) rename src/app/{new/page.tsx => (authed)/layout.tsx} (50%) delete mode 100644 src/app/[[...slug]]/page.tsx create mode 100644 src/common/contexts.ts delete mode 100644 src/common/state/sidebarOpen.tsx rename src/common/{loading => ui}/DelayedLoadingIndicator.tsx (82%) rename src/common/{errors/client => ui}/ErrorHandler.tsx (81%) rename src/{features/projects/view => common/ui}/ErrorMessage.tsx (100%) rename src/common/{loading => ui}/LoadingIndicator.tsx (98%) create mode 100644 src/features/_old_sidebar/view/Sidebar.tsx rename src/features/{sidebar => _old_sidebar}/view/SidebarHeader.tsx (100%) rename src/features/{sidebar => _old_sidebar}/view/TrailingToolbar.tsx (100%) rename src/features/{sidebar => _old_sidebar}/view/base/Drawer.tsx (100%) rename src/features/{sidebar => _old_sidebar}/view/base/SecondaryHeader.tsx (100%) rename src/features/{sidebar => _old_sidebar}/view/base/SecondaryWrapper.tsx (100%) rename src/features/{sidebar => _old_sidebar}/view/base/responsive/Drawer.tsx (100%) rename src/features/{sidebar => _old_sidebar}/view/base/responsive/SecondaryHeader.tsx (100%) rename src/features/{sidebar => _old_sidebar}/view/base/responsive/SecondaryWrapper.tsx (100%) rename src/features/{sidebar => _old_sidebar}/view/base/responsive/SidebarContainer.tsx (100%) rename src/features/{sidebar => _old_sidebar}/view/client/SidebarContainer.tsx (100%) create mode 100644 src/features/projects/data/useProjectSelection.ts rename src/features/projects/domain/{getSelection.ts => getProjectSelectionFromPath.ts} (98%) delete mode 100644 src/features/projects/domain/useProjectNavigator.ts delete mode 100644 src/features/projects/view/MainContent.tsx delete mode 100644 src/features/projects/view/ProjectsPage.tsx create mode 100644 src/features/projects/view/ServerSideCachedProjectsProvider.tsx delete mode 100644 src/features/projects/view/client/ProjectsPage.tsx create mode 100644 src/features/sidebar/data/index.ts rename src/{common/state/useSidebarOpen.tsx => features/sidebar/data/useSidebarOpen.ts} (65%) create mode 100644 src/features/sidebar/view/SplitView.tsx create mode 100644 src/features/sidebar/view/index.ts create mode 100644 src/features/sidebar/view/internal/PrimaryContainer.tsx create mode 100644 src/features/sidebar/view/internal/SecondaryContainer.tsx create mode 100644 src/features/sidebar/view/internal/sidebar-content/Header.tsx rename src/features/{projects/view => sidebar/view/internal/sidebar-content/projects}/ProjectAvatar.tsx (83%) rename src/features/{projects/view/ProjectAvatarSquircleClip.tsx => sidebar/view/internal/sidebar-content/projects/ProjectAvatarSquircle.tsx} (76%) rename src/features/{projects/view => sidebar/view/internal/sidebar-content/projects}/ProjectList.tsx (68%) rename src/features/{projects/view => sidebar/view/internal/sidebar-content/projects}/ProjectListItem.tsx (84%) rename src/features/{projects/view => sidebar/view/internal/sidebar-content/projects}/ProjectListItemPlaceholder.tsx (78%) rename src/features/{user/view => sidebar/view/internal/sidebar-content/settings}/DocumentationVisualizationPicker.tsx (100%) rename src/features/{user/view => sidebar/view/internal/sidebar-content/settings}/SettingsList.tsx (100%) rename src/features/{user/view => sidebar/view/internal/sidebar-content/user}/UserButton.tsx (72%) rename src/features/{user/view => sidebar/view/internal/sidebar-content/user}/UserSkeleton.tsx (100%) delete mode 100644 src/features/user/view/UserFooter.tsx diff --git a/__test__/projects/getSelection.test.ts b/__test__/projects/getProjectSelectionFromPath.test.ts similarity index 93% rename from __test__/projects/getSelection.test.ts rename to __test__/projects/getProjectSelectionFromPath.test.ts index a5a544ca..7991fb1d 100644 --- a/__test__/projects/getSelection.test.ts +++ b/__test__/projects/getProjectSelectionFromPath.test.ts @@ -1,7 +1,7 @@ -import { getSelection } from "../../src/features/projects/domain" +import { getProjectSelectionFromPath } from "@/features/projects/domain" test("It selects the first project when there is only one project and path is empty", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "", projects: [{ id: "foo", @@ -28,7 +28,7 @@ test("It selects the first project when there is only one project and path is em }) test("It selects the first version and specification of the specified project", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/bar", projects: [{ id: "foo", @@ -71,7 +71,7 @@ test("It selects the first version and specification of the specified project", }) test("It selects the first specification of the specified project and version", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/bar/baz2", projects: [{ id: "foo", @@ -110,7 +110,7 @@ test("It selects the first specification of the specified project and version", }) test("It selects the specification of the specified version", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/bar/baz2", projects: [{ id: "foo", @@ -153,7 +153,7 @@ test("It selects the specification of the specified version", () => { }) test("It selects the specified project, version, and specification", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/bar/baz2/hello2", projects: [{ id: "foo", @@ -196,7 +196,7 @@ test("It selects the specified project, version, and specification", () => { }) test("It returns a undefined project, version, and specification when the selected project cannot be found", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/foo", projects: [{ id: "bar", @@ -213,7 +213,7 @@ test("It returns a undefined project, version, and specification when the select }) test("It returns a undefined version and specification when the selected version cannot be found", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/foo/bar", projects: [{ id: "foo", @@ -236,7 +236,7 @@ test("It returns a undefined version and specification when the selected version }) test("It returns a undefined specification when the selected specification cannot be found", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/foo/bar/baz", projects: [{ id: "foo", @@ -263,7 +263,7 @@ test("It returns a undefined specification when the selected specification canno }) test("It moves specification ID to version ID if needed", () => { - const sut = getSelection({ + const sut = getProjectSelectionFromPath({ path: "/acme/foo/bar/baz", projects: [{ id: "foo", diff --git a/src/app/(authed)/(home)/[[...slug]]/page.tsx b/src/app/(authed)/(home)/[[...slug]]/page.tsx new file mode 100644 index 00000000..37a39ab2 --- /dev/null +++ b/src/app/(authed)/(home)/[[...slug]]/page.tsx @@ -0,0 +1,43 @@ +"use client" + +import { useContext, useEffect } from "react" +import { ProjectsContainerContext } from "@/common" +import DelayedLoadingIndicator from "@/common/ui/DelayedLoadingIndicator" +import ErrorMessage from "@/common/ui/ErrorMessage" +import { useProjectSelection } from "@/features/projects/data" +import Documentation from "@/features/projects/view/Documentation" +import { updateWindowTitle } from "@/features/projects/domain" + +export default function Page() { + const { error, isLoading } = useContext(ProjectsContainerContext) + const { project, version, specification, navigateToSelectionIfNeeded } = useProjectSelection() + // Ensure the URL reflects the current selection of project, version, and specification. + useEffect(() => { + navigateToSelectionIfNeeded() + }, [project, version, specification]) + // Update the window title to match selected project. + const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE || "" + useEffect(() => { + updateWindowTitle({ + storage: document, + defaultTitle: siteName, + project, + version, + specification + }) + }, [siteName, project, version, specification]) + if (project && version && specification) { + return + } else if (project && !version) { + return + } else if (project && !specification) { + return + } else if (isLoading) { + return + } else if (error) { + return + } else { + // No project is selected so we will not show anything. + return <> + } +} diff --git a/src/app/(authed)/(home)/layout.tsx b/src/app/(authed)/(home)/layout.tsx new file mode 100644 index 00000000..92ad9381 --- /dev/null +++ b/src/app/(authed)/(home)/layout.tsx @@ -0,0 +1,31 @@ +"use client" + +import { useContext } from "react" +import { SplitView } from "@/features/sidebar/view" +import { useProjects } from "@/features/projects/data" +// import MainContent from "@/features/projects/view/MainContent" +// import MobileToolbar from "@/features/projects/view/toolbar/MobileToolbar" +// import TrailingToolbarItem from "@/features/projects/view/toolbar/TrailingToolbarItem" + +import { + ProjectsContainerContext, + ServerSideCachedProjectsContext +} from "@/common" + +export default function Layout({ children }: { children: React.ReactNode }) { + const { projects, error, isLoading } = useProjects() + // Update projects provided to child components, using cached projects from the server if needed. + const serverSideCachedProjects = useContext(ServerSideCachedProjectsContext) + let newProjectsContainer = { projects, error, isLoading } + if (isLoading && serverSideCachedProjects) { + newProjectsContainer.isLoading = false + newProjectsContainer.projects = serverSideCachedProjects + } + return ( + + + {children} + + + ) +} diff --git a/src/features/projects/view/NewProjectPage.tsx b/src/app/(authed)/(home)/new/page.tsx similarity index 87% rename from src/features/projects/view/NewProjectPage.tsx rename to src/app/(authed)/(home)/new/page.tsx index 67ba4fd2..831a9396 100644 --- a/src/features/projects/view/NewProjectPage.tsx +++ b/src/app/(authed)/(home)/new/page.tsx @@ -1,13 +1,10 @@ import Link from "next/link" import { splitOwnerAndRepository } from "@/common" +import { env } from "@/common" -export default async function NewProjectPage({ - repositoryNameSuffix, - templateName -}: { - repositoryNameSuffix: string - templateName?: string -}) { +const Page = () => { + const repositoryNameSuffix = env.getOrThrow("REPOSITORY_NAME_SUFFIX") + const templateName = env.get("NEW_PROJECT_TEMPLATE_REPOSITORY") const projectName = "Nordisk Film" const suffixedRepositoryName = makeFullRepositoryName({ name: projectName, @@ -25,6 +22,8 @@ export default async function NewProjectPage({ ) } +export default Page + function makeFullRepositoryName({ name, suffix }: { name: string, suffix: string }) { const safeRepositoryName = name .trim() diff --git a/src/app/new/page.tsx b/src/app/(authed)/layout.tsx similarity index 50% rename from src/app/new/page.tsx rename to src/app/(authed)/layout.tsx index 2444ccc0..03f37d87 100644 --- a/src/app/new/page.tsx +++ b/src/app/(authed)/layout.tsx @@ -1,26 +1,26 @@ import { redirect } from "next/navigation" import { SessionProvider } from "next-auth/react" import { session } from "@/composition" -import ErrorHandler from "@/common/errors/client/ErrorHandler" +import ErrorHandler from "@/common/ui/ErrorHandler" import SessionBarrier from "@/features/auth/view/SessionBarrier" -import NewProjectPage from "@/features/projects/view/NewProjectPage" -import { env } from "@/common" +import { projectRepository } from "@/composition" +import ServerSideCachedProjectsProvider from "@/features/projects/view/ServerSideCachedProjectsProvider" -export default async function Page() { +export default async function Layout({ children }: { children: React.ReactNode }) { const isAuthenticated = await session.getIsAuthenticated() if (!isAuthenticated) { return redirect("/api/auth/signin") } + const projects = await projectRepository.get() return ( - + + {children} + ) -} +} \ No newline at end of file diff --git a/src/app/[[...slug]]/page.tsx b/src/app/[[...slug]]/page.tsx deleted file mode 100644 index 092f7f40..00000000 --- a/src/app/[[...slug]]/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { redirect } from "next/navigation" -import { SessionProvider } from "next-auth/react" -import { session, projectRepository } from "@/composition" -import ErrorHandler from "@/common/errors/client/ErrorHandler" -import SessionBarrier from "@/features/auth/view/SessionBarrier" -import ProjectsPage from "@/features/projects/view/ProjectsPage" - -type PageParams = { slug: string | string[] } - -export default async function Page({ params }: { params: PageParams }) { - const isAuthenticated = await session.getIsAuthenticated() - if (!isAuthenticated) { - return redirect("/api/auth/signin") - } - return ( - - - - - - - - ) -} - -function getPath(slug: string | string[] | undefined) { - if (slug === undefined) { - return "/" - } else if (typeof slug === "string") { - return "/" + slug - } else { - return slug.reduce((e, acc) => `${e}/${acc}`, "") - } -} diff --git a/src/common/contexts.ts b/src/common/contexts.ts new file mode 100644 index 00000000..6f19513f --- /dev/null +++ b/src/common/contexts.ts @@ -0,0 +1,17 @@ +"use client" + +import { createContext } from "react" +import { Project, } from "@/features/projects/domain" + +type ProjectsContainer = { + readonly projects: Project[] + readonly isLoading: boolean + readonly error?: Error +} + +export const ProjectsContainerContext = createContext({ + isLoading: true, + projects: [] +}) + +export const ServerSideCachedProjectsContext = createContext(undefined) diff --git a/src/common/index.ts b/src/common/index.ts index fdb90059..83b57c0c 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -1,3 +1,4 @@ +export * from "./contexts" export * from "./db" export * from "./errors" export * from "./github" diff --git a/src/common/state/sidebarOpen.tsx b/src/common/state/sidebarOpen.tsx deleted file mode 100644 index 2d0c3b9e..00000000 --- a/src/common/state/sidebarOpen.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { useSessionStorage } from "usehooks-ts" - -export default function useSidebarOpen() { - return useSessionStorage("isSidebarOpen", true) -} \ No newline at end of file diff --git a/src/common/loading/DelayedLoadingIndicator.tsx b/src/common/ui/DelayedLoadingIndicator.tsx similarity index 82% rename from src/common/loading/DelayedLoadingIndicator.tsx rename to src/common/ui/DelayedLoadingIndicator.tsx index ce90d90b..74071f9e 100644 --- a/src/common/loading/DelayedLoadingIndicator.tsx +++ b/src/common/ui/DelayedLoadingIndicator.tsx @@ -1,11 +1,9 @@ +"use client" + import { useState, useEffect } from "react" import LoadingIndicator from "./LoadingIndicator" -const DelayedLoadingIndicator = ({ - delay -}: { - delay?: number -}) => { +const DelayedLoadingIndicator = ({ delay }: { delay?: number }) => { const [isVisible, setVisible] = useState(false) useEffect(() => { const timer = setTimeout(() => { diff --git a/src/common/errors/client/ErrorHandler.tsx b/src/common/ui/ErrorHandler.tsx similarity index 81% rename from src/common/errors/client/ErrorHandler.tsx rename to src/common/ui/ErrorHandler.tsx index 7e928289..d206fa4b 100644 --- a/src/common/errors/client/ErrorHandler.tsx +++ b/src/common/ui/ErrorHandler.tsx @@ -3,11 +3,7 @@ import { SWRConfig } from "swr" import { FetcherError } from "@/common" -export default function ErrorHandler({ - children -}: { - children: React.ReactNode -}) { +export default function ErrorHandler({ children }: { children: React.ReactNode }) { const onSWRError = (error: FetcherError) => { if (typeof window === "undefined") { return diff --git a/src/features/projects/view/ErrorMessage.tsx b/src/common/ui/ErrorMessage.tsx similarity index 100% rename from src/features/projects/view/ErrorMessage.tsx rename to src/common/ui/ErrorMessage.tsx diff --git a/src/common/loading/LoadingIndicator.tsx b/src/common/ui/LoadingIndicator.tsx similarity index 98% rename from src/common/loading/LoadingIndicator.tsx rename to src/common/ui/LoadingIndicator.tsx index dc464219..f56dd4d8 100644 --- a/src/common/loading/LoadingIndicator.tsx +++ b/src/common/ui/LoadingIndicator.tsx @@ -1,3 +1,5 @@ +"use client" + import { useState, useEffect } from "react" import { Box, Typography } from "@mui/material" import { useTheme } from "@mui/material/styles" diff --git a/src/common/ui/ThickDivider.tsx b/src/common/ui/ThickDivider.tsx index f73cf914..1dc1da3c 100644 --- a/src/common/ui/ThickDivider.tsx +++ b/src/common/ui/ThickDivider.tsx @@ -2,11 +2,7 @@ import { SxProps } from "@mui/system" import { Box } from "@mui/material" import { useTheme } from "@mui/material/styles" -const ThickDivider = ({ - sx -}: { - sx?: SxProps -}) => { +const ThickDivider = ({ sx }: { sx?: SxProps }) => { const theme = useTheme() return ( { + return ( + <> + + + {children} + + + + ) +} + +export default Sidebar diff --git a/src/features/sidebar/view/SidebarHeader.tsx b/src/features/_old_sidebar/view/SidebarHeader.tsx similarity index 100% rename from src/features/sidebar/view/SidebarHeader.tsx rename to src/features/_old_sidebar/view/SidebarHeader.tsx diff --git a/src/features/sidebar/view/TrailingToolbar.tsx b/src/features/_old_sidebar/view/TrailingToolbar.tsx similarity index 100% rename from src/features/sidebar/view/TrailingToolbar.tsx rename to src/features/_old_sidebar/view/TrailingToolbar.tsx diff --git a/src/features/sidebar/view/base/Drawer.tsx b/src/features/_old_sidebar/view/base/Drawer.tsx similarity index 100% rename from src/features/sidebar/view/base/Drawer.tsx rename to src/features/_old_sidebar/view/base/Drawer.tsx diff --git a/src/features/sidebar/view/base/SecondaryHeader.tsx b/src/features/_old_sidebar/view/base/SecondaryHeader.tsx similarity index 100% rename from src/features/sidebar/view/base/SecondaryHeader.tsx rename to src/features/_old_sidebar/view/base/SecondaryHeader.tsx diff --git a/src/features/sidebar/view/base/SecondaryWrapper.tsx b/src/features/_old_sidebar/view/base/SecondaryWrapper.tsx similarity index 100% rename from src/features/sidebar/view/base/SecondaryWrapper.tsx rename to src/features/_old_sidebar/view/base/SecondaryWrapper.tsx diff --git a/src/features/sidebar/view/base/responsive/Drawer.tsx b/src/features/_old_sidebar/view/base/responsive/Drawer.tsx similarity index 100% rename from src/features/sidebar/view/base/responsive/Drawer.tsx rename to src/features/_old_sidebar/view/base/responsive/Drawer.tsx diff --git a/src/features/sidebar/view/base/responsive/SecondaryHeader.tsx b/src/features/_old_sidebar/view/base/responsive/SecondaryHeader.tsx similarity index 100% rename from src/features/sidebar/view/base/responsive/SecondaryHeader.tsx rename to src/features/_old_sidebar/view/base/responsive/SecondaryHeader.tsx diff --git a/src/features/sidebar/view/base/responsive/SecondaryWrapper.tsx b/src/features/_old_sidebar/view/base/responsive/SecondaryWrapper.tsx similarity index 100% rename from src/features/sidebar/view/base/responsive/SecondaryWrapper.tsx rename to src/features/_old_sidebar/view/base/responsive/SecondaryWrapper.tsx diff --git a/src/features/sidebar/view/base/responsive/SidebarContainer.tsx b/src/features/_old_sidebar/view/base/responsive/SidebarContainer.tsx similarity index 100% rename from src/features/sidebar/view/base/responsive/SidebarContainer.tsx rename to src/features/_old_sidebar/view/base/responsive/SidebarContainer.tsx diff --git a/src/features/sidebar/view/client/SidebarContainer.tsx b/src/features/_old_sidebar/view/client/SidebarContainer.tsx similarity index 100% rename from src/features/sidebar/view/client/SidebarContainer.tsx rename to src/features/_old_sidebar/view/client/SidebarContainer.tsx diff --git a/src/features/docs/view/LoadingWrapper.tsx b/src/features/docs/view/LoadingWrapper.tsx index 65c70157..8d45427e 100644 --- a/src/features/docs/view/LoadingWrapper.tsx +++ b/src/features/docs/view/LoadingWrapper.tsx @@ -1,6 +1,6 @@ import { ReactNode } from "react" import { Box } from "@mui/material" -import LoadingIndicator from "@/common/loading/LoadingIndicator" +import LoadingIndicator from "@/common/ui/LoadingIndicator" const LoadingWrapper = ({ showLoadingIndicator, diff --git a/src/features/projects/data/index.ts b/src/features/projects/data/index.ts index 8155c986..affecff1 100644 --- a/src/features/projects/data/index.ts +++ b/src/features/projects/data/index.ts @@ -1,5 +1,6 @@ export { default as GitHubProjectDataSource } from "./GitHubProjectDataSource" export * from "./GitHubProjectDataSource" export { default as useProjects } from "./useProjects" +export { default as useProjectSelection } from "./useProjectSelection" export { default as GitHubLoginDataSource } from "./GitHubLoginDataSource" export { default as GitHubRepositoryDataSource } from "./GitHubRepositoryDataSource" diff --git a/src/features/projects/data/useProjectSelection.ts b/src/features/projects/data/useProjectSelection.ts new file mode 100644 index 00000000..05b4cca4 --- /dev/null +++ b/src/features/projects/data/useProjectSelection.ts @@ -0,0 +1,71 @@ +"use client" + +import { useRouter, usePathname } from "next/navigation" +import { useContext } from "react" +import useMediaQuery from "@mui/material/useMediaQuery" +import { useTheme } from "@mui/material/styles" +import { ProjectsContainerContext } from "@/common" +import { Project, ProjectNavigator, getProjectSelectionFromPath } from "../domain" +import { useSidebarOpen } from "@/features/sidebar/data" + +export default function useProjectSelection() { + const router = useRouter() + const pathname = usePathname() + const { projects } = useContext(ProjectsContainerContext) + const selection = getProjectSelectionFromPath({ projects, path: pathname }) + const pathnameReader = { + get pathname() { + return pathname + } + } + const projectNavigator = new ProjectNavigator({ router, pathnameReader }) + const [_isSidebarOpen, setSidebarOpen] = useSidebarOpen() + const theme = useTheme() + const isDesktopLayout = useMediaQuery(theme.breakpoints.up("sm")) + return { + get project() { + return selection.project + }, + get version() { + return selection.version + }, + get specification() { + return selection.specification + }, + selectProject: (project: Project) => { + if (!isDesktopLayout) { + setSidebarOpen(false) + } + const version = project.versions[0] + const specification = version.specifications[0] + projectNavigator.navigate( + project.owner, + project.name, + version.id, + specification.id + ) + }, + selectVersion: (versionId: string) => { + projectNavigator.navigateToVersion( + selection.project!, + versionId, + selection.specification!.name + ) + }, + selectSpecification: (specificationId: string) => { + projectNavigator.navigate( + selection.project!.owner, + selection.project!.name, + selection.version!.id, specificationId + ) + }, + navigateToSelectionIfNeeded: () => { + projectNavigator.navigateIfNeeded({ + projectOwner: selection.project?.owner, + projectName: selection.project?.name, + versionId: selection.version?.id, + specificationId: selection.specification?.id + }) + } + } +} diff --git a/src/features/projects/domain/getSelection.ts b/src/features/projects/domain/getProjectSelectionFromPath.ts similarity index 98% rename from src/features/projects/domain/getSelection.ts rename to src/features/projects/domain/getProjectSelectionFromPath.ts index 087dc6f9..e98307eb 100644 --- a/src/features/projects/domain/getSelection.ts +++ b/src/features/projects/domain/getProjectSelectionFromPath.ts @@ -2,7 +2,7 @@ import Project from "./Project" import Version from "./Version" import OpenApiSpecification from "./OpenApiSpecification" -export default function getSelection({ +export default function getProjectSelectionFromPath({ projects, path }: { diff --git a/src/features/projects/domain/index.ts b/src/features/projects/domain/index.ts index 6a7a706a..2b3f10cf 100644 --- a/src/features/projects/domain/index.ts +++ b/src/features/projects/domain/index.ts @@ -1,6 +1,6 @@ export { default as CachingProjectDataSource } from "./CachingProjectDataSource" export { default as FilteringGitHubRepositoryDataSource } from "./FilteringGitHubRepositoryDataSource" -export { default as getSelection } from "./getSelection" +export { default as getProjectSelectionFromPath } from "./getProjectSelectionFromPath" export type { default as IGitHubLoginDataSource } from "./IGitHubLoginDataSource" export type { default as IGitHubRepositoryDataSource } from "./IGitHubRepositoryDataSource" export * from "./IGitHubRepositoryDataSource" @@ -16,4 +16,3 @@ export { default as ProjectNavigator } from "./ProjectNavigator" export { default as ProjectRepository } from "./ProjectRepository" export { default as updateWindowTitle } from "./updateWindowTitle" export type { default as Version } from "./Version" -export { default as useProjectNavigator } from "./useProjectNavigator" diff --git a/src/features/projects/domain/useProjectNavigator.ts b/src/features/projects/domain/useProjectNavigator.ts deleted file mode 100644 index ae2b5d43..00000000 --- a/src/features/projects/domain/useProjectNavigator.ts +++ /dev/null @@ -1,16 +0,0 @@ -"use client" -import { useRouter } from "next/navigation" -import ProjectNavigator from "./ProjectNavigator" - -export default function useProjectNavigator() { - const router = useRouter() - const pathnameReader = { - get pathname() { - if (typeof window === "undefined") { - return "" - } - return window.location.pathname - } - } - return new ProjectNavigator({ router, pathnameReader }) -} diff --git a/src/features/projects/view/DocumentationIframe.tsx b/src/features/projects/view/DocumentationIframe.tsx index 788601f4..64cfac3a 100644 --- a/src/features/projects/view/DocumentationIframe.tsx +++ b/src/features/projects/view/DocumentationIframe.tsx @@ -1,5 +1,5 @@ import { Box } from "@mui/material" -import DelayedLoadingIndicator from "@/common/loading/DelayedLoadingIndicator" +import DelayedLoadingIndicator from "@/common/ui/DelayedLoadingIndicator" import { DocumentationVisualizer } from "@/features/settings/domain" const DocumentationIframe = ({ diff --git a/src/features/projects/view/MainContent.tsx b/src/features/projects/view/MainContent.tsx deleted file mode 100644 index df22698d..00000000 --- a/src/features/projects/view/MainContent.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Project, Version, OpenApiSpecification } from "../domain" -import DelayedLoadingIndicator from "@/common/loading/DelayedLoadingIndicator" -import ErrorMessage from "./ErrorMessage" -import Documentation from "./Documentation" - -const MainContent = ({ - isLoading, - error, - project, - version, - specification -}: { - isLoading: boolean, - error?: Error, - project?: Project, - version?: Version, - specification?: OpenApiSpecification -}) => { - if (project && version && specification) { - return - } else if (isLoading) { - return - } else if (error) { - return - } else if (!project) { - return - } else if (!version) { - return - } else { - return - } -} - -export default MainContent \ No newline at end of file diff --git a/src/features/projects/view/ProjectsPage.tsx b/src/features/projects/view/ProjectsPage.tsx deleted file mode 100644 index 298920de..00000000 --- a/src/features/projects/view/ProjectsPage.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ProjectRepository } from "../domain" -import ClientProjectsPage from "./client/ProjectsPage" - -export default async function ProjectsPage({ - projectRepository, - path -}: { - projectRepository: ProjectRepository - path: string -}) { - const projects = await projectRepository.get() - return ( - - ) -} diff --git a/src/features/projects/view/ServerSideCachedProjectsProvider.tsx b/src/features/projects/view/ServerSideCachedProjectsProvider.tsx new file mode 100644 index 00000000..810a6633 --- /dev/null +++ b/src/features/projects/view/ServerSideCachedProjectsProvider.tsx @@ -0,0 +1,20 @@ +"use client" + +import { Project } from "../domain" +import { ServerSideCachedProjectsContext } from "@/common" + +const ServerSideCachedProjectsProvider = ({ + projects, + children +}: { + projects: Project[] | undefined + children: React.ReactNode +}) => { + return ( + + {children} + + ) +} + +export default ServerSideCachedProjectsProvider diff --git a/src/features/projects/view/client/ProjectsPage.tsx b/src/features/projects/view/client/ProjectsPage.tsx deleted file mode 100644 index efd27800..00000000 --- a/src/features/projects/view/client/ProjectsPage.tsx +++ /dev/null @@ -1,125 +0,0 @@ -"use client" - -import { useEffect } from "react" -import { useTheme } from "@mui/material/styles" -import useMediaQuery from "@mui/material/useMediaQuery" -import SidebarContainer from "@/features/sidebar/view/client/SidebarContainer" -import { useProjects } from "../../data" -import ProjectList from "../ProjectList" -import MainContent from "../MainContent" -import MobileToolbar from "../toolbar/MobileToolbar" -import TrailingToolbarItem from "../toolbar/TrailingToolbarItem" -import useSidebarOpen from "@/common/state/useSidebarOpen" -import { - Project, - getSelection, - updateWindowTitle, - useProjectNavigator -} from "../../domain" - -export default function ProjectsPage({ - projects: serverProjects, - path -}: { - projects?: Project[] - path: string -}) { - const theme = useTheme() - const projectNavigator = useProjectNavigator() - const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() - const isDesktopLayout = useMediaQuery(theme.breakpoints.up("sm")) - const { projects: clientProjects, error, isLoading: isClientLoading } = useProjects() - const projects = isClientLoading ? (serverProjects || []) : clientProjects - const { project, version, specification } = getSelection({ projects, path }) - const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE || "" - useEffect(() => { - updateWindowTitle({ - storage: document, - defaultTitle: siteName, - project, - version, - specification - }) - }, [project, version, specification, siteName]) - useEffect(() => { - // Ensure the URL reflects the current selection of project, version, and specification. - projectNavigator.navigateIfNeeded({ - projectOwner: project?.owner, - projectName: project?.name, - versionId: version?.id, - specificationId: specification?.id - }) - }, [projectNavigator, project, version, specification]) - useEffect(() => { - // Show the sidebar if no project is selected. - if (project === undefined) { - setSidebarOpen(true) - } - }, [project, setSidebarOpen]) - const selectProject = (project: Project) => { - if (!isDesktopLayout) { - setSidebarOpen(false) - } - const version = project.versions[0] - const specification = version.specifications[0] - projectNavigator.navigate(project.owner, project.name, version.id, specification.id) - } - const selectVersion = (versionId: string) => { - projectNavigator.navigateToVersion(project!, versionId, specification!.name) - } - const selectSpecification = (specificationId: string) => { - projectNavigator.navigate(project!.owner, project!.name, version!.id, specificationId) - } - const canCloseSidebar = project !== undefined - const toggleSidebar = (isOpen: boolean) => { - if (!isOpen && canCloseSidebar) { - setSidebarOpen(false) - } else if (isOpen) { - setSidebarOpen(true) - } - } - return ( - - } - toolbarTrailingItem={project && version && specification && - - } - mobileToolbar={project && version && specification && - - } - > - {/* If the user has not selected any project then we do not render any content */} - {project && - - } - - ) -} \ No newline at end of file diff --git a/src/features/sidebar/data/index.ts b/src/features/sidebar/data/index.ts new file mode 100644 index 00000000..c0168b3a --- /dev/null +++ b/src/features/sidebar/data/index.ts @@ -0,0 +1 @@ +export { default as useSidebarOpen } from "./useSidebarOpen" diff --git a/src/common/state/useSidebarOpen.tsx b/src/features/sidebar/data/useSidebarOpen.ts similarity index 65% rename from src/common/state/useSidebarOpen.tsx rename to src/features/sidebar/data/useSidebarOpen.ts index 2d0c3b9e..3d4c1be7 100644 --- a/src/common/state/useSidebarOpen.tsx +++ b/src/features/sidebar/data/useSidebarOpen.ts @@ -1,5 +1,5 @@ import { useSessionStorage } from "usehooks-ts" export default function useSidebarOpen() { - return useSessionStorage("isSidebarOpen", true) + return useSessionStorage("isSidebarOpen", true) } \ No newline at end of file diff --git a/src/features/sidebar/view/Sidebar.tsx b/src/features/sidebar/view/Sidebar.tsx index d8873876..83cc401c 100644 --- a/src/features/sidebar/view/Sidebar.tsx +++ b/src/features/sidebar/view/Sidebar.tsx @@ -1,18 +1,19 @@ -import { ReactNode } from "react" import { Box } from "@mui/material" -import SidebarHeader from "./SidebarHeader" -import UserFooter from "@/features/user/view/UserFooter" +import Header from "./internal/sidebar-content/Header" +import UserButton from "./internal/sidebar-content/user/UserButton" +import SettingsList from "./internal/sidebar-content/settings/SettingsList" +import ProjectList from "./internal/sidebar-content/projects/ProjectList" -const Sidebar = ({ children }: { children: ReactNode }) => { - return ( - <> - - - {children} - - - - ) +const Sidebar = () => { + return <> +
+ + + + + + + } export default Sidebar diff --git a/src/features/sidebar/view/SplitView.tsx b/src/features/sidebar/view/SplitView.tsx new file mode 100644 index 00000000..2c4e183e --- /dev/null +++ b/src/features/sidebar/view/SplitView.tsx @@ -0,0 +1,48 @@ +"use client" + +import { useEffect } from "react" +import { Stack } from "@mui/material" +import PrimaryContainer from "./internal/PrimaryContainer" +import SecondaryContainer from "./internal/SecondaryContainer" +import { useProjectSelection } from "@/features/projects/data" +import { useSidebarOpen } from "../data" +import Sidebar from "./Sidebar" + +const SplitView = ({ children }: { children?: React.ReactNode }) => { + const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() + const { project } = useProjectSelection() + const sidebarWidth = 320 + useEffect(() => { + // Show the sidebar if no project is selected. + if (project === undefined) { + setSidebarOpen(true) + } + }, [project, setSidebarOpen]) + return ( + + setSidebarOpen(false)} + > + + + + {children} + + {/* + {header} +
+ {children} +
+
*/} +
+ ) +} + +// Disable server-side rendering as this component uses the window instance to manage its state. +// export default dynamic(() => Promise.resolve(SidebarContainer), { +// ssr: false +// }) + +export default SplitView diff --git a/src/features/sidebar/view/index.ts b/src/features/sidebar/view/index.ts new file mode 100644 index 00000000..49802603 --- /dev/null +++ b/src/features/sidebar/view/index.ts @@ -0,0 +1,2 @@ +export { default as Sidebar } from "./Sidebar" +export { default as SplitView } from "./SplitView" diff --git a/src/features/sidebar/view/internal/PrimaryContainer.tsx b/src/features/sidebar/view/internal/PrimaryContainer.tsx new file mode 100644 index 00000000..04ca276c --- /dev/null +++ b/src/features/sidebar/view/internal/PrimaryContainer.tsx @@ -0,0 +1,82 @@ +import { ReactNode } from "react" +import { SxProps } from "@mui/system" +import { Drawer as MuiDrawer } from "@mui/material" + +const PrimaryContainer = ({ + width, + isOpen, + onClose, + children +}: { + width: number + isOpen: boolean + onClose?: () => void + children?: ReactNode +}) => { + return ( + <> + <_PrimaryContainer + variant="temporary" + width={width} + isOpen={isOpen} + onClose={onClose} + keepMounted={true} + sx={{ display: { xs: "block", sm: "none" } }} + > + {children} + + <_PrimaryContainer + variant="persistent" + width={width} + isOpen={isOpen} + keepMounted={false} + sx={{ display: { xs: "none", sm: "block" } }} + > + {children} + + + ) +} + +export default PrimaryContainer + +const _PrimaryContainer = ({ + variant, + width, + isOpen, + onClose, + keepMounted, + sx, + children +}: { + variant: "persistent" | "temporary", + width: number + isOpen: boolean + onClose?: () => void + keepMounted?: boolean + sx: SxProps, + children?: ReactNode +}) => { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/features/sidebar/view/internal/SecondaryContainer.tsx b/src/features/sidebar/view/internal/SecondaryContainer.tsx new file mode 100644 index 00000000..5a892bb8 --- /dev/null +++ b/src/features/sidebar/view/internal/SecondaryContainer.tsx @@ -0,0 +1,82 @@ +import { ReactNode } from "react" +import { SxProps } from "@mui/system" +import { Stack } from "@mui/material" +import { styled } from "@mui/material/styles" + +const SecondaryContainer = ({ + sidebarWidth, + offsetContent, + children +}: { + sidebarWidth: number + offsetContent: boolean + children?: ReactNode +}) => { + const sx = { overflow: "hidden" } + return ( + <> + <_SecondaryContainer + sidebarWidth={0} + isSidebarOpen={false} + sx={{ ...sx, display: { xs: "flex", sm: "none" } }} + > + {children} + + <_SecondaryContainer + sidebarWidth={sidebarWidth} + isSidebarOpen={offsetContent} + sx={{ ...sx, display: { xs: "none", sm: "flex" } }} + > + {children} + + + ) +} + +export default SecondaryContainer + +interface WrapperStackProps { + readonly sidebarWidth: number + readonly isSidebarOpen: boolean +} + +const WrapperStack = styled(Stack, { + shouldForwardProp: (prop) => prop !== "isSidebarOpen" && prop !== "sidebarWidth" +})(({ theme, sidebarWidth, isSidebarOpen }) => ({ + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen + }), + marginLeft: `-${sidebarWidth}px`, + ...(isSidebarOpen && { + transition: theme.transitions.create("margin", { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + marginLeft: 0 + }) +})) + +const _SecondaryContainer = ({ + sidebarWidth, + isSidebarOpen, + children, + sx +}: { + sidebarWidth: number + isSidebarOpen: boolean + children: ReactNode + sx?: SxProps +}) => { + return ( + + {children} + + ) +} diff --git a/src/features/sidebar/view/internal/sidebar-content/Header.tsx b/src/features/sidebar/view/internal/sidebar-content/Header.tsx new file mode 100644 index 00000000..bcad2f45 --- /dev/null +++ b/src/features/sidebar/view/internal/sidebar-content/Header.tsx @@ -0,0 +1,55 @@ +import Image from "next/image" +import Link from "next/link" +import { Box, Typography, IconButton, Tooltip } from "@mui/material" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faPlus } from "@fortawesome/free-solid-svg-icons" + +const Header = () => { + const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE + return ( + + + {`${siteName} + + {siteName} + + + + + + + + + + + ) +} + +export default Header diff --git a/src/features/projects/view/ProjectAvatar.tsx b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatar.tsx similarity index 83% rename from src/features/projects/view/ProjectAvatar.tsx rename to src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatar.tsx index 2a6056d6..c55f7aac 100644 --- a/src/features/projects/view/ProjectAvatar.tsx +++ b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatar.tsx @@ -1,8 +1,8 @@ import { SxProps } from "@mui/system" import { Avatar, Box } from "@mui/material" import { alpha, useTheme } from "@mui/material/styles" -import { Project } from "../domain" -import ProjectAvatarSquircleClip from "./ProjectAvatarSquircleClip" +import { Project } from "@/features/projects/domain" +import ProjectAvatarSquircle from "./ProjectAvatarSquircle" function ProjectAvatar({ project, @@ -21,7 +21,7 @@ function ProjectAvatar({ height: height + borderRadius * 2, position: "relative" }}> - - + } - + ) } @@ -58,3 +58,4 @@ const PlaceholderAvatar = ({ text, sx }: { text: string, sx?: SxProps }) => { ) } + diff --git a/src/features/projects/view/ProjectAvatarSquircleClip.tsx b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatarSquircle.tsx similarity index 76% rename from src/features/projects/view/ProjectAvatarSquircleClip.tsx rename to src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatarSquircle.tsx index 0ac099c5..341a058c 100644 --- a/src/features/projects/view/ProjectAvatarSquircleClip.tsx +++ b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatarSquircle.tsx @@ -1,9 +1,8 @@ -import { ReactNode } from "react" import { SxProps } from "@mui/system" import { Box } from "@mui/material" import { getSvgPath } from "figma-squircle" -const ProjectAvatarSquircleClip = ({ +const ProjectAvatarSquircle = ({ width, height, children, @@ -11,7 +10,7 @@ const ProjectAvatarSquircleClip = ({ }: { width: number, height: number, - children?: ReactNode, + children?: React.ReactNode, sx?: SxProps }) => { const svgPath = getSvgPath({ @@ -27,4 +26,4 @@ const ProjectAvatarSquircleClip = ({ ) } -export default ProjectAvatarSquircleClip \ No newline at end of file +export default ProjectAvatarSquircle diff --git a/src/features/projects/view/ProjectList.tsx b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectList.tsx similarity index 68% rename from src/features/projects/view/ProjectList.tsx rename to src/features/sidebar/view/internal/sidebar-content/projects/ProjectList.tsx index 30fb06ad..3fb6e231 100644 --- a/src/features/projects/view/ProjectList.tsx +++ b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectList.tsx @@ -1,23 +1,13 @@ +import { useContext } from "react" import { List, Box, Typography } from "@mui/material" +import { ProjectsContainerContext } from "@/common" +import { useProjectSelection } from "@/features/projects/data" import ProjectListItem from "./ProjectListItem" import ProjectListItemPlaceholder from "./ProjectListItemPlaceholder" -import { Project } from "../domain" -interface ProjectListProps { - readonly isLoading: boolean - readonly projects: Project[] - readonly selectedProjectId?: string - readonly onSelectProject: (project: Project) => void -} - -const ProjectList = ( - { - isLoading, - projects, - selectedProjectId, - onSelectProject - }: ProjectListProps -) => { +const ProjectList = () => { + const { projects, isLoading } = useContext(ProjectsContainerContext) + const projectSelection = useProjectSelection() const loadingItemCount = 6 if (isLoading || projects.length > 0) { return ( @@ -31,8 +21,8 @@ const ProjectList = ( ))} diff --git a/src/features/projects/view/ProjectListItem.tsx b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItem.tsx similarity index 84% rename from src/features/projects/view/ProjectListItem.tsx rename to src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItem.tsx index c9472256..4c05ee6b 100644 --- a/src/features/projects/view/ProjectListItem.tsx +++ b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItem.tsx @@ -6,7 +6,7 @@ import { Typography } from "@mui/material" import MenuItemHover from "@/common/ui/MenuItemHover" -import { Project } from "../domain" +import { Project } from "@/features/projects/domain" import ProjectAvatar from "./ProjectAvatar" const ProjectListItem = ({ @@ -35,7 +35,10 @@ const ProjectListItem = ({ style={{ fontSize: "1.1em", fontWeight: isSelected ? 800 : 500, - letterSpacing: 0.3 + letterSpacing: 0.3, + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis" }} > {project.displayName} diff --git a/src/features/projects/view/ProjectListItemPlaceholder.tsx b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItemPlaceholder.tsx similarity index 78% rename from src/features/projects/view/ProjectListItemPlaceholder.tsx rename to src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItemPlaceholder.tsx index d5fcde1f..77de983a 100644 --- a/src/features/projects/view/ProjectListItemPlaceholder.tsx +++ b/src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItemPlaceholder.tsx @@ -1,20 +1,20 @@ import { ListItem, ListItemText, Stack, Skeleton } from "@mui/material" import MenuItemHover from "@/common/ui/MenuItemHover" -import ProjectAvatarSquircleClip from "./ProjectAvatarSquircleClip" +import ProjectAvatarSquircle from "./ProjectAvatarSquircle" const ProjectListItemPlaceholder = () => { return ( - { animation="wave" sx={{ width: 42, height: 42 }} /> - + { + const { data: session, status } = useSession() + const isLoading = status == "loading" + return ( + + + {!isLoading && session && + + {children} + + } + {isLoading && } + + + ) +} -const UserButton = ({ session }: { session: Session }) => { +export default UserButton + +const UserButtonWithSession = ({ + session, + children +}: { + session: Session + children?: ReactNode +}) => { const [popoverAnchorElement, setPopoverAnchorElement] = useState(null) const handlePopoverClick = (event: React.MouseEvent) => { setPopoverAnchorElement(event.currentTarget) @@ -40,7 +70,7 @@ const UserButton = ({ session }: { session: Session }) => { horizontal: "left" }} > - + {children} { ) } - -export default UserButton diff --git a/src/features/user/view/UserSkeleton.tsx b/src/features/sidebar/view/internal/sidebar-content/user/UserSkeleton.tsx similarity index 100% rename from src/features/user/view/UserSkeleton.tsx rename to src/features/sidebar/view/internal/sidebar-content/user/UserSkeleton.tsx diff --git a/src/features/user/view/UserFooter.tsx b/src/features/user/view/UserFooter.tsx deleted file mode 100644 index a5b750c6..00000000 --- a/src/features/user/view/UserFooter.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useSession } from "next-auth/react" -import { List, ListItem } from "@mui/material" -import UserButton from "./UserButton" -import UserSkeleton from "./UserSkeleton" - -const UserFooter = () => { - const { data: session, status } = useSession() - const isLoading = status == "loading" - return ( - - - {!isLoading && session && } - {isLoading && } - - - ) -} - -export default UserFooter From af44f64551b8af352e2c88c17d9916a567197109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 12:40:53 +0200 Subject: [PATCH 38/76] Removes _old_sidebar --- src/features/_old_sidebar/view/Sidebar.tsx | 18 ---- .../_old_sidebar/view/SidebarHeader.tsx | 53 ------------ .../_old_sidebar/view/TrailingToolbar.tsx | 21 ----- .../_old_sidebar/view/base/Drawer.tsx | 44 ---------- .../view/base/SecondaryHeader.tsx | 85 ------------------- .../view/base/SecondaryWrapper.tsx | 50 ----------- .../view/base/responsive/Drawer.tsx | 38 --------- .../view/base/responsive/SecondaryHeader.tsx | 70 --------------- .../view/base/responsive/SecondaryWrapper.tsx | 32 ------- .../view/base/responsive/SidebarContainer.tsx | 39 --------- .../view/client/SidebarContainer.tsx | 58 ------------- 11 files changed, 508 deletions(-) delete mode 100644 src/features/_old_sidebar/view/Sidebar.tsx delete mode 100644 src/features/_old_sidebar/view/SidebarHeader.tsx delete mode 100644 src/features/_old_sidebar/view/TrailingToolbar.tsx delete mode 100644 src/features/_old_sidebar/view/base/Drawer.tsx delete mode 100644 src/features/_old_sidebar/view/base/SecondaryHeader.tsx delete mode 100644 src/features/_old_sidebar/view/base/SecondaryWrapper.tsx delete mode 100644 src/features/_old_sidebar/view/base/responsive/Drawer.tsx delete mode 100644 src/features/_old_sidebar/view/base/responsive/SecondaryHeader.tsx delete mode 100644 src/features/_old_sidebar/view/base/responsive/SecondaryWrapper.tsx delete mode 100644 src/features/_old_sidebar/view/base/responsive/SidebarContainer.tsx delete mode 100644 src/features/_old_sidebar/view/client/SidebarContainer.tsx diff --git a/src/features/_old_sidebar/view/Sidebar.tsx b/src/features/_old_sidebar/view/Sidebar.tsx deleted file mode 100644 index d8873876..00000000 --- a/src/features/_old_sidebar/view/Sidebar.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { ReactNode } from "react" -import { Box } from "@mui/material" -import SidebarHeader from "./SidebarHeader" -import UserFooter from "@/features/user/view/UserFooter" - -const Sidebar = ({ children }: { children: ReactNode }) => { - return ( - <> - - - {children} - - - - ) -} - -export default Sidebar diff --git a/src/features/_old_sidebar/view/SidebarHeader.tsx b/src/features/_old_sidebar/view/SidebarHeader.tsx deleted file mode 100644 index fbe92a7b..00000000 --- a/src/features/_old_sidebar/view/SidebarHeader.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import Image from "next/image" -import Link from "next/link" -import { Box, Typography, IconButton, Tooltip } from "@mui/material" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faPlus } from "@fortawesome/free-solid-svg-icons" - -export default function SidebarHeader() { - const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE - return ( - - - {`${siteName} - - {siteName} - - - - - - - - - - - ) -} diff --git a/src/features/_old_sidebar/view/TrailingToolbar.tsx b/src/features/_old_sidebar/view/TrailingToolbar.tsx deleted file mode 100644 index 1ab9f95c..00000000 --- a/src/features/_old_sidebar/view/TrailingToolbar.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { ReactNode } from "react" -import { Box } from "@mui/material" -import { useTheme } from "@mui/material/styles" - -export default function TrailingToolbar({ - children -}: { - children?: ReactNode -}) { - const theme = useTheme() - return ( - - {children} - - ) -} diff --git a/src/features/_old_sidebar/view/base/Drawer.tsx b/src/features/_old_sidebar/view/base/Drawer.tsx deleted file mode 100644 index 5a335588..00000000 --- a/src/features/_old_sidebar/view/base/Drawer.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { ReactNode } from "react" -import { SxProps } from "@mui/system" -import { Drawer as MuiDrawer } from "@mui/material" - -export default function Drawer({ - variant, - width, - isOpen, - onClose, - keepMounted, - sx, - children -}: { - variant: "persistent" | "temporary", - width: number - isOpen: boolean - onClose?: () => void - keepMounted?: boolean - sx: SxProps, - children?: ReactNode -}) { - return ( - - {children} - - ) -} \ No newline at end of file diff --git a/src/features/_old_sidebar/view/base/SecondaryHeader.tsx b/src/features/_old_sidebar/view/base/SecondaryHeader.tsx deleted file mode 100644 index ea60cb94..00000000 --- a/src/features/_old_sidebar/view/base/SecondaryHeader.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { ReactNode } from "react" -import { SxProps } from "@mui/system" -import { Box, Divider, IconButton, Tooltip } from "@mui/material" -import { useTheme } from "@mui/material/styles" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons" -import { isMac, useKeyboardShortcut } from "@/common" - -export default function SecondaryHeader({ - showOpenSidebar, - showCloseSidebar, - onToggleSidebarOpen, - trailingItem, - children, - sx -}: { - showOpenSidebar: boolean - showCloseSidebar: boolean - onToggleSidebarOpen: (isOpen: boolean) => void - trailingItem?: ReactNode - children?: ReactNode - sx?: SxProps -}) { - useKeyboardShortcut(event => { - const isActionKey = isMac() ? event.metaKey : event.ctrlKey - if (isActionKey && event.key === ".") { - event.preventDefault() - if (showOpenSidebar) { - onToggleSidebarOpen(true) - } else if (showCloseSidebar) { - onToggleSidebarOpen(false) - } - } - }, [showOpenSidebar, showCloseSidebar, onToggleSidebarOpen]) - const openCloseShortcutString = isMac() ? " (⌘ + .)" : " (^ + .)" - const theme = useTheme() - return ( - - - {showOpenSidebar && - - onToggleSidebarOpen(true)} - edge="start" - > - - - - } - {showCloseSidebar && - - onToggleSidebarOpen(false)} - edge="start" - > - - - - } - - {trailingItem} - - - {children} - - - ) -} diff --git a/src/features/_old_sidebar/view/base/SecondaryWrapper.tsx b/src/features/_old_sidebar/view/base/SecondaryWrapper.tsx deleted file mode 100644 index fef93718..00000000 --- a/src/features/_old_sidebar/view/base/SecondaryWrapper.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { ReactNode } from "react" -import { SxProps } from "@mui/system" -import { Stack } from "@mui/material" -import { styled } from "@mui/material/styles" - -interface WrapperStackProps { - sidebarWidth: number - isSidebarOpen: boolean -} - -const WrapperStack = styled(Stack, { - shouldForwardProp: (prop) => prop !== "isSidebarOpen" -})(({ theme, sidebarWidth, isSidebarOpen }) => ({ - transition: theme.transitions.create("margin", { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.leavingScreen - }), - marginLeft: `-${sidebarWidth}px`, - ...(isSidebarOpen && { - transition: theme.transitions.create("margin", { - easing: theme.transitions.easing.easeOut, - duration: theme.transitions.duration.enteringScreen, - }), - marginLeft: 0 - }) -})) - -export default function SecondaryWrapper({ - sidebarWidth, - isSidebarOpen, - children, - sx -}: { - sidebarWidth: number - isSidebarOpen: boolean - children: ReactNode - sx?: SxProps -}) { - return ( - - {children} - - ) -} diff --git a/src/features/_old_sidebar/view/base/responsive/Drawer.tsx b/src/features/_old_sidebar/view/base/responsive/Drawer.tsx deleted file mode 100644 index 8a027f77..00000000 --- a/src/features/_old_sidebar/view/base/responsive/Drawer.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ReactNode } from "react" -import Drawer from "../Drawer" - -export default function RespnsiveDrawer({ - width, - isOpen, - onClose, - children -}: { - width: number - isOpen: boolean - onClose?: () => void - children?: ReactNode -}) { - return ( - <> - - {children} - - - {children} - - - ) -} \ No newline at end of file diff --git a/src/features/_old_sidebar/view/base/responsive/SecondaryHeader.tsx b/src/features/_old_sidebar/view/base/responsive/SecondaryHeader.tsx deleted file mode 100644 index f48dd778..00000000 --- a/src/features/_old_sidebar/view/base/responsive/SecondaryHeader.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { ReactNode } from "react" -import { SxProps } from "@mui/system" -import { Box, IconButton, Stack, Collapse } from "@mui/material" -import SecondaryHeader from "../SecondaryHeader" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faChevronDown } from "@fortawesome/free-solid-svg-icons" - -export default function ResponsiveSecondaryHeader({ - showOpenSidebar, - showCloseSidebar, - onToggleSidebarOpen, - showMobileToolbar, - onToggleMobileToolbar, - trailingItem, - mobileToolbar, - sx -}: { - showOpenSidebar: boolean - showCloseSidebar: boolean - onToggleSidebarOpen: (isOpen: boolean) => void - showMobileToolbar: boolean - onToggleMobileToolbar: (showMobileToolbar: boolean) => void - trailingItem?: ReactNode - mobileToolbar?: ReactNode - sx?: SxProps -}) { - return ( - - {trailingItem} - {mobileToolbar && - onToggleMobileToolbar(!showMobileToolbar) } - sx={{ display: { sm: "flex", md: "none" } }} - > - - - } - - } - > - {mobileToolbar && - - - {mobileToolbar} - - - } - - ) -} diff --git a/src/features/_old_sidebar/view/base/responsive/SecondaryWrapper.tsx b/src/features/_old_sidebar/view/base/responsive/SecondaryWrapper.tsx deleted file mode 100644 index 171bf342..00000000 --- a/src/features/_old_sidebar/view/base/responsive/SecondaryWrapper.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { ReactNode } from "react" -import SecondaryWrapper from "../SecondaryWrapper" - -export default function ResponsiveSecondaryWrapper({ - sidebarWidth, - offsetContent, - children -}: { - sidebarWidth: number - offsetContent: boolean - children: ReactNode -}) { - const sx = { overflow: "hidden" } - return ( - <> - - {children} - - - {children} - - - ) -} \ No newline at end of file diff --git a/src/features/_old_sidebar/view/base/responsive/SidebarContainer.tsx b/src/features/_old_sidebar/view/base/responsive/SidebarContainer.tsx deleted file mode 100644 index 248660ab..00000000 --- a/src/features/_old_sidebar/view/base/responsive/SidebarContainer.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { ReactNode } from "react" -import { Stack } from "@mui/material" -import Drawer from "./Drawer" -import SecondaryWrapper from "./SecondaryWrapper" - -const SidebarContainer = ({ - isSidebarOpen, - onToggleSidebarOpen, - sidebar, - header, - children -}: { - isSidebarOpen: boolean, - onToggleSidebarOpen: (isSidebarOpen: boolean) => void - sidebar: ReactNode - header?: ReactNode - children?: ReactNode -}) => { - const sidebarWidth = 320 - return ( - - onToggleSidebarOpen(false)} - > - {sidebar} - - - {header} -
- {children} -
-
-
- ) -} - -export default SidebarContainer diff --git a/src/features/_old_sidebar/view/client/SidebarContainer.tsx b/src/features/_old_sidebar/view/client/SidebarContainer.tsx deleted file mode 100644 index ec7b6fc2..00000000 --- a/src/features/_old_sidebar/view/client/SidebarContainer.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" - -import dynamic from "next/dynamic" -import { ReactNode } from "react" -import { useSessionStorage } from "usehooks-ts" -import ResponsiveSidebarContainer from "../base/responsive/SidebarContainer" -import ResponsiveSecondaryHeader from "../base/responsive/SecondaryHeader" -import Sidebar from "../Sidebar" - -const SidebarContainer = ({ - isSidebarOpen, - onToggleSidebarOpen, - showHeader: _showHeader, - sidebar, - children, - toolbarTrailingItem, - mobileToolbar -}: { - isSidebarOpen: boolean, - onToggleSidebarOpen: (isSidebarOpen: boolean) => void, - showHeader?: boolean, - sidebar?: ReactNode - children?: ReactNode - toolbarTrailingItem?: ReactNode - mobileToolbar?: ReactNode -}) => { - const [showMobileToolbar, setShowMobileToolbar] = useSessionStorage("isMobileToolbarVisible", true) - const showHeader = _showHeader || _showHeader === undefined - return ( - - {sidebar} - - } - header={showHeader && - - } - > - {children} - - ) -} - -// Disable server-side rendering as this component uses the window instance to manage its state. -export default dynamic(() => Promise.resolve(SidebarContainer), { - ssr: false -}) From de45dd9d7f2d03df89a2222d2f7c58415923d808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 12:44:29 +0200 Subject: [PATCH 39/76] Fixes linting errors --- src/app/(authed)/(home)/[[...slug]]/page.tsx | 2 +- src/app/(authed)/(home)/layout.tsx | 2 +- src/app/(authed)/(home)/new/page.tsx | 3 +-- src/app/(authed)/layout.tsx | 3 +-- src/common/utils/splitOwnerAndRepository.ts | 4 +++- src/features/projects/data/useProjectSelection.ts | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/(authed)/(home)/[[...slug]]/page.tsx b/src/app/(authed)/(home)/[[...slug]]/page.tsx index 37a39ab2..b4afbb3d 100644 --- a/src/app/(authed)/(home)/[[...slug]]/page.tsx +++ b/src/app/(authed)/(home)/[[...slug]]/page.tsx @@ -14,7 +14,7 @@ export default function Page() { // Ensure the URL reflects the current selection of project, version, and specification. useEffect(() => { navigateToSelectionIfNeeded() - }, [project, version, specification]) + }, [project, version, specification, navigateToSelectionIfNeeded]) // Update the window title to match selected project. const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE || "" useEffect(() => { diff --git a/src/app/(authed)/(home)/layout.tsx b/src/app/(authed)/(home)/layout.tsx index 92ad9381..fba8e84c 100644 --- a/src/app/(authed)/(home)/layout.tsx +++ b/src/app/(authed)/(home)/layout.tsx @@ -16,7 +16,7 @@ export default function Layout({ children }: { children: React.ReactNode }) { const { projects, error, isLoading } = useProjects() // Update projects provided to child components, using cached projects from the server if needed. const serverSideCachedProjects = useContext(ServerSideCachedProjectsContext) - let newProjectsContainer = { projects, error, isLoading } + const newProjectsContainer = { projects, error, isLoading } if (isLoading && serverSideCachedProjects) { newProjectsContainer.isLoading = false newProjectsContainer.projects = serverSideCachedProjects diff --git a/src/app/(authed)/(home)/new/page.tsx b/src/app/(authed)/(home)/new/page.tsx index 831a9396..bdbbd356 100644 --- a/src/app/(authed)/(home)/new/page.tsx +++ b/src/app/(authed)/(home)/new/page.tsx @@ -1,6 +1,5 @@ import Link from "next/link" -import { splitOwnerAndRepository } from "@/common" -import { env } from "@/common" +import { env, splitOwnerAndRepository } from "@/common" const Page = () => { const repositoryNameSuffix = env.getOrThrow("REPOSITORY_NAME_SUFFIX") diff --git a/src/app/(authed)/layout.tsx b/src/app/(authed)/layout.tsx index 03f37d87..bc977cf5 100644 --- a/src/app/(authed)/layout.tsx +++ b/src/app/(authed)/layout.tsx @@ -1,9 +1,8 @@ import { redirect } from "next/navigation" import { SessionProvider } from "next-auth/react" -import { session } from "@/composition" +import { session, projectRepository } from "@/composition" import ErrorHandler from "@/common/ui/ErrorHandler" import SessionBarrier from "@/features/auth/view/SessionBarrier" -import { projectRepository } from "@/composition" import ServerSideCachedProjectsProvider from "@/features/projects/view/ServerSideCachedProjectsProvider" export default async function Layout({ children }: { children: React.ReactNode }) { diff --git a/src/common/utils/splitOwnerAndRepository.ts b/src/common/utils/splitOwnerAndRepository.ts index 3a4f80f4..4bd3a96a 100644 --- a/src/common/utils/splitOwnerAndRepository.ts +++ b/src/common/utils/splitOwnerAndRepository.ts @@ -1,6 +1,6 @@ // Split full repository names into owner and repository. // shapehq/foo becomes { owner: "shapehq", "repository": "foo" } -export default (str: string) => { +const splitOwnerAndRepository = (str: string) => { const index = str.indexOf("/") if (index === -1) { return undefined @@ -12,3 +12,5 @@ export default (str: string) => { } return { owner, repository } } + +export default splitOwnerAndRepository diff --git a/src/features/projects/data/useProjectSelection.ts b/src/features/projects/data/useProjectSelection.ts index 05b4cca4..6c9229e1 100644 --- a/src/features/projects/data/useProjectSelection.ts +++ b/src/features/projects/data/useProjectSelection.ts @@ -19,7 +19,7 @@ export default function useProjectSelection() { } } const projectNavigator = new ProjectNavigator({ router, pathnameReader }) - const [_isSidebarOpen, setSidebarOpen] = useSidebarOpen() + const [, setSidebarOpen] = useSidebarOpen() const theme = useTheme() const isDesktopLayout = useMediaQuery(theme.breakpoints.up("sm")) return { From e503107151f13b35abbfc974ab7c71460fa77d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 12:53:02 +0200 Subject: [PATCH 40/76] Moves sidebar types to internal --- src/features/sidebar/view/Sidebar.tsx | 19 ------------------- src/features/sidebar/view/SplitView.tsx | 6 +++--- src/features/sidebar/view/index.ts | 1 - .../Container.tsx} | 0 .../Container.tsx} | 0 .../{sidebar-content => sidebar}/Header.tsx | 0 .../sidebar/view/internal/sidebar/Sidebar.tsx | 19 +++++++++++++++++++ .../projects/ProjectAvatar.tsx | 0 .../projects/ProjectAvatarSquircle.tsx | 0 .../projects/ProjectList.tsx | 0 .../projects/ProjectListItem.tsx | 0 .../projects/ProjectListItemPlaceholder.tsx | 0 .../DocumentationVisualizationPicker.tsx | 0 .../settings/SettingsList.tsx | 0 .../user/UserButton.tsx | 0 .../user/UserSkeleton.tsx | 0 16 files changed, 22 insertions(+), 23 deletions(-) delete mode 100644 src/features/sidebar/view/Sidebar.tsx rename src/features/sidebar/view/internal/{PrimaryContainer.tsx => primary/Container.tsx} (100%) rename src/features/sidebar/view/internal/{SecondaryContainer.tsx => secondary/Container.tsx} (100%) rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/Header.tsx (100%) create mode 100644 src/features/sidebar/view/internal/sidebar/Sidebar.tsx rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/projects/ProjectAvatar.tsx (100%) rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/projects/ProjectAvatarSquircle.tsx (100%) rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/projects/ProjectList.tsx (100%) rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/projects/ProjectListItem.tsx (100%) rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/projects/ProjectListItemPlaceholder.tsx (100%) rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/settings/DocumentationVisualizationPicker.tsx (100%) rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/settings/SettingsList.tsx (100%) rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/user/UserButton.tsx (100%) rename src/features/sidebar/view/internal/{sidebar-content => sidebar}/user/UserSkeleton.tsx (100%) diff --git a/src/features/sidebar/view/Sidebar.tsx b/src/features/sidebar/view/Sidebar.tsx deleted file mode 100644 index 83cc401c..00000000 --- a/src/features/sidebar/view/Sidebar.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Box } from "@mui/material" -import Header from "./internal/sidebar-content/Header" -import UserButton from "./internal/sidebar-content/user/UserButton" -import SettingsList from "./internal/sidebar-content/settings/SettingsList" -import ProjectList from "./internal/sidebar-content/projects/ProjectList" - -const Sidebar = () => { - return <> -
- - - - - - - -} - -export default Sidebar diff --git a/src/features/sidebar/view/SplitView.tsx b/src/features/sidebar/view/SplitView.tsx index 2c4e183e..7456b87c 100644 --- a/src/features/sidebar/view/SplitView.tsx +++ b/src/features/sidebar/view/SplitView.tsx @@ -2,11 +2,11 @@ import { useEffect } from "react" import { Stack } from "@mui/material" -import PrimaryContainer from "./internal/PrimaryContainer" -import SecondaryContainer from "./internal/SecondaryContainer" import { useProjectSelection } from "@/features/projects/data" +import PrimaryContainer from "./internal/primary/Container" +import SecondaryContainer from "./internal/secondary/Container" +import Sidebar from "./internal/sidebar/Sidebar" import { useSidebarOpen } from "../data" -import Sidebar from "./Sidebar" const SplitView = ({ children }: { children?: React.ReactNode }) => { const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() diff --git a/src/features/sidebar/view/index.ts b/src/features/sidebar/view/index.ts index 49802603..47482d01 100644 --- a/src/features/sidebar/view/index.ts +++ b/src/features/sidebar/view/index.ts @@ -1,2 +1 @@ -export { default as Sidebar } from "./Sidebar" export { default as SplitView } from "./SplitView" diff --git a/src/features/sidebar/view/internal/PrimaryContainer.tsx b/src/features/sidebar/view/internal/primary/Container.tsx similarity index 100% rename from src/features/sidebar/view/internal/PrimaryContainer.tsx rename to src/features/sidebar/view/internal/primary/Container.tsx diff --git a/src/features/sidebar/view/internal/SecondaryContainer.tsx b/src/features/sidebar/view/internal/secondary/Container.tsx similarity index 100% rename from src/features/sidebar/view/internal/SecondaryContainer.tsx rename to src/features/sidebar/view/internal/secondary/Container.tsx diff --git a/src/features/sidebar/view/internal/sidebar-content/Header.tsx b/src/features/sidebar/view/internal/sidebar/Header.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/Header.tsx rename to src/features/sidebar/view/internal/sidebar/Header.tsx diff --git a/src/features/sidebar/view/internal/sidebar/Sidebar.tsx b/src/features/sidebar/view/internal/sidebar/Sidebar.tsx new file mode 100644 index 00000000..fb08521b --- /dev/null +++ b/src/features/sidebar/view/internal/sidebar/Sidebar.tsx @@ -0,0 +1,19 @@ +import { Box } from "@mui/material" +import Header from "./Header" +import UserButton from "./user/UserButton" +import SettingsList from "./settings/SettingsList" +import ProjectList from "./projects/ProjectList" + +const Sidebar = () => { + return <> +
+ + + + + + + +} + +export default Sidebar diff --git a/src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatar.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectAvatar.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatar.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectAvatar.tsx diff --git a/src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatarSquircle.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectAvatarSquircle.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/projects/ProjectAvatarSquircle.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectAvatarSquircle.tsx diff --git a/src/features/sidebar/view/internal/sidebar-content/projects/ProjectList.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectList.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/projects/ProjectList.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectList.tsx diff --git a/src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItem.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectListItem.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItem.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectListItem.tsx diff --git a/src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItemPlaceholder.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectListItemPlaceholder.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/projects/ProjectListItemPlaceholder.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectListItemPlaceholder.tsx diff --git a/src/features/sidebar/view/internal/sidebar-content/settings/DocumentationVisualizationPicker.tsx b/src/features/sidebar/view/internal/sidebar/settings/DocumentationVisualizationPicker.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/settings/DocumentationVisualizationPicker.tsx rename to src/features/sidebar/view/internal/sidebar/settings/DocumentationVisualizationPicker.tsx diff --git a/src/features/sidebar/view/internal/sidebar-content/settings/SettingsList.tsx b/src/features/sidebar/view/internal/sidebar/settings/SettingsList.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/settings/SettingsList.tsx rename to src/features/sidebar/view/internal/sidebar/settings/SettingsList.tsx diff --git a/src/features/sidebar/view/internal/sidebar-content/user/UserButton.tsx b/src/features/sidebar/view/internal/sidebar/user/UserButton.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/user/UserButton.tsx rename to src/features/sidebar/view/internal/sidebar/user/UserButton.tsx diff --git a/src/features/sidebar/view/internal/sidebar-content/user/UserSkeleton.tsx b/src/features/sidebar/view/internal/sidebar/user/UserSkeleton.tsx similarity index 100% rename from src/features/sidebar/view/internal/sidebar-content/user/UserSkeleton.tsx rename to src/features/sidebar/view/internal/sidebar/user/UserSkeleton.tsx From ff8651e0f4f8ac02b74606ad970ee624bf7e63e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 14:44:20 +0200 Subject: [PATCH 41/76] Truncates texts --- .../view/toolbar/SpecificationSelector.tsx | 16 ++++++++++++++-- .../projects/view/toolbar/VersionSelector.tsx | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/features/projects/view/toolbar/SpecificationSelector.tsx b/src/features/projects/view/toolbar/SpecificationSelector.tsx index c08aba1d..b1ba3394 100644 --- a/src/features/projects/view/toolbar/SpecificationSelector.tsx +++ b/src/features/projects/view/toolbar/SpecificationSelector.tsx @@ -1,5 +1,11 @@ import { SxProps } from "@mui/system" -import { SelectChangeEvent, Select, MenuItem, FormControl } from "@mui/material" +import { + SelectChangeEvent, + Select, + MenuItem, + FormControl, + Typography +} from "@mui/material" import MenuItemHover from "@/common/ui/MenuItemHover" import { OpenApiSpecification } from "../../domain" @@ -23,7 +29,13 @@ const SpecificationSelector = ({ {specifications.map(specification => - {specification.name} + + {specification.name} + )} diff --git a/src/features/projects/view/toolbar/VersionSelector.tsx b/src/features/projects/view/toolbar/VersionSelector.tsx index d8cdc8fc..5989592e 100644 --- a/src/features/projects/view/toolbar/VersionSelector.tsx +++ b/src/features/projects/view/toolbar/VersionSelector.tsx @@ -1,5 +1,11 @@ import { SxProps } from "@mui/system" -import { Select, MenuItem, SelectChangeEvent, FormControl } from "@mui/material" +import { + Select, + MenuItem, + SelectChangeEvent, + FormControl, + Typography +} from "@mui/material" import MenuItemHover from "@/common/ui/MenuItemHover" import { Version } from "../../domain" @@ -26,7 +32,13 @@ const VersionSelector = ({ {versions.map(version => - {version.name} + + {version.name} + )} From ec4c86dc916adb65decdf16be95eaf50a3517de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 14:45:49 +0200 Subject: [PATCH 42/76] Removes unused comment --- src/app/(authed)/(home)/layout.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/(authed)/(home)/layout.tsx b/src/app/(authed)/(home)/layout.tsx index fba8e84c..42c43b53 100644 --- a/src/app/(authed)/(home)/layout.tsx +++ b/src/app/(authed)/(home)/layout.tsx @@ -3,10 +3,6 @@ import { useContext } from "react" import { SplitView } from "@/features/sidebar/view" import { useProjects } from "@/features/projects/data" -// import MainContent from "@/features/projects/view/MainContent" -// import MobileToolbar from "@/features/projects/view/toolbar/MobileToolbar" -// import TrailingToolbarItem from "@/features/projects/view/toolbar/TrailingToolbarItem" - import { ProjectsContainerContext, ServerSideCachedProjectsContext From 4da59f4c1c874cbac2623e8a79b5ed6bacd48476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 14:45:56 +0200 Subject: [PATCH 43/76] Removes extraneous whitspace --- src/common/utils/isMac.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/utils/isMac.ts b/src/common/utils/isMac.ts index 4864bfa5..417f9240 100644 --- a/src/common/utils/isMac.ts +++ b/src/common/utils/isMac.ts @@ -1,5 +1,5 @@ const isMac = () => { - return window.navigator.userAgent.toLowerCase().includes("mac") + return window.navigator.userAgent.toLowerCase().includes("mac") } export default isMac \ No newline at end of file From b8d077b470efa98d674df1bde96dac08f4d9ffb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 14:46:08 +0200 Subject: [PATCH 44/76] Reorders imports --- src/app/(authed)/(home)/[[...slug]]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(authed)/(home)/[[...slug]]/page.tsx b/src/app/(authed)/(home)/[[...slug]]/page.tsx index b4afbb3d..e70225d5 100644 --- a/src/app/(authed)/(home)/[[...slug]]/page.tsx +++ b/src/app/(authed)/(home)/[[...slug]]/page.tsx @@ -4,9 +4,9 @@ import { useContext, useEffect } from "react" import { ProjectsContainerContext } from "@/common" import DelayedLoadingIndicator from "@/common/ui/DelayedLoadingIndicator" import ErrorMessage from "@/common/ui/ErrorMessage" +import { updateWindowTitle } from "@/features/projects/domain" import { useProjectSelection } from "@/features/projects/data" import Documentation from "@/features/projects/view/Documentation" -import { updateWindowTitle } from "@/features/projects/domain" export default function Page() { const { error, isLoading } = useContext(ProjectsContainerContext) From 006605960c81d3134ee670c502fa5d3ad54bf095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 14:46:18 +0200 Subject: [PATCH 45/76] Adds back header --- .../(authed)/(home)/[[...slug]]/layout.tsx | 16 +++ src/common/contexts.ts | 2 + .../projects/view/toolbar/MobileToolbar.tsx | 32 +++--- .../view/toolbar/TrailingToolbarItem.tsx | 32 +++--- .../sidebar/view/SecondarySplitHeader.tsx | 105 ++++++++++++++++++ src/features/sidebar/view/SplitView.tsx | 30 ++--- .../secondary/ToggleMobileToolbarButton.tsx | 32 ++++++ 7 files changed, 202 insertions(+), 47 deletions(-) create mode 100644 src/app/(authed)/(home)/[[...slug]]/layout.tsx create mode 100644 src/features/sidebar/view/SecondarySplitHeader.tsx create mode 100644 src/features/sidebar/view/internal/secondary/ToggleMobileToolbarButton.tsx diff --git a/src/app/(authed)/(home)/[[...slug]]/layout.tsx b/src/app/(authed)/(home)/[[...slug]]/layout.tsx new file mode 100644 index 00000000..68ecb609 --- /dev/null +++ b/src/app/(authed)/(home)/[[...slug]]/layout.tsx @@ -0,0 +1,16 @@ +import SecondarySplitHeader from "@/features/sidebar/view/SecondarySplitHeader" +import TrailingToolbarItem from "@/features/projects/view/toolbar/TrailingToolbarItem" +import MobileToolbar from "@/features/projects/view/toolbar/MobileToolbar" + +export default function Page({ children }: { children: React.ReactNode }) { + return ( + <> + > + + +
+ {children} +
+ + ) +} \ No newline at end of file diff --git a/src/common/contexts.ts b/src/common/contexts.ts index 6f19513f..e455e5b1 100644 --- a/src/common/contexts.ts +++ b/src/common/contexts.ts @@ -3,6 +3,8 @@ import { createContext } from "react" import { Project, } from "@/features/projects/domain" +export const SidebarContext = createContext<{ isToggleable: boolean }>({ isToggleable: true }) + type ProjectsContainer = { readonly projects: Project[] readonly isLoading: boolean diff --git a/src/features/projects/view/toolbar/MobileToolbar.tsx b/src/features/projects/view/toolbar/MobileToolbar.tsx index 2564fb92..1fa7b6ae 100644 --- a/src/features/projects/view/toolbar/MobileToolbar.tsx +++ b/src/features/projects/view/toolbar/MobileToolbar.tsx @@ -1,21 +1,21 @@ +"use client" + import { Stack } from "@mui/material" -import { Project, Version, OpenApiSpecification } from "../../domain" import VersionSelector from "./VersionSelector" import SpecificationSelector from "./SpecificationSelector" +import { useProjectSelection } from "../../data" -const MobileToolbar = ({ - project, - version, - specification, - onSelectVersion, - onSelectSpecification -}: { - project: Project - version: Version - specification: OpenApiSpecification - onSelectVersion: (versionId: string) => void, - onSelectSpecification: (specificationId: string) => void -}) => { +const MobileToolbar = () => { + const { + project, + version, + specification, + selectVersion, + selectSpecification + } = useProjectSelection() + if (!project || !version || !specification) { + return <> + } return ( diff --git a/src/features/projects/view/toolbar/TrailingToolbarItem.tsx b/src/features/projects/view/toolbar/TrailingToolbarItem.tsx index 537d4e20..85c9a556 100644 --- a/src/features/projects/view/toolbar/TrailingToolbarItem.tsx +++ b/src/features/projects/view/toolbar/TrailingToolbarItem.tsx @@ -1,24 +1,24 @@ +"use client" + import { SxProps } from "@mui/system" import { Stack, IconButton, Typography, Link, Tooltip } from "@mui/material" -import { Project, Version, OpenApiSpecification } from "../../domain" import VersionSelector from "./VersionSelector" import SpecificationSelector from "./SpecificationSelector" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faPen } from "@fortawesome/free-solid-svg-icons" +import { useProjectSelection } from "../../data" -const TrailingToolbarItem = ({ - project, - version, - specification, - onSelectVersion, - onSelectSpecification -}: { - project: Project - version: Version - specification: OpenApiSpecification - onSelectVersion: (versionId: string) => void, - onSelectSpecification: (specificationId: string) => void -}) => { +const TrailingToolbarItem = () => { + const { + project, + version, + specification, + selectVersion, + selectSpecification + } = useProjectSelection() + if (!project || !version || !specification) { + return <> + } const projectNameURL = version.url || project.url return ( <> @@ -55,14 +55,14 @@ const TrailingToolbarItem = ({ / {specification.editURL && diff --git a/src/features/sidebar/view/SecondarySplitHeader.tsx b/src/features/sidebar/view/SecondarySplitHeader.tsx new file mode 100644 index 00000000..9cfb4bac --- /dev/null +++ b/src/features/sidebar/view/SecondarySplitHeader.tsx @@ -0,0 +1,105 @@ +"use client" + +import { useState, useEffect, useContext } from "react" +import { useSessionStorage } from "usehooks-ts" +import { Box, IconButton, Stack, Tooltip, Divider, Collapse } from "@mui/material" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons" +import { useTheme } from "@mui/material/styles" +import { SidebarContext, isMac as checkIsMac } from "@/common" +import { useSidebarOpen } from "@/features/sidebar/data" +import ToggleMobileToolbarButton from "./internal/secondary/ToggleMobileToolbarButton" + +const Header = ({ + mobileToolbar, + children +}: { + mobileToolbar?: React.ReactNode + children?: React.ReactNode +}) => { + const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() + const [isMac, setIsMac] = useState(false) + const { isToggleable: isSidebarToggleable } = useContext(SidebarContext) + const [isMobileToolbarVisible, setMobileToolbarVisible] = useSessionStorage("isMobileToolbarVisible", true) + useEffect(() => { + // checkIsMac uses window so we delay the check. + setIsMac(checkIsMac()) + }, [isMac, setIsMac, checkIsMac]) + const openCloseKeyboardShortcut = `(${isMac ? "⌘" : "^"} + .)` + const theme = useTheme() + return ( + + + {isSidebarToggleable && !isSidebarOpen && + + setSidebarOpen(true)} + edge="start" + > + + + + } + {isSidebarToggleable && isSidebarOpen && + + setSidebarOpen(false)} + edge="start" + > + + + + } + + + {children} + {mobileToolbar && + setMobileToolbarVisible(!isMobileToolbarVisible) } + /> + } + + + + {mobileToolbar && + + + {mobileToolbar} + + + } + + + ) +} + +export default Header diff --git a/src/features/sidebar/view/SplitView.tsx b/src/features/sidebar/view/SplitView.tsx index 7456b87c..204c203b 100644 --- a/src/features/sidebar/view/SplitView.tsx +++ b/src/features/sidebar/view/SplitView.tsx @@ -2,22 +2,33 @@ import { useEffect } from "react" import { Stack } from "@mui/material" +import { isMac, useKeyboardShortcut } from "@/common" import { useProjectSelection } from "@/features/projects/data" +import { useSidebarOpen } from "../data" import PrimaryContainer from "./internal/primary/Container" import SecondaryContainer from "./internal/secondary/Container" import Sidebar from "./internal/sidebar/Sidebar" -import { useSidebarOpen } from "../data" const SplitView = ({ children }: { children?: React.ReactNode }) => { const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() const { project } = useProjectSelection() - const sidebarWidth = 320 + const canToggleSidebar = project !== undefined useEffect(() => { // Show the sidebar if no project is selected. - if (project === undefined) { + if (canToggleSidebar) { setSidebarOpen(true) } - }, [project, setSidebarOpen]) + }, [canToggleSidebar, setSidebarOpen]) + useKeyboardShortcut(event => { + const isActionKey = isMac() ? event.metaKey : event.ctrlKey + if (isActionKey && event.key === ".") { + event.preventDefault() + if (canToggleSidebar) { + setSidebarOpen(!isSidebarOpen) + } + } + }, [canToggleSidebar, setSidebarOpen]) + const sidebarWidth = 320 return ( { {children} - {/* - {header} -
- {children} -
-
*/}
) } -// Disable server-side rendering as this component uses the window instance to manage its state. -// export default dynamic(() => Promise.resolve(SidebarContainer), { -// ssr: false -// }) - export default SplitView diff --git a/src/features/sidebar/view/internal/secondary/ToggleMobileToolbarButton.tsx b/src/features/sidebar/view/internal/secondary/ToggleMobileToolbarButton.tsx new file mode 100644 index 00000000..748000e3 --- /dev/null +++ b/src/features/sidebar/view/internal/secondary/ToggleMobileToolbarButton.tsx @@ -0,0 +1,32 @@ +import { IconButton } from "@mui/material" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faChevronDown } from "@fortawesome/free-solid-svg-icons" + +const ToggleMobileToolbarButton = ({ + direction, + onToggle +}: { + direction: "up" | "down" + onToggle: () => void +}) => { + return <> + + + + +} + +export default ToggleMobileToolbarButton From 6f7020c086ed05ffe4f3aab459f54fe07a1c866f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 14:47:54 +0200 Subject: [PATCH 46/76] Fixes toolbar being force shown --- src/features/sidebar/view/SplitView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/sidebar/view/SplitView.tsx b/src/features/sidebar/view/SplitView.tsx index 204c203b..22fbde31 100644 --- a/src/features/sidebar/view/SplitView.tsx +++ b/src/features/sidebar/view/SplitView.tsx @@ -15,7 +15,7 @@ const SplitView = ({ children }: { children?: React.ReactNode }) => { const canToggleSidebar = project !== undefined useEffect(() => { // Show the sidebar if no project is selected. - if (canToggleSidebar) { + if (!canToggleSidebar) { setSidebarOpen(true) } }, [canToggleSidebar, setSidebarOpen]) From 321f3dda717c23b819b789ad9b8126ad9ebd90f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 14:51:02 +0200 Subject: [PATCH 47/76] Moves canToggleSidebar to layout --- src/app/(authed)/(home)/layout.tsx | 15 ++++++++++++--- src/features/sidebar/view/SplitView.tsx | 12 ++++++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/app/(authed)/(home)/layout.tsx b/src/app/(authed)/(home)/layout.tsx index 42c43b53..49b27871 100644 --- a/src/app/(authed)/(home)/layout.tsx +++ b/src/app/(authed)/(home)/layout.tsx @@ -2,7 +2,7 @@ import { useContext } from "react" import { SplitView } from "@/features/sidebar/view" -import { useProjects } from "@/features/projects/data" +import { useProjects, useProjectSelection } from "@/features/projects/data" import { ProjectsContainerContext, ServerSideCachedProjectsContext @@ -19,9 +19,18 @@ export default function Layout({ children }: { children: React.ReactNode }) { } return ( - + {children} - + ) } + +const SplitViewWrapper = ({ children }: { children: React.ReactNode }) => { + const { project } = useProjectSelection() + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/features/sidebar/view/SplitView.tsx b/src/features/sidebar/view/SplitView.tsx index 22fbde31..c4c92595 100644 --- a/src/features/sidebar/view/SplitView.tsx +++ b/src/features/sidebar/view/SplitView.tsx @@ -3,16 +3,20 @@ import { useEffect } from "react" import { Stack } from "@mui/material" import { isMac, useKeyboardShortcut } from "@/common" -import { useProjectSelection } from "@/features/projects/data" import { useSidebarOpen } from "../data" import PrimaryContainer from "./internal/primary/Container" import SecondaryContainer from "./internal/secondary/Container" import Sidebar from "./internal/sidebar/Sidebar" -const SplitView = ({ children }: { children?: React.ReactNode }) => { +const SplitView = ({ + canToggleSidebar: _canToggleSidebar, + children +}: { + canToggleSidebar?: boolean + children?: React.ReactNode +}) => { const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() - const { project } = useProjectSelection() - const canToggleSidebar = project !== undefined + const canToggleSidebar = _canToggleSidebar || true useEffect(() => { // Show the sidebar if no project is selected. if (!canToggleSidebar) { From 8cc81c28acf3198c0b6c8d6b7e445c47fb389cd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Fri, 26 Jul 2024 14:52:46 +0200 Subject: [PATCH 48/76] Uses React.ReactNode --- src/common/theme/ThemeRegistry.tsx | 4 ++-- src/common/ui/MenuItemHover.tsx | 3 +-- src/features/auth/view/SessionBarrier.tsx | 3 +-- src/features/docs/view/LoadingWrapper.tsx | 3 +-- src/features/sidebar/view/internal/primary/Container.tsx | 5 ++--- src/features/sidebar/view/internal/secondary/Container.tsx | 5 ++--- .../sidebar/view/internal/sidebar/settings/SettingsList.tsx | 3 +-- .../sidebar/view/internal/sidebar/user/UserButton.tsx | 6 +++--- 8 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/common/theme/ThemeRegistry.tsx b/src/common/theme/ThemeRegistry.tsx index ffb45ab7..95dbeadd 100644 --- a/src/common/theme/ThemeRegistry.tsx +++ b/src/common/theme/ThemeRegistry.tsx @@ -1,6 +1,6 @@ "use client" -import { ReactNode, useState } from "react" +import { useState } from "react" import createCache, { Options } from "@emotion/cache" import { useServerInsertedHTML } from "next/navigation" import { CacheProvider } from "@emotion/react" @@ -10,7 +10,7 @@ import theme from "./theme" type ThemeRegistryProps = { options: Options - children: ReactNode + children: React.ReactNode } // This implementation is from emotion-js diff --git a/src/common/ui/MenuItemHover.tsx b/src/common/ui/MenuItemHover.tsx index 0326248e..d627a980 100644 --- a/src/common/ui/MenuItemHover.tsx +++ b/src/common/ui/MenuItemHover.tsx @@ -1,4 +1,3 @@ -import { ReactNode } from "react" import { SxProps } from "@mui/system" import { Box } from "@mui/material" import useMediaQuery from "@mui/material/useMediaQuery" @@ -9,7 +8,7 @@ const MenuItemHover = ({ sx }: { disabled?: boolean - children: ReactNode + children: React.ReactNode sx?: SxProps }) => { const isHoverSupported = useMediaQuery("(hover: hover)") diff --git a/src/features/auth/view/SessionBarrier.tsx b/src/features/auth/view/SessionBarrier.tsx index cf4b41f4..2e5e02ad 100644 --- a/src/features/auth/view/SessionBarrier.tsx +++ b/src/features/auth/view/SessionBarrier.tsx @@ -1,4 +1,3 @@ -import { ReactNode } from "react" import { redirect } from "next/navigation" import { blockingSessionValidator } from "@/composition" import { SessionValidity } from "../domain" @@ -6,7 +5,7 @@ import { SessionValidity } from "../domain" export default async function SessionBarrier({ children }: { - children: ReactNode + children: React.ReactNode }) { const sessionValidity = await blockingSessionValidator.validateSession() switch (sessionValidity) { diff --git a/src/features/docs/view/LoadingWrapper.tsx b/src/features/docs/view/LoadingWrapper.tsx index 8d45427e..0a86e04b 100644 --- a/src/features/docs/view/LoadingWrapper.tsx +++ b/src/features/docs/view/LoadingWrapper.tsx @@ -1,4 +1,3 @@ -import { ReactNode } from "react" import { Box } from "@mui/material" import LoadingIndicator from "@/common/ui/LoadingIndicator" @@ -7,7 +6,7 @@ const LoadingWrapper = ({ children }: { showLoadingIndicator: boolean, - children: ReactNode + children: React.ReactNode }) => { return ( void - children?: ReactNode + children?: React.ReactNode }) => { return ( <> @@ -55,7 +54,7 @@ const _PrimaryContainer = ({ onClose?: () => void keepMounted?: boolean sx: SxProps, - children?: ReactNode + children?: React.ReactNode }) => { return ( { const sx = { overflow: "hidden" } return ( @@ -65,7 +64,7 @@ const _SecondaryContainer = ({ }: { sidebarWidth: number isSidebarOpen: boolean - children: ReactNode + children: React.ReactNode sx?: SxProps }) => { return ( diff --git a/src/features/sidebar/view/internal/sidebar/settings/SettingsList.tsx b/src/features/sidebar/view/internal/sidebar/settings/SettingsList.tsx index 183bad38..5825193b 100644 --- a/src/features/sidebar/view/internal/sidebar/settings/SettingsList.tsx +++ b/src/features/sidebar/view/internal/sidebar/settings/SettingsList.tsx @@ -1,4 +1,3 @@ -import { ReactNode } from "react" import { signOut } from "next-auth/react" import Link from "next/link" import { List, Button, Stack, Typography } from "@mui/material" @@ -11,7 +10,7 @@ import { faQuestionCircle, faRightFromBracket } from "@fortawesome/free-solid-sv const SettingsItem = ({ onClick, icon, children }: { onClick?: () => void icon?: IconProp - children?: ReactNode + children?: React.ReactNode }) => { return ( + + ) +} + +const Footer = () => { + return ( + + {HELP_URL && + + + Learn more about {SITE_NAME} + + + } + + ) +} \ No newline at end of file diff --git a/src/composition.ts b/src/composition.ts index 8faf4981..7284f036 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -87,6 +87,9 @@ export const { signIn, auth, handlers: authHandlers } = NextAuth({ colorScheme: "light", brandColor: "black" }, + pages: { + signIn: "/auth/signin" + }, providers: [ GithubProvider({ clientId: env.getOrThrow("GITHUB_CLIENT_ID"), diff --git a/src/features/auth/view/SignInTexts.tsx b/src/features/auth/view/SignInTexts.tsx new file mode 100644 index 00000000..a0242010 --- /dev/null +++ b/src/features/auth/view/SignInTexts.tsx @@ -0,0 +1,89 @@ +"use client" + +import { Box, Typography, SxProps } from "@mui/material" +import { useEffect, useState } from "react" + +const SignInTexts = () => { + const getRandomTextColor = ({ excluding }: { excluding?: string }) => { + console.log(excluding) + const colors = ["#01BBFE", "#00AE47", "#FCB23D"] + .filter(e => e !== excluding) + return colors[Math.floor(Math.random() * colors.length)] + } + const [characterIndex, setCharacterIndex] = useState(0) + const [textIndex, setTextIndex] = useState(0) + const [displayedText, setDisplayedText] = useState("") + const [textColor, setTextColor] = useState(getRandomTextColor({})) + const texts = [ + "is a great OpenAPI viewer", + "facilitates spec-driven development", + "puts your documentation in one place", + "adds documentation previews to pull requests" + ] + useEffect(() => { + const interval = setInterval(() => { + setDisplayedText("") + setCharacterIndex(0) + setTextColor(getRandomTextColor({ excluding: textColor })) + setTextIndex((prevIndex) => (prevIndex + 1) % texts.length) + }, 5000) + return () => clearInterval(interval) + }, [textColor]) + useEffect(() => { + const interval = setInterval(() => { + setCharacterIndex(characterIndex + 1) + setDisplayedText(texts[textIndex].substring(0, characterIndex)) + if (characterIndex === texts[textIndex].length) { + clearInterval(interval) + } + }, 50) + return () => clearInterval(interval) + }, [textIndex, characterIndex]) + const longestText = texts.reduce((a, b) => (a.length > b.length ? a : b)) + return ( + <> + + + + +   + + + + + + ) +} + +export default SignInTexts + +const Text = ({ + text, + textColor, + children, + sx +}: { + text: string, + textColor?: string, + children?: React.ReactNode, + sx?: SxProps +}) => { + return ( + + Shape Docs {text} + {children} + + ) +} \ No newline at end of file From 9b57fe92f52cb19b62b61aca12890dfcde2126b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 30 Jul 2024 11:30:20 +0200 Subject: [PATCH 69/76] Increases size required for two columns --- src/app/auth/signin/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index 1bd4cd05..a104ddc0 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -23,7 +23,7 @@ const InfoColumn = () => { return ( { > @@ -59,14 +59,14 @@ const SignInColumn = () => { /> {title} {title} From 67eff14ac5606bb331b8598ce5bb1df985c4308b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 30 Jul 2024 11:32:11 +0200 Subject: [PATCH 70/76] Decreases padding on small screens --- src/features/auth/view/SignInTexts.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/features/auth/view/SignInTexts.tsx b/src/features/auth/view/SignInTexts.tsx index a0242010..0bb4cc67 100644 --- a/src/features/auth/view/SignInTexts.tsx +++ b/src/features/auth/view/SignInTexts.tsx @@ -81,7 +81,11 @@ const Text = ({ sx?: SxProps }) => { return ( - + Shape Docs {text} {children} From 4db8422d92c08ddbb7b5f5a83e9731127030a358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 30 Jul 2024 11:33:47 +0200 Subject: [PATCH 71/76] Fixes mobile display --- src/app/auth/signin/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index a104ddc0..00fa965d 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -23,7 +23,7 @@ const InfoColumn = () => { return ( { > @@ -59,14 +59,14 @@ const SignInColumn = () => { /> {title} {title} From 5a0817c1e6a6efab2b86993b673546932f0a4bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 30 Jul 2024 11:34:02 +0200 Subject: [PATCH 72/76] Increases padding of GitHub button --- src/app/auth/signin/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index 00fa965d..97e53ae0 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -95,7 +95,7 @@ const SignInWithGitHub = () => { }} >