Better Editor Forms #56

Merged
h4kor merged 6 commits from better_forms into main 2024-02-21 19:05:08 +00:00
10 changed files with 61 additions and 406 deletions
Showing only changes of commit bd11b88338 - Show all commits

View File

@ -1,19 +1,26 @@
package app package app
import "owl-blogs/domain/model"
type AppConfig interface {
Form(binSvc model.BinaryStorageInterface) string
ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (AppConfig, error)
}
type ConfigRegister struct { type ConfigRegister struct {
configs map[string]interface{} configs map[string]AppConfig
} }
type RegisteredConfig struct { type RegisteredConfig struct {
Name string Name string
Config interface{} Config AppConfig
} }
func NewConfigRegister() *ConfigRegister { func NewConfigRegister() *ConfigRegister {
return &ConfigRegister{configs: map[string]interface{}{}} return &ConfigRegister{configs: map[string]AppConfig{}}
} }
func (r *ConfigRegister) Register(name string, config interface{}) { func (r *ConfigRegister) Register(name string, config AppConfig) {
r.configs[name] = config r.configs[name] = config
} }
@ -28,6 +35,6 @@ func (r *ConfigRegister) Configs() []RegisteredConfig {
return configs return configs
} }
func (r *ConfigRegister) GetConfig(name string) interface{} { func (r *ConfigRegister) GetConfig(name string) AppConfig {
return r.configs[name] return r.configs[name]
} }

View File

