Better Editor Forms #56
|
@ -0,0 +1,5 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
type BinaryStorageInterface interface {
|
||||||
|
Create(name string, file []byte) (*BinaryFile, error)
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type EntryContent string
|
type EntryContent string
|
||||||
|
|
||||||
|
@ -9,18 +11,20 @@ type Entry interface {
|
||||||
Content() EntryContent
|
Content() EntryContent
|
||||||
PublishedAt() *time.Time
|
PublishedAt() *time.Time
|
||||||
AuthorId() string
|
AuthorId() string
|
||||||
MetaData() interface{}
|
MetaData() EntryMetaData
|
||||||
|
|
||||||
// Optional: can return empty string
|
// Optional: can return empty string
|
||||||
Title() string
|
Title() string
|
||||||
|
|
||||||
SetID(id string)
|
SetID(id string)
|
||||||
SetPublishedAt(publishedAt *time.Time)
|
SetPublishedAt(publishedAt *time.Time)
|
||||||
SetMetaData(metaData interface{})
|
SetMetaData(metaData EntryMetaData)
|
||||||
SetAuthorId(authorId string)
|
SetAuthorId(authorId string)
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntryMetaData interface {
|
type EntryMetaData interface {
|
||||||
|
Form(binSvc BinaryStorageInterface) string
|
||||||
|
ParseFormData(data HttpFormData, binSvc BinaryStorageInterface) (EntryMetaData, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntryBase struct {
|
type EntryBase struct {
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "mime/multipart"
|
||||||
|
|
||||||
|
type HttpFormData interface {
|
||||||
|
// FormFile returns the first file by key from a MultipartForm.
|
||||||
|
FormFile(key string) (*multipart.FileHeader, error)
|
||||||
|
// FormValue returns the first value by key from a MultipartForm.
|
||||||
|
// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.
|
||||||
|
// Defaults to the empty string "" if the form value doesn't exist.
|
||||||
|
// If a default value is given, it will return that value if the form value does not exist.
|
||||||
|
// Returned value is only valid within the handler. Do not store any references.
|
||||||
|
// Make copies or use the Immutable setting instead.
|
||||||
|
FormValue(key string, defaultValue ...string) string
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
|
"owl-blogs/web/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Article struct {
|
type Article struct {
|
||||||
|
@ -12,6 +13,7 @@ type Article struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArticleMetaData struct {
|
type ArticleMetaData struct {
|
||||||
|
forms.DefaultForm
|
||||||
Title string `owl:"inputType=text"`
|
Title string `owl:"inputType=text"`
|
||||||
Content string `owl:"inputType=text widget=textarea"`
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
}
|
}
|
||||||
|
@ -28,10 +30,10 @@ func (e *Article) Content() model.EntryContent {
|
||||||
return model.EntryContent(str)
|
return model.EntryContent(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Article) MetaData() interface{} {
|
func (e *Article) MetaData() model.EntryMetaData {
|
||||||
return &e.meta
|
return &e.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Article) SetMetaData(metaData interface{}) {
|
func (e *Article) SetMetaData(metaData model.EntryMetaData) {
|
||||||
e.meta = *metaData.(*ArticleMetaData)
|
e.meta = *metaData.(*ArticleMetaData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
|
"owl-blogs/web/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Bookmark struct {
|
type Bookmark struct {
|
||||||
|
@ -12,6 +13,8 @@ type Bookmark struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type BookmarkMetaData struct {
|
type BookmarkMetaData struct {
|
||||||
|
forms.DefaultForm
|
||||||
|
|
||||||
Title string `owl:"inputType=text"`
|
Title string `owl:"inputType=text"`
|
||||||
Url string `owl:"inputType=text"`
|
Url string `owl:"inputType=text"`
|
||||||
Content string `owl:"inputType=text widget=textarea"`
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
@ -29,10 +32,10 @@ func (e *Bookmark) Content() model.EntryContent {
|
||||||
return model.EntryContent(str)
|
return model.EntryContent(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Bookmark) MetaData() interface{} {
|
func (e *Bookmark) MetaData() model.EntryMetaData {
|
||||||
return &e.meta
|
return &e.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Bookmark) SetMetaData(metaData interface{}) {
|
func (e *Bookmark) SetMetaData(metaData model.EntryMetaData) {
|
||||||
e.meta = *metaData.(*BookmarkMetaData)
|
e.meta = *metaData.(*BookmarkMetaData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
|
"owl-blogs/web/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Image struct {
|
type Image struct {
|
||||||
|
@ -12,6 +13,8 @@ type Image struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImageMetaData struct {
|
type ImageMetaData struct {
|
||||||
|
forms.DefaultForm
|
||||||
|
|
||||||
ImageId string `owl:"inputType=file"`
|
ImageId string `owl:"inputType=file"`
|
||||||
Title string `owl:"inputType=text"`
|
Title string `owl:"inputType=text"`
|
||||||
Content string `owl:"inputType=text widget=textarea"`
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
@ -29,10 +32,10 @@ func (e *Image) Content() model.EntryContent {
|
||||||
return model.EntryContent(str)
|
return model.EntryContent(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Image) MetaData() interface{} {
|
func (e *Image) MetaData() model.EntryMetaData {
|
||||||
return &e.meta
|
return &e.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Image) SetMetaData(metaData interface{}) {
|
func (e *Image) SetMetaData(metaData model.EntryMetaData) {
|
||||||
e.meta = *metaData.(*ImageMetaData)
|
e.meta = *metaData.(*ImageMetaData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
|
"owl-blogs/web/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Note struct {
|
type Note struct {
|
||||||
|
@ -12,6 +13,8 @@ type Note struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type NoteMetaData struct {
|
type NoteMetaData struct {
|
||||||
|
forms.DefaultForm
|
||||||
|
|
||||||
Content string `owl:"inputType=text widget=textarea"`
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,10 +30,10 @@ func (e *Note) Content() model.EntryContent {
|
||||||
return model.EntryContent(str)
|
return model.EntryContent(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Note) MetaData() interface{} {
|
func (e *Note) MetaData() model.EntryMetaData {
|
||||||
return &e.meta
|
return &e.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Note) SetMetaData(metaData interface{}) {
|
func (e *Note) SetMetaData(metaData model.EntryMetaData) {
|
||||||
e.meta = *metaData.(*NoteMetaData)
|
e.meta = *metaData.(*NoteMetaData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
|
"owl-blogs/web/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Page struct {
|
type Page struct {
|
||||||
|
@ -12,6 +13,8 @@ type Page struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type PageMetaData struct {
|
type PageMetaData struct {
|
||||||
|
forms.DefaultForm
|
||||||
|
|
||||||
Title string `owl:"inputType=text"`
|
Title string `owl:"inputType=text"`
|
||||||
Content string `owl:"inputType=text widget=textarea"`
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
}
|
}
|
||||||
|
@ -28,10 +31,10 @@ func (e *Page) Content() model.EntryContent {
|
||||||
return model.EntryContent(str)
|
return model.EntryContent(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Page) MetaData() interface{} {
|
func (e *Page) MetaData() model.EntryMetaData {
|
||||||
return &e.meta
|
return &e.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Page) SetMetaData(metaData interface{}) {
|
func (e *Page) SetMetaData(metaData model.EntryMetaData) {
|
||||||
e.meta = *metaData.(*PageMetaData)
|
e.meta = *metaData.(*PageMetaData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
|
"owl-blogs/web/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Recipe struct {
|
type Recipe struct {
|
||||||
|
@ -12,6 +13,8 @@ type Recipe struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecipeMetaData struct {
|
type RecipeMetaData struct {
|
||||||
|
forms.DefaultForm
|
||||||
|
|
||||||
Title string `owl:"inputType=text"`
|
Title string `owl:"inputType=text"`
|
||||||
Yield string `owl:"inputType=text"`
|
Yield string `owl:"inputType=text"`
|
||||||
Duration string `owl:"inputType=text"`
|
Duration string `owl:"inputType=text"`
|
||||||
|
@ -31,10 +34,10 @@ func (e *Recipe) Content() model.EntryContent {
|
||||||
return model.EntryContent(str)
|
return model.EntryContent(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Recipe) MetaData() interface{} {
|
func (e *Recipe) MetaData() model.EntryMetaData {
|
||||||
return &e.meta
|
return &e.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Recipe) SetMetaData(metaData interface{}) {
|
func (e *Recipe) SetMetaData(metaData model.EntryMetaData) {
|
||||||
e.meta = *metaData.(*RecipeMetaData)
|
e.meta = *metaData.(*RecipeMetaData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/render"
|
"owl-blogs/render"
|
||||||
|
"owl-blogs/web/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Reply struct {
|
type Reply struct {
|
||||||
|
@ -12,6 +13,8 @@ type Reply struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReplyMetaData struct {
|
type ReplyMetaData struct {
|
||||||
|
forms.DefaultForm
|
||||||
|
|
||||||
Title string `owl:"inputType=text"`
|
Title string `owl:"inputType=text"`
|
||||||
Url string `owl:"inputType=text"`
|
Url string `owl:"inputType=text"`
|
||||||
Content string `owl:"inputType=text widget=textarea"`
|
Content string `owl:"inputType=text widget=textarea"`
|
||||||
|
@ -29,10 +32,10 @@ func (e *Reply) Content() model.EntryContent {
|
||||||
return model.EntryContent(str)
|
return model.EntryContent(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Reply) MetaData() interface{} {
|
func (e *Reply) MetaData() model.EntryMetaData {
|
||||||
return &e.meta
|
return &e.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Reply) SetMetaData(metaData interface{}) {
|
func (e *Reply) SetMetaData(metaData model.EntryMetaData) {
|
||||||
e.meta = *metaData.(*ReplyMetaData)
|
e.meta = *metaData.(*ReplyMetaData)
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,7 +151,7 @@ func (r *DefaultEntryRepo) sqlEntryToEntry(entry sqlEntry) (model.Entry, error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("entry type not registered")
|
return nil, errors.New("entry type not registered")
|
||||||
}
|
}
|
||||||
metaData := reflect.New(reflect.TypeOf(e.MetaData()).Elem()).Interface()
|
metaData := reflect.New(reflect.TypeOf(e.MetaData()).Elem()).Interface().(model.EntryMetaData)
|
||||||
json.Unmarshal([]byte(*entry.MetaData), metaData)
|
json.Unmarshal([]byte(*entry.MetaData), metaData)
|
||||||
e.SetID(entry.Id)
|
e.SetID(entry.Id)
|
||||||
e.SetPublishedAt(entry.PublishedAt)
|
e.SetPublishedAt(entry.PublishedAt)
|
||||||
|
|
|
@ -2,10 +2,13 @@ package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"owl-blogs/domain/model"
|
"owl-blogs/domain/model"
|
||||||
|
"owl-blogs/web/forms"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockEntryMetaData struct {
|
type MockEntryMetaData struct {
|
||||||
|
forms.DefaultForm
|
||||||
|
|
||||||
Str string
|
Str string
|
||||||
Number int
|
Number int
|
||||||
Date time.Time
|
Date time.Time
|
||||||
|
@ -21,11 +24,11 @@ func (e *MockEntry) Content() model.EntryContent {
|
||||||
return model.EntryContent(e.metaData.Str)
|
return model.EntryContent(e.metaData.Str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *MockEntry) MetaData() interface{} {
|
func (e *MockEntry) MetaData() model.EntryMetaData {
|
||||||
return e.metaData
|
return e.metaData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *MockEntry) SetMetaData(metaData interface{}) {
|
func (e *MockEntry) SetMetaData(metaData model.EntryMetaData) {
|
||||||
e.metaData = metaData.(*MockEntryMetaData)
|
e.metaData = metaData.(*MockEntryMetaData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
exit status 1
|
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
|
@ -2,27 +2,27 @@ package forms
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"owl-blogs/domain/model"
|
||||||
"owl-blogs/app"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HttpFormData interface {
|
type DefaultForm struct{}
|
||||||
// FormFile returns the first file by key from a MultipartForm.
|
|
||||||
FormFile(key string) (*multipart.FileHeader, error)
|
func (meta *DefaultForm) Form(binSvc model.BinaryStorageInterface) string {
|
||||||
// FormValue returns the first value by key from a MultipartForm.
|
form := NewForm(meta, nil)
|
||||||
// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.
|
htmlForm, _ := form.HtmlForm()
|
||||||
// Defaults to the empty string "" if the form value doesn't exist.
|
return htmlForm
|
||||||
// If a default value is given, it will return that value if the form value does not exist.
|
|
||||||
// Returned value is only valid within the handler. Do not store any references.
|
|
||||||
// Make copies or use the Immutable setting instead.
|
|
||||||
FormValue(key string, defaultValue ...string) string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Form struct {
|
func (meta *DefaultForm) ParseFormData(data model.HttpFormData, binSvc model.BinaryStorageInterface) (model.EntryMetaData, error) {
|
||||||
data interface{}
|
form := NewForm(meta, binSvc)
|
||||||
binSvc *app.BinaryService
|
return form.Parse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Form[T interface{}] struct {
|
||||||
|
data T
|
||||||
|
binSvc model.BinaryStorageInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormFieldParams struct {
|
type FormFieldParams struct {
|
||||||
|
@ -36,8 +36,8 @@ type FormField struct {
|
||||||
Params FormFieldParams
|
Params FormFieldParams
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewForm(data interface{}, binaryService *app.BinaryService) *Form {
|
func NewForm[T interface{}](data T, binaryService model.BinaryStorageInterface) *Form[T] {
|
||||||
return &Form{
|
return &Form[T]{
|
||||||
data: data,
|
data: data,
|
||||||
binSvc: binaryService,
|
binSvc: binaryService,
|
||||||
}
|
}
|
||||||
|
@ -63,8 +63,10 @@ func (s *FormField) ToWidget() Widget {
|
||||||
return &TextListWidget{*s}
|
return &TextListWidget{*s}
|
||||||
case "password":
|
case "password":
|
||||||
return &PasswordWidget{*s}
|
return &PasswordWidget{*s}
|
||||||
default:
|
case "text":
|
||||||
return &TextWidget{*s}
|
return &TextWidget{*s}
|
||||||
|
default:
|
||||||
|
return &OmitWidget{*s}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +120,7 @@ func StructToFormFields(data interface{}) ([]FormField, error) {
|
||||||
return fields, nil
|
return fields, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Form) HtmlForm() (string, error) {
|
func (s *Form[T]) HtmlForm() (string, error) {
|
||||||
fields, err := StructToFormFields(s.data)
|
fields, err := StructToFormFields(s.data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -132,17 +134,19 @@ func (s *Form) HtmlForm() (string, error) {
|
||||||
return html, nil
|
return html, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Form) Parse(ctx HttpFormData) (interface{}, error) {
|
func (s *Form[T]) Parse(ctx model.HttpFormData) (T, error) {
|
||||||
|
var empty T
|
||||||
|
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return nil, fmt.Errorf("nil context")
|
return empty, fmt.Errorf("nil context")
|
||||||
}
|
}
|
||||||
dataVal := reflect.ValueOf(s.data)
|
dataVal := reflect.ValueOf(s.data)
|
||||||
if dataVal.Kind() != reflect.Ptr {
|
if dataVal.Kind() != reflect.Ptr {
|
||||||
return nil, fmt.Errorf("meta data is not a pointer")
|
return empty, fmt.Errorf("meta data is not a pointer")
|
||||||
}
|
}
|
||||||
fields, err := StructToFormFields(s.data)
|
fields, err := StructToFormFields(s.data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return empty, err
|
||||||
}
|
}
|
||||||
for _, field := range fields {
|
for _, field := range fields {
|
||||||
fieldName := field.Name
|
fieldName := field.Name
|
||||||
|
@ -158,22 +162,22 @@ func (s *Form) Parse(ctx HttpFormData) (interface{}, error) {
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, err
|
return empty, err
|
||||||
}
|
}
|
||||||
fileData, err := file.Open()
|
fileData, err := file.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return empty, err
|
||||||
}
|
}
|
||||||
defer fileData.Close()
|
defer fileData.Close()
|
||||||
fileBytes := make([]byte, file.Size)
|
fileBytes := make([]byte, file.Size)
|
||||||
_, err = fileData.Read(fileBytes)
|
_, err = fileData.Read(fileBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return empty, err
|
||||||
}
|
}
|
||||||
|
|
||||||
binaryFile, err := s.binSvc.Create(file.Filename, fileBytes)
|
binaryFile, err := s.binSvc.Create(file.Filename, fileBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return empty, err
|
||||||
}
|
}
|
||||||
|
|
||||||
metaField := dataVal.Elem().FieldByName(fieldName)
|
metaField := dataVal.Elem().FieldByName(fieldName)
|
||||||
|
|
|
@ -26,6 +26,19 @@ func (s *TextWidget) ParseValue(value string, output reflect.Value) error {
|
||||||
return nil
|
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 {
|
type PasswordWidget struct {
|
||||||
FormField
|
FormField
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue