adminforth 2.25.1 → 2.26.0-next.10

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 (77) hide show
  1. package/commands/bundle.js +2 -1
  2. package/commands/createApp/templates/Dockerfile.hbs +12 -0
  3. package/commands/createApp/templates/package.json.hbs +18 -6
  4. package/commands/createApp/templates/pnpm_templates/pnpm-lock.yaml.hbs +8 -0
  5. package/commands/createApp/templates/pnpm_templates/pnpm-workspace.yaml.hbs +2 -0
  6. package/commands/createApp/templates/readme.md.hbs +23 -4
  7. package/commands/createApp/utils.js +107 -26
  8. package/commands/createPlugin/utils.js +5 -6
  9. package/commands/postinstall.js +1 -1
  10. package/dist/auth.d.ts.map +1 -1
  11. package/dist/auth.js.map +1 -1
  12. package/dist/dataConnectors/clickhouse.d.ts +4 -0
  13. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  14. package/dist/dataConnectors/clickhouse.js +14 -0
  15. package/dist/dataConnectors/clickhouse.js.map +1 -1
  16. package/dist/dataConnectors/mongo.d.ts +4 -0
  17. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  18. package/dist/dataConnectors/mongo.js +9 -0
  19. package/dist/dataConnectors/mongo.js.map +1 -1
  20. package/dist/dataConnectors/mysql.d.ts +4 -0
  21. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  22. package/dist/dataConnectors/mysql.js +11 -0
  23. package/dist/dataConnectors/mysql.js.map +1 -1
  24. package/dist/dataConnectors/postgres.d.ts +4 -0
  25. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  26. package/dist/dataConnectors/postgres.js +11 -0
  27. package/dist/dataConnectors/postgres.js.map +1 -1
  28. package/dist/dataConnectors/sqlite.d.ts +4 -0
  29. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  30. package/dist/dataConnectors/sqlite.js +11 -0
  31. package/dist/dataConnectors/sqlite.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +3 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/modules/codeInjector.d.ts +11 -2
  37. package/dist/modules/codeInjector.d.ts.map +1 -1
  38. package/dist/modules/codeInjector.js +239 -82
  39. package/dist/modules/codeInjector.js.map +1 -1
  40. package/dist/modules/styles.d.ts +4 -0
  41. package/dist/modules/styles.d.ts.map +1 -1
  42. package/dist/modules/styles.js +4 -0
  43. package/dist/modules/styles.js.map +1 -1
  44. package/dist/modules/utils.d.ts.map +1 -1
  45. package/dist/servers/express.d.ts +0 -1
  46. package/dist/servers/express.d.ts.map +1 -1
  47. package/dist/spa/README.md +4 -4
  48. package/dist/spa/package-lock.json +1902 -1427
  49. package/dist/spa/package.json +4 -3
  50. package/dist/spa/pnpm-lock.yaml +3558 -0
  51. package/dist/spa/src/afcl/Button.vue +12 -5
  52. package/dist/spa/src/afcl/Dialog.vue +155 -126
  53. package/dist/spa/src/afcl/Modal.vue +31 -8
  54. package/dist/spa/src/afcl/Select.vue +6 -2
  55. package/dist/spa/src/components/AcceptModal.vue +31 -53
  56. package/dist/spa/src/components/MenuLink.vue +18 -4
  57. package/dist/spa/src/components/ResourceListTable.vue +11 -11
  58. package/dist/spa/src/components/ResourceListTableVirtual.vue +21 -26
  59. package/dist/spa/src/components/Sidebar.vue +1 -1
  60. package/dist/spa/src/components/ThreeDotsMenu.vue +1 -1
  61. package/dist/spa/src/components/ValueRenderer.vue +1 -1
  62. package/dist/spa/src/stores/core.ts +1 -2
  63. package/dist/spa/src/types/Back.ts +4 -3
  64. package/dist/spa/src/types/Common.ts +16 -0
  65. package/dist/spa/src/types/FrontendAPI.ts +0 -3
  66. package/dist/spa/src/utils/utils.ts +57 -2
  67. package/dist/spa/src/views/CreateView.vue +12 -11
  68. package/dist/spa/src/views/EditView.vue +9 -13
  69. package/dist/spa/vite.config.ts +29 -40
  70. package/dist/types/Back.d.ts +3 -4
  71. package/dist/types/Back.d.ts.map +1 -1
  72. package/dist/types/Back.js.map +1 -1
  73. package/dist/types/Common.d.ts +11 -0
  74. package/dist/types/Common.d.ts.map +1 -1
  75. package/dist/types/Common.js.map +1 -1
  76. package/package.json +14 -6
  77. package/scripts/postinstall.js +25 -0
