reinhardt-admin 0.4.0-alpha.10

Admin panel functionality for Reinhardt framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
//! WASM stub types for dependency injection
//!
//! These types are only used for type checking on WASM targets.
//! They provide dummy implementations of server-side types that appear
//! in Server Function signatures but are automatically injected and
//! filtered out by the `#[server_fn]` macro on the client side.

#[cfg(client)]
pub use wasm_only::*;

#[cfg(client)]
mod wasm_only {
	use std::collections::HashMap;

	use crate::types::{
		AdminAction, AdminActionOutcome, AdminError, AdminResult, Fieldset, FormFieldOverride,
		InlineStyle, PrepopulatedField,
	};
	use reinhardt_core::model_form::ModelFormTableName;
	use std::collections::HashMap;
	use std::fmt::Debug;

	/// Operation currently performed by an admin form.
	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
	pub enum AdminFormMode {
		/// Create a new record.
		Create,
		/// Update an existing record.
		Update,
	}

	/// Owned admin form values keyed by configured field name.
	pub type AdminFormData = HashMap<String, serde_json::Value>;

	/// Client-side result returned by custom admin form hooks.
	pub type AdminFormResult<T> = Result<T, AdminFormErrors>;

	/// One field-local or form-global validation error.
	#[derive(Debug, Clone, PartialEq, Eq)]
	pub struct AdminFormError {
		field: Option<String>,
		message: String,
	}

	impl AdminFormError {
		/// Return the affected field, or `None` for a form-global error.
		pub fn field(&self) -> Option<&str> {
			self.field.as_deref()
		}

		/// Return the validation message.
		pub fn message(&self) -> &str {
			&self.message
		}
	}

	/// Ordered validation errors returned by custom admin form hooks.
	#[derive(Debug, Clone, Default, PartialEq, Eq)]
	pub struct AdminFormErrors {
		errors: Vec<AdminFormError>,
	}

	impl AdminFormErrors {
		/// Create errors containing one field-local message.
		pub fn field(field: impl Into<String>, message: impl Into<String>) -> Self {
			let mut errors = Self::default();
			errors.push_field(field, message);
			errors
		}

		/// Create errors containing one form-global message.
		pub fn global(message: impl Into<String>) -> Self {
			let mut errors = Self::default();
			errors.push_global(message);
			errors
		}

		/// Append a field-local validation message.
		pub fn push_field(&mut self, field: impl Into<String>, message: impl Into<String>) {
			self.errors.push(AdminFormError {
				field: Some(field.into()),
				message: message.into(),
			});
		}

		/// Append a form-global validation message.
		pub fn push_global(&mut self, message: impl Into<String>) {
			self.errors.push(AdminFormError {
				field: None,
				message: message.into(),
			});
		}

		/// Iterate over errors in insertion order.
		pub fn iter(&self) -> impl Iterator<Item = &AdminFormError> {
			self.errors.iter()
		}

		/// Return whether no validation messages were recorded.
		pub fn is_empty(&self) -> bool {
			self.errors.is_empty()
		}
	}

	/// Object-safe hook for customizing a model admin form.
	pub trait AdminForm: Debug + Send + Sync {
		/// Return optional per-field schema overlays.
		fn schema(&self) -> Vec<FormFieldOverride> {
			Vec::new()
		}

		/// Normalize submitted data before validation and persistence.
		fn normalize(
			&self,
			_mode: AdminFormMode,
			data: AdminFormData,
		) -> AdminFormResult<AdminFormData> {
			Ok(data)
		}

		/// Validate normalized submitted data.
		fn validate(&self, _mode: AdminFormMode, _data: &AdminFormData) -> AdminFormResult<()> {
			Ok(())
		}
	}

