#pragma once #include "adminive/descriptor.hpp" #include #include #include #include #include #include #include #include namespace adminive { enum class View_Node_Kind { all_fields, field, frontend, vertical, horizontal, flow, grid, list, 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 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 std::string described_member_name() { using Owner = typename structive::Member_Pointer_Traits::object_type; const auto descriptor = describe(); std::string result; std::apply([&](const auto&... item) { ([&] { using Field = std::remove_cvref_t; using Accessor = typename Field::accessor_type; if constexpr(requires { Accessor::member; }) { if constexpr(std::same_as, std::remove_cv_t>) { 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 View_Node use() { return view_field(described_member_name()); } template View_Node use_index() { static_assert(Index < static_cast(Reflection_Adapter::field_count)); const auto descriptor = describe(); const auto& field = std::get(descriptor.fields()); return view_field(field.name()); } template 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)), ...); return result; } template View_Node frontend(Children&&... children) { return make_view_container(View_Node_Kind::frontend, std::forward(children)...); } template View_Node vertical(Children&&... children) { return make_view_container(View_Node_Kind::vertical, std::forward(children)...); } template View_Node horizontal(Children&&... children) { return make_view_container(View_Node_Kind::horizontal, std::forward(children)...); } template View_Node flow(Children&&... children) { return make_view_container(View_Node_Kind::flow, std::forward(children)...); } template View_Node grid(std::size_t columns, Children&&... children) { auto result = make_view_container(View_Node_Kind::grid, std::forward(children)...); result.columns = columns; return result; } template View_Node list(Children&&... children) { return make_view_container(View_Node_Kind::list, std::forward(children)...); } template View_Node group(std::string title, Children&&... children) { auto result = make_view_container(View_Node_Kind::group, std::forward(children)...); result.title = std::move(title); return result; } template View_Node card(std::string title, Children&&... children) { auto result = make_view_container(View_Node_Kind::card, std::forward(children)...); result.title = std::move(title); return result; } template View_Node tab(std::string title, Children&&... children) { auto result = make_view_container(View_Node_Kind::tab, std::forward(children)...); result.title = std::move(title); return result; } template View_Node tabs(Children&&... children) { return make_view_container(View_Node_Kind::tabs, std::forward(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 Form_View edit_form(View_Node body = all_fields()) { const auto descriptor = describe(); return Form_View{descriptor.name() + "_edit", descriptor.label(), "Apply", Form_Mode::edit, std::move(body), true}; } template Form_View create_form(View_Node body = all_fields()) { const auto descriptor = describe(); return Form_View{descriptor.name() + "_create", descriptor.label(), "Create", Form_Mode::create, std::move(body), true}; } template Form_View detail_view(View_Node body = all_fields()) { const auto descriptor = describe(); 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 Table_Column column(std::string label = {}) { return Table_Column{described_member_name(), std::move(label)}; } template Table_Column column_index(std::string label = {}) { const auto descriptor = describe(); const auto& field = std::get(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 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 Table_View table_view(Columns&&... columns) { const auto descriptor = describe(); 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)), ...); return result; } enum class Collection_Mode { table, list, cards }; struct Collection_Item_View { std::string title_field; std::string subtitle_field; std::string accent_field; std::vector fields; template Collection_Item_View subtitle() const { auto result = *this; result.subtitle_field = described_member_name(); return result; } template Collection_Item_View accent() const { auto result = *this; result.accent_field = described_member_name(); return result; } template Collection_Item_View body() const { auto result = *this; result.fields = {described_member_name()...}; return result; } }; template Collection_Item_View collection_item() { Collection_Item_View result; result.title_field = described_member_name(); return result; } struct Collection_View { Table_View table; Collection_Mode mode{Collection_Mode::table}; Collection_Item_View item; std::size_t cards_columns{3}; Collection_View titled(std::string value) const { auto result = *this; result.table.title = std::move(value); return result; } Collection_View id_title(std::string value) const { auto result = *this; result.table.id_label = std::move(value); return result; } Collection_View create_title(std::string value) const { auto result = *this; result.table.create_label = std::move(value); return result; } Collection_View edit_title(std::string value) const { auto result = *this; result.table.edit_label = std::move(value); return result; } Collection_View delete_title(std::string value) const { auto result = *this; result.table.delete_label = std::move(value); return result; } Collection_View actions_title(std::string value) const { auto result = *this; result.table.actions_label = std::move(value); return result; } Collection_View confirm_title(std::string value) const { auto result = *this; result.table.confirm_label = std::move(value); return result; } Collection_View delete_confirmation(std::string value) const { auto result = *this; result.table.delete_confirm = std::move(value); return result; } Collection_View status_title(std::string value) const { auto result = *this; result.table.view_status_label = std::move(value); return result; } Collection_View default_sort(std::string field_name, bool descending = false) const { auto result = *this; result.table.default_order_by = std::move(field_name); result.table.default_order_dir = descending ? "desc" : "asc"; return result; } Collection_View reorderable(bool value = true) const { auto result = *this; result.table.user_reorderable = value; return result; } Collection_View reorderable_columns(bool value = true) const { auto result = *this; result.table.column_reorderable = value; return result; } Collection_View create_layout(View_Node value) const { auto result = *this; result.table.create_body = std::move(value); return result; } Collection_View edit_layout(View_Node value) const { auto result = *this; result.table.edit_body = std::move(value); return result; } Collection_View as_table() const { auto result = *this; result.mode = Collection_Mode::table; result.item = {}; return result; } Collection_View as_list(Collection_Item_View value) const { auto result = *this; result.mode = Collection_Mode::list; result.item = std::move(value); return result; } Collection_View as_cards(Collection_Item_View value, std::size_t columns = 3) const { auto result = *this; result.mode = Collection_Mode::cards; result.item = std::move(value); result.cards_columns = columns; return result; } }; template Collection_View collection_view(Columns&&... columns) { Collection_View result; result.table = table_view(std::forward(columns)...); result.table.name = describe().name() + "_collection"; return result; } template struct Type_View_Descriptor; template Form_View describe_edit_view() { if constexpr(requires { { Type_View_Descriptor::edit() } -> std::same_as; }) { return Type_View_Descriptor::edit(); } else { return edit_form(); } } template Form_View describe_create_view() { if constexpr(requires { { Type_View_Descriptor::create() } -> std::same_as; }) { return Type_View_Descriptor::create(); } else { return create_form(); } } template Form_View describe_detail_view() { if constexpr(requires { { Type_View_Descriptor::detail() } -> std::same_as; }) { return Type_View_Descriptor::detail(); } else { return detail_view(); } } template concept Table_View_Described_Type = Described_Type && requires { { Type_View_Descriptor::table() } -> std::same_as; }; template Table_View describe_table_view() requires Table_View_Described_Type { return Type_View_Descriptor::table(); } template concept Collection_View_Described_Type = Described_Type && requires { { Type_View_Descriptor::collection() } -> std::same_as; }; template Collection_View describe_collection_view() requires Collection_View_Described_Type { return Type_View_Descriptor::collection(); } template 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 void validate_view_node_impl(const View_Node& node, bool tab_allowed) { const auto descriptor = describe(); 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::tab && !tab_allowed) { throw std::invalid_argument("tab view can only be used inside tabs"); } 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_impl(child, node.kind == View_Node_Kind::tabs); } } template void validate_view_node(const View_Node& node) { validate_view_node_impl(node, false); } template 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(view.body); } template 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(); std::vector 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(view.create_body); validate_view_node(view.edit_body); } template void validate_collection_view(const Collection_View& view) { validate_table_view(view.table); if(view.mode == Collection_Mode::table) { return; } const auto descriptor = describe(); if(view.item.title_field.empty() || !descriptor_has_field(descriptor, view.item.title_field)) { throw std::invalid_argument("collection item title field does not exist: " + view.item.title_field); } if(!view.item.subtitle_field.empty() && !descriptor_has_field(descriptor, view.item.subtitle_field)) { throw std::invalid_argument("collection item subtitle field does not exist: " + view.item.subtitle_field); } if(!view.item.accent_field.empty() && !descriptor_has_field(descriptor, view.item.accent_field)) { throw std::invalid_argument("collection item accent field does not exist: " + view.item.accent_field); } for(const auto& field : view.item.fields) { if(!descriptor_has_field(descriptor, field)) { throw std::invalid_argument("collection item body field does not exist: " + field); } } if(view.mode == Collection_Mode::cards && view.cards_columns == 0) { throw std::invalid_argument("collection cards requires at least one column"); } } }