adminforth 1.1.31 → 1.1.33

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 (105) hide show
  1. package/dist/index.js +9 -0
  2. package/dist/modules/codeInjector.js +19 -7
  3. package/dist/modules/styles.js +2 -2
  4. package/dist/spa/index.html +4 -4
  5. package/dist/spa/package-lock.json +206 -5
  6. package/dist/spa/package.json +8 -2
  7. package/dist/spa/src/App.vue +171 -72
  8. package/dist/spa/src/assets/logo.svg +19 -1
  9. package/dist/spa/src/components/AcceptModal.vue +3 -10
  10. package/dist/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
  11. package/dist/spa/src/components/CustomDatePicker.vue +19 -6
  12. package/dist/spa/src/components/CustomDateRangePicker.vue +14 -5
  13. package/dist/spa/src/components/Dropdown.vue +19 -5
  14. package/dist/spa/src/components/Filters.vue +91 -37
  15. package/dist/spa/src/components/MenuLink.vue +2 -2
  16. package/dist/spa/src/components/ResourceForm.vue +163 -103
  17. package/dist/spa/src/components/ValueRenderer.vue +22 -6
  18. package/dist/spa/src/main.ts +1 -1
  19. package/dist/spa/src/router/index.ts +30 -19
  20. package/dist/spa/src/stores/core.ts +65 -59
  21. package/dist/spa/src/stores/modal.ts +13 -3
  22. package/dist/spa/src/utils.ts +65 -8
  23. package/dist/spa/src/views/CreateView.vue +76 -16
  24. package/dist/spa/src/views/EditView.vue +76 -14
  25. package/dist/spa/src/views/ListView.vue +88 -360
  26. package/dist/spa/src/views/LoginView.vue +17 -5
  27. package/dist/spa/src/views/ResourceParent.vue +1 -1
  28. package/dist/spa/src/views/ShowView.vue +114 -19
  29. package/dist/spa/tailwind.config.js +5 -2
  30. package/dist/spa/vite.config.ts +6 -0
  31. package/index.ts +11 -0
  32. package/modules/codeInjector.ts +19 -1
  33. package/modules/styles.ts +2 -2
  34. package/package.json +1 -1
  35. package/spa/index.html +1 -1
  36. package/spa/src/App.vue +3 -2
  37. package/spa/src/components/ResourceForm.vue +1 -1
  38. package/types/AdminForthConfig.ts +15 -0
  39. package/dist/plugins/AccessControl/index.js +0 -13
  40. package/dist/plugins/AccessControl/types.js +0 -1
  41. package/dist/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +0 -248
  42. package/dist/spa/public/favicon.ico +0 -0
  43. package/dist/spa/spa/.eslintrc.cjs +0 -14
  44. package/dist/spa/spa/.vscode/extensions.json +0 -6
  45. package/dist/spa/spa/README.md +0 -39
  46. package/dist/spa/spa/env.d.ts +0 -1
  47. package/dist/spa/spa/index.html +0 -23
  48. package/dist/spa/spa/package-lock.json +0 -4359
  49. package/dist/spa/spa/package.json +0 -47
  50. package/dist/spa/spa/postcss.config.js +0 -6
  51. package/dist/spa/spa/public/favicon.ico +0 -0
  52. package/dist/spa/spa/src/App.vue +0 -270
  53. package/dist/spa/spa/src/assets/base.css +0 -0
  54. package/dist/spa/spa/src/assets/logo.svg +0 -19
  55. package/dist/spa/spa/src/components/AcceptModal.vue +0 -45
  56. package/dist/spa/spa/src/components/Breadcrumbs.vue +0 -40
  57. package/dist/spa/spa/src/components/BreadcrumbsWithButtons.vue +0 -26
  58. package/dist/spa/spa/src/components/CustomDatePicker.vue +0 -174
  59. package/dist/spa/spa/src/components/CustomDateRangePicker.vue +0 -218
  60. package/dist/spa/spa/src/components/Dropdown.vue +0 -168
  61. package/dist/spa/spa/src/components/Filters.vue +0 -211
  62. package/dist/spa/spa/src/components/HelloWorld.vue +0 -17
  63. package/dist/spa/spa/src/components/MenuLink.vue +0 -24
  64. package/dist/spa/spa/src/components/ResourceForm.vue +0 -265
  65. package/dist/spa/spa/src/components/ShowTableItem.vue +0 -14
  66. package/dist/spa/spa/src/components/SingleSkeletLoader.vue +0 -13
  67. package/dist/spa/spa/src/components/ValueRenderer.vue +0 -60
  68. package/dist/spa/spa/src/components/icons/IconCalendar.vue +0 -5
  69. package/dist/spa/spa/src/components/icons/IconCommunity.vue +0 -7
  70. package/dist/spa/spa/src/components/icons/IconDocumentation.vue +0 -7
  71. package/dist/spa/spa/src/components/icons/IconEcosystem.vue +0 -7
  72. package/dist/spa/spa/src/components/icons/IconSupport.vue +0 -7
  73. package/dist/spa/spa/src/components/icons/IconTime.vue +0 -5
  74. package/dist/spa/spa/src/components/icons/IconTooling.vue +0 -19
  75. package/dist/spa/spa/src/index.scss +0 -26
  76. package/dist/spa/spa/src/main.ts +0 -18
  77. package/dist/spa/spa/src/router/index.ts +0 -64
  78. package/dist/spa/spa/src/stores/core.ts +0 -144
  79. package/dist/spa/spa/src/stores/modal.ts +0 -48
  80. package/dist/spa/spa/src/utils.ts +0 -101
  81. package/dist/spa/spa/src/views/CreateView.vue +0 -163
  82. package/dist/spa/spa/src/views/EditView.vue +0 -165
  83. package/dist/spa/spa/src/views/HomeView.vue +0 -8
  84. package/dist/spa/spa/src/views/ListView.vue +0 -257
  85. package/dist/spa/spa/src/views/LoginView.vue +0 -134
  86. package/dist/spa/spa/src/views/ResourceParent.vue +0 -18
  87. package/dist/spa/spa/src/views/ShowView.vue +0 -188
  88. package/dist/spa/spa/tailwind.config.js +0 -15
  89. package/dist/spa/spa/tsconfig.app.json +0 -14
  90. package/dist/spa/spa/tsconfig.json +0 -11
  91. package/dist/spa/spa/tsconfig.node.json +0 -19
  92. package/dist/spa/spa/vite.config.ts +0 -49
  93. package/dist/spa/src/views/HomeView.vue +0 -8
  94. package/dist/types.js +0 -30
  95. /package/dist/spa/{spa/public → public/assets}/favicon.png +0 -0
  96. /package/dist/spa/{spa/src → src}/components/CustomRangePicker.vue +0 -0
  97. /package/dist/spa/{spa/src → src}/components/ResourceListTable.vue +0 -0
  98. /package/dist/spa/{spa/src → src}/components/Toast.vue +0 -0
  99. /package/dist/spa/{spa/src → src}/composables/useFrontendApi.ts +0 -0
  100. /package/dist/spa/{spa/src → src}/composables/useStores.ts +0 -0
  101. /package/dist/spa/{spa/src → src}/spa_types/core.ts +0 -0
  102. /package/dist/spa/{spa/src → src}/stores/filters.ts +0 -0
  103. /package/dist/spa/{spa/src → src}/stores/toast.ts +0 -0
  104. /package/dist/spa/{spa/src → src}/stores/user.ts +0 -0
  105. /package/spa/public/{favicon.png → assets/favicon.png} +0 -0
