GitFOSS
feat(pull_requests): add PullRequestDetailsView
+ 274
- 29
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1665669388427,
+  "_generatedAtUnix": 1665674159579,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -114,7 +114,7 @@
       "pathSource": "./app/views/repository/RepositoryCreateView.tsx"
     },
     "RepositoryDetailsView": {
-      "hash": "6a0f194e53255248f1fbc10172af6a87f0a8d1c6",
+      "hash": "29d660c09e9ae686b6e7c4c16d171e27fbd9b13c",
       "pathSource": "./app/views/repository/RepositoryDetailsView.tsx"
     },
     "RepositoryExploreView": {

...
@@ -133,6 +133,10 @@
       "hash": "09943e3deb491b00773600dc5510bc6a76cd680b",
       "pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestCreateView.tsx"
     },
+    "RepositoryPullRequestDetailsView": {
+      "hash": "1ecb2513fa8cee3e027d71aea376466657bb1d56",
+      "pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestDetailsView.tsx"
+    },
     "RepositoryPullRequestsView": {
       "hash": "1ad2fff2166e8417a428852f7f4579d4f2ece30d",
       "pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestsView.tsx"

new file
app/components/InlineCode.tsx
@@ -0,0 +1,76 @@
+// 3rd-party
+import React, { VFC } from "react";
+import styled, { css } from "styled-components";
+// app
+import type { WithThemeSchemeProp } from "../types";
+import { NamedColors } from "../utils/style";
+
+export interface InlineCodeProps extends React.HTMLAttributes<HTMLPreElement> {
+  children: string | JSX.Element;
+  fluid?: boolean;
+  codeProps?: React.HTMLAttributes<HTMLElement>;
+}
+
+export const InlineCode: VFC<InlineCodeProps & WithThemeSchemeProp> = ({
+  themeScheme,
+  children,
+  codeProps = {},
+  fluid = false,
+  ...preProps
+}) => {
+  return (
+    <StyledPreTag fluid={fluid} themeScheme={themeScheme} {...preProps}>
+      <StyledCodeTag fluid={fluid} {...codeProps}>
+        {children}
+      </StyledCodeTag>
+    </StyledPreTag>
+  );
+};
+
+const StyledPreTag = styled.pre<
+  WithThemeSchemeProp & Pick<InlineCodeProps, "fluid">
+>`
+  display: inline-block;
+  margin: 0 !important;
+  padding: 0 !important;
+
+  box-shadow: none !important;
+  text-shadow: none !important;
+
+  border-radius: 8px;
+
+  ${({ themeScheme }) => css`
+    background-color: ${NamedColors.CARD[themeScheme]} !important;
+    border: 1px solid ${NamedColors.BORDER_DEFAULT[themeScheme]} !important;
+  `};
+
+  ${({ fluid }) =>
+    fluid &&
+    css`
+      display: flex;
+      flex-flow: row nowrap;
+      align-items: center;
+      justify-content: flex-start;
+      min-height: 44px; // to account for the scrollbar overlayed on-top of code
+      width: 100%;
+      max-width: 100%;
+      overflow: auto;
+    `};
+`;
+
+const StyledCodeTag = styled.code<Pick<InlineCodeProps, "fluid">>`
+  display: block;
+
+  min-height: 20px;
+  width: 100%;
+  padding: 4px 8px;
+
+  font-size: 16px;
+  border-radius: 4px !important;
+
+  ${({ fluid }) =>
+    fluid &&
+    css`
+      padding: 8px 16px;
+    `};
+`;

app/components/index.ts
@@ -2,6 +2,7 @@ export { Button, ButtonAnchor } from "./Button.styled";
 export { Card } from "./Card.styled";
 export { Grid } from "./Grid";
 export * as Icons from "./icons";
+export { InlineCode } from "./InlineCode";
 export { IslandWrapper } from "./IslandWrapper.styled";
 export { Layout } from "./Layout";
 export { MarkdownToJsx } from "./MarkdownToJsx";

app/controllers/repositoryPullRequests/getRepositoryPullRequestDetailsView.ts
@@ -9,16 +9,16 @@ import { makePullRequestService } from "../../services/pullRequest";
 import { makeRepositoryService } from "../../services/repository";
 import { makeUsersService } from "../../services/user";
 // app views
-import RepositoryPullRequestsView, {
-  RepositoryPullRequestsViewProps,
-} from "../../views/repositoryPullRequests/RepositoryPullRequestsView";
+import RepositoryPullRequestDetailsView, {
+  RepositoryPullRequestDetailsViewProps,
+} from "../../views/repositoryPullRequests/RepositoryPullRequestDetailsView";
 
 const getRepositoryPullRequestDetailsView: ReqHandler = async (
   request,
   reply
 ) => {
-  const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
+  const { orgSlug, repoSlug, pullUid } =
+    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_DETAILS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

...
@@ -53,14 +53,18 @@ const getRepositoryPullRequestDetailsView: ReqHandler = async (
     }
   }
 
-  const pullRequests = await prService.getPullRequestsInRepository(repo);
+  const pullRequest = await prService.getPullRequestByUid(
+    orgSlug,
+    repoSlug,
+    pullUid
+  );
 
   const reqHandler = reply.makeRequestHandler(request, reply);
-  return reqHandler<RepositoryPullRequestsViewProps>(
-    RepositoryPullRequestsView.name,
+  return reqHandler<RepositoryPullRequestDetailsViewProps>(
+    RepositoryPullRequestDetailsView.name,
     {
       parentOrg,
-      pullRequests,
+      pullRequest,
       repo,
     }
   );

@@ -160,7 +160,7 @@ export interface AppRoutesParams extends IRouteParams {
     params: {
       orgSlug: string;
       repoSlug: string;
-      pullUid: string;
+      pullUid: number;
     };
     body: {
       reason_message: string;

...
@@ -222,21 +222,21 @@ export interface AppRoutesParams extends IRouteParams {
     params: {
       orgSlug: string;
       repoSlug: string;
-      pullUid: string;
+      pullUid: number;
     };
   };
   [AppRoute.REPOSITORY_PULL_REQUEST_DETAILS]: {
     params: {
       orgSlug: string;
       repoSlug: string;
-      pullUid: string;
+      pullUid: number;
     };
   };
   [AppRoute.REPOSITORY_PULL_REQUEST_MERGE_ACTION]: {
     params: {
       orgSlug: string;
       repoSlug: string;
-      pullUid: string;
+      pullUid: number;
     };
     body: {
       merge_summary: string;

...
@@ -247,7 +247,7 @@ export interface AppRoutesParams extends IRouteParams {
     params: {
       orgSlug: string;
       repoSlug: string;
-      pullUid: string;
+      pullUid: number;
     };
     body: {}; // TODO: define this object shape
   };

app/services/pullRequest/getPullRequest.ts -> app/services/pullRequest/getPullRequestById.ts
@@ -8,7 +8,7 @@ import type {
   PullRequestServiceDeps,
 } from "./types";
 
-const getPullRequest: ServiceMethodFactory<
+const getPullRequestById: ServiceMethodFactory<
   PullRequestServiceDeps,
   [string, PullRequestSelectOrIncludes | undefined],
   Promise<PullRequest | null>

...
@@ -25,4 +25,4 @@ const getPullRequest: ServiceMethodFactory<
   };
 };
 
-export default getPullRequest;
+export default getPullRequestById;

new file
app/services/pullRequest/getPullRequestByUid.ts
@@ -0,0 +1,35 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import type { PullRequest } from "@prisma/client";
+// app services
+import { default as makeGetRepository } from "../repository/getRepository";
+// service
+import type {
+  PullRequestSelectOrIncludes,
+  PullRequestServiceDeps,
+} from "./types";
+
+const getPullRequestByUid: ServiceMethodFactory<
+  PullRequestServiceDeps,
+  [string, string, number, PullRequestSelectOrIncludes | undefined],
+  Promise<PullRequest | null>
+> = ({ request }) => {
+  const getRepository = makeGetRepository({ request });
+  return async (orgSlug, repoSlug, pullRequestUid, selectOrIncludes) => {
+    const repo = await getRepository(orgSlug, repoSlug);
+    if (repo == null) return null;
+    const pullRequest = await request.prisma.pullRequest.findUnique({
+      ...(selectOrIncludes || {}),
+      where: {
+        uid_targetRepositoryId: {
+          targetRepositoryId: repo.id,
+          uid: pullRequestUid,
+        },
+      },
+    });
+    return pullRequest;
+  };
+};
+
+export default getPullRequestByUid;

app/services/pullRequest/index.ts
@@ -4,7 +4,8 @@ import { makeService } from "@ethicdevs/react-monolith";
 import type { PullRequestServiceDeps, PullRequestServiceAPI } from "./types";
 // service methods
 import { default as makeCreatePullRequest } from "./createPullRequest";
-import { default as makeGetPullRequest } from "./getPullRequest";
+import { default as makeGetPullRequestById } from "./getPullRequestById";
+import { default as makeGetPullRequestByUid } from "./getPullRequestByUid";
 import { default as makeGetPullRequestsInRepository } from "./getPullRequestsInRepository";
 
 export const makePullRequestService = makeService<

...
@@ -12,6 +13,7 @@ export const makePullRequestService = makeService<
   PullRequestServiceDeps
 >({
   createPullRequest: makeCreatePullRequest,
-  getPullRequest: makeGetPullRequest,
+  getPullRequestById: makeGetPullRequestById,
+  getPullRequestByUid: makeGetPullRequestByUid,
   getPullRequestsInRepository: makeGetPullRequestsInRepository,
 });

app/services/pullRequest/types.ts
@@ -24,10 +24,16 @@ export type PullRequestSelectOrIncludes =
   | { includes?: Prisma.PullRequestInclude };
 
 export interface PullRequestServiceAPI extends ServiceApiContract {
-  getPullRequest(
+  getPullRequestById(
     pullRequestId: string,
     selectOrIncludes?: PullRequestSelectOrIncludes
   ): Promise<PullRequest | null>;
+  getPullRequestByUid(
+    orgSlug: string,
+    repoSlug: string,
+    pullRequestUid: number,
+    selectOrIncludes?: PullRequestSelectOrIncludes
+  ): Promise<PullRequest | null>;
   getPullRequestsInRepository(
     repository: Repository,
     selectOrIncludes?: PullRequestSelectOrIncludes

app/views/repository/RepositoryDetailsView.tsx
@@ -17,6 +17,7 @@ import { NamedColors } from "../../utils/style";
 import {
   Card,
   Grid,
+  InlineCode,
   IslandWrapper,
   Layout,
   MarkdownToJsx,

...
@@ -205,24 +206,26 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
               )}
               <p>
                 {repo.keywords.map(
-                  (keyword, idx, arr) =>
+                  (keyword, idx, self) =>
                     keyword.trim() !== "" && (
                       <React.Fragment key={[idx, keyword].join(":")}>
                         <span>{keyword}</span>
-                        {idx < arr.length - 1 ? ", " : "."}
+                        {idx === self.length - 1 ? "." : ", "}
                       </React.Fragment>
                     )
                 )}
               </p>
               <p>
                 <strong>HTTP Clone:</strong>
-                <br />
-                <code>{cloneUrl.http}</code>
+                <InlineCode fluid themeScheme={commonProps.themeScheme}>
+                  {cloneUrl.http}
+                </InlineCode>
               </p>
               <p>
                 <strong>SSH Clone:</strong>
-                <br />
-                <code>{cloneUrl.ssh}</code>
+                <InlineCode fluid themeScheme={commonProps.themeScheme}>
+                  {cloneUrl.ssh}
+                </InlineCode>
               </p>
               <p>
                 <strong>Branches:</strong>

...
@@ -242,7 +245,7 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
                         >
                           {branch}
                         </a>
-                        {idx < self.length - 2 ? ", " : "."}
+                        {idx === self.length - 1 ? "." : ", "}
                       </React.Fragment>
                     )
                 )}

...
@@ -252,10 +255,11 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
                 <br />
                 {tags.map(
                   (tag, idx, self) =>
+                    tag != null &&
                     tag.trim() != "" && (
                       <React.Fragment key={tag}>
                         <span>{tag}</span>
-                        {idx < self.length - 2 ? ", " : "."}
+                        {idx === self.length - 1 ? "." : ", "}
                       </React.Fragment>
                     )
                 )}

new file
app/views/repositoryPullRequests/RepositoryPullRequestDetailsView.tsx
@@ -0,0 +1,113 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React from "react";
+// generated via script[generate:prisma]
+import type { Organization, PullRequest } from "@prisma/client";
+// app
+import type { CommonProps, RepositoryWithForkedFromRepo } from "../../types";
+import {
+  Grid,
+  InlineCode,
+  IslandWrapper,
+  Layout,
+  PageWrapper,
+} from "../../components";
+// app islands
+import RepositoryHero from "../../islands/RepositoryHero";
+
+export interface RepositoryPullRequestDetailsViewProps extends CommonProps {
+  parentOrg: Organization;
+  pullRequest: PullRequest;
+  repo: RepositoryWithForkedFromRepo;
+}
+
+const RepositoryPullRequestDetailsView: ReactView<RepositoryPullRequestDetailsViewProps> =
+  ({ commonProps, parentOrg, pullRequest: pr, repo }) => {
+    return (
+      <Layout {...commonProps}>
+        <PageWrapper>
+          <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+            <RepositoryHero
+              forkedFromRepo={repo.forkedFromRepo}
+              forksCount={repo.forks.length}
+              parentOrg={parentOrg}
+              path={`Pull Request / ${pr.uid}`}
+              repo={repo}
+            />
+          </IslandWrapper>
+          <Grid.Col fluid style={{ marginTop: 32 }}>
+            <Grid.Col key={pr.id} fluid>
+              <span>
+                <InlineCode themeScheme={commonProps.themeScheme}>
+                  {`[${pr.state}]`}
+                </InlineCode>
+              </span>
+              <h1 style={{ margin: 0, marginTop: 8 }}>
+                #{pr.uid} - {pr.summary}
+              </h1>
+              <span style={{ opacity: 0.67, marginTop: 8 }}>
+                wants to merge{" "}
+                <InlineCode themeScheme={commonProps.themeScheme}>
+                  {pr.sourceBranch}
+                </InlineCode>{" "}
+                into{" "}
+                <InlineCode themeScheme={commonProps.themeScheme}>
+                  {pr.targetBranch}
+                </InlineCode>
+              </span>
+              <Grid.Row
+                fluid
+                alignItems={"center"}
+                style={{ opacity: 0.67, marginTop: 8 }}
+              >
+                {new Date(pr.createdAt).getTime() <=
+                  new Date(pr.updatedAt).getTime() && (
+                  <span>
+                    opened on {new Date(pr.createdAt).toLocaleString()}
+                  </span>
+                )}
+                {((pr.closedAt == null &&
+                  new Date(pr.updatedAt).getTime() >
+                    new Date(pr.createdAt).getTime()) ||
+                  (pr.closedAt != null &&
+                    new Date(pr.updatedAt).getTime() <
+                      new Date(pr.closedAt).getTime())) && (
+                  <span>
+                    updated on {new Date(pr.updatedAt).toLocaleString()}
+                  </span>
+                )}
+                {pr.closedAt != null && (
+                  <span>
+                    closed on
+                    {new Date(pr.closedAt).toLocaleString()}
+                  </span>
+                )}
+              </Grid.Row>
+            </Grid.Col>
+            <Grid.Col fluid style={{ marginTop: 32 }}>
+              <a
+                href={`/${parentOrg.slug}/${repo.slug}/pulls/${pr.uid}?action=edit`}
+              >
+                Edit PR
+              </a>
+              <a
+                href={`/${parentOrg.slug}/${repo.slug}/pulls/${pr.uid}?action=delete`}
+              >
+                Delete PR
+              </a>
+            </Grid.Col>
+            <Grid.Col fluid>
+              <pre>
+                <code>{JSON.stringify(pr, null, 2)}</code>
+              </pre>
+            </Grid.Col>
+          </Grid.Col>
+        </PageWrapper>
+      </Layout>
+    );
+  };
+
+RepositoryPullRequestDetailsView.displayName =
+  "RepositoryPullRequestDetailsView";
+export default RepositoryPullRequestDetailsView;