From b93d54341f3a92e9fa3c3a843da4d905c206b72e Mon Sep 17 00:00:00 2001 From: Oscar Date: Mon, 20 Oct 2025 15:52:29 +0200 Subject: [PATCH 1/4] Created functionality for sidebar (diffbar) - Using the logic of the projectlist-sidebar. Created diff/route.ts to handle GET, run oasdiff and return JSON. Created DiffContent.tsx - fetches the data, compares the branches and show the data. - Finds the fromBranch based of baseRef and toBranch is chosen based id - Handles if same branch is chosen in to and from - non comparable - Components to display data. baseRef to Version if a PR exist for that branch --- package-lock.json | 2 +- package.json | 2 +- .../[owner]/[repository]/[...path]/route.ts | 56 ++++ src/common/ui/SpacedList.tsx | 22 +- .../projects/data/GitHubProjectDataSource.ts | 3 +- .../data/GitHubRepositoryDataSource.ts | 248 ++++++++++-------- .../domain/IGitHubRepositoryDataSource.ts | 1 + src/features/projects/domain/Version.ts | 3 +- .../projects/view/toolbar/Selector.tsx | 1 + src/features/sidebar/data/useDiffbarOpen.ts | 5 + .../sidebar/view/SecondarySplitHeader.tsx | 43 ++- src/features/sidebar/view/SplitView.tsx | 3 +- .../sidebar/view/internal/ClientSplitView.tsx | 92 +++++-- .../view/internal/diffbar/DiffContent.tsx | 79 ++++++ .../diffbar/components/DiffDialog.tsx | 78 ++++++ .../diffbar/components/DiffHeader.tsx | 33 +++ .../internal/diffbar/components/DiffList.tsx | 47 ++++ .../diffbar/components/DiffListItem.tsx | 63 +++++ .../diffbar/components/MonoQuotedText.tsx | 31 +++ .../diffbar/components/PopulatedDiffList.tsx | 31 +++ .../view/internal/secondary/Container.tsx | 51 +++- .../view/internal/tertiary/Container.tsx | 39 +++ tsconfig.json | 2 +- 23 files changed, 766 insertions(+), 169 deletions(-) create mode 100644 src/app/api/diff/[owner]/[repository]/[...path]/route.ts create mode 100644 src/features/sidebar/data/useDiffbarOpen.ts create mode 100644 src/features/sidebar/view/internal/diffbar/DiffContent.tsx create mode 100644 src/features/sidebar/view/internal/diffbar/components/DiffDialog.tsx create mode 100644 src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx create mode 100644 src/features/sidebar/view/internal/diffbar/components/DiffList.tsx create mode 100644 src/features/sidebar/view/internal/diffbar/components/DiffListItem.tsx create mode 100644 src/features/sidebar/view/internal/diffbar/components/MonoQuotedText.tsx create mode 100644 src/features/sidebar/view/internal/diffbar/components/PopulatedDiffList.tsx create mode 100644 src/features/sidebar/view/internal/tertiary/Container.tsx diff --git a/package-lock.json b/package-lock.json index db3041db..84404445 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13980,4 +13980,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index a8da8b3f..5e78cf62 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,9 @@ "@types/node": "^24.0.8", "@types/nprogress": "^0.2.3", "@types/pg": "^8.15.2", - "@typescript-eslint/eslint-plugin": "^8.35.1", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", + "@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/parser": "^8.31.1", "eslint": "^9.30.0", "eslint-config-next": "^15.3.4", diff --git a/src/app/api/diff/[owner]/[repository]/[...path]/route.ts b/src/app/api/diff/[owner]/[repository]/[...path]/route.ts new file mode 100644 index 00000000..36f6b6ef --- /dev/null +++ b/src/app/api/diff/[owner]/[repository]/[...path]/route.ts @@ -0,0 +1,56 @@ + import { NextRequest, NextResponse } from "next/server" +import { session, userGitHubClient } from "@/composition" +import { makeUnauthenticatedAPIErrorResponse } from "@/common" +import { execSync } from "child_process" + +interface GetDiffParams { + owner: string + repository: string + path: [string] +} + +export async function GET(req: NextRequest, { params }: { params: Promise }) { + const isAuthenticated = await session.getIsAuthenticated() + if (!isAuthenticated) { + return makeUnauthenticatedAPIErrorResponse() + } + + const { path: paramsPath, owner, repository } = await params + const path = paramsPath.join("/") + + const fromRef = req.nextUrl.searchParams.get("from") + const toRef = req.nextUrl.searchParams.get("to") + + if (!fromRef || !toRef) { + return NextResponse.json({ error: "Missing from/to parameters" }, { status: 400 }) + } + + const fullRepositoryName = repository + "-openapi" + + const spec1 = await userGitHubClient.getRepositoryContent({ + repositoryOwner: owner, + repositoryName: fullRepositoryName, + path: path, + ref: fromRef + }) + + const spec2 = await userGitHubClient.getRepositoryContent({ + repositoryOwner: owner, + repositoryName: fullRepositoryName, + path: path, + ref: toRef + }) + + const result = execSync(`oasdiff changelog --format json "${spec1.downloadURL}" "${spec2.downloadURL}"`, { encoding: 'utf8' }) + + console.log(result) + + + const diffData = JSON.parse(result) + + return NextResponse.json({ + from: fromRef, + to: toRef, + changes: diffData + }) +} \ No newline at end of file diff --git a/src/common/ui/SpacedList.tsx b/src/common/ui/SpacedList.tsx index 313acaef..8149003e 100644 --- a/src/common/ui/SpacedList.tsx +++ b/src/common/ui/SpacedList.tsx @@ -12,13 +12,21 @@ const SpacedList = ({ }) => { return ( - {React.Children.map(children, (child, idx) => ( - - {child} - - ))} + {React.Children.map(children, (child, idx) => { + const baseKey = (child as any)?.key ?? "idx"; + const key = `${String(baseKey)}-${idx}`; + return ( + + {child} + + ); + })} ) } diff --git a/src/features/projects/data/GitHubProjectDataSource.ts b/src/features/projects/data/GitHubProjectDataSource.ts index 17086513..52babc1d 100644 --- a/src/features/projects/data/GitHubProjectDataSource.ts +++ b/src/features/projects/data/GitHubProjectDataSource.ts @@ -140,7 +140,8 @@ export default class GitHubProjectDataSource implements IProjectDataSource { name: ref.name, specifications: specifications, url: `https://github.com/${ownerName}/${repositoryName}/tree/${ref.name}`, - isDefault: isDefaultRef || false + isDefault: isDefaultRef || false, + baseRef: ref.baseRef, } } diff --git a/src/features/projects/data/GitHubRepositoryDataSource.ts b/src/features/projects/data/GitHubRepositoryDataSource.ts index 569a4bd1..0a194552 100644 --- a/src/features/projects/data/GitHubRepositoryDataSource.ts +++ b/src/features/projects/data/GitHubRepositoryDataSource.ts @@ -2,139 +2,158 @@ import { GitHubRepository, IGitHubRepositoryDataSource, IGitHubLoginDataSource, - IGitHubGraphQLClient -} from "../domain" + IGitHubGraphQLClient, +} from "../domain"; type GraphQLGitHubRepository = { - readonly name: string + readonly name: string; readonly owner: { - readonly login: string - } + readonly login: string; + }; readonly defaultBranchRef: { - readonly name: string + readonly name: string; readonly target: { - readonly oid: string - } - } + readonly oid: string; + }; + }; readonly configYml?: { - readonly text: string - } + readonly text: string; + }; readonly configYaml?: { - readonly text: string - } - readonly branches: EdgesContainer - readonly tags: EdgesContainer -} + readonly text: string; + }; + readonly branches: EdgesContainer; + readonly tags: EdgesContainer; +}; type EdgesContainer = { - readonly edges: Edge[] -} + readonly edges: Edge[]; +}; type Edge = { - readonly node: T -} + readonly node: T; +}; type GraphQLGitHubRepositoryRef = { - readonly name: string + readonly name: string; readonly target: { - readonly oid: string + readonly oid: string; readonly tree: { readonly entries: { - readonly name: string - }[] - } - } -} + readonly name: string; + }[]; + }; + }; + readonly associatedPullRequests?: { + readonly nodes: { + readonly baseRefName: string; + }[]; + }; +}; + +export default class GitHubProjectDataSource + implements IGitHubRepositoryDataSource +{ + private readonly loginsDataSource: IGitHubLoginDataSource; + private readonly graphQlClient: IGitHubGraphQLClient; + private readonly repositoryNameSuffix: string; + private readonly projectConfigurationFilename: string; -export default class GitHubProjectDataSource implements IGitHubRepositoryDataSource { - private readonly loginsDataSource: IGitHubLoginDataSource - private readonly graphQlClient: IGitHubGraphQLClient - private readonly repositoryNameSuffix: string - private readonly projectConfigurationFilename: string - constructor(config: { - loginsDataSource: IGitHubLoginDataSource, - graphQlClient: IGitHubGraphQLClient, - repositoryNameSuffix: string, - projectConfigurationFilename: string + loginsDataSource: IGitHubLoginDataSource; + graphQlClient: IGitHubGraphQLClient; + repositoryNameSuffix: string; + projectConfigurationFilename: string; }) { - this.loginsDataSource = config.loginsDataSource - this.graphQlClient = config.graphQlClient - this.repositoryNameSuffix = config.repositoryNameSuffix - this.projectConfigurationFilename = config.projectConfigurationFilename.replace(/\.ya?ml$/, "") + this.loginsDataSource = config.loginsDataSource; + this.graphQlClient = config.graphQlClient; + this.repositoryNameSuffix = config.repositoryNameSuffix; + this.projectConfigurationFilename = + config.projectConfigurationFilename.replace(/\.ya?ml$/, ""); } - + async getRepositories(): Promise { - const logins = await this.loginsDataSource.getLogins() - return await this.getRepositoriesForLogins({ logins }) + const logins = await this.loginsDataSource.getLogins(); + return await this.getRepositoriesForLogins({ logins }); } - - private async getRepositoriesForLogins({ logins }: { logins: string[] }): Promise { - let searchQueries: string[] = [] + + 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`) + searchQueries.push(`"${this.repositoryNameSuffix}" in:name is:private`); // Search for public repositories belonging to a user or organization. - searchQueries = searchQueries.concat(logins.map(login => { - return `"${this.repositoryNameSuffix}" in:name user:${login} is:public` - })) - return await Promise.all(searchQueries.map(searchQuery => { - return this.getRepositoriesForSearchQuery({ searchQuery }) - })) - .then(e => e.flat()) - .then(repositories => { - // GitHub's search API does not enable searching for repositories whose name ends with "-openapi", - // only repositories whose names include "openapi" so we filter the results ourselves. - return repositories.filter(repository => { - return repository.name.endsWith(this.repositoryNameSuffix) + searchQueries = searchQueries.concat( + logins.map((login) => { + return `"${this.repositoryNameSuffix}" in:name user:${login} is:public`; }) - }) - .then(repositories => { - // Ensure we don't have duplicates in the resulting repositories. - const uniqueIdentifiers = new Set() - return repositories.filter(repository => { - const identifier = `${repository.owner.login}-${repository.name}` - const alreadyAdded = uniqueIdentifiers.has(identifier) - uniqueIdentifiers.add(identifier) - return !alreadyAdded + ); + return await Promise.all( + searchQueries.map((searchQuery) => { + return this.getRepositoriesForSearchQuery({ searchQuery }); }) - }) - .then(repositories => { - // Map from the internal model to the public model. - return repositories.map(repository => { - return { - name: repository.name, - owner: repository.owner.login, - defaultBranchRef: { - id: repository.defaultBranchRef.target.oid, - name: repository.defaultBranchRef.name - }, - configYml: repository.configYml, - configYaml: repository.configYaml, - branches: repository.branches.edges.map(branch => { - return { - id: branch.node.target.oid, - name: branch.node.name, - files: branch.node.target.tree.entries - } - }), - tags: repository.tags.edges.map(branch => { - return { - id: branch.node.target.oid, - name: branch.node.name, - files: branch.node.target.tree.entries - } - }) - } + ) + .then((e) => e.flat()) + .then((repositories) => { + // GitHub's search API does not enable searching for repositories whose name ends with "-openapi", + // only repositories whose names include "openapi" so we filter the results ourselves. + return repositories.filter((repository) => { + return repository.name.endsWith(this.repositoryNameSuffix); + }); }) - }) + .then((repositories) => { + // Ensure we don't have duplicates in the resulting repositories. + const uniqueIdentifiers = new Set(); + return repositories.filter((repository) => { + const identifier = `${repository.owner.login}-${repository.name}`; + const alreadyAdded = uniqueIdentifiers.has(identifier); + uniqueIdentifiers.add(identifier); + return !alreadyAdded; + }); + }) + .then((repositories) => { + // Map from the internal model to the public model. + return repositories.map((repository) => { + return { + name: repository.name, + owner: repository.owner.login, + defaultBranchRef: { + id: repository.defaultBranchRef.target.oid, + name: repository.defaultBranchRef.name, + }, + configYml: repository.configYml, + configYaml: repository.configYaml, + branches: repository.branches.edges.map((branch) => { + const baseRef = + branch.node.associatedPullRequests?.nodes[0]?.baseRefName; + return { + id: branch.node.target.oid, + name: branch.node.name, + baseRef: baseRef || undefined, + files: branch.node.target.tree.entries, + }; + }), + tags: repository.tags.edges.map((branch) => { + return { + id: branch.node.target.oid, + name: branch.node.name, + files: branch.node.target.tree.entries, + }; + }), + }; + }); + }); } - + private async getRepositoriesForSearchQuery(params: { - searchQuery: string, - cursor?: string + searchQuery: string; + cursor?: string; }): Promise { - const { searchQuery, cursor } = params + const { searchQuery, cursor } = params; const request = { query: ` query Repositories($searchQuery: String!, $cursor: String) { @@ -191,6 +210,11 @@ export default class GitHubProjectDataSource implements IGitHubRepositoryDataSou } } } + associatedPullRequests(first: 1, states: [OPEN, MERGED]) { + nodes { + baseRefName + } + } } } } @@ -202,23 +226,23 @@ export default class GitHubProjectDataSource implements IGitHubRepositoryDataSou } } `, - variables: { searchQuery, cursor } - } - const response = await this.graphQlClient.graphql(request) + variables: { searchQuery, cursor }, + }; + const response = await this.graphQlClient.graphql(request); if (!response.search || !response.search.results) { - return [] + return []; } - const pageInfo = response.search.pageInfo + const pageInfo = response.search.pageInfo; if (!pageInfo) { - return response.search.results + return response.search.results; } if (!pageInfo.hasNextPage || !pageInfo.endCursor) { - return response.search.results + return response.search.results; } const nextResults = await this.getRepositoriesForSearchQuery({ searchQuery, - cursor: pageInfo.endCursor - }) - return response.search.results.concat(nextResults) + cursor: pageInfo.endCursor, + }); + return response.search.results.concat(nextResults); } } diff --git a/src/features/projects/domain/IGitHubRepositoryDataSource.ts b/src/features/projects/domain/IGitHubRepositoryDataSource.ts index b48ff43e..e9be7156 100644 --- a/src/features/projects/domain/IGitHubRepositoryDataSource.ts +++ b/src/features/projects/domain/IGitHubRepositoryDataSource.ts @@ -18,6 +18,7 @@ export type GitHubRepository = { export type GitHubRepositoryRef = { readonly id: string readonly name: string + readonly baseRef?: string readonly files: { readonly name: string }[] diff --git a/src/features/projects/domain/Version.ts b/src/features/projects/domain/Version.ts index f6b69989..59659cb8 100644 --- a/src/features/projects/domain/Version.ts +++ b/src/features/projects/domain/Version.ts @@ -6,7 +6,8 @@ export const VersionSchema = z.object({ name: z.string(), specifications: OpenApiSpecificationSchema.array(), url: z.string().optional(), - isDefault: z.boolean().default(false) + isDefault: z.boolean().default(false), + baseRef: z.string().optional() }) type Version = z.infer diff --git a/src/features/projects/view/toolbar/Selector.tsx b/src/features/projects/view/toolbar/Selector.tsx index 4607d1f0..62443425 100644 --- a/src/features/projects/view/toolbar/Selector.tsx +++ b/src/features/projects/view/toolbar/Selector.tsx @@ -11,6 +11,7 @@ import MenuItemHover from "@/common/ui/MenuItemHover" interface SelectorItem { readonly id: string readonly name: string + readonly baseRef?: string } const Selector = ({ diff --git a/src/features/sidebar/data/useDiffbarOpen.ts b/src/features/sidebar/data/useDiffbarOpen.ts new file mode 100644 index 00000000..2bb6de87 --- /dev/null +++ b/src/features/sidebar/data/useDiffbarOpen.ts @@ -0,0 +1,5 @@ +import { useSessionStorage } from "usehooks-ts" + +export default function useDiffbarOpen() { + return useSessionStorage("isDiffbarOpen", true) +} \ No newline at end of file diff --git a/src/features/sidebar/view/SecondarySplitHeader.tsx b/src/features/sidebar/view/SecondarySplitHeader.tsx index 23a2e8b4..e81543ff 100644 --- a/src/features/sidebar/view/SecondarySplitHeader.tsx +++ b/src/features/sidebar/view/SecondarySplitHeader.tsx @@ -4,9 +4,10 @@ import { useState, useEffect, useContext } from "react" import { useSessionStorage } from "usehooks-ts" import { Box, IconButton, Stack, Tooltip, Collapse } from "@mui/material" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faBars, faChevronLeft } from "@fortawesome/free-solid-svg-icons" +import { faBars, faChevronLeft, faChevronRight, faCodeCompare } from "@fortawesome/free-solid-svg-icons" import { isMac as checkIsMac, SidebarTogglableContext } from "@/common" import { useSidebarOpen } from "@/features/sidebar/data" +import useDiffbarOpen from "@/features/sidebar/data/useDiffbarOpen" import ToggleMobileToolbarButton from "./internal/secondary/ToggleMobileToolbarButton" const SecondarySplitHeader = ({ @@ -17,6 +18,7 @@ const SecondarySplitHeader = ({ children?: React.ReactNode }) => { const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() + const [isDiffbarOpen, setDiffbarOpen] = useDiffbarOpen() const [isMobileToolbarVisible, setMobileToolbarVisible] = useSessionStorage("isMobileToolbarVisible", true) return ( @@ -35,6 +37,10 @@ const SecondarySplitHeader = ({ {children} + {mobileToolbar && ) } + +const ToggleDiffButton = ({ + isDiffbarOpen, + onClick +}: { + isDiffbarOpen: boolean, + onClick: (isDiffbarOpen: boolean) => void +}) => { + const [isMac, setIsMac] = useState(false) + useEffect(() => { + // checkIsMac uses window so we delay the check. + setIsMac(checkIsMac()) + }, [isMac, setIsMac]) + const isSidebarTogglable = useContext(SidebarTogglableContext) + const openCloseKeyboardShortcut = `(${isMac ? "⌘" : "^"} + K)` + const tooltip = isDiffbarOpen ? "Hide Diff" : "Show Diff" + return ( + + + onClick(!isDiffbarOpen)} + edge="end" + > + + + + + ) +} diff --git a/src/features/sidebar/view/SplitView.tsx b/src/features/sidebar/view/SplitView.tsx index 04bd4eb9..dffaefc6 100644 --- a/src/features/sidebar/view/SplitView.tsx +++ b/src/features/sidebar/view/SplitView.tsx @@ -1,6 +1,7 @@ import ClientSplitView from "./internal/ClientSplitView" import BaseSidebar from "./internal/sidebar/Sidebar" import ProjectList from "./internal/sidebar/projects/ProjectList" +import DiffContent from "./internal/diffbar/DiffContent" import { env } from "@/common" const SITE_NAME = env.getOrThrow("FRAMNA_DOCS_TITLE") @@ -8,7 +9,7 @@ const HELP_URL = env.get("FRAMNA_DOCS_HELP_URL") const SplitView = ({ children }: { children?: React.ReactNode }) => { return ( - }> + } diffContent={}> {children} ) diff --git a/src/features/sidebar/view/internal/ClientSplitView.tsx b/src/features/sidebar/view/internal/ClientSplitView.tsx index 38c6735f..2b9e844f 100644 --- a/src/features/sidebar/view/internal/ClientSplitView.tsx +++ b/src/features/sidebar/view/internal/ClientSplitView.tsx @@ -1,40 +1,61 @@ -"use client" +"use client"; -import { useEffect, useContext } from "react" -import { Stack, useMediaQuery, useTheme } from "@mui/material" -import { isMac, useKeyboardShortcut, SidebarTogglableContext } from "@/common" -import { useSidebarOpen } from "../../data" -import PrimaryContainer from "./primary/Container" -import SecondaryContainer from "./secondary/Container" +import { useEffect, useContext, useState } from "react"; +import { Stack, useMediaQuery, useTheme } from "@mui/material"; +import { isMac, useKeyboardShortcut, SidebarTogglableContext } from "@/common"; +import { useSidebarOpen } from "../../data"; +import useDiffbarOpen from "../../data/useDiffbarOpen"; +import PrimaryContainer from "./primary/Container"; +import SecondaryContainer from "./secondary/Container"; +import DiffContainer from "./tertiary/Container"; const ClientSplitView = ({ sidebar, - children + children, + diffContent, }: { - sidebar: React.ReactNode - children?: React.ReactNode + sidebar: React.ReactNode; + children?: React.ReactNode; + diffContent?: React.ReactNode; }) => { - const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() - const isSidebarTogglable = useContext(SidebarTogglableContext) - const theme = useTheme() + const [isSidebarOpen, setSidebarOpen] = useSidebarOpen(); + const [isDiffbarOpen, setDiffbarOpen] = useDiffbarOpen(); + const isSidebarTogglable = useContext(SidebarTogglableContext); + const theme = useTheme(); // Determine if the screen size is small or larger - const isSM = useMediaQuery(theme.breakpoints.up("sm")) + const isSM = useMediaQuery(theme.breakpoints.up("sm")); useEffect(() => { if (!isSidebarTogglable && !isSidebarOpen) { - setSidebarOpen(true) + setSidebarOpen(true); } - }, [isSidebarOpen, isSidebarTogglable, setSidebarOpen]) - useKeyboardShortcut(event => { - const isActionKey = isMac() ? event.metaKey : event.ctrlKey - if (isActionKey && event.key === ".") { - event.preventDefault() - if (isSidebarTogglable) { - setSidebarOpen(!isSidebarOpen) + }, [isSidebarOpen, isSidebarTogglable, setSidebarOpen]); + useKeyboardShortcut( + (event) => { + const isActionKey = isMac() ? event.metaKey : event.ctrlKey; + if (isActionKey && event.key === ".") { + event.preventDefault(); + if (isSidebarTogglable) { + setSidebarOpen(!isSidebarOpen); + } } - } - }, [isSidebarTogglable, setSidebarOpen]) - const sidebarWidth = 320 + }, + [isSidebarTogglable, setSidebarOpen] + ); + + useKeyboardShortcut( + (event) => { + const isActionKey = isMac() ? event.metaKey : event.ctrlKey; + if (isActionKey && event.key === "k") { + event.preventDefault(); + setDiffbarOpen(!isDiffbarOpen); + } + }, + [isDiffbarOpen ,setDiffbarOpen] + ); + + const sidebarWidth = 320; + const diffWidth = 320; return ( @@ -45,11 +66,24 @@ const ClientSplitView = ({ > {sidebar} - + {children} + setDiffbarOpen(false)} + > + {diffContent} + - ) -} + ); +}; -export default ClientSplitView +export default ClientSplitView; diff --git a/src/features/sidebar/view/internal/diffbar/DiffContent.tsx b/src/features/sidebar/view/internal/diffbar/DiffContent.tsx new file mode 100644 index 00000000..8c9e46d7 --- /dev/null +++ b/src/features/sidebar/view/internal/diffbar/DiffContent.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { Box } from "@mui/material"; +import { useState, useEffect } from "react"; +import { useProjectSelection } from "@/features/projects/data"; +import DiffHeader from "./components/DiffHeader"; +import DiffList from "./components/DiffList"; +import DiffDialog from "./components/DiffDialog"; + +const DiffContent = () => { + const { project, specification, version } = useProjectSelection(); + const [fromBranch, setFromBranch] = useState(""); + const [toBranch, setToBranch] = useState(""); + const [data, setData] = useState(null); + const [selectedChange, setSelectedChange] = useState(null); + const [loading, setLoading] = useState(false); + + + useEffect(() => { + setData(null); + if (version !== undefined) { + setFromBranch(version.baseRef || ""); + setToBranch(version.id); + } + }, [project, specification, version]); + + useEffect(() => { + const compare = async () => { + if (project && specification && fromBranch && toBranch) { + setLoading(true); + const res = await fetch( + `/api/diff/${project.owner}/${project.name}/${specification.id}?from=${fromBranch}&to=${toBranch}` + ); + setLoading(false); + const result = await res.json(); + setData(result); + } + }; + compare(); + }, [toBranch, fromBranch]); + + const changes = data?.changes || []; + const versions = project?.versions || []; + + const closeModal = () => setSelectedChange(null); + + return ( + + setFromBranch(ref)} + /> + + + setSelectedChange(i)} + /> + + + + + ); +}; + +export default DiffContent; diff --git a/src/features/sidebar/view/internal/diffbar/components/DiffDialog.tsx b/src/features/sidebar/view/internal/diffbar/components/DiffDialog.tsx new file mode 100644 index 00000000..e1e20c6f --- /dev/null +++ b/src/features/sidebar/view/internal/diffbar/components/DiffDialog.tsx @@ -0,0 +1,78 @@ +"use client"; + +import React from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Box, + Typography, +} from "@mui/material"; +import { softPaperSx } from "@/common/theme/theme"; +import MonoQuotedText from "./MonoQuotedText"; + +const DiffDialog = ({ + open, + change, + onClose, +}: { + open: boolean; + change: any | null; + onClose: () => void; +}) => { + return ( + theme.palette.background.default, + }, + }, + }} + > + Change Details + + {change?.path && ( + + Path: + + {change.path} + + + )} + {change?.text && ( + + Description: + + {typeof change.text === "string" ? ( + + ) : ( + change.text + )} + + + )} + + + + + + ); +}; + +export default DiffDialog; diff --git a/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx b/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx new file mode 100644 index 00000000..30dd1152 --- /dev/null +++ b/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx @@ -0,0 +1,33 @@ +"use client"; + +import React from "react"; +import { Box, Typography, FormControl, Select, MenuItem } from "@mui/material"; + +const DiffHeader = ({ + versions, + fromBranch, + onChange, +}: { + versions: any[]; + fromBranch: string; + onChange: (ref: string) => void; +}) => { + return ( + + + + Added changes from main: + + + + + ); +}; + +export default DiffHeader; diff --git a/src/features/sidebar/view/internal/diffbar/components/DiffList.tsx b/src/features/sidebar/view/internal/diffbar/components/DiffList.tsx new file mode 100644 index 00000000..e8923422 --- /dev/null +++ b/src/features/sidebar/view/internal/diffbar/components/DiffList.tsx @@ -0,0 +1,47 @@ +"use client"; + +import React from "react"; +import { Box, Typography } from "@mui/material"; +import PopulatedDiffList from "./PopulatedDiffList"; + +const DiffList = ({ + changes, + loading, + data, + selectedChange, + onClick, +}: { + changes: any[]; + loading: boolean; + data: boolean; + selectedChange: number | null; + onClick: (i: number) => void; +}) => { + if (loading) { + return ( + + + Loading changes... + + + ); + } else if (!loading && data && changes.length === 0) { + return ( + + + Non comparable + + + ); + } + + return ( + + ); +}; + +export default DiffList; diff --git a/src/features/sidebar/view/internal/diffbar/components/DiffListItem.tsx b/src/features/sidebar/view/internal/diffbar/components/DiffListItem.tsx new file mode 100644 index 00000000..1130e2c4 --- /dev/null +++ b/src/features/sidebar/view/internal/diffbar/components/DiffListItem.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React from "react"; +import { Box, Typography, ListItem, ListItemButton, Stack } from "@mui/material"; +import MenuItemHover from "@/common/ui/MenuItemHover"; +import MonoQuotedText from "./MonoQuotedText"; + +const DiffListItem = ({ + path, + text, + selected, + onClick, +}: { + path?: string; + text?: string; + selected: boolean; + onClick: () => void; +}) => { + return ( + + + + + + {path && ( + + {path} + + )} + {text && ( + + + + )} + + + + + + ); +}; + +export default DiffListItem; diff --git a/src/features/sidebar/view/internal/diffbar/components/MonoQuotedText.tsx b/src/features/sidebar/view/internal/diffbar/components/MonoQuotedText.tsx new file mode 100644 index 00000000..fe882969 --- /dev/null +++ b/src/features/sidebar/view/internal/diffbar/components/MonoQuotedText.tsx @@ -0,0 +1,31 @@ +"use client"; + +import React from "react"; +import { Box } from "@mui/material"; + + +const MonoQuotedText = ({ text }: { text: string }) => { + return ( + <> + {text.split(/(['`])([^'`]+)\1/g).map((part, i) => + i % 3 === 2 ? ( + + {part} + + ) : ( + part + ) + )} + + ); +}; + +export default MonoQuotedText; diff --git a/src/features/sidebar/view/internal/diffbar/components/PopulatedDiffList.tsx b/src/features/sidebar/view/internal/diffbar/components/PopulatedDiffList.tsx new file mode 100644 index 00000000..703f16a8 --- /dev/null +++ b/src/features/sidebar/view/internal/diffbar/components/PopulatedDiffList.tsx @@ -0,0 +1,31 @@ +"use client"; + +import React from "react"; +import SpacedList from "@/common/ui/SpacedList"; +import DiffListItem from "./DiffListItem"; + +const PopulatedDiffList = ({ + changes, + selectedChange, + onClick, +}: { + changes: any[]; + selectedChange: number | null; + onClick: (i: number) => void; +}) => { + return ( + + {changes.map((change, i) => ( + onClick(i)} + /> + ))} + + ); +}; + +export default PopulatedDiffList; diff --git a/src/features/sidebar/view/internal/secondary/Container.tsx b/src/features/sidebar/view/internal/secondary/Container.tsx index 29f2b213..72471eb2 100644 --- a/src/features/sidebar/view/internal/secondary/Container.tsx +++ b/src/features/sidebar/view/internal/secondary/Container.tsx @@ -6,11 +6,15 @@ import CustomTopLoader from "@/common/ui/CustomTopLoader" const SecondaryContainer = ({ sidebarWidth, offsetContent, + diffWidth, + offsetDiffContent, children, isSM, }: { sidebarWidth: number offsetContent: boolean + diffWidth?: number + offsetDiffContent?: boolean children?: React.ReactNode, isSM: boolean, }) => { @@ -21,6 +25,8 @@ const SecondaryContainer = ({ sidebarWidth={isSM ? sidebarWidth : 0} isSidebarOpen={isSM ? offsetContent: false} + diffWidth={isSM ? (diffWidth || 0) : 0} + isDiffOpen={isSM ? (offsetDiffContent || false) : false} sx={{ ...sx }} > {children} @@ -35,33 +41,48 @@ export default SecondaryContainer interface WrapperStackProps { readonly sidebarWidth: number readonly isSidebarOpen: boolean + readonly diffWidth: number + readonly isDiffOpen: 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, + shouldForwardProp: (prop) => prop !== "isSidebarOpen" && prop !== "sidebarWidth" && prop !== "diffWidth" && prop !== "isDiffOpen" +})(({ theme, sidebarWidth, isSidebarOpen, diffWidth, isDiffOpen }) => { + + + return { + transition: theme.transitions.create(["margin", "width"], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen }), - marginLeft: 0 - }) -})) + marginLeft: isSidebarOpen ? 0 : `-${sidebarWidth}px`, + ...(isSidebarOpen && { + transition: theme.transitions.create(["margin", "width"], { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + }), + ...(isDiffOpen && { + transition: theme.transitions.create(["margin", "width"], { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.enteringScreen, + }), + }) + }; +}) const InnerSecondaryContainer = ({ sidebarWidth, isSidebarOpen, + diffWidth, + isDiffOpen, children, sx }: { sidebarWidth: number isSidebarOpen: boolean + diffWidth: number + isDiffOpen: boolean children: React.ReactNode sx?: SxProps }) => { @@ -71,6 +92,8 @@ const InnerSecondaryContainer = ({ spacing={0} sidebarWidth={sidebarWidth} isSidebarOpen={isSidebarOpen} + diffWidth={diffWidth} + isDiffOpen={isDiffOpen} sx={{ ...sx, width: "100%", overflowY: "auto" }} > diff --git a/src/features/sidebar/view/internal/tertiary/Container.tsx b/src/features/sidebar/view/internal/tertiary/Container.tsx new file mode 100644 index 00000000..6bbe6d64 --- /dev/null +++ b/src/features/sidebar/view/internal/tertiary/Container.tsx @@ -0,0 +1,39 @@ +'use client' + +import { SxProps } from "@mui/system" +import { Box } from "@mui/material" +import { useTheme } from "@mui/material/styles" + +const DiffContainer = ({ + width, + isOpen, + onClose, + children +}: { + width: number + isOpen: boolean + onClose?: () => void + children?: React.ReactNode +}) => { + const theme = useTheme() + + if (!isOpen) { + return null + } + + return ( + + {children} + + ) +} + +export default DiffContainer \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index acc1e226..fb0dc7f4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { From 53358e70ecc5b35ce3f81e07f3aefb1ddfe61649 Mon Sep 17 00:00:00 2001 From: Oscar Date: Tue, 21 Oct 2025 14:06:02 +0200 Subject: [PATCH 2/4] CoPilot review updates --- .../[owner]/[repository]/[...path]/route.ts | 2 - .../view/toolbar/TrailingToolbarItem.tsx | 3 +- .../sidebar/view/SecondarySplitHeader.tsx | 209 ++++++++++-------- .../sidebar/view/internal/ClientSplitView.tsx | 2 +- .../view/internal/diffbar/DiffContent.tsx | 15 +- .../diffbar/components/DiffDialog.tsx | 11 +- .../diffbar/components/DiffHeader.tsx | 8 +- .../internal/diffbar/components/DiffList.tsx | 7 +- .../diffbar/components/PopulatedDiffList.tsx | 7 +- .../view/internal/tertiary/Container.tsx | 2 +- 10 files changed, 163 insertions(+), 103 deletions(-) diff --git a/src/app/api/diff/[owner]/[repository]/[...path]/route.ts b/src/app/api/diff/[owner]/[repository]/[...path]/route.ts index 36f6b6ef..8bfdc88e 100644 --- a/src/app/api/diff/[owner]/[repository]/[...path]/route.ts +++ b/src/app/api/diff/[owner]/[repository]/[...path]/route.ts @@ -42,8 +42,6 @@ export async function GET(req: NextRequest, { params }: { params: Promise { } {specification.editURL && - + { } + ) diff --git a/src/features/sidebar/view/SecondarySplitHeader.tsx b/src/features/sidebar/view/SecondarySplitHeader.tsx index e81543ff..a45e7f65 100644 --- a/src/features/sidebar/view/SecondarySplitHeader.tsx +++ b/src/features/sidebar/view/SecondarySplitHeader.tsx @@ -1,138 +1,165 @@ -"use client" +"use client"; -import { useState, useEffect, useContext } from "react" -import { useSessionStorage } from "usehooks-ts" -import { Box, IconButton, Stack, Tooltip, Collapse } from "@mui/material" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" -import { faBars, faChevronLeft, faChevronRight, faCodeCompare } from "@fortawesome/free-solid-svg-icons" -import { isMac as checkIsMac, SidebarTogglableContext } from "@/common" -import { useSidebarOpen } from "@/features/sidebar/data" -import useDiffbarOpen from "@/features/sidebar/data/useDiffbarOpen" -import ToggleMobileToolbarButton from "./internal/secondary/ToggleMobileToolbarButton" +import { useState, useEffect, useContext } from "react"; +import { useSessionStorage } from "usehooks-ts"; +import { + Box, + IconButton, + Stack, + Tooltip, + Collapse, + Divider, +} from "@mui/material"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { + faBars, + faChevronLeft, + faChevronRight, + faCodeCompare, +} from "@fortawesome/free-solid-svg-icons"; +import { isMac as checkIsMac, SidebarTogglableContext } from "@/common"; +import { useSidebarOpen } from "@/features/sidebar/data"; +import useDiffbarOpen from "@/features/sidebar/data/useDiffbarOpen"; +import ToggleMobileToolbarButton from "./internal/secondary/ToggleMobileToolbarButton"; const SecondarySplitHeader = ({ mobileToolbar, - children + children, }: { - mobileToolbar?: React.ReactNode - children?: React.ReactNode + mobileToolbar?: React.ReactNode; + children?: React.ReactNode; }) => { - const [isSidebarOpen, setSidebarOpen] = useSidebarOpen() - const [isDiffbarOpen, setDiffbarOpen] = useDiffbarOpen() - const [isMobileToolbarVisible, setMobileToolbarVisible] = useSessionStorage("isMobileToolbarVisible", true) + const [isSidebarOpen, setSidebarOpen] = useSidebarOpen(); + const [isDiffbarOpen, setDiffbarOpen] = useDiffbarOpen(); + const [isMobileToolbarVisible, setMobileToolbarVisible] = useSessionStorage( + "isMobileToolbarVisible", + true + ); return ( - + - + {children} - - {mobileToolbar && + {mobileToolbar && ( setMobileToolbarVisible(!isMobileToolbarVisible) } + onToggle={() => + setMobileToolbarVisible(!isMobileToolbarVisible) + } /> - } + )} + - {mobileToolbar && + {mobileToolbar && ( - + {mobileToolbar} - } + )} - ) -} + ); +}; -export default SecondarySplitHeader +export default SecondarySplitHeader; const ToggleSidebarButton = ({ isSidebarOpen, - onClick + onClick, }: { - isSidebarOpen: boolean, - onClick: (isSidebarOpen: boolean) => void + isSidebarOpen: boolean; + onClick: (isSidebarOpen: boolean) => void; }) => { - const [isMac, setIsMac] = useState(false) + const [isMac, setIsMac] = useState(false); useEffect(() => { // checkIsMac uses window so we delay the check. - setIsMac(checkIsMac()) - }, [isMac, setIsMac]) - const isSidebarTogglable = useContext(SidebarTogglableContext) - const openCloseKeyboardShortcut = `(${isMac ? "⌘" : "^"} + .)` - const tooltip = isSidebarOpen ? "Show Projects" : "Hide Projects" + setIsMac(checkIsMac()); + }, [isMac, setIsMac]); + const isSidebarTogglable = useContext(SidebarTogglableContext); + const openCloseKeyboardShortcut = `(${isMac ? "⌘" : "^"} + .)`; + const tooltip = isSidebarOpen ? "Show Projects" : "Hide Projects"; return ( - - onClick(!isSidebarOpen)} - edge="start" - > - - - + + onClick(!isSidebarOpen)} + edge="start" + > + + + - ) -} + ); +}; const ToggleDiffButton = ({ isDiffbarOpen, - onClick + onClick, }: { - isDiffbarOpen: boolean, - onClick: (isDiffbarOpen: boolean) => void + isDiffbarOpen: boolean; + onClick: (isDiffbarOpen: boolean) => void; }) => { - const [isMac, setIsMac] = useState(false) + const [isMac, setIsMac] = useState(false); useEffect(() => { // checkIsMac uses window so we delay the check. - setIsMac(checkIsMac()) - }, [isMac, setIsMac]) - const isSidebarTogglable = useContext(SidebarTogglableContext) - const openCloseKeyboardShortcut = `(${isMac ? "⌘" : "^"} + K)` - const tooltip = isDiffbarOpen ? "Hide Diff" : "Show Diff" + setIsMac(checkIsMac()); + }, [isMac, setIsMac]); + const isSidebarTogglable = useContext(SidebarTogglableContext); + const openCloseKeyboardShortcut = `(${isMac ? "⌘" : "^"} + K)`; + const tooltip = isDiffbarOpen ? "Hide Diff" : "Show Diff"; return ( - - onClick(!isDiffbarOpen)} - edge="end" - > - - - + + + onClick(!isDiffbarOpen)} + edge="end" + > + + + - ) -} + ); +}; diff --git a/src/features/sidebar/view/internal/ClientSplitView.tsx b/src/features/sidebar/view/internal/ClientSplitView.tsx index 2b9e844f..29f1b608 100644 --- a/src/features/sidebar/view/internal/ClientSplitView.tsx +++ b/src/features/sidebar/view/internal/ClientSplitView.tsx @@ -51,7 +51,7 @@ const ClientSplitView = ({ setDiffbarOpen(!isDiffbarOpen); } }, - [isDiffbarOpen ,setDiffbarOpen] + [isDiffbarOpen, setDiffbarOpen] ); const sidebarWidth = 320; diff --git a/src/features/sidebar/view/internal/diffbar/DiffContent.tsx b/src/features/sidebar/view/internal/diffbar/DiffContent.tsx index 8c9e46d7..0689d432 100644 --- a/src/features/sidebar/view/internal/diffbar/DiffContent.tsx +++ b/src/features/sidebar/view/internal/diffbar/DiffContent.tsx @@ -7,11 +7,22 @@ import DiffHeader from "./components/DiffHeader"; import DiffList from "./components/DiffList"; import DiffDialog from "./components/DiffDialog"; +interface DiffChange { + path?: string; + text?: string; +} + +interface DiffData { + from: string; + to: string; + changes: DiffChange[]; +} + const DiffContent = () => { const { project, specification, version } = useProjectSelection(); const [fromBranch, setFromBranch] = useState(""); const [toBranch, setToBranch] = useState(""); - const [data, setData] = useState(null); + const [data, setData] = useState(null); const [selectedChange, setSelectedChange] = useState(null); const [loading, setLoading] = useState(false); @@ -61,7 +72,7 @@ const DiffContent = () => { setSelectedChange(i)} /> diff --git a/src/features/sidebar/view/internal/diffbar/components/DiffDialog.tsx b/src/features/sidebar/view/internal/diffbar/components/DiffDialog.tsx index e1e20c6f..34b645e5 100644 --- a/src/features/sidebar/view/internal/diffbar/components/DiffDialog.tsx +++ b/src/features/sidebar/view/internal/diffbar/components/DiffDialog.tsx @@ -13,13 +13,18 @@ import { import { softPaperSx } from "@/common/theme/theme"; import MonoQuotedText from "./MonoQuotedText"; +interface ChangeDetails { + path?: string; + text?: string | React.ReactNode; +} + const DiffDialog = ({ open, change, onClose, }: { open: boolean; - change: any | null; + change: ChangeDetails | null; onClose: () => void; }) => { return ( @@ -57,7 +62,9 @@ const DiffDialog = ({ )} {change?.text && ( - Description: + + Description: + {typeof change.text === "string" ? ( diff --git a/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx b/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx index 30dd1152..a094276f 100644 --- a/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx +++ b/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx @@ -3,12 +3,18 @@ import React from "react"; import { Box, Typography, FormControl, Select, MenuItem } from "@mui/material"; +interface Version { + id: string; + name: string; +} + + const DiffHeader = ({ versions, fromBranch, onChange, }: { - versions: any[]; + versions: Version[]; fromBranch: string; onChange: (ref: string) => void; }) => { diff --git a/src/features/sidebar/view/internal/diffbar/components/DiffList.tsx b/src/features/sidebar/view/internal/diffbar/components/DiffList.tsx index e8923422..b54e3f02 100644 --- a/src/features/sidebar/view/internal/diffbar/components/DiffList.tsx +++ b/src/features/sidebar/view/internal/diffbar/components/DiffList.tsx @@ -4,6 +4,11 @@ import React from "react"; import { Box, Typography } from "@mui/material"; import PopulatedDiffList from "./PopulatedDiffList"; +interface Change { + path?: string; + text?: string; +} + const DiffList = ({ changes, loading, @@ -11,7 +16,7 @@ const DiffList = ({ selectedChange, onClick, }: { - changes: any[]; + changes: Change[]; loading: boolean; data: boolean; selectedChange: number | null; diff --git a/src/features/sidebar/view/internal/diffbar/components/PopulatedDiffList.tsx b/src/features/sidebar/view/internal/diffbar/components/PopulatedDiffList.tsx index 703f16a8..f2fa8990 100644 --- a/src/features/sidebar/view/internal/diffbar/components/PopulatedDiffList.tsx +++ b/src/features/sidebar/view/internal/diffbar/components/PopulatedDiffList.tsx @@ -4,12 +4,17 @@ import React from "react"; import SpacedList from "@/common/ui/SpacedList"; import DiffListItem from "./DiffListItem"; +interface Change { + path?: string; + text?: string; +} + const PopulatedDiffList = ({ changes, selectedChange, onClick, }: { - changes: any[]; + changes: Change[]; selectedChange: number | null; onClick: (i: number) => void; }) => { diff --git a/src/features/sidebar/view/internal/tertiary/Container.tsx b/src/features/sidebar/view/internal/tertiary/Container.tsx index 6bbe6d64..5512016d 100644 --- a/src/features/sidebar/view/internal/tertiary/Container.tsx +++ b/src/features/sidebar/view/internal/tertiary/Container.tsx @@ -26,7 +26,7 @@ const DiffContainer = ({ sx={{ width: width, height: "100%", - borderLeft: `1px ${theme.palette.divider}`, + borderLeft: `1px ${theme.palette.divider}`, backgroundColor: theme.palette.background.default, flexShrink: 0, }} From dfc5c95c30bab7614e922f2fbb7535f8e0b7d1af Mon Sep 17 00:00:00 2001 From: Oscar Date: Tue, 21 Oct 2025 14:35:53 +0200 Subject: [PATCH 3/4] Import update: - verision logic updated and moved into the second useEffect, so that dependencies can be added without getting errors when fetching data. Otherwise it's typesafety updates --- src/common/ui/SpacedList.tsx | 2 +- .../sidebar/view/internal/ClientSplitView.tsx | 2 +- .../view/internal/diffbar/DiffContent.tsx | 25 +++++++++++-------- .../diffbar/components/DiffHeader.tsx | 2 +- .../view/internal/tertiary/Container.tsx | 4 +-- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/common/ui/SpacedList.tsx b/src/common/ui/SpacedList.tsx index 8149003e..6fb96dcc 100644 --- a/src/common/ui/SpacedList.tsx +++ b/src/common/ui/SpacedList.tsx @@ -13,7 +13,7 @@ const SpacedList = ({ return ( {React.Children.map(children, (child, idx) => { - const baseKey = (child as any)?.key ?? "idx"; + const baseKey = (child as React.ReactElement)?.key ?? "idx"; const key = `${String(baseKey)}-${idx}`; return ( { const [selectedChange, setSelectedChange] = useState(null); const [loading, setLoading] = useState(false); - useEffect(() => { - setData(null); - if (version !== undefined) { - setFromBranch(version.baseRef || ""); - setToBranch(version.id); - } - }, [project, specification, version]); + setData(null); + + let currentFromBranch = fromBranch; + let currentToBranch = toBranch; + + if (version !== undefined) { + currentFromBranch = version.baseRef || ""; + currentToBranch = version.id; + setFromBranch(version.baseRef || ""); + setToBranch(version.id); + } - useEffect(() => { const compare = async () => { - if (project && specification && fromBranch && toBranch) { + if (project && specification && currentFromBranch && currentToBranch) { setLoading(true); const res = await fetch( - `/api/diff/${project.owner}/${project.name}/${specification.id}?from=${fromBranch}&to=${toBranch}` + `/api/diff/${project.owner}/${project.name}/${specification.id}?from=${currentFromBranch}&to=${currentToBranch}` ); setLoading(false); const result = await res.json(); @@ -48,7 +51,7 @@ const DiffContent = () => { } }; compare(); - }, [toBranch, fromBranch]); + }, [toBranch, fromBranch, project, specification, version]); const changes = data?.changes || []; const versions = project?.versions || []; diff --git a/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx b/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx index a094276f..dbde6242 100644 --- a/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx +++ b/src/features/sidebar/view/internal/diffbar/components/DiffHeader.tsx @@ -25,7 +25,7 @@ const DiffHeader = ({ Added changes from main: