adminforth 2.17.0-next.3 → 2.17.0-next.30

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 (68) hide show
  1. package/commands/createCustomComponent/main.js +0 -3
  2. package/commands/createPlugin/templates/index.ts.hbs +4 -0
  3. package/dist/auth.d.ts +1 -1
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +1 -2
  6. package/dist/auth.js.map +1 -1
  7. package/dist/basePlugin.d.ts +1 -0
  8. package/dist/basePlugin.d.ts.map +1 -1
  9. package/dist/basePlugin.js +3 -0
  10. package/dist/basePlugin.js.map +1 -1
  11. package/dist/dataConnectors/baseConnector.d.ts +1 -0
  12. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  13. package/dist/dataConnectors/baseConnector.js +92 -6
  14. package/dist/dataConnectors/baseConnector.js.map +1 -1
  15. package/dist/dataConnectors/clickhouse.d.ts +2 -0
  16. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  17. package/dist/dataConnectors/clickhouse.js +11 -1
  18. package/dist/dataConnectors/clickhouse.js.map +1 -1
  19. package/dist/dataConnectors/mongo.d.ts +8 -1
  20. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  21. package/dist/dataConnectors/mongo.js +67 -24
  22. package/dist/dataConnectors/mongo.js.map +1 -1
  23. package/dist/dataConnectors/mysql.d.ts +2 -0
  24. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  25. package/dist/dataConnectors/mysql.js +12 -2
  26. package/dist/dataConnectors/mysql.js.map +1 -1
  27. package/dist/dataConnectors/postgres.d.ts +2 -0
  28. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  29. package/dist/dataConnectors/postgres.js +12 -2
  30. package/dist/dataConnectors/postgres.js.map +1 -1
  31. package/dist/dataConnectors/sqlite.d.ts +2 -0
  32. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  33. package/dist/dataConnectors/sqlite.js +12 -2
  34. package/dist/dataConnectors/sqlite.js.map +1 -1
  35. package/dist/modules/configValidator.d.ts.map +1 -1
  36. package/dist/modules/configValidator.js +9 -7
  37. package/dist/modules/configValidator.js.map +1 -1
  38. package/dist/modules/restApi.d.ts.map +1 -1
  39. package/dist/modules/restApi.js +3 -3
  40. package/dist/modules/restApi.js.map +1 -1
  41. package/dist/spa/src/adminforth.ts +52 -0
  42. package/dist/spa/src/afcl/Tooltip.vue +38 -4
  43. package/dist/spa/src/components/ColumnValueInput.vue +14 -1
  44. package/dist/spa/src/components/CustomRangePicker.vue +9 -4
  45. package/dist/spa/src/components/Filters.vue +4 -4
  46. package/dist/spa/src/components/ListActionsThreeDots.vue +235 -0
  47. package/dist/spa/src/components/ResourceForm.vue +5 -5
  48. package/dist/spa/src/components/ResourceListTable.vue +26 -13
  49. package/dist/spa/src/components/ResourceListTableVirtual.vue +30 -15
  50. package/dist/spa/src/components/ShowTable.vue +2 -2
  51. package/dist/spa/src/renderers/RichText.vue +15 -0
  52. package/dist/spa/src/stores/filters.ts +1 -1
  53. package/dist/spa/src/types/Back.ts +22 -5
  54. package/dist/spa/src/types/Common.ts +3 -10
  55. package/dist/spa/src/utils.ts +8 -3
  56. package/dist/spa/src/views/CreateView.vue +22 -30
  57. package/dist/spa/src/views/EditView.vue +25 -29
  58. package/dist/spa/src/views/ListView.vue +12 -2
  59. package/dist/types/Back.d.ts +15 -4
  60. package/dist/types/Back.d.ts.map +1 -1
  61. package/dist/types/Back.js +6 -0
  62. package/dist/types/Back.js.map +1 -1
  63. package/dist/types/Common.d.ts +4 -11
  64. package/dist/types/Common.d.ts.map +1 -1
  65. package/dist/types/Common.js +2 -0
  66. package/dist/types/Common.js.map +1 -1
  67. package/package.json +1 -1
  68. package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +0 -28
