8000 presets: add LazyWrapComponentFunc and LazyWrapSetterFunc by molon · Pull Request #553 · qor5/admin · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
8000

presets: add LazyWrapComponentFunc and LazyWrapSetterFunc #553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs/docsrc/examples/examples_presets/editing.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,21 @@ func PresetsEditingCustomizationDescription(b *presets.Builder, db *gorm.DB) (

mb, cl, ce, _ = PresetsListingCustomizationBulkActions(b, db)

ce.Only("Name", "CompanyID", "Description")
ce.Only("Name", "Email", "CompanyID", "Description")

ce.Field("Description").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
return richeditor.RichEditor(db, "Body").Plugins([]string{"alignment", "video", "imageinsert", "fontcolor"}).Value(obj.(*Customer).Description).Label(field.Label)
})

// If you just want to specify the label to be displayed
wrapper := presets.WrapperFieldLabel(func(evCtx *web.EventContext, obj interface{}, field *presets.FieldContext) (name2label map[string]string, err error) {
return map[string]string{
"Name": "Customer Name",
"Email": "Customer Email",
}, nil
})
ce.Field("Name").LazyWrapComponentFunc(wrapper)
ce.Field("Email").LazyWrapComponentFunc(wrapper)
return
}

Expand Down Expand Up @@ -207,6 +217,15 @@ func PresetsEditingSetter(b *presets.Builder, db *gorm.DB) (
}
return
})
eb.Field("Name").LazyWrapSetterFunc(func(in presets.FieldSetterFunc) presets.FieldSetterFunc {
return func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) (err error) {
c := obj.(*Company)
if c.Name == "system" {
return errors.New(`You can not use "system" as name`)
}
return in(obj, field, ctx)
}
})

return
}
36 changes: 36 additions & 0 deletions docs/docsrc/examples/examples_presets/editing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,42 @@ func TestPresetsEditingSetter(t *testing.T) {
},
ExpectPortalUpdate0ContainsInOrder: []string{"name must not be empty"},
},
{
Name: "wrap setter",
Debug: true,
ReqFunc: func() *http.Request {
companyData.TruncatePut(SqlDB)
return multipartestutils.NewMultipartBuilder().
PageURL("/companies?__execute_event__=presets_Update").
AddField("Name", "system").
BuildEventFuncRequest()
},
ExpectPortalUpdate0ContainsInOrder: []string{`You can not use \"system\" as name`},
},
}

for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
multipartestutils.RunCase(t, c, pb)
})
}
}

func TestPresetsEditingCustomizationDescription(t *testing.T) {
pb := presets.New().DataOperator(gorm2op.DataOperator(TestDB))
PresetsEditingCustomizationDescription(pb, TestDB)

cases := []multipartestutils.TestCase{
{
Name: "new",
Debug: true,
ReqFunc: func() *http.Request {
return multipartestutils.NewMultipartBuilder().
PageURL("/customers?__execute_event__=presets_New").
BuildEventFuncRequest()
},
ExpectPortalUpdate0ContainsInOrder: []string{`Customer Name`, `Customer Email`, `Description`, `redactor`},
},
}

for _, c := range cases {
Expand Down
37 changes: 8 additions & 29 deletions pagebuilder/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,37 +86,16 @@ func (b *Builder) defaultTemplateInstall(pb *presets.Builder, pm *presets.ModelB
}
return
})
creating.Field("Name").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages)
return h.Div(
h.Span(msgr.Name).Class("text-subtitle-2 text-high-emphasis section-filed-label mb-1 d-sm-inline-block"),
VTextField().
Density(DensityComfortable).
Class("section-field").
Type("text").
Variant(FieldVariantOutlined).
BgColor(ColorBackground).
Attr(web.VField(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)))...).
ErrorMessages(field.Errors...).
Disabled(field.Disabled),
).Class("section-field-wrap")
wrapper := presets.WrapperFieldLabel(func(evCtx *web.EventContext, obj interface{}, field *presets.FieldContext) (name2label map[string]string, err error) {
msgr := i18n.MustGetModuleMessages(evCtx.R, I18nPageBuilderKey, Messages_en_US).(*Messages)
return map[string]string{
"Name": msgr.Name,
"Description": msgr.Description,
}, nil
})
creating.Field("Description").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent {
msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages)
creating.Field("Name").LazyWrapComponentFunc(wrapper)
creating.Field("Description").LazyWrapComponentFunc(wrapper)

