feat(12.2-03): add hasMany relation contracts, relation forms and child create
- RelationContract gains Kind (empty is belongsToMany) and ForeignKey, with kind-aware boot checks
- manage.form, view.form and pivot.form compile against the related or pivot model; $/ paths resolve inside the plugin
- view toolbarButtons accept create|update|delete|link|unlink, each the capability of its routes
- POST .../relations/{name}/records creates a child through the manage form; the server sets the hasMany key
- relation schema carries kind, deferrable and the localized forms; 17 new relation message keys in en and pl
This commit is contained in:
@@ -615,6 +615,10 @@
|
||||
"default": {
|
||||
"$ref": "#/components/schemas/cabana.jsonScalar"
|
||||
},
|
||||
"deferrable": {
|
||||
"description": "Deferrable is true on a relation-manager field whose relation can be\nmanaged before the record is first saved (RelationSchema.Deferrable):\nthe SPA shows it on the create screen.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"displayFormat": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1380,6 +1384,33 @@
|
||||
"candidateSearch": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"create": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"createSubmit": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"createTitle": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"created": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"deleteConfirm": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"deleteOneConfirm": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"deleteSelected": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"deleted": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"editPivot": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"empty": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
@@ -1389,9 +1420,24 @@
|
||||
"linkHint": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"linkSubmit": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"linked": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"pivotSaved": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"pivotSubmit": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"pivotTitle": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"previewTitle": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"unlinkConfirm": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
@@ -1400,17 +1446,43 @@
|
||||
},
|
||||
"unlinked": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"updateSubmit": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"updateTitle": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
},
|
||||
"updated": {
|
||||
"$ref": "#/components/schemas/cabana.MessageForms"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"candidateSearch",
|
||||
"create",
|
||||
"createSubmit",
|
||||
"createTitle",
|
||||
"created",
|
||||
"deleteConfirm",
|
||||
"deleteOneConfirm",
|
||||
"deleteSelected",
|
||||
"deleted",
|
||||
"editPivot",
|
||||
"empty",
|
||||
"link",
|
||||
"linkHint",
|
||||
"linkSubmit",
|
||||
"linked",
|
||||
"pivotSaved",
|
||||
"pivotSubmit",
|
||||
"pivotTitle",
|
||||
"previewTitle",
|
||||
"unlinkConfirm",
|
||||
"unlinkSelected",
|
||||
"unlinked"
|
||||
"unlinked",
|
||||
"updateSubmit",
|
||||
"updateTitle",
|
||||
"updated"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
@@ -1464,12 +1536,27 @@
|
||||
},
|
||||
"cabana.RelationSchema": {
|
||||
"properties": {
|
||||
"deferrable": {
|
||||
"description": "Deferrable is true when the relation can be managed on a record that\nis not saved yet: always for belongsToMany, and for a hasMany whose\nForeignKey is nullable.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"kind": {
|
||||
"description": "Kind is the contract kind: belongsToMany or hasMany.",
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"manage": {
|
||||
"$ref": "#/components/schemas/cabana.RelationPanel"
|
||||
},
|
||||
"manageForm": {
|
||||
"description": "ManageForm is the child create and update form (manage.form, or the\ntop-level form); ViewForm is the read-only preview form (view.form,\nor the top-level form); PivotForm edits pivot columns of a\nbelongsToMany link (pivot.form). Each is omitted when not declared.",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/cabana.FormField"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"messages": {
|
||||
"allOf": [
|
||||
{
|
||||
@@ -1481,11 +1568,25 @@
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pivotForm": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/cabana.FormField"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"view": {
|
||||
"$ref": "#/components/schemas/cabana.RelationPanel"
|
||||
},
|
||||
"viewForm": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/cabana.FormField"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"deferrable",
|
||||
"kind",
|
||||
"label",
|
||||
"manage",
|
||||
"messages",
|
||||
@@ -5104,6 +5205,140 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/{vendor}/{plugin}/{controller}/{id}/relations/{name}/records": {
|
||||
"post": {
|
||||
"description": "Creates a record through the relation's manage form (manage.form, or the top-level form of config_relation.yaml) and attaches it to the owner: a hasMany child gets the owner's key in its foreign key (the server sets it; the body cannot), a belongsToMany record gets a pivot row. The view panel must declare the create toolbar button, otherwise 403. The owner is scoped like the record show route.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Vendor",
|
||||
"in": "path",
|
||||
"name": "vendor",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Plugin",
|
||||
"in": "path",
|
||||
"name": "plugin",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Controller",
|
||||
"in": "path",
|
||||
"name": "controller",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Owner id",
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "Relation name",
|
||||
"in": "path",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.AdminRecord"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Field values of the manage form keyed by field name",
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.RecordEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Created"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"404": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Not Found"
|
||||
},
|
||||
"413": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Request Entity Too Large"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unprocessable Entity"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BackendBearer": []
|
||||
}
|
||||
],
|
||||
"summary": "Create a related record",
|
||||
"tags": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/{vendor}/{plugin}/{controller}/{id}/relations/{name}/unlink": {
|
||||
"post": {
|
||||
"parameters": [
|
||||
|
||||
140
admin/src/api/schema.d.ts
vendored
140
admin/src/api/schema.d.ts
vendored
@@ -2628,6 +2628,106 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/{vendor}/{plugin}/{controller}/{id}/relations/{name}/records": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Create a related record
|
||||
* @description Creates a record through the relation's manage form (manage.form, or the top-level form of config_relation.yaml) and attaches it to the owner: a hasMany child gets the owner's key in its foreign key (the server sets it; the body cannot), a belongsToMany record gets a pivot row. The view panel must declare the create toolbar button, otherwise 403. The owner is scoped like the record show route.
|
||||
*/
|
||||
post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description Vendor */
|
||||
vendor: string;
|
||||
/** @description Plugin */
|
||||
plugin: string;
|
||||
/** @description Controller */
|
||||
controller: string;
|
||||
/** @description Owner id */
|
||||
id: number;
|
||||
/** @description Relation name */
|
||||
name: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description Field values of the manage form keyed by field name */
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.AdminRecord"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Created */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.RecordEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Not Found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Request Entity Too Large */
|
||||
413: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unprocessable Entity */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/{vendor}/{plugin}/{controller}/{id}/relations/{name}/unlink": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -2893,6 +2993,12 @@ export interface components {
|
||||
comment?: string;
|
||||
context?: components["schemas"]["cabana.fieldContext"];
|
||||
default?: components["schemas"]["cabana.jsonScalar"];
|
||||
/**
|
||||
* @description Deferrable is true on a relation-manager field whose relation can be
|
||||
* managed before the record is first saved (RelationSchema.Deferrable):
|
||||
* the SPA shows it on the create screen.
|
||||
*/
|
||||
deferrable?: boolean;
|
||||
displayFormat?: string;
|
||||
emptyOption?: string;
|
||||
/** @description FileTypes are the allowed lower-case extensions of a fileupload field. */
|
||||
@@ -3143,13 +3249,30 @@ export interface components {
|
||||
};
|
||||
"cabana.RelationMessages": {
|
||||
candidateSearch: components["schemas"]["cabana.MessageForms"];
|
||||
create: components["schemas"]["cabana.MessageForms"];
|
||||
createSubmit: components["schemas"]["cabana.MessageForms"];
|
||||
createTitle: components["schemas"]["cabana.MessageForms"];
|
||||
created: components["schemas"]["cabana.MessageForms"];
|
||||
deleteConfirm: components["schemas"]["cabana.MessageForms"];
|
||||
deleteOneConfirm: components["schemas"]["cabana.MessageForms"];
|
||||
deleteSelected: components["schemas"]["cabana.MessageForms"];
|
||||
deleted: components["schemas"]["cabana.MessageForms"];
|
||||
editPivot: components["schemas"]["cabana.MessageForms"];
|
||||
empty: components["schemas"]["cabana.MessageForms"];
|
||||
link: components["schemas"]["cabana.MessageForms"];
|
||||
linkHint: components["schemas"]["cabana.MessageForms"];
|
||||
linkSubmit: components["schemas"]["cabana.MessageForms"];
|
||||
linked: components["schemas"]["cabana.MessageForms"];
|
||||
pivotSaved: components["schemas"]["cabana.MessageForms"];
|
||||
pivotSubmit: components["schemas"]["cabana.MessageForms"];
|
||||
pivotTitle: components["schemas"]["cabana.MessageForms"];
|
||||
previewTitle: components["schemas"]["cabana.MessageForms"];
|
||||
unlinkConfirm: components["schemas"]["cabana.MessageForms"];
|
||||
unlinkSelected: components["schemas"]["cabana.MessageForms"];
|
||||
unlinked: components["schemas"]["cabana.MessageForms"];
|
||||
updateSubmit: components["schemas"]["cabana.MessageForms"];
|
||||
updateTitle: components["schemas"]["cabana.MessageForms"];
|
||||
updated: components["schemas"]["cabana.MessageForms"];
|
||||
};
|
||||
"cabana.RelationMutationResult": {
|
||||
linked?: number;
|
||||
@@ -3165,15 +3288,32 @@ export interface components {
|
||||
toolbarButtons: string[];
|
||||
};
|
||||
"cabana.RelationSchema": {
|
||||
/**
|
||||
* @description Deferrable is true when the relation can be managed on a record that
|
||||
* is not saved yet: always for belongsToMany, and for a hasMany whose
|
||||
* ForeignKey is nullable.
|
||||
*/
|
||||
deferrable: boolean;
|
||||
/** @description Kind is the contract kind: belongsToMany or hasMany. */
|
||||
kind: string;
|
||||
label: string;
|
||||
manage: components["schemas"]["cabana.RelationPanel"];
|
||||
/**
|
||||
* @description ManageForm is the child create and update form (manage.form, or the
|
||||
* top-level form); ViewForm is the read-only preview form (view.form,
|
||||
* or the top-level form); PivotForm edits pivot columns of a
|
||||
* belongsToMany link (pivot.form). Each is omitted when not declared.
|
||||
*/
|
||||
manageForm?: components["schemas"]["cabana.FormField"][];
|
||||
/**
|
||||
* @description Messages is the relation manager's copy (D-13); the cached schema
|
||||
* carries each phrase key as its own form.
|
||||
*/
|
||||
messages: components["schemas"]["cabana.RelationMessages"];
|
||||
name: string;
|
||||
pivotForm?: components["schemas"]["cabana.FormField"][];
|
||||
view: components["schemas"]["cabana.RelationPanel"];
|
||||
viewForm?: components["schemas"]["cabana.FormField"][];
|
||||
};
|
||||
"cabana.RowAction": {
|
||||
label?: string;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
"data": {
|
||||
"name": "members",
|
||||
"label": "Members",
|
||||
"kind": "belongsToMany",
|
||||
"deferrable": true,
|
||||
"view": {
|
||||
"list": {
|
||||
"columns": [
|
||||
@@ -72,6 +74,58 @@
|
||||
},
|
||||
"empty": {
|
||||
"other": "This widget has no members."
|
||||
},
|
||||
"create": {
|
||||
"other": "New record"
|
||||
},
|
||||
"createTitle": {
|
||||
"other": "New record"
|
||||
},
|
||||
"updateTitle": {
|
||||
"other": "Edit record"
|
||||
},
|
||||
"previewTitle": {
|
||||
"other": "Record preview"
|
||||
},
|
||||
"created": {
|
||||
"other": "Record created"
|
||||
},
|
||||
"updated": {
|
||||
"other": "Record saved"
|
||||
},
|
||||
"deleteSelected": {
|
||||
"other": "Delete selected"
|
||||
},
|
||||
"deleteConfirm": {
|
||||
"other": "Delete the selected (:count)? This cannot be undone."
|
||||
},
|
||||
"deleteOneConfirm": {
|
||||
"other": "Delete this record? This cannot be undone."
|
||||
},
|
||||
"deleted": {
|
||||
"one": "Deleted :count record",
|
||||
"other": "Deleted :count records"
|
||||
},
|
||||
"pivotTitle": {
|
||||
"other": "Link details"
|
||||
},
|
||||
"pivotSaved": {
|
||||
"other": "Link details saved"
|
||||
},
|
||||
"editPivot": {
|
||||
"other": "Edit link details: :name"
|
||||
},
|
||||
"createSubmit": {
|
||||
"other": "Create record"
|
||||
},
|
||||
"updateSubmit": {
|
||||
"other": "Save record"
|
||||
},
|
||||
"pivotSubmit": {
|
||||
"other": "Save link details"
|
||||
},
|
||||
"linkSubmit": {
|
||||
"other": "Add link"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user