adminforth 1.1.65 → 1.1.66

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 (58) hide show
  1. package/dataConnectors/mongo.ts +7 -13
  2. package/dataConnectors/postgres.ts +2 -2
  3. package/dataConnectors/sqlite.ts +2 -2
  4. package/dist/dataConnectors/mongo.js +1 -9
  5. package/dist/index.js +1 -3
  6. package/dist/plugins/AuditLogPlugin/custom/AuditLogView.vue +7 -6
  7. package/dist/plugins/AuditLogPlugin/index.js +18 -13
  8. package/dist/spa/index.html +2 -2
  9. package/dist/spa/package-lock.json +17 -4
  10. package/dist/spa/package.json +2 -1
  11. package/dist/spa/spa/src/App.vue +24 -12
  12. package/dist/spa/spa/src/composables/useStores.ts +16 -4
  13. package/dist/spa/spa/src/main.ts +1 -1
  14. package/dist/spa/src/App.vue +125 -45
  15. package/dist/spa/src/assets/logo.svg +19 -1
  16. package/dist/spa/src/components/AcceptModal.vue +2 -9
  17. package/dist/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
  18. package/dist/spa/src/components/CustomDatePicker.vue +16 -3
  19. package/dist/spa/src/components/CustomDateRangePicker.vue +11 -2
  20. package/dist/spa/src/components/Dropdown.vue +6 -1
  21. package/dist/spa/src/components/Filters.vue +40 -19
  22. package/dist/spa/src/components/ResourceForm.vue +26 -24
  23. package/dist/spa/src/components/ResourceListTable.vue +419 -0
  24. package/dist/spa/src/components/Toast.vue +66 -0
  25. package/dist/spa/src/components/ValueRenderer.vue +14 -8
  26. package/dist/spa/src/composables/useFrontendApi.ts +26 -0
  27. package/dist/spa/src/composables/useStores.ts +113 -0
  28. package/dist/spa/src/main.ts +1 -1
  29. package/dist/spa/src/router/index.ts +30 -20
  30. package/dist/spa/src/spa_types/core.ts +51 -0
  31. package/dist/spa/src/stores/core.ts +48 -31
  32. package/dist/spa/src/stores/filters.ts +22 -0
  33. package/dist/spa/src/stores/modal.ts +13 -3
  34. package/dist/spa/src/stores/toast.ts +15 -0
  35. package/dist/spa/src/stores/user.ts +54 -0
  36. package/dist/spa/src/utils.ts +62 -7
  37. package/dist/spa/src/views/CreateView.vue +67 -15
  38. package/dist/spa/src/views/EditView.vue +45 -11
  39. package/dist/spa/src/views/ListView.vue +82 -366
  40. package/dist/spa/src/views/LoginView.vue +16 -4
  41. package/dist/spa/src/views/ShowView.vue +105 -21
  42. package/dist/spa/tailwind.config.js +1 -1
  43. package/dist/spa/vite.config.ts +6 -0
  44. package/index.ts +4 -5
  45. package/package.json +1 -1
  46. package/plugins/AuditLogPlugin/custom/AuditLogView.vue +7 -6
  47. package/plugins/AuditLogPlugin/index.ts +23 -15
  48. package/spa/src/App.vue +24 -12
  49. package/spa/src/composables/useStores.ts +16 -4
  50. package/spa/src/main.ts +1 -1
  51. package/types/AdminForthConfig.ts +16 -3
  52. package/types/FrontendAPI.ts +1 -15
  53. package/dist/plugins/AccessControl/index.js +0 -66
  54. package/dist/plugins/AccessControl/types.js +0 -1
  55. package/dist/spa/public/favicon.ico +0 -0
  56. package/dist/spa/spa/public/favicon.ico +0 -0
  57. package/dist/types.js +0 -30
  58. /package/dist/spa/{spa/public → public/assets}/favicon.png +0 -0
