Skip to main content

axum_admin/
entity.rs

1use crate::{adapter::DataAdapter, error::AdminError, field::Field};
2use serde_json::Value;
3use std::{collections::HashMap, future::Future, marker::PhantomData, pin::Pin};
4
5pub enum ActionTarget {
6    List,
7    Detail,
8}
9
10pub struct ActionContext {
11    pub ids: Vec<Value>,
12    pub params: HashMap<String, String>,
13}
14
15pub enum ActionResult {
16    Success(String),
17    Redirect(String),
18    Error(String),
19}
20
21type ActionHandler = Box<
22    dyn Fn(ActionContext) -> Pin<Box<dyn Future<Output = Result<ActionResult, AdminError>> + Send>>
23        + Send
24        + Sync,
25>;
26
27type BeforeSaveHook =
28    Box<dyn Fn(&mut HashMap<String, Value>) -> Result<(), AdminError> + Send + Sync>;
29
30type AfterDeleteHook = Box<dyn Fn(&Value) -> Result<(), AdminError> + Send + Sync>;
31
32pub struct CustomAction {
33    pub name: String,
34    pub label: String,
35    pub target: ActionTarget,
36    pub confirm: Option<String>,
37    pub icon: Option<String>,
38    pub class: Option<String>,
39    pub handler: ActionHandler,
40}
41
42impl CustomAction {
43    pub fn builder(name: &str, label: &str) -> CustomActionBuilder {
44        CustomActionBuilder {
45            name: name.to_string(),
46            label: label.to_string(),
47            target: ActionTarget::List,
48            confirm: None,
49            icon: None,
50            class: None,
51        }
52    }
53}
54
55pub struct CustomActionBuilder {
56    name: String,
57    label: String,
58    target: ActionTarget,
59    confirm: Option<String>,
60    icon: Option<String>,
61    class: Option<String>,
62}
63
64impl CustomActionBuilder {
65    pub fn target(mut self, target: ActionTarget) -> Self {
66        self.target = target;
67        self
68    }
69
70    pub fn confirm(mut self, message: &str) -> Self {
71        self.confirm = Some(message.to_string());
72        self
73    }
74
75    pub fn icon(mut self, icon_class: &str) -> Self {
76        self.icon = Some(icon_class.to_string());
77        self
78    }
79
80    pub fn class(mut self, css_class: &str) -> Self {
81        self.class = Some(css_class.to_string());
82        self
83    }
84
85    pub fn handler<F, Fut>(self, f: F) -> CustomAction
86    where
87        F: Fn(ActionContext) -> Fut + Send + Sync + 'static,
88        Fut: Future<Output = Result<ActionResult, AdminError>> + Send + 'static,
89    {
90        CustomAction {
91            name: self.name,
92            label: self.label,
93            target: self.target,
94            confirm: self.confirm,
95            icon: self.icon,
96            class: self.class,
97            handler: Box::new(move |ctx| Box::pin(f(ctx))),
98        }
99    }
100}
101
102#[derive(Debug, Clone, Default)]
103pub struct EntityPermissions {
104    /// Required permission to list records and open the edit form.
105    pub view: Option<String>,
106    /// Required permission to create a new record.
107    pub create: Option<String>,
108    /// Required permission to submit an edit.
109    pub edit: Option<String>,
110    /// Required permission to delete a record.
111    pub delete: Option<String>,
112}
113
114pub struct EntityAdmin {
115    pub entity_name: String,
116    pub label: String,
117    pub icon: String,
118    pub group: Option<String>,
119    pub pk_field: String,
120    pub fields: Vec<Field>,
121    pub list_display: Vec<String>,
122    pub search_fields: Vec<String>,
123    pub filter_fields: Vec<String>,
124    pub filters: Vec<Field>,
125    pub actions: Vec<CustomAction>,
126    pub bulk_delete: bool,
127    pub bulk_export: bool,
128    pub adapter: Option<Box<dyn DataAdapter>>,
129    pub before_save: Option<BeforeSaveHook>,
130    pub after_delete: Option<AfterDeleteHook>,
131    pub permissions: EntityPermissions,
132    _marker: PhantomData<()>,
133}
134
135impl EntityAdmin {
136    pub fn new<T>(_entity: &str) -> Self {
137        Self {
138            entity_name: _entity.to_string(),
139            label: crate::field::default_label(_entity),
140            icon: "fa-solid fa-layer-group".to_string(),
141            group: None,
142            pk_field: "id".to_string(),
143            fields: Vec::new(),
144            list_display: Vec::new(),
145            search_fields: Vec::new(),
146            filter_fields: Vec::new(),
147            filters: Vec::new(),
148            actions: Vec::new(),
149            bulk_delete: true,
150            bulk_export: true,
151            adapter: None,
152            before_save: None,
153            after_delete: None,
154            permissions: EntityPermissions::default(),
155            _marker: PhantomData,
156        }
157    }
158
159    #[cfg(feature = "seaorm")]
160    pub fn from_entity<E>(name: &str) -> Self
161    where
162        E: sea_orm::EntityTrait,
163        E::Column: sea_orm::ColumnTrait,
164    {
165        let fields = crate::adapters::seaorm::seaorm_fields_for::<E>();
166        Self {
167            entity_name: name.to_string(),
168            label: crate::field::default_label(name),
169            icon: "fa-solid fa-layer-group".to_string(),
170            group: None,
171            pk_field: "id".to_string(),
172            fields,
173            list_display: Vec::new(),
174            search_fields: Vec::new(),
175            filter_fields: Vec::new(),
176            filters: Vec::new(),
177            actions: Vec::new(),
178            bulk_delete: true,
179            bulk_export: true,
180            adapter: None,
181            before_save: None,
182            after_delete: None,
183            permissions: EntityPermissions::default(),
184            _marker: PhantomData,
185        }
186    }
187
188    pub fn label(mut self, label: &str) -> Self {
189        self.label = label.to_string();
190        self
191    }
192
193    pub fn pk_field(mut self, pk: &str) -> Self {
194        self.pk_field = pk.to_string();
195        self
196    }
197
198    /// Set the Font Awesome icon class for this entity in the sidebar and dashboard.
199    /// Defaults to `"fa-solid fa-layer-group"`.
200    pub fn icon(mut self, icon: &str) -> Self {
201        self.icon = icon.to_string();
202        self
203    }
204
205    /// Assign this entity to a named sidebar group. Entities sharing the same
206    /// group label are collapsed under a single expandable section.
207    pub fn group(mut self, group: &str) -> Self {
208        self.group = Some(group.to_string());
209        self
210    }
211
212    pub fn field(mut self, field: Field) -> Self {
213        if let Some(pos) = self.fields.iter().position(|f| f.name == field.name) {
214            self.fields[pos] = field;
215        } else {
216            self.fields.push(field);
217        }
218        self
219    }
220
221    pub fn list_display(mut self, fields: Vec<String>) -> Self {
222        self.list_display = fields;
223        self
224    }
225
226    pub fn search_fields(mut self, fields: Vec<String>) -> Self {
227        self.search_fields = fields;
228        self
229    }
230
231    pub fn filter_fields(mut self, fields: Vec<String>) -> Self {
232        self.filter_fields = fields;
233        self
234    }
235
236    pub fn filter(mut self, field: Field) -> Self {
237        if let Some(pos) = self.filters.iter().position(|f| f.name == field.name) {
238            self.filters[pos] = field;
239        } else {
240            self.filters.push(field);
241        }
242        self
243    }
244
245    pub fn bulk_delete(mut self, enabled: bool) -> Self {
246        self.bulk_delete = enabled;
247        self
248    }
249
250    pub fn bulk_export(mut self, enabled: bool) -> Self {
251        self.bulk_export = enabled;
252        self
253    }
254
255    pub fn adapter(mut self, adapter: Box<dyn DataAdapter>) -> Self {
256        self.adapter = Some(adapter);
257        self
258    }
259
260    pub fn action(mut self, action: CustomAction) -> Self {
261        self.actions.push(action);
262        self
263    }
264
265    pub fn before_save<F>(mut self, f: F) -> Self
266    where
267        F: Fn(&mut HashMap<String, Value>) -> Result<(), AdminError> + Send + Sync + 'static,
268    {
269        self.before_save = Some(Box::new(f));
270        self
271    }
272
273    pub fn after_delete<F>(mut self, f: F) -> Self
274    where
275        F: Fn(&Value) -> Result<(), AdminError> + Send + Sync + 'static,
276    {
277        self.after_delete = Some(Box::new(f));
278        self
279    }
280
281    /// Require `perm` to list or view this entity.
282    pub fn require_view(mut self, perm: &str) -> Self {
283        self.permissions.view = Some(perm.to_string());
284        self
285    }
286
287    /// Require `perm` to create a new record.
288    pub fn require_create(mut self, perm: &str) -> Self {
289        self.permissions.create = Some(perm.to_string());
290        self
291    }
292
293    /// Require `perm` to edit an existing record.
294    pub fn require_edit(mut self, perm: &str) -> Self {
295        self.permissions.edit = Some(perm.to_string());
296        self
297    }
298
299    /// Require `perm` to delete a record.
300    pub fn require_delete(mut self, perm: &str) -> Self {
301        self.permissions.delete = Some(perm.to_string());
302        self
303    }
304
305    /// Shortcut: require `role` for all four actions (view, create, edit, delete).
306    pub fn require_role(mut self, role: &str) -> Self {
307        let s = role.to_string();
308        self.permissions.view = Some(s.clone());
309        self.permissions.create = Some(s.clone());
310        self.permissions.edit = Some(s.clone());
311        self.permissions.delete = Some(s);
312        self
313    }
314}
315
316/// A named group of entities that renders as a collapsible section in the sidebar.
317/// Register it with `AdminApp::register()` the same way as a plain `EntityAdmin`.
318pub struct EntityGroupAdmin {
319    pub label: String,
320    pub icon: Option<String>,
321    entities: Vec<EntityAdmin>,
322}
323
324impl EntityGroupAdmin {
325    pub fn new(label: &str) -> Self {
326        Self {
327            label: label.to_string(),
328            icon: None,
329            entities: Vec::new(),
330        }
331    }
332
333    /// Optional Font Awesome icon shown next to the group label in the sidebar.
334    pub fn icon(mut self, icon: &str) -> Self {
335        self.icon = Some(icon.to_string());
336        self
337    }
338
339    /// Add an entity to this group.
340    pub fn register(mut self, entity: EntityAdmin) -> Self {
341        self.entities.push(entity);
342        self
343    }
344
345    /// Consume the group and return its entities with the group label stamped on each.
346    pub(crate) fn into_entities(self) -> Vec<EntityAdmin> {
347        self.entities
348            .into_iter()
349            .map(|mut e| {
350                e.group = Some(self.label.clone());
351                e
352            })
353            .collect()
354    }
355}