adminforth 2.4.0-next.7 → 2.4.0-next.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/commands/callTsProxy.js +14 -4
  2. package/commands/cli.js +12 -4
  3. package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
  4. package/commands/createApp/templates/index.ts.hbs +1 -1
  5. package/commands/createApp/templates/package.json.hbs +1 -1
  6. package/commands/createApp/utils.js +39 -13
  7. package/commands/createCustomComponent/configUpdater.js +25 -21
  8. package/commands/createCustomComponent/fileGenerator.js +1 -1
  9. package/commands/createCustomComponent/main.js +2 -1
  10. package/commands/createCustomComponent/templates/login/beforeLogin.vue.hbs +18 -0
  11. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  12. package/dist/dataConnectors/baseConnector.js +16 -3
  13. package/dist/dataConnectors/baseConnector.js.map +1 -1
  14. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  15. package/dist/dataConnectors/mongo.js +14 -14
  16. package/dist/dataConnectors/mongo.js.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +20 -9
  20. package/dist/index.js.map +1 -1
  21. package/dist/modules/codeInjector.d.ts.map +1 -1
  22. package/dist/modules/codeInjector.js +25 -9
  23. package/dist/modules/codeInjector.js.map +1 -1
  24. package/dist/modules/configValidator.d.ts.map +1 -1
  25. package/dist/modules/configValidator.js +50 -1
  26. package/dist/modules/configValidator.js.map +1 -1
  27. package/dist/modules/restApi.d.ts.map +1 -1
  28. package/dist/modules/restApi.js +45 -2
  29. package/dist/modules/restApi.js.map +1 -1
  30. package/dist/modules/styles.d.ts +42 -0
  31. package/dist/modules/styles.d.ts.map +1 -1
  32. package/dist/modules/styles.js +44 -2
  33. package/dist/modules/styles.js.map +1 -1
  34. package/dist/spa/index.html +1 -1
  35. package/dist/spa/src/App.vue +14 -5
  36. package/dist/spa/src/afcl/Button.vue +4 -4
  37. package/dist/spa/src/afcl/Checkbox.vue +21 -13
  38. package/dist/spa/src/afcl/CountryFlag.vue +3 -1
  39. package/dist/spa/src/afcl/Dropzone.vue +4 -2
  40. package/dist/spa/src/afcl/Input.vue +5 -3
  41. package/dist/spa/src/afcl/JsonViewer.vue +25 -0
  42. package/dist/spa/src/afcl/Link.vue +1 -1
  43. package/dist/spa/src/afcl/LinkButton.vue +1 -1
  44. package/dist/spa/src/afcl/Select.vue +44 -15
  45. package/dist/spa/src/afcl/Table.vue +8 -8
  46. package/dist/spa/src/afcl/Toggle.vue +32 -0
  47. package/dist/spa/src/afcl/Tooltip.vue +1 -1
  48. package/dist/spa/src/afcl/index.ts +2 -2
  49. package/dist/spa/src/components/AcceptModal.vue +4 -4
  50. package/dist/spa/src/components/ColumnValueInput.vue +21 -2
  51. package/dist/spa/src/components/ColumnValueInputWrapper.vue +1 -0
  52. package/dist/spa/src/components/CustomDatePicker.vue +2 -2
  53. package/dist/spa/src/components/CustomDateRangePicker.vue +1 -0
  54. package/dist/spa/src/components/Filters.vue +73 -28
  55. package/dist/spa/src/components/GroupsTable.vue +3 -3
  56. package/dist/spa/src/components/ResourceForm.vue +61 -26
  57. package/dist/spa/src/components/ResourceListTable.vue +34 -36
  58. package/dist/spa/src/components/ResourceListTableVirtual.vue +23 -25
  59. package/dist/spa/src/components/ShowTable.vue +11 -6
  60. package/dist/spa/src/components/ValueRenderer.vue +4 -4
  61. package/dist/spa/src/controls/BoolToggle.vue +34 -0
  62. package/dist/spa/src/spa_types/core.ts +7 -0
  63. package/dist/spa/src/stores/core.ts +1 -1
  64. package/dist/spa/src/types/Back.ts +46 -12
  65. package/dist/spa/src/types/Common.ts +10 -1
  66. package/dist/spa/src/types/adapters/CompletionAdapter.ts +25 -0
  67. package/dist/spa/src/types/adapters/EmailAdapter.ts +29 -0
  68. package/dist/spa/src/types/adapters/ImageGenerationAdapter.ts +50 -0
  69. package/dist/spa/src/types/adapters/OAuth2Adapter.ts +34 -0
  70. package/dist/spa/src/types/adapters/StorageAdapter.ts +73 -0
  71. package/dist/spa/src/types/adapters/index.ts +5 -0
  72. package/dist/spa/src/utils.ts +209 -0
  73. package/dist/spa/src/views/CreateView.vue +3 -3
  74. package/dist/spa/src/views/EditView.vue +1 -1
  75. package/dist/spa/src/views/ListView.vue +2 -2
  76. package/dist/spa/src/views/LoginView.vue +39 -37
  77. package/dist/spa/src/views/ResourceParent.vue +1 -1
  78. package/dist/spa/src/views/ShowView.vue +3 -3
  79. package/dist/types/Back.d.ts +40 -9
  80. package/dist/types/Back.d.ts.map +1 -1
  81. package/dist/types/Back.js.map +1 -1
  82. package/dist/types/Common.d.ts +9 -0
  83. package/dist/types/Common.d.ts.map +1 -1
  84. package/dist/types/Common.js.map +1 -1
  85. package/dist/types/adapters/CompletionAdapter.d.ts +20 -0
  86. package/dist/types/adapters/CompletionAdapter.d.ts.map +1 -0
  87. package/dist/types/adapters/CompletionAdapter.js +2 -0
  88. package/dist/types/adapters/CompletionAdapter.js.map +1 -0
  89. package/dist/types/adapters/EmailAdapter.d.ts +21 -0
  90. package/dist/types/adapters/EmailAdapter.d.ts.map +1 -0
  91. package/dist/types/adapters/EmailAdapter.js +2 -0
  92. package/dist/types/adapters/EmailAdapter.js.map +1 -0
  93. package/dist/types/adapters/ImageGenerationAdapter.d.ts +37 -0
  94. package/dist/types/adapters/ImageGenerationAdapter.d.ts.map +1 -0
  95. package/dist/types/adapters/ImageGenerationAdapter.js +2 -0
  96. package/dist/types/adapters/ImageGenerationAdapter.js.map +1 -0
  97. package/dist/types/adapters/OAuth2Adapter.d.ts +32 -0
  98. package/dist/types/adapters/OAuth2Adapter.d.ts.map +1 -0
  99. package/dist/types/adapters/OAuth2Adapter.js +2 -0
  100. package/dist/types/adapters/OAuth2Adapter.js.map +1 -0
  101. package/dist/types/adapters/StorageAdapter.d.ts +63 -0
  102. package/dist/types/adapters/StorageAdapter.d.ts.map +1 -0
  103. package/dist/types/adapters/StorageAdapter.js +2 -0
  104. package/dist/types/adapters/StorageAdapter.js.map +1 -0
  105. package/dist/types/adapters/index.d.ts +6 -0
  106. package/dist/types/adapters/index.d.ts.map +1 -0
  107. package/dist/types/adapters/index.js +2 -0
  108. package/dist/types/adapters/index.js.map +1 -0
  109. package/package.json +2 -2
  110. package/dist/spa/src/types/Adapters.ts +0 -213
  111. package/dist/types/Adapters.d.ts +0 -168
  112. package/dist/types/Adapters.d.ts.map +0 -1
  113. package/dist/types/Adapters.js +0 -2
  114. package/dist/types/Adapters.js.map +0 -1
