From 32c41106d47c2baea1623cdbc0c166ca91497104 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 31 Oct 2024 10:38:25 +0800 Subject: [PATCH 01/15] feat validate;restore delete row icon position --- .../examples_presets/perm_role_test.go | 4 +- example/admin/demo_case.go | 451 ++++++++++-------- example/integration/demo_case_test.go | 71 ++- example/integration/pagebuilder_test.go | 77 ++- go.mod | 2 +- go.sum | 2 + pagebuilder/builder.go | 19 +- pagebuilder/example/containers/in_numbers.go | 18 +- pagebuilder/helper.go | 1 - pagebuilder/page.go | 16 +- pagebuilder/settings.go | 11 +- presets/actions/const.go | 1 + presets/api.go | 7 +- presets/const.go | 5 + presets/detailing.go | 7 +- presets/editing.go | 121 ++++- presets/field.go | 28 +- presets/field_defaults.go | 34 +- presets/integration/example_test.go | 23 +- presets/integration/fields_test.go | 44 +- presets/listeditor.go | 4 +- presets/listener.go | 13 + presets/model.go | 21 +- presets/section.go | 92 +++- presets/utils.go | 24 +- 25 files changed, 754 insertions(+), 342 deletions(-) diff --git a/docs/docsrc/examples/examples_presets/perm_role_test.go b/docs/docsrc/examples/examples_presets/perm_role_test.go index ce79fe2ec..1513b161e 100644 --- a/docs/docsrc/examples/examples_presets/perm_role_test.go +++ b/docs/docsrc/examples/examples_presets/perm_role_test.go @@ -142,7 +142,7 @@ func TestPermWithoutID(t *testing.T) { BuildEventFuncRequest() return req }, - ExpectPortalUpdate0ContainsInOrder: []string{"v-assign='[form, {\"Name\":\"OldName\"}]' :error-messages='null' :disabled='false'>"}, + ExpectPortalUpdate0ContainsInOrder: []string{``}, }, Role: models.RoleViewer, }, @@ -157,7 +157,7 @@ func TestPermWithoutID(t *testing.T) { BuildEventFuncRequest() return req }, - ExpectPortalUpdate0ContainsInOrder: []string{"v-assign='[form, {\"Name\":\"OldName\"}]' :error-messages='null' :disabled='false'>"}, + ExpectPortalUpdate0ContainsInOrder: []string{``}, }, Role: models.RoleEditor, }, diff --git a/example/admin/demo_case.go b/example/admin/demo_case.go index bdafdc7db..67289fd2d 100644 --- a/example/admin/demo_case.go +++ b/example/admin/demo_case.go @@ -8,13 +8,12 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/qor5/web/v3" + v "github.com/qor5/x/v3/ui/vuetify" + vx "github.com/qor5/x/v3/ui/vuetifyx" "github.com/sunfmin/reflectutils" h "github.com/theplant/htmlgo" "gorm.io/gorm" - v "github.com/qor5/x/v3/ui/vuetify" - vx "github.com/qor5/x/v3/ui/vuetifyx" - "github.com/qor5/admin/v3/presets" ) @@ -149,43 +148,7 @@ func configureDemoCase(b *presets.Builder, db *gorm.DB) { panic(err) } mb := b.Model(&DemoCase{}) - mb.Editing().WrapValidateFunc(func(in presets.ValidateFunc) presets.ValidateFunc { - return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { - if in != nil { - in(obj, ctx) - } - p := obj.(*DemoCase) - if p.ID == 0 { - return - } - if len(p.FieldTextareaData.TextareaValidate) < 10 { - err.FieldError("FieldTextareaSection.FieldTextareaData.TextareaValidate", "input more than 10 chars") - } - if len(p.FieldPasswordData.Password) < 5 { - err.FieldError("FieldPasswordSection.FieldPasswordData.Password", "password more than 5 chars") - } - if p.FieldNumberData.NumberValidate <= 0 { - err.FieldError("FieldNumberSection.FieldNumberData.NumberValidate", "input greater than 0") - } - if len(p.SelectData.AutoComplete) == 1 { - err.FieldError("SelectSection.SelectData.editField", "select more than 1 item") - } - if p.SelectData.NormalSelect == 8 { - err.FieldError("SelectSection.SelectData.NormalSelect", "can`t select Trevor") - } - if p.DatepickerData.Date == 0 { - err.FieldError("DatepickerSection.DatepickerData.Date", "Date is required") - } - if p.DatepickerData.DateTime == 0 { - err.FieldError("DatepickerSection.DatepickerData.DateTime", "DateTime is required") - } - if p.DatepickerData.DateRange == nil || p.DatepickerData.DateRange[1] < p.DatepickerData.DateRange[0] { - err.FieldError("DatepickerSection.DatepickerData.DateRange", "End later than Start") - } - - return - } - }) + mb.Editing("Name") mb.Listing("ID", "Name") detailing := mb.Detailing( "FieldSection", @@ -198,60 +161,40 @@ func configureDemoCase(b *presets.Builder, db *gorm.DB) { "DialogSection", "AvatarSection", ) - editing := mb.Editing( - "Name", - "FieldSection", - "FieldTextareaSection", - "FieldPasswordSection", - "FieldNumberSection", - "SelectSection", - "CheckboxSection", - "DatepickerSection", - ) - - configVxField(detailing, editing, mb) - configVxFieldArea(detailing, editing, mb) - configVxFieldPassword(detailing, editing, mb) - configVxFieldNumber(detailing, editing, mb) - configVxSelect(detailing, editing, mb) - configVxCheckBox(detailing, editing, mb) - configVxDatepicker(detailing, editing, mb) + configVxField(detailing, mb) + configVxFieldArea(detailing, mb) + configVxFieldPassword(detailing, mb) + configVxFieldNumber(detailing, mb) + configVxSelect(detailing, mb) + configVxCheckBox(detailing, mb) + configVxDatepicker(detailing, mb) configVxDialog(detailing, mb) configVxAvatar(detailing, mb) return } -func DemoCaseTextField(obj interface{}, section, editField, field, label string, vErr web.ValidationErrors) *vx.VXFieldBuilder { - formKey := fmt.Sprintf("%s.%s", editField, field) - return vx.VXField(). - Label(label). - Attr(web.VField(formKey, reflectutils.MustGet(obj, formKey))...). - ErrorMessages(vErr.GetFieldErrors(formKey)...) -} - -func DemoCaseSelect(obj interface{}, section, editField, field, label string, vErr web.ValidationErrors, items interface{}) *vx.VXSelectBuilder { - formKey := fmt.Sprintf("%s.%s", editField, field) - return vx.VXSelect(). - Label(label). - Items(items). - ItemTitle("Name"). - ItemValue("ID"). - Attr(web.VField(formKey, reflectutils.MustGet(obj, formKey))...). - ErrorMessages(vErr.GetFieldErrors(formKey)...) -} - -func DemoCaseCheckBox(obj interface{}, section, editField, field, label string) *vx.VXCheckboxBuilder { - formKey := fmt.Sprintf("%s.%s", editField, field) - return vx.VXCheckbox(). - Label(label). - Attr(web.VField(formKey, reflectutils.MustGet(obj, formKey))...) -} - -func configVxField(detailing *presets.DetailingBuilder, editing *presets.EditingBuilder, mb *presets.ModelBuilder) { +// configs +func configVxField(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder) { sectionName := "FieldSection" editField := "FieldData" label := "vx-field" section := generateSection(detailing, mb, sectionName, editField, label). + WrapValidator(func(in presets.ValidateFunc) presets.ValidateFunc { + return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { + if in != nil { + in(obj, ctx) + } + p := obj.(*DemoCase) + if p.ID == 0 { + return + } + if len(p.FieldData.TextValidate) < 5 { + err.FieldError(fmt.Sprintf("%s.TextValidate", editField), "input more than 5 chars") + } + + return + } + }). EditComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { var vErr web.ValidationErrors if ve, ok := ctx.Flash.(*web.ValidationErrors); ok { @@ -267,30 +210,36 @@ func configVxField(detailing *presets.DetailingBuilder, editing *presets.Editing Label("Text(Readonly)"). ModelValue("This is Readonly Vx-Field"). Readonly(true), - DemoCaseTextField(obj, sectionName, editField, "Text", "Text", vErr). + DemoCaseTextField(obj, editField, "Text", "Text", vErr). Tips("This is Tips").Clearable(true), - DemoCaseTextField(obj, sectionName, editField, "TextValidate", "TextValidate(input more than 5 chars)", vErr).Required(true).Clearable(true), + DemoCaseTextField(obj, editField, "TextValidate", "TextValidate(input more than 5 chars)", vErr).Required(true).Clearable(true), ), ) - }). - WrapValidator(func(in presets.ValidateFunc) presets.ValidateFunc { - return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { - p := obj.(*DemoCase) - if len(p.FieldData.TextValidate) < 5 { - err.FieldError(fmt.Sprintf("%s.%s.TextValidate", sectionName, editField), "input more than 5 chars") - } - return - } }) detailing.Section(section) - editing.Section(section.Clone()) } -func configVxFieldArea(detailing *presets.DetailingBuilder, editing *presets.EditingBuilder, mb *presets.ModelBuilder) { +func configVxFieldArea(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder) { sectionName := "FieldTextareaSection" editField := "FieldTextareaData" label := "vx-field(type textarea)" section := generateSection(detailing, mb, sectionName, editField, label). + WrapValidator(func(in presets.ValidateFunc) presets.ValidateFunc { + return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { + if in != nil { + in(obj, ctx) + } + p := obj.(*DemoCase) + if p.ID == 0 { + return + } + + if len(p.FieldTextareaData.TextareaValidate) < 10 { + err.FieldError(fmt.Sprintf("%s.TextareaValidate", editField), "input more than 10 chars") + } + return + } + }). EditComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { var vErr web.ValidationErrors if ve, ok := ctx.Flash.(*web.ValidationErrors); ok { @@ -308,23 +257,37 @@ func configVxFieldArea(detailing *presets.DetailingBuilder, editing *presets.Edi ModelValue("This is Readonly Vx-Field type Textarea"). Readonly(true). Type("textarea"), - DemoCaseTextField(obj, sectionName, editField, "Textarea", "Textarea", vErr). + DemoCaseTextField(obj, editField, "Textarea", "Textarea", vErr). Tips("This is Textarea Tips"). Type("textarea").Clearable(true), - DemoCaseTextField(obj, sectionName, editField, "TextareaValidate", "TextareaValidate(input more than 10 chars)", vErr).Required(true). + DemoCaseTextField(obj, editField, "TextareaValidate", "TextareaValidate(input more than 10 chars)", vErr).Required(true). Type("textarea").Clearable(true), ), ) }) detailing.Section(section) - editing.Section(section.Clone()) } -func configVxFieldPassword(detailing *presets.DetailingBuilder, editing *presets.EditingBuilder, mb *presets.ModelBuilder) { +func configVxFieldPassword(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder) { sectionName := "FieldPasswordSection" editField := "FieldPasswordData" label := "vx-field(type password)" section := generateSection(detailing, mb, sectionName, editField, label). + WrapValidator(func(in presets.ValidateFunc) presets.ValidateFunc { + return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { + if in != nil { + in(obj, ctx) + } + p := obj.(*DemoCase) + if p.ID == 0 { + return + } + if len(p.FieldPasswordData.Password) < 5 { + err.FieldError(fmt.Sprintf("%s.Password", editField), "password more than 5 chars") + } + return + } + }). EditComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { var vErr web.ValidationErrors if ve, ok := ctx.Flash.(*web.ValidationErrors); ok { @@ -342,13 +305,12 @@ func configVxFieldPassword(detailing *presets.DetailingBuilder, editing *presets ModelValue("This is Disabled Vx-Field type Password"). Disabled(true). Type("password"), - DemoCaseTextField(obj, sectionName, editField, "Password", "Password(More Than 5 chars)", vErr). + DemoCaseTextField(obj, editField, "Password", "Password(More Than 5 chars)", vErr). Tips("Password tips"). Type("password"). Clearable(true). - PasswordVisibleToggle(true). - ErrorMessages(vErr.GetFieldErrors("FieldPasswordSection.FieldPasswordData.Password")...), - DemoCaseTextField(obj, sectionName, editField, "PasswordDefault", "PasswordDefault", vErr). + PasswordVisibleToggle(true), + DemoCaseTextField(obj, editField, "PasswordDefault", "PasswordDefault", vErr). Tips("PasswordDefault tips"). Clearable(true). Type("password"). @@ -358,14 +320,28 @@ func configVxFieldPassword(detailing *presets.DetailingBuilder, editing *presets ) }) detailing.Section(section) - editing.Section(section.Clone()) } -func configVxFieldNumber(detailing *presets.DetailingBuilder, editing *presets.EditingBuilder, mb *presets.ModelBuilder) { +func configVxFieldNumber(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder) { sectionName := "FieldNumberSection" editField := "FieldNumberData" label := "vx-field(type number)" section := generateSection(detailing, mb, sectionName, editField, label). + WrapValidator(func(in presets.ValidateFunc) presets.ValidateFunc { + return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { + if in != nil { + in(obj, ctx) + } + p := obj.(*DemoCase) + if p.ID == 0 { + return + } + if p.FieldNumberData.NumberValidate <= 0 { + err.FieldError(fmt.Sprintf("%s.NumberValidate", editField), "input greater than 0") + } + return + } + }). EditComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { var vErr web.ValidationErrors if ve, ok := ctx.Flash.(*web.ValidationErrors); ok { @@ -381,27 +357,43 @@ func configVxFieldNumber(detailing *presets.DetailingBuilder, editing *presets.E Label("Number(Readonly)"). ModelValue("This is Readonly Vx-Field type Number"). Readonly(true), - DemoCaseTextField(obj, sectionName, editField, "Number", "Number", vErr). + DemoCaseTextField(obj, editField, "Number", "Number", vErr). Tips("Number tips"). Clearable(true). Type("number"), - DemoCaseTextField(obj, sectionName, editField, "NumberValidate", "NumberValidate( > 0)", vErr). + DemoCaseTextField(obj, editField, "NumberValidate", "NumberValidate( > 0)", vErr). Tips("NumberValidate tips"). Clearable(true). - Type("number"). - ErrorMessages(vErr.GetFieldErrors("FieldNumberSection.FieldNumberData.NumberValidate")...), + Type("number"), ), ) }) detailing.Section(section) - editing.Section(section.Clone()) } -func configVxSelect(detailing *presets.DetailingBuilder, editing *presets.EditingBuilder, mb *presets.ModelBuilder) { +func configVxSelect(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder) { sectionName := "SelectSection" editField := "SelectData" label := "vx-select" section := generateSection(detailing, mb, sectionName, editField, label). + WrapValidator(func(in presets.ValidateFunc) presets.ValidateFunc { + return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { + if in != nil { + in(obj, ctx) + } + p := obj.(*DemoCase) + if p.ID == 0 { + return + } + if len(p.SelectData.AutoComplete) <= 1 { + err.FieldError(fmt.Sprintf("%s.AutoComplete", editField), "select more than 1 item") + } + if p.SelectData.NormalSelect == 8 { + err.FieldError(fmt.Sprintf("%s.NormalSelect", editField), "can`t select Trevor") + } + return + } + }). EditComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { var vErr web.ValidationErrors if ve, ok := ctx.Flash.(*web.ValidationErrors); ok { @@ -420,26 +412,22 @@ func configVxSelect(detailing *presets.DetailingBuilder, editing *presets.Editin return h.Components( v.VRow( v.VCol( - DemoCaseSelect(obj, sectionName, editField, "AutoComplete", "AutoComplete(select more than 1 item)", vErr, items). - Type("autocomplete").Multiple(true).Chips(true).ClosableChips(true).Clearable(true). - ErrorMessages(vErr.GetFieldErrors("SelectSection.SelectData.AutoComplete")...), + DemoCaseSelect(obj, editField, "AutoComplete", "AutoComplete(select more than 1 item)", vErr, items). + Type("autocomplete").Multiple(true).Chips(true).ClosableChips(true).Clearable(true), ), ), v.VRow( v.VCol( - DemoCaseSelect(obj, sectionName, editField, "NormalSelect", "", vErr, items). - Attr(":rules", `[(value) => value !== 8 || "can't select Trevor"]`). - Type("autocomplete"). - ErrorMessages(vErr.GetFieldErrors("SelectSection.SelectData.NormalSelect")...), + DemoCaseSelect(obj, editField, "NormalSelect", "", vErr, items). + Type("autocomplete"), ), ), ) }) detailing.Section(section) - editing.Section(section.Clone()) } -func configVxCheckBox(detailing *presets.DetailingBuilder, editing *presets.EditingBuilder, mb *presets.ModelBuilder) { +func configVxCheckBox(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder) { sectionName := "CheckboxSection" editField := "CheckboxData" label := "vx-checkbox" @@ -448,7 +436,7 @@ func configVxCheckBox(detailing *presets.DetailingBuilder, editing *presets.Edit return h.Components( v.VRow( v.VCol( - DemoCaseCheckBox(obj, sectionName, editField, "Checkbox", "Checkbox"). + DemoCaseCheckBox(obj, editField, "Checkbox", "Checkbox"). TrueLabel("True"). TrueIconColor(v.ColorPrimary). FalseLabel("False"). @@ -460,32 +448,6 @@ func configVxCheckBox(detailing *presets.DetailingBuilder, editing *presets.Edit ) }) detailing.Section(section) - editing.Section(section.Clone()) -} - -func cardRows(title string, splitCols int, comp ...h.HTMLComponent) *v.VCardBuilder { - var ( - rows []h.HTMLComponent - result int - row = v.VRow() - ) - - for i, c := range comp { - if i/splitCols == result { - if i%splitCols == 0 { - row = v.VRow() - rows = append(rows, row) - } else if i%splitCols == splitCols-1 { - result++ - } - row.AppendChildren(v.VCol(c).Class("text-center")) - } - } - return v.VCard( - v.VCardItem( - rows..., - ), - ).Title(title).Class("pa-2 my-4") } func configVxDialog(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder) { @@ -503,14 +465,15 @@ func configVxDialog(detailing *presets.DetailingBuilder, mb *presets.ModelBuilde h.H2(label).Class("section-title"), ).Class("section-title-wrap"), cardRows("Activator", 5, - h.Components(h.Div(h.Text("v-model")).Class("mb-2"), + h.Div( + h.Div(h.Text("v-model")).Class("mb-2"), v.VBtn("Open Dialog").Color(v.ColorPrimary). Attr("@click", "locals.dialogVisible=true"), vx.VXDialog(). Attr("v-model", "locals.dialogVisible"). Title("ModelValue"). Text(text), - ), + ).Class("text-center"), dialogActivator("Open Dialog", "Activator Slot", text, v.ColorSecondary).Title("Conform"), ), @@ -558,45 +521,32 @@ func configVxDialog(detailing *presets.DetailingBuilder, mb *presets.ModelBuilde detailing.Section(section) } -func generateSection(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder, section, editField, label string) *presets.SectionBuilder { - return presets.NewSectionBuilder(mb, section).Label(label).Editing(editField). - ViewComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { - p := obj.(*DemoCase) - j := jsoniter.Config{ - EscapeHTML: false, - }.Froze() - data := reflectutils.MustGet(p, editField) - jsonBytes, _ := j.MarshalIndent(data, "", " ") - return vx.VXReadonlyField().Value(string(jsonBytes)).Label(editField) - }) -} - -func DemoCaseDatepicker(obj interface{}, section, editField, field, label string, vErr web.ValidationErrors) *vx.VXDatePickerBuilder { - formKey := fmt.Sprintf("%s.%s", editField, field) - val := reflectutils.MustGet(obj, formKey) - return vx.VXDatepicker(). - Clearable(true). - Label(label). - Attr(web.VField(formKey, val)...). - Placeholder(field). - ErrorMessages(vErr.GetFieldErrors(fmt.Sprintf("%s.%s", section, formKey))...) -} - -func DemoCaseRangePicker(obj interface{}, section, editField, field, label string, vErr web.ValidationErrors) *vx.VXRangePickerBuilder { - formKey := fmt.Sprintf("%s.%s", editField, field) - val := reflectutils.MustGet(obj, formKey) - return vx.VXRangePicker(). - Clearable(true). - Label(label). - Attr(web.VField(formKey, val)...). - ErrorMessages(vErr.GetFieldErrors(fmt.Sprintf("%s.%s", section, formKey))...) -} - -func configVxDatepicker(detailing *presets.DetailingBuilder, editing *presets.EditingBuilder, mb *presets.ModelBuilder) { +func configVxDatepicker(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder) { label := "vx-datepicker" sectionName := "DatepickerSection" editField := "DatepickerData" section := generateSection(detailing, mb, sectionName, editField, label). + WrapValidator(func(in presets.ValidateFunc) presets.ValidateFunc { + return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { + if in != nil { + in(obj, ctx) + } + p := obj.(*DemoCase) + if p.ID == 0 { + return + } + if p.DatepickerData.Date == 0 { + err.FieldError(fmt.Sprintf("%s.Date", "DatepickerData"), "Date is required") + } + if p.DatepickerData.DateTime == 0 { + err.FieldError(fmt.Sprintf("%s.DateTime", "DatepickerData"), "DateTime is required") + } + if p.DatepickerData.DateRange == nil || p.DatepickerData.DateRange[1] <= p.DatepickerData.DateRange[0] { + err.FieldError(fmt.Sprintf("%s.DateRange", "DatepickerData"), "End later than Start") + } + return + } + }). EditComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { var vErr web.ValidationErrors if ve, ok := ctx.Flash.(*web.ValidationErrors); ok { @@ -605,44 +555,27 @@ func configVxDatepicker(detailing *presets.DetailingBuilder, editing *presets.Ed return h.Components( v.VRow( v.VCol( - DemoCaseDatepicker(obj, sectionName, editField, "Date", "date-picker(required,Within five days before and after)", vErr). + DemoCaseDatepicker(obj, editField, "Date", "date-picker(required,Within five days before and after)", vErr). DatePickerProps(map[string]string{ "min": time.Now().AddDate(0, 0, -5).Format("2006-01-02"), "max": time.Now().AddDate(0, 0, 5).Format("2006-01-02"), }), ), v.VCol( - DemoCaseDatepicker(obj, sectionName, editField, "DateTime", "datetime-picker(required)", vErr).Type("datetimepicker"), + DemoCaseDatepicker(obj, editField, "DateTime", "datetime-picker(required)", vErr).Type("datetimepicker"), ), ), v.VRow( v.VCol( - DemoCaseRangePicker(obj, sectionName, editField, "DateRange", "range-picker(end>start)", vErr).Placeholder([]string{"Start", "End"}), + DemoCaseRangePicker(obj, editField, "DateRange", "range-picker(end>start)", vErr).Placeholder([]string{"Start", "End"}), ), v.VCol( - DemoCaseRangePicker(obj, sectionName, editField, "DateRangeNeedConfirm", "range-picker (needConfirm)", vErr).NeedConfirm(true).Placeholder([]string{"Begin", "End"}), + DemoCaseRangePicker(obj, editField, "DateRangeNeedConfirm", "range-picker (needConfirm)", vErr).NeedConfirm(true).Placeholder([]string{"Begin", "End"}), ), ), ) }) detailing.Section(section) - editing.Section(section.Clone()) -} - -func dialogActivator(btn, label, text, color string) *vx.VXDialogBuilder { - return vx.VXDialog( - web.Slot( - h.Div(h.Text(label)).Class("mb-2"), - v.VBtn(btn).Color(color).Attr("v-bind", "activatorProps"), - ).Name("activator").Scope("{props: { activatorProps }}"), - ).Text(text) -} - -func avatarView[T comparable](sizes []T, show func(T) string) (comps []h.HTMLComponent) { - for _, size := range sizes { - comps = append(comps, h.Components(h.Div(h.Text(show(size))).Class("mb-2"), vx.VXAvatar().Name("ShaoXing").Size(fmt.Sprint(size)))) - } - return } func configVxAvatar(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder) { @@ -669,3 +602,105 @@ func configVxAvatar(detailing *presets.DetailingBuilder, mb *presets.ModelBuilde }) detailing.Section(section) } + +// view component +func dialogActivator(btn, label, text, color string) *vx.VXDialogBuilder { + return vx.VXDialog( + web.Slot( + h.Div( + h.Div(h.Text(label)).Class("mb-2"), + v.VBtn(btn).Color(color).Attr("v-bind", "activatorProps"), + ).Class("text-center"), + ).Name("activator").Scope("{props: { activatorProps }}"), + ).Text(text) +} + +func generateSection(detailing *presets.DetailingBuilder, mb *presets.ModelBuilder, section, editField, label string) *presets.SectionBuilder { + return presets.NewSectionBuilder(mb, section).Label(label).Editing(editField). + ViewComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { + p := obj.(*DemoCase) + j := jsoniter.Config{ + EscapeHTML: false, + }.Froze() + data := reflectutils.MustGet(p, editField) + jsonBytes, _ := j.MarshalIndent(data, "", " ") + return vx.VXReadonlyField().Value(string(jsonBytes)).Label(editField) + }) +} + +func avatarView[T comparable](sizes []T, show func(T) string) (comps []h.HTMLComponent) { + for _, size := range sizes { + comps = append(comps, h.Div( + h.Div(h.Text(show(size))).Class("mb-2"), vx.VXAvatar().Name("ShaoXing").Size(fmt.Sprint(size)), + ).Class("text-center")) + } + return +} +func cardRows(title string, splitCols int, comp ...h.HTMLComponent) *v.VCardBuilder { + var ( + rows []h.HTMLComponent + result int + row = v.VRow() + ) + + for i, c := range comp { + if i/splitCols == result { + if i%splitCols == 0 { + row = v.VRow() + rows = append(rows, row) + } else if i%splitCols == splitCols-1 { + result++ + } + row.AppendChildren(v.VCol(c)) + } + } + return v.VCard( + v.VCardItem( + rows..., + ), + ).Title(title).Class("pa-2 my-4") +} + +// vx library Compoents +func DemoCaseDatepicker(obj interface{}, editField, field, label string, vErr web.ValidationErrors) *vx.VXDatePickerBuilder { + formKey := fmt.Sprintf("%s.%s", editField, field) + return vx.VXDatepicker(). + Clearable(true). + Label(label). + Placeholder(field). + Attr(presets.VFieldError(formKey, reflectutils.MustGet(obj, formKey), vErr.GetFieldErrors(formKey))...) +} + +func DemoCaseRangePicker(obj interface{}, editField, field, label string, vErr web.ValidationErrors) *vx.VXRangePickerBuilder { + formKey := fmt.Sprintf("%s.%s", editField, field) + return vx.VXRangePicker(). + Clearable(true). + Label(label). + Attr(presets.VFieldError(formKey, reflectutils.MustGet(obj, formKey), vErr.GetFieldErrors(formKey))...) +} + +func DemoCaseTextField(obj interface{}, editField, field, label string, vErr web.ValidationErrors) *vx.VXFieldBuilder { + formKey := fmt.Sprintf("%s.%s", editField, field) + return vx.VXField(). + Label(label). + Attr(presets.VFieldError(formKey, reflectutils.MustGet(obj, formKey), vErr.GetFieldErrors(formKey))...) + +} + +func DemoCaseSelect(obj interface{}, editField, field, label string, vErr web.ValidationErrors, items interface{}) *vx.VXSelectBuilder { + formKey := fmt.Sprintf("%s.%s", editField, field) + return vx.VXSelect(). + Label(label). + Items(items). + ItemTitle("Name"). + ItemValue("ID"). + Attr(presets.VFieldError(formKey, reflectutils.MustGet(obj, formKey), vErr.GetFieldErrors(formKey))...) + +} + +func DemoCaseCheckBox(obj interface{}, editField, field, label string) *vx.VXCheckboxBuilder { + formKey := fmt.Sprintf("%s.%s", editField, field) + return vx.VXCheckbox(). + Label(label). + Attr(web.VField(formKey, reflectutils.MustGet(obj, formKey))...) +} diff --git a/example/integration/demo_case_test.go b/example/integration/demo_case_test.go index 9483fbe54..98e32bfee 100644 --- a/example/integration/demo_case_test.go +++ b/example/integration/demo_case_test.go @@ -14,7 +14,7 @@ import ( ) var demoCaseData = gofixtures.Data(gofixtures.Sql(` -INSERT INTO public.demo_cases (id, created_at, updated_at, deleted_at, name, field_data, field_textarea_data, field_password_data, field_number_data, select_data, checkbox_data, datepicker_data) VALUES (1, '2024-10-28 06:25:03.617793 +00:00', '2024-10-28 06:25:03.617793 +00:00', null, 'test', '{"Text":"121231321\u0026\u0026","TextValidate":"qor@theplant.jp"}', '{"Textarea":"","TextareaValidate":"12345678901"}', '{"Password":"12345","PasswordDefault":""}', '{"Number":0,"NumberValidate":1}', '{"AutoComplete":[1,2],"NormalSelect":1}', '{"Checkbox":true}', '{"Date":1730044800000,"DateTime":1730131200000,"DateRange":[1730044800000,1730131200000],"DateRangeNeedConfirm":null}'); +INSERT INTO public.demo_cases (id, created_at, updated_at, deleted_at, name,field_data) VALUES (1, '2024-10-10 03:18:50.316417 +00:00', '2024-10-10 03:18:50.316417 +00:00', null, '12313','{"Text":"121231321\u0026\u0026","Textarea":"1231","TextValidate":"21312","TextareaValidate":"1😋11231"}'); `, []string{`demo_cases`})) func TestDemoCase(t *testing.T) { @@ -29,7 +29,7 @@ func TestDemoCase(t *testing.T) { demoCaseData.TruncatePut(dbr) return httptest.NewRequest("GET", "/demo-cases", nil) }, - ExpectPageBodyContainsInOrder: []string{"Name", "test"}, + ExpectPageBodyContainsInOrder: []string{"Name", "12313"}, }, { Name: "Create Demo Case", @@ -40,22 +40,6 @@ func TestDemoCase(t *testing.T) { PageURL("/demo-cases"). EventFunc(actions.Update). AddField("Name", "test"). - AddField("FieldData.Text", ""). - AddField("FieldData.TextValidate", "qor@theplant.jp"). - AddField("FieldTextareaData.Textarea", ""). - AddField("FieldTextareaData.TextareaValidate", "12345678901"). - AddField("FieldPasswordData.Password", "12345"). - AddField("FieldPasswordData.PasswordDefault", ""). - AddField("FieldNumberData.Number", "0"). - AddField("FieldNumberData.NumberValidate", "1"). - AddField("SelectData.AutoComplete[0]", "1"). - AddField("SelectData.AutoComplete[1]", "2"). - AddField("SelectData.NormalSelect", "1"). - AddField("CheckboxData.Checkbox", "true"). - AddField("DatepickerData.Date", "1730044800000"). - AddField("DatepickerData.DateTime", "1730131200000"). - AddField("DatepickerData.DateRange[0]", "1730044800000"). - AddField("DatepickerData.DateRange[1]", "1730131200000"). BuildEventFuncRequest() return req }, @@ -75,13 +59,12 @@ func TestDemoCase(t *testing.T) { demoCaseData.TruncatePut(dbr) req := NewMultipartBuilder(). PageURL("/demo-cases/1"). - Query("__execute_event__", "__reload__"). BuildEventFuncRequest() return req }, ExpectPageBodyContainsInOrder: []string{ "vx-field", - `\u0026#34;121231321\u0026amp;\u0026amp;\u0026#34;`, + ""121231321&&"", "vx-field(type textarea)", "vx-field(type password)", "vx-field(type number)", @@ -329,13 +312,57 @@ func TestDemoCase(t *testing.T) { Query(presets.ParamID, "1"). AddField("DatepickerData.Date", "0"). AddField("DatepickerData.DateTime", "0"). - AddField("DatepickerData.DateRange[0]", "1730131200000"). - AddField("DatepickerData.DateRange[1]", "1730044800000"). BuildEventFuncRequest() return req }, ExpectPortalUpdate0ContainsInOrder: []string{"Date is required", "DateTime is required", "End later than Start"}, }, + { + Name: "Demo Case DatePicker Date Validate Event", + Debug: true, + ReqFunc: func() *http.Request { + demoCaseData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/demo-cases/1"). + EventFunc("section_validate_DatepickerSection"). + Query(presets.ParamID, "1"). + AddField("DatepickerData.Date", "0"). + BuildEventFuncRequest() + return req + }, + ExpectRunScriptContainsInOrder: []string{"Date is required"}, + }, + { + Name: "Demo Case DatePicker Validate Datetime Event", + Debug: true, + ReqFunc: func() *http.Request { + demoCaseData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/demo-cases/1"). + EventFunc("section_validate_DatepickerSection"). + Query(presets.ParamID, "1"). + AddField("DatepickerData.DateTime", "0"). + BuildEventFuncRequest() + return req + }, + ExpectRunScriptContainsInOrder: []string{"DateTime is required"}, + }, + { + Name: "Demo Case DatePicker Validate DateRange Event", + Debug: true, + ReqFunc: func() *http.Request { + demoCaseData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/demo-cases/1"). + EventFunc("section_validate_DatepickerSection"). + Query(presets.ParamID, "1"). + AddField("DatepickerData.DateRange[0]", "0"). + AddField("DatepickerData.DateRange[1]", "0"). + BuildEventFuncRequest() + return req + }, + ExpectRunScriptContainsInOrder: []string{"End later than Start"}, + }, } for _, c := range cases { diff --git a/example/integration/pagebuilder_test.go b/example/integration/pagebuilder_test.go index a10316496..ea3eb1d32 100644 --- a/example/integration/pagebuilder_test.go +++ b/example/integration/pagebuilder_test.go @@ -133,6 +133,35 @@ func TestPageBuilder(t *testing.T) { }, ExpectPortalUpdate0ContainsInOrder: []string{"Invalid Title"}, }, + { + Name: "Page Title Section Validate Empty", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/pages"). + Query(presets.ParamID, "1_2024-05-18-v01_International"). + AddField("Title", ""). + EventFunc("section_validate_Page"). + BuildEventFuncRequest() + return req + }, + ExpectRunScriptContainsInOrder: []string{"Invalid Title"}, + }, + { + Name: "Page Title Section Empty", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/pages"). + Query(presets.ParamID, "1_2024-05-18-v01_International"). + EventFunc("section_save_Page"). + BuildEventFuncRequest() + return req + }, + ExpectPortalUpdate0ContainsInOrder: []string{"Invalid Title"}, + }, { Name: "Category New Title Empty", Debug: true, @@ -583,6 +612,20 @@ func TestPageBuilder(t *testing.T) { } }, }, + { + Name: "InNumber Validate Items ", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderDemoContainerTestData.TruncatePut(dbr) + return NewMultipartBuilder(). + PageURL("/page_builder/in-numbers"). + EventFunc(actions.Validate). + Query(presets.ParamID, "1"). + AddField("Items[0].Heading", ""). + BuildEventFuncRequest() + }, + ExpectRunScriptContainsInOrder: []string{"Heading can`t Empty"}, + }, { Name: "Edit Demo Container", Debug: true, @@ -813,16 +856,29 @@ func TestPageBuilder(t *testing.T) { req := NewMultipartBuilder(). PageURL("/page_categories"). EventFunc(actions.Update). - Query(presets.ParamID, "1_International"). AddField("Name", "category_123"). AddField("Path", "45"). - AddField("LocaleCode", "International"). BuildEventFuncRequest() return req }, ExpectPortalUpdate0ContainsInOrder: []string{"Existing Path"}, }, + { + Name: "Page Category Validate Event Existing Path", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_categories"). + EventFunc(actions.Validate). + AddField("Path", "45"). + BuildEventFuncRequest() + + return req + }, + ExpectRunScriptContainsInOrder: []string{"Existing Path"}, + }, { Name: "Page Category Delete Related Page", Debug: true, @@ -1033,6 +1089,23 @@ func TestPageBuilder(t *testing.T) { return }, }, + { + Name: "Container Heading Validate", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderDemoContainerTestData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_builder/headings"). + EventFunc(actions.Validate). + Query(presets.ParamID, "1"). + AddField("LinkText", ""). + AddField("FontColor", "blue"). + BuildEventFuncRequest() + + return req + }, + ExpectRunScriptContainsInOrder: []string{"LinkText 不能为空"}, + }, } for _, c := range cases { diff --git a/go.mod b/go.mod index 5c51e746f..43867f246 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/pquerna/otp v1.4.0 github.com/qor/oss v0.0.0-20240729105053-88484a799a79 github.com/qor5/web v1.3.2 - github.com/qor5/web/v3 v3.0.10 + github.com/qor5/web/v3 v3.0.11-0.20241030093604-4c400ec73a70 github.com/qor5/x/v3 v3.0.13-0.20241030093302-23cc541dceba github.com/samber/lo v1.47.0 github.com/shurcooL/sanitized_anchor_name v1.0.0 diff --git a/go.sum b/go.sum index 7d3bd8378..c95136656 100644 --- a/go.sum +++ b/go.sum @@ -312,6 +312,8 @@ github.com/qor5/web v1.3.2 h1:zw796YJeDLe8vRwGR1cM+uS1ZuSkPutchBEXv2GgOhI= github.com/qor5/web v1.3.2/go.mod h1:LszskQJbFQDJwOeZC6j6afOiHxxyjrzz8B3zuBwfgKQ= github.com/qor5/web/v3 v3.0.10 h1:s1M+PR/Q8p1WfEtl2BdjaeVHfJ5mkNwc0Y6+Ek5YRBY= github.com/qor5/web/v3 v3.0.10/go.mod h1:32vdHHcZb2JimlcaclW9hLUyimdXjrllZDHTh3rl6d0= +github.com/qor5/web/v3 v3.0.11-0.20241030093604-4c400ec73a70 h1:pQoePHKwvgdcKLYLGNNaDQeHMbfPbS6b/Y6R2ZQ2Ay4= +github.com/qor5/web/v3 v3.0.11-0.20241030093604-4c400ec73a70/go.mod h1:32vdHHcZb2JimlcaclW9hLUyimdXjrllZDHTh3rl6d0= github.com/qor5/x/v3 v3.0.12 h1:MYVK+yX3AWyFk4DTGyRiwQXUuYHs/tiA6n1Y7PUrjE4= github.com/qor5/x/v3 v3.0.12/go.mod h1:Sqb0OTgq5PejVkgNQIKqXi0x2MzHSvEAjNZM0g1KNks= github.com/qor5/x/v3 v3.0.13-0.20241030093302-23cc541dceba h1:7C+R8BDneELgad2CBqegCM2Wv3LJ7BBVxX1DXK0UN0k= diff --git a/pagebuilder/builder.go b/pagebuilder/builder.go index 2dc257ea9..ebd9096fd 100644 --- a/pagebuilder/builder.go +++ b/pagebuilder/builder.go @@ -589,7 +589,7 @@ func (b *Builder) defaultCategoryInstall(pb *presets.Builder, pm *presets.ModelB return func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { comp := in(obj, field, ctx) if p, ok := comp.(*vx.VXFieldBuilder); ok { - p.Attr(web.VField(field.Name, strings.TrimPrefix(field.Value(obj).(string), "/"))...). + p.Attr(presets.VFieldError(field.Name, strings.TrimPrefix(field.Value(obj).(string), "/"), field.Errors)...). Attr("prefix", "/") } return comp @@ -1035,6 +1035,20 @@ func (b *ContainerBuilder) Install() { Go(), ), h.If(b.builder.autoSaveReload, + web.Listen( + b.mb.NotifModelsValidate(), + fmt.Sprintf(`if(payload.passed){%s}`, + web.Plaid(). + URL(b.mb.Info().ListingHref()). + EventFunc(actions.Update). + Query(presets.ParamID, web.Var("payload.id")). + Query(presets.ParamPortalName, pageBuilderRightContentPortal). + Query(presets.ParamOverlay, actions.Content). + ThenScript(web.Plaid().EventFunc(ReloadRenderPageOrTemplateEvent). + Query(paramStatus, ctx.Param(paramStatus)).MergeQuery(true).Go()). + Go(), + ), + ), web.Listen( b.mb.NotifModelsUpdated(), web.Plaid(). @@ -1137,9 +1151,6 @@ func (b *ContainerBuilder) Editing(vs ...interface{}) *presets.EditingBuilder { func (b *ContainerBuilder) configureRelatedOnlinePagesTab() { eb := b.mb.Editing() - eb.OnChangeActionFunc(func(id string, ctx *web.EventContext) (s string) { - return web.Emit(b.mb.NotifRowUpdated(), presets.PayloadRowUpdated{Id: id}) - }) eb.AppendTabsPanelFunc(func(obj interface{}, ctx *web.EventContext) (tab h.HTMLComponent, content h.HTMLComponent) { if ctx.R.FormValue(paramOpenFromSharedContainer) != "1" { return nil, nil diff --git a/pagebuilder/example/containers/in_numbers.go b/pagebuilder/example/containers/in_numbers.go index 1a0c3b37a..3eab10e89 100644 --- a/pagebuilder/example/containers/in_numbers.go +++ b/pagebuilder/example/containers/in_numbers.go @@ -4,13 +4,16 @@ import ( "database/sql/driver" "encoding/json" "errors" + "fmt" - "github.com/qor5/admin/v3/presets" "github.com/qor5/web/v3" - "github.com/qor5/admin/v3/pagebuilder" + "github.com/qor5/admin/v3/presets" + . "github.com/theplant/htmlgo" "gorm.io/gorm" + + "github.com/qor5/admin/v3/pagebuilder" ) type InNumbers struct { @@ -56,10 +59,17 @@ func RegisterInNumbersContainer(pb *pagebuilder.Builder, db *gorm.DB) { return InNumbersBody(v, input) }) mb := vb.Model(&InNumbers{}) - eb := mb.Editing("AddTopSpace", "AddBottomSpace", "AnchorID", "Heading", "Items") + eb := mb.Editing("AddTopSpace", "AddBottomSpace", "AnchorID", "Heading", "Items").ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { + p := obj.(*InNumbers) + for i, v := range p.Items { + if v.Heading == "" { + err.FieldError(fmt.Sprintf("Items[%v].Heading", i), "Heading can`t Empty") + } + } + return + }) fb := pb.GetPresetsBuilder().NewFieldsBuilder(presets.WRITE).Model(&InNumbersItem{}).Only("Heading", "Text") - eb.Field("Items").Nested(fb, &presets.DisplayFieldInSorter{Field: "Heading"}) } diff --git a/pagebuilder/helper.go b/pagebuilder/helper.go index 27f5d3dec..3ecf6a114 100644 --- a/pagebuilder/helper.go +++ b/pagebuilder/helper.go @@ -89,7 +89,6 @@ func categoryValidator(ctx *web.EventContext, category *Category, db *gorm.DB, l msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages) if category.Name == "" { err.FieldError("Name", msgr.InvalidNameMsg) - return } categoryPath := path.Clean(category.Path) diff --git a/pagebuilder/page.go b/pagebuilder/page.go index 878df2d84..96326a343 100644 --- a/pagebuilder/page.go +++ b/pagebuilder/page.go @@ -111,15 +111,6 @@ func (b *Builder) defaultPageInstall(pb *presets.Builder, pm *presets.ModelBuild return } }) - eb.WrapValidateFunc(func(in presets.ValidateFunc) presets.ValidateFunc { - return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { - p := obj.(*Page) - if err = pageValidator(ctx, p, db, b.l10n); err.HaveErrors() { - return - } - return in(obj, ctx) - } - }) titleFiled := eb.GetField("Title") if titleFiled != nil { titleFiled.LazyWrapComponentFunc(func(in presets.FieldComponentFunc) presets.FieldComponentFunc { @@ -165,12 +156,11 @@ func (b *Builder) defaultPageInstall(pb *presets.Builder, pm *presets.ModelBuild complete := presets.SelectField(obj, field, ctx). Multiple(false).Chips(false). Label(msgr.Category). - Items(categories).ItemTitle("Path").ItemValue("ID"). - ErrorMessages(field.Errors...) + Items(categories).ItemTitle("Path").ItemValue("ID") if p.CategoryID > 0 { - complete.Attr(web.VField(field.FormKey, p.CategoryID)...) + complete.Attr(presets.VFieldError(field.FormKey, p.CategoryID, field.Errors)...) } else { - complete.Attr(web.VField(field.FormKey, "")...) + complete.Attr(presets.VFieldError(field.FormKey, "", field.Errors)...) } return complete }) diff --git a/pagebuilder/settings.go b/pagebuilder/settings.go index 220e0d00a..72a81f47b 100644 --- a/pagebuilder/settings.go +++ b/pagebuilder/settings.go @@ -126,7 +126,16 @@ func detailPageEditor(dp *presets.DetailingBuilder, mb *presets.ModelBuilder, b db := b.db fields := b.filterFields([]interface{}{"Title", "CategoryID", "Slug"}) section := presets.NewSectionBuilder(mb, "Page"). - Editing(fields...) + Editing(fields...).WrapValidator(func(in presets.ValidateFunc) presets.ValidateFunc { + return func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { + p := obj.(*Page) + if err = pageValidator(ctx, p, db, b.l10n); err.HaveErrors() { + return + } + err = in(obj, ctx) + return + } + }) if b.expectField("Title") { section.ViewingField("Title").LazyWrapComponentFunc(func(in presets.FieldComponentFunc) presets.FieldComponentFunc { return func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { diff --git a/presets/actions/const.go b/presets/actions/const.go index d0b6e7b34..10f9fa5bc 100644 --- a/presets/actions/const.go +++ b/presets/actions/const.go @@ -3,6 +3,7 @@ package actions const ( New = "presets_New" Edit = "presets_Edit" + Validate = "presets_Validate" Action = "presets_Action" Update = "presets_Update" DoAction = "presets_DoAction" diff --git a/presets/api.go b/presets/api.go index 0ccc51a35..11f922750 100644 --- a/presets/api.go +++ b/presets/api.go @@ -42,10 +42,9 @@ type DataOperator interface { } type ( - SetterFunc func(obj interface{}, ctx *web.EventContext) - FieldSetterFunc func(obj interface{}, field *FieldContext, ctx *web.EventContext) (err error) - ValidateFunc func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) - OnChangeActionFunc func(id string, ctx *web.EventContext) (s string) + SetterFunc func(obj interface{}, ctx *web.EventContext) + FieldSetterFunc func(obj interface{}, field *FieldContext, ctx *web.EventContext) (err error) + ValidateFunc func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) ) type ( diff --git a/presets/const.go b/presets/const.go index ae7bdec6d..780a697c4 100644 --- a/presets/const.go +++ b/presets/const.go @@ -29,6 +29,7 @@ const ( ParamOverlayUpdateID = "overlay_update_id" ParamAfterDeleteEvent = "presets_after_delete_event" ParamPortalName = "portal_name" + ParamOperateID = "operate_id" VarsPresetsDataChanged = "presetsDataChanged" @@ -41,3 +42,7 @@ const ( ) var PhraseHasPresetsDataChanged = fmt.Sprintf("Object.values(vars.%s).some(value => value === true)", VarsPresetsDataChanged) + +const ( + ErrorMessagePostfix = "_FieldErrorMessages" +) diff --git a/presets/detailing.go b/presets/detailing.go index 7e53c6c21..bb313c270 100644 --- a/presets/detailing.go +++ b/presets/detailing.go @@ -6,11 +6,12 @@ import ( "fmt" "strings" - "github.com/qor5/admin/v3/presets/actions" "github.com/qor5/web/v3" "github.com/qor5/x/v3/perm" . "github.com/qor5/x/v3/ui/vuetify" h "github.com/theplant/htmlgo" + + "github.com/qor5/admin/v3/presets/actions" ) type ( @@ -212,7 +213,9 @@ func (b *DetailingBuilder) defaultPageFunc(ctx *web.EventContext) (r web.PageRes ).ModelValue(true).Location("top").Color("success") } - comp := web.Scope(b.ToComponent(b.mb.Info(), obj, ctx)).VSlot("{form}") + comp := web.Scope( + b.ToComponent(b.mb.Info(), obj, ctx), + ).VSlot("{form}") tabsContent := defaultToPage(commonPageConfig{ formContent: comp, tabPanels: b.tabPanels, diff --git a/presets/editing.go b/presets/editing.go index e7af79a49..ade005abe 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -3,13 +3,15 @@ package presets import ( "fmt" "strings" + "time" - "github.com/qor5/admin/v3/presets/actions" "github.com/qor5/web/v3" "github.com/qor5/x/v3/perm" . "github.com/qor5/x/v3/ui/vuetify" vx "github.com/qor5/x/v3/ui/vuetifyx" h "github.com/theplant/htmlgo" + + "github.com/qor5/admin/v3/presets/actions" ) type EditingBuilder struct { @@ -24,7 +26,6 @@ type EditingBuilder struct { sidePanel ObjectComponentFunc actionsFunc ObjectComponentFunc editingTitleFunc EditingTitleComponentFunc - onChangeAction OnChangeActionFunc idCurrentActiveProcessor IdCurrentActiveProcessor FieldsBuilder } @@ -147,11 +148,6 @@ func (b *EditingBuilder) SetterFunc(v SetterFunc) (r *EditingBuilder) { return b } -func (b *EditingBuilder) OnChangeActionFunc(v OnChangeActionFunc) (r *EditingBuilder) { - b.onChangeAction = v - return b -} - func (b *EditingBuilder) WrapSetterFunc(w func(in SetterFunc) SetterFunc) (r *EditingBuilder) { b.Setter = w(b.Setter) return b @@ -252,11 +248,13 @@ func (b *EditingBuilder) singletonPageFunc(ctx *web.EventContext) (r web.PageRes } func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.HTMLComponent { - msgr := MustGetMessages(ctx.R) - id := ctx.R.FormValue(ParamID) - overlayType := ctx.R.FormValue(ParamOverlay) - isAutoSave := b.onChangeAction != nil && overlayType == actions.Content - onChangeEvent := fmt.Sprintf(`if (vars.%s) { vars.%s.editing=true };`, VarsPresetsDataChanged, VarsPresetsDataChanged) + var ( + msgr = MustGetMessages(ctx.R) + id = ctx.R.FormValue(ParamID) + overlayType = ctx.R.FormValue(ParamOverlay) + onChangeEvent = fmt.Sprintf(`if (vars.%s) { vars.%s.editing=true };`, VarsPresetsDataChanged, VarsPresetsDataChanged) + autosave = overlayType == actions.Content + ) if b.mb.singleton { id = vx.ObjectID(obj) } @@ -359,9 +357,23 @@ func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.H formContent := web.Scope(h.Components( VCardText( h.Components(hiddenComps...), + h.Div().Style("display:none").Attr("v-on-mounted", `({window})=>{ + vars.__FormUpdatingFunc = ()=>{ vars.__FormFieldIsUpdating = true} + vars.__FormUpdatedFunc = ()=>{ window.setTimeout(()=>{vars.__FormFieldIsUpdating = false},600)} + }`), + web.Listen(b.mb.NotifModelsValidate(), + ` + vars.__FormUpdatingFunc(); + for (const key in payload.form){ + form[key] = payload.form[key] + } + vars.__FormUpdatedFunc(); +`, + ), b.ToComponent(b.mb.Info(), obj, ctx), + ), - h.If(!isAutoSave, VCardActions(actionButtons)), + h.If(!autosave, VCardActions(actionButtons)), )) asideContent := defaultToPage(commonPageConfig{ @@ -382,7 +394,7 @@ func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.H VToolbarTitle("").Class("pl-2"). Children(title), VSpacer(), - h.If(!isAutoSave, VBtn("").Icon(true).Children( + h.If(!autosave, VBtn("").Icon(true).Children( VIcon("mdi-close"), ).Attr("@click.stop", closeBtnVarScript)), ).Color("white").Elevation(0), @@ -394,10 +406,83 @@ func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.H ), ), ).VSlot("{ form }") - if isAutoSave { - return scope.OnChange(onChangeEvent + b.onChangeAction(id, ctx)) + operateID := fmt.Sprint(time.Now().UnixNano()) + if autosave { + onChangeEvent += fmt.Sprintf(`if (!vars.__FormFieldIsUpdating){%s}`, web.Plaid().URL(ctx.R.URL.Path). + BeforeScript(fmt.Sprintf(`vars.__ValidateOperateID=%q`, operateID)). + EventFunc(actions.Validate). + Query(ParamID, id). + Query(ParamOperateID, operateID). + Query(ParamOverlay, ctx.Param(ParamOverlay)). + Go()) + } else { + onChangeEvent += fmt.Sprintf(`if (!vars.__FormFieldIsUpdating){ + let differences = {}; + for (let key in form) { + if (key.endsWith(%q)){continue} + if (form[key] !== oldForm[key]) { + differences[key] = form[key]?form[key]:""; + } +} +%s +}`, ErrorMessagePostfix, + web.Plaid().URL(ctx.R.URL.Path). + BeforeScript(fmt.Sprintf(`vars.__ValidateOperateID=%q`, operateID)). + EventFunc(actions.Validate). + Form(web.Var("differences")). + Query(ParamID, id). + Query(ParamOperateID, operateID). + Query(ParamOverlay, ctx.Param(ParamOverlay)). + Go()) + } + return scope.OnChange(onChangeEvent).UseDebounce(500) +} +func (b *EditingBuilder) doValidate(ctx *web.EventContext) (r web.EventResponse, err error) { + var ( + id = ctx.Param(ParamID) + operateID = ctx.Param(ParamOperateID) + obj = b.mb.NewModelById(id) + vErr web.ValidationErrors + usingB = b + ) + + if b.mb.creating != nil && id == "" { + usingB = b.mb.creating + } + + defer func() { + web.AppendRunScripts(&r, + fmt.Sprintf(`if (vars.__ValidateOperateID==%q){%s}`, operateID, + web.Emit( + b.mb.NotifModelsValidate(), + PayloadModelsSetter{ + Form: b.ToErrorMessagesForm(ctx, &vErr), + Id: id, + Passed: !vErr.HaveErrors(), + }, + )), + ) + + if vErr.HaveErrors() && len(vErr.GetGlobalErrors()) > 0 { + web.AppendRunScripts(&r, ShowSnackbarScript(strings.Join(vErr.GetGlobalErrors(), ";"), "error")) + } + }() + + vErr = usingB.RunSetterFunc(ctx, true, obj) + if vErr.HaveErrors() { + return } - return scope.OnChange(onChangeEvent).UseDebounce(150) + + if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { + vErr.GlobalError(perm.PermissionDenied.Error()) + return + } + if usingB.Validator != nil { + if vErr = usingB.Validator(obj, ctx); vErr.HaveErrors() { + return + } + } + return } func (b *EditingBuilder) doDelete(ctx *web.EventContext) (r web.EventResponse, err1 error) { @@ -453,7 +538,7 @@ func (b *EditingBuilder) FetchAndUnmarshal(id string, removeDeletedAndSort bool, func (b *EditingBuilder) doUpdate( ctx *web.EventContext, r *web.EventResponse, - // will not close drawer/dialog +// will not close drawer/dialog silent bool, ) (created bool, err error) { id := ctx.R.FormValue(ParamID) diff --git a/presets/field.go b/presets/field.go index f1db10615..e8440e680 100644 --- a/presets/field.go +++ b/presets/field.go @@ -869,8 +869,9 @@ func (b *FieldsBuilder) toComponentWithFormValueKey(info *ModelInfo, obj interfa } comps = append(comps, comp) } - - return h.Components(comps...) + return h.Components( + comps..., + ) } func (b *FieldsBuilder) fieldToComponentWithFormValueKey(info *ModelInfo, obj interface{}, parentFormValueKey string, ctx *web.EventContext, name string, edit bool, vErr *web.ValidationErrors) h.HTMLComponent { @@ -902,7 +903,7 @@ func (b *FieldsBuilder) fieldToComponentWithFormValueKey(info *ModelInfo, obj in Name: f.name, FormKey: contextKeyPath, Label: label, - Errors: vErr.GetFieldErrors(f.name), + Errors: vErr.GetFieldErrors(contextKeyPath), NestedFieldsBuilder: f.nestedFieldsBuilder, Context: f.context, Disabled: disabled, @@ -915,6 +916,27 @@ func defaultRowFunc(obj interface{}, formKey string, content h.HTMLComponent, ct return content } +func (b *FieldsBuilder) ToErrorMessagesForm(ctx *web.EventContext, vErr *web.ValidationErrors) interface{} { + form := make(map[string]interface{}) + + for k, _ := range ctx.R.PostForm { + if k == web.EventFuncIDName { + continue + } + if strings.HasSuffix(k, "]") { + k = k[:strings.LastIndexAny(k, "[")] + } + key := k + ErrorMessagePostfix + if _, ok := form[key]; ok { + continue + } + form[key] = vErr.GetFieldErrors(k) + + } + + return form +} + func (b *FieldsBuilder) ToComponentForEach(field *FieldContext, slice interface{}, ctx *web.EventContext, rowFunc RowFunc) h.HTMLComponent { var info *ModelInfo parentKeyPath := "" diff --git a/presets/field_defaults.go b/presets/field_defaults.go index ade0b29b4..74983bc6b 100644 --- a/presets/field_defaults.go +++ b/presets/field_defaults.go @@ -7,10 +7,11 @@ import ( "time" "github.com/iancoleman/strcase" - "github.com/qor5/web/v3" "github.com/sunfmin/reflectutils" h "github.com/theplant/htmlgo" + "github.com/qor5/web/v3" + "github.com/qor5/x/v3/i18n" "github.com/qor5/x/v3/ui/vuetifyx" ) @@ -176,18 +177,16 @@ func cfTextTd(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTM func cfCheckbox(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent { return vuetifyx.VXCheckbox(). - Attr(web.VField(field.FormKey, reflectutils.MustGet(obj, field.Name).(bool))...). + Attr(VFieldError(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name).(bool)), field.Errors)...). Label(field.Label). - ErrorMessages(field.Errors...). Disabled(field.Disabled) } func cfNumber(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent { return vuetifyx.VXField(). Type("number"). - Attr(web.VField(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)))...). + Attr(VFieldError(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)), field.Errors)...). Label(field.Label). - ErrorMessages(field.Errors...). Disabled(field.Disabled) } @@ -214,7 +213,7 @@ func DateTimePicker(obj interface{}, field *FieldContext, ctx *web.EventContext) } return vuetifyx.VXDateTimePicker(). Label(field.Label). - Attr(web.VField(field.FormKey, val)...). + Attr(VFieldError(field.FormKey, val, field.Errors)...). Value(val). TimePickerProps(vuetifyx.TimePickerProps{ Format: "24hr", @@ -222,8 +221,7 @@ func DateTimePicker(obj interface{}, field *FieldContext, ctx *web.EventContext) }). ClearText(msgr.Clear). OkText(msgr.OK). - Disabled(field.Disabled). - ErrorMessages(field.Errors...) + Disabled(field.Disabled) } func cfTimeSetter(obj interface{}, field *FieldContext, ctx *web.EventContext) (err error) { @@ -245,11 +243,23 @@ func DateTimeSetter(obj interface{}, field *FieldContext, ctx *web.EventContext) func cfTextField(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent { return TextField(obj, field, ctx) } +func VFieldError(name string, value interface{}, error interface{}) []interface{} { + errKey := name + ErrorMessagePostfix + objValue := map[string]interface{}{ + name: value, + errKey: error, + } + return append([]interface{}{ + "v-model", + fmt.Sprintf("form[%s]", h.JSONString(name)), + ":error-messages", + fmt.Sprintf("form[%q]", errKey), + }, web.VAssign("form", objValue)...) +} func TextField(obj interface{}, field *FieldContext, ctx *web.EventContext) *vuetifyx.VXFieldBuilder { return vuetifyx.VXField().Label(field.Label). - Attr(web.VField(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)))...). - ErrorMessages(field.Errors...). + Attr(VFieldError(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)), field.Errors)...). Disabled(field.Disabled) } @@ -257,8 +267,8 @@ func SelectField(obj interface{}, field *FieldContext, ctx *web.EventContext) *v return vuetifyx.VXSelect(). Label(field.Label). Disabled(field.Disabled). - Attr(web.VField(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)))...). - ErrorMessages(field.Errors...) + Attr(VFieldError(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)), field.Errors)...) + } func ReadonlyText(obj interface{}, field *FieldContext, ctx *web.EventContext) *vuetifyx.VXReadonlyFieldBuilder { diff --git a/presets/integration/example_test.go b/presets/integration/example_test.go index 3a7b53b3e..9c38d114c 100644 --- a/presets/integration/example_test.go +++ b/presets/integration/example_test.go @@ -6,13 +6,14 @@ import ( "strings" "testing" - "github.com/qor5/admin/v3/presets" - "github.com/qor5/admin/v3/presets/actions" - "github.com/qor5/admin/v3/presets/examples" . "github.com/qor5/web/v3/multipartestutils" "github.com/theplant/gofixtures" "github.com/theplant/testenv" "gorm.io/gorm" + + "github.com/qor5/admin/v3/presets" + "github.com/qor5/admin/v3/presets/actions" + "github.com/qor5/admin/v3/presets/examples" ) var TestDB *gorm.DB @@ -108,13 +109,7 @@ func TestExample(t *testing.T) { EventFunc(actions.New). BuildEventFuncRequest() }, - EventResponseMatch: func(t *testing.T, er *TestEventResponse) { - partial := er.UpdatePortals[0].Body - if !strings.Contains(partial, `v-model='form["Number"]' v-assign='[form, {"Number":""}]'`) { - t.Error(`v-model='form["Number"]' v-assign='[form, {"Number":""}]'`, partial) - } - return - }, + ExpectPortalUpdate0ContainsInOrder: []string{`v-model='form["Number"]`, `v-assign='[form, {"Number":""`}, }, { @@ -152,13 +147,7 @@ func TestExample(t *testing.T) { Query(presets.ParamID, "12"). BuildEventFuncRequest() }, - EventResponseMatch: func(t *testing.T, er *TestEventResponse) { - partial := er.UpdatePortals[0].Body - if !strings.Contains(partial, `v-model='form["OwnerName"]' v-assign='[form, {"OwnerName":""}]'`) { - t.Error(`can't find v-model='form["OwnerName"]' v-assign='[form, {"OwnerName":""}]'`, partial) - } - return - }, + ExpectPortalUpdate0ContainsInOrder: []string{`v-model='form["OwnerName"]`, `v-assign='[form, {"OwnerName":""`}, }, { diff --git a/presets/integration/fields_test.go b/presets/integration/fields_test.go index c2b25f5a6..4d1113ec2 100644 --- a/presets/integration/fields_test.go +++ b/presets/integration/fields_test.go @@ -108,17 +108,17 @@ func TestFields(t *testing.T) { ctx) }, expect: ` - + - + - + - +
- +
`, @@ -132,11 +132,11 @@ func TestFields(t *testing.T) { ToComponent(mb.Info(), user, ctx) }, expect: ` - + - + - +
`, @@ -201,9 +201,9 @@ func TestFields(t *testing.T) { &web.EventContext{R: r, Flash: vd}) }, expect: fmt.Sprintf(` - + - + `, time1LocalFormatMinute, time1LocalFormatMinute, time1LocalFormatMinute, time1LocalFormatMinute), }, { @@ -225,9 +225,9 @@ func TestFields(t *testing.T) { &web.EventContext{R: r, Flash: vd}) }, expect: fmt.Sprintf(` - + - + `, "", "", "", ""), }, { @@ -249,9 +249,9 @@ func TestFields(t *testing.T) { &web.EventContext{R: r, Flash: vd}) }, expect: fmt.Sprintf(` - + - + `, "", "", "", ""), }, { @@ -369,22 +369,22 @@ func addressHTML(v Address, formKeyPrefix string) string { - +
- + - +
`, - formKeyPrefix, formKeyPrefix, v.City, - formKeyPrefix, formKeyPrefix, v.Detail.Address1, - formKeyPrefix, formKeyPrefix, v.Detail.Address2, + formKeyPrefix, formKeyPrefix, formKeyPrefix, v.City, formKeyPrefix, + formKeyPrefix, formKeyPrefix, formKeyPrefix, v.Detail.Address1, formKeyPrefix, + formKeyPrefix, formKeyPrefix, formKeyPrefix, v.Detail.Address2, formKeyPrefix, ) } @@ -543,7 +543,7 @@ func TestFieldsBuilder(t *testing.T) { %s - + `, addressHTML(Address{}, "Departments[0].Employees[0]."), addressHTML(Address{}, "Departments[0].Employees[2]."), @@ -649,7 +649,7 @@ func TestFieldsBuilder(t *testing.T) { %s - + `, addressHTML(Address{}, "Departments[0].Employees[2]."), addressHTML(Address{}, "Departments[0].Employees[0]."), diff --git a/presets/listeditor.go b/presets/listeditor.go index 13f6aac48..ed93969ba 100644 --- a/presets/listeditor.go +++ b/presets/listeditor.go @@ -93,7 +93,7 @@ func (b *ListEditorBuilder) MarshalHTML(c context.Context) (r []byte, err error) form = b.fieldContext.NestedFieldsBuilder.ToComponentForEach(b.fieldContext, b.value, ctx, func(obj interface{}, formKey string, content h.HTMLComponent, ctx *web.EventContext) h.HTMLComponent { return VCard( h.If(!b.fieldContext.Disabled, - VBtn("").Icon("mdi-delete").Class("position-absolute right-0 top-0 mt-6 mr-1"). + VBtn("").Icon("mdi-delete").Class("float-right ma-2"). Attr("@click", web.Plaid(). URL(b.fieldContext.ModelInfo.ListingHref()). EventFunc(b.removeListItemRowEvent). @@ -105,7 +105,7 @@ func (b *ListEditorBuilder) MarshalHTML(c context.Context) (r []byte, err error) Go()), ), content, - ).Class("mx-0 mb-2 px-4 pb-0 pt-4 position-relative").Variant(VariantOutlined) + ).Class("mx-0 mb-2 px-4 pb-0 pt-4").Variant(VariantOutlined) }) } diff --git a/presets/listener.go b/presets/listener.go index 7cade8dc9..a996cc80d 100644 --- a/presets/listener.go +++ b/presets/listener.go @@ -48,3 +48,16 @@ func (mb *ModelBuilder) NotifRowUpdated() string { type PayloadRowUpdated struct { Id string `json:"id"` } + +func (mb *ModelBuilder) NotifModelsValidate() string { + return fmt.Sprintf("presets_NotifModelsValidate_%T", mb.model) +} +func (mb *ModelBuilder) NotifModelsSectionValidate(name string) string { + return fmt.Sprintf("presets_NotifModelsValidate_%v_%T", name, mb.model) +} + +type PayloadModelsSetter struct { + Id string `json:"id"` + Form interface{} `json:"form"` + Passed bool `json:"passed"` +} diff --git a/presets/model.go b/presets/model.go index 94229d0c2..0c7786774 100644 --- a/presets/model.go +++ b/presets/model.go @@ -9,11 +9,13 @@ import ( "github.com/iancoleman/strcase" "github.com/jinzhu/inflection" - "github.com/qor5/admin/v3/presets/actions" "github.com/qor5/web/v3" "github.com/qor5/x/v3/i18n" "github.com/qor5/x/v3/perm" + "github.com/sunfmin/reflectutils" h "github.com/theplant/htmlgo" + + "github.com/qor5/admin/v3/presets/actions" ) type ModelBuilder struct { @@ -97,6 +99,7 @@ func (mb *ModelBuilder) LabelName(f func(evCtx *web.EventContext, singular bool) func (mb *ModelBuilder) registerDefaultEventFuncs() { mb.RegisterEventFunc(actions.New, mb.editing.formNew) mb.RegisterEventFunc(actions.Edit, mb.editing.formEdit) + mb.RegisterEventFunc(actions.Validate, mb.editing.doValidate) mb.RegisterEventFunc(actions.Update, mb.editing.defaultUpdate) mb.RegisterEventFunc(actions.DoDelete, mb.editing.doDelete) @@ -303,3 +306,19 @@ func (mb *ModelBuilder) getLabel(field NameLabel) (r string) { return humanizeString(field.name) } + +func (mb *ModelBuilder) NewModelById(id string) (r interface{}) { + r = reflect.New(mb.modelType.Elem()).Interface() + if id == "" { + return + } + if slugger, ok := r.(SlugDecoder); ok { + cs := slugger.PrimaryColumnValuesBySlug(id) + for k, v := range cs { + _ = reflectutils.Set(r, toPascalCase(k), v) + } + } else { + _ = reflectutils.Set(r, toPascalCase(ParamID), id) + } + return +} diff --git a/presets/section.go b/presets/section.go index b6344d3cd..e27684347 100644 --- a/presets/section.go +++ b/presets/section.go @@ -8,6 +8,7 @@ import ( "reflect" "strconv" "strings" + "time" "github.com/qor5/web/v3" "github.com/qor5/x/v3/i18n" @@ -425,6 +426,7 @@ func (b *SectionBuilder) registerEvent() { b.isRegistered = true b.mb.RegisterEventFunc(b.EventSave(), b.SaveDetailField) b.mb.RegisterEventFunc(b.EventEdit(), b.EditDetailField) + b.mb.RegisterEventFunc(b.EventValidate(), b.ValidateDetailField) b.mb.RegisterEventFunc(b.EventDelete(), b.DeleteDetailListField) b.mb.RegisterEventFunc(b.EventCreate(), b.CreateDetailListField) } @@ -436,6 +438,9 @@ func (b *SectionBuilder) EventEdit() string { func (b *SectionBuilder) EventSave() string { return fmt.Sprintf("section_save_%s", b.name) } +func (b *SectionBuilder) EventValidate() string { + return fmt.Sprintf("section_validate_%s", b.name) +} func (b *SectionBuilder) EventDelete() string { return fmt.Sprintf("section_delete_%s", b.name) @@ -567,19 +572,55 @@ func (b *SectionBuilder) editComponent(obj interface{}, field *FieldContext, ctx ).Class("section-body"), ) } + operateID := fmt.Sprint(time.Now().UnixNano()) + onChangeEvent += fmt.Sprintf(`if (!vars.__FormFieldIsUpdating){ + let differences = {}; + for (let key in oldForm) { + if (key.endsWith(%q)){continue} + if (form[key] !== oldForm[key]) { + differences[key] = form[key]?form[key]:""; + } + } +%s +}`, ErrorMessagePostfix, + web.Plaid().URL(ctx.R.URL.Path). + BeforeScript(fmt.Sprintf(`vars.__ValidateOperateID=%q`, operateID)). + EventFunc(b.EventValidate()). + Form(web.Var("differences")). + Query(ParamID, id). + Query(ParamOperateID, operateID). + Go()) + + comps := h.Components( + h.Div().Style("display:none").Attr("v-on-mounted", `({window})=>{ + vars.__FormUpdatingFunc = ()=>{ vars.__FormFieldIsUpdating = true} + vars.__FormUpdatedFunc = ()=>{ window.setTimeout(()=>{vars.__FormFieldIsUpdating = false},600)} + }`), + web.Listen(b.mb.NotifModelsSectionValidate(b.name), + ` + vars.__FormUpdatingFunc(); + for (const key in payload.form){ + form[key] = payload.form[key] + } + vars.__FormUpdatedFunc();`, + ), + ) + if b.isEdit { return h.Div( web.Scope( + comps, content, hiddenComp, - ).OnChange(onChangeEvent).UseDebounce(150), + ).OnChange(onChangeEvent).UseDebounce(500), ) } return h.Div( web.Scope( + comps, content, hiddenComp, - ).VSlot("{ form }").OnChange(onChangeEvent).UseDebounce(150), + ).VSlot("{ form }").OnChange(onChangeEvent).UseDebounce(500), ) } @@ -1077,6 +1118,53 @@ func (b *SectionBuilder) SaveDetailField(ctx *web.EventContext) (r web.EventResp return r, nil } +func (b *SectionBuilder) ValidateDetailField(ctx *web.EventContext) (r web.EventResponse, err error) { + var ( + id = ctx.Param(ParamID) + operateID = ctx.Param(ParamOperateID) + obj = b.mb.NewModelById(id) + vErr web.ValidationErrors + ) + if b.setter != nil { + b.setter(obj, ctx) + } + vErr = b.editingFB.Unmarshal(obj, b.mb.Info(), true, ctx) + + if vErr.HaveErrors() { + return + } + defer func() { + web.AppendRunScripts(&r, + fmt.Sprintf(`if (vars.__ValidateOperateID==%q){%s}`, operateID, + web.Emit( + b.mb.NotifModelsSectionValidate(b.name), + PayloadModelsSetter{ + Form: b.editingFB.ToErrorMessagesForm(ctx, &vErr), + Id: id, + Passed: !vErr.HaveErrors(), + }, + ), + ), + ) + if vErr.HaveErrors() && len(vErr.GetGlobalErrors()) > 0 { + web.AppendRunScripts(&r, ShowSnackbarScript(strings.Join(vErr.GetGlobalErrors(), ";"), "error")) + } + + }() + + if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { + vErr.GlobalError(perm.PermissionDenied.Error()) + return + } + if b.validator != nil { + if vErr = b.validator(obj, ctx); vErr.HaveErrors() { + return + } + } + + return +} + // EditDetailListField Event: click detail list field element edit button func (b *SectionBuilder) EditDetailListField(ctx *web.EventContext) (r web.EventResponse, err error) { var index int64 diff --git a/presets/utils.go b/presets/utils.go index 794077b35..1ae679f42 100644 --- a/presets/utils.go +++ b/presets/utils.go @@ -5,15 +5,18 @@ import ( "fmt" "net/url" "reflect" + "strings" "time" + "unicode" "github.com/pkg/errors" - "github.com/qor5/admin/v3/presets/actions" "github.com/qor5/web/v3" . "github.com/qor5/x/v3/ui/vuetify" vx "github.com/qor5/x/v3/ui/vuetifyx" "github.com/sunfmin/reflectutils" h "github.com/theplant/htmlgo" + + "github.com/qor5/admin/v3/presets/actions" ) func RecoverPrimaryColumnValuesBySlug(dec SlugDecoder, slug string) (r map[string]string, err error) { @@ -174,3 +177,22 @@ func MustJsonCopy(dst, src any) { panic(err) } } + +func toPascalCase(s string) string { + var result strings.Builder + shouldCapitalize := true + + for _, char := range s { + if char == '_' { + shouldCapitalize = true + } else { + if shouldCapitalize { + result.WriteRune(unicode.ToUpper(char)) + shouldCapitalize = false + } else { + result.WriteRune(char) + } + } + } + return result.String() +} From 2a2d1298a306cfaf7e0ec45b514025be3e1a5e6d Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 02:39:55 +0000 Subject: [PATCH 02/15] style: format code with Gofumpt This commit fixes the style issues introduced in 32c4110 according to the output from Gofumpt. Details: https://github.com/qor5/admin/pull/692 --- example/admin/demo_case.go | 3 +-- presets/editing.go | 4 ++-- presets/field.go | 2 +- presets/field_defaults.go | 3 ++- presets/listener.go | 1 + presets/section.go | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/example/admin/demo_case.go b/example/admin/demo_case.go index 67289fd2d..10b2323ac 100644 --- a/example/admin/demo_case.go +++ b/example/admin/demo_case.go @@ -636,6 +636,7 @@ func avatarView[T comparable](sizes []T, show func(T) string) (comps []h.HTMLCom } return } + func cardRows(title string, splitCols int, comp ...h.HTMLComponent) *v.VCardBuilder { var ( rows []h.HTMLComponent @@ -684,7 +685,6 @@ func DemoCaseTextField(obj interface{}, editField, field, label string, vErr web return vx.VXField(). Label(label). Attr(presets.VFieldError(formKey, reflectutils.MustGet(obj, formKey), vErr.GetFieldErrors(formKey))...) - } func DemoCaseSelect(obj interface{}, editField, field, label string, vErr web.ValidationErrors, items interface{}) *vx.VXSelectBuilder { @@ -695,7 +695,6 @@ func DemoCaseSelect(obj interface{}, editField, field, label string, vErr web.Va ItemTitle("Name"). ItemValue("ID"). Attr(presets.VFieldError(formKey, reflectutils.MustGet(obj, formKey), vErr.GetFieldErrors(formKey))...) - } func DemoCaseCheckBox(obj interface{}, editField, field, label string) *vx.VXCheckboxBuilder { diff --git a/presets/editing.go b/presets/editing.go index ade005abe..8591c1e18 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -371,7 +371,6 @@ func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.H `, ), b.ToComponent(b.mb.Info(), obj, ctx), - ), h.If(!autosave, VCardActions(actionButtons)), )) @@ -437,6 +436,7 @@ func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.H } return scope.OnChange(onChangeEvent).UseDebounce(500) } + func (b *EditingBuilder) doValidate(ctx *web.EventContext) (r web.EventResponse, err error) { var ( id = ctx.Param(ParamID) @@ -538,7 +538,7 @@ func (b *EditingBuilder) FetchAndUnmarshal(id string, removeDeletedAndSort bool, func (b *EditingBuilder) doUpdate( ctx *web.EventContext, r *web.EventResponse, -// will not close drawer/dialog + // will not close drawer/dialog silent bool, ) (created bool, err error) { id := ctx.R.FormValue(ParamID) diff --git a/presets/field.go b/presets/field.go index e8440e680..23e36a877 100644 --- a/presets/field.go +++ b/presets/field.go @@ -919,7 +919,7 @@ func defaultRowFunc(obj interface{}, formKey string, content h.HTMLComponent, ct func (b *FieldsBuilder) ToErrorMessagesForm(ctx *web.EventContext, vErr *web.ValidationErrors) interface{} { form := make(map[string]interface{}) - for k, _ := range ctx.R.PostForm { + for k := range ctx.R.PostForm { if k == web.EventFuncIDName { continue } diff --git a/presets/field_defaults.go b/presets/field_defaults.go index 74983bc6b..974c5b5b8 100644 --- a/presets/field_defaults.go +++ b/presets/field_defaults.go @@ -243,6 +243,7 @@ func DateTimeSetter(obj interface{}, field *FieldContext, ctx *web.EventContext) func cfTextField(obj interface{}, field *FieldContext, ctx *web.EventContext) h.HTMLComponent { return TextField(obj, field, ctx) } + func VFieldError(name string, value interface{}, error interface{}) []interface{} { errKey := name + ErrorMessagePostfix @@ -257,6 +258,7 @@ func VFieldError(name string, value interface{}, error interface{}) []interface{ fmt.Sprintf("form[%q]", errKey), }, web.VAssign("form", objValue)...) } + func TextField(obj interface{}, field *FieldContext, ctx *web.EventContext) *vuetifyx.VXFieldBuilder { return vuetifyx.VXField().Label(field.Label). Attr(VFieldError(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)), field.Errors)...). @@ -268,7 +270,6 @@ func SelectField(obj interface{}, field *FieldContext, ctx *web.EventContext) *v Label(field.Label). Disabled(field.Disabled). Attr(VFieldError(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)), field.Errors)...) - } func ReadonlyText(obj interface{}, field *FieldContext, ctx *web.EventContext) *vuetifyx.VXReadonlyFieldBuilder { diff --git a/presets/listener.go b/presets/listener.go index a996cc80d..6552e044d 100644 --- a/presets/listener.go +++ b/presets/listener.go @@ -52,6 +52,7 @@ type PayloadRowUpdated struct { func (mb *ModelBuilder) NotifModelsValidate() string { return fmt.Sprintf("presets_NotifModelsValidate_%T", mb.model) } + func (mb *ModelBuilder) NotifModelsSectionValidate(name string) string { return fmt.Sprintf("presets_NotifModelsValidate_%v_%T", name, mb.model) } diff --git a/presets/section.go b/presets/section.go index e27684347..9a012c05b 100644 --- a/presets/section.go +++ b/presets/section.go @@ -438,6 +438,7 @@ func (b *SectionBuilder) EventEdit() string { func (b *SectionBuilder) EventSave() string { return fmt.Sprintf("section_save_%s", b.name) } + func (b *SectionBuilder) EventValidate() string { return fmt.Sprintf("section_validate_%s", b.name) } @@ -1149,7 +1150,6 @@ func (b *SectionBuilder) ValidateDetailField(ctx *web.EventContext) (r web.Event if vErr.HaveErrors() && len(vErr.GetGlobalErrors()) > 0 { web.AppendRunScripts(&r, ShowSnackbarScript(strings.Join(vErr.GetGlobalErrors(), ";"), "error")) } - }() if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { From 2fda6550424697a39049ba7239b8861e8298e13e Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 31 Oct 2024 11:49:08 +0800 Subject: [PATCH 03/15] errkey to errorKey --- presets/field_defaults.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/presets/field_defaults.go b/presets/field_defaults.go index 974c5b5b8..1864edab6 100644 --- a/presets/field_defaults.go +++ b/presets/field_defaults.go @@ -245,17 +245,18 @@ func cfTextField(obj interface{}, field *FieldContext, ctx *web.EventContext) h. } func VFieldError(name string, value interface{}, error interface{}) []interface{} { - errKey := name + ErrorMessagePostfix - - objValue := map[string]interface{}{ - name: value, - errKey: error, - } + var ( + errorKey = name + ErrorMessagePostfix + objValue = map[string]interface{}{ + name: value, + errorKey: error, + } + ) return append([]interface{}{ "v-model", fmt.Sprintf("form[%s]", h.JSONString(name)), ":error-messages", - fmt.Sprintf("form[%q]", errKey), + fmt.Sprintf("form[%q]", errorKey), }, web.VAssign("form", objValue)...) } From d010a13dad61bc4959cbb6a85a60b956d28e9f4f Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 31 Oct 2024 15:35:43 +0800 Subject: [PATCH 04/15] reopen page detail back button;validate add fetcher --- example/integration/pagebuilder_test.go | 120 +++++++++++++++--------- pagebuilder/page.go | 22 ++--- presets/editing.go | 44 +++++---- presets/field_defaults.go | 4 +- presets/model.go | 17 ---- presets/section.go | 50 ++++++---- presets/utils.go | 21 ----- 7 files changed, 149 insertions(+), 129 deletions(-) diff --git a/example/integration/pagebuilder_test.go b/example/integration/pagebuilder_test.go index ea3eb1d32..188877cd2 100644 --- a/example/integration/pagebuilder_test.go +++ b/example/integration/pagebuilder_test.go @@ -249,11 +249,11 @@ func TestPageBuilder(t *testing.T) { t.Fatalf("Page not duplicated %v", pages) return } - var containers []*pagebuilder.Container - TestDB.Find(&containers, "page_id = ? AND page_version = ?", pages[0].ID, + var cons []*pagebuilder.Container + TestDB.Find(&cons, "page_id = ? AND page_version = ?", pages[0].ID, pages[0].Version.Version) - if len(containers) == 0 { - t.Error("Container not duplicated", containers) + if len(cons) == 0 { + t.Error("Container not duplicated", cons) } }, }, @@ -281,11 +281,11 @@ func TestPageBuilder(t *testing.T) { t.Fatalf("Page not duplicated %v", pages) return } - var containers []*pagebuilder.Container - TestDB.Find(&containers, "page_id = ? AND page_version = ?", pages[0].ID, + var cons []*pagebuilder.Container + TestDB.Find(&cons, "page_id = ? AND page_version = ?", pages[0].ID, pages[0].Version.Version) - if len(containers) == 0 { - t.Error("Container not duplicated", containers) + if len(cons) == 0 { + t.Error("Container not duplicated", cons) } }, }, @@ -326,13 +326,13 @@ func TestPageBuilder(t *testing.T) { return req }, EventResponseMatch: func(t *testing.T, er *TestEventResponse) { - var containers []pagebuilder.Container - TestDB.Order("display_order asc").Find(&containers) - if len(containers) != 3 { - t.Error("containers not add", containers) + var cons []pagebuilder.Container + TestDB.Order("display_order asc").Find(&cons) + if len(cons) != 3 { + t.Error("containers not add", cons) } - if containers[0].ModelName != "ListContent" || containers[1].ModelName != "Header" || containers[2].ModelName != "BrandGrid" { - t.Error("containers not add under", containers) + if cons[0].ModelName != "ListContent" || cons[1].ModelName != "Header" || cons[2].ModelName != "BrandGrid" { + t.Error("containers not add under", cons) } }, }, @@ -351,14 +351,14 @@ func TestPageBuilder(t *testing.T) { return req }, EventResponseMatch: func(t *testing.T, er *TestEventResponse) { - var containers []pagebuilder.Container - TestDB.Order("display_order asc").Find(&containers) - if len(containers) != 3 { - t.Fatalf("containers not add %#+v", containers) + var cons []pagebuilder.Container + TestDB.Order("display_order asc").Find(&cons) + if len(cons) != 3 { + t.Fatalf("cons not add %#+v", cons) return } - if containers[0].ModelName != "ListContent" || containers[1].ModelName != "BrandGrid" || containers[2].ModelName != "Header" { - t.Fatalf("containers not add under %#+v", containers) + if cons[0].ModelName != "ListContent" || cons[1].ModelName != "BrandGrid" || cons[2].ModelName != "Header" { + t.Fatalf("containers not add under %#+v", cons) return } }, @@ -393,10 +393,10 @@ func TestPageBuilder(t *testing.T) { return req }, EventResponseMatch: func(t *testing.T, er *TestEventResponse) { - var containers []pagebuilder.Container - TestDB.Order("display_order asc").Find(&containers) - if len(containers) != 1 { - t.Fatalf("containers not delete %#+v", containers) + var cons []pagebuilder.Container + TestDB.Order("display_order asc").Find(&cons) + if len(cons) != 1 { + t.Fatalf("containers not delete %#+v", cons) return } }, @@ -469,14 +469,14 @@ func TestPageBuilder(t *testing.T) { return req }, EventResponseMatch: func(t *testing.T, er *TestEventResponse) { - var containers []pagebuilder.Container - TestDB.Order("display_order asc").Find(&containers) - if len(containers) != 2 { - t.Error("containers not add", containers) + var cons []pagebuilder.Container + TestDB.Order("display_order asc").Find(&cons) + if len(cons) != 2 { + t.Error("containers not add", cons) return } - if containers[0].ModelName != "Header" || containers[1].ModelName != "ListContent" { - t.Error("container not move down", containers) + if cons[0].ModelName != "Header" || cons[1].ModelName != "ListContent" { + t.Error("container not move down", cons) return } }, @@ -496,14 +496,14 @@ func TestPageBuilder(t *testing.T) { return req }, EventResponseMatch: func(t *testing.T, er *TestEventResponse) { - var containers []pagebuilder.Container - TestDB.Order("display_order asc").Find(&containers) - if len(containers) != 2 { - t.Error("containers not add", containers) + var cons []pagebuilder.Container + TestDB.Order("display_order asc").Find(&cons) + if len(cons) != 2 { + t.Error("containers not add", cons) return } - if containers[0].ModelName != "Header" || containers[1].ModelName != "ListContent" { - t.Error("container not move down", containers) + if cons[0].ModelName != "Header" || cons[1].ModelName != "ListContent" { + t.Error("container not move down", cons) return } }, @@ -523,14 +523,14 @@ func TestPageBuilder(t *testing.T) { return req }, EventResponseMatch: func(t *testing.T, er *TestEventResponse) { - var containers []pagebuilder.Container - TestDB.Order("display_order asc").Find(&containers) - if len(containers) != 2 { - t.Error("containers not add", containers) + var cons []pagebuilder.Container + TestDB.Order("display_order asc").Find(&cons) + if len(cons) != 2 { + t.Error("cons not add", cons) return } - if containers[0].ModelName != "Header" || containers[1].ModelName != "ListContent" { - t.Error("container not sort move", containers) + if cons[0].ModelName != "Header" || cons[1].ModelName != "ListContent" { + t.Error("container not sort move", cons) return } }, @@ -864,6 +864,42 @@ func TestPageBuilder(t *testing.T) { }, ExpectPortalUpdate0ContainsInOrder: []string{"Existing Path"}, }, + { + Name: "Page Category Save Validate By ID Existing Path", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_categories"). + EventFunc(actions.Update). + Query(presets.ParamID, "1_International"). + AddField("Name", "category_123"). + AddField("Path", "45"). + AddField("Description", ""). + AddField("LocaleCode", "International"). + BuildEventFuncRequest() + + return req + }, + ExpectPortalUpdate0ContainsInOrder: []string{"Existing Path"}, + }, + { + Name: "Page Category Validate By ID Existing Path", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_categories"). + EventFunc(actions.Validate). + Query(presets.ParamID, "1_International"). + AddField("LocaleCode", "International"). + AddField("Path", "45"). + BuildEventFuncRequest() + + return req + }, + ExpectRunScriptContainsInOrder: []string{"Existing Path"}, + }, { Name: "Page Category Validate Event Existing Path", Debug: true, diff --git a/pagebuilder/page.go b/pagebuilder/page.go index 96326a343..e4a10c8f0 100644 --- a/pagebuilder/page.go +++ b/pagebuilder/page.go @@ -71,18 +71,18 @@ func (b *Builder) defaultPageInstall(pb *presets.Builder, pm *presets.ModelBuild Color(ColorPrimary).Size(SizeSmall).Class("px-1 mx-1").Attr("style", "height:20px") } - // listingHref := pm.Info().ListingHref() + listingHref := pm.Info().ListingHref() return h.Div( - // VBtn("").Size(SizeXSmall).Icon("mdi-arrow-left").Tile(true).Variant(VariantOutlined).Attr("@click", - // fmt.Sprintf(` - // const last = vars.__history.last(); - // if (last && last.url && last.url.startsWith(%q)) { - // $event.view.window.history.back(); - // return; - // } - // %s`, listingHref, web.GET().URL(listingHref).PushState(true).Go(), - // ), - // ), + VBtn("").Size(SizeXSmall).Icon("mdi-arrow-left").Tile(true).Variant(VariantOutlined).Attr("@click", + fmt.Sprintf(` + const last = vars.__history.last(); + if (last && last.url && last.url.startsWith(%q)) { + $event.view.window.history.back(); + return; + } + %s`, listingHref, web.GET().URL(listingHref).PushState(true).Go(), + ), + ), h.H1("{{vars.pageTitle}}").Class("page-main-title"), versionBadge.Class("mt-2 ml-2"), ).Class("d-inline-flex align-center") diff --git a/presets/editing.go b/presets/editing.go index 8591c1e18..3aeab845c 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -362,14 +362,17 @@ func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.H vars.__FormUpdatedFunc = ()=>{ window.setTimeout(()=>{vars.__FormFieldIsUpdating = false},600)} }`), web.Listen(b.mb.NotifModelsValidate(), - ` - vars.__FormUpdatingFunc(); - for (const key in payload.form){ - form[key] = payload.form[key] - } - vars.__FormUpdatedFunc(); -`, - ), + `vars.__FormUpdatingFunc(); + for (const key in payload.form){ + if (vars.__currentValidateKeys){ + if(vars.__currentValidateKeys.lastIndexOf(key)>=0){ + form[key] = payload.form[key] + } + }else{ + form[key] = payload.form[key] + } + } + vars.__FormUpdatedFunc();`), b.ToComponent(b.mb.Info(), obj, ctx), ), h.If(!autosave, VCardActions(actionButtons)), @@ -408,7 +411,7 @@ func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.H operateID := fmt.Sprint(time.Now().UnixNano()) if autosave { onChangeEvent += fmt.Sprintf(`if (!vars.__FormFieldIsUpdating){%s}`, web.Plaid().URL(ctx.R.URL.Path). - BeforeScript(fmt.Sprintf(`vars.__ValidateOperateID=%q`, operateID)). + BeforeScript(fmt.Sprintf(`vars.__currentValidateKeys=null;vars.__ValidateOperateID=%q`, operateID)). EventFunc(actions.Validate). Query(ParamID, id). Query(ParamOperateID, operateID). @@ -416,19 +419,19 @@ func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.H Go()) } else { onChangeEvent += fmt.Sprintf(`if (!vars.__FormFieldIsUpdating){ - let differences = {}; + vars.__currentValidateKeys = []; + const endKey = %q ; for (let key in form) { - if (key.endsWith(%q)){continue} + if (key.endsWith(endKey)){continue} if (form[key] !== oldForm[key]) { - differences[key] = form[key]?form[key]:""; + vars.__currentValidateKeys.push(key+endKey) } -} + } %s }`, ErrorMessagePostfix, web.Plaid().URL(ctx.R.URL.Path). BeforeScript(fmt.Sprintf(`vars.__ValidateOperateID=%q`, operateID)). EventFunc(actions.Validate). - Form(web.Var("differences")). Query(ParamID, id). Query(ParamOperateID, operateID). Query(ParamOverlay, ctx.Param(ParamOverlay)). @@ -441,7 +444,7 @@ func (b *EditingBuilder) doValidate(ctx *web.EventContext) (r web.EventResponse, var ( id = ctx.Param(ParamID) operateID = ctx.Param(ParamOperateID) - obj = b.mb.NewModelById(id) + obj = b.mb.NewModel() vErr web.ValidationErrors usingB = b ) @@ -467,7 +470,14 @@ func (b *EditingBuilder) doValidate(ctx *web.EventContext) (r web.EventResponse, web.AppendRunScripts(&r, ShowSnackbarScript(strings.Join(vErr.GetGlobalErrors(), ";"), "error")) } }() - + if id != "" { + var err1 error + obj, err1 = usingB.Fetcher(obj, id, ctx) + if err1 != nil { + vErr.GlobalError(err1.Error()) + return + } + } vErr = usingB.RunSetterFunc(ctx, true, obj) if vErr.HaveErrors() { return @@ -538,7 +548,7 @@ func (b *EditingBuilder) FetchAndUnmarshal(id string, removeDeletedAndSort bool, func (b *EditingBuilder) doUpdate( ctx *web.EventContext, r *web.EventResponse, - // will not close drawer/dialog +// will not close drawer/dialog silent bool, ) (created bool, err error) { id := ctx.R.FormValue(ParamID) diff --git a/presets/field_defaults.go b/presets/field_defaults.go index 1864edab6..13e41ca48 100644 --- a/presets/field_defaults.go +++ b/presets/field_defaults.go @@ -244,12 +244,12 @@ func cfTextField(obj interface{}, field *FieldContext, ctx *web.EventContext) h. return TextField(obj, field, ctx) } -func VFieldError(name string, value interface{}, error interface{}) []interface{} { +func VFieldError(name string, value interface{}, errorMessages interface{}) []interface{} { var ( errorKey = name + ErrorMessagePostfix objValue = map[string]interface{}{ name: value, - errorKey: error, + errorKey: errorMessages, } ) return append([]interface{}{ diff --git a/presets/model.go b/presets/model.go index 0c7786774..93f86549a 100644 --- a/presets/model.go +++ b/presets/model.go @@ -12,7 +12,6 @@ import ( "github.com/qor5/web/v3" "github.com/qor5/x/v3/i18n" "github.com/qor5/x/v3/perm" - "github.com/sunfmin/reflectutils" h "github.com/theplant/htmlgo" "github.com/qor5/admin/v3/presets/actions" @@ -306,19 +305,3 @@ func (mb *ModelBuilder) getLabel(field NameLabel) (r string) { return humanizeString(field.name) } - -func (mb *ModelBuilder) NewModelById(id string) (r interface{}) { - r = reflect.New(mb.modelType.Elem()).Interface() - if id == "" { - return - } - if slugger, ok := r.(SlugDecoder); ok { - cs := slugger.PrimaryColumnValuesBySlug(id) - for k, v := range cs { - _ = reflectutils.Set(r, toPascalCase(k), v) - } - } else { - _ = reflectutils.Set(r, toPascalCase(ParamID), id) - } - return -} diff --git a/presets/section.go b/presets/section.go index 9a012c05b..9fd6b63a5 100644 --- a/presets/section.go +++ b/presets/section.go @@ -575,19 +575,19 @@ func (b *SectionBuilder) editComponent(obj interface{}, field *FieldContext, ctx } operateID := fmt.Sprint(time.Now().UnixNano()) onChangeEvent += fmt.Sprintf(`if (!vars.__FormFieldIsUpdating){ - let differences = {}; - for (let key in oldForm) { - if (key.endsWith(%q)){continue} + vars.__currentValidateKeys = []; + const endKey = %q ; + for (let key in form) { + if (key.endsWith(endKey)){continue} if (form[key] !== oldForm[key]) { - differences[key] = form[key]?form[key]:""; - } + vars.__currentValidateKeys.push(key+endKey) } + } %s }`, ErrorMessagePostfix, web.Plaid().URL(ctx.R.URL.Path). BeforeScript(fmt.Sprintf(`vars.__ValidateOperateID=%q`, operateID)). EventFunc(b.EventValidate()). - Form(web.Var("differences")). Query(ParamID, id). Query(ParamOperateID, operateID). Go()) @@ -601,11 +601,15 @@ func (b *SectionBuilder) editComponent(obj interface{}, field *FieldContext, ctx ` vars.__FormUpdatingFunc(); for (const key in payload.form){ - form[key] = payload.form[key] - } - vars.__FormUpdatedFunc();`, - ), - ) + if (vars.__currentValidateKeys){ + if(vars.__currentValidateKeys.lastIndexOf(key)>=0){ + form[key] = payload.form[key] + } + }else{ + form[key] = payload.form[key] + } + } + vars.__FormUpdatedFunc();`)) if b.isEdit { return h.Div( @@ -1123,17 +1127,10 @@ func (b *SectionBuilder) ValidateDetailField(ctx *web.EventContext) (r web.Event var ( id = ctx.Param(ParamID) operateID = ctx.Param(ParamOperateID) - obj = b.mb.NewModelById(id) + obj = b.mb.NewModel() vErr web.ValidationErrors ) - if b.setter != nil { - b.setter(obj, ctx) - } - vErr = b.editingFB.Unmarshal(obj, b.mb.Info(), true, ctx) - if vErr.HaveErrors() { - return - } defer func() { web.AppendRunScripts(&r, fmt.Sprintf(`if (vars.__ValidateOperateID==%q){%s}`, operateID, @@ -1151,7 +1148,22 @@ func (b *SectionBuilder) ValidateDetailField(ctx *web.EventContext) (r web.Event web.AppendRunScripts(&r, ShowSnackbarScript(strings.Join(vErr.GetGlobalErrors(), ";"), "error")) } }() + if id != "" { + var err1 error + obj, err1 = b.mb.editing.Fetcher(obj, id, ctx) + if err1 != nil { + vErr.GlobalError(err1.Error()) + return + } + } + if b.setter != nil { + b.setter(obj, ctx) + } + vErr = b.editingFB.Unmarshal(obj, b.mb.Info(), true, ctx) + if vErr.HaveErrors() { + return + } if b.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { vErr.GlobalError(perm.PermissionDenied.Error()) return diff --git a/presets/utils.go b/presets/utils.go index 1ae679f42..7925c400d 100644 --- a/presets/utils.go +++ b/presets/utils.go @@ -5,9 +5,7 @@ import ( "fmt" "net/url" "reflect" - "strings" "time" - "unicode" "github.com/pkg/errors" "github.com/qor5/web/v3" @@ -177,22 +175,3 @@ func MustJsonCopy(dst, src any) { panic(err) } } - -func toPascalCase(s string) string { - var result strings.Builder - shouldCapitalize := true - - for _, char := range s { - if char == '_' { - shouldCapitalize = true - } else { - if shouldCapitalize { - result.WriteRune(unicode.ToUpper(char)) - shouldCapitalize = false - } else { - result.WriteRune(char) - } - } - } - return result.String() -} From 4fbec7bade79d5128e02782c07b976d3c4713003 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 07:35:57 +0000 Subject: [PATCH 05/15] style: format code with Gofumpt This commit fixes the style issues introduced in d010a13 according to the output from Gofumpt. Details: https://github.com/qor5/admin/pull/692 --- presets/editing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presets/editing.go b/presets/editing.go index 3aeab845c..8e9944718 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -548,7 +548,7 @@ func (b *EditingBuilder) FetchAndUnmarshal(id string, removeDeletedAndSort bool, func (b *EditingBuilder) doUpdate( ctx *web.EventContext, r *web.EventResponse, -// will not close drawer/dialog + // will not close drawer/dialog silent bool, ) (created bool, err error) { id := ctx.R.FormValue(ParamID) From 88418f0515a2fc00e53c604bb7fab028887176b7 Mon Sep 17 00:00:00 2001 From: Ethan Date: Thu, 31 Oct 2024 16:41:11 +0800 Subject: [PATCH 06/15] fix page slug to errorField ; Create Update Button use local isFetching --- pagebuilder/page.go | 2 +- presets/editing.go | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pagebuilder/page.go b/pagebuilder/page.go index e4a10c8f0..72d1595af 100644 --- a/pagebuilder/page.go +++ b/pagebuilder/page.go @@ -130,7 +130,7 @@ func (b *Builder) defaultPageInstall(pb *presets.Builder, pm *presets.ModelBuild msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages) return comp.(*vx.VXFieldBuilder). Label(msgr.Slug). - Attr(web.VField(field.FormKey, strings.TrimPrefix(p.Slug, "/"))...). + Attr(presets.VFieldError(field.FormKey, strings.TrimPrefix(p.Slug, "/"), field.Errors)...). Disabled(field.Disabled).Attr("prefix", "/") } }).LazyWrapSetterFunc(func(in presets.FieldSetterFunc) presets.FieldSetterFunc { diff --git a/presets/editing.go b/presets/editing.go index 8e9944718..b37151452 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -329,16 +329,20 @@ func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.H var actionButtons h.HTMLComponent = h.Components( VSpacer(), h.Iff(!noPerm, func() h.HTMLComponent { - return VBtn(buttonLabel). - Color("primary"). - Variant(VariantFlat). - Attr(":disabled", "isFetching"). - Attr(":loading", "isFetching"). - Attr("@click", web.Plaid(). - EventFunc(actions.Update). - Queries(queries). - URL(b.mb.Info().ListingHref()). - Go()) + return web.Scope( + VBtn(buttonLabel). + Color("primary"). + Variant(VariantFlat). + Attr(":disabled", "xLocals.isFetching"). + Attr(":loading", "xLocals.isFetching"). + Attr("@click", web.Plaid(). + BeforeScript("xLocals.isFetching=true"). + EventFunc(actions.Update). + Queries(queries). + AfterScript("xLocals.isFetching=false"). + URL(b.mb.Info().ListingHref()). + Go()), + ).VSlot("{locals:xLocals}").Init("{isFetching:false}") }), ) From a31cf54efc2f26fda0f2f381d663ba6e52b4acc1 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 1 Nov 2024 14:12:23 +0800 Subject: [PATCH 07/15] fix page detail slug CategoryID to VFieldError --- pagebuilder/settings.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pagebuilder/settings.go b/pagebuilder/settings.go index 72a81f47b..7a6009c31 100644 --- a/pagebuilder/settings.go +++ b/pagebuilder/settings.go @@ -152,7 +152,7 @@ func detailPageEditor(dp *presets.DetailingBuilder, mb *presets.ModelBuilder, b comp := in(obj, field, ctx) p := obj.(*Page) return comp.(*vx.VXFieldBuilder).Label(msgr.Slug). - Attr(web.VField(field.FormKey, strings.TrimPrefix(p.Slug, "/"))...). + Attr(presets.VFieldError(field.FormKey, strings.TrimPrefix(p.Slug, "/"), field.Errors)...). Attr("prefix", "/") } }).LazyWrapSetterFunc(func(in presets.FieldSetterFunc) presets.FieldSetterFunc { @@ -195,12 +195,11 @@ func detailPageEditor(dp *presets.DetailingBuilder, mb *presets.ModelBuilder, b complete := presets.SelectField(obj, field, ctx). Multiple(false).Chips(false). Label(msgr.Category). - Items(categories).ItemTitle("Path").ItemValue("ID"). - ErrorMessages(field.Errors...) + Items(categories).ItemTitle("Path").ItemValue("ID") if p.CategoryID > 0 { - complete.Attr(web.VField(field.FormKey, p.CategoryID)...) + complete.Attr(presets.VFieldError(field.FormKey, p.CategoryID, field.Errors)...) } else { - complete.Attr(web.VField(field.FormKey, "")...) + complete.Attr(presets.VFieldError(field.FormKey, "", field.Errors)...) } return complete }) From e1aa46667f7590c62c3bf2abe17238c6cd2a8b28 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 1 Nov 2024 14:23:05 +0800 Subject: [PATCH 08/15] web.VField to presets.VFieldError --- .../examples_admin/page_builder_with_campaign.go | 4 ++-- example/admin/config.go | 5 ++--- pagebuilder/example/containers/heading.go | 5 ++--- pagebuilder/example/containers/list_content_lite.go | 5 ++--- pagebuilder/example/containers/tag.go | 11 ++++++----- 5 files changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/docsrc/examples/examples_admin/page_builder_with_campaign.go b/docs/docsrc/examples/examples_admin/page_builder_with_campaign.go index b71458133..28eec4112 100644 --- a/docs/docsrc/examples/examples_admin/page_builder_with_campaign.go +++ b/docs/docsrc/examples/examples_admin/page_builder_with_campaign.go @@ -324,7 +324,7 @@ func PageBuilderExample(b *presets.Builder, db *gorm.DB) http.Handler { return vuetify.VTextField(). Variant(vuetify.FieldVariantUnderlined). Label(field.Label). - Attr(web.VField(field.FormKey, field.Value(obj))...) + Attr(presets.VFieldError(field.FormKey, field.Value(obj), field.Errors)...) }) // only pages view containers set OnlyPages true @@ -339,7 +339,7 @@ func PageBuilderExample(b *presets.Builder, db *gorm.DB) http.Handler { return vuetify.VTextField(). Variant(vuetify.FieldVariantUnderlined). Label(field.Label). - Attr(web.VField(field.FormKey, field.Value(obj))...) + Attr(presets.VFieldError(field.FormKey, field.Value(obj), field.Errors)...) }) // Campaigns Menu diff --git a/example/admin/config.go b/example/admin/config.go index 0cde1b4f4..1afe3aaa8 100644 --- a/example/admin/config.go +++ b/example/admin/config.go @@ -687,10 +687,9 @@ func configPost( return tiptap.TiptapEditor(db, field.Name). Extensions(extensions). MarkdownTheme("github"). // Match tiptap.ThemeGithubCSSComponentsPack - Attr(web.VField(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)))...). + Attr(presets.VFieldError(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)), field.Errors)...). Label(field.Label). - Disabled(field.Disabled). - ErrorMessages(field.Errors...) + Disabled(field.Disabled) }) dp.Section(detailSection) return m diff --git a/pagebuilder/example/containers/heading.go b/pagebuilder/example/containers/heading.go index 6c53d1460..5c4c33af2 100644 --- a/pagebuilder/example/containers/heading.go +++ b/pagebuilder/example/containers/heading.go @@ -52,10 +52,9 @@ func RegisterHeadingContainer(pb *pagebuilder.Builder, db *gorm.DB) { return tiptap.TiptapEditor(db, field.Name). Extensions(extensions). MarkdownTheme("github"). // Match tiptap.ThemeGithubCSSComponentsPack - Attr(web.VField(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)))...). + Attr(presets.VFieldError(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)), field.Errors)...). Label(field.Label). - Disabled(field.Disabled). - ErrorMessages(field.Errors...) + Disabled(field.Disabled) }) ed.ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { p := obj.(*Heading) diff --git a/pagebuilder/example/containers/list_content_lite.go b/pagebuilder/example/containers/list_content_lite.go index 5d9d5234c..62e1fca64 100644 --- a/pagebuilder/example/containers/list_content_lite.go +++ b/pagebuilder/example/containers/list_content_lite.go @@ -75,10 +75,9 @@ func RegisterListContentLiteContainer(pb *pagebuilder.Builder, db *gorm.DB) { return tiptap.TiptapEditor(db, field.Name). Extensions(extensions). MarkdownTheme("github"). // Match tiptap.ThemeGithubCSSComponentsPack - Attr(web.VField(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)))...). + Attr(presets.VFieldError(field.FormKey, fmt.Sprint(reflectutils.MustGet(obj, field.Name)), field.Errors)...). Label(field.Label). - Disabled(field.Disabled). - ErrorMessages(field.Errors...) + Disabled(field.Disabled) }) eb.Field("Items").Nested(fb, &presets.DisplayFieldInSorter{Field: "Heading"}) } diff --git a/pagebuilder/example/containers/tag.go b/pagebuilder/example/containers/tag.go index fb34b2438..cdd1326cd 100644 --- a/pagebuilder/example/containers/tag.go +++ b/pagebuilder/example/containers/tag.go @@ -1,11 +1,12 @@ package containers import ( - "github.com/qor5/admin/v3/pagebuilder" - "github.com/qor5/admin/v3/presets" "github.com/qor5/web/v3" v "github.com/qor5/x/v3/ui/vuetify" . "github.com/theplant/htmlgo" + + "github.com/qor5/admin/v3/pagebuilder" + "github.com/qor5/admin/v3/presets" ) type tag struct { @@ -40,7 +41,7 @@ func SetTagComponent(pb *pagebuilder.Builder, eb *presets.EditingBuilder) { fb.Field("FontColor").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) HTMLComponent { return v.VAutocomplete(). Variant(v.FieldVariantUnderlined). - Attr(web.VField(field.FormKey, field.Value(obj))...). + Attr(presets.VFieldError(field.FormKey, field.Value(obj), field.Errors)...). Label(field.Label). Items(TagFontColors) }) @@ -48,7 +49,7 @@ func SetTagComponent(pb *pagebuilder.Builder, eb *presets.EditingBuilder) { fb.Field("BackgroundColor").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) HTMLComponent { return v.VAutocomplete(). Variant(v.FieldVariantUnderlined). - Attr(web.VField(field.FormKey, field.Value(obj))...). + Attr(presets.VFieldError(field.FormKey, field.Value(obj), field.Errors)...). Label(field.Label). Items(TagBackgroundColors) }) @@ -56,7 +57,7 @@ func SetTagComponent(pb *pagebuilder.Builder, eb *presets.EditingBuilder) { fb.Field("Icon").ComponentFunc(func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) HTMLComponent { return v.VAutocomplete(). Variant(v.FieldVariantUnderlined). - Attr(web.VField(field.FormKey, field.Value(obj))...). + Attr(presets.VFieldError(field.FormKey, field.Value(obj), field.Errors)...). Label(field.Label). Items(TagIcons) }) From 2ccaf6b76ae4e3dbb6ad5e9dc00c71b9745b3ab6 Mon Sep 17 00:00:00 2001 From: Ethan Date: Fri, 1 Nov 2024 16:27:03 +0800 Subject: [PATCH 09/15] fix Items Validate Failed Error Show;fix localizeMenu existLocals --- example/integration/pagebuilder_test.go | 37 ++++++++++++++++++++ l10n/config.go | 5 +-- pagebuilder/builder.go | 3 ++ pagebuilder/example/containers/in_numbers.go | 3 ++ presets/editing.go | 16 ++++----- presets/section.go | 3 +- 6 files changed, 54 insertions(+), 13 deletions(-) diff --git a/example/integration/pagebuilder_test.go b/example/integration/pagebuilder_test.go index 188877cd2..1e40b207c 100644 --- a/example/integration/pagebuilder_test.go +++ b/example/integration/pagebuilder_test.go @@ -14,6 +14,7 @@ import ( "gorm.io/gorm/logger" "github.com/qor5/admin/v3/example/admin" + "github.com/qor5/admin/v3/l10n" "github.com/qor5/admin/v3/pagebuilder" "github.com/qor5/admin/v3/pagebuilder/example/containers" "github.com/qor5/admin/v3/presets" @@ -36,6 +37,7 @@ func TestMain(m *testing.M) { var pageBuilderData = gofixtures.Data(gofixtures.Sql(` INSERT INTO public.page_builder_categories (id, created_at, updated_at, deleted_at, name, path, description, locale_code) VALUES (1, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'category_123', '/12', '', 'International'); +INSERT INTO public.page_builder_categories (id, created_at, updated_at, deleted_at, name, path, description, locale_code) VALUES (1, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'category_123', '/12', '', 'China'); INSERT INTO public.page_builder_categories (id, created_at, updated_at, deleted_at, name, path, description, locale_code) VALUES (2, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'category_456', '/45', '', 'International'); SELECT setval('page_builder_categories_id_seq', 1, true); INSERT INTO public.page_builder_pages (id, created_at, updated_at, deleted_at, title, slug, category_id, status, online_url, scheduled_start_at, scheduled_end_at, actual_start_at, actual_end_at, version, version_name, parent_version, locale_code, seo) VALUES (1, '2024-05-17 15:25:39.716658 +00:00', '2024-05-17 15:25:39.716658 +00:00', null, '12312', '/123', 1, 'draft', '', null, null, null, null, '2024-05-18-v01', '2024-05-18-v01', '', 'International', '{"OpenGraphImageFromMediaLibrary":{"ID":0,"Url":"","VideoLink":"","FileName":"","Description":""}}'); @@ -1142,6 +1144,41 @@ func TestPageBuilder(t *testing.T) { }, ExpectRunScriptContainsInOrder: []string{"LinkText 不能为空"}, }, + { + Name: "Container In Number Delete First Row Validate", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderDemoContainerTestData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_builder/in-numbers"). + EventFunc(actions.Update). + AddField("__Deleted.Items", "0"). + AddField("Items[1].Heading", ""). + AddField("Items[1].Text", "blue"). + BuildEventFuncRequest() + + return req + }, + ExpectPortalUpdate0ContainsInOrder: []string{"Items[0].Heading"}, + ExpectPortalUpdate0NotContains: []string{"Items[1].Heading"}, + }, + { + Name: "Category DoLocalize", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_categories"). + EventFunc(l10n.DoLocalize). + Query(presets.ParamID, "1_International"). + AddField("localize_from", "International"). + AddField("localize_to", "Japan"). + BuildEventFuncRequest() + + return req + }, + ExpectRunScriptContainsInOrder: []string{"Successfully Localized"}, + }, } for _, c := range cases { diff --git a/l10n/config.go b/l10n/config.go index 31602b59d..ca4060e11 100644 --- a/l10n/config.go +++ b/l10n/config.go @@ -5,12 +5,13 @@ import ( "reflect" "slices" - "github.com/qor5/admin/v3/presets" "github.com/qor5/web/v3" . "github.com/qor5/x/v3/ui/vuetify" "github.com/sunfmin/reflectutils" h "github.com/theplant/htmlgo" "gorm.io/gorm" + + "github.com/qor5/admin/v3/presets" ) const ( @@ -126,7 +127,7 @@ func (b *Builder) localizeMenu(obj interface{}, chips h.HTMLComponents, field *p Go(), )), ).VSlot(`{locals:menuLocals}`). - Init(fmt.Sprintf(`{locales:%v}`, h.JSONString(existLocales))) + Init(fmt.Sprintf(`{locales:[]}`)) } func runSwitchLocaleFunc(lb *Builder) func(ctx *web.EventContext) (r h.HTMLComponent) { diff --git a/pagebuilder/builder.go b/pagebuilder/builder.go index ebd9096fd..29fff6792 100644 --- a/pagebuilder/builder.go +++ b/pagebuilder/builder.go @@ -941,6 +941,9 @@ func (b *ContainerBuilder) setFieldsLazyWrapComponentFunc(fields *presets.Fields field.LazyWrapComponentFunc(func(in presets.FieldComponentFunc) presets.FieldComponentFunc { return func(obj interface{}, field *presets.FieldContext, ctx *web.EventContext) h.HTMLComponent { comp := in(obj, field, ctx) + if ctx.Param(presets.ParamOverlay) != actions.Content { + return comp + } formKey := field.ModelInfo.URIName() + "_" + field.FormKey if p, ok := comp.(TagInterface); ok { p.SetAttr("ref", formKey) diff --git a/pagebuilder/example/containers/in_numbers.go b/pagebuilder/example/containers/in_numbers.go index 3eab10e89..6e23a5c70 100644 --- a/pagebuilder/example/containers/in_numbers.go +++ b/pagebuilder/example/containers/in_numbers.go @@ -62,6 +62,9 @@ func RegisterInNumbersContainer(pb *pagebuilder.Builder, db *gorm.DB) { eb := mb.Editing("AddTopSpace", "AddBottomSpace", "AnchorID", "Heading", "Items").ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { p := obj.(*InNumbers) for i, v := range p.Items { + if v == nil { + continue + } if v.Heading == "" { err.FieldError(fmt.Sprintf("Items[%v].Heading", i), "Heading can`t Empty") } diff --git a/presets/editing.go b/presets/editing.go index b37151452..160ef3231 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -474,15 +474,7 @@ func (b *EditingBuilder) doValidate(ctx *web.EventContext) (r web.EventResponse, web.AppendRunScripts(&r, ShowSnackbarScript(strings.Join(vErr.GetGlobalErrors(), ";"), "error")) } }() - if id != "" { - var err1 error - obj, err1 = usingB.Fetcher(obj, id, ctx) - if err1 != nil { - vErr.GlobalError(err1.Error()) - return - } - } - vErr = usingB.RunSetterFunc(ctx, true, obj) + obj, vErr = usingB.FetchAndUnmarshal(id, false, ctx) if vErr.HaveErrors() { return } @@ -552,7 +544,7 @@ func (b *EditingBuilder) FetchAndUnmarshal(id string, removeDeletedAndSort bool, func (b *EditingBuilder) doUpdate( ctx *web.EventContext, r *web.EventResponse, - // will not close drawer/dialog +// will not close drawer/dialog silent bool, ) (created bool, err error) { id := ctx.R.FormValue(ParamID) @@ -564,6 +556,10 @@ func (b *EditingBuilder) doUpdate( } obj, vErr := usingB.FetchAndUnmarshal(id, true, ctx) + + modifiedIndexes := ContextModifiedIndexesBuilder(ctx).FromHidden(ctx.R) + modifiedIndexes.deletedValues = make(map[string][]string) + modifiedIndexes.sortedValues = make(map[string][]string) if vErr.HaveErrors() { usingB.UpdateOverlayContent(ctx, r, obj, "", &vErr) return created, &vErr diff --git a/presets/section.go b/presets/section.go index 9fd6b63a5..20c25d7f0 100644 --- a/presets/section.go +++ b/presets/section.go @@ -1148,6 +1148,7 @@ func (b *SectionBuilder) ValidateDetailField(ctx *web.EventContext) (r web.Event web.AppendRunScripts(&r, ShowSnackbarScript(strings.Join(vErr.GetGlobalErrors(), ";"), "error")) } }() + if id != "" { var err1 error obj, err1 = b.mb.editing.Fetcher(obj, id, ctx) @@ -1160,7 +1161,7 @@ func (b *SectionBuilder) ValidateDetailField(ctx *web.EventContext) (r web.Event b.setter(obj, ctx) } - vErr = b.editingFB.Unmarshal(obj, b.mb.Info(), true, ctx) + vErr = b.editingFB.Unmarshal(obj, b.mb.Info(), false, ctx) if vErr.HaveErrors() { return } From 725556836f5134916175fe84b0ee620679722f1b Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Fri, 1 Nov 2024 08:27:40 +0000 Subject: [PATCH 10/15] style: format code with Gofumpt This commit fixes the style issues introduced in 2ccaf6b according to the output from Gofumpt. Details: https://github.com/qor5/admin/pull/699 --- presets/editing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/presets/editing.go b/presets/editing.go index 160ef3231..b7b61bfaa 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -544,7 +544,7 @@ func (b *EditingBuilder) FetchAndUnmarshal(id string, removeDeletedAndSort bool, func (b *EditingBuilder) doUpdate( ctx *web.EventContext, r *web.EventResponse, -// will not close drawer/dialog + // will not close drawer/dialog silent bool, ) (created bool, err error) { id := ctx.R.FormValue(ParamID) From cf5197011fc198c58c0bfc26db377e7d4db521be Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 4 Nov 2024 15:22:49 +0800 Subject: [PATCH 11/15] demo case Name Can`t Empty --- example/admin/demo_case.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/example/admin/demo_case.go b/example/admin/demo_case.go index 10b2323ac..1efa80ca1 100644 --- a/example/admin/demo_case.go +++ b/example/admin/demo_case.go @@ -148,7 +148,13 @@ func configureDemoCase(b *presets.Builder, db *gorm.DB) { panic(err) } mb := b.Model(&DemoCase{}) - mb.Editing("Name") + mb.Editing("Name").ValidateFunc(func(obj interface{}, ctx *web.EventContext) (err web.ValidationErrors) { + p := obj.(*DemoCase) + if p.Name == "" { + err.FieldError("Name", "Name Can`t Empty") + } + return + }) mb.Listing("ID", "Name") detailing := mb.Detailing( "FieldSection", From 2aa137afdf9e826d2643c4a6ee325f42e1e01867 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 4 Nov 2024 18:07:23 +0800 Subject: [PATCH 12/15] preset ModelBuilder add mustGetMessages func;fix Category delete just current locale code item --- example/integration/pagebuilder_test.go | 38 +++++++++++++++++++++++++ pagebuilder/builder.go | 13 ++++++++- pagebuilder/messages.go | 18 ++++++++---- presets/detailing.go | 4 +-- presets/editing.go | 8 +++--- presets/field_defaults.go | 2 +- presets/listing_builder.go | 7 +++-- presets/listing_compo.go | 2 +- presets/model.go | 21 +++++++++++++- presets/utils.go | 4 +-- 10 files changed, 96 insertions(+), 21 deletions(-) diff --git a/example/integration/pagebuilder_test.go b/example/integration/pagebuilder_test.go index 1e40b207c..dbb4dbcec 100644 --- a/example/integration/pagebuilder_test.go +++ b/example/integration/pagebuilder_test.go @@ -39,6 +39,8 @@ var pageBuilderData = gofixtures.Data(gofixtures.Sql(` INSERT INTO public.page_builder_categories (id, created_at, updated_at, deleted_at, name, path, description, locale_code) VALUES (1, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'category_123', '/12', '', 'International'); INSERT INTO public.page_builder_categories (id, created_at, updated_at, deleted_at, name, path, description, locale_code) VALUES (1, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'category_123', '/12', '', 'China'); INSERT INTO public.page_builder_categories (id, created_at, updated_at, deleted_at, name, path, description, locale_code) VALUES (2, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'category_456', '/45', '', 'International'); +INSERT INTO public.page_builder_categories (id, created_at, updated_at, deleted_at, name, path, description, locale_code) VALUES (3, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'category_34', '/34', '', 'International'); +INSERT INTO public.page_builder_categories (id, created_at, updated_at, deleted_at, name, path, description, locale_code) VALUES (3, '2024-05-17 15:25:31.134801 +00:00', '2024-05-17 15:25:31.134801 +00:00', null, 'category_34', '/34', '', 'China'); SELECT setval('page_builder_categories_id_seq', 1, true); INSERT INTO public.page_builder_pages (id, created_at, updated_at, deleted_at, title, slug, category_id, status, online_url, scheduled_start_at, scheduled_end_at, actual_start_at, actual_end_at, version, version_name, parent_version, locale_code, seo) VALUES (1, '2024-05-17 15:25:39.716658 +00:00', '2024-05-17 15:25:39.716658 +00:00', null, '12312', '/123', 1, 'draft', '', null, null, null, null, '2024-05-18-v01', '2024-05-18-v01', '', 'International', '{"OpenGraphImageFromMediaLibrary":{"ID":0,"Url":"","VideoLink":"","FileName":"","Description":""}}'); SELECT setval('page_builder_pages_id_seq', 1, true); @@ -1179,6 +1181,42 @@ func TestPageBuilder(t *testing.T) { }, ExpectRunScriptContainsInOrder: []string{"Successfully Localized"}, }, + { + Name: "Category Delete Confirmation", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_categories"). + EventFunc(actions.DeleteConfirmation). + Query(presets.ParamID, "1_International"). + BuildEventFuncRequest() + + return req + }, + ExpectPortalUpdate0ContainsInOrder: []string{"this will remove all the records in all localized languages"}, + }, + { + Name: "Category Delete", + Debug: true, + ReqFunc: func() *http.Request { + pageBuilderData.TruncatePut(dbr) + req := NewMultipartBuilder(). + PageURL("/page_categories"). + EventFunc(actions.DoDelete). + Query(presets.ParamID, "3_International"). + BuildEventFuncRequest() + + return req + }, + EventResponseMatch: func(t *testing.T, er *TestEventResponse) { + var count int64 + TestDB.Model(pagebuilder.Category{}).Where("id=3").Count(&count) + if count != 0 { + t.Fatalf("Category Delete Failed %v", count) + } + }, + }, } for _, c := range cases { diff --git a/pagebuilder/builder.go b/pagebuilder/builder.go index 29fff6792..0284eebfe 100644 --- a/pagebuilder/builder.go +++ b/pagebuilder/builder.go @@ -541,6 +541,17 @@ func (b *Builder) defaultCategoryInstall(pb *presets.Builder, pm *presets.ModelB db := b.db lb := pm.Listing("Name", "Path", "Description") + pm.WrapMustGetMessages(func(f func(r *http.Request) *presets.Messages) func(r *http.Request) *presets.Messages { + return func(r *http.Request) *presets.Messages { + messages := f(r) + if b.l10n == nil { + return messages + } + msgr := i18n.MustGetModuleMessages(r, I18nPageBuilderKey, Messages_en_US).(*Messages) + messages.DeleteConfirmationText = msgr.CategoryDeleteConfirmationText + return messages + } + }) pm.LabelName(func(evCtx *web.EventContext, singular bool) string { msgr := i18n.MustGetModuleMessages(evCtx.R, I18nPageBuilderKey, Messages_en_US).(*Messages) if singular { @@ -614,7 +625,7 @@ func (b *Builder) defaultCategoryInstall(pb *presets.Builder, pm *presets.ModelB err = errors.New(msgr.UnableDeleteCategoryMsg) return } - if err = db.Model(&Category{}).Where("id = ? AND locale_code = ?", ID, Locale).Delete(&Category{}).Error; err != nil { + if err = db.Model(&Category{}).Where("id = ?", ID).Delete(&Category{}).Error; err != nil { return } return diff --git a/pagebuilder/messages.go b/pagebuilder/messages.go index cd8144f0f..905336903 100644 --- a/pagebuilder/messages.go +++ b/pagebuilder/messages.go @@ -92,6 +92,8 @@ type Messages struct { AddPageTemplate string Name string Description string + + CategoryDeleteConfirmationText string } var Messages_en_US = &Messages{ @@ -177,9 +179,10 @@ var Messages_en_US = &Messages{ AreWantDeleteContainer: func(v string) string { return fmt.Sprintf("Are you sure you want to delete %v?", v) }, - AddPageTemplate: "Add Page Template", - Name: "Name", - Description: "Description", + AddPageTemplate: "Add Page Template", + Name: "Name", + Description: "Description", + CategoryDeleteConfirmationText: "this will remove all the records in all localized languages", } var Messages_zh_CN = &Messages{ @@ -268,6 +271,8 @@ var Messages_zh_CN = &Messages{ AddPageTemplate: "添加页面模版", Name: "名称", Description: "说明", + + CategoryDeleteConfirmationText: "这将删除所有本地化语言中的所有记录", } var Messages_ja_JP = &Messages{ @@ -353,9 +358,10 @@ var Messages_ja_JP = &Messages{ AreWantDeleteContainer: func(v string) string { return fmt.Sprintf("%v を削除してもよろしいですか?", v) }, - AddPageTemplate: "ページテンプレートを追加", - Name: "名前", - Description: "説明", + AddPageTemplate: "ページテンプレートを追加", + Name: "名前", + Description: "説明", + CategoryDeleteConfirmationText: "これは、すべてのローカライズされた言語のすべてのレコードを削除します", } type ModelsI18nModulePage struct { diff --git a/presets/detailing.go b/presets/detailing.go index bb313c270..41e90175f 100644 --- a/presets/detailing.go +++ b/presets/detailing.go @@ -183,7 +183,7 @@ func (b *DetailingBuilder) defaultPageFunc(ctx *web.EventContext) (r web.PageRes return } - msgr := MustGetMessages(ctx.R) + msgr := b.mb.mustGetMessages(ctx.R) title := msgr.DetailingObjectTitle(b.mb.Info().LabelName(ctx, true), getPageTitle(obj, id)) if b.titleFunc != nil { style, ok := ctx.ContextValue(ctxKeyDetailingStyle{}).(DetailingStyle) @@ -394,7 +394,7 @@ func (b *DetailingBuilder) openActionDialog(ctx *web.EventContext) (r web.EventR } func (b *DetailingBuilder) actionForm(action *ActionBuilder, ctx *web.EventContext) h.HTMLComponent { - msgr := MustGetMessages(ctx.R) + msgr := b.mb.mustGetMessages(ctx.R) id := ctx.R.FormValue(ParamID) if id == "" { diff --git a/presets/editing.go b/presets/editing.go index b7b61bfaa..6e5ba36e6 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -230,7 +230,7 @@ func (b *EditingBuilder) singletonPageFunc(ctx *web.EventContext) (r web.PageRes return } - msgr := MustGetMessages(ctx.R) + msgr := b.mb.mustGetMessages(ctx.R) title := msgr.EditingObjectTitle(b.mb.Info().LabelName(ctx, true), "") r.PageTitle = title obj, err := b.Fetcher(b.mb.NewModel(), "", ctx) @@ -249,7 +249,7 @@ func (b *EditingBuilder) singletonPageFunc(ctx *web.EventContext) (r web.PageRes func (b *EditingBuilder) editFormFor(obj interface{}, ctx *web.EventContext) h.HTMLComponent { var ( - msgr = MustGetMessages(ctx.R) + msgr = b.mb.mustGetMessages(ctx.R) id = ctx.R.FormValue(ParamID) overlayType = ctx.R.FormValue(ParamOverlay) onChangeEvent = fmt.Sprintf(`if (vars.%s) { vars.%s.editing=true };`, VarsPresetsDataChanged, VarsPresetsDataChanged) @@ -544,7 +544,7 @@ func (b *EditingBuilder) FetchAndUnmarshal(id string, removeDeletedAndSort bool, func (b *EditingBuilder) doUpdate( ctx *web.EventContext, r *web.EventResponse, - // will not close drawer/dialog +// will not close drawer/dialog silent bool, ) (created bool, err error) { id := ctx.R.FormValue(ParamID) @@ -631,7 +631,7 @@ func (b *EditingBuilder) doUpdate( func (b *EditingBuilder) defaultUpdate(ctx *web.EventContext) (r web.EventResponse, err error) { created, uErr := b.doUpdate(ctx, &r, false) if uErr == nil { - msgr := MustGetMessages(ctx.R) + msgr := b.mb.mustGetMessages(ctx.R) if created { ShowMessage(&r, msgr.SuccessfullyCreated, "") } else { diff --git a/presets/field_defaults.go b/presets/field_defaults.go index 13e41ca48..a846c7636 100644 --- a/presets/field_defaults.go +++ b/presets/field_defaults.go @@ -284,7 +284,7 @@ func cfReadonlyText(obj interface{}, field *FieldContext, ctx *web.EventContext) } func ReadonlyCheckbox(obj interface{}, field *FieldContext, ctx *web.EventContext) *vuetifyx.VXCheckboxBuilder { - msgr := MustGetMessages(ctx.R) + msgr := field.ModelInfo.mb.mustGetMessages(ctx.R) return vuetifyx.VXCheckbox(). Title(field.Label). TrueLabel(msgr.CheckboxTrueLabel). diff --git a/presets/listing_builder.go b/presets/listing_builder.go index bb5c904de..ed589a6af 100644 --- a/presets/listing_builder.go +++ b/presets/listing_builder.go @@ -5,7 +5,6 @@ import ( "sync" "github.com/iancoleman/strcase" - "github.com/qor5/admin/v3/presets/actions" "github.com/qor5/web/v3" "github.com/qor5/web/v3/stateful" "github.com/qor5/x/v3/perm" @@ -14,6 +13,8 @@ import ( "github.com/samber/lo" h "github.com/theplant/htmlgo" "github.com/theplant/relay" + + "github.com/qor5/admin/v3/presets/actions" ) type ListingStyle string @@ -313,7 +314,7 @@ func (b *ListingBuilder) defaultPageFunc(evCtx *web.EventContext) (r web.PageRes } func (b *ListingBuilder) getTitle(evCtx *web.EventContext, style ListingStyle) (title string, titleCompo h.HTMLComponent, err error) { - title = MustGetMessages(evCtx.R).ListingObjectTitle(b.mb.Info().LabelName(evCtx, false)) + title = b.mb.mustGetMessages(evCtx.R).ListingObjectTitle(b.mb.Info().LabelName(evCtx, false)) if b.titleFunc != nil { return b.titleFunc(evCtx, style, title) } @@ -377,7 +378,7 @@ func (b *ListingBuilder) openListingDialog(evCtx *web.EventContext) (r web.Event } func (b *ListingBuilder) deleteConfirmation(evCtx *web.EventContext) (r web.EventResponse, err error) { - msgr := MustGetMessages(evCtx.R) + msgr := b.mb.mustGetMessages(evCtx.R) r.UpdatePortals = append(r.UpdatePortals, &web.PortalUpdate{ Name: DeleteConfirmPortalName, diff --git a/presets/listing_compo.go b/presets/listing_compo.go index 4d01df6fb..3a26a68e9 100644 --- a/presets/listing_compo.go +++ b/presets/listing_compo.go @@ -1231,5 +1231,5 @@ func (c *ListingCompo) DoAction(ctx context.Context, req DoActionRequest) (r web func (c *ListingCompo) MustGetEventContext(ctx context.Context) (*web.EventContext, *Messages) { evCtx := web.MustGetEventContext(ctx) - return evCtx, MustGetMessages(evCtx.R) + return evCtx, c.lb.mb.mustGetMessages(evCtx.R) } diff --git a/presets/model.go b/presets/model.go index 93f86549a..7294756a1 100644 --- a/presets/model.go +++ b/presets/model.go @@ -44,6 +44,7 @@ type ModelBuilder struct { modelInfo *ModelInfo singleton bool plugins []ModelPlugin + mustGetMessages func(r *http.Request) *Messages web.EventsHub } @@ -64,7 +65,7 @@ func NewModelBuilder(p *Builder, model interface{}) (mb *ModelBuilder) { mb.newListing() mb.newDetailing() mb.newEditing() - + mb.mustGetMessages = mb.defaultMustGetMessages return } @@ -79,6 +80,14 @@ func (mb *ModelBuilder) HasDetailing() bool { func (mb *ModelBuilder) GetSingleton() bool { return mb.singleton } +func (mb *ModelBuilder) MustGetMessages(in func(r *http.Request) *Messages) *ModelBuilder { + mb.mustGetMessages = in + return mb +} +func (mb *ModelBuilder) WrapMustGetMessages(w func(func(r *http.Request) *Messages) func(r *http.Request) *Messages) *ModelBuilder { + mb.mustGetMessages = w(mb.mustGetMessages) + return mb +} func (mb *ModelBuilder) RightDrawerWidth(v string) *ModelBuilder { mb.rightDrawerWidth = v @@ -305,3 +314,13 @@ func (mb *ModelBuilder) getLabel(field NameLabel) (r string) { return humanizeString(field.name) } + +func (mb *ModelBuilder) defaultMustGetMessages(r *http.Request) *Messages { + messages := &Messages{} + srcVal := reflect.ValueOf(MustGetMessages(r)).Elem() + dstVal := reflect.ValueOf(messages).Elem() + for i := 0; i < srcVal.NumField(); i++ { + dstVal.Field(i).Set(srcVal.Field(i)) + } + return messages +} diff --git a/presets/utils.go b/presets/utils.go index 7925c400d..bcb1a8e10 100644 --- a/presets/utils.go +++ b/presets/utils.go @@ -55,7 +55,7 @@ func EditDeleteRowMenuItemFuncs(mi *ModelInfo, url string, editExtraParams url.V func editRowMenuItemFunc(mi *ModelInfo, url string, editExtraParams url.Values) vx.RowMenuItemFunc { return func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent { - msgr := MustGetMessages(ctx.R) + msgr := mi.mb.mustGetMessages(ctx.R) if mi.mb.Info().Verifier().Do(PermUpdate).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { return nil } @@ -80,7 +80,7 @@ func editRowMenuItemFunc(mi *ModelInfo, url string, editExtraParams url.Values) func deleteRowMenuItemFunc(mi *ModelInfo, url string, editExtraParams url.Values) vx.RowMenuItemFunc { return func(obj interface{}, id string, ctx *web.EventContext) h.HTMLComponent { - msgr := MustGetMessages(ctx.R) + msgr := mi.mb.mustGetMessages(ctx.R) if mi.mb.Info().Verifier().Do(PermDelete).ObjectOn(obj).WithReq(ctx.R).IsAllowed() != nil { return nil } From d6e44685d0755b1b274cf2212cff4696d9286549 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:08:46 +0000 Subject: [PATCH 13/15] style: format code with Gofumpt This commit fixes the style issues introduced in 2aa137a according to the output from Gofumpt. Details: https://github.com/qor5/admin/pull/704 --- presets/editing.go | 2 +- presets/model.go | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/presets/editing.go b/presets/editing.go index 6e5ba36e6..811d8509f 100644 --- a/presets/editing.go +++ b/presets/editing.go @@ -544,7 +544,7 @@ func (b *EditingBuilder) FetchAndUnmarshal(id string, removeDeletedAndSort bool, func (b *EditingBuilder) doUpdate( ctx *web.EventContext, r *web.EventResponse, -// will not close drawer/dialog + // will not close drawer/dialog silent bool, ) (created bool, err error) { id := ctx.R.FormValue(ParamID) diff --git a/presets/model.go b/presets/model.go index 7294756a1..03c63bd0e 100644 --- a/presets/model.go +++ b/presets/model.go @@ -80,10 +80,12 @@ func (mb *ModelBuilder) HasDetailing() bool { func (mb *ModelBuilder) GetSingleton() bool { return mb.singleton } + func (mb *ModelBuilder) MustGetMessages(in func(r *http.Request) *Messages) *ModelBuilder { mb.mustGetMessages = in return mb } + func (mb *ModelBuilder) WrapMustGetMessages(w func(func(r *http.Request) *Messages) func(r *http.Request) *Messages) *ModelBuilder { mb.mustGetMessages = w(mb.mustGetMessages) return mb From 20737f786ecfe65d327f4b75e7f1bc3b18ae1084 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 4 Nov 2024 18:24:10 +0800 Subject: [PATCH 14/15] fix category delete with locale --- pagebuilder/builder.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pagebuilder/builder.go b/pagebuilder/builder.go index 0284eebfe..9105bba7b 100644 --- a/pagebuilder/builder.go +++ b/pagebuilder/builder.go @@ -612,13 +612,15 @@ func (b *Builder) defaultCategoryInstall(pb *presets.Builder, pm *presets.ModelB }) eb.DeleteFunc(func(obj interface{}, id string, ctx *web.EventContext) (err error) { - cs := obj.(presets.SlugDecoder).PrimaryColumnValuesBySlug(id) - ID := cs["id"] - Locale := cs[l10n.SlugLocaleCode] - msgr := i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages) + var ( + cs = obj.(presets.SlugDecoder).PrimaryColumnValuesBySlug(id) + ID = cs[presets.ParamID] + msgr = i18n.MustGetModuleMessages(ctx.R, I18nPageBuilderKey, Messages_en_US).(*Messages) + + count int64 + ) - var count int64 - if err = db.Model(&Page{}).Where("category_id = ? AND locale_code = ?", ID, Locale).Count(&count).Error; err != nil { + if err = db.Model(&Page{}).Where("category_id = ?", ID).Count(&count).Error; err != nil { return } if count > 0 { From ce8c1581fc26cae523cdd9e0e28e8a08d5e37660 Mon Sep 17 00:00:00 2001 From: Ethan Date: Tue, 5 Nov 2024 14:55:38 +0800 Subject: [PATCH 15/15] fix fmt. Sprintf usage --- l10n/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l10n/config.go b/l10n/config.go index ca4060e11..d1d9eed6e 100644 --- a/l10n/config.go +++ b/l10n/config.go @@ -127,7 +127,7 @@ func (b *Builder) localizeMenu(obj interface{}, chips h.HTMLComponents, field *p Go(), )), ).VSlot(`{locals:menuLocals}`). - Init(fmt.Sprintf(`{locales:[]}`)) + Init(`{locales:[]}`) } func runSwitchLocaleFunc(lb *Builder) func(ctx *web.EventContext) (r h.HTMLComponent) {