@@ -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"
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
@@ -22,11 +22,17 @@
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="filtersStore.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
- :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 || []"
30
36
  />
31
37
 
32
38
  <Dropdown
@@ -34,7 +40,7 @@
34
40
  :options="c.enum"
35
41
  :allowCustom="c.allowCustom"
36
42
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
37
- :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 || []"
38
44
  />
39
45
 
40
46
  <input
@@ -48,9 +54,9 @@
48
54
  <CustomDateRangePicker
49
55
  v-else-if="['datetime'].includes(c.type)"
50
56
  :column="c"
51
- :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"
52
58
  @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
53
- :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"
54
60
  @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
55
61
  />
56
62
 
@@ -62,22 +68,32 @@
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) && c.allowMinMaxQuery"
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
+ />
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>
81
97
 
82
98
  </li>
83
99
  </ul>
@@ -85,33 +101,71 @@
85
101
 
86
102
  <div class="flex justify-end gap-2">
87
103
  <button
88
- :disabled="!filters.length"
104
+ :disabled="!filtersStore.filters.length"
89
105
  type="button"
90
- 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"
91
107
  @click="clear">Clear all</button>
92
108
 
93
109
  </div>
94
110
  </div>
95
111
 
96
- <div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
112
+ <div v-if="show" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
97
113
  @click="$emit('hide')">
