diff --git a/apps/backend/src/libs/modules/server-application/server-application.ts b/apps/backend/src/libs/modules/server-application/server-application.ts index fcd3b16da..0eba4ee9c 100644 --- a/apps/backend/src/libs/modules/server-application/server-application.ts +++ b/apps/backend/src/libs/modules/server-application/server-application.ts @@ -3,6 +3,7 @@ import { database } from "~/libs/modules/database/database.js"; import { logger } from "~/libs/modules/logger/logger.js"; import { activityLogController } from "~/modules/activity-logs/activity-logs.js"; import { authController } from "~/modules/auth/auth.js"; +import { contributorController } from "~/modules/contributors/contributors.js"; import { groupController } from "~/modules/groups/groups.js"; import { permissionController } from "~/modules/permissions/permissions.js"; import { projectApiKeyController } from "~/modules/project-api-keys/project-api-keys.js"; @@ -25,6 +26,7 @@ const apiV1 = new BaseServerApplicationApi( ...projectGroupController.routes, ...projectPermissionsController.routes, ...projectController.routes, + ...contributorController.routes, ...userController.routes, ...groupController.routes, ...projectApiKeyController.routes, diff --git a/apps/backend/src/modules/contributors/contributor.controller.ts b/apps/backend/src/modules/contributors/contributor.controller.ts new file mode 100644 index 000000000..e9e1903e0 --- /dev/null +++ b/apps/backend/src/modules/contributors/contributor.controller.ts @@ -0,0 +1,86 @@ +import { APIPath } from "~/libs/enums/enums.js"; +import { + type APIHandlerResponse, + BaseController, +} from "~/libs/modules/controller/controller.js"; +import { HTTPCode } from "~/libs/modules/http/http.js"; +import { type Logger } from "~/libs/modules/logger/logger.js"; + +import { type ContributorService } from "./contributor.service.js"; +import { ContributorsApiPath } from "./libs/enums/enums.js"; + +/** + * @swagger + * components: + * schemas: + * Contributor: + * type: object + * properties: + * id: + * type: number + * minimum: 1 + * name: + * type: string + * gitEmails: + * type: array + * items: + * type: object + * properties: + * id: + * type: number + * minimum: 1 + * email: + * type: string + * projects: + * type: array + * items: + * type: object + * properties: + * id: + * type: number + * minimum: 1 + * name: + * type: string + */ +class ContributorController extends BaseController { + private contributorService: ContributorService; + + public constructor(logger: Logger, contributorService: ContributorService) { + super(logger, APIPath.CONTRIBUTORS); + + this.contributorService = contributorService; + + this.addRoute({ + handler: () => this.findAll(), + method: "GET", + path: ContributorsApiPath.ROOT, + }); + } + + /** + * @swagger + * /contributors: + * get: + * description: Returns an array of contributors + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * items: + * type: array + * items: + * $ref: "#/components/schemas/Contributor" + */ + private async findAll(): Promise { + return { + payload: await this.contributorService.findAll(), + status: HTTPCode.OK, + }; + } +} + +export { ContributorController }; diff --git a/apps/backend/src/modules/contributors/contributor.entity.ts b/apps/backend/src/modules/contributors/contributor.entity.ts index 6c2499765..c59a8f9ca 100644 --- a/apps/backend/src/modules/contributors/contributor.entity.ts +++ b/apps/backend/src/modules/contributors/contributor.entity.ts @@ -3,47 +3,82 @@ import { type Entity } from "~/libs/types/types.js"; import { type ContributorGetAllItemResponseDto } from "./libs/types/types.js"; class ContributorEntity implements Entity { + private gitEmails: { email: string; id: number }[]; private id: null | number; - private name: string; + private projects: { id: number; name: string }[]; - private constructor({ id, name }: { id: null | number; name: string }) { + private constructor({ + gitEmails, + id, + name, + projects, + }: { + gitEmails: { email: string; id: number }[]; + id: null | number; + name: string; + projects: { id: number; name: string }[]; + }) { this.id = id; this.name = name; + this.gitEmails = gitEmails; + this.projects = projects; } public static initialize({ + gitEmails, id, name, + projects, }: { + gitEmails: { email: string; id: number }[]; id: number; name: string; + projects: { id: number; name: string }[]; }): ContributorEntity { return new ContributorEntity({ + gitEmails, id, name, + projects, }); } - public static initializeNew({ name }: { name: string }): ContributorEntity { + public static initializeNew({ + gitEmails = [], + name, + projects = [], + }: { + gitEmails?: { email: string; id: number }[]; + name: string; + projects?: { id: number; name: string }[]; + }): ContributorEntity { return new ContributorEntity({ + gitEmails, id: null, name, + projects, }); } public toNewObject(): { + gitEmails: { email: string; id: number }[]; name: string; + projects: { id: number; name: string }[]; } { return { + gitEmails: this.gitEmails, name: this.name, + projects: this.projects, }; } public toObject(): ContributorGetAllItemResponseDto { return { + gitEmails: this.gitEmails, id: this.id as number, name: this.name, + projects: this.projects, }; } } diff --git a/apps/backend/src/modules/contributors/contributor.model.ts b/apps/backend/src/modules/contributors/contributor.model.ts index 89ca3e07f..2841d0711 100644 --- a/apps/backend/src/modules/contributors/contributor.model.ts +++ b/apps/backend/src/modules/contributors/contributor.model.ts @@ -6,8 +6,12 @@ import { } from "~/libs/modules/database/database.js"; import { GitEmailModel } from "~/modules/git-emails/git-emails.js"; +import { type ProjectModel } from "../projects/project.model.js"; + class ContributorModel extends AbstractModel { + public gitEmails!: GitEmailModel[]; public name!: string; + public projects!: ProjectModel[]; public static override get relationMappings(): RelationMappings { return { diff --git a/apps/backend/src/modules/contributors/contributor.repository.ts b/apps/backend/src/modules/contributors/contributor.repository.ts index 50d538c3c..d594f9a63 100644 --- a/apps/backend/src/modules/contributors/contributor.repository.ts +++ b/apps/backend/src/modules/contributors/contributor.repository.ts @@ -1,3 +1,5 @@ +import { raw } from "objection"; + import { type Repository } from "~/libs/types/types.js"; import { ContributorEntity } from "./contributor.entity.js"; @@ -15,9 +17,7 @@ class ContributorRepository implements Repository { const contributor = await this.contributorModel .query() - .insert({ - name, - }) + .insert({ name }) .execute(); return ContributorEntity.initialize(contributor); @@ -28,22 +28,51 @@ class ContributorRepository implements Repository { } public async find(id: number): Promise { - const item = await this.contributorModel.query().findById(id).execute(); + const contributor = await this.contributorModel + .query() + .findById(id) + .withGraphFetched("gitEmails"); - return item ? ContributorEntity.initialize(item) : null; + if (!contributor) { + return null; + } + + return ContributorEntity.initialize(contributor); } - public findAll(): ReturnType { - return Promise.resolve({ items: [] }); + public async findAll(): Promise<{ items: ContributorEntity[] }> { + const contributorsWithProjectsAndEmails = await this.contributorModel + .query() + .select("contributors.*") + .select( + raw( + "COALESCE(ARRAY_AGG(DISTINCT jsonb_build_object('id', projects.id, 'name', projects.name)) FILTER (WHERE projects.id IS NOT NULL), '{}') AS projects", + ), + ) + .leftJoin("git_emails", "contributors.id", "git_emails.contributor_id") + .leftJoin("activity_logs", "git_emails.id", "activity_logs.git_email_id") + .leftJoin("projects", "activity_logs.project_id", "projects.id") + .groupBy("contributors.id") + .withGraphFetched("gitEmails"); + + return { + items: contributorsWithProjectsAndEmails.map((contributor) => { + return ContributorEntity.initialize(contributor); + }), + }; } public async findByName(name: string): Promise { - const item = await this.contributorModel + const contributor = await this.contributorModel .query() .findOne({ name }) - .execute(); + .withGraphFetched("gitEmails"); - return item ? ContributorEntity.initialize(item) : null; + if (!contributor) { + return null; + } + + return ContributorEntity.initialize(contributor); } public update(): ReturnType { diff --git a/apps/backend/src/modules/contributors/contributors.service.ts b/apps/backend/src/modules/contributors/contributor.service.ts similarity index 77% rename from apps/backend/src/modules/contributors/contributors.service.ts rename to apps/backend/src/modules/contributors/contributor.service.ts index edbba075a..097e78368 100644 --- a/apps/backend/src/modules/contributors/contributors.service.ts +++ b/apps/backend/src/modules/contributors/contributor.service.ts @@ -8,6 +8,7 @@ import { ContributorError } from "./libs/exceptions/exceptions.js"; import { type ContributorCreateRequestDto, type ContributorGetAllItemResponseDto, + type ContributorGetAllResponseDto, } from "./libs/types/types.js"; class ContributorService implements Service { @@ -48,8 +49,22 @@ class ContributorService implements Service { return item.toObject(); } - public findAll(): ReturnType { - return Promise.resolve({ items: [] }); + public async findAll(): Promise { + const contributors = await this.contributorRepository.findAll(); + + return { + items: contributors.items.map((item) => { + const contributor = item.toObject(); + + return { + ...contributor, + gitEmails: contributor.gitEmails.map((gitEmail) => ({ + email: gitEmail.email, + id: gitEmail.id, + })), + }; + }), + }; } public async findByName( @@ -57,7 +72,11 @@ class ContributorService implements Service { ): Promise { const item = await this.contributorRepository.findByName(name); - return item ? item.toObject() : null; + if (!item) { + return null; + } + + return item.toObject(); } public update(): ReturnType { diff --git a/apps/backend/src/modules/contributors/contributors.ts b/apps/backend/src/modules/contributors/contributors.ts index 7f205678a..9b48695b7 100644 --- a/apps/backend/src/modules/contributors/contributors.ts +++ b/apps/backend/src/modules/contributors/contributors.ts @@ -1,11 +1,17 @@ +import { logger } from "~/libs/modules/logger/logger.js"; + +import { ContributorController } from "./contributor.controller.js"; import { ContributorModel } from "./contributor.model.js"; import { ContributorRepository } from "./contributor.repository.js"; -import { ContributorService } from "./contributors.service.js"; +import { ContributorService } from "./contributor.service.js"; const contributorRepository = new ContributorRepository(ContributorModel); const contributorService = new ContributorService(contributorRepository); +const contributorController = new ContributorController( + logger, + contributorService, +); export { ContributorModel } from "./contributor.model.js"; -export { ContributorRepository } from "./contributor.repository.js"; -export { ContributorService } from "./contributors.service.js"; -export { contributorService }; +export { ContributorService } from "./contributor.service.js"; +export { contributorController, contributorService }; diff --git a/apps/backend/src/modules/contributors/libs/enums/enums.ts b/apps/backend/src/modules/contributors/libs/enums/enums.ts new file mode 100644 index 000000000..856dfaeae --- /dev/null +++ b/apps/backend/src/modules/contributors/libs/enums/enums.ts @@ -0,0 +1 @@ +export { ContributorsApiPath } from "@git-fit/shared"; diff --git a/apps/frontend/src/index.tsx b/apps/frontend/src/index.tsx index e029daa95..e5b7f2871 100644 --- a/apps/frontend/src/index.tsx +++ b/apps/frontend/src/index.tsx @@ -14,6 +14,7 @@ import { store } from "~/libs/modules/store/store.js"; import { AccessManagement } from "~/pages/access-management/access-management.jsx"; import { Analytics } from "~/pages/analytics/analytics.jsx"; import { Auth } from "~/pages/auth/auth.jsx"; +import { Contributors } from "~/pages/contributors/contributors.jsx"; import { NoAccess } from "~/pages/no-access/no-access.jsx"; import { NotFound } from "~/pages/not-found/not-found.jsx"; import { Profile } from "~/pages/profile/profile.jsx"; @@ -57,6 +58,14 @@ createRoot(document.querySelector("#root") as HTMLElement).render( ), path: AppRoute.PROFILE, }, + { + element: ( + + + + ), + path: AppRoute.CONTRIBUTORS, + }, { element: ( diff --git a/apps/frontend/src/libs/constants/navigation-items.constant.ts b/apps/frontend/src/libs/constants/navigation-items.constant.ts index df7b39681..32a04b9cc 100644 --- a/apps/frontend/src/libs/constants/navigation-items.constant.ts +++ b/apps/frontend/src/libs/constants/navigation-items.constant.ts @@ -14,6 +14,11 @@ const SIDEBAR_ITEMS: NavigationItem[] = [ label: "Access Management", pagePermissions: [PermissionKey.MANAGE_USER_ACCESS], }, + { + href: AppRoute.CONTRIBUTORS, + icon: "contributors", + label: "Contributors", + }, { href: AppRoute.ANALYTICS, icon: "analytics", diff --git a/apps/frontend/src/libs/enums/app-route.enum.ts b/apps/frontend/src/libs/enums/app-route.enum.ts index 16cd630a9..31968d2b8 100644 --- a/apps/frontend/src/libs/enums/app-route.enum.ts +++ b/apps/frontend/src/libs/enums/app-route.enum.ts @@ -2,6 +2,7 @@ const AppRoute = { ACCESS_MANAGEMENT: "/access-management", ANALYTICS: "/analytics", ANY: "*", + CONTRIBUTORS: "/contributors", NO_ACCESS: "/no-access", PROFILE: "/profile", PROJECT: "/projects/:id", diff --git a/apps/frontend/src/libs/modules/store/libs/types/extra-arguments.type.ts b/apps/frontend/src/libs/modules/store/libs/types/extra-arguments.type.ts index e37448416..3a53090a8 100644 --- a/apps/frontend/src/libs/modules/store/libs/types/extra-arguments.type.ts +++ b/apps/frontend/src/libs/modules/store/libs/types/extra-arguments.type.ts @@ -1,6 +1,7 @@ import { type Storage } from "~/libs/modules/storage/storage.js"; import { type ToastNotifier } from "~/libs/modules/toast-notifier/toast-notifier.js"; import { type authApi } from "~/modules/auth/auth.js"; +import { type contributorApi } from "~/modules/contributors/contributors.js"; import { type groupApi } from "~/modules/groups/groups.js"; import { type permissionApi } from "~/modules/permissions/permissions.js"; import { type projectApiKeysApi } from "~/modules/project-api-keys/project-api-keys.js"; @@ -11,6 +12,7 @@ import { type userApi } from "~/modules/users/users.js"; type ExtraArguments = { authApi: typeof authApi; + contributorApi: typeof contributorApi; groupApi: typeof groupApi; permissionApi: typeof permissionApi; projectApi: typeof projectApi; diff --git a/apps/frontend/src/libs/modules/store/libs/types/root-reducer.type.ts b/apps/frontend/src/libs/modules/store/libs/types/root-reducer.type.ts index b01f848b0..26fdfa6ae 100644 --- a/apps/frontend/src/libs/modules/store/libs/types/root-reducer.type.ts +++ b/apps/frontend/src/libs/modules/store/libs/types/root-reducer.type.ts @@ -1,4 +1,5 @@ import { type reducer as authReducer } from "~/modules/auth/auth.js"; +import { type reducer as contributorsReducer } from "~/modules/contributors/contributors.js"; import { type reducer as groupsReducer } from "~/modules/groups/groups.js"; import { type reducer as permissionsReducer } from "~/modules/permissions/permissions.js"; import { type reducer as projectApiKeysReducer } from "~/modules/project-api-keys/project-api-keys.js"; @@ -9,6 +10,7 @@ import { type reducer as usersReducer } from "~/modules/users/users.js"; type RootReducer = { auth: ReturnType; + contributors: ReturnType; groups: ReturnType; permissions: ReturnType; projectApiKeys: ReturnType; diff --git a/apps/frontend/src/libs/modules/store/store.module.ts b/apps/frontend/src/libs/modules/store/store.module.ts index aac167c19..99873dcd7 100644 --- a/apps/frontend/src/libs/modules/store/store.module.ts +++ b/apps/frontend/src/libs/modules/store/store.module.ts @@ -10,6 +10,10 @@ import { type Config } from "~/libs/modules/config/config.js"; import { storage } from "~/libs/modules/storage/storage.js"; import { toastNotifier } from "~/libs/modules/toast-notifier/toast-notifier.js"; import { authApi, reducer as authReducer } from "~/modules/auth/auth.js"; +import { + contributorApi, + reducer as contributorsReducer, +} from "~/modules/contributors/contributors.js"; import { groupApi, reducer as groupsReducer } from "~/modules/groups/groups.js"; import { permissionApi, @@ -57,6 +61,7 @@ class Store { }, reducer: { auth: authReducer, + contributors: contributorsReducer, groups: groupsReducer, permissions: permissionReducer, projectApiKeys: projectApiKeysReducer, @@ -71,6 +76,7 @@ class Store { public get extraArguments(): ExtraArguments { return { authApi, + contributorApi, groupApi, permissionApi, projectApi, diff --git a/apps/frontend/src/modules/contributors/contributors-api.ts b/apps/frontend/src/modules/contributors/contributors-api.ts new file mode 100644 index 000000000..82e402b03 --- /dev/null +++ b/apps/frontend/src/modules/contributors/contributors-api.ts @@ -0,0 +1,33 @@ +import { APIPath } from "~/libs/enums/enums.js"; +import { BaseHTTPApi } from "~/libs/modules/api/api.js"; +import { type HTTP } from "~/libs/modules/http/http.js"; +import { type Storage } from "~/libs/modules/storage/storage.js"; + +import { ContributorsApiPath } from "./libs/enums/enums.js"; +import { type ContributorGetAllResponseDto } from "./libs/types/types.js"; + +type Constructor = { + baseUrl: string; + http: HTTP; + storage: Storage; +}; + +class ContributorApi extends BaseHTTPApi { + public constructor({ baseUrl, http, storage }: Constructor) { + super({ baseUrl, http, path: APIPath.CONTRIBUTORS, storage }); + } + + public async getAll(): Promise { + const response = await this.load( + this.getFullEndpoint(ContributorsApiPath.ROOT, {}), + { + hasAuth: true, + method: "GET", + }, + ); + + return await response.json(); + } +} + +export { ContributorApi }; diff --git a/apps/frontend/src/modules/contributors/contributors.ts b/apps/frontend/src/modules/contributors/contributors.ts new file mode 100644 index 000000000..a7f80cbfb --- /dev/null +++ b/apps/frontend/src/modules/contributors/contributors.ts @@ -0,0 +1,18 @@ +import { config } from "~/libs/modules/config/config.js"; +import { http } from "~/libs/modules/http/http.js"; +import { storage } from "~/libs/modules/storage/storage.js"; + +import { ContributorApi } from "./contributors-api.js"; + +const contributorApi = new ContributorApi({ + baseUrl: config.ENV.API.ORIGIN_URL, + http, + storage, +}); + +export { contributorApi }; +export { + type ContributorGetAllItemResponseDto, + type ContributorGetAllResponseDto, +} from "./libs/types/types.js"; +export { actions, reducer } from "./slices/contributors.js"; diff --git a/apps/frontend/src/modules/contributors/libs/enums/enums.ts b/apps/frontend/src/modules/contributors/libs/enums/enums.ts new file mode 100644 index 000000000..856dfaeae --- /dev/null +++ b/apps/frontend/src/modules/contributors/libs/enums/enums.ts @@ -0,0 +1 @@ +export { ContributorsApiPath } from "@git-fit/shared"; diff --git a/apps/frontend/src/modules/contributors/libs/types/types.ts b/apps/frontend/src/modules/contributors/libs/types/types.ts new file mode 100644 index 000000000..3d4070e96 --- /dev/null +++ b/apps/frontend/src/modules/contributors/libs/types/types.ts @@ -0,0 +1,4 @@ +export { + type ContributorGetAllItemResponseDto, + type ContributorGetAllResponseDto, +} from "@git-fit/shared"; diff --git a/apps/frontend/src/modules/contributors/slices/actions.ts b/apps/frontend/src/modules/contributors/slices/actions.ts new file mode 100644 index 000000000..655c2a893 --- /dev/null +++ b/apps/frontend/src/modules/contributors/slices/actions.ts @@ -0,0 +1,18 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; + +import { type AsyncThunkConfig } from "~/libs/types/types.js"; + +import { type ContributorGetAllResponseDto } from "../libs/types/types.js"; +import { name as sliceName } from "./contributor.slice.js"; + +const loadAll = createAsyncThunk< + ContributorGetAllResponseDto, + undefined, + AsyncThunkConfig +>(`${sliceName}/load-all`, async (_, { extra }) => { + const { contributorApi } = extra; + + return await contributorApi.getAll(); +}); + +export { loadAll }; diff --git a/apps/frontend/src/modules/contributors/slices/contributor.slice.ts b/apps/frontend/src/modules/contributors/slices/contributor.slice.ts new file mode 100644 index 000000000..9706f61c0 --- /dev/null +++ b/apps/frontend/src/modules/contributors/slices/contributor.slice.ts @@ -0,0 +1,38 @@ +import { createSlice } from "@reduxjs/toolkit"; + +import { DataStatus } from "~/libs/enums/enums.js"; +import { type ValueOf } from "~/libs/types/types.js"; + +import { type ContributorGetAllItemResponseDto } from "../libs/types/types.js"; +import { loadAll } from "./actions.js"; + +type State = { + contributors: ContributorGetAllItemResponseDto[]; + dataStatus: ValueOf; +}; + +const initialState: State = { + contributors: [], + dataStatus: DataStatus.IDLE, +}; + +const { actions, name, reducer } = createSlice({ + extraReducers(builder) { + builder.addCase(loadAll.pending, (state) => { + state.dataStatus = DataStatus.PENDING; + }); + builder.addCase(loadAll.fulfilled, (state, action) => { + state.contributors = action.payload.items; + state.dataStatus = DataStatus.FULFILLED; + }); + builder.addCase(loadAll.rejected, (state) => { + state.contributors = []; + state.dataStatus = DataStatus.REJECTED; + }); + }, + initialState, + name: "contributors", + reducers: {}, +}); + +export { actions, name, reducer }; diff --git a/apps/frontend/src/modules/contributors/slices/contributors.ts b/apps/frontend/src/modules/contributors/slices/contributors.ts new file mode 100644 index 000000000..ef393eb33 --- /dev/null +++ b/apps/frontend/src/modules/contributors/slices/contributors.ts @@ -0,0 +1,10 @@ +import { loadAll } from "./actions.js"; +import { actions } from "./contributor.slice.js"; + +const allActions = { + ...actions, + loadAll, +}; + +export { allActions as actions }; +export { reducer } from "./contributor.slice.js"; diff --git a/apps/frontend/src/pages/contributors/contributors.tsx b/apps/frontend/src/pages/contributors/contributors.tsx new file mode 100644 index 000000000..05a53d38d --- /dev/null +++ b/apps/frontend/src/pages/contributors/contributors.tsx @@ -0,0 +1,47 @@ +import { PageLayout, Table } from "~/libs/components/components.js"; +import { DataStatus } from "~/libs/enums/enums.js"; +import { + useAppDispatch, + useAppSelector, + useEffect, +} from "~/libs/hooks/hooks.js"; +import { actions as contributorActions } from "~/modules/contributors/contributors.js"; + +import { + getContributorColumns, + getContributorRows, +} from "./libs/helpers/helpers.js"; +import { type ContributorRow } from "./libs/types/types.js"; +import styles from "./styles.module.css"; + +const Contributors = (): JSX.Element => { + const { contributors, dataStatus } = useAppSelector( + ({ contributors }) => contributors, + ); + + const dispatch = useAppDispatch(); + + useEffect(() => { + void dispatch(contributorActions.loadAll()); + }, [dispatch]); + + const contributorsColumns = getContributorColumns(); + const contributorsData = getContributorRows(contributors); + + const isLoading = + dataStatus === DataStatus.IDLE || dataStatus === DataStatus.PENDING; + + return ( + +

Contributors

+
+ + columns={contributorsColumns} + data={contributorsData} + /> +
+
+ ); +}; + +export { Contributors }; diff --git a/apps/frontend/src/pages/contributors/libs/helpers/get-contributor-columns.helper.ts b/apps/frontend/src/pages/contributors/libs/helpers/get-contributor-columns.helper.ts new file mode 100644 index 000000000..2aeaf1f92 --- /dev/null +++ b/apps/frontend/src/pages/contributors/libs/helpers/get-contributor-columns.helper.ts @@ -0,0 +1,22 @@ +import { type TableColumn } from "~/libs/types/types.js"; + +import { type ContributorRow } from "../types/types.js"; + +const getContributorColumns = (): TableColumn[] => [ + { + accessorKey: "name", + header: "Name", + }, + { + accessorFn: (contributor: ContributorRow): string => + contributor.gitEmails.join(", "), + header: "Git Emails", + }, + { + accessorFn: (contributor: ContributorRow): string => + contributor.projects.join(", "), + header: "Projects", + }, +]; + +export { getContributorColumns }; diff --git a/apps/frontend/src/pages/contributors/libs/helpers/get-contributor-rows.helper.ts b/apps/frontend/src/pages/contributors/libs/helpers/get-contributor-rows.helper.ts new file mode 100644 index 000000000..5cd903159 --- /dev/null +++ b/apps/frontend/src/pages/contributors/libs/helpers/get-contributor-rows.helper.ts @@ -0,0 +1,14 @@ +import { type ContributorGetAllItemResponseDto } from "~/modules/contributors/contributors.js"; + +import { type ContributorRow } from "../types/types.js"; + +const getContributorRows = ( + contributors: ContributorGetAllItemResponseDto[], +): ContributorRow[] => + contributors.map((contributor) => ({ + gitEmails: contributor.gitEmails.map((email) => email.email), + name: contributor.name, + projects: contributor.projects.map((project) => project.name), + })); + +export { getContributorRows }; diff --git a/apps/frontend/src/pages/contributors/libs/helpers/helpers.ts b/apps/frontend/src/pages/contributors/libs/helpers/helpers.ts new file mode 100644 index 000000000..ada642654 --- /dev/null +++ b/apps/frontend/src/pages/contributors/libs/helpers/helpers.ts @@ -0,0 +1,2 @@ +export { getContributorColumns } from "./get-contributor-columns.helper.js"; +export { getContributorRows } from "./get-contributor-rows.helper.js"; diff --git a/apps/frontend/src/pages/contributors/libs/types/contributor-row.type.ts b/apps/frontend/src/pages/contributors/libs/types/contributor-row.type.ts new file mode 100644 index 000000000..d8580f273 --- /dev/null +++ b/apps/frontend/src/pages/contributors/libs/types/contributor-row.type.ts @@ -0,0 +1,7 @@ +type ContributorRow = { + gitEmails: string[]; + name: string; + projects: string[]; +}; + +export { type ContributorRow }; diff --git a/apps/frontend/src/pages/contributors/libs/types/types.ts b/apps/frontend/src/pages/contributors/libs/types/types.ts new file mode 100644 index 000000000..491b23a59 --- /dev/null +++ b/apps/frontend/src/pages/contributors/libs/types/types.ts @@ -0,0 +1 @@ +export { type ContributorRow } from "./contributor-row.type.js"; diff --git a/apps/frontend/src/pages/contributors/styles.module.css b/apps/frontend/src/pages/contributors/styles.module.css new file mode 100644 index 000000000..322bb20d3 --- /dev/null +++ b/apps/frontend/src/pages/contributors/styles.module.css @@ -0,0 +1,10 @@ +.title { + margin: 0; + font-size: 30px; + font-weight: 600; + line-height: 120%; +} + +.contributors-table { + margin-top: 32px; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 61c88e954..14c568c3d 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -58,6 +58,7 @@ export { ContributorError, type ContributorGetAllItemResponseDto, type ContributorGetAllResponseDto, + ContributorsApiPath, } from "./modules/contributors/contributors.js"; export { type GitEmailCreateRequestDto, diff --git a/packages/shared/src/libs/enums/api-path.enum.ts b/packages/shared/src/libs/enums/api-path.enum.ts index 8f2c12347..880afd3a9 100644 --- a/packages/shared/src/libs/enums/api-path.enum.ts +++ b/packages/shared/src/libs/enums/api-path.enum.ts @@ -1,6 +1,7 @@ const APIPath = { ACTIVITY_LOGS: "/activity-logs", AUTH: "/auth", + CONTRIBUTORS: "/contributors", GROUPS: "/groups", PERMISSIONS: "/permissions", PROJECT_API_KEYS: "/project-api-keys", diff --git a/packages/shared/src/modules/contributors/contributors.ts b/packages/shared/src/modules/contributors/contributors.ts index 13e0b6f6e..92c66088d 100644 --- a/packages/shared/src/modules/contributors/contributors.ts +++ b/packages/shared/src/modules/contributors/contributors.ts @@ -1,3 +1,4 @@ +export { ContributorsApiPath } from "./libs/enums/enums.js"; export { ContributorError } from "./libs/exceptions/exceptions.js"; export { type ContributorCreateRequestDto, diff --git a/packages/shared/src/modules/contributors/libs/enums/contributors-api-path.enum.ts b/packages/shared/src/modules/contributors/libs/enums/contributors-api-path.enum.ts new file mode 100644 index 000000000..cb790a257 --- /dev/null +++ b/packages/shared/src/modules/contributors/libs/enums/contributors-api-path.enum.ts @@ -0,0 +1,5 @@ +const ContributorsApiPath = { + ROOT: "/", +} as const; + +export { ContributorsApiPath }; diff --git a/packages/shared/src/modules/contributors/libs/enums/enums.ts b/packages/shared/src/modules/contributors/libs/enums/enums.ts new file mode 100644 index 000000000..147a2d425 --- /dev/null +++ b/packages/shared/src/modules/contributors/libs/enums/enums.ts @@ -0,0 +1 @@ +export { ContributorsApiPath } from "./contributors-api-path.enum.js"; diff --git a/packages/shared/src/modules/contributors/libs/types/contributor-get-all-item-response-dto.type.ts b/packages/shared/src/modules/contributors/libs/types/contributor-get-all-item-response-dto.type.ts index d852a8bbb..177cf6666 100644 --- a/packages/shared/src/modules/contributors/libs/types/contributor-get-all-item-response-dto.type.ts +++ b/packages/shared/src/modules/contributors/libs/types/contributor-get-all-item-response-dto.type.ts @@ -1,6 +1,14 @@ type ContributorGetAllItemResponseDto = { + gitEmails: { + email: string; + id: number; + }[]; id: number; name: string; + projects: { + id: number; + name: string; + }[]; }; export { type ContributorGetAllItemResponseDto };