Skip to main content

coil_admin/model/
descriptors.rs

1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4pub enum AdminResourceKind {
5    Dashboard,
6    ResourceIndex,
7    DetailView,
8    Workflow,
9    Audit,
10    Settings,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum NavigationSection {
15    Overview,
16    Content,
17    Commerce,
18    Memberships,
19    Events,
20    Media,
21    System,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum WidgetSlot {
26    Header,
27    Summary,
28    Sidebar,
29    Footer,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum BulkActionKind {
34    Publish,
35    Refund,
36    Cancel,
37    CheckIn,
38    Export,
39    Custom,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct AccessibilityContract {
44    pub skip_link_target: String,
45    pub live_region_id: String,
46    pub error_summary_id: String,
47    pub table_caption_required: bool,
48    pub focus_restore_target: String,
49}
50
51impl AccessibilityContract {
52    pub fn standard() -> Self {
53        Self {
54            skip_link_target: "admin-main".to_string(),
55            live_region_id: "admin-status".to_string(),
56            error_summary_id: "admin-errors".to_string(),
57            table_caption_required: true,
58            focus_restore_target: "page-title".to_string(),
59        }
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct AdminResourceDescriptor {
65    pub id: AdminResourceId,
66    pub route: String,
67    pub title: String,
68    pub nav_label: String,
69    pub section: NavigationSection,
70    pub kind: AdminResourceKind,
71    pub required_capability: Capability,
72}
73
74impl AdminResourceDescriptor {
75    pub fn new(
76        id: AdminResourceId,
77        route: impl Into<String>,
78        title: impl Into<String>,
79        nav_label: impl Into<String>,
80        section: NavigationSection,
81        kind: AdminResourceKind,
82        required_capability: Capability,
83    ) -> Result<Self, AdminModelError> {
84        Ok(Self {
85            id,
86            route: validate_route("resource_route", route.into())?,
87            title: require_non_empty("resource_title", title.into())?,
88            nav_label: require_non_empty("resource_nav_label", nav_label.into())?,
89            section,
90            kind,
91            required_capability,
92        })
93    }
94
95    pub fn from_contribution(
96        contribution: &AdminResourceContribution,
97    ) -> Result<Self, AdminModelError> {
98        Self::new(
99            AdminResourceId::new(contribution.id.clone())?,
100            contribution.route.clone(),
101            contribution.title.clone(),
102            contribution.nav_label.clone(),
103            contribution.section.into(),
104            contribution.kind.into(),
105            contribution.required_capability,
106        )
107    }
108}
109
110impl From<CoreAdminNavigationSection> for NavigationSection {
111    fn from(value: CoreAdminNavigationSection) -> Self {
112        match value {
113            CoreAdminNavigationSection::Overview => Self::Overview,
114            CoreAdminNavigationSection::Content => Self::Content,
115            CoreAdminNavigationSection::Commerce => Self::Commerce,
116            CoreAdminNavigationSection::Memberships => Self::Memberships,
117            CoreAdminNavigationSection::Events => Self::Events,
118            CoreAdminNavigationSection::Media => Self::Media,
119            CoreAdminNavigationSection::System => Self::System,
120        }
121    }
122}
123
124impl From<CoreAdminContributionKind> for AdminResourceKind {
125    fn from(value: CoreAdminContributionKind) -> Self {
126        match value {
127            CoreAdminContributionKind::Dashboard => Self::Dashboard,
128            CoreAdminContributionKind::ResourceIndex => Self::ResourceIndex,
129            CoreAdminContributionKind::DetailView => Self::DetailView,
130            CoreAdminContributionKind::Workflow => Self::Workflow,
131            CoreAdminContributionKind::Audit => Self::Audit,
132            CoreAdminContributionKind::Settings => Self::Settings,
133        }
134    }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct AdminWidgetDescriptor {
139    pub id: AdminWidgetId,
140    pub title: String,
141    pub slot: WidgetSlot,
142    pub required_capability: Option<Capability>,
143    pub resource_route: Option<String>,
144}
145
146impl AdminWidgetDescriptor {
147    pub fn new(
148        id: AdminWidgetId,
149        title: impl Into<String>,
150        slot: WidgetSlot,
151        required_capability: Option<Capability>,
152        resource_route: Option<String>,
153    ) -> Result<Self, AdminModelError> {
154        Ok(Self {
155            id,
156            title: require_non_empty("widget_title", title.into())?,
157            slot,
158            required_capability,
159            resource_route: resource_route
160                .map(|route| validate_route("widget_resource_route", route))
161                .transpose()?,
162        })
163    }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub struct WorkflowAction {
168    pub id: WorkflowId,
169    pub title: String,
170    pub bulk_action: BulkActionKind,
171    pub required_capability: Capability,
172    pub success_message: String,
173}
174
175impl WorkflowAction {
176    pub fn new(
177        id: WorkflowId,
178        title: impl Into<String>,
179        bulk_action: BulkActionKind,
180        required_capability: Capability,
181        success_message: impl Into<String>,
182    ) -> Result<Self, AdminModelError> {
183        Ok(Self {
184            id,
185            title: require_non_empty("workflow_title", title.into())?,
186            bulk_action,
187            required_capability,
188            success_message: require_non_empty("workflow_success_message", success_message.into())?,
189        })
190    }
191
192    pub fn from_bulk_operation(
193        definition: &CoreBulkOperationDefinition,
194    ) -> Result<Self, AdminModelError> {
195        Self::new(
196            WorkflowId::new(definition.id.clone())?,
197            definition.title.clone(),
198            map_bulk_action_kind(definition.kind),
199            definition.required_capability,
200            format!("{} queued", definition.title),
201        )
202    }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct AuditEntry {
207    pub id: AuditEntryId,
208    pub actor_id: String,
209    pub capability: Capability,
210    pub resource_kind: ResourceKind,
211    pub resource_id: String,
212    pub action: String,
213}
214
215impl AuditEntry {
216    pub fn new(
217        id: AuditEntryId,
218        actor_id: impl Into<String>,
219        capability: Capability,
220        resource_kind: ResourceKind,
221        resource_id: impl Into<String>,
222        action: impl Into<String>,
223    ) -> Result<Self, AdminModelError> {
224        Ok(Self {
225            id,
226            actor_id: require_non_empty("actor_id", actor_id.into())?,
227            capability,
228            resource_kind,
229            resource_id: require_non_empty("resource_id", resource_id.into())?,
230            action: require_non_empty("action", action.into())?,
231        })
232    }
233}
234
235#[derive(Debug, Clone, Default, PartialEq, Eq)]
236pub struct OperatorAccessContext {
237    capabilities: HashSet<Capability>,
238}
239
240impl OperatorAccessContext {
241    pub fn new() -> Self {
242        Self::default()
243    }
244
245    pub fn with_capability(mut self, capability: Capability) -> Self {
246        self.capabilities.insert(capability);
247        self
248    }
249
250    pub fn allows(&self, capability: Capability) -> bool {
251        self.capabilities.contains(&capability)
252    }
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct BulkActionPlan {
257    pub workflow_id: WorkflowId,
258    pub resource_count: usize,
259    pub message: String,
260}
261
262pub(super) fn map_bulk_action_kind(kind: CoreBulkOperationKind) -> BulkActionKind {
263    match kind {
264        CoreBulkOperationKind::Publish => BulkActionKind::Publish,
265        CoreBulkOperationKind::Unpublish => BulkActionKind::Custom,
266        CoreBulkOperationKind::Reindex => BulkActionKind::Custom,
267        CoreBulkOperationKind::Export => BulkActionKind::Export,
268        CoreBulkOperationKind::Cancel => BulkActionKind::Cancel,
269        CoreBulkOperationKind::CheckIn => BulkActionKind::CheckIn,
270        CoreBulkOperationKind::Custom => BulkActionKind::Custom,
271    }
272}
273
274pub(super) fn map_extension_widget_slot(surface: &str) -> WidgetSlot {
275    if surface.contains("header") {
276        WidgetSlot::Header
277    } else if surface.contains("sidebar") {
278        WidgetSlot::Sidebar
279    } else if surface.contains("footer") {
280        WidgetSlot::Footer
281    } else {
282        WidgetSlot::Summary
283    }
284}