Skip to content

Commit 22846e7

Browse files
feat: implement conditional column display logic with showIf property
1 parent 411ff9d commit 22846e7

File tree

7 files changed

+281
-6
lines changed

7 files changed

+281
-6
lines changed

adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,167 @@ export default {
143143

144144
>⚠️ Please note that sticky columns can only be applied to one column per resource.
145145
146+
### Conditional display
147+
You can conditionally display columns in forms and views based on the values of other fields in the current record using the `showIf` property. This enables dynamic layouts that automatically adapt to user input, creating more intuitive and context-aware interfaces.
148+
149+
```typescript title="./resources/apartments.ts"
150+
export default {
151+
resourceId: 'aparts',
152+
columns: [
153+
{
154+
name: 'apartment_type',
155+
enum: [
156+
{ value: 'studio', label: 'Studio' },
157+
{ value: 'apartment', label: 'Apartment' },
158+
{ value: 'penthouse', label: 'Penthouse' }
159+
]
160+
},
161+
{
162+
name: 'number_of_rooms',
163+
type: AdminForthDataTypes.INTEGER,
164+
//diff-add
165+
showIf: { apartment_type: { $not: 'studio' } }
166+
},
167+
{
168+
name: 'has_balcony',
169+
type: AdminForthDataTypes.BOOLEAN,
170+
//diff-add
171+
showIf: { apartment_type: 'penthouse' }
172+
}
173+
]
174+
}
175+
```
176+
177+
#### Logical Operators
178+
179+
Use `$and` and `$or` operators to create complex conditional logic:
180+
181+
```typescript title="./resources/apartments.ts"
182+
export default {
183+
columns: [
184+
{
185+
name: 'premium_features',
186+
type: AdminForthDataTypes.JSON,
187+
//diff-add
188+
showIf: {
189+
//diff-add
190+
$and: [
191+
//diff-add
192+
{ price: { $gte: 500000 } },
193+
//diff-add
194+
{ apartment_type: { $in: ['penthouse', 'apartment'] } }
195+
//diff-add
196+
]
197+
//diff-add
198+
}
199+
},
200+
{
201+
name: 'discount_reason',
202+
type: AdminForthDataTypes.STRING,
203+
//diff-add
204+
showIf: {
205+
//diff-add
206+
$or: [
207+
//diff-add
208+
{ price: { $lt: 100000 } },
209+
//diff-add
210+
{ listed: false }
211+
//diff-add
212+
]
213+
//diff-add
214+
}
215+
}
216+
]
217+
}
218+
```
219+
220+
#### Comparison Operators
221+
222+
Use various comparison operators for numeric and string fields:
223+
224+
```typescript title="./resources/apartments.ts"
225+
export default {
226+
columns: [
227+
{
228+
name: 'luxury_amenities',
229+
//diff-add
230+
showIf: { square_meter: { $gt: 100 } }
231+
},
232+
{
233+
name: 'budget_options',
234+
//diff-add
235+
showIf: { price: { $lte: 200000 } }
236+
},
237+
{
238+
name: 'special_offers',
239+
//diff-add
240+
showIf: { country: { $nin: ['US', 'GB'] } }
241+
}
242+
]
243+
}
244+
```
245+
246+
#### Array Operators
247+
248+
For fields that contain arrays, use array-specific operators:
249+
250+
```typescript title="./resources/apartments.ts"
251+
export default {
252+
columns: [
253+
{
254+
name: 'pet_policy',
255+
//diff-add
256+
showIf: { amenities: { $includes: 'pet_friendly' } }
257+
},
258+
{
259+
name: 'security_deposit',
260+
//diff-add
261+
showIf: { features: { $nincludes: 'furnished' } }
262+
}
263+
]
264+
}
265+
```
266+
267+
#### Available Operators
268+
269+
The following operators are available for use in `showIf` conditions:
270+
271+
**Equality Operators:**
272+
- `$eq` - Equal to (default if no operator specified)
273+
- `{ price: { $eq: 100000 } }` or `{ price: 100000 }`
274+
- `$not` - Not equal to
275+
- `{ apartment_type: { $not: 'studio' } }`
276+
277+
**Comparison Operators:**
278+
- `$gt` - Greater than
279+
- `{ square_meter: { $gt: 100 } }`
280+
- `$gte` - Greater than or equal to
281+
- `{ price: { $gte: 500000 } }`
282+
- `$lt` - Less than
283+
- `{ price: { $lt: 100000 } }`
284+
- `$lte` - Less than or equal to
285+
- `{ price: { $lte: 200000 } }`
286+
287+
**Array Operators:**
288+
- `$in` - Value is in array
289+
- `{ apartment_type: { $in: ['penthouse', 'apartment'] } }`
290+
- `$nin` - Value is not in array
291+
- `{ country: { $nin: ['US', 'GB'] } }`
292+
- `$includes` - Array includes value
293+
- `{ amenities: { $includes: 'pet_friendly' } }`
294+
- `$nincludes` - Array does not include value
295+
- `{ features: { $nincludes: 'furnished' } }`
296+
297+
**Logical Operators:**
298+
- `$and` - Logical AND operation
299+
- `{ $and: [{ price: { $gte: 500000 } }, { listed: true }] }`
300+
- `$or` - Logical OR operation
301+
- `{ $or: [{ price: { $lt: 100000 } }, { listed: false }] }`
302+
303+
> ⚠️ **Warning**: When using `showIf` with complex conditions, ensure that:
304+
> - `$and` and `$or` operators contain arrays of conditions
305+
> - `$in` and `$nin` operators contain arrays of values
306+
> - `$includes` and `$nincludes` operators are only used on columns marked as arrays (`isArray: { enabled: true }`)
146307
### Page size
147308

148309
use `options.listPageSize` to define how many records will be shown on the page

adminforth/modules/configValidator.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
AdminForthComponentDeclaration ,
2626
AdminForthResourcePages,
2727
AdminForthDataTypes,
28+
Predicate,
2829
} from "../types/Common.js";
2930
import AdminForth from "adminforth";
3031
import { AdminForthConfigMenuItem } from "adminforth";
@@ -781,6 +782,48 @@ export default class ConfigValidator implements IConfigValidator {
781782
errors.push(`Resource "${res.resourceId}" has more than one listSticky column. Only one column can be sticky in the list view.`);
782783
}
783784

