1
1
import CheckOutlined from "@mui/icons-material/CheckOutlined" ;
2
2
import FileCopyOutlined from "@mui/icons-material/FileCopyOutlined" ;
3
- import FormControlLabel from "@mui/material/FormControlLabel" ;
4
- import Radio from "@mui/material/Radio" ;
5
- import RadioGroup from "@mui/material/RadioGroup" ;
6
3
import { API } from "api/api" ;
7
4
import { DetailedError } from "api/errors" ;
8
5
import type {
@@ -11,56 +8,48 @@ import type {
11
8
FriendlyDiagnostic ,
12
9
PreviewParameter ,
13
10
Template ,
14
- User ,
15
11
} from "api/typesGenerated" ;
16
12
import { ErrorAlert } from "components/Alert/ErrorAlert" ;
17
13
import { Button } from "components/Button/Button" ;
18
- import { FormSection } from "components/Form/Form" ;
19
- import { Loader } from "components/Loader/Loader" ;
14
+ import { Label } from "components/Label/Label" ;
15
+ import { RadioGroup , RadioGroupItem } from "components/RadioGroup/RadioGroup" ;
16
+ import { Skeleton } from "components/Skeleton/Skeleton" ;
17
+ import { useAuthenticated } from "hooks" ;
20
18
import { useEffectEvent } from "hooks/hookPolyfills" ;
21
19
import { useClipboard } from "hooks/useClipboard" ;
22
20
import {
23
21
Diagnostics ,
24
22
DynamicParameter ,
25
23
} from "modules/workspaces/DynamicParameter/DynamicParameter" ;
26
24
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout" ;
27
- import {
28
- type FC ,
29
- useCallback ,
30
- useEffect ,
31
- useMemo ,
32
- useRef ,
33
- useState ,
34
- } from "react" ;
25
+ import { type FC , useEffect , useMemo , useRef , useState } from "react" ;
35
26
import { Helmet } from "react-helmet-async" ;
36
- import { useQuery } from "react-query" ;
37
27
import { pageTitle } from "utils/page" ;
38
28
39
29
type ButtonValues = Record < string , string > ;
40
30
41
31
const TemplateEmbedPageExperimental : FC = ( ) => {
42
32
const { template } = useTemplateLayoutContext ( ) ;
33
+ const { user : me } = useAuthenticated ( ) ;
43
34
const [ latestResponse , setLatestResponse ] =
44
35
useState < DynamicParametersResponse | null > ( null ) ;
45
36
const wsResponseId = useRef < number > ( - 1 ) ;
46
37
const ws = useRef < WebSocket | null > ( null ) ;
47
38
const [ wsError , setWsError ] = useState < Error | null > ( null ) ;
48
39
49
- const { data : authenticatedUser } = useQuery < User > ( {
50
- queryKey : [ "authenticatedUser" ] ,
51
- queryFn : ( ) => API . getAuthenticatedUser ( ) ,
52
- } ) ;
53
-
54
- const sendMessage = useCallback ( ( formValues : Record < string , string > ) => {
55
- const request : DynamicParametersRequest = {
56
- id : wsResponseId . current + 1 ,
57
- inputs : formValues ,
58
- } ;
59
- if ( ws . current && ws . current . readyState === WebSocket . OPEN ) {
60
- ws . current . send ( JSON . stringify ( request ) ) ;
61
- wsResponseId . current = wsResponseId . current + 1 ;
62
- }
63
- } , [ ] ) ;
40
+ const sendMessage = useEffectEvent (
41
+ ( formValues : Record < string , string > , ownerId ?: string ) => {
42
+ const request : DynamicParametersRequest = {
43
+ id : wsResponseId . current + 1 ,
44
+ owner_id : me . id ,
45
+ inputs : formValues ,
46
+ } ;
47
+ if ( ws . current && ws . current . readyState === WebSocket . OPEN ) {
48
+ ws . current . send ( JSON . stringify ( request ) ) ;
49
+ wsResponseId . current = wsResponseId . current + 1 ;
50
+ }
51
+ } ,
52
+ ) ;
64
53
65
54
const onMessage = useEffectEvent ( ( response : DynamicParametersResponse ) => {
66
55
if ( latestResponse && latestResponse ?. id >= response . id ) {
@@ -71,25 +60,29 @@ const TemplateEmbedPageExperimental: FC = () => {
71
60
} ) ;
72
61
73
62
useEffect ( ( ) => {
74
- if ( ! template . active_version_id || ! authenticatedUser ) {
63
+ if ( ! template . active_version_id || ! me ) {
75
64
return ;
76
65
}
77
66
78
67
const socket = API . templateVersionDynamicParameters (
79
- authenticatedUser . id ,
80
68
template . active_version_id ,
69
+ me . id ,
81
70
{
82
71
onMessage,
83
72
onError : ( error ) => {
84
- setWsError ( error ) ;
73
+ if ( ws . current === socket ) {
74
+ setWsError ( error ) ;
75
+ }
85
76
} ,
86
77
onClose : ( ) => {
87
- setWsError (
88
- new DetailedError (
89
- "Websocket connection for dynamic parameters unexpectedly closed." ,
90
- "Refresh the page to reset the form." ,
91
- ) ,
92
- ) ;
78
+ if ( ws . current === socket ) {
79
+ setWsError (
80
+ new DetailedError (
81
+ "Websocket connection for dynamic parameters unexpectedly closed." ,
82
+ "Refresh the page to reset the form." ,
83
+ ) ,
84
+ ) ;
85
+ }
93
86
} ,
94
87
} ,
95
88
) ;
@@ -99,7 +92,7 @@ const TemplateEmbedPageExperimental: FC = () => {
99
92
return ( ) => {
100
93
socket . close ( ) ;
101
94
} ;
102
- } , [ authenticatedUser , template . active_version_id , onMessage ] ) ;
95
+ } , [ template . active_version_id , onMessage , me ] ) ;
103
96
104
97
const sortedParams = useMemo ( ( ) => {
105
98
if ( ! latestResponse ?. parameters ) {
@@ -108,6 +101,9 @@ const TemplateEmbedPageExperimental: FC = () => {
108
101
return [ ...latestResponse . parameters ] . sort ( ( a , b ) => a . order - b . order ) ;
109
102
} , [ latestResponse ?. parameters ] ) ;
110
103
104
+ const isLoading =
105
+ ws . current ?. readyState === WebSocket . CONNECTING || ! latestResponse ;
106
+
111
107
return (
112
108
< >
113
109
< Helmet >
@@ -119,6 +115,7 @@ const TemplateEmbedPageExperimental: FC = () => {
119
115
diagnostics = { latestResponse ?. diagnostics ?? [ ] }
120
116
error = { wsError }
121
117
sendMessage = { sendMessage }
118
+ isLoading = { isLoading }
122
119
/>
123
120
</ >
124
121
) ;
@@ -130,6 +127,7 @@ interface TemplateEmbedPageViewProps {
130
127
diagnostics : readonly FriendlyDiagnostic [ ] ;
131
128
error : unknown ;
132
129
sendMessage : ( message : Record < string , string > ) => void ;
130
+ isLoading : boolean ;
133
131
}
134
132
135
133
const TemplateEmbedPageView : FC < TemplateEmbedPageViewProps > = ( {
@@ -138,45 +136,46 @@ const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
138
136
diagnostics,
139
137
error,
140
138
sendMessage,
139
+ isLoading,
141
140
} ) => {
142
- const [ buttonValues , setButtonValues ] = useState < ButtonValues | undefined > ( ) ;
143
- const [ localParameters , setLocalParameters ] = useState <
144
- Record < string , string >
145
- > ( { } ) ;
141
+ const [ formState , setFormState ] = useState < {
142
+ mode : "manual" | "auto" ;
143
+ paramValues : Record < string , string > ;
144
+ } > ( {
145
+ mode : "manual" ,
146
+ paramValues : { } ,
147
+ } ) ;
146
148
147
149
useEffect ( ( ) => {
148
150
if ( parameters ) {
149
- const initialInputs : Record < string , string > = { } ;
150
- const currentMode = buttonValues ?. mode || "manual" ;
151
- const initialButtonParamValues : ButtonValues = { mode : currentMode } ;
152
-
151
+ const serverParamValues : Record < string , string > = { } ;
153
152
for ( const p of parameters ) {
154
153
const initialVal = p . value ?. valid ? p . value . value : "" ;
155
- initialInputs [ p . name ] = initialVal ;
156
- initialButtonParamValues [ `param.${ p . name } ` ] = initialVal ;
154
+ serverParamValues [ p . name ] = initialVal ;
157
155
}
158
- setLocalParameters ( initialInputs ) ;
156
+ setFormState ( ( prev ) => ( { ...prev , paramValues : serverParamValues } ) ) ;
157
+ }
158
+ } , [ parameters ] ) ;
159
159
160
- setButtonValues ( initialButtonParamValues ) ;
160
+ const buttonValues = useMemo ( ( ) => {
161
+ const values : ButtonValues = { mode : formState . mode } ;
162
+ for ( const [ key , value ] of Object . entries ( formState . paramValues ) ) {
163
+ values [ `param.${ key } ` ] = value ;
161
164
}
162
- } , [ parameters , buttonValues ?. mode ] ) ;
165
+ return values ;
166
+ } , [ formState ] ) ;
163
167
164
168
const handleChange = (
165
169
changedParamInfo : PreviewParameter ,
166
170
newValue : string ,
167
171
) => {
168
- const newFormInputs = {
169
- ...localParameters ,
172
+ const newParamValues = {
173
+ ...formState . paramValues ,
170
174
[ changedParamInfo . name ] : newValue ,
171
175
} ;
172
- setLocalParameters ( newFormInputs ) ;
173
-
174
- setButtonValues ( ( prevButtonValues ) => ( {
175
- ...( prevButtonValues || { } ) ,
176
- [ `param.${ changedParamInfo . name } ` ] : newValue ,
177
- } ) ) ;
176
+ setFormState ( ( prev ) => ( { ...prev , paramValues : newParamValues } ) ) ;
178
177
179
- const formInputsToSend : Record < string , string > = { ...newFormInputs } ;
178
+ const formInputsToSend : Record < string , string > = { ...newParamValues } ;
180
179
for ( const p of parameters ) {
181
180
if ( ! ( p . name in formInputsToSend ) ) {
182
181
formInputsToSend [ p . name ] = p . value ?. valid ? p . value . value : "" ;
@@ -186,68 +185,84 @@ const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
186
185
sendMessage ( formInputsToSend ) ;
187
186
} ;
188
187
189
- useEffect ( ( ) => {
190
- if ( ! buttonValues && parameters . length === 0 ) {
191
- setButtonValues ( { mode : "manual" } ) ;
192
- } else if ( buttonValues && ! buttonValues . mode && parameters . length > 0 ) {
193
- setButtonValues ( ( prev ) => ( { ...prev , mode : "manual" } ) ) ;
194
- }
195
- } , [ buttonValues , parameters ] ) ;
196
-
197
- if ( ! buttonValues || ( ! parameters && ! error ) ) {
198
- return < Loader /> ;
199
- }
200
-
201
188
return (
202
189
< >
203
190
< div className = "flex items-start gap-12" >
204
- < div className = "flex flex-col gap-5 max-w-screen-md" >
205
- { Boolean ( error ) && < ErrorAlert error = { error } /> }
206
- { diagnostics . length > 0 && < Diagnostics diagnostics = { diagnostics } /> }
207
- < div className = "flex flex-col" >
208
- < FormSection
209
- title = "Creation mode"
210
- description = "By changing the mode to automatic, when the user clicks the button, the workspace will be created automatically instead of showing a form to the user."
211
- >
212
- < RadioGroup
213
- defaultValue = { buttonValues ?. mode || "manual" }
214
- onChange = { ( _ , v ) => {
215
- setButtonValues ( ( prevButtonValues ) => ( {
216
- ...( prevButtonValues || { } ) ,
217
- mode : v ,
218
- } ) ) ;
219
- } }
220
- >
221
- < FormControlLabel
222
- value = "manual"
223
- control = { < Radio size = "small" /> }
224
- label = "Manual"
225
- />
226
- < FormControlLabel
227
- value = "auto"
228
- control = { < Radio size = "small" /> }
229
- label = "Automatic"
230
- />
231
- </ RadioGroup >
232
- </ FormSection >
233
-
234
- { parameters . length > 0 && (
191
+ < div className = "w-full flex flex-col gap-5 max-w-screen-md" >
192
+ { isLoading ? (
193
+ < div className = "flex flex-col gap-9" >
194
+ < div className = "flex flex-col gap-2" >
195
+ < Skeleton className = "h-5 w-1/3" />
196
+ < Skeleton className = "h-9 w-full" />
197
+ </ div >
198
+ < div className = "flex flex-col gap-2" >
199
+ < Skeleton className = "h-5 w-1/3" />
200
+ < Skeleton className = "h-9 w-full" />
201
+ </ div >
202
+ < div className = "flex flex-col gap-2" >
203
+ < Skeleton className = "h-5 w-1/3" />
204
+ < Skeleton className = "h-9 w-full" />
205
+ </ div >
206
+ </ div >
207
+ ) : (
208
+ < >
209
+ { Boolean ( error ) && < ErrorAlert error = { error } /> }
210
+ { diagnostics . length > 0 && (
211
+ < Diagnostics diagnostics = { diagnostics } />
212
+ ) }
235
213
< div className = "flex flex-col gap-9" >
236
- { parameters . map ( ( parameter ) => {
237
- const isDisabled = parameter . styling ?. disabled ;
238
- return (
239
- < DynamicParameter
240
- key = { parameter . name }
241
- parameter = { parameter }
242
- onChange = { ( value ) => handleChange ( parameter , value ) }
243
- disabled = { isDisabled }
244
- value = { localParameters [ parameter . name ] || "" }
245
- />
246
- ) ;
247
- } ) }
214
+ < section className = "flex flex-col gap-2" >
215
+ < div >
216
+ < h2 className = "text-lg font-bold m-0" > Creation mode</ h2 >
217
+ < p className = "text-sm text-content-secondary m-0" >
218
+ When set to automatic mode, clicking the button will
219
+ create the workspace automatically without displaying a
220
+ form to the user.
221
+ </ p >
222
+ </ div >
223
+ < RadioGroup
224
+ value = { formState . mode }
225
+ onValueChange = { ( v ) => {
226
+ setFormState ( ( prev ) => ( {
227
+ ...prev ,
228
+ mode : v as "manual" | "auto" ,
229
+ } ) ) ;
230
+ } }
231
+ >
232
+ < div className = "flex items-center gap-3" >
233
+ < RadioGroupItem value = "manual" id = "manual" />
234
+ < Label htmlFor = { "manual" } className = "cursor-pointer" >
235
+ Manual
236
+ </ Label >
237
+ </ div >
238
+ < div className = "flex items-center gap-3" >
239
+ < RadioGroupItem value = "auto" id = "automatic" />
240
+ < Label htmlFor = { "automatic" } className = "cursor-pointer" >
241
+ Automatic
242
+ </ Label >
243
+ </ div >
244
+ </ RadioGroup >
245
+ </ section >
246
+
247
+ { parameters . length > 0 && (
248
+ < div className = "flex flex-col gap-9" >
249
+ { parameters . map ( ( parameter ) => {
250
+ const isDisabled = parameter . styling ?. disabled ;
251
+ return (
252
+ < DynamicParameter
253
+ key = { parameter . name }
254
+ parameter = { parameter }
255
+ onChange = { ( value ) => handleChange ( parameter , value ) }
256
+ disabled = { isDisabled }
257
+ value = { formState . paramValues [ parameter . name ] || "" }
258
+ />
259
+ ) ;
260
+ } ) }
261
+ </ div >
262
+ ) }
248
263
</ div >
249
- ) }
250
- </ div >
264
+ </ >
265
+ ) }
251
266
</ div >
252
267
253
268
< ButtonPreview template = { template } buttonValues = { buttonValues } />
@@ -285,7 +300,7 @@ const ButtonPreview: FC<ButtonPreviewProps> = ({ template, buttonValues }) => {
285
300
286
301
return (
287
302
< div
288
- className = "sticky top-10 flex gap-16 h-80 p-14 flex-1 flex-col items-center justify-center
303
+ className = "sticky top-10 flex gap-16 h-96 flex-1 flex-col items-center justify-center
289
304
rounded-lg border border-border border-solid bg-surface-secondary"
290
305
>
291
306
< img src = "/open-in-coder.svg" alt = "Open in Coder button" />
0 commit comments