	/// Client-side P1 symbol-parity shape of an inline model configuration.
	///
	/// The WASM side is inert metadata: constructing and reading this value has
	/// no network, database, filesystem, or registration side effects.
	#[derive(Clone, Debug)]
	pub struct InlineModelAdmin {
		key: String,
		child_model: String,
		foreign_key: String,
		fields: Vec<String>,
		style: InlineStyle,
		extra: usize,
		can_delete: bool,
	}

	impl InlineModelAdmin {
		/// Preserve the native constructor shape for shared code.
		///
		/// This is a P1 parity constructor; it records metadata only and does not
		/// perform native validation, persistence, or registration.
		pub fn new<P, C>(
			child_model: impl Into<String>,
			foreign_key: impl Into<String>,
			fields: &[&str],
		) -> AdminResult<Self>
		where
			C: ModelFormTableName,
		{
			let _ = std::marker::PhantomData::<(P, C)>;
			let child_model = child_model.into();
			let foreign_key = foreign_key.into();
			Ok(Self {
				key: format!(
					"{}-{}",
					identifier_part(<C as ModelFormTableName>::table_name()),
					identifier_part(&foreign_key)
				),
				child_model,
				foreign_key,
				fields: fields.iter().map(|field| (*field).to_owned()).collect(),
				style: InlineStyle::Tabular,
				extra: 0,
				can_delete: false,
			})
		}

		/// Preserve the native style builder shape for shared code.
		pub fn style(mut self, style: InlineStyle) -> Self {
			self.style = style;
			self
		}

		/// Preserve the native extra-row builder shape for shared code.
		pub fn extra(mut self, extra: usize) -> Self {
			self.extra = extra.min(100);
			self
		}

		/// Preserve the native delete builder shape for shared code.
		pub fn can_delete(mut self, can_delete: bool) -> Self {
			self.can_delete = can_delete;
			self
		}

		/// Stable key used by flat inline control names.
		pub fn key(&self) -> &str {
			&self.key
		}

		/// Child model display name.
		pub fn child_model(&self) -> &str {
			&self.child_model
		}

		/// Generated relationship identifier on the child model.
		pub fn foreign_key(&self) -> &str {
			&self.foreign_key
		}

		/// Editable child fields.
		pub fn fields(&self) -> &[String] {
			&self.fields
		}

		/// Configured presentation style.
		pub fn style_value(&self) -> InlineStyle {
			self.style
		}

		/// Number of blank rows appended to loaded children.
		pub fn extra_rows(&self) -> usize {
			self.extra
		}

		/// Whether explicit child deletion is enabled.
		pub fn delete_enabled(&self) -> bool {
			self.can_delete
		}
	}

	fn identifier_part(value: &str) -> String {
		value
			.chars()
			.map(|character| {
				if character.is_ascii_alphanumeric() || matches!(character, '_' | '-') {
					character.to_ascii_lowercase()
				} else {
					'_'
				}
			})
			.collect::<String>()
			.trim_matches('_')
			.to_owned()
	}
	/// Dummy AdminSite type for WASM type checking
	///
	/// This type is never actually used in WASM code, as the `#[server_fn]`
	/// macro removes all dependency injection parameters from client stubs.
	/// It exists purely for type checking purposes.
	pub struct AdminSite;

	/// Dummy AdminDatabase type for WASM type checking
	///
	/// This type is never actually used in WASM code, as the `#[server_fn]`
	/// macro removes all dependency injection parameters from client stubs.
	/// It exists purely for type checking purposes.
	pub struct AdminDatabase;

	/// Dummy admin action transaction type for WASM type checking.
	///
	/// This type is never actually used in WASM code because the server owns
	/// action transactions.
	pub struct AdminActionTransaction;

	/// Dummy AdminRecord type for WASM type checking
	///
	/// This type is never actually used in WASM code.
	pub struct AdminRecord;

	/// Dummy admin query type for WASM type checking.
	///
	/// This type is never actually used in WASM code.
	pub struct AdminQuery;

