@@ -10,6 +10,7 @@ import (
1010 "html/template"
1111 "io"
1212 "io/fs"
13+ "maps"
1314 "net/http"
1415 "net/url"
1516 "os"
@@ -61,12 +62,13 @@ type Action struct {
6162 containerProxyUrl string
6263 hidden map [string ]bool // params which are not shown in the UI
6364 Links []ActionLink // links to other actions
65+ showValidate bool
6466}
6567
6668// NewAction creates a new action
6769func NewAction (logger * types.Logger , sourceFS * appfs.SourceFs , isDev bool , name , description , apath string , run , suggest starlark.Callable ,
6870 params []apptype.AppParam , paramValuesStr map [string ]string , paramDict starlark.StringDict ,
69- appPath string , styleType types.StyleType , containerProxyUrl string , hidden []string ) (* Action , error ) {
71+ appPath string , styleType types.StyleType , containerProxyUrl string , hidden []string , showValidate bool ) (* Action , error ) {
7072
7173 funcMap := system .GetFuncMap ()
7274
@@ -126,6 +128,7 @@ func NewAction(logger *types.Logger, sourceFS *appfs.SourceFs, isDev bool, name,
126128 StyleType : styleType ,
127129 containerProxyUrl : containerProxyUrl ,
128130 hidden : hiddenParams ,
131+ showValidate : showValidate ,
129132 // Links, AppTemplate and Theme names are initialized later
130133 }, nil
131134}
@@ -158,14 +161,33 @@ func GetEmbeddedTemplates() (map[string][]byte, error) {
158161
159162func (a * Action ) BuildRouter () (* chi.Mux , error ) {
160163 r := chi .NewRouter ()
161- r .Post ("/" , a .runAction )
162164 r .Get ("/" , a .getForm )
165+ r .Post ("/" , a .runAction )
166+ r .Post ("/suggest" , a .suggestAction )
167+ r .Post ("/validate" , a .validateAction )
163168
164169 r .Handle ("/astatic/*" , http .StripPrefix (path .Join (a .pagePath ), hashfs .FileServer (embedFS )))
165170 return r , nil
166171}
167172
168173func (a * Action ) runAction (w http.ResponseWriter , r * http.Request ) {
174+ a .execAction (w , r , false , false )
175+ }
176+
177+ func (a * Action ) suggestAction (w http.ResponseWriter , r * http.Request ) {
178+ a .execAction (w , r , true , false )
179+ }
180+
181+ func (a * Action ) validateAction (w http.ResponseWriter , r * http.Request ) {
182+ a .execAction (w , r , false , true )
183+ }
184+
185+ func (a * Action ) execAction (w http.ResponseWriter , r * http.Request , isSuggest , isValidate bool ) {
186+ if isSuggest && a .suggest == nil {
187+ http .Error (w , "suggest not supported for this action" , http .StatusNotImplemented )
188+ return
189+ }
190+
169191 thread := & starlark.Thread {
170192 Name : a .name ,
171193 Print : func (_ * starlark.Thread , msg string ) { fmt .Println (msg ) },
@@ -180,16 +202,6 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
180202
181203 r .ParseMultipartForm (10 << 20 ) // 10 MB max file size
182204 var err error
183- dryRun := false
184- dryRunStr := r .Form .Get ("dry-run" )
185- if dryRunStr != "" {
186- dryRun , err = strconv .ParseBool (dryRunStr )
187- if err != nil {
188- http .Error (w , fmt .Sprintf ("invalid value for dry-run: %s" , dryRunStr ), http .StatusBadRequest )
189- return
190- }
191- }
192-
193205 deferredCleanup := func () error {
194206 // Check for any deferred cleanups
195207 err = RunDeferredCleanup (thread )
@@ -281,9 +293,16 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
281293
282294 argsValue := Args {members : args }
283295
296+ callable := a .run
297+ callInput := starlark.Tuple {starlark .Bool (isValidate ), & argsValue }
298+ if isSuggest {
299+ callable = a .suggest
300+ callInput = starlark.Tuple {& argsValue }
301+ }
302+
284303 // Call the handler function
285304 var ret starlark.Value
286- ret , err = starlark .Call (thread , a . run , starlark. Tuple { starlark . Bool ( dryRun ), & argsValue } , nil )
305+ ret , err = starlark .Call (thread , callable , callInput , nil )
287306
288307 if err == nil {
289308 pluginErrLocal := thread .Local (types .TL_PLUGIN_API_FAILED_ERROR )
@@ -313,11 +332,16 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
313332 msg = msg + " : " + firstFrame
314333 }
315334
316- // No err handler defined, abort
335+ // err handler is not supported for actions
317336 http .Error (w , msg , http .StatusInternalServerError )
318337 return
319338 }
320339
340+ if isSuggest {
341+ a .handleSuggestResponse (w , ret )
342+ return
343+ }
344+
321345 var valuesMap []map [string ]any
322346 var valuesStr []string
323347 var status string
@@ -423,6 +447,11 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
423447 }
424448 }
425449
450+ if isValidate {
451+ // No need to render the results
452+ return
453+ }
454+
426455 err = a .renderResults (w , report , valuesMap , valuesStr )
427456 if err != nil {
428457 http .Error (w , err .Error (), http .StatusInternalServerError )
@@ -721,9 +750,128 @@ func (a *Action) getForm(w http.ResponseWriter, r *http.Request) {
721750 "darkTheme" : a .DarkTheme ,
722751 "links" : linksWithQS ,
723752 "hasFileUpload" : hasFileUpload ,
753+ "showSuggest" : a .suggest != nil ,
754+ "showValidate" : a .showValidate ,
724755 }
725756 err := a .actionTemplate .ExecuteTemplate (w , "form.go.html" , input )
726757 if err != nil {
727758 http .Error (w , err .Error (), http .StatusInternalServerError )
728759 }
729760}
761+
762+ func (a * Action ) handleSuggestResponse (w http.ResponseWriter , retVal starlark.Value ) {
763+ ret , err := starlark_type .UnmarshalStarlark (retVal )
764+ if err != nil {
765+ http .Error (w , fmt .Sprintf ("error unmarshalling suggest response: %s" , err ), http .StatusInternalServerError )
766+ return
767+ }
768+
769+ message , retIsString := ret .(string )
770+ if ! retIsString {
771+ message = "Suggesting values"
772+ }
773+
774+ err = a .actionTemplate .ExecuteTemplate (w , "status" , message )
775+ if err != nil {
776+ http .Error (w , err .Error (), http .StatusInternalServerError )
777+ return
778+ }
779+
780+ if retIsString {
781+ // No suggestions available
782+ return
783+ }
784+
785+ retDict := map [string ]any {}
786+ switch retType := ret .(type ) {
787+ case map [string ]any :
788+ for k , v := range retType {
789+ retDict [k ] = v
790+ }
791+ case map [string ]string :
792+ for k , v := range retType {
793+ retDict [k ] = v
794+ }
795+ case map [string ]int :
796+ for k , v := range retType {
797+ retDict [k ] = v
798+ }
799+ case map [string ]bool :
800+ for k , v := range retType {
801+ retDict [k ] = v
802+ }
803+ case map [string ][]string :
804+ for k , v := range retType {
805+ retDict [k ] = v
806+ }
807+ default :
808+ http .Error (w , fmt .Sprintf ("invalid suggest response type: %T, expected dict" , retType ), http .StatusInternalServerError )
809+ return
810+ }
811+
812+ paramMap := map [string ]apptype.AppParam {}
813+ for _ , p := range a .params {
814+ paramMap [p .Name ] = p
815+ }
816+
817+ keys := slices .Collect (maps .Keys (retDict ))
818+ slices .Sort (keys )
819+ for _ , key := range keys {
820+ value := retDict [key ]
821+ p , ok := paramMap [key ]
822+ if ! ok || strings .HasPrefix (key , OPTIONS_PREFIX ) {
823+ a .Info ().Msgf ("ignoring suggest response for param: %s" , key )
824+ continue
825+ }
826+ param := ParamDef {
827+ Name : p .Name ,
828+ Description : p .Description ,
829+ }
830+
831+ param .Value = fmt .Sprintf ("%v" , value )
832+ param .InputType = "text"
833+
834+ valueList , valueIsList := value .([]string )
835+ if p .DisplayType == apptype .DisplayTypeFileUpload {
836+ http .Error (w , fmt .Sprintf ("suggest not supported for file upload param: %s" , p .Name ), http .StatusInternalServerError )
837+ return
838+ } else if p .Type == starlark_type .STRING && valueIsList {
839+ param .InputType = "select"
840+ param .Value = valueList [0 ]
841+ param .Options = valueList
842+ } else if p .Type == starlark_type .BOOLEAN {
843+ boolValue , err := strconv .ParseBool (fmt .Sprintf ("%v" , value ))
844+ if err != nil {
845+ http .Error (w , fmt .Sprintf ("invalid value for %s: %s" , p .Name , value ), http .StatusInternalServerError )
846+ return
847+ }
848+ if boolValue {
849+ param .Value = "checked"
850+ }
851+ param .InputType = "checkbox"
852+ }
853+
854+ if p .DisplayType != "" {
855+ switch p .DisplayType {
856+ case apptype .DisplayTypePassword :
857+ param .DisplayType = "password"
858+ case apptype .DisplayTypeTextArea :
859+ param .DisplayType = "textarea"
860+ case apptype .DisplayTypeFileUpload :
861+ param .DisplayType = "file"
862+ default :
863+ http .Error (w , fmt .Sprintf ("invalid display type for %s: %s" , p .Name , p .DisplayType ), http .StatusInternalServerError )
864+ return
865+ }
866+ param .DisplayTypeOptions = p .DisplayTypeOptions
867+ } else {
868+ param .DisplayType = "text"
869+ }
870+
871+ err = a .actionTemplate .ExecuteTemplate (w , "param_suggest" , param )
872+ if err != nil {
873+ http .Error (w , err .Error (), http .StatusInternalServerError )
874+ return
875+ }
876+ }
877+ }
0 commit comments