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.
- package/commands/createCustomComponent/main.js +0 -3
- package/commands/createPlugin/templates/index.ts.hbs +4 -0
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +1 -2
- package/dist/auth.js.map +1 -1
- package/dist/basePlugin.d.ts +1 -0
- package/dist/basePlugin.d.ts.map +1 -1
- package/dist/basePlugin.js +3 -0
- package/dist/basePlugin.js.map +1 -1
- package/dist/dataConnectors/baseConnector.d.ts +1 -0
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +92 -6
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts +2 -0
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +11 -1
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts +8 -1
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +67 -24
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/dataConnectors/mysql.d.ts +2 -0
- package/dist/dataConnectors/mysql.d.ts.map +1 -1
- package/dist/dataConnectors/mysql.js +12 -2
- package/dist/dataConnectors/mysql.js.map +1 -1
- package/dist/dataConnectors/postgres.d.ts +2 -0
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +12 -2
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/dataConnectors/sqlite.d.ts +2 -0
- package/dist/dataConnectors/sqlite.d.ts.map +1 -1
- package/dist/dataConnectors/sqlite.js +12 -2
- package/dist/dataConnectors/sqlite.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +9 -7
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +3 -3
- package/dist/modules/restApi.js.map +1 -1
- package/dist/spa/src/adminforth.ts +52 -0
- package/dist/spa/src/afcl/Tooltip.vue +38 -4
- package/dist/spa/src/components/ColumnValueInput.vue +14 -1
- package/dist/spa/src/components/CustomRangePicker.vue +9 -4
- package/dist/spa/src/components/Filters.vue +4 -4
- package/dist/spa/src/components/ListActionsThreeDots.vue +235 -0
- package/dist/spa/src/components/ResourceForm.vue +5 -5
- package/dist/spa/src/components/ResourceListTable.vue +26 -13
- package/dist/spa/src/components/ResourceListTableVirtual.vue +30 -15
- package/dist/spa/src/components/ShowTable.vue +2 -2
- package/dist/spa/src/renderers/RichText.vue +15 -0
- package/dist/spa/src/stores/filters.ts +1 -1
- package/dist/spa/src/types/Back.ts +22 -5
- package/dist/spa/src/types/Common.ts +3 -10
- package/dist/spa/src/utils.ts +8 -3
- package/dist/spa/src/views/CreateView.vue +22 -30
- package/dist/spa/src/views/EditView.vue +25 -29
- package/dist/spa/src/views/ListView.vue +12 -2
- package/dist/types/Back.d.ts +15 -4
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js +6 -0
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +4 -11
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js +2 -0
- package/dist/types/Common.js.map +1 -1
- package/package.json +1 -1
- 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'
|
|
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'
|
|
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-
|
|
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-
|
|
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
|
|
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.
|