@@ -10,7 +10,7 @@
10
10
  <div class="h-2 bg-lightListSkeletLoader rounded-full dark:bg-darkListSkeletLoader max-w-[360px]"></div>
11
11
  </div>
12
12
  </div>
13
- <table v-else class=" w-full text-sm text-left rtl:text-right text-lightListTableText dark:text-darkListTableText rounded-default">
13
+ <table v-else class="w-full text-sm text-left rtl:text-right text-lightListTableText dark:text-darkListTableText rounded-default">
14
14
 
15
15
  <tbody>
16
16
  <!-- table header -->
@@ -64,7 +64,7 @@
64
64
  <!-- table header end -->
65
65
  <SkeleteLoader
66
66
  v-if="!rows"
67
- :columns="resource?.columns.filter((c: AdminForthResourceColumnInputCommon) => c.showIn?.list).length + 2"
67
+ :columns="resource?.columns.filter((c: AdminForthResourceColumnCommon) => c.showIn?.list).length + 2"
68
68
  :rows="rowHeights.length || 3"
69
69
  :row-heights="rowHeights"
70
70
  :column-widths="columnWidths"
@@ -182,7 +182,7 @@
182
182
 
183
183
  <template v-if="resource.options?.actions">
184
184
  <Tooltip
185
- v-for="action in resource.options.actions.filter(a => a.showIn?.list || a.showIn?.listQuickIcon)"
185
+ v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
186
186
  :key="action.id"
187
187
  >
188
188
  <component
@@ -339,7 +339,7 @@ import {
339
339
  } from '@iconify-prerendered/vue-flowbite';
340
340
  import router from '@/router';
341
341
  import { Tooltip } from '@/afcl';
342
- import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
342
+ import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
343
343
  import { useAdminforth } from '@/adminforth';
344
344
  import Checkbox from '@/afcl/Checkbox.vue';
345
345
  import ListActionsThreeDots from '@/components/ListActionsThreeDots.vue';
@@ -360,7 +360,7 @@ const props = defineProps<{
360
360
  customActionsInjection?: any[],
361
361
  tableBodyStartInjection?: any[],
362
362
  customActionIconsThreeDotsMenuItems?: any[]
363
- tableRowReplaceInjection?: AdminForthComponentDeclaration,
363
+ tableRowReplaceInjection?: AdminForthComponentDeclarationFull,
364
364
  }>();
365
365
 
366
366
  // emits, update page
@@ -436,7 +436,7 @@ watch(() => props.rows, (newRows) => {
436
436
  columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el: HTMLElement) => el.offsetWidth)];
437
437
  });
438
438
 
