adminforth 2.7.19 → 2.8.0

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 (107) hide show
  1. package/commands/createApp/templates/index.ts.hbs +2 -1
  2. package/commands/createCustomComponent/main.js +1 -0
  3. package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
  4. package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
  5. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  6. package/dist/dataConnectors/baseConnector.js +33 -15
  7. package/dist/dataConnectors/baseConnector.js.map +1 -1
  8. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  9. package/dist/dataConnectors/clickhouse.js +15 -0
  10. package/dist/dataConnectors/clickhouse.js.map +1 -1
  11. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  12. package/dist/dataConnectors/mongo.js +30 -1
  13. package/dist/dataConnectors/mongo.js.map +1 -1
  14. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  15. package/dist/dataConnectors/mysql.js +11 -0
  16. package/dist/dataConnectors/mysql.js.map +1 -1
  17. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  18. package/dist/dataConnectors/postgres.js +11 -0
  19. package/dist/dataConnectors/postgres.js.map +1 -1
  20. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  21. package/dist/dataConnectors/sqlite.js +11 -0
  22. package/dist/dataConnectors/sqlite.js.map +1 -1
  23. package/dist/modules/codeInjector.d.ts +1 -0
  24. package/dist/modules/codeInjector.d.ts.map +1 -1
  25. package/dist/modules/codeInjector.js +4 -0
  26. package/dist/modules/codeInjector.js.map +1 -1
  27. package/dist/modules/configValidator.d.ts.map +1 -1
  28. package/dist/modules/configValidator.js +6 -2
  29. package/dist/modules/configValidator.js.map +1 -1
  30. package/dist/modules/restApi.d.ts.map +1 -1
  31. package/dist/modules/restApi.js +2 -0
  32. package/dist/modules/restApi.js.map +1 -1
  33. package/dist/modules/styles.d.ts +8 -1
  34. package/dist/modules/styles.d.ts.map +1 -1
  35. package/dist/modules/styles.js +9 -2
  36. package/dist/modules/styles.js.map +1 -1
  37. package/dist/spa/src/App.vue +12 -4
  38. package/dist/spa/src/adminforth.ts +31 -11
  39. package/dist/spa/src/afcl/BarChart.vue +2 -2
  40. package/dist/spa/src/afcl/Checkbox.vue +2 -2
  41. package/dist/spa/src/afcl/Dialog.vue +38 -21
  42. package/dist/spa/src/afcl/Dropzone.vue +2 -2
  43. package/dist/spa/src/afcl/Input.vue +1 -1
  44. package/dist/spa/src/afcl/LinkButton.vue +1 -1
  45. package/dist/spa/src/afcl/PieChart.vue +5 -5
  46. package/dist/spa/src/afcl/Select.vue +17 -12
  47. package/dist/spa/src/afcl/Table.vue +202 -72
  48. package/dist/spa/src/afcl/Textarea.vue +31 -0
  49. package/dist/spa/src/afcl/Toggle.vue +2 -2
  50. package/dist/spa/src/afcl/Tooltip.vue +0 -1
  51. package/dist/spa/src/afcl/index.ts +1 -1
  52. package/dist/spa/src/components/AcceptModal.vue +1 -1
  53. package/dist/spa/src/components/ColumnValueInput.vue +11 -11
  54. package/dist/spa/src/components/ColumnValueInputWrapper.vue +2 -2
  55. package/dist/spa/src/components/CustomRangePicker.vue +5 -5
  56. package/dist/spa/src/components/ErrorMessage.vue +21 -0
  57. package/dist/spa/src/components/Filters.vue +7 -6
  58. package/dist/spa/src/components/GroupsTable.vue +2 -1
  59. package/dist/spa/src/components/MenuLink.vue +3 -3
  60. package/dist/spa/src/components/ResourceForm.vue +35 -27
  61. package/dist/spa/src/components/ResourceListTable.vue +42 -42
  62. package/dist/spa/src/components/ResourceListTableVirtual.vue +41 -41
  63. package/dist/spa/src/components/ShowTable.vue +3 -3
  64. package/dist/spa/src/components/SkeleteLoader.vue +2 -2
  65. package/dist/spa/src/components/ThreeDotsMenu.vue +69 -10
  66. package/dist/spa/src/components/Toast.vue +25 -2
  67. package/dist/spa/src/components/ValueRenderer.vue +39 -12
  68. package/dist/spa/src/i18n.ts +1 -1
  69. package/dist/spa/src/shims-vue.d.ts +5 -0
  70. package/dist/spa/src/spa_types/core.ts +1 -1
  71. package/dist/spa/src/stores/modal.ts +6 -1
  72. package/dist/spa/src/stores/toast.ts +22 -3
  73. package/dist/spa/src/types/Back.ts +31 -6
  74. package/dist/spa/src/types/Common.ts +33 -24
  75. package/dist/spa/src/types/FrontendAPI.ts +21 -5
  76. package/dist/spa/src/types/adapters/EmailAdapter.ts +2 -4
  77. package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
  78. package/dist/spa/src/types/adapters/index.ts +1 -0
  79. package/dist/spa/src/utils.ts +8 -7
  80. package/dist/spa/src/views/CreateView.vue +15 -16
  81. package/dist/spa/src/views/EditView.vue +23 -17
  82. package/dist/spa/src/views/ListView.vue +116 -66
  83. package/dist/spa/src/views/LoginView.vue +2 -9
  84. package/dist/spa/src/views/ResourceParent.vue +1 -1
  85. package/dist/spa/src/views/ShowView.vue +59 -39
  86. package/dist/spa/src/websocket.ts +6 -1
  87. package/dist/spa/tsconfig.app.json +1 -1
  88. package/dist/spa/vite.config.ts +45 -2
  89. package/dist/types/Back.d.ts +16 -1
  90. package/dist/types/Back.d.ts.map +1 -1
  91. package/dist/types/Back.js +15 -0
  92. package/dist/types/Back.js.map +1 -1
  93. package/dist/types/Common.d.ts +27 -22
  94. package/dist/types/Common.d.ts.map +1 -1
  95. package/dist/types/Common.js.map +1 -1
  96. package/dist/types/FrontendAPI.d.ts +21 -3
  97. package/dist/types/FrontendAPI.d.ts.map +1 -1
  98. package/dist/types/FrontendAPI.js.map +1 -1
  99. package/dist/types/adapters/EmailAdapter.d.ts +2 -3
  100. package/dist/types/adapters/EmailAdapter.d.ts.map +1 -1
  101. package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
  102. package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
  103. package/dist/types/adapters/ImageVisionAdapter.js +2 -0
  104. package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
  105. package/dist/types/adapters/index.d.ts +1 -0
  106. package/dist/types/adapters/index.d.ts.map +1 -1
  107. package/package.json +2 -1
