Skip to content

Commit fa8a97c

Browse files
committed
#44: Added support for Action input of type file, textarea and password
1 parent c61e5f2 commit fa8a97c

File tree

4 files changed

+238
-61
lines changed

4 files changed

+238
-61
lines changed

internal/app/action/action.go

Lines changed: 98 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"io/fs"
1313
"net/http"
1414
"net/url"
15+
"os"
1516
"path"
1617
"slices"
1718
"strconv"
@@ -177,7 +178,7 @@ func (a *Action) runAction(w http.ResponseWriter, r *http.Request) {
177178
}
178179
isHtmxRequest := r.Header.Get("HX-Request") == "true"
179180

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

211212
qsParams := url.Values{}
212213

214+
var tempDir string
213215
// Update args with submitted form values
214216
for _, param := range a.params {
215-
formValue := r.Form.Get(param.Name)
216-
if formValue == "" {
217-
if param.Type == starlark_type.BOOLEAN {
218-
// Form does not submit unchecked checkboxes, set to false
219-
args[param.Name] = starlark.Bool(false)
220-
qsParams.Add(param.Name, "false")
217+
if a.hidden[param.Name] {
218+
continue
219+
}
220+
221+
if param.DisplayType == apptype.DisplayTypeFileUpload {
222+
f, fh, err := r.FormFile(param.Name)
223+
if err == http.ErrMissingFile {
224+
args[param.Name] = starlark.String("")
225+
continue
221226
}
222-
} else {
223-
newVal, err := apptype.ParamStringToType(param.Name, param.Type, formValue)
227+
228+
if err != nil {
229+
http.Error(w, fmt.Sprintf("error getting file %s: %s", param.Name, err), http.StatusBadRequest)
230+
return
231+
}
232+
233+
if tempDir == "" {
234+
tempDir, err = os.MkdirTemp("", "clace-file-upload-*")
235+
if err != nil {
236+
http.Error(w, err.Error(), http.StatusInternalServerError)
237+
return
238+
}
239+
240+
defer func() {
241+
if remErr := os.RemoveAll(tempDir); remErr != nil {
242+
a.Error().Err(remErr).Msg("error removing temp dir")
243+
}
244+
}()
245+
}
246+
247+
fullPath := path.Join(tempDir, fh.Filename)
248+
destFile, err := os.Create(fullPath)
224249
if err != nil {
225-
http.Error(w, err.Error(), http.StatusBadRequest)
250+
http.Error(w, err.Error(), http.StatusInternalServerError)
251+
return
252+
}
253+
defer destFile.Close()
254+
255+
// Write contents of uploaded file to destFile
256+
if _, err = io.Copy(destFile, f); err != nil {
257+
http.Error(w, err.Error(), http.StatusInternalServerError)
226258
return
227259
}
228-
args[param.Name] = newVal
229-
qsParams.Add(param.Name, formValue)
260+
args[param.Name] = starlark.String(fullPath)
261+
} else {
262+
// Not file upload, regular param
263+
formValue := r.Form.Get(param.Name)
264+
if formValue == "" {
265+
if param.Type == starlark_type.BOOLEAN {
266+
// Form does not submit unchecked checkboxes, set to false
267+
args[param.Name] = starlark.Bool(false)
268+
qsParams.Add(param.Name, "false")
269+
}
270+
} else {
271+
newVal, err := apptype.ParamStringToType(param.Name, param.Type, formValue)
272+
if err != nil {
273+
http.Error(w, err.Error(), http.StatusBadRequest)
274+
return
275+
}
276+
args[param.Name] = newVal
277+
qsParams.Add(param.Name, formValue)
278+
}
230279
}
231280
}
232281

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

542591
type ParamDef struct {
543-
Name string
544-
Description string
545-
Value any
546-
InputType string
547-
Options []string
592+
Name string
593+
Description string
594+
Value any
595+
InputType string
596+
Options []string
597+
DisplayType string
598+
DisplayTypeOptions string
548599
}
549600

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

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

665+
if p.DisplayType != "" {
666+
switch p.DisplayType {
667+
case apptype.DisplayTypePassword:
668+
param.DisplayType = "password"
669+
case apptype.DisplayTypeTextArea:
670+
param.DisplayType = "textarea"
671+
case apptype.DisplayTypeFileUpload:
672+
param.DisplayType = "file"
673+
hasFileUpload = true
674+
default:
675+
http.Error(w, fmt.Sprintf("invalid display type for %s: %s", p.Name, p.DisplayType), http.StatusInternalServerError)
676+
return
677+
}
678+
param.DisplayTypeOptions = p.DisplayTypeOptions
679+
} else {
680+
param.DisplayType = "text"
681+
}
682+
613683
params = append(params, param)
614684
}
615685

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

626696
input := map[string]any{
627-
"dev": a.isDev,
628-
"name": a.name,
629-
"description": a.description,
630-
"appPath": a.appPath,
631-
"pagePath": a.pagePath,
632-
"params": params,
633-
"styleType": string(a.StyleType),
634-
"lightTheme": a.LightTheme,
635-
"darkTheme": a.DarkTheme,
636-
"links": linksWithQS,
697+
"dev": a.isDev,
698+
"name": a.name,
699+
"description": a.description,
700+
"appPath": a.appPath,
701+
"pagePath": a.pagePath,
702+
"params": params,
703+
"styleType": string(a.StyleType),
704+
"lightTheme": a.LightTheme,
705+
"darkTheme": a.DarkTheme,
706+
"links": linksWithQS,
707+
"hasFileUpload": hasFileUpload,
637708
}
638709
err := a.actionTemplate.ExecuteTemplate(w, "form.go.html", input)
639710
if err != nil {

internal/app/action/form.go.html

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@
3636
</p>
3737

3838
<div class="card w-full shadow-2xl p-6 rounded-lg">
39-
<form method="post">
40-
<!-- Name Field -->
39+
<form
40+
method="post"
41+
{{ if .hasFileUpload }}
42+
enctype="multipart/form-data" hx-encoding="multipart/form-data"
43+
{{ end }}>
4144
{{ range .params }}
4245
<div class="grid grid-cols-2 gap-4 mb-4 items-center">
4346
<label class="label flex-col items-start" for="param_{{ .Name }}">
@@ -77,12 +80,41 @@
7780
</div>
7881
{{ else }}
7982
<div>
80-
<input
81-
id="param_{{ .Name }}"
82-
name="{{ .Name }}"
83-
type="text"
84-
class="input input-bordered w-full"
85-
value="{{ .Value }}" />
83+
{{ if eq .DisplayType "textarea" }}
84+
<textarea
85+
id="param_{{ .Name }}"
86+
name="{{ .Name }}"
87+
class="textarea textarea-primary w-full"
88+
{{ if .DisplayTypeOptions }}
89+
rows="{{ .DisplayTypeOptions }}"
90+
{{ end }}>
91+
{{- .Value -}}</textarea
92+
>
93+
{{ else if eq .DisplayType "password" }}
94+
<input
95+
id="param_{{ .Name }}"
96+
name="{{ .Name }}"
97+
type="password"
98+
class="input input-bordered w-full"
99+
value="{{ .Value }}" />
100+
{{ else if eq .DisplayType "file" }}
101+
<input
102+
id="param_{{ .Name }}"
103+
name="{{ .Name }}"
104+
type="file"
105+
{{ if .DisplayTypeOptions }}
106+
accept="{{ .DisplayTypeOptions }}"
107+
{{ end }}
108+
class="file-input file-input-primary w-full"
109+
value="{{ .Value }}" />
110+
{{ else }}
111+
<input
112+
id="param_{{ .Name }}"
113+
name="{{ .Name }}"
114+
type="{{ .DisplayType }}"
115+
class="input input-bordered w-full"
116+
value="{{ .Value }}" />
117+
{{ end }}
86118
<div id="param_{{ .Name }}_error" class="text-error mt-1"></div>
87119
</div>
88120
{{ end }}

internal/app/apptype/param_loader.go

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"regexp"
1010
"strconv"
11+
"strings"
1112

1213
"github.com/claceio/clace/internal/app/starlark_type"
1314
"go.starlark.net/starlark"
@@ -18,14 +19,24 @@ const (
1819
PARAM = "param"
1920
)
2021

22+
type DisplayType string
23+
24+
const (
25+
DisplayTypePassword DisplayType = "password"
26+
DisplayTypeTextArea DisplayType = "textarea"
27+
DisplayTypeFileUpload DisplayType = "file"
28+
)
29+
2130
// AppParam represents a parameter in an app.
2231
type AppParam struct {
23-
Index int
24-
Name string
25-
Description string
26-
Required bool
27-
Type starlark_type.TypeName
28-
DefaultValue starlark.Value
32+
Index int
33+
Name string
34+
Description string
35+
Required bool
36+
Type starlark_type.TypeName
37+
DefaultValue starlark.Value
38+
DisplayType DisplayType
39+
DisplayTypeOptions string
2940
}
3041

3142
func ReadParamInfo(fileName string, inp []byte) (map[string]AppParam, error) {
@@ -80,6 +91,14 @@ func validateParamInfo(paramInfo map[string]AppParam) error {
8091
default:
8192
return fmt.Errorf("unknown type %s for %s", p.Type, p.Name)
8293
}
94+
95+
if p.DisplayType != "" && p.DisplayType != DisplayTypePassword && p.DisplayType != DisplayTypeTextArea && p.DisplayType != DisplayTypeFileUpload {
96+
return fmt.Errorf("unknown display type %s for %s", p.DisplayType, p.Name)
97+
}
98+
99+
if p.DisplayType != "" && p.Type != starlark_type.STRING {
100+
return fmt.Errorf("display_type %s is allowed for string type %s only", p.DisplayType, p.Name)
101+
}
83102
}
84103
return nil
85104
}
@@ -89,12 +108,12 @@ func LoadParamInfo(fileName string, data []byte) (map[string]AppParam, error) {
89108
index := 0
90109

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

96115
if err := starlark.UnpackArgs(PARAM, args, kwargs, "name", &name, "type?", &dataType, "default?", &defaultValue,
97-
"description?", &description, "required?", &required); err != nil {
116+
"description?", &description, "required?", &required, "display_type?", &displayType); err != nil {
98117
return nil, err
99118
}
100119

@@ -126,34 +145,43 @@ func LoadParamInfo(fileName string, data []byte) (map[string]AppParam, error) {
126145
}
127146
}
128147

148+
dt, dto, _ := strings.Cut(string(displayType), ":")
149+
129150
index += 1
130151
definedParams[string(name)] = AppParam{
131-
Index: index,
132-
Name: string(name),
133-
Type: typeVal,
134-
DefaultValue: defaultValue,
135-
Description: string(description),
136-
Required: bool(required),
152+
Index: index,
153+
Name: string(name),
154+
Type: typeVal,
155+
DefaultValue: defaultValue,
156+
Description: string(description),
157+
Required: bool(required),
158+
DisplayType: DisplayType(dt),
159+
DisplayTypeOptions: dto,
137160
}
138161

139162
paramDict := starlark.StringDict{
140-
"index": starlark.MakeInt(index),
141-
"name": name,
142-
"type": dataType,
143-
"default": defaultValue,
144-
"description": description,
145-
"required": required,
163+
"index": starlark.MakeInt(index),
164+
"name": name,
165+
"type": dataType,
166+
"default": defaultValue,
167+
"description": description,
168+
"required": required,
169+
"display_type": displayType,
170+
"display_type_options": starlark.String(dto),
146171
}
147172
return starlarkstruct.FromStringDict(starlark.String(PARAM), paramDict), nil
148173
}
149174