return h.Div(
h.Span(msgr.Description).Class("text-subtitle-2 text-high-emphasis section-filed-label mb-1 d-sm-inline-block"),
VTextField().
Density(DensityComfortable).
Class("section-field").
Type("text").
Variant(FieldVariantOutlined).
BgColor(ColorBackground).
Attr(web.VField(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)))...).
ErrorMessages(field.Errors...).
Disabled(field.Disabled),
).Class("section-field-wrap")
})
b.templateModel = template
b.RegisterModelBuilderTemplate(pm, template)

Expand Down
80 changes: 60 additions & 20 deletions presets/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
v "github.com/qor5/x/v3/ui/vuetify"
"github.com/sunfmin/reflectutils"
h "github.com/theplant/htmlgo"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

type FieldContext struct {
Expand Down Expand Up @@ -60,10 +62,21 @@
return fc.Context.Value(key)
}

type FieldsBuilder struct {
model interface{}
defaults *FieldDefaults
fieldLabels []string
fields []*FieldBuilder
// string / []string / *FieldsSection
fieldsLayout []interface{}
}

type FieldBuilder struct {
NameLabel
compFunc FieldComponentFunc
lazyWrapCompFunc func(in FieldComponentFunc) FieldComponentFunc
setterFunc FieldSetterFunc
lazyWrapSetterFunc func(in FieldSetterFunc) FieldSetterFunc
context context.Context
rt reflect.Type
nestedFieldsBuilder *FieldsBuilder
Expand Down Expand Up @@ -109,7 +122,9 @@
r.name = b.name
r.label = b.label
r.compFunc = b.compFunc
r.lazyWrapCompFunc = b.lazyWrapCompFunc
r.setterFunc = b.setterFunc
r.lazyWrapSetterFunc = b.lazyWrapSetterFunc
r.nestedFieldsBuilder = b.nestedFieldsBuilder
r.context = b.context
r.rt = b.rt
Expand All @@ -125,11 +140,51 @@
return b
}

