11import CheckOutlined from "@mui/icons-material/CheckOutlined" ;
22import 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" ;
63import { API } from "api/api" ;
74import { DetailedError } from "api/errors" ;
85import type {
@@ -11,56 +8,48 @@ import type {
118FriendlyDiagnostic ,
129PreviewParameter ,
1310Template ,
14- User ,
1511} from "api/typesGenerated" ;
1612import { ErrorAlert } from "components/Alert/ErrorAlert" ;
1713import { 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" ;
2018import { useEffectEvent } from "hooks/hookPolyfills" ;
2119import { useClipboard } from "hooks/useClipboard" ;
2220import {
2321Diagnostics ,
2422DynamicParameter ,
2523} from "modules/workspaces/DynamicParameter/DynamicParameter" ;
2624import { 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" ;
3526import { Helmet } from "react-helmet-async" ;
36- import { useQuery } from "react-query" ;
3727import { pageTitle } from "utils/page" ;
3828
3929type ButtonValues = Record < string , string > ;
4030
4131const TemplateEmbedPageExperimental :FC = ( ) => {
4232const { template} = useTemplateLayoutContext ( ) ;
33+ const { user :me } = useAuthenticated ( ) ;
4334const [ latestResponse , setLatestResponse ] =
4435useState < DynamicParametersResponse | null > ( null ) ;
4536const wsResponseId = useRef < number > ( - 1 ) ;
4637const ws = useRef < WebSocket | null > ( null ) ;
4738const [ wsError , setWsError ] = useState < Error | null > ( null ) ;
4839
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+ ) ;
6453
6554const onMessage = useEffectEvent ( ( response :DynamicParametersResponse ) => {
6655if ( latestResponse && latestResponse ?. id >= response . id ) {
@@ -71,25 +60,29 @@ const TemplateEmbedPageExperimental: FC = () => {
7160} ) ;
7261
7362useEffect ( ( ) => {
74- if ( ! template . active_version_id || ! authenticatedUser ) {
63+ if ( ! template . active_version_id || ! me ) {
7564return ;
7665}
7766
7867const socket = API . templateVersionDynamicParameters (
79- authenticatedUser . id ,
8068template . active_version_id ,
69+ me . id ,
8170{
8271onMessage,
8372onError :( error ) => {
84- setWsError ( error ) ;
73+ if ( ws . current === socket ) {
74+ setWsError ( error ) ;
75+ }
8576} ,
8677onClose :( ) => {
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+ }
9386} ,
9487} ,
9588) ;
@@ -99,7 +92,7 @@ const TemplateEmbedPageExperimental: FC = () => {
9992return ( ) => {
10093socket . close ( ) ;
10194} ;
102- } , [ authenticatedUser , template . active_version_id , onMessage ] ) ;
95+ } , [ template . active_version_id , onMessage , me ] ) ;
10396
10497const sortedParams = useMemo ( ( ) => {
10598if ( ! latestResponse ?. parameters ) {
@@ -108,6 +101,9 @@ const TemplateEmbedPageExperimental: FC = () => {
108101return [ ...latestResponse . parameters ] . sort ( ( a , b ) => a . order - b . order ) ;
109102} , [ latestResponse ?. parameters ] ) ;
110103
104+ const isLoading =
105+ ws . current ?. readyState === WebSocket . CONNECTING || ! latestResponse ;
106+
111107return (
112108< >
113109< Helmet >
@@ -119,6 +115,7 @@ const TemplateEmbedPageExperimental: FC = () => {
119115diagnostics = { latestResponse ?. diagnostics ?? [ ] }
120116error = { wsError }
121117sendMessage = { sendMessage }
118+ isLoading = { isLoading }
122119/>
123120</ >
124121) ;
@@ -130,6 +127,7 @@ interface TemplateEmbedPageViewProps {
130127diagnostics :readonly FriendlyDiagnostic [ ] ;
131128error :unknown ;
132129sendMessage :( message :Record < string , string > ) => void ;
130+ isLoading :boolean ;
133131}
134132
135133const TemplateEmbedPageView :FC < TemplateEmbedPageViewProps > = ( {
@@ -138,45 +136,46 @@ const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
138136diagnostics,
139137error,
140138sendMessage,
139+ isLoading,
141140} ) => {
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+ } ) ;
146148
147149useEffect ( ( ) => {
148150if ( 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 > = { } ;
153152for ( const p of parameters ) {
154153const initialVal = p . value ?. valid ?p . value . value :"" ;
155- initialInputs [ p . name ] = initialVal ;
156- initialButtonParamValues [ `param.${ p . name } ` ] = initialVal ;
154+ serverParamValues [ p . name ] = initialVal ;
157155}
158- setLocalParameters ( initialInputs ) ;
156+ setFormState ( ( prev ) => ( { ...prev , paramValues :serverParamValues } ) ) ;
157+ }
158+ } , [ parameters ] ) ;
159159
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 ;
161164}
162- } , [ parameters , buttonValues ?. mode ] ) ;
165+ return values ;
166+ } , [ formState ] ) ;
163167
164168const handleChange = (
165169changedParamInfo :PreviewParameter ,
166170newValue :string ,
167171) => {
168- const newFormInputs = {
169- ...localParameters ,
172+ const newParamValues = {
173+ ...formState . paramValues ,
170174[ changedParamInfo . name ] :newValue ,
171175} ;
172- setLocalParameters ( newFormInputs ) ;
173-
174- setButtonValues ( ( prevButtonValues ) => ( {
175- ...( prevButtonValues || { } ) ,
176- [ `param.${ changedParamInfo . name } ` ] :newValue ,
177- } ) ) ;
176+ setFormState ( ( prev ) => ( { ...prev , paramValues :newParamValues } ) ) ;
178177
179- const formInputsToSend :Record < string , string > = { ...newFormInputs } ;
178+ const formInputsToSend :Record < string , string > = { ...newParamValues } ;
180179for ( const p of parameters ) {
181180if ( ! ( p . name in formInputsToSend ) ) {
182181formInputsToSend [ p . name ] = p . value ?. valid ?p . value . value :"" ;
@@ -186,68 +185,84 @@ const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
186185sendMessage ( formInputsToSend ) ;
187186} ;
188187
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-
201188return (
202189< >
203190< 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+ ) }
235213< 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+ ) }
248263</ div >
249- ) }
250- </ div >
264+ </ >
265+ ) }
251266</ div >
252267
253268< ButtonPreview template = { template } buttonValues = { buttonValues } />
@@ -285,7 +300,7 @@ const ButtonPreview: FC<ButtonPreviewProps> = ({ template, buttonValues }) => {
285300
286301return (
287302< 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
289304 rounded-lg border border-border border-solid bg-surface-secondary"
290305>
291306< img src = "/open-in-coder.svg" alt = "Open in Coder button" />