@@ -51,8 +51,8 @@
51
51
  </div>
52
52
  <span
53
53
  class="bg-red-100 text-red-800 text-xs font-medium me-1 px-1 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
54
- v-if="sort.findIndex((s) => s.field === c.name) !== -1 && sort?.length > 1">
55
- {{ sort.findIndex((s) => s.field === c.name) + 1 }}
54
+ v-if="sort.findIndex((s: any) => s.field === c.name) !== -1 && sort?.length > 1">
55
+ {{ sort.findIndex((s: any) => s.field === c.name) + 1 }}
56
56
  </span>
57
57
 
58
58
  </div>
@@ -68,7 +68,7 @@
68
68
  <!-- table header end -->
69
69
  <SkeleteLoader
70
70
  v-if="!rows"
71
- :columns="resource?.columns.filter(c => c.showIn.list).length + 2"
71
+ :columns="resource?.columns.filter((c: AdminForthResourceColumnCommon) => c.showIn?.list).length + 2"
72
72
  :rows="rowHeights.length || 20"
73
73
  :row-heights="rowHeights"
74
74
  :column-widths="columnWidths"
@@ -99,13 +99,13 @@
99
99
  ref="rowRefs"
100
100
  class="bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover"
101
101
  :class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
102
- @mounted="(el) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
102
+ @mounted="(el: any) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
103
103
  >
104
104
  <td class="w-4 p-4 cursor-default" @click="(e)=>e.stopPropagation()">
105
105
  <Checkbox
106
106
  :model-value="checkboxesInternal.includes(row._primaryKeyValue)"
107
- @change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
108
- @click="(e)=>e.stopPropagation()"
107
+ @change="(e: any)=>{addToCheckedValues(row._primaryKeyValue)}"
108
+ @click="(e: any)=>e.stopPropagation()"
109
109
  >
110
110
  <span class="sr-only">{{ $t('checkbox') }}</span>
111
111
  </Checkbox>
@@ -113,8 +113,8 @@
113
113
  <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
114
114
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
115
115
  <component
116
- :is="c?.components?.list ? getCustomComponent(c.components.list) : ValueRenderer"
117
- :meta="c?.components?.list?.meta"
116
+ :is="c?.components?.list ? getCustomComponent(typeof c.components.list === 'string' ? { file: c.components.list } : c.components.list) : ValueRenderer"
117
+ :meta="typeof c?.components?.list === 'object' ? c.components.list.meta : undefined"
118
118
  :column="c"
119
119
  :record="row"
120
120
  :adminUser="coreStore.adminUser"
@@ -125,7 +125,7 @@
125
125
  <div class="flex text-lightPrimary dark:text-darkPrimary items-center">
126
126
  <Tooltip>
127
127
  <RouterLink
128
- v-if="resource.options?.allowedActions.show"
128
+ v-if="resource.options?.allowedActions?.show"
129
129
  :to="{
130
130
  name: 'resource-show',
