adminforth 2.4.0-next.7 → 2.4.0-next.71
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.
- package/commands/callTsProxy.js +14 -4
- package/commands/cli.js +12 -4
- package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
- package/commands/createApp/templates/index.ts.hbs +1 -1
- package/commands/createApp/templates/package.json.hbs +1 -1
- package/commands/createApp/utils.js +39 -13
- package/commands/createCustomComponent/configUpdater.js +25 -21
- package/commands/createCustomComponent/fileGenerator.js +1 -1
- package/commands/createCustomComponent/main.js +2 -1
- package/commands/createCustomComponent/templates/login/beforeLogin.vue.hbs +18 -0
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +16 -3
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +14 -14
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -9
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +25 -9
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +50 -1
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +45 -2
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.d.ts +42 -0
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +44 -2
- package/dist/modules/styles.js.map +1 -1
- package/dist/spa/index.html +1 -1
- package/dist/spa/src/App.vue +14 -5
- package/dist/spa/src/afcl/Button.vue +4 -4
- package/dist/spa/src/afcl/Checkbox.vue +21 -13
- package/dist/spa/src/afcl/CountryFlag.vue +3 -1
- package/dist/spa/src/afcl/Dropzone.vue +4 -2
- package/dist/spa/src/afcl/Input.vue +5 -3
- package/dist/spa/src/afcl/JsonViewer.vue +25 -0
- package/dist/spa/src/afcl/Link.vue +1 -1
- package/dist/spa/src/afcl/LinkButton.vue +1 -1
- package/dist/spa/src/afcl/Select.vue +44 -15
- package/dist/spa/src/afcl/Table.vue +8 -8
- package/dist/spa/src/afcl/Toggle.vue +32 -0
- package/dist/spa/src/afcl/Tooltip.vue +1 -1
- package/dist/spa/src/afcl/index.ts +2 -2
- package/dist/spa/src/components/AcceptModal.vue +4 -4
- package/dist/spa/src/components/ColumnValueInput.vue +21 -2
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +1 -0
- package/dist/spa/src/components/CustomDatePicker.vue +2 -2
- package/dist/spa/src/components/CustomDateRangePicker.vue +1 -0
- package/dist/spa/src/components/Filters.vue +73 -28
- package/dist/spa/src/components/GroupsTable.vue +3 -3
- package/dist/spa/src/components/ResourceForm.vue +61 -26
- package/dist/spa/src/components/ResourceListTable.vue +34 -36
- package/dist/spa/src/components/ResourceListTableVirtual.vue +23 -25
- package/dist/spa/src/components/ShowTable.vue +11 -6
- package/dist/spa/src/components/ValueRenderer.vue +4 -4
- package/dist/spa/src/controls/BoolToggle.vue +34 -0
- package/dist/spa/src/spa_types/core.ts +7 -0
- package/dist/spa/src/stores/core.ts +1 -1
- package/dist/spa/src/types/Back.ts +46 -12
- package/dist/spa/src/types/Common.ts +10 -1
- package/dist/spa/src/types/adapters/CompletionAdapter.ts +25 -0
- package/dist/spa/src/types/adapters/EmailAdapter.ts +29 -0
- package/dist/spa/src/types/adapters/ImageGenerationAdapter.ts +50 -0
- package/dist/spa/src/types/adapters/OAuth2Adapter.ts +34 -0
- package/dist/spa/src/types/adapters/StorageAdapter.ts +73 -0
- package/dist/spa/src/types/adapters/index.ts +5 -0
- package/dist/spa/src/utils.ts +209 -0
- package/dist/spa/src/views/CreateView.vue +3 -3
- package/dist/spa/src/views/EditView.vue +1 -1
- package/dist/spa/src/views/ListView.vue +2 -2
- package/dist/spa/src/views/LoginView.vue +39 -37
- package/dist/spa/src/views/ResourceParent.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +3 -3
- package/dist/types/Back.d.ts +40 -9
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +9 -0
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/adapters/CompletionAdapter.d.ts +20 -0
- package/dist/types/adapters/CompletionAdapter.d.ts.map +1 -0
- package/dist/types/adapters/CompletionAdapter.js +2 -0
- package/dist/types/adapters/CompletionAdapter.js.map +1 -0
- package/dist/types/adapters/EmailAdapter.d.ts +21 -0
- package/dist/types/adapters/EmailAdapter.d.ts.map +1 -0
- package/dist/types/adapters/EmailAdapter.js +2 -0
- package/dist/types/adapters/EmailAdapter.js.map +1 -0
- package/dist/types/adapters/ImageGenerationAdapter.d.ts +37 -0
- package/dist/types/adapters/ImageGenerationAdapter.d.ts.map +1 -0
- package/dist/types/adapters/ImageGenerationAdapter.js +2 -0
- package/dist/types/adapters/ImageGenerationAdapter.js.map +1 -0
- package/dist/types/adapters/OAuth2Adapter.d.ts +32 -0
- package/dist/types/adapters/OAuth2Adapter.d.ts.map +1 -0
- package/dist/types/adapters/OAuth2Adapter.js +2 -0
- package/dist/types/adapters/OAuth2Adapter.js.map +1 -0
- package/dist/types/adapters/StorageAdapter.d.ts +63 -0
- package/dist/types/adapters/StorageAdapter.d.ts.map +1 -0
- package/dist/types/adapters/StorageAdapter.js +2 -0
- package/dist/types/adapters/StorageAdapter.js.map +1 -0
- package/dist/types/adapters/index.d.ts +6 -0
- package/dist/types/adapters/index.d.ts.map +1 -0
- package/dist/types/adapters/index.js +2 -0
- package/dist/types/adapters/index.js.map +1 -0
- package/package.json +2 -2
- package/dist/spa/src/types/Adapters.ts +0 -213
- package/dist/types/Adapters.d.ts +0 -168
- package/dist/types/Adapters.d.ts.map +0 -1
- package/dist/types/Adapters.js +0 -2
- package/dist/types/Adapters.js.map +0 -1
|
@@ -19,12 +19,26 @@
|
|
|
19
19
|
ref="input"
|
|
20
20
|
class="w-full min-w-24"
|
|
21
21
|
:options="columnOptions[column.name] || []"
|
|
22
|
+
:searchDisabled="!column.foreignResource.searchableFields"
|
|
23
|
+
@scroll-near-end="loadMoreOptions && loadMoreOptions(column.name)"
|
|
24
|
+
@search="(searchTerm) => {
|
|
25
|
+
if (column.foreignResource.searchableFields && onSearchInput && onSearchInput[column.name]) {
|
|
26
|
+
onSearchInput[column.name](searchTerm);
|
|
27
|
+
}
|
|
28
|
+
}"
|
|
22
29
|
teleportToBody
|
|
23
30
|
:placeholder = "columnOptions[column.name]?.length ?$t('Select...'): $t('There are no options available')"
|
|
24
31
|
:modelValue="value"
|
|
25
32
|
:readonly="(column.editReadonly && source === 'edit') || readonly"
|
|
26
33
|
@update:modelValue="$emit('update:modelValue', $event)"
|
|
27
|
-
|
|
34
|
+
>
|
|
35
|
+
<template #extra-item v-if="columnLoadingState && columnLoadingState[column.name]?.loading">
|
|
36
|
+
<div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
|
|
37
|
+
<Spinner class="w-4 h-4" />
|
|
38
|
+
{{ $t('Loading...') }}
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
</Select>
|
|
28
42
|
<Select
|
|
29
43
|
v-else-if="column.enum"
|
|
30
44
|
ref="input"
|
|
@@ -142,7 +156,8 @@
|
|
|
142
156
|
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
143
157
|
import Select from '@/afcl/Select.vue';
|
|
144
158
|
import Input from '@/afcl/Input.vue';
|
|
145
|
-
import
|
|
159
|
+
import Spinner from '@/afcl/Spinner.vue';
|
|
160
|
+
import { ref, inject } from 'vue';
|
|
146
161
|
import { getCustomComponent } from '@/utils';
|
|
147
162
|
import { useI18n } from 'vue-i18n';
|
|
148
163
|
import { useCoreStore } from '@/stores/core';
|
|
@@ -171,6 +186,10 @@
|
|
|
171
186
|
}
|
|
172
187
|
);
|
|
173
188
|
|
|
189
|
+
const columnLoadingState = inject('columnLoadingState', {} as any);
|
|
190
|
+
const onSearchInput = inject('onSearchInput', {} as any);
|
|
191
|
+
const loadMoreOptions = inject('loadMoreOptions', (() => {}) as any);
|
|
192
|
+
|
|
174
193
|
const input = ref(null);
|
|
175
194
|
|
|
176
195
|
const getBooleanOptions = (column: any) => {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
:currentValues="currentValues"
|
|
14
14
|
:mode="mode"
|
|
15
15
|
:columnOptions="columnOptions"
|
|
16
|
+
:unmasked="unmasked"
|
|
16
17
|
:deletable="!column.editReadonly"
|
|
17
18
|
@update:modelValue="setCurrentValue(column.name, $event, arrayItemIndex)"
|
|
18
19
|
@update:unmasked="$emit('update:unmasked', column.name)"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</div>
|
|
11
11
|
|
|
12
12
|
<input ref="datepickerStartEl" type="text"
|
|
13
|
-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
13
|
+
class="af-pick-date-button bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
14
14
|
:placeholder="$t('Select date')" :disabled="readonly" />
|
|
15
15
|
|
|
16
16
|
</div>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
</div>
|
|
27
27
|
|
|
28
28
|
<input v-model="startTime" type="time" id="start-time" onfocus="this.showPicker()" onclick="this.showPicker()" step="1"
|
|
29
|
-
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
29
|
+
class="af-pick-time-button bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary"
|
|
30
30
|
value="00:00" :disabled="readonly" required/>
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<!-- drawer component -->
|
|
3
3
|
<div id="drawer-navigation"
|
|
4
4
|
|
|
5
|
-
class="fixed
|
|
5
|
+
class="af-filters-sidebar 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
6
|
|
|
7
7
|
:class="show ? 'top-0 transform-none' : ''"
|
|
8
8
|
tabindex="-1" aria-labelledby="drawer-navigation-label"
|
|
@@ -43,9 +43,23 @@
|
|
|
43
43
|
:multiple="c.filterOptions.multiselect"
|
|
44
44
|
class="w-full"
|
|
45
45
|
:options="columnOptions[c.name] || []"
|
|
46
|
+
:searchDisabled="!c.foreignResource.searchableFields"
|
|
47
|
+
@scroll-near-end="loadMoreOptions(c.name)"
|
|
48
|
+
@search="(searchTerm) => {
|
|
49
|
+
if (c.foreignResource.searchableFields && onSearchInput[c.name]) {
|
|
50
|
+
onSearchInput[c.name](searchTerm);
|
|
51
|
+
}
|
|
52
|
+
}"
|
|
46
53
|
@update:modelValue="onFilterInput[c.name]({ column: c, operator: c.filterOptions.multiselect ? 'in' : 'eq', value: c.filterOptions.multiselect ? ($event.length ? $event : undefined) : $event || undefined })"
|
|
47
54
|
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === (c.filterOptions.multiselect ? 'in' : 'eq'))?.value || (c.filterOptions.multiselect ? [] : '')"
|
|
48
|
-
|
|
55
|
+
>
|
|
56
|
+
<template #extra-item v-if="columnLoadingState[c.name]?.loading">
|
|
57
|
+
<div class="text-center text-gray-400 dark:text-gray-300 py-2 flex items-center justify-center gap-2">
|
|
58
|
+
<Spinner class="w-4 h-4" />
|
|
59
|
+
{{ $t('Loading...') }}
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
</Select>
|
|
49
63
|
<Select
|
|
50
64
|
:multiple="c.filterOptions.multiselect"
|
|
51
65
|
class="w-full"
|
|
@@ -136,17 +150,17 @@
|
|
|
136
150
|
</template>
|
|
137
151
|
|
|
138
152
|
<script setup>
|
|
139
|
-
import { watch, computed } from 'vue';
|
|
153
|
+
import { watch, computed, ref, reactive } from 'vue';
|
|
140
154
|
import { useI18n } from 'vue-i18n';
|
|
141
155
|
import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
|
|
142
|
-
import { callAdminForthApi } from '@/utils';
|
|
156
|
+
import { callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers } from '@/utils';
|
|
143
157
|
import { useRouter } from 'vue-router';
|
|
144
|
-
import { computedAsync } from '@vueuse/core'
|
|
145
158
|
import CustomRangePicker from "@/components/CustomRangePicker.vue";
|
|
146
159
|
import { useFiltersStore } from '@/stores/filters';
|
|
147
160
|
import { getCustomComponent } from '@/utils';
|
|
148
161
|
import Input from '@/afcl/Input.vue';
|
|
149
162
|
import Select from '@/afcl/Select.vue';
|
|
163
|
+
import Spinner from '@/afcl/Spinner.vue';
|
|
150
164
|
import debounce from 'debounce';
|
|
151
165
|
|
|
152
166
|
const filtersStore = useFiltersStore();
|
|
@@ -165,31 +179,54 @@ const columnsWithFilter = computed(
|
|
|
165
179
|
() => props.columns?.filter(column => column.showIn.filter) || []
|
|
166
180
|
);
|
|
167
181
|
|
|
168
|
-
const columnOptions =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
});
|
|
186
|
-
ret[column.name] = list.items;
|
|
182
|
+
const columnOptions = ref({});
|
|
183
|
+
const columnLoadingState = reactive({});
|
|
184
|
+
const columnOffsets = reactive({});
|
|
185
|
+
const columnEmptyResultsCount = reactive({});
|
|
186
|
+
|
|
187
|
+
watch(() => props.columns, async (newColumns) => {
|
|
188
|
+
if (!newColumns) return;
|
|
189
|
+
|
|
190
|
+
for (const column of newColumns) {
|
|
191
|
+
if (column.foreignResource) {
|
|
192
|
+
if (!columnOptions.value[column.name]) {
|
|
193
|
+
columnOptions.value[column.name] = [];
|
|
194
|
+
columnLoadingState[column.name] = { loading: false, hasMore: true };
|
|
195
|
+
columnOffsets[column.name] = 0;
|
|
196
|
+
columnEmptyResultsCount[column.name] = 0;
|
|
197
|
+
|
|
198
|
+
await loadMoreOptions(column.name);
|
|
187
199
|
}
|
|
188
|
-
}
|
|
189
|
-
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}, { immediate: true });
|
|
203
|
+
|
|
204
|
+
// Function to load more options for a specific column
|
|
205
|
+
async function loadMoreOptions(columnName, searchTerm = '') {
|
|
206
|
+
return loadMoreForeignOptions({
|
|
207
|
+
columnName,
|
|
208
|
+
searchTerm,
|
|
209
|
+
columns: props.columns,
|
|
210
|
+
resourceId: router.currentRoute.value.params.resourceId,
|
|
211
|
+
columnOptions,
|
|
212
|
+
columnLoadingState,
|
|
213
|
+
columnOffsets,
|
|
214
|
+
columnEmptyResultsCount
|
|
215
|
+
});
|
|
216
|
+
}
|
|
190
217
|
|
|
191
|
-
|
|
192
|
-
|
|
218
|
+
async function searchOptions(columnName, searchTerm) {
|
|
219
|
+
return searchForeignOptions({
|
|
220
|
+
columnName,
|
|
221
|
+
searchTerm,
|
|
222
|
+
columns: props.columns,
|
|
223
|
+
resourceId: router.currentRoute.value.params.resourceId,
|
|
224
|
+
columnOptions,
|
|
225
|
+
columnLoadingState,
|
|
226
|
+
columnOffsets,
|
|
227
|
+
columnEmptyResultsCount
|
|
228
|
+
});
|
|
229
|
+
}
|
|
193
230
|
|
|
194
231
|
|
|
195
232
|
// sync 'body' class 'overflow-hidden' with show prop show
|
|
@@ -221,6 +258,14 @@ const onFilterInput = computed(() => {
|
|
|
221
258
|
}, {});
|
|
222
259
|
});
|
|
223
260
|
|
|
261
|
+
const onSearchInput = computed(() => {
|
|
262
|
+
return createSearchInputHandlers(
|
|
263
|
+
props.columns,
|
|
264
|
+
searchOptions,
|
|
265
|
+
(column) => column.filterOptions?.debounceTimeMs || 300
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
224
269
|
function setFilterItem({ column, operator, value }) {
|
|
225
270
|
|
|
226
271
|
const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:shadow-2xl">
|
|
3
|
-
<div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-
|
|
3
|
+
<div v-if="group.groupName && !group.noTitle" class="text-md font-semibold px-6 py-3 flex flex-1 items-center dark:border-darkFormBorder text-gray-700 bg-lightFormHeading dark:bg-darkFormHeading dark:text-gray-400 rounded-t-lg">
|
|
4
4
|
{{ group.groupName }}
|
|
5
5
|
</div>
|
|
6
6
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
7
|
-
<thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-
|
|
7
|
+
<thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-gray-700 uppercase dark:text-gray-400 bg-lightFormHeading dark:bg-darkFormHeading block md:table-row-group ">
|
|
8
8
|
<tr>
|
|
9
9
|
<th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-6 py-3 hidden md:w-52 md:table-cell">
|
|
10
10
|
{{ $t('Field') }}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
v-for="(column, i) in group.columns"
|
|
20
20
|
:key="column.name"
|
|
21
21
|
v-if="currentValues !== null"
|
|
22
|
-
class="bg-
|
|
22
|
+
class="bg-lightForm dark:bg-darkForm dark:border-darkFormBorder block md:table-row"
|
|
23
23
|
:class="{ 'border-b': i !== group.columns.length - 1}"
|
|
24
24
|
>
|
|
25
25
|
<td class="px-6 py-4 flex items-center block md:table-cell pb-0 md:pb-4"
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
|
|
64
64
|
<script setup lang="ts">
|
|
65
65
|
|
|
66
|
-
import { applyRegexValidation, callAdminForthApi} from '@/utils';
|
|
66
|
+
import { applyRegexValidation, callAdminForthApi, loadMoreForeignOptions, searchForeignOptions, createSearchInputHandlers} from '@/utils';
|
|
67
67
|
import { computedAsync } from '@vueuse/core';
|
|
68
|
-
import { computed, onMounted, ref, watch } from 'vue';
|
|
68
|
+
import { computed, onMounted, reactive, ref, watch, provide } from 'vue';
|
|
69
69
|
import { useRouter, useRoute } from 'vue-router';
|
|
70
70
|
import { useCoreStore } from "@/stores/core";
|
|
71
71
|
import GroupsTable from '@/components/GroupsTable.vue';
|
|
@@ -95,6 +95,11 @@ const currentValues = ref(null);
|
|
|
95
95
|
const customComponentsInValidity = ref({});
|
|
96
96
|
const customComponentsEmptiness = ref({});
|
|
97
97
|
|
|
98
|
+
const columnOptions = ref<Record<string, any[]>>({});
|
|
99
|
+
const columnLoadingState = reactive<Record<string, { loading: boolean; hasMore: boolean }>>({});
|
|
100
|
+
const columnOffsets = reactive<Record<string, number>>({});
|
|
101
|
+
const columnEmptyResultsCount = reactive<Record<string, number>>({});
|
|
102
|
+
|
|
98
103
|
const columnError = (column) => {
|
|
99
104
|
const val = computed(() => {
|
|
100
105
|
if (!currentValues.value) {
|
|
@@ -186,7 +191,7 @@ const validateValue = (type, value, column) => {
|
|
|
186
191
|
};
|
|
187
192
|
|
|
188
193
|
|
|
189
|
-
const setCurrentValue = (key, value, index=null) => {
|
|
194
|
+
const setCurrentValue = (key, value, index = null) => {
|
|
190
195
|
const col = props.resource.columns.find((column) => column.name === key);
|
|
191
196
|
// if field is an array, we need to update the array or individual element
|
|
192
197
|
if (col.type === 'json' && col.isArray?.enabled) {
|
|
@@ -239,6 +244,23 @@ const setCurrentValue = (key, value, index=null) => {
|
|
|
239
244
|
emit('update:record', up);
|
|
240
245
|
};
|
|
241
246
|
|
|
247
|
+
watch(() => props.resource.columns, async (newColumns) => {
|
|
248
|
+
if (!newColumns) return;
|
|
249
|
+
|
|
250
|
+
for (const column of newColumns) {
|
|
251
|
+
if (column.foreignResource) {
|
|
252
|
+
if (!columnOptions.value[column.name]) {
|
|
253
|
+
columnOptions.value[column.name] = [];
|
|
254
|
+
columnLoadingState[column.name] = { loading: false, hasMore: true };
|
|
255
|
+
columnOffsets[column.name] = 0;
|
|
256
|
+
columnEmptyResultsCount[column.name] = 0;
|
|
257
|
+
|
|
258
|
+
await loadMoreOptions(column.name);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}, { immediate: true });
|
|
263
|
+
|
|
242
264
|
onMounted(() => {
|
|
243
265
|
currentValues.value = Object.assign({}, props.record);
|
|
244
266
|
// json values should transform to string
|
|
@@ -266,29 +288,31 @@ onMounted(() => {
|
|
|
266
288
|
emit('update:isValid', isValid.value);
|
|
267
289
|
});
|
|
268
290
|
|
|
269
|
-
|
|
270
|
-
return (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
async function loadMoreOptions(columnName: string, searchTerm = '') {
|
|
292
|
+
return loadMoreForeignOptions({
|
|
293
|
+
columnName,
|
|
294
|
+
searchTerm,
|
|
295
|
+
columns: props.resource.columns,
|
|
296
|
+
resourceId: router.currentRoute.value.params.resourceId as string,
|
|
297
|
+
columnOptions,
|
|
298
|
+
columnLoadingState,
|
|
299
|
+
columnOffsets,
|
|
300
|
+
columnEmptyResultsCount
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function searchOptions(columnName: string, searchTerm: string) {
|
|
305
|
+
return searchForeignOptions({
|
|
306
|
+
columnName,
|
|
307
|
+
searchTerm,
|
|
308
|
+
columns: props.resource.columns,
|
|
309
|
+
resourceId: router.currentRoute.value.params.resourceId as string,
|
|
310
|
+
columnOptions,
|
|
311
|
+
columnLoadingState,
|
|
312
|
+
columnOffsets,
|
|
313
|
+
columnEmptyResultsCount
|
|
314
|
+
});
|
|
315
|
+
}
|
|
292
316
|
|
|
293
317
|
|
|
294
318
|
const editableColumns = computed(() => {
|
|
@@ -330,6 +354,17 @@ const getOtherColumns = () => {
|
|
|
330
354
|
|
|
331
355
|
const otherColumns = getOtherColumns();
|
|
332
356
|
|
|
357
|
+
const onSearchInput = computed(() => {
|
|
358
|
+
return createSearchInputHandlers(
|
|
359
|
+
props.resource.columns,
|
|
360
|
+
searchOptions
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
provide('columnLoadingState', columnLoadingState);
|
|
365
|
+
provide('onSearchInput', onSearchInput);
|
|
366
|
+
provide('loadMoreOptions', loadMoreOptions);
|
|
367
|
+
|
|
333
368
|
watch(() => isValid.value, (value) => {
|
|
334
369
|
emit('update:isValid', value);
|
|
335
370
|
});
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
<!-- skelet loader -->
|
|
7
7
|
<div role="status" v-if="!resource || !resource.columns"
|
|
8
8
|
class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded shadow animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
|
|
9
|
-
|
|
10
9
|
<div role="status" class="max-w-sm animate-pulse">
|
|
11
10
|
<div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
|
|
12
11
|
</div>
|
|
@@ -17,13 +16,13 @@
|
|
|
17
16
|
<!-- table header -->
|
|
18
17
|
<tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
|
|
19
18
|
<td scope="col" class="p-4">
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
</
|
|
19
|
+
<Checkbox
|
|
20
|
+
:modelValue="allFromThisPageChecked"
|
|
21
|
+
:disabled="!rows || !rows.length"
|
|
22
|
+
@update:modelValue="selectAll"
|
|
23
|
+
>
|
|
24
|
+
<span class="sr-only">{{ $t('checkbox') }}</span>
|
|
25
|
+
</Checkbox>
|
|
27
26
|
</td>
|
|
28
27
|
|
|
29
28
|
<td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
|
|
@@ -91,18 +90,16 @@
|
|
|
91
90
|
|
|
92
91
|
:class="{'border-b': rowI !== rows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
|
|
93
92
|
>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
</div>
|
|
105
|
-
</td>
|
|
93
|
+
<td class="w-4 p-4 cursor-default" @click="(e)=>e.stopPropagation()">
|
|
94
|
+
<Checkbox
|
|
95
|
+
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
|
|
96
|
+
@change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
|
|
97
|
+
@click="(e)=>e.stopPropagation()"
|
|
98
|
+
>
|
|
99
|
+
<span class="sr-only">{{ $t('checkbox') }}</span>
|
|
100
|
+
</Checkbox>
|
|
101
|
+
</td>
|
|
102
|
+
|
|
106
103
|
<td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
|
|
107
104
|
<!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
|
|
108
105
|
<component
|
|
@@ -128,7 +125,7 @@
|
|
|
128
125
|
}"
|
|
129
126
|
|
|
130
127
|
>
|
|
131
|
-
<IconEyeSolid class="w-5 h-5 me-2"/>
|
|
128
|
+
<IconEyeSolid class="af-show-icon w-5 h-5 me-2"/>
|
|
132
129
|
</RouterLink>
|
|
133
130
|
|
|
134
131
|
<template v-slot:tooltip>
|
|
@@ -147,7 +144,7 @@
|
|
|
147
144
|
}
|
|
148
145
|
}"
|
|
149
146
|
>
|
|
150
|
-
<IconPenSolid class="w-5 h-5 me-2"/>
|
|
147
|
+
<IconPenSolid class="af-edit-icon w-5 h-5 me-2"/>
|
|
151
148
|
</RouterLink>
|
|
152
149
|
<template v-slot:tooltip>
|
|
153
150
|
{{ $t('Edit item') }}
|
|
@@ -159,7 +156,7 @@
|
|
|
159
156
|
v-if="resource.options?.allowedActions.delete"
|
|
160
157
|
@click="deleteRecord(row)"
|
|
161
158
|
>
|
|
162
|
-
<IconTrashBinSolid class="w-5 h-5 me-2"/>
|
|
159
|
+
<IconTrashBinSolid class="af-delete-icon w-5 h-5 me-2"/>
|
|
163
160
|
</button>
|
|
164
161
|
|
|
165
162
|
<template v-slot:tooltip>
|
|
@@ -199,14 +196,14 @@
|
|
|
199
196
|
<!-- pagination
|
|
200
197
|
totalRows in v-if is used to not hide page input during loading when user puts cursor into it and edit directly (rows gets null there during edit)
|
|
201
198
|
-->
|
|
202
|
-
<div class="flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3"
|
|
203
|
-
v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
|
|
204
|
-
>
|
|
199
|
+
<div class="af-pagination-container flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3">
|
|
205
200
|
|
|
206
|
-
<div class="inline-flex "
|
|
201
|
+
<div class="af-pagination-buttons-container inline-flex "
|
|
202
|
+
v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
|
|
203
|
+
>
|
|
207
204
|
<!-- Buttons -->
|
|
208
205
|
<button
|
|
209
|
-
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 rounded-s 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"
|
|
206
|
+
class="af-pagination-prev-button flex items-center py-1 px-3 gap-1 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 rounded-s 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"
|
|
210
207
|
@click="page--; pageInput = page.toString();" :disabled="page <= 1">
|
|
211
208
|
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
212
209
|
viewBox="0 0 14 10">
|
|
@@ -218,14 +215,14 @@
|
|
|
218
215
|
</span>
|
|
219
216
|
</button>
|
|
220
217
|
<button
|
|
221
|
-
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 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"
|
|
218
|
+
class="af-pagination-first-page-button flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 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"
|
|
222
219
|
@click="page = 1; pageInput = page.toString();" :disabled="page <= 1">
|
|
223
220
|
<!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
|
|
224
221
|
1
|
|
225
222
|
</button>
|
|
226
223
|
<div
|
|
227
224
|
contenteditable="true"
|
|
228
|
-
class="min-w-10 outline-none inline-block w-auto min-w-10 py-1.5 px-3 text-sm text-center text-gray-700 border border-gray-300 dark:border-gray-700 dark:text-gray-400 dark:bg-gray-800 z-10"
|
|
225
|
+
class="af-pagination-input min-w-10 outline-none inline-block w-auto min-w-10 py-1.5 px-3 text-sm text-center text-gray-700 border border-gray-300 dark:border-gray-700 dark:text-gray-400 dark:bg-gray-800 z-10"
|
|
229
226
|
@keydown="onPageKeydown($event)"
|
|
230
227
|
@input="onPageInput($event)"
|
|
231
228
|
@blur="validatePageInput()"
|
|
@@ -234,14 +231,14 @@
|
|
|
234
231
|
</div>
|
|
235
232
|
|
|
236
233
|
<button
|
|
237
|
-
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 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"
|
|
234
|
+
class="af-pagination-last-page-button flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 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"
|
|
238
235
|
@click="page = totalPages; pageInput = page.toString();" :disabled="page >= totalPages">
|
|
239
236
|
{{ totalPages }}
|
|
240
237
|
|
|
241
238
|
<!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
|
|
242
239
|
</button>
|
|
243
240
|
<button
|
|
244
|
-
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 rounded-e 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"
|
|
241
|
+
class="af-pagination-next-button flex items-center py-1 px-3 gap-1 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 rounded-e 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"
|
|
245
242
|
@click="page++; pageInput = page.toString();" :disabled="page >= totalPages">
|
|
246
243
|
<span class="hidden sm:inline">{{ $t('Next') }}</span>
|
|
247
244
|
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
@@ -254,10 +251,10 @@
|
|
|
254
251
|
|
|
255
252
|
<!-- Help text -->
|
|
256
253
|
<span class="text-sm text-gray-700 dark:text-gray-400">
|
|
257
|
-
<span v-if="((page || 1) - 1) * pageSize + 1 > totalRows">{{ $t('Wrong Page') }} </span>
|
|
258
|
-
<template v-else>
|
|
254
|
+
<span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
|
|
255
|
+
<template v-else-if="resource && totalRows > 0">
|
|
259
256
|
|
|
260
|
-
<span class="hidden sm:inline">
|
|
257
|
+
<span class="af-pagination-info hidden sm:inline">
|
|
261
258
|
<i18n-t keypath="Showing {from} to {to} of {total} Entries" tag="p" >
|
|
262
259
|
<template v-slot:from>
|
|
263
260
|
<strong>{{ from }}</strong>
|
|
@@ -307,12 +304,13 @@ import {
|
|
|
307
304
|
import {
|
|
308
305
|
IconEyeSolid,
|
|
309
306
|
IconPenSolid,
|
|
310
|
-
IconTrashBinSolid
|
|
307
|
+
IconTrashBinSolid,
|
|
311
308
|
} from '@iconify-prerendered/vue-flowbite';
|
|
312
309
|
import router from '@/router';
|
|
313
310
|
import { Tooltip } from '@/afcl';
|
|
314
311
|
import type { AdminForthResourceCommon } from '@/types/Common';
|
|
315
312
|
import adminforth from '@/adminforth';
|
|
313
|
+
import Checkbox from '@/afcl/Checkbox.vue';
|
|
316
314
|
|
|
317
315
|
const coreStore = useCoreStore();
|
|
318
316
|
const { t } = useI18n();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:style="`height: ${containerHeight}px; will-change: transform;`"
|
|
6
6
|
@scroll="handleScroll"
|
|
7
7
|
ref="containerRef"
|
|
8
|
-
>
|
|
8
|
+
>
|
|
9
9
|
<!-- skelet loader -->
|
|
10
10
|
<div role="status" v-if="!resource || !resource.columns"
|
|
11
11
|
class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded shadow animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
<!-- table header -->
|
|
21
21
|
<tr class="t-header sticky z-10 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
|
|
22
22
|
<td scope="col" class="p-4">
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
</
|
|
23
|
+
<Checkbox
|
|
24
|
+
:modelValue="allFromThisPageChecked"
|
|
25
|
+
:disabled="!rows || !rows.length"
|
|
26
|
+
@update:modelValue="selectAll"
|
|
27
|
+
>
|
|
28
|
+
<span class="sr-only">{{ $t('checkbox') }}</span>
|
|
29
|
+
</Checkbox>
|
|
30
30
|
</td>
|
|
31
31
|
|
|
32
32
|
<td v-for="c in columnsListed" ref="headerRefs" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
|
|
@@ -101,17 +101,14 @@
|
|
|
101
101
|
:class="{'border-b': rowI !== visibleRows.length - 1, 'cursor-pointer': row._clickUrl !== null}"
|
|
102
102
|
@mounted="(el) => updateRowHeight(`row_${row._primaryKeyValue}`, el.offsetHeight)"
|
|
103
103
|
>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer">
|
|
113
|
-
<label for="checkbox-table-search-1" class="sr-only">{{ $t('checkbox') }}</label>
|
|
114
|
-
</div>
|
|
104
|
+
<td class="w-4 p-4 cursor-default" @click="(e)=>e.stopPropagation()">
|
|
105
|
+
<Checkbox
|
|
106
|
+
:model-value="checkboxesInternal.includes(row._primaryKeyValue)"
|
|
107
|
+
@change="(e)=>{addToCheckedValues(row._primaryKeyValue)}"
|
|
108
|
+
@click="(e)=>e.stopPropagation()"
|
|
109
|
+
>
|
|
110
|
+
<span class="sr-only">{{ $t('checkbox') }}</span>
|
|
111
|
+
</Checkbox>
|
|
115
112
|
</td>
|
|
116
113
|
<td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
|
|
117
114
|
<!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
|
|
@@ -216,11 +213,11 @@
|
|
|
216
213
|
<!-- pagination
|
|
217
214
|
totalRows in v-if is used to not hide page input during loading when user puts cursor into it and edit directly (rows gets null there during edit)
|
|
218
215
|
-->
|
|
219
|
-
<div class="flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3"
|
|
220
|
-
v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
|
|
221
|
-
>
|
|
216
|
+
<div class="flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3">
|
|
222
217
|
|
|
223
|
-
<div class="inline-flex "
|
|
218
|
+
<div class="inline-flex "
|
|
219
|
+
v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
|
|
220
|
+
>
|
|
224
221
|
<!-- Buttons -->
|
|
225
222
|
<button
|
|
226
223
|
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 rounded-s 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"
|
|
@@ -271,8 +268,8 @@
|
|
|
271
268
|
|
|
272
269
|
<!-- Help text -->
|
|
273
270
|
<span class="text-sm text-gray-700 dark:text-gray-400">
|
|
274
|
-
<span v-if="((page || 1) - 1) * pageSize + 1 > totalRows">{{ $t('Wrong Page') }} </span>
|
|
275
|
-
<template v-else>
|
|
271
|
+
<span v-if="((((page || 1) - 1) * pageSize + 1 > totalRows) && totalRows > 0)">{{ $t('Wrong Page') }} </span>
|
|
272
|
+
<template v-else-if="resource && totalRows > 0">
|
|
276
273
|
|
|
277
274
|
<span class="hidden sm:inline">
|
|
278
275
|
<i18n-t keypath="Showing {from} to {to} of {total} Entries" tag="p" >
|
|
@@ -330,6 +327,7 @@ import router from '@/router';
|
|
|
330
327
|
import { Tooltip } from '@/afcl';
|
|
331
328
|
import type { AdminForthResourceCommon } from '@/types/Common';
|
|
332
329
|
import adminforth from '@/adminforth';
|
|
330
|
+
import Checkbox from '@/afcl/Checkbox.vue';
|
|
333
331
|
|
|
334
332
|
const coreStore = useCoreStore();
|
|
335
333
|
const { t } = useI18n();
|