GitFOSS
feat(organization): add an "OrganizationDetailsView" where to display org repositories (depending on auth/owner/member-ship)
+ 200
- 2
@@ -1,5 +1,5 @@
 {
-  "_generatedAtUnix": 1663788290484,
+  "_generatedAtUnix": 1663791331427,
   "_hashAlgorithm": "sha1",
   "_version": 2,
   "islands": {

...
@@ -55,6 +55,10 @@
       "hash": "6c5d677d4ba6710ec60f043c03a0e08cd2384de3",
       "pathSource": "./app/views/auth/RegisterView.tsx"
     },
+    "OrganizationDetailsView": {
+      "hash": "45e8b5ff8ecc7f95f5485949c796d8c19e004bfa",
+      "pathSource": "./app/views/organization/OrganizationDetailsView.tsx"
+    },
     "RepositoryBrowserView": {
       "hash": "932ee3dcdabd946d727876e494b58264c3043069",
       "pathSource": "./app/views/repository/RepositoryBrowserView.tsx"

new file
app/controllers/organization/getOrganizationDetailsView.ts
@@ -0,0 +1,68 @@
+// 1st-party
+import type { ReqHandler } from "@ethicdevs/react-monolith";
+import {
+  Organization,
+  OrganizationMembership,
+  Repository,
+  ResourceVisibility,
+  User,
+} from "@prisma/client";
+// app
+import { AppRoute, AppRoutesParams } from "../../routes";
+// app services
+import { makeOrganizationService } from "../../services/organization";
+// app views
+import OrganizationDetailsView, {
+  OrganizationDetailsViewProps,
+} from "../../views/organization/OrganizationDetailsView";
+
+const getOrganizationDetailsView: ReqHandler = async (request, reply) => {
+  const { curr_user_uid } = request.session.data;
+  const { orgSlug } =
+    request.params as AppRoutesParams[AppRoute.ORGANIZATION_DETAILS]["params"];
+
+  const orgService = makeOrganizationService({ request });
+  const organization = (await orgService.getOrganizationBySlug(orgSlug, {
+    memberships: true,
+    owner: true,
+    repositories: {
+      where: { visibility: ResourceVisibility.PUBLIC },
+    },
+  })) as Organization & {
+    memberships: OrganizationMembership[];
+    owner: User;
+    repositories: Repository[];
+  };
+
+  if (organization == null) {
+    return reply.status(404).callNotFound();
+  }
+
+  const currentUserIsOwner =
+    curr_user_uid != null && organization.owner.id === curr_user_uid;
+  const currentUserMembership =
+    curr_user_uid != null
+      ? organization.memberships.find(
+          (member) => member.userId === curr_user_uid
+        ) || null
+      : null;
+
+  if (
+    currentUserIsOwner ||
+    (currentUserMembership != null && currentUserMembership.revokedAt == null)
+  ) {
+    organization.repositories = await orgService.getOrganizationRepositories(
+      organization
+    );
+  }
+
+  const reqHandler = reply.makeRequestHandler(request, reply);
+  return reqHandler<OrganizationDetailsViewProps>(
+    OrganizationDetailsView.name,
+    {
+      organization,
+    }
+  );
+};
+
+export default getOrganizationDetailsView;

new file
app/controllers/organization/index.ts
@@ -0,0 +1,5 @@
+import { default as getOrganizationDetailsView } from "./getOrganizationDetailsView";
+
+export const OrganizationController = {
+  getOrganizationDetailsView,
+};

@@ -10,6 +10,7 @@ import { ResourceVisibility } from "@prisma/client";
 import { authenticatedOrLogin, guestOrRedirect } from "./utils/server";
 // app controllers
 import { AuthController } from "./controllers/auth";
+import { OrganizationController } from "./controllers/organization";
 import { RepositoryController } from "./controllers/repository";
 import * as HomeController from "./controllers/HomeController";
 import * as ThemeController from "./controllers/ThemeController";

...
@@ -23,6 +24,7 @@ export enum AppRoute {
   AUTH_LOGIN_ACTION = "auth.login.action",
   AUTH_LOGOUT_ACTION = "auth.logout.action",
   USER_DASHBOARD = "user.dashboard",
+  ORGANIZATION_DETAILS = "organization.details",
   REPOSITORY_EXPLORE = "repository.explore",
   REPOSITORY_COMMITS_LOG = "repository.commits_log",
   REPOSITORY_CREATE = "repository.create",

...
@@ -53,6 +55,11 @@ export interface AppRoutesParams extends IRouteParams {
   };
   [AppRoute.AUTH_LOGOUT_ACTION]: undefined;
   [AppRoute.USER_DASHBOARD]: undefined;
+  [AppRoute.ORGANIZATION_DETAILS]: {
+    params: {
+      orgSlug: string;
+    };
+  };
   [AppRoute.REPOSITORY_EXPLORE]: undefined;
   [AppRoute.REPOSITORY_COMMITS_LOG]: {
     params: {

...
@@ -134,6 +141,18 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
   },
   [AppRoute.AUTH_LOGOUT_ACTION]: undefined,
   [AppRoute.USER_DASHBOARD]: undefined,
+  [AppRoute.ORGANIZATION_DETAILS]: {
+    params: {
+      type: "object",
+      required: ["orgSlug"],
+      additionalProperties: false,
+      properties: {
+        orgSlug: {
+          type: "string",
+        },
+      },
+    },
+  },
   [AppRoute.REPOSITORY_EXPLORE]: undefined,
   [AppRoute.REPOSITORY_COMMITS_LOG]: {
     params: {

...
@@ -324,6 +343,14 @@ const RootAppRouter: AppRouter = () => {
           handler={AuthController.getDashboardView}
         />
         {/* --- */}
+        <Router.Route
+          name={AppRoute.ORGANIZATION_DETAILS}
+          method={"GET"}
+          path={"/:orgSlug"}
+          schema={AppRoutesSchemas[AppRoute.ORGANIZATION_DETAILS]}
+          handler={OrganizationController.getOrganizationDetailsView}
+        />
+        {/* --- */}
         <Router.Route
           name={AppRoute.REPOSITORY_EXPLORE}
           method={"GET"}

new file
app/services/organization/getOrganizationRepositories.ts
@@ -0,0 +1,32 @@
+// 1st-party
+import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
+// generated via script[generate:prisma]
+import { Organization, Repository } from "@prisma/client";
+// app
+import type { OrganizationServiceDeps } from "./types";
+
+const getOrganizationRepositories: ServiceMethodFactory<
+  OrganizationServiceDeps,
+  [Organization],
+  Promise<(Repository & { parentOrg: Organization })[]>
+> = ({ request }) => {
+  return async (org) => {
+    const orgRepos = await request.prisma.repository.findMany({
+      include: {
+        organization: true,
+      },
+      where: {
+        organization: {
+          id: org.id,
+        },
+      },
+    });
+
+    return orgRepos.map(({ organization: parentOrg, ...repo }) => ({
+      ...repo,
+      parentOrg,
+    }));
+  };
+};
+
+export default getOrganizationRepositories;

app/services/organization/index.ts
@@ -5,6 +5,7 @@ import type { OrganizationServiceAPI, OrganizationServiceDeps } from "./types";
 // service methods
 import { default as makeGetOrganizationById } from "./getOrganizationById";
 import { default as makeGetOrganizationBySlug } from "./getOrganizationBySlug";
+import { default as makeGetOrganizationRepositories } from "./getOrganizationRepositories";
 
 export const makeOrganizationService = makeService<
   OrganizationServiceAPI,

...
@@ -12,4 +13,5 @@ export const makeOrganizationService = makeService<
 >({
   getOrganizationById: makeGetOrganizationById,
   getOrganizationBySlug: makeGetOrganizationBySlug,
+  getOrganizationRepositories: makeGetOrganizationRepositories,
 });

app/services/organization/types.ts
@@ -3,7 +3,7 @@ import { ServiceApiContract } from "@ethicdevs/react-monolith";
 // 3rd-party
 import { FastifyRequest } from "fastify";
 // generated via script[generate:prisma]
-import { Organization, Prisma } from "@prisma/client";
+import { Organization, Prisma, Repository } from "@prisma/client";
 
 // service
 export interface OrganizationServiceAPI extends ServiceApiContract {

...
@@ -15,6 +15,9 @@ export interface OrganizationServiceAPI extends ServiceApiContract {
     orgSlug: string,
     include?: Prisma.OrganizationInclude
   ): Promise<Organization | null>;
+  getOrganizationRepositories(
+    organization: Organization
+  ): Promise<(Repository & { parentOrg: Organization })[]>;
 }
 
 export interface OrganizationServiceDeps {

new file
app/views/organization/OrganizationDetailsView.tsx
@@ -0,0 +1,57 @@
+// 1st-party
+import type { ReactView } from "@ethicdevs/react-monolith";
+// 3rd-party
+import React, { useMemo } from "react";
+// generated via script[generate:prisma]
+import type { Organization, Repository } from "@prisma/client";
+// app
+import type { CommonProps } from "../../types";
+import { Layout, PageWrapper } from "../../components";
+// app islands
+import RepositoriesList from "../../islands/RepositoriesList";
+
+export interface OrganizationDetailsViewProps extends CommonProps {
+  organization: Organization & { repositories: Repository[] };
+}
+
+const OrganizationDetailsView: ReactView<OrganizationDetailsViewProps> = ({
+  commonProps,
+  organization,
+}) => {
+  const orgRepositories = useMemo(
+    () =>
+      organization.repositories.map((repo) => ({
+        ...repo,
+        parentOrg: organization,
+      })),
+    [organization]
+  );
+
+  return (
+    <Layout {...commonProps}>
+      <PageWrapper>
+        <h1>{organization.displayName || organization.slug}</h1>
+        {organization.websiteUrl != null && (
+          <p>
+            <a
+              href={organization.websiteUrl}
+              target={"_blank"}
+              rel={"noopener noreferer noreferrer"}
+            >
+              {organization.websiteUrl}
+            </a>
+          </p>
+        )}
+        <div
+          data-islandid={`${RepositoriesList.name}$$0`}
+          style={{ width: "100%" }}
+        >
+          <RepositoriesList repositories={orgRepositories} />
+        </div>
+      </PageWrapper>
+    </Layout>
+  );
+};
+
+OrganizationDetailsView.displayName = "OrganizationDetailsView";
+export default OrganizationDetailsView;