From a0ec795e85d9857c11e4e5762cb08616b92339cb Mon Sep 17 00:00:00 2001 From: Simona Domnisoru Date: Tue, 26 Sep 2023 10:53:49 +0200 Subject: [PATCH 1/3] feat: assign an user when scheduling an enrollment event --- .../WidgetsForEnrollmentAddEventPage.feature | 5 +++ .../WidgetsForEnrollmentAddEventPage/index.js | 11 ++++++ i18n/en.pot | 4 +- .../components/FormFields/UserField/index.js | 1 + .../Assignee/Assignee.component.js | 38 +++++++++++++++++++ .../Assignee/Assignee.types.js | 7 ++++ .../WidgetEventSchedule/Assignee/index.js | 2 + .../WidgetEventSchedule.actions.js | 3 ++ .../WidgetEventSchedule.component.js | 10 ++++- .../WidgetEventSchedule.container.js | 7 ++++ .../WidgetEventSchedule.epics.js | 2 + .../widgetEventSchedule.types.js | 4 ++ 12 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.component.js create mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.types.js create mode 100644 src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/index.js diff --git a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature b/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature index f5db1e30e2..faca6d2e4c 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature +++ b/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage.feature @@ -96,3 +96,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/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js b/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js index 1640f4243c..e70a5bbd68 100644 --- a/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js +++ b/cypress/integration/WidgetsForEnrollmentPages/WidgetsForEnrollmentAddEventPage/index.js @@ -2,3 +2,14 @@ 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 e179d736a4..bc3b72d48c 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-09-04T07:07:59.195Z\n" -"PO-Revision-Date: 2023-09-04T07:07:59.195Z\n" +"POT-Creation-Date: 2023-09-25T08:33:17.277Z\n" +"PO-Revision-Date: 2023-09-25T08:33:17.277Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." 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/WidgetEventSchedule/Assignee/Assignee.component.js b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.component.js new file mode 100644 index 0000000000..9c92ecf460 --- /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: 'center', + 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 +122,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 +163,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, From 74f12d820579290e01a89828c3e86ba168c83a2a Mon Sep 17 00:00:00 2001 From: Simona Domnisoru Date: Tue, 17 Oct 2023 11:30:35 +0200 Subject: [PATCH 2/3] feat: no match found message --- i18n/en.pot | 10 +++---- .../DataEntry/Assignee/Assignee.component.js | 2 +- .../UserField/UserSearch.component.js | 27 ++++++++++++++++++- .../DataEntry/Assignee/Assignee.component.js | 2 +- .../Assignee/Assignee.component.js | 2 +- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 06e8de8d33..3d00b54968 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2023-09-29T14:14:34.330Z\n" -"PO-Revision-Date: 2023-09-29T14:14:34.330Z\n" +"POT-Creation-Date: 2023-10-17T09:30:37.474Z\n" +"PO-Revision-Date: 2023-10-17T09:30:37.474Z\n" msgid "Choose one or more dates..." msgstr "Choose one or more dates..." @@ -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" @@ -950,9 +953,6 @@ msgstr "Organisation unit could not be loaded" 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..af14a8d494 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, }); } @@ -138,6 +152,7 @@ class UserSearchPlain extends React.Component { handleInputChange = (value: string) => { this.cancelablePromise && this.cancelablePromise.cancel(); + this.setState({ noMatch: false }); if (value.length > 1) { const searchPromise = this.search(value); @@ -294,6 +309,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 +325,7 @@ class UserSearchPlain extends React.Component { value={this.domNames} > {this.renderInput()} + {this.renderNoMatchFound()} {this.renderSuggestions()}
@@ -308,4 +333,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/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 index 9c92ecf460..012cd0f72a 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.component.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/Assignee/Assignee.component.js @@ -8,7 +8,7 @@ import type { Props } from './Assignee.types'; const getStyles = () => ({ container: { display: 'flex', - alignItems: 'center', + alignItems: 'baseline', padding: '8px 8px 8px 12px', }, label: { From 2be6deb779c55b3c1d5401775ea7a4568c30570d Mon Sep 17 00:00:00 2001 From: Simona Domnisoru Date: Wed, 18 Oct 2023 09:39:20 +0200 Subject: [PATCH 3/3] feat: save all assigned user properties into enrollmentDomain --- .../UserField/UserSearch.component.js | 2 ++ .../components/FormFields/UserField/types.js | 2 ++ .../WidgetEventSchedule.container.js | 3 ++- .../capture-core/converters/clientToServer.js | 17 +++++++++++++++++ .../capture-core/converters/index.js | 2 +- src/core_modules/capture-core/flow/apiTypes.js | 8 ++++++++ 6 files changed, 32 insertions(+), 2 deletions(-) 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 af14a8d494..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 @@ -147,6 +147,8 @@ class UserSearchPlain extends React.Component { id: au.id, name: au.displayName, username: au.username, + firstName: au.firstName, + surname: au.surname, })); }); 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/WidgetEventSchedule/WidgetEventSchedule.container.js b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js index a6813a6334..367ab395bb 100644 --- a/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js +++ b/src/core_modules/capture-core/components/WidgetEventSchedule/WidgetEventSchedule.container.js @@ -18,6 +18,7 @@ import { import { requestScheduleEvent } from './WidgetEventSchedule.actions'; import { NoAccess } from './AccessVerification'; import { useCategoryCombinations } from '../DataEntryDhis2Helpers/AOC/useCategoryCombinations'; +import { convertAssigneeToServer } from '../../converters'; export const WidgetEventSchedule = ({ enrollmentId, @@ -81,7 +82,7 @@ export const WidgetEventSchedule = ({ onSaveExternal: onSave, onSaveSuccessActionType, onSaveErrorActionType, - ...(assignee && { assignedUser: { uid: assignee.id } }), + ...(assignee && { assignedUser: convertAssigneeToServer(assignee) }), })); }, [ dispatch, 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,