adminforth 1.13.0-next.9 → 1.14.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 (67) hide show
  1. package/commands/createApp/templates/adminuser.ts.hbs +9 -7
  2. package/commands/createPlugin/templates/package.json.hbs +1 -1
  3. package/dist/dataConnectors/baseConnector.d.ts +10 -14
  4. package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
  5. package/dist/dataConnectors/baseConnector.js +76 -30
  6. package/dist/dataConnectors/baseConnector.js.map +1 -1
  7. package/dist/dataConnectors/clickhouse.d.ts +14 -23
  8. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  9. package/dist/dataConnectors/clickhouse.js +68 -27
  10. package/dist/dataConnectors/clickhouse.js.map +1 -1
  11. package/dist/dataConnectors/mongo.d.ts +12 -16
  12. package/dist/dataConnectors/mongo.d.ts.map +1 -1
  13. package/dist/dataConnectors/mongo.js +15 -11
  14. package/dist/dataConnectors/mongo.js.map +1 -1
  15. package/dist/dataConnectors/mysql.d.ts +8 -13
  16. package/dist/dataConnectors/mysql.d.ts.map +1 -1
  17. package/dist/dataConnectors/mysql.js +45 -26
  18. package/dist/dataConnectors/mysql.js.map +1 -1
  19. package/dist/dataConnectors/postgres.d.ts +8 -13
  20. package/dist/dataConnectors/postgres.d.ts.map +1 -1
  21. package/dist/dataConnectors/postgres.js +45 -28
  22. package/dist/dataConnectors/postgres.js.map +1 -1
  23. package/dist/dataConnectors/sqlite.d.ts +7 -4
  24. package/dist/dataConnectors/sqlite.d.ts.map +1 -1
  25. package/dist/dataConnectors/sqlite.js +48 -22
  26. package/dist/dataConnectors/sqlite.js.map +1 -1
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +5 -7
  30. package/dist/index.js.map +1 -1
  31. package/dist/modules/configValidator.d.ts +4 -0
  32. package/dist/modules/configValidator.d.ts.map +1 -1
  33. package/dist/modules/configValidator.js +69 -9
  34. package/dist/modules/configValidator.js.map +1 -1
  35. package/dist/modules/operationalResource.d.ts +4 -4
  36. package/dist/modules/operationalResource.d.ts.map +1 -1
  37. package/dist/modules/operationalResource.js +16 -2
  38. package/dist/modules/operationalResource.js.map +1 -1
  39. package/dist/modules/restApi.d.ts.map +1 -1
  40. package/dist/modules/restApi.js +163 -57
  41. package/dist/modules/restApi.js.map +1 -1
  42. package/dist/spa/src/afcl/Dialog.vue +13 -0
  43. package/dist/spa/src/afcl/Select.vue +27 -2
  44. package/dist/spa/src/components/ColumnValueInput.vue +12 -5
  45. package/dist/spa/src/components/ColumnValueInputWrapper.vue +77 -0
  46. package/dist/spa/src/components/Filters.vue +16 -8
  47. package/dist/spa/src/components/GroupsTable.vue +15 -50
  48. package/dist/spa/src/components/ResourceForm.vue +19 -6
  49. package/dist/spa/src/components/ResourceListTable.vue +39 -7
  50. package/dist/spa/src/components/ShowTable.vue +11 -3
  51. package/dist/spa/src/components/SkeleteLoader.vue +11 -3
  52. package/dist/spa/src/components/ThreeDotsMenu.vue +17 -1
  53. package/dist/spa/src/components/ValueRenderer.vue +18 -2
  54. package/dist/spa/src/types/Back.ts +67 -23
  55. package/dist/spa/src/types/Common.ts +26 -2
  56. package/dist/spa/src/views/EditView.vue +6 -2
  57. package/dist/spa/src/views/ListView.vue +5 -2
  58. package/dist/spa/src/views/ShowView.vue +17 -1
  59. package/dist/types/Back.d.ts +59 -22
  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 +26 -4
  64. package/dist/types/Common.d.ts.map +1 -1
  65. package/dist/types/Common.js +3 -0
  66. package/dist/types/Common.js.map +1 -1
  67. package/package.json +1 -1
