Skip to content

ModelAdmin and save hooks

GisilaStudio.register builds a ModelAdmin from its arguments. You configure models through register; ModelAdmin and buildModelAdmin are exported so you can inspect or test the resolved configuration. GisilaStudio has no method that accepts a prebuilt ModelAdmin.

Transforms a submitted value before the studio writes it.

typedef StudioSaveHook = FutureOr<Object?> Function(Object? value);

When to use: to hash a password, normalise an email, trim or validate a value. Hooks are the only extension point in the save path. For changes that span several columns or tables, use your own controller instead.

  • Receives the parsed value: a String for text columns, int/double/DateTime/bool for those types, null for a blank nullable field.
  • Returns the value to store. May be async.
  • Throw a FormatException to reject the input; its message is shown on the field and the form is re-rendered with status 422.
  • Runs on add and edit, only for columns that are being written. A password column left blank on edit keeps its stored value and skips the hook. Columns in readonlyFields or excludeFields are never written, so their hooks never run.
  • Keys are column names; camelCase keys are resolved to snake_case columns like the other options.

Example

import 'package:gisila_studio/gisila_studio.dart';
studio.register<User>(
UserTable.metadata,
onSave: {
'password': (v) => hashPassword(v as String),
'email': (v) {
final email = (v as String).trim().toLowerCase();
if (!email.contains('@')) throw const FormatException('Enter a valid email.');
return email;
},
},
);

See also: Save hooks and custom actions.

Configuration controlling how one model appears and behaves. All column lists are normalised to database column names when constructed.

class ModelAdmin<T> {
ModelAdmin({
required this.meta,
required this.displayName,
required this.displayNamePlural,
List<String>? listDisplay,
List<String> searchFields = const [],
List<String> readonlyFields = const [],
List<String> excludeFields = const [],
List<String>? passwordFields,
List<String> autoNowOnUpdateFields = const [],
Map<String, StudioSaveHook> onSave = const {},
this.pageSize = 25,
List<String> ordering = const [],
this.group,
});
}

When to use: read it in tests to check how your register options resolve (for example that 'firstName' became 'first_name'). Prefer buildModelAdmin, which fills in the display names.

Member Type Description
meta TableMeta<T> The ORM metadata.
displayName String Singular label.
displayNamePlural String Plural label.
listDisplay List<String>? Configured list columns, or null for the default.
searchFields List<String> Search columns.
readonlyFields List<String> Read-only form columns.
excludeFields List<String> Columns left off forms.
passwordFields List<String> Secret columns (default: names containing password, passwd or secret).
autoNowOnUpdateFields List<String> Columns stamped with the UTC time on save.
onSave Map<String, StudioSaveHook> Hooks keyed by column.
pageSize int Rows per page. Default 25.
ordering List<String> Order terms; - prefix kept for descending.
group String? Sidebar group; null shows under “General”.
effectiveListDisplay List<String> listDisplay, or the first 6 columns not in passwordFields.
orderBySql String SQL ORDER BY terms, e.g. "date_joined" DESC, "email" ASC; the primary key ascending when ordering is empty.
isEditable(column) bool Not in readonlyFields or excludeFields.
isOnForm(column) bool Not in excludeFields.

Sidebar groups are ordered with a few known labels first (Users & Auth, Catalog, Business, Products, Commerce, Social, Moderation, Platform, General), then any others in registration order.

Example

import 'package:test/test.dart';
test('camelCase names resolve to columns', () {
final admin = buildModelAdmin<User>(
UserTable.metadata,
listDisplay: ['firstName', 'email'],
ordering: ['-dateJoined'],
);
expect(admin.listDisplay, ['first_name', 'email']);
expect(admin.orderBySql, '"date_joined" DESC');
expect(admin.displayName, 'User');
});

Builds a ModelAdmin with default display names. This is what register calls.

ModelAdmin<T> buildModelAdmin<T>(
TableMeta<T> meta, {
String? displayName,
String? displayNamePlural,
List<String>? listDisplay,
List<String> searchFields = const [],
List<String> readonlyFields = const [],
List<String> excludeFields = const [],
List<String>? passwordFields,
List<String> autoNowOnUpdateFields = const [],
Map<String, StudioSaveHook> onSave = const {},
int pageSize = 25,
List<String> ordering = const [],
String? group,
});

When to use: in tests, to get the same resolved configuration register would build. displayName defaults to the table name split on _ and title-cased; displayNamePlural to displayName + 's'. All other parameters are as in register.