Files
Adminive/backend/library/include/adminive/view.hpp
T
2026-08-08 16:55:22 +08:00

632 lines
22 KiB
C++

#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,
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<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 frontend(Children&&... children) {
return make_view_container(View_Node_Kind::frontend, std::forward<Children>(children)...);
}
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 list(Children&&... children) {
return make_view_container(View_Node_Kind::list, std::forward<Children>(children)...);
}
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;
}
enum class Collection_Mode {
table,
list,
cards
};
struct Collection_Item_View {
std::string title_field;
std::string subtitle_field;
std::string accent_field;
std::vector<std::string> fields;
template <auto Member>
Collection_Item_View subtitle() const {
auto result = *this;
result.subtitle_field = described_member_name<Member>();
return result;
}
template <auto Member>
Collection_Item_View accent() const {
auto result = *this;
result.accent_field = described_member_name<Member>();
return result;
}
template <auto... Members>
Collection_Item_View body() const {
auto result = *this;
result.fields = {described_member_name<Members>()...};
return result;
}
};
template <auto Member>
Collection_Item_View collection_item() {
Collection_Item_View result;
result.title_field = described_member_name<Member>();
return result;
}
struct Collection_View {
Table_View table;
Collection_Mode mode{Collection_Mode::table};
Collection_Item_View item;
std::size_t cards_columns{3};
Collection_View titled(std::string value) const {
auto result = *this;
result.table.title = std::move(value);
return result;
}
Collection_View id_title(std::string value) const {
auto result = *this;
result.table.id_label = std::move(value);
return result;
}
Collection_View create_title(std::string value) const {
auto result = *this;
result.table.create_label = std::move(value);
return result;
}
Collection_View edit_title(std::string value) const {
auto result = *this;
result.table.edit_label = std::move(value);
return result;
}
Collection_View delete_title(std::string value) const {
auto result = *this;
result.table.delete_label = std::move(value);
return result;
}
Collection_View actions_title(std::string value) const {
auto result = *this;
result.table.actions_label = std::move(value);
return result;
}
Collection_View confirm_title(std::string value) const {
auto result = *this;
result.table.confirm_label = std::move(value);
return result;
}
Collection_View delete_confirmation(std::string value) const {
auto result = *this;
result.table.delete_confirm = std::move(value);
return result;
}
Collection_View status_title(std::string value) const {
auto result = *this;
result.table.view_status_label = std::move(value);
return result;
}
Collection_View default_sort(std::string field_name, bool descending = false) const {
auto result = *this;
result.table.default_order_by = std::move(field_name);
result.table.default_order_dir = descending ? "desc" : "asc";
return result;
}
Collection_View reorderable(bool value = true) const {
auto result = *this;
result.table.user_reorderable = value;
return result;
}
Collection_View reorderable_columns(bool value = true) const {
auto result = *this;
result.table.column_reorderable = value;
return result;
}
Collection_View create_layout(View_Node value) const {
auto result = *this;
result.table.create_body = std::move(value);
return result;
}
Collection_View edit_layout(View_Node value) const {
auto result = *this;
result.table.edit_body = std::move(value);
return result;
}
Collection_View as_table() const {
auto result = *this;
result.mode = Collection_Mode::table;
result.item = {};
return result;
}
Collection_View as_list(Collection_Item_View value) const {
auto result = *this;
result.mode = Collection_Mode::list;
result.item = std::move(value);
return result;
}
Collection_View as_cards(Collection_Item_View value, std::size_t columns = 3) const {
auto result = *this;
result.mode = Collection_Mode::cards;
result.item = std::move(value);
result.cards_columns = columns;
return result;
}
};
template <Described_Type T, class... Columns>
Collection_View collection_view(Columns&&... columns) {
Collection_View result;
result.table = table_view<T>(std::forward<Columns>(columns)...);
result.table.name = describe<T>().name() + "_collection";
return result;
}
template <class T>
struct Type_View_Descriptor;
template <Described_Type T>
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 T>
concept Collection_View_Described_Type = Described_Type<T> && requires {
{ Type_View_Descriptor<T>::collection() } -> std::same_as<Collection_View>;
};
template <Described_Type T>
Collection_View describe_collection_view() requires Collection_View_Described_Type<T> {
return Type_View_Descriptor<T>::collection();
}
template <class Descriptor>
bool descriptor_has_field(const Descriptor& descriptor, std::string_view name) {
bool found{};
std::apply([&](const auto&... field) {
((found = found || field.name() == name), ...);
}, descriptor.fields());
return found;
}
template <Described_Type T>
void validate_view_node_impl(const View_Node& node, bool tab_allowed) {
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::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<T>(child, node.kind == View_Node_Kind::tabs);
}
}
template <Described_Type T>
void validate_view_node(const View_Node& node) {
validate_view_node_impl<T>(node, false);
}
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);
}
template <Described_Type T>
void validate_collection_view(const Collection_View& view) {
validate_table_view<T>(view.table);
if(view.mode == Collection_Mode::table) {
return;
}
const auto descriptor = describe<T>();
if(view.item.title_field.empty() || !descriptor_has_field(descriptor, view.item.title_field)) {
throw std::invalid_argument("collection item title field does not exist: " + view.item.title_field);
}
if(!view.item.subtitle_field.empty() && !descriptor_has_field(descriptor, view.item.subtitle_field)) {
throw std::invalid_argument("collection item subtitle field does not exist: " + view.item.subtitle_field);
}
if(!view.item.accent_field.empty() && !descriptor_has_field(descriptor, view.item.accent_field)) {
throw std::invalid_argument("collection item accent field does not exist: " + view.item.accent_field);
}
for(const auto& field : view.item.fields) {
if(!descriptor_has_field(descriptor, field)) {
throw std::invalid_argument("collection item body field does not exist: " + field);
}
}
if(view.mode == Collection_Mode::cards && view.cards_columns == 0) {
throw std::invalid_argument("collection cards requires at least one column");
}
}
}