@@ -1 +1,19 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
1
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g clip-path="url(#clip0_1_8)">
3
+ <path d="M8.034 6.006V13H6.097V12.194C5.59433 12.8007 4.86633 13.104 3.913 13.104C3.25433 13.104 2.65633 12.9567 2.119 12.662C1.59033 12.3673 1.17433 11.947 0.871 11.401C0.567667 10.855 0.416 10.2223 0.416 9.503C0.416 8.78367 0.567667 8.151 0.871 7.605C1.17433 7.059 1.59033 6.63867 2.119 6.344C2.65633 6.04933 3.25433 5.902 3.913 5.902C4.80567 5.902 5.50333 6.18367 6.006 6.747V6.006H8.034ZM4.264 11.44C4.77533 11.44 5.2 11.2667 5.538 10.92C5.876 10.5647 6.045 10.0923 6.045 9.503C6.045 8.91367 5.876 8.44567 5.538 8.099C5.2 7.74367 4.77533 7.566 4.264 7.566C3.744 7.566 3.315 7.74367 2.977 8.099C2.639 8.44567 2.47 8.91367 2.47 9.503C2.47 10.0923 2.639 10.5647 2.977 10.92C3.315 11.2667 3.744 11.44 4.264 11.44Z" fill="url(#paint0_linear_1_8)"/>
4
+ <path d="M13.317 5.538C12.589 5.538 12.0387 5.69833 11.666 6.019C11.2933 6.331 11.107 6.80333 11.107 7.436V7.995H14.851V9.685H11.107V13H9.001V7.449C9.001 6.279 9.365 5.369 10.093 4.719C10.8297 4.069 11.8567 3.744 13.174 3.744C13.694 3.744 14.1837 3.80033 14.643 3.913C15.1023 4.017 15.501 4.173 15.839 4.381L15.189 6.045C14.669 5.707 14.045 5.538 13.317 5.538Z" fill="url(#paint1_linear_1_8)"/>
5
+ </g>
6
+ <defs>
7
+ <linearGradient id="paint0_linear_1_8" x1="1.5" y1="6.93333" x2="7.31883" y2="11.8926" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#656E7A"/>
9
+ <stop offset="1" stop-color="#99A6B8"/>
10
+ </linearGradient>
11
+ <linearGradient id="paint1_linear_1_8" x1="12.5" y1="3.73333" x2="9.68642" y2="12.7016" gradientUnits="userSpaceOnUse">
12
+ <stop stop-color="#48C5FF"/>
13
+ <stop offset="1" stop-color="#A1E1FF"/>
14
+ </linearGradient>
15
+ <clipPath id="clip0_1_8">
16
+ <rect width="16" height="16" fill="white"/>
17
+ </clipPath>
18
+ </defs>
19
+ </svg>
@@ -3,13 +3,6 @@ import { ref,onMounted } from 'vue'
3
3
  import { useModalStore } from '@/stores/modal';
4
4
 
5
5
  const modalStore = useModalStore();
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
6
  </script>
14
7
 
15
8
  <template>
@@ -28,10 +21,10 @@ const modalStore = useModalStore();
28
21
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
29
22
  </svg>
30
23
  <h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ modalStore?.modalContent?.content }}</h3>
31
- <button @click="()=>{modalStore.onAcceptFunction();modalStore.togleModal()}" type="button" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center">
24
+ <button @click="()=>{ modalStore.onAcceptFunction(true);modalStore.togleModal()}" type="button" class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center">
32
25
  {{ modalStore?.modalContent?.acceptText }}
33
26
  </button>
34
- <button @click="modalStore.togleModal" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 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">{{ modalStore?.modalContent?.cancelText }}</button>
27
+ <button @click="()=>{modalStore.onAcceptFunction(false);modalStore.togleModal()}" type="button" class="py-2.5 px-5 ms-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 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">{{ modalStore?.modalContent?.cancelText }}</button>
35
28
  </div>
36
29
  </div>
37
30
  </div>
@@ -2,7 +2,7 @@
2
2
  <div>
3
3
  <div class="flex items-center justify-between mb-3 flex-wrap gap-y-2">
4
4
  <Breadcrumbs />
5
- <div class="flex items-center space-x-1">
5
+ <div class="flex items-center space-x-1 flex-wrap gap-y-1">
6
6
  <slot></slot>
7
7
  </div>
8
8
  </div>
@@ -67,6 +67,9 @@ const props = defineProps({
67
67
  },
68
68
  label: {
69
69
  type: String,
70
+ },
71
+ autoHide: {
72
+ type: Boolean,
70
73
  }
71
74
  });
72
75
 
