632 lines
22 KiB
C++
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");
|
|
}
|
|
}
|
|
}
|