@@ -34,7 +34,7 @@
34
34
  </div>
35
35
  <teleport to="body" v-if="teleportToBody && showDropdown">
36
36
  <div ref="dropdownEl" :style="getDropdownPosition" :class="{'shadow-none': isTop}"
37
- class="fixed z-50 w-full bg-white shadow-lg dark:shadow-black dark:bg-gray-700
37
+ class="fixed z-[5] w-full bg-white shadow-lg dark:shadow-black dark:bg-gray-700
38
38
  dark:border-gray-600 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm max-h-48">
39
39
  <div
40
40
  v-for="item in filteredItems"
@@ -202,6 +202,23 @@ watch(
202
202
  }
203
203
  );
204
204
 
205
+ const handleScroll = () => {
206
+ if (showDropdown.value && inputEl.value) {
207
+ const rect = inputEl.value.getBoundingClientRect();
208
+ const style = {
209
+ left: `${rect.left}px`,
210
+ top: isTop.value && dropdownHeight.value
211
+ ? `${rect.top - dropdownHeight.value - 8}px`
212
+ : `${rect.bottom + 8}px`,
213
+ width: `${rect.width}px`
214
+ };
215
+
216
+ if (dropdownEl.value) {
217
+ Object.assign(dropdownEl.value.style, style);
218
+ }
219
+ }
220
+ };
221
+
205
222
  onMounted(() => {
206
223
  updateFromProps();
207
224
 
@@ -214,7 +231,11 @@ onMounted(() => {
214
231
  });
215
232
 
216
233
  addClickListener();
217
-
234
+
235
+ // Add scroll listeners if teleportToBody is true
236
+ if (props.teleportToBody) {
237
+ window.addEventListener('scroll', handleScroll, true);
238
+ }
218
239
  });
219
240
 