@ -77,9 +77,9 @@ func TestEditorFormPost(t *testing.T) {
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile("ImageId", filepath.Base(file.Name())) part, _ := writer.CreateFormFile("image", filepath.Base(file.Name()))
io.Copy(part, file) io.Copy(part, file)
part, _ = writer.CreateFormField("Content") part, _ = writer.CreateFormField("content")
io.WriteString(part, "test content") io.WriteString(part, "test content")
writer.Close() writer.Close()

View File

@ -6,6 +6,7 @@ import (
"owl-blogs/app/repository" "owl-blogs/app/repository"
"owl-blogs/domain/model" "owl-blogs/domain/model"
entrytypes "owl-blogs/entry_types" entrytypes "owl-blogs/entry_types"
"owl-blogs/render"
"github.com/Davincible/goinsta/v3" "github.com/Davincible/goinsta/v3"
) )
@ -20,6 +21,20 @@ type InstagramConfig struct {
Password string `owl:"widget=password"` Password string `owl:"widget=password"`
} }
// Form implements app.AppConfig.
func (cfg *InstagramConfig) Form(binSvc model.BinaryStorageInterface) string {
f, _ := render.RenderTemplateToString("forms/InstagramConfig", cfg)
return f
}
// ParseFormData implements app.AppConfig.
func (*InstagramConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (app.AppConfig, error) {
return &InstagramConfig{
User: data.FormValue("User"),
Password: data.FormValue("Password"),
}, nil
}
func RegisterInstagram( func RegisterInstagram(
configRepo repository.ConfigRepository, configRepo repository.ConfigRepository,
configRegister *app.ConfigRegister, configRegister *app.ConfigRegister,

View File

@ -0,0 +1,8 @@
<label for="PreferredUsername">Preferred Username</label>
<input type="text" name="PreferredUsername" value="{{.PreferredUsername}}" />
<label for="PublicKeyPem">PublicKeyPem</label>
<textarea name="PublicKeyPem" rows="4">{{.PublicKeyPem}}</textarea>
<label for="PrivateKeyPem">PrivateKeyPem</label>
<textarea name="PrivateKeyPem" rows="4">{{.PrivateKeyPem}}</textarea>

View File

@ -0,0 +1,5 @@
<label for="User">User</label>
<input type="text" name="User" value="{{.User}}" />
<label for="Password">Password</label>
<input type="password" name="Password" value="{{.Password}}" />

View File

@ -6,6 +6,7 @@ import (
"owl-blogs/app/repository" "owl-blogs/app/repository"
"owl-blogs/config" "owl-blogs/config"
"owl-blogs/domain/model" "owl-blogs/domain/model"
"owl-blogs/render"
vocab "github.com/go-ap/activitypub" vocab "github.com/go-ap/activitypub"
@ -25,6 +26,21 @@ type ActivityPubConfig struct {
PrivateKeyPem string `owl:"inputType=text widget=textarea"` PrivateKeyPem string `owl:"inputType=text widget=textarea"`
} }
// Form implements app.AppConfig.
func (cfg *ActivityPubConfig) Form(binSvc model.BinaryStorageInterface) string {
f, _ := render.RenderTemplateToString("forms/ActivityPubConfig", cfg)
return f
}
// ParseFormData implements app.AppConfig.
func (*ActivityPubConfig) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (app.AppConfig, error) {
return &ActivityPubConfig{
PreferredUsername: data.FormValue("PreferredUsername"),
PublicKeyPem: data.FormValue("PublicKeyPem"),
PrivateKeyPem: data.FormValue("PrivateKeyPem"),
}, nil
}
type WebfingerResponse struct { type WebfingerResponse struct {
Subject string `json:"subject"` Subject string `json:"subject"`
Links []WebfingerLink `json:"links"` Links []WebfingerLink `json:"links"`

View File

@ -4,7 +4,6 @@ import (
"owl-blogs/app" "owl-blogs/app"
"owl-blogs/app/repository" "owl-blogs/app/repository"
"owl-blogs/render" "owl-blogs/render"
"owl-blogs/web/forms"
"sort" "sort"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -14,6 +13,7 @@ type adminHandler struct {
configRepo repository.ConfigRepository configRepo repository.ConfigRepository
configRegister *app.ConfigRegister configRegister *app.ConfigRegister
typeRegistry *app.EntryTypeRegistry typeRegistry *app.EntryTypeRegistry
binSvc *app.BinaryService
} }
type adminContet struct { type adminContet struct {
@ -75,8 +75,7 @@ func (h *adminHandler) HandleConfigGet(c *fiber.Ctx) error {
} }
siteConfig := getSiteConfig(h.configRepo) siteConfig := getSiteConfig(h.configRepo)
form := forms.NewForm(config, nil) htmlForm := config.Form(h.binSvc)
htmlForm, err := form.HtmlForm()
if err != nil { if err != nil {
return err return err
} }
@ -93,9 +92,7 @@ func (h *adminHandler) HandleConfigPost(c *fiber.Ctx) error {
return c.SendStatus(404) return c.SendStatus(404)
} }
form := forms.NewForm(config, nil) newConfig, err := config.ParseFormData(c, h.binSvc)
newConfig, err := form.Parse(c)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,185 +0,0 @@
package forms
import (
"fmt"
"owl-blogs/domain/model"
"reflect"
"strings"
)
type Form[T interface{}] struct {
data T
binSvc model.BinaryStorageInterface
}
type FormFieldParams struct {
InputType string
Widget string
}
type FormField struct {
Name string
Value reflect.Value
Params FormFieldParams
}
func NewForm[T interface{}](data T, binaryService model.BinaryStorageInterface) *Form[T] {
return &Form[T]{
data: data,
binSvc: binaryService,
}
}
func (s *FormFieldParams) ApplyTag(tagKey string, tagValue string) error {
switch tagKey {
case "inputType":
s.InputType = tagValue
case "widget":
s.Widget = tagValue
default:
return fmt.Errorf("unknown tag key: %v", tagKey)
}
return nil
}
func (s *FormField) ToWidget() Widget {
switch s.Params.Widget {
case "textarea":
return &TextareaWidget{*s}
case "textlist":
return &TextListWidget{*s}
case "password":
return &PasswordWidget{*s}
case "text":
return &TextWidget{*s}
default:
return &OmitWidget{*s}
}
}
func (s *FormField) Html() string {
html := ""
html += fmt.Sprintf("<label for=\"%v\">%v</label>\n", s.Name, s.Name)
if s.Params.InputType == "file" {
html += fmt.Sprintf("<input type=\"%v\" name=\"%v\" id=\"%v\" value=\"%v\" />\n", s.Params.InputType, s.Name, s.Name, s.Value)
} else {
html += s.ToWidget().Html()
html += "\n"
}
return html
}
func FieldToFormField(field reflect.StructField, value reflect.Value) (FormField, error) {
formField := FormField{
Name: field.Name,
Value: value,
Params: FormFieldParams{},
}
tag := field.Tag.Get("owl")
for _, param := range strings.Split(tag, " ") {
parts := strings.Split(param, "=")
if len(parts) != 2 {
continue
}
err := formField.Params.ApplyTag(parts[0], parts[1])
if err != nil {
return FormField{}, err
}
}
return formField, nil
}
func StructToFormFields(data interface{}) ([]FormField, error) {
dataValue := reflect.Indirect(reflect.ValueOf(data))
dataType := reflect.TypeOf(data).Elem()
numFields := dataType.NumField()
fields := []FormField{}
for i := 0; i < numFields; i++ {
field, err := FieldToFormField(
dataType.Field(i),
dataValue.FieldByIndex([]int{i}),
)
if err != nil {
return nil, err
}
fields = append(fields, field)
}
return fields, nil
}
func (s *Form[T]) HtmlForm() (string, error) {
fields, err := StructToFormFields(s.data)
if err != nil {
return "", err
}
html := ""
for _, field := range fields {
html += field.Html()
}
return html, nil
}
func (s *Form[T]) Parse(ctx model.HttpFormData) (T, error) {
var empty T
if ctx == nil {
return empty, fmt.Errorf("nil context")
}
dataVal := reflect.ValueOf(s.data)
if dataVal.Kind() != reflect.Ptr {
return empty, fmt.Errorf("meta data is not a pointer")
}
fields, err := StructToFormFields(s.data)
if err != nil {
return empty, err
}
for _, field := range fields {
fieldName := field.Name
if field.Params.InputType == "file" {
file, err := ctx.FormFile(fieldName)
if err != nil {
// If field already has a value, we can ignore the error
if field.Value != reflect.Zero(field.Value.Type()) {
metaField := dataVal.Elem().FieldByName(fieldName)
if metaField.IsValid() {
metaField.SetString(field.Value.String())
}
continue
}
return empty, err
}
fileData, err := file.Open()
if err != nil {
return empty, err
}
defer fileData.Close()
fileBytes := make([]byte, file.Size)
_, err = fileData.Read(fileBytes)
if err != nil {
return empty, err
}
binaryFile, err := s.binSvc.Create(file.Filename, fileBytes)
if err != nil {
return empty, err
}
metaField := dataVal.Elem().FieldByName(fieldName)
if metaField.IsValid() {
metaField.SetString(binaryFile.Id)
}
} else {
formValue := ctx.FormValue(fieldName)
metaField := dataVal.Elem().FieldByName(fieldName)
if metaField.IsValid() {
field.ToWidget().ParseValue(formValue, metaField)
}
}
}
return s.data, nil
}

View File

@ -1,108 +0,0 @@
package forms_test
import (
"bytes"
"io"
"mime/multipart"
"os"
"owl-blogs/app"
"owl-blogs/infra"
"owl-blogs/test"
"owl-blogs/web/forms"
"path"
"path/filepath"
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
type MockData struct {
Image string `owl:"inputType=file"`
Content string `owl:"inputType=text"`
}
type MockFormData struct {
fileHeader *multipart.FileHeader
}
func NewMockFormData() *MockFormData {
fileDir, _ := os.Getwd()
fileName := "../../test/fixtures/test.png"
filePath := path.Join(fileDir, fileName)
file, err := os.Open(filePath)
if err != nil {
panic(err)
}
defer file.Close()
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("ImagePath", filepath.Base(file.Name()))
if err != nil {
panic(err)
}
io.Copy(part, file)
writer.Close()
multipartForm := multipart.NewReader(body, writer.Boundary())
formData, err := multipartForm.ReadForm(0)
if err != nil {
panic(err)
}
fileHeader := formData.File["ImagePath"][0]
return &MockFormData{fileHeader: fileHeader}
}
func (f *MockFormData) FormFile(key string) (*multipart.FileHeader, error) {
return f.fileHeader, nil
}
func (f *MockFormData) FormValue(key string, defaultValue ...string) string {
return key
}
func TestFieldToFormField(t *testing.T) {
field := reflect.TypeOf(&MockData{}).Elem().Field(0)
formField, err := forms.FieldToFormField(field, reflect.Value{})
require.NoError(t, err)
require.Equal(t, "Image", formField.Name)
require.Equal(t, "file", formField.Params.InputType)
}
func TestStructToFields(t *testing.T) {
fields, err := forms.StructToFormFields(&MockData{})
require.NoError(t, err)
require.Len(t, fields, 2)
require.Equal(t, "Image", fields[0].Name)
require.Equal(t, "file", fields[0].Params.InputType)
require.Equal(t, "Content", fields[1].Name)
require.Equal(t, "text", fields[1].Params.InputType)
}
func TestForm_HtmlForm(t *testing.T) {
form := forms.NewForm(&MockData{}, nil)
html, err := form.HtmlForm()
require.NoError(t, err)
require.Contains(t, html, "<input type=\"file\" name=\"Image\"")
require.Contains(t, html, "<input type=\"text\" name=\"Content\"")
}
func TestFormParseNil(t *testing.T) {
form := forms.NewForm(&MockData{}, nil)
_, err := form.Parse(nil)
require.Error(t, err)
}
func TestFormParse(t *testing.T) {
binRepo := infra.NewBinaryFileRepo(test.NewMockDb())
binService := app.NewBinaryFileService(binRepo)
form := forms.NewForm(&MockData{}, binService)
data, err := form.Parse(NewMockFormData())
require.NoError(t, err)
require.NotZero(t, data.Image)
require.Equal(t, "Content", data.Content)
}

View File

@ -1,100 +0,0 @@
package forms
import (
"fmt"
"reflect"
"strings"
)
type Widget interface {
Html() string
ParseValue(value string, output reflect.Value) error
}
type TextWidget struct {
FormField
}
func (s *TextWidget) Html() string {
html := ""
html += fmt.Sprintf("<input type=\"text\" name=\"%v\" value=\"%v\">\n", s.Name, s.Value.String())
return html
}
func (s *TextWidget) ParseValue(value string, output reflect.Value) error {
output.SetString(value)
return nil
}
type OmitWidget struct {
FormField
}
func (s *OmitWidget) Html() string {
html := ""
return html
}
func (s *OmitWidget) ParseValue(value string, output reflect.Value) error {
return nil
}
type PasswordWidget struct {
FormField
}
func (s *PasswordWidget) Html() string {
html := ""
html += fmt.Sprintf("<input type=\"password\" name=\"%v\" value=\"%v\">\n", s.Name, s.Value.String())
return html
}
func (s *PasswordWidget) ParseValue(value string, output reflect.Value) error {
output.SetString(value)
return nil
}
type TextareaWidget struct {
FormField
}
func (s *TextareaWidget) Html() string {
html := ""
html += fmt.Sprintf("<textarea name=\"%v\" rows=\"20\">%v</textarea>\n", s.Name, s.Value.String())
return html
}
func (s *TextareaWidget) ParseValue(value string, output reflect.Value) error {
output.SetString(value)
return nil
}
type TextListWidget struct {
FormField
}
func (s *TextListWidget) Html() string {
valueList := s.Value.Interface().([]string)
value := strings.Join(valueList, "\n")
html := ""
html += fmt.Sprintf("<textarea name=\"%v\" rows=\"20\">%v</textarea>\n", s.Name, value)
return html
}
func (s *TextListWidget) ParseValue(value string, output reflect.Value) error {
list := strings.Split(value, "\n")
// trim entries
for i, item := range list {
list[i] = strings.TrimSpace(item)
}
// remove empty entries
for i := len(list) - 1; i >= 0; i-- {
if list[i] == "" {
list = append(list[:i], list[i+1:]...)
}
}
output.Set(reflect.ValueOf(list))
return nil
}