131
131
  params: {
@@ -145,7 +145,7 @@
145
145
 
146
146
  <Tooltip>
147
147
  <RouterLink
148
- v-if="resource.options?.allowedActions.edit"
148
+ v-if="resource.options?.allowedActions?.edit"
149
149
  :to="{
150
150
  name: 'resource-edit',
151
151
  params: {
@@ -163,7 +163,7 @@
163
163
 
164
164
  <Tooltip>
165
165
  <button
166
- v-if="resource.options?.allowedActions.delete"
166
+ v-if="resource.options?.allowedActions?.delete"
167
167
  @click="deleteRecord(row)"
168
168
  >
169
169
  <IconTrashBinSolid class="w-5 h-5 me-2"/>
@@ -325,7 +325,7 @@ import {
325
325
  } from '@iconify-prerendered/vue-flowbite';
326
326
  import router from '@/router';
327
327
  import { Tooltip } from '@/afcl';
328
- import type { AdminForthResourceCommon } from '@/types/Common';
328
+ import type { AdminForthResourceCommon, AdminForthResourceColumnCommon } from '@/types/Common';
329
329
  import adminforth from '@/adminforth';
330
330
  import Checkbox from '@/afcl/Checkbox.vue';
331
331
 
@@ -359,7 +359,7 @@ const emits = defineEmits([
359
359
  const checkboxesInternal: Ref<any[]> = ref([]);
360
360
  const pageInput = ref('1');
361
361
  const page = ref(1);
362
- const sort = ref([]);
362
+ const sort: Ref<Array<{field: string, direction: string}>> = ref([]);
363
363
 
364
364
 
365
365
  const from = computed(() => ((page.value || 1) - 1) * props.pageSize + 1);
@@ -368,11 +368,11 @@ const to = computed(() => Math.min((page.value || 1) * props.pageSize, props.tot
368
368
  watch(() => page.value, (newPage) => {
369
369
  emits('update:page', newPage);
370
370
  });
371
- async function onPageKeydown(event) {
371
+ async function onPageKeydown(event: any) {
372
372
  // page input should accept only numbers, arrow keys and backspace
373
373
  if (['Enter', 'Space'].includes(event.code) ||
374
374
  (!['Backspace', 'ArrowRight', 'ArrowLeft'].includes(event.code)
375
- && isNaN(String.fromCharCode(event.keyCode)))) {
375
+ && isNaN(Number(String.fromCharCode(event.keyCode || 0))))) {
376
376
  event.preventDefault();
377
377
  if (event.code === 'Enter') {
378
378
  validatePageInput();
@@ -393,7 +393,7 @@ watch(() => props.checkboxes, (newCheckboxes) => {
393
393
  checkboxesInternal.value = newCheckboxes;
394
394
  });
395
395
 
396
- watch(() => props.sort, (newSort) => {
396
+ watch(() => props.sort, (newSort: any) => {
397
397
  sort.value = newSort;
398
398
  });
399
399
 
@@ -404,17 +404,17 @@ watch(() => props.page, (newPage) => {
404
404
  page.value = newPage;
405
405
  });
406
406
 
407
- const rowRefs = useTemplateRef('rowRefs');
408
- const headerRefs = useTemplateRef('headerRefs');
409
- const rowHeights = ref([]);
410
- const columnWidths = ref([]);
407
+ const rowRefs = useTemplateRef<HTMLElement[]>('rowRefs');
408
+ const headerRefs = useTemplateRef<HTMLElement[]>('headerRefs');
409
+ const rowHeights = ref<number[]>([]);
410
+ const columnWidths = ref<number[]>([]);
411
411
  watch(() => props.rows, (newRows) => {
412
412
  // rows are set to null when new records are loading
413
- rowHeights.value = newRows || !rowRefs.value ? [] : rowRefs.value.map((el) => el.offsetHeight);
414
- columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el) => el.offsetWidth)];
413
+ rowHeights.value = newRows || !rowRefs.value ? [] : rowRefs.value.map((el: HTMLElement) => el.offsetHeight);
414
+ columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el: HTMLElement) => el.offsetWidth)];
415
415
  });
416
416
 
417
- function addToCheckedValues(id) {
417
+ function addToCheckedValues(id: any) {
418
418
  if (checkboxesInternal.value.includes(id)) {
419
419
  checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
420
420
  } else {
@@ -423,17 +423,17 @@ function addToCheckedValues(id) {
423
423
  checkboxesInternal.value = [ ...checkboxesInternal.value ]
424
424
  }
425
425
 
426
- const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn.list));
426
+ const columnsListed = computed(() => props.resource?.columns?.filter((c: AdminForthResourceColumnCommon) => c.showIn?.list));
427
427
 
428
- async function selectAll(value) {
428
+ async function selectAll() {
429
429
  if (!allFromThisPageChecked.value) {
430
- props.rows.forEach((r) => {
430
+ props.rows?.forEach((r) => {
431
431
  if (!checkboxesInternal.value.includes(r._primaryKeyValue)) {
432
432
  checkboxesInternal.value.push(r._primaryKeyValue)
433
433
  }
434
434
  });
435
435
  } else {
436
- props.rows.forEach((r) => {
436
+ props.rows?.forEach((r) => {
437
437
  checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r._primaryKeyValue);
438
438
  });
439
439
  }
@@ -446,15 +446,15 @@ const allFromThisPageChecked = computed(() => {
446
446
  if (!props.rows || !props.rows.length) return false;
447
447
  return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
448
448
  });
449
- const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
450
- const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
449
+ const ascArr = computed(() => sort.value.filter((s: any) => s.direction === 'asc').map((s: any) => s.field));
450
+ const descArr = computed(() => sort.value.filter((s: any) => s.direction === 'desc').map((s: any) => s.field));
451
451
 
452
452
 
453
- function onSortButtonClick(event, field) {
453
+ function onSortButtonClick(event: any, field: any) {
454
454
  // if ctrl key is pressed, add to sort otherwise sort by this field
455
455
  // in any case if field is already in sort, toggle direction
456
456
 
457
- const sortIndex = sort.value.findIndex((s) => s.field === field);
457
+ const sortIndex = sort.value.findIndex((s: any) => s.field === field);
458
458
  if (sortIndex === -1) {
459
459
  // field is not in sort, add it
460
460
  if (event.ctrlKey) {
@@ -475,11 +475,11 @@ function onSortButtonClick(event, field) {
475
475
 
476
476
  const clickTarget = ref(null);
477
477
 
478
- async function onClick(e,row) {
478
+ async function onClick(e: any,row: any) {
479
479
  if(clickTarget.value === e.target) return;
480
480
  clickTarget.value = e.target;
481
481
  await new Promise((resolve) => setTimeout(resolve, 100));
482
- if (window.getSelection().toString()) return;
482
+ if (window.getSelection()?.toString()) return;
483
483
  else {
484
484
  if (row._clickUrl === null) {
485
485
  // user asked to nothing on click
@@ -494,7 +494,7 @@ async function onClick(e,row) {
494
494
  router.resolve({
495
495
  name: 'resource-show',
496
496
  params: {
497
- resourceId: props.resource.resourceId,
497
+ resourceId: props.resource?.resourceId,
498
498
  primaryKey: row._primaryKeyValue,
499
499
  },
500
500
  }).href,
@@ -512,7 +512,7 @@ async function onClick(e,row) {
512
512
  router.push({
513
513
  name: 'resource-show',
514
514
  params: {
515
- resourceId: props.resource.resourceId,
515
+ resourceId: props.resource?.resourceId,
516
516
  primaryKey: row._primaryKeyValue,
517
517
  },
518
518
  });
@@ -521,7 +521,7 @@ async function onClick(e,row) {
521
521
  }
522
522
  }
523
523
 
524
- async function deleteRecord(row) {
524
+ async function deleteRecord(row: any) {
525
525
  const data = await adminforth.confirm({
526
526
  message: t('Are you sure you want to delete this item?'),
527
527
  yes: t('Delete'),
@@ -533,7 +533,7 @@ async function deleteRecord(row) {
533
533
  path: '/delete_record',
534
534
  method: 'POST',
535
535
  body: {
536
- resourceId: props.resource.resourceId,
536
+ resourceId: props.resource?.resourceId,
537
537
  primaryKey: row._primaryKeyValue,
538
538
  }
539
539
  });
@@ -551,16 +551,16 @@ async function deleteRecord(row) {
551
551
  }
552
552
  }
553
553
 
554
- const actionLoadingStates = ref({});
554
+ const actionLoadingStates = ref<Record<string | number, boolean>>({});
555
555
 
556
- async function startCustomAction(actionId, row) {
556
+ async function startCustomAction(actionId: string, row: any) {
557
557
  actionLoadingStates.value[actionId] = true;
558
558
 
559
559
  const data = await callAdminForthApi({
560
560
  path: '/start_custom_action',
561
561
  method: 'POST',
562
562
  body: {
563
- resourceId: props.resource.resourceId,
563
+ resourceId: props.resource?.resourceId,
564
564
  actionId: actionId,
565
565
  recordId: row._primaryKeyValue
566
566
  }
@@ -598,7 +598,7 @@ async function startCustomAction(actionId, row) {
598
598
  }
599
599
  }
600
600
 
601
- function onPageInput(event) {
601
+ function onPageInput(event: any) {
602
602
  pageInput.value = event.target.innerText;
603
603
  }
604
604
 
@@ -62,10 +62,11 @@
62
62
  import { getCustomComponent } from '@/utils';
63
63
  import { useCoreStore } from '@/stores/core';
64
64
  import { computed } from 'vue';
65
+ import type { AdminForthResourceCommon } from '@/types/Common';
65
66
  const props = withDefaults(defineProps<{
66
67
  columns: Array<{
67
68
  name: string;
68
- label: string;
69
+ label?: string;
69
70
  components?: {
70
71
  show?: {
71
72
  file: string;
@@ -77,10 +78,9 @@
77
78
  };
78
79
  };
79
80
  }>;
80
- source: string;
81
81
  groupName?: string | null;
82
82
  noTitle?: boolean;
83
- resource: Record<string, any>;
83
+ resource: AdminForthResourceCommon | null;
84
84
  record: Record<string, any>;
85
85
  isRounded?: boolean;
86
86
  }>(), {
@@ -26,8 +26,8 @@ const props = withDefaults(defineProps<{
26
26
  rowHeights?: number[];
27
27
  columnWidths?: number[];
28
28
  }>(), {
29
- rowHeights: [],
30
- columnWidths: [],
29
+ rowHeights: () => [],
30
+ columnWidths: () => [],
31
31
  });
32
32
 
33
33
  </script>
@@ -1,5 +1,5 @@
1
1
  <template >
2
- <template v-if="threeDotsDropdownItems?.length || customActions?.length">
2
+ <template v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action: AdminForthBulkActionCommon) => action.showInThreeDotsDropdown))">
3
3
  <button
4
4
  data-dropdown-toggle="listThreeDotsDropdown"
5
5
  class="flex items-center py-2 px-2 text-sm font-medium text-lightThreeDotsMenuIconDots focus:outline-none bg-lightThreeDotsMenuIconBackground rounded border border-lightThreeDotsMenuIconBackgroundBorder hover:bg-lightThreeDotsMenuIconBackgroundHover hover:text-lightThreeDotsMenuIconDotsHover focus:z-10 focus:ring-4 focus:ring-lightThreeDotsMenuIconFocus dark:focus:ring-darkThreeDotsMenuIconFocus dark:bg-darkThreeDotsMenuIconBackground dark:text-darkThreeDotsMenuIconDots dark:border-darkThreeDotsMenuIconBackgroundBorder dark:hover:text-darkThreeDotsMenuIconDotsHover dark:hover:bg-darkThreeDotsMenuIconBackgroundHover rounded-default"
@@ -14,16 +14,26 @@
14
14
  id="listThreeDotsDropdown"
15
15
  class="z-20 hidden bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600">
16
16
  <ul class="py-2 text-sm text-lightThreeDotsMenuBodyText dark:text-darkThreeDotsMenuBodyText" aria-labelledby="dropdownMenuIconButton">
17
- <li v-for="item in threeDotsDropdownItems" :key="`dropdown-item-${item.label}`">
18
- <a href="#" class="block px-4 py-2 hover:bg-lightThreeDotsMenuBodyBackgroundHover hover:text-lightThreeDotsMenuBodyTextHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover">
19
- <component :is="getCustomComponent(item)"
17
+ <li v-for="(item, i) in threeDotsDropdownItems" :key="`dropdown-item-${i}`">
18
+ <a href="#"
19
+ class="block px-4 py-2 hover:bg-lightThreeDotsMenuBodyBackgroundHover hover:text-lightThreeDotsMenuBodyTextHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover"
20
+ :class="{
21
+ 'pointer-events-none': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
22
+ 'opacity-50': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
23
+ 'cursor-not-allowed': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
24
+ }"
25
+ @click="injectedComponentClick(i)">
26
+ <component :ref="(el: any) => setComponentRef(el, i)" :is="getCustomComponent(item)"
20
27
  :meta="item.meta"
21
28
  :resource="coreStore.resource"
22
29
  :adminUser="coreStore.adminUser"
30
+ :checkboxes="checkboxes"
31
+ :updateList="props.updateList"
32
+ :clearCheckboxes="clearCheckboxes"
23
33
  />
24
34
  </a>
25
35
  </li>
26
- <li v-for="action in customActions" :key="action.id">
36
+ <li v-if="customActions" v-for="action in customActions" :key="action.id">
27
37
  <a href="#" @click.prevent="handleActionClick(action)" class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover">
28
38
  <div class="flex items-center gap-2">
29
39
  <component
@@ -35,6 +45,24 @@
35
45
  </div>
36
46
  </a>
37
47
  </li>
48
+ <li v-for="action in bulkActions?.filter((a:AdminForthBulkActionCommon ) => a.showInThreeDotsDropdown)" :key="action.id">
49
+ <a href="#" @click.prevent="startBulkAction(action.id)"
50
+ class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover"
51
+ :class="{
52
+ 'pointer-events-none': checkboxes && checkboxes.length === 0,
53
+ 'opacity-50': checkboxes && checkboxes.length === 0,
54
+ 'cursor-not-allowed': checkboxes && checkboxes.length === 0
55
+ }">
56
+ <div class="flex items-center gap-2">
57
+ <component
58
+ v-if="action.icon"
59
+ :is="getIcon(action.icon)"
60
+ class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
61
+ />
62
+ {{ action.label }}
63
+ </div>
64
+ </a>
65
+ </li>
38
66
  </ul>
39
67
  </div>
40
68
  </template>
@@ -47,17 +75,36 @@ import { useCoreStore } from '@/stores/core';
47
75
  import adminforth from '@/adminforth';
48
76
  import { callAdminForthApi } from '@/utils';
49
77
  import { useRoute, useRouter } from 'vue-router';
78
+ import type { AdminForthComponentDeclarationFull, AdminForthBulkActionCommon, AdminForthActionInput } from '@/types/Common.js';
79
+ import { ref, type ComponentPublicInstance } from 'vue';
50
80
 
51
81
  const route = useRoute();
52
82
  const coreStore = useCoreStore();
53
83
  const router = useRouter();
84
+ const threeDotsDropdownItemsRefs = ref<Array<ComponentPublicInstance | null>>([]);
54
85
 
55
86
  const props = defineProps({
56
- threeDotsDropdownItems: Array,
57
- customActions: Array
87
+ threeDotsDropdownItems: Array<AdminForthComponentDeclarationFull>,
88
+ customActions: Array<AdminForthActionInput>,
89
+ bulkActions: Array<AdminForthBulkActionCommon>,
90
+ checkboxes: Array,
91
+ updateList: {
92
+ type: Function,
93
+ },
94
+ clearCheckboxes: {
95
+ type: Function
96
+ }
58
97
  });
59
98
 
60
- async function handleActionClick(action) {
99
+ const emit = defineEmits(['startBulkAction']);
100
+
101
+ function setComponentRef(el: ComponentPublicInstance | null, index: number) {
102
+ if (el) {
103
+ threeDotsDropdownItemsRefs.value[index] = el;
104
+ }
105
+ }
106
+
107
+ async function handleActionClick(action: AdminForthActionInput) {
61
108
  adminforth.list.closeThreeDotsDropdown();
62
109
 
63
110
  const actionId = action.id;
@@ -88,8 +135,8 @@ async function handleActionClick(action) {
88
135
 
89
136
  if (data?.ok) {
90
137
  await coreStore.fetchRecord({
91
- resourceId: route.params.resourceId,
92
- primaryKey: route.params.primaryKey,
138
+ resourceId: route.params.resourceId as string,
139
+ primaryKey: route.params.primaryKey as string,
93
140
  source: 'show',
94
141
  });
95
142
 
@@ -108,4 +155,16 @@ async function handleActionClick(action) {
108
155
  });
109
156
  }
110
157
  }
158
+
159
+ function startBulkAction(actionId: string) {
160
+ adminforth.list.closeThreeDotsDropdown();
161
+ emit('startBulkAction', actionId);
162
+ }
163
+
164
+ async function injectedComponentClick(index: number) {
165
+ const componentRef = threeDotsDropdownItemsRefs.value[index];
166
+ if (componentRef && 'click' in componentRef) {
167
+ (componentRef as any).click?.();
168
+ }
169
+ }
111
170
  </script>
@@ -30,7 +30,18 @@
30
30
  </div>
31
31
 
32
32
  <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
33
- <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>{{toast.message}}</div>
33
+ <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>
34
+ <div class="flex flex-col items-center justify-center">
35
+ {{toast.message}}
36
+ <div v-if="toast.buttons" class="flex justify-center mt-2 gap-2">
37
+ <div v-for="button in toast.buttons" class="rounded-md bg-lightButtonsBackground hover:bg-lightButtonsHover text-lightButtonsText dark:bg-darkPrimary dark:hover:bg-darkButtonsBackground dark:text-darkButtonsText">
38
+ <button @click="onButtonClick(button.value)" class="px-2 py-1 rounded hover:bg-black/5 dark:hover:bg-white/10">
39
+ {{ button.label }}
40
+ </button>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </div>
34
45
  <button @click="closeToast" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-lightToastCloseIconBackground text-lightToastCloseIcon hover:text-lightToastCloseIconHover rounded-lg focus:ring-2 focus:ring-lightToastCloseIconFocusRing p-1.5 hover:bg-lightToastCloseIconBackgroundHover inline-flex items-center justify-center h-8 w-8 dark:text-darkToastCloseIcon dark:hover:text-darkToastCloseIconHover dark:bg-darkToastCloseIconBackground dark:hover:bg-darkToastCloseIconBackgroundHover dark:focus:ring-darkToastCloseIconFocusRing" >
35
46
  <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
36
47
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
@@ -53,16 +64,28 @@ const props = defineProps<{
53
64
  variant: string;
54
65
  id: string;
55
66
  timeout?: number|'unlimited';
67
+ buttons?: { value: any; label: string }[];
56
68
  }
57
69
  }>();
58
70
  function closeToast() {
71
+ // resolve with undefined on close (X button)
72
+ toastStore.resolveToast(props.toast.id);
73
+ emit('close');
74
+ }
75
+
76
+ function onButtonClick(value: any) {
77
+ toastStore.resolveToast(props.toast.id, value);
59
78
  emit('close');
60
79
  }
61
80
 
62
81
  onMounted(() => {
63
82
  if (props.toast.timeout === 'unlimited') return;
64
83
  else {
65
- setTimeout(() => {emit('close');}, (props.toast.timeout || 10) * 1e3 );
84
+ setTimeout(() => {
85
+ // resolve with undefined on auto-timeout
86
+ toastStore.resolveToast(props.toast.id);
87
+ emit('close');
88
+ }, (props.toast.timeout || 10) * 1e3 );
66
89
  }
67
90
  });
68
91
 
@@ -12,13 +12,40 @@
12
12
  >
13
13
  <RouterLink
14
14
  class="font-medium text-lightSidebarText dark:text-darkSidebarText hover:brightness-110 whitespace-nowrap"
15
- :to="{ name: 'resource-show', params: { primaryKey: foreignResource.pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }"
15
+ :to="{
16
+ name: 'resource-show',
17
+ params: {
18
+ primaryKey: foreignResource.pk,
19
+ resourceId: column.foreignResource
20
+ ? (
21
+ column.foreignResource.resourceId
22
+ || column.foreignResource.polymorphicResources?.find(
23
+ (pr: any) => pr.whenValue === record[column.foreignResource?.polymorphicOn!]
24
+ )?.resourceId
25
+ )
26
+ : undefined
27
+ }
28
+ }"
16
29
  >
17
30
  {{ foreignResource.label }}
18
31
  </RouterLink>
19
32
  </span>
20
33
  <RouterLink v-else-if="record[column.name]" class="font-medium text-lightPrimary dark:text-darkPrimary hover:brightness-110 whitespace-nowrap"
21
- :to="{ name: 'resource-show', params: { primaryKey: record[column.name].pk, resourceId: column.foreignResource.resourceId || column.foreignResource.polymorphicResources.find((pr) => pr.whenValue === record[column.foreignResource.polymorphicOn]).resourceId } }">
34
+ :to="{
35
+ name: 'resource-show',
36
+ params: {
37
+ primaryKey: record[column.name].pk,
38
+ resourceId: column.foreignResource
39
+ ? (
40
+ column.foreignResource.resourceId
41
+ || column.foreignResource.polymorphicResources?.find(
42
+ (pr: any) => pr.whenValue === record[column.foreignResource?.polymorphicOn!]
43
+ )?.resourceId
44
+ )
45
+ : undefined
46
+ }
47
+ }"
48
+ >
22
49
  {{ record[column.name].label }}
23
50
  </RouterLink>
24
51
  <div v-else>
@@ -38,13 +65,13 @@
38
65
  <template v-for="(arrayItem, arrayItemIndex) in record[column.name]">
39
66
  <span
40
67
  v-if="column.isArray.itemType === 'boolean' && arrayItem"
41
- :key="`${column.name}-${arrayItemIndex}`"
68
+ :key="`${column.name}-${arrayItemIndex}-true`"
42
69
  class="af-true-value-icon bg-green-100 whitespace-nowrap text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">
43
70
  {{ $t('Yes') }}
44
71
  </span>
45
72
  <span
46
73
  v-else-if="column.isArray.itemType === 'boolean'"
47
- :key="`${column.name}-${arrayItemIndex}`"
74
+ :key="`${column.name}-${arrayItemIndex}-false`"
48
75
  class="af-false-value-icon bg-red-100 whitespace-nowrap text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">
49
76
  {{ $t('No') }}
50
77
  </span>
@@ -53,30 +80,30 @@
53
80
  :key="`${column.name}-${arrayItemIndex}`"
54
81
  class="rounded-md m-0.5 bg-lightAnnouncementBG dark:bg-darkAnnouncementBG text-lightAnnouncementText dark:text-darkAnnouncementText py-0.5 px-2.5 text-sm"
55
82
  >
56
- {{ checkEmptyValues(getArrayItemDisplayValue(arrayItem, column), route.meta.type) }}
83
+ {{ checkEmptyValues(getArrayItemDisplayValue(arrayItem, column), route.meta.type as "show" | "list") }}
57
84
  </span>
58
85
  </template>
59
86
  </span>
60
87
  <span v-else-if="column.enum">
61
- {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label || record[column.name], route.meta.type) }}
88
+ {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label || record[column.name], route.meta.type as "show" | "list") }}
62
89
  </span>
63
90
  <span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
64
- {{ checkEmptyValues(formatDateTime(record[column.name]), route.meta.type) }}
91
+ {{ checkEmptyValues(formatDateTime(record[column.name]), route.meta.type as "show" | "list") }}
65
92
  </span>
66
93
  <span v-else-if="column.type === 'date'" class="whitespace-nowrap">
67
- {{ checkEmptyValues(formatDate(record[column.name]), route.meta.type) }}
94
+ {{ checkEmptyValues(formatDate(record[column.name]), route.meta.type as "show" | "list") }}
68
95
  </span>
69
96
  <span v-else-if="column.type === 'time'" class="whitespace-nowrap">
70
- {{ checkEmptyValues(formatTime(record[column.name]), route.meta.type) }}
97
+ {{ checkEmptyValues(formatTime(record[column.name]), route.meta.type as "show" | "list") }}
71
98
  </span>
72
99
  <span v-else-if="column.type === 'decimal'">
73
- {{ checkEmptyValues(record[column.name] && parseFloat(record[column.name]), route.meta.type) }}
100
+ {{ checkEmptyValues(record[column.name] && parseFloat(record[column.name]), route.meta.type as "show" | "list") }}
74
101
  </span>
75
102
  <span v-else-if="column.type === 'json'">
76
103
  <JsonViewer class="min-w-[6rem]" :value="record[column.name]" :expandDepth="column.extra?.jsonCollapsedLevel" copyable sort :theme="coreStore.theme"/>
77
104
  </span>
78
105
  <span v-else>
79
- {{ checkEmptyValues(record[column.name],route.meta.type) }}
106
+ {{ checkEmptyValues(record[column.name], route.meta.type as "show" | "list") }}
80
107
  </span>
81
108
  </div>
82
109
  </template>
@@ -122,7 +149,7 @@ function formatTime(time: string) {
122
149
  return dayjs(`0000-00-00 ${time}`).format(coreStore.config?.timeFormat || 'HH:mm:ss');
123
150
  }
124
151
 
125
- function getArrayItemDisplayValue(value, column) {
152
+ function getArrayItemDisplayValue(value: any, column: AdminForthResourceColumnCommon) {
126
153
  if (column.isArray?.itemType === 'datetime') {
127
154
  return formatDateTime(value);
128
155
  } else if (column.isArray?.itemType === 'date') {
@@ -3,7 +3,7 @@ import { createApp } from 'vue';
3
3
 
4
4
 
5
5
  // taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
6
- function slavicPluralRule(choice, choicesLength, orgRule) {
6
+ function slavicPluralRule(choice: number, choicesLength: number, orgRule: any) {
7
7
  if (choice === 0) {
8
8
  return 0
9
9
  }
@@ -0,0 +1,5 @@
1
+ declare module '*.vue' {
2
+ import type { DefineComponent } from 'vue';
3
+ const component: DefineComponent<{}, {}, any>;
4
+ export default component;
5
+ }
@@ -1,4 +1,4 @@
1
- import type { AdminForthResource, AdminForthResourceColumn } from '../types/AdminForthConfig';
1
+ import type { AdminForthResource, AdminForthResourceColumn } from '../types/Back.js';
2
2
 
3
3
  export type resourceById = {
4
4
  [key: string]: AdminForthResource;
@@ -29,7 +29,12 @@ export const useModalStore = defineStore('modal', () => {
29
29
  onCancelFunction.value = func;
30
30
  }
31
31
  function setModalContent(content: ModalContentType) {
32
- modalContent.value = content;
32
+ modalContent.value = {
33
+ title: content.title || 'title',
34
+ content: content.content || 'content',
35
+ acceptText: content.acceptText || 'acceptText',
36
+ cancelText: content.cancelText || 'cancelText',
37
+ };
33
38
  }
34
39
  function resetmodalState() {
35
40
  isOpened.value = false;
@@ -12,19 +12,38 @@ export const useToastStore = defineStore('toast', () => {
12
12
  watch(route, () => {
13
13
  // on route change clear all toasts older then 5 seconds
14
14
  const now = +new Date();
15
- toasts.value = toasts.value.filter((t) => now - t.createdAt < 5000);
15
+ toasts.value = toasts.value.filter((t) => t?.timeout === 'unlimited' || now - t.createdAt < 5000);
16
16
  });
17
17
 
18
- const addToast = (toast: { message: string; variant: string }) => {
18
+ const addToast = (toast: {
19
+ message?: string;
20
+ messageHtml?: string;
21
+ variant: string;
22
+ timeout?: number | 'unlimited';
23
+ buttons?: { value: any; label: string }[];
24
+ onResolve?: (value?: any) => void;
25
+ }): string => {
19
26
  const toastId = uuid();
20
27
  toasts.value.push({
21
28
  ...toast,
22
29
  id: toastId,
23
30
  createdAt: +new Date(),
24
31
  });
32
+ return toastId;
25
33
  };
26
34
  const removeToast = (toast: { id: string }) => {
27
35
  toasts.value = toasts.value.filter((t) => t.id !== toast.id);
28
36
  };
29
- return { toasts, addToast, removeToast };
37
+
38
+ const resolveToast = (toastId: string, value?: any) => {
39
+ const t = toasts.value.find((x) => x.id === toastId);
40
+ try {
41
+ t?.onResolve?.(value);
42
+ } catch {
43
+ // no-op
44
+ }
45
+ toasts.value = toasts.value.filter((x) => x.id !== toastId);
46
+ };
47
+
48
+ return { toasts, addToast, removeToast, resolveToast };
30
49
  });