utopia_cms 0.3.0
utopia_cms: ^0.3.0 copied to clipboard
Flutter CMS and admin panel toolkit - sortable, filterable CRUD tables and per-row actions over Firebase, Supabase, Hasura or any GraphQL backend.
Utopia CMS (Core) #
Utopia CMS is a Flutter framework for building CMS and admin panels - the back-office dashboards and internal tools that manage your app's data. It gives you animated, high-performance CRUD screens: sortable, filterable tables with create / edit / delete flows and per-row actions, backed by Firebase, Supabase, Hasura or any GraphQL backend.
Motivation #
Creating CMS panels in Flutter can be costly compared to using No-Code/Low-Code solutions. However, we believe that it is still beneficial for a project as it ensures maintainability and allows for the creation of outstanding UI, which is often lacking in existing solutions. That's why we have developed this Low-Code library to optimize the process of creating customizable panels.
Example #
This is a simple example which integrates with GraphQL server and creates complete CMS layout with one page and management flow.
class Example extends StatelessWidget {
final String? pageId;
final void Function(String pageId) onPageChanged;
final GraphQLClient client;
const Example({required this.pageId, required this.onPageChanged, required this.client});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CmsWidget(
selectedPageId: MutableValue.delegate(() => pageId ?? 'users', onPageChanged),
items: [
CmsWidgetItem.page(
id: 'products',
icon: Icon(Icons.shopping_basket_outlined),
title: Text('Products'),
content: _buildProductsPage(),
),
],
),
);
}
CmsTablePage _buildProductsPage() {
return CmsTablePage(
title: "Products",
delegate: CmsHasura.delegate(
client: client,
table: Tables.products,
fields: TableFields.products,
archivedFilter: const CmsFilterNotEquals("archived", true),
),
entries: [
CmsTextEntry(key: "name", modifier: const CmsEntryModifier(sortable: true)),
CmsTextEntry(key: "description", flex: 4),
],
);
}
}
CMS overview & basic features #
The utopia_cms_core library provides the following features for creating server-layer, responsive table-based pages, edit/create flows, and internal navigation. It also supports integration with custom pages and offers a set of helpful widgets to maintain a coherent theme in your application.
CmsWidget #
Wraps whole application and creates a proper paging behavior using a built-in navigation menu.
Responsiveness #
The panel is responsive down to phone widths via CmsBreakpoints. Below 900px the sidebar collapses into a
drawer (opened by a burger next to the page title); at or above it, it stays an in-layout rail. Independently,
each page resolves a CmsPageType (mobile / tablet / web) from the width it actually receives, tuning table
layout, form column nesting and paddings - and opening create / edit full-screen on mobile.
CmsThemeData #
Modifies the styling of the Widgets, determines fonts and colors.
CmsTablePage #
This is a standalone widget for table-based content management. By default, it displays a sortable and filterable table,
introduces infinite-scroll paging (30 items per page by default, configurable via pagingLimit), creates edit and create subpages, and supports item removal. The data is
provided by CmsDelegate and displayed by CmsEntry. Requests may be filtered via CmsFilterEntry.
CmsEntry #
This interface handles the display and management of data. There is a pre-created set of primitives for interacting with basic data types.
| Name | Description |
|---|---|
| CmsTextEntry | Handles generic String variables |
| CmsNumEntry | Handles numeric variables |
| CmsDropdownEntry | For managing set of options and singular choice |
| CmsCountryEntry | Country picker (single selection) |
| CmsBoolEntry | Handles bool variables |
| CmsDateEntry | Handles Date variables |
| CmsLinkEntry | Handles clickable links (opens via url_launcher) |
| CmsMediaEntry | Handles files (img, vid, doc, unknown) |
| CmsSingleMediaEntry | Single file (scalar URL, not a list) |
| CmsToManyDropdownEntry | M2M relationships multi selection dropdown |
You can create custom entries by referring to the implementation of any primitive and the CmsEntry
CmsEntryModifier #
Every CmsEntry accepts an optional modifier (CmsEntryModifier) that tunes how its column and field
behave. pinned controls the table column only - the create / edit form always receives every entry - and
pairs with Responsiveness to drop columns on smaller screens.
| Option | Default | Effect |
|---|---|---|
pinned |
shown everywhere | CmsPageType predicate gating the table column (e.g. (t) => !t.isMobile hides it on phones) |
sortable |
false |
Sort the table by this field (the CmsDelegate must support it) |
sortInvertNulls |
false |
Invert ordering of null values (no-op unless sortable) |
editable |
true |
false = read-only in edit, hidden from create (e.g. an id) |
initializable |
true |
Whether the field appears in the create flow |
required |
true |
Whether a value is required to create / edit |
expanded |
false |
Render the field on its own full-width row in the form |
CmsFilterEntry #
This interface handles filtering fields of the table.
| Name | Description |
|---|---|
| CmsFilterSearchEntry | Handles generic String full search |
| CmsFilterDateEntry | Handles date ranges |
You can create custom entries by referring to the implementation of any primitive and the CmsFilterEntry
CmsFilter #
A composable predicate that describes which rows a CmsDelegate should return. A CmsFilterEntry builds
one from user input (via filterFromValues), and you can also pass one directly - like the archivedFilter
in the example above - to scope an entire table.
| Constructor | Matches |
|---|---|
CmsFilter.all() |
everything (no-op) |
CmsFilter.equals(field, value) |
field == value |
CmsFilter.notEquals(field, value) |
field != value |
CmsFilter.containsString(field, value, {caseSensitive}) |
substring match |
CmsFilter.inList(field, values) |
field in values |
CmsFilter.greaterOrEq(field, value) |
field >= value |
CmsFilter.lesserOrEq(field, value) |
field <= value |
CmsFilter.and(filters) / CmsFilter.or(filters) |
combine sub-filters |
CmsFilter.not(filter) |
negate a filter |
Filters also compose with operators: a & b, a | b and ~a.
CmsDelegate #
This is the main interface for handling CmsTablePage. It is not suitable on its own for handling to-many relationships.
To achieve this functionality, refer to Relationships
The library provides pre-created delegates:
| Name | Description |
|---|---|
| utopia_cms_firebase | Firebase delegate integration |
| utopia_cms_supabase | Supabase delegate integration |
| utopia_cms_graphql | Generic GraphQL delegate integration |
| utopia_cms_hasura | Hasura delegate integration |
To create your custom delegate, refer to the implementation of any delegate and the CmsDelegate interface.
CmsTableAction #
Adds a custom per-row action to the row's pop-up menu, alongside the auto-generated edit / delete. Provide a
list via CmsTablePage.customActions. onPressed receives the row's JsonMap; when shouldUpdateTable is
true and it returns a non-null map, that row is refreshed in place.
CmsTableAction(
label: 'Copy flutter pub add',
shouldUpdateTable: false,
onPressed: (row) async {
await Clipboard.setData(ClipboardData(text: 'flutter pub add ${row['name']}'));
return null;
},
)
CmsManagementSectionEntry #
Injects a custom section (a sliver) into the generated create / edit form, beyond the auto-built entry fields.
Provide a list via CmsTablePage.managementSectionEntries. Each section chooses whether it shows on edit and
/ or create (showEdit / showCreate) and builds its body from the current JsonMap. This is the usual
place to render to-many relationship editors (see Relationships).
CmsManagementSectionEntry(
title: 'Tags',
showCreate: true,
sliverBuilder: (value, isEdit) => SliverToBoxAdapter(child: TagsEditor(item: value)),
)
Relationships #
Handling relation-based entries in the system is slightly more complex. For a particular entry, you need to use
the CmsToManyDelegate
and register additional callbacks to CmsManagementBaseState that is available via Provider
final baseState = Provider.of<CmsManagementBaseState>(context);
baseState.addOnSavedCallback(
(value) async {
return delegate.update(...);
}
);
The library provides the following existing solutions for relationships:
Existing CmsToManyDelegate implementations |
Source |
|---|---|
CmsHasuraOneToManyDelegate, CmsHasuraManyToManyDelegate |
utopia_cms_hasura |
Media #
Media such as images or videos are handled by CmsMediaDelegate which introduces upload and delete functions.
The library provides no existing generic solutions for relationships yet, but here's an example
class FileDelegate implements CmsMediaDelegate {
final CmsGraphQLService graphQLService;
final GraphQLClient client;
const FileDelegate(this.graphQLService, this.client);
Future<CmsMediaUploadRes> upload(XFile file) async {
final (uploadUrl, downloadUrl) = await _createAttachment(mimeType: file.mimeType!);
await _upload(uploadUrl, file);
return CmsMediaUploadRes(downloadUrl: downloadUrl, ref: "XD");
}
Future<(String, String)> _createAttachment({required String mimeType}) async {
final result = await graphQLService.mutate(
client,
name: 'createAttachment',
arguments: {'data': {'contentType': mimeType}.toValueNodeUnsafe()},
fields: {CmsGraphQLField('uploadUrl'), CmsGraphQLField('downloadUrl')},
);
result as Map<String, dynamic>;
return (result['uploadUrl'] as String, result['downloadUrl'] as String);
}
Future<void> _upload(String url, XFile file) async {
final webFile = await HttpRequest.request(file.path, responseType: 'blob');
final request = await HttpRequest.request(url, method: 'PUT', mimeType: file.mimeType!, sendData: webFile.response);
if(request.status != HttpStatus.ok) throw Exception("Failed to upload");
}
}
Widgets #
The package exports its basic UI components in order to allow maintaining a coherent theme in your custom pages.
- CmsFieldWrapper
- CmsTextField
- CmsDropdownField
- CmsDatePicker
- CmsCountryField
- CmsSwitch
- CmsMediaField
- CmsButton
- CmsChip
- CmsChipList
- CmsCard
- CmsDivider
- CmsPageWrapper
- CmsHeader
- CmsVideoPlayer
- CmsLoader
- CmsMockLoadingBox
AI assistants #
This package ships agent rules and a skill - the CmsWidget shell, CmsTablePage,
delegates (Firebase/Supabase/Hasura/GraphQL), the entry catalog, filters, and
custom actions - that work with any agentic coding tool (Claude, Codex, Cursor,
and others) via AGENTS.md and the
Utopia skills marketplace.
Add them with utopia init agents / utopia init skills.
Related packages #
| Package | What it adds |
|---|---|
| utopia_hooks | State management with hooks |
| utopia_arch | The full architecture bundle |
| utopia_cms_firebase | Firebase delegate |
| utopia_cms_supabase | Supabase delegate |
| utopia_cms_hasura | Hasura delegate |
Built by Utopiasoft.
Contributing #
👾 Contributions are welcome - open an issue to discuss a change, or send a pull request.
License #
BSD-3-Clause. See LICENSE.