@@ -7,8 +7,11 @@ import { useUserStore } from './stores/user';
7
7
  import { Dropdown } from 'flowbite';
8
8
  import adminforth from './adminforth';
9
9
  import sanitizeHtml from 'sanitize-html'
10
+ import debounce from 'debounce';
10
11
 
11
12
  const LS_LANG_KEY = `afLanguage`;
13
+ const MAX_CONSECUTIVE_EMPTY_RESULTS = 2;
14
+ const ITEMS_PER_PAGE_LIMIT = 100;
12
15
 
13
16
  export async function callApi({path, method, body=undefined}: {
14
17
  path: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
@@ -208,4 +211,210 @@ export function protectAgainstXSS(value: string) {
208
211
  'img': [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ]
209
212
  }
210
213
  });
214
+ }
215
+
216
+ export function isPolymorphicColumn(column: any): boolean {
217
+ return !!(column.foreignResource?.polymorphicResources && column.foreignResource.polymorphicResources.length > 0);
218
+ }
219
+
220
+ export function handleForeignResourcePagination(
221
+ column: any,
222
+ items: any[],
223
+ emptyResultsCount: number = 0,
224
+ isSearching: boolean = false
225
+ ): { hasMore: boolean; emptyResultsCount: number } {
226
+ const isPolymorphic = isPolymorphicColumn(column);
227
+
228
+ if (isPolymorphic) {
229
+ if (isSearching) {
230
+ return {
231
+ hasMore: items.length > 0,
232
+ emptyResultsCount: 0
233
+ };
234
+ } else {
235
+ if (items.length === 0) {
236
+ const newEmptyCount = emptyResultsCount + 1;
237
+ return {
238
+ hasMore: newEmptyCount < MAX_CONSECUTIVE_EMPTY_RESULTS, // Stop loading after 2 consecutive empty results
239
+ emptyResultsCount: newEmptyCount
240
+ };
241
+ } else {
242
+ return {
243
+ hasMore: true,
244
+ emptyResultsCount: 0
245
+ };
246
+ }
247
+ }
248
+ } else {
249
+ return {
250
+ hasMore: items.length === ITEMS_PER_PAGE_LIMIT,
251
+ emptyResultsCount: 0
252
+ };
253
+ }
254
+ }
255
+
256
+ export async function loadMoreForeignOptions({
257
+ columnName,
258
+ searchTerm = '',
259
+ columns,
260
+ resourceId,
261
+ columnOptions,
262
+ columnLoadingState,
263
+ columnOffsets,
264
+ columnEmptyResultsCount
265
+ }: {
266
+ columnName: string;
267
+ searchTerm?: string;
268
+ columns: any[];
269
+ resourceId: string;
270
+ columnOptions: any;
271
+ columnLoadingState: any;
272
+ columnOffsets: any;
273
+ columnEmptyResultsCount: any;
274
+ }) {
275
+ const column = columns?.find(c => c.name === columnName);
276
+ if (!column || !column.foreignResource) return;
277
+
278
+ const state = columnLoadingState[columnName];
279
+ if (state.loading || !state.hasMore) return;
280
+
281
+ state.loading = true;
282
+
283
+ try {
284
+ const list = await callAdminForthApi({
285
+ method: 'POST',
286
+ path: `/get_resource_foreign_data`,
287
+ body: {
288
+ resourceId,
289
+ column: columnName,
290
+ limit: 100,
291
+ offset: columnOffsets[columnName],
292
+ search: searchTerm,
293
+ },
294
+ });
295
+
296
+ if (!list || !Array.isArray(list.items)) {
297
+ console.warn(`Unexpected API response for column ${columnName}:`, list);
298
+ state.hasMore = false;
299
+ return;
300
+ }
301
+
302
+ if (!columnOptions.value) {
303
+ columnOptions.value = {};
304
+ }
305
+ if (!columnOptions.value[columnName]) {
306
+ columnOptions.value[columnName] = [];
307
+ }
308
+ columnOptions.value[columnName].push(...list.items);
309
+
310
+ columnOffsets[columnName] += 100;
311
+
312
+ const paginationResult = handleForeignResourcePagination(
313
+ column,
314
+ list.items,
315
+ columnEmptyResultsCount[columnName] || 0,
316
+ false // not searching
317
+ );
318
+
319
+ columnEmptyResultsCount[columnName] = paginationResult.emptyResultsCount;
320
+ state.hasMore = paginationResult.hasMore;
321
+
322
+ } catch (error) {
323
+ console.error('Error loading more options:', error);
324
+ } finally {
325
+ state.loading = false;
326
+ }
327
+ }
328
+
329
+ export async function searchForeignOptions({
330
+ columnName,
331
+ searchTerm,
332
+ columns,
333
+ resourceId,
334
+ columnOptions,
335
+ columnLoadingState,
336
+ columnOffsets,
337
+ columnEmptyResultsCount
338
+ }: {
339
+ columnName: string;
340
+ searchTerm: string;
341
+ columns: any[];
342
+ resourceId: string;
343
+ columnOptions: any;
344
+ columnLoadingState: any;
345
+ columnOffsets: any;
346
+ columnEmptyResultsCount: any;
347
+ }) {
348
+ const column = columns?.find(c => c.name === columnName);
349
+
350
+ if (!column || !column.foreignResource || !column.foreignResource.searchableFields) {
351
+ return;
352
+ }
353
+
354
+ const state = columnLoadingState[columnName];
355
+ if (state.loading) return;
356
+
357
+ state.loading = true;
358
+
359
+ try {
360
+ const list = await callAdminForthApi({
361
+ method: 'POST',
362
+ path: `/get_resource_foreign_data`,
363
+ body: {
364
+ resourceId,
365
+ column: columnName,
366
+ limit: 100,
367
+ offset: 0,
368
+ search: searchTerm,
369
+ },
370
+ });
371
+
372
+ if (!list || !Array.isArray(list.items)) {
373
+ console.warn(`Unexpected API response for column ${columnName}:`, list);
374
+ state.hasMore = false;
375
+ return;
376
+ }
377
+
378
+ if (!columnOptions.value) {
379
+ columnOptions.value = {};
380
+ }
381
+ columnOptions.value[columnName] = list.items;
382
+ columnOffsets[columnName] = 100;
383
+
384
+ const paginationResult = handleForeignResourcePagination(
385
+ column,
386
+ list.items,
387
+ columnEmptyResultsCount[columnName] || 0,
388
+ true // is searching
389
+ );
390
+
391
+ columnEmptyResultsCount[columnName] = paginationResult.emptyResultsCount;
392
+ state.hasMore = paginationResult.hasMore;
393
+
394
+ } catch (error) {
395
+ console.error('Error searching options:', error);
396
+ } finally {
397
+ state.loading = false;
398
+ }
399
+ }
400
+
401
+ export function createSearchInputHandlers(
402
+ columns: any[],
403
+ searchFunction: (columnName: string, searchTerm: string) => void,
404
+ getDebounceMs?: (column: any) => number
405
+ ) {
406
+ if (!columns) return {};
407
+
408
+ return columns.reduce((acc, c) => {
409
+ if (c.foreignResource && c.foreignResource.searchableFields) {
410
+ const debounceMs = getDebounceMs ? getDebounceMs(c) : 300;
411
+ return {
412
+ ...acc,
413
+ [c.name]: debounce((searchTerm: string) => {
414
+ searchFunction(c.name, searchTerm);
415
+ }, debounceMs),
416
+ };
417
+ }
418
+ return acc;
419
+ }, {} as Record<string, (searchTerm: string) => void>);
211
420
  }