785+
const conditionalColumns = res.columns.filter(c => c.showIf);
786+
if (conditionalColumns.length) {
787+
const checkConditionArrays = (predicate: Predicate, column: AdminForthResourceColumn) => {
788+
if ("$and" in predicate && !Array.isArray(predicate.$and)) {
789+
errors.push(`Resource "${res.resourceId}" column "${column.name}" has showIf with $and that is not an array`);
790+
} else if ("$and" in predicate && Array.isArray(predicate.$and)) {
791+
predicate.$and.forEach((item) => checkConditionArrays(item, column));
792+
}
793+
794+
if ("$or" in predicate && !Array.isArray(predicate.$or)) {
795+
errors.push(`Resource "${res.resourceId}" column "${column.name}" has showIf with $or that is not an array`);
796+
} else if ("$or" in predicate && Array.isArray(predicate.$or)) {
797+
predicate.$or.forEach((item) => checkConditionArrays(item, column));
798+
}
799+
800+
const fieldEntries = Object.entries(predicate).filter(([key]) => !key.startsWith('$'));
801+
if (fieldEntries.length > 0) {
802+
fieldEntries.forEach(([field, condition]) => {
803+
if (typeof condition !== 'object') {
804+
return;
805+
}
806+
if ("$in" in condition && !Array.isArray(condition.$in)) {
807+
errors.push(`Resource "${res.resourceId}" column "${column.name}" has showIf with $in that is not an array`);
808+
}
809+
if ("$nin" in condition && !Array.isArray(condition.$nin)) {
810+
errors.push(`Resource "${res.resourceId}" column "${column.name}" has showIf with $nin that is not an array`);
811+
}
812+
if ("$includes" in condition && !column.isArray) {
813+
errors.push(`Resource "${res.resourceId}" has showIf with $includes on non-array column "${column.name}"`);
814+
}
815+
if ("$nincludes" in condition && !column.isArray) {
816+
errors.push(`Resource "${res.resourceId}" has showIf with $nincludes on non-array column "${column.name}"`);
817+
}
818+
});
819+
}
820+
};
821+
822+
conditionalColumns.forEach((column) => {
823+
checkConditionArrays(column.showIf, column);
824+
});
825+
}
826+
784827
const options: Partial<AdminForthResource['options']> = {...resInput.options, bulkActions: undefined, allowedActions: undefined};
785828