98
114
  </div>
99
115
  </template>
100
116
 
101
117
  <script setup>
102
- import { watch, computed } from 'vue'
118
+ import { watch, computed, ref, onMounted } from 'vue'
103
119
  import Dropdown from '@/components/Dropdown.vue';
104
120
  import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
121
+ import { callAdminForthApi } from '@/utils';
122
+ import { useRouter } from 'vue-router';
123
+ import { computedAsync } from '@vueuse/core'
124
+ import CustomRangePicker from "@/components/CustomRangePicker.vue";
125
+ import { useFiltersStore } from '@/stores/filters';
126
+
127
+ const filtersStore = useFiltersStore();
128
+
105
129
 
106
130
  // props: columns
107
131
  // add support for v-model:filers
108
132
  const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
109
133
  const emits = defineEmits(['update:filters', 'hide']);
110
134
 
111
- const columnsWithFilter = computed(
135
+ const router = useRouter();
136
+
137
+
138
+ const columnsWithFilter = computed(
112
139
  () => props.columns?.filter(column => column.showIn.includes('filter')) || []
113
140
  );
114
141
 
142
+ const columnOptions = computedAsync(async () => {
143
+ const ret = {};
144
+ if (!props.columns) {
145
+ return ret;
146
+ }
147
+ await Promise.all(
148
+ Object.values(props.columns).map(async (column) => {
149
+ if (column.foreignResource) {
150
+ const list = await callAdminForthApi({
151
+ method: 'POST',
152
+ path: `/get_resource_foreign_data`,
153
+ body: {
154
+ resourceId: router.currentRoute.value.params.resourceId,
155
+ column: column.name,
156
+ limit: 1000,
157
+ offset: 0,
158
+ },
159
+ });
160
+ ret[column.name] = list.items;
161
+ }
162
+ })
163
+ );
164
+
165
+ return ret;
166
+ }, {});
167
+
168
+
115
169
  // sync 'body' class 'overflow-hidden' with show prop show
116
170
  watch(() => props.show, (show) => {
117
171
  if (show) {
@@ -130,28 +184,28 @@ watch(() => props.show, (show) => {
130
184
 
131
185
  function setFilterItem({ column, operator, value }) {
132
186
 
133
- 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);
134
188
  if (value === undefined) {
135
189
  if (index !== -1) {
136
- props.filters.splice(index, 1);
190
+ filtersStore.filters.splice(index, 1);
137
191
  }
138
- emits('update:filters', [...props.filters]);
192
+ emits('update:filters', [...filtersStore.filters]);
139
193
  return;
140
194
  } else {
141
195
  if (index === -1) {
142
- props.filters.push({ field: column.name, value, operator });
196
+ filtersStore.setFilter({ field: column.name, value, operator });
143
197
  } else {
144
- props.filters[index].value = value;
198
+ filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
145
199
  }
146
200
  }
147
- emits('update:filters', [...props.filters]);
201
+ emits('update:filters', [...filtersStore.filters]);
148
202
  }
149
203
 
150
204
  function getFilterItem({ column, operator }) {
151
- 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 || '';
152
206
  }
153
207
 
154
208
  async function clear() {
155
- emits('update:filters', []);
209
+ filtersStore.clearFilters();
156
210
  }
157
211
  </script>
@@ -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
  }"
@@ -1,12 +1,12 @@
1
1
  <template>
2
- <div>
2
+ <div class="rounded-default">
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 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">
9
- <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
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-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,114 @@
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 "> <!--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
+ </td>
46
+ <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
47
+ <Dropdown
48
+ single
49
+ v-if="column.foreignResource"
50
+ :options="columnOptions[column.name] || []"
51
+ :placeholder = "columnOptions[column.name]?.length || 'There are no options available'"
52
+ :modelValue="currentValues[column.name]"
53
+ @update:modelValue="setCurrentValue(column.name, $event)"
54
+ ></Dropdown>
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
+ auto-hide
83
+ @update:valueStart="setCurrentValue(column.name, $event)"
84
+ />
85
+ <input
86
+ v-else-if="['decimal', 'float'].includes(column.type)"
87
+ type="number"
88
+ step="0.1"
89
+ 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"
90
+ placeholder="0.0"
91
+ :value="currentValues[column.name]"
92
+ @input="setCurrentValue(column.name, $event.target.value)"
93
+ />
94
+ <textarea
95
+ v-else-if="['text'].includes(column.type)"
96
+ 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"
97
+ placeholder="Text"
98
+ :value="currentValues[column.name]"
99
+ @input="setCurrentValue(column.name, $event.target.value)"
100
+ >
101
+ </textarea>
102
+ <input
103
+ v-else
104
+ :type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
105
+ 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"
106
+ placeholder="Text"
107
+ :value="currentValues[column.name]"
108
+ @input="setCurrentValue(column.name, $event.target.value)"
109
+ autocomplete="false"
110
+ data-lpignore="true"
111
+ readonly
112
+ onfocus="this.removeAttribute('readonly');"
113
+ >
114
+
115
+ <button
116
+ v-if="column.masked"
117
+ type="button"
118
+ @click="unmasked[column.name] = !unmasked[column.name]"
119
+ class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
120
+ >
121
+ <IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
122
+ <IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
123
+ </button>
124
+ <div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
125
+
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>
127
+ </td>
128
+ </template>
112
129
  </tr>
113
130
 
114
131
  </tbody>
@@ -121,19 +138,24 @@
121
138
 
122
139
  <script setup>
123
140
 
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
141
  import CustomDatePicker from "@/components/CustomDatePicker.vue";
142
+ import Dropdown from '@/components/Dropdown.vue';
143
+ import { useCoreStore } from '@/stores/core';
144
+ import { callAdminForthApi } from '@/utils';
145
+ import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
146
+ import { computedAsync } from '@vueuse/core';
147
+ import { initFlowbite } from 'flowbite';
148
+ import { computed, onMounted, ref, watch } from 'vue';
149
+ import { useRouter } from 'vue-router';
150
+
151
+ const router = useRouter();
131
152
 
132
153
  const props = defineProps({
133
154
  loading: Boolean,
134
- resourceColumns: Object,
155
+ resource: Object,
135
156
  record: Object,
136
157
  validating: Boolean,
158
+ customComponentsPerColumn: Object,
137
159
  });
138
160
 
139
161
 
@@ -145,6 +167,7 @@ const emit = defineEmits(['update:record', 'update:isValid']);
145
167
 
146
168
  const currentValues = ref({});
147
169
 
170
+
148
171
  const columnError = (column) => {
149
172
  const val = computed(() => {
150
173
  if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
@@ -166,14 +189,28 @@ const columnError = (column) => {
166
189
  return `This field must be less than ${column.maxValue}`;
167
190
  }
168
191
  }
192
+ if ( column.validation && column.validation.length ){
193
+ const validationArray = column.validation;
194
+ for (let i = 0; i < validationArray.length; i++) {
195
+ if (validationArray[i].regExp) {
196
+ const regExp = new RegExp(validationArray[i].regExp);
197
+ let value = currentValues.value[column.name];
198
+ if (value === undefined || value === null) {
199
+ value = '';
200
+ }
201
+ if (!regExp.test(value)) {
202
+ return validationArray[i].message;
203
+ }
204
+ }
205
+ }
206
+
207
+ }
169
208
  return null;
170
209
  });
171
210
  return val.value;
172
211
  };
173
212
 
174
213
 
175
-
176
-
177
214
  const setCurrentValue = (key, value) => {
178
215
  currentValues.value[key] = value;
179
216
 
@@ -185,13 +222,36 @@ onMounted(() => {
185
222
  currentValues.value[key] = props.record[key];
186
223
  });
187
224
  initFlowbite();
225
+
226
+
188
227
  });
189
228
 
229
+ const columnOptions = computedAsync(async () => {
230
+ return (await Promise.all(
231
+ Object.values(props.resource.columns).map(async (column) => {
232
+ if (column.foreignResource) {
233
+ const list = await callAdminForthApi({
234
+ method: 'POST',
235
+ path: `/get_resource_foreign_data`,
236
+ body: {
237
+ resourceId: router.currentRoute.value.params.resourceId,
238
+ column: column.name,
239
+ limit: 1000,
240
+ offset: 0,
241
+ },
242
+ });
243
+ return { [column.name]: list.items };
244
+ }
245
+ })
246
+ )).reduce((acc, val) => Object.assign(acc, val), {})
247
+
248
+ }, {});
249
+
190
250
  const coreStore = useCoreStore();
191
251
 
192
252
  const editableColumns = computed(() => {
193
253
  const mode = props.record ? 'edit' : 'create';
194
- return props.resourceColumns?.filter(column => column.showIn.includes(mode));
254
+ return props.resource?.columns?.filter(column => column.showIn.includes(mode));
195
255
  });
196
256
 
197
257
  const isValid = computed(() => {
@@ -1,18 +1,29 @@
1
1
  <template>
2
2
  <div>
3
- <span v-if="column.type === 'boolean'">
4
- <span v-if="row[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Yes</span>
3
+
4
+ <span v-if="column.foreignResource">
5
+ <RouterLink v-if="record[column.name]" class="font-medium text-blue-600 dark:text-blue-500 hover:brightness-110"
6
+ :to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: record[column.name].pk } }">
7
+ {{ record[column.name].label }}
8
+ </RouterLink>
9
+ <div v-else>
10
+ <span class="text-gray-400">-</span>
11
+ </div>
12
+ </span>
13
+
14
+ <span v-else-if="column.type === 'boolean'">
15
+ <span v-if="record[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Yes</span>
5
16
  <span v-else class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">No</span>
6
17
  </span>
7
18
  <span v-else-if="column.enum">
8
- {{ column.enum.find(e => e.value === row[column.name])?.label }}
19
+ {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label,route.meta.type) }}
9
20
  </span>
