94分控件
This commit is contained in:
@@ -240,7 +240,9 @@ tabs / tab
|
||||
|
||||
后端因此仍然可以完整决定“这十几个字段如何拼接”,但组合关系不再污染字段定义。一个类型可以分别拥有 Edit/Create/Detail View。
|
||||
|
||||
Table View 是一等视图,而不是普通 Field Presentation 的附属标记。它负责:
|
||||
Collection View 是集合展示的一等视图。`table/list/cards` 是同一个集合契约的三个 mode,共享字段、查询、CRUD、排序与 create/edit layout;list/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 与字段权限推导 payload:readonly 字段可以出现在初始展示数据里,但 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
|
||||
```
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user