1
- import { executeRequestInAsync } from "@modules/app/requestExecutor" ;
1
+ import {
2
+ executeRequestInAsync ,
3
+ executeRequestInSync ,
4
+ } from "@modules/app/requestExecutor" ;
2
5
import { IncomingMessageProps } from "@modules/app/types" ;
3
6
import { panelLogger } from "@modules/logger" ;
4
7
import {
@@ -7,6 +10,7 @@ import {
7
10
useEffect ,
8
11
useMemo ,
9
12
useReducer ,
13
+ useRef ,
10
14
} from "react" ;
11
15
import documentationSlice , {
12
16
initialState ,
@@ -21,6 +25,7 @@ import documentationSlice, {
21
25
updateColumnsInCurrentDocsData ,
22
26
updateConversationsRightPanelState ,
23
27
updateCurrentDocsData ,
28
+ updateCurrentDocsTests ,
24
29
updateSelectedConversationGroup ,
25
30
updateUserInstructions ,
26
31
} from "./state/documentationSlice" ;
@@ -31,7 +36,7 @@ import {
31
36
MetadataColumn ,
32
37
} from "./state/types" ;
33
38
import { ContextProps } from "./types" ;
34
- import { getGenerationsInModel } from "./utils" ;
39
+ import { getGenerationsInModel , isStateDirty } from "./utils" ;
35
40
import DocumentationEditor from "./DocumentationEditor" ;
36
41
import { ConversationGroup , DbtDocsShareDetails } from "@lib" ;
37
42
import { TelemetryEvents } from "@telemetryEvents" ;
@@ -43,14 +48,38 @@ export const DocumentationContext = createContext<ContextProps>({
43
48
dispatch : ( ) => null ,
44
49
} ) ;
45
50
51
+ type IncomingMessageEvent = MessageEvent <
52
+ IncomingMessageProps & {
53
+ docs ?: DBTDocumentation ;
54
+ tests ?: DBTModelTest [ ] ;
55
+ project ?: string ;
56
+ columns ?: DBTDocumentation [ "columns" ] ;
57
+ model ?: string ;
58
+ name ?: string ;
59
+ description ?: string ;
60
+ collaborationEnabled ?: boolean ;
61
+ missingDocumentationMessage ?: {
62
+ message : string ;
63
+ type : "error" | "warning" ;
64
+ } ;
65
+ }
66
+ > ;
67
+
68
+ enum ActionState {
69
+ CANCEL_STAY = "Stay" ,
70
+ DISCARD_PROCEED = "Discard" ,
71
+ SAVE_PROCEED = "Save changes" ,
72
+ }
73
+
46
74
const DocumentationProvider = ( ) : JSX . Element => {
47
75
const {
48
76
state : { isComponentsApiInitialized } ,
49
77
} = useAppContext ( ) ;
50
78
const [ state , dispatch ] = useReducer (
51
79
documentationSlice . reducer ,
52
- documentationSlice . getInitialState ( )
80
+ documentationSlice . getInitialState ( ) ,
53
81
) ;
82
+ const stateRef = useRef ( state ) ;
54
83
55
84
const updateFocus = ( name ?: string ) => {
56
85
dispatch ( setInsertedEntityName ( name ) ) ;
@@ -84,107 +113,147 @@ const DocumentationProvider = (): JSX.Element => {
84
113
updateSelectedConversationGroup ( {
85
114
shareId,
86
115
conversationGroupId : conversation_group_id ,
87
- } )
116
+ } ) ,
88
117
) ;
89
118
} ;
90
119
91
- const onMessage = useCallback (
92
- (
93
- event : MessageEvent <
94
- IncomingMessageProps & {
95
- docs ?: DBTDocumentation ;
96
- tests ?: DBTModelTest [ ] ;
97
- project ?: string ;
98
- columns ?: DBTDocumentation [ "columns" ] ;
99
- model ?: string ;
100
- name ?: string ;
101
- description ?: string ;
102
- collaborationEnabled ?: boolean ;
103
- missingDocumentationMessage ?: {
104
- message : string ;
105
- type : "error" | "warning" ;
106
- } ;
107
- }
108
- >
109
- ) => {
110
- const { command, ...params } = event . data ;
111
- switch ( command ) {
112
- case "viewConversation" :
113
- handleViewConversation (
114
- params as unknown as Parameters < typeof handleViewConversation > [ "0" ]
115
- ) ;
120
+ const renderDocumentation = ( event : IncomingMessageEvent ) => {
121
+ dispatch (
122
+ setIncomingDocsData ( {
123
+ docs : event . data . docs ,
124
+ tests : event . data . tests ,
125
+ } ) ,
126
+ ) ;
127
+ dispatch ( setProject ( event . data . project ) ) ;
128
+ dispatch (
129
+ updateCollaborationEnabled ( Boolean ( event . data . collaborationEnabled ) ) ,
130
+ ) ;
131
+ dispatch (
132
+ setMissingDocumentationMessage ( event . data . missingDocumentationMessage ) ,
133
+ ) ;
134
+ } ;
135
+
136
+ const onMessage = useCallback ( ( event : IncomingMessageEvent ) => {
137
+ const { command, ...params } = event . data ;
138
+ switch ( command ) {
139
+ case "viewConversation" :
140
+ handleViewConversation (
141
+ params as unknown as Parameters < typeof handleViewConversation > [ "0" ] ,
142
+ ) ;
143
+ break ;
144
+ case "conversations:updates" :
145
+ handleConversationUpdates (
146
+ params as unknown as Parameters <
147
+ typeof handleConversationUpdates
148
+ > [ "0" ] ,
149
+ ) ;
150
+ break ;
151
+ case "renderDocumentation" : {
152
+ if (
153
+ event . data . docs ?. uniqueId ===
154
+ stateRef . current . currentDocsData ?. uniqueId
155
+ ) {
116
156
break ;
117
- case "conversations:updates" :
118
- handleConversationUpdates (
119
- params as unknown as Parameters <
120
- typeof handleConversationUpdates
121
- > [ "0" ]
122
- ) ;
157
+ }
158
+ if ( ! isStateDirty ( stateRef . current ) ) {
159
+ renderDocumentation ( event ) ;
123
160
break ;
124
- case "renderDocumentation" :
125
- dispatch (
126
- setIncomingDocsData ( {
127
- docs : event . data . docs ,
128
- tests : event . data . tests ,
129
- } )
130
- ) ;
131
- dispatch ( setProject ( event . data . project ) ) ;
132
- dispatch (
133
- updateCollaborationEnabled ( Boolean ( event . data . collaborationEnabled ) )
134
- ) ;
161
+ }
162
+ const { currentDocsData, currentDocsTests } = stateRef . current ;
163
+ executeRequestInSync ( "showWarningMessage" , {
164
+ infoMessage : `You have unsaved changes in model: ‘${ currentDocsData ?. name } ’. Would you
165
+ like to discard the changes, save them and proceed, or remain in the
166
+ current state?` ,
167
+ items : [
168
+ ActionState . DISCARD_PROCEED ,
169
+ ActionState . CANCEL_STAY ,
170
+ ActionState . SAVE_PROCEED ,
171
+ ] ,
172
+ } )
173
+ . then ( async ( action ) => {
174
+ switch ( action ) {
175
+ case ActionState . SAVE_PROCEED : {
176
+ const result = ( await executeRequestInSync (
177
+ "saveDocumentation" ,
178
+ {
179
+ ...currentDocsData ,
180
+ updatedTests : currentDocsTests ,
181
+ dialogType : "Existing file" ,
182
+ } ,
183
+ ) ) as { saved : boolean } ;
184
+ if ( result . saved ) {
185
+ dispatch ( updateCurrentDocsData ( event . data . docs ) ) ;
186
+ dispatch ( updateCurrentDocsTests ( event . data . tests ) ) ;
187
+ }
188
+ renderDocumentation ( event ) ;
189
+ break ;
190
+ }
191
+ case ActionState . DISCARD_PROCEED : {
192
+ dispatch ( updateCurrentDocsData ( event . data . docs ) ) ;
193
+ dispatch ( updateCurrentDocsTests ( event . data . tests ) ) ;
194
+ renderDocumentation ( event ) ;
195
+ break ;
196
+ }
197
+ case ActionState . CANCEL_STAY : {
198
+ break ;
199
+ }
200
+ default :
201
+ break ;
202
+ }
203
+ } )
204
+ . catch ( ( err ) => {
205
+ panelLogger . error (
206
+ "error while showing unsaved changes dialog" ,
207
+ err ,
208
+ ) ;
209
+ } ) ;
210
+ break ;
211
+ }
212
+ case "renderColumnsFromMetadataFetch" :
213
+ if ( event . data . columns ) {
135
214
dispatch (
136
- setMissingDocumentationMessage (
137
- event . data . missingDocumentationMessage
138
- )
215
+ updateColumnsAfterSync ( {
216
+ columns : event . data . columns ,
217
+ } ) ,
139
218
) ;
140
- break ;
141
- case "renderColumnsFromMetadataFetch" :
142
- if ( event . data . columns ) {
143
- dispatch (
144
- updateColumnsAfterSync ( {
145
- columns : event . data . columns ,
146
- } )
147
- ) ;
148
- }
149
- break ;
150
- case "docgen:insert" :
151
- panelLogger . info ( "received new doc gen" , event . data ) ;
152
- // insert model desc
153
- if ( params . model ) {
154
- dispatch (
155
- updateCurrentDocsData ( {
156
- description : params . description ,
157
- name : params . model ,
158
- isNewGeneration : true ,
159
- } )
160
- ) ;
161
- updateFocus ( params . model ) ;
162
- return ;
163
- }
164
- // insert column desc
219
+ }
220
+ break ;
221
+ case "docgen:insert" :
222
+ panelLogger . info ( "received new doc gen" , event . data ) ;
223
+ // insert model desc
224
+ if ( params . model ) {
165
225
dispatch (
166
- updateColumnsInCurrentDocsData ( {
167
- columns : [ params as Partial < MetadataColumn > ] ,
226
+ updateCurrentDocsData ( {
227
+ description : params . description ,
228
+ name : params . model ,
168
229
isNewGeneration : true ,
169
- } )
230
+ } ) ,
170
231
) ;
171
- updateFocus ( ( params as Partial < MetadataColumn > ) . name ) ;
232
+ updateFocus ( params . model ) ;
233
+ return ;
234
+ }
235
+ // insert column desc
236
+ dispatch (
237
+ updateColumnsInCurrentDocsData ( {
238
+ columns : [ params as Partial < MetadataColumn > ] ,
239
+ isNewGeneration : true ,
240
+ } ) ,
241
+ ) ;
242
+ updateFocus ( ( params as Partial < MetadataColumn > ) . name ) ;
172
243
173
- break ;
174
- default :
175
- break ;
176
- }
177
- } ,
178
- [ ]
179
- ) ;
244
+ break ;
245
+ default :
246
+ break ;
247
+ }
248
+ } , [ ] ) ;
180
249
181
250
const loadGenerationsHistory = ( project : string , model : string ) => {
182
251
getGenerationsInModel ( project , model )
183
252
. then ( ( data ) => {
184
253
dispatch ( setGenerationsHistory ( data ) ) ;
185
254
} )
186
255
. catch ( ( err ) =>
187
- panelLogger . error ( "error while loading generations history" , err )
256
+ panelLogger . error ( "error while loading generations history" , err ) ,
188
257
) ;
189
258
} ;
190
259
@@ -197,8 +266,8 @@ const DocumentationProvider = (): JSX.Element => {
197
266
if ( userInstructions ) {
198
267
dispatch (
199
268
updateUserInstructions (
200
- JSON . parse ( userInstructions ) as DocsGenerateUserInstructions
201
- )
269
+ JSON . parse ( userInstructions ) as DocsGenerateUserInstructions ,
270
+ ) ,
202
271
) ;
203
272
}
204
273
loadGenerationsHistory ( state . project , state . currentDocsData . name ) ;
@@ -220,9 +289,14 @@ const DocumentationProvider = (): JSX.Element => {
220
289
state,
221
290
dispatch,
222
291
} ) ,
223
- [ state , dispatch ]
292
+ [ state , dispatch ] ,
224
293
) ;
225
294
295
+ // hack to get latest state in onMessage
296
+ useEffect ( ( ) => {
297
+ stateRef . current = state ;
298
+ } , [ state ] ) ;
299
+
226
300
if ( ! isComponentsApiInitialized ) {
227
301
return < div > Loading...</ div > ;
228
302
}
0 commit comments