feat(10.1-02): render header and form partials through an allowlisted node renderer
- partialNodes rebuilds the server node tree with h() under the server's tag, attribute and URL lists - PartialHost owns the skeleton, empty and failure states and keeps nodes visible on refetch - type: partial is a valueless group-labelled field rendered on create and update - ListView shows headerPartial above the list card and refetches it after bulk delete - summer-partial and summer-stats style kit in main.css, documented in the cabana README; dist rebuilt
This commit is contained in:
@@ -24,6 +24,12 @@
|
||||
"action": "lookup",
|
||||
"actionLabel": "Look up",
|
||||
"fill": ["name"]
|
||||
},
|
||||
{
|
||||
"name": "summary",
|
||||
"label": "Summary",
|
||||
"type": "partial",
|
||||
"path": "summary"
|
||||
}
|
||||
],
|
||||
"messages": {
|
||||
|
||||
106
admin/tests/fixtures/extension.list-schema.json
vendored
Normal file
106
admin/tests/fixtures/extension.list-schema.json
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"data": {
|
||||
"title": "Widgets",
|
||||
"recordsPerPage": 20,
|
||||
"perPageOptions": [],
|
||||
"showSearch": true,
|
||||
"showSetup": false,
|
||||
"showCheckboxes": true,
|
||||
"showSorting": true,
|
||||
"searchTerm": "",
|
||||
"recordUrl": "acme/demo/widgets/update/:id",
|
||||
"toolbarButtons": [
|
||||
"create",
|
||||
"delete"
|
||||
],
|
||||
"toolbarActions": [],
|
||||
"assets": {
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"/admin-test/assets/acme/demo/css/demo.css?v=abc"
|
||||
]
|
||||
},
|
||||
"columns": [
|
||||
{
|
||||
"key": "name",
|
||||
"label": "Name",
|
||||
"searchable": true,
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "code",
|
||||
"label": "Code",
|
||||
"searchable": true,
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "tags",
|
||||
"label": "Tags",
|
||||
"searchable": false,
|
||||
"sortable": false,
|
||||
"type": "relation",
|
||||
"relation": "tags",
|
||||
"select": "name"
|
||||
},
|
||||
{
|
||||
"key": "created_at",
|
||||
"label": "Created",
|
||||
"searchable": false,
|
||||
"sortable": true,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"key": "active",
|
||||
"label": "Active",
|
||||
"searchable": false,
|
||||
"sortable": false,
|
||||
"type": "switch"
|
||||
}
|
||||
],
|
||||
"filters": [],
|
||||
"rowActions": [],
|
||||
"bulkActions": [],
|
||||
"messages": {
|
||||
"create": {
|
||||
"other": "New widget"
|
||||
},
|
||||
"deleteConfirm": {
|
||||
"one": "Delete :count widget?",
|
||||
"other": "Delete :count widgets?"
|
||||
},
|
||||
"deleteSelected": {
|
||||
"other": "Delete selected"
|
||||
},
|
||||
"deleted": {
|
||||
"one": "Deleted :count widget",
|
||||
"other": "Deleted :count widgets"
|
||||
},
|
||||
"empty": {
|
||||
"other": "No widgets yet."
|
||||
},
|
||||
"emptySearch": {
|
||||
"other": "Nothing found"
|
||||
},
|
||||
"emptySearchHint": {
|
||||
"other": "No widget matches “:term”."
|
||||
},
|
||||
"recordCount": {
|
||||
"one": ":count widget",
|
||||
"other": ":count widgets"
|
||||
},
|
||||
"searchPrompt": {
|
||||
"other": "Search widgets…"
|
||||
},
|
||||
"selected": {
|
||||
"other": "Selected :count"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"locale": "en"
|
||||
},
|
||||
"headerPartial": "stats"
|
||||
},
|
||||
"meta": {
|
||||
"locale": "en"
|
||||
}
|
||||
}
|
||||
151
admin/tests/fixtures/extension.partial.json
vendored
Normal file
151
admin/tests/fixtures/extension.partial.json
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"data": {
|
||||
"nodes": [
|
||||
{
|
||||
"tag": "dl",
|
||||
"attrs": {
|
||||
"class": "summer-stats"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"tag": "div",
|
||||
"attrs": {
|
||||
"class": "summer-stat"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"tag": "dt",
|
||||
"attrs": {
|
||||
"class": "summer-stat__label"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"text": "All widgets"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "dd",
|
||||
"attrs": {
|
||||
"class": "summer-stat__value"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"text": "12"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"attrs": {
|
||||
"class": "summer-stat"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"tag": "dt",
|
||||
"attrs": {
|
||||
"class": "summer-stat__label"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"text": "Small"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "dd",
|
||||
"attrs": {
|
||||
"class": "summer-stat__value"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"text": "5"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "script",
|
||||
"children": [
|
||||
{
|
||||
"text": "window.hijacked = true"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "p",
|
||||
"attrs": {
|
||||
"id": "hijack",
|
||||
"style": "color:red",
|
||||
"data-note": "kept"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"tag": "a",
|
||||
"attrs": {
|
||||
"href": "javascript:alert(1)",
|
||||
"onclick": "alert(1)",
|
||||
"class": "danger-link"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"text": "Unsafe link"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": " "
|
||||
},
|
||||
{
|
||||
"tag": "a",
|
||||
"attrs": {
|
||||
"href": "/admin-test/acme/demo/widgets",
|
||||
"title": "All widgets"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"text": "Safe link"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": " "
|
||||
},
|
||||
{
|
||||
"tag": "a",
|
||||
"attrs": {
|
||||
"href": "//evil.example.test/x"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"text": "Protocol-relative link"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "custom-box",
|
||||
"children": [
|
||||
{
|
||||
"tag": "p",
|
||||
"attrs": {
|
||||
"class": "unwrapped"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"text": "Use <b>bold</b> & <i>italic</i>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
11
admin/tests/fixtures/typed.ts
vendored
11
admin/tests/fixtures/typed.ts
vendored
@@ -8,6 +8,8 @@ import langJson from './lang.json'
|
||||
import navigationJson from './navigation.json'
|
||||
import settingsJson from './settings.json'
|
||||
import extensionFormSchemaJson from './extension.form-schema.json'
|
||||
import extensionListSchemaJson from './extension.list-schema.json'
|
||||
import extensionPartialJson from './extension.partial.json'
|
||||
import formSchemaJson from './widgets.form-schema.json'
|
||||
import listJson from './widgets.list.json'
|
||||
import listSchemaJson from './widgets.list-schema.json'
|
||||
@@ -30,6 +32,15 @@ export const settingsFixture: {
|
||||
export const formSchemaFixture: S['cabana.Envelope-cabana_FormView'] = formSchemaJson
|
||||
/** A form with a plugin widget and its controller script (Phase 10.1). */
|
||||
export const extensionFormSchemaFixture: S['cabana.Envelope-cabana_FormView'] = extensionFormSchemaJson
|
||||
/** A list with a header partial and a controller stylesheet (Phase 10.1). */
|
||||
export const extensionListSchemaFixture: S['cabana.Envelope-cabana_ListSchema'] = extensionListSchemaJson
|
||||
/**
|
||||
* A rendered partial with a stats strip and hostile nodes the client drops.
|
||||
* An assertion, not an annotation: TypeScript widens sibling attrs objects of
|
||||
* a JSON array with `key?: undefined`, which no string index signature
|
||||
* accepts. The assertion still refuses a fixture that does not overlap.
|
||||
*/
|
||||
export const extensionPartialFixture = extensionPartialJson as S['cabana.Envelope-cabana_PartialView']
|
||||
export const listFixture: Rows = listJson
|
||||
export const listSchemaFixture: S['cabana.Envelope-cabana_ListSchema'] = listSchemaJson
|
||||
export const optionsFixture: {
|
||||
|
||||
Reference in New Issue
Block a user