feat(12.1-01): list row states from one controller call per page
- pact.ListRowStates with the fixed RowState set deleted, negative, disabled - list response meta.row_states keyed by row id; unknown values dropped - list messages rowStateDeleted, rowStateNegative, rowStateDisabled - update writes through the scope the load used, so a soft-deleted record a controller includes stays soft-deleted - DataTable row state badges and text styles - roster fixture, smoke tests, OpenAPI, TS types, dist, READMEs, docs
This commit is contained in:
@@ -49,6 +49,22 @@ type rosterSpy struct {
|
||||
mu sync.Mutex
|
||||
bulk []pact.AdminBulkActionInput
|
||||
record []pact.AdminRecordActionInput
|
||||
// states counts ListRowStates calls and keeps the size of each page.
|
||||
states []int
|
||||
}
|
||||
|
||||
func (s *rosterSpy) recordStates(n int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.states = append(s.states, n)
|
||||
}
|
||||
|
||||
func (s *rosterSpy) takeStates() []int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
out := s.states
|
||||
s.states = nil
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *rosterSpy) recordOne(in pact.AdminRecordActionInput) {
|
||||
@@ -125,12 +141,52 @@ func (rosterController) RequiredPermissions() []string { return []string{"acme.r
|
||||
func (rosterController) NewRecord() any { return &rosterPerson{} }
|
||||
|
||||
// ListExtendQuery and FormExtendQuery scope every lookup to the acme tenant,
|
||||
// so a person of another tenant is out of scope.
|
||||
// so a person of another tenant is out of scope. Both include soft-deleted
|
||||
// people, as a WinterCMS controller with withTrashed does.
|
||||
func (rosterController) ListExtendQuery(_ context.Context, db *gorm.DB) *gorm.DB {
|
||||
return db.Where("tenant = ?", "acme")
|
||||
return db.Unscoped().Where("tenant = ?", "acme")
|
||||
}
|
||||
func (rosterController) FormExtendQuery(_ context.Context, db *gorm.DB) *gorm.DB {
|
||||
return db.Where("tenant = ?", "acme")
|
||||
return db.Unscoped().Where("tenant = ?", "acme")
|
||||
}
|
||||
|
||||
// ListRowStates marks a page of people: deleted when soft-deleted, negative
|
||||
// when banned, disabled when not active. It answers out of order and with a
|
||||
// duplicate and, for a person named Odd, a value outside the fixed set, so
|
||||
// the framework's reduction is visible.
|
||||
func (c rosterController) ListRowStates(ctx context.Context, db *gorm.DB, records []any) ([][]pact.RowState, error) {
|
||||
c.spy.recordStates(len(records))
|
||||
if _, inTx := cabana.TxFromContext(ctx); inTx || db == nil {
|
||||
return nil, fmt.Errorf("a list hook gets the list handle, not a transaction")
|
||||
}
|
||||
out := make([][]pact.RowState, len(records))
|
||||
for i, record := range records {
|
||||
person := record.(*rosterPerson)
|
||||
if !person.Active {
|
||||
out[i] = append(out[i], pact.RowStateDisabled, pact.RowStateDisabled)
|
||||
}
|
||||
if person.Banned {
|
||||
out[i] = append(out[i], pact.RowStateNegative)
|
||||
}
|
||||
if person.DeletedAt.Valid {
|
||||
out[i] = append(out[i], pact.RowStateDeleted)
|
||||
}
|
||||
if person.Name == "Odd" {
|
||||
out[i] = append(out[i], pact.RowState("starred"))
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// FormAfterDelete removes the person for good inside the delete's
|
||||
// transaction: the list keeps soft-deleted people, so deleting one there is
|
||||
// permanent.
|
||||
func (rosterController) FormAfterDelete(ctx context.Context, model any) error {
|
||||
tx, ok := cabana.TxFromContext(ctx)
|
||||
if !ok {
|
||||
return fmt.Errorf("no transaction on the context")
|
||||
}
|
||||
return tx.Unscoped().Delete(model).Error
|
||||
}
|
||||
|
||||
// AdminBulkActions: activate needs acme.roster.manage and sets active on the
|
||||
@@ -152,7 +208,8 @@ func (c rosterController) AdminBulkActions() []pact.AdminBulkAction {
|
||||
if person.Active {
|
||||
continue
|
||||
}
|
||||
if err := tx.Model(person).Update("active", true).Error; err != nil {
|
||||
// Unscoped: the list scope includes soft-deleted people.
|
||||
if err := tx.Unscoped().Model(person).Update("active", true).Error; err != nil {
|
||||
return pact.AdminBulkActionResult{}, err
|
||||
}
|
||||
changed++
|
||||
@@ -194,7 +251,7 @@ func (c rosterController) AdminRecordActions() []pact.AdminRecordAction {
|
||||
if !ok {
|
||||
return pact.AdminRecordActionResult{}, fmt.Errorf("no transaction on the context")
|
||||
}
|
||||
if err := tx.Model(in.Record).Update("active", true).Error; err != nil {
|
||||
if err := tx.Unscoped().Model(in.Record).Update("active", true).Error; err != nil {
|
||||
return pact.AdminRecordActionResult{}, err
|
||||
}
|
||||
return pact.AdminRecordActionResult{Message: "acme.roster::lang.people.activated"}, nil
|
||||
@@ -210,7 +267,7 @@ func (c rosterController) AdminRecordActions() []pact.AdminRecordAction {
|
||||
if !ok {
|
||||
return pact.AdminRecordActionResult{}, fmt.Errorf("no transaction on the context")
|
||||
}
|
||||
if err := tx.Model(in.Record).Update("banned", false).Error; err != nil {
|
||||
if err := tx.Unscoped().Model(in.Record).Update("banned", false).Error; err != nil {
|
||||
return pact.AdminRecordActionResult{}, err
|
||||
}
|
||||
return pact.AdminRecordActionResult{}, nil
|
||||
|
||||
Reference in New Issue
Block a user