Skip to content

Commit 4aa8a36

Browse files
committed
Merge branch 'next' of https://github.com/devforth/adminforth into next
2 parents 512ed8c + c854afe commit 4aa8a36

36 files changed

+1259
-676
lines changed

adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ Now create file `CheckReadingTime.vue` in the `custom` folder of your project:
277277
```html title="./custom/CheckReadingTime.vue"
278278
<template>
279279
<div class="text-gray-500 text-sm">
280-
<div @click="checkReadingTime" class="cursor-pointer flex gap-2 items-center">
280+
<div class="cursor-pointer flex gap-2 items-center">
281281
Check reading time
282282
</div>
283283
</div>
@@ -287,6 +287,10 @@ Now create file `CheckReadingTime.vue` in the `custom` folder of your project:
287287
import { getReadingTime} from "text-analyzer";
288288
import adminforth from '@/adminforth';
289289
290+
defineExpose({
291+
click,
292+
});
293+
290294
function checkReadingTime() {
291295
const text = document.querySelector('[data-af-column="description"]')?.innerText;
292296
if (text) {
@@ -298,6 +302,11 @@ function checkReadingTime() {
298302
}
299303
adminforth.list.closeThreeDotsDropdown();
300304
}
305+
306+
function click() {
307+
checkReadingTime();
308+
}
309+
301310
</script>
302311
```
303312
@@ -312,6 +321,7 @@ npm i text-analyzer
312321
313322
> ☝️ Please note that we are using AdminForth [Frontend API](/docs/api/FrontendAPI/interfaces/FrontendAPIInterface/) `adminforth.list.closeThreeDotsDropdown();` to close the dropdown after the item is clicked.
314323
324+
>☝️ Please note that the injected component might have an exposed click function as well as a defined click function, which executes the click on component logic.
315325
316326
## List table custom action icons
317327

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -797,7 +797,7 @@ const isoFlagToEmoji = (iso) => iso.toUpperCase().replace(/./g, char => String.f
797797

798798

799799

800-
## Pagination
800+
### Pagination
801801

802802
Table provides front-end side pagination. You can set `pageSize` (default is 10) to set how many rows to show per page.
803803
If there is less then `pageSize` rows, pagination will not be shown.
@@ -829,6 +829,38 @@ If there is less then `pageSize` rows, pagination will not be shown.
829829
</div>
830830
</div>
831831

832+
### Server-side pagination
833+
834+
To load pages dynamically, simply pass async callback to data:
835+
836+
```ts
837+
async function loadPageData(data) {
838+
const { offset, limit } = data;
839+
// in real app do await callAdminForthApi or await fetch to get date, use offset and limit value to slice data
840+
return {
841+
data: [
842+
{ name: 'John', age: offset, country: 'US' },
843+
{ name: 'Rick', age: offset+1, country: 'CA' },
844+
{ name: 'Alice', age: offset+2, country: 'BR' },
845+
],
846+
total: 3 // should return total amount of records in database
847+
}
848+
}
849+
850+
<Table
851+
:columns="[
852+
{ label: 'Name', fieldName: 'name' },
853+
{ label: 'Age', fieldName: 'age' },
854+
{ label: 'Country', fieldName: 'country' },
855+
]"
856+
//diff-remove
857+
:data="[...]
858+
//diff-add
859+
:data="loadPageData"
860+
861+
:pageSize="3"> </Table>
862+
```
863+
> 👆 The page size is used as the limit for pagination.
832864
833865
## ProgressBar
834866

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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Main features:
55
- Stores all translation strings in your application in a single AdminForth resource. You can set [allowed actions](/docs/tutorial/Customization/limitingAccess/) only to Developers/Translators role if you don't want other users to see/edit the translations.
66
- Supports AI completion adapters to help with translations. For example, you can use OpenAI ChatGPT to generate translations. Supports correct pluralization, even for Slavic languages.
77
- Supports any number of languages.
8+
- Supports BCP47 language codes (e.g., `en-GB`, `pt-BR`) for regional language variants.
9+
- Configurable primary language.
810

911

1012
Under the hood it uses vue-i18n library and provides several additional facilities to make the translation process easier.
@@ -46,6 +48,8 @@ model translations {
4648
4749
If you want more languages, just add more fields like `uk_string`, `ja_string`, `fr_string`, `es_string` to the model.
4850
51+
> 💡 **Tip**: For regional language variants, you can also use BCP47 codes like `en-GB`, `pt-BR`, `fr-CA` and add corresponding fields like `enGB_string`, `ptBR_string`, `frCA_string`. See the [BCP47 Language Code Support](#bcp47-language-code-support) section for more details.
52+
4953
Next, add resource for translations:
5054
5155
```ts title='./resources/translations.ts'
@@ -86,6 +90,9 @@ export default {
8690
// will hel to filter out incomplete translations
8791
completedFieldName: 'completedLangs',
8892

93+
// optional: set primary language (defaults to 'en' if not specified)
94+
// primaryLanguage: 'fr', // Uncomment to set French as primary language
95+
8996
completeAdapter: new CompletionAdapterOpenAIChatGPT({
9097
openAiApiKey: process.env.OPENAI_API_KEY as string,
9198
model: 'gpt-4o-mini',
@@ -205,6 +212,125 @@ You can add translations for each language manually or use Bulk actions to gener
205212
206213
For simplicity you can also use filter to get only untranslated strings and complete them one by one (filter name "Fully translated" in the filter).
207214
215+
## BCP47 Language Code Support
216+
217+
The i18n plugin supports BCP47 language tags, which allow you to specify regional variants of languages. This is particularly useful for applications that need to support different regional dialects or cultural variations.
218+
219+
### Supported Language Code Formats
220+
221+
- **ISO 639-1 codes**: Basic language codes like `en`, `fr`, `de`, `es`
222+
- **BCP47 tags**: Regional variants like `en-GB` (British English), `pt-BR` (Brazilian Portuguese), `en-US` (American English)
223+
224+
### Example with Regional Variants
225+
226+
Here's how to configure the plugin to support both basic and regional language codes:
227+
228+
```ts title='./resources/translations.ts'
229+
export default {
230+
dataSource: "maindb",
231+
table: "translations",
232+
resourceId: "translations",
233+
label: "Translations",
234+
235+
recordLabel: (r: any) => `✍️ ${r.en_string}`,
236+
plugins: [
237+
new I18nPlugin({
238+
// Support both basic and regional language codes
239+
supportedLanguages: ['en', 'en-GB', 'en-US', 'pt', 'pt-BR', 'fr', 'fr-CA'],
240+
241+
// Map language codes to database field names
242+
translationFieldNames: {
243+
en: 'en_string',
244+
'en-GB': 'enGB_string',
245+
'en-US': 'enUS_string',
246+
pt: 'pt_string',
247+
'pt-BR': 'ptBR_string',
248+
fr: 'fr_string',
249+
'fr-CA': 'frCA_string',
250+
},
251+
252+
categoryFieldName: 'category',
253+
sourceFieldName: 'source',
254+
completedFieldName: 'completedLangs',
255+
256+
completeAdapter: new CompletionAdapterOpenAIChatGPT({
257+
openAiApiKey: process.env.OPENAI_API_KEY as string,
258+
model: 'gpt-4o-mini',
259+
expert: {
260+
temperature: 0.5,
261+
},
262+
}),
263+
}),
264+
],
265+
// ... rest of configuration
266+
} as AdminForthResourceInput;
267+
```
268+
269+
### Database Schema for Regional Variants
270+
271+
When using BCP47 codes, make sure your database schema includes fields for each regional variant:
272+
273+
```ts title='./schema.prisma'
274+
model translations {
275+
id String @id
276+
en_string String
277+
created_at DateTime
278+
279+
// Basic language codes
280+
fr_string String?
281+
pt_string String?
282+
283+
// Regional variants (BCP47)
284+
enGB_string String? // British English
285+
enUS_string String? // American English
286+
ptBR_string String? // Brazilian Portuguese
287+
frCA_string String? // Canadian French
288+
289+
category String
290+
source String?
291+
completedLangs String?
292+
293+
@@index([en_string, category])
294+
@@index([category])
295+
@@index([completedLangs])
296+
}
297+
```
298+
299+
## Primary Language Configuration
300+
301+
The `primaryLanguage` option allows you to set the default language for your application. This is particularly useful when your application's primary language is not English.
302+
303+
### How Primary Language Works
304+
305+
- **Default Language**: The language shown to users by default
306+
- **Fallback Chain**: When a translation is missing, the system falls back to: `requested language``primaryLanguage``English`
307+
- **Translation Source**: English (`en_string`) is always used as the source for AI translations, regardless of the primary language setting
308+
309+
### Example: Portuguese as Primary Language
310+
311+
```ts title='./resources/translations.ts'
312+
export default {
313+
// ... other configuration
314+
plugins: [
315+
new I18nPlugin({
316+
// Set Portuguese as the primary language
317+
primaryLanguage: 'pt-BR',
318+
319+
supportedLanguages: ['pt-BR', 'en', 'en-GB', 'es', 'fr'],
320+
translationFieldNames: {
321+
'pt-BR': 'ptBR_string',
322+
en: 'en_string',
323+
'en-GB': 'enGB_string',
324+
es: 'es_string',
325+
fr: 'fr_string',
326+
},
327+
328+
// ... rest of configuration
329+
}),
330+
],
331+
// ... rest of configuration
332+
} as AdminForthResourceInput;
333+
```
208334
209335
## Translation for custom components
210336
@@ -458,6 +584,8 @@ If you don't use params, you can use `tr` without third param:
458584
}
459585
```
460586
587+
> 🔄 **Fallback Behavior**: The `tr` function automatically handles fallbacks when translations are missing. It follows this chain: `requested language``primaryLanguage``English`. This ensures users always see content in a language they can understand, even if specific translations are missing.
588+
461589
> 🙅‍♂️ Temporary limitation: For now all translations strings for backend (adminforth internal and for from custom APIs)
462590
appear in Translations resource and table only after they are used. So greeting string will appear in the Translations table only after the first request to the API which reaches the `tr` function call.
463591
> So to collect all translations you should use your app for some time and make sure all strings are used at

adminforth/documentation/docs/tutorial/07-Plugins/16-email-invite.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ export default {
9898
//diff-add
9999
emailField: 'email',
100100
//diff-add
101-
passwordField: 'password_hash',
101+
passwordField: 'password',
102102
//diff-add
103103
sendFrom: 'noreply@yourapp.com',
104104
//diff-add
@@ -164,7 +164,7 @@ export default {
164164
new EmailInvitePlugin({
165165
emailField: 'email',
166166
sendFrom: 'noreply@yourapp.com',
167-
passwordField: 'password_hash',
167+
passwordField: 'password',
168168
adapter: new EmailAdapterAwsSes(/* ... */),
169169
//diff-add
170170
emailConfirmedField: 'email_confirmed', // Enable email confirmation
@@ -191,7 +191,7 @@ import EmailAdapterMailgun from "@adminforth/email-adapter-mailgun";
191191
plugins: [
192192
new EmailInvitePlugin({
193193
emailField: 'email',
194-
passwordField: 'password_hash',
194+
passwordField: 'password',
195195
sendFrom: 'noreply@yourapp.com',
196196
//diff-add
197197
adapter: new EmailAdapterMailgun({

adminforth/modules/styles.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,9 @@ export const styles = () => ({
251251
lightTableOddBackground: "#FFFFFF",
252252
lightTablePaginationText: "#6B7280",
253253
lightTablePaginationNumeration: "#111827",
254+
lightTablePaginationInputBackground: "#FFFFFF",
255+
lightTablePaginationInputBorder: "#D1D5DB",
256+
lightTablePaginationInputText: "#111827",
254257
lightUnactivePaginationButtonBackground: "#FFFFFF",
255258
lightUnactivePaginationButtonText: "#6B7280",
256259
lightUnactivePaginationButtonBorder: "#D1D5DB",
@@ -580,6 +583,9 @@ export const styles = () => ({
580583
darkTableOddBackground: "#111827",
581584
darkTablePaginationText: "#9CA3AF",
582585
darkTablePaginationNumeration: "#FFFFFF",
586+
darkTablePaginationInputBackground: "#1f2937",
587+
darkTablePaginationInputBorder: "#374151",
588+
darkTablePaginationInputText: "#FFFFFF",
583589
darkUnactivePaginationButtonBackground: "#1F2937",
584590
darkUnactivePaginationButtonText: "#9CA3AF",
585591
darkUnactivePaginationButtonBorder: "#374151",

adminforth/spa/src/adminforth.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { FilterParams, FrontendAPIInterface } from "./types/FrontendAPI";
2-
import type { FrontendAPIInterface, ConfirmParams, AlertParams, } from '@/types/FrontendAPI';
3-
import type { AdminForthFilterOperators, AdminForthResourceColumn } from '@/types/Common';
1+
import type { FilterParams, FrontendAPIInterface, ConfirmParams, AlertParams, } from '@/types/FrontendAPI';
2+
import type { AdminForthFilterOperators, AdminForthResourceColumnCommon } from '@/types/Common';
43
import { useToastStore } from '@/stores/toast';
54
import { useModalStore } from '@/stores/modal';
65
import { useCoreStore } from '@/stores/core';
@@ -85,7 +84,7 @@ class FrontendAPI implements FrontendAPIInterface {
8584
}
8685
}
8786

88-
confirm(params: ConfirmParams): Promise<void> {
87+
confirm(params: ConfirmParams): Promise<boolean> {
8988
return new Promise((resolve, reject) => {
9089
this.modalStore.setModalContent({
9190
content: params.message,
@@ -112,7 +111,7 @@ class FrontendAPI implements FrontendAPIInterface {
112111
throw new Error(`Cannot use ${this.setListFilter.name} filter on a list page`)
113112
} else {
114113
console.log(this.coreStore.resourceColumnsWithFilters,'core store')
115-
const filterField = this.coreStore.resourceColumnsWithFilters.find((col: AdminForthResourceColumn) => col.name === filter.field)
114+
const filterField = this.coreStore.resourceColumnsWithFilters.find((col: AdminForthResourceColumnCommon) => col.name === filter.field)
116115
if(!filterField){
117116
throw new Error(`Field ${filter.field} is not available for filtering`)
118117
}
@@ -123,7 +122,7 @@ class FrontendAPI implements FrontendAPIInterface {
123122

124123
setListFilter(filter: FilterParams): void {
125124
if(this.listFilterValidation(filter)){
126-
if(this.filtersStore.filters.some((f) => {return f.field === filter.field && f.operator === filter.operator})){
125+
if(this.filtersStore.filters.some((f: any) => {return f.field === filter.field && f.operator === filter.operator})){
127126
throw new Error(`Filter ${filter.field} with operator ${filter.operator} already exists`)
128127
} else {
129128
this.filtersStore.setFilter(filter)

adminforth/spa/src/afcl/BarChart.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ const optionsBase = {
6060
tooltip: {
6161
shared: true,
6262
intersect: false,
63-
formatter: function (value) {
63+
formatter: function (value: any) {
6464
return value
6565
},
6666
},
@@ -71,7 +71,7 @@ const optionsBase = {
7171
fontFamily: "Inter, sans-serif",
7272
cssClass: 'text-xs font-normal fill-gray-500 dark:fill-gray-400'
7373
},
74-
formatter: function (value) {
74+
formatter: function (value: any) {
7575
return value
7676
}
7777
},

adminforth/spa/src/afcl/Checkbox.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
type="checkbox"
77
:checked="props.modelValue"
88
:disabled="props.disabled"
9-
@change="$emit('update:modelValue', $event.target.checked)"
9+
@change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
1010
class="peer appearance-none min-w-4 min-h-4 bg-lightCheckboxBgUnchecked border border-lightCheckboxBorderColor rounded-sm checked:bg-lightCheckboxBgChecked
1111
focus:ring-lightFocusRing dark:focus:ring-darkFocusRing dark:focus:ring-darkFocusRing
1212
focus:ring-2 dark:bg-darkCheckboxBgUnchecked dark:border-darkCheckboxBorderColor dark:checked:bg-darkCheckboxBgChecked cursor-pointer"

adminforth/spa/src/afcl/Dropzone.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<form class="flex items-center justify-center w-full"
44
@dragover.prevent="dragging = true"
55
@dragleave.prevent="dragging = false"
6-
@drop.prevent="dragging = false; doEmit($event.dataTransfer.files)"
6+
@drop.prevent="dragging = false; doEmit(($event.dataTransfer as DataTransfer).files)"
77
>
88
<label :id="id" class="flex flex-col items-center justify-center w-full border-2 border-dashed rounded-lg cursor-pointer
99
hover:bg-lightDropzoneBackgroundHover hover:border-lightDropzoneBorderHover dark:hover:border-darkDropzoneBorderHover dark:hover:bg-darkDropzoneBackgroundHover"
@@ -42,7 +42,7 @@
4242
</div>
4343
<input :id="id" type="file" class="hidden"
4444
:accept="props.extensions.join(', ')"
45-
@change="doEmit($event.target.files)"
45+
@change="$event.target && doEmit(($event.target as HTMLInputElement).files!)"
4646
:multiple="props.multiple || false"
4747
/>
4848
</label>

adminforth/spa/src/afcl/Input.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
ref="input"
1313
v-bind="$attrs"
1414
:type="type"
15-
@input="$emit('update:modelValue', type === 'number' ? Number($event.target?.value) : $event.target?.value)"
15+
@input="$emit('update:modelValue', type === 'number' ? Number(($event.target as HTMLInputElement)?.value) : ($event.target as HTMLInputElement)?.value)"
1616
:value="modelValue"
1717
aria-describedby="helper-text-explanation"
1818
class="afcl-input inline-flex bg-lightInputBackground border border-lightInputBorder text-lightInputText text-sm rounded-0 focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary

0 commit comments

Comments
 (0)