Skip to content

Commit 882fb67

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents c4a99e8 + 436b828 commit 882fb67

File tree

22 files changed

+365
-211
lines changed

22 files changed

+365
-211
lines changed

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Why AdminForth:
2727
* Define express APIs and call them from your components and pages
2828
* Use various modern back-office-must-have plugins like audit log, files/image upload, TOTP 2FA, I18N, Copilot-style AI writing and image generation
2929

30+
3031
## Project initialisation
3132

3233
```
@@ -56,27 +57,35 @@ npx adminforth create-app
5657

5758
# For developers
5859

59-
```
60+
The most convenient way to add new features or fixes is using `dev-demo`. It imports the source code of the repository and plugins so you can edit them and see changes on the fly.
61+
62+
Fork repo, pull it and do next:
63+
64+
65+
```sh
6066
cd adminforth
6167
npm ci
6268
npm run build
6369

64-
6570
# this will install all official plugins and link adminforth package, if plugin installed it will git pull and npm ci
6671
npm run install-plugins
6772

6873
# same for official adapters
6974
npm run install-adapters
75+
```
7076

71-
# this is dev demo for development
77+
To run dev demo:
78+
```sh
7279
cd dev-demo
7380
cp .env.sample .env
7481
npm ci
7582
npm run migrate
7683
npm start
7784
```
7885

79-
Add some columns to a database. Open .prisma file, modify it, and run:
86+
## Adding columns to a database in dev-demo
87+
88+
Open `.prisma` file, modify it, and run:
8089

