adminforth 1.1.31 → 1.1.32

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 (33) hide show
  1. package/dist/plugins/AccessControl/index.js +56 -3
  2. package/dist/spa/index.html +2 -2
  3. package/dist/spa/package-lock.json +189 -1
  4. package/dist/spa/package.json +6 -1
  5. package/dist/spa/spa/src/App.vue +2 -0
  6. package/dist/spa/spa/src/components/ResourceForm.vue +1 -1
  7. package/dist/spa/src/App.vue +76 -57
  8. package/dist/spa/src/components/AcceptModal.vue +1 -1
  9. package/dist/spa/src/components/CustomDatePicker.vue +3 -3
  10. package/dist/spa/src/components/CustomDateRangePicker.vue +3 -3
  11. package/dist/spa/src/components/CustomRangePicker.vue +148 -0
  12. package/dist/spa/src/components/Dropdown.vue +13 -4
  13. package/dist/spa/src/components/Filters.vue +55 -22
  14. package/dist/spa/src/components/MenuLink.vue +2 -2
  15. package/dist/spa/src/components/ResourceForm.vue +157 -99
  16. package/dist/spa/src/components/ValueRenderer.vue +11 -1
  17. package/dist/spa/src/router/index.ts +3 -2
  18. package/dist/spa/src/stores/core.ts +22 -33
  19. package/dist/spa/src/utils.ts +5 -3
  20. package/dist/spa/src/views/CreateView.vue +15 -7
  21. package/dist/spa/src/views/EditView.vue +36 -8
  22. package/dist/spa/src/views/ListView.vue +35 -23
  23. package/dist/spa/src/views/LoginView.vue +1 -1
  24. package/dist/spa/src/views/ResourceParent.vue +1 -1
  25. package/dist/spa/src/views/ShowView.vue +20 -9
  26. package/dist/spa/tailwind.config.js +5 -2
  27. package/package.json +1 -1
  28. package/spa/src/App.vue +2 -0
  29. package/spa/src/components/ResourceForm.vue +1 -1
  30. package/dist/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +0 -248
  31. package/dist/spa/spa/src/components/ShowTableItem.vue +0 -14
  32. package/dist/spa/spa/src/views/HomeView.vue +0 -8
  33. package/dist/spa/src/views/HomeView.vue +0 -8