786829
options.allowedActions = this.validateAndNormalizeAllowedActions(resInput, errors);

adminforth/spa/src/components/ResourceForm.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363

6464
<script setup lang="ts">
6565
66-
import { applyRegexValidation, callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers} from '@/utils';
66+
import { applyRegexValidation, callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers, checkShowIf } from '@/utils';
6767
import { computedAsync } from '@vueuse/core';
6868
import { computed, onMounted, reactive, ref, watch, provide, type Ref } from 'vue';
6969
import { useRouter, useRoute } from 'vue-router';
@@ -322,7 +322,7 @@ async function searchOptions(columnName: string, searchTerm: string) {
322322
323323
324324
const editableColumns = computed(() => {
325-
return props.resource?.columns?.filter(column => column.showIn?.[mode.value]);
325+
return props.resource?.columns?.filter(column => column.showIn?.[mode.value] && (currentValues.value ? checkShowIf(column, currentValues.value) : true));
326326
});
327327
328328
const isValid = computed(() => {

adminforth/spa/src/components/ShowTable.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
dark:bg-darkShowTablesBodyBackground dark:border-darkShowTableBodyBorder block md:table-row"
2525
>
2626
<component
27-
v-if="column.components?.showRow"
27+
v-if="column.components?.showRow && checkShowIf(column, record)"
2828
:is="getCustomComponent(column.components.showRow)"
2929
:meta="column.components.showRow.meta"
3030
:column="column"
3131
:resource="coreStore.resource"
3232
:record="coreStore.record"
3333
/>
34-
<template v-else>
34+
<template v-else-if="checkShowIf(column, record)">
3535
<td class="px-6 py-4 relative block md:table-cell font-bold md:font-normal pb-0 md:pb-4">
3636
{{ column.label }}
3737
</td>
@@ -59,14 +59,15 @@
5959

6060
<script setup lang="ts">
6161
import ValueRenderer from '@/components/ValueRenderer.vue';
62-
import { getCustomComponent } from '@/utils';
62+
import { getCustomComponent, checkShowIf } from '@/utils';
6363
import { useCoreStore } from '@/stores/core';
6464
import { computed } from 'vue';
65-
import type { AdminForthResourceCommon } from '@/types/Common';
65+
import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon } from '@/types/Common';
6666
const props = withDefaults(defineProps<{
6767
columns: Array<{
6868
name: string;
6969
label?: string;
70+
showIf?: AdminForthResourceColumnInputCommon['showIf'];
7071
components?: {
7172
show?: {
7273
file: string;

adminforth/spa/src/utils.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Dropdown } from 'flowbite';
88
import adminforth from './adminforth';
99
import sanitizeHtml from 'sanitize-html'
1010
import debounce from 'debounce';
11+
import type { AdminForthResourceColumnInputCommon, Predicate } from '@/types/Common';
1112

1213
const LS_LANG_KEY = `afLanguage`;
1314
const MAX_CONSECUTIVE_EMPTY_RESULTS = 2;
@@ -421,4 +422,60 @@ export function createSearchInputHandlers(
421422
}
422423
return acc;
423424
}, {} as Record<string, (searchTerm: string) => void>);
425+
}
426+
427+
export function checkShowIf(c: AdminForthResourceColumnInputCommon, record: Record<string, any>) {
428+
if (!c.showIf) return true;
429+
430+
const evaluatePredicate = (predicate: Predicate): boolean => {
431+
const results: boolean[] = [];
432+
433+
if ("$and" in predicate) {
434+
results.push(predicate.$and.every(evaluatePredicate));
435+
}
436+
437+
if ("$or" in predicate) {
438+
results.push(predicate.$or.some(evaluatePredicate));
439+
}
440+
441+
const fieldEntries = Object.entries(predicate).filter(([key]) => !key.startsWith('$'));
442+
if (fieldEntries.length > 0) {
443+
const fieldResult = fieldEntries.every(([field, condition]) => {
444+
const recordValue = record[field];
445+
446+
if (condition === undefined) {
447+
return true;
448+
}
449+
if (typeof condition !== "object" || condition === null) {
450+
return recordValue === condition;
451+
}
452+
453+
if ("$eq" in condition) return recordValue === condition.$eq;
454+
if ("$not" in condition) return recordValue !== condition.$not;
455+
if ("$gt" in condition) return recordValue > condition.$gt;
456+
if ("$gte" in condition) return recordValue >= condition.$gte;
457+
if ("$lt" in condition) return recordValue < condition.$lt;
458+
if ("$lte" in condition) return recordValue <= condition.$lte;
459+
if ("$in" in condition) return (Array.isArray(condition.$in) && condition.$in.includes(recordValue));
460+
if ("$nin" in condition) return (Array.isArray(condition.$nin) && !condition.$nin.includes(recordValue));
461+
if ("$includes" in condition)
462+
return (
463+
Array.isArray(recordValue) &&
464+
recordValue.includes(condition.$includes)
465+
);
466+
if ("$nincludes" in condition)
467+
return (
468+
Array.isArray(recordValue) &&
469+
!recordValue.includes(condition.$nicludes)
470+
);
471+
472+
return true;
473+
});
474+
results.push(fieldResult);
475+
}
476+
477+
return results.every(result => result);
478+
};
479+
480+
return evaluatePredicate(c.showIf);
424481
}

adminforth/types/Common.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ export type AllowedActionsResolved = {
6767
[key in AllowedActionsEnum]: boolean
6868
}
6969

70+
// conditional operators for predicates
71+
type Value = any;
72+
type Operators = { $eq: Value } | { $not: Value } | { $gt: Value } | { $gte: Value } | { $lt: Value } | { $lte: Value } | { $in: Value[] } | { $nin: Value[] } | { $includes: Value } | { $nincludes: Value };
73+
export type Predicate = { $and: Predicate[] } | { $or: Predicate[] } | { [key: string]: Operators | Value };
74+
7075
export interface AdminUser {
7176
/**
7277
* primaryKey field value of user in table which is defined by {@link AdminForthConfig.auth.usersResourceId}
@@ -876,6 +881,11 @@ export interface AdminForthResourceColumnInputCommon {
876881
* Sticky position for column
877882
*/
878883
listSticky?: boolean;
884+
885+
/**
886+
* Show field only if certain conditions are met.
887+
*/
888+
showIf?: Predicate;
879889
}
880890

881891
export interface AdminForthResourceColumnCommon extends AdminForthResourceColumnInputCommon {

dev-demo/resources/apartments.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ export default {
118118
showIn: {list: true, create: true, edit: true, filter: true, show: true}, // the default is full set
119119
maxLength: 255, // you can set max length for string fields
120120
minLength: 3, // you can set min length for string fields
121+
showIf: {number_of_rooms: {
122+
$in: [3, 4]
123+
}},
121124
components: {
122125
// edit: {
123126
// file: '@@/IdShow.vue',

0 commit comments

Comments
 (0)