@@ -6,14 +6,18 @@ import { useEffect, useRef, useState } from "react";
66import { FormattedMessage , useIntl } from "react-intl" ;
77import * as Yup from "yup" ;
88import { createTimeEntry , updateIssue } from "../../api/redmine" ;
9+ import useMyAccount from "../../hooks/useMyAccount" ;
10+ import useProjectUsers from "../../hooks/useProjectUsers" ;
911import useSettings from "../../hooks/useSettings" ;
1012import useTimeEntryActivities from "../../hooks/useTimeEntryActivities" ;
1113import { TCreateTimeEntry , TIssue , TRedmineError } from "../../types/redmine" ;
1214import { formatHours } from "../../utils/date" ;
1315import Button from "../general/Button" ;
1416import DateField from "../general/DateField" ;
1517import InputField from "../general/InputField" ;
18+ import LoadingSpinner from "../general/LoadingSpinner" ;
1619import Modal from "../general/Modal" ;
20+ import ReactSelectFormik from "../general/ReactSelectFormik" ;
1721import SelectField from "../general/SelectField" ;
1822import Toast from "../general/Toast" ;
1923import TimeEntryPreview from "../time/TimeEntryPreview" ;
@@ -33,21 +37,26 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
3337
3438 const formik = useRef < FormikProps < TCreateTimeEntry > > ( null ) ;
3539
40+ const myAccount = useMyAccount ( ) ;
3641 const timeEntryActivities = useTimeEntryActivities ( ) ;
42+ const users = useProjectUsers ( issue . project . id , { enabled : settings . options . addSpentTimeForOtherUsers } ) ;
3743
3844 useEffect ( ( ) => {
39- formik . current ?. setFieldValue ( "activity_id" , timeEntryActivities . find ( ( entry ) => entry . is_default ) ?. id ?? undefined ) ;
40- } , [ timeEntryActivities ] ) ;
45+ formik . current ?. setFieldValue ( "activity_id" , timeEntryActivities . data . find ( ( entry ) => entry . is_default ) ?. id ?? undefined ) ;
46+ } , [ timeEntryActivities . data ] ) ;
4147
4248 const createTimeEntryMutation = useMutation ( {
4349 mutationFn : ( entry : TCreateTimeEntry ) => createTimeEntry ( entry ) ,
44- onSuccess : ( ) => {
45- queryClient . invalidateQueries ( [ "timeEntries" ] ) ;
46- onSuccess ( ) ;
50+ onSuccess : ( _ , entry ) => {
51+ // if entry created for me => invalidate query
52+ if ( ! entry . user_id || entry . user_id === myAccount . data ?. id ) {
53+ queryClient . invalidateQueries ( [ "timeEntries" ] ) ;
54+ }
4755 } ,
4856 } ) ;
4957
5058 const [ doneRatio , setDoneRatio ] = useState ( issue . done_ratio ) ;
59+
5160 const updateIssueMutation = useMutation ( {
5261 mutationFn : ( data : { done_ratio : number } ) => updateIssue ( issue . id , data ) ,
5362 onSuccess : ( ) => {
@@ -63,12 +72,14 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
6372 innerRef = { formik }
6473 initialValues = { {
6574 issue_id : issue . id ,
75+ user_id : undefined ,
6676 spent_on : new Date ( ) ,
6777 activity_id : undefined ,
6878 hours : Number ( ( time / 1000 / 60 / 60 ) . toFixed ( 2 ) ) ,
6979 comments : "" ,
7080 } }
7181 validationSchema = { Yup . object ( {
82+ user_id : Yup . array ( Yup . number ( ) ) ,
7283 spent_on : Yup . date ( ) . max ( new Date ( ) , formatMessage ( { id : "issues.modal.add-spent-time.date.validation.in-future" } ) ) ,
7384 hours : Yup . number ( )
7485 . required ( formatMessage ( { id : "issues.modal.add-spent-time.hours.validation.required" } ) )
@@ -81,8 +92,17 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
8192 if ( issue . done_ratio !== doneRatio ) {
8293 await updateIssueMutation . mutateAsync ( { done_ratio : doneRatio } ) ;
8394 }
84- await createTimeEntryMutation . mutateAsync ( values ) ;
95+ if ( values . user_id && Array . isArray ( values . user_id ) && values . user_id . length > 0 ) {
96+ // create for multiple users
97+ for ( const userId of values . user_id as number [ ] ) {
98+ await createTimeEntryMutation . mutateAsync ( { ...values , user_id : userId } ) ;
99+ }
100+ } else {
101+ // create for me
102+ await createTimeEntryMutation . mutateAsync ( { ...values , user_id : undefined } ) ;
103+ }
85104 setSubmitting ( false ) ;
105+ if ( ! createTimeEntryMutation . isError ) onSuccess ( ) ;
86106 } }
87107 >
88108 { ( { isSubmitting, touched, errors, values } ) => (
@@ -99,32 +119,59 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
99119
100120 { values . spent_on && < TimeEntryPreview date = { startOfDay ( values . spent_on ) } previewHours = { values . hours } /> }
101121
102- < Field
103- type = "date"
104- name = "spent_on"
105- title = { formatMessage ( { id : "issues.modal.add-spent-time.date" } ) }
106- placeholder = { formatMessage ( { id : "issues.modal.add-spent-time.date" } ) }
107- required
108- as = { DateField }
109- size = "sm"
110- error = { touched . spent_on && errors . spent_on }
111- options = { { maxDate : new Date ( ) } }
112- />
113- < Field
114- type = "number"
115- name = "hours"
116- title = { formatMessage ( { id : "issues.modal.add-spent-time.hours" } ) }
117- placeholder = { formatMessage ( { id : "issues.modal.add-spent-time.hours" } ) }
118- min = "0"
119- step = "0.01"
120- max = "24"
121- required
122- as = { InputField }
123- size = "sm"
124- extraText = { values . hours >= 0 && values . hours <= 24 ? formatHours ( values . hours ) + " h" : undefined }
125- error = { touched . hours && errors . hours }
126- autoComplete = "off"
127- />
122+ < div className = "grid grid-cols-5 gap-x-2" >
123+ < div className = "col-span-3" >
124+ < Field
125+ type = "number"
126+ name = "hours"
127+ title = { formatMessage ( { id : "issues.modal.add-spent-time.hours" } ) }
128+ placeholder = { formatMessage ( { id : "issues.modal.add-spent-time.hours" } ) }
129+ min = "0"
130+ step = "0.01"
131+ max = "24"
132+ required
133+ as = { InputField }
134+ size = "sm"
135+ className = "appearance-none"
136+ extraText = { values . hours >= 0 && values . hours <= 24 ? formatHours ( values . hours ) + " h" : undefined }
137+ error = { touched . hours && errors . hours }
138+ autoComplete = "off"
139+ />
140+ </ div >
141+ < div className = "col-span-2" >
142+ < Field
143+ type = "date"
144+ name = "spent_on"
145+ title = { formatMessage ( { id : "issues.modal.add-spent-time.date" } ) }
146+ placeholder = { formatMessage ( { id : "issues.modal.add-spent-time.date" } ) }
147+ required
148+ as = { DateField }
149+ size = "sm"
150+ error = { touched . spent_on && errors . spent_on }
151+ options = { { maxDate : new Date ( ) } }
152+ />
153+ </ div >
154+ </ div >
155+
156+ { settings . options . addSpentTimeForOtherUsers && (
157+ < Field
158+ type = "select"
159+ name = "user_id"
160+ title = { formatMessage ( { id : "issues.modal.add-spent-time.user" } ) }
161+ placeholder = { formatMessage ( { id : "issues.modal.add-spent-time.user" } ) }
162+ as = { ReactSelectFormik }
163+ options = { users . data . map ( ( user ) => ( {
164+ value : user . id ,
165+ label : user . id === myAccount . data ?. id ? `${ user . name } (${ formatMessage ( { id : "issues.modal.add-spent-time.user.me" } ) } )` : user . name ,
166+ } ) ) }
167+ error = { touched . user_id && errors . user_id }
168+ isClearable
169+ isMulti
170+ closeMenuOnSelect = { false }
171+ isLoading = { users . isLoading }
172+ />
173+ ) }
174+
128175 < Field
129176 type = "text"
130177 name = "comments"
@@ -145,7 +192,7 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
145192 size = "sm"
146193 error = { touched . activity_id && errors . activity_id }
147194 >
148- { timeEntryActivities . map ( ( activity ) => (
195+ { timeEntryActivities . data . map ( ( activity ) => (
149196 < >
150197 < option key = { activity . id } value = { activity . id } >
151198 { activity . name }
@@ -154,8 +201,9 @@ const CreateTimeEntryModal = ({ issue, time, onClose, onSuccess }: PropTypes) =>
154201 ) ) }
155202 </ Field >
156203
157- < Button type = "submit" disabled = { isSubmitting } >
204+ < Button type = "submit" disabled = { isSubmitting } className = "flex items-center justify-center gap-x-2" >
158205 < FormattedMessage id = "issues.modal.add-spent-time.submit" />
206+ { isSubmitting && < LoadingSpinner /> }
159207 </ Button >
160208 </ div >
161209 </ Form >
0 commit comments