@@ -0,0 +1,235 @@
1
+ <template>
2
+ <div class="relative inline-block">
3
+ <div
4
+ ref="triggerRef"
5
+ class="border border-gray-300 dark:border-gray-700 dark:border-opacity-0 border-opacity-0 hover:border-opacity-100 dark:hover:border-opacity-100 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
6
+ @click="toggleMenu"
7
+ >
8
+ <IconDotsHorizontalOutline class="w-6 h-6 text-lightPrimary dark:text-darkPrimary" />
9
+ </div>
10
+ <teleport to="body">
11
+ <div
12
+ v-if="showMenu"
13
+ ref="menuRef"
14
+ class="z-50 bg-white dark:bg-gray-900 rounded-md shadow-lg border dark:border-gray-700 py-1"
15
+ :style="menuStyles"
16
+ >
17
+ <template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('show'))">
18
+ <RouterLink
19
+ v-if="resourceOptions?.allowedActions?.show"
20
+ class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
21
+ :to="{
22
+ name: 'resource-show',
23
+ params: {
24
+ resourceId: props.resourceId,
25
+ primaryKey: record._primaryKeyValue,
26
+ }
27
+ }"
28
+
29
+ >
30
+ <IconEyeSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
31
+ {{ $t('Show item') }}
32
+ </RouterLink>
33
+ </template>
34
+ <template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('edit'))">
35
+ <RouterLink
36
+ v-if="resourceOptions?.allowedActions?.edit"
37
+ class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
38
+ :to="{
39
+ name: 'resource-edit',
40
+ params: {
41
+ resourceId: props.resourceId,
42
+ primaryKey: record._primaryKeyValue,
43
+ }
44
+ }"
45
+ >
46
+ <IconPenSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
47
+ {{ $t('Edit item') }}
48
+ </RouterLink>
49
+ </template>
50
+ <template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('delete'))">
51
+ <button
52
+ v-if="resourceOptions?.allowedActions?.delete"
53
+ class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
54
+ @click="deleteRecord(record)"
55
+ >
56
+ <IconTrashBinSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
57
+ {{ $t('Delete item') }}
58
+ </button>
59
+ </template>
60
+ <div v-for="action in (resourceOptions.actions ?? []).filter(a => a.showIn?.listThreeDotsMenu)" :key="action.id" >
61
+ <button class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300" @click="() => { startCustomAction(action.id, record); showMenu = false; }">
62
+ <component
63
+ :is="action.customComponent ? getCustomComponent(action.customComponent) : CallActionWrapper"
64
+ :meta="action.customComponent?.meta"
65
+ :row="record"
66
+ :resource="resource"
67
+ :adminUser="adminUser"
68
+ @callAction="(payload? : Object) => startCustomAction(action.id, record, payload)"
69
+ >
70
+ <component
71
+ v-if="action.icon"
72
+ :is="getIcon(action.icon)"
73
+ class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"
74
+ />
75
+ {{ $t(action.name) }}
76
+ </component>
77
+ </button>
78
+ </div>
79
+ <template v-if="customActionIconsThreeDotsMenuItems">
80
+ <component
81
+ v-for="c in customActionIconsThreeDotsMenuItems"
82
+ :is="getCustomComponent(c)"
83
+ :meta="c.meta"
84
+ :resource="coreStore.resource"
85
+ :adminUser="coreStore.adminUser"
86
+ :record="record"
87
+ :updateRecords="props.updateRecords"
88
+ />
89
+ </template>
90
+ </div>
91
+ </teleport>
92
+ </div>
93
+ </template>
94
+
95
+ <script lang="ts" setup>
96
+ import {
97
+ IconEyeSolid,
98
+ IconPenSolid,
99
+ IconTrashBinSolid,
100
+ IconDotsHorizontalOutline
101
+ } from '@iconify-prerendered/vue-flowbite';
102
+ import { onMounted, onBeforeUnmount, ref, nextTick, watch } from 'vue';
103
+ import { getIcon, getCustomComponent } from '@/utils';
104
+ import { useCoreStore } from '@/stores/core';
105
+ import CallActionWrapper from '@/components/CallActionWrapper.vue'
106
+
107
+ const coreStore = useCoreStore();
108
+ const showMenu = ref(false);
109
+ const triggerRef = ref<HTMLElement | null>(null);
110
+ const menuRef = ref<HTMLElement | null>(null);
111
+ const menuStyles = ref<Record<string, string>>({});
112
+
113
+ const props = defineProps<{
114
+ resourceOptions: any;
115
+ record: any;
116
+ customActionIconsThreeDotsMenuItems: any[];
117
+ resourceId: string;
118
+ deleteRecord: (record: any) => void;
119
+ updateRecords: () => void;
120
+ startCustomAction: (actionId: string, record: any) => void;
121
+ }>();
122
+
123
+ onMounted(() => {
124
+ window.addEventListener('scroll', handleScrollOrResize, true);
125
+ window.addEventListener('resize', handleScrollOrResize);
126
+ document.addEventListener('click', handleOutsideClick, true);
127
+ });
128
+
129
+ onBeforeUnmount(() => {
130
+ window.removeEventListener('scroll', handleScrollOrResize, true);
131
+ window.removeEventListener('resize', handleScrollOrResize);
132
+ document.removeEventListener('click', handleOutsideClick, true);
133
+ });
134
+
135
+ watch(showMenu, async (isOpen) => {
136
+ if (isOpen) {
137
+ await nextTick();
138
+ // First pass: after DOM mount
139
+ updateMenuPosition();
140
+ // Second pass: after layout/paint to catch width changes (fonts/icons)
141
+ requestAnimationFrame(() => {
142
+ updateMenuPosition();
143
+ // Final safety: one micro-delay retry if width was still 0
144
+ setTimeout(() => updateMenuPosition(), 0);
145
+ });
146
+ }
147
+ });
148
+
149
+ function toggleMenu() {
150
+ if (!showMenu.value) {
151
+ // Provisional position to avoid flashing at left:0 on first open
152
+ const el = triggerRef.value;
153
+ if (el) {
154
+ const rect = el.getBoundingClientRect();
155
+ const gap = 8;
156
+ menuStyles.value = {
157
+ position: 'fixed',
158
+ top: `${Math.round(rect.bottom)}px`,
159
+ left: `${Math.round(rect.left)}px`,
160
+ };
161
+ }
162
+ }
163
+ showMenu.value = !showMenu.value;
164
+ }
165
+
166
+ function updateMenuPosition() {
167
+ const el = triggerRef.value;
168
+ if (!el) return;
169
+ const rect = el.getBoundingClientRect();
170
+ const margin = 8; // gap around the trigger/menu
171
+ const menuEl = menuRef.value;
172
+ // Measure current menu size to align and decide flipping
173
+ let menuWidth = rect.width; // fallback to trigger width
174
+ let menuHeight = 0;
175
+ if (menuEl) {
176
+ const menuRect = menuEl.getBoundingClientRect();
177
+ // Prefer bounding rect; fallback to offset/scroll width if needed
178
+ const measuredW = menuRect.width || menuEl.offsetWidth || menuEl.scrollWidth;
179
+ if (measuredW > 0) menuWidth = measuredW;
180
+ const measuredH = menuRect.height || menuEl.offsetHeight || menuEl.scrollHeight;
181
+ if (measuredH > 0) menuHeight = measuredH;
182
+ }
183
+ // Right-align: right edge of menu == right edge of trigger
184
+ let left = rect.right - menuWidth;
185
+ // Clamp within viewport with small margin so it doesn't render off-screen
186
+ const minLeft = margin;
187
+ const maxLeft = Math.max(minLeft, window.innerWidth - margin - menuWidth);
188
+ left = Math.min(Math.max(left, minLeft), maxLeft);
189
+
190
+ // Determine whether to place above or below based on available space
191
+ const spaceBelow = window.innerHeight - rect.bottom - margin;
192
+ const spaceAbove = rect.top - margin;
193
+ const maxMenuHeight = Math.max(0, window.innerHeight - 2 * margin);
194
+
195
+ let top: number;
196
+ if (menuHeight === 0) {
197
+ // Unknown height yet (first pass). Prefer placing below; a subsequent pass will correct if needed.
198
+ top = rect.bottom + margin;
199
+ } else if (menuHeight <= spaceBelow) {
200
+ // Enough space below
201
+ top = rect.bottom + margin;
202
+ } else if (menuHeight <= spaceAbove) {
203
+ // Not enough below but enough above -> flip
204
+ top = rect.top - margin - menuHeight;
205
+ } else {
206
+ // Not enough space on either side: pick the side with more room and clamp within viewport
207
+ if (spaceBelow >= spaceAbove) {
208
+ top = Math.min(rect.bottom + margin, window.innerHeight - margin - menuHeight);
209
+ } else {
210
+ top = Math.max(margin, rect.top - margin - menuHeight);
211
+ }
212
+ }
213
+
214
+ menuStyles.value = {
215
+ position: 'fixed',
216
+ top: `${Math.round(top)}px`,
217
+ left: `${Math.round(left)}px`,
218
+ maxHeight: `${Math.round(maxMenuHeight)}px`,
219
+ overflowY: 'auto',
220
+ };
221
+ }
222
+
223
+ function handleScrollOrResize() {
224
+ showMenu.value = false;
225
+ }
226
+
227
+ function handleOutsideClick(e: MouseEvent) {
228
+ const target = e.target as Node | null;
229
+ if (!target) return;
230
+ if (menuRef.value?.contains(target)) return;
231
+ if (triggerRef.value?.contains(target)) return;
232
+ showMenu.value = false;
233
+ }
234
+
235
+ </script>
@@ -206,7 +206,7 @@ const setCurrentValue = (key: any, value: any, index = null) => {
206
206
  } else if (index === currentValues.value[key].length) {
207
207
  currentValues.value[key].push(null);
208
208
  } else {
209
- if (['integer', 'float', 'decimal'].includes(col.isArray.itemType)) {
209
+ if (['integer', 'float'].includes(col.isArray.itemType)) {
210
210
  if (value || value === 0) {
211
211
  currentValues.value[key][index] = +value;
212
212
  } else {
@@ -215,12 +215,12 @@ const setCurrentValue = (key: any, value: any, index = null) => {
215
215
  } else {
216
216
  currentValues.value[key][index] = value;
217
217
  }
218
- if (col?.isArray && ['text', 'richtext', 'string'].includes(col.isArray.itemType) && col.enforceLowerCase) {
218
+ if (col?.isArray && ['text', 'richtext', 'string', 'decimal'].includes(col.isArray.itemType) && col.enforceLowerCase) {
219
219
  currentValues.value[key][index] = currentValues.value[key][index].toLowerCase();
220
220
  }
221
221
  }
222
222
  } else {
223
- if (col?.type && ['integer', 'float', 'decimal'].includes(col.type)) {
223
+ if (col?.type && ['integer', 'float'].includes(col.type)) {
224
224
  if (value || value === 0) {
225
225
  currentValues.value[key] = +value;
226
226
  } else {
@@ -229,7 +229,7 @@ const setCurrentValue = (key: any, value: any, index = null) => {
229
229
  } else {
230
230
  currentValues.value[key] = value;
231
231
  }
232
- if (col?.type && ['text', 'richtext', 'string'].includes(col?.type) && col.enforceLowerCase) {
232
+ if (col?.type && ['text', 'richtext', 'string', 'decimal'].includes(col?.type) && col.enforceLowerCase) {
233
233
  currentValues.value[key] = currentValues.value[key].toLowerCase();
234
234
  }
235
235
  }
@@ -322,7 +322,7 @@ async function searchOptions(columnName: string, searchTerm: string) {
322
322
 
323
323
 
324
324
  const editableColumns = computed(() => {
325
- return props.resource?.columns?.filter(column => column.showIn?.[mode.value] && (currentValues.value ? checkShowIf(column, currentValues.value) : true));
325
+ return props.resource?.columns?.filter(column => column.showIn?.[mode.value] && (currentValues.value ? checkShowIf(column, currentValues.value, props.resource.columns) : true));
326
326
  });
327
327
 
328
328
  const isValid = computed(() => {
@@ -119,8 +119,8 @@
119
119
  />
120
120
  </td>
121
121
  <td class=" items-center px-2 md:px-3 lg:px-6 py-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
122
- <div class="flex text-lightPrimary dark:text-darkPrimary items-center">
123
- <Tooltip>
122
+ <div class="flex text-lightPrimary dark:text-darkPrimary items-center gap-2">
123
+ <Tooltip v-if="resource.options?.baseActionsAsQuickIcons && resource.options?.baseActionsAsQuickIcons.includes('show')">
124
124
  <RouterLink
125
125
  v-if="resource.options?.allowedActions?.show"
126
126
  :to="{
@@ -139,8 +139,7 @@
139
139
  {{ $t('Show item') }}
140
140
  </template>
141
141
  </Tooltip>
142
-
143
- <Tooltip>
142
+ <Tooltip v-if="resource.options?.baseActionsAsQuickIcons && resource.options?.baseActionsAsQuickIcons.includes('edit')" >
144
143
  <RouterLink
145
144
  v-if="resource.options?.allowedActions?.edit"
146
145
  :to="{
@@ -157,8 +156,7 @@
157
156
  {{ $t('Edit item') }}
158
157
  </template>
159
158
  </Tooltip>
160
-
161
- <Tooltip>
159
+ <Tooltip v-if="resource.options?.baseActionsAsQuickIcons && resource.options?.baseActionsAsQuickIcons.includes('delete')">
162
160
  <button
163
161
  v-if="resource.options?.allowedActions?.delete"
164
162
  @click="deleteRecord(row)"
@@ -170,7 +168,6 @@
170
168
  {{ $t('Delete item') }}
171
169
  </template>
172
170
  </Tooltip>
173
-
174
171
  <template v-if="customActionsInjection">
175
172
  <component
176
173
  v-for="c in customActionsInjection"
@@ -185,7 +182,7 @@
185
182
 
186
183
  <template v-if="resource.options?.actions">
187
184
  <Tooltip
188
- v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
185
+ v-for="action in resource.options.actions.filter(a => a.showIn?.list || a.showIn?.listQuickIcon)"
189
186
  :key="action.id"
190
187
  >
191
188
  <component
@@ -198,12 +195,13 @@
198
195
  >
199
196
  <button
200
197
  type="button"
198
+ class="border border-gray-300 dark:border-gray-700 dark:border-opacity-0 border-opacity-0 hover:border-opacity-100 dark:hover:border-opacity-100 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
201
199
  :disabled="rowActionLoadingStates?.[action.id]"
202
200
  >
203
201
  <component
204
202
  v-if="action.icon"
205
203
  :is="getIcon(action.icon)"
206
- class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"
204
+ class="w-6 h-6 text-lightPrimary dark:text-darkPrimary"
207
205
  />
208
206
  </button>
209
207
  </component>
@@ -213,6 +211,16 @@
213
211
  </template>
214
212
  </Tooltip>
215
213
  </template>
214
+ <ListActionsThreeDots
215
+ v-if="showListActionsThreeDots"
216
+ :resourceOptions="resource?.options"
217
+ :record="row"
218
+ :updateRecords="()=>emits('update:records', true)"
219
+ :deleteRecord="deleteRecord"
220
+ :resourceId="resource.resourceId"
221
+ :startCustomAction="startCustomAction"
222
+ :customActionIconsThreeDotsMenuItems="customActionIconsThreeDotsMenuItems"
223
+ />
216
224
  </div>
217
225
 
218
226
  </td>
@@ -323,20 +331,18 @@ import { useCoreStore } from '@/stores/core';
323
331
  import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
324
332
  import SkeleteLoader from '@/components/SkeleteLoader.vue';
325
333
  import { getIcon } from '@/utils';
326
- import {
327
- IconInboxOutline,
328
- } from '@iconify-prerendered/vue-flowbite';
329
-
330
334
  import {
331
335
  IconEyeSolid,
332
336
  IconPenSolid,
333
337
  IconTrashBinSolid,
338
+ IconInboxOutline
334
339
  } from '@iconify-prerendered/vue-flowbite';
335
340
  import router from '@/router';
336
341
  import { Tooltip } from '@/afcl';
337
342
  import type { AdminForthResourceCommon, AdminForthResourceColumnInputCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
338
343
  import adminforth from '@/adminforth';
339
344
  import Checkbox from '@/afcl/Checkbox.vue';
345
+ import ListActionsThreeDots from '@/components/ListActionsThreeDots.vue';
340
346
  import CallActionWrapper from '@/components/CallActionWrapper.vue'
341
347
 
342
348
  const coreStore = useCoreStore();
@@ -352,6 +358,7 @@ const props = defineProps<{
352
358
  noRoundings?: boolean,
353
359
  customActionsInjection?: any[],
354
360
  tableBodyStartInjection?: any[],
361
+ customActionIconsThreeDotsMenuItems?: any[]
355
362
  tableRowReplaceInjection?: AdminForthComponentDeclaration,
356
363
  }>();
357
364
 
@@ -369,6 +376,12 @@ const pageInput = ref('1');
369
376
  const page = ref(1);
370
377
  const sort: Ref<Array<{field: string, direction: string}>> = ref([]);
371
378
 
379
+ const showListActionsThreeDots = computed(() => {
380
+ return props.resource?.options?.actions?.some(a => a.showIn?.listThreeDotsMenu) // show if any action is set to show in three dots menu
381
+ || (props.customActionIconsThreeDotsMenuItems && props.customActionIconsThreeDotsMenuItems.length > 0) // or if there are custom action icons for three dots menu
382
+ || !props.resource?.options.baseActionsAsQuickIcons // or if there is no baseActionsAsQuickIcons
383
+ || (props.resource?.options.baseActionsAsQuickIcons && props.resource?.options.baseActionsAsQuickIcons.length < 3) // if there all 3 base actions are shown as quick icons - hide three dots icon
384
+ })
372
385
 
373
386
  const from = computed(() => ((page.value || 1) - 1) * props.pageSize + 1);
374
387
  const to = computed(() => Math.min((page.value || 1) * props.pageSize, props.totalRows));
@@ -129,8 +129,8 @@
129
129
  />
130
130
  </td>
131
131
  <td class=" items-center px-2 md:px-3 lg:px-6 py-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
132
- <div class="flex text-lightPrimary dark:text-darkPrimary items-center">
133
- <Tooltip>
132
+ <div class="flex text-lightPrimary dark:text-darkPrimary items-center gap-2">
133
+ <Tooltip v-if="resource.options?.baseActionsAsQuickIcons && resource.options?.baseActionsAsQuickIcons.includes('show')">
134
134
  <RouterLink
135
135
  v-if="resource.options?.allowedActions?.show"
136
136
  :to="{
@@ -142,15 +142,14 @@
142
142
  }"
143
143
 
144
144
  >
145
- <IconEyeSolid class="w-5 h-5 me-2"/>
145
+ <IconEyeSolid class="af-show-icon w-5 h-5 me-2"/>
146
146
  </RouterLink>
147
147
 
148
148
  <template v-slot:tooltip>
149
149
  {{ $t('Show item') }}
150
150
  </template>
151
151
  </Tooltip>
152
-
153
- <Tooltip>
152
+ <Tooltip v-if="resource.options?.baseActionsAsQuickIcons && resource.options?.baseActionsAsQuickIcons.includes('edit')" >
154
153
  <RouterLink
155
154
  v-if="resource.options?.allowedActions?.edit"
156
155
  :to="{
@@ -161,26 +160,24 @@
161
160
  }
162
161
  }"
163
162
  >
164
- <IconPenSolid class="w-5 h-5 me-2"/>
163
+ <IconPenSolid class="af-edit-icon w-5 h-5 me-2"/>
165
164
  </RouterLink>
166
165
  <template v-slot:tooltip>
167
166
  {{ $t('Edit item') }}
168
167
  </template>
169
168
  </Tooltip>
170
-
171
- <Tooltip>
169
+ <Tooltip v-if="resource.options?.baseActionsAsQuickIcons && resource.options?.baseActionsAsQuickIcons.includes('delete')">
172
170
  <button
173
171
  v-if="resource.options?.allowedActions?.delete"
174
172
  @click="deleteRecord(row)"
175
173
  >
176
- <IconTrashBinSolid class="w-5 h-5 me-2"/>
174
+ <IconTrashBinSolid class="af-delete-icon w-5 h-5 me-2"/>
177
175
  </button>
178
176
 
179
177
  <template v-slot:tooltip>
180
178
  {{ $t('Delete item') }}
181
179
  </template>
182
- </Tooltip>
183
-
180
+ </Tooltip>
184
181
  <template v-if="customActionsInjection">
185
182
  <component
186
183
  v-for="c in customActionsInjection"
@@ -192,10 +189,9 @@
192
189
  :updateRecords="()=>emits('update:records', true)"
193
190
  />
194
191
  </template>
195
-
196
- <template v-if="resource.options?.actions">
192
+ <template v-if="resource.options?.actions">
197
193
  <Tooltip
198
- v-for="action in resource.options.actions.filter(a => a.showIn?.list)"
194
+ v-for="action in resource.options.actions.filter(a => a.showIn?.list || a.showIn?.listQuickIcon)"
199
195
  :key="action.id"
200
196
  >
201
197
  <CallActionWrapper
@@ -212,12 +208,13 @@
212
208
  >
213
209
  <button
214
210
  type="button"
211
+ class="border border-gray-300 dark:border-gray-700 dark:border-opacity-0 border-opacity-0 hover:border-opacity-100 dark:hover:border-opacity-100 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer"
215
212
  :disabled="rowActionLoadingStates?.[action.id]"
216
213
  >
217
214
  <component
218
215
  v-if="action.icon"
219
216
  :is="getIcon(action.icon)"
220
- class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"
217
+ class="w-6 h-6 text-lightPrimary dark:text-darkPrimary"
221
218
  />
222
219
  </button>
223
220
  </component>
@@ -228,6 +225,16 @@
228
225
  </template>
229
226
  </Tooltip>
230
227
  </template>
228
+ <ListActionsThreeDots
229
+ v-if="showListActionsThreeDots"
230
+ :resourceOptions="resource?.options"
231
+ :record="row"
232
+ :updateRecords="()=>emits('update:records', true)"
233
+ :deleteRecord="deleteRecord"
234
+ :resourceId="resource.resourceId"
235
+ :startCustomAction="startCustomAction"
236
+ :customActionIconsThreeDotsMenuItems="customActionIconsThreeDotsMenuItems"
237
+ />
231
238
  </div>
232
239
  </td>
233
240
  </component>
@@ -359,6 +366,7 @@ import { Tooltip } from '@/afcl';
359
366
  import type { AdminForthResourceCommon, AdminForthResourceColumnCommon, AdminForthComponentDeclaration } from '@/types/Common';
360
367
  import adminforth from '@/adminforth';
361
368
  import Checkbox from '@/afcl/Checkbox.vue';
369
+ import ListActionsThreeDots from '@/components/ListActionsThreeDots.vue';
362
370
  import CallActionWrapper from '@/components/CallActionWrapper.vue'
363
371
 
364
372
  const coreStore = useCoreStore();
@@ -377,6 +385,7 @@ const props = defineProps<{
377
385
  containerHeight?: number,
378
386
  itemHeight?: number,
379
387
  bufferSize?: number,
388
+ customActionIconsThreeDotsMenuItems?: any[]
380
389
  tableRowReplaceInjection?: AdminForthComponentDeclaration
381
390
  }>();
382
391
 
@@ -394,6 +403,12 @@ const pageInput = ref('1');
394
403
  const page = ref(1);
395
404
  const sort: Ref<Array<{field: string, direction: string}>> = ref([]);
396
405
 
406
+ const showListActionsThreeDots = computed(() => {
407
+ return props.resource?.options?.actions?.some(a => a.showIn?.listThreeDotsMenu) // show if any action is set to show in three dots menu
408
+ || (props.customActionIconsThreeDotsMenuItems && props.customActionIconsThreeDotsMenuItems.length > 0) // or if there are custom action icons for three dots menu
409
+ || !props.resource?.options.baseActionsAsQuickIcons // or if there is no baseActionsAsQuickIcons
410
+ || (props.resource?.options.baseActionsAsQuickIcons && props.resource?.options.baseActionsAsQuickIcons.length < 3) // if there all 3 base actions are shown as quick icons - hide three dots icon
411
+ })
397
412
 
398
413
  const from = computed(() => ((page.value || 1) - 1) * props.pageSize + 1);
399
414
  const to = computed(() => Math.min((page.value || 1) * props.pageSize, props.totalRows));
@@ -24,14 +24,14 @@
24
24
  dark:bg-darkShowTablesBodyBackground dark:border-darkShowTableBodyBorder block md:table-row"
25
25
  >
26
26
  <component
27
- v-if="column.components?.showRow && checkShowIf(column, record)"
27
+ v-if="column.components?.showRow && checkShowIf(column, record, resource?.columns || [])"
28
28
  :is="getCustomComponent(column.components.showRow)"
29
29
  :meta="column.components.showRow.meta"
30
30
  :column="column"
31
31
  :resource="coreStore.resource"
32
32
  :record="coreStore.record"
33
33
  />
34
- <template v-else-if="checkShowIf(column, record)">
34
+ <template v-else-if="checkShowIf(column, record, resource?.columns || [])">
35
35
  <td class="px-6 py-4 relative block md:table-cell font-bold md:font-normal pb-0 md:pb-4">
36
36
  {{ column.label }}
37
37
  </td>
@@ -22,4 +22,19 @@ const htmlContent = protectAgainstXSS(props.record[props.column.name])
22
22
  /* You can add default styles here if needed */
23
23
  word-break: break-word;
24
24
  }
25
+ .rich-text :deep(table) {
26
+ border-collapse: collapse;
27
+ border: 1px solid #ddd;
28
+ }
29
+
30
+ .rich-text :deep(table th),
31
+ .rich-text :deep(table td) {
32
+ border: 1px solid #ddd;
33
+ padding: 8px;
34
+ }
35
+
36
+ .rich-text :deep(table th) {
37
+ background-color: #f5f5f5;
38
+ font-weight: 600;
39
+ }
25
40
  </style>
@@ -15,7 +15,7 @@ export const useFiltersStore = defineStore('filters', () => {
15
15
  }
16
16
  const setFilter = (filter: any) => {
17
17
  const index = filters.value.findIndex(f => f.field === filter.field);
18
- if (filters.value[index]) {
18
+ if (filters.value[index] && filters.value[index].operator === filter.value.operator) {
19
19
  filters.value[index] = filter;
20
20
  return;
21
21
  }
@@ -1,7 +1,7 @@
1
1
  import type { Express, Request } from 'express';
2
2
  import type { Writable } from 'stream';
3
3
 
4
- import { ActionCheckSource, AdminForthFilterOperators, AdminForthSortDirections, AllowedActionsEnum,
4
+ import { ActionCheckSource, AdminForthFilterOperators, AdminForthSortDirections, AllowedActionsEnum, AdminForthResourcePages,
5
5
  type AdminForthComponentDeclaration,
6
6
  type AdminForthResourceCommon,
7
7
  type AdminUser, type AllowedActionsResolved,
@@ -12,7 +12,6 @@ import { ActionCheckSource, AdminForthFilterOperators, AdminForthSortDirections,
12
12
  type AdminForthComponentDeclarationFull,
13
13
  type AdminForthConfigMenuItem,
14
14
  type AnnouncementBadgeResponse,
15
- AdminForthResourcePages,
16
15
  type AdminForthResourceColumnInputCommon,
17
16
  } from './Common.js';
18
17
 
@@ -129,7 +128,7 @@ export interface IAdminForthSingleFilter {
129
128
  operator?: AdminForthFilterOperators.EQ | AdminForthFilterOperators.NE
130
129
  | AdminForthFilterOperators.GT | AdminForthFilterOperators.LT | AdminForthFilterOperators.GTE
131
130
  | AdminForthFilterOperators.LTE | AdminForthFilterOperators.LIKE | AdminForthFilterOperators.ILIKE
132
- | AdminForthFilterOperators.IN | AdminForthFilterOperators.NIN;
131
+ | AdminForthFilterOperators.IN | AdminForthFilterOperators.NIN | AdminForthFilterOperators.IS_EMPTY | AdminForthFilterOperators.IS_NOT_EMPTY;
133
132
  value?: any;
134
133
  rightField?: string;
135
134
  insecureRawSQL?: string;
@@ -307,7 +306,7 @@ export interface IAdminForthAuth {
307
306
 
308
307
  removeCustomCookie({response, name}: {response: any, name: string}): void;
309
308
 
310
- setCustomCookie({response, payload}: {response: any, payload: {name: string, value: string, expiry: number, expirySeconds: number, httpOnly: boolean}}): void;
309
+ setCustomCookie({response, payload}: {response: any, payload: {name: string, value: string, expiry?: number, expirySeconds: number, httpOnly: boolean}}): void;
311
310
 
312
311
  getCustomCookie({cookies, name}: {cookies: {key: string, value: string}[], name: string}): string | null;
313
312
 
@@ -469,6 +468,13 @@ export interface IAdminForthPlugin {
469
468
  instanceUniqueRepresentation(pluginOptions: any) : string;
470
469
 
471
470
 
471
+ /**
472
+ * If this method returns true, AdminForth will allow only one instance of plugin per whole app
473
+ * (only for case when we are creating copy of resource and activating plugins)
474
+ * If false, multiple instances of plugin can be installed on different resources.
475
+ */
476
+ shouldHaveSingleInstancePerWholeApp?(): boolean;
477
+
472
478
  /**
473
479
  * Optional method which will be called after AdminForth discovers all resources and their columns.
474
480
  * Can be used to validate types of columns, check if some columns are missing, etc.
@@ -848,6 +854,7 @@ export interface AdminForthActionInput {
848
854
  name: string;
849
855
  showIn?: {
850
856
  list?: boolean,
857
+ listThreeDotsMenu?: boolean,
851
858
  showButton?: boolean,
852
859
  showThreeDotsMenu?: boolean,
853
860
  };
@@ -861,6 +868,7 @@ export interface AdminForthActionInput {
861
868
  resource: AdminForthResource;
862
869
  recordId: string;
863
870
  adminUser: AdminUser;
871
+ response: IAdminForthHttpResponse;
864
872
  extra?: HttpExtra;
865
873
  tr: Function;
866
874
  }) => Promise<{
@@ -1102,7 +1110,7 @@ export interface AdminForthInputConfig {
1102
1110
  /**
1103
1111
  * Add custom page to the settings page
1104
1112
  */
1105
- userMenuSettingsPages: {
1113
+ userMenuSettingsPages?: {
1106
1114
  icon?: string,
1107
1115
  pageLabel: string,
1108
1116
  slug?: string,
@@ -1282,6 +1290,14 @@ export class Filters {
1282
1290
  subFilters,
1283
1291
  };
1284
1292
  }
1293
+
1294
+ static IS_EMPTY(field: string): IAdminForthSingleFilter {
1295
+ return { field, operator: AdminForthFilterOperators.IS_EMPTY, value: null };
1296
+ }
1297
+
1298
+ static IS_NOT_EMPTY(field: string): IAdminForthSingleFilter {
1299
+ return { field, operator: AdminForthFilterOperators.IS_NOT_EMPTY, value: null };
1300
+ }
1285
1301
  }
1286
1302
 
1287
1303
  export type FDataSort = (field: string, direction: AdminForthSortDirections) => IAdminForthSort;
@@ -1358,6 +1374,7 @@ export type AllowedActions = {
1358
1374
  */
1359
1375
  export interface ResourceOptionsInput extends Omit<NonNullable<AdminForthResourceInputCommon['options']>, 'allowedActions' | 'bulkActions'> {
1360
1376
 
1377
+ baseActionsAsQuickIcons?: ('show' | 'edit' | 'delete')[],
1361
1378
  /**
1362
1379
  * Custom bulk actions list. Bulk actions available in list view when user selects multiple records by
1363
1380
  * using checkboxes.