150175
builtins := starlark.StringDict{
151-
PARAM: starlark.NewBuiltin(PARAM, paramBuiltin),
152-
string(starlark_type.INT): starlark.String(starlark_type.INT),
153-
string(starlark_type.STRING): starlark.String(starlark_type.STRING),
154-
string(starlark_type.BOOLEAN): starlark.String(starlark_type.BOOLEAN),
155-
string(starlark_type.DICT): starlark.String(starlark_type.DICT),
156-
string(starlark_type.LIST): starlark.String(starlark_type.LIST),
176+
PARAM: starlark.NewBuiltin(PARAM, paramBuiltin),
177+
string(starlark_type.INT): starlark.String(starlark_type.INT),
178+
string(starlark_type.STRING): starlark.String(starlark_type.STRING),
179+
string(starlark_type.BOOLEAN): starlark.String(starlark_type.BOOLEAN),
180+
string(starlark_type.DICT): starlark.String(starlark_type.DICT),
181+
string(starlark_type.LIST): starlark.String(starlark_type.LIST),
182+
strings.ToUpper(string(DisplayTypePassword)): starlark.String(DisplayTypePassword),
183+
strings.ToUpper(string(DisplayTypeTextArea)): starlark.String(DisplayTypeTextArea),
184+
strings.ToUpper(string(DisplayTypeFileUpload)): starlark.String(DisplayTypeFileUpload),
157185
}
158186

159187
thread := &starlark.Thread{

0 commit comments

Comments
 (0)