feat(10.1-01): serve controller JS/CSS and run registered toolbar actions
- boardwalk exports ContentType and SetSecurityHeaders
- pact.AdminClientAssets files are read and hashed at boot and served by exact
key under {prefix}/assets/{vendor}/{plugin}/ with nosniff, CSP, CORP,
no-cache and an ETag; a miss falls through to the SPA
- list and form schemas carry assets URLs with a ?v= hash
- toolbar.buttons resolves create, delete and registered actions after decode;
toolbarActions is permission-filtered per admin
- POST .../toolbar/{action} behind requireAjax and action permissions
This commit is contained in:
@@ -125,7 +125,7 @@ func compileList(pluginID string, ctl pact.AdminController, fsys fs.FS) (*ListSc
|
||||
return nil, bootErr(pluginID, ctl.ID(), cfgPath, fmt.Errorf("unsupported search mode %s", mode))
|
||||
}
|
||||
}
|
||||
buttons, err := compileToolbarButtons(doc.Toolbar, doc.ShowCheckboxes)
|
||||
buttons, toolbarActions, err := compileToolbarButtons(ctl, doc.Toolbar, doc.ShowCheckboxes)
|
||||
if err != nil {
|
||||
return nil, bootErr(pluginID, ctl.ID(), cfgPath, err)
|
||||
}
|
||||
@@ -171,6 +171,7 @@ func compileList(pluginID string, ctl pact.AdminController, fsys fs.FS) (*ListSc
|
||||
SearchPrompt: prompt,
|
||||
DefaultSort: sort,
|
||||
ToolbarButtons: buttons,
|
||||
ToolbarActions: toolbarActions,
|
||||
Columns: columns,
|
||||
Filters: filters,
|
||||
RowActions: rowActions,
|
||||
@@ -278,16 +279,15 @@ func compilePageOptions(recordsPerPage int, declared []int) ([]int, error) {
|
||||
return options, nil
|
||||
}
|
||||
|
||||
// toolbarButtons is the declarative toolbar.buttons list (D-14): built-in
|
||||
// actions in display order. A scalar (Winter's partial name) is rejected
|
||||
// with a pointer at the list syntax.
|
||||
// toolbarButtons is the declarative toolbar.buttons list (D-14, D-12):
|
||||
// action names in display order. The built-in create and delete sit next to
|
||||
// names the controller registers through pact.HasAdminActions; decode has no
|
||||
// controller, so membership is resolved in compileToolbarButtons. A scalar
|
||||
// (Winter's partial name) is rejected with a pointer at the list syntax.
|
||||
type toolbarButtons struct {
|
||||
items []string
|
||||
}
|
||||
|
||||
// toolbarActions are the built-in toolbar actions; custom actions are Phase 10.1.
|
||||
var toolbarActions = map[string]struct{}{"create": {}, "delete": {}}
|
||||
|
||||
func (b *toolbarButtons) UnmarshalYAML(node ast.Node) error {
|
||||
node = unwrapNode(node)
|
||||
switch n := node.(type) {
|
||||
@@ -301,10 +301,7 @@ func (b *toolbarButtons) UnmarshalYAML(node ast.Node) error {
|
||||
for _, item := range values {
|
||||
action, err := nodeString(unwrapNode(item))
|
||||
if err != nil {
|
||||
return fmt.Errorf("toolbar.buttons entries must be action names (create, delete)")
|
||||
}
|
||||
if _, ok := toolbarActions[action]; !ok {
|
||||
return fmt.Errorf("toolbar.buttons: unsupported action %s (want create or delete)", action)
|
||||
return fmt.Errorf("toolbar.buttons entries must be action names")
|
||||
}
|
||||
if _, dup := seen[action]; dup {
|
||||
return fmt.Errorf("toolbar.buttons: duplicate action %s", action)
|
||||
@@ -315,21 +312,43 @@ func (b *toolbarButtons) UnmarshalYAML(node ast.Node) error {
|
||||
b.items = items
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("toolbar.buttons must be a list of actions (create, delete); the Winter partial %q is not supported", nodeText(node))
|
||||
return fmt.Errorf("toolbar.buttons must be a list of actions (create, delete or registered actions); the Winter partial %q is not supported", nodeText(node))
|
||||
}
|
||||
}
|
||||
|
||||
func compileToolbarButtons(toolbar *listToolbar, showCheckboxes bool) ([]string, error) {
|
||||
// compileToolbarButtons resolves every toolbar.buttons name against the
|
||||
// built-in create and delete and the controller's registered actions: the one
|
||||
// action namespace widgets use too. It returns the names in declared order and
|
||||
// the registered entries with their (source) labels.
|
||||
func compileToolbarButtons(ctl pact.AdminController, toolbar *listToolbar, showCheckboxes bool) ([]string, []ToolbarAction, error) {
|
||||
if toolbar == nil || len(toolbar.Buttons.items) == 0 {
|
||||
return []string{}, nil
|
||||
return []string{}, []ToolbarAction{}, nil
|
||||
}
|
||||
out := append([]string(nil), toolbar.Buttons.items...)
|
||||
for _, action := range out {
|
||||
if action == "delete" && !showCheckboxes {
|
||||
return nil, fmt.Errorf("toolbar.buttons: delete needs showCheckboxes: true")
|
||||
registered := map[string]pact.AdminAction{}
|
||||
if src, ok := ctl.(pact.HasAdminActions); ok && src != nil {
|
||||
for _, action := range src.AdminActions() {
|
||||
registered[action.Name] = action
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
out := append([]string(nil), toolbar.Buttons.items...)
|
||||
custom := []ToolbarAction{}
|
||||
for _, name := range out {
|
||||
if builtinToolbarActions[name] {
|
||||
if name == "delete" && !showCheckboxes {
|
||||
return nil, nil, fmt.Errorf("toolbar.buttons: delete needs showCheckboxes: true")
|
||||
}
|
||||
continue
|
||||
}
|
||||
action, ok := registered[name]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("toolbar.buttons: unsupported action %s (want create, delete or an action the controller registers)", name)
|
||||
}
|
||||
if strings.TrimSpace(action.Label) == "" {
|
||||
return nil, nil, fmt.Errorf("toolbar.buttons: action %s needs a label", name)
|
||||
}
|
||||
custom = append(custom, ToolbarAction{Name: name, Label: action.Label})
|
||||
}
|
||||
return out, custom, nil
|
||||
}
|
||||
|
||||
func compileDefaultSort(doc *listSortDocument, columns []ListColumn) (*ListSort, error) {
|
||||
@@ -433,6 +452,15 @@ func (s *ListSchema) Localize(ctx context.Context, tr *phrasebook.Translator) (*
|
||||
out.SearchPrompt = translateKey(ctx, tr, s.SearchPrompt)
|
||||
out.PerPageOptions = append([]int(nil), s.PerPageOptions...)
|
||||
out.ToolbarButtons = append([]string(nil), s.ToolbarButtons...)
|
||||
out.ToolbarActions = make([]ToolbarAction, len(s.ToolbarActions))
|
||||
for i, action := range s.ToolbarActions {
|
||||
action.Label = translateKey(ctx, tr, action.Label)
|
||||
out.ToolbarActions[i] = action
|
||||
}
|
||||
out.Assets = ControllerAssets{
|
||||
Scripts: append([]string{}, s.Assets.Scripts...),
|
||||
Styles: append([]string{}, s.Assets.Styles...),
|
||||
}
|
||||
if s.DefaultSort != nil {
|
||||
sort := *s.DefaultSort
|
||||
out.DefaultSort = &sort
|
||||
|
||||
Reference in New Issue
Block a user