	/// Dummy admin request context type for WASM type checking.
	///
	/// This type is never actually used in WASM code.
	pub struct AdminRequestContext;

	/// Admin user trait stub for WASM type checking.
	///
	/// This trait is never actually used in WASM code.
	pub trait AdminUser: Send + Sync {
		/// Whether the user account is active.
		fn is_active(&self) -> bool;

		/// Whether the user is a staff member.
		fn is_staff(&self) -> bool;

		/// Whether the user is a superuser.
		fn is_superuser(&self) -> bool;

		/// The username for audit logging.
		fn get_username(&self) -> &str;
	}

	/// Changelist column descriptor stub for WASM type checking.
	#[derive(Debug, Clone, PartialEq, Eq)]
	pub enum ListColumn {
		/// A database-backed field column.
		Field {
			/// Field name to read from the result row.
			field: String,
			/// Display label for the column header.
			label: String,
		},
		/// A value computed after the result row is fetched.
		Computed {
			/// Stable key used in responses and computed-value lookup.
			key: String,
			/// Display label for the column header.
			label: String,
			/// Database field used when this computed column is sorted.
			sort_field: Option<String>,
		},
	}

	/// Model admin trait stub for WASM type checking.
	///
	/// This trait is never actually used in WASM code.
	#[async_trait::async_trait]
	pub trait ModelAdmin: Send + Sync {
		/// Get the model name.
		fn model_name(&self) -> &str;

		/// Get the database table name.
		fn table_name(&self) -> &str {
			""
		}

		/// Get the primary key field name.
		fn pk_field(&self) -> &str {
			"id"
		}

		/// Fields to display in list view.
		fn list_display(&self) -> Vec<&str> {
			vec!["id"]
		}

		/// Owned descriptors for columns displayed in list view.
		fn list_columns(&self) -> Vec<ListColumn> {
			self.list_display()
				.into_iter()
				.map(|field| ListColumn::Field {
					field: field.to_string(),
					label: field.to_string(),
				})
				.collect()
		}

		/// Resolve a computed changelist column for a fetched result row.
		fn computed_list_value(
			&self,
			key: &str,
			_row: &HashMap<String, serde_json::Value>,
		) -> crate::types::AdminResult<serde_json::Value> {
			Err(crate::types::AdminError::TemplateError(format!(
				"No computed list column is configured for key '{key}'"
			)))
		}

		/// Date or datetime field used for hierarchical changelist navigation.
		fn date_hierarchy(&self) -> Option<&str> {
			None
		}

		/// Fields that can be edited directly in list view.
		fn list_editable(&self) -> Vec<&str> {
			vec![]
		}

		/// Fields that can be used for filtering.
		fn list_filter(&self) -> Vec<&str> {
			vec![]
		}

		/// Fields that can be searched.
		fn search_fields(&self) -> Vec<&str> {
			vec![]
		}

		/// Many-to-many fields rendered with a horizontal selector.
		fn filter_horizontal(&self) -> Vec<&str> {
			vec![]
		}

		/// Many-to-many fields rendered with a vertical selector.
		fn filter_vertical(&self) -> Vec<&str> {
			vec![]
		}

		/// Fields to display in forms.
		fn fields(&self) -> Option<Vec<&str>> {
			None
		}

		/// Fieldsets to display in forms.
		fn fieldsets(&self) -> Option<Vec<Fieldset>> {
			None
		}

		/// Related child model configurations.
		fn inlines(&self) -> Vec<InlineModelAdmin> {
			Vec::new()
		}

		/// Read-only fields.
		fn readonly_fields(&self) -> Vec<&str> {
			vec![]
		}

		/// Relation fields rendered with autocomplete controls.
		fn autocomplete_fields(&self) -> Vec<&str> {
			vec![]
		}

		/// Relation fields rendered as raw ID inputs.
		fn raw_id_fields(&self) -> Vec<&str> {
			vec![]
		}

