Skip to content

Commit c9c4f00

Browse files
authored
Merge branch 'next' into feature/AdminForth/1364/here-https-adminfo-on-backend-
2 parents b75387a + 5952880 commit c9c4f00

File tree

29 files changed

+381
-59
lines changed

29 files changed

+381
-59
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,22 @@ import { Express, Request, Response } from "express";
22
import { IAdminForth } from "adminforth";
33
export function initApi(app: Express, admin: IAdminForth) {
44
app.get(`${admin.config.baseUrl}/api/hello/`,
5+
6+
// you can use data API to work with your database https://adminforth.dev/docs/tutorial/Customization/dataApi/
57
async (req: Request, res: Response) => {
8+
// req.adminUser to get info about the admin users
69
const allUsers = await admin.resource("adminuser").list([]);
710
res.json({
8-
message: "Hello from AdminForth API!",
11+
message: "List of admin users from AdminForth API",
912
users: allUsers,
1013
});
11-
}
14+
},
15+
16+
// you can use admin.express.authorize to get info about the current user
17+
admin.express.authorize(
18+
async (req: Request, res: Response) => {
19+
res.json({ message: "Current adminuser from AdminForth API", adminUser: req.adminUser });
20+
}
21+
)
1222
);
1323
}

adminforth/dataConnectors/clickhouse.ts

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
149149
return dayjs.unix(+value).toISOString();
150150
} else if (field._underlineType.startsWith('DateTime')
151151
|| field._underlineType.startsWith('String')
152-
|| field._underlineType.startsWith('FixedString')) {
152+
|| field._underlineType.startsWith('FixedString')
153+
|| field._underlineType.startsWith('Nullable(String)')
154+
|| field._underlineType.startsWith('Nullable(FixedString)')) {
153155
const v = dayjs(value).toISOString();
154156
return v;
155157
} else {
@@ -163,7 +165,10 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
163165
} else if (field.type == AdminForthDataTypes.BOOLEAN) {
164166
return value === null ? null : !!value;
165167
} else if (field.type == AdminForthDataTypes.JSON) {
166-
if (field._underlineType.startsWith('String') || field._underlineType.startsWith('FixedString')) {
168+
if (field._underlineType.startsWith('String')
169+
|| field._underlineType.startsWith('FixedString')
170+
|| field._underlineType.startsWith('Nullable(String)')
171+
|| field._underlineType.startsWith('Nullable(FixedString)')) {
167172
try {
168173
return JSON.parse(value);
169174
} catch (e) {
@@ -186,7 +191,9 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
186191
return dayjs(value).unix();
187192
} else if (field._underlineType.startsWith('DateTime')
188193
|| field._underlineType.startsWith('String')
189-
|| field._underlineType.startsWith('FixedString')) {
194+
|| field._underlineType.startsWith('FixedString')
195+
|| field._underlineType.startsWith('Nullable(String)')
196+
|| field._underlineType.startsWith('Nullable(FixedString)')) {
190197
// value is iso string now, convert to unix timestamp
191198
const iso = dayjs(value).format('YYYY-MM-DDTHH:mm:ss');
192199
return iso;
@@ -195,7 +202,10 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
195202
return value === null ? null : (value ? 1 : 0);
196203
} else if (field.type == AdminForthDataTypes.JSON) {
197204
// check underline type is text or string
198-
if (field._underlineType.startsWith('String') || field._underlineType.startsWith('FixedString')) {
205+
if (field._underlineType.startsWith('String')
206+
|| field._underlineType.startsWith('FixedString')
207+
|| field._underlineType.startsWith('Nullable(String)')
208+
|| field._underlineType.startsWith('Nullable(FixedString)')) {
199209
return JSON.stringify(value);
200210
} else {
201211
afLogger.warn(`AdminForth: JSON field is not a string/text but ${field._underlineType}, this is not supported yet`);
@@ -226,6 +236,21 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
226236
[AdminForthSortDirections.asc]: 'ASC',
227237
[AdminForthSortDirections.desc]: 'DESC',
228238
};
239+
240+
isArrayType(underlineType: string): boolean {
241+
return underlineType.startsWith('Array(') || underlineType.startsWith('Nullable(Array(');
242+
}
243+
244+
isNullableType(underlineType: string): boolean {
245+
return underlineType.startsWith('Nullable(');
246+
}
247+
248+
isStringLikeType(underlineType: string): boolean {
249+
return underlineType.startsWith('String')
250+
|| underlineType.startsWith('FixedString')
251+
|| underlineType.startsWith('Nullable(String)')
252+
|| underlineType.startsWith('Nullable(FixedString)');
253+
}
229254

230255
getFilterString(resource: AdminForthResource, filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): string {
231256
if ((filter as IAdminForthSingleFilter).field) {
@@ -247,6 +272,49 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
247272
return `${field} ${operator}`;
248273
}
249274

275+
if ((filter.operator == AdminForthFilterOperators.LIKE || filter.operator == AdminForthFilterOperators.ILIKE)
276+
&& column.isArray?.enabled) {
277+
placeholder = '{f$?:String}';
278+
279+
if (this.isArrayType(column._underlineType)) {
280+
const arrayField = this.isNullableType(column._underlineType) ? `assumeNotNull(${field})` : field;
281+
const arrayMatch = `arrayExists(item -> toString(item) ${operator} ${placeholder}, ${arrayField})`;
282+
return this.isNullableType(column._underlineType)
283+
? `${field} IS NOT NULL AND ${arrayMatch}`
284+
: arrayMatch;
285+
}
286+
287+
if (this.isStringLikeType(column._underlineType)) {
288+
return `${field} ${operator} ${placeholder}`;
289+
}
290+
}
291+
292+
if ((filter.operator == AdminForthFilterOperators.IN || filter.operator == AdminForthFilterOperators.NIN)
293+
&& column.isArray?.enabled
294+
&& this.isArrayType(column._underlineType)) {
295+
const itemType = column._underlineType
296+
.replace(/^Nullable\(/, '')
297+
.match(/^Array\((.*)\)$/)?.[1];
298+
299+
if (!itemType) {
300+
throw new Error(`Unable to determine item type for array field '${column.name}' with type '${column._underlineType}'`);
301+
}
302+
303+
placeholder = `{f$?:Array(${itemType})}`;
304+
const arrayField = this.isNullableType(column._underlineType) ? `assumeNotNull(${field})` : field;
305+
const hasAnyExpression = `hasAny(${arrayField}, ${placeholder})`;
306+
307+
if (filter.operator == AdminForthFilterOperators.NIN) {
308+
return this.isNullableType(column._underlineType)
309+
? `(${field} IS NULL OR NOT ${hasAnyExpression})`
310+
: `NOT ${hasAnyExpression}`;
311+
}
312+
313+
return this.isNullableType(column._underlineType)
314+
? `${field} IS NOT NULL AND ${hasAnyExpression}`
315+
: hasAnyExpression;
316+
}
317+
250318
if (column._underlineType.startsWith('Decimal')) {
251319
field = `toDecimal64(${field}, 8)`;
252320
placeholder = `toDecimal64({f$?:String}, 8)`;
@@ -293,20 +361,24 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
293361
}).join(` ${this.OperatorsMap[filter.operator]} `);
294362
}
295363

296-
getFilterParams(filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): any[] {
364+
getFilterParams(resource: AdminForthResource, filter: IAdminForthSingleFilter | IAdminForthAndOrFilter): any[] {
297365
if ((filter as IAdminForthSingleFilter).field) {
298366
if ((filter as IAdminForthSingleFilter).rightField) {
299367
// No params for field-to-field comparisons
300368
return [];
301369
}
302370
// filter is a Single filter
371+
const column = resource.dataSourceColumns.find((col) => col.name == (filter as IAdminForthSingleFilter).field);
303372

304373
// Handle IS_EMPTY and IS_NOT_EMPTY operators - no params needed
305374
if (filter.operator == AdminForthFilterOperators.IS_EMPTY || filter.operator == AdminForthFilterOperators.IS_NOT_EMPTY) {
306375
return [];
307376
} else if (filter.operator == AdminForthFilterOperators.LIKE || filter.operator == AdminForthFilterOperators.ILIKE) {
308377
return [{ 'f': `%${filter.value}%` }];
309378
} else if (filter.operator == AdminForthFilterOperators.IN || filter.operator == AdminForthFilterOperators.NIN) {
379+
if (column?.isArray?.enabled && this.isArrayType(column._underlineType)) {
380+
return [{ 'f': filter.value }];
381+
}
310382
return [{ 'p': filter.value }];
311383
} else if (filter.operator == AdminForthFilterOperators.EQ && filter.value === null) {
312384
// there is no param for IS NULL filter
@@ -326,15 +398,15 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
326398

327399
// filter is a AndOrFilter
328400
return (filter as IAdminForthAndOrFilter).subFilters.reduce((params: any[], f: IAdminForthSingleFilter | IAdminForthAndOrFilter) => {
329-
return params.concat(this.getFilterParams(f));
401+
return params.concat(this.getFilterParams(resource, f));
330402
}, []);
331403
}
332404

333-
whereParams(filters: IAdminForthAndOrFilter): any {
405+
whereParams(resource: AdminForthResource, filters: IAdminForthAndOrFilter): any {
334406
if (filters.subFilters.length === 0) {
335407
return {};
336408
}
337-
const paramsArray = this.getFilterParams(filters);
409+
const paramsArray = this.getFilterParams(resource, filters);
338410
const params = paramsArray.reduce((acc, param, paramIndex) => {
339411
if (param.f !== undefined) {
340412
acc[`f${paramIndex}`] = param.f;
@@ -362,7 +434,7 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
362434
params: {},
363435
}
364436
}
365-
const params = this.whereParams(filters);
437+
const params = this.whereParams(resource, filters);
366438
const where = Object.keys(params).reduce((w, paramKey) => {
367439
// remove first char of string (will be "f" or "p") to leave only index
368440
const keyIndex = paramKey.substring(1);
@@ -388,6 +460,7 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
388460
}).join(', ');
389461
const tableName = resource.table;
390462

463+
console.log('getDataWithOriginalTypes called with filters', JSON.stringify(filters), 'and sort', JSON.stringify(sort));
391464
const { where, params } = this.whereClause(resource, filters);
392465

393466
const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';

adminforth/documentation/docs/tutorial/03-Customization/09-Actions.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ Keep the `<slot />` (that's where AdminForth renders the default button) and emi
224224
<!-- Keep the slot: AdminForth renders the default action button/icon here -->
225225
<!-- Emit `callAction` (optionally with a payload) to trigger the action when the wrapper is clicked -->
226226
<!-- Example: provide `meta.extra` to send custom data. In list views we merge with `row` so recordId context is kept. -->
227-
<div :style="styleObj" @click="emit('callAction', { ...props.row, ...(props.meta?.extra ?? {}) })">
227+
<div :style="styleObj" @click="click({ ...props.row, ...(props.meta?.extra ?? {}) })">
228228
<slot />
229229
</div>
230230
</template>
@@ -248,6 +248,14 @@ const styleObj = computed(() => ({
248248
borderRadius: (props.meta?.radius ?? 8) + 'px',
249249
padding: (props.meta?.padding ?? 2) + 'px',
250250
}));
251+
252+
function click(payload: any) {
253+
emit('callAction', { ...props.row, ...(props.meta?.extra ?? {}) })
254+
}
255+
//we need to define this expose, because padding is added by adminforth wrapper and to trigger click on this padding we use this expose
256+
defineExpose({
257+
click
258+
});
251259
</script>
252260
```
253261

adminforth/documentation/docs/tutorial/05-ListOfAdapters.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,18 @@ Up to the winter 2026 OpenAI models are one of the most powerful image generatio
106106

107107
---
108108

109+
### Gemini (Nano Banana) Image Generation Adapter
110+
111+
```
112+
pnpm i @adminforth/image-generation-adapter-nano-banana
113+
```
114+
115+
Uses the latest gemini-3.1-flash-image-preview model for instant image generation with text descriptions.
116+
117+
This model is the top of the Nano Banana line as of 2026, combining the lightning-fast speed of the Flash series with the improved detail of version 3.1. The adapter allows you to integrate the advanced capabilities of previous models into your interface, providing high-precision visualization for even the most specific and complex queries.
118+
119+
---
120+
109121
## 💾 Storage Adapters
110122

111123

adminforth/documentation/docs/tutorial/08-Plugins/14-markdown.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,32 @@ plugins: [
205205

206206
>👆 Full list of buttons you can enable or disable via topPanelSettings:
207207
bold, italic, underline, strike, h1, h2, h3, ul, ol, link, codeBlock
208+
209+
210+
## Change size of show view markdown text
211+
By default in markdown plugin turned cumpact preview mode, but you can turn it off:
212+
213+
214+
```ts
215+
new MarkdownPlugin({
216+
fieldName: 'description',
217+
//diff-add
218+
compactShowPreview: false,
219+
...
220+
})
221+
222+
```
223+
224+
## Limiting height of the markdown renderer on show view
225+
If you have really long markdown text and you don't wan't it to take the whole space on show view page, you can use `maxShowViewContainerHeightPx`, that will add max-height to the markdown renderer and button `show more`, so you can expand it.
226+
227+
```ts
228+
new MarkdownPlugin({
229+
fieldName: 'description',
230+
compactShowPreview: false,
231+
//diff-add
232+
maxShowViewContainerHeightPx: 400,
233+
...
234+
})
235+
236+
```

adminforth/documentation/docs/tutorial/08-Plugins/19-login-captcha.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ To use the plugin, add it to your user resource file. Here's an example:
2525
```ts title="./resources/adminuser.ts"
2626
// Import the plugin and adapter
2727
import CaptchaPlugin from "@adminforth/login-captcha";
28-
import CaptchaAdapterCloudflare from "@adminforth/captcha-adapter-cloudflare";
28+
import CaptchaAdapterCloudflare from "@adminforth/login-captcha-adapter-cloudflare";
2929

3030
...
3131

adminforth/documentation/docs/tutorial/08-Plugins/23-background-jobs.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,11 @@ For example:
438438
439439
```ts
440440
...
441+
//diff-add
442+
import { useBackgroundJobApi } from '@/custom/plugins/BackgroundJobsPlugin/useBackgroundJobApi.ts';
443+
444+
//diff-add
445+
const backgroundJobApi = useBackgroundJobApi();
441446

442447
const res = await callAdminForthApi({
443448
path: `/plugin/${props.meta.pluginInstanceId}/translate-selected-to-languages`,
@@ -453,7 +458,7 @@ For example:
453458
const jobId = res.jobId;
454459
if (jobId) {
455460
//diff-add
456-
window.OpenJobInfoPopup(jobId);
461+
backgroundJobApi.openJobInfoPopup(jobId);
457462
}
458463
}
459464

0 commit comments

Comments
 (0)