@@ -0,0 +1,148 @@
1
+ <template>
2
+ <div class="flex flex-wrap gap-2">
3
+ <input
4
+ type="number" aria-describedby="helper-text-explanation"
5
+ 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"
6
+ placeholder="From"
7
+ v-model="start"
8
+ >
9
+
10
+ <input
11
+ type="number" aria-describedby="helper-text-explanation"
12
+ 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"
13
+ placeholder="To"
14
+ v-model="end"
15
+ >
16
+
17
+ <button
18
+ v-if="isChanged"
19
+ type="button"
20
+ class="flex items-center p-0.5 ml-auto 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"
21
+ @click="clear">Clear
22
+ </button>
23
+
24
+ <div class="w-full">
25
+ <vue-slider
26
+ class="custom-slider"
27
+ :dot-size="20"
28
+ height="7.99px"
29
+ :min="minFormatted"
30
+ :max="maxFormatted"
31
+ v-model="sliderValue"
32
+ @update:model-value="updateFromSlider($event)"
33
+ />
34
+ </div>
35
+ </div>
36
+ </template>
37
+ <script setup lang="ts">
38
+ import VueSlider from 'vue-slider-component';
39
+ import 'vue-slider-component/theme/antd.css'
40
+ import {computed, onMounted, ref, watch} from "vue";
41
+ import debounce from 'debounce'
42
+
43
+ const props = defineProps({
44
+ valueStart: {
45
+ default: '',
46
+ },
47
+ valueEnd: {
48
+ default: '',
49
+ },
50
+ min: {
51
+ default: 0,
52
+ },
53
+ max: {
54
+ default: 10,
55
+ },
56
+ });
57
+
58
+ const emit = defineEmits(['update:valueStart', 'update:valueEnd']);
59
+
60
+ const minFormatted = computed(() => Math.floor(props.min));
61
+ const maxFormatted = computed(() => Math.ceil(props.max));
62
+
63
+ const isChanged = computed(() => {
64
+ return start.value && start.value !== minFormatted.value || end.value && end.value !== maxFormatted.value;
65
+ });
66
+
67
+ const start = ref(props.valueStart);
68
+ const end = ref(props.valueEnd);
69
+
70
+ const sliderValue = ref([minFormatted.value, maxFormatted.value]);
71
+
72
+ const updateFromSlider =
73
+ debounce((value: [number, number]) => {
74
+ start.value = value[0] === minFormatted.value ? '': value[0];
75
+ end.value = value[1] === maxFormatted.value ? '': value[1];
76
+ }, 500);
77
+
78
+ onMounted(() => {
79
+ updateStartFromProps();
80
+ updateEndFromProps();
81
+
82
+ watch(() => props.valueStart, (value) => {
83
+ updateStartFromProps();
84
+ });
85
+
86
+ watch(() => props.valueEnd, (value) => {
87
+ updateEndFromProps();
88
+ });
89
+ })
90
+
91
+ function updateStartFromProps() {
92
+ start.value = props.valueStart;
93
+ setSliderValues(start.value, end.value)
94
+ }
95
+
96
+ function updateEndFromProps() {
97
+ end.value = props.valueEnd;
98
+ setSliderValues(start.value, end.value)
99
+ }
100
+
101
+ watch(start, () => {
102
+ console.log('⚡ emit', start.value)
103
+ emit('update:valueStart', start.value)
104
+ })
105
+
106
+ watch(end, () => {
107
+ console.log('⚡ emit', end.value)
108
+ emit('update:valueEnd', end.value);
109
+ })
110
+
111
+ const clear = () => {
112
+ start.value = ''
113
+ end.value = ''
114
+ setSliderValues('', '')
115
+ }
116
+
117
+ function setSliderValues(start, end) {
118
+ sliderValue.value = [start || minFormatted.value, end || maxFormatted.value];
119
+ }
120
+ </script>
121
+
122
+ <style lang="scss" scoped>
123
+ .custom-slider {
124
+ &:deep(.vue-slider-rail) {
125
+ background-color: rgb(229 231 235);
126
+ }
127
+
128
+ &:deep(.vue-slider-dot-handle) {
129
+ background-color: #1c64f2;
130
+ border: none;
131
+ box-shadow: none;
132
+ }
133
+
134
+ &:deep(.vue-slider-dot-handle:hover) {
135
+ background-color: #1c64f2;
136
+ border: none;
137
+ box-shadow: none;
138
+ }
139
+
140
+ &:deep(.vue-slider-process) {
141
+ background-color: rgb(225 239 254);
142
+ }
143
+
144
+ &:deep(.vue-slider-process:hover) {
145
+ background-color: rgb(225 239 254);
146
+ }
147
+ }
148
+ </style>
@@ -9,7 +9,8 @@
9
9
  :placeholder="selectedItems.length ? '' : 'Select...'"
10
10
  />
11
11
  <div class="absolute inset-y-0 left-2 flex items-center pr-2 flex-wrap">
12
- <div v-for="item in selectedItems" :key="item.name" class="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
12
+ {{ }}
13
+ <div v-for="item in selectedItems" :key="item?.name" class="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">
13
14
  <span>{{ item.label }}</span>
14
15
  <button
15
16
  type="button"
@@ -34,7 +35,7 @@
34
35
  <IconCaretUpSolid v-else class="h-5 w-5 text-gray-400" />
35
36
  </div>
36
37
  </div>
37
- <div v-if="showDropdown" class="absolute z-10 mt-1 w-full bg-white shadow-lg rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
38
+ <div v-if="showDropdown" class="absolute z-10 mt-1 w-full bg-white shadow-lg dark:shadow-black rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
38
39
  <div
