adminforth 1.6.2-next.3 → 1.6.2-next.4

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 (93) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +0 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/spa/.eslintrc.cjs +14 -0
  5. package/dist/spa/README.md +39 -0
  6. package/dist/spa/env.d.ts +1 -0
  7. package/dist/spa/index.html +23 -0
  8. package/dist/spa/package-lock.json +5062 -0
  9. package/dist/spa/package.json +58 -0
  10. package/dist/spa/postcss.config.js +6 -0
  11. package/dist/spa/public/assets/favicon.png +0 -0
  12. package/dist/spa/src/App.vue +432 -0
  13. package/dist/spa/src/adminforth.ts +160 -0
  14. package/dist/spa/src/afcl/AreaChart.vue +160 -0
  15. package/dist/spa/src/afcl/BarChart.vue +170 -0
  16. package/dist/spa/src/afcl/Button.vue +27 -0
  17. package/dist/spa/src/afcl/Checkbox.vue +24 -0
  18. package/dist/spa/src/afcl/Dropzone.vue +128 -0
  19. package/dist/spa/src/afcl/Input.vue +41 -0
  20. package/dist/spa/src/afcl/Link.vue +17 -0
  21. package/dist/spa/src/afcl/LinkButton.vue +25 -0
  22. package/dist/spa/src/afcl/PieChart.vue +175 -0
  23. package/dist/spa/src/afcl/ProgressBar.vue +57 -0
  24. package/dist/spa/src/afcl/Select.vue +246 -0
  25. package/dist/spa/src/afcl/Skeleton.vue +26 -0
  26. package/dist/spa/src/afcl/Spinner.vue +9 -0
  27. package/dist/spa/src/afcl/Table.vue +116 -0
  28. package/dist/spa/src/afcl/Tooltip.vue +43 -0
  29. package/dist/spa/src/afcl/VerticalTabs.vue +49 -0
  30. package/dist/spa/src/afcl/index.ts +20 -0
  31. package/dist/spa/src/assets/base.css +2 -0
  32. package/dist/spa/src/assets/logo.svg +19 -0
  33. package/dist/spa/src/components/AcceptModal.vue +44 -0
  34. package/dist/spa/src/components/Breadcrumbs.vue +41 -0
  35. package/dist/spa/src/components/BreadcrumbsWithButtons.vue +25 -0
  36. package/dist/spa/src/components/CustomDatePicker.vue +180 -0
  37. package/dist/spa/src/components/CustomDateRangePicker.vue +218 -0
  38. package/dist/spa/src/components/CustomRangePicker.vue +156 -0
  39. package/dist/spa/src/components/Filters.vue +232 -0
  40. package/dist/spa/src/components/GroupsTable.vue +218 -0
  41. package/dist/spa/src/components/HelloWorld.vue +17 -0
  42. package/dist/spa/src/components/MenuLink.vue +41 -0
  43. package/dist/spa/src/components/ResourceForm.vue +260 -0
  44. package/dist/spa/src/components/ResourceListTable.vue +486 -0
  45. package/dist/spa/src/components/ShowTable.vue +81 -0
  46. package/dist/spa/src/components/SingleSkeletLoader.vue +13 -0
  47. package/dist/spa/src/components/SkeleteLoader.vue +18 -0
  48. package/dist/spa/src/components/ThreeDotsMenu.vue +43 -0
  49. package/dist/spa/src/components/Toast.vue +78 -0
  50. package/dist/spa/src/components/ValueRenderer.vue +141 -0
  51. package/dist/spa/src/components/icons/IconCalendar.vue +5 -0
  52. package/dist/spa/src/components/icons/IconCommunity.vue +7 -0
  53. package/dist/spa/src/components/icons/IconDocumentation.vue +7 -0
  54. package/dist/spa/src/components/icons/IconEcosystem.vue +7 -0
  55. package/dist/spa/src/components/icons/IconSupport.vue +7 -0
  56. package/dist/spa/src/components/icons/IconTime.vue +5 -0
  57. package/dist/spa/src/components/icons/IconTooling.vue +19 -0
  58. package/dist/spa/src/composables/useFrontendApi.ts +28 -0
  59. package/dist/spa/src/i18n.ts +54 -0
  60. package/dist/spa/src/index.scss +34 -0
  61. package/dist/spa/src/main.ts +22 -0
  62. package/dist/spa/src/renderers/CompactField.vue +46 -0
  63. package/dist/spa/src/renderers/CompactUUID.vue +46 -0
  64. package/dist/spa/src/renderers/CountryFlag.vue +65 -0
  65. package/dist/spa/src/renderers/HumanNumber.vue +58 -0
  66. package/dist/spa/src/renderers/RelativeTime.vue +42 -0
  67. package/dist/spa/src/renderers/URL.vue +18 -0
  68. package/dist/spa/src/router/index.ts +70 -0
  69. package/dist/spa/src/spa_types/core.ts +51 -0
  70. package/dist/spa/src/stores/core.ts +228 -0
  71. package/dist/spa/src/stores/filters.ts +27 -0
  72. package/dist/spa/src/stores/modal.ts +48 -0
  73. package/dist/spa/src/stores/toast.ts +30 -0
  74. package/dist/spa/src/stores/user.ts +79 -0
  75. package/dist/spa/src/types/Adapters.ts +26 -0
  76. package/dist/spa/src/types/Back.ts +1344 -0
  77. package/dist/spa/src/types/Common.ts +940 -0
  78. package/dist/spa/src/types/FrontendAPI.ts +189 -0
  79. package/dist/spa/src/utils.ts +184 -0
  80. package/dist/spa/src/views/CreateView.vue +167 -0
  81. package/dist/spa/src/views/EditView.vue +171 -0
  82. package/dist/spa/src/views/ListView.vue +442 -0
  83. package/dist/spa/src/views/LoginView.vue +199 -0
  84. package/dist/spa/src/views/PageNotFound.vue +20 -0
  85. package/dist/spa/src/views/ResourceParent.vue +50 -0
  86. package/dist/spa/src/views/ShowView.vue +209 -0
  87. package/dist/spa/src/websocket.ts +129 -0
  88. package/dist/spa/tailwind.config.js +19 -0
  89. package/dist/spa/tsconfig.app.json +14 -0
  90. package/dist/spa/tsconfig.json +11 -0
  91. package/dist/spa/tsconfig.node.json +19 -0
  92. package/dist/spa/vite.config.ts +52 -0
  93. package/package.json +1 -1
