Skip to content

Commit 034b09a

Browse files
authored
Merge pull request #774 from CDLUC3/feature/281/JS-create-editable-user-profile-page
Updates for the Admin User Profile page
2 parents 8c6ec9f + e5eaacd commit 034b09a

14 files changed

Lines changed: 716 additions & 73 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## v1.1.0
44

55
### Added
6+
- Added `isArchived` field to the `users` table to help us filter those users out when returned in `users` response [#281]
7+
- Added `findByProjectIdWithPagination` method to the `PlanSearchResult` model for the `plans` resolver [#281]
8+
- Added `userProjects` query to return all projects for a specified user, with search term and pagination [#281]
9+
- Added `updateUserRole` mutation for admins to change a user's role, `updateUserInfo` mutation for super admins to update a specified user's profile, and an `archiveUser` mutation to archive a user[#281]
610
- Added a default researc h output table question to the default template
711
- Added data migration to add `displayAbbreviation` and `displayDomain` to the `affiliations` table
812
- Added data migration to backfill those new DB fields
@@ -102,6 +106,8 @@
102106
- added data-migration to fix question JSON so that `"selected": 0` is now `"selected": false` (and `1` -> `true`).
103107

104108
### Updated
109+
- Updated `plans` resolver to have pagination for a specified `userId` with optional `search term`, and added a chained resolver for `PlanSearchResult` for `templateOwnerAffiliationName` [#281]
110+
- Updated `PlanSearchResult` model to include `createdById` and `templateOwnerAffiliationName` for the Admin Users page [#281]
105111
- Update local migration to add RO question to default template
106112
- Updated `users` resolver to include `role` and `affiliationId` [#240]
107113
- Added `findByAffiliationId` and `search` to pass in `role` as optional [#240]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE `users`
2+
ADD COLUMN `isArchived` tinyint(1) NOT NULL DEFAULT '0' AFTER `active`;

src/models/Plan.ts

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ import { PlanGuidance } from "./Guidance";
1313
import { VersionedTemplate } from "./VersionedTemplate";
1414
import { Project } from "./Project";
1515
import { Tag } from "./Tag";
16+
import {
17+
PaginatedQueryResults,
18+
PaginationOptions,
19+
PaginationOptionsForCursors,
20+
PaginationOptionsForOffsets,
21+
PaginationType
22+
} from '../types/general';
23+
import { prepareObjectForLogs } from '../logger';
1624

1725
export const DEFAULT_TEMPORARY_DMP_ID_PREFIX = 'temp-dmpId-';
1826

@@ -52,6 +60,7 @@ export class PlanSearchResult {
5260
public id: number;
5361
public createdBy: string;
5462
public created: string;
63+
public createdById: number;
5564
public modifiedBy: string;
5665
public modified: string;
5766
public title: string;
@@ -62,6 +71,7 @@ export class PlanSearchResult {
6271
public members: string;
6372
public templateTitle: string;
6473
public versionedTemplateId: number;
74+
public templateOwnerAffiliationName: string;
6575

6676
// The following fields will only be set when the plan is published!
6777
public dmpId: string;
@@ -72,6 +82,7 @@ export class PlanSearchResult {
7282
this.id = options.id;
7383
this.createdBy = options.createdBy;
7484
this.created = options.created;
85+
this.createdById = options.createdById;
7586
this.modifiedBy = options.modifiedBy;
7687
this.modified = options.modified;
7788
this.title = options.title;
@@ -80,23 +91,24 @@ export class PlanSearchResult {
8091
this.featured = options.featured ?? false;
8192
this.funding = options.funding;
8293
this.members = options.members;
83-
this.templateTitle = options.title;
94+
this.templateTitle = options.templateTitle;
8495
this.versionedTemplateId = options.versionedTemplateId;
96+
this.templateOwnerAffiliationName = options.templateOwnerAffiliationName;
8597

8698
this.dmpId = options.dmpId;
8799
this.registeredBy = options.registeredBy;
88100
this.registered = options.registered;
89101
}
90102

91103
/**
92-
* Find high-level details about the plans for a project. This information is
93-
* meant to supply an overview of the plans.
94-
*
95-
* @param reference The caller's reference string for logging purposes'
96-
* @param context The Apollo context object
97-
* @param projectId The ID of the project to return plans for
98-
* @returns An array of PlanSearchResult objects
99-
*/
104+
* Find high-level details about the plans for a project. This information is
105+
* meant to supply an overview of the plans.
106+
*
107+
* @param reference The caller's reference string for logging purposes'
108+
* @param context The Apollo context object
109+
* @param projectId The ID of the project to return plans for
110+
* @returns An array of PlanSearchResult objects
111+
*/
100112
static async findByProjectId(reference: string, context: MyContext, projectId: number): Promise<PlanSearchResult[]> {
101113
const sql = 'SELECT p.id, ' +
102114
'CONCAT(cu.givenName, CONCAT(\' \', cu.surName)) createdBy, p.created, ' +
@@ -125,6 +137,97 @@ export class PlanSearchResult {
125137
const results = await Plan.query(context, sql, [projectId?.toString()], reference);
126138
return Array.isArray(results) ? results.map((entry) => new PlanSearchResult(entry)) : [];
127139
}
140+
141+
/**
142+
* Find projects/plans for a specified userId, with pagination and optional search term filtering.
143+
* This method returns a paginated list of PlanSearchResult objects that match the search criteria.
144+
*
145+
* @param reference The caller's reference string for logging purposes'
146+
* @param context The Apollo context object
147+
* @param userId The ID of the user to return projects for
148+
* @param options Pagination options for the query
149+
* @param term Optional search term to filter the results
150+
* @returns An array of PlanSearchResult objects
151+
*/
152+
static async findByUserIdWithPagination(
153+
reference: string,
154+
context: MyContext,
155+
userId: number,
156+
options: PaginationOptions = Plan.getDefaultPaginationOptions(),
157+
term?: string,
158+
): Promise<PaginatedQueryResults<PlanSearchResult>> {
159+
const whereFilters = ['p.createdById = ?'];
160+
const values = [userId.toString()];
161+
162+
// Handle the incoming search term
163+
const searchTerm = (term ?? '').toLowerCase().trim();
164+
if (searchTerm) {
165+
whereFilters.push(`(
166+
LOWER(p.title) LIKE ? OR
167+
LOWER(vt.name) LIKE ?
168+
)`);
169+
values.push(`%${searchTerm}%`, `%${searchTerm}%`);
170+
}
171+
172+
const sqlStatement = `
173+
SELECT p.id, p.createdById,
174+
CONCAT(cu.givenName, ' ', cu.surName) createdBy, p.created,
175+
CONCAT(cm.givenName, ' ', cm.surName) modifiedBy, p.modified,
176+
p.versionedTemplateId, p.title, p.status, p.visibility, p.dmpId,
177+
vt.name AS templateTitle,
178+
CONCAT(cr.givenName, ' ', cr.surName) registeredBy, p.registered, p.featured,
179+
GROUP_CONCAT(DISTINCT CONCAT(prc.givenName, ' ', prc.surName, ' (', r.label, ')')) members,
180+
GROUP_CONCAT(DISTINCT fundings.name) funding
181+
FROM plans p
182+
LEFT JOIN users cu ON cu.id = p.createdById
183+
LEFT JOIN users cm ON cm.id = p.modifiedById
184+
LEFT JOIN users cr ON cr.id = p.registeredById
185+
LEFT JOIN versionedTemplates vt ON vt.id = p.versionedTemplateId
186+
LEFT JOIN planMembers plc ON plc.planId = p.id
187+
LEFT JOIN projectMembers prc ON prc.id = plc.projectMemberId
188+
LEFT JOIN planMemberRoles plcr ON plc.id = plcr.planMemberId
189+
LEFT JOIN memberRoles r ON plcr.memberRoleId = r.id
190+
LEFT JOIN planFundings ON planFundings.planId = p.id
191+
LEFT JOIN projectFundings ON projectFundings.id = planFundings.projectFundingId
192+
LEFT JOIN affiliations fundings ON projectFundings.affiliationId = fundings.uri
193+
`;
194+
195+
const groupBy = `
196+
GROUP BY p.id, p.createdById,cu.givenName, cu.surName, cm.givenName, cm.surName,
197+
p.title, p.status, p.visibility,
198+
p.dmpId, cr.givenName, cr.surName, p.registered, p.featured, vt.name
199+
`;
200+
201+
let opts;
202+
if (options.type === PaginationType.OFFSET) {
203+
opts = {
204+
...options,
205+
availableSortFields: ['p.title', 'p.status', 'p.created', 'p.modified', 'p.registered', 'p.visibility'],
206+
} as PaginationOptionsForOffsets;
207+
} else {
208+
opts = {
209+
...options,
210+
cursorField: 'CONCAT(p.title, p.id)',
211+
} as PaginationOptionsForCursors;
212+
}
213+
214+
if (isNullOrUndefined(opts.sortField)) opts.sortField = 'p.created';
215+
if (isNullOrUndefined(opts.sortDir)) opts.sortDir = 'DESC';
216+
opts.countField = 'p.id';
217+
218+
const response: PaginatedQueryResults<PlanSearchResult> = await Plan.queryWithPagination(
219+
context,
220+
sqlStatement,
221+
whereFilters,
222+
groupBy,
223+
values,
224+
opts,
225+
reference,
226+
);
227+
228+
context.logger.debug(prepareObjectForLogs({ options, response }), reference);
229+
return response;
230+
}
128231
}
129232

130233
export enum PlanSectionType {

src/models/User.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export class User extends MySqlModel {
5656

5757
public locked?: boolean;
5858
public active?: boolean;
59+
public isArchived?: boolean;
5960

6061
public tableName = 'users';
6162

@@ -81,6 +82,7 @@ export class User extends MySqlModel {
8182
this.notify_on_feedback_complete = options.notify_on_feedback_complete ?? true;
8283
this.notify_on_plan_shared = options.notify_on_plan_shared ?? true;
8384
this.notify_on_plan_visibility_change = options.notify_on_plan_visibility_change ?? true;
85+
this.isArchived = options.isArchived ?? false;
8486

8587
this.prepForSave();
8688
}
@@ -218,6 +220,8 @@ export class User extends MySqlModel {
218220
const whereFilters = ['u.affiliationId = ?'];
219221
const values = [affiliationId];
220222

223+
whereFilters.push('u.isArchived = 0');
224+
221225
if (!isNullOrUndefined(role)) {
222226
whereFilters.push('u.role = ?');
223227
values.push(role);
@@ -288,6 +292,8 @@ export class User extends MySqlModel {
288292
const whereFilters: string[] = [];
289293
const values: string[] = [];
290294

295+
whereFilters.push('u.isArchived = 0');
296+
291297
// Handle the incoming search term
292298
const searchTerm = (term ?? '').toLowerCase().trim();
293299
if (!isNullOrUndefined(searchTerm)) {

src/models/__mocks__/Plan.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const planToPlanSearchResult = (plan: Plan): PlanSearchResult => {
5454
return {
5555
id: plan.id,
5656
createdBy: casual.full_name,
57+
createdById: plan.createdById,
5758
created: plan.created,
5859
modifiedBy: casual.full_name,
5960
modified: plan.modified,
@@ -68,6 +69,7 @@ const planToPlanSearchResult = (plan: Plan): PlanSearchResult => {
6869
members: casual.full_name,
6970
templateTitle: casual.title,
7071
versionedTemplateId: plan.versionedTemplateId,
72+
templateOwnerAffiliationName: casual.company_name,
7173
}
7274
}
7375

0 commit comments

Comments
 (0)