// WrapperFieldLabel a snippet for LazyWrapComponentFunc
func WrapperFieldLabel(mapper func(evCtx *web.EventContext, obj interface{}, field *FieldContext) (name2label map[string]string, err error)) func(in FieldComponentFunc) FieldComponentFunc {
return func(in FieldComponentFunc) FieldComponentFunc {
return func(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent {
m, err := mapper(ctx, obj, field)
if err != nil {
panic(err)

Check warning on line 149 in presets/field.go

View check run for this annotation

Codecov / codecov/patch

presets/field.go#L149

Added line #L149 was not covered by tests
}
if label, ok := m[field.Name]; ok {
field.Label = label
}
return in(obj, field, ctx)
}
}
}

func (b *FieldBuilder) LazyWrapComponentFunc(w func(in FieldComponentFunc) FieldComponentFunc) (r *FieldBuilder) {
b.lazyWrapCompFunc = w
return b
}

func (b *FieldBuilder) lazyCompFunc() FieldComponentFunc {
if b.lazyWrapCompFunc == nil {
return b.compFunc
}
return b.lazyWrapCompFunc(b.compFunc)
}

func (b *FieldBuilder) SetterFunc(v FieldSetterFunc) (r *FieldBuilder) {
b.setterFunc = v
return b
}

func (b *FieldBuilder) LazyWrapSetterFunc(w func(in FieldSetterFunc) FieldSetterFunc) (r *FieldBuilder) {
b.lazyWrapSetterFunc = w
return b
}

func (b *FieldBuilder) lazySetterFunc() FieldSetterFunc {
if b.lazyWrapSetterFunc == nil {
return b.setterFunc
}
return b.lazyWrapSetterFunc(b.setterFunc)
}

func (b *FieldBuilder) WithContextValue(key interface{}, val interface{}) (r *FieldBuilder) {
if b.context == nil {
b.context = context.Background()
Expand Down Expand Up @@ -218,15 +273,6 @@
label string
}

type FieldsBuilder struct {
model interface{}
defaults 9E81 *FieldDefaults
fieldLabels []string
fields []*FieldBuilder
// string / []string / *FieldsSection
fieldsLayout []interface{}
}

func CloneFieldsLayout(layout []interface{}) (r []interface{}) {
r = make([]interface{}, len(layout))
for i, v := range layout {
Expand Down Expand Up @@ -266,78 +312,78 @@
}
return
}

func (b *FieldsBuilder) Defaults(v *FieldDefaults) (r *FieldsBuilder) {
b.defaults = v
return b
}

func (b *FieldsBuilder) Unmarshal(toObj interface{}, info *ModelInfo, removeDeletedAndSort bool, ctx *web.EventContext) (vErr web.ValidationErrors) {
t := reflect.TypeOf(toObj)
if t.Kind() != reflect.Ptr {
panic("toObj must be pointer")
}

fromObj := reflect.New(t.Elem()).Interface()
// don't panic for fields that set in SetterFunc
_ = ctx.UnmarshalForm(fromObj)
// testingutils.PrintlnJson("Unmarshal fromObj", fromObj)

modifiedIndexes := ContextModifiedIndexesBuilder(ctx).FromHidden(ctx.R)

return b.SetObjectFields(fromObj, toObj, &FieldContext{
ModelInfo: info,
}, removeDeletedAndSort, modifiedIndexes, ctx)
}

func (b *FieldsBuilder) SetObjectFields(fromObj interface{}, toObj interface{}, parent *FieldContext, removeDeletedAndSort bool, modifiedIndexes *ModifiedIndexesBuilder, ctx *web.EventContext) (vErr web.ValidationErrors) {
for _, f := range b.fields {
info := parent.ModelInfo
if info != nil {
if info.Verifier().Do(PermCreate).ObjectOn(toObj).SnakeOn("f_"+f.name).WithReq(ctx.R).IsAllowed() != nil && info.Verifier().Do(PermUpdate).ObjectOn(toObj).SnakeOn("f_"+f.name).WithReq(ctx.R).IsAllowed() != nil {
continue
}
}

if f.nestedFieldsBuilder != nil {
formKey := f.name
if parent != nil && parent.FormKey != "" {
formKey = fmt.Sprintf("%s.%s", parent.FormKey, f.name)
}
switch f.rt.Kind() {
case reflect.Slice:
b.setWithChildFromObjs(fromObj, formKey, f, info, modifiedIndexes, toObj, removeDeletedAndSort, ctx)
b.setToObjNilOrDelete(toObj, formKey, f, modifiedIndexes, removeDeletedAndSort)
continue
default:
pf := &FieldContext{
ModelInfo: info,
FormKey: formKey,
}
rt := reflectutils.GetType(toObj, f.name)
childFromObj := reflectutils.MustGet(fromObj, f.name)
if childFromObj == nil {
childFromObj = reflect.New(rt.Elem()).Interface()
}
childToObj := reflectutils.MustGet(toObj, f.name)
if childToObj == nil {
childToObj = reflect.New(rt.Elem()).Interface()
}
if rt.Kind() == reflect.Struct {
prv := reflect.New(rt)
prv.Elem().Set(reflect.ValueOf(childToObj))
childToObj = prv.Interface()
}
f.nestedFieldsBuilder.SetObjectFields(childFromObj, childToObj, pf, removeDeletedAndSort, modifiedIndexes, ctx)
if err := reflectutils.Set(toObj, f.name, childToObj); err != nil {
panic(err)
}
continue
}
}

val, err1 := reflectutils.Get(fromObj, f.name)
if err1 == nil {

Check notice on line 386 in presets/field.go

View check run for this annotation

codefactor.io / CodeFactor

presets/field.go#L315-L386

Complex Method
reflectutils.Set(toObj, f.name, val)
}

Expand All @@ -349,7 +395,7 @@
if parent != nil && parent.FormKey != "" {
keyPath = fmt.Sprintf("%s.%s", parent.FormKey, f.name)
}
err1 = f.setterFunc(toObj, &FieldContext{
err1 = f.lazySetterFunc()(toObj, &FieldContext{
ModelInfo: info,
FormKey: keyPath,
Name: f.name,
Expand Down Expand Up @@ -395,8 +441,6 @@
if err != nil {
panic(err)
}

return
}

func (b *FieldsBuilder) setWithChildFromObjs(
Expand Down Expand Up @@ -498,7 +542,7 @@
}
human = append(human, l)
}
return strings.Title(string(human))
return cases.Title(language.Und, cases.NoLower).String(string(human))
}

func (b *FieldsBuilder) getLabel(field NameLabel) (r string) {
Expand Down Expand Up @@ -552,14 +596,10 @@
case string:
ns = append(ns, t)
case []string:
for _, n := range t {
ns = append(ns, n)
}
ns = append(ns, t...)
case *FieldsSection:
for _, row := range t.Rows {
for _, n := range row {
ns = append(ns, n)
}
ns = append(ns, row...)
}
default:
panic("unknown fields layout, must be string/[]string/*FieldsSection")
Expand Down Expand Up @@ -595,104 +635,104 @@
}
}

func (b *FieldsBuilder) Except(patterns ...string) (r *FieldsBuilder) {
if len(patterns) == 0 {
return b
}

r = b.Clone()

if len(b.fieldsLayout) == 0 {
for _, f := range b.fields {
if hasMatched(patterns, f.name) {
continue
}
r.appendFieldAfterClone(b, f.name)
}
return r
}

fieldsLayout := []any{}
for _, iv := range b.fieldsLayout {
switch t := iv.(type) {
case string:
if !hasMatched(patterns, t) {
fieldsLayout = append(fieldsLayout, t)
}
case []string:
ns := []string{}
for _, n := range t {
if !hasMatched(patterns, n) {
ns = append(ns, n)
}
}
if len(ns) > 0 {
fieldsLayout = append(fieldsLayout, ns)
}
case *FieldsSection:
section := &FieldsSection{
Title: t.Title,
Rows: [][]string{},
}
for _, row := range t.Rows {
ns := []string{}
for _, n := range row {
if !hasMatched(patterns, n) {
ns = append(ns, n)
}
}
if len(ns) > 0 {
section.Rows = append(section.Rows, ns)
}
}
if len(section.Rows) > 0 {
fieldsLayout = append(fieldsLayout, section)
}
default:
panic("unknown fields layout, must be string/[]string/*FieldsSection")
}
}
r.fieldsLayout = fieldsLayout
for _, fn := range r.getFieldNamesFromLayout() {
r.appendFieldAfterClone(b, fn)
}
return
}

Check notice on line 701 in presets/field.go

View check run for this annotation

codefactor.io / CodeFactor

presets/field.go#L638-L701

Complex Method
func (b *FieldsBuilder) String() (r string) {
var names []string
for _, f := range b.fields {
names = append(names, f.name)
}
return fmt.Sprint(names)
}

func (b *FieldsBuilder) ToComponent(info *ModelInfo, obj interface{}, ctx *web.EventContext) h.HTMLComponent {
return b.toComponentWithModifiedIndexes(info, obj, "", ctx)
}

func (b *FieldsBuilder) toComponentWithModifiedIndexes(info *ModelInfo, obj interface{}, parentFormValueKey string, ctx *web.EventContext) h.HTMLComponent {
modifiedIndexes := ContextModifiedIndexesBuilder(ctx)
return b.toComponentWithFormValueKey(info, obj, parentFormValueKey, modifiedIndexes, ctx)
}

func (b *FieldsBuilder) toComponentWithFormValueKey(info *ModelInfo, obj interface{}, parentFormValueKey string, modifiedIndexes *ModifiedIndexesBuilder, ctx *web.EventContext) h.HTMLComponent {
var comps []h.HTMLComponent
if parentFormValueKey == "" {
comps = append(comps, modifiedIndexes.ToFormHidden())
}

vErr, _ := ctx.Flash.(*web.ValidationErrors)
if vErr == nil {
vErr = &web.ValidationErrors{}
}

id := ObjectID(obj)
edit := id != ""

var layout []interface{}
if b.fieldsLayout == nil {
layout = make([]interface{}, 0, len(b.fields))

Check notice on line 735 in presets/field.go

View check run for this annotation

codefactor.io / CodeFactor

presets/field.go#L647-L735

Complex Method
for _, f := range b.fields {
layout = append(layout, f.name)
}
Expand Down Expand Up @@ -789,7 +829,7 @@
disabled = info.Verifier().Do(PermCreate).ObjectOn(obj).SnakeOn("f_"+f.name).WithReq(ctx.R).IsAllowed() != nil
}
}
return f.compFunc(obj, &FieldContext{
return f.lazyCompFunc()(obj, &FieldContext{
ModelInfo: info,
Name: f.name,
FormKey: contextKeyPath,
Expand Down
38 changes: 19 additions & 19 deletions presets/listing_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"github.com/qor5/web/v3"
"github.com/qor5/web/v3/stateful"
"github.com/qor5/x/v3/perm"
. "github.com/qor5/x/v3/ui/vuetify"
v "github.com/qor5/x/v3/ui/vuetify"
vx "github.com/qor5/x/v3/ui/vuetifyx"
"github.com/samber/lo"
h "github.com/theplant/htmlgo"
Expand Down Expand Up @@ -114,7 +114,7 @@
return b
}

// Deprecated: Use WrapCell instead.
// CellWrapperFunc Deprecated: Use WrapCell instead.
func (b *ListingBuilder) CellWrapperFunc(cwf vx.CellWrapperFunc) (r *ListingBuilder) {
b.cellWrapperFunc = cwf
return b
Expand Down Expand Up @@ -173,7 +173,7 @@
return b
}

// The title must not return empty, and titleCompo can return nil
// Title The title must not return empty, and titleCompo can return nil
func (b *ListingBuilder) Title(f func(evCtx *web.EventContext, style ListingStyle, defaultTitle string) (title string, titleCompo h.HTMLComponent, err error)) (r *ListingBuilder) {
b.titleFunc = f
return b
Expand Down Expand Up @@ -259,7 +259,7 @@

func (b *ListingBuilder) cellComponentFunc(f *FieldBuilder) vx.CellComponentFunc {
return func(obj interface{}, fieldName string, ctx *web.EventContext) h.HTMLComponent {
return f.compFunc(obj, b.mb.getComponentFuncField(f), ctx)
return f.lazyCompFunc()(obj, b.mb.getComponentFuncField(f), ctx)
}
}

Expand Down Expand Up @@ -301,8 +301,8 @@
}
evCtx.WithContextValue(ctxActionsComponentTeleportToID, compo.ActionsComponentTeleportToID())

r.Body = VLayout(
VMain(
r.Body = v.VLayout(
v.VMain(
b.mb.p.dc.MustInject(injectorName, stateful.SyncQuery(compo)),
),
)
Expand Down Expand Up @@ -347,18 +347,18 @@
};
}`, compo.CompoID())

content := VCard().Attr("id", compo.CompoID()).Children(
VCardTitle().Class("d-flex align-center h-abs-26 py-6 px-6 content-box").Children(
content := v.VCard().Attr("id", compo.CompoID()).Children(
v.VCardTitle().Class("d-flex align-center h-abs-26 py-6 px-6 content-box").Children(
titleCompo,
VSpacer(),
v.VSpacer(),
h.Div().Id(compo.ActionsComponentTeleportToID()),
VBtn("").Elevation(0).Size(SizeXSmall).Icon("mdi-close").Class("ml-2 dialog-close-btn").Attr("@click", CloseListingDialogVarScript),
v.VBtn("").Elevation(0).Size(v.SizeXSmall).Icon("mdi-close").Class("ml-2 dialog-close-btn").Attr("@click", CloseListingDialogVarScript),
),
VCardText().Class("pa-0").Children(
v.VCardText().Class("pa-0").Children(
b.mb.p.dc.MustInject(injectorName, stateful.ParseQuery(compo)),
),
)
dialog := VDialog(content).Attr("v-model", "vars.presetsListingDialog").Scrollable(true)
dialog := v.VDialog(content).Attr("v-model", "vars.presetsListingDialog").Scrollable(true)
if b.dialogWidth != "" {
dialog.Width(b.dialogWidth)
}
Expand All @@ -379,15 +379,15 @@
r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{
Name: DeleteConfirmPortalName,
Body: web.Scope().VSlot("{ locals }").Init(`{deleteConfirmation:true}`).Children(
VDialog().MaxWidth("600px").Attr("v-model", "locals.deleteConfirmation").Children(
VCard(
VCardTitle(
v.VDialog().MaxWidth("600px").Attr("v-model", "locals.deleteConfirmation").Children(
v.VCard(
v.VCardTitle(

Check warning on line 384 in presets/listing_builder.go

View check run for this annotation

Codecov / codecov/patch

presets/listing_builder.go#L382-L384

Added lines #L382 - L384 were not covered by tests
h.Text(msgr.DeleteConfirmationText),
),
VCardActions(
VSpacer(),
VBtn(msgr.Cancel).Variant(VariantFlat).Class("ml-2").Attr("@click", "locals.deleteConfirmation = false"),
VBtn(msgr.Delete).Color("primary").Variant(VariantFlat).Theme(ThemeDark).Attr("@click", web.Plaid().
v.VCardActions(
v.VSpacer(),
v.VBtn(msgr.Cancel).Variant(v.VariantFlat).Class("ml-2").Attr("@click", "locals.deleteConfirmation = false"),
v.VBtn(msgr.Delete).Color("primary").Variant(v.VariantFlat).Theme(v.ThemeDark).Attr("@click", web.Plaid().

Check warning on line 390 in presets/listing_builder.go

View check run for this annotation

Codecov / codecov/patch

presets/listing_builder.go#L387-L390

Added lines #L387 - L390 were not covered by tests
EventFunc(actions.DoDelete).
Queries(evCtx.Queries()).
URL(b.mb.Info().ListingHref()).
Expand Down
7 changes: 7 additions & 0 deletions presets/presets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,10 @@ func TestCloneFieldsLayout(t *testing.T) {
}
require.Equal(t, src, CloneFieldsLayout(src))
}

func TestHumanizeString(t *testing.T) {
assert.Equal(t, "Hello World", humanizeString("HelloWorld"))
assert.Equal(t, "Hello World", humanizeString("helloWorld"))
assert.Equal(t, "Order Item", humanizeString("OrderItem"))
assert.Equal(t, "CNN Name", humanizeString("CNNName"))
}
Loading
Loading
0