Skip to main content

actix_admin/
builder.rs

1use crate::routes::{
2    create_get, create_post, delete, delete_many, download, edit_get, edit_post, index, list,
3    not_found, show,
4};
5use crate::{
6    prelude::*,
7    routes::{
8        bulk_action, delete_file, display_card_grid, export_csv, search,
9        ActixAdminBulkActionDispatch,
10    },
11    ActixAdminMenuElement,
12};
13use actix_web::{web, Route};
14use std::collections::{BTreeMap, HashMap};
15use std::fs;
16
17/// Represents a builder entity which helps generating the ActixAdmin configuration.
18///
19/// Prefer calling the inherent methods on `ActixAdminBuilder` directly; the
20/// old `ActixAdminBuilderTrait` still exists as a compatibility shim but is
21/// now just a re-export of the inherent methods and does not need to be
22/// brought into scope.
23pub struct ActixAdminBuilder {
24    pub scopes: HashMap<String, actix_web::Scope>,
25    pub custom_routes: Vec<(String, Route)>,
26    pub actix_admin: ActixAdmin,
27    pub custom_index: Option<Route>,
28}
29
30/// Compatibility trait for pre-0.8 code that did
31/// `use actix_admin::builder::ActixAdminBuilderTrait;` before calling any
32/// builder method. All the real behavior now lives as inherent methods on
33/// [`ActixAdminBuilder`]; this trait simply re-exports them so existing
34/// user code keeps compiling. New code should not implement it.
35pub trait ActixAdminBuilderTrait {
36    fn new(configuration: ActixAdminConfiguration) -> Self;
37    fn get_scope(self) -> actix_web::Scope;
38    fn get_actix_admin(&self) -> ActixAdmin;
39}
40
41impl ActixAdminBuilderTrait for ActixAdminBuilder {
42    fn new(configuration: ActixAdminConfiguration) -> Self {
43        Self::new(configuration)
44    }
45    fn get_scope(self) -> actix_web::Scope {
46        Self::get_scope(self)
47    }
48    fn get_actix_admin(&self) -> ActixAdmin {
49        Self::get_actix_admin(self)
50    }
51}
52
53impl ActixAdminBuilder {
54    pub fn new(configuration: ActixAdminConfiguration) -> Self {
55        ActixAdminBuilder {
56            actix_admin: ActixAdmin {
57                entity_names: BTreeMap::new(),
58                view_models: HashMap::new(),
59                card_grids: HashMap::new(),
60                configuration,
61                tera: crate::tera_templates::get_tera(),
62                support_path: None,
63            },
64            custom_routes: Vec::new(),
65            scopes: HashMap::new(),
66            custom_index: None,
67        }
68    }
69
70    pub fn add_entity<E: ActixAdminViewModelTrait + 'static>(
71        &mut self,
72        view_model: &ActixAdminViewModel,
73    ) {
74        self.add_entity_to_category::<E>(view_model, "");
75    }
76
77    pub fn add_entity_to_category<E: ActixAdminViewModelTrait + 'static>(
78        &mut self,
79        view_model: &ActixAdminViewModel,
80        category_name: &str,
81    ) {
82        self.scopes.insert(
83            E::get_entity_name(),
84            web::scope(&format!("/{}", E::get_entity_name()))
85                .route("/list", web::get().to(list::<E>))
86                .route("/export_csv", web::get().to(export_csv::<E>))
87                .route("/create", web::get().to(create_get::<E>))
88                .route("/search", web::get().to(search::<E>))
89                .route("/create", web::post().to(create_post::<E>))
90                .route("/edit/{id}", web::get().to(edit_get::<E>))
91                .route("/edit/{id}", web::post().to(edit_post::<E>))
92                .route("/delete", web::delete().to(delete_many::<E>))
93                .route("/delete/{id}", web::delete().to(delete::<E>))
94                .route("/show/{id}", web::get().to(show::<E>))
95                .route("/file/{id}/{column_name}", web::get().to(download::<E>))
96                .route(
97                    "/file/{id}/{column_name}",
98                    web::delete().to(delete_file::<E>),
99                )
100                .default_service(web::to(not_found)),
101        );
102
103        if let Err(e) = fs::create_dir_all(format!(
104            "{}/{}",
105            self.actix_admin.configuration.file_upload_directory,
106            E::get_entity_name()
107        )) {
108            // Don't panic at startup if the process lacks write permission or
109            // the upload directory isn't reachable yet. Entities without file
110            // fields never touch this path; entities with file fields will
111            // surface the error to the user at upload time via a 500.
112            log::warn!(
113                "actix_admin: could not create upload directory for entity `{}`: {e}",
114                E::get_entity_name()
115            );
116        }
117
118        let menu_element = ActixAdminMenuElement {
119            name: E::get_entity_name(),
120            link: E::get_entity_name(),
121            is_custom_handler: false,
122        };
123        self.push_menu_element(category_name, menu_element, false);
124
125        self.actix_admin
126            .view_models
127            .insert(E::get_entity_name(), view_model.clone());
128    }
129
130    pub fn add_custom_handler_for_index(&mut self, route: Route) {
131        self.custom_index = Some(route);
132    }
133
134    /// Register a custom bulk action on an entity. `action` is the metadata
135    /// rendered in the list-page actions dropdown; the entity type `E` must
136    /// provide a `run_bulk_action` implementation (via
137    /// `impl ActixAdminBulkActionDispatch for Entity`) that matches on
138    /// `action.name` and executes the requested work.
139    pub fn add_bulk_action_for_entity<
140        E: ActixAdminViewModelTrait + ActixAdminBulkActionDispatch + 'static,
141    >(
142        &mut self,
143        action: ActixAdminBulkAction,
144    ) {
145        let entity_name = E::get_entity_name();
146        let vm = self
147            .actix_admin
148            .view_models
149            .get_mut(&entity_name)
150            .unwrap_or_else(|| panic!("add_bulk_action_for_entity: entity `{entity_name}` must be registered via add_entity first"));
151        let is_first_action = vm.bulk_actions.is_empty();
152        vm.bulk_actions.push(action);
153
154        // Register the `/action/{name}` route the first time we get an
155        // action for this entity, so entities that never opt in don't have
156        // to satisfy the ActixAdminBulkActionDispatch bound.
157        if is_first_action {
158            let scope = self
159                .scopes
160                .remove(&entity_name)
161                .unwrap_or_else(|| web::scope(&format!("/{}", entity_name)));
162            self.scopes.insert(
163                entity_name,
164                scope.route("/action/{name}", web::post().to(bulk_action::<E>)),
165            );
166        }
167    }
168
169    pub fn add_custom_handler_to_category(
170        &mut self,
171        menu_element_name: &str,
172        path: &str,
173        route: Route,
174        add_to_menu: bool,
175        category_name: &str,
176    ) {
177        self.custom_routes.push((path.to_string(), route));
178
179        if add_to_menu {
180            let menu_element = ActixAdminMenuElement {
181                name: menu_element_name.to_string(),
182                link: path.replacen("/", "", 1),
183                is_custom_handler: true,
184            };
185            self.push_menu_element(category_name, menu_element, true);
186        }
187    }
188
189    pub fn add_card_grid(
190        &mut self,
191        menu_element_name: &str,
192        path: &str,
193        elements: Vec<Vec<String>>,
194        add_to_menu: bool,
195    ) {
196        self.add_card_grid_to_category(menu_element_name, path, elements, add_to_menu, "");
197    }
198
199    pub fn add_card_grid_to_category(
200        &mut self,
201        menu_element_name: &str,
202        path: &str,
203        elements: Vec<Vec<String>>,
204        add_to_menu: bool,
205        category_name: &str,
206    ) {
207        self.custom_routes
208            .push((path.to_string(), web::get().to(display_card_grid)));
209        self.actix_admin
210            .card_grids
211            .insert(path.replace("/", ""), elements);
212
213        if add_to_menu {
214            let menu_element = ActixAdminMenuElement {
215                name: menu_element_name.to_string(),
216                link: path.replacen("/", "", 1),
217                is_custom_handler: true,
218            };
219            self.push_menu_element(category_name, menu_element, true);
220        }
221    }
222
223    pub fn add_custom_handler(
224        &mut self,
225        menu_element_name: &str,
226        path: &str,
227        route: Route,
228        add_to_menu: bool,
229    ) {
230        self.add_custom_handler_to_category(menu_element_name, path, route, add_to_menu, "");
231    }
232
233    pub fn add_support_handler(&mut self, arg: &str, support: Route) {
234        self.custom_routes.push((arg.to_string(), support));
235        self.actix_admin.support_path = Some(arg.replace("/", ""));
236    }
237
238    pub fn add_custom_handler_for_entity<E: ActixAdminViewModelTrait + 'static>(
239        &mut self,
240        menu_element_name: &str,
241        path: &str,
242        route: Route,
243        add_to_menu: bool,
244    ) {
245        self.add_custom_handler_for_entity_in_category::<E>(
246            menu_element_name,
247            path,
248            route,
249            "",
250            add_to_menu,
251        );
252    }
253
254    pub fn add_custom_handler_for_entity_in_category<E: ActixAdminViewModelTrait + 'static>(
255        &mut self,
256        menu_element_name: &str,
257        path: &str,
258        route: Route,
259        category_name: &str,
260        add_to_menu: bool,
261    ) {
262        let menu_element = ActixAdminMenuElement {
263            name: menu_element_name.to_string(),
264            link: format!("{}{}", E::get_entity_name(), path),
265            is_custom_handler: true,
266        };
267
268        let entity_name = E::get_entity_name();
269        let scope = self
270            .scopes
271            .remove(&entity_name)
272            .unwrap_or_else(|| web::scope(&format!("/{}", entity_name)));
273        self.scopes.insert(entity_name, scope.route(path, route));
274
275        if add_to_menu {
276            if let Some(entity_list) = self.actix_admin.entity_names.get_mut(category_name) {
277                if !entity_list.contains(&menu_element) {
278                    entity_list.push(menu_element);
279                }
280            }
281        }
282    }
283
284    pub fn get_scope(self) -> actix_web::Scope {
285        let index_handler = self.custom_index.unwrap_or_else(|| web::get().to(index));
286        let mut admin_scope = web::scope(self.actix_admin.configuration.base_path)
287            .route("/", index_handler)
288            .default_service(web::to(not_found));
289
290        for (_, scope) in self.scopes {
291            admin_scope = admin_scope.service(scope);
292        }
293        for (path, route) in self.custom_routes {
294            admin_scope = admin_scope.route(&path, route);
295        }
296        admin_scope
297    }
298
299    pub fn get_actix_admin(&self) -> ActixAdmin {
300        self.actix_admin.clone()
301    }
302}
303
304impl ActixAdminBuilder {
305    /// Insert `element` under `category_name` in the menu, creating the category
306    /// entry if it doesn't exist. If `dedupe` is true, skip elements already present.
307    fn push_menu_element(
308        &mut self,
309        category_name: &str,
310        element: ActixAdminMenuElement,
311        dedupe: bool,
312    ) {
313        let list = self
314            .actix_admin
315            .entity_names
316            .entry(category_name.to_string())
317            .or_default();
318        if !dedupe || !list.contains(&element) {
319            list.push(element);
320        }
321    }
322}