11import  {  nanoid  }  from  "nanoid" ; 
2- import  {  useId  }  from  "react" ; 
2+ import  {  computed  }  from  "nanostores" ; 
3+ import  { 
4+   forwardRef , 
5+   useId , 
6+   useMemo , 
7+   useRef , 
8+   useState , 
9+   type  ComponentProps , 
10+ }  from  "react" ; 
311import  {  useStore  }  from  "@nanostores/react" ; 
4- import  {  InputField  }  from  "@webstudio-is/design-system" ; 
12+ import  {  isFeatureEnabled  }  from  "@webstudio-is/feature-flags" ; 
13+ import  {  GearIcon  }  from  "@webstudio-is/icons" ; 
14+ import  { 
15+   EnhancedTooltip , 
16+   Flex , 
17+   FloatingPanel , 
18+   InputField , 
19+   NestedInputButton , 
20+   theme , 
21+ }  from  "@webstudio-is/design-system" ; 
522import  {  isLiteralExpression ,  Resource ,  type  Prop  }  from  "@webstudio-is/sdk" ; 
623import  { 
724  BindingControl , 
825  BindingPopover , 
926  type  BindingVariant , 
1027}  from  "~/builder/shared/binding-popover" ; 
11- import  { 
12-   type  ControlProps , 
13-   useLocalValue , 
14-   humanizeAttribute , 
15-   VerticalLayout , 
16- }  from  "../shared" ; 
17- import  {  $resources  }  from  "~/shared/nano-states" ; 
18- import  {  $selectedInstanceResourceScope  }  from  "../resource-panel" ; 
28+ import  {  $props ,  $resources  }  from  "~/shared/nano-states" ; 
1929import  {  computeExpression  }  from  "~/shared/data-variables" ; 
2030import  {  updateWebstudioData  }  from  "~/shared/instance-utils" ; 
31+ import  {  $selectedInstance  }  from  "~/shared/awareness" ; 
32+ import  { 
33+   $selectedInstanceResourceScope , 
34+   UrlField , 
35+   MethodField , 
36+   Headers , 
37+   parseResource , 
38+ }  from  "../resource-panel" ; 
39+ import  {  type  ControlProps ,  useLocalValue ,  VerticalLayout  }  from  "../shared" ; 
2140import  {  PropertyLabel  }  from  "../property-label" ; 
2241
23- export  const  ResourceControl  =  ( { 
24-   meta, 
25-   prop, 
42+ // dirty, dirty hack 
43+ const  areAllFormErrorsVisible  =  ( form : null  |  HTMLFormElement )  =>  { 
44+   if  ( form  ===  null )  { 
45+     return  false ; 
46+   } 
47+   // check all errors in form fields are visible 
48+   for  ( const  element  of  form . elements )  { 
49+     if  ( 
50+       element  instanceof  HTMLInputElement  || 
51+       element  instanceof  HTMLTextAreaElement 
52+     )  { 
53+       // field is invalid and the error is not visible 
54+       if  ( 
55+         element . validity . valid  ===  false  && 
56+         // rely on data-color=error convention in webstudio design system 
57+         element . getAttribute ( "data-color" )  !==  "error" 
58+       )  { 
59+         return  false ; 
60+       } 
61+     } 
62+   } 
63+   return  true ; 
64+ } ; 
65+ 
66+ const  ResourceButton  =  forwardRef < 
67+   HTMLButtonElement , 
68+   ComponentProps < typeof  NestedInputButton > 
69+ > ( ( props ,  ref )  =>  { 
70+   return  ( 
71+     < EnhancedTooltip  content = "Edit Resource" > 
72+       < NestedInputButton  { ...props }  ref = { ref }  aria-label = "Edit Resource" > 
73+         < GearIcon  /> 
74+       </ NestedInputButton > 
75+     </ EnhancedTooltip > 
76+   ) ; 
77+ } ) ; 
78+ ResourceButton . displayName  =  "ResourceButton" ; 
79+ 
80+ const  ResourceForm  =  ( {  resource } : {  resource : Resource  } )  =>  { 
81+   const  {  scope,  aliases }  =  useStore ( $selectedInstanceResourceScope ) ; 
82+   const  [ url ,  setUrl ]  =  useState ( resource . url ) ; 
83+   const  [ method ,  setMethod ]  =  useState < Resource [ "method" ] > ( resource . method ) ; 
84+   const  [ headers ,  setHeaders ]  =  useState < Resource [ "headers" ] > ( resource . headers ) ; 
85+   return  ( 
86+     < Flex 
87+       direction = "column" 
88+       css = { { 
89+         width : theme . spacing [ 30 ] , 
90+         overflow : "hidden" , 
91+         gap : theme . spacing [ 9 ] , 
92+         p : theme . spacing [ 9 ] , 
93+       } } 
94+     > 
95+       < UrlField 
96+         scope = { scope } 
97+         aliases = { aliases } 
98+         value = { url } 
99+         onChange = { setUrl } 
100+         onCurlPaste = { ( curl )  =>  { 
101+           // update all feilds when curl is paste into url field 
102+           setUrl ( JSON . stringify ( curl . url ) ) ; 
103+           setMethod ( curl . method ) ; 
104+           setHeaders ( 
105+             curl . headers . map ( ( header )  =>  ( { 
106+               name : header . name , 
107+               value : JSON . stringify ( header . value ) , 
108+             } ) ) 
109+           ) ; 
110+         } } 
111+       /> 
112+       < MethodField  value = { method }  onChange = { setMethod }  /> 
113+       < Headers 
114+         scope = { scope } 
115+         aliases = { aliases } 
116+         headers = { headers } 
117+         onChange = { setHeaders } 
118+       /> 
119+     </ Flex > 
120+   ) ; 
121+ } ; 
122+ 
123+ const  ResourceControlPanel  =  ( { 
124+   resource, 
26125  propName, 
126+   onChange, 
127+ } : { 
128+   resource : Resource ; 
129+   propName : string ; 
130+   onChange : ( resource : Resource )  =>  void ; 
131+ } )  =>  { 
132+   const  [ isResourceOpen ,  setIsResourceOpen ]  =  useState ( false ) ; 
133+   const  form  =  useRef < HTMLFormElement > ( null ) ; 
134+   return  ( 
135+     < FloatingPanel 
136+       title = "Edit Resource" 
137+       open = { isResourceOpen } 
138+       onOpenChange = { ( isOpen )  =>  { 
139+         if  ( isOpen )  { 
140+           setIsResourceOpen ( true ) ; 
141+           return ; 
142+         } 
143+         // attempt to save form on close 
144+         if  ( areAllFormErrorsVisible ( form . current ) )  { 
145+           form . current ?. requestSubmit ( ) ; 
146+           setIsResourceOpen ( false ) ; 
147+         }  else  { 
148+           form . current ?. checkValidity ( ) ; 
149+           // prevent closing when not all errors are shown to user 
150+         } 
151+       } } 
152+       content = { 
153+         < form 
154+           ref = { form } 
155+           // ref={formRef} 
156+           noValidate = { true } 
157+           // exclude from the flow 
158+           style = { {  display : "contents"  } } 
159+           onSubmit = { ( event )  =>  { 
160+             event . preventDefault ( ) ; 
161+             if  ( event . currentTarget . checkValidity ( ) )  { 
162+               const  formData  =  new  FormData ( event . currentTarget ) ; 
163+               const  newResource  =  parseResource ( { 
164+                 id : resource ?. id  ??  nanoid ( ) , 
165+                 name : resource ?. name  ??  propName , 
166+                 formData, 
167+               } ) ; 
168+               onChange ( newResource ) ; 
169+             } 
170+           } } 
171+         > 
172+           { /* submit is not triggered when press enter on input without submit button */ } 
173+           < button  hidden > </ button > 
174+           < ResourceForm  resource = { resource }  /> 
175+         </ form > 
176+       } 
177+     > 
178+       < ResourceButton  /> 
179+     </ FloatingPanel > 
180+   ) ; 
181+ } ; 
182+ 
183+ const  $methodPropValue  =  computed ( 
184+   [ $selectedInstance ,  $props ] , 
185+   ( instance ,  props ) : Resource [ "method" ]  =>  { 
186+     for  ( const  prop  of  props . values ( ) )  { 
187+       if  ( 
188+         prop . instanceId  ===  instance ?. id  && 
189+         prop . type  ===  "string"  && 
190+         prop . name  ===  "method" 
191+       )  { 
192+         const  value  =  prop . value . toLowerCase ( ) ; 
193+         if  ( 
194+           value  ===  "get"  || 
195+           value  ===  "post"  || 
196+           value  ===  "put"  || 
197+           value  ===  "delete" 
198+         )  { 
199+           return  value ; 
200+         } 
201+         break ; 
202+       } 
203+     } 
204+     return  "post" ; 
205+   } 
206+ ) ; 
207+ 
208+ export  const  ResourceControl  =  ( { 
27209  instanceId, 
210+   propName, 
211+   prop, 
28212} : ControlProps < "resource" > )  =>  { 
29213  const  resources  =  useStore ( $resources ) ; 
30214  const  {  variableValues,  scope,  aliases }  =  useStore ( 
31215    $selectedInstanceResourceScope 
32216  ) ; 
33- 
34-   let  computedValue :  unknown ; 
35-   let  expression : string  =  JSON . stringify ( "" ) ; 
217+    const   methodPropValue   =   useStore ( $methodPropValue ) ; 
218+   let  resource :  undefined   |   Resource ; 
219+   let  urlExpression : string  =  JSON . stringify ( "" ) ; 
36220  if  ( prop ?. type  ===  "string" )  { 
37-     expression  =  JSON . stringify ( prop . value ) ; 
38-     computedValue  =  prop . value ; 
221+     urlExpression  =  JSON . stringify ( prop . value ) ; 
39222  } 
40223  if  ( prop ?. type  ===  "expression" )  { 
41-     expression  =  prop . value ; 
42-     computedValue  =  computeExpression ( prop . value ,  variableValues ) ; 
224+     urlExpression  =  prop . value ; 
43225  } 
44226  if  ( prop ?. type  ===  "resource" )  { 
45-     const   resource  =  resources . get ( prop . value ) ; 
227+     resource  =  resources . get ( prop . value ) ; 
46228    if  ( resource )  { 
47-       expression  =  resource . url ; 
48-       computedValue  =  computeExpression ( resource . url ,  variableValues ) ; 
229+       urlExpression  =  resource . url ; 
49230    } 
50231  } 
232+   // create temporary resource 
233+   const  resourceId  =  useMemo ( ( )  =>  resource ?. id  ??  nanoid ( ) ,  [ resource ] ) ; 
234+   resource  ??=  { 
235+     id : resourceId , 
236+     name : propName , 
237+     url : urlExpression , 
238+     method : methodPropValue , 
239+     headers : [ {  name : "Content-Type" ,  value : `"application/json"`  } ] , 
240+   } ; 
51241
52-   const  updateResourceUrl  =  ( urlExpression :  string )  =>  { 
242+   const  updateResource  =  ( newResource :  Resource )  =>  { 
53243    updateWebstudioData ( ( data )  =>  { 
54244      if  ( prop ?. type  ===  "resource" )  { 
55-         const  resource  =  data . resources . get ( prop . value ) ; 
56-         if  ( resource )  { 
57-           resource . url  =  urlExpression ; 
58-         } 
245+         data . resources . set ( newResource . id ,  newResource ) ; 
59246      }  else  { 
60-         let  method : Resource [ "method" ]  =  "post" ; 
61-         for  ( const  prop  of  data . props . values ( ) )  { 
62-           if  ( 
63-             prop . instanceId  ===  instanceId  && 
64-             prop . type  ===  "string"  && 
65-             prop . name  ===  "method" 
66-           )  { 
67-             const  value  =  prop . value . toLowerCase ( ) ; 
68-             if  ( 
69-               value  ===  "get"  || 
70-               value  ===  "post"  || 
71-               value  ===  "put"  || 
72-               value  ===  "delete" 
73-             )  { 
74-               method  =  value ; 
75-             } 
76-             break ; 
77-           } 
78-         } 
79- 
80-         const  newResource : Resource  =  { 
81-           id : nanoid ( ) , 
82-           name : propName , 
83-           url : urlExpression , 
84-           method, 
85-           headers : [ {  name : "Content-Type" ,  value : `"application/json"`  } ] , 
86-         } ; 
87247        const  newProp : Prop  =  { 
88248          id : prop ?. id  ??  nanoid ( ) , 
89249          instanceId, 
@@ -98,15 +258,15 @@ export const ResourceControl = ({
98258  } ; 
99259
100260  const  id  =  useId ( ) ; 
101-   const  label  =  humanizeAttribute ( meta . label  ||  propName ) ; 
102261  let  variant : BindingVariant  =  "bound" ; 
103262  let  readOnly  =  true ; 
104-   if  ( isLiteralExpression ( expression ) )  { 
263+   if  ( isLiteralExpression ( urlExpression ) )  { 
105264    variant  =  "default" ; 
106265    readOnly  =  false ; 
107266  } 
108-   const  localValue  =  useLocalValue ( String ( computedValue  ??  "" ) ,  ( value )  => 
109-     updateResourceUrl ( JSON . stringify ( value ) ) 
267+   const  localValue  =  useLocalValue ( 
268+     String ( computeExpression ( resource . url ,  variableValues )  ??  "" ) , 
269+     ( value )  =>  updateResource ( {  ...resource ,  url : JSON . stringify ( value )  } ) 
110270  ) ; 
111271
112272  return  ( 
@@ -121,20 +281,34 @@ export const ResourceControl = ({
121281          onChange = { ( event )  =>  localValue . set ( event . target . value ) } 
122282          onBlur = { localValue . save } 
123283          onSubmit = { localValue . save } 
284+           suffix = { 
285+             isFeatureEnabled ( "resourceProp" )  &&  ( 
286+               < ResourceControlPanel 
287+                 resource = { resource } 
288+                 propName = { propName } 
289+                 onChange = { updateResource } 
290+               /> 
291+             ) 
292+           } 
124293        /> 
125294        < BindingPopover 
126295          scope = { scope } 
127296          aliases = { aliases } 
128297          validate = { ( value )  =>  { 
129298            if  ( value  !==  undefined  &&  typeof  value  !==  "string" )  { 
130-               return  `${ label }  expects a  string value` ; 
299+               return  `Expected URL  string value` ; 
131300            } 
132301          } } 
133302          variant = { variant } 
134-           value = { expression } 
135-           onChange = { ( newExpression )  =>  updateResourceUrl ( newExpression ) } 
303+           value = { urlExpression } 
304+           onChange = { ( newExpression )  => 
305+             updateResource ( {  ...resource ,  url : newExpression  } ) 
306+           } 
136307          onRemove = { ( evaluatedValue )  => 
137-             updateResourceUrl ( JSON . stringify ( String ( evaluatedValue ) ) ) 
308+             updateResource ( { 
309+               ...resource , 
310+               url : JSON . stringify ( String ( evaluatedValue ) ) , 
311+             } ) 
138312          } 
139313        /> 
140314      </ BindingControl > 
0 commit comments