diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature index bcdfb2ff8a..24017fb326 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature @@ -99,3 +99,8 @@ Feature: The user interacts with the widgets on the enrollment add event page When you click switch tab to Schedule Then you should see Schedule tab And you should see suggested date: 08-01 + + Scenario: You can assign a user when scheduling an event + Given you land on the enrollment edit event page by having typed /#/enrollmentEventNew?enrollmentId=zRfAPUpjoG3&orgUnitId=DiszpKrYNg8&programId=M3xtLkYBlKI&stageId=uvMKOn1oWvd&teiId=S3JjTA4QMNe + When you click switch tab to Schedule + Then you can assign a user when scheduling the event diff --git a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js index 1640f4243c..547b242c5a 100644 --- a/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js +++ b/cypress/e2e/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js @@ -1,4 +1,16 @@ +import { Then } from '@badeball/cypress-cucumber-preprocessor'; import '../sharedSteps'; import '../WidgetEnrollment'; import '../WidgetProfile'; import '../WidgetTab'; + +Then('you can assign a user when scheduling the event', () => { + cy.get('[data-test="assignee-section"]').within(() => { + cy.get('[data-test="capture-ui-input"]').click(); + cy.get('[data-test="capture-ui-input"]').type('Tracker demo'); + cy.contains('Tracker demo User').click(); + }); + cy.get('[data-test="assignee-section"]').within(() => { + cy.get('[data-test="dhis2-uicore-chip"]').contains('Tracker demo User').should('exist'); + }); +}); diff --git a/i18n/en.pot b/i18n/en.pot index 3597cba560..5078cc26d8 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -530,6 +530,9 @@ msgstr "start typing to search" msgid "suggestions could not be retrieved" msgstr "suggestions could not be retrieved" +msgid "No results found" +msgstr "No results found" + msgid "No items to display" msgstr "No items to display" @@ -962,9 +965,6 @@ msgstr "Could not retrieve metadata. Please try again later." msgid "Possible duplicates found" msgstr "Possible duplicates found" -msgid "No results found" -msgstr "No results found" - msgid "An error occurred loading possible duplicates" msgstr "An error occurred loading possible duplicates" diff --git a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/Assignee/Assignee.component.js b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/Assignee/Assignee.component.js index 1a81f5f219..44910d1616 100644 --- a/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/Assignee/Assignee.component.js +++ b/src/core_modules/capture-core/components/DataEntries/SingleEventRegistrationEntry/DataEntryWrapper/DataEntry/Assignee/Assignee.component.js @@ -7,7 +7,7 @@ import { UserField } from '../../../../../FormFields/UserField/UserField.compone const getStyles = () => ({ container: { display: 'flex', - alignItems: 'center', + alignItems: 'baseline', padding: '8px 8px 8px 12px', }, containerVertical: { diff --git a/src/core_modules/capture-core/components/FormFields/UserField/UserSearch.component.js b/src/core_modules/capture-core/components/FormFields/UserField/UserSearch.component.js index ed3bf21c9e..61c017d761 100644 --- a/src/core_modules/capture-core/components/FormFields/UserField/UserSearch.component.js +++ b/src/core_modules/capture-core/components/FormFields/UserField/UserSearch.component.js @@ -1,7 +1,9 @@ // @flow import * as React from 'react'; import { v4 as uuid } from 'uuid'; +import { withStyles } from '@material-ui/core/styles'; import i18n from '@dhis2/d2-i18n'; +import { colors } from '@dhis2/ui'; import { makeCancelablePromise } from 'capture-core-utils'; import { Input } from './Input.component'; import { SearchSuggestions } from './SearchSuggestions.component'; @@ -10,6 +12,13 @@ import type { User } from './types'; import { withApiUtils } from '../../../HOC'; import type { QuerySingleResource } from '../../../utils/api/api.types'; +const getStyles = (theme: Theme) => ({ + noMatchFound: { + color: colors.red600, + fontSize: theme.typography.pxToRem(14), + }, +}); + type Props = { onSet: (user: User) => void, inputWrapperClasses: Object, @@ -18,6 +27,7 @@ type Props = { inputPlaceholderText?: ?string, useUpwardList?: ?boolean, querySingleResource: QuerySingleResource, + ...CssClasses, }; type State = { @@ -26,6 +36,7 @@ type State = { suggestionsError?: ?string, highlightedSuggestion?: ?User, inputKey: number, + noMatch: boolean, }; const exitBehaviours = { @@ -46,6 +57,7 @@ class UserSearchPlain extends React.Component { suggestions: [], searchValue: '', inputKey: 0, + noMatch: false, }; this.domNames = { @@ -82,6 +94,7 @@ class UserSearchPlain extends React.Component { suggestions, highlightedSuggestion: undefined, searchValue, + noMatch: suggestions.length === 0, }); } @@ -90,6 +103,7 @@ class UserSearchPlain extends React.Component { suggestions: [], highlightedSuggestion: undefined, searchValue: '', + noMatch: false, }); } @@ -133,11 +147,14 @@ class UserSearchPlain extends React.Component { id: au.id, name: au.displayName, username: au.username, + firstName: au.firstName, + surname: au.surname, })); }); handleInputChange = (value: string) => { this.cancelablePromise && this.cancelablePromise.cancel(); + this.setState({ noMatch: false }); if (value.length > 1) { const searchPromise = this.search(value); @@ -294,6 +311,15 @@ class UserSearchPlain extends React.Component { ); } + renderNoMatchFound() { + const { noMatch } = this.state; + const { classes } = this.props; + + return noMatch ? ( + {i18n.t('No results found')} + ) : null; + } + render() { return (
@@ -301,6 +327,7 @@ class UserSearchPlain extends React.Component { value={this.domNames} > {this.renderInput()} + {this.renderNoMatchFound()} {this.renderSuggestions()}
@@ -308,4 +335,4 @@ class UserSearchPlain extends React.Component { } } -export const UserSearch = withApiUtils(UserSearchPlain); +export const UserSearch = withStyles(getStyles)(withApiUtils(UserSearchPlain)); diff --git a/src/core_modules/capture-core/components/FormFields/UserField/index.js b/src/core_modules/capture-core/components/FormFields/UserField/index.js index 3e88ffe311..e534c4387a 100644 --- a/src/core_modules/capture-core/components/FormFields/UserField/index.js +++ b/src/core_modules/capture-core/components/FormFields/UserField/index.js @@ -1,2 +1,3 @@ // @flow export { UserField } from './UserField.component'; +export type { User as UserFormField } from './types'; diff --git a/src/core_modules/capture-core/components/FormFields/UserField/types.js b/src/core_modules/capture-core/components/FormFields/UserField/types.js index 0695b283f1..c8d34e711a 100644 --- a/src/core_modules/capture-core/components/FormFields/UserField/types.js +++ b/src/core_modules/capture-core/components/FormFields/UserField/types.js @@ -3,4 +3,6 @@ export type User = { id: string, username: string, name: string, + firstName: string, + surname: string, }; diff --git a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/Assignee/Assignee.component.js b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/Assignee/Assignee.component.js index 165082a0c0..cae71ab09f 100644 --- a/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/Assignee/Assignee.component.js +++ b/src/core_modules/capture-core/components/WidgetEnrollmentEventNew/DataEntry/Assignee/Assignee.component.js @@ -7,7 +7,7 @@ import { UserField } from '../../../FormFields/UserField/UserField.component'; const getStyles = () => ({ container: { display: 'flex', - alignItems: 'center', + alignItems: 'baseline', padding: '8px 8px 8px 12px', }, containerVertical: { diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.component.js new file mode 100644 index 0000000000..012cd0f72a --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.component.js @@ -0,0 +1,38 @@ +// @flow +import React from 'react'; +import i18n from '@dhis2/d2-i18n'; +import { withStyles } from '@material-ui/core/styles'; +import { UserField } from '../../FormFields/UserField/UserField.component'; +import type { Props } from './Assignee.types'; + +const getStyles = () => ({ + container: { + display: 'flex', + alignItems: 'baseline', + padding: '8px 8px 8px 12px', + }, + label: { + fontSize: 14, + flexBasis: 200, + color: '#212934', + }, + field: { + flexBasis: 150, + flexGrow: 1, + }, +}); + +const AssigneePlain = (props: Props) => { + const { classes, assignee, ...passOnProps } = props; + return ( +
+
{i18n.t('Assignee')}
+
+ {/* $FlowFixMe[cannot-spread-inexact] automated comment */} + +
+
+ ); +}; + +export const Assignee = withStyles(getStyles)(AssigneePlain); diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js new file mode 100644 index 0000000000..fdbbccd9f2 --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js @@ -0,0 +1,7 @@ +// @flow +import type { UserFormField } from '../../FormFields/UserField'; + +export type Props = { + ...CssClasses, + assignee?: UserFormField, +}; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/index.js b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/index.js new file mode 100644 index 0000000000..0af6b7501f --- /dev/null +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/index.js @@ -0,0 +1,2 @@ +// @flow +export { Assignee } from './Assignee.component'; diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js index b1e092d37f..6dc9c0a5bc 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.actions.js @@ -21,6 +21,7 @@ export const requestScheduleEvent = ({ onSaveExternal, onSaveSuccessActionType, onSaveErrorActionType, + assignedUser, }: { scheduleDate: string, comments: Array<{value: string}>, @@ -34,6 +35,7 @@ export const requestScheduleEvent = ({ onSaveExternal: (eventServerValues: Object, uid: string) => void, onSaveSuccessActionType?: string, onSaveErrorActionType?: string, + assignedUser?: {uid: string}, }) => actionCreator(scheduleEventWidgetActionTypes.EVENT_SCHEDULE_REQUEST)({ scheduleDate, @@ -48,6 +50,7 @@ export const requestScheduleEvent = ({ onSaveExternal, onSaveSuccessActionType, onSaveErrorActionType, + assignedUser, }); export const scheduleEvent = ( diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js index e68f737994..ca2a1f7fbb 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.component.js @@ -10,7 +10,7 @@ import { ScheduleText } from './ScheduleText'; import { CommentSection } from '../WidgetComment'; import type { Props } from './widgetEventSchedule.types'; import { CategoryOptions } from './CategoryOptions/CategoryOptions.component'; - +import { Assignee } from './Assignee'; const styles = () => ({ wrapper: { @@ -66,9 +66,12 @@ const WidgetEventSchedulePlain = ({ suggestedScheduleDate, comments, programCategory, + enableUserAssignment, selectedCategories, onClickCategoryOption, onResetCategoryOption, + onSetAssignee, + assignee, categoryOptionsError, ...passOnProps }: Props) => ( @@ -119,6 +122,11 @@ const WidgetEventSchedulePlain = ({ handleAddComment={onAddComment} /> + {enableUserAssignment && ( + + + + )} { @@ -119,6 +123,7 @@ export const WidgetEventSchedule = ({ setComments([...comments, newComment]); }; + const onSetAssignee = useCallback(user => setAssignee(user), []); const onClickCategoryOption = useCallback((optionId: string, categoryId: string) => { setSelectedCategories(prevCategoryOptions => ({ ...prevCategoryOptions, @@ -159,11 +164,13 @@ export const WidgetEventSchedule = ({ return ( diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.epics.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.epics.js index 62d9ffccf5..4dad5a415f 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.epics.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.epics.js @@ -28,6 +28,7 @@ export const scheduleEnrollmentEventEpic = (action$: InputObservable, store: Red onSaveExternal, onSaveSuccessActionType, onSaveErrorActionType, + assignedUser, } = action.payload; const { events } = store.value; @@ -46,6 +47,7 @@ export const scheduleEnrollmentEventEpic = (action$: InputObservable, store: Red programStage: stageId, status: 'SCHEDULE', notes: comments ?? [], + assignedUser, }] }; if (attributeCategoryOptions) { diff --git a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js index 85ef3325bb..f42a7e8f87 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/widgetEventSchedule.types.js @@ -1,5 +1,6 @@ // @flow import type { ProgramCategory, CategoryOption } from './CategoryOptions/CategoryOptions.types'; +import type { UserFormField } from '../FormFields/UserField'; export type ContainerProps = {| programId: string, @@ -38,7 +39,10 @@ export type Props = {| selectedCategories?: ?{ [categoryId: string]: CategoryOption }, programCategory?: ProgramCategory, categoryOptionsError?: ?{[categoryId: string]: { touched: boolean, valid: boolean} }, + enableUserAssignment?: boolean, onSchedule: () => void, + onSetAssignee: (user: UserFormField) => void, + assignee?: UserFormField, onCancel: () => void, setScheduleDate: (date: string) => void, onAddComment: (comment: string) => void, diff --git a/src/core_modules/capture-core/converters/clientToServer.js b/src/core_modules/capture-core/converters/clientToServer.js index 7477352af6..49b4af1021 100644 --- a/src/core_modules/capture-core/converters/clientToServer.js +++ b/src/core_modules/capture-core/converters/clientToServer.js @@ -8,6 +8,13 @@ type RangeValue = { to: number, } +type Assignee = { + id: string, + username: string, + name: string, + firstName: string, + surname: string, +} function convertDate(rawValue: string): string { const editedDate = rawValue; @@ -66,3 +73,13 @@ export function convertCategoryOptionsToServer(value: {[categoryId: string]: str } return value; } + +export function convertAssigneeToServer(assignee: Assignee): ApiAssignedUser { + return { + uid: assignee.id, + displayName: assignee.name, + username: assignee.username, + firstName: assignee.firstName, + surname: assignee.surname, + }; +} diff --git a/src/core_modules/capture-core/converters/index.js b/src/core_modules/capture-core/converters/index.js index 74b4d0e019..82eb3936a6 100644 --- a/src/core_modules/capture-core/converters/index.js +++ b/src/core_modules/capture-core/converters/index.js @@ -2,7 +2,7 @@ export { convertValue as convertClientToForm } from './clientToForm'; export { convertValue as convertClientToList } from './clientToList'; export { convertValue as convertClientToView, convertDateWithTimeForView } from './clientToView'; -export { convertValue as convertClientToServer } from './clientToServer'; +export { convertValue as convertClientToServer, convertAssigneeToServer } from './clientToServer'; export { convertValue as convertFormToClient } from './formToClient'; export { convertValue as convertServerToClient, diff --git a/src/core_modules/capture-core/flow/apiTypes.js b/src/core_modules/capture-core/flow/apiTypes.js index b11decd46e..8d3589c5dd 100644 --- a/src/core_modules/capture-core/flow/apiTypes.js +++ b/src/core_modules/capture-core/flow/apiTypes.js @@ -1,5 +1,13 @@ // @flow +declare type ApiAssignedUser = {| + uid: string, + username: string, + displayName: string, + firstName: string, + surname: string, +|}; + declare type ApiDataValue = { dataElement: string, value: string,