From 77fddfa356b923b50f1ef6926715dffa88de4b64 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sun, 27 Sep 2026 16:07:07 +0800 Subject: [PATCH 1/5] =?UTF-8?q?test=E2=9C=85:=20hard-delete=20the=20genera?= =?UTF-8?q?tor=20fixtures,=20as=20the=20delete=20endpoint=20does?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dropFixture soft-deleted the sys_tables rows it cleaned up. The config page's business-name check counts soft-deleted rows, so a second test importing the same table could not save its configuration. The delete endpoint removes these rows with Unscoped; the fixture now does too. --- app/other/apis/tools/gen_primary_key_test.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/other/apis/tools/gen_primary_key_test.go b/app/other/apis/tools/gen_primary_key_test.go index 93b51bf0..7be331fe 100644 --- a/app/other/apis/tools/gen_primary_key_test.go +++ b/app/other/apis/tools/gen_primary_key_test.go @@ -197,11 +197,13 @@ func TestGeneratorHandlesEveryPrimaryKeyShape(t *testing.T) { func dropFixture(t *testing.T, db *gorm.DB, table string) { t.Helper() + // Unscoped, as the delete endpoint is: a soft-deleted row still holds its + // business name, and the config page's uniqueness check counts it. var ids []int - db.Model(&tools.SysTables{}).Where("table_name = ?", table).Pluck("table_id", &ids) + db.Unscoped().Model(&tools.SysTables{}).Where("table_name = ?", table).Pluck("table_id", &ids) if len(ids) > 0 { - db.Where("table_id IN ?", ids).Delete(&tools.SysColumns{}) - db.Where("table_id IN ?", ids).Delete(&tools.SysTables{}) + db.Unscoped().Where("table_id IN ?", ids).Delete(&tools.SysColumns{}) + db.Unscoped().Where("table_id IN ?", ids).Delete(&tools.SysTables{}) } if err := db.Exec("DROP TABLE IF EXISTS " + table).Error; err != nil { t.Fatalf("dropping %s: %v", table, err) From d30b9c103fd1b36e1615f7763c7418dea8771f94 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sun, 27 Sep 2026 16:07:09 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix=F0=9F=90=9B:=20import=20"time"=20in=20a?= =?UTF-8?q?=20generated=20model=20only=20for=20fields=20it=20declares?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The model template imported "time" for any queryable time.Time column, though whether a column is queryable only shapes the DTO. A queryable created_at, which the model takes from models.ModelTime rather than declaring, left the import unused and the generated package failed to compile. --- template/v4/model.go.template | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/template/v4/model.go.template b/template/v4/model.go.template index ad8f4360..589a0b9e 100644 --- a/template/v4/model.go.template +++ b/template/v4/model.go.template @@ -1,14 +1,14 @@ package models import ( + {{- /* + The model declares a time.Time field for every such column except the + three models.ModelTime supplies. Whether a column is queryable is the + DTO's business, not the model's: counting it here imported "time" for a + queryable created_at that the model never declares. + */ -}} {{- $bb := false -}} {{- range .Columns -}} - {{- $z := .IsQuery -}} - {{- if ($z) -}} - {{- if eq .GoType "time.Time" -}}{{- $bb = true -}}{{- end -}} - {{- end -}} - {{- end -}} - {{- range .Columns -}} {{- if eq .GoField "CreatedAt" -}} {{- else if eq .GoField "UpdatedAt" -}} {{- else if eq .GoField "DeletedAt" -}} From de33e527fcd4bdcef0d913df9c0899432a10a7e5 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sun, 27 Sep 2026 16:07:09 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix=F0=9F=90=9B:=20treat=20a=20column=20sav?= =?UTF-8?q?ed=20with=20isQuery=20"0"=20as=20not=20queryable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config page saves an unticked query box as "0", and the DTO and API templates tested IsQuery with a bare if, which is true for any non-empty string. A column ticked and then unticked became a filter of the generated list request and a documented query parameter anyway. They now require "1", as the TypeScript and Vue templates already do. --- template/v4/dto.go.template | 7 +++---- template/v4/no_actions/apis.go.template | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/template/v4/dto.go.template b/template/v4/dto.go.template index 7619b066..10d6eb74 100644 --- a/template/v4/dto.go.template +++ b/template/v4/dto.go.template @@ -10,8 +10,8 @@ import ( {{- range .Columns -}}{{- if .Pk -}}{{- $pkType = .GoType -}}{{- if and (eq .GoField "Id") (eq .GoType "int") -}}{{- $embed = true -}}{{- end -}}{{- end -}}{{- end -}} {{- $bb := false -}} {{- range .Columns -}} - {{- $z := .IsQuery -}} - {{- if ($z) -}} + {{- /* "1" exactly: the config page saves an unticked box as "0", which a bare if reads as true. */ -}} + {{- if eq .IsQuery "1" -}} {{- if eq .GoType "time.Time" -}}{{- $bb = true -}}{{- end -}} {{- end -}} {{- end -}} @@ -36,8 +36,7 @@ type {{.ClassName}}GetPageReq struct { dto.Pagination `search:"-"` {{- $tablename := .TBName -}} {{- range .Columns -}} - {{$z := .IsQuery}} - {{- if ($z) }} + {{- if eq .IsQuery "1" }} {{.GoField}} {{.GoType}} `form:"{{.JsonField}}" search:"type:{{if eq .QueryType "EQ"}}exact{{ else if eq .QueryType "NE"}}iexact{{ else if eq .QueryType "LIKE"}}contains{{ else if eq .QueryType "GT"}}gt{{ else if eq .QueryType "GTE"}}gte{{ else if eq .QueryType "LT"}}lt{{ else if eq .QueryType "LTE"}}lte{{- end }};column:{{.ColumnName}};table:{{$tablename}}" comment:"{{.ColumnComment}}"` {{- end }} {{- end }} diff --git a/template/v4/no_actions/apis.go.template b/template/v4/no_actions/apis.go.template index a0825963..adfc9c99 100644 --- a/template/v4/no_actions/apis.go.template +++ b/template/v4/no_actions/apis.go.template @@ -24,8 +24,7 @@ type {{.ClassName}} struct { // @Tags {{.TableComment}} {{- $tablename := .TBName -}} {{- range .Columns -}} -{{$z := .IsQuery}} -{{- if ($z) }} +{{- if eq .IsQuery "1" }} // @Param {{.JsonField}} query {{.GoType}} false "{{.ColumnComment}}" {{- end -}} {{- end }} From 8cbbd6eddd46072579df7d0f0e0e00bffb9caa23 Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sun, 27 Sep 2026 16:07:10 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat=E2=9C=A8:=20show=20created=5Fat=20and?= =?UTF-8?q?=20updated=5Fat=20on=20the=20generator's=20config=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config page hid both columns along with the ones the framework fills in, so a generated page could not list or filter by when a row was created or last changed without editing sys_columns by hand. They are now shown; id, create_by, update_by and deleted_at stay hidden. The templates already keep both out of the create/edit form. The test imports a table, ticks list and query on created_at through the config page's own endpoints, generates it, and filters the generated list with the value the generated date picker sends. --- .../apis/tools/gen_audit_columns_test.go | 298 ++++++++++++++++++ app/other/models/tools/sys_columns.go | 12 +- 2 files changed, 303 insertions(+), 7 deletions(-) create mode 100644 app/other/apis/tools/gen_audit_columns_test.go diff --git a/app/other/apis/tools/gen_audit_columns_test.go b/app/other/apis/tools/gen_audit_columns_test.go new file mode 100644 index 00000000..018f8a41 --- /dev/null +++ b/app/other/apis/tools/gen_audit_columns_test.go @@ -0,0 +1,298 @@ +package tools + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "text/template" + + "github.com/gin-gonic/gin" + "github.com/go-admin-team/go-admin-core/v2/sdk/config" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "go-admin/app/other/models/tools" +) + +// The config page reads a table's columns through SysTable.Get and saves them +// back through SysTable.Update. created_at and updated_at used to be filtered +// out of what Get returns, so no generated page could list, sort or filter by +// when a row was created unless someone edited sys_columns by hand. This +// drives that page's round trip, generates the table, and runs the result. +// genEnv is a generator pointed at the MySQL test database, writing into out +// and resolving its templates from this repository. +type genEnv struct { + db *gorm.DB + root string + out string +} + +func newGenEnv(t *testing.T) genEnv { + t.Helper() + dsn := os.Getenv(genMySQLDSNEnv) + if dsn == "" { + if os.Getenv("CI") != "" { + t.Fatalf("%s is not set while CI is: the generator must not go untested", genMySQLDSNEnv) + } + t.Skipf("%s is not set; the importer only reads MySQL", genMySQLDSNEnv) + } + gin.SetMode(gin.TestMode) + root, err := filepath.Abs("../../../..") + if err != nil { + t.Fatal(err) + } + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + t.Fatalf("connecting to %s: %v", genMySQLDSNEnv, err) + } + var dbName string + if err := db.Raw("SELECT DATABASE()").Scan(&dbName).Error; err != nil || dbName == "" { + t.Fatalf("reading the database name: %v", err) + } + + out := t.TempDir() + restore := [3]string{config.DatabaseConfig.Driver, config.GenConfig.DBName, config.GenConfig.FrontPath} + t.Cleanup(func() { + config.DatabaseConfig.Driver, config.GenConfig.DBName, config.GenConfig.FrontPath = restore[0], restore[1], restore[2] + }) + config.DatabaseConfig.Driver = "mysql" + config.GenConfig.DBName = dbName + config.GenConfig.FrontPath = filepath.Join(out, "ui") + + if err := db.AutoMigrate(&tools.SysTables{}, &tools.SysColumns{}); err != nil { + t.Fatalf("migrating the generator's own tables: %v", err) + } + if err := os.Symlink(filepath.Join(root, "template"), filepath.Join(out, "template")); err != nil { + t.Fatal(err) + } + t.Chdir(out) + return genEnv{db: db, root: root, out: out} +} + +// importTable creates table with the given column definitions and imports it +// into the generator, returning the route parameter that names it. +func (g genEnv) importTable(t *testing.T, table, columns string) gin.Params { + t.Helper() + dropFixture(t, g.db, table) + if err := g.db.Exec("CREATE TABLE " + table + " (" + columns + ")").Error; err != nil { + t.Fatalf("creating %s: %v", table, err) + } + t.Cleanup(func() { dropFixture(t, g.db, table) }) + if code := callHandler(t, g.db, SysTable{}.Insert, "/?tables="+table, nil); code != http.StatusOK { + t.Fatalf("importing %s: response code %d", table, code) + } + var row tools.SysTables + if err := g.db.Where("table_name = ?", table).First(&row).Error; err != nil { + t.Fatalf("finding %s after import: %v", table, err) + } + return gin.Params{{Key: "tableId", Value: itoa(row.TableId)}} +} + +func (g genEnv) page(t *testing.T, table string) string { + t.Helper() + b, err := os.ReadFile(filepath.Join(g.out, "ui", "views", "admin", strings.ReplaceAll(table, "_", "-"), "index.vue")) + if err != nil { + t.Fatal(err) + } + return string(b) +} + +func TestAuditTimestampsCanBeListedAndQueried(t *testing.T) { + g := newGenEnv(t) + db, root, out := g.db, g.root, g.out + const table = "gau_event" + tableID := g.importTable(t, table, "id int NOT NULL AUTO_INCREMENT PRIMARY KEY,"+auditColumns) + + // What the config page is shown. + var detail struct { + List []map[string]any `json:"list"` + Info map[string]any `json:"info"` + } + callJSON(t, db, SysTable{}.Get, http.MethodGet, "", tableID, &detail) + shown := map[string]map[string]any{} + for _, c := range detail.List { + shown[c["columnName"].(string)] = c + } + for _, name := range []string{"created_at", "updated_at"} { + if shown[name] == nil { + t.Errorf("the config page is not shown %s", name) + } + } + // Filled in by the framework on every write; a setting on them would + // either do nothing or break the generated model. + for _, name := range []string{"id", "create_by", "update_by", "deleted_at"} { + if shown[name] != nil { + t.Errorf("the config page is shown %s", name) + } + } + if t.Failed() { + return + } + + // What the page sends back once someone ticks created_at's list and query + // boxes and picks "greater than or equal". + shown["created_at"]["isList"] = "1" + shown["created_at"]["isQuery"] = "1" + shown["created_at"]["queryType"] = "GTE" + shown["updated_at"]["isList"] = "1" + // Ticked and then unticked: the checkbox's false-value. + shown["name"]["isQuery"] = "0" + detail.Info["columns"] = detail.List + body, _ := json.Marshal(detail.Info) + if code := callJSON(t, db, SysTable{}.Update, http.MethodPut, string(body), nil, nil); code != http.StatusOK { + t.Fatalf("saving the table config: response code %d", code) + } + + if code := callHandler(t, db, Gen{}.GenCode, "/", tableID); code != http.StatusOK { + t.Fatalf("generation failed with response code %d", code) + } + + vue := g.page(t, table) + for _, want := range []string{ + ``, + ``, + `v-model="table.query.createdAt"`, + } { + if !strings.Contains(vue, want) { + t.Errorf("the generated page has no %s", want) + } + } + for _, unwanted := range []string{"form.model.createdAt", "form.model.updatedAt"} { + if strings.Contains(vue, unwanted) { + t.Errorf("the generated form edits %s, which the framework fills in", unwanted) + } + } + + dto, err := os.ReadFile(filepath.Join(out, "app", "admin", "service", "dto", table+".go")) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(dto), `form:"name"`) { + t.Error("name, saved with isQuery \"0\", is a search field of the generated list request") + } + + generated := backendFiles(out, root, table) + test := filepath.Join(out, "generated_audit_test.go") + f, err := os.Create(test) + if err != nil { + t.Fatal(err) + } + if err := auditQueryTest.Execute(f, table); err != nil { + t.Fatal(err) + } + f.Close() + generated[filepath.Join(root, "app/admin/apis/zz_generated_audit_test.go")] = test + overlay := filepath.Join(out, "overlay.json") + b, _ := json.Marshal(map[string]any{"Replace": generated}) + if err := os.WriteFile(overlay, b, 0o644); err != nil { + t.Fatal(err) + } + + goCmd(t, root, "vet", "-overlay="+overlay, "./app/admin/...") + ran := goCmd(t, root, "test", "-overlay="+overlay, "-count=1", "-v", "-run", "^TestGeneratedAuditQuery$", "./app/admin/apis/") + if !strings.Contains(string(ran), "--- PASS: TestGeneratedAuditQuery ") { + t.Error("TestGeneratedAuditQuery did not run") + } +} + +// callJSON runs one handler with a JSON body and decodes the response's data +// into data when it is not nil. It returns the code the body carries. +func callJSON(t *testing.T, db *gorm.DB, h gin.HandlerFunc, method, body string, params gin.Params, data any) int { + t.Helper() + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(method, "/", strings.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + c.Params = params + c.Set("db", db) + h(c) + var res struct { + Code int `json:"code"` + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil { + t.Fatalf("decoding the response: %v (body %q)", err, w.Body.String()) + } + if data != nil { + if err := json.Unmarshal(res.Data, data); err != nil { + t.Fatalf("decoding the response data: %v (body %q)", err, w.Body.String()) + } + } + if res.Code != http.StatusOK { + t.Logf("response: %s", w.Body.String()) + } + return res.Code +} + +// auditQueryTest lists the generated table filtered on created_at, sent in +// the format the generated page's date picker sends (value-format +// "YYYY-MM-DD[T]HH:mm:ssZ"). +var auditQueryTest = template.Must(template.New("audit").Parse(`package apis + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + "time" + + "github.com/gin-gonic/gin" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func TestGeneratedAuditQuery(t *testing.T) { + db, err := gorm.Open(mysql.Open(os.Getenv("GO_ADMIN_TEST_MYSQL_DSN")), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + t.Fatal(err) + } + gin.SetMode(gin.TestMode) + r := gin.New() + r.Use(func(c *gin.Context) { c.Set("db", db.WithContext(c)); c.Next() }) + r.GET("/x", GauEvent{}.GetPage) + + old := time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local) + recent := time.Now().Add(-time.Hour) + for name, at := range map[string]time.Time{"old": old, "recent": recent} { + if err := db.Exec("INSERT INTO {{.}} (name, created_at, updated_at) VALUES (?, ?, ?)", name, at, at).Error; err != nil { + t.Fatal(err) + } + } + + list := func(query string) []map[string]any { + t.Helper() + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/x?pageIndex=1&pageSize=10"+query, nil)) + var res struct { + Code int + Data struct{ List []map[string]any } + } + if err := json.Unmarshal(w.Body.Bytes(), &res); err != nil || res.Code != 200 { + t.Fatalf("listing %q: code %d, err %v, body %s", query, res.Code, err, w.Body.String()) + } + return res.Data.List + } + + all := list("") + if len(all) != 2 { + t.Fatalf("unfiltered list has %d rows, want 2", len(all)) + } + if all[0]["createdAt"] == nil || all[0]["updatedAt"] == nil { + t.Errorf("a listed row carries no createdAt/updatedAt: %v", all[0]) + } + + since := time.Date(2025, 1, 1, 0, 0, 0, 0, time.Local).Format("2006-01-02T15:04:05Z07:00") + got := list("&createdAt=" + url.QueryEscape(since)) + if len(got) != 1 || got[0]["name"] != "recent" { + t.Errorf("createdAt >= %s listed %v, want only the recent row", since, got) + } +} +`)) diff --git a/app/other/models/tools/sys_columns.go b/app/other/models/tools/sys_columns.go index e901cbfa..758a8a56 100644 --- a/app/other/models/tools/sys_columns.go +++ b/app/other/models/tools/sys_columns.go @@ -70,13 +70,11 @@ func (e *SysColumns) GetList(tx *gorm.DB, exclude bool) ([]SysColumns, error) { table := tx.Table("sys_columns") table = table.Where("table_id = ? ", e.TableId) if exclude { - notIn := make([]string, 0, 6) - notIn = append(notIn, "id") - notIn = append(notIn, "create_by") - notIn = append(notIn, "update_by") - notIn = append(notIn, "created_at") - notIn = append(notIn, "updated_at") - notIn = append(notIn, "deleted_at") + // Columns the framework fills in on every write, which a setting on the + // config page could not change. created_at and updated_at are not + // among them: whether a page lists or filters by them is a choice the + // page makes, and the templates already keep them out of the form. + notIn := []string{"id", "create_by", "update_by", "deleted_at"} table = table.Where(" column_name not in(?)", notIn) } From 2642d0959d88dd028c665b80fe0b727ef80a204c Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Sun, 27 Sep 2026 16:07:10 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix=F0=9F=90=9B:=20leave=20the=20form=20out?= =?UTF-8?q?=20of=20a=20generated=20page=20with=20nothing=20to=20enter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A table whose columns are all its key and ones the framework fills in got an add button and an edit link opening an empty form. The page now leaves out both, the dialog, useForm and the API calls only the form used. A page with anything to enter is generated byte for byte as before. --- .../apis/tools/gen_audit_columns_test.go | 36 +++++++++++++++++++ template/v4/vue.go.template | 32 +++++++++++------ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/app/other/apis/tools/gen_audit_columns_test.go b/app/other/apis/tools/gen_audit_columns_test.go index 018f8a41..4a34ac9c 100644 --- a/app/other/apis/tools/gen_audit_columns_test.go +++ b/app/other/apis/tools/gen_audit_columns_test.go @@ -200,6 +200,42 @@ func TestAuditTimestampsCanBeListedAndQueried(t *testing.T) { } } +// A table whose only columns are its key and the ones the framework fills in +// leaves the create/edit dialog nothing to show. The page used to render it +// anyway: an add button and an edit link opening an empty form. +func TestAPageWithNothingToEnterHasNoForm(t *testing.T) { + g := newGenEnv(t) + const table = "gau_stamp" + tableID := g.importTable(t, table, `id int NOT NULL AUTO_INCREMENT PRIMARY KEY, + create_by bigint NULL, + update_by bigint NULL, + created_at datetime(3) NULL, + updated_at datetime(3) NULL, + deleted_at bigint NOT NULL DEFAULT 0`) + + if code := callHandler(t, g.db, Gen{}.GenCode, "/", tableID); code != http.StatusOK { + t.Fatalf("generation failed with response code %d", code) + } + vue := g.page(t, table) + for _, unwanted := range []string{" @@ -112,9 +120,11 @@ {{- end}} + {{- if $hasForm}} + {{- end}} @@ -273,18 +287,14 @@ import ProTable from '@/components/ProTable/index.vue' {{- if $hasDatetime}} import DateCell from '@/components/DateCell/index.vue' {{- end}} -{{- if $hasDict}} -{{- if $hasDictList}} -import { useTable, useForm, useRemove, useDict, dictLabel } from '@/composables' -{{- else}} -import { useTable, useForm, useRemove, useDict } from '@/composables' -{{- end}} -{{- else}} -import { useTable, useForm, useRemove } from '@/composables' -{{- end}} +import { useTable{{if $hasForm}}, useForm{{end}}, useRemove{{if $hasDict}}, useDict{{if $hasDictList}}, dictLabel{{end}}{{end}} } from '@/composables' +{{- if $hasForm}} import { add{{.ClassName}}, del{{.ClassName}}, get{{.ClassName}}, list{{.ClassName}}, update{{.ClassName}} } from '@/api/{{.PackageName}}/{{.MLTBName}}' +{{- else}} +import { del{{.ClassName}}, list{{.ClassName}} } from '@/api/{{.PackageName}}/{{.MLTBName}}' +{{- end}} import type { {{.ClassName}}, {{.ClassName}}Query } from '@/api/{{.PackageName}}/{{.MLTBName}}' {{- /* Two columns pointing at the same foreign table must not import it twice -- @@ -386,6 +396,7 @@ const rules = computed(() => ({ {{$rFirst := true}} {{- end}} })) {{- end}} +{{- if $hasForm}} const form = useForm<{{.ClassName}}, {{$pkType}}>({ defaultModel: () => ({{"{"}} {{.PkJsonField}}: undefined{{range .Columns}}{{if and (eq .IsInsert "1") (not .Pk) (ne .GoField "CreatedAt") (ne .GoField "UpdatedAt") (ne .GoField "DeletedAt") (ne .GoField "UpdateBy") (ne .GoField "CreateBy")}}, {{.JsonField}}: {{if eq .DefaultValue ""}}undefined{{else if eq .GoType "int"}}{{.DefaultValue}}{{else}}'{{js .DefaultValue}}'{{end}}{{end}}{{end}} {{"}"}}), @@ -396,6 +407,7 @@ const form = useForm<{{.ClassName}}, {{$pkType}}>({ api: { get: get{{.ClassName}}, add: add{{.ClassName}}, update: update{{.ClassName}} }, onSuccess: () => table.getList() }) +{{- end}} const { remove } = useRemove({ api: del{{.ClassName}},