1mod actions;
4mod build;
5mod detail;
6mod forms;
7pub(crate) mod gate;
8mod headers;
9mod list;
10mod pages;
11mod register;
12mod relations;
13mod search;
14mod shell;
15pub(crate) mod state;
16#[cfg(test)]
17pub(crate) mod test_support;
18pub mod url;
19mod write;
20
21use std::path::PathBuf;
22
23use topcoat::{asset::Asset, font::Font, router::LayoutRenderFn};
24
25#[cfg(test)]
26pub(crate) use self::search::TABLE_SEARCH_PATH;
27pub use self::{build::RouterBuilderPanelExt, gate::can_list, shell::Brand};
28use self::{
29 build::is_directory_pattern,
30 register::{PageRegistration, Registration, ResourceRegistration},
31 shell::ShellAssets,
32};
33pub(crate) use self::{build::route_path, forms::parse_form_body, gate::panel_prefix};
34use crate::{
35 DeclarationError, DeclarationErrorKind, Page,
36 resource::{Resource, ResourceDef},
37};
38
39pub fn wired_table<R: Resource>(
47 cx: &topcoat::context::Cx,
48) -> topcoat::Result<crate::table::Table<R::Model>> {
49 let resource = crate::resource::require_mounted::<R>(cx)?;
50 Ok(self::list::wire_table_actions(cx, &resource, false))
51}
52
53pub fn navigation<R: Resource>(cx: &topcoat::context::Cx) -> Option<crate::NavigationItem> {
56 topcoat::context::try_app_context::<crate::resource::MountScope>(cx)?;
58 crate::resource::mounted::<R>(cx).map(|resource| resource.navigation.clone())
59}
60
61pub struct Panel {
78 prefix: String,
79 shell_assets: Option<ShellAssets>,
80 brand: Option<Brand>,
81 dark_mode: Option<bool>,
82 layout: Option<LayoutRenderFn>,
84 registrations: Vec<Box<dyn Registration>>,
86 frame_ancestors: Option<String>,
89 configuration_errors: Vec<DeclarationError>,
91 uploads: Option<crate::upload::InstalledUploader>,
93 served_dirs: Vec<(String, PathBuf)>,
95 login_hint: Option<String>,
96 auth: crate::auth::Auth,
97}
98
99impl Panel {
100 pub fn new(prefix: impl Into<String>) -> Self {
102 let raw = prefix.into();
103 let trimmed = raw.trim().trim_matches('/').to_string();
104 let prefix = if trimmed.is_empty() {
105 "/admin".to_string()
106 } else {
107 format!("/{trimmed}")
108 };
109 let configuration_errors = prefix
110 .trim_matches('/')
111 .split('/')
112 .filter_map(|segment| build::validate_route_segment("panel prefix", segment).err())
113 .map(DeclarationError::panel)
114 .collect();
115 Self {
116 prefix,
117 shell_assets: None,
118 brand: None,
119 dark_mode: None,
120 layout: None,
121 registrations: Vec::new(),
122 frame_ancestors: Some(headers::DEFAULT_FRAME_ANCESTORS.to_string()),
123 configuration_errors,
124 uploads: None,
125 served_dirs: Vec::new(),
126 login_hint: None,
127 auth: crate::auth::Auth::default(),
128 }
129 }
130
131 pub fn prefix(&self) -> &str {
133 &self.prefix
134 }
135
136 pub fn uploads(mut self, uploader: impl crate::Uploader) -> Self {
139 self.uploads = Some(crate::upload::InstalledUploader::new(uploader));
140 self
141 }
142
143 pub fn serve_dir(mut self, path: impl Into<String>, dir: impl Into<PathBuf>) -> Self {
146 let path = path.into();
147 if !is_directory_pattern(&path) {
148 self.configuration_errors.push(DeclarationError::panel(
149 DeclarationErrorKind::ServeDirWithoutCatchAll { path: path.clone() },
150 ));
151 }
152 self.served_dirs.push((path, dir.into()));
153 self
154 }
155
156 pub fn shell_assets(mut self, stylesheet: Asset, font: Font) -> Self {
159 self.shell_assets = Some(ShellAssets { stylesheet, font });
160 self
161 }
162
163 pub fn resource<R: Resource>(self) -> Self {
166 self.resource_with::<R>(|def| def)
167 }
168
169 pub fn resource_with<R: Resource>(
176 mut self,
177 customize: impl FnOnce(ResourceDef<R>) -> ResourceDef<R> + Send + 'static,
178 ) -> Self {
179 self.registrations
180 .push(Box::new(ResourceRegistration::<R>(Box::new(customize))));
181 self
182 }
183
184 pub fn page<P: Page>(mut self) -> Self {
187 self.registrations.push(Box::new(PageRegistration::<P> {
188 home: false,
189 _page: std::marker::PhantomData,
190 }));
191 self
192 }
193
194 pub fn home<P: Page>(mut self) -> Self {
197 self.registrations.push(Box::new(PageRegistration::<P> {
198 home: true,
199 _page: std::marker::PhantomData,
200 }));
201 self
202 }
203
204 pub fn layout(mut self, render: LayoutRenderFn) -> Self {
207 self.layout = Some(render);
208 self
209 }
210
211 pub fn brand(mut self, brand: Brand) -> Self {
213 self.brand = Some(brand);
214 self
215 }
216
217 pub fn frame_ancestors(mut self, ancestors: impl Into<String>) -> Self {
220 self.frame_ancestors = Some(ancestors.into());
221 self
222 }
223
224 pub fn without_frame_ancestors(mut self) -> Self {
226 self.frame_ancestors = None;
227 self
228 }
229
230 pub fn dark_mode(mut self, enabled: bool) -> Self {
232 self.dark_mode = Some(enabled);
233 self
234 }
235
236 pub fn auth(mut self, auth: crate::auth::Auth) -> Self {
239 self.auth = auth;
240 self
241 }
242
243 pub fn login_hint(mut self, hint: impl Into<String>) -> Self {
245 self.login_hint = Some(hint.into());
246 self
247 }
248}
249
250enum Root {
252 Redirect(String),
254 Home,
256}
257
258#[cfg(test)]
259mod tests;