Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,79 @@ 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 `<tr>…</tr>` 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"
<template>
<tr class="border border-gray-200 dark:border-gray-700 rounded-sm">
<slot />
</tr>

</template>

<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
record: any
resource: any
meta: any
adminUser: any
}>();
</script>
```

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 `<tr>…</tr>` fragment. For example, to replace the standard set of cells with a single full‑width cell, render:

```vue
<tr>
<td :colspan="columnsCount">
<slot />
</td>
</tr>
```

Notes and tips:
- Requirements:
- Required `<tr></tr>` structure around `<slot />`

## 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.
Expand Down
7 changes: 6 additions & 1 deletion adminforth/modules/configValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]];
Expand Down Expand Up @@ -869,7 +874,7 @@ export default class ConfigValidator implements IConfigValidator {
// Validate page-specific allowed injection keys
const possiblePages = ['list', 'show', 'create', 'edit'];
const allowedInjectionsByPage: Record<string, string[]> = {
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'],
Expand Down
26 changes: 17 additions & 9 deletions adminforth/spa/src/components/ResourceListTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,20 @@
</td>
</tr>

<tr @click="onClick($event,row)"
v-else v-for="(row, rowI) in rows" :key="`row_${row._primaryKeyValue}`"
ref="rowRefs"
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"

:class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
>
<component
v-else
v-for="(row, rowI) in rows"
:is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
:key="`row_${row._primaryKeyValue}`"
:record="row"
:resource="resource"
:adminUser="coreStore.adminUser"
:meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
@click="onClick($event, row)"
ref="rowRefs"
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
:class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
>
<td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
<Checkbox
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
Expand Down Expand Up @@ -210,7 +217,7 @@
</div>

</td>
</tr>
</component>
</tbody>
</table>
</div>
Expand Down Expand Up @@ -328,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';

Expand All @@ -345,6 +352,7 @@ const props = defineProps<{
noRoundings?: boolean,
customActionsInjection?: any[],
tableBodyStartInjection?: any[],
tableRowReplaceInjection?: AdminForthComponentDeclaration,
}>();

// emits, update page
Expand Down
28 changes: 18 additions & 10 deletions adminforth/spa/src/components/ResourceListTableVirtual.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,21 @@
</tr>

<!-- Visible rows -->
<tr @click="onClick($event,row)"
v-for="(row, rowI) in visibleRows"
:key="`row_${row._primaryKeyValue}`"
ref="rowRefs"
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
:class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
@mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
>
<component
v-else
v-for="(row, rowI) in visibleRows"
:is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
:key="`row_${row._primaryKeyValue}`"
:record="row"
:resource="resource"
:adminUser="coreStore.adminUser"
:meta="tableRowReplaceInjection ? tableRowReplaceInjection.meta : undefined"
@click="onClick($event, row)"
ref="rowRefs"
class="list-table-body-row bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
:class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
@mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
>
<td class="w-4 p-4 cursor-default sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading" @click="(e)=>e.stopPropagation()">
<Checkbox
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
Expand Down Expand Up @@ -224,7 +231,7 @@
</template>
</div>
</td>
</tr>
</component>

<!-- Bottom spacer -->
<tr v-if="totalHeight > 0">
Expand Down Expand Up @@ -350,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';

Expand All @@ -370,6 +377,7 @@ const props = defineProps<{
containerHeight?: number,
itemHeight?: number,
bufferSize?: number,
tableRowReplaceInjection?: AdminForthComponentDeclaration
}>();

// emits, update page
Expand Down
6 changes: 6 additions & 0 deletions adminforth/spa/src/views/ListView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 || undefined"
:container-height="1100"
:item-height="52.5"
:buffer-size="listBufferSize"
Expand Down Expand Up @@ -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 || undefined"
/>

<component
Expand Down
1 change: 1 addition & 0 deletions adminforth/types/Common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ export interface AdminForthResourceInputCommon {
threeDotsDropdownItems?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
customActionIcons?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
tableBodyStart?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
tableRowReplace?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
},

/**
Expand Down