8190
```
8291
npm run namemigration -- --name desctiption_of_changes

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ export const admin = new AdminForth({
1313
rememberMeDays: 30,
1414
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',
1515
loginBackgroundPosition: '1/2',
16-
demoCredentials: "adminforth:adminforth",
17-
loginPromptHTML: "Use email <b>adminforth</b> and password <b>adminforth</b> to login",
1816
},
1917
customization: {
2018
brandName: "{{appName}}",
@@ -24,6 +22,7 @@ export const admin = new AdminForth({
2422
datesFormat: 'DD MMM',
2523
timeFormat: 'HH:mm a',
2624
showBrandNameInSidebar: true,
25+
emptyFieldPlaceholder: '-',
2726
styles: {
2827
colors: {
2928
light: {

adminforth/dataConnectors/mysql.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,15 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
8686
} else if (baseType.startsWith('json')) {
8787
field.type = AdminForthDataTypes.JSON;
8888
field._underlineType = 'json';
89-
} else if (baseType.startsWith('date')) {
90-
field.type = AdminForthDataTypes.DATE;
91-
field._underlineType = 'date';
9289
} else if (baseType.startsWith('time')) {
9390
field.type = AdminForthDataTypes.TIME;
9491
field._underlineType = 'time';
9592
} else if (baseType.startsWith('datetime') || baseType.startsWith('timestamp')) {
9693
field.type = AdminForthDataTypes.DATETIME;
9794
field._underlineType = 'timestamp';
95+
} else if (baseType.startsWith('date')) {
96+
field.type = AdminForthDataTypes.DATE;
97+
field._underlineType = 'date';
9898
} else if (baseType.startsWith('year')) {
9999
field.type = AdminForthDataTypes.INTEGER;
100100
field._underlineType = 'year';
@@ -232,7 +232,7 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
232232
console.log('🪲📜 MySQL Q:', q, 'values:', filterValues);
233233
}
234234
const [results] = await this.client.query(q, filterValues);
235-
return +results[0].count;
235+
return +results[0]["COUNT(*)"];
236236
}
237237

238238
async getMinMaxForColumnsWithOriginalTypes({ resource, columns }) {
253 KB
Loading

adminforth/documentation/blog/2025-02-19-compose-ec2-deployment-ci-registry/index.md

Lines changed: 60 additions & 45 deletions
Large diffs are not rendered by default.

adminforth/documentation/blog/authors.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
ivanb:
2-
name: Ivan Borshcho
2+
name: Ivan Borshchov
33
title: Maintainer of AdminForth
44
url: https://github.com/ivictbor
55
image_url: https://avatars.githubusercontent.com/u/1838656?v=4

adminforth/documentation/docs/tutorial/05-Plugins/01-AuditLog.md

Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -54,54 +54,59 @@ Also, it excludes itself to avoid infinte logging loop.
5454
Add this code in `auditLogs.ts`:
5555
5656
```ts title='./resources/auditLogs.ts'
57-
export default {
58-
dataSource: 'maindb',
59-
table: 'audit_logs',
60-
columns: [
61-
{ name: 'id', primaryKey: true, required: false, fillOnCreate: ({initialRecord}: any) => uuid(),
62-
showIn: {
63-
list: false,
64-
edit: false,
65-
create: false,
66-
filter: false,
67-
} },
68-
{ name: 'created_at', required: false },
69-
{ name: 'resource_id', required: false },
70-
{ name: 'user_id', required: false,
71-
foreignResource: {
72-
resourceId: 'adminuser',
73-
} },
74-
{ name: 'action', required: false },
75-
{ name: 'diff', required: false, type: AdminForthDataTypes.JSON, showIn: {
76-
list: false,
77-
edit: false,
78-
create: false,
79-
filter: false,
80-
} },
81-
{ name: 'record_id', required: false },
82-
],
83-
options: {
84-
allowedActions: {
57+
58+
import AuditLogPlugin from "@adminforth/audit-log/index.js";
59+
import { AdminForthDataTypes } from "adminforth";
60+
import { randomUUID } from "crypto";
61+
62+
export default {
63+
dataSource: 'maindb',
64+
table: 'audit_logs',
65+
columns: [
66+
{ name: 'id', primaryKey: true, required: false, fillOnCreate: ({initialRecord}: any) => randomUUID(),
67+
showIn: {
68+
list: false,
69+
edit: false,
70+
create: false,
71+
filter: false,
72+
} },
73+
{ name: 'created_at', required: false },
74+
{ name: 'resource_id', required: false },
75+
{ name: 'user_id', required: false,
76+
foreignResource: {
77+
resourceId: 'adminuser',
78+
} },
79+
{ name: 'action', required: false },
80+
{ name: 'diff', required: false, type: AdminForthDataTypes.JSON, showIn: {
81+
list: false,
8582
edit: false,
86-
delete: false,
87-
create: false
83+
create: false,
84+
filter: false,
85+
} },
86+
{ name: 'record_id', required: false },
87+
],
88+
options: {
89+
allowedActions: {
90+
edit: false,
91+
delete: false,
92+
create: false
93+
}
94+
},
95+
plugins: [
96+
new AuditLogPlugin({
97+
// if you want to exclude some resources from logging
98+
//excludeResourceIds: ['adminuser'],
99+
resourceColumns: {
100+
resourceIdColumnName: 'resource_id',
101+
resourceActionColumnName: 'action',
102+
resourceDataColumnName: 'diff',
103+
resourceUserIdColumnName: 'user_id',
104+
resourceRecordIdColumnName: 'record_id',
105+
resourceCreatedColumnName: 'created_at'
88106
}
89-
},
90-
plugins: [
91-
new AuditLogPlugin({
92-
// if you want to exclude some resources from logging
93-
//excludeResourceIds: ['adminuser'],
94-
resourceColumns: {
95-
resourceIdColumnName: 'resource_id',
96-
resourceActionColumnName: 'action',
97-
resourceDataColumnName: 'diff',
98-
resourceUserIdColumnName: 'user_id',
99-
resourceRecordIdColumnName: 'record_id',
100-
resourceCreatedColumnName: 'created_at'
101-
}
102-
}),
103-
],
104-
}
107+
}),
108+
],
109+
}
105110
```
106111
107112
Then you need to import `./resources/auditLogs`:

adminforth/documentation/docs/tutorial/05-Plugins/10-i18n.md

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ model translations {
4040
// we need both indexes on en_string+category and separately on category
4141
@@index([en_string, category])
4242
@@index([category])
43+
@@index([completedLangs])
4344
}
4445
```
4546
@@ -454,7 +455,7 @@ import { admin } from '../index';
454455
455456
You can use this module not only to translate admin area of your application but also to translate other parts like SEO-facing or user-facing services.
456457
This will allow you to reuse the same functionality and AI completion adapters for all your translations and manage them in one place.
457-
For example in this app we will consider translating a Nuxt.js SEO-centric frontend which we want to translate with [vue-i18n](https://vue-i18n.intlify.dev/).
458+
For example in this app we will consider translating a Nuxt.js SEO-centric frontend which we want to translate with [nuxt i18n](https://i18n.nuxtjs.org/).
458459
459460
To do it you need to use 2 exposed methods from the plugin: `feedCategoryTranslations` and `getCategoryTranslations`.
460461
@@ -478,7 +479,7 @@ First of all, at some step, e.g. CI pipeline you should get all translation stri
478479

479480
admin.getPluginByClassName<I18nPlugin>('I18nPlugin').feedCategoryTranslations(
480481
messagesForFeed,
481-
'nextApp'
482+
'seoApp'
482483
)
483484

484485
res.json({
@@ -489,60 +490,99 @@ First of all, at some step, e.g. CI pipeline you should get all translation stri
489490

490491
```
491492
492-
For extracting 18n messages we use [vue-i18n-extract](https://github.com/Spittal/vue-i18n-extract) package.
493+
For extracting i18n messages we use [vue-i18n-extract](https://github.com/Spittal/vue-i18n-extract) package.
493494
You can add extract command to `package.json`:
494495
495496
```json
496497
{
497498
"scripts": {
498-
"i18n:extract": "echo '{}' > i18n-empty.json && vue-i18n-extract report --vueFiles './src/**/*.?(js|vue)' --output ./i18n-messages.json --languageFiles 'i18n-empty.json' --add",
499-
"i18n:feed-to-backoffice": "npm run i18n:extract && curl -X POST -H 'Content-Type: application/json' -d @i18n-messages.json http://adminforth:3000/feed-nuxt-strings"
499+
"i18n:extract": "echo '{}' > i18n-empty.json && vue-i18n-extract report --vueFiles './?(pages|components)/**/*.?(js|vue)' --output ./i18n-messages.json --languageFiles 'i18n-empty.json' --add",
500+
"i18n:feed-to-local-backoffice": "npm run i18n:extract && curl -X POST -H 'Content-Type: application/json' -d @i18n-messages.json http://localhost:3000/feed-nuxt-strings"
500501
}
501502
}
502503
```
503504
504-
Make sure to replace `adminforth:3000` with AdminForth API URL. We are assuming it is docker container name in internal docker network.
505+
> For plain non-nuxt apps `--vueFiles './?(pages|components)/**/*.?(js|vue)'` should be replaced with `--vueFiles './src/**/*.?(js|vue)'`
505506
506-
So in the pipeline you should run `npm run i18n:feed-to-backoffice` to extract messages from your Nuxt.js app and feed them to AdminForth.
507+
Make sure to replace `localhost:3000` with AdminForth API URL.
507508
508-
> 👆 The example method is just a stub, please make sure you not expose endpoint to public or add some simple authorization on it,
509-
> otherwise someone might flood you with dump translations requests.
510-
511-
Then in your Nuxt.js app you should call this API and store the strings in the same.
509+
So locally you can run `npm run i18n:feed-to-local-backoffice` to extract messages from your Nuxt.js app and feed them to AdminForth.
512510
513511
Next part. When we will need translations on the nuxt instance, we should use [vue-i18n's lazy loading feature](https://vue-i18n.intlify.dev/guide/advanced/lazy):
514512
515513
```typescript title="./your-nuxt-app-source-file.ts"
516-
import { callAdminForthApi } from '@/utils';
517-
518-
export async function loadLocaleMessages(i18n, locale) {
519-
// load locale messages with dynamic import
520-
const messages = await callAdminForthApi({
521-
path: `/api/translations/?lang=${locale}`,
514+
const { locale, setLocaleMessage, } = useI18n();
515+
const { data: messages } = await useFetch(
516+
useRuntimeConfig().public.adminUrl + `api/translations/?lang=${locale.value}`,
517+
{
522518
method: 'GET',
523-
});
524-
525-
// set locale and locale message
526-
i18n.global.setLocaleMessage(locale, messages.default)
527-
528-
return nextTick()
519+
}
520+
);
521+
if (messages.value) {
522+
setLocaleMessage(locale.value, messages.value);
523+
} else {
524+
console.error('Translations or language data are missing.');
529525
}
530526
```
531527
532-
See [vue-i18n's lazy loading feature](https://vue-i18n.intlify.dev/guide/advanced/lazy) to understand where better to call `loadLocaleMessages` function.
533-
534528
Here is how API for messages will look:
535529
536530
```ts title="./index.ts"
537531
app.get(`${ADMIN_BASE_URL}/api/translations/`,
538532
async (req, res) => {
539-
const lang = req.query.lang;
540-
const messages = await admin.getPluginByClassName<I18nPlugin>('I18nPlugin').getCategoryTranslations('nextApp', lang);
533+
const lang = req.query.lang as string;
534+
const messages = await admin.getPluginByClassName<I18nPlugin>('I18nPlugin').getCategoryTranslations('seoApp', lang);
541535
res.json(messages);
542536
}
543537
);
544538
```
545539
540+
Also in `nuxt.config.ts` you can set `prefix_except_default` strategy and put it in `i18n` config as this:
541+
542+
```
543+
...
544+
i18n: {
545+
locales: ['en', 'uk', 'ja', 'fr'],
546+
defaultLocale: 'en',
547+
strategy: 'prefix_except_default',
548+
detectBrowserLanguage: false,
549+
},
550+
...
551+
```
552+
553+
554+
#### Feeding messages during build time
555+
556+
For Dockerized pipelines you can feed messages when building the image in Docker build time:
557+
558+
```dockerfile
559+
FROM node:22-alpine
560+
WORKDIR /app
561+
ADD package.json /app
562+
ADD package-lock.json /app
563+
RUN true | npm ci
564+
ADD . /app
565+
RUN --mount=type=cache,target=/app/node_modules/.cache true | npm run build
566+
567+
//diff-add
568+
# Run i18n extraction and API call
569+
//diff-add
570+
RUN npm run i18n:extract && \
571+
//diff-add
572+
curl -X POST -H 'Content-Type: application/json' -d @i18n-messages.json http://adminforth:3000/bo/api/feed-nuxt-strings
573+
574+
CMD ["node", ".output/server/index.mjs"]
575+
EXPOSE 3000
576+
```
577+
578+
This assumes that `http://adminforth:3000/` is the name of the Docker service running AdminForth.
579+
580+
This will work if you are building the image in the same Docker network as AdminForth. Otherwise you might need use absolute URL of the AdminForth instance.
581+
582+
> 👆 The example method is just a stub, please make sure you not expose endpoint to public or add some simple authorization on it,
583+
> otherwise someone might flood you with dump translations requests.
584+
585+
546586
### Get language names
547587

548588
Also you can use handy method to get language names in native and English form with emoji flags:
@@ -575,3 +615,16 @@ Response will look like this:
575615
"emojiFlag": "🇦🇷"
576616
},
577617
```
618+
619+
### Disable translation for admin (external app only)
620+
621+
If you want to use this plugin only for external app, and not backoffice, which is probably the case, you can disable translation for admin by setting `externalAppOnly` to `true`:
622+
623+
```ts title="./resources/translations.ts"
624+
new I18nPlugin({
625+
//diff-add
626+
externalAppOnly: true,
627+
supportedLanguages: ['en', 'uk', 'ja', 'fr'],
628+
...
629+
}),
630+
```

0 commit comments

Comments
 (0)