94分控件

This commit is contained in:
2026-08-08 16:55:22 +08:00
parent b21043e3b8
commit d289f8d6e2
14 changed files with 1115 additions and 302 deletions
+15 -5
View File
@@ -240,7 +240,9 @@ tabs / tab
后端因此仍然可以完整决定“这十几个字段如何拼接”,但组合关系不再污染字段定义。一个类型可以分别拥有 Edit/Create/Detail View。
Table View 是一等视图,而不是普通 Field Presentation 的附属标记。它负责:
Collection View 是集合展示的一等视图。`table/list/cards` 是同一个集合契约的三个 mode,共享字段、查询、CRUD、排序与 create/edit layoutlist/cards 的 title/subtitle/body/accent 也属于 `adminive.view` 语义,不允许先生成 table AMIS 再回头修改 renderer JSON。`Table_View` 继续作为 Collection View 内部的表格/查询基础契约,并保留原有 API 语义。
Table View 负责:
```text
哪些字段成为列
@@ -276,6 +278,8 @@ tabs / tab
`slot` 只是引用一个已经生成的业务组件。例如“上方配置表单 + 下方设备表格”可以由后端描述为一个 vertical composition,而不需要业务代码直接拼 AMIS `grid/card/tpl` JSON。`list` 表示显式的纵向条目集合;`frontend` 则只声明子节点及其顺序,不声明最终布局,让前端根据设备、容器或产品形态决定横排、竖排、列表、卡片或表格等视觉组织。
`frontend` 交给独立前端消费时,只给 Composition View 仍不够,因为 slot 名本身不说明组件从哪里取得。因此正式外部协议使用 `adminive.composition-manifest`Manifest 同时包含 Composition View 和 Slot Contract registry。每个 contract 可以声明 `component_kind``descriptor_api``view_api``data_api``amis_api` 以及当前 renderer schema。后端在 Manifest 边界验证 Composition 中使用的 slot 都有唯一 contract,前端只按 View 顺序和 Contract 渲染,不维护第二份 slot/业务字段表。
### 5.5 后端与前端的最终边界
后端负责:
@@ -286,8 +290,9 @@ tabs / tab
单字段 presentation
字段属于哪个视图
字段顺序与逻辑分组
form/table/page 的语义组合
表格排序、搜索、筛选等后端能力
form/collection/page 的语义组合
collection 排序、搜索、筛选等后端能力
slot contract 与组件来源
```
前端负责:
@@ -325,7 +330,7 @@ AMIS Adapter
AMIS JSON
```
不能反过来让 `Field_Descriptor` 直接保存任意 AMIS JSON;否则 Adminive 会从 backend-driven schema 退化成“在 C++ 里写前端 JSON”。
不能反过来让 `Field_Descriptor` 直接保存任意 AMIS JSON;否则 Adminive 会从 backend-driven schema 退化成“在 C++ 里写前端 JSON”。AMIS 表单提交同样只能由 View 与字段权限推导 payloadreadonly 字段可以出现在初始展示数据里,但 submit `api.data` 只包含当前 View 选中的 `editable`/`creatable` 字段。这里不能通过放宽 Resource Service 去兼容前端整对象回传,否则会破坏默认只读与 422 拒绝不可写字段的既有安全语义。
## 6. 默认只读,显式声明可写
@@ -919,9 +924,11 @@ schema validation
Field Presentation 与结构事实分离
Form View 的 frontend/group/card/grid/flow/list/tabs 组合
Table View 的列选择与顺序
Collection 的 table/list/cards 三种 Adapter 结果与独立 CRUD id/reload 目标
Collection View 的 table/list/cards mode、item 语义和独立 CRUD id/reload 目标
Form/Collection submit payload 只包含 View 中可 editable/creatable 的字段
sortable/searchable/filterable/fixed 的后端能力约束
Composition View 的 slot、frontend 与跨组件组合
Composition Manifest 的 Slot Contract 完整性
/view JSON 与 AMIS Adapter 的语义一致性
```
@@ -943,6 +950,9 @@ JSON encode/decode
Enum Adapter
Reflection Adapter
Object Adapter
Value Adapter
Control Adapter
Polymorphic Adapter
HTTP Adapter
AMIS Adapter
```
+168 -98
View File
@@ -411,6 +411,49 @@ inline std::string_view form_permission(Form_Mode mode) noexcept {
}
return "readable";
}
inline std::size_t amis_grid_width(std::size_t index, std::size_t count) noexcept {
if(count == 0) {
return 12;
}
if(count >= 12) {
return 1;
}
const std::size_t width = 12 / count;
return width + (index % count < 12 % count ? 1 : 0);
}
inline void collect_amis_submit_fields(const View_Node& node, std::vector<std::string>& fields, bool& all_fields_selected) {
if(all_fields_selected) {
return;
}
if(node.kind == View_Node_Kind::all_fields) {
all_fields_selected = true;
fields.clear();
return;
}
if(node.kind == View_Node_Kind::field && std::find(fields.begin(), fields.end(), node.field) == fields.end()) {
fields.push_back(node.field);
}
for(const auto& child : node.children) {
collect_amis_submit_fields(child, fields, all_fields_selected);
}
}
template <Json_Type Json>
Json make_amis_submit_data(const Json& descriptor, const View_Node& body, std::string_view permission) {
std::vector<std::string> selected_fields;
bool all_fields_selected{};
collect_amis_submit_fields(body, selected_fields, all_fields_selected);
Json result = json_object<Json>();
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
const auto& field = Json_Adapter<Json>::at(fields, index);
const std::string name = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "name"));
const bool selected = all_fields_selected || std::find(selected_fields.begin(), selected_fields.end(), name) != selected_fields.end();
if(selected && json_get<Json, bool>(Json_Adapter<Json>::at(field, permission))) {
json_set(result, name, "${" + name + "}");
}
}
return result;
}
template <Json_Type Json>
void apply_view_node_options(Json& result, const View_Node& node, std::string_view prefix) {
if(!node.description.empty()) {
@@ -481,11 +524,10 @@ Json make_amis_view_node(const View_Node& node, const Json& descriptor_json, std
if(node.kind == View_Node_Kind::horizontal || node.kind == View_Node_Kind::grid) {
Json columns = json_array<Json>();
const std::size_t count = node.kind == View_Node_Kind::grid && node.columns != 0 ? node.columns : std::max<std::size_t>(1, node.children.size());
const std::size_t width = std::max<std::size_t>(1, 12 / count);
for(const auto& child : node.children) {
for(std::size_t index = 0; index < node.children.size(); ++index) {
Json column = json_object<Json>();
json_set(column, "md", width);
json_set(column, "body", make_amis_view_body<Json, T>(child, descriptor_json, permission, prefix));
json_set(column, "md", amis_grid_width(index, count));
json_set(column, "body", make_amis_view_body<Json, T>(node.children[index], descriptor_json, permission, prefix));
json_append(columns, std::move(column));
}
Json result = json_object<Json>();
@@ -650,11 +692,10 @@ Json make_amis_composition_node(const Composition_Node& node, const std::map<std
if(node.kind == Composition_Node_Kind::horizontal || node.kind == Composition_Node_Kind::grid) {
Json columns = json_array<Json>();
const std::size_t count = node.kind == Composition_Node_Kind::grid && node.columns != 0 ? node.columns : std::max<std::size_t>(1, node.children.size());
const std::size_t width = std::max<std::size_t>(1, 12 / count);
for(const auto& child : node.children) {
for(std::size_t index = 0; index < node.children.size(); ++index) {
Json column = json_object<Json>();
json_set(column, "md", width);
json_set(column, "body", make_amis_composition_body<Json>(child, slots));
json_set(column, "md", amis_grid_width(index, count));
json_set(column, "body", make_amis_composition_body<Json>(node.children[index], slots));
json_append(columns, std::move(column));
}
Json result = json_object<Json>();
@@ -844,6 +885,7 @@ Json to_amis_form_schema(const T& value, const Form_View& view, std::string subm
Json api = json_object<Json>();
json_set(api, "method", "post");
json_set(api, "url", std::move(submit_api));
json_set(api, "data", make_amis_submit_data<Json>(descriptor, view.body, form_permission(view.mode)));
json_set(result, "api", std::move(api));
}
}
@@ -861,45 +903,63 @@ Json to_amis_form_schema(const T& value, const Form_View& view, std::string subm
return adminive::to_amis_form_schema<Json>(item, view, std::move(submit_api));
});
}
inline std::string collection_field_label(const Collection_View& view, std::string_view field) {
for(const auto& column : view.table.columns) {
if(column.field == field) {
return column.label.empty() ? make_label(field) : column.label;
}
}
return make_label(field);
}
template <Json_Type Json>
Json make_amis_semantic_collection_item(const Collection_View& view) {
Json body = json_array<Json>();
for(const auto& field : view.item.fields) {
Json line = json_object<Json>();
json_set(line, "type", "tpl");
json_set(line, "tpl", "<span class=\"text-muted\">" + collection_field_label(view, field) + "</span><strong>${" + field + "}</strong>");
json_append(body, std::move(line));
}
if(!view.item.accent_field.empty()) {
Json accent = json_object<Json>();
json_set(accent, "type", "tpl");
json_set(accent, "tpl", "<div style=\"height:6px;border-radius:999px;background:${" + view.item.accent_field + "};margin-top:10px\"></div>");
json_append(body, std::move(accent));
}
if(view.mode == Collection_Mode::list) {
Json result = json_object<Json>();
json_set(result, "title", "${" + view.item.title_field + "}");
if(!view.item.subtitle_field.empty()) {
json_set(result, "subTitle", "${" + view.item.subtitle_field + "}");
}
json_set(result, "body", std::move(body));
return result;
}
Json header = json_object<Json>();
json_set(header, "title", "${" + view.item.title_field + "}");
if(!view.item.subtitle_field.empty()) {
json_set(header, "subTitle", "${" + view.item.subtitle_field + "}");
}
Json result = json_object<Json>();
json_set(result, "header", std::move(header));
json_set(result, "body", std::move(body));
return result;
}
template <Json_Type Json, class T>
requires Described_Type<Object_Model_Type<T>>
Json to_amis_table_schema(std::string base_api, const Table_View& view, const Json* item_status_descriptor = nullptr, std::uint64_t item_status_interval = 2000, const Json* overview_status_descriptor = nullptr, std::string overview_status_api = {}, std::uint64_t overview_status_interval = 2000) {
Json make_amis_collection_schema(std::string base_api, const Table_View& view, std::string mode, const Json* item, std::size_t cards_columns, const Json* item_status_descriptor, std::uint64_t item_status_interval, const Json* overview_status_descriptor, std::string overview_status_api, std::uint64_t overview_status_interval) {
using Model = Object_Model_Type<T>;
validate_table_view<Model>(view);
const Json descriptor = to_descriptor_json<Json, T>();
const auto object_descriptor = describe<Model>();
const std::string crud_id = view.name + "_crud";
Json columns = json_array<Json>();
Json id_column = json_object<Json>();
json_set(id_column, "name", "id");
json_set(id_column, "label", view.id_label);
json_set(id_column, "sortable", true);
json_append(columns, std::move(id_column));
for(const auto& definition : view.columns) {
Json column_json = make_typed_amis_column_by_name(descriptor, object_descriptor, definition.field);
if(!definition.label.empty()) {
json_set(column_json, "label", definition.label);
}
json_set(column_json, "sortable", definition.sortable);
if(definition.searchable) {
json_set(column_json, "searchable", true);
}
if(definition.filterable) {
json_set(column_json, "filterable", true);
}
if(definition.fixed == Table_Fixed::left) {
json_set(column_json, "fixed", "left");
} else if(definition.fixed == Table_Fixed::right) {
json_set(column_json, "fixed", "right");
}
json_append(columns, std::move(column_json));
}
if(item_status_descriptor) {
json_append(columns, make_amis_object_status_column<Json>(*item_status_descriptor, base_api + "/${id}/status", item_status_interval, view.view_status_label));
std::string crud_id = view.name + "_crud";
if(mode != "table") {
crud_id += "_" + mode;
}
Json create_api = json_object<Json>();
json_set(create_api, "method", "post");
json_set(create_api, "url", base_api);
json_set(create_api, "data", make_amis_submit_data<Json>(descriptor, view.create_body, "creatable"));
Json create_form = json_object<Json>();
json_set(create_form, "type", "form");
json_set(create_form, "api", std::move(create_api));
@@ -909,6 +969,7 @@ Json to_amis_table_schema(std::string base_api, const Table_View& view, const Js
Json edit_api = json_object<Json>();
json_set(edit_api, "method", "put");
json_set(edit_api, "url", base_api + "/${id}");
json_set(edit_api, "data", make_amis_submit_data<Json>(descriptor, view.edit_body, "editable"));
Json edit_form = json_object<Json>();
json_set(edit_form, "type", "form");
json_set(edit_form, "api", std::move(edit_api));
@@ -948,16 +1009,13 @@ Json to_amis_table_schema(std::string base_api, const Table_View& view, const Js
Json buttons = json_array<Json>();
json_append(buttons, std::move(edit_button));
json_append(buttons, std::move(delete_button));
Json operation = json_object<Json>();
json_set(operation, "type", "operation");
json_set(operation, "label", view.actions_label);
json_set(operation, "buttons", std::move(buttons));
json_append(columns, std::move(operation));
Json toggler = json_object<Json>();
json_set(toggler, "type", "columns-toggler");
json_set(toggler, "draggable", view.column_reorderable);
Json toolbar = json_array<Json>();
json_append(toolbar, std::move(toggler));
if(mode == "table") {
Json toggler = json_object<Json>();
json_set(toggler, "type", "columns-toggler");
json_set(toggler, "draggable", view.column_reorderable);
json_append(toolbar, std::move(toggler));
}
json_append(toolbar, "reload");
json_append(toolbar, std::move(create_button));
Json crud_api = json_object<Json>();
@@ -970,7 +1028,52 @@ Json to_amis_table_schema(std::string base_api, const Table_View& view, const Js
json_set(crud, "syncLocation", false);
json_set(crud, "primaryField", "id");
json_set(crud, "headerToolbar", std::move(toolbar));
json_set(crud, "columns", std::move(columns));
if(mode == "table") {
Json columns = json_array<Json>();
Json id_column = json_object<Json>();
json_set(id_column, "name", "id");
json_set(id_column, "label", view.id_label);
json_set(id_column, "sortable", true);
json_append(columns, std::move(id_column));
for(const auto& definition : view.columns) {
Json column_json = make_typed_amis_column_by_name(descriptor, object_descriptor, definition.field);
if(!definition.label.empty()) {
json_set(column_json, "label", definition.label);
}
json_set(column_json, "sortable", definition.sortable);
if(definition.searchable) {
json_set(column_json, "searchable", true);
}
if(definition.filterable) {
json_set(column_json, "filterable", true);
}
if(definition.fixed == Table_Fixed::left) {
json_set(column_json, "fixed", "left");
} else if(definition.fixed == Table_Fixed::right) {
json_set(column_json, "fixed", "right");
}
json_append(columns, std::move(column_json));
}
if(item_status_descriptor) {
json_append(columns, make_amis_object_status_column<Json>(*item_status_descriptor, base_api + "/${id}/status", item_status_interval, view.view_status_label));
}
Json operation = json_object<Json>();
json_set(operation, "type", "operation");
json_set(operation, "label", view.actions_label);
json_set(operation, "buttons", buttons);
json_append(columns, std::move(operation));
json_set(crud, "columns", std::move(columns));
} else {
Json item_schema = *item;
if(!Json_Adapter<Json>::contains(item_schema, "actions")) {
json_set(item_schema, "actions", buttons);
}
json_set(crud, "mode", mode);
json_set(crud, mode == "list" ? "listItem" : "card", std::move(item_schema));
if(mode == "cards") {
json_set(crud, "columnsCount", cards_columns);
}
}
if(!view.default_order_by.empty()) {
Json params = json_object<Json>();
json_set(params, "orderBy", view.default_order_by);
@@ -995,64 +1098,31 @@ Json to_amis_table_schema(std::string base_api, const Table_View& view, const Js
json_set(result, "body", std::move(page_body));
return result;
}
template <Json_Type Json>
Json amis_collection_actions(const Json& crud) {
const auto& columns = Json_Adapter<Json>::at(crud, "columns");
return Json_Adapter<Json>::at(Json_Adapter<Json>::at(columns, Json_Adapter<Json>::size(columns) - 1), "buttons");
}
template <Json_Type Json>
void replace_amis_reload_target(Json& value, std::string_view old_target, std::string_view new_target) {
if(Json_Adapter<Json>::is_object(value)) {
if(Json_Adapter<Json>::contains(value, "reload")) {
auto& reload = Json_Adapter<Json>::at(value, "reload");
if(Json_Adapter<Json>::is_string(reload) && json_get<Json, std::string>(reload) == old_target) {
json_set(value, "reload", new_target);
}
}
for(const auto& key : Json_Adapter<Json>::keys(value)) {
replace_amis_reload_target<Json>(Json_Adapter<Json>::at(value, key), old_target, new_target);
}
return;
}
if(Json_Adapter<Json>::is_array(value)) {
for(std::size_t index = 0; index < Json_Adapter<Json>::size(value); ++index) {
replace_amis_reload_target<Json>(Json_Adapter<Json>::at(value, index), old_target, new_target);
}
}
}
template <Json_Type Json>
Json make_amis_collection_variant(Json schema, std::string mode, std::string item_key, Json item, std::size_t columns_count = 0) {
auto& body = Json_Adapter<Json>::at(schema, "body");
auto& crud = Json_Adapter<Json>::at(body, Json_Adapter<Json>::size(body) - 1);
const std::string old_id = json_get<Json, std::string>(Json_Adapter<Json>::at(crud, "id"));
const std::string id = old_id + "_" + mode;
if(!Json_Adapter<Json>::contains(item, "actions")) {
json_set(item, "actions", amis_collection_actions<Json>(crud));
}
const auto& old_toolbar = Json_Adapter<Json>::at(crud, "headerToolbar");
Json toolbar = json_array<Json>();
for(std::size_t index = 1; index < Json_Adapter<Json>::size(old_toolbar); ++index) {
json_append(toolbar, Json_Adapter<Json>::at(old_toolbar, index));
}
Json_Adapter<Json>::erase(crud, "columns");
json_set(crud, "id", id);
json_set(crud, "headerToolbar", std::move(toolbar));
json_set(crud, "mode", std::move(mode));
json_set(crud, item_key, std::move(item));
if(columns_count != 0) {
json_set(crud, "columnsCount", columns_count);
}
replace_amis_reload_target<Json>(crud, old_id, id);
return schema;
template <Json_Type Json, class T>
requires Described_Type<Object_Model_Type<T>>
Json to_amis_table_schema(std::string base_api, const Table_View& view, const Json* item_status_descriptor = nullptr, std::uint64_t item_status_interval = 2000, const Json* overview_status_descriptor = nullptr, std::string overview_status_api = {}, std::uint64_t overview_status_interval = 2000) {
return make_amis_collection_schema<Json, T>(std::move(base_api), view, "table", nullptr, 0, item_status_descriptor, item_status_interval, overview_status_descriptor, std::move(overview_status_api), overview_status_interval);
}
template <Json_Type Json, class T>
requires Described_Type<Object_Model_Type<T>>
Json to_amis_list_schema(std::string base_api, const Table_View& view, Json list_item, const Json* item_status_descriptor = nullptr, std::uint64_t item_status_interval = 2000, const Json* overview_status_descriptor = nullptr, std::string overview_status_api = {}, std::uint64_t overview_status_interval = 2000) {
return make_amis_collection_variant<Json>(to_amis_table_schema<Json, T>(std::move(base_api), view, item_status_descriptor, item_status_interval, overview_status_descriptor, std::move(overview_status_api), overview_status_interval), "list", "listItem", std::move(list_item));
return make_amis_collection_schema<Json, T>(std::move(base_api), view, "list", &list_item, 0, item_status_descriptor, item_status_interval, overview_status_descriptor, std::move(overview_status_api), overview_status_interval);
}
template <Json_Type Json, class T>
requires Described_Type<Object_Model_Type<T>>
Json to_amis_cards_schema(std::string base_api, const Table_View& view, Json card, std::size_t columns_count = 3, const Json* item_status_descriptor = nullptr, std::uint64_t item_status_interval = 2000, const Json* overview_status_descriptor = nullptr, std::string overview_status_api = {}, std::uint64_t overview_status_interval = 2000) {
return make_amis_collection_variant<Json>(to_amis_table_schema<Json, T>(std::move(base_api), view, item_status_descriptor, item_status_interval, overview_status_descriptor, std::move(overview_status_api), overview_status_interval), "cards", "card", std::move(card), columns_count);
return make_amis_collection_schema<Json, T>(std::move(base_api), view, "cards", &card, columns_count, item_status_descriptor, item_status_interval, overview_status_descriptor, std::move(overview_status_api), overview_status_interval);
}
template <Json_Type Json, class T>
requires Described_Type<Object_Model_Type<T>>
Json to_amis_collection_schema(std::string base_api, const Collection_View& view, const Json* item_status_descriptor = nullptr, std::uint64_t item_status_interval = 2000, const Json* overview_status_descriptor = nullptr, std::string overview_status_api = {}, std::uint64_t overview_status_interval = 2000) {
using Model = Object_Model_Type<T>;
validate_collection_view<Model>(view);
if(view.mode == Collection_Mode::table) {
return make_amis_collection_schema<Json, T>(std::move(base_api), view.table, "table", nullptr, 0, item_status_descriptor, item_status_interval, overview_status_descriptor, std::move(overview_status_api), overview_status_interval);
}
Json item = make_amis_semantic_collection_item<Json>(view);
return make_amis_collection_schema<Json, T>(std::move(base_api), view.table, std::string(collection_mode_name(view.mode)), &item, view.cards_columns, item_status_descriptor, item_status_interval, overview_status_descriptor, std::move(overview_status_api), overview_status_interval);
}
}
@@ -1,4 +1,5 @@
#pragma once
#include <algorithm>
#include <cstddef>
#include <stdexcept>
#include <string>
@@ -137,6 +138,14 @@ struct Composition_View {
return result;
}
};
struct Composition_Slot_Contract {
std::string name;
std::string component_kind;
std::string descriptor_api;
std::string view_api;
std::string data_api;
std::string amis_api;
};
inline Composition_View composition_view(std::string name, Composition_Node body) {
return Composition_View{std::move(name), {}, std::move(body)};
}
@@ -170,4 +179,42 @@ inline void validate_composition_view(const Composition_View& view) {
}
validate_composition_node(view.body);
}
inline void collect_composition_slots(const Composition_Node& node, std::vector<std::string>& result) {
if(node.kind == Composition_Node_Kind::slot) {
result.push_back(node.slot);
}
for(const auto& child : node.children) {
collect_composition_slots(child, result);
}
}
inline void validate_composition_slot_contracts(const Composition_View& view, const std::vector<Composition_Slot_Contract>& contracts) {
validate_composition_view(view);
std::vector<std::string> slots;
collect_composition_slots(view.body, slots);
for(const auto& slot_name : slots) {
const auto iterator = std::find_if(contracts.begin(), contracts.end(), [&](const Composition_Slot_Contract& contract) {
return contract.name == slot_name;
});
if(iterator == contracts.end()) {
throw std::invalid_argument("composition slot contract is not provided: " + slot_name);
}
}
for(std::size_t index = 0; index < contracts.size(); ++index) {
const auto& contract = contracts[index];
if(contract.name.empty()) {
throw std::invalid_argument("composition slot contract name must not be empty");
}
if(contract.component_kind.empty()) {
throw std::invalid_argument("composition slot contract component kind must not be empty: " + contract.name);
}
for(std::size_t other = index + 1; other < contracts.size(); ++other) {
if(contracts[other].name == contract.name) {
throw std::invalid_argument("duplicate composition slot contract: " + contract.name);
}
}
if(std::find(slots.begin(), slots.end(), contract.name) == slots.end()) {
throw std::invalid_argument("composition slot contract is not referenced by view: " + contract.name);
}
}
}
}
+171
View File
@@ -347,6 +347,144 @@ Table_View table_view(Columns&&... columns) {
(result.columns.push_back(std::forward<Columns>(columns)), ...);
return result;
}
enum class Collection_Mode {
table,
list,
cards
};
struct Collection_Item_View {
std::string title_field;
std::string subtitle_field;
std::string accent_field;
std::vector<std::string> fields;
template <auto Member>
Collection_Item_View subtitle() const {
auto result = *this;
result.subtitle_field = described_member_name<Member>();
return result;
}
template <auto Member>
Collection_Item_View accent() const {
auto result = *this;
result.accent_field = described_member_name<Member>();
return result;
}
template <auto... Members>
Collection_Item_View body() const {
auto result = *this;
result.fields = {described_member_name<Members>()...};
return result;
}
};
template <auto Member>
Collection_Item_View collection_item() {
Collection_Item_View result;
result.title_field = described_member_name<Member>();
return result;
}
struct Collection_View {
Table_View table;
Collection_Mode mode{Collection_Mode::table};
Collection_Item_View item;
std::size_t cards_columns{3};
Collection_View titled(std::string value) const {
auto result = *this;
result.table.title = std::move(value);
return result;
}
Collection_View id_title(std::string value) const {
auto result = *this;
result.table.id_label = std::move(value);
return result;
}
Collection_View create_title(std::string value) const {
auto result = *this;
result.table.create_label = std::move(value);
return result;
}
Collection_View edit_title(std::string value) const {
auto result = *this;
result.table.edit_label = std::move(value);
return result;
}
Collection_View delete_title(std::string value) const {
auto result = *this;
result.table.delete_label = std::move(value);
return result;
}
Collection_View actions_title(std::string value) const {
auto result = *this;
result.table.actions_label = std::move(value);
return result;
}
Collection_View confirm_title(std::string value) const {
auto result = *this;
result.table.confirm_label = std::move(value);
return result;
}
Collection_View delete_confirmation(std::string value) const {
auto result = *this;
result.table.delete_confirm = std::move(value);
return result;
}
Collection_View status_title(std::string value) const {
auto result = *this;
result.table.view_status_label = std::move(value);
return result;
}
Collection_View default_sort(std::string field_name, bool descending = false) const {
auto result = *this;
result.table.default_order_by = std::move(field_name);
result.table.default_order_dir = descending ? "desc" : "asc";
return result;
}
Collection_View reorderable(bool value = true) const {
auto result = *this;
result.table.user_reorderable = value;
return result;
}
Collection_View reorderable_columns(bool value = true) const {
auto result = *this;
result.table.column_reorderable = value;
return result;
}
Collection_View create_layout(View_Node value) const {
auto result = *this;
result.table.create_body = std::move(value);
return result;
}
Collection_View edit_layout(View_Node value) const {
auto result = *this;
result.table.edit_body = std::move(value);
return result;
}
Collection_View as_table() const {
auto result = *this;
result.mode = Collection_Mode::table;
result.item = {};
return result;
}
Collection_View as_list(Collection_Item_View value) const {
auto result = *this;
result.mode = Collection_Mode::list;
result.item = std::move(value);
return result;
}
Collection_View as_cards(Collection_Item_View value, std::size_t columns = 3) const {
auto result = *this;
result.mode = Collection_Mode::cards;
result.item = std::move(value);
result.cards_columns = columns;
return result;
}
};
template <Described_Type T, class... Columns>
Collection_View collection_view(Columns&&... columns) {
Collection_View result;
result.table = table_view<T>(std::forward<Columns>(columns)...);
result.table.name = describe<T>().name() + "_collection";
return result;
}
template <class T>
struct Type_View_Descriptor;
template <Described_Type T>
@@ -381,6 +519,14 @@ template <Described_Type T>
Table_View describe_table_view() requires Table_View_Described_Type<T> {
return Type_View_Descriptor<T>::table();
}
template <class T>
concept Collection_View_Described_Type = Described_Type<T> && requires {
{ Type_View_Descriptor<T>::collection() } -> std::same_as<Collection_View>;
};
template <Described_Type T>
Collection_View describe_collection_view() requires Collection_View_Described_Type<T> {
return Type_View_Descriptor<T>::collection();
}
template <class Descriptor>
bool descriptor_has_field(const Descriptor& descriptor, std::string_view name) {
bool found{};
@@ -457,4 +603,29 @@ void validate_table_view(const Table_View& view) {
validate_view_node<T>(view.create_body);
validate_view_node<T>(view.edit_body);
}
template <Described_Type T>
void validate_collection_view(const Collection_View& view) {
validate_table_view<T>(view.table);
if(view.mode == Collection_Mode::table) {
return;
}
const auto descriptor = describe<T>();
if(view.item.title_field.empty() || !descriptor_has_field(descriptor, view.item.title_field)) {
throw std::invalid_argument("collection item title field does not exist: " + view.item.title_field);
}
if(!view.item.subtitle_field.empty() && !descriptor_has_field(descriptor, view.item.subtitle_field)) {
throw std::invalid_argument("collection item subtitle field does not exist: " + view.item.subtitle_field);
}
if(!view.item.accent_field.empty() && !descriptor_has_field(descriptor, view.item.accent_field)) {
throw std::invalid_argument("collection item accent field does not exist: " + view.item.accent_field);
}
for(const auto& field : view.item.fields) {
if(!descriptor_has_field(descriptor, field)) {
throw std::invalid_argument("collection item body field does not exist: " + field);
}
}
if(view.mode == Collection_Mode::cards && view.cards_columns == 0) {
throw std::invalid_argument("collection cards requires at least one column");
}
}
}
+86 -7
View File
@@ -2,6 +2,7 @@
#include "adminive/composition.hpp"
#include "adminive/json.hpp"
#include "adminive/view.hpp"
#include <map>
namespace adminive {
inline std::string_view composition_node_kind_name(Composition_Node_Kind value) noexcept {
switch(value) {
@@ -81,6 +82,39 @@ Json to_view_json(const Composition_View& view) {
json_set(result, "body", composition_node_to_json<Json>(view.body));
return result;
}
template <Json_Type Json>
Json to_composition_manifest_json(const Composition_View& view, const std::vector<Composition_Slot_Contract>& contracts, const std::map<std::string, Json>& schemas) {
validate_composition_slot_contracts(view, contracts);
Json slots = json_object<Json>();
for(const auto& contract : contracts) {
const auto schema = schemas.find(contract.name);
if(schema == schemas.end()) {
throw std::invalid_argument("composition slot schema is not provided: " + contract.name);
}
Json item = json_object<Json>();
json_set(item, "component_kind", contract.component_kind);
if(!contract.descriptor_api.empty()) {
json_set(item, "descriptor_api", contract.descriptor_api);
}
if(!contract.view_api.empty()) {
json_set(item, "view_api", contract.view_api);
}
if(!contract.data_api.empty()) {
json_set(item, "data_api", contract.data_api);
}
if(!contract.amis_api.empty()) {
json_set(item, "amis_api", contract.amis_api);
}
json_set(item, "schema", schema->second);
json_set(slots, contract.name, std::move(item));
}
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.composition-manifest");
json_set(result, "protocol_version", 1);
json_set(result, "view", to_view_json<Json>(view));
json_set(result, "slots", std::move(slots));
return result;
}
inline std::string_view view_node_kind_name(View_Node_Kind value) noexcept {
switch(value) {
case View_Node_Kind::all_fields:
@@ -179,13 +213,19 @@ Json to_view_json(const Form_View& view) {
json_set(result, "body", view_node_to_json<Json>(view.body));
return result;
}
template <Json_Type Json, Described_Type T>
Json to_view_json(const Table_View& view) {
validate_table_view<T>(view);
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.view");
json_set(result, "protocol_version", 1);
json_set(result, "kind", "table");
inline std::string_view collection_mode_name(Collection_Mode value) noexcept {
switch(value) {
case Collection_Mode::table:
return "table";
case Collection_Mode::list:
return "list";
case Collection_Mode::cards:
return "cards";
}
return "table";
}
template <Json_Type Json>
void append_table_view_json(Json& result, const Table_View& view) {
json_set(result, "name", view.name);
json_set(result, "title", view.title);
json_set(result, "id_label", view.id_label);
@@ -216,6 +256,45 @@ Json to_view_json(const Table_View& view) {
json_set(result, "columns", std::move(columns));
json_set(result, "create_body", view_node_to_json<Json>(view.create_body));
json_set(result, "edit_body", view_node_to_json<Json>(view.edit_body));
}
template <Json_Type Json, Described_Type T>
Json to_view_json(const Table_View& view) {
validate_table_view<T>(view);
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.view");
json_set(result, "protocol_version", 1);
json_set(result, "kind", "table");
append_table_view_json(result, view);
return result;
}
template <Json_Type Json, Described_Type T>
Json to_view_json(const Collection_View& view) {
validate_collection_view<T>(view);
Json result = json_object<Json>();
json_set(result, "protocol", "adminive.view");
json_set(result, "protocol_version", 1);
json_set(result, "kind", "collection");
json_set(result, "mode", std::string(collection_mode_name(view.mode)));
append_table_view_json(result, view.table);
if(view.mode != Collection_Mode::table) {
Json item = json_object<Json>();
json_set(item, "title_field", view.item.title_field);
if(!view.item.subtitle_field.empty()) {
json_set(item, "subtitle_field", view.item.subtitle_field);
}
if(!view.item.accent_field.empty()) {
json_set(item, "accent_field", view.item.accent_field);
}
Json fields = json_array<Json>();
for(const auto& field : view.item.fields) {
json_append(fields, field);
}
json_set(item, "fields", std::move(fields));
json_set(result, "item", std::move(item));
}
if(view.mode == Collection_Mode::cards) {
json_set(result, "cards_columns", view.cards_columns);
}
return result;
}
}