39
40
  v-for="item in filteredItems"
40
41
  :key="item.value"
@@ -76,10 +77,14 @@ const showDropdown = ref(false);
76
77
  const selectedItems = ref([]);
77
78
 
78
79
  function updateFromProps() {
79
- console.log('⚡ updateFromProps', props.modelValue)
80
80
  if (props.modelValue !== undefined) {
81
81
  if (props.single) {
82
- selectedItems.value = [props.options.find(item => item.value === props.modelValue)];
82
+ const el = props.options.find(item => item.value === props.modelValue);
83
+ if (el) {
84
+ selectedItems.value = [el];
85
+ } else {
86
+ selectedItems.value = [];
87
+ }
83
88
  } else {
84
89
  selectedItems.value = props.options.filter(item => props.modelValue.includes(item.value));
85
90
  }
@@ -93,6 +98,10 @@ onMounted(() => {
93
98
  updateFromProps();
94
99
  });
95
100
 
101
+ watch(() => props.options, () => {
102
+ updateFromProps();
103
+ });
104
+
96
105
  addClickListener();
97
106
 
98
107
  });
@@ -2,7 +2,7 @@
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"
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"
6
6
 
7
7
  :class="show ? 'top-0 transform-none' : ''"
8
8
  tabindex="-1" aria-labelledby="drawer-navigation-label"
@@ -22,8 +22,14 @@
22
22
  <li v-for="c in columnsWithFilter" :key="c">
23
23
  <p class="dark:text-gray-400">{{ c.label }}</p>
24
24
 
25
- <Dropdown
26
- v-if="c.type === 'boolean'"
25
+ <Dropdown
26
+ v-if="c.foreignResource"
27
+ :options="columnOptions[c.name] || []"
28
+ @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
29
+ :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
30
+ />
31
+ <Dropdown
32
+ v-else-if="c.type === 'boolean'"
27
33
  :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
28
34
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
29
35
  :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
@@ -62,22 +68,15 @@
62
68
  :value="getFilterItem({ column: c, operator: 'ilike' })"
63
69
  >
64
70
 
65
- <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
66
- <input
67
- type="number" aria-describedby="helper-text-explanation"
68
- 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"
69
- placeholder="From"
70
- @input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
71
- :value="getFilterItem({ column: c, operator: 'gte' })"
72
- >
73
- <input
74
- type="number" aria-describedby="helper-text-explanation"
75
- 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"
76
- placeholder="To"
77
- @input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
78
- :value="getFilterItem({ column: c, operator: 'lte' })"
79
- >
80
- </div>
71
+ <CustomRangePicker
72
+ v-else-if="['integer', 'decimal', 'float'].includes(c.type)"
73
+ :min="c.min"
74
+ :max="c.max"
75
+ :valueStart="getFilterItem({ column: c, operator: 'gte' })"
76
+ @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
77
+ :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
78
+ @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
79
+ />
81
80
 
82
81
  </li>
83
82
  </ul>
@@ -93,25 +92,59 @@
93
92
  </div>
94
93
  </div>
95
94
 
96
- <div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
95
+ <div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-50"
97
96
  @click="$emit('hide')">
98
97
  </div>
99
98
  </template>
100
99
 
101
100
  <script setup>
102
- import { watch, computed } from 'vue'
101
+ import { watch, computed, ref, onMounted } from 'vue'
103
102
  import Dropdown from '@/components/Dropdown.vue';
104
103
  import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
104
+ import { callAdminForthApi } from '@/utils';
105
+ import { useRouter } from 'vue-router';
106
+ import { computedAsync } from '@vueuse/core'
107
+ import CustomRangePicker from "@/components/CustomRangePicker.vue";
105
108
 
106
109
  // props: columns
107
110
  // add support for v-model:filers
108
111
  const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
109
112
  const emits = defineEmits(['update:filters', 'hide']);
110
113
 
