diff --git a/src/entities/project.ts b/src/entities/project.ts index f10daa91b..711e1334b 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -217,6 +217,7 @@ export class Project extends BaseEntity { @Field(_type => [Category], { nullable: true }) @ManyToMany(_type => Category, category => category.projects, { nullable: true, + eager: true, }) @JoinTable() categories: Category[]; diff --git a/src/server/adminJs/tabs/components/ProjectCategories.tsx b/src/server/adminJs/tabs/components/ProjectCategories.tsx new file mode 100644 index 000000000..5706131cb --- /dev/null +++ b/src/server/adminJs/tabs/components/ProjectCategories.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { withTheme } from 'styled-components'; +import { Section, Label } from '@adminjs/design-system'; + +const ProjectUpdates = props => { + const categories = props?.record?.params?.categories; + return ( +
+ +
+ {categories?.map(category => { + return ( +
+
+
+ +

+ {category.name || ''} - Id: {category.id} +

+
+
+ ); + })} +
+
+
+ ); +}; + +export default withTheme(ProjectUpdates); diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index d0cf6fbe2..a61aedfa6 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -55,6 +55,7 @@ import { User } from '../../../entities/user'; import { refreshProjectEstimatedMatchingView } from '../../../services/projectViewsService'; import { extractAdminJsReferrerUrlParams } from '../adminJs'; import { relateManyProjectsToQfRound } from '../../../repositories/qfRoundRepository2'; +import { Category } from '../../../entities/category'; // add queries depending on which filters were selected export const buildProjectsQuery = ( @@ -446,6 +447,13 @@ export const addProjectsToQfRound = async ( }; }; +export const extractCategoryIds = (payload: any) => { + if (!payload) return; + return Object.keys(payload) + .filter(key => key.startsWith('categoryIds.')) + .map(key => payload[key]); +}; + export const addSingleProjectToQfRound = async ( context: AdminJsContextInterface, request: AdminJsRequestInterface, @@ -486,6 +494,14 @@ export const fillSocialProfileAndQfRounds: After< const projectUpdates = await findProjectUpdatesByProjectId(projectId); const project = await findProjectById(projectId); const adminJsBaseUrl = process.env.SERVER_URL; + let categories; + if (project) { + categories = await Category.createQueryBuilder('category') + .innerJoin('category.projects', 'projects') + .where('projects.id = :id', { id: project.id }) + .orderBy('category.name', 'ASC') + .getMany(); + } response.record = { ...record, params: { @@ -499,6 +515,11 @@ export const fillSocialProfileAndQfRounds: After< adminJsBaseUrl, }, }; + + if (categories) { + response.record.params.categoryIds = categories; + response.record.params.categories = categories; + } return response; }; @@ -660,7 +681,7 @@ export const projectsTab = { id: { isVisible: { list: false, - filter: false, + filter: true, show: true, edit: false, }, @@ -831,12 +852,36 @@ export const projectsTab = { edit: false, }, }, + categoryIds: { + type: 'reference', + isArray: true, + reference: 'Category', + isVisible: { + list: false, + filter: false, + show: true, + edit: true, + }, + components: { + show: adminJs.bundle('./components/ProjectCategories'), + }, + availableValues: async _record => { + const categories = await Category.createQueryBuilder('category') + .where('category.isActive = :isActive', { isActive: true }) + .orderBy('category.name', 'ASC') + .getMany(); + return categories.map(category => ({ + value: category.id, + label: `${category.id} - ${category.name}`, + })); + }, + }, isImported: { isVisible: { list: false, filter: true, show: true, - edit: false, + edit: true, }, }, totalReactions: { @@ -924,6 +969,24 @@ export const projectsTab = { isVisible: false, isAccessible: ({ currentAdmin }) => canAccessProjectAction({ currentAdmin }, ResourceActions.NEW), + before: async request => { + if (request.payload.categories) { + request.payload.categories = ( + request.payload.categories as string[] + ).map(id => ({ id: parseInt(id, 10) })); + } + return request; + }, + after: async response => { + const { request } = response; + const project = await Project.findOne({ + where: { id: request?.record?.id }, + }); + const categoryIds = extractCategoryIds(request.record.params); + await saveCategories(project!, categoryIds || []); + + return response; + }, }, bulkDelete: { isVisible: false, @@ -952,6 +1015,16 @@ export const projectsTab = { } const project = await findProjectById(Number(request.payload.id)); + if (project) { + await Category.query( + ` + DELETE FROM project_categories_category + WHERE "projectId" = $1 + `, + [project.id], + ); + } + if ( project && Number(request?.payload?.statusId) !== project?.status?.id @@ -1014,6 +1087,7 @@ export const projectsTab = { // We put these status changes in payload, so in after hook we would know to send notification for users request.payload.statusChanges = statusChanges.join(','); } + return request; }, after: async ( @@ -1151,10 +1225,13 @@ export const projectsTab = { }); } } + const categoryIds = extractCategoryIds(request.record.params); + await Promise.all([ refreshUserProjectPowerView(), refreshProjectFuturePowerView(), refreshProjectPowerView(), + saveCategories(project!, categoryIds || []), ]); return request; }, @@ -1350,3 +1427,23 @@ export const projectsTab = { }, }, }; + +async function saveCategories(project: Project, categoryIds?: string[]) { + if (!project) return; + if (!categoryIds || categoryIds?.length === 0) return; + + await Category.query( + ` + DELETE FROM project_categories_category + WHERE "projectId" = $1 + `, + [project.id], + ); + + const categories = await Category.createQueryBuilder('category') + .where('category.id IN (:...ids)', { ids: categoryIds }) + .getMany(); + + project.categories = categories; + await project.save(); +}