@@ -0,0 +1,156 @@
1
+ <template>
2
+ <div class="flex flex-wrap gap-2">
3
+ <input
4
+ :min="minFormatted"
5
+ :max="maxFormatted"
6
+ type="number" aria-describedby="helper-text-explanation"
7
+ 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"
8
+ :placeholder="$t('From')"
9
+ v-model="start"
10
+ >
11
+
12
+ <input
13
+ :min="minFormatted"
14
+ :max="maxFormatted"
15
+ type="number" aria-describedby="helper-text-explanation"
16
+ 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"
17
+ :placeholder="$t('To')"
18
+ v-model="end"
19
+ >
20
+
21
+ <button
22
+ v-if="isChanged"
23
+ type="button"
24
+ 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-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 disabled:opacity-50 disabled:cursor-not-allowed"
25
+ @click="clear">Clear
26
+ </button>
27
+
28
+ <div v-if="min && max" class="w-full px-2.5">
29
+ <vue-slider
30
+ class="custom-slider"
31
+ :dot-size="20"
32
+ height="7.99px"
33
+ :min="minFormatted"
34
+ :max="maxFormatted"
35
+ v-model="sliderValue"
36
+ @update:model-value="updateFromSlider($event)"
37
+ />
38
+ </div>
39
+ </div>
40
+ </template>
41
+ <script setup lang="ts">
42
+ import VueSlider from 'vue-slider-component';
43
+ import 'vue-slider-component/theme/antd.css'
44
+ import {computed, onMounted, ref, watch} from "vue";
45
+ import debounce from 'debounce'
46
+
47
+ const props = defineProps({
48
+ valueStart: {
49
+ default: '',
50
+ },
51
+ valueEnd: {
52
+ default: '',
53
+ },
54
+ min: {},
55
+ max: {},
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
+ watch([minFormatted,maxFormatted], () => {
112
+ setSliderValues(minFormatted.value, maxFormatted.value)
113
+ })
114
+
115
+ const clear = () => {
116
+ start.value = ''
117
+ end.value = ''
118
+ setSliderValues('', '')
119
+ }
120
+
121
+ function setSliderValues(start, end) {
122
+ sliderValue.value = [start || minFormatted.value, end || maxFormatted.value];
123
+ }
124
+ </script>
125
+
126
+ <style lang="scss" scoped>
127
+ .custom-slider {
128
+ &:deep(.vue-slider-rail) {
129
+ background-color: rgb(229 231 235);
130
+ }
131
+
132
+ &:deep(.vue-slider-dot-handle) {
133
+ // apply bg-blue-500 to the handle when active
134
+ @apply bg-lightPrimary;
135
+ border: none;
136
+ box-shadow: none;
137
+ }
138
+
139
+ &:deep(.vue-slider-dot-handle:hover) {
140
+ @apply bg-lightPrimary;
141
+ filter: brightness(1.1);
142
+ border: none;
143
+ box-shadow: none;
144
+ }
145
+
146
+ &:deep(.vue-slider-process) {
147
+ @apply bg-lightPrimaryOpacity;
148
+
149
+ }
150
+
151
+ &:deep(.vue-slider-process:hover) {
152
+ filter: brightness(1.1);
153
+ @apply bg-lightPrimaryOpacity;
154
+ }
155
+ }
156
+ </style>
@@ -0,0 +1,232 @@
1
+ <template>
2
+ <!-- drawer component -->
3
+ <div id="drawer-navigation"
4
+
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-gray-900"
6
+
7
+ :class="show ? 'top-0 transform-none' : ''"
8
+ tabindex="-1" aria-labelledby="drawer-navigation-label"
9
+ :style="{ height: `calc(100vh ` }"
10
+ >
11
+ <h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
12
+ {{ $t('Filters') }}
13
+
14
+ <button type="button" @click="$emit('hide')" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 absolute end-2.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" >
15
+ <svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
16
+ <span class="sr-only">{{ $t('Close menu') }}</span>
17
+ </button>
18
+ </h5>
19
+
20
+ <div class="py-4 ">
21
+ <ul class="space-y-3 font-medium">
22
+ <li v-for="c in columnsWithFilter" :key="c">
23
+ <p class="dark:text-gray-400">{{ c.label }}</p>
24
+
25
+ <Select
26
+ v-if="c.foreignResource"
27
+ multiple
28
+ class="w-full"
29
+ :options="columnOptions[c.name] || []"
30
+ @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
31
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
32
+ />
33
+ <Select
34
+ multiple
35
+ class="w-full"
36
+ v-else-if="c.type === 'boolean'"
37
+ :options="[
38
+ { label: $t('Yes'), value: true },
39
+ { label: $t('No'), value: false },
40
+ // if field is not required, undefined might be there, and user might want to filter by it
41
+ ...(c.required ? [] : [ { label: $t('Unset'), value: undefined } ])
42
+ ]"
43
+ @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
44
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
45
+ />
46
+
47
+ <Select
48
+ multiple
49
+ class="w-full"
50
+ v-else-if="c.enum"
51
+ :options="c.enum"
52
+ @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event || undefined })"
53
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
54
+ />
55
+
56
+ <input
57
+ v-else-if="[ 'string', 'text' ].includes(c.type)"
58
+ type="text" class="w-full py-1 px-2 border border-gray-300 rounded-md 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"
59
+ :placeholder="$t('Search')"
60
+ @input="setFilterItem({ column: c, operator: 'ilike', value: $event.target.value || undefined })"
61
+ :value="getFilterItem({ column: c, operator: 'ilike' })"
62
+ >
63
+
64
+ <CustomDateRangePicker
65
+ v-else-if="['datetime'].includes(c.type)"
66
+ :column="c"
67
+ :valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
68
+ @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
69
+ :valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
70
+ @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
71
+ />
72
+
73
+ <input
74
+ v-else-if="[ 'date', 'time' ].includes(c.type)"
75
+ type="text" class="w-full py-1 px-2 border border-gray-300 rounded-md"
76
+ :placeholder="$t('Search datetime')"
77
+ @input="setFilterItem({ column: c, operator: 'ilike', value: $event.target.value || undefined })"
78
+ :value="getFilterItem({ column: c, operator: 'ilike' })"
79
+ >
80
+
81
+ <CustomRangePicker
82
+ v-else-if="['integer', 'decimal', 'float'].includes(c.type) && c.allowMinMaxQuery"
83
+ :min="getFilterMinValue(c.name)"
84
+ :max="getFilterMaxValue(c.name)"
85
+ :valueStart="getFilterItem({ column: c, operator: 'gte' })"
86
+ @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
87
+ :valueEnd="getFilterItem({ column: c, operator: 'lte' })"
88
+ @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
89
+ />
90
+
91
+ <div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
92
+ <input
93
+ type="number" aria-describedby="helper-text-explanation"
94
+ 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"
95
+ :placeholder="$t('From')"
96
+ @input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
97
+ :value="getFilterItem({ column: c, operator: 'gte' })"
98
+ >
99
+ <input
100
+ type="number" aria-describedby="helper-text-explanation"
101
+ 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"
102
+ :placeholder="$t('To')"
103
+ @input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
104
+ :value="getFilterItem({ column: c, operator: 'lte' })"
105
+ >
106
+ </div>
107
+
108
+ </li>
109
+ </ul>
110
+ </div>
111
+
112
+ <div class="flex justify-end gap-2">
113
+ <button
114
+ :disabled="!filtersStore.filters.length"
115
+ type="button"
116
+ 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-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 disabled:opacity-50 disabled:cursor-not-allowed"
117
+ @click="clear">{{ $t('Clear all') }}</button>
118
+
119
+ </div>
120
+ </div>
121
+
122
+ <div v-if="show" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
123
+ @click="$emit('hide')">
124
+ </div>
125
+ </template>
126
+
127
+ <script setup>
128
+ import { watch, computed } from 'vue'
129
+ import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
130
+ import { callAdminForthApi } from '@/utils';
131
+ import { useRouter } from 'vue-router';
132
+ import { computedAsync } from '@vueuse/core'
133
+ import CustomRangePicker from "@/components/CustomRangePicker.vue";
134
+ import { useFiltersStore } from '@/stores/filters';
135
+ import Select from '@/afcl/Select.vue';
136
+
137
+ const filtersStore = useFiltersStore();
138
+
139
+
140
+ // props: columns
141
+ // add support for v-model:filers
142
+ const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
143
+ const emits = defineEmits(['update:filters', 'hide']);
144
+
145
+ const router = useRouter();
146
+
147
+
148
+ const columnsWithFilter = computed(
149
+ () => props.columns?.filter(column => column.showIn.includes('filter')) || []
150
+ );
151
+
152
+ const columnOptions = computedAsync(async () => {
153
+ const ret = {};
154
+ if (!props.columns) {
155
+ return ret;
156
+ }
157
+ await Promise.all(
158
+ Object.values(props.columns).map(async (column) => {
159
+ if (column.foreignResource) {
160
+ const list = await callAdminForthApi({
161
+ method: 'POST',
162
+ path: `/get_resource_foreign_data`,
163
+ body: {
164
+ resourceId: router.currentRoute.value.params.resourceId,
165
+ column: column.name,
166
+ limit: 1000,
167
+ offset: 0,
168
+ },
169
+ });
170
+ ret[column.name] = list.items;
171
+ }
172
+ })
173
+ );
174
+
175
+ return ret;
176
+ }, {});
177
+
178
+
179
+ // sync 'body' class 'overflow-hidden' with show prop show
180
+ watch(() => props.show, (show) => {
181
+ if (show) {
182
+ document.body.classList.add('overflow-hidden');
183
+ } else {
184
+ document.body.classList.remove('overflow-hidden');
185
+ }
186
+ });
187
+
188
+ // filters is a array of objects
189
+ // {
190
+ // field: 'name',
191
+ // value: 'John',
192
+ // operator: 'like'
193
+ // }
194
+
195
+ function setFilterItem({ column, operator, value }) {
196
+
197
+ const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
198
+ if (value === undefined) {
199
+ if (index !== -1) {
200
+ filtersStore.filters.splice(index, 1);
201
+ }
202
+ } else {
203
+ if (index === -1) {
204
+ filtersStore.setFilter({ field: column.name, value, operator });
205
+ } else {
206
+ filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
207
+ }
208
+ }
209
+ emits('update:filters', [...filtersStore.filters]);
210
+ }
211
+
212
+ function getFilterItem({ column, operator }) {
213
+ return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
214
+ }
215
+
216
+ async function clear() {
217
+ filtersStore.clearFilters();
218
+ emits('update:filters', [...filtersStore.filters]);
219
+ }
220
+
221
+ function getFilterMinValue(columnName) {
222
+ if(props.columnsMinMax && props.columnsMinMax[columnName]) {
223
+ return props.columnsMinMax[columnName]?.min
224
+ }
225
+ }
226
+
227
+ function getFilterMaxValue(columnName) {
228
+ if(props.columnsMinMax && props.columnsMinMax[columnName]) {
229
+ return props.columnsMinMax[columnName]?.max
230
+ }
231
+ }
232
+ </script>
@@ -0,0 +1,218 @@
1
+ <template>
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">
4
+ {{ group.groupName }}
5
+ </div>
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 ">
8
+ <tr>
9
+ <th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-6 py-3 hidden md:w-52 md:table-cell">
10
+ {{ $t('Field') }}
11
+ </th>
12
+ <th scope="col" :class="{'rounded-tr-lg': !group.groupName}" class="px-6 py-3 hidden md:table-cell">
13
+ {{ $t('Value') }}
14
+ </th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ <tr
19
+ v-for="(column, i) in group.columns"
20
+ :key="column.name"
21
+ v-if="currentValues !== null"
22
+ class="bg-ligftForm dark:bg-gray-800 dark:border-gray-700 block md:table-row"
23
+ :class="{ 'border-b': i !== group.columns.length - 1}"
24
+ >
25
+ <td class="px-6 py-4 flex items-center block md:table-cell pb-0 md:pb-4"
26
+ :class="{'rounded-bl-lg border-b-none': i === group.columns.length - 1}"> <!--align-top-->
27
+ <span class="flex items-center gap-1">
28
+ {{ column.label }}
29
+ <Tooltip v-if="column.required[mode]">
30
+
31
+ <IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
32
+ :class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
33
+ />
34
+
35
+ <template #tooltip>
36
+ {{ $t('Required field') }}
37
+ </template>
38
+ </Tooltip>
39
+ </span>
40
+ </td>
41
+ <td class="px-6 py-4 whitespace-pre-wrap relative block md:table-cell rounded-br-lg "
42
+ :class="{'rounded-br-lg': i === group.columns.length - 1}">
43
+ <template v-if="column?.components?.[props.source]?.file">
44
+ <component
45
+ :is="getCustomComponent(column.components[props.source])"
46
+ :column="column"
47
+ :value="currentValues[column.name]"
48
+ @update:value="setCurrentValue(column.name, $event)"
49
+ :meta="column.components[props.source].meta"
50
+ :record="currentValues"
51
+ @update:inValidity="customComponentsInValidity[column.name] = $event"
52
+ @update:emptiness="customComponentsEmptiness[column.name] = $event"
53
+ />
54
+ </template>
55
+ <template v-else>
56
+ <Select
57
+ class="w-full"
58
+ v-if="column.foreignResource"
59
+ :options="columnOptions[column.name] || []"
60
+ :placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
61
+ :modelValue="currentValues[column.name]"
62
+ :readonly="column.editReadonly && source === 'edit'"
63
+ @update:modelValue="setCurrentValue(column.name, $event)"
64
+ ></Select>
65
+ <Select
66
+ class="w-full"
67
+ v-else-if="column.enum"
68
+ :options="column.enum"
69
+ :modelValue="currentValues[column.name]"
70
+ :readonly="column.editReadonly && source === 'edit'"
71
+ @update:modelValue="setCurrentValue(column.name, $event)"
72
+ ></Select>
73
+ <Select
74
+ class="w-full"
75
+ v-else-if="column.type === 'boolean'"
76
+ :options="getBooleanOptions(column)"
77
+ :modelValue="currentValues[column.name]"
78
+ :readonly="column.editReadonly && source === 'edit'"
79
+ @update:modelValue="setCurrentValue(column.name, $event)"
80
+ ></Select>
81
+ <input
82
+ v-else-if="['integer'].includes(column.type)"
83
+ type="number"
84
+ step="1"
85
+ 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"
86
+ placeholder="0"
87
+ :readonly="column.editReadonly && source === 'edit'"
88
+ :value="currentValues[column.name]"
89
+ @input="setCurrentValue(column.name, $event.target.value)"
90
+ >
91
+ <CustomDatePicker
92
+ v-else-if="['datetime'].includes(column.type)"
93
+ :column="column"
94
+ :valueStart="currentValues[column.name]"
95
+ auto-hide
96
+ @update:valueStart="setCurrentValue(column.name, $event)"
97
+ :readonly="column.editReadonly && source === 'edit'"
98
+ />
99
+ <input
100
+ v-else-if="['decimal', 'float'].includes(column.type)"
101
+ type="number"
102
+ step="0.1"
103
+ 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"
104
+ placeholder="0.0"
105
+ :value="currentValues[column.name]"
106
+ @input="setCurrentValue(column.name, $event.target.value)"
107
+ :readonly="column.editReadonly && source === 'edit'"
108
+ />
109
+ <textarea
110
+ v-else-if="['text', 'richtext'].includes(column.type)"
111
+ 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"
112
+ :placeholder="$t('Text')"
113
+ :value="currentValues[column.name]"
114
+ @input="setCurrentValue(column.name, $event.target.value)"
115
+ :readonly="column.editReadonly && source === 'edit'"
116
+ >
117
+ </textarea>
118
+ <textarea
119
+ v-else-if="['json'].includes(column.type)"
120
+ 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"
121
+ :placeholder="$t('Text')"
122
+ :value="currentValues[column.name]"
123
+ @input="setCurrentValue(column.name, $event.target.value)"
124
+ >
125
+ </textarea>
126
+ <input
127
+ v-else
128
+ :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
129
+ 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"
130
+ :placeholder="$t('Text')"
131
+ :value="currentValues[column.name]"
132
+ @input="setCurrentValue(column.name, $event.target.value)"
133
+ autocomplete="false"
134
+ data-lpignore="true"
135
+ readonly
136
+ ref="readonlyInputs"
137
+ @focus="onFocusHandler($event, column, source)"
138
+ >
139
+
140
+ <button
141
+ v-if="column.masked"
142
+ type="button"
143
+ @click="unmasked[column.name] = !unmasked[column.name]"
144
+ class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
145
+ >
146
+ <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
147
+ <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
148
+ </button>
149
+ </template>
150
+ <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
151
+ <div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
152
+ </td>
153
+ </tr>
154
+ </tbody>
155
+ </table>
156
+ </div>
157
+ </template>
158
+
159
+ <script setup lang="ts">
160
+ import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
161
+ import CustomDatePicker from "@/components/CustomDatePicker.vue";
162
+ import Select from '@/afcl/Select.vue';
163
+ import { getCustomComponent } from '@/utils';
164
+ import { Tooltip } from '@/afcl';
165
+ import { ref, computed, watch, type Ref } from 'vue';
166
+ import { useI18n } from 'vue-i18n';
167
+
168
+ const { t } = useI18n();
169
+
170
+ const props = defineProps<{
171
+ source: 'create' | 'edit',
172
+ group: any,
173
+ mode: string,
174
+ validating: boolean,
175
+ currentValues: any,
176
+ unmasked: any,
177
+ columnError: (column: any) => string,
178
+ setCurrentValue: (columnName: string, value: any) => void,
179
+ columnOptions: any,
180
+ }>();
181
+
182
+ const customComponentsInValidity: Ref<Record<string, boolean>> = ref({});
183
+ const customComponentsEmptiness: Ref<Record<string, boolean>> = ref({});
184
+
185
+ const getBooleanOptions = (column: any) => {
186
+ const options: Array<{ label: string; value: boolean | null }> = [
187
+ { label: t('Yes'), value: true },
188
+ { label: t('No'), value: false },
189
+ ];
190
+ if (!column.required[props.mode]) {
191
+ options.push({ label: t('Unset'), value: null });
192
+ }
193
+ return options;
194
+ };
195
+ function onFocusHandler(event:FocusEvent, column:any, source:string, ) {
196
+ const focusedInput = event.target as HTMLInputElement;
197
+ if(!focusedInput) return;
198
+ if (column.editReadonly && source === 'edit') return;
199
+ else {
200
+ focusedInput.removeAttribute('readonly');
201
+ }
202
+ }
203
+
204
+
205
+ const emit = defineEmits(['update:customComponentsInValidity', 'update:customComponentsEmptiness']);
206
+
207
+
208
+
209
+
210
+ watch(customComponentsInValidity, (newVal) => {
211
+ emit('update:customComponentsInValidity', newVal);
212
+ });
213
+
214
+ watch(customComponentsEmptiness, (newVal) => {
215
+ emit('update:customComponentsEmptiness', newVal);
216
+ });
217
+
218
+ </script>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ defineProps<{
3
+ msg: string
4
+ }>()
5
+ </script>
6
+
7
+ <template>
8
+ <div class="greetings">
9
+ <h1 class="green">{{ msg }}</h1>
10
+ <h3>
11
+ {{ $t('You’ve successfully created a project with') }}
12
+ </h3>
13
+ </div>
14
+ </template>
15
+
16
+ <style scoped>
17
+ </style>
@@ -0,0 +1,41 @@
1
+ <template>
2
+ <RouterLink
3
+ :to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
4
+ class="flex group items-center py-2 text-lightSidebarText dark:text-darkSidebarText rounded-default hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarActive dark:active:bg-darkSidebarHover" role="menuitem"
5
+ :class="{
6
+ 'px-4': isChild,
7
+ 'px-2': !isChild,
8
+ 'bg-lightSidebarItemActive dark:bg-darkSidebarItemActive': item.resourceId ?
9
+ ($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
10
+ ($route.name === item.path)
11
+ }"
12
+ >
13
+ <component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons dark:text-darkSidebarIcons transition duration-75 group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover" ></component>
14
+ <span class="ms-3">{{ item.label }}</span>
15
+ <span v-if="item.badge" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
16
+ fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent"
17
+ >
18
+
19
+ <Tooltip v-if="item.badgeTooltip">
20
+ {{ item.badge }}
21
+
22
+ <template #tooltip>
23
+ {{ item.badgeTooltip }}
24
+ </template>
25
+ </Tooltip>
26
+ <template v-else>
27
+ {{ item.badge }}
28
+ </template>
29
+
30
+ </span>
31
+
32
+ </RouterLink>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { getIcon } from '@/utils';
37
+ import { Tooltip } from '@/afcl';
38
+ const props = defineProps(['item', 'isChild']);
39
+
40
+
41
+ </script>