From d36b564267c50156b6e700186d040d34640b2696 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Fri, 28 Nov 2025 17:15:32 +0200 Subject: [PATCH 1/5] feat: add tableRowReplace injection for custom row rendering in list view --- .../03-Customization/08-pageInjections.md | 64 +++++++++++++++++++ adminforth/modules/configValidator.ts | 2 +- .../components/ResourceListTableVirtual.vue | 25 +++++--- adminforth/spa/src/views/ListView.vue | 6 ++ 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md index 8e329daf..5edf1d69 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md @@ -396,6 +396,70 @@ cd custom npm i @iconify-prerendered/vue-mdi ``` +## List table row replace injection + +`tableRowReplace` lets you fully control how each list table row is rendered. Instead of the default table `…` markup, AdminForth will mount your Vue component per record and use its returned DOM to display the row. Use this when you need custom row layouts, extra controls, or conditional styling that goes beyond column-level customization. + +Supported forms: +- Single component: `pageInjections.list.tableRowReplace = '@@/MyRowRenderer.vue'` +- Object form with meta: `pageInjections.list.tableRowReplace = { file: '@@/MyRowRenderer.vue', meta: { /* optional */ } }` +- If an array is provided, the first element is used. + +Example configuration: + +```ts title="/resources/apartments.ts" +{ + resourceId: 'aparts', + ... + options: { + pageInjections: { + list: { + tableRowReplace: { + file: '@@/ApartRowRenderer.vue', + meta: { + // You can pass any meta your component may read + } + } + } + } + } +} +``` + +Minimal component example (decorate default row with a border): + +```vue title="/custom/ApartRowRenderer.vue" + + + +``` + +Component contract: +- Inputs + - `record`: the current record object + - `resource`: the resource config object + - `meta`: the meta object passed in the injection config +- Slots + - Default slot: the table’s standard row content (cells) will be projected here. Your component can wrap or style it. +- Output + - Render a full `…` fragment. For simple decoration, render a single `` and wrap `` inside your layout. + +Notes and tips: +- Requirements: + - Required structure around + ## List table beforeActionButtons `beforeActionButtons` allows injecting one or more compact components into the header bar of the list page, directly to the left of the default action buttons (`Create`, `Filter`, bulk actions, three‑dots menu). Use it for small inputs (quick search, toggle, status chip) rather than large panels. diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index 3ff78e4a..0b5a423a 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -869,7 +869,7 @@ export default class ConfigValidator implements IConfigValidator { // Validate page-specific allowed injection keys const possiblePages = ['list', 'show', 'create', 'edit']; const allowedInjectionsByPage: Record = { - list: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'beforeActionButtons', 'bottom', 'threeDotsDropdownItems', 'customActionIcons', 'tableBodyStart'], + list: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'beforeActionButtons', 'bottom', 'threeDotsDropdownItems', 'customActionIcons', 'tableBodyStart', 'tableRowReplace'], show: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems'], edit: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'saveButton'], create: ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'saveButton'], diff --git a/adminforth/spa/src/components/ResourceListTableVirtual.vue b/adminforth/spa/src/components/ResourceListTableVirtual.vue index 4e5a900c..157c98a4 100644 --- a/adminforth/spa/src/components/ResourceListTableVirtual.vue +++ b/adminforth/spa/src/components/ResourceListTableVirtual.vue @@ -93,14 +93,21 @@ - + - + diff --git a/adminforth/spa/src/views/ListView.vue b/adminforth/spa/src/views/ListView.vue index 22f39a3a..0809e619 100644 --- a/adminforth/spa/src/views/ListView.vue +++ b/adminforth/spa/src/views/ListView.vue @@ -143,6 +143,9 @@ ? [coreStore.resourceOptions.pageInjections.list.tableBodyStart] : [] " + :tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace) + ? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0] + : coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || null" :container-height="1100" :item-height="52.5" :buffer-size="listBufferSize" @@ -173,6 +176,9 @@ ? [coreStore.resourceOptions.pageInjections.list.tableBodyStart] : [] " + :tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace) + ? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0] + : coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || null" /> Date: Fri, 28 Nov 2025 17:23:55 +0200 Subject: [PATCH 2/5] feat: add tableRowReplaceInjection prop for customizable row rendering in ResourceListTable components --- .../03-Customization/08-pageInjections.md | 3 ++- .../spa/src/components/ResourceListTable.vue | 24 ++++++++++++------- .../components/ResourceListTableVirtual.vue | 1 + 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md index 5edf1d69..f66eeb00 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md @@ -442,6 +442,7 @@ const props = defineProps<{ record: any resource: any meta: any + adminUser: any }>(); ``` @@ -458,7 +459,7 @@ Component contract: Notes and tips: - Requirements: - - Required structure around + - Required `` structure around `` ## List table beforeActionButtons diff --git a/adminforth/spa/src/components/ResourceListTable.vue b/adminforth/spa/src/components/ResourceListTable.vue index aaef9cce..0416784c 100644 --- a/adminforth/spa/src/components/ResourceListTable.vue +++ b/adminforth/spa/src/components/ResourceListTable.vue @@ -83,13 +83,20 @@ - + - + @@ -345,6 +352,7 @@ const props = defineProps<{ noRoundings?: boolean, customActionsInjection?: any[], tableBodyStartInjection?: any[], + tableRowReplaceInjection?: any, }>(); // emits, update page diff --git a/adminforth/spa/src/components/ResourceListTableVirtual.vue b/adminforth/spa/src/components/ResourceListTableVirtual.vue index 157c98a4..1ba4ecc9 100644 --- a/adminforth/spa/src/components/ResourceListTableVirtual.vue +++ b/adminforth/spa/src/components/ResourceListTableVirtual.vue @@ -377,6 +377,7 @@ const props = defineProps<{ containerHeight?: number, itemHeight?: number, bufferSize?: number, + tableRowReplaceInjection?: any }>(); // emits, update page From 6f0c30ece76d8f40df802b3ebd164b25215dd4a8 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Mon, 1 Dec 2025 16:34:29 +0200 Subject: [PATCH 3/5] feat: enhance tableRowReplaceInjection type and simplify its usage in ResourceListTable components --- .../spa/src/components/ResourceListTable.vue | 4 ++-- .../src/components/ResourceListTableVirtual.vue | 4 ++-- adminforth/spa/src/utils.ts | 17 ++++++++++++++++- adminforth/spa/src/views/ListView.vue | 8 ++------ adminforth/types/Common.ts | 1 + 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/adminforth/spa/src/components/ResourceListTable.vue b/adminforth/spa/src/components/ResourceListTable.vue index 0416784c..d940068b 100644 --- a/adminforth/spa/src/components/ResourceListTable.vue +++ b/adminforth/spa/src/components/ResourceListTable.vue @@ -335,7 +335,7 @@ import { } from '@iconify-prerendered/vue-flowbite'; import router from '@/router'; import { Tooltip } from '@/afcl'; -import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon } from '@/types/Common'; +import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common'; import adminforth from '@/adminforth'; import Checkbox from '@/afcl/Checkbox.vue'; @@ -352,7 +352,7 @@ const props = defineProps<{ noRoundings?: boolean, customActionsInjection?: any[], tableBodyStartInjection?: any[], - tableRowReplaceInjection?: any, + tableRowReplaceInjection?: AdminForthComponentDeclaration, }>(); // emits, update page diff --git a/adminforth/spa/src/components/ResourceListTableVirtual.vue b/adminforth/spa/src/components/ResourceListTableVirtual.vue index 1ba4ecc9..900ecba9 100644 --- a/adminforth/spa/src/components/ResourceListTableVirtual.vue +++ b/adminforth/spa/src/components/ResourceListTableVirtual.vue @@ -357,7 +357,7 @@ import { } from '@iconify-prerendered/vue-flowbite'; import router from '@/router'; import { Tooltip } from '@/afcl'; -import type { AdminForthResourceCommon, AdminForthResourceColumnCommon } from '@/types/Common'; +import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common'; import adminforth from '@/adminforth'; import Checkbox from '@/afcl/Checkbox.vue'; @@ -377,7 +377,7 @@ const props = defineProps<{ containerHeight?: number, itemHeight?: number, bufferSize?: number, - tableRowReplaceInjection?: any + tableRowReplaceInjection?: AdminForthComponentDeclaration }>(); // emits, update page diff --git a/adminforth/spa/src/utils.ts b/adminforth/spa/src/utils.ts index f1a894d6..6ca964e1 100644 --- a/adminforth/spa/src/utils.ts +++ b/adminforth/spa/src/utils.ts @@ -67,7 +67,22 @@ export async function callAdminForthApi({ path, method, body=undefined, headers= } } -export function getCustomComponent({ file, meta }: { file: string, meta?: any }) { +export function getCustomComponent(input: any) { + let file: string | undefined; + if (Array.isArray(input)) { + input = input?.[0]; + } + if (typeof input === 'string') { + file = input; + } else if (input && typeof input === 'object') { + file = (input as { file?: string }).file; + } + + if (!file || typeof file !== 'string') { + // Fallback to a neutral element to avoid runtime errors + return 'div'; + } + const name = file.replace(/@/g, '').replace(/\./g, '').replace(/\//g, ''); return resolveComponent(name); } diff --git a/adminforth/spa/src/views/ListView.vue b/adminforth/spa/src/views/ListView.vue index 0809e619..3821593e 100644 --- a/adminforth/spa/src/views/ListView.vue +++ b/adminforth/spa/src/views/ListView.vue @@ -143,9 +143,7 @@ ? [coreStore.resourceOptions.pageInjections.list.tableBodyStart] : [] " - :tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace) - ? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0] - : coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || null" + :tableRowReplaceInjection="coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || undefined" :container-height="1100" :item-height="52.5" :buffer-size="listBufferSize" @@ -176,9 +174,7 @@ ? [coreStore.resourceOptions.pageInjections.list.tableBodyStart] : [] " - :tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace) - ? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0] - : coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || null" + :tableRowReplaceInjection="coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || null" /> , customActionIcons?: AdminForthComponentDeclaration | Array, tableBodyStart?: AdminForthComponentDeclaration | Array, + tableRowReplace?: AdminForthComponentDeclaration }, /** From b1beb76933fb01604ec02fde100006295e23f214 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Tue, 2 Dec 2025 12:07:39 +0200 Subject: [PATCH 4/5] feat: enhance tableRowReplace injection to support array input and validate element count --- adminforth/modules/configValidator.ts | 5 +++++ adminforth/spa/src/utils.ts | 17 +---------------- adminforth/spa/src/views/ListView.vue | 8 ++++++-- adminforth/types/Common.ts | 2 +- 4 files changed, 13 insertions(+), 19 deletions(-) diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index 0b5a423a..d525da8a 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -45,6 +45,11 @@ export default class ConfigValidator implements IConfigValidator { } validateAndListifyInjection(obj, key, errors) { + if (key.includes('tableRowReplace')) { + if (obj[key].length > 1) { + throw new Error(`tableRowReplace injection supports only one element, but received ${obj[key].length}.`); + } + } if (!Array.isArray(obj[key])) { // not array obj[key] = [obj[key]]; diff --git a/adminforth/spa/src/utils.ts b/adminforth/spa/src/utils.ts index 6ca964e1..f1a894d6 100644 --- a/adminforth/spa/src/utils.ts +++ b/adminforth/spa/src/utils.ts @@ -67,22 +67,7 @@ export async function callAdminForthApi({ path, method, body=undefined, headers= } } -export function getCustomComponent(input: any) { - let file: string | undefined; - if (Array.isArray(input)) { - input = input?.[0]; - } - if (typeof input === 'string') { - file = input; - } else if (input && typeof input === 'object') { - file = (input as { file?: string }).file; - } - - if (!file || typeof file !== 'string') { - // Fallback to a neutral element to avoid runtime errors - return 'div'; - } - +export function getCustomComponent({ file, meta }: { file: string, meta?: any }) { const name = file.replace(/@/g, '').replace(/\./g, '').replace(/\//g, ''); return resolveComponent(name); } diff --git a/adminforth/spa/src/views/ListView.vue b/adminforth/spa/src/views/ListView.vue index 3821593e..b863520e 100644 --- a/adminforth/spa/src/views/ListView.vue +++ b/adminforth/spa/src/views/ListView.vue @@ -143,7 +143,9 @@ ? [coreStore.resourceOptions.pageInjections.list.tableBodyStart] : [] " - :tableRowReplaceInjection="coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || undefined" + :tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace) + ? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0] + : coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || undefined" :container-height="1100" :item-height="52.5" :buffer-size="listBufferSize" @@ -174,7 +176,9 @@ ? [coreStore.resourceOptions.pageInjections.list.tableBodyStart] : [] " - :tableRowReplaceInjection="coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || null" + :tableRowReplaceInjection="Array.isArray(coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace) + ? coreStore.resourceOptions.pageInjections.list.tableRowReplace[0] + : coreStore.resourceOptions?.pageInjections?.list?.tableRowReplace || undefined" /> , customActionIcons?: AdminForthComponentDeclaration | Array, tableBodyStart?: AdminForthComponentDeclaration | Array, - tableRowReplace?: AdminForthComponentDeclaration + tableRowReplace?: AdminForthComponentDeclaration | Array, }, /** From efe8f415f3434d3d3d1aaf693baae0fb5b93ed25 Mon Sep 17 00:00:00 2001 From: Maksym Pipkun Date: Tue, 2 Dec 2025 12:13:28 +0200 Subject: [PATCH 5/5] docs: update component contract to include example for rendering full-width cell --- .../tutorial/03-Customization/08-pageInjections.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md index f66eeb00..45e7a6a8 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md @@ -455,7 +455,15 @@ Component contract: - Slots - Default slot: the table’s standard row content (cells) will be projected here. Your component can wrap or style it. - Output - - Render a full `…` fragment. For simple decoration, render a single `` and wrap `` inside your layout. + - Render a full `…` fragment. For example, to replace the standard set of cells with a single full‑width cell, render: + +```vue + + + + + +``` Notes and tips: - Requirements: