@antelopejs/interface-dms 0.10.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +21 -10
- package/dist/attachments.d.ts +15 -2
- package/dist/attachments.js +14 -1
- package/dist/attachments.js.map +1 -1
- package/dist/auth/db/models/sessions.model.d.ts +7 -5
- package/dist/auth/db/models/sessions.model.js +2 -9
- package/dist/auth/db/models/sessions.model.js.map +1 -1
- package/dist/auth/db/models/userExternalIdentities.model.d.ts +7 -5
- package/dist/auth/db/models/users.model.d.ts +7 -5
- package/dist/auth/db/tables/users.table.d.ts +4 -2
- package/dist/auth/db/tables/users.table.js.map +1 -1
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/internal/jwt-auth-decorator.d.ts +1 -1
- package/dist/auth/internal/jwt-auth-decorator.js +6 -3
- package/dist/auth/internal/jwt-auth-decorator.js.map +1 -1
- package/dist/auth/internal/request-authenticators.d.ts +12 -0
- package/dist/auth/internal/request-authenticators.js +53 -10
- package/dist/auth/internal/request-authenticators.js.map +1 -1
- package/dist/base/activity-feed.d.ts +34 -32
- package/dist/base/activity-feed.js +2 -0
- package/dist/base/activity-feed.js.map +1 -1
- package/dist/base/banner.d.ts +12 -10
- package/dist/base/banner.js +2 -0
- package/dist/base/banner.js.map +1 -1
- package/dist/base/block-registry/internal/describe.js +3 -9
- package/dist/base/block-registry/internal/describe.js.map +1 -1
- package/dist/base/block-registry/internal/ui.js +1 -1
- package/dist/base/block-registry/internal/ui.js.map +1 -1
- package/dist/base/card.d.ts +11 -9
- package/dist/base/card.js +2 -0
- package/dist/base/card.js.map +1 -1
- package/dist/base/chart-card.d.ts +4 -2
- package/dist/base/chart-card.js +2 -0
- package/dist/base/chart-card.js.map +1 -1
- package/dist/base/chart-schemas.d.ts +148 -148
- package/dist/base/chart.d.ts +65 -0
- package/dist/base/chart.js +65 -0
- package/dist/base/chart.js.map +1 -1
- package/dist/base/code-block.d.ts +5 -3
- package/dist/base/code-block.js +2 -0
- package/dist/base/code-block.js.map +1 -1
- package/dist/base/custom.d.ts +5 -0
- package/dist/base/custom.js +5 -0
- package/dist/base/custom.js.map +1 -1
- package/dist/base/data-types/default/address.d.ts +5 -0
- package/dist/base/data-types/default/address.js +5 -0
- package/dist/base/data-types/default/address.js.map +1 -1
- package/dist/base/data-types/default/choices.d.ts +20 -0
- package/dist/base/data-types/default/choices.js +20 -0
- package/dist/base/data-types/default/choices.js.map +1 -1
- package/dist/base/data-types/default/files.d.ts +22 -5
- package/dist/base/data-types/default/files.js +20 -0
- package/dist/base/data-types/default/files.js.map +1 -1
- package/dist/base/data-types/default/numbers.d.ts +15 -0
- package/dist/base/data-types/default/numbers.js +25 -9
- package/dist/base/data-types/default/numbers.js.map +1 -1
- package/dist/base/data-types/default/relations.d.ts +10 -2
- package/dist/base/data-types/default/relations.js +9 -6
- package/dist/base/data-types/default/relations.js.map +1 -1
- package/dist/base/data-types/default/strings.d.ts +25 -0
- package/dist/base/data-types/default/strings.js +25 -0
- package/dist/base/data-types/default/strings.js.map +1 -1
- package/dist/base/data-types/default-types.d.ts +1 -1
- package/dist/base/data-types/field-types.d.ts +10 -0
- package/dist/base/data-types/field-types.js +10 -0
- package/dist/base/data-types/field-types.js.map +1 -1
- package/dist/base/data-types/internal/relation-endpoints.js +1 -0
- package/dist/base/data-types/internal/relation-endpoints.js.map +1 -1
- package/dist/base/data-types/status-type.d.ts +5 -0
- package/dist/base/data-types/status-type.js +5 -0
- package/dist/base/data-types/status-type.js.map +1 -1
- package/dist/base/display.d.ts +6 -6
- package/dist/base/empty-state.d.ts +15 -13
- package/dist/base/empty-state.js +2 -0
- package/dist/base/empty-state.js.map +1 -1
- package/dist/base/form-block-schema.d.ts +50 -50
- package/dist/base/form-schema.d.ts +2 -0
- package/dist/base/form-schema.js +2 -0
- package/dist/base/form-schema.js.map +1 -1
- package/dist/base/grid.d.ts +4 -0
- package/dist/base/grid.js +4 -0
- package/dist/base/grid.js.map +1 -1
- package/dist/base/guard.d.ts +30 -28
- package/dist/base/guard.js +2 -0
- package/dist/base/guard.js.map +1 -1
- package/dist/base/helpers/archive-mode-helpers.d.ts +1 -1
- package/dist/base/helpers/file-refs.js +15 -15
- package/dist/base/helpers/file-refs.js.map +1 -1
- package/dist/base/internal/form-schema-validation.js +7 -1
- package/dist/base/internal/form-schema-validation.js.map +1 -1
- package/dist/base/internal/realtime-topics.js.map +1 -1
- package/dist/base/internal/searchable.d.ts +2 -1
- package/dist/base/internal/searchable.js +1 -3
- package/dist/base/internal/searchable.js.map +1 -1
- package/dist/base/key-value-list.d.ts +21 -19
- package/dist/base/key-value-list.js +2 -0
- package/dist/base/key-value-list.js.map +1 -1
- package/dist/base/kpi-card.d.ts +4 -2
- package/dist/base/kpi-card.js +2 -0
- package/dist/base/kpi-card.js.map +1 -1
- package/dist/base/live-toggle.d.ts +2 -0
- package/dist/base/live-toggle.js +2 -0
- package/dist/base/live-toggle.js.map +1 -1
- package/dist/base/meter.d.ts +23 -21
- package/dist/base/meter.js +2 -0
- package/dist/base/meter.js.map +1 -1
- package/dist/base/nav-card-grid.d.ts +24 -19
- package/dist/base/nav-card-grid.js +5 -0
- package/dist/base/nav-card-grid.js.map +1 -1
- package/dist/base/page-toolbar.d.ts +2 -0
- package/dist/base/page-toolbar.js +2 -0
- package/dist/base/page-toolbar.js.map +1 -1
- package/dist/base/period-selector.d.ts +3 -1
- package/dist/base/period-selector.js +2 -0
- package/dist/base/period-selector.js.map +1 -1
- package/dist/base/placeholder.d.ts +2 -0
- package/dist/base/placeholder.js +2 -0
- package/dist/base/placeholder.js.map +1 -1
- package/dist/base/resource-form-schema.d.ts +2 -2
- package/dist/base/searchable.d.ts +3 -2
- package/dist/base/searchable.js +2 -2
- package/dist/base/searchable.js.map +1 -1
- package/dist/base/section.d.ts +6 -2
- package/dist/base/section.js +4 -0
- package/dist/base/section.js.map +1 -1
- package/dist/base/stack.d.ts +6 -0
- package/dist/base/stack.js +6 -0
- package/dist/base/stack.js.map +1 -1
- package/dist/base/stat-group.d.ts +16 -14
- package/dist/base/stat-group.js +2 -0
- package/dist/base/stat-group.js.map +1 -1
- package/dist/base/tab.d.ts +10 -8
- package/dist/base/tab.js +2 -0
- package/dist/base/tab.js.map +1 -1
- package/dist/base/table-view/column-display.d.ts +1 -1
- package/dist/base/table-view/column-display.js.map +1 -1
- package/dist/base/table-view/data-functions.d.ts +10 -4
- package/dist/base/table-view/data-functions.js.map +1 -1
- package/dist/base/table-view/factory.d.ts +5 -0
- package/dist/base/table-view/factory.js +8 -4
- package/dist/base/table-view/factory.js.map +1 -1
- package/dist/base/table-view/internal/auth.d.ts +17 -3
- package/dist/base/table-view/internal/auth.js +26 -5
- package/dist/base/table-view/internal/auth.js.map +1 -1
- package/dist/base/table-view/internal/column-conditions.d.ts +8 -1
- package/dist/base/table-view/internal/column-conditions.js +25 -13
- package/dist/base/table-view/internal/column-conditions.js.map +1 -1
- package/dist/base/table-view/internal/column-display.d.ts +1 -2
- package/dist/base/table-view/internal/column-display.js +3 -9
- package/dist/base/table-view/internal/column-display.js.map +1 -1
- package/dist/base/table-view/internal/factory-helpers.d.ts +1 -1
- package/dist/base/table-view/internal/factory-helpers.js +17 -14
- package/dist/base/table-view/internal/factory-helpers.js.map +1 -1
- package/dist/base/table-view/internal/factory-lifecycle-created.d.ts +2 -3
- package/dist/base/table-view/internal/factory-lifecycle-created.js +5 -5
- package/dist/base/table-view/internal/factory-lifecycle-created.js.map +1 -1
- package/dist/base/table-view/internal/factory-lifecycle-filter.d.ts +2 -2
- package/dist/base/table-view/internal/factory-private-actions.d.ts +2 -2
- package/dist/base/table-view/internal/files.d.ts +2 -1
- package/dist/base/table-view/internal/files.js +14 -5
- package/dist/base/table-view/internal/files.js.map +1 -1
- package/dist/base/table-view/internal/footer.d.ts +3 -3
- package/dist/base/table-view/internal/footer.js +16 -15
- package/dist/base/table-view/internal/footer.js.map +1 -1
- package/dist/base/table-view/internal/guards.d.ts +27 -5
- package/dist/base/table-view/internal/guards.js +127 -49
- package/dist/base/table-view/internal/guards.js.map +1 -1
- package/dist/base/table-view/internal/meta-helpers.d.ts +1 -1
- package/dist/base/table-view/internal/meta-helpers.js +3 -4
- package/dist/base/table-view/internal/meta-helpers.js.map +1 -1
- package/dist/base/table-view/internal/meta.js +2 -3
- package/dist/base/table-view/internal/meta.js.map +1 -1
- package/dist/base/table-view/internal/option-contract.d.ts +1 -1
- package/dist/base/table-view/internal/option-footer.d.ts +2 -2
- package/dist/base/table-view/internal/option-row-actions.d.ts +1 -1
- package/dist/base/table-view/internal/realtime.d.ts +52 -11
- package/dist/base/table-view/internal/realtime.js +23 -16
- package/dist/base/table-view/internal/realtime.js.map +1 -1
- package/dist/base/table-view/internal/resource-form.d.ts +1 -1
- package/dist/base/table-view/internal/resource-form.js +1 -1
- package/dist/base/table-view/internal/resource-form.js.map +1 -1
- package/dist/base/table-view/internal/row-actions-schema.d.ts +12 -12
- package/dist/base/table-view/internal/row-rule-shape.d.ts +11 -5
- package/dist/base/table-view/internal/row-rule-shape.js +60 -30
- package/dist/base/table-view/internal/row-rule-shape.js.map +1 -1
- package/dist/base/table-view/internal/row-rules.d.ts +28 -1
- package/dist/base/table-view/internal/row-rules.js +8 -19
- package/dist/base/table-view/internal/row-rules.js.map +1 -1
- package/dist/base/table-view/internal/tabs.d.ts +1 -1
- package/dist/base/table-view/internal/tabs.js +1 -4
- package/dist/base/table-view/internal/tabs.js.map +1 -1
- package/dist/base/table-view/internal/validation.d.ts +2 -2
- package/dist/base/table-view/internal/validation.js +7 -7
- package/dist/base/table-view/internal/validation.js.map +1 -1
- package/dist/base/table-view/meta.d.ts +7 -6
- package/dist/base/table-view/meta.js +12 -7
- package/dist/base/table-view/meta.js.map +1 -1
- package/dist/base/table-view/resource-form.d.ts +3 -1
- package/dist/base/table-view/resource-form.js +2 -0
- package/dist/base/table-view/resource-form.js.map +1 -1
- package/dist/base/table-view/routes.d.ts +73 -21
- package/dist/base/table-view/routes.js +13 -10
- package/dist/base/table-view/routes.js.map +1 -1
- package/dist/base/table-view/schema.d.ts +152 -152
- package/dist/base/table-view/source.js +4 -1
- package/dist/base/table-view/source.js.map +1 -1
- package/dist/base/table-view.d.ts +1 -1
- package/dist/base/table-view.js.map +1 -1
- package/dist/base/top-list-card.d.ts +35 -33
- package/dist/base/top-list-card.js +2 -0
- package/dist/base/top-list-card.js.map +1 -1
- package/dist/base/types/error.js +5 -2
- package/dist/base/types/error.js.map +1 -1
- package/dist/base/types/guards.d.ts +4 -4
- package/dist/base/types/json.d.ts +1 -10
- package/dist/base/types/row-action.d.ts +7 -7
- package/dist/component-options/confirm-dialog.d.ts +2 -0
- package/dist/component-options/confirm-dialog.js.map +1 -1
- package/dist/component-options/data-type.d.ts +2 -2
- package/dist/component-options/data-type.js +7 -7
- package/dist/component-options/data-type.js.map +1 -1
- package/dist/component-options/row-action.d.ts +14 -9
- package/dist/component-slots.d.ts +11 -2
- package/dist/component-slots.js +1 -0
- package/dist/component-slots.js.map +1 -1
- package/dist/component.d.ts +7 -8
- package/dist/component.js +17 -25
- package/dist/component.js.map +1 -1
- package/dist/data-controllers/members.d.ts +11 -7
- package/dist/data-controllers/members.js +1 -1
- package/dist/data-controllers/members.js.map +1 -1
- package/dist/data-controllers/roles.d.ts +32 -10
- package/dist/db/internal/exportJobs.model.d.ts +7 -5
- package/dist/db/internal/inviteResolutions.model.d.ts +7 -5
- package/dist/db/internal/tenantLifecycle.model.d.ts +7 -5
- package/dist/db/models/roles.model.d.ts +7 -5
- package/dist/db/models/tenantMembers.model.d.ts +7 -5
- package/dist/db/models/tenants.model.d.ts +7 -5
- package/dist/db/models/user_invites.model.d.ts +7 -5
- package/dist/db/tables/tenants.table.d.ts +9 -0
- package/dist/db/tables/tenants.table.js +4 -0
- package/dist/db/tables/tenants.table.js.map +1 -1
- package/dist/guards.d.ts +1 -1
- package/dist/hooks.d.ts +10 -9
- package/dist/hooks.js +42 -33
- package/dist/hooks.js.map +1 -1
- package/dist/internal/attachments.d.ts +2 -2
- package/dist/internal/component-builder.js +2 -1
- package/dist/internal/component-builder.js.map +1 -1
- package/dist/internal/component-serialization.d.ts +2 -4
- package/dist/internal/component-serialization.js +4 -3
- package/dist/internal/component-serialization.js.map +1 -1
- package/dist/internal/component-slots.d.ts +19 -2
- package/dist/internal/component-slots.js +35 -12
- package/dist/internal/component-slots.js.map +1 -1
- package/dist/internal/component-watch-filter.d.ts +11 -0
- package/dist/internal/component-watch-filter.js +22 -0
- package/dist/internal/component-watch-filter.js.map +1 -0
- package/dist/internal/component-watches.js +0 -1
- package/dist/internal/component-watches.js.map +1 -1
- package/dist/internal/own-entry.d.ts +7 -0
- package/dist/internal/own-entry.js +13 -0
- package/dist/internal/own-entry.js.map +1 -0
- package/dist/invite-extension-payloads.d.ts +14 -2
- package/dist/invite-extensions/internal/delivery.js +1 -2
- package/dist/invite-extensions/internal/delivery.js.map +1 -1
- package/dist/invite-extensions/internal/edit.d.ts +2 -1
- package/dist/invite-extensions/internal/edit.js +8 -9
- package/dist/invite-extensions/internal/edit.js.map +1 -1
- package/dist/invite-extensions/internal/form-slot.js +8 -15
- package/dist/invite-extensions/internal/form-slot.js.map +1 -1
- package/dist/invite-extensions/internal/payload-slices.d.ts +33 -3
- package/dist/invite-extensions/internal/payload-slices.js +84 -19
- package/dist/invite-extensions/internal/payload-slices.js.map +1 -1
- package/dist/invite-extensions/internal/types.d.ts +10 -1
- package/dist/invite-resolution.js +6 -4
- package/dist/invite-resolution.js.map +1 -1
- package/dist/json.d.ts +10 -0
- package/dist/json.js +3 -0
- package/dist/json.js.map +1 -0
- package/dist/layout-banners.d.ts +5 -2
- package/dist/layout-banners.js.map +1 -1
- package/dist/page/controllers.d.ts +2 -2
- package/dist/page/internal/extension-validation.js +3 -2
- package/dist/page/internal/extension-validation.js.map +1 -1
- package/dist/page/internal/metadata-layout.js +1 -1
- package/dist/page/internal/metadata-layout.js.map +1 -1
- package/dist/page/internal/metadata-publish.js +3 -2
- package/dist/page/internal/metadata-publish.js.map +1 -1
- package/dist/page/internal/metadata-registration.js +5 -4
- package/dist/page/internal/metadata-registration.js.map +1 -1
- package/dist/page/internal/metadata-upload-registration.d.ts +3 -1
- package/dist/page/internal/metadata-upload-registration.js +9 -0
- package/dist/page/internal/metadata-upload-registration.js.map +1 -1
- package/dist/page/internal/metadata-uploads.d.ts +18 -5
- package/dist/page/internal/metadata-uploads.js +7 -8
- package/dist/page/internal/metadata-uploads.js.map +1 -1
- package/dist/page/internal/page-record.d.ts +2 -3
- package/dist/page/internal/page-record.js +19 -14
- package/dist/page/internal/page-record.js.map +1 -1
- package/dist/page/layout-filter.js +17 -6
- package/dist/page/layout-filter.js.map +1 -1
- package/dist/page/metadata.js +5 -7
- package/dist/page/metadata.js.map +1 -1
- package/dist/request-tenant.d.ts +10 -4
- package/dist/request-tenant.js +16 -34
- package/dist/request-tenant.js.map +1 -1
- package/dist/tenant-export.d.ts +2 -1
- package/dist/tenant-lifecycle.d.ts +13 -4
- package/dist/tenant-lifecycle.js +51 -18
- package/dist/tenant-lifecycle.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/uploads.js +42 -59
- package/dist/uploads.js.map +1 -1
- package/dist/utils/fire-and-forget.js.map +1 -1
- package/dist/utils/internal/decorated-class.d.ts +14 -0
- package/dist/utils/internal/decorated-class.js +37 -0
- package/dist/utils/internal/decorated-class.js.map +1 -0
- package/dist/utils/internal/type-check.d.ts +9 -0
- package/dist/utils/internal/type-check.js +13 -5
- package/dist/utils/internal/type-check.js.map +1 -1
- package/dist/utils/internal/value-parser.d.ts +1 -1
- package/dist/utils/internal/value-parser.js +22 -9
- package/dist/utils/internal/value-parser.js.map +1 -1
- package/dist/watch-actions.d.ts +1 -1
- package/docs/1.core/1.introduction.md +82 -77
- package/docs/1.core/10.layout-banners.md +57 -45
- package/docs/1.core/11.modules-and-dynamic-menus.md +178 -0
- package/docs/1.core/12.frontend-modules-and-request-helpers.md +155 -0
- package/docs/1.core/13.tenant-data-export.md +120 -0
- package/docs/1.core/2.pages-and-modules.md +140 -367
- package/docs/1.core/3.components.md +21 -22
- package/docs/1.core/4.permissions.md +92 -71
- package/docs/1.core/5.tenancy.md +112 -86
- package/docs/1.core/6.invites.md +90 -65
- package/docs/1.core/7.hooks.md +96 -166
- package/docs/1.core/8.realtime.md +44 -45
- package/docs/1.core/9.quick-actions.md +37 -30
- package/docs/2.auth/1.introduction.md +33 -35
- package/docs/2.auth/2.authentication.md +60 -38
- package/docs/2.auth/3.tokens-and-sessions.md +56 -55
- package/docs/2.auth/4.users.md +82 -52
- package/docs/2.auth/5.external-identities.md +25 -26
- package/docs/3.base/1.introduction.md +64 -60
- package/docs/3.base/10.display-blocks.md +15 -16
- package/docs/3.base/11.page-layouts.md +273 -0
- package/docs/3.base/12.table-row-actions.md +122 -0
- package/docs/3.base/13.table-filters-and-forms.md +104 -0
- package/docs/3.base/14.table-events-and-helpers.md +114 -0
- package/docs/3.base/15.table-sources-and-gates.md +24 -0
- package/docs/3.base/2.types.md +44 -41
- package/docs/3.base/3.forms.md +27 -28
- package/docs/3.base/4.tables.md +34 -367
- package/docs/3.base/5.charts-and-widgets.md +28 -29
- package/docs/3.base/6.layout.md +21 -270
- package/docs/3.base/8.data-types.md +39 -40
- package/docs/3.base/9.helpers.md +36 -38
- package/docs/4.notifications/1.introduction.md +22 -26
- package/docs/4.notifications/2.categories-and-subjects.md +58 -55
- package/docs/4.notifications/3.sending.md +72 -68
- package/docs/5.html-render/1.introduction.md +119 -81
- package/docs/5.html-render/2.built-in-emails.md +71 -51
- package/docs/index.md +18 -29
- package/docs/tsconfig.json +4 -0
- package/package.json +12 -12
- package/docs/images/emails-admin-invite.png +0 -0
- package/docs/images/emails-change-notice.png +0 -0
- package/docs/images/emails-change-verification.png +0 -0
- package/docs/images/emails-digest-logo-dark.png +0 -0
- package/docs/images/emails-digest-logo-light.png +0 -0
- package/docs/images/emails-digest.png +0 -0
- package/docs/images/emails-export-partial.png +0 -0
- package/docs/images/emails-export-ready.png +0 -0
- package/docs/images/emails-reset-password.png +0 -0
- package/docs/images/emails-two-factor.png +0 -0
- package/docs/images/emails-user-validation.png +0 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: "Display
|
|
2
|
+
title: "Display blocks"
|
|
3
3
|
description: "Seven builders cover the summaries and notices around a page's data: `StatGroup`, `KeyValueList`, `NavCardGrid`, `EmptyState`, `Banner`, `Card` and `CodeBlock`."
|
|
4
|
+
mode: reference
|
|
4
5
|
---
|
|
5
6
|
|
|
6
|
-
# Display Blocks
|
|
7
|
-
|
|
8
7
|
## Overview
|
|
9
8
|
|
|
10
9
|
Seven builders cover the summaries and notices around a page's data: `StatGroup`, `KeyValueList`, `NavCardGrid`, `EmptyState`, `Banner`, `Card` and `CodeBlock`. Each returns a `ComponentBuilder` and registers its block type with an options schema, so `ListBlockTypes()` describes it to a page builder. `Card` is a container; the others are leaves. The task-shaped chapter is [Display blocks](/docs/components/display-blocks).
|
|
@@ -33,7 +32,7 @@ import {
|
|
|
33
32
|
|
|
34
33
|
Every factory but `EmptyState` takes its options as optional, so a block placed before it is configured is the bare call `StatGroup()`. `EmptyState(options)` requires its options, `title` included.
|
|
35
34
|
|
|
36
|
-
## Shared
|
|
35
|
+
## Shared vocabulary
|
|
37
36
|
|
|
38
37
|
`display` holds what the six share; the tones come from `types/tone`, also re-exported by the `base` barrel. `color` is for what is clicked (buttons, row actions, header actions, tabs), `tone` for what shows a state (pills, icon wells, banners, meters, notifications, blocks).
|
|
39
38
|
|
|
@@ -52,9 +51,9 @@ Every factory but `EmptyState` takes its options as optional, so a block placed
|
|
|
52
51
|
|
|
53
52
|
A block that can drop its frame does so with `card?: boolean` (`KeyValueList`, `EmptyState`, and the `ActivityFeed`, `Meter` and `Section` of [Lists & settings](/docs/components/lists-and-settings)); `Card` itself has `padded`. In the page editor a `fetchUrl` is picked in the data-source picker (widget `dataSource`).
|
|
54
53
|
|
|
55
|
-
## List
|
|
54
|
+
## List blocks
|
|
56
55
|
|
|
57
|
-
`StatGroupProps`, `KeyValueListProps` and `NavCardGridProps
|
|
56
|
+
`StatGroupProps`, `KeyValueListProps` and `NavCardGridProps`, and `ActivityFeedProps` ([Lists and settings](/docs/components/lists-and-settings)), extend `BaseComponentProps` and `BlockItemsSource`. Their `items` are static; a `fetchUrl` replaces them with the `items` of its answer, `{ items: T[] }`, in the same item shape. An `ActivityFeed`'s answer may add a `count` (`ActivityFeedResponse`), drawn beside its title in place of its `count` option. When there is nothing to list, the block shows `empty`.
|
|
58
57
|
|
|
59
58
|
`fetchUrl` may name the record of the page it is on: `{{params.X}}` is filled with the route parameter `X` of the page URL (`{{params.id:n}}` for the n-th of a repeated name), `{{query.X}}` with its query parameter `X`, as in a form's `fetchUrl`. The block reads it again when they change and requests nothing while a token has no value. The same holds for the `fetchUrl` of `KpiCard`, `TopListCard`, `Meter`, `ChartCard` and the charts, and for `TabProps.badgesUrl`.
|
|
60
59
|
|
|
@@ -80,7 +79,7 @@ A block that can drop its frame does so with `card?: boolean` (`KeyValueList`, `
|
|
|
80
79
|
|
|
81
80
|
`STAT_GROUP_LAYOUTS` is `joined`, `cards`; `KEY_VALUE_TYPES` is `text`, `status`, `money`, `date`, `link`, `mono`.
|
|
82
81
|
|
|
83
|
-
## Composed
|
|
82
|
+
## Composed texts
|
|
84
83
|
|
|
85
84
|
`StatGroupItem.eyebrow`, `.value` and `.detail`, `KeyValueListItem.label`, `.value` and `.detail`, `NavCardItem.title`, `.description`, `.state`, `.tag` and `.readout` lines, `ActivityFeedItem.title`, `.meta` entries, `.detail` and `.time`, `ActivityFeedProps.badge`, `MeterProps.hint` and `.valueLabel`, `MeterSegment.label`, `TopListItem.title`, `.description` and `.valueLabel`, `BannerProps.title` and `.description`, and a layout banner's `text` are typed `BlockText`: a plain string as before, or a `ComposedText` the frontend writes in the reader's locale. The block schemas accept both (`ValidateBlockOptions` refuses a parameter of an unknown `type`) and describe the field with the `text` widget. The task-shaped guide is [Composed texts](/docs/components/block-fundamentals#composed-texts).
|
|
86
85
|
|
|
@@ -99,7 +98,7 @@ A block that can drop its frame does so with `card?: boolean` (`KeyValueList`, `
|
|
|
99
98
|
| `COMPOSED_TEXT_DATE_FORMATS` | constant | `day`, `short`, `medium`, `long` (`ComposedTextDateFormat`). |
|
|
100
99
|
| `COMPOSED_TEXT_NUMBER_FORMATS` | constant | `decimal`, `percent`, `compact` (`ComposedTextNumberFormat`). |
|
|
101
100
|
|
|
102
|
-
## Static
|
|
101
|
+
## Static blocks
|
|
103
102
|
|
|
104
103
|
| Props | Options |
|
|
105
104
|
| ----------------- | ----------------------------------------------------------------------------------------------------- |
|
|
@@ -111,37 +110,37 @@ A block that can drop its frame does so with `card?: boolean` (`KeyValueList`, `
|
|
|
111
110
|
|
|
112
111
|
`EMPTY_STATE_VARIANTS` is `no-data`, `no-result`, `no-access`, `error`; `EMPTY_STATE_SIZES` is `sm`, `md`, `lg`. `BANNER_TONES` is `info`, `success`, `warning`, `error`, `primary`; `BANNER_SIZES` is `md`, `sm`. `CARD_VARIANTS` is `default`, `elevated`. The three props extend `BaseComponentProps` (`watchActions`) like the list blocks. `BannerTone` is `Extract<Tone, …>` over `BANNER_TONES`. A banner's `dismissKey` defaults to the block's id on its page (`welcome` for `.child("welcome", Banner(…))`), which holds when blocks are added or moved. A banner's `fetchUrl` answers a `BannerContent` (the content fields of `BannerProps`, its `actions` being `BannerAction[]`: a `BlockLinkAction` or a `BannerButtonAction`, the `label`, `icon`, `variant`, `color`, `target` and `confirm` of a `CustomButtonSerialized`), or `null`, `{}` or a 204 for no banner; its fields override the options one by one. `realtimeTopic` registers each topic on the page the banner is placed on, as a chart's does.
|
|
113
112
|
|
|
114
|
-
## Code
|
|
113
|
+
## Code block
|
|
115
114
|
|
|
116
115
|
`CodeBlockProps` extends `BaseComponentProps`: `code`, `language` (`CodeBlockLanguage`, default `text`), `copy` (default `true`), `title` (`BlockText`), `maxLines`, `wrap`, `fetchUrl` and `fetchUrlMethod`. `CODE_BLOCK_LANGUAGES` is `json`, `javascript`, `typescript`, `html`, `sql`, `shell`, `text`. A `fetchUrl` answers a `CodeBlockResponse`, `{ code, language? }`, which replaces `code` and `language`; it takes the `{{params.X}}` / `{{query.X}}` tokens and `BlockFunctions.REFRESH_PAGE` like the list blocks. `CodeBlock(options?)` takes its options as optional.
|
|
117
116
|
|
|
118
|
-
## Card
|
|
117
|
+
## Card slots
|
|
119
118
|
|
|
120
119
|
`Card` registers `container: true` and two slots, `CARD_ACTIONS_SLOT` (`"actions"`, next to the head's link buttons) and `CARD_FOOTER_SLOT` (`"footer"`, the foot band). Children without a slot render in the body; each slot holds one child.
|
|
121
120
|
|
|
122
|
-
```typescript
|
|
121
|
+
```typescript fragment
|
|
123
122
|
Card({ title: "Connectors", count: 4 })
|
|
124
123
|
.child("list", KeyValueList({ card: false, items }))
|
|
125
124
|
.child("foot", HStack(), { slot: CARD_FOOTER_SLOT });
|
|
126
125
|
```
|
|
127
126
|
|
|
128
|
-
## Live
|
|
127
|
+
## Live toggle
|
|
129
128
|
|
|
130
129
|
`LiveToggle(options?)` (subpath `live-toggle`, block type `LiveToggle`, component `dms-live-toggle-block`, group `data`) is the Live / Pause switch of a page declared with `live` (`PageLiveOptions` from `@antelopejs/interface-dms/page`). `LiveToggleProps` extends `BaseComponentProps`: `align` (`LIVE_TOGGLE_ALIGNS`: `left`, `center`, `right`; default `right`) `showUpdatedAt` (default `true`), the time since the page last read its data, counted in the browser, `variant` (`LIVE_TOGGLE_VARIANTS`: `segmented`, `pill`; default `segmented`), `size` (`LIVE_TOGGLE_SIZES`: `xs`, `sm`, `md`; default the holding `PageToolbar`'s, else `sm`) and `missedLabel`, an i18n key or text given `{count}` naming the missed events while paused. The state belongs to the page, not to the block; on a page without `live` the block renders nothing.
|
|
131
130
|
|
|
132
|
-
```typescript
|
|
131
|
+
```typescript fragment
|
|
133
132
|
import { LiveToggle } from "@antelopejs/interface-dms/base/live-toggle";
|
|
134
133
|
|
|
135
134
|
static toolbar = HStack().child("spacer", Spacer()).child("live", LiveToggle());
|
|
136
135
|
```
|
|
137
136
|
|
|
138
|
-
## Page
|
|
137
|
+
## Page toolbar
|
|
139
138
|
|
|
140
|
-
`PageToolbar(options?)` (subpath `page-toolbar`, block type `PageToolbar`, component `dms-page-toolbar-block`, group `data`, a container) is the context bar of a page with a period or live data, first in its body. `PageToolbarProps` extends `BaseComponentProps`: `size` (`PAGE_TOOLBAR_SIZES`: `xs`, `sm`, `md`; default `sm`), the size of the `LiveToggle` and `PeriodSelector` it holds unless they set one, and `showUpdatedAt` and `refresh` (both default `true`), the "updated N s ago" label and the refresh button it shows where no `LiveToggle` is on screen
|
|
139
|
+
`PageToolbar(options?)` (subpath `page-toolbar`, block type `PageToolbar`, component `dms-page-toolbar-block`, group `data`, a container) is the context bar of a page with a period or live data, first in its body. `PageToolbarProps` extends `BaseComponentProps`: `size` (`PAGE_TOOLBAR_SIZES`: `xs`, `sm`, `md`; default `sm`), the size of the `LiveToggle` and `PeriodSelector` it holds unless they set one, and `showUpdatedAt` and `refresh` (both default `true`), the "updated N s ago" label and the refresh button it shows where no `LiveToggle` is on screen (a page without `live`), counted from the page's last read. Its children render in one order whatever order they were added in: the other blocks, a spacer, the updated label or the `LiveToggle`, then the `PeriodSelector`.
|
|
141
140
|
|
|
142
141
|
`pageToolbar(options?)` builds one: `PageToolbarOptions` extends `PageToolbarProps` with `leading` (`Record<string, Component>`, the blocks on the left by child id), `live` (`LiveToggleProps | false`, child `live`, over `PAGE_TOOLBAR_LIVE_DEFAULTS`) and `period` (`PeriodSelectorProps | false`, child `period`, over `PAGE_TOOLBAR_PERIOD_DEFAULTS`: segmented `last-hour` / `last-24h` / `last-7-days` / `last-30-days`, `defaultPreset: "last-24h"`, `rangeLabel: "relative"`, right aligned). Unless `period` sets `comparisonControl`, it is `"menu"` when the period offers more than one comparison (`none` included; the selector's default list when `comparisons` is unset), on `defaultComparison` or else the first one besides `none`, and `"hidden"` with a single comparison or none.
|
|
143
142
|
|
|
144
|
-
```typescript
|
|
143
|
+
```typescript fragment
|
|
145
144
|
import { pageToolbar } from "@antelopejs/interface-dms/base/page-toolbar";
|
|
146
145
|
|
|
147
146
|
static toolbar = pageToolbar({
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Page layouts"
|
|
3
|
+
description: The layouts a page renders in, the buttons of its header, and pages that fill the panel.
|
|
4
|
+
mode: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
A page renders its components inside a layout. This page covers the layouts `interface-dms` exports, the header actions a page declares, and pages that fill the panel. The containers that arrange components inside a page are in [Layout and containers](/docs/interfaces/base/layout).
|
|
8
|
+
|
|
9
|
+
## Page layouts
|
|
10
|
+
|
|
11
|
+
A page layout is the third argument of `PageController`. It is optional: a page that omits it is registered with `DefaultLayout()` and its default options.
|
|
12
|
+
|
|
13
|
+
```typescript fragment
|
|
14
|
+
import {
|
|
15
|
+
PageController,
|
|
16
|
+
pagesCategory,
|
|
17
|
+
RegisterPage,
|
|
18
|
+
} from "@antelopejs/interface-dms/page";
|
|
19
|
+
import { DefaultLayout } from "@antelopejs/interface-dms/base/layouts";
|
|
20
|
+
|
|
21
|
+
@RegisterPage()
|
|
22
|
+
export class OverviewPage extends PageController(
|
|
23
|
+
"overview",
|
|
24
|
+
{ displayName: "Overview", icon: "i-ph-gauge", category: pagesCategory },
|
|
25
|
+
DefaultLayout({ hideHeader: true }),
|
|
26
|
+
) {
|
|
27
|
+
static content = overviewGrid;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`DefaultLayout(options?)` returns the `ComponentInfo` for `dms-default-layout`.
|
|
32
|
+
|
|
33
|
+
| Option | Type | Description |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `fullWidth` | `boolean` | Let the content span the whole panel instead of a constrained column. Defaults to `true`. |
|
|
36
|
+
| `hideHeader` | `boolean` | Omit the page header. |
|
|
37
|
+
| `fillHeight` | `boolean` | Let the page fill the height of the panel instead of growing with its content, for a tool that scrolls inside itself. See [Pages that fill the panel](#pages-that-fill-the-panel). Defaults to `false`. |
|
|
38
|
+
| `headerActions` | `CustomButton[]` | Buttons right of the page title, in order. See [Header actions](#header-actions). |
|
|
39
|
+
| `record` | `PageRecordLoader` | Reads the record the page shows, with the layout, for the header buttons acting on it and every `availability` of the page. See [Buttons acting on the page's record](#buttons-acting-on-the-pages-record). |
|
|
40
|
+
|
|
41
|
+
Dashboard pages are full-width unless they opt out, which is what a table, a grid of cards or a chart wants. Set `fullWidth: false` for a page that only holds a form, where a constrained column stays readable, or use `FormPageLayout`, which is exactly that:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { FormPageLayout } from "@antelopejs/interface-dms/base/layouts";
|
|
45
|
+
|
|
46
|
+
const layout = FormPageLayout({ hideHeader: true });
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Its optional argument accepts `DefaultLayoutOptions` without `fullWidth`; the return value is a `ComponentInfo`.
|
|
50
|
+
|
|
51
|
+
The generated `new` and `edit` pages of a table view use it, unless their page's category chain declares a layout, so a form page you write by hand matches them without your having to remember the flag.
|
|
52
|
+
|
|
53
|
+
`SettingsLayout(options?)` returns the `ComponentInfo` for `dms-settings-layout`: the dashboard frame with the settings navigation next to the page, in the column width every settings page shares. Its options are `DefaultLayoutOptions` without `fullWidth`. `settingsCategory` declares it as its `layout`, so the pages under the settings root get it without naming it ([Settings pages](/docs/building/settings-pages)).
|
|
54
|
+
|
|
55
|
+
`EmptyLayout()` takes no options and returns the `ComponentInfo` for `dms-empty-layout`, with an empty options object. Use it for pages that draw their own chrome, such as sign-in and onboarding screens.
|
|
56
|
+
|
|
57
|
+
Because they return a plain `ComponentInfo` rather than a `ComponentBuilder`, the page layouts have no `.child()`, `.meta()` or `.watch()`.
|
|
58
|
+
|
|
59
|
+
### Header actions
|
|
60
|
+
|
|
61
|
+
A page puts its main actions next to its title, such as a primary "New invoice", an "Export" or a link to the docs, with `headerActions`: the same `CustomButton`s as a table's toolbar (see [Tables](/docs/components/tables)), plus an `order` (`PageHeaderButton`), so a header button opens a link, runs a [quick action](/docs/interfaces/core/quick-actions), opens a drawer or a modal, calls an API or runs an export, with a confirmation and an availability of its own:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { DefaultLayout } from "@antelopejs/interface-dms/base/layouts";
|
|
65
|
+
|
|
66
|
+
DefaultLayout({
|
|
67
|
+
headerActions: [
|
|
68
|
+
{
|
|
69
|
+
id: "docs",
|
|
70
|
+
label: "Documentation",
|
|
71
|
+
icon: "i-ph-book-open",
|
|
72
|
+
target: { type: "external", url: "https://antelopejs.com/docs", newTab: true },
|
|
73
|
+
},
|
|
74
|
+
// An empty label lends the quick action's name (and its icon).
|
|
75
|
+
{ id: "new", label: "", color: "primary", target: { type: "quickAction", id: "billing:new-invoice" } },
|
|
76
|
+
{
|
|
77
|
+
id: "audit",
|
|
78
|
+
label: "Audit log",
|
|
79
|
+
icon: "i-ph-scroll",
|
|
80
|
+
target: { type: "page", url: "/audit" },
|
|
81
|
+
permission: "audit", // pages.billing.audit on the page pages.billing
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Field | Type | Description |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `id` | `string` | Key of the button, unique in the header. Defaults to its place (`header-0`, …). |
|
|
90
|
+
| `label` | `string` | Button text (i18n key with `$` or literal). A quick action lends its own name to an empty label. |
|
|
91
|
+
| `icon` | `string` | Icon name. A quick action lends its own icon. |
|
|
92
|
+
| `target` | `ActionTarget` | What the button does: `page` or `external` (drawn as a link), `quickAction` (`{ id }`: `category:id`, or the bare id), `drawer`, `modal`, `api`, `exportJob`, `download` (`{ url, filename? }`: fetched with the user's session and saved). |
|
|
93
|
+
| `confirm` | `ConfirmDialog \| { from }` | Asked before the target runs. |
|
|
94
|
+
| `color` | `"primary" \| "neutral" \| …` | Defaults to `neutral`, an outline button; any other color is solid. |
|
|
95
|
+
| `variant` | `ButtonVariant` | Overrides the fill derived from the color. |
|
|
96
|
+
| `permission` | `string \| Action` | Action of the page, relative to its permission id (`"export"` on `pages.sales` → `pages.sales.export`), or an `Action` of any component, the user must hold to see the button. |
|
|
97
|
+
| `permissionId` | `string` | Absolute permission id the user must hold to see the button; wins over `permission`. |
|
|
98
|
+
| `availability` | `CustomButtonAvailability` | Disables the button for a request it answers `{ reason }` with, the reason shown in a tooltip; leaves it out of the header for one it answers `{ hidden: true }`. Receives the page URL's query as `context.query`. |
|
|
99
|
+
| `order` | `number` | Place among the header's buttons, smallest first; defaults to `0`. See [Ordering the header](#ordering-the-header). |
|
|
100
|
+
|
|
101
|
+
#### Buttons that follow the page URL
|
|
102
|
+
|
|
103
|
+
A page scoped by its URL, such as `?workspace=web` or a record id in the path, declares its buttons once and lets them follow the URL. A target's URL (`page`, `external`, `download`, `api`, `exportJob`) takes the tokens of a block's `fetchUrl`: `{{params.X}}` is filled with the route parameter `X` of the page, `{{query.X}}` with its query parameter `X`, percent-encoded, again whenever the URL changes. A query pair whose value is a token the page has no value for is left out (`?workspace={{query.workspace}}` on a page without `?workspace=` leads to the target's default), and any other token without a value is left empty. A `download` target's `filename` takes the same tokens, written as they are; while one has no value, the file is saved under the server's name:
|
|
104
|
+
|
|
105
|
+
```ts fragment
|
|
106
|
+
{
|
|
107
|
+
id: "download-language",
|
|
108
|
+
label: "$lang.queue.download", // "Download {locale}.json"
|
|
109
|
+
icon: "i-ph-download-simple",
|
|
110
|
+
target: {
|
|
111
|
+
type: "download",
|
|
112
|
+
url: "/api/lang/translations/export?workspace={{query.workspace}}&locale={{query.locale}}",
|
|
113
|
+
filename: "{{query.locale}}.json",
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
A label's translation takes the page's route and query parameters as its own (a query parameter wins over a route parameter of the same name), so `"Download {locale}.json"` names the language of `?locale=fr` and follows it as the URL changes.
|
|
119
|
+
|
|
120
|
+
An `availability` receives the page URL's query as `context.query` (one value per name; caller-supplied, so check it like any input), its route parameters as `context.params` (`/projects/project/p1` on a page registered at `/projects/project/:id` is `{ id: "p1" }`), and the record the layout read as `context.record` ([Buttons acting on the page's record](#buttons-acting-on-the-pages-record)). The header asks again whenever the query or the route parameters change, and once a write refreshed the page (only when one of its buttons declares an `availability` or the layout reads a `record`), and keeps the buttons it has until the answer lands. The keys the DMS writes for the view itself never cause a request: `record` (the row open in a side panel), `live` (the paused live mode) and a period selector's `<id>.period`, `<id>.compare`, `<id>.from`, `<id>.to`, `<id>.compareFrom` and `<id>.compareTo`. A query key may carry dots, so `{{query.api.period}}` reads a period selector's choice.
|
|
121
|
+
|
|
122
|
+
```typescript fragment
|
|
123
|
+
DefaultLayout({
|
|
124
|
+
headerActions: [
|
|
125
|
+
{
|
|
126
|
+
id: "matrix",
|
|
127
|
+
label: "$lang.queue.open_matrix",
|
|
128
|
+
icon: "i-ph-table",
|
|
129
|
+
target: { type: "page", url: "/modules/lang/translations?workspace={{query.workspace}}&missingIn={{params.locale}}" },
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "download",
|
|
133
|
+
label: "$lang.queue.download",
|
|
134
|
+
target: { type: "api", url: "/api/lang/export?workspace={{query.workspace}}", successMessage: "$lang.exported" },
|
|
135
|
+
availability: async ({ query }) =>
|
|
136
|
+
(await workspaceExists(query?.workspace)) ? undefined : { reason: "$lang.workspace.unknown" },
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A table view of the page places its own buttons in the header with `placement: "header"`: a custom button, or its built-in add (`rowActions: { add: { placement: "header" } }`): the server adds them after the layout's own buttons, served as the table view was (left out without its permission or when its `availability` hides it, disabled with the reason its `availability` gives), and the table view runs them, in place. They take the order `0`.
|
|
143
|
+
|
|
144
|
+
Who sees a button is settled on the server when the layout is served: a button whose `permission` or `permissionId` the user lacks is left out of the layout, and a quick-action button only shows to the users the quick action is served to, which are the users its page admits. A page drawn by a Vue page component can add its own buttons next to these with `usePageHeaderActions`; call it before the first `await` of the component's setup, so the server renders the buttons with the page and they never pop in. A page whose changes save as soon as they are made shows the shared **Saved instantly** pill there with `useInstantSaveHeader` ([Instant save or a save bar](/docs/components/lists-and-settings/section#instant-save-or-a-save-bar)).
|
|
145
|
+
|
|
146
|
+
#### Buttons acting on the page's record
|
|
147
|
+
|
|
148
|
+
A page about one record (a site, an invoice) reads it with its layout: `record` is a function the server calls each time it serves the layout, with the request's context (the page URL's `params` and `query`, the `user` and the `tenantId`), and answers the record, or `undefined` when there is none to show. A button's target names its fields as `{{record.X}}`, filled in by the server and percent-encoded: a maps link to the site's address, an API call on its own id. A button acting on the record, one whose target names `{{record.X}}` or declaring `requiresRecord: true` (an "Edit" link that only needs `{{params.id}}`), is left out of a page whose record was not found, or whose `record` threw (logged). Every `availability` of the page reads the record as `context.record`, so a button can be disabled on the record's state without reading it again:
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { BasicDataModel, Field, GetModel, RegisterTable, Table } from "@antelopejs/interface-database-decorators";
|
|
152
|
+
import { DefaultLayout } from "@antelopejs/interface-dms/base/layouts";
|
|
153
|
+
|
|
154
|
+
@RegisterTable("sites", "example_sites")
|
|
155
|
+
class Site extends Table {
|
|
156
|
+
@Field("string") declare address: string;
|
|
157
|
+
@Field("boolean") declare closed: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
class SiteModel extends BasicDataModel(Site, "sites") {}
|
|
161
|
+
|
|
162
|
+
DefaultLayout({
|
|
163
|
+
record: async ({ params }) => {
|
|
164
|
+
const site = await GetModel(SiteModel).get(params?.id ?? "");
|
|
165
|
+
return site && { ...site };
|
|
166
|
+
},
|
|
167
|
+
headerActions: [
|
|
168
|
+
{
|
|
169
|
+
label: "$sites.directions",
|
|
170
|
+
icon: "i-ph-map-pin",
|
|
171
|
+
target: { type: "external", url: "https://www.google.com/maps/search/?api=1&query={{record.address}}", newTab: true },
|
|
172
|
+
},
|
|
173
|
+
{ label: "$sites.edit", requiresRecord: true, target: { type: "page", url: "/sites/{{params.id}}/edit" } },
|
|
174
|
+
{
|
|
175
|
+
label: "$sites.close",
|
|
176
|
+
target: { type: "api", url: "/api/sites/close?id={{params.id}}", successMessage: "$sites.closed" },
|
|
177
|
+
availability: ({ record }) => (record?.closed ? { reason: "$sites.already_closed" } : undefined),
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The record travels with the layout, so the header is drawn with it, on the server too, and never waits for it. Like an `availability`, it follows the page URL: the header asks for its buttons again when the route parameters or the query change, and once a write refreshed the page (`refreshPageBlocks()`, which a header button calls after a change, and a form once it saved). Its fields only shape the buttons; the route behind each one still checks the record on its own.
|
|
184
|
+
|
|
185
|
+
#### A record deleted under the page
|
|
186
|
+
|
|
187
|
+
A page reading a `record` leaves it once it is deleted, in every tab and for every member on it, with no code of the module. On each event of one of the page's realtime topics (its table views' rows, a `realtimeTopic`, a topic bound with `RegisterPageTopic`), the page asks its layout again: a record the server found before and now finds none of (its `record` answers `undefined`) was deleted. A toast says so, and the page opens the list it is a detail of (`MenuOptions.parent`); without one, it stays, on the "not found" state of its blocks. A `record` that throws counts as unknown, never as deleted, and a page opened on a record that never existed shows its blocks' "not found" state without a toast. Publish on one of the page's topics when the record is deleted: a table view's delete does it on its own.
|
|
188
|
+
|
|
189
|
+
#### Hidden or disabled
|
|
190
|
+
|
|
191
|
+
An `availability` has three answers. `undefined` leaves the button as declared, `{ reason }` disables it with the reason in a tooltip, for something the user can change ("No seat left: upgrade the plan"). `{ hidden: true }` leaves it out of the header, as a missing permission does, for a button that makes no sense in the scope the page shows. Like a disabled one, a hidden button follows the page URL: the header asks again when the query changes, so a button can disappear on one `?workspace=` and come back on another, and the server-rendered header already carries the answer for the URL it was rendered for.
|
|
192
|
+
|
|
193
|
+
```typescript fragment
|
|
194
|
+
DefaultLayout({
|
|
195
|
+
headerActions: [
|
|
196
|
+
{
|
|
197
|
+
id: "add-key",
|
|
198
|
+
label: "$lang.keys.add",
|
|
199
|
+
color: "primary",
|
|
200
|
+
target: { type: "drawer", component: AddKeyForm() },
|
|
201
|
+
availability: async ({ query }) =>
|
|
202
|
+
(await workspaceOf(query?.workspace))?.readOnly ? { hidden: true } : undefined,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A detail page reads its record from `context.params`, and hides the buttons that act on it when the record does not exist. The page then shows its "not found" state with no "Edit" or "Delete" beside the title:
|
|
209
|
+
|
|
210
|
+
```typescript fragment
|
|
211
|
+
DefaultLayout({
|
|
212
|
+
headerActions: [
|
|
213
|
+
{
|
|
214
|
+
id: "edit",
|
|
215
|
+
label: "$projects.edit",
|
|
216
|
+
target: { type: "page", url: "/projects/project/{{params.id}}/edit" },
|
|
217
|
+
availability: async ({ params }) =>
|
|
218
|
+
(await projectExists(params?.id)) ? undefined : { hidden: true },
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Hiding only shapes the UI: the route behind the button must still refuse the operation on its own.
|
|
225
|
+
|
|
226
|
+
#### Ordering the header
|
|
227
|
+
|
|
228
|
+
The header shows, left to right, the buttons the layout declares, the buttons a table view places there (`placement: "header"`), then the actions a Vue component of the page adds with `usePageHeaderActions` ([UI toolkit](/docs/frontend-composables/ui-toolkit#page-header-actions-usepageheaderactions)). To interleave them, give a button an `order` and the component's actions one too (`usePageHeaderActions(render, { order })`): the header sorts everything by `order`, smallest first, `0` when none is given, and at an equal order keeps the served buttons first, in the order above. So `{ order: -1 }` on a component's actions puts them before every served button left at the default:
|
|
229
|
+
|
|
230
|
+
```typescript fragment
|
|
231
|
+
// The served "Add key" stays at the default order, 0.
|
|
232
|
+
DefaultLayout({ headerActions: [{ id: "add-key", label: "$lang.keys.add", color: "primary", target }] });
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```vue [app/components/TranslationMatrix.vue]
|
|
236
|
+
<script setup lang="ts">
|
|
237
|
+
import { usePageHeaderActions } from "#dms-layout/app/composables/layout/usePageHeaderActions";
|
|
238
|
+
|
|
239
|
+
// Export, built in the browser, goes before the served "Add key".
|
|
240
|
+
usePageHeaderActions(() => h(UButton, { label: t("lang.export"), icon: "i-ph-download-simple", onClick: exportCsv }), { order: -1 });
|
|
241
|
+
</script>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Pages that fill the panel
|
|
245
|
+
|
|
246
|
+
A page built around one tool, such as a file explorer, a data grid or a graph canvas, usually wants that tool to take the height of the panel and scroll inside itself, rather than make the whole page scroll. Set `fillHeight` on its layout:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { DefaultLayout } from "@antelopejs/interface-dms/base/layouts";
|
|
250
|
+
|
|
251
|
+
DefaultLayout({ fillHeight: true })
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The layout then lays the page out as a flex column the height of the panel. The page header keeps its own height and the height left under it goes to the **last component** of the page; the components above it keep theirs. Give the root of that component `flex-1 min-h-0`, and let an element inside it scroll:
|
|
255
|
+
|
|
256
|
+
```vue [app/components/MyExplorer.vue]
|
|
257
|
+
<template>
|
|
258
|
+
<div class="flex min-h-0 flex-1 flex-col gap-4">
|
|
259
|
+
<MyToolbar />
|
|
260
|
+
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
261
|
+
<!-- the rows -->
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</template>
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
A custom page, a page component your frontend layer registers under the page's slug, sits straight in the layout, so its own root takes `flex-1 min-h-0` the same way.
|
|
268
|
+
|
|
269
|
+
Do not size the tool from the viewport, with `h-[calc(100vh-…)]` or an offset measured in the browser. The dashboard header, the [layout banners](/docs/interfaces/core/layout-banners), the page header and the panel padding all take height above the page, and that height changes: a banner shows up or is dismissed, a description wraps. The layout accounts for all of it.
|
|
270
|
+
|
|
271
|
+
- **Minimum height.** The page area, header included, never gets shorter than 30rem: on a shorter panel it stops shrinking and the panel scrolls again. The floor sits on the page area rather than on the tool, because a `min-h-*` set inside the page overflows the column, and the panel then scrolls past its own bottom padding. A tool that needs more room than what is left to it at that floor, such as one that shares its component with a card of figures, can still set its own `min-h-*`, at that cost.
|
|
272
|
+
- **Every width.** The page fills the panel on a phone too: the panel is the part of the dashboard that scrolls at every width. A page whose panes stack on a narrow screen (`grid-cols-1 lg:grid-cols-[…]`) decides how they share the height there, or lets its own root scroll below that breakpoint (`max-lg:overflow-y-auto`).
|
|
273
|
+
- **Several components.** Only the last one fills. Put the tool last, under whatever summarizes it, or keep both in one component that lays them out itself.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Table row actions"
|
|
3
|
+
description: The row actions of a table view, archive mode, and the guards that run before each write.
|
|
4
|
+
mode: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
These are the options of a [table view](/docs/interfaces/base/tables) that act on its rows: row actions and their extras, archive mode, and the guards its routes run before each write.
|
|
8
|
+
|
|
9
|
+
## Row actions
|
|
10
|
+
|
|
11
|
+
`TableViewRowActionOptions<T>` configures the row menu. Every built-in action takes `true`, `false`, or a `RowActionConfig<T>` carrying a rule.
|
|
12
|
+
|
|
13
|
+
| Option | Type | Description |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `add` | `boolean \| RowActionConfig<T>` | Create a row. |
|
|
16
|
+
| `edit` | `boolean \| RowActionConfig<T>` | Edit a row. |
|
|
17
|
+
| `details` | `boolean \| RowActionConfig<T>` | Open the read-only view. |
|
|
18
|
+
| `duplicate` | `boolean \| RowActionConfig<T>` | Open the `new` form pre-filled from the row. |
|
|
19
|
+
| `delete` | `boolean \| RowActionConfig<T>` | Delete a row. |
|
|
20
|
+
| `archive` | `boolean \| RowActionConfig<T>` | Archive a row. |
|
|
21
|
+
| `restore` | `boolean \| RowActionConfig<T>` | Restore an archived row. |
|
|
22
|
+
| `copyLink` | `boolean \| RowActionConfig<T>` | Copy a link to the row. |
|
|
23
|
+
| `hasSelection` | `boolean` | Enable row selection. |
|
|
24
|
+
| `custom` | `CustomRowAction<T>[]` | Extra actions pointing at an `ActionTarget`. |
|
|
25
|
+
|
|
26
|
+
```typescript fragment
|
|
27
|
+
static table = TableView(memberDataAPI, {
|
|
28
|
+
rowActions: {
|
|
29
|
+
add: false,
|
|
30
|
+
edit: { isEnabled: true, isVisible: true },
|
|
31
|
+
delete: { isEnabled: true, rule: { field: "isTenantOwner", notEquals: true } },
|
|
32
|
+
hasSelection: true,
|
|
33
|
+
custom: [
|
|
34
|
+
{
|
|
35
|
+
label: "Validate email",
|
|
36
|
+
icon: "i-ph-check-circle",
|
|
37
|
+
target: {
|
|
38
|
+
type: "api",
|
|
39
|
+
url: "/settings/workspace/members/{{row._id}}/validate-email",
|
|
40
|
+
method: "POST",
|
|
41
|
+
successMessage: "Email validated",
|
|
42
|
+
},
|
|
43
|
+
rule: { field: "isValidated", notEquals: true },
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A double-click on a row opens `edit`, or `details` when edit is off. It is a single click on a table declaring `rowClick: "single"` (`TableViewRowClick`), which leaves a click on a control of the row (a button, a link, a checkbox) or one ending a text selection to them. A custom action flagged `isDefault` takes that slot instead. The first flagged action whose `rule` accepts the row wins, and the click opens its `ActionTarget` like the menu entry does. This is how a table whose detail view is a custom drawer keeps the row affordance: without a click target the row is inert and no longer lights up on hover.
|
|
51
|
+
|
|
52
|
+
```typescript fragment
|
|
53
|
+
custom: [
|
|
54
|
+
{
|
|
55
|
+
label: "Details",
|
|
56
|
+
icon: "i-ph-info",
|
|
57
|
+
isDefault: true,
|
|
58
|
+
target: { type: "drawer", component: CustomComponent("SendDetailDrawer"), title: "Details" },
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Rules are enforced on both sides. A data controller has one **writing** table view: a `TableView` enabling at least one write action (add, duplicate, edit, delete, archive or restore) that the controller offers. Its `rowActions` (archive-mode defaults included) become the controller's `controllerRowActionRules`, along with its `rowIdKey` and `strictRuleValidation`. The `edit`, `delete`, `archive` and `restore` routes validate the affected ids against those rules before running. Rows that fail the rule are dropped from the operation; with `strictRuleValidation` the whole request is rejected instead.
|
|
64
|
+
|
|
65
|
+
A second writing table view over the same controller fails the registration of its page with a `WritingTableViewConflictError` (a `PageDeclarationConflictError`, rethrown even from a nested component) naming both table views. Read-only table views, with every write action set to `false` and no `archiveMode`, may share the controller: they open no `new` / `edit` form and leave its rules and options alone. A page re-registering (a hot reload) re-claims the controller; a page that went away releases it. To give the same data a second writing screen, derive a controller (see [Show the same data in two writing screens](/docs/components/derived-controller)).
|
|
66
|
+
|
|
67
|
+
Permissions are applied on top. The table view registers `list` and `select` as component actions always, `add`, `edit` and `view` when the matching generated form exists, `delete` when the controller declares the delete endpoint, `archive`, `restore` and `viewArchived` under `archiveMode`, and `export` when the three export endpoints are registered. Each action carries a permission id of the form `<component-permission>.<action>`. When the viewer lacks the permission the corresponding row action is forced to `false` and the corresponding generated form is withheld. The rule algebra itself (leaf operators, `and`, `or`, `not`, and `RowActionConfig`) is documented in [Shared types](/docs/interfaces/base/types).
|
|
68
|
+
|
|
69
|
+
Several pages may each mount their own `TableView` over one data controller, and they all share its data routes. A read action (`list`, `select`, `view`, `export`, `viewArchived`) accepts the permission from any table view a page mounts on that controller; a write action (`add`, `edit`, `delete`, `archive`, `restore`) accepts only the writing table view's, so a user granted `add` on a read-only table view cannot create rows. A read-only table view still declares the write actions the controller offers: while no table view writes through the controller, they keep its write routes behind permission ids nobody is granted by default. The inline add button of a relation field pointing at the controller follows the same rule (`addPermissionIds`).
|
|
70
|
+
|
|
71
|
+
A `new` or `edit` `ResourceForm` block over the controller submits to the same routes, so the native files staged from its fields are accepted on save as well, alongside those of the writing table view. It adds no permission of its own to the union: the routes still require the action's permission from a table view of the controller when one is mounted, exactly as for the block's other fields. The `new` and `edit` forms a page-mode table view mounts on its form sub-pages count the same way: each is a component of its own sub-page, so the files staged from it are accepted on save, under the same table view permissions.
|
|
72
|
+
|
|
73
|
+
`TableViewRowActionOptionsSerialized` is the transported shape: rules and flags untyped, `custom` as `CustomRowActionSerialized[]`, plus a `showArchived?: boolean` field the builder fills from the `viewArchived` permission.
|
|
74
|
+
|
|
75
|
+
### Row action extras
|
|
76
|
+
|
|
77
|
+
`TableViewRowActionOptions` also takes `menuLabel`: the accessible name of a row's ⋯ menu button, its `{field}` braces filled from the row (a `$` key's translation receives the row's fields), "Actions for <label>" by default with `labelKey`, "Row actions" without. A `CustomRowAction`'s `label` fills its `{field}` braces from the row the same way.
|
|
78
|
+
|
|
79
|
+
`RowActionConfig` (built-in actions) also takes `label`, `icon` and `confirm` (an `ActionConfirm`: a `ConfirmDialog`, or `{ from }`, a URL answering one for the row, on delete, archive and restore, for a single row); `BulkRowActionConfig` (delete, archive, restore) adds `successMessage`. `CustomRowAction` also takes `confirm`, `permission` (`ButtonPermission`: an action of the table by name, or an `Action`), `permissionId` (an absolute permission id, which wins over `permission`), `color`, `variant`, `showLabel`, `bulk` (`true` or `{ allMatching: true }`: offered on the selection bar, see below) and `deepLink` (a drawer, modal or side panel it opens is kept in the URL as `?record=<id>` (by default for a side panel, which `false` opts out of), or `?<tableId>.record=<id>` on a page with several table views). An action the caller fails the permission for is stripped per request like a custom button; it is only hidden, so the route its target calls must check the permission itself.
|
|
80
|
+
|
|
81
|
+
A bulk custom action sends the selected ids as a repeated `ids` query parameter, or `allMatching=true` with the table's filters, search and archive view after "Select all N matching". `resolveBulkRowIds(this, ctx, user)`, called from a route of the table's data controller, or `resolveBulkRowIds({ model, tableView }, ctx, user)` (`BulkRowSource`: the model the calling route is given, and the table's `TableViewMeta`) from any other route, a page's included, answers the ids the action covers: the selected ones, or every matching row read like the list reads them, refusing more than `MAX_BULK_MATCHING_ROWS` (413). Either way the caller needs the table's `list` permission (403 otherwise). `BULK_ALL_MATCHING_KEY` is the query flag. The `api` action target also takes `body` (the confirmation's field values merged in) and `copy` (response field copied to the clipboard). `CustomButton` takes `placement` (`"toolbar"`, the default, or `"header"`: drawn in the page header, which the server adds it to); `rowActions.add` takes it too (`AddRowActionConfig`).
|
|
82
|
+
|
|
83
|
+
When several table views share a data controller, each read action of its routes is guarded by the permission ids of every mounted table view, each write action by the writing table view's (`TableViewMeta.actionPermissionIds(action)`; any one suffices). `TableViewMeta.componentBuilders` lists them, `writingComponentBuilders` the ones that write.
|
|
84
|
+
|
|
85
|
+
## Archive mode
|
|
86
|
+
|
|
87
|
+
With `archiveMode: true`, the archive and restore actions replace deletion. The builder calls `applyArchiveModeDefaultRules` (see [Helpers](/docs/interfaces/base/helpers)) to give every relevant action a default rule against the `@ArchiveField` column, so archived rows offer restore and delete while live rows offer edit, duplicate and archive. Actions you configured keep their other settings; every action not turned off receives the default rule, ANDed with its own rule when it has one.
|
|
88
|
+
|
|
89
|
+
```typescript fragment
|
|
90
|
+
static table = TableView(articleDataAPI, {
|
|
91
|
+
archiveMode: true,
|
|
92
|
+
rowActions: { edit: true, delete: true, archive: true, restore: true },
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Guards
|
|
97
|
+
|
|
98
|
+
A guard runs before the mutating route and aborts it by throwing. Use `assert` from `@antelopejs/interface-api-util` to fail with an HTTP status and a message key. `this` inside a guard is the data controller instance.
|
|
99
|
+
|
|
100
|
+
```typescript fragment
|
|
101
|
+
import { assert } from "@antelopejs/interface-api-util";
|
|
102
|
+
import { getRequestTenantId } from "@antelopejs/interface-dms/request-tenant";
|
|
103
|
+
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
104
|
+
import { TenantMemberModel } from "@antelopejs/interface-dms/db";
|
|
105
|
+
|
|
106
|
+
static table = TableView(memberDataAPI, {
|
|
107
|
+
guards: {
|
|
108
|
+
edit: async (ctx, { current, body }) => {
|
|
109
|
+
const demoting = current.isTenantOwner && body.isTenantOwner === false;
|
|
110
|
+
if (!demoting) return;
|
|
111
|
+
const model = GetModel(TenantMemberModel, getRequestTenantId(ctx));
|
|
112
|
+
const remaining = await model.countOwnersExcluding([current.userId]);
|
|
113
|
+
assert(remaining > 0, 409, "$page.settings.members.error.last_owner");
|
|
114
|
+
},
|
|
115
|
+
delete: async (ctx, { ids }) => {
|
|
116
|
+
// ids is the full set of rows the request targets
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The `edit` guard receives `{ id, body, current }`, where `current` is the row as it stands in the database. The route loads it before calling the guard and answers `404` when it does not exist. The `new` guard receives `{ body }`, parsed from the request body whether it arrived as a buffer, a string or an object. The `delete`, `archive` and `restore` guards receive `{ ids, values }`: `values` is the answer of the action's confirmation `fields`, sent as the request body, and is unset when the request carried none (see [Confirmations](/docs/components/tables/confirmations#what-the-rows-take-with-them)). Like the rules, guards are stored on the controller metadata the first time a table view declares them, so they apply to every table view on that controller. The argument interfaces are documented in [Shared types](/docs/interfaces/base/types).
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Table filters and forms"
|
|
3
|
+
description: The filters, tabs and displays of a table view, and the add, edit and view forms it generates.
|
|
4
|
+
mode: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
These are the options of a [table view](/docs/interfaces/base/tables) that narrow and show its rows, and the forms it generates from the controller's fields.
|
|
8
|
+
|
|
9
|
+
## Filters, tabs and displays
|
|
10
|
+
|
|
11
|
+
`defaultFilters` seeds the visible filter bar. `queryParamFilters` and `routeParamFilters` add hidden filters instead: each is a `Record<string, QueryParamFilter>` / `Record<string, RouteParamFilter>` mapping a URL parameter name onto `{ field: string; mode?: string }`, filtering the table without appearing in the UI or in the viewer's stored preferences. A `QueryParamFilter` also takes `visible?: boolean` and `label?: string`: a visible one shows as a removable chip of the filters row while the URL applies it. `queryParamFilters` reads the page's query string, `routeParamFilters` reads the parameters of the page slug (for example `:id` on a detail page). Both also seed `submitDefaults` on the generated forms, so a row created from a filtered view inherits the filter's value. An active `queryParamFilters` filter makes the table read as filtered (its empty body is the `filtered` state, without a clear action), and a change of its value in the URL lists from page 1.
|
|
12
|
+
|
|
13
|
+
```typescript fragment
|
|
14
|
+
static table = TableView(invoiceDataAPI, {
|
|
15
|
+
routeParamFilters: { id: { field: "customerId" } },
|
|
16
|
+
defaultSort: { field: "createdAt", desc: true },
|
|
17
|
+
tabs: [
|
|
18
|
+
{ id: "open", label: "Open", filter: { accessorKey: "status", value: "open", mode: "is" } },
|
|
19
|
+
{ id: "paid", label: "Paid", filter: { accessorKey: "status", value: "paid", mode: "is" } },
|
|
20
|
+
],
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`TableViewTab` is `{ id, label, filter?: TableViewTabFilter, icon?, textColor?: ColorValue, iconColor?: ColorValue }` plus the link fields (`to`, `countFrom`), the gate (`permission`, `permissionId`) and `navBadge?: boolean | TableViewTabNavBadge` (`{ tone?: Tone }`), and `TableViewTabFilter` is `{ accessorKey: string; value?: string; mode: string }`. A tab gives a `filter` or a `to`, never both (registration throws). `permission` is relative to the table view's permission id (`"list"` → `<table permission id>.list`) or an `Action`; `permissionId` is absolute and wins; a link tab to a page controller follows that page's own permission without either. In the page editor, the tab's permission picker sets `permissionId`, and `permission` is the advanced "Action" text field.
|
|
25
|
+
|
|
26
|
+
`navBadge: true` publishes the tab's counter as the navigation badge of the page it stands for: this table's page for a filter tab, the linked page for a link tab, which then needs a page controller `to` and a data controller `countFrom`; a path or a location logs a warning and publishes nothing. The server counts it on each page visit (the page payload's `navBadges`, never the cached site layout), with the caller's own `list` permission on that table, for the pages the caller can open only; the badge shows in the main and the settings navigation. A count of zero shows no badge, a page's static `MenuOptions.badge` wins over it, and while the table is on screen its own counter keeps it fresh. `TableView.fromSource` tables do not support it. `navBadge: { tone }` draws the badge in that tone instead of neutral; counts landing on one page add up, in the strongest tone. Declaring at least one tab also surfaces an implicit "all" tab that applies no filter.
|
|
27
|
+
|
|
28
|
+
`displays` declares alternative renderings of the same data:
|
|
29
|
+
|
|
30
|
+
| Field | Type | Description |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `id` | `string` | Display identifier. Required. |
|
|
33
|
+
| `options` | `Record<string, unknown>` | Options handed to the display. |
|
|
34
|
+
| `component` | `Component` | Frontend component, resolved by name from the global registry. |
|
|
35
|
+
| `selfManagedData` | `boolean` | The display fetches its own data. |
|
|
36
|
+
| `capabilities` | `TableViewDisplayCapabilities` | Which toolbar features the display supports: `columnManagement`, `filters`, `search`, `sorting`, `tabs`, all optional booleans. |
|
|
37
|
+
|
|
38
|
+
`TableViewDisplayOptionSerialized` is the same with `component` serialized.
|
|
39
|
+
|
|
40
|
+
`kanban` is a shorthand for one particular display: the builder appends a `displays` entry with id `"kanban"` carrying the kanban options, unless `displays` already places `{ id: "kanban" }`, which then receives them.
|
|
41
|
+
|
|
42
|
+
| Option | Type | Description |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `groupByField` | `string` | Column the cards are grouped by. Must be a non-multiple `SelectType`, a `BooleanType` or a `StatusType` column. Required. |
|
|
45
|
+
| `draggable` | `boolean` | Allow dragging a card to another column to change the group field. Defaults to `true`. |
|
|
46
|
+
| `columnMaxHeight` | `string` | CSS max-height of a column's card list before it scrolls. Defaults to `"60vh"`. |
|
|
47
|
+
|
|
48
|
+
`KanbanOptionsSerialized` is `KanbanOptions` plus the serialized `card` (`TableViewCardOptionsSerialized`).
|
|
49
|
+
|
|
50
|
+
`card` (`TableViewCardOptions`: `{ fields?: string[]; component?: Component; minWidth?: string }`) is the card the `kanban` and `cards` displays draw for each row; it joins the options of both display entries. The component receives the same props in both: `row`, `rowId`, `columns`, `labelKey`, `actions`, `selected`, `select(value?)`, `open()`, on the kanban `groupValue`, and on the `cards` display of a table ordered by hand `reorder` (`{ enabled, dragging, handle }`, the card's move handle).
|
|
51
|
+
|
|
52
|
+
## Generated forms
|
|
53
|
+
|
|
54
|
+
`TableView` builds up to three forms from the columns, one per `FormMode`, using each column's data type for the input and the controller's mandatory and access metadata for required and disabled state. The `new` form fetches from `<location>/get?id={{query.duplicate}}` and submits to `<location>/new`; the `edit` form fetches from `<location>/get?id={{params.id}}&_presence=1` (the extra parameter makes the `Get` route record the viewer's presence on that row) and submits to `<location>/edit?id={{params.id}}`; the `view` form only fetches, from `<location>/get?id={{params.id}}`. A mode whose column set is empty produces no form and no matching action.
|
|
55
|
+
|
|
56
|
+
`formContainer` decides where they open:
|
|
57
|
+
|
|
58
|
+
```typescript fragment
|
|
59
|
+
type FormContainer =
|
|
60
|
+
| { type: "drawer"; pages?: FormContainerPages<FormContainerPageTexts> }
|
|
61
|
+
| { type: "modal"; size?: ModalSize; pages?: FormContainerPages<FormContainerPageTexts> }
|
|
62
|
+
| { type: "page"; pages?: FormContainerPages<FormContainerPageConfig> };
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Page mode is the default, used when `formContainer` is omitted. In page mode the builder registers a hidden sub-page per form under the table's page, each guarded by the matching action's permission.
|
|
66
|
+
|
|
67
|
+
The sub-pages are named after the **table view**, not after the page: their default slugs are `<key>/new`, `<key>/:id/edit` and `<key>/:id/view`, where `<key>` is the key the table view is mounted under, so `static invoices = TableView(...)` on `/billing` gives `/billing/invoices/new`. It is the *simple* key even for a table view nested deep in a layout (`.child("invoiceTable", TableView(...))` gives `invoiceTable`, not `layout/body/main/invoiceTable`), so the URL stays readable and survives the layout being reorganised. That is what lets one page carry several table views, nested or not, each with form pages of its own. The key does not depend on the page declaring a `permission.id` of its own. A table view instance belongs to the first page that mounts it: its form routes cannot be keyed on a second page mounting the same instance in page mode, which is an error, so give each page its own `TableView(...)`.
|
|
68
|
+
|
|
69
|
+
`pages` (`{ new?, edit?, details? }`) titles the forms in every container: a `FormContainerPageTexts` entry is `{ displayName?, description? }` (`$`-prefixed: i18n keys), falling back to "New entry", "Edit entry" and "Entry details". In page mode an entry is a `FormContainerPageConfig`, which also overrides the sub-page:
|
|
70
|
+
|
|
71
|
+
| Option | Type | Description |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `urlSlug` | `string` | Slug of the sub-page, relative to the page. **No key segment is inserted**, so a declared slug is full control over the URL. The `edit` and `details` slugs must contain `:id`, otherwise `TableView` throws. A slug starting with `/` addresses a page of its own; leading `..` segments each drop one segment of the carrying page. |
|
|
74
|
+
| `displayName` | `string` | Form and sub-page title. |
|
|
75
|
+
| `description` | `string` | Form and sub-page description. |
|
|
76
|
+
| `customPage` | `boolean` | Do not create the sub-page, because you registered your own page at that URL. The slug is still used for navigation. |
|
|
77
|
+
|
|
78
|
+
Whichever slugs apply, the builder resolves them against the slug of the page
|
|
79
|
+
carrying the table view and serializes the three URLs as `formPages`
|
|
80
|
+
(`{ new, edit, view }`), `:id` placeholder included. The frontend navigates to
|
|
81
|
+
those URLs as they stand, substituting the row id, and the current route
|
|
82
|
+
params for the placeholders the carrying page contributes, rather than
|
|
83
|
+
rebuilding them from the browser path. A `customPage` entry registers no
|
|
84
|
+
sub-page and still gets its URL serialized: it is navigated to all the same.
|
|
85
|
+
|
|
86
|
+
A page below a `:id` page therefore carries two `:id` placeholders
|
|
87
|
+
(`/workspaces/:id/invoiceTable/:id/view`). The row id takes the **last** one
|
|
88
|
+
and the route params of the carrying page fill the rest, so the form reads the
|
|
89
|
+
row id from `params.id`, the same thing it means on a flat `/customers/:id/edit`,
|
|
90
|
+
which is what the generated `fetchUrl` and `submitUrl` count on.
|
|
91
|
+
|
|
92
|
+
The id of the page above is not lost to the repetition: when a route repeats a
|
|
93
|
+
placeholder name, every occurrence is also exposed under `<name>:<n>`, numbered
|
|
94
|
+
from 1 in the order they appear in the pattern. On
|
|
95
|
+
`/workspaces/:id/invoiceTable/:id/view` a form reads `{{params.id}}` or
|
|
96
|
+
`{{params.id:2}}` for the row and `{{params.id:1}}` for the workspace carrying
|
|
97
|
+
it. A name that occurs once gets no indexed key. `{{params.id}}` is all there
|
|
98
|
+
is to read on every single-placeholder route.
|
|
99
|
+
|
|
100
|
+
Two page-mode table views on one page whose form URLs resolve to the same path
|
|
101
|
+
(the same explicit `urlSlug`, or the same key under two different parents)
|
|
102
|
+
**fail the registration** of the page, naming both table views and the path.
|
|
103
|
+
The check covers the page's own components and the table views a page extension
|
|
104
|
+
contributes later. Give one of them a `urlSlug` of its own to resolve it.
|