220
241
  const filteredItems = computed(() => {
@@ -268,6 +289,10 @@ const toogleItem = (item) => {
268
289
 
269
290
  onUnmounted(() => {
270
291
  removeClickListener();
292
+ // Remove scroll listeners if teleportToBody is true
293
+ if (props.teleportToBody) {
294
+ window.removeEventListener('scroll', handleScroll, true);
295
+ }
271
296
  });
272
297
 
273
298
  const getDropdownPosition = computed(() => {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="flex">
2
+ <div class="flex" :class="{ 'opacity-50' : column.editReadonly && source === 'edit' }">
3
3
  <component
4
4
  v-if="column?.components?.[props.source]?.file"
5
5
  :is="getCustomComponent(column.components[props.source])"
@@ -16,8 +16,9 @@
16
16
  <Select
17
17
  v-else-if="column.foreignResource"
18
18
  ref="input"
19
- class="w-full"
19
+ class="w-full min-w-24"
20
20
  :options="columnOptions[column.name] || []"
21
+ teleportToBody
21
22
  :placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
22
23
  :modelValue="value"
23
24
  :readonly="column.editReadonly && source === 'edit'"
@@ -26,8 +27,9 @@
26
27
  <Select
27
28
  v-else-if="column.enum"
28
29
  ref="input"
29
- class="w-full"
30
+ class="w-full min-w-24"
30
31
  :options="column.enum"
32
+ teleportToBody
31
33
  :modelValue="value"
32
34
  :readonly="column.editReadonly && source === 'edit'"
33
35
  @update:modelValue="$emit('update:modelValue', $event)"
@@ -35,8 +37,9 @@
35
37
  <Select
36
38
  v-else-if="(type || column.type) === 'boolean'"
37
39
  ref="input"
38
- class="w-full"
40
+ class="w-full min-w-24"
39
41
  :options="getBooleanOptions(column)"
42
+ teleportToBody
40
43
  :modelValue="value"
41
44
  :readonly="column.editReadonly && source === 'edit'"
42
45
  @update:modelValue="$emit('update:modelValue', $event)"
@@ -48,6 +51,8 @@
48
51
  step="1"
49
52
  class="w-40"
50
53
  placeholder="0"
54
+ :min="![undefined, null].includes(column.minValue) ? column.minValue : ''"
55
+ :max="![undefined, null].includes(column.maxValue) ? column.maxValue : ''"
51
56
  :prefix="column.inputPrefix"
52
57
  :suffix="column.inputSuffix"
53
58
  :readonly="column.editReadonly && source === 'edit'"
@@ -70,6 +75,8 @@
70
75
  step="0.1"
71
76
  class="w-40"
72
77
  placeholder="0.0"
78
+ :min="![undefined, null].includes(column.minValue) ? column.minValue : ''"
79
+ :max="![undefined, null].includes(column.maxValue) ? column.maxValue : ''"
73
80
  :prefix="column.inputPrefix"
74
81
  :suffix="column.inputSuffix"
75
82
  :modelValue="value"
@@ -168,7 +175,7 @@
168
175
  { label: t('Yes'), value: true },
169
176
  { label: t('No'), value: false },
170
177
  ];
171
- if (!column.required[props.mode]) {
178
+ if (!column.required[props.mode] && !column.isArray?.enabled) {
172
179
  options.push({ label: t('Unset'), value: null });
173
180
  }
174
181
  return options;
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <template v-if="column.isArray?.enabled">
3
+ <div class="flex flex-col">
4
+ <ColumnValueInput
5
+ v-for="(arrayItemValue, arrayItemIndex) in currentValues[column.name]"
6
+ :key="`${column.name}-${arrayItemIndex}`"
7
+ ref="arrayItemRefs"
8
+ :class="{'mt-2': arrayItemIndex}"
9
+ :source="source"
10
+ :column="column"
11
+ :type="column.isArray.itemType"
12
+ :value="arrayItemValue"
13
+ :currentValues="currentValues"
14
+ :mode="mode"
15
+ :columnOptions="columnOptions"
16
+ :deletable="!column.editReadonly"
17
+ @update:modelValue="setCurrentValue(column.name, $event, arrayItemIndex)"
18
+ @update:unmasked="$emit('update:unmasked', column.name)"
19
+ @update:inValidity="$emit('update:inValidity', { name: column.name, value: $event })"
20
+ @update:emptiness="$emit('update:emptiness', { name: column.name, value: $event })"
21
+ @delete="setCurrentValue(column.name, currentValues[column.name].filter((_, index) => index !== arrayItemIndex))"
22
+ />
23
+ </div>
24
+ <div class="flex items-center">
25
+ <button
26
+ v-if="!column.editReadonly"
27
+ type="button"
28
+ @click="addArrayItem"
29
+ class="flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
30
+ :class="{'mt-2': currentValues[column.name].length}"
31
+ >
32
+ <IconPlusOutline class="w-4 h-4 me-2"/>
33
+ {{ $t('Add') }}
34
+ </button>
35
+ </div>
36
+ </template>
37
+ <ColumnValueInput
38
+ v-else
39
+ :source="source"
40
+ :column="column"
41
+ :value="currentValues[column.name]"
42
+ :currentValues="currentValues"
43
+ :mode="mode"
44
+ :columnOptions="columnOptions"
45
+ :unmasked="unmasked"
46
+ @update:modelValue="setCurrentValue(column.name, $event)"
47
+ @update:unmasked="$emit('update:unmasked', column.name)"
48
+ @update:inValidity="$emit('update:inValidity', { name: column.name, value: $event })"
49
+ @update:emptiness="$emit('update:emptiness', { name: column.name, value: $event })"
50
+ />
51
+ </template>
52
+
53
+ <script setup lang="ts">
54
+ import { IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
55
+ import ColumnValueInput from "./ColumnValueInput.vue";
56
+ import { ref, nextTick } from 'vue';
57
+
58
+ const props = defineProps<{
59
+ source: 'create' | 'edit',
60
+ column: any,
61
+ currentValues: any,
62
+ mode: string,
63
+ columnOptions: any,
64
+ unmasked: any,
65
+ setCurrentValue: Function
66
+ }>();
67
+
68
+ const emit = defineEmits(['update:unmasked', 'update:inValidity', 'update:emptiness', 'focus-last-input']);
69
+
70
+ const arrayItemRefs = ref([]);
71
+
72
+ async function addArrayItem() {
73
+ props.setCurrentValue(props.column.name, props.currentValues[props.column.name], props.currentValues[props.column.name].length);
74
+ await nextTick();
75
+ arrayItemRefs.value[arrayItemRefs.value.length - 1].focus();
76
+ }
77
+ </script>
@@ -24,11 +24,11 @@
24
24
 
25
25
  <Select
26
26
  v-if="c.foreignResource"
27
- multiple
27
+ :multiple="c.filterOptions.multiselect"
28
28
  class="w-full"
29
29
  :options="columnOptions[c.name] || []"
30
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
31
- :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
30
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
31
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || []"
32
32
  />
33
33
  <Select
34
34
  multiple
@@ -44,13 +44,13 @@
44
44
  :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
45
45
  />
46
46
 
47
- <Select
48
- multiple
47
+ <Select
48
+ :multiple="c.filterOptions.multiselect"
49
49
  class="w-full"
50
50
  v-else-if="c.enum"
51
51
  :options="c.enum"
52
- @update:modelValue="onFilterInput[c.name]({ column: c, operator: 'in', value: $event.length ? $event : undefined })"
53
- :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
52
+ @update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
53
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || []"
54
54
  />
55
55
 
56
56
  <Input
@@ -118,7 +118,8 @@
118
118
  </template>
119
119
 
120
120
  <script setup>
121
- import { watch, computed } from 'vue'
121
+ import { watch, computed } from 'vue';
122
+ import { useI18n } from 'vue-i18n';
122
123
  import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
123
124
  import { callAdminForthApi } from '@/utils';
124
125
  import { useRouter } from 'vue-router';
@@ -130,6 +131,7 @@ import Select from '@/afcl/Select.vue';
130
131
  import debounce from 'debounce';
131
132
 
132
133
  const filtersStore = useFiltersStore();
134
+ const { t } = useI18n();
133
135
 
134
136
 
135
137
  // props: columns
@@ -163,6 +165,12 @@ const columnOptions = computedAsync(async () => {
163
165
  },
164
166
  });
165
167
  ret[column.name] = list.items;
168
+ if (!column.filterOptions.multiselect) {
169
+ ret[column.name].push({
170
+ label: t('Unset'),
171
+ value: '',
172
+ });
173
+ }
166
174
  }
167
175
  })
168
176
  );
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl">
3
- <div v-if="group.groupName" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
3
+ <div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
4
4
  {{ group.groupName }}
5
5
  </div>
6
6
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
7
- <thead class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group ">
7
+ <thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group ">
8
8
  <tr>
9
9
  <th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-6 py-3 hidden md:w-52 md:table-cell">
10
10
  {{ $t('Field') }}
@@ -42,50 +42,17 @@
42
42
  class="px-6 py-4 whitespace-pre-wrap relative block md:table-cell"
43
43
  :class="{'rounded-br-lg': i === group.columns.length - 1}"
44
44
  >
45
- <template v-if="column.isArray?.enabled">
46
- <ColumnValueInput
47
- v-for="(arrayItemValue, arrayItemIndex) in currentValues[column.name]"
48
- :key="`${column.name}-${arrayItemIndex}`"
49
- ref="arrayItemRefs"
50
- :class="{'mt-2': arrayItemIndex}"
51
- :source="source"
52
- :column="column"
53
- :type="column.isArray.itemType"
54
- :value="arrayItemValue"
55
- :currentValues="currentValues"
56
- :mode="mode"
57
- :columnOptions="columnOptions"
58
- :deletable="!column.editReadonly"
59
- @update:modelValue="setCurrentValue(column.name, $event, arrayItemIndex)"
60
- @update:unmasked="unmasked[column.name] = !unmasked[column.name]"
61
- @update:inValidity="customComponentsInValidity[column.name] = $event"
62
- @update:emptiness="customComponentsEmptiness[column.name] = $event"
63
- @delete="setCurrentValue(column.name, currentValues[column.name].filter((_, index) => index !== arrayItemIndex))"
64
- />
65
- <button
66
- v-if="!column.editReadonly"
67
- type="button"
68
- @click="setCurrentValue(column.name, currentValues[column.name], currentValues[column.name].length); focusOnLastInput(column.name)"
69
- class="flex items-center py-1 px-3 me-2 text-sm font-medium rounded-default text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
70
- :class="{'mt-2': currentValues[column.name].length}"
71
- >
72
- <IconPlusOutline class="w-4 h-4 me-2"/>
73
- {{ $t('Add') }}
74
- </button>
75
- </template>
76
- <ColumnValueInput
77
- v-else
45
+ <ColumnValueInputWrapper
78
46
  :source="source"
79
47
  :column="column"
80
- :value="currentValues[column.name]"
81
48
  :currentValues="currentValues"
82
49
  :mode="mode"
83
50
  :columnOptions="columnOptions"
84
51
  :unmasked="unmasked"
85
- @update:modelValue="setCurrentValue(column.name, $event)"
86
- @update:unmasked="unmasked[column.name] = !unmasked[column.name]"
87
- @update:inValidity="customComponentsInValidity[column.name] = $event"
88
- @update:emptiness="customComponentsEmptiness[column.name] = $event"
52
+ :setCurrentValue="setCurrentValue"
53
+ @update:unmasked="unmasked[$event] = !unmasked[$event]"
54
+ @update:inValidity="customComponentsInValidity[$event.name] = $event.value"
55
+ @update:emptiness="customComponentsEmptiness[$event.name] = $event.value"
89
56
  />
90
57
  <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
91
58
  <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
@@ -98,10 +65,10 @@
98
65
 
99
66
  <script setup lang="ts">
100
67
  import { IconExclamationCircleSolid, IconPlusOutline } from '@iconify-prerendered/vue-flowbite';
101
- import ColumnValueInput from "@/components/ColumnValueInput.vue";
102
68
  import { Tooltip } from '@/afcl';
103
69
  import { ref, computed, watch, nextTick, type Ref } from 'vue';
104
70
  import { useI18n } from 'vue-i18n';
71
+ import ColumnValueInputWrapper from "@/components/ColumnValueInputWrapper.vue";
105
72
 
106
73
  const { t } = useI18n();
107
74
 
@@ -117,19 +84,17 @@
117
84
  columnOptions: any,
118
85
  }>();
