feat(12.1-02): read-only preview screen with a status hint and record actions
- config_form.yaml preview block (optional headerPartial), reported in the form schema as preview
- fields with context: preview show only on the preview screen and are never written
- form messages preview and edit; recordActions without a preview block stops boot
- SPA route {id}/preview, PreviewView and PreviewField, record actions in the footer
- mapWinterUrl maps preview/:id; the update form returns to the preview
- summer-callout partial style classes for status hints
- README, docs, OpenAPI document, TS types and the embedded build updated
This commit is contained in:
@@ -31,12 +31,14 @@ const rosterDir = "testdata/roster"
|
||||
// rosterPerson is the fixture model: a person of one tenant who can be
|
||||
// active, banned and soft-deleted.
|
||||
type rosterPerson struct {
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Tenant string `gorm:"column:tenant"`
|
||||
Name string `gorm:"column:name"`
|
||||
Email string `gorm:"column:email"`
|
||||
Active bool `gorm:"column:active"`
|
||||
Banned bool `gorm:"column:banned"`
|
||||
ID uint `gorm:"column:id;primaryKey"`
|
||||
Tenant string `gorm:"column:tenant"`
|
||||
Name string `gorm:"column:name"`
|
||||
Email string `gorm:"column:email"`
|
||||
Active bool `gorm:"column:active"`
|
||||
Banned bool `gorm:"column:banned"`
|
||||
// JoinedIP is shown on the preview screen only (context: preview).
|
||||
JoinedIP *string `gorm:"column:joined_ip"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
@@ -178,6 +180,35 @@ func (c rosterController) ListRowStates(ctx context.Context, db *gorm.DB, record
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// rosterStatus is the curated view model of the preview status hint: the
|
||||
// callout tone and the phrase keys of its title and text. It is empty when
|
||||
// no state applies, and the template then renders nothing.
|
||||
type rosterStatus struct {
|
||||
Tone, Title, Text string
|
||||
}
|
||||
|
||||
// PartialData serves the preview header partial `status`: one callout by
|
||||
// precedence banned, archived, not active.
|
||||
func (rosterController) PartialData(_ context.Context, name string, record any) (any, error) {
|
||||
if name != "status" {
|
||||
return nil, fmt.Errorf("unknown partial %s", name)
|
||||
}
|
||||
person, ok := record.(*rosterPerson)
|
||||
if !ok || person == nil {
|
||||
return rosterStatus{}, nil
|
||||
}
|
||||
const keys = "acme.roster::lang.people."
|
||||
switch {
|
||||
case person.Banned:
|
||||
return rosterStatus{Tone: "danger", Title: keys + "banned_title", Text: keys + "banned_text"}, nil
|
||||
case person.DeletedAt.Valid:
|
||||
return rosterStatus{Tone: "danger", Title: keys + "deleted_title", Text: keys + "deleted_text"}, nil
|
||||
case !person.Active:
|
||||
return rosterStatus{Tone: "warning", Title: keys + "inactive_title", Text: keys + "inactive_text"}, nil
|
||||
}
|
||||
return rosterStatus{}, nil
|
||||
}
|
||||
|
||||
// rosterLocked is the sentinel name of a person the roster's actions refuse.
|
||||
const rosterLocked = "Locked"
|
||||
|
||||
@@ -381,6 +412,46 @@ func newRosterEnv(t *testing.T) (*rosterEnv, *gorm.DB) {
|
||||
return env, gdb
|
||||
}
|
||||
|
||||
// rosterTree is the roster fixture tree as an in-memory file system with the
|
||||
// given files replaced or added (boot-error tests).
|
||||
func rosterTree(t *testing.T, replace map[string]string) fstest.MapFS {
|
||||
t.Helper()
|
||||
out := fstest.MapFS{}
|
||||
err := filepath.WalkDir(rosterDir, func(name string, entry fs.DirEntry, err error) error {
|
||||
if err != nil || entry.IsDir() {
|
||||
return err
|
||||
}
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(rosterDir, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out[filepath.ToSlash(rel)] = &fstest.MapFile{Data: data}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, body := range replace {
|
||||
out[name] = &fstest.MapFile{Data: []byte(body)}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// rosterBoot activates the roster plugin over fsys and returns the boot error.
|
||||
func rosterBoot(t *testing.T, fsys fs.FS) error {
|
||||
t.Helper()
|
||||
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Environ: []string{"SUMMER_ENV=development", "SUMMER_ADMIN__JWT__SECRET=" + adminTestSecret}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = cabana.Activate(backpack.New(cfg), []party.Plugin{rosterPlugin{spy: &rosterSpy{}, fsys: fsys}})
|
||||
return err
|
||||
}
|
||||
|
||||
// rosterInsert stores one person and returns its id.
|
||||
func rosterInsert(t *testing.T, gdb *gorm.DB, person rosterPerson) uint {
|
||||
t.Helper()
|
||||
|
||||
Reference in New Issue
Block a user