439
- function addToCheckedValues(id: string) {
439
+ function addToCheckedValues(id: string | number) {
440
440
  if (checkboxesInternal.value.includes(id)) {
441
441
  checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
442
442
  } else {
@@ -468,7 +468,7 @@ const allFromThisPageChecked = computed(() => {
468
468
  if (!props.rows || !props.rows.length) return false;
469
469
  return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
470
470
  });
471
- const ascArr = computed(() => sort.value.filter((s:any) => s.direction === 'asc').map((s: any) => s.field));
471
+ const ascArr = computed(() => sort.value.filter((s: any) => s.direction === 'asc').map((s: any) => s.field));
472
472
  const descArr = computed(() => sort.value.filter((s: any) => s.direction === 'desc').map((s: any) => s.field));
473
473
 
474
474
 
@@ -487,9 +487,9 @@ function onSortButtonClick(event: any, field: string) {
487
487
  } else {
488
488
  const sortField = sort.value[sortIndex];
489
489
  if (sortField.direction === 'asc') {
490
- sort.value = sort.value.map((s: any) => s.field === field ? {field, direction: 'desc'} : s);
490
+ sort.value = sort.value.map((s) => s.field === field ? {field, direction: 'desc'} : s);
491
491
  } else {
492
- sort.value = sort.value.filter((s: any) => s.field !== field);
492
+ sort.value = sort.value.filter((s) => s.field !== field);
493
493
  }
494
494
  }
495
495
  }
@@ -576,7 +576,7 @@ async function deleteRecord(row: any) {
576
576
  const actionLoadingStates = ref<Record<string | number, boolean>>({});
577
577
 
578
578
  async function startCustomAction(actionId: string, row: any, extraData: Record<string, any> = {}) {
579
- console.log('Starting custom action', actionId, row);
579
+
580
580
  actionLoadingStates.value[actionId] = true;
581
581
 
582
582
  const data = await callAdminForthApi({
@@ -586,7 +586,7 @@ async function startCustomAction(actionId: string, row: any, extraData: Record<s
586
586
  resourceId: props.resource?.resourceId,
587
587
  actionId: actionId,
588
588
  recordId: row._primaryKeyValue,
589
- extra: extraData,
589
+ extra: extraData
590
590
  }
591
591
  });
592
592
 
@@ -18,8 +18,8 @@
18
18
 
19
19
  <tbody>
20
20
  <!-- table header -->
21
- <tr class="t-header sticky z-20 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
22
- <td scope="col" class="list-table-header-cell p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
21
+ <tr class="t-header sticky z-20 top-0 text-xs text-lightListTableHeadingText bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-darkListTableHeadingText">
22
+ <td scope="col" class="list-table-header-cell p-4 sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading">
23
23
  <Checkbox
24
24
  :modelValue="allFromThisPageChecked"
25
25
  :disabled="!rows || !rows.length"
@@ -29,7 +29,7 @@
29
29
  </Checkbox>
30
30
  </td>
31
31
 
32
- <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="list-table-header-cell px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
32
+ <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="list-table-header-cell px-2 md:px-3 lg:px-6 py-3" :class="{'sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading': c.listSticky}">
33
33
 
34
34
  <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
35
35
  class="flex items-center " :class="{'cursor-pointer':c.sortable}">
@@ -69,7 +69,7 @@
69
69
  <SkeleteLoader
70
70
  v-if="!rows"
71
71
  :columns="resource?.columns.filter((c: AdminForthResourceColumnCommon) => c.showIn?.list).length + 2"
72
- :rows="rowHeights.length || 20"
72
+ :rows="rowHeights.length || 3"
73
73
  :row-heights="rowHeights"
74
74
  :column-widths="columnWidths"
75
75
  />
@@ -94,7 +94,6 @@
94
94
 
95
95
  <!-- Visible rows -->
96
96
  <component
97
- v-else
98
97
  v-for="(row, rowI) in visibleRows"
99
98
  :is="tableRowReplaceInjection ? getCustomComponent(tableRowReplaceInjection) : 'tr'"
100
99
  :key="`row_${row._primaryKeyValue}`"
@@ -108,7 +107,7 @@
108
107
  :class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
109
108
  @mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
110
109
  >
111
- <td class="w-4 p-4 cursor-default sticky-column bg-lightListTableHeading dark:bg-darkListTableHeading" @click="(e)=>e.stopPropagation()">
110
+ <td class="w-4 p-4 cursor-default sticky-column bg-lightListTable dark:bg-darkListTable" @click="(e)=>e.stopPropagation()">
112
111
  <Checkbox
113
112
  :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
114
113
  @change="(e: any)=>{addToCheckedValues(row._primaryKeyValue)}"
@@ -116,7 +115,7 @@
116
115
  >
117
116
  <span class="sr-only">{{ $t('checkbox') }}</span>
118
117
  </Checkbox>
119
- </td>
118
+ </td>
120
119
  <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4" :class="{'sticky-column bg-lightListTable dark:bg-darkListTable': c.listSticky}">
121
120
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
122
121
  <component
@@ -177,7 +176,7 @@
177
176
  <template v-slot:tooltip>
178
177
  {{ $t('Delete item') }}
179
178
  </template>
180
- </Tooltip>
179
+ </Tooltip>
181
180
  <template v-if="customActionsInjection">
182
181
  <component
183
182
  v-for="c in customActionsInjection"
@@ -191,7 +190,7 @@
191
190
  </template>
192
191
  <template v-if="resource.options?.actions">
193
192
  <Tooltip
194
- v-for="action in resource.options.actions.filter(a => a.showIn?.list || a.showIn?.listQuickIcon)"
193
+ v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
195
194
  :key="action.id"
196
195
  >
197
196
  <CallActionWrapper
@@ -251,9 +250,9 @@
251
250
  <!-- pagination
252
251
  totalRows in v-if is used to not hide page input during loading when user puts cursor into it and edit directly (rows gets null there during edit)
253
252
  -->
254
- <div class="flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3">
253
+ <div class="af-pagination-container flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3">
255
254
 
256
- <div class="inline-flex "
255
+ <div class="af-pagination-buttons-container inline-flex "
257
256
  v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
258
257
  >
259
258
  <!-- Buttons -->
@@ -279,11 +278,10 @@
279
278
  type="text"
280
279
  v-model="pageInput"
281
280
  :style="{ width: `${Math.max(1, pageInput.length+4)}ch` }"
282
- class="af-pagination-input min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightListTablePaginationCurrentPageText border border-lightListTablePaginationBorder dark:border-darkListTablePaginationBorder dark:text-darkListTablePaginationCurrentPageText dark:bg-darkListTablePaginationBackgoround z-10"
281
+ class="af-pagination-input min-w-10 outline-none inline-block py-1.5 px-3 text-sm text-center text-lightListTablePaginationCurrentPageText border border-lightListTablePaginationBorder dark:border-darkListTablePaginationBorder dark:text-darkListTablePaginationCurrentPageText dark:bg-darkListTablePaginationBackgoround z-10"
283
282
  @keydown="onPageKeydown($event)"
284
283
  @blur="validatePageInput()"
285
- >
286
- </input>
284
+ />
287
285
 
288
286
  <button
289
287
  class="af-pagination-last-page-button flex items-center py-1 px-3 text-sm font-medium text-lightListTablePaginationText focus:outline-none bg-lightListTablePaginationBackgoround border-l-0 border border-lightListTablePaginationBorder hover:bg-lightListTablePaginationBackgoroundHover hover:text-lightListTablePaginationTextHover focus:z-10 focus:ring-4 focus:ring-lightListTablePaginationFocusRing dark:focus:ring-darkListTablePaginationFocusRing dark:bg-darkListTablePaginationBackgoround dark:text-darkListTablePaginationText dark:border-darkListTablePaginationBorder dark:hover:text-white dark:hover:bg-darkListTablePaginationBackgoroundHover disabled:opacity-50"
@@ -309,7 +307,7 @@
309
307
  <span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
310
308
  <template v-else-if="resource && totalRows > 0">
311
309
 
312
- <span class="hidden sm:inline">
310
+ <span class="af-pagination-info hidden sm:inline">
313
311
  <i18n-t keypath="Showing {from} to {to} of {total} Entries" tag="p" >
314
312
  <template v-slot:from>
315
313
  <strong>{{ from }}</strong>
@@ -343,7 +341,7 @@
343
341
  <script setup lang="ts">
344
342
 
345
343
 
346
- import { computed, onMounted, ref, watch, useTemplateRef, nextTick, type Ref, onUnmounted } from 'vue';
344
+ import { computed, onMounted, ref, watch, useTemplateRef, nextTick, type Ref } from 'vue';
347
345
  import { callAdminForthApi } from '@/utils';
348
346
  import { useI18n } from 'vue-i18n';
349
347
  import ValueRenderer from '@/components/ValueRenderer.vue';
@@ -352,18 +350,15 @@ import { useCoreStore } from '@/stores/core';
352
350
  import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
353
351
  import SkeleteLoader from '@/components/SkeleteLoader.vue';
354
352
  import { getIcon } from '@/utils';
355
- import {
356
- IconInboxOutline,
357
- } from '@iconify-prerendered/vue-flowbite';
358
-
359
353
  import {
360
354
  IconEyeSolid,
361
355
  IconPenSolid,
362
- IconTrashBinSolid
356
+ IconTrashBinSolid,
357
+ IconInboxOutline
363
358
  } from '@iconify-prerendered/vue-flowbite';
364
359
  import router from '@/router';
365
360
  import { Tooltip } from '@/afcl';
366
- import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
361
+ import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
367
362
  import { useAdminforth } from '@/adminforth';
368
363
  import Checkbox from '@/afcl/Checkbox.vue';
369
364
  import ListActionsThreeDots from '@/components/ListActionsThreeDots.vue';
@@ -387,7 +382,7 @@ const props = defineProps<{
387
382
  itemHeight?: number,
388
383
  bufferSize?: number,
389
384
  customActionIconsThreeDotsMenuItems?: any[]
390
- tableRowReplaceInjection?: AdminForthComponentDeclaration
385
+ tableRowReplaceInjection?: AdminForthComponentDeclarationFull
391
386
  }>();
392
387
 
393
388
  // emits, update page
@@ -463,7 +458,7 @@ watch(() => props.rows, (newRows) => {
463
458
  columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el: HTMLElement) => el.offsetWidth)];
464
459
  });
465
460
 
466
- function addToCheckedValues(id: any) {
461
+ function addToCheckedValues(id: string | number) {
467
462
  if (checkboxesInternal.value.includes(id)) {
468
463
  checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
469
464
  } else {
@@ -499,7 +494,7 @@ const ascArr = computed(() => sort.value.filter((s: any) => s.direction === 'asc
499
494
  const descArr = computed(() => sort.value.filter((s: any) => s.direction === 'desc').map((s: any) => s.field));
500
495
 
501
496
 
502
- function onSortButtonClick(event: any, field: any) {
497
+ function onSortButtonClick(event: any, field: string) {
503
498
  // if ctrl key is pressed, add to sort otherwise sort by this field
504
499
  // in any case if field is already in sort, toggle direction
505
500
 
@@ -524,7 +519,7 @@ function onSortButtonClick(event: any, field: any) {
524
519
 
525
520
  const clickTarget = ref(null);
526
521
 
527
- async function onClick(e: any,row: any) {
522
+ async function onClick(e: any, row: any) {
528
523
  if(clickTarget.value === e.target) return;
529
524
  clickTarget.value = e.target;
530
525
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -28,7 +28,7 @@
28
28
  :class="{
29
29
  'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))) }"
30
30
  />
31
- <img :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
31
+ <img v-if="coreStore.config?.iconOnlySidebar?.logo" :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
32
32
  <span
33
33
  v-if="coreStore.config?.showBrandNameInSidebar && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))"
34
34
  class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
@@ -18,7 +18,7 @@
18
18
  'hidden': !showDropdown,
19
19
  'left-0 md:left-auto': checkboxes && checkboxes.length > 0
20
20
  }"
21
- class="absolute z-30 mt-3 bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-auto max-w-64 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600 right-0">
21
+ class="absolute z-30 mt-3 bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-max max-w-64 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600 right-0">
22
22
  <ul class="py-2 text-sm text-lightThreeDotsMenuBodyText dark:text-darkThreeDotsMenuBodyText" aria-labelledby="dropdownMenuIconButton">
23
23
  <li v-for="(item, i) in threeDotsDropdownItems" :key="`dropdown-item-${i}`">
24
24
  <div
@@ -117,7 +117,7 @@ import timezone from 'dayjs/plugin/timezone';
117
117
  import {checkEmptyValues} from '@/utils';
118
118
  import { useRoute, useRouter } from 'vue-router';
119
119
  import { JsonViewer } from "vue3-json-viewer";
120
- import "vue3-json-viewer/dist/index.css";
120
+ import "vue3-json-viewer/dist/vue3-json-viewer.css";
121
121
  import type { AdminForthResourceColumnCommon } from '@/types/Common';
122
122
 
123
123
  import { useCoreStore } from '@/stores/core';
@@ -92,6 +92,7 @@ export const useCoreStore = defineStore('core', () => {
92
92
 
93
93
  // console.log('🔔 subscribeToMenuBadges', mi.badge, JSON.stringify(mi));
94
94
  if (mi.badge !== undefined) {
95
+ websocket.unsubscribe(`/opentopic/update-menu-badge/${mi.itemId}`);
95
96
  websocket.subscribe(`/opentopic/update-menu-badge/${mi.itemId}`, ({ badge }) => {
96
97
  mi.badge = badge;
97
98
  });
@@ -121,8 +122,6 @@ export const useCoreStore = defineStore('core', () => {
121
122
  item.badge = badge;
122
123
  }
123
124
  });
124
- // TODO: This thing was created for something. Find out why
125
- // websocket.unsubscribeAll();
126
125
  subscribeToMenuBadges();
127
126
 
128
127
  }
@@ -1206,8 +1206,8 @@ interface AdminForthInputConfigCustomization {
1206
1206
  *
1207
1207
  * ```bashcreating rec
1208
1208
  * cd custom
1209
- * npm init -y
1210
- * npm install highcharts highcharts-vue
1209
+ * pnpm init -y
1210
+ * pnpm install highcharts highcharts-vue
1211
1211
  * ```
1212
1212
  *
1213
1213
  * And specify vueUsesFile in AdminForth config:
@@ -1760,6 +1760,8 @@ export interface IOperationalResource {
1760
1760
  update: (primaryKey: any, record: any) => Promise<any>;
1761
1761
 
1762
1762
  delete: (primaryKey: any) => Promise<boolean>;
1763
+
1764
+ deleteMany?(recordIds: any[]): Promise<number>;
1763
1765
 
1764
1766
  dataConnector: IAdminForthDataSourceConnectorBase;
1765
1767
  }
@@ -1811,7 +1813,6 @@ export type AllowedActions = {
1811
1813
  */
1812
1814
  export interface ResourceOptionsInput extends Omit<NonNullable<AdminForthResourceInputCommon['options']>, 'allowedActions' | 'bulkActions'> {
1813
1815
 
1814
- baseActionsAsQuickIcons?: ('show' | 'edit' | 'delete')[],
1815
1816
  /**
1816
1817
  * Custom bulk actions list. Bulk actions available in list view when user selects multiple records by
1817
1818
  * using checkboxes.
@@ -361,10 +361,24 @@ export interface AdminForthResourceInputCommon {
361
361
  recordLabel?: (item: any) => string,
362
362
 
363
363
 
364
+ /**
365
+ * If true, user will not see warning about unsaved changes when tries to leave edit or create page with unsaved changes.
366
+ * default is false
367
+ */
368
+ dontShowWarningAboutUnsavedChanges?: boolean,
369
+
364
370
  /**
365
371
  * General options for resource.
366
372
  */
367
373
  options?: {
374
+
375
+
376
+ /**
377
+ * Show quick action icons for base actions (show, edit, delete) in list view.
378
+ * By default, they are inside three dots dropdown menu.
379
+ */
380
+ baseActionsAsQuickIcons?: ('show' | 'edit' | 'delete')[],
381
+
368
382
 
369
383
  /**
370
384
  * Default sort for list view.
@@ -1116,6 +1130,8 @@ export interface AdminForthConfigMenuItem {
1116
1130
  * Item id will be automatically generated from hashed resourceId+Path+label
1117
1131
  */
1118
1132
  itemId?: string, // todo move to runtime type
1133
+
1134
+ url?: string
1119
1135
  }
1120
1136
 
1121
1137
 
@@ -213,6 +213,3 @@ export enum AlertVariant {
213
213
  }
214
214
 
215
215
 
216
-
217
-
218
-
@@ -5,13 +5,13 @@ import router from "../router";
5
5
  import { useCoreStore } from '../stores/core';
6
6
  import { useUserStore } from '../stores/user';
7
7
  import { Dropdown } from 'flowbite';
8
- import adminforth from '../adminforth';
8
+ import adminforth, { useAdminforth } from '../adminforth';
9
9
  import sanitizeHtml from 'sanitize-html'
10
10
  import debounce from 'debounce';
11
11
  import type { AdminForthResourceColumnInputCommon, Predicate } from '@/types/Common';
12
12
  import { i18nInstance } from '../i18n'
13
13
  import { useI18n } from 'vue-i18n';
14
-
14
+ import { onBeforeRouteLeave } from 'vue-router';
15
15
 
16
16
 
17
17
 
@@ -616,4 +616,59 @@ export function getTimeAgoString(date: Date): string {
616
616
  const days = Math.floor(diffInSeconds / 86400);
617
617
  return `${days} ${days === 1 ? 'day' : 'days'} ago`;
618
618
  }
619
+ }
620
+
621
+ export class leaveGuardActiveClass {
622
+ private active = false;
623
+
624
+ isActive() {
625
+ return this.active;
626
+ }
627
+
628
+ setActive(value: boolean) {
629
+ this.active = value;
630
+ }
631
+ }
632
+
633
+ export async function onBeforeRouteLeaveCreateEditViewGuard(initialValues: any, record: any, checkIfWeCanLeavePage: () => boolean, leaveGuardActive: leaveGuardActiveClass, isActive: { value: boolean }): Promise<void> {
634
+
635
+ const { confirm } = useAdminforth();
636
+ const { t } = useI18n();
637
+
638
+ onBeforeRouteLeave(async (to, from) => {
639
+ if (!isActive.value) {
640
+ return true;
641
+ }
642
+
643
+
644
+ if (leaveGuardActive.isActive()) {
645
+ return false;
646
+ }
647
+
648
+ if (checkIfWeCanLeavePage()) {
649
+ return true;
650
+ }
651
+
652
+ leaveGuardActive.setActive(true);
653
+
654
+ try {
655
+ const { changedFields } = compareOldAndNewRecord(
656
+ initialValues.value,
657
+ record.value
658
+ );
659
+
660
+ const messageHtml =
661
+ generateMessageHtmlForRecordChange(changedFields, t);
662
+
663
+ const answer = await confirm({
664
+ messageHtml,
665
+ yes: t('Yes'),
666
+ no: t('No'),
667
+ });
668
+
669
+ return answer;
670
+ } finally {
671
+ leaveGuardActive.setActive(false);
672
+ }
673
+ });
619
674
  }
@@ -79,7 +79,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
79
79
  import ResourceForm from '@/components/ResourceForm.vue';
80
80
  import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
81
81
  import { useCoreStore } from '@/stores/core';
82
- import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, generateMessageHtmlForRecordChange } from '@/utils';
82
+ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, checkShowIf, compareOldAndNewRecord, onBeforeRouteLeaveCreateEditViewGuard, leaveGuardActiveClass, onBeforeRouteLeaveCreateEditView } from '@/utils';
83
83
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
84
84
  import { onMounted, onBeforeMount, onBeforeUnmount, ref, watch, nextTick } from 'vue';
85
85
  import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
@@ -115,6 +115,7 @@ const readonlyColumns = ref([]);
115
115
 
116
116
  const cancelButtonClicked = ref(false);
117
117
  const wasSaveSuccessful = ref(false);
118
+ const useLeaveGuard = ref( false );
118
119
 
119
120
  async function onUpdateRecord(newRecord: any) {
120
121
  record.value = newRecord;
@@ -125,6 +126,9 @@ function checkIfWeCanLeavePage() {
125
126
  }
126
127
 
127
128
  function onBeforeUnload(event: BeforeUnloadEvent) {
129
+ if (!useLeaveGuard.value) {
130
+ return;
131
+ }
128
132
  if (!checkIfWeCanLeavePage()) {
129
133
  event.preventDefault();
130
134
  event.returnValue = '';
@@ -137,23 +141,20 @@ onBeforeUnmount(() => {
137
141
  window.removeEventListener('beforeunload', onBeforeUnload);
138
142
  });
139
143
 
140
- onBeforeRouteLeave(async (to, from, next) => {
141
- if (!checkIfWeCanLeavePage()) {
142
- const { changedFields } = compareOldAndNewRecord(initialValues.value, record.value);
143
-
144
- const messageHtml = generateMessageHtmlForRecordChange(changedFields, t);
145
144
 
146
- const answer = await confirm({ messageHtml: messageHtml, yes: t('Yes'), no: t('No') });
147
- if (!answer) return next(false);
148
- }
149
- next();
150
- });
145
+ const leaveGuardActive = new leaveGuardActiveClass();
146
+
147
+ onBeforeRouteLeaveCreateEditViewGuard(initialValues, record, checkIfWeCanLeavePage, leaveGuardActive, useLeaveGuard);
148
+
149
+
151
150
 
152
151
  onBeforeMount(() => {
153
152
  clearSaveInterceptors(route.params.resourceId as string);
154
153
  });
155
154
 
156
155
  onMounted(async () => {
156
+ useLeaveGuard.value = coreStore.resource?.options?.dontShowWarningAboutUnsavedChanges !== true;
157
+
157
158
  loading.value = true;
158
159
  await coreStore.fetchResourceFull({
159
160
  resourceId: route.params.resourceId as string,
@@ -74,7 +74,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
74
74
  import ResourceForm from '@/components/ResourceForm.vue';
75
75
  import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
76
76
  import { useCoreStore } from '@/stores/core';
77
- import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, compareOldAndNewRecord, generateMessageHtmlForRecordChange } from '@/utils';
77
+ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions, initThreeDotsDropdown, compareOldAndNewRecord, generateMessageHtmlForRecordChange, leaveGuardActiveClass, onBeforeRouteLeaveCreateEditViewGuard } from '@/utils';
78
78
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
79
79
  import { computed, onMounted, onBeforeMount, ref, type Ref, nextTick, watch, onBeforeUnmount } from 'vue';
80
80
  import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router';
@@ -104,8 +104,12 @@ const record: Ref<Record<string, any>> = ref({});
104
104
  const initialRecord = computed(() => coreStore.record);
105
105
  const wasSaveSuccessful = ref(false);
106
106
  const cancelButtonClicked = ref(false);
107
-
107
+ const useLeaveGuard = ref( false );
108
+
108
109
  function onBeforeUnload(event: BeforeUnloadEvent) {
110
+ if (!useLeaveGuard.value) {
111
+ return;
112
+ }
109
113
  if (!checkIfWeCanLeavePage()) {
110
114
  event.preventDefault();
111
115
  event.returnValue = '';
@@ -122,17 +126,8 @@ onBeforeUnmount(() => {
122
126
  window.removeEventListener('beforeunload', onBeforeUnload);
123
127
  });
124
128
 
125
- onBeforeRouteLeave(async (to, from, next) => {
126
- if (!checkIfWeCanLeavePage()) {
127
- const { changedFields } = compareOldAndNewRecord(initialRecord.value, record.value);
128
-
129
- const messageHtml = generateMessageHtmlForRecordChange(changedFields, t);
130
-
131
- const answer = await confirm({ messageHtml: messageHtml, yes: t('Yes'), no: t('No') });
132
- if (!answer) return next(false);
133
- }
134
- next();
135
- });
129
+ const leaveGuardActive = new leaveGuardActiveClass();
130
+ onBeforeRouteLeaveCreateEditViewGuard(initialRecord, record, checkIfWeCanLeavePage, leaveGuardActive, useLeaveGuard);
136
131
 
137
132
  const resourceFormRef = ref<InstanceType<typeof ResourceForm> | null>(null);
138
133
 
@@ -162,6 +157,7 @@ onBeforeMount(() => {
162
157
  });
163
158
 
164
159
  onMounted(async () => {
160
+ useLeaveGuard.value = coreStore.resource?.options?.dontShowWarningAboutUnsavedChanges !== true;
165
161
  loading.value = true;
166
162
 
167
163
  await coreStore.fetchResourceFull({
@@ -15,46 +15,6 @@ async function getNextAvailablePort(startPort: number | undefined) {
15
15
  return await portfinder.getPortPromise({ port: startPort });
16
16
  };
17
17
 
18
- function ignoreTailwindErrors(): Plugin {
19
- return {
20
- name: 'ignore-tailwind-errors',
21
- configureServer(server) {
22
- server.middlewares.use((req, res, next) => {
23
- const originalWrite = res.write;
24
- res.write = function(chunk) {
25
- if (typeof chunk === 'string' && chunk.includes('tailwind')) {
26
- return true;
27
- }
28
- return originalWrite.call(this, chunk);
29
- };
30
- next();
31
- });
32
- },
33
- config(config, { command }) {
34
- if (command === 'build') {
35
- // Override PostCSS config for build
36
- config.css = config.css || {};
37
- config.css.postcss = {
38
- plugins: [
39
- {
40
- postcssPlugin: 'ignore-tailwind-errors',
41
- Once(root, helpers) {
42
- try {
43
- return tailwindcss()(root, helpers);
44
- } catch (error: any) {
45
- console.warn('TailwindCSS warning ignored:', error.message);
46
- return;
47
- }
48
- }
49
- }
50
- ]
51
- };
52
- }
53
- }
54
- };
55
- }
56
-
57
-
58
18
  const appPort = await getNextAvailablePort(5173);
59
19
  const hmrPort = await getNextAvailablePort(5273);
60
20
  console.log(`SPA port: ${appPort}. HMR port: ${hmrPort}`);
@@ -89,7 +49,36 @@ export default defineConfig({
89
49
  return id.toString().split('node_modules/')[1].split('/')[0].toString();
90
50
  }
91
51
  },
52
+
53
+ // by default servers doesnt returns dotfiles
54
+ // so if we'll generate file with name like ".pnpm-BLnlxqcJ.js"
55
+ // it won't be loaded
56
+ assetFileNames: (chunkInfo) => {
57
+ if (chunkInfo.name && chunkInfo.name.startsWith('.pnpm')) {
58
+ return `assets/pnpm-${chunkInfo.name.slice(6)}-[hash].[ext]`;
59
+ }
60
+ return 'assets/[name]-[hash].[ext]';
61
+ },
62
+ entryFileNames: (chunkInfo) => {
63
+ if (chunkInfo.name && chunkInfo.name.startsWith('.pnpm')) {
64
+ return `assets/pnpm-${chunkInfo.name.slice(6)}-[hash].js`;
65
+ }
66
+ return 'assets/[name]-[hash].js';
67
+ },
68
+ chunkFileNames: (chunkInfo) => {
69
+ if (chunkInfo.name && chunkInfo.name.startsWith('.pnpm')) {
70
+ return `assets/pnpm-${chunkInfo.name.slice(6)}-[hash].js`;
71
+ }
72
+ return 'assets/[name]-[hash].js';
73
+ }
92
74
  },
93
75
  },
94
76
  },
77
+ css: {
78
+ preprocessorOptions: {
79
+ scss: {
80
+ api: 'modern-compiler'
81
+ }
82
+ }
83
+ }
95
84
  })