#pragma once #include "adminive/status.hpp" #include #include #include #include #include #include #include namespace adminive { inline std::string make_field_name(std::string_view prefix, std::string_view name) { if(prefix.empty()) { return std::string(name); } return std::string(prefix) + "." + std::string(name); } inline std::string escape_amis_string_literal(std::string_view value) { std::string result; result.reserve(value.size()); for(const char character : value) { if(character == '\\' || character == '\'') { result.push_back('\\'); } if(character == '\n') { result += "\\n"; } else if(character == '\r') { result += "\\r"; } else { result.push_back(character); } } return result; } inline std::string resolve_visible_on(std::string expression, std::string_view prefix) { constexpr std::string_view marker{"$self"}; std::size_t position{}; while((position = expression.find(marker, position)) != std::string::npos) { const bool dotted = position + marker.size() < expression.size() && expression[position + marker.size()] == '.'; std::string replacement(prefix); if(!replacement.empty() && dotted) { replacement.push_back('.'); } const std::size_t count = marker.size() + (dotted ? 1 : 0); expression.replace(position, count, replacement); position += replacement.size(); } return expression; } template void apply_visible_on(Json& control, const Json& field, std::string_view prefix) { if(Json_Adapter::contains(field, "visible_on")) { json_set(control, "visibleOn", resolve_visible_on(json_get(Json_Adapter::at(field, "visible_on")), prefix)); } } template Json make_amis_form_actions(std::string submit_label = "Apply") { Json reset = json_object(); json_set(reset, "type", "reset"); json_set(reset, "label", "Reset"); Json submit = json_object(); json_set(submit, "type", "submit"); json_set(submit, "label", std::move(submit_label)); json_set(submit, "level", "primary"); Json result = json_array(); json_append(result, std::move(reset)); json_append(result, std::move(submit)); return result; } template Json make_amis_form_body(const Json& descriptor, std::string_view permission, std::string_view prefix = {}); template Json make_typed_amis_form_body(const Json& descriptor, std::string_view permission, std::string_view prefix = {}); template requires Polymorphic_Type Json make_typed_amis_polymorphic_control(const Json& field, std::string_view permission, std::string_view prefix); template Json make_amis_polymorphic_control(const Json& field, std::string_view permission, std::string_view prefix) { const std::string name = json_get(Json_Adapter::at(field, "name")); const std::string full_name = make_field_name(prefix, name); const auto& metadata = Json_Adapter::at(field, "polymorphic"); const std::string discriminator = json_get(Json_Adapter::at(metadata, "discriminator")); Json selector = json_object(); json_set(selector, "type", "select"); json_set(selector, "name", make_field_name(full_name, discriminator)); json_set(selector, "label", Json_Adapter::at(metadata, "discriminator_label")); json_set(selector, "options", Json_Adapter::at(metadata, "options")); json_set(selector, "required", true); Json body = json_array(); json_append(body, std::move(selector)); const auto& variants = Json_Adapter::at(metadata, "variants"); for(std::size_t index = 0; index < Json_Adapter::size(variants); ++index) { const auto& variant = Json_Adapter::at(variants, index); const std::string value = json_get(Json_Adapter::at(variant, "value")); Json group = json_object(); json_set(group, "type", "container"); json_set(group, "visibleOn", "${" + make_field_name(full_name, discriminator) + " == '" + escape_amis_string_literal(value) + "'}"); json_set(group, "body", make_amis_form_body(Json_Adapter::at(variant, "descriptor"), permission, full_name)); json_append(body, std::move(group)); } Json result = json_object(); json_set(result, "type", "fieldset"); json_set(result, "title", Json_Adapter::at(field, "label")); json_set(result, "body", std::move(body)); json_set(result, "collapsable", true); json_set(result, "collapsed", false); if(Json_Adapter::contains(field, "description")) { json_set(result, "description", Json_Adapter::at(field, "description")); } apply_visible_on(result, field, prefix); return result; } template requires Polymorphic_Type Json make_typed_amis_polymorphic_control(const Json& field, std::string_view permission, std::string_view prefix) { using Adapter = Polymorphic_Adapter, Json>; const std::string name = json_get(Json_Adapter::at(field, "name")); const std::string full_name = make_field_name(prefix, name); const auto& metadata = Json_Adapter::at(field, "polymorphic"); const std::string discriminator(Adapter::discriminator()); Json selector = json_object(); json_set(selector, "type", "select"); json_set(selector, "name", make_field_name(full_name, discriminator)); json_set(selector, "label", Json_Adapter::at(metadata, "discriminator_label")); json_set(selector, "options", Json_Adapter::at(metadata, "options")); json_set(selector, "required", true); Json body = json_array(); json_append(body, std::move(selector)); std::apply([&](const auto&... variant) { ([&] { using Variant = typename std::remove_cvref_t::type; const Json descriptor = to_descriptor_json(); Json group = json_object(); json_set(group, "type", "container"); json_set(group, "visibleOn", "${" + make_field_name(full_name, discriminator) + " == '" + escape_amis_string_literal(variant.value) + "'}"); json_set(group, "body", make_typed_amis_form_body>(descriptor, permission, full_name)); json_append(body, std::move(group)); }(), ...); }, Adapter::variants()); Json result = json_object(); json_set(result, "type", "fieldset"); json_set(result, "title", Json_Adapter::at(field, "label")); json_set(result, "body", std::move(body)); json_set(result, "collapsable", true); json_set(result, "collapsed", false); if(Json_Adapter::contains(field, "description")) { json_set(result, "description", Json_Adapter::at(field, "description")); } apply_visible_on(result, field, prefix); return result; } template Json make_default_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) { const std::string name = json_get(Json_Adapter::at(field, "name")); const std::string full_name = make_field_name(prefix, name); const std::string value_type = json_get(Json_Adapter::at(field, "value_type")); if(value_type == "object" && Json_Adapter::contains(field, "children")) { Json descriptor = json_object(); json_set(descriptor, "fields", Json_Adapter::at(field, "children")); Json control = json_object(); json_set(control, "type", "fieldset"); json_set(control, "title", Json_Adapter::at(field, "label")); json_set(control, "body", make_amis_form_body(descriptor, permission, full_name)); json_set(control, "collapsable", true); json_set(control, "collapsed", false); if(Json_Adapter::contains(field, "description")) { json_set(control, "description", Json_Adapter::at(field, "description")); } apply_visible_on(control, field, prefix); return control; } if(value_type == "polymorphic" && Json_Adapter::contains(field, "polymorphic")) { return make_amis_polymorphic_control(field, permission, prefix); } Json control = json_object(); json_set(control, "name", full_name); json_set(control, "label", Json_Adapter::at(field, "label")); if(Json_Adapter::contains(field, "description")) { json_set(control, "description", Json_Adapter::at(field, "description")); } apply_visible_on(control, field, prefix); if(!json_get(Json_Adapter::at(field, permission))) { json_set(control, "type", "static"); return control; } if(Json_Adapter::contains(field, "widget")) { json_set(control, "type", Json_Adapter::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 std::string control_type = json_get(Json_Adapter::at(control, "type")); if(Json_Adapter::contains(field, "nullable") && json_get(Json_Adapter::at(field, "nullable"))) { json_set(control, "clearable", true); } if(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") { json_set(control, "clearable", true); } if(json_get(Json_Adapter::at(field, "required"))) { json_set(control, "required", true); } if(Json_Adapter::contains(field, "minimum")) { json_set(control, "min", Json_Adapter::at(field, "minimum")); } if(Json_Adapter::contains(field, "maximum")) { json_set(control, "max", Json_Adapter::at(field, "maximum")); } if(Json_Adapter::contains(field, "multiple_of")) { json_set(control, "step", Json_Adapter::at(field, "multiple_of")); } if(Json_Adapter::contains(field, "validation_message")) { Json errors = json_object(); json_set(errors, "isInt", Json_Adapter::at(field, "validation_message")); json_set(control, "validationErrors", std::move(errors)); } if(Json_Adapter::contains(field, "options")) { json_set(control, "options", Json_Adapter::at(field, "options")); } return control; } template Json make_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) { return make_default_amis_control(field, permission, prefix); } template Json make_typed_amis_control(const Json& field, std::string_view permission, std::string_view prefix = {}) { using Storage = std::remove_cvref_t; using Value = Adapted_Value_Type; using Control_Value = Optional_Unwrapped_Type; const Control_Context context{permission, prefix}; if constexpr(Control_Adapter_With_Control) { return Control_Adapter::make_control(field, context); } else if constexpr(!std::same_as && Control_Adapter_With_Control) { return Control_Adapter::make_control(field, context); } else if constexpr(!std::same_as && Control_Adapter_With_Control) { return Control_Adapter::make_control(field, context); } else if constexpr(Polymorphic_Type) { return make_typed_amis_polymorphic_control(field, permission, prefix); } else if constexpr(Described_Type || Snapshot_Adapted_Object) { using Model = Object_Model_Type; const std::string name = json_get(Json_Adapter::at(field, "name")); const std::string full_name = make_field_name(prefix, name); const Json descriptor = to_descriptor_json(); Json control = json_object(); json_set(control, "type", "fieldset"); json_set(control, "title", Json_Adapter::at(field, "label")); json_set(control, "body", make_typed_amis_form_body(descriptor, permission, full_name)); json_set(control, "collapsable", true); json_set(control, "collapsed", false); if(Json_Adapter::contains(field, "description")) { json_set(control, "description", Json_Adapter::at(field, "description")); } apply_visible_on(control, field, prefix); return control; } else { return make_default_amis_control(field, permission, prefix); } } template Json make_amis_form_body(const Json& descriptor, std::string_view permission, std::string_view prefix) { Json body = json_array(); const auto& fields = Json_Adapter::at(descriptor, "fields"); for(std::size_t index = 0; index < Json_Adapter::size(fields); ++index) { const auto& field = Json_Adapter::at(fields, index); if(json_get(Json_Adapter::at(field, "visible"))) { json_append(body, make_default_amis_control(field, permission, prefix)); } } return body; } template void append_typed_amis_form_fields(Json& body, const Json& fields, const Descriptor& descriptor, std::string_view permission, std::string_view prefix) { using Fields = std::remove_cvref_t; if constexpr(Index < std::tuple_size_v) { const auto& item = std::get(descriptor.fields()); const auto& field = Json_Adapter::at(fields, Index); if(json_get(Json_Adapter::at(field, "visible"))) { using Field = std::remove_cvref_t; json_append(body, make_typed_amis_control(field, permission, prefix)); } append_typed_amis_form_fields(body, fields, descriptor, permission, prefix); } } template Json make_typed_amis_form_body(const Json& descriptor, std::string_view permission, std::string_view prefix) { Json body = json_array(); const auto& fields = Json_Adapter::at(descriptor, "fields"); const auto object_descriptor = describe(); append_typed_amis_form_fields<0>(body, fields, object_descriptor, permission, prefix); return body; } template Json make_default_amis_column(const Json& field) { Json column = json_object(); json_set(column, "name", Json_Adapter::at(field, "name")); json_set(column, "label", Json_Adapter::at(field, "list_label")); if(Json_Adapter::contains(field, "order")) { json_set(column, "order", Json_Adapter::at(field, "order")); } if(Json_Adapter::contains(field, "sortable")) { json_set(column, "sortable", Json_Adapter::at(field, "sortable")); } const std::string value_type = json_get(Json_Adapter::at(field, "value_type")); if(value_type == "boolean") { json_set(column, "type", "status"); } else if(value_type == "enum" && Json_Adapter::contains(field, "options")) { json_set(column, "type", "mapping"); Json map = json_object(); const auto& options = Json_Adapter::at(field, "options"); for(std::size_t index = 0; index < Json_Adapter::size(options); ++index) { const auto& option = Json_Adapter::at(options, index); json_set(map, json_get(Json_Adapter::at(option, "value")), Json_Adapter::at(option, "label")); } json_set(column, "map", std::move(map)); } return column; } template Json make_typed_amis_column(const Json& field) { using Storage = std::remove_cvref_t; using Value = Adapted_Value_Type; using Control_Value = Optional_Unwrapped_Type; if constexpr(Control_Adapter_With_Column) { return Control_Adapter::make_column(field); } else if constexpr(!std::same_as && Control_Adapter_With_Column) { return Control_Adapter::make_column(field); } else if constexpr(!std::same_as && Control_Adapter_With_Column) { return Control_Adapter::make_column(field); } else { return make_default_amis_column(field); } } template struct Amis_Ordered_Column { int order{}; Json column; }; template void append_typed_amis_columns(std::vector>& columns, const Json& fields, const Descriptor& descriptor) { using Fields = std::remove_cvref_t; if constexpr(Index < std::tuple_size_v) { const auto& item = std::get(descriptor.fields()); const auto& field = Json_Adapter::at(fields, Index); using Field = std::remove_cvref_t; using Member = typename Field::member_type; using Storage = std::remove_cvref_t; using Value = Adapted_Value_Type; using Control_Value = Optional_Unwrapped_Type; constexpr bool custom_column = Control_Adapter_With_Column || (!std::same_as && Control_Adapter_With_Column) || (!std::same_as && Control_Adapter_With_Column); const std::string value_type = json_get(Json_Adapter::at(field, "value_type")); const bool structural = value_type == "object" || value_type == "array" || value_type == "map" || value_type == "polymorphic"; if(json_get(Json_Adapter::at(field, "readable")) && json_get(Json_Adapter::at(field, "list_visible")) && (!structural || custom_column)) { columns.push_back(Amis_Ordered_Column{json_get(Json_Adapter::at(field, "order")), make_typed_amis_column(field)}); } append_typed_amis_columns(columns, fields, descriptor); } } template Json make_amis_status_body(const Json& descriptor) { Json body = json_array(); const auto& fields = Json_Adapter::at(descriptor, "fields"); for(std::size_t index = 0; index < Json_Adapter::size(fields); ++index) { const auto& field = Json_Adapter::at(fields, index); const std::string name = json_get(Json_Adapter::at(field, "name")); const std::string label = json_get(Json_Adapter::at(field, "label")); const bool color = json_get(Json_Adapter::at(field, "color")); std::string tpl = "
" + label + "
"; Json item = json_object(); json_set(item, "type", "tpl"); json_set(item, "tpl", std::move(tpl)); json_append(body, std::move(item)); } Json result = json_object(); json_set(result, "type", "container"); json_set(result, "className", "adminive-object-status"); json_set(result, "body", std::move(body)); return result; } template Json make_amis_status_service(const Json& descriptor, std::string status_api, std::uint64_t interval = 2000) { Json panel = json_object(); json_set(panel, "type", "panel"); json_set(panel, "title", Json_Adapter::at(descriptor, "label")); json_set(panel, "body", make_amis_status_body(descriptor)); Json api = json_object(); json_set(api, "method", "get"); json_set(api, "url", std::move(status_api)); Json result = json_object(); json_set(result, "type", "service"); json_set(result, "name", json_get(Json_Adapter::at(descriptor, "name")) + "_status"); json_set(result, "api", std::move(api)); json_set(result, "initFetch", true); json_set(result, "interval", interval); json_set(result, "silentPolling", true); json_set(result, "showErrorMsg", true); json_set(result, "body", std::move(panel)); return result; } template Json make_amis_object_status_service(const Json& descriptor, std::string status_api, std::uint64_t interval) { Json api = json_object(); json_set(api, "method", "get"); json_set(api, "url", std::move(status_api)); Json result = json_object(); json_set(result, "type", "service"); json_set(result, "api", std::move(api)); json_set(result, "initFetch", true); json_set(result, "interval", interval); json_set(result, "silentPolling", true); json_set(result, "showErrorMsg", true); json_set(result, "body", make_amis_status_body(descriptor)); return result; } template Json make_amis_object_status_column(const Json& descriptor, std::string status_api, std::uint64_t interval, std::string action_label) { Json pop_over = json_object(); json_set(pop_over, "trigger", "click"); json_set(pop_over, "position", "left-top"); json_set(pop_over, "showIcon", false); json_set(pop_over, "body", make_amis_object_status_service(descriptor, std::move(status_api), interval)); Json result = json_object(); json_set(result, "type", "tpl"); json_set(result, "label", Json_Adapter::at(descriptor, "label")); json_set(result, "tpl", "" + std::move(action_label) + ""); json_set(result, "popOver", std::move(pop_over)); return result; } template requires Described_Type> || Snapshot_Adapted_Object Json to_amis_form_schema(const T& value, std::string submit_api = {}, std::string submit_label = "Apply") { using Model = Object_Model_Type; const Json descriptor = to_descriptor_json(); Json result = json_object(); json_set(result, "type", "form"); json_set(result, "title", Json_Adapter::at(descriptor, "label")); json_set(result, "data", to_frontend_json(value)); json_set(result, "body", make_typed_amis_form_body(descriptor, "editable")); json_set(result, "actions", make_amis_form_actions(std::move(submit_label))); json_set(result, "affixFooter", true); if(!submit_api.empty()) { Json api = json_object(); json_set(api, "method", "post"); json_set(api, "url", std::move(submit_api)); json_set(result, "api", std::move(api)); } return result; } template requires Described_Type> Json to_amis_crud_schema(std::string base_api, const Json* status_descriptor = nullptr, std::uint64_t status_interval = 2000) { using Model = Object_Model_Type; const Json descriptor = to_descriptor_json(); const Json& list = Json_Adapter::at(descriptor, "list"); const std::string crud_id = json_get(Json_Adapter::at(descriptor, "name")) + "_crud"; Json columns = json_array(); Json id_column = json_object(); json_set(id_column, "name", "id"); json_set(id_column, "label", Json_Adapter::at(list, "id_label")); json_set(id_column, "sortable", true); json_append(columns, std::move(id_column)); std::vector> ordered_columns; const auto& fields = Json_Adapter::at(descriptor, "fields"); const auto object_descriptor = describe(); append_typed_amis_columns<0>(ordered_columns, fields, object_descriptor); std::stable_sort(ordered_columns.begin(), ordered_columns.end(), [](const Amis_Ordered_Column& left, const Amis_Ordered_Column& right) { return left.order < right.order; }); for(auto& column : ordered_columns) { json_append(columns, std::move(column.column)); } if(status_descriptor) { json_append(columns, make_amis_object_status_column(*status_descriptor, base_api + "/${id}/status", status_interval, json_get(Json_Adapter::at(list, "view_status_label")))); } const std::string create_label = json_get(Json_Adapter::at(list, "create_label")); const std::string edit_label = json_get(Json_Adapter::at(list, "edit_label")); Json create_api = json_object(); json_set(create_api, "method", "post"); json_set(create_api, "url", base_api); Json create_form = json_object(); 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(descriptor, "creatable")); json_set(create_form, "actions", make_amis_form_actions(create_label)); Json edit_api = json_object(); json_set(edit_api, "method", "put"); json_set(edit_api, "url", base_api + "/${id}"); Json edit_form = json_object(); 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(descriptor, "editable")); json_set(edit_form, "actions", make_amis_form_actions(json_get(Json_Adapter::at(list, "confirm_label")))); Json create_dialog = json_object(); json_set(create_dialog, "title", create_label + " " + json_get(Json_Adapter::at(descriptor, "label"))); json_set(create_dialog, "body", std::move(create_form)); Json create_button = json_object(); json_set(create_button, "type", "button"); json_set(create_button, "label", 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_set(edit_dialog, "title", edit_label + " " + json_get(Json_Adapter::at(descriptor, "label"))); json_set(edit_dialog, "body", std::move(edit_form)); Json edit_button = json_object(); json_set(edit_button, "type", "button"); json_set(edit_button, "label", edit_label); json_set(edit_button, "level", "link"); json_set(edit_button, "actionType", "dialog"); json_set(edit_button, "dialog", std::move(edit_dialog)); Json delete_api = json_object(); json_set(delete_api, "method", "delete"); json_set(delete_api, "url", base_api + "/${id}"); Json delete_button = json_object(); json_set(delete_button, "type", "button"); json_set(delete_button, "label", Json_Adapter::at(list, "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::at(list, "delete_confirm")); json_set(delete_button, "api", std::move(delete_api)); json_set(delete_button, "reload", crud_id); Json buttons = json_array(); json_append(buttons, std::move(edit_button)); json_append(buttons, std::move(delete_button)); Json operation = json_object(); json_set(operation, "type", "operation"); json_set(operation, "label", Json_Adapter::at(list, "actions_label")); json_set(operation, "buttons", std::move(buttons)); json_append(columns, std::move(operation)); Json toggler = json_object(); json_set(toggler, "type", "columns-toggler"); json_set(toggler, "draggable", Json_Adapter::at(list, "column_reorderable")); Json toolbar = json_array(); json_append(toolbar, std::move(toggler)); json_append(toolbar, "reload"); json_append(toolbar, std::move(create_button)); Json crud_api = json_object(); json_set(crud_api, "method", "get"); json_set(crud_api, "url", base_api); Json crud = json_object(); json_set(crud, "type", "crud"); json_set(crud, "id", crud_id); json_set(crud, "api", std::move(crud_api)); json_set(crud, "syncLocation", false); 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_Adapter::at(list, "default_order_by")); if(!default_order_by.empty()) { Json params = json_object(); json_set(params, "orderBy", default_order_by); json_set(params, "orderDir", Json_Adapter::at(list, "default_order_dir")); json_set(crud, "defaultParams", std::move(params)); } if(json_get(Json_Adapter::at(list, "user_reorderable"))) { json_set(crud, "draggable", true); Json order_api = json_object(); json_set(order_api, "method", "post"); json_set(order_api, "url", base_api + "/order"); json_set(crud, "saveOrderApi", std::move(order_api)); } Json result = json_object(); json_set(result, "type", "page"); json_set(result, "title", Json_Adapter::at(list, "management_label")); json_set(result, "body", std::move(crud)); return result; } template requires Described_Type> 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(std::move(base_api), object_status_descriptor, object_status_interval); Json crud = std::move(Json_Adapter::at(result, "body")); Json body = json_array(); json_append(body, make_amis_status_service(status_descriptor, std::move(status_api), status_interval)); json_append(body, std::move(crud)); json_set(result, "body", std::move(body)); return result; } }