119
86
 
120
- const arrayItemRefs = ref([]);
121
-
122
87
  const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
123
88
  const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
89
+ const allColumnsHaveCustomComponent = computed(() => {
90
+ return props.group.columns.every(column => {
91
+ const componentKey = `${props.source}Row` as keyof typeof column.components;
92
+ return column.components?.[componentKey];
93
+ });
94
+ });
124
95
 
125
96
  const emit = defineEmits(['update:customComponentsInValidity', 'update:customComponentsEmptiness']);
126
97
 
127
- async function focusOnLastInput(column) {
128
- // wait for element to register
129
- await nextTick();
130
- arrayItemRefs.value[arrayItemRefs.value.length - 1].focus();
131
- }
132
-
133
98
  watch(customComponentsInValidity.value, (newVal) => {
134
99
  emit('update:customComponentsInValidity', newVal);
135
100
  });
@@ -138,4 +103,4 @@
138
103
  emit('update:customComponentsEmptiness', newVal);
139
104
  });
140
105
 
141
- </script>
106
+ </script>
@@ -190,8 +190,12 @@ const setCurrentValue = (key, value, index=null) => {
190
190
  } else if (index === currentValues.value[key].length) {
191
191
  currentValues.value[key].push(null);
192
192
  } else {
193
- if (['integer', 'float'].includes(col.isArray.itemType) && (value || value === 0)) {
194
- currentValues.value[key][index] = +value;
193
+ if (['integer', 'float', 'decimal'].includes(col.isArray.itemType)) {
194
+ if (value || value === 0) {
195
+ currentValues.value[key][index] = +value;
196
+ } else {
197
+ currentValues.value[key][index] = null;
198
+ }
195
199
  } else {
196
200
  currentValues.value[key][index] = value;
197
201
  }
@@ -200,8 +204,12 @@ const setCurrentValue = (key, value, index=null) => {
200
204
  }
201
205
  }
202
206
  } else {
203
- if (['integer', 'float'].includes(col.type) && (value || value === 0)) {
204
- currentValues.value[key] = +value;
207
+ if (['integer', 'float', 'decimal'].includes(col.type)) {
208
+ if (value || value === 0) {
209
+ currentValues.value[key] = +value;
210
+ } else {
211
+ currentValues.value[key] = null;
212
+ }
205
213
  } else {
206
214
  currentValues.value[key] = value;
207
215
  }
@@ -237,7 +245,12 @@ onMounted(() => {
237
245
  currentValues.value[column.name] = [];
238
246
  } else {
239
247
  // else copy array to prevent mutation
240
- currentValues.value[column.name] = [...currentValues.value[column.name]];
248
+ if (Array.isArray(currentValues.value[column.name])) {
249
+ currentValues.value[column.name] = [...currentValues.value[column.name]];
250
+ } else {
251
+ // fallback for old data
252
+ currentValues.value[column.name] = [`${currentValues.value[column.name]}`];
253
+ }
241
254
  }
242
255
  } else if (currentValues.value[column.name]) {
243
256
  currentValues.value[column.name] = JSON.stringify(currentValues.value[column.name], null, 2);
@@ -263,7 +276,7 @@ const columnOptions = computedAsync(async () => {
263
276
  },
264
277
  });
265
278
 
266
- if (!column.required[props.source]) list.items.push({ value: null, label: column.foreignResource.unsetLabel });
279
+ if (!column.required[props.source] && !column.isArray?.enabled) list.items.push({ value: null, label: column.foreignResource.unsetLabel });
267
280
 
268
281
  return { [column.name]: list.items };
269
282
  }
@@ -14,19 +14,19 @@
14
14
  <table v-else class=" w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-default">
15
15
 
16
16
  <tbody>
17
-
18
17
  <!-- table header -->
19
18
  <tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
20
19
  <td scope="col" class="p-4">
21
- <div v-if="rows && rows.length" class="flex items-center">
20
+ <div class="flex items-center">
22
21
  <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
23
- class="w-4 h-4 cursor-pointer text-blue-600 bg-gray-100 border-gray-300 rounded
22
+ :disabled="!rows || !rows.length"
23
+ class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded
24
24
  focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
25
25
  <label for="checkbox-all-search" class="sr-only">{{ $t('checkbox') }}</label>
26
26
  </div>
27
27
  </td>
28
28
 
29
- <td v-for="c in columnsListed" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
29
+ <td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
30
30
 
31
31
  <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
32
32
  class="flex items-center " :class="{'cursor-pointer':c.sortable}">
@@ -59,13 +59,18 @@
59
59
  {{ $t('Actions') }}
60
60
  </td>
61
61
  </tr>
62
+ <tr v-for="c in tableBodyStartInjection" :key="c.id" class="align-top border-b border-lightListBorder dark:border-darkListTableBorder">
63
+ <component :is="getCustomComponent(c)" :meta="c.meta" :resource="resource" :adminUser="coreStore.adminUser" />
64
+ </tr>
62
65
  <!-- table header end -->
63
66
  <SkeleteLoader
64
67
  v-if="!rows"
65
68
  :columns="resource?.columns.filter(c => c.showIn.list).length + 2"
66
69
  :rows="rowHeights.length || 3"
67
70
  :row-heights="rowHeights"
71
+ :column-widths="columnWidths"
68
72
  />
73
+
69
74
  <tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
70
75
  <td :colspan="resource?.columns.length + 2">
71
76
 
@@ -178,7 +183,7 @@
178
183
  <button
179
184
  @click="startCustomAction(action.id, row)"
180
185
  >
181
- <component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary"></component>
186
+ <component v-if="action.icon" :is="getIcon(action.icon)" class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"></component>
182
187
  </button>
183
188
  <template v-slot:tooltip>
184
189
  {{ action.name }}
@@ -320,6 +325,7 @@ const props = defineProps<{
320
325
  sort: any[],
321
326
  noRoundings?: boolean,
322
327
  customActionsInjection?: any[],
328
+ tableBodyStartInjection?: any[],
323
329
  }>();
324
330
 
325
331
  // emits, update page
@@ -376,10 +382,13 @@ watch(() => props.page, (newPage) => {
376
382
  });
377
383
 
378
384
  const rowRefs = useTemplateRef('rowRefs');
385
+ const headerRefs = useTemplateRef('headerRefs');
379
386
  const rowHeights = ref([]);
387
+ const columnWidths = ref([]);
380
388
  watch(() => props.rows, (newRows) => {
381
389
  // rows are set to null when new records are loading
382
390
  rowHeights.value = newRows || !rowRefs.value ? [] : rowRefs.value.map((el) => el.offsetHeight);
391
+ columnWidths.value = newRows || !headerRefs.value ? [] : [48, ...headerRefs.value.map((el) => el.offsetWidth)];
383
392
  });
384
393
 
385
394
  function addToCheckedValues(id) {
@@ -411,7 +420,7 @@ async function selectAll(value) {
411
420
  const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
412
421
 
413
422
  const allFromThisPageChecked = computed(() => {
414
- if (!props.rows) return false;
423
+ if (!props.rows || !props.rows.length) return false;
415
424
  return props.rows.every((r) => checkboxesInternal.value.includes(r._primaryKeyValue));
416
425
  });
417
426
  const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
@@ -536,6 +545,20 @@ async function startCustomAction(actionId, row) {
536
545
 
537
546
  actionLoadingStates.value[actionId] = false;
538
547
 
548
+ if (data?.redirectUrl) {
549
+ // Check if the URL should open in a new tab
550
+ if (data.redirectUrl.includes('target=_blank')) {
551
+ window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
552
+ } else {
553
+ // Navigate within the app
554
+ if (data.redirectUrl.startsWith('http')) {
555
+ window.location.href = data.redirectUrl;
556
+ } else {
557
+ router.push(data.redirectUrl);
558
+ }
559
+ }
560
+ return;
561
+ }
539
562
  if (data?.ok) {
540
563
  emits('update:records', true);
541
564
 
@@ -553,4 +576,13 @@ async function startCustomAction(actionId, row) {
553
576
  }
554
577
 
555
578
 
556
- </script>
579
+ </script>
580
+
581
+ <style lang="scss" scoped>
582
+ input[type="checkbox"][disabled] {
583
+ @apply opacity-50;
584
+ }
585
+ input[type="checkbox"]:not([disabled]) {
586
+ @apply cursor-pointer;
587
+ }
588
+ </style>
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <div class="overflow-x-auto rounded-default shadow-resourseFormShadow dark:shadow-darkResourseFormShadow">
3
- <div v-if="groupName" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
3
+ <div v-if="groupName && !noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-gray-600 text-gray-700 bg-lightFormHeading dark:bg-gray-700 dark:text-gray-400 rounded-t-lg">
4
4
  {{ groupName }}
5
5
  </div>
6
6
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 table-fixed">
7
- <thead class="text-gray-700 dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group">
7
+ <thead v-if="!allColumnsHaveCustomComponent" class="text-gray-700 dark:text-gray-400 bg-lightFormHeading dark:bg-gray-700 block md:table-row-group">
8
8
  <tr>
9
9
  <th scope="col" class="px-6 py-3 text-xs uppercase hidden md:w-52 md:table-cell">
10
10
  {{ $t('Field') }}
@@ -59,7 +59,8 @@
59
59
  import ValueRenderer from '@/components/ValueRenderer.vue';
60
60
  import { getCustomComponent } from '@/utils';
61
61
  import { useCoreStore } from '@/stores/core';
62
- defineProps<{
62
+ import { computed } from 'vue';
63
+ const props = defineProps<{
63
64
  columns: Array<{
64
65
  name: string;
65
66
  label: string;
@@ -74,10 +75,17 @@
74
75
  };
75
76
  };
76
77
  }>;
78
+ source: string;
77
79
  groupName?: string | null;
80
+ noTitle?: boolean;
78
81
  resource: Record<string, any>;
79
82
  record: Record<string, any>;
80
83
  }>();
81
84
 
82
85
  const coreStore = useCoreStore();
86
+ const allColumnsHaveCustomComponent = computed(() => {
87
+ return props.columns.every(column => {
88
+ return column.components?.showRow;
89
+ });
90
+ });
83
91
  </script>
@@ -1,10 +1,16 @@
1
1
  <template>
2
2
  <tr
3
3
  v-for="(r, ri) in new Array(props.rows)"
4
- class="bg-lightListTable border-b dark:bg-darkListTable dark:border-darkListBorder"
5
- :style="[props.rowHeights[ri] !== undefined ? `height: ${props.rowHeights[ri]}px` : '' ]"
4
+ class="bg-lightListTable dark:bg-darkListTable dark:border-darkListBorder"
5
+ :class="{'border-b': ri !== props.rows - 1}"
6
+ :style="[`height: ${props.rowHeights[ri] !== undefined ? props.rowHeights[ri] : 52.5}px`]"
6
7
  >
7
- <td v-for="c in new Array(props.columns)" class="items-center px-6 py-8 cursor-default" >
8
+ <td
9
+ v-for="(c, ci) in new Array(props.columns)" class="items-center px-6 py-4 cursor-default"
10
+ :style="[props.columnWidths[ci] !== undefined
11
+ ? `min-width: ${props.columnWidths[ci]}px; width: ${props.columnWidths[ci]}px;`
12
+ : '']"
13
+ >
8
14
  <div role="status" class="max-w-sm animate-pulse">
9
15
  <div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
10
16
  </div>
@@ -18,8 +24,10 @@ const props = withDefaults(defineProps<{
18
24
  columns: number;
19
25
  rows: number;
20
26
  rowHeights?: number[];
27
+ columnWidths?: number[];
21
28
  }>(), {
22
29
  rowHeights: [],
30
+ columnWidths: [],
23
31
  });
24
32
 
25
33
  </script>
@@ -46,10 +46,11 @@ import { getCustomComponent, getIcon } from '@/utils';
46
46
  import { useCoreStore } from '@/stores/core';
47
47
  import adminforth from '@/adminforth';
48
48
  import { callAdminForthApi } from '@/utils';
49
- import { useRoute } from 'vue-router';
49
+ import { useRoute, useRouter } from 'vue-router';
50
50
 
51
51
  const route = useRoute();
52
52
  const coreStore = useCoreStore();
53
+ const router = useRouter();
53
54
 
54
55
  const props = defineProps({
55
56
  threeDotsDropdownItems: Array,
@@ -69,6 +70,21 @@ async function handleActionClick(action) {
69
70
  recordId: route.params.primaryKey
70
71
  }
71
72
  });
73
+
74
+ if (data?.redirectUrl) {
75
+ // Check if the URL should open in a new tab
76
+ if (data.redirectUrl.includes('target=_blank')) {
77
+ window.open(data.redirectUrl.replace('&target=_blank', '').replace('?target=_blank', ''), '_blank');
78
+ } else {
79
+ // Navigate within the app
80
+ if (data.redirectUrl.startsWith('http')) {
81
+ window.location.href = data.redirectUrl;
82
+ } else {
83
+ router.push(data.redirectUrl);
84
+ }
85
+ }
86
+ return;
87
+ }
72
88
 
73
89
  if (data?.ok) {
74
90
  await coreStore.fetchRecord({