分层重构
This commit is contained in:
@@ -1,19 +1,32 @@
|
||||
# Adminive Backend-Driven Complete Example
|
||||
|
||||
Adminive 使用 C++20 模板、Concepts 和可替换适配器描述业务类型。同一份后端描述驱动 JSON 双向转换、事务式赋值、前端可编辑性、校验、amis CRUD、枚举列表列、树状配置、联动条件、日期控件、颜色控件和一次性提交。属性能力、Schema 与同步拓扑由 `third_party/Structive` 提供,Adminive 只叠加后台管理领域的描述、JSON、HTTP、AMIS 和事务适配。核心层不依赖具体 JSON 库、枚举反射库或结构体反射库。前端只读取 `/admin/amis` 并渲染后端返回的页面 JSON,不包含业务字段或业务判断。
|
||||
Adminive 使用 C++20 模板、Concepts 和可替换适配器,把后端已有的 C++ 业务结构投影成后台管理界面。同一份后端定义同时驱动对象结构 JSON、数据访问、字段控件、表单/卡片/流式/网格/Tabs/表格等语义组合、查询能力、HTTP 接口和 AMIS 页面。属性能力、Schema 与同步拓扑由 `third_party/Structive` 提供;Adminive 在其上分层描述 Field Presentation、View Schema、页面 Composition、JSON/HTTP/AMIS 和事务适配。前端不维护第二份业务字段清单,只消费后端生成的 `/descriptor`、`/view`、`/data` 与 `/amis`。
|
||||
|
||||
## 设计理念
|
||||
|
||||
Adminive 的核心定位是:**让后端已有的 C++ 结构直接驱动后台管理能力,而不是再建立一套独立的数据模型。**
|
||||
|
||||
整体职责分为两层:
|
||||
整体职责按语义分层:
|
||||
|
||||
```text
|
||||
Structive
|
||||
负责 Property、Schema、intrinsic read/write 与 synchronization
|
||||
|
||||
Adminive
|
||||
负责后台领域 metadata、JSON、HTTP、AMIS、managed transaction 与 frontend projection
|
||||
Structive / Object Schema
|
||||
Property、类型、intrinsic read/write、constraint、synchronization
|
||||
│
|
||||
▼
|
||||
Adminive Field Presentation
|
||||
单个字段的 label、description、control、options、visible_on
|
||||
│
|
||||
▼
|
||||
Adminive View Schema
|
||||
Form/Table 的字段选择、顺序、分组和数据交互能力
|
||||
│
|
||||
▼
|
||||
Adminive Composition View
|
||||
Form/Table/Status 等组件之间的 card/grid/flow/tabs 等页面组合
|
||||
│
|
||||
▼
|
||||
AMIS Adapter / Frontend
|
||||
翻译成具体 AMIS JSON,并负责最终视觉样式和响应式表现
|
||||
```
|
||||
|
||||
当前设计遵守几个核心原则:
|
||||
@@ -21,14 +34,68 @@ Adminive
|
||||
- Structive 管“对象本身是什么”,Adminive 管“后台系统怎样消费它”。
|
||||
- 字段默认 intrinsic read-only,只有 `.editable()`、`.creatable()` 或 `.read_write()` 显式进入 managed mutation path。
|
||||
- `read_only` 是能够影响真实运行成本的结构信息:只读存储属性不为自身贡献 lock slot/mutex,managed read 走零锁路径。
|
||||
- `editable`、`sensitive`、`widget` 等是 Adminive 领域 metadata,不是通用 Property 访问控制。外部系统是否暴露字段由外部系统自己决定。
|
||||
- `editable`、`sensitive` 与 `Field_Presentation` 是 Adminive 领域 metadata;表格排序、搜索、筛选和页面组合属于独立 `View Schema`,都不是通用 Property 访问控制。
|
||||
- Synchronization 只负责进程内 mutable consistency;`Resource_Transaction` 负责外部副作用的 prepare/commit/rollback;数据库事务继续由数据库负责。
|
||||
- JSON、HTTP、枚举反射和结构体反射都通过 Adapter 接入,第三方依赖不进入 Adminive Core 协议。
|
||||
- 前端只消费后端描述,不维护第二份业务 Schema。
|
||||
- 后端拥有页面的语义结构;前端只消费后端描述并负责具体渲染、样式与响应式细节,不维护第二份业务 Schema。
|
||||
- 静态可知的能力尽量在编译期表达,动态输入才进入 runtime adapter。
|
||||
|
||||
完整的设计理念、职责边界、同步原则、事务分层和 API 演进规则见 [backend/library/DESIGN.md](backend/library/DESIGN.md)。
|
||||
|
||||
### 后端 UI 的三层描述与组合层
|
||||
|
||||
一个字段“是什么”、一个字段“怎么呈现”、一组字段“怎么组成业务视图”,以及多个业务视图“怎么组成页面”是四个不同问题。Adminive 不再把它们塞进同一个 Field Descriptor:
|
||||
|
||||
```text
|
||||
Object Schema 结构事实
|
||||
Field Presentation 单字段呈现
|
||||
View Schema Form/Table 语义组合
|
||||
Composition View 多组件页面组合
|
||||
```
|
||||
|
||||
后端保留完整的组合能力,包括 `vertical`、`horizontal`、`flow`、`grid`、`group`、`card`、`tabs` 和表格。区别只在于这些能力现在位于独立层:字段定义不再携带表格列顺序/排序等信息,AMIS Adapter 也不再承担业务组合决策。
|
||||
|
||||
例如一个配置对象可以只定义字段事实和单字段控件:
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD(T, host).editable().label("Host").text_input();
|
||||
ADMINIVE_FIELD(T, port).editable().label("Port").number_input();
|
||||
ADMINIVE_FIELD(T, color).editable().label("Color").color_input();
|
||||
```
|
||||
|
||||
同一个类型的编辑视图再独立决定组合:
|
||||
|
||||
```cpp
|
||||
return edit_form<T>(vertical(
|
||||
group("Network", horizontal(use<&T::host>(), use<&T::port>())),
|
||||
card("Appearance", use<&T::color>())
|
||||
));
|
||||
```
|
||||
|
||||
如果同一个类型还需要列表,表格能力同样单独定义:
|
||||
|
||||
```cpp
|
||||
return table_view<T>(
|
||||
column<&T::host>("Host").search().fix(Table_Fixed::left),
|
||||
column<&T::port>("Port").sort(),
|
||||
column<&T::color>("Color")
|
||||
).default_sort("port");
|
||||
```
|
||||
|
||||
其中 `.sort()`、`.search()`、`.filter()` 不只是前端提示:Collection Service 只接受后端 View Schema 明确声明的查询能力。前端不能自行把一个后端不支持的字段变成可排序或可搜索字段。
|
||||
|
||||
对于“上面一个配置表单、下面一个设备表格”这类跨组件页面,使用 `Composition_View`:
|
||||
|
||||
```cpp
|
||||
const auto page = composition_view("device_page", compose::vertical(
|
||||
compose::heading("Devices"),
|
||||
compose::slot("config"),
|
||||
compose::slot("table")
|
||||
));
|
||||
```
|
||||
|
||||
`slot` 引用已经生成的 Form/Table/Status 等组件,后端仍然决定语义组合,AMIS Adapter 只负责把组合翻译成具体 AMIS JSON。前端可以进一步决定 CSS、断点、列宽等纯视觉细节。
|
||||
|
||||
## 后端分层
|
||||
|
||||
`backend/library` 是库级协议层,只有头文件,不依赖也不链接任何具体 JSON、HTTP Server、枚举反射或结构体反射实现。它依赖项目内的 `third_party/Structive`,由 Structive 负责 intrinsic property capability、Schema 和 synchronization;Adminive 自身包含 descriptor、JSON 适配协议、AMIS schema、状态协议、managed adapter 和抽象 `Resource_Service`:
|
||||
@@ -358,23 +425,38 @@ Adminive/
|
||||
|
||||
`example.hpp` 位于 `backend/service/src`,只作为完整示例实现,不属于公共库头文件。
|
||||
|
||||
## 列字段名称、显示名称和排序
|
||||
## 字段呈现与表格列是两层
|
||||
|
||||
Field Descriptor 只描述字段自身和单字段呈现:
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
|
||||
.creatable()
|
||||
.editable()
|
||||
.required()
|
||||
.list_label("Mode")
|
||||
.order(5)
|
||||
.sortable();
|
||||
.select_input();
|
||||
```
|
||||
|
||||
- `name` 来自成员名称,用于 JSON 字段和接口参数。
|
||||
- `label` 用于表单标签。
|
||||
- `list_label` 用于列表表头,未设置时回退到 `label`。
|
||||
- `order` 控制列表列顺序,未设置时按字段声明顺序生成默认值。
|
||||
- `sortable` 控制该列能否排序,后端只接受已声明为可排序的字段。
|
||||
表格列属于 `Table_View`,列顺序就是 `table_view()` 中的声明顺序,排序、搜索、筛选和固定列能力也在这里声明:
|
||||
|
||||
```cpp
|
||||
template <>
|
||||
struct adminive::Type_View_Descriptor<Radio_State> {
|
||||
static adminive::Table_View table() {
|
||||
using T = Radio_State;
|
||||
return adminive::table_view<T>(
|
||||
adminive::column<&T::mode>("Mode").sort().filter(),
|
||||
adminive::column<&T::port>("Port").sort().search().fix(adminive::Table_Fixed::left),
|
||||
adminive::column<&T::buffer_count>("Buffers").sort()
|
||||
).default_sort("buffer_count");
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
- Field Descriptor 的 `name` 是稳定的数据字段名。
|
||||
- `Field_Presentation::label/control/options` 描述一个字段单独出现时如何呈现。
|
||||
- `Table_View` 决定哪些字段进入表格、列顺序、列标题、固定位置以及可排序/搜索/筛选能力。
|
||||
- Collection Service 只接受 `Table_View` 已声明的查询字段,因此 View Schema 同时是前端语义和后端能力契约。
|
||||
|
||||
## 枚举列表项
|
||||
|
||||
@@ -395,16 +477,14 @@ struct Radio_State {
|
||||
};
|
||||
```
|
||||
|
||||
描述中不需要手写选项:
|
||||
描述中不需要手写枚举值集合,字段只声明自己的呈现;表格能力由 `Table_View` 另行决定:
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
|
||||
.creatable()
|
||||
.editable()
|
||||
.required()
|
||||
.list_label("Mode")
|
||||
.order(5)
|
||||
.sortable();
|
||||
.select_input();
|
||||
```
|
||||
|
||||
示例通过可选的 `magic_enum` 桥接生成;核心只依赖 `Enum_Adapter`:
|
||||
@@ -413,12 +493,16 @@ ADMINIVE_FIELD_LABEL(T, mode, "Operating Mode")
|
||||
{
|
||||
"name": "mode",
|
||||
"value_type": "enum",
|
||||
"options": [
|
||||
{"label": "Receive", "value": "receive"},
|
||||
{"label": "Transmit", "value": "transmit"},
|
||||
{"label": "Duplex", "value": "duplex"},
|
||||
{"label": "Maintenance", "value": "maintenance"}
|
||||
]
|
||||
"presentation": {
|
||||
"label": "Operating Mode",
|
||||
"control": "select",
|
||||
"options": [
|
||||
{"label": "Receive", "value": "receive"},
|
||||
{"label": "Transmit", "value": "transmit"},
|
||||
{"label": "Duplex", "value": "duplex"},
|
||||
{"label": "Maintenance", "value": "maintenance"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -525,11 +609,11 @@ enum class -> select
|
||||
ADMINIVE_FIELD_LABEL(T, effective_date, "Effective Date")
|
||||
.editable()
|
||||
.required()
|
||||
.widget("input-date");
|
||||
.date_input();
|
||||
ADMINIVE_FIELD_LABEL(T, accent_color, "Accent Color")
|
||||
.editable()
|
||||
.required()
|
||||
.widget("input-color");
|
||||
.color_input();
|
||||
```
|
||||
|
||||
日期控件自动补充:
|
||||
@@ -610,6 +694,7 @@ resource.register_status<&Radio_State::status>(2000);
|
||||
GET /admin/amis
|
||||
GET /admin/status
|
||||
GET /admin/radio_states/descriptor
|
||||
GET /admin/radio_states/view
|
||||
GET /admin/radio_states/status/descriptor
|
||||
GET /admin/radio_states/amis
|
||||
GET /admin/radio_states
|
||||
@@ -620,10 +705,12 @@ PUT /admin/radio_states/{id}
|
||||
PATCH /admin/radio_states/{id}
|
||||
DELETE /admin/radio_states/{id}
|
||||
GET /admin/config/radio/descriptor
|
||||
GET /admin/config/radio/view
|
||||
GET /admin/config/radio/data
|
||||
GET /admin/config/radio/amis
|
||||
POST /admin/config/radio/data
|
||||
GET /admin/config/alerts/descriptor
|
||||
GET /admin/config/alerts/view
|
||||
GET /admin/config/alerts/data
|
||||
GET /admin/config/alerts/amis
|
||||
POST /admin/config/alerts/data
|
||||
@@ -631,6 +718,8 @@ POST /admin/config/alerts/data
|
||||
|
||||
## 前端
|
||||
|
||||
默认前端仍然只需要读取 `/admin/amis` 并渲染最终页面;`/descriptor`、`/view` 和 `/data` 是稳定的后端语义接口,供自定义前端、调试器或其他 Adapter 直接消费。业务字段名单、表格列和组合关系都不在前端硬编码。
|
||||
|
||||
创建外部 `node_modules` 相对符号链接:
|
||||
|
||||
```powershell
|
||||
|
||||
+248
-82
@@ -6,14 +6,23 @@
|
||||
|
||||
Adminive 的目标不是重新发明对象模型、反射系统或属性系统,而是把后端已经存在的 C++ 结构投影成后台管理能力。
|
||||
|
||||
可以把两层职责概括为:
|
||||
可以把职责分成结构层、字段呈现层、视图组合层和最终渲染层:
|
||||
|
||||
```text
|
||||
Structive
|
||||
Structive / Object Schema
|
||||
描述并管理“这个 C++ 对象本身是什么”
|
||||
|
||||
Adminive
|
||||
描述“后台管理系统应该怎样展示、编辑、传输和提交这个对象”
|
||||
│
|
||||
▼
|
||||
Field Presentation
|
||||
描述“单个字段应该以什么语义控件呈现”
|
||||
│
|
||||
▼
|
||||
View Schema / Composition
|
||||
描述“字段和业务组件如何组成表单、表格和页面”
|
||||
│
|
||||
▼
|
||||
AMIS Adapter / Frontend
|
||||
把语义结构翻译成具体 UI,并负责纯视觉与响应式细节
|
||||
```
|
||||
|
||||
因此 Adminive 更接近一个 backend-driven administration projection layer,而不是一个新的 Property Core。
|
||||
@@ -27,8 +36,10 @@ C++ business type
|
||||
Adminive descriptor
|
||||
│
|
||||
├── JSON encode / decode
|
||||
├── frontend schema
|
||||
├── AMIS CRUD / form / table
|
||||
├── field presentation
|
||||
├── form / table view schema
|
||||
├── page composition
|
||||
├── AMIS adapter
|
||||
├── HTTP resource service
|
||||
├── validation / error path
|
||||
├── managed update
|
||||
@@ -98,16 +109,17 @@ managed read 走零锁路径
|
||||
Adminive 负责后台管理领域信息和适配:
|
||||
|
||||
```text
|
||||
字段名称与显示名称
|
||||
字段名称与 Field Presentation
|
||||
editable / creatable
|
||||
sensitive
|
||||
required
|
||||
widget
|
||||
visible / visible_on
|
||||
list column metadata
|
||||
排序信息
|
||||
control / visible_on
|
||||
Form View / Table View
|
||||
vertical / horizontal / flow / grid / group / card / tabs
|
||||
表格列、排序、搜索、筛选、固定列与重排语义
|
||||
Composition View / Slot
|
||||
JSON 描述协议
|
||||
AMIS schema
|
||||
AMIS adapter
|
||||
HTTP resource semantics
|
||||
frontend patch
|
||||
Object_Adapter
|
||||
@@ -145,17 +157,22 @@ Adminive 字段同时包含两类信息,但这两类信息不能混为一谈
|
||||
字段 accessor
|
||||
```
|
||||
|
||||
第二类是后台领域元数据:
|
||||
第二类是后台领域信息,但它本身还必须继续分层:
|
||||
|
||||
```text
|
||||
是否允许前端编辑
|
||||
是否允许创建时输入
|
||||
是否在列表中展示
|
||||
是否敏感
|
||||
使用什么 widget
|
||||
显示什么 label
|
||||
Field Presentation
|
||||
label / description / control / options / visible_on
|
||||
|
||||
View Schema
|
||||
字段选择 / 顺序 / group / card / grid / flow / tabs
|
||||
table columns / sort / search / filter / fixed / reorder
|
||||
|
||||
Composition View
|
||||
Form / Table / Status 等完整组件之间的页面组合
|
||||
```
|
||||
|
||||
字段自身的呈现信息不能携带“它在某个表格里排第几列”这种视图语义;同一个 Property 可以同时出现在 Edit View、Detail View 和多个 Table View 中,而不需要重新定义字段本身。
|
||||
|
||||
例如:
|
||||
|
||||
```cpp
|
||||
@@ -181,7 +198,129 @@ managed writable
|
||||
|
||||
这条边界必须长期保持。
|
||||
|
||||
## 5. 默认只读,显式声明可写
|
||||
## 5. 后端 UI 的分层模型
|
||||
|
||||
Adminive 的目标是让后端修改 C++ 结构和后台描述后,前端无需同步维护业务字段清单即可得到新的界面。因此后端必须拥有页面的**语义结构**,但不应该把所有语义都塞进 Field Descriptor,也不应该直接把 AMIS JSON 当成业务描述语言。
|
||||
|
||||
### 5.1 Object Schema:字段是什么
|
||||
|
||||
Object Schema 由 Structive 与 Adminive Descriptor 共同建立,负责字段名、真实 C++ 类型、intrinsic writable、constraint、managed synchronization 等结构事实。这一层不知道表格、Card、Tabs 或 AMIS。
|
||||
|
||||
### 5.2 Field Presentation:一个字段怎么呈现
|
||||
|
||||
`Field_Presentation` 只描述单字段呈现:
|
||||
|
||||
```text
|
||||
label
|
||||
description
|
||||
control
|
||||
visible_on
|
||||
enum labels/options
|
||||
```
|
||||
|
||||
`control` 使用 `text / multiline_text / number / boolean / select / date / color` 等语义类型,而不是把 `input-text`、`input-date` 之类 AMIS 组件名写进结构描述。AMIS Adapter 再负责语义控件到 AMIS 组件的映射。
|
||||
|
||||
### 5.3 View Schema:这些字段怎么组成一个业务视图
|
||||
|
||||
Form View 可以组合:
|
||||
|
||||
```text
|
||||
all_fields
|
||||
field
|
||||
vertical
|
||||
horizontal
|
||||
flow
|
||||
grid
|
||||
group
|
||||
card
|
||||
tabs / tab
|
||||
```
|
||||
|
||||
后端因此仍然可以完整决定“这十几个字段如何拼接”,但组合关系不再污染字段定义。一个类型可以分别拥有 Edit/Create/Detail View。
|
||||
|
||||
Table View 是一等视图,而不是普通 Field Presentation 的附属标记。它负责:
|
||||
|
||||
```text
|
||||
哪些字段成为列
|
||||
列顺序与标题
|
||||
fixed left/right
|
||||
sortable
|
||||
searchable
|
||||
filterable
|
||||
default sort
|
||||
row reorder
|
||||
create/edit form layout
|
||||
```
|
||||
|
||||
这里的查询能力同时约束后端 Collection Service。前端不能因为自己画了一个搜索框,就让一个未声明为 searchable/filterable 的字段获得后端查询能力。
|
||||
|
||||
### 5.4 Composition View:完整业务组件怎么组成页面
|
||||
|
||||
当页面需要组合已经成型的 Form、Table、Status 或其他业务组件时,使用独立 `Composition_View`。它提供:
|
||||
|
||||
```text
|
||||
slot
|
||||
heading
|
||||
vertical
|
||||
horizontal
|
||||
flow
|
||||
grid
|
||||
group
|
||||
card
|
||||
tabs / tab
|
||||
```
|
||||
|
||||
`slot` 只是引用一个已经生成的业务组件。例如“上方配置表单 + 下方设备表格”可以由后端描述为一个 vertical composition,而不需要业务代码直接拼 AMIS `grid/card/tpl` JSON。
|
||||
|
||||
### 5.5 后端与前端的最终边界
|
||||
|
||||
后端负责:
|
||||
|
||||
```text
|
||||
字段存在与否
|
||||
字段 intrinsic capability
|
||||
单字段 presentation
|
||||
字段属于哪个视图
|
||||
字段顺序与逻辑分组
|
||||
form/table/page 的语义组合
|
||||
表格排序、搜索、筛选等后端能力
|
||||
```
|
||||
|
||||
前端负责:
|
||||
|
||||
```text
|
||||
具体 renderer
|
||||
CSS / theme
|
||||
响应式断点
|
||||
最终列宽和像素级间距
|
||||
设备尺寸下的视觉降级
|
||||
```
|
||||
|
||||
因此核心原则是:
|
||||
|
||||
> **后端拥有页面的语义结构,前端拥有页面的视觉实现。**
|
||||
|
||||
这样既满足“删字段、加字段、改配置只改后端”,又避免后端被某个具体前端框架的 CSS/Grid 实现锁死。
|
||||
|
||||
### 5.6 AMIS 只是 Adapter
|
||||
|
||||
依赖方向必须保持:
|
||||
|
||||
```text
|
||||
Object Schema
|
||||
↓
|
||||
Field Presentation
|
||||
↓
|
||||
View Schema / Composition
|
||||
↓
|
||||
AMIS Adapter
|
||||
↓
|
||||
AMIS JSON
|
||||
```
|
||||
|
||||
不能反过来让 `Field_Descriptor` 直接保存任意 AMIS JSON;否则 Adminive 会从 backend-driven schema 退化成“在 C++ 里写前端 JSON”。
|
||||
|
||||
## 6. 默认只读,显式声明可写
|
||||
|
||||
Adminive 字段默认是 intrinsic read-only。
|
||||
|
||||
@@ -215,9 +354,9 @@ Adminive 字段默认是 intrinsic read-only。
|
||||
|
||||
同步、事务和适配器可以据此建立正确行为。
|
||||
|
||||
## 6. 四种字段声明语义
|
||||
## 7. 四种字段声明语义
|
||||
|
||||
### 6.1 普通字段
|
||||
### 7.1 普通字段
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD(Config, id)
|
||||
@@ -233,7 +372,7 @@ intrinsic read-only
|
||||
|
||||
它仍然是普通 C++ 成员。直接修改 raw member 属于 raw C++ path,不属于 Adminive/Structive managed contract。
|
||||
|
||||
### 6.2 `.editable()`
|
||||
### 7.2 `.editable()`
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD(Config, worker_count).editable()
|
||||
@@ -249,7 +388,7 @@ frontend update editable
|
||||
|
||||
这是后台表单最常见的可编辑字段。
|
||||
|
||||
### 6.3 `.creatable()`
|
||||
### 7.3 `.creatable()`
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD(Device, address).creatable()
|
||||
@@ -265,7 +404,7 @@ intrinsic read-write
|
||||
|
||||
它表达创建流程的领域能力,不等同于普通 update editable。
|
||||
|
||||
### 6.4 `.read_write()`
|
||||
### 7.4 `.read_write()`
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD(Config, backend_state).read_write()
|
||||
@@ -281,7 +420,7 @@ intrinsic read-write
|
||||
|
||||
适合配置加载、后台代码、服务内部更新等受控路径。
|
||||
|
||||
### 6.5 `.unsynchronized()`
|
||||
### 7.5 `.unsynchronized()`
|
||||
|
||||
```cpp
|
||||
ADMINIVE_FIELD(Config, atomic_counter).read_write().unsynchronized()
|
||||
@@ -305,7 +444,7 @@ intrinsic writable
|
||||
|
||||
`.unsynchronized()` 不是性能开关,更不是“我觉得这里应该没事”。它是对同步责任的显式转移。
|
||||
|
||||
## 7. Adminive 不做访问控制系统
|
||||
## 8. Adminive 不做访问控制系统
|
||||
|
||||
Adminive 中的 `editable`、`readable`、`sensitive` 等信息服务于具体后台适配语义,但它们不构成一个通用安全边界。
|
||||
|
||||
@@ -343,7 +482,7 @@ persistence
|
||||
|
||||
`sensitive()` 可以指导 Adminive JSON/HTTP 等适配器避免泄漏敏感内容,但不应被误解为 C++ 内存级权限机制。
|
||||
|
||||
## 8. Managed path 与 raw C++ path
|
||||
## 9. Managed path 与 raw C++ path
|
||||
|
||||
Adminive 不试图取代普通 C++ 对象。
|
||||
|
||||
@@ -380,7 +519,7 @@ managed path 才承诺:
|
||||
|
||||
> 增强原生 C++ 结构,而不是建立一堵无法绕过的对象墙。
|
||||
|
||||
## 9. 为什么 Managed 回调不能泄漏引用
|
||||
## 10. 为什么 Managed 回调不能泄漏引用
|
||||
|
||||
Managed read/write 的锁只在回调作用域中有效。
|
||||
|
||||
@@ -412,7 +551,7 @@ ranges view
|
||||
|
||||
这不是 API 限制,而是锁生命周期本身决定的安全边界。
|
||||
|
||||
## 10. 同步模型:只保护 mutable consistency
|
||||
## 11. 同步模型:只保护 mutable consistency
|
||||
|
||||
Structive synchronization 只解决一个问题:
|
||||
|
||||
@@ -435,7 +574,7 @@ HTTP 权限
|
||||
|
||||
这种选择应该由一致性需求驱动,而不是为了“简单”把所有对象默认放到一把全局锁下。
|
||||
|
||||
## 11. 嵌套结构的同步原则
|
||||
## 12. 嵌套结构的同步原则
|
||||
|
||||
嵌套结构不能简单采用“父对象只要有一个 writable 子字段,所有后代访问都锁父对象”的粗粒度规则。
|
||||
|
||||
@@ -456,7 +595,7 @@ unsynchronized writable leaf
|
||||
|
||||
同步边界来自真实 mutation possibility,而不是单纯来自对象树层级。
|
||||
|
||||
## 12. Resource_Service 的职责
|
||||
## 13. Resource_Service 的职责
|
||||
|
||||
`Resource_Service` 是 Adminive 的应用事务编排层,不是新的 Property Core。
|
||||
|
||||
@@ -477,11 +616,11 @@ commit external side effect
|
||||
|
||||
`Resource_Service` 不应该再创建一棵重复的 per-field mutex 系统。
|
||||
|
||||
## 13. 三种“事务”不能混为一谈
|
||||
## 14. 三种“事务”不能混为一谈
|
||||
|
||||
Adminive 中至少存在三类不同一致性问题:
|
||||
|
||||
### 13.1 内存对象同步
|
||||
### 14.1 内存对象同步
|
||||
|
||||
由 Structive synchronization 负责。
|
||||
|
||||
@@ -491,7 +630,7 @@ lock slot
|
||||
synchronization topology
|
||||
```
|
||||
|
||||
### 13.2 Adminive 外部副作用事务
|
||||
### 14.2 Adminive 外部副作用事务
|
||||
|
||||
由 `Resource_Transaction<Model>` 负责。
|
||||
|
||||
@@ -503,7 +642,7 @@ rollback
|
||||
|
||||
它可以代表配置文件、设备、远端服务等外部动作。
|
||||
|
||||
### 13.3 数据库事务
|
||||
### 14.3 数据库事务
|
||||
|
||||
由数据库自身负责。
|
||||
|
||||
@@ -511,7 +650,7 @@ rollback
|
||||
|
||||
三者可以在一次业务流程中协作,但职责必须保持独立。
|
||||
|
||||
## 14. Adapter First
|
||||
## 15. Adapter First
|
||||
|
||||
Adminive Core 不应直接绑定具体第三方库。
|
||||
|
||||
@@ -538,7 +677,7 @@ HTTP adapter
|
||||
|
||||
如果未来接入新的 JSON/HTTP/Reflection 实现,应优先增加 Adapter,而不是修改 Adminive 的中心算法。
|
||||
|
||||
## 15. Object_Adapter:运行时对象与配置模型分离
|
||||
## 16. Object_Adapter:运行时对象与配置模型分离
|
||||
|
||||
不是所有运行时对象都适合作为可复制配置对象。
|
||||
|
||||
@@ -575,7 +714,7 @@ frontend schema
|
||||
|
||||
这比要求所有业务类型变成“为了后台框架而设计的 DTO”更符合低侵入原则。
|
||||
|
||||
## 16. Frontend 应保持被动
|
||||
## 17. Frontend 应保持被动
|
||||
|
||||
Adminive 是 backend-driven 系统,因此前端不应该重新维护:
|
||||
|
||||
@@ -601,7 +740,7 @@ CRUD schema
|
||||
|
||||
前端可以拥有纯 UI 行为,但不应该成为第二份业务 Schema。
|
||||
|
||||
## 17. 编译期与运行期的分工
|
||||
## 18. 编译期与运行期的分工
|
||||
|
||||
Adminive 建议遵守以下规则:
|
||||
|
||||
@@ -626,7 +765,7 @@ Adminive 建议遵守以下规则:
|
||||
更好的优化机会
|
||||
```
|
||||
|
||||
## 18. Metadata 必须具有真实语义
|
||||
## 19. Metadata 必须具有真实语义
|
||||
|
||||
对 Adminive 来说,metadata 不应该只是“最终转成 JSON 的标签集合”。
|
||||
|
||||
@@ -649,13 +788,16 @@ sensitive
|
||||
required
|
||||
→ 改变输入验证
|
||||
|
||||
widget
|
||||
→ 改变前端控件
|
||||
Field Presentation control
|
||||
→ 改变单字段呈现控件
|
||||
|
||||
View Schema
|
||||
→ 改变字段组合、表格查询能力和页面语义结构
|
||||
```
|
||||
|
||||
如果新增 metadata 没有明确消费者,应谨慎加入 Core descriptor,避免逐渐变成无边界的“万能标签袋”。
|
||||
|
||||
## 19. Core 的非目标
|
||||
## 20. Core 的非目标
|
||||
|
||||
Adminive Core 不应该演化成:
|
||||
|
||||
@@ -674,11 +816,11 @@ Structive 的替代品
|
||||
|
||||
保持非目标清晰,是长期保持 API 简洁的重要手段。
|
||||
|
||||
## 20. 新功能应该放在哪里
|
||||
## 21. 新功能应该放在哪里
|
||||
|
||||
新增能力时,可以按下面的判断顺序决定归属。
|
||||
|
||||
### 20.1 它描述 Property 本身吗?
|
||||
### 21.1 它描述 Property 本身吗?
|
||||
|
||||
例如:
|
||||
|
||||
@@ -690,21 +832,22 @@ generic constraint
|
||||
|
||||
优先考虑 Structive。
|
||||
|
||||
### 20.2 它描述后台管理领域吗?
|
||||
### 21.2 它描述后台管理领域吗?
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
editable
|
||||
widget
|
||||
list column
|
||||
AMIS schema
|
||||
Field Presentation
|
||||
Form/Table View
|
||||
Composition View
|
||||
AMIS adapter
|
||||
frontend visible condition
|
||||
```
|
||||
|
||||
属于 Adminive。
|
||||
|
||||
### 20.3 它绑定具体第三方库吗?
|
||||
### 21.3 它绑定具体第三方库吗?
|
||||
|
||||
例如:
|
||||
|
||||
@@ -718,7 +861,7 @@ Boost.PFR
|
||||
|
||||
放 Adapter/bridge 层。
|
||||
|
||||
### 20.4 它属于具体业务项目吗?
|
||||
### 21.4 它属于具体业务项目吗?
|
||||
|
||||
例如:
|
||||
|
||||
@@ -730,7 +873,7 @@ Boost.PFR
|
||||
|
||||
留在业务层,不进入 Adminive Core。
|
||||
|
||||
## 21. API 演进原则
|
||||
## 22. API 演进原则
|
||||
|
||||
后续修改 Adminive API 时建议遵守:
|
||||
|
||||
@@ -745,13 +888,13 @@ Boost.PFR
|
||||
9. 高级能力允许高级 API,但应与普通使用路径分层。
|
||||
10. 对行为语义的修改必须同时补契约测试和文档。
|
||||
|
||||
## 22. 测试原则
|
||||
## 23. 测试原则
|
||||
|
||||
Adminive 的测试不应只验证“JSON 长得对”。
|
||||
|
||||
至少应该覆盖四类契约:
|
||||
至少应该覆盖五类契约:
|
||||
|
||||
### 22.1 Descriptor contract
|
||||
### 23.1 Descriptor contract
|
||||
|
||||
```text
|
||||
字段 metadata
|
||||
@@ -761,7 +904,18 @@ nested descriptor
|
||||
schema validation
|
||||
```
|
||||
|
||||
### 22.2 Managed synchronization contract
|
||||
### 23.2 View contract
|
||||
|
||||
```text
|
||||
Field Presentation 与结构事实分离
|
||||
Form View 的 group/card/grid/flow/tabs 组合
|
||||
Table View 的列选择与顺序
|
||||
sortable/searchable/filterable/fixed 的后端能力约束
|
||||
Composition View 的 slot 与跨组件组合
|
||||
/view JSON 与 AMIS Adapter 的语义一致性
|
||||
```
|
||||
|
||||
### 23.3 Managed synchronization contract
|
||||
|
||||
```text
|
||||
read-only zero-lock
|
||||
@@ -772,7 +926,7 @@ callback lifetime
|
||||
multi-thread blocking relationship
|
||||
```
|
||||
|
||||
### 22.3 Adapter contract
|
||||
### 23.4 Adapter contract
|
||||
|
||||
```text
|
||||
JSON encode/decode
|
||||
@@ -780,9 +934,10 @@ Enum Adapter
|
||||
Reflection Adapter
|
||||
Object Adapter
|
||||
HTTP Adapter
|
||||
AMIS Adapter
|
||||
```
|
||||
|
||||
### 22.4 Transaction contract
|
||||
### 23.5 Transaction contract
|
||||
|
||||
```text
|
||||
prepare failure
|
||||
@@ -795,7 +950,7 @@ Config Store persistence
|
||||
|
||||
这些测试保护的是设计边界,而不仅是当前实现细节。
|
||||
|
||||
## 23. 设计判断清单
|
||||
## 24. 设计判断清单
|
||||
|
||||
在提交一个新设计前,可以快速检查:
|
||||
|
||||
@@ -803,6 +958,7 @@ Config Store persistence
|
||||
[ ] 这是 Structive 已经解决的问题吗?
|
||||
[ ] 这是 Adminive 后台领域真正需要的概念吗?
|
||||
[ ] 它是否把第三方库耦合进 Core?
|
||||
[ ] 它是否把 Field Presentation、View Schema 和 AMIS Adapter 混成一层?
|
||||
[ ] 它是否让 metadata 真正改变行为?
|
||||
[ ] 它是否把访问控制错误塞进 Property Core?
|
||||
[ ] 它是否创建了第二套同步或事务机制?
|
||||
@@ -814,34 +970,44 @@ Config Store persistence
|
||||
|
||||
如果多数问题无法明确回答,通常意味着抽象边界还没有收敛。
|
||||
|
||||
## 24. 最终架构图
|
||||
## 25. 最终架构图
|
||||
|
||||
```text
|
||||
C++ Business Types
|
||||
│
|
||||
▼
|
||||
Adminive Descriptor
|
||||
│
|
||||
┌─────────────────┴─────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
Adminive domain metadata Structive schema
|
||||
editable/widget/sensitive/... capability/synchronization
|
||||
│ │
|
||||
└─────────────────┬─────────────────┘
|
||||
▼
|
||||
Managed object view
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
▼ ▼ ▼
|
||||
JSON Adapter Resource Service AMIS Schema
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
serialization HTTP transaction Frontend
|
||||
Structive / Object Schema
|
||||
type / capability / synchronization
|
||||
│
|
||||
▼
|
||||
Resource_Transaction
|
||||
prepare/commit/rollback
|
||||
Adminive Field Descriptor
|
||||
editable / required / sensitive
|
||||
│
|
||||
▼
|
||||
Field Presentation Layer
|
||||
label / control / options / visible
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
▼ ▼
|
||||
Form / Detail View Table View
|
||||
group/card/grid/flow/tabs columns/sort/search/filter
|
||||
│ │
|
||||
└──────────────┬──────────────┘
|
||||
▼
|
||||
Composition View
|
||||
Form/Table/Status slots + page layout
|
||||
│
|
||||
┌─────────────────┼─────────────────┐
|
||||
▼ ▼ ▼
|
||||
View JSON Resource Service AMIS Adapter
|
||||
│
|
||||
▼
|
||||
Frontend
|
||||
│
|
||||
renderer/theme/responsive layout
|
||||
|
||||
JSON / HTTP / Enum / Reflection / AMIS 都位于消费或 Adapter 层;
|
||||
Structive 和字段结构层不知道具体前端框架。
|
||||
```
|
||||
|
||||
最重要的依赖方向始终是:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -262,6 +262,13 @@ struct Type_Descriptor<adapter_test::Config> {
|
||||
});
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<adapter_test::Config> {
|
||||
static Table_View table() {
|
||||
using T = adapter_test::Config;
|
||||
return table_view<T>(column_index<T, 0>("线程数").sort(), column_index<T, 1>("模式").sort().filter(), column_index<T, 2>("名称").search());
|
||||
}
|
||||
};
|
||||
}
|
||||
int main() {
|
||||
using Json = adapter_test::Mini_Json;
|
||||
@@ -270,7 +277,8 @@ int main() {
|
||||
const auto& fields = adminive::Json_Adapter<Json>::at(descriptor, "fields");
|
||||
assert((adminive::json_get<Json, std::string>(adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 0), "name")) == "worker_count"));
|
||||
assert((adminive::json_get<Json, int>(adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 0), "default")) == 4));
|
||||
const auto& options = adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 1), "options");
|
||||
const auto& presentation = adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(fields, 1), "presentation");
|
||||
const auto& options = adminive::Json_Adapter<Json>::at(presentation, "options");
|
||||
assert((adminive::json_get<Json, std::string>(adminive::Json_Adapter<Json>::at(adminive::Json_Adapter<Json>::at(options, 0), "label")) == "主用"));
|
||||
Config config;
|
||||
Json patch = adminive::json_object<Json>();
|
||||
|
||||
@@ -68,6 +68,9 @@ if(BUILD_TESTING)
|
||||
add_executable(Adminive_Managed_Test "${CMAKE_CURRENT_LIST_DIR}/tests/managed_test.cpp")
|
||||
target_link_libraries(Adminive_Managed_Test PRIVATE Adminive::Default)
|
||||
add_test(NAME Adminive_Managed_Test COMMAND Adminive_Managed_Test)
|
||||
add_executable(Adminive_View_Schema_Test "${CMAKE_CURRENT_LIST_DIR}/tests/view_schema_test.cpp")
|
||||
target_link_libraries(Adminive_View_Schema_Test PRIVATE Adminive::Default)
|
||||
add_test(NAME Adminive_View_Schema_Test COMMAND Adminive_View_Schema_Test)
|
||||
add_executable(Adminive_Drogon_Adapter_Test "${CMAKE_CURRENT_LIST_DIR}/tests/drogon_adapter_test.cpp")
|
||||
target_include_directories(Adminive_Drogon_Adapter_Test BEFORE PRIVATE "${CMAKE_CURRENT_LIST_DIR}/tests/fake_drogon")
|
||||
target_link_libraries(Adminive_Drogon_Adapter_Test PRIVATE Adminive::Default)
|
||||
@@ -79,6 +82,7 @@ if(BUILD_TESTING)
|
||||
target_compile_options(Adminive_Safety_Test PRIVATE /permissive-)
|
||||
target_compile_options(Adminive_Config_Store_Test PRIVATE /permissive-)
|
||||
target_compile_options(Adminive_Managed_Test PRIVATE /permissive-)
|
||||
target_compile_options(Adminive_View_Schema_Test PRIVATE /permissive-)
|
||||
endif()
|
||||
endif()
|
||||
install(TARGETS Adminive_Service Adminive_Nlohmann Adminive_MagicEnum Adminive_BoostPfr Adminive_Httplib Adminive_Default EXPORT AdminiveServiceTargets)
|
||||
|
||||
@@ -87,6 +87,9 @@ public:
|
||||
template <class Managed, std::size_t Root_Index, auto... Members>
|
||||
requires (!std::is_const_v<Managed>) && std::same_as<typename Managed_Field<Managed, Root_Index, Members...>::value_type, std::remove_cvref_t<T>>
|
||||
explicit Drogon_Resource(Managed_Field<Managed, Root_Index, Members...> value, std::string path, Transaction transaction = {}) : service_(std::make_shared<Service>(value, std::move(path), std::move(transaction))) {}
|
||||
Json view_schema() const {
|
||||
return service_->view_schema();
|
||||
}
|
||||
Json amis_schema() const {
|
||||
return service_->amis_schema();
|
||||
}
|
||||
@@ -99,6 +102,11 @@ public:
|
||||
return service->descriptor_response();
|
||||
});
|
||||
}, get_constraints);
|
||||
app.registerHandler(service->path() + "/view", [service](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
complete_drogon_request<Json>(callback, [service] {
|
||||
return service->view_response();
|
||||
});
|
||||
}, get_constraints);
|
||||
app.registerHandler(service->path() + "/data", [service](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
complete_drogon_request<Json>(callback, [service] {
|
||||
return service->data_response();
|
||||
@@ -126,7 +134,7 @@ private:
|
||||
std::shared_ptr<Service> service_;
|
||||
};
|
||||
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 Drogon_Collection_Resource {
|
||||
public:
|
||||
using Service = Collection_Service<T, Json, Lock>;
|
||||
@@ -145,6 +153,9 @@ public:
|
||||
Json items_json() const {
|
||||
return service_->items_json();
|
||||
}
|
||||
Json view_schema() const {
|
||||
return service_->view_schema();
|
||||
}
|
||||
Json amis_schema() const {
|
||||
return service_->amis_schema();
|
||||
}
|
||||
@@ -160,6 +171,11 @@ public:
|
||||
return service->descriptor_response();
|
||||
});
|
||||
}, get_constraints);
|
||||
app.registerHandler(service->path() + "/view", [service](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
complete_drogon_request<Json>(callback, [service] {
|
||||
return service->view_response();
|
||||
});
|
||||
}, get_constraints);
|
||||
app.registerHandler(service->path() + "/amis", [service](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
complete_drogon_request<Json>(callback, [service] {
|
||||
return service->amis_response();
|
||||
@@ -183,6 +199,16 @@ public:
|
||||
query.per_page = read_drogon_size_parameter(request, "perPage", 20);
|
||||
query.order_by = request->getParameter("orderBy");
|
||||
query.order_dir = request->getParameter("orderDir");
|
||||
const auto view = describe_table_view<T>();
|
||||
for(const auto& column : view.columns) {
|
||||
if(!column.searchable && !column.filterable) {
|
||||
continue;
|
||||
}
|
||||
const std::string value = request->getParameter(column.field);
|
||||
if(!value.empty()) {
|
||||
query.fields.insert_or_assign(column.field, value);
|
||||
}
|
||||
}
|
||||
complete_drogon_request<Json>(callback, [service, query = std::move(query)]() mutable {
|
||||
return service->list_response(std::move(query));
|
||||
});
|
||||
|
||||
@@ -41,6 +41,9 @@ public:
|
||||
template <class Managed, std::size_t Root_Index, auto... Members>
|
||||
requires (!std::is_const_v<Managed>) && std::same_as<typename Managed_Field<Managed, Root_Index, Members...>::value_type, std::remove_cvref_t<T>>
|
||||
explicit Http_Resource(Managed_Field<Managed, Root_Index, Members...> value, std::string path, Transaction transaction = {}) : service_(std::make_shared<Service>(value, std::move(path), std::move(transaction))) {}
|
||||
Json view_schema() const {
|
||||
return service_->view_schema();
|
||||
}
|
||||
Json amis_schema() const {
|
||||
return service_->amis_schema();
|
||||
}
|
||||
@@ -49,6 +52,9 @@ public:
|
||||
server.Get(service->path() + "/descriptor", [service](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, service->descriptor_response());
|
||||
});
|
||||
server.Get(service->path() + "/view", [service](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, service->view_response());
|
||||
});
|
||||
server.Get(service->path() + "/data", [service](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, service->data_response());
|
||||
});
|
||||
@@ -63,7 +69,7 @@ private:
|
||||
std::shared_ptr<Service> service_;
|
||||
};
|
||||
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 Http_Collection_Resource {
|
||||
public:
|
||||
using Service = Collection_Service<T, Json, Lock>;
|
||||
@@ -82,6 +88,9 @@ public:
|
||||
Json items_json() const {
|
||||
return service_->items_json();
|
||||
}
|
||||
Json view_schema() const {
|
||||
return service_->view_schema();
|
||||
}
|
||||
Json amis_schema() const {
|
||||
return service_->amis_schema();
|
||||
}
|
||||
@@ -90,6 +99,9 @@ public:
|
||||
server.Get(service->path() + "/descriptor", [service](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, service->descriptor_response());
|
||||
});
|
||||
server.Get(service->path() + "/view", [service](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, service->view_response());
|
||||
});
|
||||
server.Get(service->path() + "/amis", [service](const httplib::Request&, httplib::Response& response) {
|
||||
write_http_response(response, service->amis_response());
|
||||
});
|
||||
@@ -109,6 +121,12 @@ public:
|
||||
query.order_by = request.get_param_value("orderBy");
|
||||
query.order_dir = request.has_param("orderDir") ? request.get_param_value("orderDir") : "asc";
|
||||
}
|
||||
const auto view = describe_table_view<T>();
|
||||
for(const auto& column : view.columns) {
|
||||
if((column.searchable || column.filterable) && request.has_param(column.field)) {
|
||||
query.fields.insert_or_assign(column.field, request.get_param_value(column.field));
|
||||
}
|
||||
}
|
||||
write_http_response(response, service->list_response(std::move(query)));
|
||||
});
|
||||
server.Get(item_pattern(service->path()), [service](const httplib::Request& request, httplib::Response& response) {
|
||||
|
||||
@@ -18,8 +18,11 @@ std::vector<Network_Device_Row> make_network_rows() {
|
||||
};
|
||||
}
|
||||
Json make_table_page(std::string_view title, std::string_view color, Json config_form, Json table_page) {
|
||||
Json heading{{"type", "tpl"}, {"tpl", "<h3 style=\"margin:0;color:" + std::string(color) + "\">" + std::string(title) + "</h3>"}};
|
||||
return Json{{"type", "container"}, {"body", Json::array({std::move(heading), std::move(config_form), std::move(table_page.at("body"))})}};
|
||||
const auto view = composition_view("device_table", compose::vertical(compose::heading(std::string(title)).accent(std::string(color)), compose::slot("config"), compose::slot("table")));
|
||||
std::map<std::string, Json> slots;
|
||||
slots.insert_or_assign("config", std::move(config_form));
|
||||
slots.insert_or_assign("table", std::move(table_page.at("body")));
|
||||
return to_amis_composition_schema<Json>(view, slots);
|
||||
}
|
||||
}
|
||||
Serial_Device_Table::Serial_Device_Table(Config_Store& store) : store_(store), config_resource_(store.serial_table(), "/admin/device_tables/serial/config", store.serial_table_transaction()), rows_("/admin/device_tables/serial/items", make_serial_rows()) {}
|
||||
@@ -34,7 +37,7 @@ std::string_view Serial_Device_Table::type_label() const noexcept {
|
||||
}
|
||||
Json Serial_Device_Table::amis_schema() const {
|
||||
const Serial_Table_Config config = store_.serial_table().snapshot();
|
||||
return make_table_page(config.group_name, config.accent_color, to_amis_form_schema<Json>(config, "/admin/device_tables/serial/config/data", "确认修改"), rows_.amis_schema());
|
||||
return make_table_page(config.group_name, config.accent_color, to_amis_form_schema<Json>(config, describe_edit_view<Serial_Table_Config>(), "/admin/device_tables/serial/config/data"), rows_.amis_schema());
|
||||
}
|
||||
void Serial_Device_Table::bind(httplib::Server& server) {
|
||||
config_resource_.bind(server);
|
||||
@@ -52,7 +55,7 @@ std::string_view Network_Device_Table::type_label() const noexcept {
|
||||
}
|
||||
Json Network_Device_Table::amis_schema() const {
|
||||
const Network_Table_Config config = store_.network_table().snapshot();
|
||||
return make_table_page(config.group_name, config.accent_color, to_amis_form_schema<Json>(config, "/admin/device_tables/network/config/data", "确认修改"), rows_.amis_schema());
|
||||
return make_table_page(config.group_name, config.accent_color, to_amis_form_schema<Json>(config, describe_edit_view<Network_Table_Config>(), "/admin/device_tables/network/config/data"), rows_.amis_schema());
|
||||
}
|
||||
void Network_Device_Table::bind(httplib::Server& server) {
|
||||
config_resource_.bind(server);
|
||||
|
||||
@@ -19,138 +19,119 @@ template <>
|
||||
struct Type_Descriptor<example::Endpoint_Config> {
|
||||
static auto get() {
|
||||
using T = example::Endpoint_Config;
|
||||
return object<T>(
|
||||
"endpoint_config",
|
||||
ADMINIVE_FIELD_LABEL(T, enabled, "启用端点").editable().creatable().description("控制当前端点是否参与运行"),
|
||||
ADMINIVE_FIELD_LABEL(T, host, "主机地址").editable().creatable().required().description("字符串输入示例"),
|
||||
ADMINIVE_FIELD_LABEL(T, port, "端口").editable().creatable().required().description("整数输入示例")
|
||||
).label("端点配置");
|
||||
return object<T>("endpoint_config", ADMINIVE_FIELD_LABEL(T, enabled, "启用端点").editable().creatable().description("控制当前端点是否参与运行"), ADMINIVE_FIELD_LABEL(T, host, "主机地址").editable().creatable().required().description("字符串输入示例"), ADMINIVE_FIELD_LABEL(T, port, "端口").editable().creatable().required().description("整数输入示例")).label("端点配置");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Appearance_Config> {
|
||||
static auto get() {
|
||||
using T = example::Appearance_Config;
|
||||
return object<T>(
|
||||
"appearance_config",
|
||||
ADMINIVE_FIELD_LABEL(T, panel_title, "面板标题").editable().creatable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, effective_date, "生效日期").editable().creatable().required().widget("input-date"),
|
||||
ADMINIVE_FIELD_LABEL(T, accent_color, "主题颜色").editable().creatable().required().widget("input-color")
|
||||
).label("界面配置");
|
||||
return object<T>("appearance_config", ADMINIVE_FIELD_LABEL(T, panel_title, "面板标题").editable().creatable().required(), ADMINIVE_FIELD_LABEL(T, effective_date, "生效日期").editable().creatable().required().date_input(), ADMINIVE_FIELD_LABEL(T, accent_color, "主题颜色").editable().creatable().required().color_input()).label("界面配置");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Radio_Service_Config> {
|
||||
static auto get() {
|
||||
using T = example::Radio_Service_Config;
|
||||
return object<T>(
|
||||
"radio_service_config",
|
||||
ADMINIVE_FIELD_LABEL(T, profile_name, "配置名称").editable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, mode, "运行模式").editable().required().enum_label<example::Radio_Mode::receive>("接收").enum_label<example::Radio_Mode::transmit>("发送").enum_label<example::Radio_Mode::duplex>("双工").enum_label<example::Radio_Mode::maintenance>("维护"),
|
||||
ADMINIVE_FIELD_LABEL(T, worker_count, "工作线程数").editable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, receive_gain, "接收增益").editable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, primary_endpoint, "主端点").editable(),
|
||||
ADMINIVE_FIELD_LABEL(T, backup_endpoint, "备用端点").editable().visible_on("${$self.mode == 'duplex'}"),
|
||||
ADMINIVE_FIELD_LABEL(T, appearance, "界面显示").editable()
|
||||
).label("无线电服务配置").confirm_label("确认修改");
|
||||
return object<T>("radio_service_config", ADMINIVE_FIELD_LABEL(T, profile_name, "配置名称").editable().required(), ADMINIVE_FIELD_LABEL(T, mode, "运行模式").editable().required().enum_label<example::Radio_Mode::receive>("接收").enum_label<example::Radio_Mode::transmit>("发送").enum_label<example::Radio_Mode::duplex>("双工").enum_label<example::Radio_Mode::maintenance>("维护"), ADMINIVE_FIELD_LABEL(T, worker_count, "工作线程数").editable().required(), ADMINIVE_FIELD_LABEL(T, receive_gain, "接收增益").editable().required(), ADMINIVE_FIELD_LABEL(T, primary_endpoint, "主端点").editable(), ADMINIVE_FIELD_LABEL(T, backup_endpoint, "备用端点").editable().visible_on("${$self.mode == 'duplex'}"), ADMINIVE_FIELD_LABEL(T, appearance, "界面显示").editable()).label("无线电服务配置");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<example::Radio_Service_Config> {
|
||||
static Form_View edit() {
|
||||
using T = example::Radio_Service_Config;
|
||||
return edit_form<T>(tabs(tab("基础", group("运行参数", use<&T::profile_name>(), use<&T::mode>(), horizontal(use<&T::worker_count>(), use<&T::receive_gain>()))), tab("端点", horizontal(card("主端点", use<&T::primary_endpoint>()), card("备用端点", use<&T::backup_endpoint>()))), tab("界面", card("显示设置", use<&T::appearance>())))).submit("确认修改");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Alert_Config> {
|
||||
static auto get() {
|
||||
using T = example::Alert_Config;
|
||||
return object<T>(
|
||||
"alert_config",
|
||||
ADMINIVE_FIELD_LABEL(T, enabled, "启用告警").editable(),
|
||||
ADMINIVE_FIELD_LABEL(T, rule_name, "规则名称").editable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, channel, "发送通道").editable().required().enum_label<example::Alert_Channel::dashboard>("控制台").enum_label<example::Alert_Channel::email>("邮件").enum_label<example::Alert_Channel::webhook>("Webhook"),
|
||||
ADMINIVE_FIELD_LABEL(T, minimum_level, "最低日志级别").editable().required().enum_label<example::Log_Level::trace>("跟踪").enum_label<example::Log_Level::debug>("调试").enum_label<example::Log_Level::info>("信息").enum_label<example::Log_Level::warning>("警告").enum_label<example::Log_Level::error>("错误"),
|
||||
ADMINIVE_FIELD_LABEL(T, repeat_minutes, "重复间隔(分钟)").editable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, effective_date, "生效日期").editable().required().widget("input-date"),
|
||||
ADMINIVE_FIELD_LABEL(T, highlight_color, "高亮颜色").editable().required().widget("input-color"),
|
||||
ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook 端点").editable().visible_on("${$self.enabled && $self.channel == 'webhook'}")
|
||||
).label("告警配置").confirm_label("确认修改");
|
||||
return object<T>("alert_config", ADMINIVE_FIELD_LABEL(T, enabled, "启用告警").editable(), ADMINIVE_FIELD_LABEL(T, rule_name, "规则名称").editable().required(), ADMINIVE_FIELD_LABEL(T, channel, "发送通道").editable().required().enum_label<example::Alert_Channel::dashboard>("控制台").enum_label<example::Alert_Channel::email>("邮件").enum_label<example::Alert_Channel::webhook>("Webhook"), ADMINIVE_FIELD_LABEL(T, minimum_level, "最低日志级别").editable().required().enum_label<example::Log_Level::trace>("跟踪").enum_label<example::Log_Level::debug>("调试").enum_label<example::Log_Level::info>("信息").enum_label<example::Log_Level::warning>("警告").enum_label<example::Log_Level::error>("错误"), ADMINIVE_FIELD_LABEL(T, repeat_minutes, "重复间隔(分钟)").editable().required(), ADMINIVE_FIELD_LABEL(T, effective_date, "生效日期").editable().required().date_input(), ADMINIVE_FIELD_LABEL(T, highlight_color, "高亮颜色").editable().required().color_input(), ADMINIVE_FIELD_LABEL(T, webhook_endpoint, "Webhook 端点").editable().visible_on("${$self.enabled && $self.channel == 'webhook'}")).label("告警配置");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<example::Alert_Config> {
|
||||
static Form_View edit() {
|
||||
using T = example::Alert_Config;
|
||||
return edit_form<T>(vertical(group("告警规则", horizontal(use<&T::enabled>(), use<&T::rule_name>()), grid(3, use<&T::channel>(), use<&T::minimum_level>(), use<&T::repeat_minutes>())), flow(card("生效日期", use<&T::effective_date>()), card("高亮颜色", use<&T::highlight_color>())), group("Webhook", use<&T::webhook_endpoint>()).collapsible_when())).submit("确认修改");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Serial_Table_Config> {
|
||||
static auto get() {
|
||||
using T = example::Serial_Table_Config;
|
||||
return object<T>(
|
||||
"serial_table_config",
|
||||
ADMINIVE_FIELD_LABEL(T, group_name, "分组名称").editable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, default_baud_rate, "默认波特率").editable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, default_parity, "默认校验位").editable().required().enum_label<example::Serial_Parity::none>("无校验").enum_label<example::Serial_Parity::odd>("奇校验").enum_label<example::Serial_Parity::even>("偶校验"),
|
||||
ADMINIVE_FIELD_LABEL(T, accent_color, "界面颜色").editable().required().widget("input-color")
|
||||
).label("串口表格配置").confirm_label("确认修改");
|
||||
return object<T>("serial_table_config", ADMINIVE_FIELD_LABEL(T, group_name, "分组名称").editable().required(), ADMINIVE_FIELD_LABEL(T, default_baud_rate, "默认波特率").editable().required(), ADMINIVE_FIELD_LABEL(T, default_parity, "默认校验位").editable().required().enum_label<example::Serial_Parity::none>("无校验").enum_label<example::Serial_Parity::odd>("奇校验").enum_label<example::Serial_Parity::even>("偶校验"), ADMINIVE_FIELD_LABEL(T, accent_color, "界面颜色").editable().required().color_input()).label("串口表格配置");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<example::Serial_Table_Config> {
|
||||
static Form_View edit() {
|
||||
using T = example::Serial_Table_Config;
|
||||
return edit_form<T>(horizontal(group("默认参数", use<&T::group_name>(), use<&T::default_baud_rate>(), use<&T::default_parity>()), card("显示", use<&T::accent_color>()))).submit("确认修改");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Network_Table_Config> {
|
||||
static auto get() {
|
||||
using T = example::Network_Table_Config;
|
||||
return object<T>(
|
||||
"network_table_config",
|
||||
ADMINIVE_FIELD_LABEL(T, group_name, "分组名称").editable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, default_protocol, "默认协议").editable().required().enum_label<example::Network_Protocol::tcp>("TCP").enum_label<example::Network_Protocol::udp>("UDP").enum_label<example::Network_Protocol::websocket>("WebSocket"),
|
||||
ADMINIVE_FIELD_LABEL(T, timeout_ms, "超时时间(毫秒)").editable().required(),
|
||||
ADMINIVE_FIELD_LABEL(T, tls_enabled, "默认启用 TLS").editable(),
|
||||
ADMINIVE_FIELD_LABEL(T, accent_color, "界面颜色").editable().required().widget("input-color")
|
||||
).label("网络表格配置").confirm_label("确认修改");
|
||||
return object<T>("network_table_config", ADMINIVE_FIELD_LABEL(T, group_name, "分组名称").editable().required(), ADMINIVE_FIELD_LABEL(T, default_protocol, "默认协议").editable().required().enum_label<example::Network_Protocol::tcp>("TCP").enum_label<example::Network_Protocol::udp>("UDP").enum_label<example::Network_Protocol::websocket>("WebSocket"), ADMINIVE_FIELD_LABEL(T, timeout_ms, "超时时间(毫秒)").editable().required(), ADMINIVE_FIELD_LABEL(T, tls_enabled, "默认启用 TLS").editable(), ADMINIVE_FIELD_LABEL(T, accent_color, "界面颜色").editable().required().color_input()).label("网络表格配置");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<example::Network_Table_Config> {
|
||||
static Form_View edit() {
|
||||
using T = example::Network_Table_Config;
|
||||
return edit_form<T>(grid(2, group("网络默认值", use<&T::group_name>(), use<&T::default_protocol>(), use<&T::timeout_ms>(), use<&T::tls_enabled>()), card("显示", use<&T::accent_color>()))).submit("确认修改");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Application_Config> {
|
||||
static auto get() {
|
||||
using T = example::Application_Config;
|
||||
return object<T>(
|
||||
"application_config",
|
||||
ADMINIVE_FIELD_LABEL(T, default_device_type, "默认设备类型").read_write(),
|
||||
ADMINIVE_FIELD_LABEL(T, radio_service, "无线电服务").read_write(),
|
||||
ADMINIVE_FIELD_LABEL(T, alerts, "告警").read_write(),
|
||||
ADMINIVE_FIELD_LABEL(T, serial_table, "串口表格").read_write(),
|
||||
ADMINIVE_FIELD_LABEL(T, network_table, "网络表格").read_write()
|
||||
).label("Adminive 配置");
|
||||
return object<T>("application_config", ADMINIVE_FIELD_LABEL(T, default_device_type, "默认设备类型").read_write(), ADMINIVE_FIELD_LABEL(T, radio_service, "无线电服务").read_write(), ADMINIVE_FIELD_LABEL(T, alerts, "告警").read_write(), ADMINIVE_FIELD_LABEL(T, serial_table, "串口表格").read_write(), ADMINIVE_FIELD_LABEL(T, network_table, "网络表格").read_write()).label("Adminive 配置");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Radio_State> {
|
||||
static auto get() {
|
||||
using T = example::Radio_State;
|
||||
return object<T>(
|
||||
"radio_state",
|
||||
ADMINIVE_FIELD_LABEL(T, mode, "运行模式").creatable().editable().required().list_label("模式").order(5).sortable().enum_label<example::Radio_Mode::receive>("接收").enum_label<example::Radio_Mode::transmit>("发送").enum_label<example::Radio_Mode::duplex>("双工").enum_label<example::Radio_Mode::maintenance>("维护"),
|
||||
ADMINIVE_FIELD_LABEL(T, port, "监听端口").creatable().editable().required().list_label("监听端口").order(20).sortable(),
|
||||
ADMINIVE_FIELD_LABEL(T, buffer_count, "缓冲区数量").creatable().editable().required().list_label("缓冲区").order(10).sortable(),
|
||||
ADMINIVE_FIELD_LABEL(T, low_watermark, "低水位").creatable().editable().list_label("低水位").order(30).sortable(),
|
||||
ADMINIVE_FIELD_LABEL(T, high_watermark, "高水位").creatable().editable().list_label("高水位").order(40).sortable()
|
||||
).label("无线电状态").validator(T::Watermark_Validator{}).id_label("编号").create_label("新增").edit_label("编辑").delete_label("删除").actions_label("操作").confirm_label("确认修改").delete_confirm("确定删除这条无线电状态吗?").view_status_label("查看状态 ▾").management_label("无线电状态列表").default_sort("buffer_count");
|
||||
return object<T>("radio_state", ADMINIVE_FIELD_LABEL(T, mode, "运行模式").creatable().editable().required().enum_label<example::Radio_Mode::receive>("接收").enum_label<example::Radio_Mode::transmit>("发送").enum_label<example::Radio_Mode::duplex>("双工").enum_label<example::Radio_Mode::maintenance>("维护"), ADMINIVE_FIELD_LABEL(T, port, "监听端口").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, buffer_count, "缓冲区数量").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, low_watermark, "低水位").creatable().editable(), ADMINIVE_FIELD_LABEL(T, high_watermark, "高水位").creatable().editable()).label("无线电状态").validator(T::Watermark_Validator{});
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<example::Radio_State> {
|
||||
static Table_View table() {
|
||||
using T = example::Radio_State;
|
||||
return table_view<T>(column<&T::mode>("模式").sort().filter(), column<&T::buffer_count>("缓冲区").sort(), column<&T::port>("监听端口").sort().search(), column<&T::low_watermark>("低水位").sort(), column<&T::high_watermark>("高水位").sort()).titled("无线电状态列表").id_title("编号").create_title("新增").edit_title("编辑").delete_title("删除").actions_title("操作").confirm_title("确认修改").delete_confirmation("确定删除这条无线电状态吗?").status_title("查看状态 ▾").default_sort("buffer_count");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Serial_Device_Row> {
|
||||
static auto get() {
|
||||
using T = example::Serial_Device_Row;
|
||||
return object<T>(
|
||||
"serial_device",
|
||||
ADMINIVE_FIELD_LABEL(T, port_name, "串口名称").creatable().editable().required().list_label("串口").order(10).sortable(),
|
||||
ADMINIVE_FIELD_LABEL(T, baud_rate, "波特率").creatable().editable().required().list_label("波特率").order(20).sortable(),
|
||||
ADMINIVE_FIELD_LABEL(T, parity, "校验位").creatable().editable().required().list_label("校验位").order(30).sortable().enum_label<example::Serial_Parity::none>("无校验").enum_label<example::Serial_Parity::odd>("奇校验").enum_label<example::Serial_Parity::even>("偶校验"),
|
||||
ADMINIVE_FIELD_LABEL(T, enabled, "启用").creatable().editable().list_label("启用").order(40).sortable()
|
||||
).label("串口设备").id_label("编号").create_label("新增串口").edit_label("编辑").delete_label("删除").actions_label("操作").confirm_label("确认修改").delete_confirm("确定删除这个串口吗?").management_label("串口设备列表").user_reorderable();
|
||||
return object<T>("serial_device", ADMINIVE_FIELD_LABEL(T, port_name, "串口名称").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, baud_rate, "波特率").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, parity, "校验位").creatable().editable().required().enum_label<example::Serial_Parity::none>("无校验").enum_label<example::Serial_Parity::odd>("奇校验").enum_label<example::Serial_Parity::even>("偶校验"), ADMINIVE_FIELD_LABEL(T, enabled, "启用").creatable().editable()).label("串口设备");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<example::Serial_Device_Row> {
|
||||
static Table_View table() {
|
||||
using T = example::Serial_Device_Row;
|
||||
return table_view<T>(column<&T::port_name>("串口").sort().search().fix(Table_Fixed::left), column<&T::baud_rate>("波特率").sort(), column<&T::parity>("校验位").sort().filter(), column<&T::enabled>("启用").sort().filter()).titled("串口设备列表").id_title("编号").create_title("新增串口").edit_title("编辑").delete_title("删除").actions_title("操作").confirm_title("确认修改").delete_confirmation("确定删除这个串口吗?").reorderable();
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<example::Network_Device_Row> {
|
||||
static auto get() {
|
||||
using T = example::Network_Device_Row;
|
||||
return object<T>(
|
||||
"network_device",
|
||||
ADMINIVE_FIELD_LABEL(T, endpoint, "网络地址").creatable().editable().required().list_label("地址").order(10).sortable(),
|
||||
ADMINIVE_FIELD_LABEL(T, protocol, "协议").creatable().editable().required().list_label("协议").order(20).sortable().enum_label<example::Network_Protocol::tcp>("TCP").enum_label<example::Network_Protocol::udp>("UDP").enum_label<example::Network_Protocol::websocket>("WebSocket"),
|
||||
ADMINIVE_FIELD_LABEL(T, timeout_ms, "超时时间").creatable().editable().required().list_label("超时(毫秒)").order(30).sortable(),
|
||||
ADMINIVE_FIELD_LABEL(T, tls_enabled, "启用 TLS").creatable().editable().list_label("TLS").order(40).sortable()
|
||||
).label("网络设备").id_label("编号").create_label("新增网络设备").edit_label("编辑").delete_label("删除").actions_label("操作").confirm_label("确认修改").delete_confirm("确定删除这个网络设备吗?").management_label("网络设备列表").user_reorderable();
|
||||
return object<T>("network_device", ADMINIVE_FIELD_LABEL(T, endpoint, "网络地址").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, protocol, "协议").creatable().editable().required().enum_label<example::Network_Protocol::tcp>("TCP").enum_label<example::Network_Protocol::udp>("UDP").enum_label<example::Network_Protocol::websocket>("WebSocket"), ADMINIVE_FIELD_LABEL(T, timeout_ms, "超时时间").creatable().editable().required(), ADMINIVE_FIELD_LABEL(T, tls_enabled, "启用 TLS").creatable().editable()).label("网络设备");
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<example::Network_Device_Row> {
|
||||
static Table_View table() {
|
||||
using T = example::Network_Device_Row;
|
||||
return table_view<T>(column<&T::endpoint>("地址").sort().search().fix(Table_Fixed::left), column<&T::protocol>("协议").sort().filter(), column<&T::timeout_ms>("超时(毫秒)").sort(), column<&T::tls_enabled>("TLS").sort().filter()).titled("网络设备列表").id_title("编号").create_title("新增网络设备").edit_title("编辑").delete_title("删除").actions_title("操作").confirm_title("确认修改").delete_confirmation("确定删除这个网络设备吗?").reorderable();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ void describe_example() {
|
||||
std::cout << adminive::to_descriptor_json<Json, Radio_State>().dump(2) << '\n';
|
||||
std::cout << adminive::to_json<Json>(state).dump(2) << '\n';
|
||||
std::cout << adminive::to_descriptor_json<Json, Radio_Service_Config>().dump(2) << '\n';
|
||||
std::cout << adminive::to_amis_form_schema<Json>(service, "/admin/config/radio/data", "确认修改").dump(2) << '\n';
|
||||
std::cout << adminive::to_amis_form_schema<Json>(service, adminive::describe_edit_view<Radio_Service_Config>(), "/admin/config/radio/data").dump(2) << '\n';
|
||||
std::cout << adminive::to_descriptor_json<Json, Alert_Config>().dump(2) << '\n';
|
||||
std::cout << adminive::to_amis_form_schema<Json>(alerts, "/admin/config/alerts/data", "确认修改").dump(2) << '\n';
|
||||
std::cout << adminive::to_amis_form_schema<Json>(alerts, adminive::describe_edit_view<Alert_Config>(), "/admin/config/alerts/data").dump(2) << '\n';
|
||||
}
|
||||
}
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -134,12 +134,12 @@ struct Control_Adapter<advanced_test::Color_Code, advanced_test::Json> {
|
||||
advanced_test::Json result;
|
||||
result["type"] = "input-color";
|
||||
result["name"] = make_field_name(context.prefix, field.at("name").get<std::string>());
|
||||
result["label"] = field.at("label");
|
||||
result["label"] = field.at("presentation").at("label");
|
||||
result["clearable"] = true;
|
||||
return result;
|
||||
}
|
||||
static advanced_test::Json make_column(const advanced_test::Json& field) {
|
||||
return advanced_test::Json{{"type", "tpl"}, {"name", field.at("name")}, {"label", field.at("list_label")}, {"tpl", "<span style=\"color:${color}\">${color}</span>"}};
|
||||
return advanced_test::Json{{"type", "tpl"}, {"name", field.at("name")}, {"label", field.at("presentation").at("label")}, {"tpl", "<span style=\"color:${color}\">${color}</span>"}};
|
||||
}
|
||||
};
|
||||
template <>
|
||||
@@ -171,7 +171,7 @@ template <>
|
||||
struct Type_Descriptor<advanced_test::Control_Config> {
|
||||
static auto get() {
|
||||
using T = advanced_test::Control_Config;
|
||||
return object<T>("control", "Control", ADMINIVE_FIELD(T, color).editable().list_label("颜色"));
|
||||
return object<T>("control", "Control", ADMINIVE_FIELD(T, color).editable().label("颜色"));
|
||||
}
|
||||
};
|
||||
template <>
|
||||
@@ -255,7 +255,14 @@ template <>
|
||||
struct Type_Descriptor<advanced_test::Invalid_Sort_Config> {
|
||||
static auto get() {
|
||||
using T = advanced_test::Invalid_Sort_Config;
|
||||
return object<T>("invalid_sort", "Invalid Sort", ADMINIVE_FIELD(T, value)).default_sort("value");
|
||||
return object<T>("invalid_sort", "Invalid Sort", ADMINIVE_FIELD(T, value));
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<advanced_test::Invalid_Sort_Config> {
|
||||
static Table_View table() {
|
||||
using T = advanced_test::Invalid_Sort_Config;
|
||||
return table_view<T>(column<&T::value>()).default_sort("value");
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -299,10 +306,10 @@ int main() {
|
||||
assert(managed_inherited.field<0>().snapshot() == 12);
|
||||
assert(managed_inherited.field<1>().snapshot() == "changed");
|
||||
Control_Config controls;
|
||||
const auto control_form = adminive::to_amis_form_schema<Json>(controls);
|
||||
const auto control_form = adminive::to_amis_form_schema<Json>(controls, adminive::describe_edit_view<Control_Config>());
|
||||
assert(control_form.at("body").at(0).at("type") == "input-color");
|
||||
Device_Config devices;
|
||||
const auto device_form = adminive::to_amis_form_schema<Json>(devices);
|
||||
const auto device_form = adminive::to_amis_form_schema<Json>(devices, adminive::describe_edit_view<Device_Config>());
|
||||
assert(device_form.at("body").at(0).at("type") == "fieldset");
|
||||
assert(device_form.at("body").at(0).at("body").at(0).at("type") == "select");
|
||||
assert(device_form.at("body").at(0).at("body").at(1).at("body").at(2).at("type") == "input-color");
|
||||
@@ -333,7 +340,7 @@ int main() {
|
||||
assert(duplicate_rejected);
|
||||
bool invalid_sort_rejected{};
|
||||
try {
|
||||
static_cast<void>(adminive::to_descriptor_json<Json, Invalid_Sort_Config>());
|
||||
adminive::validate_table_view<Invalid_Sort_Config>(adminive::describe_table_view<Invalid_Sort_Config>());
|
||||
} catch(const std::invalid_argument&) {
|
||||
invalid_sort_rejected = true;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,13 @@ struct Type_Descriptor<drogon_test::Config> {
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<drogon_test::Config> {
|
||||
static Table_View table() {
|
||||
using T = drogon_test::Config;
|
||||
return table_view<T>(column<&T::name>("Name").sort().search());
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_Descriptor<drogon_test::Status> {
|
||||
static auto get() {
|
||||
using T = drogon_test::Status;
|
||||
@@ -63,9 +70,15 @@ int main() {
|
||||
assert(app.filters("/config/data", drogon::Post).size() == 2);
|
||||
assert(app.filters("/status/data", drogon::Get).size() == 2);
|
||||
auto request = std::make_shared<drogon::HttpRequest>();
|
||||
drogon::HttpResponsePtr response;
|
||||
app.handle("/config/view", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
|
||||
response = value;
|
||||
});
|
||||
assert(response->status == drogon::k200OK);
|
||||
assert(Json::parse(response->body).at("data").at("kind") == "form");
|
||||
request->body = R"({"name":"updated"})";
|
||||
request->user = "wyc";
|
||||
drogon::HttpResponsePtr response;
|
||||
response.reset();
|
||||
app.handle("/config/data", drogon::Post, request, [&response](const drogon::HttpResponsePtr& value) {
|
||||
response = value;
|
||||
});
|
||||
@@ -102,7 +115,14 @@ int main() {
|
||||
assert(response->status == drogon::k200OK);
|
||||
adminive::Drogon_Collection_Resource<drogon_test::Config, Json> collection("/configs", {drogon_test::Config{"first"}});
|
||||
collection.bind(app, options);
|
||||
request->parameters = {{"page", "1"}, {"perPage", "10"}};
|
||||
response.reset();
|
||||
app.handle("/configs/view", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
|
||||
response = value;
|
||||
});
|
||||
assert(response->status == drogon::k200OK);
|
||||
assert(Json::parse(response->body).at("data").at("kind") == "table");
|
||||
assert(Json::parse(response->body).at("data").at("columns").at(0).at("searchable") == true);
|
||||
request->parameters = {{"page", "1"}, {"perPage", "10"}, {"name", "fir"}};
|
||||
response.reset();
|
||||
app.handle("/configs", drogon::Get, request, [&response](const drogon::HttpResponsePtr& value) {
|
||||
response = value;
|
||||
|
||||
@@ -115,7 +115,7 @@ int main() {
|
||||
assert(Probe_Lock::exclusive_locks.load(std::memory_order_relaxed) == 0);
|
||||
assert(Probe_Lock::shared_locks.load(std::memory_order_relaxed) == 0);
|
||||
const auto descriptor = adminive::to_descriptor_json<Json, Section_Config>();
|
||||
assert(descriptor.at("protocol_version") == 4);
|
||||
assert(descriptor.at("protocol_version") == 5);
|
||||
assert(!descriptor.at("fields").at(0).contains("lock_mode"));
|
||||
assert(descriptor.at("fields").at(0).at("writable") == true);
|
||||
assert(descriptor.at("fields").at(0).at("synchronized") == true);
|
||||
|
||||
@@ -276,7 +276,7 @@ int main() {
|
||||
update = adminive::apply_frontend_create<Json>(created, Json{{"child", Json{{"editable_value", 5}}}});
|
||||
assert(!update.success);
|
||||
assert(update.field_errors.contains("child.required_value"));
|
||||
const auto root_form = adminive::to_amis_form_schema<Json>(root);
|
||||
const auto root_form = adminive::to_amis_form_schema<Json>(root, adminive::describe_edit_view<Root_Config>());
|
||||
assert(root_form.at("body").at(0).at("body").at(2).at("visibleOn") == "${child.editable_value > 0}");
|
||||
Numeric_Config numeric;
|
||||
update = adminive::apply_frontend_patch<Json>(numeric, Json{{"signed_value", 1.5}});
|
||||
@@ -286,7 +286,7 @@ int main() {
|
||||
update = adminive::apply_frontend_patch<Json>(numeric, Json{{"signed_value", std::numeric_limits<std::uint64_t>::max()}});
|
||||
assert(!update.success);
|
||||
Optional_Config optional;
|
||||
const auto optional_form = adminive::to_amis_form_schema<Json>(optional);
|
||||
const auto optional_form = adminive::to_amis_form_schema<Json>(optional, adminive::describe_edit_view<Optional_Config>());
|
||||
assert(optional_form.at("body").at(0).at("clearable") == true);
|
||||
assert(optional_form.at("body").at(1).at("clearable") == true);
|
||||
update = adminive::apply_frontend_patch<Json>(optional, Json{{"count", nullptr}, {"note", nullptr}});
|
||||
|
||||
+120
-56
@@ -1,33 +1,91 @@
|
||||
#include "example_descriptors.hpp"
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
namespace {
|
||||
using Json = adminive::example::Json;
|
||||
const Json* find_named_node(const Json& value, std::string_view name) {
|
||||
if(value.is_object()) {
|
||||
if(value.contains("name") && value.at("name").is_string() && value.at("name").get<std::string>() == name) {
|
||||
return &value;
|
||||
}
|
||||
for(const auto& [key, child] : value.items()) {
|
||||
static_cast<void>(key);
|
||||
if(const Json* result = find_named_node(child, name)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if(value.is_array()) {
|
||||
for(const auto& child : value) {
|
||||
if(const Json* result = find_named_node(child, name)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
const Json* find_titled_node(const Json& value, std::string_view title) {
|
||||
if(value.is_object()) {
|
||||
if(value.contains("title") && value.at("title").is_string() && value.at("title").get<std::string>() == title) {
|
||||
return &value;
|
||||
}
|
||||
for(const auto& [key, child] : value.items()) {
|
||||
static_cast<void>(key);
|
||||
if(const Json* result = find_titled_node(child, title)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} else if(value.is_array()) {
|
||||
for(const auto& child : value) {
|
||||
if(const Json* result = find_titled_node(child, title)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
int main() {
|
||||
using adminive::example::Alert_Config;
|
||||
using adminive::example::Radio_Item_Status;
|
||||
using adminive::example::Radio_Mode;
|
||||
using adminive::example::Radio_Service_Config;
|
||||
using adminive::example::Radio_State;
|
||||
using Json = adminive::example::Json;
|
||||
Radio_State state;
|
||||
const auto descriptor = adminive::to_descriptor_json<Json, Radio_State>();
|
||||
assert(descriptor.at("protocol") == "adminive.resource");
|
||||
assert(descriptor.at("protocol_version") == 5);
|
||||
assert(descriptor.at("name") == "radio_state");
|
||||
assert(descriptor.at("fields").size() == 5);
|
||||
assert(descriptor.at("fields").at(0).at("name") == "mode");
|
||||
assert(descriptor.at("fields").at(0).at("label") == "运行模式");
|
||||
assert(descriptor.at("fields").at(0).at("list_label") == "模式");
|
||||
assert(descriptor.at("fields").at(0).at("value_type") == "enum");
|
||||
assert(descriptor.at("fields").at(0).at("options").size() == 4);
|
||||
assert(descriptor.at("fields").at(0).at("options").at(0).at("value") == "receive");
|
||||
assert(descriptor.at("fields").at(0).at("options").at(2).at("value") == "duplex");
|
||||
assert(descriptor.at("fields").at(0).at("order") == 5);
|
||||
assert(descriptor.at("fields").at(0).at("sortable") == true);
|
||||
assert(descriptor.at("fields").at(1).at("name") == "port");
|
||||
assert(descriptor.at("fields").at(1).at("list_label") == "监听端口");
|
||||
assert(descriptor.at("fields").at(1).at("minimum") == 1);
|
||||
assert(descriptor.at("fields").at(1).at("maximum") == 65535);
|
||||
assert(descriptor.at("fields").at(2).at("name") == "buffer_count");
|
||||
const auto& mode_field = descriptor.at("fields").at(0);
|
||||
assert(mode_field.at("name") == "mode");
|
||||
assert(mode_field.at("presentation").at("label") == "运行模式");
|
||||
assert(mode_field.at("value_type") == "enum");
|
||||
assert(mode_field.at("presentation").at("options").size() == 4);
|
||||
assert(mode_field.at("presentation").at("options").at(0).at("value") == "receive");
|
||||
assert(mode_field.at("presentation").at("options").at(2).at("value") == "duplex");
|
||||
const auto& port_field = descriptor.at("fields").at(1);
|
||||
assert(port_field.at("name") == "port");
|
||||
assert(port_field.at("presentation").at("label") == "监听端口");
|
||||
assert(port_field.at("minimum") == 1);
|
||||
assert(port_field.at("maximum") == 65535);
|
||||
assert(descriptor.at("fields").at(2).at("multiple_of") == 2);
|
||||
assert(!descriptor.contains("list"));
|
||||
const auto table_view = adminive::describe_table_view<Radio_State>();
|
||||
assert(table_view.columns.size() == 5);
|
||||
assert(table_view.columns.at(0).field == "mode");
|
||||
assert(table_view.columns.at(0).label == "模式");
|
||||
assert(table_view.columns.at(0).sortable);
|
||||
assert(table_view.columns.at(0).filterable);
|
||||
assert(table_view.columns.at(2).field == "port");
|
||||
assert(table_view.columns.at(2).searchable);
|
||||
assert(table_view.default_order_by == "buffer_count");
|
||||
const auto table_view_json = adminive::to_view_json<Json, Radio_State>(table_view);
|
||||
assert(table_view_json.at("protocol") == "adminive.view");
|
||||
assert(table_view_json.at("kind") == "table");
|
||||
assert(table_view_json.at("columns").at(0).at("field") == "mode");
|
||||
assert(table_view_json.at("columns").at(0).at("filterable") == true);
|
||||
const auto initial = adminive::to_json<Json>(state);
|
||||
assert(initial.at("mode") == "receive");
|
||||
assert(initial.at("port") == 9999);
|
||||
@@ -57,29 +115,27 @@ int main() {
|
||||
assert(!result.success);
|
||||
assert(result.field_errors.contains("mode"));
|
||||
assert(result.field_errors.contains("buffer_count"));
|
||||
const auto form = adminive::to_amis_form_schema<Json>(state, "/admin/radio_state", "Confirm Changes");
|
||||
const auto form_view = adminive::edit_form<Radio_State>().submit("Confirm Changes");
|
||||
const auto form = adminive::to_amis_form_schema<Json>(state, form_view, "/admin/radio_state");
|
||||
assert(form.at("type") == "form");
|
||||
assert(form.at("body").size() == 5);
|
||||
assert(form.at("body").at(0).at("type") == "select");
|
||||
assert(form.at("body").at(0).at("options").size() == 4);
|
||||
assert(form.at("body").at(1).at("type") == "input-number");
|
||||
assert(form.at("body").at(2).at("step") == 2);
|
||||
assert(form.at("actions").size() == 2);
|
||||
assert(form.at("actions").at(0).at("type") == "reset");
|
||||
assert(form.at("actions").at(1).at("type") == "submit");
|
||||
assert(form.at("actions").at(1).at("label") == "Confirm Changes");
|
||||
const auto crud = adminive::to_amis_crud_schema<Json, Radio_State>("/admin/radio_states");
|
||||
const auto crud = adminive::to_amis_table_schema<Json, Radio_State>("/admin/radio_states", table_view);
|
||||
assert(crud.at("type") == "page");
|
||||
assert(crud.at("body").at("type") == "crud");
|
||||
assert(crud.at("body").at("columns").size() == 7);
|
||||
assert(crud.at("body").at("columns").at(1).at("name") == "mode");
|
||||
assert(crud.at("body").at("columns").at(1).at("type") == "mapping");
|
||||
assert(crud.at("body").at("columns").at(1).at("map").at("duplex") == "双工");
|
||||
assert(crud.at("body").at("columns").at(2).at("name") == "buffer_count");
|
||||
assert(crud.at("body").at("columns").at(3).at("name") == "port");
|
||||
assert(!crud.at("body").contains("interval"));
|
||||
const auto& create_form = crud.at("body").at("headerToolbar").at(2).at("dialog").at("body");
|
||||
assert(create_form.at("actions").at(1).at("type") == "submit");
|
||||
const auto& crud_body = crud.at("body").at(0);
|
||||
assert(crud_body.at("type") == "crud");
|
||||
assert(crud_body.at("columns").size() == 7);
|
||||
assert(crud_body.at("columns").at(1).at("name") == "mode");
|
||||
assert(crud_body.at("columns").at(1).at("type") == "mapping");
|
||||
assert(crud_body.at("columns").at(1).at("map").at("duplex") == "双工");
|
||||
assert(crud_body.at("columns").at(1).at("filterable") == true);
|
||||
assert(crud_body.at("columns").at(3).at("name") == "port");
|
||||
assert(crud_body.at("columns").at(3).at("searchable") == true);
|
||||
const auto& create_form = crud_body.at("headerToolbar").at(2).at("dialog").at("body");
|
||||
assert(create_form.at("actions").at(1).at("label") == "新增");
|
||||
const auto status_descriptor = adminive::to_status_descriptor_json<Json, Radio_Item_Status>();
|
||||
assert(status_descriptor.at("protocol") == "adminive.status");
|
||||
@@ -94,7 +150,7 @@ int main() {
|
||||
assert(first_status.at("refresh_sequence").at("value") == 1);
|
||||
assert(second_status.at("refresh_sequence").at("value") == 2);
|
||||
const auto overview_descriptor = adminive::to_status_descriptor_json<Json, adminive::example::Server_Status>();
|
||||
const auto status_crud = adminive::to_amis_crud_status_schema<Json, Radio_State>("/admin/radio_states", "/admin/status", overview_descriptor, 2000, &status_descriptor);
|
||||
const auto status_crud = adminive::to_amis_table_schema<Json, Radio_State>("/admin/radio_states", table_view, &status_descriptor, 2000, &overview_descriptor, "/admin/status", 2000);
|
||||
assert(status_crud.at("body").is_array());
|
||||
assert(status_crud.at("body").at(0).at("type") == "service");
|
||||
assert(status_crud.at("body").at(1).at("columns").size() == 8);
|
||||
@@ -117,38 +173,46 @@ int main() {
|
||||
assert(service_descriptor.at("fields").at(3).at("value_type") == "number");
|
||||
assert(service_descriptor.at("fields").at(4).at("value_type") == "object");
|
||||
assert(service_descriptor.at("fields").at(4).at("children").size() == 3);
|
||||
assert(service_descriptor.at("fields").at(5).at("visible_on") == "${$self.mode == 'duplex'}");
|
||||
assert(service_descriptor.at("fields").at(6).at("children").at(1).at("widget") == "input-date");
|
||||
assert(service_descriptor.at("fields").at(6).at("children").at(2).at("widget") == "input-color");
|
||||
const auto service_form = adminive::to_amis_form_schema<Json>(service_config, "/admin/config/radio/data", "Confirm Changes");
|
||||
assert(service_form.at("body").at(0).at("type") == "input-text");
|
||||
assert(service_form.at("body").at(1).at("type") == "select");
|
||||
assert(service_form.at("body").at(2).at("type") == "input-number");
|
||||
assert(service_form.at("body").at(3).at("type") == "input-number");
|
||||
assert(service_form.at("body").at(4).at("type") == "fieldset");
|
||||
assert(service_form.at("body").at(4).at("body").at(1).at("name") == "primary_endpoint.host");
|
||||
assert(service_form.at("body").at(5).at("visibleOn") == "${mode == 'duplex'}");
|
||||
assert(service_form.at("body").at(6).at("body").at(1).at("type") == "input-date");
|
||||
assert(service_form.at("body").at(6).at("body").at(1).at("valueFormat") == "YYYY-MM-DD");
|
||||
assert(service_form.at("body").at(6).at("body").at(2).at("type") == "input-color");
|
||||
assert(service_form.at("actions").at(1).at("type") == "submit");
|
||||
assert(service_descriptor.at("fields").at(5).at("presentation").at("visible_on") == "${$self.mode == 'duplex'}");
|
||||
assert(service_descriptor.at("fields").at(6).at("children").at(1).at("presentation").at("control") == "date");
|
||||
assert(service_descriptor.at("fields").at(6).at("children").at(2).at("presentation").at("control") == "color");
|
||||
const auto service_view = adminive::describe_edit_view<Radio_Service_Config>();
|
||||
const auto service_view_json = adminive::to_view_json<Json, Radio_Service_Config>(service_view);
|
||||
assert(service_view_json.at("body").at("kind") == "tabs");
|
||||
assert(service_view_json.at("body").at("children").size() == 3);
|
||||
const auto service_form = adminive::to_amis_form_schema<Json>(service_config, service_view, "/admin/config/radio/data");
|
||||
assert(service_form.at("body").at(0).at("type") == "tabs");
|
||||
const Json* profile_control = find_named_node(service_form, "profile_name");
|
||||
const Json* mode_control = find_named_node(service_form, "mode");
|
||||
const Json* worker_control = find_named_node(service_form, "worker_count");
|
||||
const Json* date_control = find_named_node(service_form, "appearance.effective_date");
|
||||
const Json* color_control = find_named_node(service_form, "appearance.accent_color");
|
||||
assert(profile_control && profile_control->at("type") == "input-text");
|
||||
assert(mode_control && mode_control->at("type") == "select");
|
||||
assert(worker_control && worker_control->at("type") == "input-number");
|
||||
assert(date_control && date_control->at("type") == "input-date");
|
||||
assert(color_control && color_control->at("type") == "input-color");
|
||||
assert(service_form.at("actions").at(1).at("label") == "确认修改");
|
||||
result = adminive::apply_frontend_patch<Json>(service_config, Json{{"profile_name", "Duplex Profile"}, {"mode", "duplex"}, {"worker_count", 8}, {"receive_gain", 2.5}, {"primary_endpoint", Json{{"enabled", true}, {"host", "10.0.0.1"}, {"port", 9200}}}, {"backup_endpoint", Json{{"enabled", true}, {"host", "10.0.0.2"}, {"port", 9201}}}, {"appearance", Json{{"panel_title", "Duplex Radio"}, {"effective_date", "2026-08-07"}, {"accent_color", "#16a34a"}}}});
|
||||
assert(result.success);
|
||||
assert(service_config.mode == Radio_Mode::duplex);
|
||||
assert(service_config.primary_endpoint.host == "10.0.0.1");
|
||||
assert(service_config.appearance.accent_color == "#16a34a");
|
||||
Alert_Config alert_config;
|
||||
const auto alert_form = adminive::to_amis_form_schema<Json>(alert_config, "/admin/config/alerts/data", "Confirm Changes");
|
||||
assert(alert_form.at("body").size() == 8);
|
||||
assert(alert_form.at("body").at(1).at("type") == "input-text");
|
||||
assert(alert_form.at("body").at(2).at("type") == "select");
|
||||
assert(alert_form.at("body").at(3).at("type") == "select");
|
||||
assert(alert_form.at("body").at(4).at("type") == "input-number");
|
||||
assert(alert_form.at("body").at(5).at("type") == "input-date");
|
||||
assert(alert_form.at("body").at(6).at("type") == "input-color");
|
||||
assert(alert_form.at("body").at(7).at("type") == "fieldset");
|
||||
assert(alert_form.at("body").at(7).at("visibleOn") == "${enabled && channel == 'webhook'}");
|
||||
const auto alert_view = adminive::describe_edit_view<Alert_Config>();
|
||||
const auto alert_view_json = adminive::to_view_json<Json, Alert_Config>(alert_view);
|
||||
assert(alert_view_json.at("body").at("kind") == "vertical");
|
||||
const auto alert_form = adminive::to_amis_form_schema<Json>(alert_config, alert_view, "/admin/config/alerts/data");
|
||||
assert(find_named_node(alert_form, "rule_name"));
|
||||
assert(find_named_node(alert_form, "effective_date")->at("type") == "input-date");
|
||||
assert(find_named_node(alert_form, "highlight_color")->at("type") == "input-color");
|
||||
const Json* webhook_group = find_titled_node(alert_form, "Webhook 端点");
|
||||
assert(webhook_group);
|
||||
assert(webhook_group->at("visibleOn") == "${enabled && channel == 'webhook'}");
|
||||
adminive::example::Http_Resource<Radio_Service_Config> service_resource(service_config, "/admin/config/radio");
|
||||
const auto resource_view = service_resource.view_schema();
|
||||
assert(resource_view.at("protocol") == "adminive.view");
|
||||
assert(resource_view.at("kind") == "form");
|
||||
const auto resource_form = service_resource.amis_schema();
|
||||
assert(resource_form.at("api").at("method") == "post");
|
||||
assert(resource_form.at("api").at("url") == "/admin/config/radio/data");
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#include "adminive/adminive.hpp"
|
||||
#include "adminive/adapters/nlohmann_json.hpp"
|
||||
#include <cassert>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
namespace view_test {
|
||||
using Json = nlohmann::json;
|
||||
struct Config {
|
||||
std::string name{"Alpha"};
|
||||
int count{3};
|
||||
bool enabled{true};
|
||||
std::string color{"#336699"};
|
||||
};
|
||||
}
|
||||
namespace adminive {
|
||||
template <>
|
||||
struct Type_Descriptor<view_test::Config> {
|
||||
static auto get() {
|
||||
using T = view_test::Config;
|
||||
return object<T>("view_config", "View Config", ADMINIVE_FIELD(T, name).editable().creatable().label("Name").text_input(), ADMINIVE_FIELD(T, count).editable().creatable().label("Count").number_input(), ADMINIVE_FIELD(T, enabled).editable().creatable().label("Enabled").boolean_input(), ADMINIVE_FIELD(T, color).editable().creatable().label("Color").color_input());
|
||||
}
|
||||
};
|
||||
template <>
|
||||
struct Type_View_Descriptor<view_test::Config> {
|
||||
static Form_View edit() {
|
||||
using T = view_test::Config;
|
||||
return edit_form<T>(vertical(group("Identity", horizontal(use<&T::name>(), use<&T::enabled>())), grid(2, use<&T::count>(), card("Appearance", use<&T::color>())), flow(card("Name Card", use<&T::name>()), card("Count Card", use<&T::count>())), tabs(tab("General", use<&T::name>(), use<&T::count>()), tab("Visual", use<&T::color>())))).submit("Save");
|
||||
}
|
||||
static Table_View table() {
|
||||
using T = view_test::Config;
|
||||
return table_view<T>(column<&T::name>("Name").search().fix(Table_Fixed::left), column<&T::count>("Count").sort(), column<&T::enabled>("Enabled").filter(), column<&T::color>("Color")).titled("Configs").default_sort("count", true).create_layout(grid(2, use<&T::name>(), use<&T::count>(), use<&T::enabled>(), use<&T::color>())).edit_layout(vertical(use<&T::name>(), horizontal(use<&T::count>(), use<&T::enabled>()), use<&T::color>()));
|
||||
}
|
||||
};
|
||||
}
|
||||
int main() {
|
||||
using view_test::Config;
|
||||
using view_test::Json;
|
||||
const auto descriptor = adminive::to_descriptor_json<Json, Config>();
|
||||
assert(descriptor.at("fields").at(0).at("presentation").at("label") == "Name");
|
||||
assert(descriptor.at("fields").at(0).at("presentation").at("control") == "text");
|
||||
assert(!descriptor.at("fields").at(0).contains("sortable"));
|
||||
const auto form_view = adminive::describe_edit_view<Config>();
|
||||
adminive::validate_form_view<Config>(form_view);
|
||||
const auto form_json = adminive::to_view_json<Json, Config>(form_view);
|
||||
assert(form_json.at("kind") == "form");
|
||||
assert(form_json.at("body").at("kind") == "vertical");
|
||||
assert(form_json.at("body").at("children").at(0).at("kind") == "group");
|
||||
assert(form_json.at("body").at("children").at(1).at("kind") == "grid");
|
||||
assert(form_json.at("body").at("children").at(2).at("kind") == "flow");
|
||||
assert(form_json.at("body").at("children").at(3).at("kind") == "tabs");
|
||||
Config config;
|
||||
const auto form_amis = adminive::to_amis_form_schema<Json>(config, form_view, "/config/data");
|
||||
assert(form_amis.at("type") == "form");
|
||||
assert(form_amis.at("actions").at(1).at("label") == "Save");
|
||||
assert(form_amis.at("body").at(0).at("type") == "fieldset");
|
||||
assert(form_amis.at("body").at(1).at("type") == "grid");
|
||||
assert(form_amis.at("body").at(2).at("type") == "flex");
|
||||
assert(form_amis.at("body").at(3).at("type") == "tabs");
|
||||
const auto table_view = adminive::describe_table_view<Config>();
|
||||
adminive::validate_table_view<Config>(table_view);
|
||||
const auto table_json = adminive::to_view_json<Json, Config>(table_view);
|
||||
assert(table_json.at("kind") == "table");
|
||||
assert(table_json.at("columns").at(0).at("searchable") == true);
|
||||
assert(table_json.at("columns").at(0).at("fixed") == "left");
|
||||
assert(table_json.at("columns").at(1).at("sortable") == true);
|
||||
assert(table_json.at("columns").at(2).at("filterable") == true);
|
||||
assert(table_json.at("default_order_by") == "count");
|
||||
assert(table_json.at("default_order_dir") == "desc");
|
||||
const auto composition = adminive::composition_view("config_page", adminive::compose::vertical(adminive::compose::heading("Config Page").accent("#123456"), adminive::compose::horizontal(adminive::compose::slot("form"), adminive::compose::slot("table"))));
|
||||
const auto composition_json = adminive::to_view_json<Json>(composition);
|
||||
assert(composition_json.at("kind") == "composition");
|
||||
assert(composition_json.at("body").at("children").at(0).at("kind") == "heading");
|
||||
const auto composition_amis = adminive::to_amis_composition_schema<Json>(composition, {{"form", form_amis}, {"table", Json{{"type", "tpl"}, {"tpl", "table"}}}});
|
||||
assert(composition_amis.at("body").at(0).at("type") == "tpl");
|
||||
assert(composition_amis.at("body").at(1).at("type") == "grid");
|
||||
const auto table_amis = adminive::to_amis_table_schema<Json, Config>("/configs", table_view);
|
||||
assert(table_amis.at("type") == "page");
|
||||
assert(table_amis.at("body").at(0).at("columns").at(1).at("searchable") == true);
|
||||
assert(table_amis.at("body").at(0).at("columns").at(2).at("sortable") == true);
|
||||
assert(table_amis.at("body").at(0).at("columns").at(3).at("filterable") == true);
|
||||
adminive::Collection_Service<Config, Json, adminive::No_Lock> collection("/configs", {Config{"Alpha", 3, true, "#111111"}, Config{"Beta", 7, false, "#222222"}, Config{"Alphabet", 5, true, "#333333"}});
|
||||
adminive::Collection_Query query;
|
||||
query.fields["name"] = "alpha";
|
||||
auto response = collection.list_response(query);
|
||||
assert(response.status == 200);
|
||||
assert(response.body.at("data").at("total") == 2);
|
||||
assert(response.body.at("data").at("items").at(0).at("count") == 5);
|
||||
query = {};
|
||||
query.fields["enabled"] = "true";
|
||||
response = collection.list_response(query);
|
||||
assert(response.status == 200);
|
||||
assert(response.body.at("data").at("total") == 2);
|
||||
query = {};
|
||||
query.order_by = "count";
|
||||
query.order_dir = "asc";
|
||||
response = collection.list_response(query);
|
||||
assert(response.status == 200);
|
||||
assert(response.body.at("data").at("items").at(0).at("count") == 3);
|
||||
query = {};
|
||||
query.fields["color"] = "#111111";
|
||||
response = collection.list_response(query);
|
||||
assert(response.status == 400);
|
||||
bool invalid_grid{};
|
||||
try {
|
||||
adminive::validate_form_view<Config>(adminive::edit_form<Config>(adminive::grid(0, adminive::use<&Config::name>())));
|
||||
} catch(const std::invalid_argument&) {
|
||||
invalid_grid = true;
|
||||
}
|
||||
assert(invalid_grid);
|
||||
bool invalid_tabs{};
|
||||
try {
|
||||
adminive::validate_form_view<Config>(adminive::edit_form<Config>(adminive::tabs(adminive::use<&Config::name>())));
|
||||
} catch(const std::invalid_argument&) {
|
||||
invalid_tabs = true;
|
||||
}
|
||||
assert(invalid_tabs);
|
||||
bool invalid_sort{};
|
||||
try {
|
||||
adminive::validate_table_view<Config>(adminive::table_view<Config>(adminive::column<&Config::name>()).default_sort("name"));
|
||||
} catch(const std::invalid_argument&) {
|
||||
invalid_sort = true;
|
||||
}
|
||||
assert(invalid_sort);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user