1use crate::{
4 ListParams,
5 config::AdminConfig,
6 data::value_to_plain_string,
7 field::{FieldDefinition, FieldType},
8 model::ModelDefinition,
9 ui::{Breadcrumb, CellType, FilterDef, Pagination, TableCell, TableColumn, TableRow},
10};
11use crate::render::escape as html_escape;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ListView {
19 pub model_name: String,
21 pub verbose_name: String,
23 pub title: String,
25 pub breadcrumbs: Vec<Breadcrumb>,
27 pub columns: Vec<TableColumn>,
29 pub rows: Vec<TableRow>,
31 pub pagination: Pagination,
33 pub filters: Vec<FilterDef>,
35 pub search_query: Option<String>,
37 pub can_add: bool,
39 pub can_delete: bool,
41 pub can_export: bool,
43 pub add_url: String,
45 pub search_placeholder: String,
47 pub has_search: bool,
49 pub has_filters: bool,
51}
52
53impl ListView {
54 pub fn new(model: &ModelDefinition, params: ListParams, per_page: usize) -> Self {
60 let columns = model
61 .display_fields()
62 .iter()
63 .map(|f| TableColumn {
64 field: f.name.clone(),
65 label: f.label.clone(),
66 sortable: f.sortable,
67 sort_direction: if params.sort.as_deref() == Some(&f.name) {
68 Some(match params.order {
69 Some(crate::SortOrder::Desc) => crate::ui::SortDirection::Desc,
70 _ => crate::ui::SortDirection::Asc,
71 })
72 } else {
73 None
74 },
75 css_class: None,
76 width: None,
77 })
78 .collect();
79
80 let filters = model
81 .filterable_fields()
82 .iter()
83 .map(|f| FilterDef {
84 field: f.name.clone(),
85 label: f.label.clone(),
86 filter_type: match f.field_type {
87 crate::field::FieldType::Boolean => crate::ui::FilterType::Boolean,
88 crate::field::FieldType::Enum => crate::ui::FilterType::Select,
89 crate::field::FieldType::Date | crate::field::FieldType::DateTime => {
90 crate::ui::FilterType::DateRange
91 }
92 _ => crate::ui::FilterType::Text,
93 },
94 choices: f
95 .choices
96 .as_ref()
97 .map(|choices| {
98 choices
99 .iter()
100 .map(|c| crate::ui::FilterChoice {
101 value: c.value.clone(),
102 label: c.label.clone(),
103 count: None,
104 })
105 .collect()
106 })
107 .unwrap_or_default(),
108 current: params.filters.get(&f.name).cloned(),
109 })
110 .collect();
111
112 Self {
113 model_name: model.name.clone(),
114 verbose_name: model.verbose_name.clone(),
115 title: model.verbose_name.clone(),
116 breadcrumbs: vec![
117 Breadcrumb::new("Dashboard").url("/admin"),
118 Breadcrumb::new(&model.verbose_name),
119 ],
120 columns,
121 rows: Vec::new(), pagination: Pagination::new(params.page(), per_page.max(1), 0),
123 filters,
124 search_query: params.search,
125 can_add: model.can_add,
126 can_delete: model.can_delete,
127 can_export: model.can_export,
128 add_url: format!("/admin/{}/add", model.name),
129 search_placeholder: format!("Search {}...", model.search_fields.join(", ")),
130 has_search: !model.search_fields.is_empty(),
131 has_filters: !model.list_filter.is_empty(),
132 }
133 }
134
135 pub fn with_rows(mut self, rows: Vec<TableRow>, total: usize) -> Self {
137 self.rows = rows;
138 self.pagination = Pagination::new(self.pagination.page, self.pagination.per_page, total);
139 self
140 }
141
142 pub fn with_json_rows(
146 self,
147 model: &ModelDefinition,
148 config: &AdminConfig,
149 records: &[serde_json::Value],
150 total: usize,
151 ) -> Self {
152 let rows = records
153 .iter()
154 .map(|record| build_table_row(model, config, record))
155 .collect();
156 self.with_rows(rows, total)
157 }
158}
159
160fn build_table_row(
162 model: &ModelDefinition,
163 config: &AdminConfig,
164 record: &serde_json::Value,
165) -> TableRow {
166 let id = record
167 .get(&model.primary_key)
168 .map(value_to_plain_string)
169 .unwrap_or_default();
170
171 let cells = model
172 .display_fields()
173 .iter()
174 .map(|field| {
175 let value = record
176 .get(&field.name)
177 .cloned()
178 .unwrap_or(serde_json::Value::Null);
179 let (rendered, cell_type) = render_cell(field, config, &value);
180 TableCell {
181 field: field.name.clone(),
182 value,
183 rendered,
184 cell_type,
185 }
186 })
187 .collect();
188
189 TableRow {
190 id,
191 cells,
192 selected: false,
193 css_class: None,
194 }
195}
196
197fn render_cell(
200 field: &FieldDefinition,
201 config: &AdminConfig,
202 value: &serde_json::Value,
203) -> (String, CellType) {
204 match field.field_type {
205 FieldType::Date => {
206 let raw = value_to_plain_string(value);
207 (html_escape(&config.format_date(&raw)), CellType::Date)
208 }
209 FieldType::DateTime => {
210 let raw = value_to_plain_string(value);
211 (
212 html_escape(&config.format_datetime(&raw)),
213 CellType::DateTime,
214 )
215 }
216 FieldType::Boolean => (render_value(value), CellType::Boolean),
217 FieldType::Integer | FieldType::BigInteger | FieldType::Float | FieldType::Decimal => {
218 (render_value(value), CellType::Number)
219 }
220 FieldType::Email => (html_escape(&value_to_plain_string(value)), CellType::Email),
221 FieldType::Url => (html_escape(&value_to_plain_string(value)), CellType::Url),
222 _ => (render_value(value), CellType::Text),
223 }
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct DetailView {
229 pub model_name: String,
231 pub verbose_name: String,
233 pub title: String,
235 pub breadcrumbs: Vec<Breadcrumb>,
237 pub id: String,
239 pub fields: Vec<FieldValue>,
241 pub fieldsets: Vec<ViewFieldset>,
243 pub can_edit: bool,
245 pub can_delete: bool,
247 pub edit_url: String,
249 pub delete_url: String,
251 pub list_url: String,
253 pub inlines: Vec<InlineView>,
255}
256
257impl DetailView {
258 pub fn new(model: &ModelDefinition, id: String) -> Self {
260 Self {
261 model_name: model.name.clone(),
262 verbose_name: model.verbose_name_singular.clone(),
263 title: format!("{} #{}", model.verbose_name_singular, id),
264 breadcrumbs: vec![
265 Breadcrumb::new("Dashboard").url("/admin"),
266 Breadcrumb::new(&model.verbose_name).url(format!("/admin/{}", model.name)),
267 Breadcrumb::new(&id),
268 ],
269 id: id.clone(),
270 fields: Vec::new(), fieldsets: Vec::new(),
272 can_edit: model.can_edit,
273 can_delete: model.can_delete,
274 edit_url: format!("/admin/{}/{}/edit", model.name, id),
275 delete_url: format!("/admin/{}/{}/delete", model.name, id),
276 list_url: format!("/admin/{}", model.name),
277 inlines: Vec::new(),
278 }
279 }
280
281 pub fn with_data(mut self, data: serde_json::Value) -> Self {
283 if let Some(obj) = data.as_object() {
284 self.fields = obj
285 .iter()
286 .map(|(k, v)| FieldValue {
287 name: k.clone(),
288 label: k.replace('_', " "),
289 value: v.clone(),
290 rendered: render_value(v),
291 readonly: false,
292 })
293 .collect();
294 }
295 self
296 }
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct CreateView {
302 pub model_name: String,
304 pub verbose_name: String,
306 pub title: String,
308 pub breadcrumbs: Vec<Breadcrumb>,
310 pub fields: Vec<FormField>,
312 pub fieldsets: Vec<ViewFieldset>,
314 pub submit_url: String,
316 pub cancel_url: String,
318 pub inlines: Vec<InlineView>,
320}
321
322impl CreateView {
323 pub fn new(model: &ModelDefinition) -> Self {
325 let fields = model
326 .form_fields()
327 .iter()
328 .map(|f| FormField::from_definition(f))
329 .collect();
330
331 Self {
332 model_name: model.name.clone(),
333 verbose_name: model.verbose_name_singular.clone(),
334 title: format!("Add {}", model.verbose_name_singular),
335 breadcrumbs: vec![
336 Breadcrumb::new("Dashboard").url("/admin"),
337 Breadcrumb::new(&model.verbose_name).url(format!("/admin/{}", model.name)),
338 Breadcrumb::new("Add"),
339 ],
340 fields,
341 fieldsets: Vec::new(),
342 submit_url: format!("/admin/{}/add", model.name),
343 cancel_url: format!("/admin/{}", model.name),
344 inlines: Vec::new(),
345 }
346 }
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct EditView {
352 pub model_name: String,
354 pub verbose_name: String,
356 pub title: String,
358 pub breadcrumbs: Vec<Breadcrumb>,
360 pub id: String,
362 pub fields: Vec<FormField>,
364 pub fieldsets: Vec<ViewFieldset>,
366 pub submit_url: String,
368 pub cancel_url: String,
370 pub delete_url: String,
372 pub can_delete: bool,
374 pub inlines: Vec<InlineView>,
376}
377
378impl EditView {
379 pub fn new(model: &ModelDefinition, id: String) -> Self {
381 let fields = model
382 .form_fields()
383 .iter()
384 .map(|f| FormField::from_definition(f))
385 .collect();
386
387 Self {
388 model_name: model.name.clone(),
389 verbose_name: model.verbose_name_singular.clone(),
390 title: format!("Edit {} #{}", model.verbose_name_singular, id),
391 breadcrumbs: vec![
392 Breadcrumb::new("Dashboard").url("/admin"),
393 Breadcrumb::new(&model.verbose_name).url(format!("/admin/{}", model.name)),
394 Breadcrumb::new(&id).url(format!("/admin/{}/{}", model.name, id)),
395 Breadcrumb::new("Edit"),
396 ],
397 id: id.clone(),
398 fields,
399 fieldsets: Vec::new(),
400 submit_url: format!("/admin/{}/{}/edit", model.name, id),
401 cancel_url: format!("/admin/{}/{}", model.name, id),
402 delete_url: format!("/admin/{}/{}/delete", model.name, id),
403 can_delete: model.can_delete,
404 inlines: Vec::new(),
405 }
406 }
407
408 pub fn with_data(mut self, data: serde_json::Value) -> Self {
410 if let Some(obj) = data.as_object() {
411 for field in &mut self.fields {
412 if let Some(v) = obj.get(&field.name) {
413 field.value = v.clone();
414 }
415 }
416 }
417 self
418 }
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct FieldValue {
424 pub name: String,
426 pub label: String,
428 pub value: serde_json::Value,
430 pub rendered: String,
432 pub readonly: bool,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct FormField {
439 pub name: String,
441 pub label: String,
443 pub widget: String,
445 pub value: serde_json::Value,
447 pub required: bool,
449 pub readonly: bool,
451 pub help_text: Option<String>,
453 pub placeholder: Option<String>,
455 pub choices: Option<Vec<(String, String)>>,
457 pub errors: Vec<String>,
459 pub attrs: std::collections::HashMap<String, String>,
461}
462
463impl FormField {
464 pub fn from_definition(field: &FieldDefinition) -> Self {
466 let mut attrs = std::collections::HashMap::new();
467
468 if let Some(max_len) = field.max_length {
469 attrs.insert("maxlength".to_string(), max_len.to_string());
470 }
471 if let Some(min) = field.min_value {
472 attrs.insert("min".to_string(), min.to_string());
473 }
474 if let Some(max) = field.max_value {
475 attrs.insert("max".to_string(), max.to_string());
476 }
477
478 Self {
479 name: field.name.clone(),
480 label: field.label.clone(),
481 widget: format!("{:?}", field.widget).to_lowercase(),
482 value: serde_json::Value::Null,
483 required: field.required,
484 readonly: field.readonly,
485 help_text: field.help_text.clone(),
486 placeholder: field.placeholder.clone(),
487 choices: field.choices.as_ref().map(|c| {
488 c.iter()
489 .map(|ch| (ch.value.clone(), ch.label.clone()))
490 .collect()
491 }),
492 errors: Vec::new(),
493 attrs,
494 }
495 }
496
497 pub fn with_value(mut self, value: serde_json::Value) -> Self {
499 self.value = value;
500 self
501 }
502
503 pub fn add_error(&mut self, error: impl Into<String>) {
505 self.errors.push(error.into());
506 }
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct ViewFieldset {
512 pub name: Option<String>,
514 pub description: Option<String>,
516 pub fields: Vec<String>,
518 pub collapsible: bool,
520 pub collapsed: bool,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct InlineView {
527 pub model_name: String,
529 pub verbose_name: String,
531 pub rows: Vec<InlineRow>,
533 pub extra: usize,
535 pub can_delete: bool,
537 pub fields: Vec<String>,
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct InlineRow {
544 pub id: Option<String>,
546 pub fields: Vec<FormField>,
548 pub is_new: bool,
550 pub delete: bool,
552}
553
554fn render_value(value: &serde_json::Value) -> String {
556 match value {
557 serde_json::Value::Null => "—".to_string(),
558 serde_json::Value::Bool(b) => {
559 if *b {
560 r#"<span class="badge badge-success">Yes</span>"#.to_string()
561 } else {
562 r#"<span class="badge badge-error">No</span>"#.to_string()
563 }
564 }
565 serde_json::Value::Number(n) => n.to_string(),
566 serde_json::Value::String(s) => html_escape(s),
567 serde_json::Value::Array(arr) => {
568 format!("[{} items]", arr.len())
569 }
570 serde_json::Value::Object(_) => "[Object]".to_string(),
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use crate::field::FieldType;
578
579 #[test]
580 fn test_create_list_view() {
581 let model = ModelDefinition::builder("user")
582 .id_field()
583 .field(FieldDefinition::new("name", FieldType::String).searchable())
584 .field(FieldDefinition::new("email", FieldType::Email))
585 .list_display(["id", "name", "email"])
586 .search_fields(["name", "email"])
587 .build();
588
589 let view = ListView::new(&model, ListParams::default(), 25);
590
591 assert_eq!(view.model_name, "user");
592 assert_eq!(view.columns.len(), 3);
593 assert!(view.has_search);
594 assert_eq!(view.pagination.per_page, 25);
595 }
596
597 #[test]
598 fn test_list_view_json_rows() {
599 let model = ModelDefinition::builder("user")
600 .id_field()
601 .field(FieldDefinition::new("name", FieldType::String))
602 .list_display(["id", "name"])
603 .build();
604
605 let records = vec![
606 serde_json::json!({ "id": 1, "name": "Alice" }),
607 serde_json::json!({ "id": 2, "name": "Bob" }),
608 ];
609 let view = ListView::new(&model, ListParams::default(), 25).with_json_rows(
610 &model,
611 &AdminConfig::default(),
612 &records,
613 2,
614 );
615
616 assert_eq!(view.rows.len(), 2);
617 assert_eq!(view.rows[0].id, "1");
618 assert_eq!(view.rows[0].cells[1].rendered, "Alice");
619 assert_eq!(view.pagination.total_items, 2);
620 }
621
622 #[test]
623 fn test_edit_view_with_data() {
624 let model = ModelDefinition::builder("user")
625 .id_field()
626 .field(FieldDefinition::new("name", FieldType::String))
627 .build();
628
629 let view = EditView::new(&model, "5".to_string())
630 .with_data(serde_json::json!({ "name": "Carol" }));
631
632 assert_eq!(view.id, "5");
633 let name = view.fields.iter().find(|f| f.name == "name").unwrap();
634 assert_eq!(name.value, serde_json::json!("Carol"));
635 }
636
637 #[test]
638 fn test_render_value() {
639 assert_eq!(render_value(&serde_json::Value::Null), "—");
640 assert!(render_value(&serde_json::Value::Bool(true)).contains("Yes"));
641 assert_eq!(render_value(&serde_json::json!(42)), "42");
642 assert_eq!(render_value(&serde_json::json!("test")), "test");
643 }
644
645 #[test]
646 fn test_html_escape() {
647 assert_eq!(html_escape("<script>"), "<script>");
648 assert_eq!(html_escape("a & b"), "a & b");
649 }
650}