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 01/24] 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 02/24] 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 03/24] 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 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 04/24] 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 05/24] 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 06/24] 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 07/24] 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 08/24] 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 09/24] 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 10/24] 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 11/24] 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 12/24] 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 13/24] 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 14/24] 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 15/24] 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 16/24] 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 17/24] 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 18/24] 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 19/24] 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 20/24] 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 21/24] 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 (