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/__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/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/__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: "" } diff --git a/src/app/(authed)/(home)/[[...slug]]/layout.tsx b/src/app/(authed)/(home)/[[...slug]]/layout.tsx new file mode 100644 index 00000000..36fb09c1 --- /dev/null +++ b/src/app/(authed)/(home)/[[...slug]]/layout.tsx @@ -0,0 +1,23 @@ +"use client" + +import SecondarySplitHeader from "@/features/sidebar/view/SecondarySplitHeader" +import TrailingToolbarItem from "@/features/projects/view/toolbar/TrailingToolbarItem" +import MobileToolbar from "@/features/projects/view/toolbar/MobileToolbar" +import { useProjectSelection } from "@/features/projects/data" + +export default function Page({ children }: { children: React.ReactNode }) { + const { project } = useProjectSelection() + if (!project) { + return <> + } + return ( + <> + > + + +
+ {children} +
+ + ) +} \ No newline at end of file diff --git a/src/app/(authed)/(home)/[[...slug]]/page.tsx b/src/app/(authed)/(home)/[[...slug]]/page.tsx new file mode 100644 index 00000000..e70225d5 --- /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 { updateWindowTitle } from "@/features/projects/domain" +import { useProjectSelection } from "@/features/projects/data" +import Documentation from "@/features/projects/view/Documentation" + +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, navigateToSelectionIfNeeded]) + // 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..49b27871 --- /dev/null +++ b/src/app/(authed)/(home)/layout.tsx @@ -0,0 +1,36 @@ +"use client" + +import { useContext } from "react" +import { SplitView } from "@/features/sidebar/view" +import { useProjects, useProjectSelection } from "@/features/projects/data" +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) + const newProjectsContainer = { projects, error, isLoading } + if (isLoading && serverSideCachedProjects) { + newProjectsContainer.isLoading = false + newProjectsContainer.projects = serverSideCachedProjects + } + return ( + + + {children} + + + ) +} + +const SplitViewWrapper = ({ children }: { children: React.ReactNode }) => { + const { project } = useProjectSelection() + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/app/(authed)/(home)/new/page.tsx b/src/app/(authed)/(home)/new/page.tsx new file mode 100644 index 00000000..bdbbd356 --- /dev/null +++ b/src/app/(authed)/(home)/new/page.tsx @@ -0,0 +1,56 @@ +import Link from "next/link" +import { env, splitOwnerAndRepository } from "@/common" + +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, + suffix: repositoryNameSuffix + }) + const newGitHubRepositoryLink = makeNewGitHubRepositoryLink({ + templateName, + repositoryName: suffixedRepositoryName, + description: `Contains OpenAPI specifications for ${projectName}` + }) + return ( + + {newGitHubRepositoryLink} + + ) +} + +export default Page + +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 +} diff --git a/src/app/(authed)/layout.tsx b/src/app/(authed)/layout.tsx new file mode 100644 index 00000000..bc977cf5 --- /dev/null +++ b/src/app/(authed)/layout.tsx @@ -0,0 +1,25 @@ +import { redirect } from "next/navigation" +import { SessionProvider } from "next-auth/react" +import { session, projectRepository } from "@/composition" +import ErrorHandler from "@/common/ui/ErrorHandler" +import SessionBarrier from "@/features/auth/view/SessionBarrier" +import ServerSideCachedProjectsProvider from "@/features/projects/view/ServerSideCachedProjectsProvider" + +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..e455e5b1 --- /dev/null +++ b/src/common/contexts.ts @@ -0,0 +1,19 @@ +"use client" + +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 + 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/useSidebarOpen.tsx b/src/common/state/useSidebarOpen.tsx deleted file mode 100644 index 2d0c3b9e..00000000 --- a/src/common/state/useSidebarOpen.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/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/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/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/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 window.navigator.userAgent.toLowerCase().includes("mac") + return window.navigator.userAgent.toLowerCase().includes("mac") } export default isMac \ No newline at end of file diff --git a/src/features/auth/view/SessionBarrier.tsx b/src/features/auth/view/SessionBarrier.tsx index 8f1fdf0d..2e5e02ad 100644 --- a/src/features/auth/view/SessionBarrier.tsx +++ b/src/features/auth/view/SessionBarrier.tsx @@ -1,16 +1,17 @@ -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 }: { - children: ReactNode + children: React.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} - - - - - ) -} 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} - - ) -} diff --git a/src/features/docs/view/LoadingWrapper.tsx b/src/features/docs/view/LoadingWrapper.tsx index 65c70157..0a86e04b 100644 --- a/src/features/docs/view/LoadingWrapper.tsx +++ b/src/features/docs/view/LoadingWrapper.tsx @@ -1,13 +1,12 @@ -import { ReactNode } from "react" import { Box } from "@mui/material" -import LoadingIndicator from "@/common/loading/LoadingIndicator" +import LoadingIndicator from "@/common/ui/LoadingIndicator" const LoadingWrapper = ({ showLoadingIndicator, children }: { showLoadingIndicator: boolean, - children: ReactNode + children: React.ReactNode }) => { return ( e.login) - return [viewer.login].concat(organizations) + 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 e94e228a..569a4bd1 100644 --- a/src/features/projects/data/GitHubRepositoryDataSource.ts +++ b/src/features/projects/data/GitHubRepositoryDataSource.ts @@ -69,11 +69,7 @@ export default class GitHubProjectDataSource implements IGitHubRepositoryDataSou return await this.getRepositoriesForLogins({ logins }) } - private async getRepositoriesForLogins({ - logins - }: { - logins: 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. 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..6c9229e1 --- /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 [, 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/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/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/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/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} + )} 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/sidebarOpen.tsx b/src/features/sidebar/data/useSidebarOpen.ts similarity index 65% rename from src/common/state/sidebarOpen.tsx rename to src/features/sidebar/data/useSidebarOpen.ts index 2d0c3b9e..3d4c1be7 100644 --- a/src/common/state/sidebarOpen.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/SecondarySplitHeader.tsx b/src/features/sidebar/view/SecondarySplitHeader.tsx new file mode 100644 index 00000000..785bce43 --- /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]) + 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/Sidebar.tsx b/src/features/sidebar/view/Sidebar.tsx deleted file mode 100644 index d8873876..00000000 --- a/src/features/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/sidebar/view/SplitView.tsx b/src/features/sidebar/view/SplitView.tsx new file mode 100644 index 00000000..78cadccf --- /dev/null +++ b/src/features/sidebar/view/SplitView.tsx @@ -0,0 +1,52 @@ +"use client" + +import { useEffect } from "react" +import { Stack } from "@mui/material" +import { isMac, useKeyboardShortcut } from "@/common" +import { useSidebarOpen } from "../data" +import PrimaryContainer from "./internal/primary/Container" +import SecondaryContainer from "./internal/secondary/Container" +import Sidebar from "./internal/sidebar/Sidebar" + +const SplitView = ({ + canToggleSidebar: _canToggleSidebar, + children +}: { + canToggleSidebar?: boolean + children?: React.ReactNode +}) => { + const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() + const canToggleSidebar = _canToggleSidebar !== undefined ? _canToggleSidebar : true + useEffect(() => { + // Show the sidebar if no project is selected. + if (!canToggleSidebar) { + setSidebarOpen(true) + } + }, [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 ( + + setSidebarOpen(false)} + > + + + + {children} + + + ) +} + +export default SplitView diff --git a/src/features/sidebar/view/TrailingToolbar.tsx b/src/features/sidebar/view/TrailingToolbar.tsx deleted file mode 100644 index 1ab9f95c..00000000 --- a/src/features/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/sidebar/view/base/Drawer.tsx b/src/features/sidebar/view/base/Drawer.tsx deleted file mode 100644 index 5a335588..00000000 --- a/src/features/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/sidebar/view/base/SecondaryHeader.tsx b/src/features/sidebar/view/base/SecondaryHeader.tsx deleted file mode 100644 index 0e4a0177..00000000 --- a/src/features/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/sidebar/view/base/responsive/Drawer.tsx b/src/features/sidebar/view/base/responsive/Drawer.tsx deleted file mode 100644 index 8a027f77..00000000 --- a/src/features/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/sidebar/view/base/responsive/SecondaryHeader.tsx b/src/features/sidebar/view/base/responsive/SecondaryHeader.tsx deleted file mode 100644 index f48dd778..00000000 --- a/src/features/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/sidebar/view/base/responsive/SecondaryWrapper.tsx b/src/features/sidebar/view/base/responsive/SecondaryWrapper.tsx deleted file mode 100644 index 171bf342..00000000 --- a/src/features/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/sidebar/view/base/responsive/SidebarContainer.tsx b/src/features/sidebar/view/base/responsive/SidebarContainer.tsx deleted file mode 100644 index 248660ab..00000000 --- a/src/features/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/sidebar/view/client/SidebarContainer.tsx b/src/features/sidebar/view/client/SidebarContainer.tsx deleted file mode 100644 index ec7b6fc2..00000000 --- a/src/features/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 -}) diff --git a/src/features/sidebar/view/index.ts b/src/features/sidebar/view/index.ts new file mode 100644 index 00000000..47482d01 --- /dev/null +++ b/src/features/sidebar/view/index.ts @@ -0,0 +1 @@ +export { default as SplitView } from "./SplitView" diff --git a/src/features/sidebar/view/internal/primary/Container.tsx b/src/features/sidebar/view/internal/primary/Container.tsx new file mode 100644 index 00000000..b6714e1e --- /dev/null +++ b/src/features/sidebar/view/internal/primary/Container.tsx @@ -0,0 +1,81 @@ +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?: React.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?: React.ReactNode +}) => { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/features/sidebar/view/base/SecondaryWrapper.tsx b/src/features/sidebar/view/internal/secondary/Container.tsx similarity index 54% rename from src/features/sidebar/view/base/SecondaryWrapper.tsx rename to src/features/sidebar/view/internal/secondary/Container.tsx index fef93718..7de24922 100644 --- a/src/features/sidebar/view/base/SecondaryWrapper.tsx +++ b/src/features/sidebar/view/internal/secondary/Container.tsx @@ -1,15 +1,46 @@ -import { ReactNode } from "react" import { SxProps } from "@mui/system" import { Stack } from "@mui/material" import { styled } from "@mui/material/styles" -interface WrapperStackProps { +const SecondaryContainer = ({ + sidebarWidth, + offsetContent, + children +}: { sidebarWidth: number - isSidebarOpen: boolean + offsetContent: boolean + children?: React.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" + shouldForwardProp: (prop) => prop !== "isSidebarOpen" && prop !== "sidebarWidth" })(({ theme, sidebarWidth, isSidebarOpen }) => ({ transition: theme.transitions.create("margin", { easing: theme.transitions.easing.sharp, @@ -25,7 +56,7 @@ const WrapperStack = styled(Stack, { }) })) -export default function SecondaryWrapper({ +const _SecondaryContainer = ({ sidebarWidth, isSidebarOpen, children, @@ -33,9 +64,9 @@ export default function SecondaryWrapper({ }: { sidebarWidth: number isSidebarOpen: boolean - children: ReactNode + children: React.ReactNode sx?: SxProps -}) { +}) => { return ( void +}) => { + return <> + + + + +} + +export default ToggleMobileToolbarButton diff --git a/src/features/sidebar/view/SidebarHeader.tsx b/src/features/sidebar/view/internal/sidebar/Header.tsx similarity index 66% rename from src/features/sidebar/view/SidebarHeader.tsx rename to src/features/sidebar/view/internal/sidebar/Header.tsx index 916fdb5e..bcad2f45 100644 --- a/src/features/sidebar/view/SidebarHeader.tsx +++ b/src/features/sidebar/view/internal/sidebar/Header.tsx @@ -1,11 +1,10 @@ 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 Header = () => { const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE return ( - {/* - - + + + - */} + ) } + +export default Header 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/projects/view/ProjectAvatar.tsx b/src/features/sidebar/view/internal/sidebar/projects/ProjectAvatar.tsx similarity index 83% rename from src/features/projects/view/ProjectAvatar.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectAvatar.tsx index 2a6056d6..c55f7aac 100644 --- a/src/features/projects/view/ProjectAvatar.tsx +++ b/src/features/sidebar/view/internal/sidebar/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/projects/ProjectAvatarSquircle.tsx similarity index 76% rename from src/features/projects/view/ProjectAvatarSquircleClip.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectAvatarSquircle.tsx index 0ac099c5..341a058c 100644 --- a/src/features/projects/view/ProjectAvatarSquircleClip.tsx +++ b/src/features/sidebar/view/internal/sidebar/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/projects/ProjectList.tsx similarity index 68% rename from src/features/projects/view/ProjectList.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectList.tsx index 30fb06ad..3fb6e231 100644 --- a/src/features/projects/view/ProjectList.tsx +++ b/src/features/sidebar/view/internal/sidebar/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/projects/ProjectListItem.tsx similarity index 84% rename from src/features/projects/view/ProjectListItem.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectListItem.tsx index c9472256..4c05ee6b 100644 --- a/src/features/projects/view/ProjectListItem.tsx +++ b/src/features/sidebar/view/internal/sidebar/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/projects/ProjectListItemPlaceholder.tsx similarity index 78% rename from src/features/projects/view/ProjectListItemPlaceholder.tsx rename to src/features/sidebar/view/internal/sidebar/projects/ProjectListItemPlaceholder.tsx index d5fcde1f..77de983a 100644 --- a/src/features/projects/view/ProjectListItemPlaceholder.tsx +++ b/src/features/sidebar/view/internal/sidebar/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 }} /> - + void icon?: IconProp - children?: ReactNode + children?: React.ReactNode }) => { return (