feat(organization): add an "OrganizationDetailsView" where to display org repositories (depending on auth/owner/member-ship)@@ -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"
@@ -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;
@@ -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"}
@@ -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;
@@ -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,
});
@@ -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 {
@@ -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;