		/// Return an optional custom form adapter.
		fn form(&self) -> Option<&dyn AdminForm> {
			None
		}

		/// Return optional per-field form schema overlays.
		fn formfield_overrides(&self) -> Vec<FormFieldOverride> {
			Vec::new()
		}

		/// Return client-side field prepopulation rules.
		fn prepopulated_fields(&self) -> Vec<PrepopulatedField> {
			Vec::new()
		}

		/// Return a display label for an object represented by field values.
		fn object_label(&self, _values: &HashMap<String, serde_json::Value>) -> Option<String> {
			None
		}

		/// Ordering for list view.
		fn ordering(&self) -> Vec<&str> {
			vec!["-id"]
		}

		/// Number of items per page.
		fn list_per_page(&self) -> Option<usize> {
			None
		}

		/// One-level forward foreign keys to select with each changelist row.
		fn list_select_related(&self) -> Vec<&str> {
			vec![]
		}

		/// Customize the changelist query for a request.
		async fn get_queryset(
			&self,
			_user: &dyn AdminUser,
			_request: &AdminRequestContext,
			query: AdminQuery,
		) -> crate::types::AdminResult<AdminQuery> {
			Ok(query)
		}

		/// Actions available for this model.
		fn actions(&self) -> Vec<AdminAction> {
			Vec::new()
		}

		/// Executes an action for the selected model instances.
		async fn execute_action(
			&self,
			action: &str,
			_ids: &[String],
			_transaction: &mut AdminActionTransaction,
			_user: &dyn AdminUser,
		) -> AdminResult<AdminActionOutcome> {
			Err(AdminError::ValidationError(format!(
				"Invalid action: {action}"
			)))
		}

		/// Check if user has permission to view this model.
		async fn has_view_permission(&self, _user: &dyn AdminUser) -> bool {
			false
		}

		/// Check if user has permission to add records for this model.
		async fn has_add_permission(&self, _user: &dyn AdminUser) -> bool {
			false
		}

		/// Check if user has permission to change records for this model.
		async fn has_change_permission(&self, _user: &dyn AdminUser) -> bool {
			false
		}

		/// Check if user has permission to delete records for this model.
		async fn has_delete_permission(&self, _user: &dyn AdminUser) -> bool {
			false
		}
	}

	/// Dummy ModelAdminConfig type for WASM type checking
	///
	/// This type is never actually used in WASM code.
	pub struct ModelAdminConfig;

	/// Dummy ModelAdminConfigBuilder type for WASM type checking
	///
	/// This type is never actually used in WASM code.
	pub struct ModelAdminConfigBuilder;

	/// Dummy ExportFormat type for WASM type checking
	///
	/// This type is never actually used in WASM code.
	#[derive(serde::Serialize, serde::Deserialize)]
	pub struct ExportFormat;

	/// Dummy ImportBuilder type for WASM type checking
	///
	/// This type is never actually used in WASM code.
	pub struct ImportBuilder;

	/// Dummy ImportError type for WASM type checking
	///
	/// This type is never actually used in WASM code.
	pub struct ImportError;

	/// Dummy ImportFormat type for WASM type checking
	///
	/// This type is never actually used in WASM code.
	#[derive(serde::Serialize, serde::Deserialize)]
	pub struct ImportFormat;

	/// Dummy ImportResult type for WASM type checking
	///
	/// This type is never actually used in WASM code.
	pub struct ImportResult;

	// The assertion function is intentionally never called; compiling its
	// signature keeps the WASM trait-object shapes in sync with the native API.
	#[allow(dead_code)]
	fn assert_admin_trait_shapes(
		admin: &dyn ModelAdmin,
		_user: &dyn AdminUser,
		_query: AdminQuery,
		_request: &AdminRequestContext,
		record: &std::collections::HashMap<String, serde_json::Value>,
	) {
		let _: Option<String> = admin.object_label(record);
	}
}