diff --git a/packages/admin-panel/src/routes/surveys/surveys.js b/packages/admin-panel/src/routes/surveys/surveys.js index 816082a1af..754dcaba5b 100644 --- a/packages/admin-panel/src/routes/surveys/surveys.js +++ b/packages/admin-panel/src/routes/surveys/surveys.js @@ -541,7 +541,7 @@ const QUESTION_COLUMNS = [ fieldName: 'permissionGroup', optionsEndpoint: 'permissionGroups', optionLabelKey: 'name', - optionValueKey: 'name', + optionValueKey: 'id', labelTooltip: 'Select the permission group the user list should be filtered by', }, ], diff --git a/packages/central-server/src/apiV2/import/importSurveys/ConfigImporter/ConfigImporter.js b/packages/central-server/src/apiV2/import/importSurveys/ConfigImporter/ConfigImporter.js index 84556a777f..ed08da68f5 100644 --- a/packages/central-server/src/apiV2/import/importSurveys/ConfigImporter/ConfigImporter.js +++ b/packages/central-server/src/apiV2/import/importSurveys/ConfigImporter/ConfigImporter.js @@ -12,6 +12,7 @@ import { processConditionConfig } from './processConditionConfig'; import { processAutocompleteConfig } from './processAutocompleteConfig'; import { processEntityConfig } from './processEntityConfig'; import { processTaskConfig } from './processTaskConfig'; +import { processUserConfig } from './processUserConfig'; const { CODE_GENERATOR, ARITHMETIC, CONDITION, AUTOCOMPLETE, ENTITY, PRIMARY_ENTITY, TASK, USER } = ANSWER_TYPES; @@ -91,7 +92,8 @@ export class ConfigImporter { return { task: taskConfig }; } case USER: { - return { user: config }; + const userConfig = await processUserConfig(this.models, config); + return { user: userConfig }; } default: diff --git a/packages/central-server/src/apiV2/import/importSurveys/ConfigImporter/processUserConfig.js b/packages/central-server/src/apiV2/import/importSurveys/ConfigImporter/processUserConfig.js index e69de29bb2..99277bda07 100644 --- a/packages/central-server/src/apiV2/import/importSurveys/ConfigImporter/processUserConfig.js +++ b/packages/central-server/src/apiV2/import/importSurveys/ConfigImporter/processUserConfig.js @@ -0,0 +1,22 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +const translatePermissionGroup = async (config, models) => { + const { permissionGroup } = config; + const actualPermissionGroup = await models.permissionGroup.findOne({ name: permissionGroup }); + if (!actualPermissionGroup) { + throw new Error(`Permission group ${permissionGroup} not found`); + } + + return { + ...config, + permissionGroup: actualPermissionGroup.id, + }; +}; + +export const processUserConfig = async (models, config) => { + const translatedConfig = await translatePermissionGroup(config, models); + return translatedConfig; +}; diff --git a/packages/datatrak-web-server/src/app/createApp.ts b/packages/datatrak-web-server/src/app/createApp.ts index 83bc542d98..aa59d0bc08 100644 --- a/packages/datatrak-web-server/src/app/createApp.ts +++ b/packages/datatrak-web-server/src/app/createApp.ts @@ -29,6 +29,8 @@ import { GenerateLoginTokenRoute, LeaderboardRequest, LeaderboardRoute, + PermissionGroupUsersRequest, + PermissionGroupUsersRoute, ProjectRequest, ProjectRoute, ProjectsRequest, @@ -88,6 +90,7 @@ export async function createApp() { .get('tasks/:taskId', handleWith(TaskRoute)) .get('surveyResponse/:id', handleWith(SingleSurveyResponseRoute)) .get('users/:surveyCode/:countryCode', handleWith(SurveyUsersRoute)) + .get('users/:countryCode', handleWith(PermissionGroupUsersRoute)) // Post Routes .post('tasks', handleWith(CreateTaskRoute)) .put('tasks/:taskId', handleWith(EditTaskRoute)) diff --git a/packages/datatrak-web-server/src/routes/PermissionGroupUsersRoute.ts b/packages/datatrak-web-server/src/routes/PermissionGroupUsersRoute.ts new file mode 100644 index 0000000000..0dec291671 --- /dev/null +++ b/packages/datatrak-web-server/src/routes/PermissionGroupUsersRoute.ts @@ -0,0 +1,43 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +/** + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +import { Request } from 'express'; +import { Route } from '@tupaia/server-boilerplate'; +import { DatatrakWebUsersRequest } from '@tupaia/types'; +import { getFilteredUsers } from '../utils'; + +export type PermissionGroupUsersRequest = Request< + DatatrakWebUsersRequest.Params, + DatatrakWebUsersRequest.ResBody, + DatatrakWebUsersRequest.ReqBody, + DatatrakWebUsersRequest.ReqQuery +>; + +export class PermissionGroupUsersRoute extends Route { + public async buildResponse() { + const { models, params, query } = this.req; + const { countryCode } = params; + + const { searchTerm, permissionGroupId } = query; + + if (!permissionGroupId) { + throw new Error('Permission group id is required'); + } + + // get the permission group + const permissionGroup = await models.permissionGroup.findById(permissionGroupId); + + if (!permissionGroup) { + throw new Error(`Permission group with id '${permissionGroupId}' not found`); + } + + return getFilteredUsers(models, countryCode, permissionGroup, searchTerm); + } +} diff --git a/packages/datatrak-web-server/src/routes/SurveyUsersRoute.ts b/packages/datatrak-web-server/src/routes/SurveyUsersRoute.ts index d72b7812e1..40b75589f0 100644 --- a/packages/datatrak-web-server/src/routes/SurveyUsersRoute.ts +++ b/packages/datatrak-web-server/src/routes/SurveyUsersRoute.ts @@ -5,33 +5,16 @@ import { Request } from 'express'; import { Route } from '@tupaia/server-boilerplate'; -import { DatatrakWebSurveyUsersRequest, EntityType } from '@tupaia/types'; -import { QUERY_CONJUNCTIONS } from '@tupaia/database'; - -const USERS_EXCLUDED_FROM_LIST = [ - 'edmofro@gmail.com', // Edwin - 'kahlinda.mahoney@gmail.com', // Kahlinda - 'lparish1980@gmail.com', // Lewis - 'sus.lake@gmail.com', // Susie - 'michaelnunan@hotmail.com', // Michael - 'vanbeekandrew@gmail.com', // Andrew - 'gerardckelly@gmail.com', // Gerry K - 'geoffreyfisher@hotmail.com', // Geoff F - 'josh@sussol.net', // mSupply API Client - 'unicef.laos.edu@gmail.com', // Laos Schools Data Collector - 'tamanu-server@tupaia.org', // Tamanu Server - 'public@tupaia.org', // Public User -]; +import { DatatrakWebUsersRequest } from '@tupaia/types'; +import { getFilteredUsers } from '../utils'; export type SurveyUsersRequest = Request< - DatatrakWebSurveyUsersRequest.Params, - DatatrakWebSurveyUsersRequest.ResBody, - DatatrakWebSurveyUsersRequest.ReqBody, - DatatrakWebSurveyUsersRequest.ReqQuery + DatatrakWebUsersRequest.Params, + DatatrakWebUsersRequest.ResBody, + DatatrakWebUsersRequest.ReqBody, + DatatrakWebUsersRequest.ReqQuery >; -const DEFAULT_PAGE_SIZE = 100; - export class SurveyUsersRoute extends Route { public async buildResponse() { const { models, params, query } = this.req; @@ -58,45 +41,6 @@ export class SurveyUsersRoute extends Route { throw new Error(`Permission group with id ${permissionGroupId} not found`); } - // get the ancestors of the permission group - const permissionGroupWithAncestors = await permissionGroup.getAncestors(); - - const entity = await models.entity.findOne({ - country_code: countryCode, - type: EntityType.country, - }); - - // get the user entity permissions for the permission group and its ancestors - const userEntityPermissions = await models.userEntityPermission.find({ - permission_group_id: permissionGroupWithAncestors.map(p => p.id), - entity_id: entity.id, - }); - - const userIds = userEntityPermissions.map(uep => uep.user_id); - - const usersFilter = { - id: userIds, - email: { comparator: 'not in', comparisonValue: USERS_EXCLUDED_FROM_LIST }, - [QUERY_CONJUNCTIONS.RAW]: { - // exclude E2E users and any internal users - sql: `(email NOT LIKE '%tupaia.org' AND email NOT LIKE '%beyondessential.com.au' AND email NOT LIKE '%@bes.au')`, - }, - } as Record; - - if (searchTerm) { - usersFilter.full_name = { comparator: 'ilike', comparisonValue: `${searchTerm}%` }; - } - - const users = await models.user.find(usersFilter, { - sort: ['full_name ASC'], - limit: DEFAULT_PAGE_SIZE, - }); - const userData = users.map(user => ({ - id: user.id, - name: user.full_name, - })); - - // only return the id and name of the users - return userData; + return getFilteredUsers(models, countryCode, permissionGroup, searchTerm); } } diff --git a/packages/datatrak-web-server/src/routes/index.ts b/packages/datatrak-web-server/src/routes/index.ts index 49fa0d556b..70084946ab 100644 --- a/packages/datatrak-web-server/src/routes/index.ts +++ b/packages/datatrak-web-server/src/routes/index.ts @@ -26,3 +26,7 @@ export { TaskRequest, TaskRoute } from './TaskRoute'; export { SurveyUsersRequest, SurveyUsersRoute } from './SurveyUsersRoute'; export { CreateTaskRequest, CreateTaskRoute } from './CreateTaskRoute'; export { EditTaskRequest, EditTaskRoute } from './EditTaskRoute'; +export { + PermissionGroupUsersRequest, + PermissionGroupUsersRoute, +} from './PermissionGroupUsersRoute'; diff --git a/packages/datatrak-web-server/src/utils/getFilteredUsers.ts b/packages/datatrak-web-server/src/utils/getFilteredUsers.ts new file mode 100644 index 0000000000..bd4e097012 --- /dev/null +++ b/packages/datatrak-web-server/src/utils/getFilteredUsers.ts @@ -0,0 +1,71 @@ +/** + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +import { QUERY_CONJUNCTIONS } from '@tupaia/database'; +import { Country, EntityType } from '@tupaia/types'; +import { DatatrakWebServerModelRegistry } from '../types'; +import { PermissionGroupRecord } from '@tupaia/server-boilerplate'; + +const USERS_EXCLUDED_FROM_LIST = [ + 'edmofro@gmail.com', // Edwin + 'kahlinda.mahoney@gmail.com', // Kahlinda + 'lparish1980@gmail.com', // Lewis + 'sus.lake@gmail.com', // Susie + 'michaelnunan@hotmail.com', // Michael + 'vanbeekandrew@gmail.com', // Andrew + 'gerardckelly@gmail.com', // Gerry K + 'geoffreyfisher@hotmail.com', // Geoff F + 'josh@sussol.net', // mSupply API Client + 'unicef.laos.edu@gmail.com', // Laos Schools Data Collector + 'tamanu-server@tupaia.org', // Tamanu Server + 'public@tupaia.org', // Public User +]; + +const DEFAULT_PAGE_SIZE = 100; + +export const getFilteredUsers = async ( + models: DatatrakWebServerModelRegistry, + countryCode: Country['code'], + permissionGroup: PermissionGroupRecord, + searchTerm?: string, +) => { + // get the ancestors of the permission group + const permissionGroupWithAncestors = await permissionGroup.getAncestors(); + const entity = await models.entity.findOne({ + country_code: countryCode, + type: EntityType.country, + }); + + // get the user entity permissions for the permission group and its ancestors + const userEntityPermissions = await models.userEntityPermission.find({ + permission_group_id: permissionGroupWithAncestors.map(p => p.id), + entity_id: entity.id, + }); + + const userIds = userEntityPermissions.map(uep => uep.user_id); + + const usersFilter = { + id: userIds, + email: { comparator: 'not in', comparisonValue: USERS_EXCLUDED_FROM_LIST }, + [QUERY_CONJUNCTIONS.RAW]: { + // exclude E2E users and any internal users + sql: `(email NOT LIKE '%tupaia.org' AND email NOT LIKE '%beyondessential.com.au' AND email NOT LIKE '%@bes.au')`, + }, + } as Record; + + if (searchTerm) { + usersFilter.full_name = { comparator: 'ilike', comparisonValue: `${searchTerm}%` }; + } + + const users = await models.user.find(usersFilter, { + sort: ['full_name ASC'], + limit: DEFAULT_PAGE_SIZE, + }); + + return users.map(user => ({ + id: user.id, + name: user.full_name, + })); +}; diff --git a/packages/datatrak-web-server/src/utils/index.ts b/packages/datatrak-web-server/src/utils/index.ts index 996544de76..e0cb614c5b 100644 --- a/packages/datatrak-web-server/src/utils/index.ts +++ b/packages/datatrak-web-server/src/utils/index.ts @@ -7,3 +7,4 @@ export { sortSearchResults } from './sortSearchResults'; export { addRecentEntities } from './addRecentEntities'; export * from './formatTaskResponse'; export * from './formatTaskChanges'; +export { getFilteredUsers } from './getFilteredUsers'; diff --git a/packages/datatrak-web/src/__tests__/features/Questions/UserQuestion.test.tsx b/packages/datatrak-web/src/__tests__/features/Questions/UserQuestion.test.tsx new file mode 100644 index 0000000000..283a81d6d1 --- /dev/null +++ b/packages/datatrak-web/src/__tests__/features/Questions/UserQuestion.test.tsx @@ -0,0 +1,106 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ +import React, { Ref } from 'react'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { renderComponent } from '../../helpers/render'; +import { UserQuestion } from '../../../features/Questions'; + +jest.mock('../../../features/Survey/SurveyContext/SurveyContext.tsx', () => ({ + useSurveyForm: () => ({ + countryCode: 'DL', + }), +})); + +jest.mock('../../../api/queries/useUser', () => { + return { + useUser: jest.fn().mockReturnValue({}), + }; +}); + +const users = [ + { + name: 'Teddy Bear', + id: '1', + }, + { + name: 'Grizzly Bear', + id: '2', + }, + { + name: 'Panda Bear', + id: '3', + }, + { + name: 'Koala Bear', + id: '4', + }, + { + name: 'Polar Bear', + id: '5', + }, +]; +const server = setupServer( + rest.get('*/v1/users/DL', (_, res, ctx) => { + return res(ctx.status(200), ctx.json(users)); + }), +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +describe('User Question', () => { + const onChange = jest.fn(); + const props = { + id: 'theId', + label: 'Who should the task be assigned to?', + name: 'assignee', + config: { + user: { + permissionGroup: 'Public', + }, + }, + controllerProps: { + value: null, + onChange, + ref: { + current: {}, + } as Ref, + }, + }; + + it('renders the user question component without crashing', () => { + renderComponent(); + }); + + it('renders all the options', async () => { + renderComponent(); + + const openButton = screen.getByTitle('Open'); + openButton.click(); + + const displayOptions = await screen.findAllByRole('option'); + expect(displayOptions.length).toBe(users.length); + displayOptions.forEach((option, index) => { + const text = users[index].name; + expect(option).toHaveTextContent(text); + }); + }); + + it('Calls the onChange method with the option value', async () => { + renderComponent(); + + const openButton = screen.getByTitle('Open'); + openButton.click(); + + const displayOption = await screen.findByRole('option', { name: users[0].name }); + userEvent.click(displayOption); + + expect(onChange).toHaveBeenCalledWith(users[0].id); + }); +}); diff --git a/packages/datatrak-web/src/api/queries/index.ts b/packages/datatrak-web/src/api/queries/index.ts index 4d8e231997..ef2d3db7ae 100644 --- a/packages/datatrak-web/src/api/queries/index.ts +++ b/packages/datatrak-web/src/api/queries/index.ts @@ -23,3 +23,4 @@ export { useEntities } from './useEntities'; export { useTasks } from './useTasks'; export { useTask } from './useTask'; export { useSurveyUsers } from './useSurveyUsers'; +export { usePermissionGroupUsers } from './usePermissionGroupUsers'; diff --git a/packages/datatrak-web/src/api/queries/usePermissionGroupUsers.ts b/packages/datatrak-web/src/api/queries/usePermissionGroupUsers.ts new file mode 100644 index 0000000000..a6fad146d0 --- /dev/null +++ b/packages/datatrak-web/src/api/queries/usePermissionGroupUsers.ts @@ -0,0 +1,28 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ + +import { useQuery } from 'react-query'; +import { Country, DatatrakWebUsersRequest, PermissionGroup } from '@tupaia/types'; +import { get } from '../api'; + +export const usePermissionGroupUsers = ( + countryCode?: Country['code'], + permissionGroupId?: PermissionGroup['id'], + searchTerm?: string, +) => { + return useQuery( + ['users', permissionGroupId, countryCode, searchTerm], + (): Promise => + get(`users/${countryCode}`, { + params: { + searchTerm, + permissionGroupId, + }, + }), + { + enabled: !!permissionGroupId && !!countryCode, + }, + ); +}; diff --git a/packages/datatrak-web/src/api/queries/useSurveyUsers.ts b/packages/datatrak-web/src/api/queries/useSurveyUsers.ts index d79a8819ea..7d7601ad79 100644 --- a/packages/datatrak-web/src/api/queries/useSurveyUsers.ts +++ b/packages/datatrak-web/src/api/queries/useSurveyUsers.ts @@ -4,7 +4,7 @@ */ import { useQuery } from 'react-query'; -import { Country, DatatrakWebSurveyUsersRequest } from '@tupaia/types'; +import { Country, DatatrakWebUsersRequest } from '@tupaia/types'; import { get } from '../api'; import { Survey } from '../../types'; @@ -15,7 +15,7 @@ export const useSurveyUsers = ( ) => { return useQuery( ['surveyUsers', surveyCode, countryCode, searchTerm], - (): Promise => + (): Promise => get(`users/${surveyCode}/${countryCode}`, { params: { searchTerm, diff --git a/packages/datatrak-web/src/components/Autocomplete.tsx b/packages/datatrak-web/src/components/Autocomplete.tsx index db45ece660..4c28deee25 100644 --- a/packages/datatrak-web/src/components/Autocomplete.tsx +++ b/packages/datatrak-web/src/components/Autocomplete.tsx @@ -8,6 +8,8 @@ import styled from 'styled-components'; import { Check } from '@material-ui/icons'; import { Autocomplete as BaseAutocomplete } from '@tupaia/ui-components'; import { Paper } from '@material-ui/core'; +import { MOBILE_BREAKPOINT } from '../constants'; +import { InputHelperText } from './InputHelperText'; const OptionWrapper = styled.div` width: 100%; @@ -103,3 +105,52 @@ export const Autocomplete = styled(BaseAutocomplete).attrs(props => ({ box-shadow: none; } `; + +export const QuestionAutocomplete = styled(Autocomplete).attrs({ + textFieldProps: { + FormHelperTextProps: { + component: InputHelperText, + }, + }, + placeholder: 'Search...', +})` + .MuiFormControl-root { + margin-bottom: 0; + } + + .MuiFormLabel-root { + font-size: 0.875rem; + line-height: 1.2; + @media (min-width: ${MOBILE_BREAKPOINT}) { + font-size: 1rem; + } + } + .MuiOutlinedInput-notchedOutline { + border: none; + } + + .MuiInputBase-root { + max-width: 25rem; + border-bottom: 1px solid ${({ theme }) => theme.palette.text.secondary}; + border-radius: 0; + order: 2; // make the helper text appear above the input + &.Mui-focused { + border-bottom-color: ${({ theme }) => theme.palette.primary.main}; + } + } + + .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline { + border: none; + } + .MuiInputBase-input.MuiAutocomplete-input.MuiInputBase-inputAdornedEnd { + padding: 0.6rem 0; + font-size: 0.875rem; + } + + .MuiAutocomplete-inputRoot .MuiAutocomplete-endAdornment { + right: 0; + } + .MuiIconButton-root { + color: ${({ theme }) => theme.palette.text.secondary}; + } +`; diff --git a/packages/datatrak-web/src/components/index.ts b/packages/datatrak-web/src/components/index.ts index fe7d66d3f4..4731951a85 100644 --- a/packages/datatrak-web/src/components/index.ts +++ b/packages/datatrak-web/src/components/index.ts @@ -6,7 +6,7 @@ export { PageContainer } from './PageContainer'; export * from './Icons'; export * from './SelectList'; -export { Autocomplete } from './Autocomplete'; +export { Autocomplete, QuestionAutocomplete } from './Autocomplete'; export { Button } from './Button'; export { ButtonLink } from './ButtonLink'; export { CancelConfirmModal } from './CancelConfirmModal'; diff --git a/packages/datatrak-web/src/features/Questions/AutocompleteQuestion.tsx b/packages/datatrak-web/src/features/Questions/AutocompleteQuestion.tsx index a66abdb5e6..1765cfc511 100644 --- a/packages/datatrak-web/src/features/Questions/AutocompleteQuestion.tsx +++ b/packages/datatrak-web/src/features/Questions/AutocompleteQuestion.tsx @@ -4,58 +4,12 @@ */ import React, { useState } from 'react'; -import styled from 'styled-components'; import throttle from 'lodash.throttle'; import { createFilterOptions } from '@material-ui/lab'; import { Option } from '@tupaia/types'; import { SurveyQuestionInputProps } from '../../types'; import { useAutocompleteOptions } from '../../api'; -import { MOBILE_BREAKPOINT } from '../../constants'; -import { Autocomplete as BaseAutocomplete, InputHelperText } from '../../components'; - -const Autocomplete = styled(BaseAutocomplete)` - width: calc(100% - 3.5rem); - max-width: 25rem; - - .MuiFormControl-root { - margin-bottom: 0; - } - - .MuiFormLabel-root { - font-size: 0.875rem; - line-height: 1.2; - @media (min-width: ${MOBILE_BREAKPOINT}) { - font-size: 1rem; - } - } - .MuiOutlinedInput-notchedOutline { - border: none; - } - - .MuiInputBase-root { - border-bottom: 1px solid ${({ theme }) => theme.palette.text.primary}; - border-radius: 0; - order: 2; // make the helper text appear above the input - &.Mui-focused { - border-bottom-color: ${({ theme }) => theme.palette.primary.main}; - } - } - - .MuiOutlinedInput-root.Mui-focused .MuiOutlinedInput-notchedOutline { - border: none; - } - .MuiInputBase-input.MuiAutocomplete-input.MuiInputBase-inputAdornedEnd { - padding: 0.6rem 0; - font-size: 0.875rem; - } - - .MuiAutocomplete-inputRoot .MuiAutocomplete-endAdornment { - right: 0; - } - .MuiIconButton-root { - color: ${({ theme }) => theme.palette.text.primary}; - } -`; +import { InputHelperText, QuestionAutocomplete } from '../../components'; export const AutocompleteQuestion = ({ id, @@ -121,7 +75,7 @@ export const AutocompleteQuestion = ({ return ( <> - { + const [searchValue, setSearchValue] = useState(''); + const { countryCode } = useSurveyForm(); + const { + data: users, + isLoading, + isFetched, + isError, + error, + } = usePermissionGroupUsers(countryCode, config?.user?.permissionGroup, searchValue); + + const options = + users?.map(user => ({ + label: user.name, + value: user.id, + })) ?? []; + + const selectedValue = options.find(option => option.value === value); + + return ( + <> + onChange(newSelectedOption?.value ?? null)} + onInputChange={throttle((_, newValue) => { + setSearchValue(newValue); + }, 200)} + inputValue={searchValue} + inputRef={ref} + error={isError || invalid} + getOptionLabel={option => option.label} + loading={isLoading || !isFetched} + getOptionSelected={option => option.value === value} + /> + {error && {(error as Error).message}} + + ); +}; diff --git a/packages/datatrak-web/src/features/Questions/index.ts b/packages/datatrak-web/src/features/Questions/index.ts index 77eff62dd5..245c8c2e87 100644 --- a/packages/datatrak-web/src/features/Questions/index.ts +++ b/packages/datatrak-web/src/features/Questions/index.ts @@ -15,3 +15,4 @@ export { AutocompleteQuestion } from './AutocompleteQuestion'; export { ReadOnlyQuestion } from './ReadOnlyQuestion'; export { PhotoQuestion } from './PhotoQuestion'; export { FileQuestion } from './FileQuestion'; +export { UserQuestion } from './UserQuestion'; diff --git a/packages/datatrak-web/src/features/Survey/Components/SurveyQuestion.tsx b/packages/datatrak-web/src/features/Survey/Components/SurveyQuestion.tsx index 6b239884c8..ecb0a86af6 100644 --- a/packages/datatrak-web/src/features/Survey/Components/SurveyQuestion.tsx +++ b/packages/datatrak-web/src/features/Survey/Components/SurveyQuestion.tsx @@ -21,6 +21,7 @@ import { ReadOnlyQuestion, PhotoQuestion, FileQuestion, + UserQuestion, } from '../../Questions'; import { SurveyQuestionFieldProps } from '../../../types'; import { useSurveyForm } from '..'; @@ -60,6 +61,7 @@ export enum QUESTION_TYPES { Arithmetic = ReadOnlyQuestion, Condition = ReadOnlyQuestion, File = FileQuestion, + User = UserQuestion, } /** diff --git a/packages/datatrak-web/src/features/Tasks/AssigneeInput.tsx b/packages/datatrak-web/src/features/Tasks/AssigneeInput.tsx index 529ff9bf0f..9807fa3e55 100644 --- a/packages/datatrak-web/src/features/Tasks/AssigneeInput.tsx +++ b/packages/datatrak-web/src/features/Tasks/AssigneeInput.tsx @@ -4,12 +4,12 @@ */ import React, { useEffect, useState } from 'react'; import throttle from 'lodash.throttle'; -import { Country, DatatrakWebSurveyUsersRequest } from '@tupaia/types'; +import { Country, DatatrakWebUsersRequest } from '@tupaia/types'; import { Autocomplete } from '../../components'; import { useSurveyUsers } from '../../api'; import { Survey } from '../../types'; -type User = DatatrakWebSurveyUsersRequest.ResBody[0]; +type User = DatatrakWebUsersRequest.ResBody[0]; interface AssigneeInputProps { value: string | null; diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts index 7ea631b62e..6691944e7d 100644 --- a/packages/types/src/schemas/schemas.ts +++ b/packages/types/src/schemas/schemas.ts @@ -40611,24 +40611,8 @@ export const UserQuestionConfigSchema = { "type": "object", "properties": { "permissionGroup": { - "description": "If this is a question value, the user list will be filtered by the value of the question. If this is a permission group name, the user list will be filtered by the permission group.", - "anyOf": [ - { - "type": "object", - "properties": { - "questionId": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "questionId" - ] - }, - { - "type": "string" - } - ] + "description": "Filters the users by permission group.", + "type": "string" } }, "additionalProperties": false, @@ -41269,24 +41253,8 @@ export const SurveyScreenComponentConfigSchema = { "type": "object", "properties": { "permissionGroup": { - "description": "If this is a question value, the user list will be filtered by the value of the question. If this is a permission group name, the user list will be filtered by the permission group.", - "anyOf": [ - { - "type": "object", - "properties": { - "questionId": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "questionId" - ] - }, - { - "type": "string" - } - ] + "description": "Filters the users by permission group.", + "type": "string" } }, "additionalProperties": false, diff --git a/packages/types/src/types/models-extra/survey/surveyScreenComponent.ts b/packages/types/src/types/models-extra/survey/surveyScreenComponent.ts index ca088ae0ea..6a9938092e 100644 --- a/packages/types/src/types/models-extra/survey/surveyScreenComponent.ts +++ b/packages/types/src/types/models-extra/survey/surveyScreenComponent.ts @@ -71,9 +71,9 @@ export type ArithmeticQuestionConfig = { export type UserQuestionConfig = { /** - * @description If this is a question value, the user list will be filtered by the value of the question. If this is a permission group name, the user list will be filtered by the permission group. + * @description Filters the users by permission group. */ - permissionGroup: QuestionValue | PermissionGroup['name']; + permissionGroup: PermissionGroup['id']; }; export type TaskQuestionConfig = { diff --git a/packages/types/src/types/requests/datatrak-web-server/SurveyUsersRequest.ts b/packages/types/src/types/requests/datatrak-web-server/UsersRequest.ts similarity index 77% rename from packages/types/src/types/requests/datatrak-web-server/SurveyUsersRequest.ts rename to packages/types/src/types/requests/datatrak-web-server/UsersRequest.ts index f459e06fc1..e2770b648a 100644 --- a/packages/types/src/types/requests/datatrak-web-server/SurveyUsersRequest.ts +++ b/packages/types/src/types/requests/datatrak-web-server/UsersRequest.ts @@ -3,6 +3,8 @@ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd */ +import { PermissionGroup } from '../../models'; + export type Params = Record; type UserResponse = { @@ -14,4 +16,5 @@ export type ResBody = UserResponse[]; export type ReqBody = Record; export interface ReqQuery { searchTerm?: string; + permissionGroupId?: PermissionGroup['id']; } diff --git a/packages/types/src/types/requests/datatrak-web-server/index.ts b/packages/types/src/types/requests/datatrak-web-server/index.ts index f1a7b96b4c..08fee7d243 100644 --- a/packages/types/src/types/requests/datatrak-web-server/index.ts +++ b/packages/types/src/types/requests/datatrak-web-server/index.ts @@ -18,5 +18,5 @@ export * as DatatrakWebGenerateLoginTokenRequest from './GenerateLoginTokenReque export * as DatatrakWebEntityDescendantsRequest from './EntityDescendantsRequest'; export * as DatatrakWebTasksRequest from './TasksRequest'; export * as DatatrakWebTaskRequest from './TaskRequest'; -export * as DatatrakWebSurveyUsersRequest from './SurveyUsersRequest'; +export * as DatatrakWebUsersRequest from './UsersRequest'; export * as DatatrakWebTaskChangeRequest from './TaskChangeRequest'; diff --git a/packages/types/src/types/requests/index.ts b/packages/types/src/types/requests/index.ts index e784e4eee4..019bbe1a77 100644 --- a/packages/types/src/types/requests/index.ts +++ b/packages/types/src/types/requests/index.ts @@ -21,7 +21,7 @@ export { DatatrakWebEntityDescendantsRequest, DatatrakWebTasksRequest, DatatrakWebTaskRequest, - DatatrakWebSurveyUsersRequest, + DatatrakWebUsersRequest, DatatrakWebTaskChangeRequest, } from './datatrak-web-server'; export {