10
21
  <span v-else-if="column.type === 'datetime'">
11
- {{ formatDate(row[column.name]) }}
22
+ {{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
12
23
 
13
24
  </span>
14
25
  <span v-else>
15
- {{ row[column.name] }}
26
+ {{ checkEmptyValues(record[column.name],route.meta.type) }}
16
27
  </span>
17
28
  </div>
18
29
  </template>
@@ -23,17 +34,22 @@
23
34
  import dayjs from 'dayjs';
24
35
  import utc from 'dayjs/plugin/utc';
25
36
  import timezone from 'dayjs/plugin/timezone';
37
+ import {checkEmptyValues} from '@/utils';
38
+ import { useRoute, useRouter } from 'vue-router';
39
+
26
40
 
27
41
  import { useCoreStore } from '@/stores/core';
28
42
 
29
43
  const coreStore = useCoreStore();
44
+ const route = useRoute();
45
+
30
46
 
31
47
  dayjs.extend(utc);
32
48
  dayjs.extend(timezone);
33
49
 
34
50
  const props = defineProps({
35
51
  column: Object,
36
- row: Object
52
+ record: Object
37
53
  });
38
54
 
39
55
 
@@ -9,7 +9,7 @@ import router from './router'
9
9
  const app = createApp(App)
10
10
  /* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */
11
11
 
12
-
12
+
13
13
  app.use(createPinia())
14
14
  app.use(router)
15
15