diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index 7b572f110..6b0d03ec7 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -63,6 +63,7 @@ module.exports = { clientId: 'COMMON_SERVICES_CLIENT_ID', clientSecret: 'COMMON_SERVICES_CLIENT_SECRET', scope: 'COMMON_SERVICES_SCOPE', + url: 'COMMON_SERVICES_URL', }, microsoftGraph: { tokenEndpoint: 'MICROSOFT_GRAPH_TOKEN_ENDPOINT', diff --git a/src/const/fieldTypes.ts b/src/const/fieldTypes.ts index e2e2d7c0c..dd138f1a8 100644 --- a/src/const/fieldTypes.ts +++ b/src/const/fieldTypes.ts @@ -5,6 +5,7 @@ export const MULTISELECT_TYPES: string[] = [ 'tagbox', 'owner', 'users', + 'people', ]; /** List of date field types */ diff --git a/src/routes/download/index.ts b/src/routes/download/index.ts index 1b5550dec..27f36827e 100644 --- a/src/routes/download/index.ts +++ b/src/routes/download/index.ts @@ -141,7 +141,8 @@ router.get('/form/records/:id', async (req, res) => { const records = await Record.find(filter); const rows = await getRows( columns, - getAccessibleFields(records, formAbility) + getAccessibleFields(records, formAbility), + req.headers.authorization ); const type = (req.query ? req.query.type : 'xlsx').toString(); const filename = formatFilename(form.name); @@ -318,7 +319,7 @@ router.get('/resource/records/:id', async (req, res) => { archived: { $ne: true }, }); } - const rows = await getRows(columns, records); + const rows = await getRows(columns, records, req.headers.authorization); const type = (req.query ? req.query.type : 'xlsx').toString(); const filename = formatFilename(resource.name); return await fileBuilder(res, filename, columns, rows, type); diff --git a/src/routes/proxy/index.ts b/src/routes/proxy/index.ts index 89c9dca3b..8c0fc05f1 100644 --- a/src/routes/proxy/index.ts +++ b/src/routes/proxy/index.ts @@ -18,6 +18,23 @@ const router = express.Router(); /** Placeholder to hide settings in UI once saved */ const SETTING_PLACEHOLDER = '●●●●●●●●●●●●●'; +/** common services configuration */ +export const commonServicesConfig = new ApiConfiguration({ + name: 'common-services', + status: 'active', + authType: 'service-to-service', + endpoint: config.get('commonServices.url'), + settings: CryptoJS.AES.encrypt( + JSON.stringify({ + authTargetUrl: config.get('commonServices.tokenEndpoint'), + apiClientID: config.get('commonServices.clientId'), + scope: config.get('commonServices.scope'), + safeSecret: config.get('commonServices.clientSecret'), + }), + config.get('encryption.key') + ).toString(), +}); + /** * Proxy API request * @@ -171,6 +188,16 @@ router.post('/ping/**', async (req: Request, res: Response) => { } }); +router.all('/common-services/**', async (req, res) => { + try { + const path = req.originalUrl.split('common-services').pop().substring(1); + await proxyAPIRequest(req, res, commonServicesConfig, path); + } catch (err) { + logger.error(err.message, { stack: err.stack }); + return res.status(500).send(req.t('common.errors.internalServerError')); + } +}); + /** * Forward requests to actual API using the API Configuration */ diff --git a/src/schema/query/index.ts b/src/schema/query/index.ts index 4d0e6fdda..f4392f312 100644 --- a/src/schema/query/index.ts +++ b/src/schema/query/index.ts @@ -41,6 +41,7 @@ import referenceDataAggregation from './referenceDataAggregation.query'; import dataset from './dataset.query'; import emailNotifications from './emailNotifications.query'; import types from './types.query'; +import people from './people.query'; /** GraphQL query type definition */ const Query = new GraphQLObjectType({ @@ -88,6 +89,7 @@ const Query = new GraphQLObjectType({ layers, layer, draftRecords, + people, }, }); diff --git a/src/schema/query/people.query.ts b/src/schema/query/people.query.ts new file mode 100644 index 000000000..abfbdaa98 --- /dev/null +++ b/src/schema/query/people.query.ts @@ -0,0 +1,67 @@ +import { GraphQLError, GraphQLInt, GraphQLList } from 'graphql'; +import { logger } from '@services/logger.service'; +import { Context } from '@server/apollo/context'; +import GraphQLJSON from 'graphql-type-json'; +import { PersonType } from '@schema/types/person.type'; +import { getPeople } from '@utils/proxy'; + +/** Arguments for the people query */ +type PeopleArgs = { + filter?: any; + offset?: number; + limitItems?: number | null; +}; + +/** + * Return distant users from common services + */ +export default { + type: new GraphQLList(PersonType), + args: { + filter: { type: GraphQLJSON }, + offset: { type: GraphQLInt }, + limitItems: { type: GraphQLInt }, + }, + async resolve(parent, args: PeopleArgs, context: Context) { + if (!args.filter) return []; + try { + // Formatted filter used by the API + const getFormattedFilter = (filter: any) => { + const formattedFilter = `{${filter.logic.toUpperCase()}:[ + ${filter.filters.map((el: any) => { + if (el.operator === 'like') { + el.value = `"%${el.value}%"`; + } else if (el.operator === 'in') { + el.value = el.value.map((e) => `"${e}"`); + el.value = `[${el.value}]`; + } + return `{ ${el.field}_${el.operator}: ${el.value} }`; + })} + ] + }`; + return formattedFilter.replace(/\s/g, ''); + }; + const filter = getFormattedFilter(args.filter); + const people = await getPeople( + context.token, + filter, + args.offset, + args.limitItems + ); + if (people) { + return people.map((person) => { + const updatedPerson = { ...person }; + updatedPerson.id = updatedPerson.userid; + delete updatedPerson.userid; + return updatedPerson; + }); + } + return []; + } catch (err) { + logger.error(err.message, { stack: err.stack }); + throw new GraphQLError( + context.i18next.t('common.errors.internalServerError') + ); + } + }, +}; diff --git a/src/schema/types/draftRecord.type.ts b/src/schema/types/draftRecord.type.ts index d4d40551b..ef2dd8c28 100644 --- a/src/schema/types/draftRecord.type.ts +++ b/src/schema/types/draftRecord.type.ts @@ -50,7 +50,8 @@ export const DraftRecordType = new GraphQLObjectType({ if ( field.choices || field.choicesByUrl || - field.choicesByGraphQL + field.choicesByGraphQL || + ['people', 'singlepeople'].includes(field.type) ) { res[name] = await getDisplayText( field, diff --git a/src/schema/types/person.type.ts b/src/schema/types/person.type.ts new file mode 100644 index 000000000..7e147f40a --- /dev/null +++ b/src/schema/types/person.type.ts @@ -0,0 +1,14 @@ +import { GraphQLObjectType, GraphQLString } from 'graphql'; + +/** + * GraphQL Person type. + */ +export const PersonType = new GraphQLObjectType({ + name: 'Person', + fields: () => ({ + id: { type: GraphQLString }, + firstname: { type: GraphQLString }, + lastname: { type: GraphQLString }, + emailaddress: { type: GraphQLString }, + }), +}); diff --git a/src/schema/types/record.type.ts b/src/schema/types/record.type.ts index 992ad76a6..62ee9725b 100644 --- a/src/schema/types/record.type.ts +++ b/src/schema/types/record.type.ts @@ -75,7 +75,8 @@ export const RecordType = new GraphQLObjectType({ if ( field.choices || field.choicesByUrl || - field.choicesByGraphQL + field.choicesByGraphQL || + ['people', 'singlepeople'].includes(field.type) ) { res[name] = await getDisplayText( field, diff --git a/src/schema/types/resource.type.ts b/src/schema/types/resource.type.ts index 5cb9b7ff6..87972fedb 100644 --- a/src/schema/types/resource.type.ts +++ b/src/schema/types/resource.type.ts @@ -224,6 +224,7 @@ export const ResourceType = new GraphQLObjectType({ { id: r._id } ), })); + return { pageInfo: { hasNextPage, diff --git a/src/utils/aggregation/setDisplayText.ts b/src/utils/aggregation/setDisplayText.ts index eb11c2cd8..42d75f751 100644 --- a/src/utils/aggregation/setDisplayText.ts +++ b/src/utils/aggregation/setDisplayText.ts @@ -38,7 +38,10 @@ const setDisplayText = async ( const formField = lookAt.find((field: any) => { return ( lookFor === field.name && - (field.choices || field.choicesByUrl || field.choicesByGraphQL) + (field.choices || + field.choicesByUrl || + field.choicesByGraphQL || + ['people', 'singlepeople'].includes(field.type)) ); }); if (formField) { @@ -47,10 +50,14 @@ const setDisplayText = async ( return { ...(await acc) }; } }; - const fieldWithChoices = await mappedFields.reduce(reducer, {}); - for (const [key, field] of Object.entries(fieldWithChoices)) { + const fieldWithChoices: any = await mappedFields.reduce(reducer, {}); + for (const [key, field] of Object.entries(fieldWithChoices)) { // Fetch choices from source ( static / rest / graphql ) - const choices = await getFullChoices(field, context); + let peopleIds = []; + if (['people', 'singlepeople'].includes(field.type)) { + peopleIds = items.map((item) => item[key]); + } + const choices = await getFullChoices(field, context, peopleIds); for (const item of items) { const fieldValue = get(item, key, null); if (fieldValue) { diff --git a/src/utils/files/getRows.ts b/src/utils/files/getRows.ts index 152f78375..af0e6ab76 100644 --- a/src/utils/files/getRows.ts +++ b/src/utils/files/getRows.ts @@ -1,17 +1,20 @@ import get from 'lodash/get'; import set from 'lodash/set'; import { getText } from '../form/getDisplayText'; +import { getPeople, getPeopleFilter } from '@utils/proxy'; /** * Transforms records into export rows, using fields definition. * * @param columns definition of export columns. * @param records list of records. + * @param token used to make graphql queries * @returns list of export rows. */ export const getRows = async ( columns: any[], - records: any[] + records: any[], + token?: string ): Promise => { const rows = []; for (const record of records) { @@ -113,6 +116,29 @@ export const getRows = async ( } break; } + case 'people': + case 'singlepeople': { + const value = get(data, column.field); + const filter = getPeopleFilter(value); + const people = await getPeople(token, filter); + if (!people) { + return; + } + set( + row, + column.name, + people + .map((x) => { + const fullname = + x.firstname && x.lastname + ? `${x.firstname}, ${x.lastname}` + : x.firstname || x.lastname; + return `${fullname} (${x.emailaddress})`; + }) + .join(', ') + ); + break; + } default: { const value = column.default ? get(record, column.field) diff --git a/src/utils/files/getRowsFromMeta.ts b/src/utils/files/getRowsFromMeta.ts index 05c9d122b..8eae632f9 100644 --- a/src/utils/files/getRowsFromMeta.ts +++ b/src/utils/files/getRowsFromMeta.ts @@ -58,6 +58,8 @@ export const getRowsFromMeta = (columns: any[], records: any[]): any[] => { set(row, column.name, Array.isArray(value) ? value.join(',') : value); break; } + case 'people': + case 'singlepeople': case 'users': { let value: any = get(record, column.field); const choices = column.meta.field.choices || []; @@ -92,16 +94,8 @@ export const getRowsFromMeta = (columns: any[], records: any[]): any[] => { set(row, column.name, Array.isArray(value) ? value.join(',') : value); break; } - case 'multipletext': { - const value = get(record, column.name); - set(row, column.name, value); - break; - } - case 'matrix': { - const value = get(record, column.name); - set(row, column.name, value); - break; - } + case 'multipletext': + case 'matrix': case 'matrixdropdown': { const value = get(record, column.name); set(row, column.name, value); diff --git a/src/utils/form/getDisplayText.ts b/src/utils/form/getDisplayText.ts index 1f89352e1..a32753bd4 100644 --- a/src/utils/form/getDisplayText.ts +++ b/src/utils/form/getDisplayText.ts @@ -5,6 +5,7 @@ import { logger } from '@services/logger.service'; import axios from 'axios'; import get from 'lodash/get'; import jsonpath from 'jsonpath'; +import { getPeople, getPeopleFilter } from '@utils/proxy'; /** * Gets display text from choice value. @@ -38,11 +39,13 @@ export const getText = (choices: any[], value: any): string => { * * @param field field to get value of. * @param context provides the data sources context. + * @param peopleIds ids of people to fetch * @returns Choice list of the field. */ export const getFullChoices = async ( field: any, - context: Context + context: Context, + peopleIds?: string[] | string ): Promise<{ value: string; text: string }[] | string[]> => { try { if (field.choicesByUrl) { @@ -110,6 +113,23 @@ export const getFullChoices = async ( choices.push({ [valueField]: 'other', [textField]: 'Other' }); } return choices; + } else if (['people', 'singlepeople'].includes(field.type)) { + // Generate a filter to only fetch users we need + const filter = getPeopleFilter(peopleIds); + const people = await getPeople(context.token, filter); + if (!people) { + return []; + } + return people.map((x: any) => { + const fullname = + x.firstname && x.lastname + ? `${x.firstname}, ${x.lastname}` + : x.firstname || x.lastname; + return { + text: `${fullname} (${x.emailaddress})`, + value: x.userid, + }; + }); } else { return field.choices; } @@ -133,7 +153,7 @@ const getDisplayText = async ( context: Context ): Promise => { const choices: { value: string; text: string }[] | string[] = - await getFullChoices(field, context); + await getFullChoices(field, context, [value]); if (choices && choices.length) { if (Array.isArray(value)) { return value.map((x) => getText(choices, x)); diff --git a/src/utils/form/getFieldType.ts b/src/utils/form/getFieldType.ts index eadef92ea..4fdff0ac9 100644 --- a/src/utils/form/getFieldType.ts +++ b/src/utils/form/getFieldType.ts @@ -81,6 +81,10 @@ export const getFieldType = async (question: { return 'users'; case 'owner': return 'owner'; + case 'people': + return 'people'; + case 'singlepeople': + return 'singlepeople'; case 'geospatial': return 'geospatial'; default: diff --git a/src/utils/form/metadata.helper.ts b/src/utils/form/metadata.helper.ts index d77b8caca..4954a6878 100644 --- a/src/utils/form/metadata.helper.ts +++ b/src/utils/form/metadata.helper.ts @@ -328,6 +328,27 @@ export const getMetaData = async ( fieldMeta._field = field; break; } + case 'people': + fieldMeta.editor = 'people'; + fieldMeta.multiSelect = true; + fieldMeta.filter = { + operators: [ + 'isempty', + 'isnotempty', + 'eq', + 'neq', + 'contains', + 'doesnotcontain', + ], + }; + break; + case 'singlepeople': { + fieldMeta.editor = 'people'; + fieldMeta.filter = { + operators: ['isempty', 'isnotempty', 'eq', 'neq'], + }; + break; + } default: { break; } diff --git a/src/utils/history/recordHistory.ts b/src/utils/history/recordHistory.ts index 8b43b19bb..e31ed613f 100644 --- a/src/utils/history/recordHistory.ts +++ b/src/utils/history/recordHistory.ts @@ -358,32 +358,32 @@ export class RecordHistory { }); } else { // Otherwise, get the display value from choices stored in the field/choicesByUrl - const choices = await getFullChoices(field, this.options.context); + const choices = await getFullChoices( + field, + this.options.context, + [].concat(change.old).concat(change.new) + ); if (change.old !== undefined) { - if (isArray(change.old)) { - change.old = [ - ...new Set( - change.old.map((item: string) => - getOptionFromChoices(item, choices) - ) - ), - ]; - } else { - change.old = getOptionFromChoices(change.old, choices); - } + change.old = Array.isArray(change.old) + ? [ + ...new Set( + change.old.map((item: string) => + getOptionFromChoices(item, choices) + ) + ), + ] + : getOptionFromChoices(change.old, choices); } if (change.new !== undefined) { - if (isArray(change.new)) { - change.new = [ - ...new Set( - change.new.map((item: string) => - getOptionFromChoices(item, choices) - ) - ), - ]; - } else { - change.new = getOptionFromChoices(change.new, choices); - } + change.new = Array.isArray(change.new) + ? [ + ...new Set( + change.new.map((item: string) => + getOptionFromChoices(item, choices) + ) + ), + ] + : getOptionFromChoices(change.new, choices); } } }; @@ -609,6 +609,10 @@ export class RecordHistory { if (change.new !== undefined) change.new = new Date(change.new).toTimeString(); break; + case 'people': + case 'singlepeople': + await formatSelectable(field, change); + break; default: // for all other cases, keep the values break; diff --git a/src/utils/proxy/getPeople.ts b/src/utils/proxy/getPeople.ts new file mode 100644 index 000000000..b283167b6 --- /dev/null +++ b/src/utils/proxy/getPeople.ts @@ -0,0 +1,68 @@ +import axios from 'axios'; +import { getToken } from './authManagement'; +import { commonServicesConfig } from '@routes/proxy'; +import { isArray } from 'lodash'; + +/** + * Generate a filter to only fetch users we need + * + * @param people list of people + * @returns the filter to get people + */ +export const getPeopleFilter = (people: string[] | string) => { + people = isArray(people) ? people : [people]; + const formattedFilter = `{ +userid_in: +[${people.map((el) => `"${el}"`)}] +}`; + return formattedFilter.replace(/\s/g, ''); +}; + +/** + * Fetches the people + * + * @param accessToken The authorization token + * @param filter The filter used for fetching the distant users + * @param offset offset to query users + * @param limitItems number of maximum items to fetch + * @returns the choices + */ +export const getPeople = async ( + accessToken: string, + filter: any, + offset = 0, + limitItems = null +): Promise => { + const query = `query { + users( + filter: ${filter} + offset: ${offset} + ${limitItems ? `limitItems: ${limitItems}` : ''} + ) { + userid + firstname + lastname + emailaddress + } + }`; + try { + const token = await getToken(commonServicesConfig, accessToken); + let people: any[] = []; + await axios({ + url: `${commonServicesConfig.endpoint}/graphql`, + method: 'post', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + data: { + query: query, + }, + }).then(({ data }) => { + people = data?.data?.users; + }); + return people; + } catch { + return []; + } +}; diff --git a/src/utils/proxy/index.ts b/src/utils/proxy/index.ts index 336d85a33..ce4002cc3 100644 --- a/src/utils/proxy/index.ts +++ b/src/utils/proxy/index.ts @@ -1,2 +1,3 @@ export * from './authManagement'; export * from './getChoices'; +export * from './getPeople'; diff --git a/src/utils/schema/introspection/getFieldType.ts b/src/utils/schema/introspection/getFieldType.ts index ab48c05a2..7164d82ed 100644 --- a/src/utils/schema/introspection/getFieldType.ts +++ b/src/utils/schema/introspection/getFieldType.ts @@ -122,6 +122,12 @@ const getFieldType = ( case 'owner': { return GraphQLJSON; } + case 'people': { + return GraphQLJSON; + } + case 'singlepeople': { + return GraphQLString; + } case 'geospatial': { return GraphQLJSON; } diff --git a/src/utils/schema/resolvers/Meta/getMetaFieldResolver.ts b/src/utils/schema/resolvers/Meta/getMetaFieldResolver.ts index 94e3b19c5..a61b78b5f 100644 --- a/src/utils/schema/resolvers/Meta/getMetaFieldResolver.ts +++ b/src/utils/schema/resolvers/Meta/getMetaFieldResolver.ts @@ -4,14 +4,17 @@ import getMetaOwnerResolver from './getMetaOwnerResolver'; import getMetaUsersResolver from './getMetaUsersResolver'; import getMetaRadioResolver from './getMetaRadiogroupResolver'; import getMetaTagboxResolver from './getMetaTagboxResolver'; +import getMetaPeopleResolver from './getMetaPeopleResolver'; +import { Context } from '@server/apollo/context'; /** * Return GraphQL resolver of the field, based on its type. * * @param field field definition. + * @param context graphQL context. * @returns resolver of the field. */ -const getMetaFieldResolver = (field: any) => { +const getMetaFieldResolver = (field: any, context: Context) => { switch (field.type) { case 'dropdown': { return getMetaDropdownResolver(field); @@ -28,6 +31,10 @@ const getMetaFieldResolver = (field: any) => { case 'users': { return getMetaUsersResolver(field); } + case 'people': + case 'singlepeople': { + return getMetaPeopleResolver(field, context); + } case 'owner': { return getMetaOwnerResolver(field); } diff --git a/src/utils/schema/resolvers/Meta/getMetaPeopleResolver.ts b/src/utils/schema/resolvers/Meta/getMetaPeopleResolver.ts new file mode 100644 index 000000000..e0ff39bde --- /dev/null +++ b/src/utils/schema/resolvers/Meta/getMetaPeopleResolver.ts @@ -0,0 +1,57 @@ +import { Record } from '@models'; +import { Context } from '@server/apollo/context'; +import { getPeople, getPeopleFilter } from '@utils/proxy'; +import { isArray } from 'lodash'; + +/** + * Return people meta resolver. + * + * @param field field definition. + * @param context graphQL context. + * @returns People resolver. + */ +const getMetaPeopleResolver = async (field: any, context: Context) => { + // Optimize the query by only fetching target field + const records = await Record.find( + { + resource: field.resource, + archived: false, + }, + { [`data.${field.name}`]: 1 } + ); + const peopleIds = []; + records.forEach((record) => { + const propertyValue = record.data[field.name]; + if (isArray(propertyValue)) + propertyValue?.flat().forEach((id: string) => { + if (!peopleIds.includes(id)) { + peopleIds.push(id); + } + }); + else { + peopleIds.push(propertyValue); + } + }); + const filter = getPeopleFilter(peopleIds); + const people = await getPeople(context.token, filter); + if (!people) { + return []; + } + + delete field.resource; + + return Object.assign(field, { + choices: people.map((x: any) => { + const fullname = + x.firstname && x.lastname + ? `${x.firstname}, ${x.lastname}` + : x.firstname || x.lastname; + return { + text: `${fullname} (${x.emailaddress})`, + value: x.userid, + }; + }), + }); +}; + +export default getMetaPeopleResolver; diff --git a/src/utils/schema/resolvers/Meta/index.ts b/src/utils/schema/resolvers/Meta/index.ts index e3121258e..fcc44ccb5 100644 --- a/src/utils/schema/resolvers/Meta/index.ts +++ b/src/utils/schema/resolvers/Meta/index.ts @@ -161,7 +161,7 @@ export const getMetaResolver = ( .reduce( (resolvers, fieldName) => Object.assign({}, resolvers, { - [fieldName]: (parent) => { + [fieldName]: (parent, args, context) => { const field = relationshipFields.includes(fieldName) ? parent[ fieldName.slice( @@ -170,7 +170,10 @@ export const getMetaResolver = ( ) ] : parent[fieldName]; - return getMetaFieldResolver(field); + if (['people', 'singlepeople'].includes(field.type)) { + field.resource = id; + } + return getMetaFieldResolver(field, context); }, }), {} diff --git a/src/utils/schema/resolvers/Query/all.ts b/src/utils/schema/resolvers/Query/all.ts index 0b8967648..1a8946145 100644 --- a/src/utils/schema/resolvers/Query/all.ts +++ b/src/utils/schema/resolvers/Query/all.ts @@ -12,7 +12,7 @@ import getStyle from './getStyle'; import getSortAggregation from './getSortAggregation'; import mongoose from 'mongoose'; import buildReferenceDataAggregation from '@utils/aggregation/buildReferenceDataAggregation'; -import { getAccessibleFields } from '@utils/form'; +import { getAccessibleFields, getFullChoices } from '@utils/form'; import buildCalculatedFieldPipeline from '@utils/aggregation/buildCalculatedFieldPipeline'; import { logger } from '@services/logger.service'; import checkPageSize from '@utils/schema/errors/checkPageSize.util'; @@ -191,8 +191,13 @@ const getQueryFields = ( * * @param records Records array to be sorted * @param sortArgs Sort arguments + * @param sortArgs.sortField sort field + * @param sortArgs.sortOrder sort order */ -const sortRecords = (records: any[], sortArgs: any): void => { +const sortRecords = ( + records: any[], + sortArgs: { sortField: string; sortOrder: 'asc' | 'desc' } +): void => { if (sortArgs.sortField && sortArgs.sortOrder) { const sortField = FLAT_DEFAULT_FIELDS.includes(sortArgs.sortField) ? sortArgs.sortField @@ -514,6 +519,32 @@ export default (entityName: string, fieldsByName: any, idsByName: any) => totalCount = aggregation[0]?.totalCount[0]?.count || 0; } + const fullSortField = fields.find((field) => field.name === sortField); + if ( + fullSortField && + ['people', 'singlepeople'].includes(fullSortField.type) + ) { + const peopleIds = items.map((item) => item.data[sortField]); + const choices = await getFullChoices(fullSortField, context, peopleIds); + + // Assuming choices is an array of objects like [{text, value}] + const choicesMap = new Map( + (choices as { value: string; text: string }[]).map((choice) => [ + choice.value as string, + choice.text as string, + ]) + ); + + // Sort the items based on the corresponding text field in choices + items.sort((a, b) => { + const textA = choicesMap.get(a.data[sortField]); + const textB = choicesMap.get(b.data[sortField]); + return sortOrder === 'asc' + ? textA.localeCompare(textB) + : -textA.localeCompare(textB); + }); + } + // Deal with resource/resources questions on THIS form const resourcesFields: any[] = fields.reduce((arr, field) => { if (field.type === 'resource' || field.type === 'resources') { diff --git a/src/utils/schema/resolvers/Query/getSortAggregation.ts b/src/utils/schema/resolvers/Query/getSortAggregation.ts index d0bbd8ccb..dae8a8b8d 100644 --- a/src/utils/schema/resolvers/Query/getSortAggregation.ts +++ b/src/utils/schema/resolvers/Query/getSortAggregation.ts @@ -27,7 +27,10 @@ const getSortAggregation = async ( // If we need to populate choices to sort on the text value if ( field && - (field.choices || field.choicesByUrl || field.choicesByGraphQL) + (field.choices || + field.choicesByUrl || + field.choicesByGraphQL || + ['people, singlepeople'].includes(field.type)) ) { const choices = (await getFullChoices(field, context)) || []; const choicesValue = choices.map((x) => x.value);