@@ -100,8 +103,7 @@ function updateFromProps() {
100
103
  if (!props.valueStart) {
101
104
  datepickerStartEl.value.value = '';
102
105
  startTime.value = '';
103
- }
104
- else {
106
+ } else {
105
107
  datepickerObject.value.setDate(dayjs(props.valueStart).format('DD MMM YYYY'));
106
108
  startTime.value = dayjs(props.valueStart).format('HH:mm:ss')
107
109
  }
@@ -121,7 +123,13 @@ watch(start, () => {
121
123
  })
122
124
 
123
125
  function initDatepickers() {
124
- datepickerObject.value = new Datepicker(datepickerStartEl.value, { format: 'dd M yyyy' });
126
+ const options = {format: 'dd M yyyy'};
127
+
128
+ if (props.autoHide) {
129
+ options.autohide = true;
130
+ }
131
+
132
+ datepickerObject.value = new Datepicker(datepickerStartEl.value, options);
125
133
 
126
134
  addChangeDateListener();
127
135
  }
@@ -134,6 +142,10 @@ function removeChangeDateListener() {
134
142
  datepickerStartEl.value.removeEventListener('changeDate', setStartDate);
135
143
  }
136
144
 
145
+ function destroyDatepickerElement() {
146
+ datepickerObject.value.destroy();
147
+ }
148
+
137
149
  function setStartDate(event) {
138
150
  startDate.value = event.detail.date
139
151
  }
@@ -157,5 +169,6 @@ onMounted(() => {
157
169
 
158
170
  onBeforeUnmount(() => {
159
171
  removeChangeDateListener();
172
+ destroyDatepickerElement();
160
173
  });
161
174
  </script>
@@ -101,6 +101,9 @@ const endDate = ref('');
101
101
  const startTime = ref('');
102
102
  const endTime = ref('');
103
103
 
104
+ const datepickerStartObject = ref('')
105
+ const datepickerEndObject = ref('')
106
+
104
107
  const start = computed(() => {
105
108
  if (!startDate.value) {
106
109
  return;
@@ -162,9 +165,9 @@ watch(end, () => {
162
165
 
163
166
  function initDatepickers() {
164
167
 
165
- new Datepicker(datepickerStartEl.value, {format: 'dd M yyyy'});
168
+ datepickerStartObject.value = new Datepicker(datepickerStartEl.value, {format: 'dd M yyyy'});
166
169
 
167
- new Datepicker(datepickerEndEl.value, {format: 'dd M yyyy'});
170
+ datepickerEndObject.value = new Datepicker(datepickerEndEl.value, {format: 'dd M yyyy'});
168
171
  addChangeDateListener();
169
172
  }
170
173
 
@@ -178,6 +181,11 @@ function removeChangeDateListener() {
178
181
  datepickerEndEl.value.removeEventListener('changeDate', setEndDate);
179
182
  }
180
183
 
184
+ function destroyDatepickerElement() {
185
+ datepickerStartObject.value.destroy();
186
+ datepickerEndObject.value.destroy();
187
+ }
188
+
181
189
  function setStartDate(event) {
182
190
  startDate.value = event.detail.date
183
191
  }
@@ -205,5 +213,6 @@ onMounted(() => {
205
213
 
206
214
  onBeforeUnmount(() => {
207
215
  removeChangeDateListener();
216
+ destroyDatepickerElement();
208
217
  })
209
218
  </script>
@@ -6,7 +6,7 @@
6
6
  v-model="search"
7
7
  @focus="showDropdown = true"
8
8
  class="block w-full pl-3 pr-10 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm transition duration-150 ease-in-out dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
9
- :placeholder="selectedItems.length ? '' : 'Select...'"
9
+ :placeholder="selectedItems.length ? '' : placeholder || 'Select...'"
10
10
  />
11
11
  <div class="absolute inset-y-0 left-2 flex items-center pr-2 flex-wrap">
12
12
  {{ }}
@@ -67,6 +67,11 @@ const props = defineProps({
67
67
  type: Boolean,
68
68
  default: false,
69
69
  },
70
+ placeholder: {
71
+ type: String,
72
+ default: '',
73
+
74
+ },
70
75
  });
71
76
 
72
77
  const emit = defineEmits(['update:modelValue']);
@@ -2,11 +2,11 @@
2
2
  <!-- drawer component -->
3
3
  <div id="drawer-navigation"
4
4
 
5
- class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
5
+ class="fixed right-0 z-50 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl dark:shadow-black"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
9
- :style="{ height: `calc(100vh - 3.5rem)` }"
9
+ :style="{ height: `calc(100vh ` }"
10
10
  >
11
11
  <h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
12
12
  Filters
@@ -26,13 +26,13 @@
26
26
  v-if="c.foreignResource"
27
27
  :options="columnOptions[c.name] || []"
28
28
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
29
- :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
29
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
30
30
  />
31
31
  <Dropdown
32
32
  v-else-if="c.type === 'boolean'"
33
33
  :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
34
34
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
35
- :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
35
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
36
36
  />
37
37
 
38
38
  <Dropdown
@@ -40,7 +40,7 @@
40
40
  :options="c.enum"
41
41
  :allowCustom="c.allowCustom"
42
42
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
43
- :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
43
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
44
44
  />
45
45
 
46
46
  <input
@@ -54,9 +54,9 @@
54
54
  <CustomDateRangePicker
55
55
  v-else-if="['datetime'].includes(c.type)"
56
56
  :column="c"
57
- :valueStart="filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
57
+ :valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
58
58
  @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
59
- :valueEnd="filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
59
+ :valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
60
60
  @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
61
61
  />
62
62
 
@@ -69,7 +69,7 @@
69
69
  >
70
70
 
71
71
  <CustomRangePicker
72
- v-else-if="['integer', 'decimal', 'float'].includes(c.type)"
72
+ v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
73
73
  :min="c.min"
74
74
  :max="c.max"
75
75
  :valueStart="getFilterItem({ column: c, operator: 'gte' })"
@@ -77,6 +77,23 @@
77
77
  :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
78
78
  @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
79
79
  />
80
+
81
+ <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
82
+ <input
83
+ type="number" aria-describedby="helper-text-explanation"
84
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
85
+ placeholder="From"
86
+ @input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
87
+ :value="getFilterItem({ column: c, operator: 'gte' })"
88
+ >
89
+ <input
90
+ type="number" aria-describedby="helper-text-explanation"
91
+ class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
92
+ placeholder="To"
93
+ @input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
94
+ :value="getFilterItem({ column: c, operator: 'lte' })"
95
+ >
96
+ </div>
80
97
 
81
98
  </li>
82
99
  </ul>
@@ -84,15 +101,15 @@
84
101
 
85
102
  <div class="flex justify-end gap-2">
86
103
  <button
87
- :disabled="!filters.length"
104
+ :disabled="!filtersStore.filters.length"
88
105
  type="button"
89
- class="flex items-center py-1 px-3 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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 disabled:opacity-50 disabled:cursor-not-allowed"
106
+ class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 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 disabled:opacity-50 disabled:cursor-not-allowed"
90
107
  @click="clear">Clear all</button>
91
108
 
92
109
  </div>
93
110
  </div>
94
111
 
95
- <div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-50"
112
+ <div v-if="show" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
96
113
  @click="$emit('hide')">
97
114
  </div>
98
115
  </template>
@@ -105,6 +122,10 @@ import { callAdminForthApi } from '@/utils';
105
122
  import { useRouter } from 'vue-router';
106
123
  import { computedAsync } from '@vueuse/core'
107
124
  import CustomRangePicker from "@/components/CustomRangePicker.vue";
125
+ import { useFiltersStore } from '@/stores/filters';
126
+
127
+ const filtersStore = useFiltersStore();
128
+
108
129
 
109
130
  // props: columns
110
131
  // add support for v-model:filers
@@ -163,28 +184,28 @@ watch(() => props.show, (show) => {
163
184
 
164
185
  function setFilterItem({ column, operator, value }) {
165
186
 
166
- const index = props.filters.findIndex(f => f.field === column.name && f.operator === operator);
187
+ const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
167
188
  if (value === undefined) {
168
189
  if (index !== -1) {
169
- props.filters.splice(index, 1);
190
+ filtersStore.filters.splice(index, 1);
170
191
  }
171
- emits('update:filters', [...props.filters]);
192
+ emits('update:filters', [...filtersStore.filters]);
172
193
  return;
173
194
  } else {
174
195
  if (index === -1) {
175
- props.filters.push({ field: column.name, value, operator });
196
+ filtersStore.setFilter({ field: column.name, value, operator });
176
197
  } else {
177
- props.filters[index].value = value;
198
+ filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
178
199
  }
179
200
  }
180
- emits('update:filters', [...props.filters]);
201
+ emits('update:filters', [...filtersStore.filters]);
181
202
  }
182
203
 
183
204
  function getFilterItem({ column, operator }) {
184
- return props.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
205
+ return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
185
206
  }
186
207
 
187
208
  async function clear() {
188
- emits('update:filters', []);
209
+ filtersStore.clearFilters();
189
210
  }
190
211
  </script>
@@ -1,11 +1,11 @@
1
1
  <template>
2
- <div>
2
+ <div class="rounded-default">
3
3
 
4
4
  <div
5
- class="relative shadow-resourse-form-shadow sm:rounded-lg dark:shadow-2xl dark:shadow-black"
5
+ class="relative shadow-resourse-form-shadow sm:rounded-lg dark:shadow-2xl dark:shadow-black rounded-default"
6
6
  >
7
7
  <form autocomplete="off" @submit.prevent>
8
- <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
8
+ <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 ">
9
9
  <thead class="text-xs text-gray-700 uppercase bg-form-view-heading dark:bg-gray-700 dark:text-gray-400">
10
10
  <tr>
11
11
  <th scope="col" class="px-6 py-3">
@@ -30,28 +30,28 @@
30
30
  />
31
31
  </template>
32
32
  <template v-else>
33
- <td class="px-6 py-4 whitespace-nowrap">
34
- {{ column.label }}
35
- <span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
36
- <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
37
- :class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
38
- />
39
- </span>
40
- <div :id="`tooltip-show-${i}`"
41
- role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
42
- Required field
43
- <div class="tooltip-arrow" data-popper-arrow></div>
44
- </div>
33
+ <td class="px-6 py-4 whitespace-nowrap "> <!--align-top-->
34
+ {{ column.label }}
35
+ <span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
36
+ <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
37
+ :class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
38
+ />
39
+ </span>
40
+ <div :id="`tooltip-show-${i}`"
41
+ role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
42
+ Required field
43
+ <div class="tooltip-arrow" data-popper-arrow></div>
44
+ </div>
45
45
  </td>
46
46
  <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
47
-
48
47
  <Dropdown
49
48
  single
50
49
  v-if="column.foreignResource"
51
50
  :options="columnOptions[column.name] || []"
51
+ :placeholder = "columnOptions[column.name]?.length || 'There are no options available'"
52
52
  :modelValue="currentValues[column.name]"
53
53
  @update:modelValue="setCurrentValue(column.name, $event)"
54
- />
54
+ ></Dropdown>
55
55
  <Dropdown
56
56
  single
57
57
  v-else-if="column.enum"
@@ -79,6 +79,7 @@
79
79
  v-else-if="['datetime'].includes(column.type)"
80
80
  :column="column"
81
81
  :valueStart="currentValues[column.name]"
82
+ auto-hide
82
83
  @update:valueStart="setCurrentValue(column.name, $event)"
83
84
  />
84
85
  <input
@@ -120,9 +121,6 @@
120
121
  <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
121
122
  <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
122
123
  </button>
123
-
124
-
125
-
126
124
  <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
127
125
 
128
126
  <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
@@ -154,7 +152,7 @@ const router = useRouter();
154
152
 
155
153
  const props = defineProps({
156
154
  loading: Boolean,
157
- resourceColumns: Object,
155
+ resource: Object,
158
156
  record: Object,
159
157
  validating: Boolean,
160
158
  customComponentsPerColumn: Object,
@@ -196,7 +194,11 @@ const columnError = (column) => {
196
194
  for (let i = 0; i < validationArray.length; i++) {
197
195
  if (validationArray[i].regExp) {
198
196
  const regExp = new RegExp(validationArray[i].regExp);
199
- if (!regExp.test(currentValues.value[column.name])) {
197
+ let value = currentValues.value[column.name];
198
+ if (value === undefined || value === null) {
199
+ value = '';
200
+ }
201
+ if (!regExp.test(value)) {
200
202
  return validationArray[i].message;
201
203
  }
202
204
  }
@@ -226,7 +228,7 @@ onMounted(() => {
226
228
 
227
229
  const columnOptions = computedAsync(async () => {
228
230
  return (await Promise.all(
229
- Object.values(props.resourceColumns).map(async (column) => {
231
+ Object.values(props.resource.columns).map(async (column) => {
230
232
  if (column.foreignResource) {
231
233
  const list = await callAdminForthApi({
232
234
  method: 'POST',
@@ -249,7 +251,7 @@ const coreStore = useCoreStore();
249
251
 
250
252
  const editableColumns = computed(() => {
251
253
  const mode = props.record ? 'edit' : 'create';
252
- return props.resourceColumns?.filter(column => column.showIn.includes(mode));
254
+ return props.resource?.columns?.filter(column => column.showIn.includes(mode));
253
255
  });
254
256
 
255
257
  const isValid = computed(() => {