111
- const columnsWithFilter = computed(
114
+ const router = useRouter();
115
+
116
+
117
+ const columnsWithFilter = computed(
112
118
  () => props.columns?.filter(column => column.showIn.includes('filter')) || []
113
119
  );
114
120
 
121
+ const columnOptions = computedAsync(async () => {
122
+ const ret = {};
123
+ if (!props.columns) {
124
+ return ret;
125
+ }
126
+ await Promise.all(
127
+ Object.values(props.columns).map(async (column) => {
128
+ if (column.foreignResource) {
129
+ const list = await callAdminForthApi({
130
+ method: 'POST',
131
+ path: `/get_resource_foreign_data`,
132
+ body: {
133
+ resourceId: router.currentRoute.value.params.resourceId,
134
+ column: column.name,
135
+ limit: 1000,
136
+ offset: 0,
137
+ },
138
+ });
139
+ ret[column.name] = list.items;
140
+ }
141
+ })
142
+ );
143
+
144
+ return ret;
145
+ }, {});
146
+
147
+
115
148
  // sync 'body' class 'overflow-hidden' with show prop show
116
149
  watch(() => props.show, (show) => {
117
150
  if (show) {
@@ -1,11 +1,11 @@
1
1
  <template>
2
2
  <RouterLink
3
3
  :to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
4
- class="flex items-center py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem"
4
+ class="flex items-center py-2 text-nav-menu-text rounded-default hover:bg-nav-menu-bg-hover active:bg-nav-menu-bg-active" role="menuitem"
5
5
  :class="{
6
6
  'px-4': isChild,
7
7
  'px-2': !isChild,
8
- 'bg-blue-100 dark:bg-gray-700': item.resourceId ?
8
+ 'bg-nav-menu-bg-active dark:bg-gray-700': item.resourceId ?
9
9
  ($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
10
10
  ($route.name === item.path)
11
11
  }"
@@ -2,11 +2,11 @@
2
2
  <div>
3
3
 
4
4
  <div
5
- class="relative shadow-md sm:rounded-lg dark:shadow-2xl"
5
+ class="relative shadow-resourse-form-shadow sm:rounded-lg dark:shadow-2xl dark:shadow-black"
6
6
  >
7
7
  <form autocomplete="off" @submit.prevent>
8
8
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
9
- <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
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">
12
12
  Field
@@ -18,97 +18,116 @@
18
18
  </thead>
19
19
  <tbody>
20
20
  <tr v-for="column, i in editableColumns" :key="column.name"
21
- class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700"
21
+ class="bg-form-view-bg dark:bg-gray-800 border-b dark:border-gray-700"
22
22
  >
23
- <td class="px-6 py-4 whitespace-nowrap">
24
- {{ column.label }}
25
- <span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
26
- <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
27
- :class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
23
+ <!-- if column is in customComponentsPerColumn, use this component. If not, use this code -->
24
+ <template v-if="customComponentsPerColumn[column.name]">
25
+ <component
26
+ :is="customComponentsPerColumn[column.name]"
27
+ :column="column"
28
+ :value="currentValues[column.name]"
29
+ @update:value="setCurrentValue(column.name, $event)"
28
30
  />
29
- </span>
30
- <div :id="`tooltip-show-${i}`"
31
- 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">
32
- Required field
33
- <div class="tooltip-arrow" data-popper-arrow></div>
34
- </div>
35
- </td>
36
- <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
37
- <Dropdown
38
- single
39
- v-if="column.enum"
40
- :options="column.enum"
41
- :modelValue="currentValues[column.name]"
42
- @update:modelValue="setCurrentValue(column.name, $event)"
43
- />
44
- <Dropdown
45
- single
46
- v-else-if="column.type === 'boolean'"
47
- :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
48
- :modelValue="currentValues[column.name]"
49
- @update:modelValue="setCurrentValue(column.name, $event)"
50
- />
51
- <input
52
- v-else-if="['integer'].includes(column.type)"
53
- type="number"
54
- step="1"
55
- 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-40 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"
56
- placeholder="0"
57
- :value="currentValues[column.name]"
58
- @input="setCurrentValue(column.name, $event.target.value)"
59
- >
60
- <CustomDatePicker
61
- v-else-if="['datetime'].includes(column.type)"
62
- :column="column"
63
- :valueStart="currentValues[column.name]"
64
- @update:valueStart="setCurrentValue(column.name, $event)"
65
- />
66
- <input
67
- v-else-if="['decimal', 'float'].includes(column.type)"
68
- type="number"
69
- step="0.1"
70
- 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-40 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"
71
- placeholder="0.0"
72
- :value="currentValues[column.name]"
73
- @input="setCurrentValue(column.name, $event.target.value)"
74
- />
75
- <textarea
76
- v-else-if="['text'].includes(column.type)"
77
- 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-full 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"
78
- placeholder="Text"
79
- :value="currentValues[column.name]"
80
- @input="setCurrentValue(column.name, $event.target.value)"
81
- >
82
- </textarea>
83
- <input
84
- v-else
85
- :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
86
- 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-full 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"
87
- placeholder="Text"
88
- :value="currentValues[column.name]"
89
- @input="setCurrentValue(column.name, $event.target.value)"
90
- autocomplete="false"
91
- data-lpignore="true"
92
- readonly
93
- onfocus="this.removeAttribute('readonly');"
94
- >
95
-
96
- <button
97
- v-if="column.masked"
98
- type="button"
99
- @click="unmasked[column.name] = !unmasked[column.name]"
100
- class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
101
- >
102
- <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
103
- <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
104
- </button>
105
-
106
-
107
-
108
- <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
109
-
110
- <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
111
- </td>
31
+ </template>
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>
45
+ </td>
46
+ <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
47
+
48
+ <Dropdown
49
+ single
50
+ v-if="column.foreignResource"
51
+ :options="columnOptions[column.name] || []"
52
+ :modelValue="currentValues[column.name]"
53
+ @update:modelValue="setCurrentValue(column.name, $event)"
54
+ />
55
+ <Dropdown
56
+ single
57
+ v-else-if="column.enum"
58
+ :options="column.enum"
59
+ :modelValue="currentValues[column.name]"
60
+ @update:modelValue="setCurrentValue(column.name, $event)"
61
+ />
62
+ <Dropdown
63
+ single
64
+ v-else-if="column.type === 'boolean'"
65
+ :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
66
+ :modelValue="currentValues[column.name]"
67
+ @update:modelValue="setCurrentValue(column.name, $event)"
68
+ />
69
+ <input
70
+ v-else-if="['integer'].includes(column.type)"
71
+ type="number"
72
+ step="1"
73
+ 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-40 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"
74
+ placeholder="0"
75
+ :value="currentValues[column.name]"
76
+ @input="setCurrentValue(column.name, $event.target.value)"
77
+ >
78
+ <CustomDatePicker
79
+ v-else-if="['datetime'].includes(column.type)"
80
+ :column="column"
81
+ :valueStart="currentValues[column.name]"
82
+ @update:valueStart="setCurrentValue(column.name, $event)"
83
+ />
84
+ <input
85
+ v-else-if="['decimal', 'float'].includes(column.type)"
86
+ type="number"
87
+ step="0.1"
88
+ 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-40 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"
89
+ placeholder="0.0"
90
+ :value="currentValues[column.name]"
91
+ @input="setCurrentValue(column.name, $event.target.value)"
92
+ />
93
+ <textarea
94
+ v-else-if="['text'].includes(column.type)"
95
+ 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-full 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"
96
+ placeholder="Text"
97
+ :value="currentValues[column.name]"
98
+ @input="setCurrentValue(column.name, $event.target.value)"
99
+ >
100
+ </textarea>
101
+ <input
102
+ v-else
103
+ :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
104
+ 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-full 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"
105
+ placeholder="Text"
106
+ :value="currentValues[column.name]"
107
+ @input="setCurrentValue(column.name, $event.target.value)"
108
+ autocomplete="false"
109
+ data-lpignore="true"
110
+ readonly
111
+ onfocus="this.removeAttribute('readonly');"
112
+ >
113
+
114
+ <button
115
+ v-if="column.masked"
116
+ type="button"
117
+ @click="unmasked[column.name] = !unmasked[column.name]"
118
+ class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
119
+ >
120
+ <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
121
+ <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
122
+ </button>
123
+
124
+
125
+
126
+ <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
127
+
128
+ <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
129
+ </td>
130
+ </template>
112
131
  </tr>
113
132
 
114
133
  </tbody>
@@ -121,19 +140,24 @@
121
140
 
122
141
  <script setup>
123
142
 
124
- import { ref, computed, onMounted, watch } from 'vue';
125
- import { useCoreStore } from '@/stores/core';
126
- import Dropdown from '@/components/Dropdown.vue';
127
- import { IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
128
- import { initFlowbite } from 'flowbite'
129
- import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
130
143
  import CustomDatePicker from "@/components/CustomDatePicker.vue";
144
+ import Dropdown from '@/components/Dropdown.vue';
145
+ import { useCoreStore } from '@/stores/core';
146
+ import { callAdminForthApi } from '@/utils';
147
+ import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
148
+ import { computedAsync } from '@vueuse/core';
149
+ import { initFlowbite } from 'flowbite';
150
+ import { computed, onMounted, ref, watch } from 'vue';
151
+ import { useRouter } from 'vue-router';
152
+
153
+ const router = useRouter();
131
154
 
132
155
  const props = defineProps({
133
156
  loading: Boolean,
134
157
  resourceColumns: Object,
135
158
  record: Object,
136
159
  validating: Boolean,
160
+ customComponentsPerColumn: Object,
137
161
  });
138
162
 
139
163
 
@@ -145,6 +169,7 @@ const emit = defineEmits(['update:record', 'update:isValid']);
145
169
 
146
170
  const currentValues = ref({});
147
171
 
172
+
148
173
  const columnError = (column) => {
149
174
  const val = computed(() => {
150
175
  if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
@@ -166,14 +191,24 @@ const columnError = (column) => {
166
191
  return `This field must be less than ${column.maxValue}`;
167
192
  }
168
193
  }
194
+ if ( column.validation && column.validation.length ){
195
+ const validationArray = column.validation;
196
+ for (let i = 0; i < validationArray.length; i++) {
197
+ if (validationArray[i].regExp) {
198
+ const regExp = new RegExp(validationArray[i].regExp);
199
+ if (!regExp.test(currentValues.value[column.name])) {
200
+ return validationArray[i].message;
201
+ }
202
+ }
203
+ }
204
+
205
+ }
169
206
  return null;
170
207
  });
171
208
  return val.value;
172
209
  };
173
210
 
174
211
 
175
-
176
-
177
212
  const setCurrentValue = (key, value) => {
178
213
  currentValues.value[key] = value;
179
214
 
@@ -185,8 +220,31 @@ onMounted(() => {
185
220
  currentValues.value[key] = props.record[key];
186
221
  });
187
222
  initFlowbite();
223
+
224
+
188
225
  });
189
226
 
227
+ const columnOptions = computedAsync(async () => {
228
+ return (await Promise.all(
229
+ Object.values(props.resourceColumns).map(async (column) => {
230
+ if (column.foreignResource) {
231
+ const list = await callAdminForthApi({
232
+ method: 'POST',
233
+ path: `/get_resource_foreign_data`,
234
+ body: {
235
+ resourceId: router.currentRoute.value.params.resourceId,
236
+ column: column.name,
237
+ limit: 1000,
238
+ offset: 0,
239
+ },
240
+ });
241
+ return { [column.name]: list.items };
242
+ }
243
+ })
244
+ )).reduce((acc, val) => Object.assign(acc, val), {})
245
+
246
+ }, {});
247
+
190
248
  const coreStore = useCoreStore();
191
249
 
192
250
  const editableColumns = computed(() => {