Skip to content

Commit b812e12

Browse files
authored
Merge branch 'next' into AdminForth/743
2 parents e41b2fe + 07ffa98 commit b812e12

34 files changed

+751
-138
lines changed

adminforth/commands/createApp/templates/index.ts.hbs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import AdminForth from 'adminforth';
33
import usersResource from "./resources/adminuser.js";
44
import { fileURLToPath } from 'url';
55
import path from 'path';
6+
import { Filters } from 'adminforth';
67

78
const ADMIN_BASE_URL = '';
89

@@ -15,6 +16,12 @@ export const admin = new AdminForth({
1516
rememberMeDays: 30,
1617
loginBackgroundImage: 'https://images.unsplash.com/photo-1534239697798-120952b76f2b?q=80&w=3389&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
1718
loginBackgroundPosition: '1/2',
19+
loginPromptHTML: async () => {
20+
const adminforthUserExists = await admin.resource("adminuser").count(Filters.EQ('email', 'adminforth')) > 0;
21+
if (adminforthUserExists) {
22+
return "Please use <b>adminforth</b> as username and <b>adminforth</b> as password"
23+
}
24+
},
1825
},
1926
customization: {
2027
brandName: "{{appName}}",

adminforth/commands/createCustomComponent/configLoader.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import fs from 'fs/promises';
22
import path from 'path';
33
import chalk from 'chalk';
44
import jiti from 'jiti';
5+
import dotenv from "dotenv";
56

7+
dotenv.config({ path: '.env.local', override: true });
8+
dotenv.config({ path: '.env', override: true });
69

710
export async function loadAdminForthConfig() {
811
const configFileName = 'index.ts';

adminforth/dataConnectors/baseConnector.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,23 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
114114
const fieldObj = resource.dataSourceColumns.find((col) => col.name == (filters as IAdminForthSingleFilter).field);
115115
if (!fieldObj) {
116116
const similar = suggestIfTypo(resource.dataSourceColumns.map((col) => col.name), (filters as IAdminForthSingleFilter).field);
117-
throw new Error(`Field '${(filters as IAdminForthSingleFilter).field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
117+
118+
let isPolymorphicTarget = false;
119+
if (global.adminforth?.config?.resources) {
120+
isPolymorphicTarget = global.adminforth.config.resources.some(res =>
121+
res.dataSourceColumns.some(col =>
122+
col.foreignResource?.polymorphicResources?.some(pr =>
123+
pr.resourceId === resource.resourceId
124+
)
125+
)
126+
);
127+
}
128+
if (isPolymorphicTarget) {
129+
process.env.HEAVY_DEBUG && console.log(`⚠️ Field '${(filters as IAdminForthSingleFilter).field}' not found in polymorphic target resource '${resource.resourceId}', allowing query to proceed.`);
130+
return { ok: true, error: '' };
131+
} else {
132+
throw new Error(`Field '${(filters as IAdminForthSingleFilter).field}' not found in resource '${resource.resourceId}'. ${similar ? `Did you mean '${similar}'?` : ''}`);
133+
}
118134
}
119135
// value normalization
120136
if (filters.operator == AdminForthFilterOperators.IN || filters.operator == AdminForthFilterOperators.NIN) {

adminforth/documentation/docs/tutorial/001-gettingStarted.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ export default {
292292
name: 'realtor_id',
293293
foreignResource: {
294294
resourceId: 'adminuser',
295+
searchableFields: ["id", "email"], // fields available for search in filter
295296
}
296297
}
297298
],

adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ const props = defineProps<{
194194
meta: any;
195195
resource: AdminForthResourceCommon;
196196
adminUser: AdminUser;
197+
readonly: boolean;
197198
}>();
198199
199200
const emit = defineEmits(["update:value"]);

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,31 @@ export default {
640640
],
641641
```
642642

643+
### Searchable fields
644+
645+
Enable search in filter dropdown by specifying which fields to search:
646+
647+
```typescript title="./resources/apartments.ts"
648+
export default {
649+
name: 'apartments',
650+
columns: [
651+
...
652+
{
653+
name: "realtor_id",
654+
foreignResource: {
655+
resourceId: 'adminuser',
656+
//diff-add
657+
searchableFields: ["id", "email"],
658+
//diff-add
659+
searchIsCaseSensitive: true, // default false
660+
},
661+
},
662+
],
663+
},
664+
...
665+
],
666+
```
667+
643668
### Polymorphic foreign resources
644669

645670
Sometimes it is needed for one column to be a foreign key for multiple tables. For example, given the following schema:

adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,31 @@ const enable = ref(false)
362362
</div>
363363
</div>
364364

365+
366+
## Toggle
367+
368+
<div class="split-screen" >
369+
<div >
370+
371+
```ts
372+
import Toggle from '@/afcl/Toggle.vue';
373+
```
374+
375+
376+
```html
377+
<Toggle
378+
:disabled="false"
379+
@update:modelValue="toggleSwitchHandler">
380+
<p>Click me</p>
381+
</Toggle>
382+
```
383+
</div>
384+
<div>
385+
![AFCL Checkbox](image-94.png)
386+
</div>
387+
</div>
388+
389+
365390
## Dialog (Pop-up)
366391

367392
<div class="split-screen" >
19.7 KB
Loading

adminforth/index.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ class AdminForth implements IAdminForth {
511511
async createResourceRecord(
512512
{ resource, record, adminUser, extra }:
513513
{ resource: AdminForthResource, record: any, adminUser: AdminUser, extra?: HttpExtra }
514-
): Promise<{ error?: string, createdRecord?: any }> {
514+
): Promise<{ error?: string, createdRecord?: any, newRecordId?: any }> {
515515

516516
const err = this.validateRecordValues(resource, record, 'create');
517517
if (err) {
@@ -528,8 +528,18 @@ class AdminForth implements IAdminForth {
528528
adminforth: this,
529529
extra,
530530
});
531-
if (!resp || (!resp.ok && !resp.error)) {
532-
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
531+
if (!resp || (typeof resp.ok !== 'boolean' && (!resp.error && !resp.newRecordId))) {
532+
throw new Error(
533+
`Invalid return value from beforeSave hook. Expected: { ok: boolean, error?: string | null, newRecordId?: any }.\n` +
534+
`Note: Return { ok: false, error: null, newRecordId } to stop creation and redirect to an existing record.`
535+
);
536+
}
537+
if (resp.ok === false && !resp.error) {
538+
const { error, ok, newRecordId } = resp;
539+
return {
540+
error: error ?? 'Operation aborted by hook',
541+
newRecordId: newRecordId
542+
};
533543
}
534544
if (resp.error) {
535545
return { error: resp.error };
@@ -605,8 +615,11 @@ class AdminForth implements IAdminForth {
605615
adminforth: this,
606616
extra,
607617
});
608-
if (!resp || (!resp.ok && !resp.error)) {
609-
throw new Error(`Hook beforeSave must return object with {ok: true} or { error: 'Error' } `);
618+
if (!resp || typeof resp.ok !== 'boolean') {
619+
throw new Error(`Hook beforeSave must return { ok: boolean, error?: string | null }`);
620+
}
621+
if (resp.ok === false && !resp.error) {
622+
return { error: resp.error ?? 'Operation aborted by hook' };
610623
}
611624
if (resp.error) {
612625
return { error: resp.error };

adminforth/modules/configValidator.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,46 @@ export default class ConfigValidator implements IConfigValidator {
658658
}
659659
}
660660

661+
if (col.foreignResource.searchableFields) {
662+
const searchableFields = Array.isArray(col.foreignResource.searchableFields)
663+
? col.foreignResource.searchableFields
664+
: [col.foreignResource.searchableFields];
665+
666+
searchableFields.forEach((fieldName) => {
667+
if (typeof fieldName !== 'string') {
668+
errors.push(`Resource "${res.resourceId}" column "${col.name}" foreignResource.searchableFields must contain only strings`);
669+
return;
670+
}
671+
672+
if (col.foreignResource.resourceId) {
673+
const targetResource = this.inputConfig.resources.find((r) => r.resourceId === col.foreignResource.resourceId || r.table === col.foreignResource.resourceId);
674+
if (targetResource) {
675+
const targetColumn = targetResource.columns.find((targetCol) => targetCol.name === fieldName);
676+
if (!targetColumn) {
677+
const similar = suggestIfTypo(targetResource.columns.map((c) => c.name), fieldName);
678+
errors.push(`Resource "${res.resourceId}" column "${col.name}" foreignResource.searchableFields contains field "${fieldName}" which does not exist in target resource "${targetResource.resourceId || targetResource.table}". ${similar ? `Did you mean "${similar}"?` : ''}`);
679+
}
680+
}
681+
} else if (col.foreignResource.polymorphicResources) {
682+
let hasFieldInAnyResource = false;
683+
for (const pr of col.foreignResource.polymorphicResources) {
684+
if (pr.resourceId) {
685+
const targetResource = this.inputConfig.resources.find((r) => r.resourceId === pr.resourceId || r.table === pr.resourceId);
686+
if (targetResource) {
687+
const hasField = targetResource.columns.some((targetCol) => targetCol.name === fieldName);
688+
if (hasField) {
689+
hasFieldInAnyResource = true;
690+
}
691+
}
692+
}
693+
}
694+
if (!hasFieldInAnyResource) {
695+
errors.push(`Resource "${res.resourceId}" column "${col.name}" foreignResource.searchableFields contains field "${fieldName}" which does not exist in any of the polymorphic target resources`);
696+
}
697+
}
698+
});
699+
}
700+
661701
if (col.foreignResource.unsetLabel) {
662702
if (typeof col.foreignResource.unsetLabel !== 'string') {
663703
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource unsetLabel which is not a string`);
@@ -666,6 +706,12 @@ export default class ConfigValidator implements IConfigValidator {
666706
// set default unset label
667707
col.foreignResource.unsetLabel = 'Unset';
668708
}
709+
710+
// Set default searchIsCaseSensitive
711+
if (col.foreignResource.searchIsCaseSensitive === undefined) {
712+
col.foreignResource.searchIsCaseSensitive = false;
713+
}
714+
669715
const befHook = col.foreignResource.hooks?.dropdownList?.beforeDatasourceRequest;
670716
if (befHook) {
671717
if (!Array.isArray(befHook)) {

0 commit comments

Comments
 (0)