@@ -13,14 +13,14 @@
13
13
  <BreadcrumbsWithButtons>
14
14
  <!-- save and cancle -->
15
15
  <button @click="$router.back()"
16
- class="flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
16
+ class="af-cancel-button flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
17
17
  >
18
18
  {{ $t('Cancel') }}
19
19
  </button>
20
20
 
21
21
  <button
22
22
  @click="saveRecord"
23
- class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
23
+ class="af-save-button flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
24
24
  :disabled="saving || (validating && !isValid)"
25
25
  >
26
26
  <svg v-if="saving"
@@ -154,7 +154,7 @@ async function saveRecord() {
154
154
  record: record.value,
155
155
  },
156
156
  });
157
- if (response?.error) {
157
+ if (response?.error && response?.error !== 'Operation aborted by hook') {
158
158
  showErrorTost(response.error);
159
159
  }
160
160
  saving.value = false;
@@ -177,7 +177,7 @@ async function saveRecord() {
177
177
  record: updates,
178
178
  },
179
179
  });
180
- if (resp.error) {
180
+ if (resp.error && resp.error !== 'Operation aborted by hook') {
181
181
  showErrorTost(resp.error);
182
182
  } else {
183
183
  adminforth.alert({
@@ -74,14 +74,14 @@
74
74
 
75
75
  <RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
76
76
  :to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
77
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
77
+ class="af-create-button flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
78
78
  >
79
79
  <IconPlusOutline class="w-4 h-4 me-2"/>
80
80
  {{ $t('Create') }}
81
81
  </RouterLink>
82
82
 
83
83
  <button
84
- class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
84
+ class="af-filter-button flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
85
85
  @click="()=>{filtersShow = !filtersShow}"
86
86
  v-if="coreStore.resource?.options?.allowedActions?.filter"
87
87
  >
@@ -4,7 +4,7 @@
4
4
  'background-image': 'url(' + loadFile(coreStore.config?.loginBackgroundImage) + ')',
5
5
  'background-size': 'cover',
6
6
  'background-position': 'center',
7
- 'background-blend-mode': 'darken'
7
+ 'background-blend-mode': coreStore.config?.removeBackgroundBlendMode ? 'normal' : 'darken'
8
8
  }: {}"
9
9
  >
10
10
 
@@ -23,44 +23,57 @@
23
23
 
24
24
  <!-- Main modal -->
25
25
  <div id="authentication-modal" tabindex="-1"
26
- class="overflow-y-auto flex flex-grow
26
+ class="af-login-modal overflow-y-auto flex flex-grow
27
27
  overflow-x-hidden z-50 min-w-[350px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
28
28
  <div class="relative p-4 w-full max-h-full max-w-[400px]">
29
29
  <!-- Modal content -->
30
- <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
30
+ <div class="af-login-modal-content relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
31
31
  <!-- Modal header -->
32
- <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
33
- <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
32
+ <div class="af-login-modal-header flex items-center justify-between flex-col p-4 md:p-5 border-b rounded-t dark:border-gray-600">
33
+
34
+ <template v-if="coreStore?.config?.loginPageInjections?.panelHeader.length > 0">
35
+ <component
36
+ v-for="(c, index) in coreStore?.config?.loginPageInjections?.panelHeader || []"
37
+ :key="index"
38
+ :is="getCustomComponent(c)"
39
+ :meta="c.meta"
40
+ />
41
+ </template>
42
+ <h3 v-else class="text-xl font-semibold text-gray-900 dark:text-white">
34
43
  {{ $t('Sign in to') }} {{ coreStore.config?.brandName }}
35
44
  </h3>
36
45
  </div>
37
46
  <!-- Modal body -->
38
- <div class="p-4 md:p-5">
47
+ <div class="af-login-modal-body p-4 md:p-5">
39
48
  <form class="space-y-4" @submit.prevent>
40
49
  <div>
41
50
  <label for="username" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ $t('Your') }} {{ coreStore.config?.usernameFieldName?.toLowerCase() }}</label>
42
- <input
51
+ <Input
52
+ v-model="username"
43
53
  autocomplete="username"
44
54
  type="username"
45
55
  name="username"
46
56
  id="username"
47
57
  ref="usernameInput"
48
- oninput="setCustomValidity('')"
49
58
  @keydown.enter="passwordInput.focus()"
50
- class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" placeholder="name@company.com" required />
59
+ class="w-full"
60
+ placeholder="name@company.com" required />
51
61
  </div>
52
- <div class="relative">
62
+ <div class="">
53
63
  <label for="password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ $t('Your password') }}</label>
54
- <input
64
+ <Input
65
+ v-model="password"
55
66
  ref="passwordInput"
56
67
  autocomplete="current-password"
57
- oninput="setCustomValidity('')"
58
68
  @keydown.enter="login"
59
- :type="!showPw ? 'password': 'text'" name="password" id="password" placeholder="••••••••" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white" required />
60
- <button type="button" @click="showPw = !showPw" class="absolute top-12 right-3 -translate-y-1/2 text-gray-400 dark:text-gray-300">
61
- <IconEyeSolid class="w-5 h-5" v-if="!showPw" />
62
- <IconEyeSlashSolid class="w-5 h-5" v-else />
63
- </button>
69
+ :type="!showPw ? 'password': 'text'" name="password" id="password" placeholder="••••••••" class="w-full" required>
70
+ <template #rightIcon>
71
+ <button type="button" @click="showPw = !showPw" class="text-gray-400 dark:text-gray-300">
72
+ <IconEyeSolid class="w-5 h-5" v-if="!showPw" />
73
+ <IconEyeSlashSolid class="w-5 h-5" v-else />
74
+ </button>
75
+ </template>
76
+ </Input>
64
77
  </div>
65
78
 
66
79
  <div v-if="coreStore.config.rememberMeDays"
@@ -79,7 +92,7 @@
79
92
  :meta="c.meta"
80
93
  />
81
94
 
82
- <div v-if="error" class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
95
+ <div v-if="error" class="af-login-modal-error flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
83
96
  <svg class="flex-shrink-0 inline w-4 h-4 me-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
84
97
  <path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
85
98
  </svg>
@@ -121,7 +134,7 @@ import { useUserStore } from '@/stores/user';
121
134
  import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
122
135
  import { callAdminForthApi, loadFile } from '@/utils';
123
136
  import { useRoute, useRouter } from 'vue-router';
124
- import { Button, Checkbox } from '@/afcl';
137
+ import { Button, Checkbox, Input } from '@/afcl';
125
138
  import { useI18n } from 'vue-i18n';
126
139
 
127
140
  const { t } = useI18n();
@@ -129,6 +142,8 @@ const { t } = useI18n();
129
142
  const passwordInput = ref(null);
130
143
  const usernameInput = ref(null);
131
144
  const rememberMeValue= ref(false);
145
+ const username = ref('');
146
+ const password = ref('');
132
147
 
133
148
  const route = useRoute();
134
149
  const router = useRouter();
@@ -159,28 +174,15 @@ onBeforeMount(() => {
159
174
 
160
175
  onMounted(async () => {
161
176
  if (coreStore.config?.demoCredentials) {
162
- const [username, password] = coreStore.config.demoCredentials.split(':');
163
- usernameInput.value.value = username;
164
- passwordInput.value.value = password;
177
+ const [demoUsername, demoPassword] = coreStore.config.demoCredentials.split(':');
178
+ username.value = demoUsername;
179
+ password.value = demoPassword;
165
180
  }
166
181
  usernameInput.value.focus();
167
182
  });
168
183
 
169
184
 
170
185
  async function login() {
171
-
172
- const username = usernameInput.value.value;
173
- const password = passwordInput.value.value;
174
-
175
- if (!username) {
176
- usernameInput.value.setCustomValidity(t('Please fill out this field.'));
177
- return;
178
- }
179
- if (!password) {
180
- passwordInput.value.setCustomValidity(t('Please fill out this field.'));
181
- return;
182
- }
183
-
184
186
  if (inProgress.value) {
185
187
  return;
186
188
  }
@@ -189,8 +191,8 @@ async function login() {
189
191
  path: '/login',
190
192
  method: 'POST',
191
193
  body: {
192
- username,
193
- password,
194
+ username: username.value,
195
+ password: password.value,
194
196
  rememberMe: rememberMeValue.value,
195
197
  }
196
198
  });
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :key="`${$route?.params.resourceId}---${$route?.params.primaryKey}`" class="p-4 flex"
2
+ <div :key="`${$route?.params.resourceId}---${$route?.params.primaryKey}`" class="af-resource-parent p-4 flex"
3
3
  :class="limitHeightToPage ? 'h-[calc(100dvh-3.5rem)]': undefined"
4
4
  >
5
5
  <RouterView/>
@@ -28,21 +28,21 @@
28
28
  </template>
29
29
  <RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
30
30
  :to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
31
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
31
+ class="af-add-new-button flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
32
32
  >
33
33
  <IconPlusOutline class="w-4 h-4 me-2"/>
34
34
  {{ $t('Add new') }}
35
35
  </RouterLink>
36
36
 
37
37
  <RouterLink v-if="coreStore?.resourceOptions?.allowedActions?.edit" :to="{ name: 'resource-edit', params: { resourceId: $route.params.resourceId, primaryKey: $route.params.primaryKey } }"
38
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-default border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
38
+ class="flex items-center af-edit-button py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-default border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
39
39
  >
40
40
  <IconPenSolid class="w-4 h-4" />
41
41
  {{ $t('Edit') }}
42
42
  </RouterLink>
43
43
 
44
44
  <button v-if="coreStore?.resourceOptions?.allowedActions?.delete" @click="deleteRecord"
45
- class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
45
+ class="flex items-center af-delete-button py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
46
46
  >
47
47
  <IconTrashBinSolid class="w-4 h-4" />
48
48
  {{ $t('Delete') }}
@@ -62,14 +62,14 @@ export interface IExpressHttpServer extends IHttpServer {
62
62
  * Adds adminUser to request object if user is authorized. Drops request with 401 status if user is not authorized.
63
63
  * @param callable : Function which will be called if user is authorized.
64
64
  *
65
- * Example:
66
65
  *
66
+ * @example
67
67
  * ```ts
68
- * expressApp.get('/myApi', authorize((req, res) => \{
68
+ * expressApp.get('/myApi', authorize((req, res) => {
69
69
  * console.log('User is authorized', req.adminUser);
70
- * res.json(\{ message: 'Hello World' \});
71
- * \}));
72
- * ``
70
+ * res.json({ message: 'Hello World' });
71
+ * }));
72
+ * ```
73
73
  *
74
74
  */
75
75
  authorize(callable: Function): void;
@@ -315,6 +315,7 @@ export interface IAdminForth {
315
315
  }): Promise<{
316
316
  error?: string;
317
317
  createdRecord?: any;
318
+ newRecordId?: any;
318
319
  }>;
319
320
  updateResourceRecord(params: {
320
321
  resource: AdminForthResource;
@@ -451,6 +452,7 @@ export type BeforeDataSourceRequestFunction = (params: {
451
452
  }) => Promise<{
452
453
  ok: boolean;
453
454
  error?: string;
455
+ newRecordId?: string;
454
456
  }>;
455
457
  /**
456
458
  * Modify response to change how data is returned after fetching from database.
@@ -509,7 +511,7 @@ export type BeforeEditSaveFunction = (params: {
509
511
  extra?: HttpExtra;
510
512
  }) => Promise<{
511
513
  ok: boolean;
512
- error?: string;
514
+ error?: string | null;
513
515
  }>;
514
516
  export type BeforeCreateSaveFunction = (params: {
515
517
  resource: AdminForthResource;
@@ -519,7 +521,8 @@ export type BeforeCreateSaveFunction = (params: {
519
521
  extra?: HttpExtra;
520
522
  }) => Promise<{
521
523
  ok: boolean;
522
- error?: string;
524
+ error?: string | null;
525
+ newRecordId?: string;
523
526
  }>;
524
527
  export type AfterCreateSaveFunction = (params: {
525
528
  resource: AdminForthResource;
@@ -602,6 +605,10 @@ interface AdminForthInputConfigCustomization {
602
605
  * Your app name
603
606
  */
604
607
  brandName?: string;
608
+ /**
609
+ * Whether to use single theme for the app
610
+ */
611
+ singleTheme?: 'light' | 'dark';
605
612
  /**
606
613
  * Whether to show brand name in sidebar
607
614
  * default is true
@@ -718,6 +725,7 @@ interface AdminForthInputConfigCustomization {
718
725
  */
719
726
  loginPageInjections?: {
720
727
  underInputs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>;
728
+ panelHeader?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>;
721
729
  };
722
730
  /**
723
731
  * Custom panel components or array of components which will be displayed in different parts of the admin panel.
@@ -728,6 +736,14 @@ interface AdminForthInputConfigCustomization {
728
736
  sidebar?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>;
729
737
  everyPageBottom?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>;
730
738
  };
739
+ /**
740
+ * Allows adding custom elements (e.g., <link>, <script>, <meta>) to the <head> of the HTML document.
741
+ * Each item must include a tag name and a set of attributes.
742
+ */
743
+ customHeadItems?: {
744
+ tagName: string;
745
+ attributes: Record<string, string | boolean>;
746
+ }[];
731
747
  }
732
748
  export interface AdminForthActionInput {
733
749
  name: string;
@@ -884,6 +900,12 @@ export interface AdminForthInputConfig {
884
900
  * Default: '1/2'
885
901
  */
886
902
  loginBackgroundPosition?: 'over' | '1/2' | '1/3' | '2/3' | '3/4' | '2/5' | '3/5';
903
+ /**
904
+ * If true, background blend mode will be removed from login background image when position is 'over'
905
+ *
906
+ * Default: false
907
+ */
908
+ removeBackgroundBlendMode?: boolean;
887
909
  /**
888
910
  * Function or functions which will be called before user try to login.
889
911
  * Each function will resive User object as an argument
@@ -984,6 +1006,7 @@ export interface AdminForthConfigCustomization extends Omit<AdminForthInputConfi
984
1006
  customPages: Array<AdminForthPageDeclaration>;
985
1007
  loginPageInjections: {
986
1008
  underInputs: Array<AdminForthComponentDeclarationFull>;
1009
+ panelHeader: Array<AdminForthComponentDeclarationFull>;
987
1010
  };
988
1011
  globalInjections: {
989
1012
  userMenu: Array<AdminForthComponentDeclarationFull>;
@@ -991,6 +1014,10 @@ export interface AdminForthConfigCustomization extends Omit<AdminForthInputConfi
991
1014
  sidebar: Array<AdminForthComponentDeclarationFull>;
992
1015
  everyPageBottom: Array<AdminForthComponentDeclarationFull>;
993
1016
  };
1017
+ customHeadItems?: {
1018
+ tagName: string;
1019
+ attributes: Record<string, string | boolean>;
1020
+ }[];
994
1021
  }
995
1022
  export interface AdminForthConfig extends Omit<AdminForthInputConfig, 'customization' | 'resources'> {
996
1023
  baseUrl: string;
@@ -1164,9 +1191,13 @@ export interface AdminForthResource extends Omit<AdminForthResourceInput, 'optio
1164
1191
  };
1165
1192
  create?: {
1166
1193
  /**
1194
+ * Should return `ok: true` to continue saving pipeline and allow creating record in database, and `ok: false` to interrupt pipeline and prevent record creation.
1195
+ * If you need to show error on UI, set `error: \<error message\>` in response.
1196
+ *
1167
1197
  * Typical use-cases:
1168
- * - Validate record before saving to database and interrupt execution if validation failed (`allowedActions.create` should be preferred in most cases)
1169
- * - fill-in adminUser as creator of record
1198
+ * - Create record by custom code (return `{ ok: false, newRecordId: <id of created record from custom code> }`)
1199
+ * - Validate record before saving to database and interrupt execution if validation failed (return `{ ok: false, error: <validation error> }`), though `allowedActions.create` should be preferred in most cases
1200
+ * - fill-in adminUser as creator of record (set `record.<some field> = x; return \{ ok: true \}`)
1170
1201
  * - Attach additional data to record before saving to database (mostly fillOnCreate should be used instead)
1171
1202
  */
1172
1203
  beforeSave?: Array<BeforeCreateSaveFunction>;