分层重构
This commit is contained in:
@@ -2,10 +2,14 @@
|
||||
#include "adminive/adapter.hpp"
|
||||
#include "adminive/amis.hpp"
|
||||
#include "adminive/collection.hpp"
|
||||
#include "adminive/composition.hpp"
|
||||
#include "adminive/concepts.hpp"
|
||||
#include "adminive/descriptor.hpp"
|
||||
#include "adminive/http.hpp"
|
||||
#include "adminive/json.hpp"
|
||||
#include "adminive/status.hpp"
|
||||
#include "adminive/view.hpp"
|
||||
#include "adminive/view_json.hpp"
|
||||
#include "adminive/managed.hpp"
|
||||
#include "adminive/presentation.hpp"
|
||||
#include "adminive/value.hpp"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
#include "adminive/status.hpp"
|
||||
#include "adminive/view_json.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -47,9 +49,18 @@ inline std::string resolve_visible_on(std::string expression, std::string_view p
|
||||
return expression;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
const Json& field_presentation(const Json& field) {
|
||||
return Json_Adapter<Json>::at(field, "presentation");
|
||||
}
|
||||
template <Json_Type Json>
|
||||
std::string field_label(const Json& field) {
|
||||
return json_get<Json, std::string>(Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
|
||||
}
|
||||
template <Json_Type Json>
|
||||
void apply_visible_on(Json& control, const Json& field, std::string_view prefix) {
|
||||
if(Json_Adapter<Json>::contains(field, "visible_on")) {
|
||||
json_set(control, "visibleOn", resolve_visible_on(json_get<Json, std::string>(Json_Adapter<Json>::at(field, "visible_on")), prefix));
|
||||
const auto& presentation = field_presentation<Json>(field);
|
||||
if(Json_Adapter<Json>::contains(presentation, "visible_on")) {
|
||||
json_set(control, "visibleOn", resolve_visible_on(json_get<Json, std::string>(Json_Adapter<Json>::at(presentation, "visible_on")), prefix));
|
||||
}
|
||||
}
|
||||
template <Json_Type Json>
|
||||
@@ -99,12 +110,12 @@ Json make_amis_polymorphic_control(const Json& field, std::string_view permissio
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "fieldset");
|
||||
json_set(result, "title", Json_Adapter<Json>::at(field, "label"));
|
||||
json_set(result, "title", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
|
||||
json_set(result, "body", std::move(body));
|
||||
json_set(result, "collapsable", true);
|
||||
json_set(result, "collapsed", false);
|
||||
if(Json_Adapter<Json>::contains(field, "description")) {
|
||||
json_set(result, "description", Json_Adapter<Json>::at(field, "description"));
|
||||
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
|
||||
json_set(result, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
|
||||
}
|
||||
apply_visible_on(result, field, prefix);
|
||||
return result;
|
||||
@@ -138,16 +149,55 @@ Json make_typed_amis_polymorphic_control(const Json& field, std::string_view per
|
||||
}, Adapter::variants());
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "fieldset");
|
||||
json_set(result, "title", Json_Adapter<Json>::at(field, "label"));
|
||||
json_set(result, "title", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
|
||||
json_set(result, "body", std::move(body));
|
||||
json_set(result, "collapsable", true);
|
||||
json_set(result, "collapsed", false);
|
||||
if(Json_Adapter<Json>::contains(field, "description")) {
|
||||
json_set(result, "description", Json_Adapter<Json>::at(field, "description"));
|
||||
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
|
||||
json_set(result, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
|
||||
}
|
||||
apply_visible_on(result, field, prefix);
|
||||
return result;
|
||||
}
|
||||
inline std::string_view amis_control_type(std::string_view control, std::string_view value_type) {
|
||||
if(control == "text") {
|
||||
return "input-text";
|
||||
}
|
||||
if(control == "multiline_text") {
|
||||
return "textarea";
|
||||
}
|
||||
if(control == "number") {
|
||||
return "input-number";
|
||||
}
|
||||
if(control == "boolean") {
|
||||
return "switch";
|
||||
}
|
||||
if(control == "select") {
|
||||
return "select";
|
||||
}
|
||||
if(control == "date") {
|
||||
return "input-date";
|
||||
}
|
||||
if(control == "color") {
|
||||
return "input-color";
|
||||
}
|
||||
if(control != "automatic") {
|
||||
throw std::invalid_argument("unknown field control: " + std::string(control));
|
||||
}
|
||||
if(value_type == "boolean") {
|
||||
return "switch";
|
||||
}
|
||||
if(value_type == "integer" || value_type == "number") {
|
||||
return "input-number";
|
||||
}
|
||||
if(value_type == "enum") {
|
||||
return "select";
|
||||
}
|
||||
if(value_type == "string") {
|
||||
return "input-text";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_default_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) {
|
||||
const std::string name = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "name"));
|
||||
@@ -158,12 +208,12 @@ Json make_default_amis_control(const Json& field, std::string_view permission, s
|
||||
json_set(descriptor, "fields", Json_Adapter<Json>::at(field, "children"));
|
||||
Json control = json_object<Json>();
|
||||
json_set(control, "type", "fieldset");
|
||||
json_set(control, "title", Json_Adapter<Json>::at(field, "label"));
|
||||
json_set(control, "title", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
|
||||
json_set(control, "body", make_amis_form_body<Json>(descriptor, permission, full_name));
|
||||
json_set(control, "collapsable", true);
|
||||
json_set(control, "collapsed", false);
|
||||
if(Json_Adapter<Json>::contains(field, "description")) {
|
||||
json_set(control, "description", Json_Adapter<Json>::at(field, "description"));
|
||||
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
|
||||
json_set(control, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
|
||||
}
|
||||
apply_visible_on(control, field, prefix);
|
||||
return control;
|
||||
@@ -173,37 +223,29 @@ Json make_default_amis_control(const Json& field, std::string_view permission, s
|
||||
}
|
||||
Json control = json_object<Json>();
|
||||
json_set(control, "name", full_name);
|
||||
json_set(control, "label", Json_Adapter<Json>::at(field, "label"));
|
||||
if(Json_Adapter<Json>::contains(field, "description")) {
|
||||
json_set(control, "description", Json_Adapter<Json>::at(field, "description"));
|
||||
json_set(control, "label", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
|
||||
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
|
||||
json_set(control, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
|
||||
}
|
||||
apply_visible_on(control, field, prefix);
|
||||
if(!json_get<Json, bool>(Json_Adapter<Json>::at(field, permission))) {
|
||||
json_set(control, "type", "static");
|
||||
return control;
|
||||
}
|
||||
if(Json_Adapter<Json>::contains(field, "widget")) {
|
||||
json_set(control, "type", Json_Adapter<Json>::at(field, "widget"));
|
||||
} else if(value_type == "boolean") {
|
||||
json_set(control, "type", "switch");
|
||||
} else if(value_type == "integer" || value_type == "number") {
|
||||
json_set(control, "type", "input-number");
|
||||
} else if(value_type == "enum") {
|
||||
json_set(control, "type", "select");
|
||||
} else if(value_type == "string") {
|
||||
json_set(control, "type", "input-text");
|
||||
} else {
|
||||
throw std::invalid_argument("field '" + full_name + "' requires an explicit widget or Control_Adapter");
|
||||
const auto control_type = amis_control_type(json_get<Json, std::string>(Json_Adapter<Json>::at(field_presentation<Json>(field), "control")), value_type);
|
||||
if(control_type.empty()) {
|
||||
throw std::invalid_argument("field '" + full_name + "' requires an explicit field control or Control_Adapter");
|
||||
}
|
||||
const std::string control_type = json_get<Json, std::string>(Json_Adapter<Json>::at(control, "type"));
|
||||
json_set(control, "type", std::string(control_type));
|
||||
const std::string resolved_control_type = json_get<Json, std::string>(Json_Adapter<Json>::at(control, "type"));
|
||||
if(Json_Adapter<Json>::contains(field, "nullable") && json_get<Json, bool>(Json_Adapter<Json>::at(field, "nullable"))) {
|
||||
json_set(control, "clearable", true);
|
||||
}
|
||||
if(control_type == "input-date") {
|
||||
if(resolved_control_type == "input-date") {
|
||||
json_set(control, "valueFormat", "YYYY-MM-DD");
|
||||
json_set(control, "displayFormat", "YYYY-MM-DD");
|
||||
json_set(control, "clearable", true);
|
||||
} else if(control_type == "input-color") {
|
||||
} else if(resolved_control_type == "input-color") {
|
||||
json_set(control, "clearable", true);
|
||||
}
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "required"))) {
|
||||
@@ -223,8 +265,8 @@ Json make_default_amis_control(const Json& field, std::string_view permission, s
|
||||
json_set(errors, "isInt", Json_Adapter<Json>::at(field, "validation_message"));
|
||||
json_set(control, "validationErrors", std::move(errors));
|
||||
}
|
||||
if(Json_Adapter<Json>::contains(field, "options")) {
|
||||
json_set(control, "options", Json_Adapter<Json>::at(field, "options"));
|
||||
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "options")) {
|
||||
json_set(control, "options", Json_Adapter<Json>::at(field_presentation<Json>(field), "options"));
|
||||
}
|
||||
return control;
|
||||
}
|
||||
@@ -253,12 +295,12 @@ Json make_typed_amis_control(const Json& field, std::string_view permission, std
|
||||
const Json descriptor = to_descriptor_json<Json, Control_Value>();
|
||||
Json control = json_object<Json>();
|
||||
json_set(control, "type", "fieldset");
|
||||
json_set(control, "title", Json_Adapter<Json>::at(field, "label"));
|
||||
json_set(control, "title", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
|
||||
json_set(control, "body", make_typed_amis_form_body<Json, Model>(descriptor, permission, full_name));
|
||||
json_set(control, "collapsable", true);
|
||||
json_set(control, "collapsed", false);
|
||||
if(Json_Adapter<Json>::contains(field, "description")) {
|
||||
json_set(control, "description", Json_Adapter<Json>::at(field, "description"));
|
||||
if(Json_Adapter<Json>::contains(field_presentation<Json>(field), "description")) {
|
||||
json_set(control, "description", Json_Adapter<Json>::at(field_presentation<Json>(field), "description"));
|
||||
}
|
||||
apply_visible_on(control, field, prefix);
|
||||
return control;
|
||||
@@ -272,9 +314,7 @@ Json make_amis_form_body(const Json& descriptor, std::string_view permission, st
|
||||
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);
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "visible"))) {
|
||||
json_append(body, make_default_amis_control<Json>(field, permission, prefix));
|
||||
}
|
||||
json_append(body, make_default_amis_control<Json>(field, permission, prefix));
|
||||
}
|
||||
return body;
|
||||
}
|
||||
@@ -284,10 +324,8 @@ void append_typed_amis_form_fields(Json& body, const Json& fields, const Descrip
|
||||
if constexpr(Index < std::tuple_size_v<Fields>) {
|
||||
const auto& item = std::get<Index>(descriptor.fields());
|
||||
const auto& field = Json_Adapter<Json>::at(fields, Index);
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "visible"))) {
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
json_append(body, make_typed_amis_control<Json, typename Field::member_type>(field, permission, prefix));
|
||||
}
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
json_append(body, make_typed_amis_control<Json, typename Field::member_type>(field, permission, prefix));
|
||||
append_typed_amis_form_fields<Index + 1>(body, fields, descriptor, permission, prefix);
|
||||
}
|
||||
}
|
||||
@@ -303,20 +341,14 @@ template <Json_Type Json>
|
||||
Json make_default_amis_column(const Json& field) {
|
||||
Json column = json_object<Json>();
|
||||
json_set(column, "name", Json_Adapter<Json>::at(field, "name"));
|
||||
json_set(column, "label", Json_Adapter<Json>::at(field, "list_label"));
|
||||
if(Json_Adapter<Json>::contains(field, "order")) {
|
||||
json_set(column, "order", Json_Adapter<Json>::at(field, "order"));
|
||||
}
|
||||
if(Json_Adapter<Json>::contains(field, "sortable")) {
|
||||
json_set(column, "sortable", Json_Adapter<Json>::at(field, "sortable"));
|
||||
}
|
||||
json_set(column, "label", Json_Adapter<Json>::at(field_presentation<Json>(field), "label"));
|
||||
const std::string value_type = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type"));
|
||||
if(value_type == "boolean") {
|
||||
json_set(column, "type", "status");
|
||||
} else if(value_type == "enum" && Json_Adapter<Json>::contains(field, "options")) {
|
||||
} else if(value_type == "enum" && Json_Adapter<Json>::contains(field_presentation<Json>(field), "options")) {
|
||||
json_set(column, "type", "mapping");
|
||||
Json map = json_object<Json>();
|
||||
const auto& options = Json_Adapter<Json>::at(field, "options");
|
||||
const auto& options = Json_Adapter<Json>::at(field_presentation<Json>(field), "options");
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(options); ++index) {
|
||||
const auto& option = Json_Adapter<Json>::at(options, index);
|
||||
json_set(map, json_get<Json, std::string>(Json_Adapter<Json>::at(option, "value")), Json_Adapter<Json>::at(option, "label"));
|
||||
@@ -340,31 +372,316 @@ Json make_typed_amis_column(const Json& field) {
|
||||
return make_default_amis_column<Json>(field);
|
||||
}
|
||||
}
|
||||
template <Json_Type Json>
|
||||
struct Amis_Ordered_Column {
|
||||
int order{};
|
||||
Json column;
|
||||
};
|
||||
template <std::size_t Index, Json_Type Json, class Descriptor>
|
||||
void append_typed_amis_columns(std::vector<Amis_Ordered_Column<Json>>& columns, const Json& fields, const Descriptor& descriptor) {
|
||||
template <std::size_t Index = 0, Json_Type Json, class Descriptor>
|
||||
Json make_typed_amis_control_by_name(const Json& descriptor_json, const Descriptor& descriptor, std::string_view field_name, std::string_view permission, std::string_view prefix) {
|
||||
using Fields = std::remove_cvref_t<decltype(descriptor.fields())>;
|
||||
if constexpr(Index < std::tuple_size_v<Fields>) {
|
||||
const auto& item = std::get<Index>(descriptor.fields());
|
||||
const auto& field = Json_Adapter<Json>::at(fields, Index);
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
using Member = typename Field::member_type;
|
||||
using Storage = std::remove_cvref_t<Member>;
|
||||
using Value = Adapted_Value_Type<Storage, Json>;
|
||||
using Control_Value = Optional_Unwrapped_Type<Value>;
|
||||
constexpr bool custom_column = Control_Adapter_With_Column<Storage, Json> || (!std::same_as<Storage, Value> && Control_Adapter_With_Column<Value, Json>) || (!std::same_as<Value, Control_Value> && Control_Adapter_With_Column<Control_Value, Json>);
|
||||
const std::string value_type = json_get<Json, std::string>(Json_Adapter<Json>::at(field, "value_type"));
|
||||
const bool structural = value_type == "object" || value_type == "array" || value_type == "map" || value_type == "polymorphic";
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(field, "readable")) && json_get<Json, bool>(Json_Adapter<Json>::at(field, "list_visible")) && (!structural || custom_column)) {
|
||||
columns.push_back(Amis_Ordered_Column<Json>{json_get<Json, int>(Json_Adapter<Json>::at(field, "order")), make_typed_amis_column<Json, Member>(field)});
|
||||
if(item.name() == field_name) {
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
return make_typed_amis_control<Json, typename Field::member_type>(Json_Adapter<Json>::at(Json_Adapter<Json>::at(descriptor_json, "fields"), Index), permission, prefix);
|
||||
}
|
||||
append_typed_amis_columns<Index + 1>(columns, fields, descriptor);
|
||||
return make_typed_amis_control_by_name<Index + 1>(descriptor_json, descriptor, field_name, permission, prefix);
|
||||
} else {
|
||||
throw std::invalid_argument("view field does not exist: " + std::string(field_name));
|
||||
}
|
||||
}
|
||||
template <std::size_t Index = 0, Json_Type Json, class Descriptor>
|
||||
Json make_typed_amis_column_by_name(const Json& descriptor_json, const Descriptor& descriptor, std::string_view field_name) {
|
||||
using Fields = std::remove_cvref_t<decltype(descriptor.fields())>;
|
||||
if constexpr(Index < std::tuple_size_v<Fields>) {
|
||||
const auto& item = std::get<Index>(descriptor.fields());
|
||||
if(item.name() == field_name) {
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
return make_typed_amis_column<Json, typename Field::member_type>(Json_Adapter<Json>::at(Json_Adapter<Json>::at(descriptor_json, "fields"), Index));
|
||||
}
|
||||
return make_typed_amis_column_by_name<Index + 1>(descriptor_json, descriptor, field_name);
|
||||
} else {
|
||||
throw std::invalid_argument("table column field does not exist: " + std::string(field_name));
|
||||
}
|
||||
}
|
||||
inline std::string_view form_permission(Form_Mode mode) noexcept {
|
||||
switch(mode) {
|
||||
case Form_Mode::display:
|
||||
return "readable";
|
||||
case Form_Mode::edit:
|
||||
return "editable";
|
||||
case Form_Mode::create:
|
||||
return "creatable";
|
||||
}
|
||||
return "readable";
|
||||
}
|
||||
template <Json_Type Json>
|
||||
void apply_view_node_options(Json& result, const View_Node& node, std::string_view prefix) {
|
||||
if(!node.description.empty()) {
|
||||
json_set(result, "description", node.description);
|
||||
}
|
||||
if(!node.visible_on.empty()) {
|
||||
json_set(result, "visibleOn", resolve_visible_on(node.visible_on, prefix));
|
||||
}
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json make_amis_view_body(const View_Node& node, const Json& descriptor_json, std::string_view permission, std::string_view prefix);
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json make_amis_view_node(const View_Node& node, const Json& descriptor_json, std::string_view permission, std::string_view prefix) {
|
||||
const auto descriptor = describe<T>();
|
||||
if(node.kind == View_Node_Kind::field) {
|
||||
return make_typed_amis_control_by_name(descriptor_json, descriptor, node.field, permission, prefix);
|
||||
}
|
||||
if(node.kind == View_Node_Kind::group) {
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "fieldset");
|
||||
json_set(result, "title", node.title);
|
||||
json_set(result, "body", make_amis_view_body<Json, T>(View_Node{View_Node_Kind::vertical, {}, {}, {}, {}, 0, false, false, node.children}, descriptor_json, permission, prefix));
|
||||
if(node.collapsible) {
|
||||
json_set(result, "collapsable", true);
|
||||
json_set(result, "collapsed", node.collapsed);
|
||||
}
|
||||
apply_view_node_options(result, node, prefix);
|
||||
return result;
|
||||
}
|
||||
if(node.kind == View_Node_Kind::card) {
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "card");
|
||||
if(!node.title.empty()) {
|
||||
Json header = json_object<Json>();
|
||||
json_set(header, "title", node.title);
|
||||
json_set(result, "header", std::move(header));
|
||||
}
|
||||
json_set(result, "body", make_amis_view_body<Json, T>(View_Node{View_Node_Kind::vertical, {}, {}, {}, {}, 0, false, false, node.children}, descriptor_json, permission, prefix));
|
||||
apply_view_node_options(result, node, prefix);
|
||||
return result;
|
||||
}
|
||||
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) {
|
||||
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_append(columns, std::move(column));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "grid");
|
||||
json_set(result, "columns", std::move(columns));
|
||||
apply_view_node_options(result, node, prefix);
|
||||
return result;
|
||||
}
|
||||
if(node.kind == View_Node_Kind::flow) {
|
||||
Json items = json_array<Json>();
|
||||
for(const auto& child : node.children) {
|
||||
Json item = json_object<Json>();
|
||||
json_set(item, "body", make_amis_view_body<Json, T>(child, descriptor_json, permission, prefix));
|
||||
json_append(items, std::move(item));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "flex");
|
||||
json_set(result, "items", std::move(items));
|
||||
apply_view_node_options(result, node, prefix);
|
||||
return result;
|
||||
}
|
||||
if(node.kind == View_Node_Kind::tabs) {
|
||||
Json tab_items = json_array<Json>();
|
||||
for(const auto& child : node.children) {
|
||||
Json item = json_object<Json>();
|
||||
json_set(item, "title", child.title);
|
||||
json_set(item, "body", make_amis_view_body<Json, T>(View_Node{View_Node_Kind::vertical, {}, {}, {}, {}, 0, false, false, child.children}, descriptor_json, permission, prefix));
|
||||
if(!child.visible_on.empty()) {
|
||||
json_set(item, "visibleOn", resolve_visible_on(child.visible_on, prefix));
|
||||
}
|
||||
json_append(tab_items, std::move(item));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "tabs");
|
||||
json_set(result, "tabs", std::move(tab_items));
|
||||
apply_view_node_options(result, node, prefix);
|
||||
return result;
|
||||
}
|
||||
if(node.kind == View_Node_Kind::tab) {
|
||||
throw std::invalid_argument("tab view can only be used inside tabs");
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "container");
|
||||
json_set(result, "body", make_amis_view_body<Json, T>(View_Node{View_Node_Kind::vertical, {}, {}, {}, {}, 0, false, false, node.children}, descriptor_json, permission, prefix));
|
||||
apply_view_node_options(result, node, prefix);
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
void append_amis_view_node(Json& body, const View_Node& node, const Json& descriptor_json, std::string_view permission, std::string_view prefix) {
|
||||
if(node.kind == View_Node_Kind::all_fields) {
|
||||
Json fields = make_typed_amis_form_body<Json, T>(descriptor_json, permission, prefix);
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(fields); ++index) {
|
||||
json_append(body, Json_Adapter<Json>::at(fields, index));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(node.kind == View_Node_Kind::vertical) {
|
||||
for(const auto& child : node.children) {
|
||||
append_amis_view_node<Json, T>(body, child, descriptor_json, permission, prefix);
|
||||
}
|
||||
return;
|
||||
}
|
||||
json_append(body, make_amis_view_node<Json, T>(node, descriptor_json, permission, prefix));
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json make_amis_view_body(const View_Node& node, const Json& descriptor_json, std::string_view permission, std::string_view prefix) {
|
||||
Json result = json_array<Json>();
|
||||
append_amis_view_node<Json, T>(result, node, descriptor_json, permission, prefix);
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
void apply_composition_node_options(Json& result, const Composition_Node& node) {
|
||||
if(!node.description.empty()) {
|
||||
json_set(result, "description", node.description);
|
||||
}
|
||||
if(!node.visible_on.empty()) {
|
||||
json_set(result, "visibleOn", node.visible_on);
|
||||
}
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_composition_body(const Composition_Node& node, const std::map<std::string, Json>& slots);
|
||||
template <Json_Type Json>
|
||||
Json make_amis_composition_node(const Composition_Node& node, const std::map<std::string, Json>& slots) {
|
||||
if(node.kind == Composition_Node_Kind::slot) {
|
||||
const auto iterator = slots.find(node.slot);
|
||||
if(iterator == slots.end()) {
|
||||
throw std::invalid_argument("composition slot is not provided: " + node.slot);
|
||||
}
|
||||
return iterator->second;
|
||||
}
|
||||
if(node.kind == Composition_Node_Kind::heading) {
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "tpl");
|
||||
std::string html = "<h3 style=\"margin:0";
|
||||
if(!node.accent_color.empty()) {
|
||||
html += ";color:" + node.accent_color;
|
||||
}
|
||||
html += "\">" + node.title + "</h3>";
|
||||
json_set(result, "tpl", std::move(html));
|
||||
apply_composition_node_options(result, node);
|
||||
return result;
|
||||
}
|
||||
if(node.kind == Composition_Node_Kind::group) {
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "panel");
|
||||
json_set(result, "title", node.title);
|
||||
json_set(result, "body", make_amis_composition_body<Json>(Composition_Node{Composition_Node_Kind::vertical, {}, {}, {}, {}, {}, 0, false, false, node.children}, slots));
|
||||
if(node.collapsible) {
|
||||
json_set(result, "collapsable", true);
|
||||
json_set(result, "collapsed", node.collapsed);
|
||||
}
|
||||
apply_composition_node_options(result, node);
|
||||
return result;
|
||||
}
|
||||
if(node.kind == Composition_Node_Kind::card) {
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "card");
|
||||
if(!node.title.empty()) {
|
||||
Json header = json_object<Json>();
|
||||
json_set(header, "title", node.title);
|
||||
json_set(result, "header", std::move(header));
|
||||
}
|
||||
json_set(result, "body", make_amis_composition_body<Json>(Composition_Node{Composition_Node_Kind::vertical, {}, {}, {}, {}, {}, 0, false, false, node.children}, slots));
|
||||
apply_composition_node_options(result, node);
|
||||
return result;
|
||||
}
|
||||
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) {
|
||||
Json column = json_object<Json>();
|
||||
json_set(column, "md", width);
|
||||
json_set(column, "body", make_amis_composition_body<Json>(child, slots));
|
||||
json_append(columns, std::move(column));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "grid");
|
||||
json_set(result, "columns", std::move(columns));
|
||||
apply_composition_node_options(result, node);
|
||||
return result;
|
||||
}
|
||||
if(node.kind == Composition_Node_Kind::flow) {
|
||||
Json items = json_array<Json>();
|
||||
for(const auto& child : node.children) {
|
||||
Json item = json_object<Json>();
|
||||
json_set(item, "body", make_amis_composition_body<Json>(child, slots));
|
||||
json_append(items, std::move(item));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "flex");
|
||||
json_set(result, "items", std::move(items));
|
||||
apply_composition_node_options(result, node);
|
||||
return result;
|
||||
}
|
||||
if(node.kind == Composition_Node_Kind::tabs) {
|
||||
Json tab_items = json_array<Json>();
|
||||
for(const auto& child : node.children) {
|
||||
Json item = json_object<Json>();
|
||||
json_set(item, "title", child.title);
|
||||
json_set(item, "body", make_amis_composition_body<Json>(Composition_Node{Composition_Node_Kind::vertical, {}, {}, {}, {}, {}, 0, false, false, child.children}, slots));
|
||||
if(!child.visible_on.empty()) {
|
||||
json_set(item, "visibleOn", child.visible_on);
|
||||
}
|
||||
json_append(tab_items, std::move(item));
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "tabs");
|
||||
json_set(result, "tabs", std::move(tab_items));
|
||||
apply_composition_node_options(result, node);
|
||||
return result;
|
||||
}
|
||||
if(node.kind == Composition_Node_Kind::tab) {
|
||||
throw std::invalid_argument("composition tab can only be used inside tabs");
|
||||
}
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "container");
|
||||
json_set(result, "body", make_amis_composition_body<Json>(Composition_Node{Composition_Node_Kind::vertical, {}, {}, {}, {}, {}, 0, false, false, node.children}, slots));
|
||||
apply_composition_node_options(result, node);
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
void append_amis_composition_node(Json& body, const Composition_Node& node, const std::map<std::string, Json>& slots) {
|
||||
if(node.kind == Composition_Node_Kind::vertical) {
|
||||
for(const auto& child : node.children) {
|
||||
append_amis_composition_node(body, child, slots);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(node.kind == Composition_Node_Kind::slot) {
|
||||
const auto iterator = slots.find(node.slot);
|
||||
if(iterator == slots.end()) {
|
||||
throw std::invalid_argument("composition slot is not provided: " + node.slot);
|
||||
}
|
||||
if(Json_Adapter<Json>::is_array(iterator->second)) {
|
||||
for(std::size_t index = 0; index < Json_Adapter<Json>::size(iterator->second); ++index) {
|
||||
json_append(body, Json_Adapter<Json>::at(iterator->second, index));
|
||||
}
|
||||
} else {
|
||||
json_append(body, iterator->second);
|
||||
}
|
||||
return;
|
||||
}
|
||||
json_append(body, make_amis_composition_node<Json>(node, slots));
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_composition_body(const Composition_Node& node, const std::map<std::string, Json>& slots) {
|
||||
Json result = json_array<Json>();
|
||||
append_amis_composition_node(result, node, slots);
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json to_amis_composition_schema(const Composition_View& view, const std::map<std::string, Json>& slots) {
|
||||
validate_composition_view(view);
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", view.title.empty() ? "container" : "panel");
|
||||
if(!view.title.empty()) {
|
||||
json_set(result, "title", view.title);
|
||||
}
|
||||
json_set(result, "body", make_amis_composition_body<Json>(view.body, slots));
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json make_amis_status_body(const Json& descriptor) {
|
||||
Json body = json_array<Json>();
|
||||
@@ -441,64 +758,75 @@ Json make_amis_object_status_column(const Json& descriptor, std::string status_a
|
||||
}
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<std::remove_cvref_t<T>> || Snapshot_Adapted_Object<T>
|
||||
Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::string submit_label = "Apply") {
|
||||
Json to_amis_form_schema(const T& value, const Form_View& view, std::string submit_api = {}) {
|
||||
using Model = Object_Model_Type<T>;
|
||||
validate_form_view<Model>(view);
|
||||
const Json descriptor = to_descriptor_json<Json, T>();
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "form");
|
||||
json_set(result, "title", Json_Adapter<Json>::at(descriptor, "label"));
|
||||
json_set(result, "title", view.title);
|
||||
json_set(result, "data", to_frontend_json<Json>(value));
|
||||
json_set(result, "body", make_typed_amis_form_body<Json, Model>(descriptor, "editable"));
|
||||
json_set(result, "actions", make_amis_form_actions<Json>(std::move(submit_label)));
|
||||
json_set(result, "affixFooter", true);
|
||||
if(!submit_api.empty()) {
|
||||
Json api = json_object<Json>();
|
||||
json_set(api, "method", "post");
|
||||
json_set(api, "url", std::move(submit_api));
|
||||
json_set(result, "api", std::move(api));
|
||||
json_set(result, "body", make_amis_view_body<Json, Model>(view.body, descriptor, form_permission(view.mode), {}));
|
||||
if(view.mode != Form_Mode::display) {
|
||||
json_set(result, "actions", make_amis_form_actions<Json>(view.submit_label));
|
||||
json_set(result, "affixFooter", view.affix_footer);
|
||||
if(!submit_api.empty()) {
|
||||
Json api = json_object<Json>();
|
||||
json_set(api, "method", "post");
|
||||
json_set(api, "url", std::move(submit_api));
|
||||
json_set(result, "api", std::move(api));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Managed_Value_Type T>
|
||||
Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::string submit_label = "Apply") {
|
||||
Json to_amis_form_schema(const T& value, const Form_View& view, std::string submit_api = {}) {
|
||||
return value.read([&](const auto& item) {
|
||||
return adminive::to_amis_form_schema<Json>(item, std::move(submit_api), std::move(submit_label));
|
||||
return adminive::to_amis_form_schema<Json>(item, view, std::move(submit_api));
|
||||
});
|
||||
}
|
||||
template <Json_Type Json, Managed_Field_Type T>
|
||||
Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::string submit_label = "Apply") {
|
||||
Json to_amis_form_schema(const T& value, const Form_View& view, std::string submit_api = {}) {
|
||||
return value.read([&](const auto& item) {
|
||||
return adminive::to_amis_form_schema<Json>(item, std::move(submit_api), std::move(submit_label));
|
||||
return adminive::to_amis_form_schema<Json>(item, view, std::move(submit_api));
|
||||
});
|
||||
}
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<Object_Model_Type<T>>
|
||||
Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = nullptr, std::uint64_t status_interval = 2000) {
|
||||
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) {
|
||||
using Model = Object_Model_Type<T>;
|
||||
validate_table_view<Model>(view);
|
||||
const Json descriptor = to_descriptor_json<Json, T>();
|
||||
const Json& list = Json_Adapter<Json>::at(descriptor, "list");
|
||||
const std::string crud_id = json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "name")) + "_crud";
|
||||
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", Json_Adapter<Json>::at(list, "id_label"));
|
||||
json_set(id_column, "label", view.id_label);
|
||||
json_set(id_column, "sortable", true);
|
||||
json_append(columns, std::move(id_column));
|
||||
std::vector<Amis_Ordered_Column<Json>> ordered_columns;
|
||||
const auto& fields = Json_Adapter<Json>::at(descriptor, "fields");
|
||||
const auto object_descriptor = describe<Model>();
|
||||
append_typed_amis_columns<0>(ordered_columns, fields, object_descriptor);
|
||||
std::stable_sort(ordered_columns.begin(), ordered_columns.end(), [](const Amis_Ordered_Column<Json>& left, const Amis_Ordered_Column<Json>& right) {
|
||||
return left.order < right.order;
|
||||
});
|
||||
for(auto& column : ordered_columns) {
|
||||
json_append(columns, std::move(column.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(status_descriptor) {
|
||||
json_append(columns, make_amis_object_status_column<Json>(*status_descriptor, base_api + "/${id}/status", status_interval, json_get<Json, std::string>(Json_Adapter<Json>::at(list, "view_status_label"))));
|
||||
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));
|
||||
}
|
||||
const std::string create_label = json_get<Json, std::string>(Json_Adapter<Json>::at(list, "create_label"));
|
||||
const std::string edit_label = json_get<Json, std::string>(Json_Adapter<Json>::at(list, "edit_label"));
|
||||
Json create_api = json_object<Json>();
|
||||
json_set(create_api, "method", "post");
|
||||
json_set(create_api, "url", base_api);
|
||||
@@ -506,8 +834,8 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
|
||||
json_set(create_form, "type", "form");
|
||||
json_set(create_form, "api", std::move(create_api));
|
||||
json_set(create_form, "reload", crud_id);
|
||||
json_set(create_form, "body", make_typed_amis_form_body<Json, Model>(descriptor, "creatable"));
|
||||
json_set(create_form, "actions", make_amis_form_actions<Json>(create_label));
|
||||
json_set(create_form, "body", make_amis_view_body<Json, Model>(view.create_body, descriptor, "creatable", {}));
|
||||
json_set(create_form, "actions", make_amis_form_actions<Json>(view.create_label));
|
||||
Json edit_api = json_object<Json>();
|
||||
json_set(edit_api, "method", "put");
|
||||
json_set(edit_api, "url", base_api + "/${id}");
|
||||
@@ -515,23 +843,23 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
|
||||
json_set(edit_form, "type", "form");
|
||||
json_set(edit_form, "api", std::move(edit_api));
|
||||
json_set(edit_form, "reload", crud_id);
|
||||
json_set(edit_form, "body", make_typed_amis_form_body<Json, Model>(descriptor, "editable"));
|
||||
json_set(edit_form, "actions", make_amis_form_actions<Json>(json_get<Json, std::string>(Json_Adapter<Json>::at(list, "confirm_label"))));
|
||||
json_set(edit_form, "body", make_amis_view_body<Json, Model>(view.edit_body, descriptor, "editable", {}));
|
||||
json_set(edit_form, "actions", make_amis_form_actions<Json>(view.confirm_label));
|
||||
Json create_dialog = json_object<Json>();
|
||||
json_set(create_dialog, "title", create_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
|
||||
json_set(create_dialog, "title", view.create_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
|
||||
json_set(create_dialog, "body", std::move(create_form));
|
||||
Json create_button = json_object<Json>();
|
||||
json_set(create_button, "type", "button");
|
||||
json_set(create_button, "label", create_label);
|
||||
json_set(create_button, "label", view.create_label);
|
||||
json_set(create_button, "level", "primary");
|
||||
json_set(create_button, "actionType", "dialog");
|
||||
json_set(create_button, "dialog", std::move(create_dialog));
|
||||
Json edit_dialog = json_object<Json>();
|
||||
json_set(edit_dialog, "title", edit_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
|
||||
json_set(edit_dialog, "title", view.edit_label + " " + json_get<Json, std::string>(Json_Adapter<Json>::at(descriptor, "label")));
|
||||
json_set(edit_dialog, "body", std::move(edit_form));
|
||||
Json edit_button = json_object<Json>();
|
||||
json_set(edit_button, "type", "button");
|
||||
json_set(edit_button, "label", edit_label);
|
||||
json_set(edit_button, "label", view.edit_label);
|
||||
json_set(edit_button, "level", "link");
|
||||
json_set(edit_button, "actionType", "dialog");
|
||||
json_set(edit_button, "dialog", std::move(edit_dialog));
|
||||
@@ -540,11 +868,11 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
|
||||
json_set(delete_api, "url", base_api + "/${id}");
|
||||
Json delete_button = json_object<Json>();
|
||||
json_set(delete_button, "type", "button");
|
||||
json_set(delete_button, "label", Json_Adapter<Json>::at(list, "delete_label"));
|
||||
json_set(delete_button, "label", view.delete_label);
|
||||
json_set(delete_button, "level", "link");
|
||||
json_set(delete_button, "className", "text-danger");
|
||||
json_set(delete_button, "actionType", "ajax");
|
||||
json_set(delete_button, "confirmText", Json_Adapter<Json>::at(list, "delete_confirm"));
|
||||
json_set(delete_button, "confirmText", view.delete_confirm);
|
||||
json_set(delete_button, "api", std::move(delete_api));
|
||||
json_set(delete_button, "reload", crud_id);
|
||||
Json buttons = json_array<Json>();
|
||||
@@ -552,12 +880,12 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
|
||||
json_append(buttons, std::move(delete_button));
|
||||
Json operation = json_object<Json>();
|
||||
json_set(operation, "type", "operation");
|
||||
json_set(operation, "label", Json_Adapter<Json>::at(list, "actions_label"));
|
||||
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", Json_Adapter<Json>::at(list, "column_reorderable"));
|
||||
json_set(toggler, "draggable", view.column_reorderable);
|
||||
Json toolbar = json_array<Json>();
|
||||
json_append(toolbar, std::move(toggler));
|
||||
json_append(toolbar, "reload");
|
||||
@@ -573,35 +901,29 @@ Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = n
|
||||
json_set(crud, "primaryField", "id");
|
||||
json_set(crud, "headerToolbar", std::move(toolbar));
|
||||
json_set(crud, "columns", std::move(columns));
|
||||
const std::string default_order_by = json_get<Json, std::string>(Json_Adapter<Json>::at(list, "default_order_by"));
|
||||
if(!default_order_by.empty()) {
|
||||
if(!view.default_order_by.empty()) {
|
||||
Json params = json_object<Json>();
|
||||
json_set(params, "orderBy", default_order_by);
|
||||
json_set(params, "orderDir", Json_Adapter<Json>::at(list, "default_order_dir"));
|
||||
json_set(params, "orderBy", view.default_order_by);
|
||||
json_set(params, "orderDir", view.default_order_dir);
|
||||
json_set(crud, "defaultParams", std::move(params));
|
||||
}
|
||||
if(json_get<Json, bool>(Json_Adapter<Json>::at(list, "user_reorderable"))) {
|
||||
if(view.user_reorderable) {
|
||||
json_set(crud, "draggable", true);
|
||||
Json order_api = json_object<Json>();
|
||||
json_set(order_api, "method", "post");
|
||||
json_set(order_api, "url", base_api + "/order");
|
||||
json_set(crud, "saveOrderApi", std::move(order_api));
|
||||
}
|
||||
Json page_body = json_array<Json>();
|
||||
if(overview_status_descriptor && !overview_status_api.empty()) {
|
||||
json_append(page_body, make_amis_status_service<Json>(*overview_status_descriptor, std::move(overview_status_api), overview_status_interval));
|
||||
}
|
||||
json_append(page_body, std::move(crud));
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "type", "page");
|
||||
json_set(result, "title", Json_Adapter<Json>::at(list, "management_label"));
|
||||
json_set(result, "body", std::move(crud));
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, class T>
|
||||
requires Described_Type<Object_Model_Type<T>>
|
||||
Json to_amis_crud_status_schema(std::string base_api, std::string status_api, const Json& status_descriptor, std::uint64_t status_interval = 2000, const Json* object_status_descriptor = nullptr, std::uint64_t object_status_interval = 2000) {
|
||||
Json result = to_amis_crud_schema<Json, T>(std::move(base_api), object_status_descriptor, object_status_interval);
|
||||
Json crud = std::move(Json_Adapter<Json>::at(result, "body"));
|
||||
Json body = json_array<Json>();
|
||||
json_append(body, make_amis_status_service<Json>(status_descriptor, std::move(status_api), status_interval));
|
||||
json_append(body, std::move(crud));
|
||||
json_set(result, "body", std::move(body));
|
||||
json_set(result, "title", view.title);
|
||||
json_set(result, "body", std::move(page_body));
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
#include "adminive/http.hpp"
|
||||
#include "adminive/status.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -17,9 +20,10 @@ struct Collection_Query {
|
||||
std::size_t per_page{20};
|
||||
std::string order_by;
|
||||
std::string order_dir;
|
||||
std::map<std::string, std::string> fields;
|
||||
};
|
||||
template <Described_Type T, Json_Type Json, Basic_Lock Lock = std::mutex>
|
||||
requires std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
requires Table_View_Described_Type<T> && std::default_initializable<T> && std::copy_constructible<T> && std::assignable_from<T&, T>
|
||||
class Collection_Service {
|
||||
public:
|
||||
explicit Collection_Service(std::string path, std::vector<T> initial = {}) : path_(std::move(path)) {
|
||||
@@ -53,7 +57,7 @@ public:
|
||||
return static_cast<bool>(status_reader_);
|
||||
}
|
||||
bool user_reorderable() const {
|
||||
return describe<T>().list_options().user_reorderable_;
|
||||
return describe_table_view<T>().user_reorderable;
|
||||
}
|
||||
std::size_t size() const {
|
||||
std::scoped_lock lock(mutex_);
|
||||
@@ -67,18 +71,24 @@ public:
|
||||
}
|
||||
return items;
|
||||
}
|
||||
Json view_schema() const {
|
||||
return to_view_json<Json, T>(describe_table_view<T>());
|
||||
}
|
||||
Json amis_schema() const {
|
||||
const Json* descriptor = status_reader_ ? &status_descriptor_ : nullptr;
|
||||
if(overview_status_api_.empty()) {
|
||||
return to_amis_crud_schema<Json, T>(path_, descriptor, status_interval_);
|
||||
}
|
||||
return to_amis_crud_status_schema<Json, T>(path_, overview_status_api_, overview_status_descriptor_, overview_status_interval_, descriptor, status_interval_);
|
||||
const Json* item_status = status_reader_ ? &status_descriptor_ : nullptr;
|
||||
const Json* overview_status = overview_status_api_.empty() ? nullptr : &overview_status_descriptor_;
|
||||
return to_amis_table_schema<Json, T>(path_, describe_table_view<T>(), item_status, status_interval_, overview_status, overview_status_api_, overview_status_interval_);
|
||||
}
|
||||
Http_Response<Json> descriptor_response() const noexcept {
|
||||
return safe_response([&] {
|
||||
return make_http_success<Json>(to_descriptor_json<Json, T>());
|
||||
});
|
||||
}
|
||||
Http_Response<Json> view_response() const noexcept {
|
||||
return safe_response([&] {
|
||||
return make_http_success<Json>(view_schema());
|
||||
});
|
||||
}
|
||||
Http_Response<Json> amis_response() const noexcept {
|
||||
return safe_response([&] {
|
||||
return make_http_success<Json>(amis_schema());
|
||||
@@ -113,6 +123,7 @@ public:
|
||||
for(const auto& entry : entries_) {
|
||||
ordered_entries.push_back(&entry);
|
||||
}
|
||||
apply_filters(query, ordered_entries);
|
||||
apply_sort(query, ordered_entries);
|
||||
const std::size_t page = query.page == 0 ? 1 : query.page;
|
||||
const std::size_t per_page = query.per_page == 0 ? 20 : query.per_page;
|
||||
@@ -230,15 +241,67 @@ private:
|
||||
const std::string right_value = dump_json(right);
|
||||
return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
|
||||
}
|
||||
static const Table_Column* find_table_column(const Table_View& view, std::string_view name) {
|
||||
const auto iterator = std::find_if(view.columns.begin(), view.columns.end(), [&](const Table_Column& column) {
|
||||
return column.field == name;
|
||||
});
|
||||
return iterator == view.columns.end() ? nullptr : &*iterator;
|
||||
}
|
||||
static bool is_sortable_field(const std::string& name) {
|
||||
if(name == "id") {
|
||||
return true;
|
||||
}
|
||||
bool sortable{};
|
||||
std::apply([&](const auto&... field) {
|
||||
((field.name() == name ? sortable = field.is_sortable() : false), ...);
|
||||
}, describe<T>().fields());
|
||||
return sortable;
|
||||
const auto view = describe_table_view<T>();
|
||||
const auto* column = find_table_column(view, name);
|
||||
return column && column->sortable;
|
||||
}
|
||||
static std::string query_text(const Json& value) {
|
||||
if(Json_Adapter<Json>::is_string(value)) {
|
||||
return json_get<Json, std::string>(value);
|
||||
}
|
||||
if(Json_Adapter<Json>::is_boolean(value)) {
|
||||
return json_get<Json, bool>(value) ? "true" : "false";
|
||||
}
|
||||
if(Json_Adapter<Json>::is_number(value)) {
|
||||
char buffer[64];
|
||||
const auto [end, error] = std::to_chars(buffer, buffer + sizeof(buffer), Json_Adapter<Json>::number(value), std::chars_format::general);
|
||||
if(error == std::errc{}) {
|
||||
return std::string(buffer, end);
|
||||
}
|
||||
}
|
||||
return dump_json(value);
|
||||
}
|
||||
static std::string lowercase(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) {
|
||||
return static_cast<char>(std::tolower(character));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
static bool matches_query(const Json& value, std::string_view query, const Table_Column& column) {
|
||||
const std::string text = query_text(value);
|
||||
if(column.searchable) {
|
||||
return lowercase(text).find(lowercase(std::string(query))) != std::string::npos;
|
||||
}
|
||||
return text == query;
|
||||
}
|
||||
static void apply_filters(const Collection_Query& query, std::vector<const Entry*>& entries) {
|
||||
if(query.fields.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto view = describe_table_view<T>();
|
||||
for(const auto& [name, value] : query.fields) {
|
||||
const auto* column = find_table_column(view, name);
|
||||
if(!column || (!column->searchable && !column->filterable)) {
|
||||
throw std::invalid_argument("table query field is not searchable or filterable: " + name);
|
||||
}
|
||||
entries.erase(std::remove_if(entries.begin(), entries.end(), [&](const Entry* entry) {
|
||||
const Json encoded = to_frontend_json<Json>(entry->value);
|
||||
if(!Json_Adapter<Json>::contains(encoded, name)) {
|
||||
return true;
|
||||
}
|
||||
return !matches_query(Json_Adapter<Json>::at(encoded, name), value, *column);
|
||||
}), entries.end());
|
||||
}
|
||||
}
|
||||
static std::vector<std::uint64_t> read_order_ids(const Json& value) {
|
||||
std::vector<std::uint64_t> result;
|
||||
@@ -262,10 +325,9 @@ private:
|
||||
return result;
|
||||
}
|
||||
void apply_sort(const Collection_Query& query, std::vector<const Entry*>& entries) const {
|
||||
const auto descriptor = describe<T>();
|
||||
const auto& options = descriptor.list_options();
|
||||
const std::string order_by = query.order_by.empty() ? options.default_order_by : query.order_by;
|
||||
const std::string order_dir = query.order_by.empty() ? options.default_order_dir : query.order_dir.empty() ? "asc" : query.order_dir;
|
||||
const auto view = describe_table_view<T>();
|
||||
const std::string order_by = query.order_by.empty() ? view.default_order_by : query.order_by;
|
||||
const std::string order_dir = query.order_by.empty() ? view.default_order_dir : query.order_dir.empty() ? "asc" : query.order_dir;
|
||||
if(order_by.empty() || !is_sortable_field(order_by)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
namespace adminive {
|
||||
enum class Composition_Node_Kind {
|
||||
slot,
|
||||
heading,
|
||||
vertical,
|
||||
horizontal,
|
||||
flow,
|
||||
grid,
|
||||
group,
|
||||
card,
|
||||
tabs,
|
||||
tab
|
||||
};
|
||||
struct Composition_Node {
|
||||
Composition_Node_Kind kind{Composition_Node_Kind::vertical};
|
||||
std::string slot;
|
||||
std::string title;
|
||||
std::string description;
|
||||
std::string visible_on;
|
||||
std::string accent_color;
|
||||
std::size_t columns{};
|
||||
bool collapsible{};
|
||||
bool collapsed{};
|
||||
std::vector<Composition_Node> children;
|
||||
Composition_Node titled(std::string value) const {
|
||||
auto result = *this;
|
||||
result.title = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Composition_Node described(std::string value) const {
|
||||
auto result = *this;
|
||||
result.description = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Composition_Node visible_when(std::string value) const {
|
||||
auto result = *this;
|
||||
result.visible_on = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Composition_Node accent(std::string value) const {
|
||||
auto result = *this;
|
||||
result.accent_color = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Composition_Node collapsible_when(bool value = true, bool initially_collapsed = false) const {
|
||||
auto result = *this;
|
||||
result.collapsible = value;
|
||||
result.collapsed = value && initially_collapsed;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
namespace compose {
|
||||
inline Composition_Node slot(std::string name) {
|
||||
Composition_Node result;
|
||||
result.kind = Composition_Node_Kind::slot;
|
||||
result.slot = std::move(name);
|
||||
return result;
|
||||
}
|
||||
inline Composition_Node heading(std::string title) {
|
||||
Composition_Node result;
|
||||
result.kind = Composition_Node_Kind::heading;
|
||||
result.title = std::move(title);
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
Composition_Node make_container(Composition_Node_Kind kind, Children&&... children) {
|
||||
Composition_Node result;
|
||||
result.kind = kind;
|
||||
result.children.reserve(sizeof...(Children));
|
||||
(result.children.push_back(std::forward<Children>(children)), ...);
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
Composition_Node vertical(Children&&... children) {
|
||||
return make_container(Composition_Node_Kind::vertical, std::forward<Children>(children)...);
|
||||
}
|
||||
template <class... Children>
|
||||
Composition_Node horizontal(Children&&... children) {
|
||||
return make_container(Composition_Node_Kind::horizontal, std::forward<Children>(children)...);
|
||||
}
|
||||
template <class... Children>
|
||||
Composition_Node flow(Children&&... children) {
|
||||
return make_container(Composition_Node_Kind::flow, std::forward<Children>(children)...);
|
||||
}
|
||||
template <class... Children>
|
||||
Composition_Node grid(std::size_t columns, Children&&... children) {
|
||||
auto result = make_container(Composition_Node_Kind::grid, std::forward<Children>(children)...);
|
||||
result.columns = columns;
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
Composition_Node group(std::string title, Children&&... children) {
|
||||
auto result = make_container(Composition_Node_Kind::group, std::forward<Children>(children)...);
|
||||
result.title = std::move(title);
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
Composition_Node card(std::string title, Children&&... children) {
|
||||
auto result = make_container(Composition_Node_Kind::card, std::forward<Children>(children)...);
|
||||
result.title = std::move(title);
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
Composition_Node tab(std::string title, Children&&... children) {
|
||||
auto result = make_container(Composition_Node_Kind::tab, std::forward<Children>(children)...);
|
||||
result.title = std::move(title);
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
Composition_Node tabs(Children&&... children) {
|
||||
return make_container(Composition_Node_Kind::tabs, std::forward<Children>(children)...);
|
||||
}
|
||||
}
|
||||
struct Composition_View {
|
||||
std::string name;
|
||||
std::string title;
|
||||
Composition_Node body;
|
||||
Composition_View titled(std::string value) const {
|
||||
auto result = *this;
|
||||
result.title = std::move(value);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
inline Composition_View composition_view(std::string name, Composition_Node body) {
|
||||
return Composition_View{std::move(name), {}, std::move(body)};
|
||||
}
|
||||
inline void validate_composition_node(const Composition_Node& node) {
|
||||
if(node.kind == Composition_Node_Kind::slot && node.slot.empty()) {
|
||||
throw std::invalid_argument("composition slot name must not be empty");
|
||||
}
|
||||
if(node.kind == Composition_Node_Kind::grid && node.columns == 0) {
|
||||
throw std::invalid_argument("composition grid requires at least one column");
|
||||
}
|
||||
if(node.kind == Composition_Node_Kind::tabs) {
|
||||
for(const auto& child : node.children) {
|
||||
if(child.kind != Composition_Node_Kind::tab) {
|
||||
throw std::invalid_argument("composition tabs accepts only tab children");
|
||||
}
|
||||
}
|
||||
}
|
||||
for(const auto& child : node.children) {
|
||||
validate_composition_node(child);
|
||||
}
|
||||
}
|
||||
inline void validate_composition_view(const Composition_View& view) {
|
||||
if(view.name.empty()) {
|
||||
throw std::invalid_argument("composition view name must not be empty");
|
||||
}
|
||||
validate_composition_node(view.body);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "adminive/adapter.hpp"
|
||||
#include "adminive/concepts.hpp"
|
||||
#include "adminive/presentation.hpp"
|
||||
#include <cctype>
|
||||
#include <concepts>
|
||||
#include <map>
|
||||
@@ -93,22 +94,13 @@ inline std::string make_label(std::string_view name) {
|
||||
}
|
||||
struct Field_Metadata {
|
||||
std::string name;
|
||||
std::string label;
|
||||
std::string list_label;
|
||||
std::string description;
|
||||
std::string widget;
|
||||
std::string visible_on;
|
||||
std::map<std::string, std::string> enum_labels;
|
||||
Field_Presentation presentation;
|
||||
bool editable{};
|
||||
bool creatable{};
|
||||
bool readable{true};
|
||||
bool sensitive{};
|
||||
bool include_default{true};
|
||||
bool visible{true};
|
||||
bool required{};
|
||||
bool list_visible{true};
|
||||
int order{-1};
|
||||
bool sortable{};
|
||||
};
|
||||
template <class Accessor, bool Managed_Writable = false, bool Synchronized = true>
|
||||
class Field_Descriptor {
|
||||
@@ -121,11 +113,11 @@ public:
|
||||
static constexpr bool synchronized = Synchronized;
|
||||
explicit Field_Descriptor(std::string name) {
|
||||
metadata_.name = std::move(name);
|
||||
metadata_.label = make_label(metadata_.name);
|
||||
metadata_.presentation.label = make_label(metadata_.name);
|
||||
}
|
||||
Field_Descriptor(std::string name, std::string label) {
|
||||
metadata_.name = std::move(name);
|
||||
metadata_.label = std::move(label);
|
||||
metadata_.presentation.label = std::move(label);
|
||||
}
|
||||
explicit Field_Descriptor(Field_Metadata metadata) : metadata_(std::move(metadata)) {}
|
||||
auto editable() const {
|
||||
@@ -163,54 +155,50 @@ public:
|
||||
result.metadata_.include_default = value;
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor visible(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.metadata_.visible = value;
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor required(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.metadata_.required = value;
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor list_visible(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.metadata_.list_visible = value;
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.metadata_.label = std::move(value);
|
||||
result.metadata_.presentation.label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor description(std::string value) const {
|
||||
auto result = *this;
|
||||
result.metadata_.description = std::move(value);
|
||||
result.metadata_.presentation.description = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor widget(std::string value) const {
|
||||
Field_Descriptor control(Field_Control value) const {
|
||||
auto result = *this;
|
||||
result.metadata_.widget = std::move(value);
|
||||
result.metadata_.presentation.control = value;
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor text_input() const {
|
||||
return control(Field_Control::text);
|
||||
}
|
||||
Field_Descriptor multiline_text() const {
|
||||
return control(Field_Control::multiline_text);
|
||||
}
|
||||
Field_Descriptor number_input() const {
|
||||
return control(Field_Control::number);
|
||||
}
|
||||
Field_Descriptor boolean_input() const {
|
||||
return control(Field_Control::boolean);
|
||||
}
|
||||
Field_Descriptor select_input() const {
|
||||
return control(Field_Control::select);
|
||||
}
|
||||
Field_Descriptor date_input() const {
|
||||
return control(Field_Control::date);
|
||||
}
|
||||
Field_Descriptor color_input() const {
|
||||
return control(Field_Control::color);
|
||||
}
|
||||
Field_Descriptor visible_on(std::string value) const {
|
||||
auto result = *this;
|
||||
result.metadata_.visible_on = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor list_label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.metadata_.list_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor order(int value) const {
|
||||
auto result = *this;
|
||||
result.metadata_.order = value;
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor sortable(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.metadata_.sortable = value;
|
||||
result.metadata_.presentation.visible_on = std::move(value);
|
||||
return result;
|
||||
}
|
||||
template <auto Value>
|
||||
@@ -221,12 +209,12 @@ public:
|
||||
throw std::invalid_argument("enum adapter returned an empty value name");
|
||||
}
|
||||
auto result = *this;
|
||||
result.metadata_.enum_labels.insert_or_assign(std::string(name), std::move(label));
|
||||
result.metadata_.presentation.enum_labels.insert_or_assign(std::string(name), std::move(label));
|
||||
return result;
|
||||
}
|
||||
Field_Descriptor enum_label(std::string value_name, std::string label) const {
|
||||
auto result = *this;
|
||||
result.metadata_.enum_labels.insert_or_assign(std::move(value_name), std::move(label));
|
||||
result.metadata_.presentation.enum_labels.insert_or_assign(std::move(value_name), std::move(label));
|
||||
return result;
|
||||
}
|
||||
template <class Object>
|
||||
@@ -242,23 +230,23 @@ public:
|
||||
const std::string& name() const noexcept {
|
||||
return metadata_.name;
|
||||
}
|
||||
const std::string& label() const noexcept {
|
||||
return metadata_.label;
|
||||
const Field_Presentation& presentation() const noexcept {
|
||||
return metadata_.presentation;
|
||||
}
|
||||
const std::string& list_label() const noexcept {
|
||||
return metadata_.list_label.empty() ? metadata_.label : metadata_.list_label;
|
||||
const std::string& label() const noexcept {
|
||||
return metadata_.presentation.label;
|
||||
}
|
||||
const std::string& description() const noexcept {
|
||||
return metadata_.description;
|
||||
return metadata_.presentation.description;
|
||||
}
|
||||
const std::string& widget() const noexcept {
|
||||
return metadata_.widget;
|
||||
Field_Control control() const noexcept {
|
||||
return metadata_.presentation.control;
|
||||
}
|
||||
const std::string& visible_on() const noexcept {
|
||||
return metadata_.visible_on;
|
||||
return metadata_.presentation.visible_on;
|
||||
}
|
||||
const std::map<std::string, std::string>& enum_labels() const noexcept {
|
||||
return metadata_.enum_labels;
|
||||
return metadata_.presentation.enum_labels;
|
||||
}
|
||||
bool is_editable() const noexcept {
|
||||
return metadata_.editable;
|
||||
@@ -275,21 +263,9 @@ public:
|
||||
bool includes_default() const noexcept {
|
||||
return metadata_.include_default;
|
||||
}
|
||||
bool is_visible() const noexcept {
|
||||
return metadata_.visible;
|
||||
}
|
||||
bool is_required() const noexcept {
|
||||
return metadata_.required;
|
||||
}
|
||||
int order() const noexcept {
|
||||
return metadata_.order;
|
||||
}
|
||||
bool is_sortable() const noexcept {
|
||||
return metadata_.sortable;
|
||||
}
|
||||
bool is_list_visible() const noexcept {
|
||||
return metadata_.list_visible;
|
||||
}
|
||||
private:
|
||||
template <bool New_Writable, bool New_Synchronized>
|
||||
auto rebind() const {
|
||||
@@ -338,98 +314,22 @@ struct No_Object_Validator {
|
||||
template <class T>
|
||||
void operator()(const T&) const noexcept {}
|
||||
};
|
||||
struct Object_List_Options {
|
||||
std::string id_label{"ID"};
|
||||
std::string create_label{"Create"};
|
||||
std::string edit_label{"Edit"};
|
||||
std::string delete_label{"Delete"};
|
||||
std::string actions_label{"Actions"};
|
||||
std::string confirm_label{"Confirm Changes"};
|
||||
std::string delete_confirm{"Delete this record?"};
|
||||
std::string view_status_label{"View status"};
|
||||
std::string management_label;
|
||||
std::string default_order_by;
|
||||
std::string default_order_dir{"asc"};
|
||||
bool user_reorderable_{};
|
||||
bool column_reorderable_{true};
|
||||
};
|
||||
template <class T, class Validator, Field_Descriptor_Type... Fields>
|
||||
class Object_Descriptor {
|
||||
public:
|
||||
using object_type = T;
|
||||
using validator_type = Validator;
|
||||
using fields_type = std::tuple<Fields...>;
|
||||
Object_Descriptor(std::string name, std::string label, Validator validator, std::tuple<Fields...> fields, Object_List_Options list_options = {}) : name_(std::move(name)), label_(std::move(label)), validator_(std::move(validator)), fields_(std::move(fields)), list_options_(std::move(list_options)) {}
|
||||
Object_Descriptor(std::string name, std::string label, Validator validator, std::tuple<Fields...> fields) : name_(std::move(name)), label_(std::move(label)), validator_(std::move(validator)), fields_(std::move(fields)) {}
|
||||
template <class New_Validator>
|
||||
auto validator(New_Validator value) const {
|
||||
return Object_Descriptor<T, New_Validator, Fields...>(name_, label_, std::move(value), fields_, list_options_);
|
||||
return Object_Descriptor<T, New_Validator, Fields...>(name_, label_, std::move(value), fields_);
|
||||
}
|
||||
Object_Descriptor label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.label_ = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor id_label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.list_options_.id_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor create_label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.list_options_.create_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor edit_label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.list_options_.edit_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor delete_label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.list_options_.delete_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor actions_label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.list_options_.actions_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor confirm_label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.list_options_.confirm_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor delete_confirm(std::string value) const {
|
||||
auto result = *this;
|
||||
result.list_options_.delete_confirm = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor view_status_label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.list_options_.view_status_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor management_label(std::string value) const {
|
||||
auto result = *this;
|
||||
result.list_options_.management_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor default_sort(std::string field_name, bool descending = false) const {
|
||||
auto result = *this;
|
||||
result.list_options_.default_order_by = std::move(field_name);
|
||||
result.list_options_.default_order_dir = descending ? "desc" : "asc";
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor user_reorderable(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.list_options_.user_reorderable_ = value;
|
||||
return result;
|
||||
}
|
||||
Object_Descriptor column_reorderable(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.list_options_.column_reorderable_ = value;
|
||||
return result;
|
||||
}
|
||||
const std::string& name() const noexcept {
|
||||
return name_;
|
||||
}
|
||||
@@ -442,15 +342,11 @@ public:
|
||||
const std::tuple<Fields...>& fields() const noexcept {
|
||||
return fields_;
|
||||
}
|
||||
const Object_List_Options& list_options() const noexcept {
|
||||
return list_options_;
|
||||
}
|
||||
private:
|
||||
std::string name_;
|
||||
std::string label_;
|
||||
Validator validator_;
|
||||
std::tuple<Fields...> fields_;
|
||||
Object_List_Options list_options_;
|
||||
};
|
||||
template <class Descriptor>
|
||||
void validate_descriptor(const Descriptor& descriptor) {
|
||||
@@ -468,29 +364,6 @@ void validate_descriptor(const Descriptor& descriptor) {
|
||||
}
|
||||
}(), ...);
|
||||
}, descriptor.fields());
|
||||
const auto& options = descriptor.list_options();
|
||||
if(options.default_order_dir != "asc" && options.default_order_dir != "desc") {
|
||||
throw std::invalid_argument("default sort direction must be 'asc' or 'desc'");
|
||||
}
|
||||
if(options.default_order_by.empty()) {
|
||||
return;
|
||||
}
|
||||
bool found{};
|
||||
bool sortable{};
|
||||
std::apply([&](const auto&... field) {
|
||||
([&] {
|
||||
if(field.name() == options.default_order_by) {
|
||||
found = true;
|
||||
sortable = field.is_sortable();
|
||||
}
|
||||
}(), ...);
|
||||
}, descriptor.fields());
|
||||
if(!found) {
|
||||
throw std::invalid_argument("default sort field does not exist: " + options.default_order_by);
|
||||
}
|
||||
if(!sortable) {
|
||||
throw std::invalid_argument("default sort field is not sortable: " + options.default_order_by);
|
||||
}
|
||||
}
|
||||
template <class T, Field_Descriptor_Type... Fields>
|
||||
auto object(std::string name, Fields... fields) {
|
||||
|
||||
@@ -114,10 +114,14 @@ public:
|
||||
const std::string& path() const noexcept {
|
||||
return path_;
|
||||
}
|
||||
Json view_schema() const {
|
||||
return to_view_json<Json, Model>(describe_edit_view<Model>());
|
||||
}
|
||||
Json amis_schema() const {
|
||||
Json result;
|
||||
const auto view = describe_edit_view<Model>();
|
||||
read_access_([&](const Object& value) {
|
||||
result = to_amis_form_schema<Json>(value, path_ + "/data", describe<Model>().list_options().confirm_label);
|
||||
result = to_amis_form_schema<Json>(value, view, path_ + "/data");
|
||||
});
|
||||
return result;
|
||||
}
|
||||
@@ -126,6 +130,11 @@ public:
|
||||
return make_http_success<Json>(to_descriptor_json<Json, Object>());
|
||||
});
|
||||
}
|
||||
Http_Response<Json> view_response() const noexcept {
|
||||
return safe_response([&] {
|
||||
return make_http_success<Json>(view_schema());
|
||||
});
|
||||
}
|
||||
Http_Response<Json> data_response() const noexcept {
|
||||
return safe_response([&] {
|
||||
Http_Response<Json> response;
|
||||
|
||||
@@ -395,7 +395,6 @@ requires Described_Type<Model>
|
||||
Json model_descriptor_json(const Model* defaults = nullptr) {
|
||||
const auto descriptor = describe<Model>();
|
||||
Json fields = json_array<Json>();
|
||||
int field_index{};
|
||||
std::apply([&](const auto&... item) {
|
||||
([&] {
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
@@ -403,70 +402,46 @@ Json model_descriptor_json(const Model* defaults = nullptr) {
|
||||
using Value = Adapted_Value_Type<Member, Json>;
|
||||
using Descriptor_Value = Optional_Unwrapped_Type<Value>;
|
||||
Json field_json = json_object<Json>();
|
||||
const int order = item.order() < 0 ? field_index : item.order();
|
||||
++field_index;
|
||||
json_set(field_json, "name", item.name());
|
||||
json_set(field_json, "label", item.label());
|
||||
json_set(field_json, "list_label", item.list_label());
|
||||
json_set(field_json, "value_type", value_type_name<Json, Member>());
|
||||
json_set(field_json, "nullable", Optional_Type<Value>);
|
||||
json_set(field_json, "readable", item.is_readable());
|
||||
json_set(field_json, "sensitive", item.is_sensitive());
|
||||
json_set(field_json, "editable", item.is_editable());
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
json_set(field_json, "writable", Field::managed_writable);
|
||||
json_set(field_json, "synchronized", Field::managed_writable && Field::synchronized);
|
||||
json_set(field_json, "creatable", item.is_creatable());
|
||||
json_set(field_json, "visible", item.is_visible());
|
||||
json_set(field_json, "required", item.is_required());
|
||||
json_set(field_json, "list_visible", item.is_list_visible());
|
||||
json_set(field_json, "order", order);
|
||||
json_set(field_json, "sortable", item.is_sortable());
|
||||
if(defaults && item.is_readable() && !item.is_sensitive() && item.includes_default()) {
|
||||
json_set(field_json, "default", encode_json_value<Json>(item.get(*defaults)));
|
||||
}
|
||||
Json presentation = json_object<Json>();
|
||||
json_set(presentation, "label", item.label());
|
||||
json_set(presentation, "control", std::string(field_control_name(item.control())));
|
||||
if(!item.description().empty()) {
|
||||
json_set(field_json, "description", item.description());
|
||||
}
|
||||
if(!item.widget().empty()) {
|
||||
json_set(field_json, "widget", item.widget());
|
||||
json_set(presentation, "description", item.description());
|
||||
}
|
||||
if(!item.visible_on().empty()) {
|
||||
json_set(field_json, "visible_on", item.visible_on());
|
||||
json_set(presentation, "visible_on", item.visible_on());
|
||||
}
|
||||
auto options = enum_options<Json, Member>(item.enum_labels());
|
||||
if(Json_Adapter<Json>::size(options) != 0) {
|
||||
json_set(presentation, "options", std::move(options));
|
||||
}
|
||||
json_set(field_json, "presentation", std::move(presentation));
|
||||
if constexpr(Described_Type<Descriptor_Value> || Snapshot_Adapted_Object<Descriptor_Value>) {
|
||||
json_set(field_json, "children", Json_Adapter<Json>::at(to_descriptor_json<Json, Descriptor_Value>(), "fields"));
|
||||
}
|
||||
append_constraints<Json, Member>(field_json);
|
||||
auto options = enum_options<Json, Member>(item.enum_labels());
|
||||
if(Json_Adapter<Json>::size(options) != 0) {
|
||||
json_set(field_json, "options", std::move(options));
|
||||
}
|
||||
json_append(fields, std::move(field_json));
|
||||
}(), ...);
|
||||
}, descriptor.fields());
|
||||
const auto& options = descriptor.list_options();
|
||||
Json list = json_object<Json>();
|
||||
json_set(list, "id_label", options.id_label);
|
||||
json_set(list, "create_label", options.create_label);
|
||||
json_set(list, "edit_label", options.edit_label);
|
||||
json_set(list, "delete_label", options.delete_label);
|
||||
json_set(list, "actions_label", options.actions_label);
|
||||
json_set(list, "confirm_label", options.confirm_label);
|
||||
json_set(list, "delete_confirm", options.delete_confirm);
|
||||
json_set(list, "view_status_label", options.view_status_label);
|
||||
json_set(list, "management_label", options.management_label.empty() ? descriptor.label() + " Management" : options.management_label);
|
||||
json_set(list, "default_order_by", options.default_order_by);
|
||||
json_set(list, "default_order_dir", options.default_order_dir);
|
||||
json_set(list, "user_reorderable", options.user_reorderable_);
|
||||
json_set(list, "column_reorderable", options.column_reorderable_);
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "protocol", "adminive.resource");
|
||||
json_set(result, "protocol_version", 4);
|
||||
json_set(result, "protocol_version", 5);
|
||||
json_set(result, "name", descriptor.name());
|
||||
json_set(result, "label", descriptor.label());
|
||||
json_set(result, "value_type", "object");
|
||||
json_set(result, "list", std::move(list));
|
||||
json_set(result, "fields", std::move(fields));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
namespace adminive {
|
||||
enum class Field_Control {
|
||||
automatic,
|
||||
text,
|
||||
multiline_text,
|
||||
number,
|
||||
boolean,
|
||||
select,
|
||||
date,
|
||||
color
|
||||
};
|
||||
inline std::string_view field_control_name(Field_Control value) noexcept {
|
||||
switch(value) {
|
||||
case Field_Control::automatic:
|
||||
return "automatic";
|
||||
case Field_Control::text:
|
||||
return "text";
|
||||
case Field_Control::multiline_text:
|
||||
return "multiline_text";
|
||||
case Field_Control::number:
|
||||
return "number";
|
||||
case Field_Control::boolean:
|
||||
return "boolean";
|
||||
case Field_Control::select:
|
||||
return "select";
|
||||
case Field_Control::date:
|
||||
return "date";
|
||||
case Field_Control::color:
|
||||
return "color";
|
||||
}
|
||||
return "automatic";
|
||||
}
|
||||
struct Field_Presentation {
|
||||
std::string label;
|
||||
std::string description;
|
||||
Field_Control control{Field_Control::automatic};
|
||||
std::string visible_on;
|
||||
std::map<std::string, std::string> enum_labels;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
#pragma once
|
||||
#include "adminive/descriptor.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
namespace adminive {
|
||||
enum class View_Node_Kind {
|
||||
all_fields,
|
||||
field,
|
||||
vertical,
|
||||
horizontal,
|
||||
flow,
|
||||
grid,
|
||||
group,
|
||||
card,
|
||||
tabs,
|
||||
tab
|
||||
};
|
||||
struct View_Node {
|
||||
View_Node_Kind kind{View_Node_Kind::vertical};
|
||||
std::string field;
|
||||
std::string title;
|
||||
std::string description;
|
||||
std::string visible_on;
|
||||
std::size_t columns{};
|
||||
bool collapsible{};
|
||||
bool collapsed{};
|
||||
std::vector<View_Node> children;
|
||||
View_Node titled(std::string value) const {
|
||||
auto result = *this;
|
||||
result.title = std::move(value);
|
||||
return result;
|
||||
}
|
||||
View_Node described(std::string value) const {
|
||||
auto result = *this;
|
||||
result.description = std::move(value);
|
||||
return result;
|
||||
}
|
||||
View_Node visible_when(std::string value) const {
|
||||
auto result = *this;
|
||||
result.visible_on = std::move(value);
|
||||
return result;
|
||||
}
|
||||
View_Node collapsible_when(bool value = true, bool initially_collapsed = false) const {
|
||||
auto result = *this;
|
||||
result.collapsible = value;
|
||||
result.collapsed = value && initially_collapsed;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
inline View_Node all_fields() {
|
||||
View_Node result;
|
||||
result.kind = View_Node_Kind::all_fields;
|
||||
return result;
|
||||
}
|
||||
inline View_Node view_field(std::string name) {
|
||||
View_Node result;
|
||||
result.kind = View_Node_Kind::field;
|
||||
result.field = std::move(name);
|
||||
return result;
|
||||
}
|
||||
template <auto Member>
|
||||
std::string described_member_name() {
|
||||
using Owner = typename Member_Pointer_Traits<Member>::owner_type;
|
||||
const auto descriptor = describe<Owner>();
|
||||
std::string result;
|
||||
std::apply([&](const auto&... item) {
|
||||
([&] {
|
||||
using Field = std::remove_cvref_t<decltype(item)>;
|
||||
using Accessor = typename Field::accessor_type;
|
||||
if constexpr(requires { Accessor::member; }) {
|
||||
if constexpr(std::same_as<std::remove_cv_t<decltype(Accessor::member)>, std::remove_cv_t<decltype(Member)>>) {
|
||||
if constexpr(Accessor::member == Member) {
|
||||
result = item.name();
|
||||
}
|
||||
}
|
||||
}
|
||||
}(), ...);
|
||||
}, descriptor.fields());
|
||||
if(result.empty()) {
|
||||
throw std::invalid_argument("view member is not registered in the object descriptor");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <auto Member>
|
||||
View_Node use() {
|
||||
return view_field(described_member_name<Member>());
|
||||
}
|
||||
template <Reflected_Type T, std::size_t Index>
|
||||
View_Node use_index() {
|
||||
static_assert(Index < static_cast<std::size_t>(Reflection_Adapter<T>::field_count));
|
||||
const auto descriptor = describe<T>();
|
||||
const auto& field = std::get<Index>(descriptor.fields());
|
||||
return view_field(field.name());
|
||||
}
|
||||
template <class... Children>
|
||||
View_Node make_view_container(View_Node_Kind kind, Children&&... children) {
|
||||
View_Node result;
|
||||
result.kind = kind;
|
||||
result.children.reserve(sizeof...(Children));
|
||||
(result.children.push_back(std::forward<Children>(children)), ...);
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
View_Node vertical(Children&&... children) {
|
||||
return make_view_container(View_Node_Kind::vertical, std::forward<Children>(children)...);
|
||||
}
|
||||
template <class... Children>
|
||||
View_Node horizontal(Children&&... children) {
|
||||
return make_view_container(View_Node_Kind::horizontal, std::forward<Children>(children)...);
|
||||
}
|
||||
template <class... Children>
|
||||
View_Node flow(Children&&... children) {
|
||||
return make_view_container(View_Node_Kind::flow, std::forward<Children>(children)...);
|
||||
}
|
||||
template <class... Children>
|
||||
View_Node grid(std::size_t columns, Children&&... children) {
|
||||
auto result = make_view_container(View_Node_Kind::grid, std::forward<Children>(children)...);
|
||||
result.columns = columns;
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
View_Node group(std::string title, Children&&... children) {
|
||||
auto result = make_view_container(View_Node_Kind::group, std::forward<Children>(children)...);
|
||||
result.title = std::move(title);
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
View_Node card(std::string title, Children&&... children) {
|
||||
auto result = make_view_container(View_Node_Kind::card, std::forward<Children>(children)...);
|
||||
result.title = std::move(title);
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
View_Node tab(std::string title, Children&&... children) {
|
||||
auto result = make_view_container(View_Node_Kind::tab, std::forward<Children>(children)...);
|
||||
result.title = std::move(title);
|
||||
return result;
|
||||
}
|
||||
template <class... Children>
|
||||
View_Node tabs(Children&&... children) {
|
||||
return make_view_container(View_Node_Kind::tabs, std::forward<Children>(children)...);
|
||||
}
|
||||
enum class Form_Mode {
|
||||
display,
|
||||
edit,
|
||||
create
|
||||
};
|
||||
struct Form_View {
|
||||
std::string name;
|
||||
std::string title;
|
||||
std::string submit_label{"Apply"};
|
||||
Form_Mode mode{Form_Mode::edit};
|
||||
View_Node body{all_fields()};
|
||||
bool affix_footer{true};
|
||||
Form_View titled(std::string value) const {
|
||||
auto result = *this;
|
||||
result.title = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Form_View submit(std::string value) const {
|
||||
auto result = *this;
|
||||
result.submit_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Form_View affix(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.affix_footer = value;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
template <Described_Type T>
|
||||
Form_View edit_form(View_Node body = all_fields()) {
|
||||
const auto descriptor = describe<T>();
|
||||
return Form_View{descriptor.name() + "_edit", descriptor.label(), "Apply", Form_Mode::edit, std::move(body), true};
|
||||
}
|
||||
template <Described_Type T>
|
||||
Form_View create_form(View_Node body = all_fields()) {
|
||||
const auto descriptor = describe<T>();
|
||||
return Form_View{descriptor.name() + "_create", descriptor.label(), "Create", Form_Mode::create, std::move(body), true};
|
||||
}
|
||||
template <Described_Type T>
|
||||
Form_View detail_view(View_Node body = all_fields()) {
|
||||
const auto descriptor = describe<T>();
|
||||
return Form_View{descriptor.name() + "_detail", descriptor.label(), {}, Form_Mode::display, std::move(body), false};
|
||||
}
|
||||
enum class Table_Fixed {
|
||||
none,
|
||||
left,
|
||||
right
|
||||
};
|
||||
struct Table_Column {
|
||||
std::string field;
|
||||
std::string label;
|
||||
bool sortable{};
|
||||
bool searchable{};
|
||||
bool filterable{};
|
||||
Table_Fixed fixed{Table_Fixed::none};
|
||||
Table_Column titled(std::string value) const {
|
||||
auto result = *this;
|
||||
result.label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_Column sort(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.sortable = value;
|
||||
return result;
|
||||
}
|
||||
Table_Column search(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.searchable = value;
|
||||
return result;
|
||||
}
|
||||
Table_Column filter(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.filterable = value;
|
||||
return result;
|
||||
}
|
||||
Table_Column fix(Table_Fixed value) const {
|
||||
auto result = *this;
|
||||
result.fixed = value;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
template <auto Member>
|
||||
Table_Column column(std::string label = {}) {
|
||||
return Table_Column{described_member_name<Member>(), std::move(label)};
|
||||
}
|
||||
template <Reflected_Type T, std::size_t Index>
|
||||
Table_Column column_index(std::string label = {}) {
|
||||
const auto descriptor = describe<T>();
|
||||
const auto& field = std::get<Index>(descriptor.fields());
|
||||
return Table_Column{field.name(), std::move(label)};
|
||||
}
|
||||
struct Table_View {
|
||||
std::string name;
|
||||
std::string title;
|
||||
std::string id_label{"ID"};
|
||||
std::string create_label{"Create"};
|
||||
std::string edit_label{"Edit"};
|
||||
std::string delete_label{"Delete"};
|
||||
std::string actions_label{"Actions"};
|
||||
std::string confirm_label{"Confirm Changes"};
|
||||
std::string delete_confirm{"Delete this record?"};
|
||||
std::string view_status_label{"View status"};
|
||||
std::string default_order_by;
|
||||
std::string default_order_dir{"asc"};
|
||||
bool user_reorderable{};
|
||||
bool column_reorderable{true};
|
||||
std::vector<Table_Column> columns;
|
||||
View_Node create_body{all_fields()};
|
||||
View_Node edit_body{all_fields()};
|
||||
Table_View titled(std::string value) const {
|
||||
auto result = *this;
|
||||
result.title = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View id_title(std::string value) const {
|
||||
auto result = *this;
|
||||
result.id_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View create_title(std::string value) const {
|
||||
auto result = *this;
|
||||
result.create_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View edit_title(std::string value) const {
|
||||
auto result = *this;
|
||||
result.edit_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View delete_title(std::string value) const {
|
||||
auto result = *this;
|
||||
result.delete_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View actions_title(std::string value) const {
|
||||
auto result = *this;
|
||||
result.actions_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View confirm_title(std::string value) const {
|
||||
auto result = *this;
|
||||
result.confirm_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View delete_confirmation(std::string value) const {
|
||||
auto result = *this;
|
||||
result.delete_confirm = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View status_title(std::string value) const {
|
||||
auto result = *this;
|
||||
result.view_status_label = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View default_sort(std::string field_name, bool descending = false) const {
|
||||
auto result = *this;
|
||||
result.default_order_by = std::move(field_name);
|
||||
result.default_order_dir = descending ? "desc" : "asc";
|
||||
return result;
|
||||
}
|
||||
Table_View reorderable(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.user_reorderable = value;
|
||||
return result;
|
||||
}
|
||||
Table_View reorderable_columns(bool value = true) const {
|
||||
auto result = *this;
|
||||
result.column_reorderable = value;
|
||||
return result;
|
||||
}
|
||||
Table_View create_layout(View_Node value) const {
|
||||
auto result = *this;
|
||||
result.create_body = std::move(value);
|
||||
return result;
|
||||
}
|
||||
Table_View edit_layout(View_Node value) const {
|
||||
auto result = *this;
|
||||
result.edit_body = std::move(value);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
template <Described_Type T, class... Columns>
|
||||
Table_View table_view(Columns&&... columns) {
|
||||
const auto descriptor = describe<T>();
|
||||
Table_View result;
|
||||
result.name = descriptor.name() + "_table";
|
||||
result.title = descriptor.label() + " Management";
|
||||
result.columns.reserve(sizeof...(Columns));
|
||||
(result.columns.push_back(std::forward<Columns>(columns)), ...);
|
||||
return result;
|
||||
}
|
||||
template <class T>
|
||||
struct Type_View_Descriptor;
|
||||
template <Described_Type T>
|
||||
Form_View describe_edit_view() {
|
||||
if constexpr(requires { { Type_View_Descriptor<T>::edit() } -> std::same_as<Form_View>; }) {
|
||||
return Type_View_Descriptor<T>::edit();
|
||||
} else {
|
||||
return edit_form<T>();
|
||||
}
|
||||
}
|
||||
template <Described_Type T>
|
||||
Form_View describe_create_view() {
|
||||
if constexpr(requires { { Type_View_Descriptor<T>::create() } -> std::same_as<Form_View>; }) {
|
||||
return Type_View_Descriptor<T>::create();
|
||||
} else {
|
||||
return create_form<T>();
|
||||
}
|
||||
}
|
||||
template <Described_Type T>
|
||||
Form_View describe_detail_view() {
|
||||
if constexpr(requires { { Type_View_Descriptor<T>::detail() } -> std::same_as<Form_View>; }) {
|
||||
return Type_View_Descriptor<T>::detail();
|
||||
} else {
|
||||
return detail_view<T>();
|
||||
}
|
||||
}
|
||||
template <class T>
|
||||
concept Table_View_Described_Type = Described_Type<T> && requires {
|
||||
{ Type_View_Descriptor<T>::table() } -> std::same_as<Table_View>;
|
||||
};
|
||||
template <Described_Type T>
|
||||
Table_View describe_table_view() requires Table_View_Described_Type<T> {
|
||||
return Type_View_Descriptor<T>::table();
|
||||
}
|
||||
template <class Descriptor>
|
||||
bool descriptor_has_field(const Descriptor& descriptor, std::string_view name) {
|
||||
bool found{};
|
||||
std::apply([&](const auto&... field) {
|
||||
((found = found || field.name() == name), ...);
|
||||
}, descriptor.fields());
|
||||
return found;
|
||||
}
|
||||
template <Described_Type T>
|
||||
void validate_view_node(const View_Node& node) {
|
||||
const auto descriptor = describe<T>();
|
||||
if(node.kind == View_Node_Kind::field && !descriptor_has_field(descriptor, node.field)) {
|
||||
throw std::invalid_argument("view field does not exist: " + node.field);
|
||||
}
|
||||
if(node.kind == View_Node_Kind::grid && node.columns == 0) {
|
||||
throw std::invalid_argument("grid view requires at least one column");
|
||||
}
|
||||
if(node.kind == View_Node_Kind::tabs) {
|
||||
for(const auto& child : node.children) {
|
||||
if(child.kind != View_Node_Kind::tab) {
|
||||
throw std::invalid_argument("tabs view accepts only tab children");
|
||||
}
|
||||
}
|
||||
}
|
||||
for(const auto& child : node.children) {
|
||||
validate_view_node<T>(child);
|
||||
}
|
||||
}
|
||||
template <Described_Type T>
|
||||
void validate_form_view(const Form_View& view) {
|
||||
if(view.name.empty()) {
|
||||
throw std::invalid_argument("form view name must not be empty");
|
||||
}
|
||||
validate_view_node<T>(view.body);
|
||||
}
|
||||
template <Described_Type T>
|
||||
void validate_table_view(const Table_View& view) {
|
||||
if(view.name.empty()) {
|
||||
throw std::invalid_argument("table view name must not be empty");
|
||||
}
|
||||
const auto descriptor = describe<T>();
|
||||
std::vector<std::string> names;
|
||||
names.reserve(view.columns.size());
|
||||
for(const auto& column : view.columns) {
|
||||
if(!descriptor_has_field(descriptor, column.field)) {
|
||||
throw std::invalid_argument("table column field does not exist: " + column.field);
|
||||
}
|
||||
if(std::find(names.begin(), names.end(), column.field) != names.end()) {
|
||||
throw std::invalid_argument("duplicate table column field: " + column.field);
|
||||
}
|
||||
names.push_back(column.field);
|
||||
}
|
||||
if(!view.default_order_by.empty()) {
|
||||
const auto iterator = std::find_if(view.columns.begin(), view.columns.end(), [&](const Table_Column& column) {
|
||||
return column.field == view.default_order_by;
|
||||
});
|
||||
if(iterator == view.columns.end()) {
|
||||
throw std::invalid_argument("default sort field is not a table column: " + view.default_order_by);
|
||||
}
|
||||
if(!iterator->sortable) {
|
||||
throw std::invalid_argument("default sort field is not sortable: " + view.default_order_by);
|
||||
}
|
||||
}
|
||||
if(view.default_order_dir != "asc" && view.default_order_dir != "desc") {
|
||||
throw std::invalid_argument("default sort direction must be 'asc' or 'desc'");
|
||||
}
|
||||
validate_view_node<T>(view.create_body);
|
||||
validate_view_node<T>(view.edit_body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
#pragma once
|
||||
#include "adminive/composition.hpp"
|
||||
#include "adminive/json.hpp"
|
||||
#include "adminive/view.hpp"
|
||||
namespace adminive {
|
||||
inline std::string_view composition_node_kind_name(Composition_Node_Kind value) noexcept {
|
||||
switch(value) {
|
||||
case Composition_Node_Kind::slot:
|
||||
return "slot";
|
||||
case Composition_Node_Kind::heading:
|
||||
return "heading";
|
||||
case Composition_Node_Kind::vertical:
|
||||
return "vertical";
|
||||
case Composition_Node_Kind::horizontal:
|
||||
return "horizontal";
|
||||
case Composition_Node_Kind::flow:
|
||||
return "flow";
|
||||
case Composition_Node_Kind::grid:
|
||||
return "grid";
|
||||
case Composition_Node_Kind::group:
|
||||
return "group";
|
||||
case Composition_Node_Kind::card:
|
||||
return "card";
|
||||
case Composition_Node_Kind::tabs:
|
||||
return "tabs";
|
||||
case Composition_Node_Kind::tab:
|
||||
return "tab";
|
||||
}
|
||||
return "vertical";
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json composition_node_to_json(const Composition_Node& node) {
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "kind", std::string(composition_node_kind_name(node.kind)));
|
||||
if(!node.slot.empty()) {
|
||||
json_set(result, "slot", node.slot);
|
||||
}
|
||||
if(!node.title.empty()) {
|
||||
json_set(result, "title", node.title);
|
||||
}
|
||||
if(!node.description.empty()) {
|
||||
json_set(result, "description", node.description);
|
||||
}
|
||||
if(!node.visible_on.empty()) {
|
||||
json_set(result, "visible_on", node.visible_on);
|
||||
}
|
||||
if(!node.accent_color.empty()) {
|
||||
json_set(result, "accent_color", node.accent_color);
|
||||
}
|
||||
if(node.columns != 0) {
|
||||
json_set(result, "columns", node.columns);
|
||||
}
|
||||
if(node.collapsible) {
|
||||
json_set(result, "collapsible", true);
|
||||
json_set(result, "collapsed", node.collapsed);
|
||||
}
|
||||
if(!node.children.empty()) {
|
||||
Json children = json_array<Json>();
|
||||
for(const auto& child : node.children) {
|
||||
json_append(children, composition_node_to_json<Json>(child));
|
||||
}
|
||||
json_set(result, "children", std::move(children));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json to_view_json(const Composition_View& view) {
|
||||
validate_composition_view(view);
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "protocol", "adminive.view");
|
||||
json_set(result, "protocol_version", 1);
|
||||
json_set(result, "kind", "composition");
|
||||
json_set(result, "name", view.name);
|
||||
if(!view.title.empty()) {
|
||||
json_set(result, "title", view.title);
|
||||
}
|
||||
json_set(result, "body", composition_node_to_json<Json>(view.body));
|
||||
return result;
|
||||
}
|
||||
inline std::string_view view_node_kind_name(View_Node_Kind value) noexcept {
|
||||
switch(value) {
|
||||
case View_Node_Kind::all_fields:
|
||||
return "all_fields";
|
||||
case View_Node_Kind::field:
|
||||
return "field";
|
||||
case View_Node_Kind::vertical:
|
||||
return "vertical";
|
||||
case View_Node_Kind::horizontal:
|
||||
return "horizontal";
|
||||
case View_Node_Kind::flow:
|
||||
return "flow";
|
||||
case View_Node_Kind::grid:
|
||||
return "grid";
|
||||
case View_Node_Kind::group:
|
||||
return "group";
|
||||
case View_Node_Kind::card:
|
||||
return "card";
|
||||
case View_Node_Kind::tabs:
|
||||
return "tabs";
|
||||
case View_Node_Kind::tab:
|
||||
return "tab";
|
||||
}
|
||||
return "vertical";
|
||||
}
|
||||
inline std::string_view form_mode_name(Form_Mode value) noexcept {
|
||||
switch(value) {
|
||||
case Form_Mode::display:
|
||||
return "display";
|
||||
case Form_Mode::edit:
|
||||
return "edit";
|
||||
case Form_Mode::create:
|
||||
return "create";
|
||||
}
|
||||
return "display";
|
||||
}
|
||||
inline std::string_view table_fixed_name(Table_Fixed value) noexcept {
|
||||
switch(value) {
|
||||
case Table_Fixed::none:
|
||||
return "none";
|
||||
case Table_Fixed::left:
|
||||
return "left";
|
||||
case Table_Fixed::right:
|
||||
return "right";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
template <Json_Type Json>
|
||||
Json view_node_to_json(const View_Node& node) {
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "kind", std::string(view_node_kind_name(node.kind)));
|
||||
if(!node.field.empty()) {
|
||||
json_set(result, "field", node.field);
|
||||
}
|
||||
if(!node.title.empty()) {
|
||||
json_set(result, "title", node.title);
|
||||
}
|
||||
if(!node.description.empty()) {
|
||||
json_set(result, "description", node.description);
|
||||
}
|
||||
if(!node.visible_on.empty()) {
|
||||
json_set(result, "visible_on", node.visible_on);
|
||||
}
|
||||
if(node.columns != 0) {
|
||||
json_set(result, "columns", node.columns);
|
||||
}
|
||||
if(node.collapsible) {
|
||||
json_set(result, "collapsible", true);
|
||||
json_set(result, "collapsed", node.collapsed);
|
||||
}
|
||||
if(!node.children.empty()) {
|
||||
Json children = json_array<Json>();
|
||||
for(const auto& child : node.children) {
|
||||
json_append(children, view_node_to_json<Json>(child));
|
||||
}
|
||||
json_set(result, "children", std::move(children));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
template <Json_Type Json, Described_Type T>
|
||||
Json to_view_json(const Form_View& view) {
|
||||
validate_form_view<T>(view);
|
||||
Json result = json_object<Json>();
|
||||
json_set(result, "protocol", "adminive.view");
|
||||
json_set(result, "protocol_version", 1);
|
||||
json_set(result, "kind", "form");
|
||||
json_set(result, "name", view.name);
|
||||
json_set(result, "title", view.title);
|
||||
json_set(result, "mode", std::string(form_mode_name(view.mode)));
|
||||
json_set(result, "submit_label", view.submit_label);
|
||||
json_set(result, "affix_footer", view.affix_footer);
|
||||
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");
|
||||
json_set(result, "name", view.name);
|
||||
json_set(result, "title", view.title);
|
||||
json_set(result, "id_label", view.id_label);
|
||||
json_set(result, "create_label", view.create_label);
|
||||
json_set(result, "edit_label", view.edit_label);
|
||||
json_set(result, "delete_label", view.delete_label);
|
||||
json_set(result, "actions_label", view.actions_label);
|
||||
json_set(result, "confirm_label", view.confirm_label);
|
||||
json_set(result, "delete_confirm", view.delete_confirm);
|
||||
json_set(result, "view_status_label", view.view_status_label);
|
||||
json_set(result, "default_order_by", view.default_order_by);
|
||||
json_set(result, "default_order_dir", view.default_order_dir);
|
||||
json_set(result, "user_reorderable", view.user_reorderable);
|
||||
json_set(result, "column_reorderable", view.column_reorderable);
|
||||
Json columns = json_array<Json>();
|
||||
for(const auto& column : view.columns) {
|
||||
Json item = json_object<Json>();
|
||||
json_set(item, "field", column.field);
|
||||
if(!column.label.empty()) {
|
||||
json_set(item, "label", column.label);
|
||||
}
|
||||
json_set(item, "sortable", column.sortable);
|
||||
json_set(item, "searchable", column.searchable);
|
||||
json_set(item, "filterable", column.filterable);
|
||||
json_set(item, "fixed", std::string(table_fixed_name(column.fixed)));
|
||||
json_append(columns, std::move(item));
|
||||
}
|
||||
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));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user