1
1
import { Callout } from '@app-builder/components' ;
2
2
import { ReviewStatusTag } from '@app-builder/components/Decisions/ReviewStatusTag' ;
3
3
import { ExternalLink } from '@app-builder/components/ExternalLink' ;
4
- import { FormErrorOrDescription } from '@app-builder/components/Form/FormErrorOrDescription' ;
5
- import { FormField } from '@app-builder/components/Form/FormField' ;
6
- import { FormLabel } from '@app-builder/components/Form/FormLabel' ;
7
- import { FormSelect } from '@app-builder/components/Form/FormSelect' ;
8
- import { FormTextArea } from '@app-builder/components/Form/FormTextArea' ;
4
+ import { FormErrorOrDescription } from '@app-builder/components/Form/Tanstack/FormErrorOrDescription' ;
5
+ import { FormLabel } from '@app-builder/components/Form/Tanstack/FormLabel' ;
9
6
import { setToastMessage } from '@app-builder/components/MarbleToaster' ;
10
7
import { LoadingIcon } from '@app-builder/components/Spinner' ;
11
8
import { nonPendingReviewStatuses } from '@app-builder/models/decision' ;
@@ -14,13 +11,12 @@ import { blockingReviewDocHref } from '@app-builder/services/documentation-href'
14
11
import { serverServices } from '@app-builder/services/init.server' ;
15
12
import { getRoute } from '@app-builder/utils/routes' ;
16
13
import type * as Ariakit from '@ariakit/react' ;
17
- import { FormProvider , getFormProps , getInputProps , useForm } from '@conform-to/react' ;
18
- import { getZodConstraint , parseWithZod } from '@conform-to/zod' ;
19
14
import { type ActionFunctionArgs , json } from '@remix-run/node' ;
20
15
import { useFetcher } from '@remix-run/react' ;
21
- import * as React from 'react' ;
16
+ import { useForm } from '@tanstack/react-form' ;
17
+ import { useEffect } from 'react' ;
22
18
import { Trans , useTranslation } from 'react-i18next' ;
23
- import { Button , ModalV2 } from 'ui-design-system' ;
19
+ import { Button , ModalV2 , Select , TextArea } from 'ui-design-system' ;
24
20
import { z } from 'zod' ;
25
21
26
22
const reviewDecisionSchema = z . object ( {
@@ -35,37 +31,53 @@ export async function action({ request }: ActionFunctionArgs) {
35
31
i18nextService : { getFixedT } ,
36
32
toastSessionService : { getSession, commitSession } ,
37
33
} = serverServices ;
38
- const { cases } = await authService . isAuthenticated ( request , {
39
- failureRedirect : getRoute ( '/sign-in' ) ,
40
- } ) ;
41
-
42
- const formData = await request . formData ( ) ;
43
- const submission = parseWithZod ( formData , {
44
- schema : reviewDecisionSchema ,
45
- } ) ;
46
34
47
- if ( submission . status !== 'success' ) {
48
- return json ( submission . reply ( ) ) ;
35
+ const [ t , session , rawData , { cases } ] = await Promise . all ( [
36
+ getFixedT ( request , [ 'common' ] ) ,
37
+ getSession ( request ) ,
38
+ request . json ( ) ,
39
+ authService . isAuthenticated ( request , {
40
+ failureRedirect : getRoute ( '/sign-in' ) ,
41
+ } ) ,
42
+ ] ) ;
43
+
44
+ const { data, success, error } = reviewDecisionSchema . safeParse ( rawData ) ;
45
+
46
+ if ( ! success ) {
47
+ return json (
48
+ { status : 'error' , errors : error . flatten ( ) } ,
49
+ {
50
+ headers : { 'Set-Cookie' : await commitSession ( session ) } ,
51
+ } ,
52
+ ) ;
49
53
}
50
54
51
55
try {
52
- await cases . reviewDecision ( submission . value ) ;
53
-
54
- return json ( submission . reply ( ) ) ;
55
- } catch ( error ) {
56
- const session = await getSession ( request ) ;
57
- const t = await getFixedT ( request , [ 'common' , 'cases' ] ) ;
56
+ await cases . reviewDecision ( data ) ;
58
57
59
- const message = t ( 'common:errors.unknown' ) ;
58
+ setToastMessage ( session , {
59
+ type : 'success' ,
60
+ message : t ( 'common:success.save' ) ,
61
+ } ) ;
60
62
63
+ return json (
64
+ { status : 'success' , errors : [ ] } ,
65
+ {
66
+ headers : { 'Set-Cookie' : await commitSession ( session ) } ,
67
+ } ,
68
+ ) ;
69
+ } catch ( error ) {
61
70
setToastMessage ( session , {
62
71
type : 'error' ,
63
- message,
72
+ message : t ( 'common:errors.unknown' ) ,
64
73
} ) ;
65
74
66
- return json ( submission . reply ( { formErrors : [ message ] } ) , {
67
- headers : { 'Set-Cookie' : await commitSession ( session ) } ,
68
- } ) ;
75
+ return json (
76
+ { status : 'error' , errors : [ ] } ,
77
+ {
78
+ headers : { 'Set-Cookie' : await commitSession ( session ) } ,
79
+ } ,
80
+ ) ;
69
81
}
70
82
}
71
83
@@ -99,113 +111,133 @@ function ReviewDecisionContent({
99
111
setOpen : ( open : boolean ) => void ;
100
112
} ) {
101
113
const { t } = useTranslation ( [ 'common' , 'cases' ] ) ;
102
-
103
114
const fetcher = useFetcher < typeof action > ( ) ;
104
- React . useEffect ( ( ) => {
115
+
116
+ useEffect ( ( ) => {
105
117
if ( fetcher ?. data ?. status === 'success' ) {
106
118
setOpen ( false ) ;
107
119
}
108
120
} , [ setOpen , fetcher ?. data ?. status ] ) ;
109
121
110
- const [ form , fields ] = useForm ( {
111
- shouldRevalidate : 'onInput' ,
112
- defaultValue : {
122
+ const form = useForm ( {
123
+ defaultValues : {
113
124
decisionId,
125
+ reviewComment : '' ,
126
+ reviewStatus : 'decline' ,
114
127
} ,
115
- lastResult : fetcher . data ,
116
- constraint : getZodConstraint ( reviewDecisionSchema ) ,
117
- onValidate ( { formData } ) {
118
- return parseWithZod ( formData , {
119
- schema : reviewDecisionSchema ,
120
- } ) ;
128
+ onSubmit : ( { value, formApi } ) => {
129
+ if ( formApi . state . isValid ) {
130
+ fetcher . submit ( value , {
131
+ method : 'POST' ,
132
+ action : getRoute ( '/ressources/cases/review-decision' ) ,
133
+ encType : 'application/json' ,
134
+ } ) ;
135
+ }
136
+ } ,
137
+ validators : {
138
+ onChangeAsync : reviewDecisionSchema ,
139
+ onBlurAsync : reviewDecisionSchema ,
140
+ onSubmitAsync : reviewDecisionSchema ,
121
141
} ,
122
142
} ) ;
123
143
124
144
return (
125
- < FormProvider context = { form . context } >
126
- < fetcher . Form
127
- method = "post"
128
- action = { getRoute ( '/ressources/cases/review-decision' ) }
129
- { ...getFormProps ( form ) }
130
- >
131
- < ModalV2 . Title > { t ( 'cases:case_detail.review_decision.title' ) } </ ModalV2 . Title >
132
- < div className = "flex flex-col gap-6 p-6" >
133
- < ModalV2 . Description render = { < Callout variant = "outlined" /> } >
134
- < p className = "whitespace-pre text-wrap" >
135
- < Trans
136
- t = { t }
137
- i18nKey = "cases:case_detail.review_decision.callout"
138
- components = { {
139
- DocLink : < ExternalLink href = { blockingReviewDocHref } /> ,
140
- } }
141
- />
142
- </ p >
143
- </ ModalV2 . Description >
144
-
145
- < input
146
- { ...getInputProps ( fields . decisionId , {
147
- type : 'hidden' ,
148
- } ) }
149
- />
150
-
151
- < FormField name = { fields . reviewStatus . name } className = "flex flex-col gap-2" >
152
- < FormLabel > { t ( 'cases:case_detail.review_decision.review_status.label' ) } </ FormLabel >
153
- < FormSelect . Default
154
- className = "h-10 w-full"
155
- options = { nonPendingReviewStatuses }
156
- placeholder = { t ( 'cases:case_detail.review_decision.review_status.placeholder' ) }
157
- contentClassName = "max-w-[var(--radix-select-trigger-width)]"
158
- >
159
- { nonPendingReviewStatuses . map ( ( reviewStatus ) => {
160
- const disabled = sanctionCheck && sanctionCheck . status !== 'no_hit' ;
161
-
162
- return disabled && reviewStatus === 'approve' ? (
163
- < div className = "flex flex-col items-start gap-2 p-1" >
164
- < ReviewStatusTag
165
- key = { reviewStatus }
166
- disabled
167
- border = "square"
168
- size = "big"
169
- reviewStatus = { reviewStatus }
170
- />
171
- < span className = "text-grey-50 text-xs" >
172
- { t ( 'cases:case_detail.review_decision.disabled_approve' ) }
173
- </ span >
174
- </ div >
175
- ) : (
176
- < FormSelect . DefaultItem key = { reviewStatus } value = { reviewStatus } >
177
- < ReviewStatusTag border = "square" size = "big" reviewStatus = { reviewStatus } />
178
- </ FormSelect . DefaultItem >
179
- ) ;
180
- } ) }
181
- </ FormSelect . Default >
182
- < FormErrorOrDescription />
183
- </ FormField >
184
-
185
- < FormField name = { fields . reviewComment . name } className = "flex flex-col gap-2" >
186
- < FormLabel > { t ( 'cases:case_detail.review_decision.comment.label' ) } </ FormLabel >
187
- < FormTextArea
188
- className = "w-full"
189
- placeholder = { t ( 'cases:case_detail.review_decision.comment.placeholder' ) }
145
+ < form
146
+ onSubmit = { ( e ) => {
147
+ e . preventDefault ( ) ;
148
+ e . stopPropagation ( ) ;
149
+ form . handleSubmit ( ) ;
150
+ } }
151
+ >
152
+ < ModalV2 . Title > { t ( 'cases:case_detail.review_decision.title' ) } </ ModalV2 . Title >
153
+ < div className = "flex flex-col gap-6 p-6" >
154
+ < ModalV2 . Description render = { < Callout variant = "outlined" /> } >
155
+ < p className = "whitespace-pre text-wrap" >
156
+ < Trans
157
+ t = { t }
158
+ i18nKey = "cases:case_detail.review_decision.callout"
159
+ components = { {
160
+ DocLink : < ExternalLink href = { blockingReviewDocHref } /> ,
161
+ } }
190
162
/>
191
- < FormErrorOrDescription />
192
- </ FormField >
193
-
194
- < div className = "flex flex-1 flex-row gap-2" >
195
- < ModalV2 . Close render = { < Button className = "flex-1" variant = "secondary" /> } >
196
- { t ( 'common:cancel' ) }
197
- </ ModalV2 . Close >
198
- < Button className = "flex-1" variant = "primary" type = "submit" >
199
- < LoadingIcon
200
- icon = "case-manager"
201
- className = "size-5"
202
- loading = { fetcher . state === 'submitting' }
163
+ </ p >
164
+ </ ModalV2 . Description >
165
+
166
+ < form . Field name = "reviewStatus" >
167
+ { ( field ) => (
168
+ < div className = "flex flex-col gap-2" >
169
+ < FormLabel name = { field . name } >
170
+ { t ( 'cases:case_detail.review_decision.review_status.label' ) }
171
+ </ FormLabel >
172
+ < Select . Default
173
+ className = "h-10 w-full"
174
+ defaultValue = { field . state . value }
175
+ onValueChange = { field . handleChange }
176
+ placeholder = { t ( 'cases:case_detail.review_decision.review_status.placeholder' ) }
177
+ //contentClassName="max-w-[var(--radix-select-trigger-width)]"
178
+ >
179
+ { nonPendingReviewStatuses . map ( ( reviewStatus ) => {
180
+ const disabled = sanctionCheck && sanctionCheck . status !== 'no_hit' ;
181
+
182
+ return disabled && reviewStatus === 'approve' ? (
183
+ < div className = "flex flex-col items-start gap-2 p-1" >
184
+ < ReviewStatusTag
185
+ key = { reviewStatus }
186
+ disabled
187
+ border = "square"
188
+ size = "big"
189
+ reviewStatus = { reviewStatus }
190
+ />
191
+ < span className = "text-grey-50 text-xs" >
192
+ { t ( 'cases:case_detail.review_decision.disabled_approve' ) }
193
+ </ span >
194
+ </ div >
195
+ ) : (
196
+ < Select . DefaultItem key = { reviewStatus } value = { reviewStatus } >
197
+ < ReviewStatusTag border = "square" size = "big" reviewStatus = { reviewStatus } />
198
+ </ Select . DefaultItem >
199
+ ) ;
200
+ } ) }
201
+ </ Select . Default >
202
+ < FormErrorOrDescription errors = { field . state . meta . errors } />
203
+ </ div >
204
+ ) }
205
+ </ form . Field >
206
+
207
+ < form . Field name = "reviewComment" >
208
+ { ( field ) => (
209
+ < div className = "flex flex-col gap-2" >
210
+ < FormLabel name = { field . name } >
211
+ { t ( 'cases:case_detail.review_decision.comment.label' ) }
212
+ </ FormLabel >
213
+ < TextArea
214
+ className = "w-full"
215
+ name = { field . name }
216
+ defaultValue = { field . state . value }
217
+ onChange = { ( e ) => field . handleChange ( e . currentTarget . value ) }
218
+ onBlur = { field . handleBlur }
219
+ borderColor = { field . state . meta . errors . length === 0 ? 'greyfigma-90' : 'redfigma-47' }
220
+ placeholder = { t ( 'cases:case_detail.review_decision.comment.placeholder' ) }
203
221
/>
204
- { t ( 'cases:case_detail.review_decision' ) }
205
- </ Button >
206
- </ div >
207
- </ div >
208
- </ fetcher . Form >
209
- </ FormProvider >
222
+ < FormErrorOrDescription />
223
+ </ div >
224
+ ) }
225
+ </ form . Field >
226
+ </ div >
227
+
228
+ < div className = "flex flex-1 flex-row gap-2" >
229
+ < ModalV2 . Close render = { < Button className = "flex-1" variant = "secondary" /> } >
230
+ { t ( 'common:cancel' ) }
231
+ </ ModalV2 . Close >
232
+ < Button className = "flex-1" variant = "primary" type = "submit" >
233
+ < LoadingIcon
234
+ icon = "case-manager"
235
+ className = "size-5"
236
+ loading = { fetcher . state === 'submitting' }
237
+ />
238
+ { t ( 'cases:case_detail.review_decision' ) }
239
+ </ Button >
240
+ </ div >
241
+ </ form >
210
242
) ;
211
243
}
0 commit comments