3
3
import type { DragEndEvent } from "@dnd-kit/core" ;
4
4
5
5
import * as React from "react" ;
6
- import { useCallback , useReducer , useRef } from "react" ;
7
- import { usePathname , useRouter , useSearchParams } from "next/navigation" ;
6
+ import { useCallback , useMemo , useReducer , useRef } from "react" ;
8
7
import { DndContext , KeyboardSensor , PointerSensor , useSensor , useSensors } from "@dnd-kit/core" ;
9
8
import { restrictToParentElement , restrictToVerticalAxis } from "@dnd-kit/modifiers" ;
10
9
import {
@@ -15,7 +14,8 @@ import {
15
14
import { zodResolver } from "@hookform/resolvers/zod" ;
16
15
import { useFieldArray , useForm } from "react-hook-form" ;
17
16
18
- import type { Stages } from "db/public" ;
17
+ import type { FormElementsId , NewFormElements , Stages } from "db/public" ;
18
+ import { formElementsInitializerSchema } from "db/public" ;
19
19
import { logger } from "logger" ;
20
20
import { Form , FormControl , FormField , FormItem } from "ui/form" ;
21
21
import { useUnsavedChangesWarning } from "ui/hooks" ;
@@ -35,6 +35,7 @@ import { ElementPanel } from "./ElementPanel";
35
35
import { FormBuilderProvider } from "./FormBuilderContext" ;
36
36
import { FormElement } from "./FormElement" ;
37
37
import { formBuilderSchema , isButtonElement } from "./types" ;
38
+ import { useIsChanged } from "./useIsChanged" ;
38
39
39
40
const elementPanelReducer : React . Reducer < PanelState , PanelEvent > = ( prevState , event ) => {
40
41
const { eventName } = event ;
@@ -107,13 +108,68 @@ type Props = {
107
108
stages : Stages [ ] ;
108
109
} ;
109
110
111
+ /**
112
+ * Only sends the dirty fields to the server
113
+ */
114
+ const preparePayload = ( {
115
+ formValues,
116
+ defaultValues,
117
+ } : {
118
+ defaultValues : Omit < FormBuilderSchema , "id" > ;
119
+ formValues : FormBuilderSchema ;
120
+ } ) => {
121
+ const { upserts, deletes } = formValues . elements . reduce < {
122
+ upserts : NewFormElements [ ] ;
123
+ deletes : FormElementsId [ ] ;
124
+ } > (
125
+ ( acc , element , index ) => {
126
+ if ( element . deleted ) {
127
+ if ( element . elementId ) {
128
+ acc . deletes . push ( element . elementId ) ;
129
+ }
130
+ } else if ( ! element . elementId ) {
131
+ // Newly created elements have no elementId
132
+ acc . upserts . push (
133
+ formElementsInitializerSchema . parse ( { formId : formValues . formId , ...element } )
134
+ ) ;
135
+ } else if ( element . updated ) {
136
+ // check whether the element is reeeaally updated minus the updated field
137
+ const { updated : _ , id : _id , ...elementWithoutUpdated } = element ;
138
+ const { updated, id, ...defaultElement } =
139
+ defaultValues . elements . find ( ( e ) => e . elementId === element . elementId ) ?? { } ;
140
+
141
+ if ( JSON . stringify ( defaultElement ) === JSON . stringify ( elementWithoutUpdated ) ) {
142
+ return acc ;
143
+ }
144
+
145
+ acc . upserts . push (
146
+ formElementsInitializerSchema . parse ( {
147
+ ...element ,
148
+ formId : formValues . formId ,
149
+ id : element . elementId ,
150
+ } )
151
+ ) ; // TODO: only update changed columns
152
+ }
153
+ return acc ;
154
+ } ,
155
+ { upserts : [ ] , deletes : [ ] }
156
+ ) ;
157
+
158
+ const access = formValues . access !== defaultValues . access ? formValues . access : undefined ;
159
+
160
+ return {
161
+ formId : formValues . formId ,
162
+ upserts,
163
+ deletes,
164
+ access,
165
+ } ;
166
+ } ;
167
+
110
168
export function FormBuilder ( { pubForm, id, stages } : Props ) {
111
- const router = useRouter ( ) ;
112
- const pathname = usePathname ( ) ;
113
- const params = useSearchParams ( ) ;
114
- const form = useForm < FormBuilderSchema > ( {
115
- resolver : zodResolver ( formBuilderSchema ) ,
116
- values : {
169
+ const [ isChanged , setIsChanged ] = useIsChanged ( ) ;
170
+
171
+ const defaultValues = useMemo ( ( ) => {
172
+ return {
117
173
elements : pubForm . elements . map ( ( e ) => {
118
174
// Do not include extra fields here
119
175
const { slug, id, fieldName, ...rest } = e ;
@@ -122,7 +178,12 @@ export function FormBuilder({ pubForm, id, stages }: Props) {
122
178
} ) ,
123
179
access : pubForm . access ,
124
180
formId : pubForm . id ,
125
- } ,
181
+ } ;
182
+ } , [ pubForm ] ) ;
183
+
184
+ const form = useForm < FormBuilderSchema > ( {
185
+ resolver : zodResolver ( formBuilderSchema ) ,
186
+ values : defaultValues ,
126
187
} ) ;
127
188
128
189
const sidebarRef = useRef ( null ) ;
@@ -145,22 +206,25 @@ export function FormBuilder({ pubForm, id, stages }: Props) {
145
206
control : form . control ,
146
207
} ) ;
147
208
209
+ const formValues = form . getValues ( ) ;
210
+
148
211
useUnsavedChangesWarning ( form . formState . isDirty ) ;
149
212
213
+ const payload = useMemo (
214
+ ( ) => preparePayload ( { formValues, defaultValues } ) ,
215
+ [ formValues , defaultValues ]
216
+ ) ;
217
+
150
218
React . useEffect ( ( ) => {
151
- const newParams = new URLSearchParams ( params ) ;
152
- if ( form . formState . isDirty ) {
153
- newParams . set ( "unsavedChanges" , "true" ) ;
154
- } else {
155
- newParams . delete ( "unsavedChanges" ) ;
156
- }
157
- router . replace ( `${ pathname } ?${ newParams . toString ( ) } ` , { scroll : false } ) ;
158
- } , [ form . formState . isDirty , params ] ) ;
219
+ setIsChanged (
220
+ payload . upserts . length > 0 || payload . deletes . length > 0 || payload . access != null
221
+ ) ;
222
+ } , [ payload ] ) ;
159
223
160
224
const runSaveForm = useServerAction ( saveForm ) ;
225
+
161
226
const onSubmit = async ( formData : FormBuilderSchema ) => {
162
- //TODO: only submit dirty fields
163
- const result = await runSaveForm ( formData ) ;
227
+ const result = await runSaveForm ( payload ) ;
164
228
if ( didSucceed ( result ) ) {
165
229
toast ( {
166
230
className : "rounded border-emerald-100 bg-emerald-50" ,
@@ -250,7 +314,7 @@ export function FormBuilder({ pubForm, id, stages }: Props) {
250
314
dispatch = { dispatch }
251
315
slug = { pubForm . slug }
252
316
stages = { stages }
253
- isDirty = { form . formState . isDirty }
317
+ isDirty = { isChanged }
254
318
>
255
319
< Tabs defaultValue = "builder" className = "pr-[380px]" >
256
320
< div className = "px-6" >
0 commit comments