Skip to content

Commit

Permalink
#44: Added support for Action input of type file, textarea and password
Browse files Browse the repository at this point in the history
  • Loading branch information
akclace committed Nov 27, 2024
1 parent c61e5f2 commit fa8a97c
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 61 deletions.
125 changes: 98 additions & 27 deletions internal/app/action/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"io/fs"
"net/http"
"net/url"
"os"
"path"
"slices"
"strconv"
Expand Down Expand Up @@ -177,7 +178,7 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
}
isHtmxRequest := r.Header.Get("HX-Request") == "true"

r.ParseForm()
r.ParseMultipartForm(10 << 20) // 10 MB max file size
var err error
dryRun := false
dryRunStr := r.Form.Get("dry-run")
Expand Down Expand Up @@ -210,23 +211,71 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {

qsParams := url.Values{}

var tempDir string
// Update args with submitted form values
for _, param := range a.params {
formValue := r.Form.Get(param.Name)
if formValue == "" {
if param.Type == starlark_type.BOOLEAN {
// Form does not submit unchecked checkboxes, set to false
args[param.Name] = starlark.Bool(false)
qsParams.Add(param.Name, "false")
if a.hidden[param.Name] {
continue
}

if param.DisplayType == apptype.DisplayTypeFileUpload {
f, fh, err := r.FormFile(param.Name)
if err == http.ErrMissingFile {
args[param.Name] = starlark.String("")
continue
}
} else {
newVal, err := apptype.ParamStringToType(param.Name, param.Type, formValue)

if err != nil {
http.Error(w, fmt.Sprintf("error getting file %s: %s", param.Name, err), http.StatusBadRequest)
return
}

if tempDir == "" {
tempDir, err = os.MkdirTemp("", "clace-file-upload-*")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

defer func() {
if remErr := os.RemoveAll(tempDir); remErr != nil {
a.Error().Err(remErr).Msg("error removing temp dir")
}
}()
}

fullPath := path.Join(tempDir, fh.Filename)
destFile, err := os.Create(fullPath)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer destFile.Close()

// Write contents of uploaded file to destFile
if _, err = io.Copy(destFile, f); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
args[param.Name] = newVal
qsParams.Add(param.Name, formValue)
args[param.Name] = starlark.String(fullPath)
} else {
// Not file upload, regular param
formValue := r.Form.Get(param.Name)
if formValue == "" {
if param.Type == starlark_type.BOOLEAN {
// Form does not submit unchecked checkboxes, set to false
args[param.Name] = starlark.Bool(false)
qsParams.Add(param.Name, "false")
}
} else {
newVal, err := apptype.ParamStringToType(param.Name, param.Type, formValue)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
args[param.Name] = newVal
qsParams.Add(param.Name, formValue)
}
}
}

Expand Down Expand Up @@ -540,11 +589,13 @@ func RunDeferredCleanup(thread *starlark.Thread) error {
}

type ParamDef struct {
Name string
Description string
Value any
InputType string
Options []string
Name string
Description string
Value any
InputType string
Options []string
DisplayType string
DisplayTypeOptions string
}

const (
Expand All @@ -570,6 +621,7 @@ func (a *Action) getForm(w http.ResponseWriter, r *http.Request) {
}
}

hasFileUpload := false
for _, p := range a.params {
if strings.HasPrefix(p.Name, OPTIONS_PREFIX) || a.hidden[p.Name] {
continue
Expand Down Expand Up @@ -610,6 +662,24 @@ func (a *Action) getForm(w http.ResponseWriter, r *http.Request) {
param.Value = value
}

if p.DisplayType != "" {
switch p.DisplayType {
case apptype.DisplayTypePassword:
param.DisplayType = "password"
case apptype.DisplayTypeTextArea:
param.DisplayType = "textarea"
case apptype.DisplayTypeFileUpload:
param.DisplayType = "file"
hasFileUpload = true
default:
http.Error(w, fmt.Sprintf("invalid display type for %s: %s", p.Name, p.DisplayType), http.StatusInternalServerError)
return
}
param.DisplayTypeOptions = p.DisplayTypeOptions
} else {
param.DisplayType = "text"
}

params = append(params, param)
}

Expand All @@ -624,16 +694,17 @@ func (a *Action) getForm(w http.ResponseWriter, r *http.Request) {
}

input := map[string]any{
"dev": a.isDev,
"name": a.name,
"description": a.description,
"appPath": a.appPath,
"pagePath": a.pagePath,
"params": params,
"styleType": string(a.StyleType),
"lightTheme": a.LightTheme,
"darkTheme": a.DarkTheme,
"links": linksWithQS,
"dev": a.isDev,
"name": a.name,
"description": a.description,
"appPath": a.appPath,
"pagePath": a.pagePath,
"params": params,
"styleType": string(a.StyleType),
"lightTheme": a.LightTheme,
"darkTheme": a.DarkTheme,
"links": linksWithQS,
"hasFileUpload": hasFileUpload,
}
err := a.actionTemplate.ExecuteTemplate(w, "form.go.html", input)
if err != nil {
Expand Down
48 changes: 40 additions & 8 deletions internal/app/action/form.go.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@
</p>

<div class="card w-full shadow-2xl p-6 rounded-lg">
<form method="post">
<!-- Name Field -->
<form
method="post"
{{ if .hasFileUpload }}
enctype="multipart/form-data" hx-encoding="multipart/form-data"
{{ end }}>
{{ range .params }}
<div class="grid grid-cols-2 gap-4 mb-4 items-center">
<label class="label flex-col items-start" for="param_{{ .Name }}">
Expand Down Expand Up @@ -77,12 +80,41 @@
</div>
{{ else }}
<div>
<input
id="param_{{ .Name }}"
name="{{ .Name }}"
type="text"
class="input input-bordered w-full"
value="{{ .Value }}" />
{{ if eq .DisplayType "textarea" }}
<textarea
id="param_{{ .Name }}"
name="{{ .Name }}"
class="textarea textarea-primary w-full"
{{ if .DisplayTypeOptions }}
rows="{{ .DisplayTypeOptions }}"
{{ end }}>
{{- .Value -}}</textarea
>
{{ else if eq .DisplayType "password" }}
<input
id="param_{{ .Name }}"
name="{{ .Name }}"
type="password"
class="input input-bordered w-full"
value="{{ .Value }}" />
{{ else if eq .DisplayType "file" }}
<input
id="param_{{ .Name }}"
name="{{ .Name }}"
type="file"
{{ if .DisplayTypeOptions }}
accept="{{ .DisplayTypeOptions }}"
{{ end }}
class="file-input file-input-primary w-full"
value="{{ .Value }}" />
{{ else }}
<input
id="param_{{ .Name }}"
name="{{ .Name }}"
type="{{ .DisplayType }}"
class="input input-bordered w-full"
value="{{ .Value }}" />
{{ end }}
<div id="param_{{ .Name }}_error" class="text-error mt-1"></div>
</div>
{{ end }}
Expand Down
80 changes: 54 additions & 26 deletions internal/app/apptype/param_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"regexp"
"strconv"
"strings"

"github.com/claceio/clace/internal/app/starlark_type"
"go.starlark.net/starlark"
Expand All @@ -18,14 +19,24 @@ const (
PARAM = "param"
)

type DisplayType string

const (
DisplayTypePassword DisplayType = "password"
DisplayTypeTextArea DisplayType = "textarea"
DisplayTypeFileUpload DisplayType = "file"
)

// AppParam represents a parameter in an app.
type AppParam struct {
Index int
Name string
Description string
Required bool
Type starlark_type.TypeName
DefaultValue starlark.Value
Index int
Name string
Description string
Required bool
Type starlark_type.TypeName
DefaultValue starlark.Value
DisplayType DisplayType
DisplayTypeOptions string
}

func ReadParamInfo(fileName string, inp []byte) (map[string]AppParam, error) {
Expand Down Expand Up @@ -80,6 +91,14 @@ func validateParamInfo(paramInfo map[string]AppParam) error {
default:
return fmt.Errorf("unknown type %s for %s", p.Type, p.Name)
}

if p.DisplayType != "" && p.DisplayType != DisplayTypePassword && p.DisplayType != DisplayTypeTextArea && p.DisplayType != DisplayTypeFileUpload {
return fmt.Errorf("unknown display type %s for %s", p.DisplayType, p.Name)
}

if p.DisplayType != "" && p.Type != starlark_type.STRING {
return fmt.Errorf("display_type %s is allowed for string type %s only", p.DisplayType, p.Name)
}
}
return nil
}
Expand All @@ -89,12 +108,12 @@ func LoadParamInfo(fileName string, data []byte) (map[string]AppParam, error) {
index := 0

paramBuiltin := func(_ *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var name, description, dataType starlark.String
var name, description, dataType, displayType starlark.String
var defaultValue starlark.Value = starlark.None
var required starlark.Bool = starlark.Bool(true)

if err := starlark.UnpackArgs(PARAM, args, kwargs, "name", &name, "type?", &dataType, "default?", &defaultValue,
"description?", &description, "required?", &required); err != nil {
"description?", &description, "required?", &required, "display_type?", &displayType); err != nil {
return nil, err
}

Expand Down Expand Up @@ -126,34 +145,43 @@ func LoadParamInfo(fileName string, data []byte) (map[string]AppParam, error) {
}
}

dt, dto, _ := strings.Cut(string(displayType), ":")

index += 1
definedParams[string(name)] = AppParam{
Index: index,
Name: string(name),
Type: typeVal,
DefaultValue: defaultValue,
Description: string(description),
Required: bool(required),
Index: index,
Name: string(name),
Type: typeVal,
DefaultValue: defaultValue,
Description: string(description),
Required: bool(required),
DisplayType: DisplayType(dt),
DisplayTypeOptions: dto,
}

paramDict := starlark.StringDict{
"index": starlark.MakeInt(index),
"name": name,
"type": dataType,
"default": defaultValue,
"description": description,
"required": required,
"index": starlark.MakeInt(index),
"name": name,
"type": dataType,
"default": defaultValue,
"description": description,
"required": required,
"display_type": displayType,
"display_type_options": starlark.String(dto),
}
return starlarkstruct.FromStringDict(starlark.String(PARAM), paramDict), nil
}

builtins := starlark.StringDict{
PARAM: starlark.NewBuiltin(PARAM, paramBuiltin),
string(starlark_type.INT): starlark.String(starlark_type.INT),
string(starlark_type.STRING): starlark.String(starlark_type.STRING),
string(starlark_type.BOOLEAN): starlark.String(starlark_type.BOOLEAN),
string(starlark_type.DICT): starlark.String(starlark_type.DICT),
string(starlark_type.LIST): starlark.String(starlark_type.LIST),
PARAM: starlark.NewBuiltin(PARAM, paramBuiltin),
string(starlark_type.INT): starlark.String(starlark_type.INT),
string(starlark_type.STRING): starlark.String(starlark_type.STRING),
string(starlark_type.BOOLEAN): starlark.String(starlark_type.BOOLEAN),
string(starlark_type.DICT): starlark.String(starlark_type.DICT),
string(starlark_type.LIST): starlark.String(starlark_type.LIST),
strings.ToUpper(string(DisplayTypePassword)): starlark.String(DisplayTypePassword),
strings.ToUpper(string(DisplayTypeTextArea)): starlark.String(DisplayTypeTextArea),
strings.ToUpper(string(DisplayTypeFileUpload)): starlark.String(DisplayTypeFileUpload),
}

thread := &starlark.Thread{
Expand Down
Loading

0 comments on commit fa8a97c

Please sign in to comment.