@adminforth/bulk-ai-flow 1.25.2 → 1.26.1
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/build.log +2 -2
- package/custom/ImageGenerationCarousel.vue +106 -147
- package/custom/Swiper.vue +1 -1
- package/custom/VisionAction.vue +336 -109
- package/custom/VisionTable.vue +382 -222
- package/dist/custom/ImageGenerationCarousel.vue +106 -147
- package/dist/custom/Swiper.vue +1 -1
- package/dist/custom/VisionAction.vue +336 -109
- package/dist/custom/VisionTable.vue +382 -222
- package/dist/index.js +2 -1
- package/index.ts +2 -3
- package/package.json +3 -3
- package/types.ts +6 -0
|
@@ -1,228 +1,322 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
makePaginationSticky
|
|
2
|
+
<div class="vision-table-container overflow-auto pr-2" style="max-height: calc(100vh - 260px);">
|
|
3
|
+
<div class="records-grid p-1 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
|
|
4
|
+
<div
|
|
5
|
+
v-for="item in paginatedRecords"
|
|
6
|
+
:key="item.id"
|
|
7
|
+
class="record-card bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-default p-4 shadow-sm hover:shadow-md transition-shadow relative flex flex-col justify-between gap-4"
|
|
9
8
|
>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<div class="max-w-[100px] flex items-center justify-center">
|
|
17
|
-
<Checkbox
|
|
18
|
-
v-model="item.isChecked"
|
|
19
|
-
/>
|
|
20
|
-
</div>
|
|
21
|
-
</template>
|
|
22
|
-
<!-- IMAGE CELL TEMPLATE -->
|
|
23
|
-
<template #cell:images="{item}">
|
|
24
|
-
<div class="flex flex-shrink-0 gap-2">
|
|
25
|
-
<div v-if="item.images.length" v-for="image in item.images" :key="image">
|
|
26
|
-
<div class="mt-2 flex items-center justify-center gap-2">
|
|
27
|
-
<img
|
|
28
|
-
v-if="isValidUrl(image)"
|
|
29
|
-
:src="image"
|
|
30
|
-
class="w-20 h-20 object-cover rounded cursor-pointer border hover:border-blue-500 transition my-2"
|
|
31
|
-
@click="zoomImage(image)"
|
|
9
|
+
<div class="flex flex-col h-full">
|
|
10
|
+
<div class="record-card-header flex items-start justify-between gap-4 mb-4">
|
|
11
|
+
<div class="flex items-center gap-3 w-full min-w-0">
|
|
12
|
+
<Checkbox
|
|
13
|
+
v-model="item.isChecked"
|
|
14
|
+
class="shrink-0"
|
|
32
15
|
/>
|
|
33
|
-
|
|
34
|
-
|
|
16
|
+
<!-- CRITICAL WRAPPER -->
|
|
17
|
+
<div class="flex-1 min-w-0 w-0">
|
|
18
|
+
<h3 class="font-bold text-gray-900 dark:text-white w-full truncate overflow-hidden whitespace-nowrap">
|
|
19
|
+
{{ item.label || item.data?.name || $t('Item Record') }}
|
|
20
|
+
</h3>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="flex items-center gap-3 shrink-0" v-if="hasNonImageOutputFields">
|
|
24
|
+
<div class="flex items-center gap-2">
|
|
25
|
+
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">
|
|
26
|
+
{{ cardValueMode?.[String(item.id)] === 'old' ? $t('OLD VALUE') : $t('NEW VALUE') }}
|
|
27
|
+
</span>
|
|
28
|
+
<button
|
|
29
|
+
@click="toggleCardValueMode(item.id)"
|
|
30
|
+
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
|
31
|
+
:class="cardValueMode?.[String(item.id)] === 'old' ? 'bg-lightPrimary dark:bg-darkPrimary' : 'bg-gray-200 dark:bg-gray-700'"
|
|
32
|
+
>
|
|
33
|
+
<span
|
|
34
|
+
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform dark:bg-gray-200"
|
|
35
|
+
:class="cardValueMode?.[String(item.id)] === 'old' ? 'translate-x-6' : 'translate-x-1'"
|
|
36
|
+
/>
|
|
37
|
+
</button>
|
|
35
38
|
</div>
|
|
36
39
|
</div>
|
|
37
40
|
</div>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</div>
|
|
41
|
-
<transition name="fade">
|
|
41
|
+
|
|
42
|
+
<div class="flex flex-col gap-3 flex-grow">
|
|
42
43
|
<div
|
|
43
|
-
v-if="
|
|
44
|
-
class="
|
|
45
|
-
@click.self="closeZoom"
|
|
44
|
+
v-if="props.meta.isAttachFiles"
|
|
45
|
+
class="source-images-section w-full flex flex-col items-center justify-center bg-gray-50 dark:bg-gray-700 border border-dashed border-gray-200 dark:border-gray-600 h-full max-h-[260px] max-w-[446.5px] rounded-default text-center "
|
|
46
46
|
>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<Tooltip>
|
|
69
|
-
<IconRefreshOutline class="my-2 w-5 h-5 hover:text-blue-500 cursor-pointer" :class="{ 'opacity-50 cursor-not-allowed hover': shouldDisableRegenerateFieldIcon(item, n) }" @click="regerenerateFieldIconClick(item, n)"/>
|
|
70
|
-
<template #tooltip>
|
|
71
|
-
{{ shouldDisableRegenerateFieldIcon(item, n) ? $t("Can't analyze image without source image") : $t('Regenerate') }}
|
|
47
|
+
<!-- HAS IMAGES -->
|
|
48
|
+
<template v-if="item.images?.length">
|
|
49
|
+
<template
|
|
50
|
+
v-for="(image, index) in item.images"
|
|
51
|
+
:key="image || index"
|
|
52
|
+
>
|
|
53
|
+
<img
|
|
54
|
+
v-if="image && isValidUrl(image)"
|
|
55
|
+
:src="image"
|
|
56
|
+
class="w-full h-full object-cover rounded-default cursor-pointer border hover:border-blue-500 transition"
|
|
57
|
+
@click="zoomImage(image)"
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<div
|
|
61
|
+
v-else
|
|
62
|
+
class="text-xs text-red-500 flex flex-col items-center justify-center h-full p-2"
|
|
63
|
+
>
|
|
64
|
+
<IconExclamationCircleSolid class="w-6 h-6 mb-1" />
|
|
65
|
+
<p>{{ $t('Invalid source image') }}</p>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
72
68
|
</template>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
69
|
+
|
|
70
|
+
<!-- EMPTY ARRAY -->
|
|
71
|
+
<template v-else-if="item.images?.length === 0">
|
|
72
|
+
<div class="text-gray-400 dark:text-gray-500 flex flex-col items-center justify-center w-full h-[258px]">
|
|
73
|
+
<svg
|
|
74
|
+
class="w-20 h-20 mb-2 stroke-1 text-gray-300 dark:text-gray-600"
|
|
75
|
+
fill="none"
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
viewBox="0 0 24 24"
|
|
78
|
+
>
|
|
79
|
+
<path
|
|
80
|
+
stroke-linecap="round"
|
|
81
|
+
stroke-linejoin="round"
|
|
82
|
+
stroke-width="2"
|
|
83
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
84
|
+
/>
|
|
85
|
+
</svg>
|
|
86
|
+
<p class="text-xs font-medium">{{ $t('No images found') }}</p>
|
|
87
87
|
</div>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<!-- NO DATA -->
|
|
91
|
+
<template v-else>
|
|
92
|
+
<div class="text-gray-400 dark:text-gray-500 flex flex-col items-center justify-center h-full">
|
|
93
|
+
<svg
|
|
94
|
+
class="w-20 h-20 mb-2 stroke-1 text-gray-300 dark:text-gray-600"
|
|
95
|
+
fill="none"
|
|
96
|
+
stroke="currentColor"
|
|
97
|
+
viewBox="0 0 24 24"
|
|
98
|
+
>
|
|
99
|
+
<path
|
|
100
|
+
stroke-linecap="round"
|
|
101
|
+
stroke-linejoin="round"
|
|
102
|
+
stroke-width="2"
|
|
103
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
104
|
+
/>
|
|
105
|
+
</svg>
|
|
106
|
+
<p class="text-xs font-medium">{{ $t('No image found') }}</p>
|
|
103
107
|
</div>
|
|
104
|
-
<template #tooltip>
|
|
105
|
-
<p class="max-w-[200px]">{{ item.oldData?.[n] ?? "No old value" }}</p>
|
|
106
108
|
</template>
|
|
107
|
-
</Tooltip>
|
|
108
109
|
</div>
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
|
|
111
|
+
<div class="fields-section flex flex-col gap-3">
|
|
112
|
+
<div
|
|
113
|
+
v-for="n in customFieldNames"
|
|
114
|
+
:key="n"
|
|
115
|
+
class="flex flex-col relative"
|
|
116
|
+
@mouseenter="(() => { setHover(item.id, n, true) })"
|
|
117
|
+
@mouseleave="(() => { setHover(item.id, n, false) })"
|
|
118
|
+
>
|
|
119
|
+
<div class="flex justify-between items-center mb-1">
|
|
120
|
+
<label class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
121
|
+
{{ n.replace(/([A-Z])/g, ' $1').trim() }}
|
|
122
|
+
</label>
|
|
123
|
+
|
|
124
|
+
<div class="flex gap-1 items-center" v-if="!isInColumnImage(n)">
|
|
125
|
+
<Tooltip v-if="checkForError(item, n)">
|
|
126
|
+
<IconExclamationCircleSolid class="w-4 h-4 text-red-500" />
|
|
127
|
+
<template #tooltip>{{ checkForError(item, n) }}</template>
|
|
128
|
+
</Tooltip>
|
|
129
|
+
</div>
|
|
118
130
|
</div>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
|
|
132
|
+
<div v-if="isInColumnImage(n)">
|
|
133
|
+
<div v-if="item.aiStatus?.generatedImages" class="flex flex-col gap-2">
|
|
134
|
+
<div v-if="isValidUrl(item.data?.[n])" class="flex items-center gap-3 relative w-full">
|
|
135
|
+
<Skeleton v-if="imageLoadErrors[item.id]?.[n]" type="image" class="w-full h-[280px]" />
|
|
136
|
+
<img
|
|
137
|
+
v-else
|
|
138
|
+
:key="item.data?.[n]"
|
|
139
|
+
:src="item.data?.[n]"
|
|
140
|
+
class="w-full max-h-[280px] object-cover border cursor-pointer hover:border-blue-500 rounded-default"
|
|
141
|
+
@error="() => { imageLoadErrors[item.id] ??= {}; imageLoadErrors[item.id][n] = true }"
|
|
142
|
+
@click="() => zoomImage(item.data?.[n])"
|
|
143
|
+
style="image-rendering: auto;"
|
|
144
|
+
/>
|
|
145
|
+
<p
|
|
146
|
+
v-if="isImageHasPreviewUrl[n]"
|
|
147
|
+
class="text-xs text-blue-500 hover:underline cursor-pointer transition-opacity"
|
|
148
|
+
:class="{ 'opacity-0': !isHovered(item.id, n) }"
|
|
149
|
+
@click="() => { item.openImageCompare[n] = true }"
|
|
150
|
+
>
|
|
151
|
+
{{ $t('old image') }}
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
<div v-else>
|
|
155
|
+
<div class="text-gray-400 dark:text-gray-500 flex flex-col items-center justify-center w-full h-[258px] bg-gray-50 dark:bg-gray-700 border border-dashed border-gray-200 dark:border-gray-600">
|
|
156
|
+
<svg
|
|
157
|
+
class="w-20 h-20 mb-2 stroke-1 text-gray-300 dark:text-gray-600"
|
|
158
|
+
fill="none"
|
|
159
|
+
stroke="currentColor"
|
|
160
|
+
viewBox="0 0 24 24"
|
|
161
|
+
>
|
|
162
|
+
<path
|
|
163
|
+
stroke-linecap="round"
|
|
164
|
+
stroke-linejoin="round"
|
|
165
|
+
stroke-width="2"
|
|
166
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
167
|
+
/>
|
|
168
|
+
</svg>
|
|
169
|
+
<p class="text-xs font-medium">{{ $t('No images found') }}</p>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<ImageCompare
|
|
174
|
+
v-if="item.openImageCompare?.[n]"
|
|
175
|
+
:meta="props.meta"
|
|
176
|
+
:columnName="n"
|
|
177
|
+
:oldImage="item.oldData?.[n]"
|
|
178
|
+
:newImage="item.data?.[n]"
|
|
179
|
+
@close="item.openImageCompare[n] = false"
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
<div v-else>
|
|
183
|
+
<Skeleton type="image" class="w-full h-[258px] rounded-default" />
|
|
184
|
+
</div>
|
|
134
185
|
</div>
|
|
135
|
-
<template #tooltip>
|
|
136
|
-
{{ item.oldData?.[n] ?? "No old value" }}
|
|
137
|
-
</template>
|
|
138
|
-
</Tooltip>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
186
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
<div>
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
187
|
+
<div v-else-if="(isAnalyzing(item, n) && !item.regeneratingFieldsStatus?.[n])">
|
|
188
|
+
<div v-if="isInColumnEnum(n)">
|
|
189
|
+
<Select
|
|
190
|
+
v-if="cardValueMode?.[String(item.id)] !== 'old'"
|
|
191
|
+
class="w-full"
|
|
192
|
+
:options="convertColumnEnumToSelectOptions(props.meta.columnEnums, n)"
|
|
193
|
+
v-model="item.data[n]"
|
|
194
|
+
:teleportToTop="true"
|
|
195
|
+
/>
|
|
196
|
+
|
|
197
|
+
<input
|
|
198
|
+
v-else
|
|
199
|
+
type="text"
|
|
200
|
+
:value="item.oldData?.[n] || item.oldData?.[n] === 0 ? item.oldData[n] : $t('no old value')"
|
|
201
|
+
disabled
|
|
202
|
+
class="w-full h-10 px-3 border border-gray-200 dark:border-gray-600 rounded-default bg-gray-50 dark:bg-gray-700 text-gray-400 dark:text-gray-500 text-sm focus:outline-none"
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div v-else-if="typeof item.data?.[n] === 'string' || typeof item.data?.[n] === 'object'">
|
|
207
|
+
<textarea
|
|
208
|
+
:value="cardValueMode?.[String(item.id)] === 'old' ? (item.oldData?.[n] || item.oldData?.[n] === 0 ? item.oldData[n] : $t('no old value')) : item.data[n]"
|
|
209
|
+
@input="(e) => { if (cardValueMode?.[String(item.id)] !== 'old') item.data[n] = (e.target as HTMLInputElement).value }"
|
|
210
|
+
:disabled="cardValueMode?.[String(item.id)] === 'old'"
|
|
211
|
+
class="w-full text-sm p-2 border border-gray-200 dark:border-gray-600 rounded-default bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 focus:outline-none focus:border-blue-500 dark:focus:border-blue-400 disabled:bg-gray-50 dark:disabled:bg-gray-700 disabled:text-gray-400 dark:disabled:text-gray-500"
|
|
212
|
+
:class="props.meta.isAttachFiles ? 'min-h-[42px]' : 'min-h-[120px]'"
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<div v-else-if="typeof item.data?.[n] === 'boolean'" class="flex items-center h-10">
|
|
217
|
+
<Toggle v-model="item.data[n]" />
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div v-else>
|
|
221
|
+
<input
|
|
222
|
+
:type="cardValueMode?.[String(item.id)] === 'old' && !item.oldData?.[n] && item.oldData?.[n] !== 0 ? 'text' : 'number'"
|
|
223
|
+
:value="cardValueMode?.[String(item.id)] === 'old' ? (item.oldData?.[n] || item.oldData?.[n] === 0 ? item.oldData[n] : $t('no old value')) : item.data[n]"
|
|
224
|
+
@input="(e) => { if (cardValueMode?.[String(item.id)] !== 'old') item.data[n] = Number((e.target as HTMLInputElement).value) }"
|
|
225
|
+
:disabled="cardValueMode?.[String(item.id)] === 'old'"
|
|
226
|
+
class="w-full h-10 px-3 border border-gray-200 dark:border-gray-600 rounded-default bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 focus:outline-none focus:border-blue-500 dark:focus:border-blue-400 disabled:bg-gray-50 dark:disabled:bg-gray-700 disabled:text-gray-400 dark:disabled:text-gray-500 text-sm"
|
|
175
227
|
/>
|
|
176
228
|
</div>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div v-else>
|
|
232
|
+
<Skeleton type="input" class="w-full h-10 rounded-default" />
|
|
233
|
+
</div>
|
|
181
234
|
</div>
|
|
182
235
|
</div>
|
|
183
|
-
|
|
184
|
-
<GenerationCarousel
|
|
185
|
-
v-if="item.openGenerationCarousel[n]"
|
|
186
|
-
:images="item.carouselSaveImages[n]"
|
|
187
|
-
:recordId="item.id"
|
|
188
|
-
:meta="props.meta"
|
|
189
|
-
:fieldName="n"
|
|
190
|
-
:carouselImageIndex="item.carouselImageIndex[n]"
|
|
191
|
-
:regenerateImagesRefreshRate="regenerateImagesRefreshRate"
|
|
192
|
-
:sourceImage="item.images && item.images.length ? item.images : null"
|
|
193
|
-
:imageGenerationPrompts="imageGenerationPrompts[n]"
|
|
194
|
-
@error="handleError"
|
|
195
|
-
@close="item.openGenerationCarousel[n] = false"
|
|
196
|
-
@selectImage="updateSelectedImage"
|
|
197
|
-
@updateCarouselIndex="updateActiveIndex"
|
|
198
|
-
/>
|
|
199
|
-
<ImageCompare
|
|
200
|
-
v-if="item.openImageCompare[n]"
|
|
201
|
-
:meta="props.meta"
|
|
202
|
-
:columnName="n"
|
|
203
|
-
:oldImage="item.oldData?.[n]"
|
|
204
|
-
:newImage="item.data?.[n]"
|
|
205
|
-
@close="item.openImageCompare[n] = false"
|
|
206
|
-
/>
|
|
207
|
-
</div>
|
|
236
|
+
|
|
208
237
|
</div>
|
|
209
238
|
</div>
|
|
210
239
|
|
|
211
|
-
<div
|
|
212
|
-
|
|
240
|
+
<div class="record-actions flex justify-end shrink-0">
|
|
241
|
+
<button
|
|
242
|
+
@click="() => canRegenerate(item) && regenerateRecord(item.id)"
|
|
243
|
+
:disabled="!canRegenerate(item)"
|
|
244
|
+
class="px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-default hover:bg-gray-50 dark:hover:bg-gray-600 flex items-center gap-1.5 transition disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-gray-700"
|
|
245
|
+
>
|
|
246
|
+
<IconRefreshOutline class="w-3.5 h-3.5" /> {{ $t('Regenerate') }}
|
|
247
|
+
</button>
|
|
213
248
|
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
|
|
214
253
|
|
|
215
|
-
|
|
216
|
-
|
|
254
|
+
<Teleport to="body">
|
|
255
|
+
<transition name="fade">
|
|
256
|
+
<div v-if="zoomedImage" class="zoom-overlay fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm" @click="closeZoom">
|
|
257
|
+
<transition name="zoom">
|
|
258
|
+
<img v-if="zoomedImage" :src="zoomedImage" class="max-w-[90vw] max-h-[90vh] rounded-default object-contain shadow-2xl cursor-zoom-out" @click.stop="closeZoom" />
|
|
259
|
+
</transition>
|
|
217
260
|
</div>
|
|
218
|
-
</
|
|
219
|
-
</
|
|
261
|
+
</transition>
|
|
262
|
+
</Teleport>
|
|
263
|
+
|
|
264
|
+
<nav v-show="totalItems > 0" class="pagination-nav bg-lightTableBackground dark:bg-darkTableBackground py-3 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 mt-4 rounded-default">
|
|
265
|
+
<span class="text-sm font-normal text-center text-lightTablePaginationText dark:text-darkTablePaginationText sm:mb-4 md:mb-0 block w-full md:inline md:w-auto">
|
|
266
|
+
{{ $t('Showing') }}
|
|
267
|
+
<span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ fromIndex }}</span>
|
|
268
|
+
{{ $t('to') }}
|
|
269
|
+
<span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ toIndex }}</span>
|
|
270
|
+
{{ $t('of') }}
|
|
271
|
+
<span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ totalItems }}</span>
|
|
272
|
+
</span>
|
|
273
|
+
<div class="af-pagination-container flex flex-row items-center gap-3">
|
|
274
|
+
<div class="inline-flex">
|
|
275
|
+
<button
|
|
276
|
+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText bg-lightActivePaginationButtonBackground border-r-0 rounded-s hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
277
|
+
:disabled="currentPage <= 1"
|
|
278
|
+
@click="previousPage"
|
|
279
|
+
>
|
|
280
|
+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
|
|
281
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5H1m0 0 4 4M1 5l4-4"/>
|
|
282
|
+
</svg>
|
|
283
|
+
</button>
|
|
284
|
+
<button
|
|
285
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-r-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
286
|
+
:disabled="currentPage <= 1"
|
|
287
|
+
@click="goToPage(1)"
|
|
288
|
+
>1</button>
|
|
289
|
+
<input
|
|
290
|
+
type="text"
|
|
291
|
+
v-model="pageInput"
|
|
292
|
+
:style="{ width: `${Math.max(1, pageInput.length + 4)}ch` }"
|
|
293
|
+
class="min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightTablePaginationInputText border border-lightTablePaginationInputBorder bg-lightTablePaginationInputBackground dark:border-darkTablePaginationInputBorder dark:text-darkTablePaginationInputText dark:bg-darkTablePaginationInputBackground z-10"
|
|
294
|
+
@keydown="onPageKeydown"
|
|
295
|
+
@blur="validatePageInput"
|
|
296
|
+
/>
|
|
297
|
+
<button
|
|
298
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-l-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
299
|
+
:disabled="currentPage >= totalPages"
|
|
300
|
+
@click="goToPage(totalPages)"
|
|
301
|
+
>{{ totalPages }}</button>
|
|
302
|
+
<button
|
|
303
|
+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText focus:outline-none bg-lightActivePaginationButtonBackground border-l-0 rounded-e hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
304
|
+
:disabled="currentPage >= totalPages"
|
|
305
|
+
@click="nextPage"
|
|
306
|
+
>
|
|
307
|
+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
|
|
308
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
|
309
|
+
</svg>
|
|
310
|
+
</button>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</nav>
|
|
314
|
+
</div>
|
|
220
315
|
</template>
|
|
221
316
|
|
|
222
317
|
<script lang="ts" setup>
|
|
223
318
|
import { ref, watch, computed, reactive } from 'vue'
|
|
224
319
|
import { Select, Input, Textarea, Table, Checkbox, Skeleton, Toggle, Tooltip } from '@/afcl'
|
|
225
|
-
import GenerationCarousel from './ImageGenerationCarousel.vue'
|
|
226
320
|
import ImageCompare from './ImageCompare.vue';
|
|
227
321
|
import { IconRefreshOutline, IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
228
322
|
|
|
@@ -239,14 +333,32 @@ const props = defineProps<{
|
|
|
239
333
|
outputImageFields: string[],
|
|
240
334
|
outputFieldsForAnalizeFromImages: string[],
|
|
241
335
|
outputPlainFields: string[],
|
|
336
|
+
overwriteExistingValues: boolean,
|
|
242
337
|
}>();
|
|
243
|
-
const emit = defineEmits(['error', 'regenerateImages', 'regenerateCell']);
|
|
338
|
+
const emit = defineEmits(['error', 'regenerateImages', 'regenerateCell', 'regenerateRecord']);
|
|
339
|
+
|
|
340
|
+
function regenerateRecord(recordId: any) {
|
|
341
|
+
emit('regenerateRecord', { recordId });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function canRegenerate(item: any): boolean {
|
|
345
|
+
if (props.overwriteExistingValues) return true;
|
|
346
|
+
for (const n of props.customFieldNames) {
|
|
347
|
+
const val = item.data?.[n];
|
|
348
|
+
if (val === null || val === undefined || val === '') return true;
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
244
352
|
|
|
245
|
-
const
|
|
246
|
-
const
|
|
353
|
+
const DEFAULT_PAGE_SIZE = 6;
|
|
354
|
+
const initialPageSize = props.meta?.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
355
|
+
console.log(props.meta?.pageSize);
|
|
356
|
+
|
|
357
|
+
const pagination = reactive({ offset: 0, limit: initialPageSize });
|
|
247
358
|
const zoomedImage = ref(null);
|
|
248
359
|
const hovers = ref<Record<string, Record<string, boolean>>>({});
|
|
249
360
|
const tableRef = ref(null);
|
|
361
|
+
const cardValueMode = ref<Record<string, 'old' | 'new'>>({});
|
|
250
362
|
|
|
251
363
|
defineExpose({
|
|
252
364
|
refresh() {
|
|
@@ -258,6 +370,13 @@ defineExpose({
|
|
|
258
370
|
|
|
259
371
|
const paginatedRecords = computed(() => props.records.slice(pagination.offset, pagination.offset + pagination.limit));
|
|
260
372
|
|
|
373
|
+
const imageLoadErrors = ref<Record<string, Record<string, boolean>>>({})
|
|
374
|
+
|
|
375
|
+
const totalItems = computed(() => props.records.length);
|
|
376
|
+
const currentPage = computed(() => Math.floor(pagination.offset / pagination.limit) + 1);
|
|
377
|
+
const totalPages = computed(() => Math.max(1, Math.ceil(totalItems.value / pagination.limit)));
|
|
378
|
+
const fromIndex = computed(() => totalItems.value === 0 ? 0 : Math.min(pagination.offset + 1, totalItems.value));
|
|
379
|
+
const toIndex = computed(() => Math.min(pagination.offset + pagination.limit, totalItems.value));
|
|
261
380
|
const tableDataProvider = async ({ offset, limit }) => {
|
|
262
381
|
pagination.offset = offset;
|
|
263
382
|
pagination.limit = limit;
|
|
@@ -267,8 +386,29 @@ const tableDataProvider = async ({ offset, limit }) => {
|
|
|
267
386
|
};
|
|
268
387
|
};
|
|
269
388
|
|
|
389
|
+
watch(() => props.records.length, (newLength) => {
|
|
390
|
+
const maxOffset = Math.max(0, (Math.ceil(newLength / pagination.limit) - 1) * pagination.limit);
|
|
391
|
+
if (pagination.offset > maxOffset) {
|
|
392
|
+
pagination.offset = maxOffset;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
watch(() => props.meta?.pageSize, (newSize) => {
|
|
397
|
+
const size = Number(newSize) || DEFAULT_PAGE_SIZE;
|
|
398
|
+
pagination.limit = size;
|
|
399
|
+
const maxOffset = Math.max(0, (Math.ceil(props.records.length / pagination.limit) - 1) * pagination.limit);
|
|
400
|
+
if (pagination.offset > maxOffset) {
|
|
401
|
+
pagination.offset = maxOffset;
|
|
402
|
+
}
|
|
403
|
+
}, { immediate: true });
|
|
404
|
+
|
|
270
405
|
watch(() => props.records, (newVal) => {
|
|
271
406
|
hovers.value = Object.fromEntries((newVal || []).map(record => [String(record.id), {}]));
|
|
407
|
+
const newCardValueMode = {};
|
|
408
|
+
(newVal || []).forEach(record => {
|
|
409
|
+
newCardValueMode[String(record.id)] = 'new';
|
|
410
|
+
});
|
|
411
|
+
cardValueMode.value = newCardValueMode;
|
|
272
412
|
}, { immediate: true, deep: true });
|
|
273
413
|
|
|
274
414
|
function zoomImage(img) {
|
|
@@ -291,6 +431,17 @@ function isInColumnImage(key: string): boolean {
|
|
|
291
431
|
return props.meta.outputImageFields?.includes(key) || false;
|
|
292
432
|
}
|
|
293
433
|
|
|
434
|
+
const hasNonImageOutputFields = computed(() => {
|
|
435
|
+
if (!props.customFieldNames) return false;
|
|
436
|
+
|
|
437
|
+
for (const fieldName of props.customFieldNames) {
|
|
438
|
+
const isImage = isInColumnImage(fieldName);
|
|
439
|
+
if (!isImage) return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return false;
|
|
443
|
+
});
|
|
444
|
+
|
|
294
445
|
function convertColumnEnumToSelectOptions(columnEnumArray: any[], key: string) {
|
|
295
446
|
const col = columnEnumArray.find(c => c.name === key);
|
|
296
447
|
if (!col) return [];
|
|
@@ -300,31 +451,6 @@ function convertColumnEnumToSelectOptions(columnEnumArray: any[], key: string) {
|
|
|
300
451
|
}));
|
|
301
452
|
}
|
|
302
453
|
|
|
303
|
-
function updateSelectedImage(image: string, id: any, fieldName: string) {
|
|
304
|
-
const record = props.records.find(rec => String(rec.id) === String(id));
|
|
305
|
-
if (record) {
|
|
306
|
-
record.data[fieldName] = image;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function handleError({ isError, errorMessage }) {
|
|
311
|
-
emit('error', {
|
|
312
|
-
isError,
|
|
313
|
-
errorMessage
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function regenerateImages(recordId: any) {
|
|
318
|
-
emit('regenerateImages', { recordId });
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function updateActiveIndex(newIndex: number, id: any, fieldName: string) {
|
|
322
|
-
const record = props.records.find(rec => String(rec.id) === String(id));
|
|
323
|
-
if (record) {
|
|
324
|
-
record.carouselImageIndex[fieldName] = newIndex;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
454
|
function isValidUrl(str: string): boolean {
|
|
329
455
|
try {
|
|
330
456
|
new URL(str);
|
|
@@ -399,8 +525,37 @@ function isHovered(recordId: string | number, field: string) {
|
|
|
399
525
|
return Boolean(hovers.value?.[String(recordId)]?.[field]);
|
|
400
526
|
}
|
|
401
527
|
|
|
528
|
+
function toggleCardValueMode(recordId: string | number) {
|
|
529
|
+
const key = String(recordId);
|
|
530
|
+
const currentMode = cardValueMode.value[key] || 'new';
|
|
531
|
+
cardValueMode.value[key] = currentMode === 'new' ? 'old' : 'new';
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const pageInput = ref(String(currentPage.value));
|
|
535
|
+
watch(currentPage, (val) => { pageInput.value = String(val); });
|
|
536
|
+
|
|
537
|
+
function goToPage(page: number) {
|
|
538
|
+
const clampedPage = Math.min(Math.max(page, 1), totalPages.value);
|
|
539
|
+
pagination.offset = (clampedPage - 1) * pagination.limit;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function previousPage() {
|
|
543
|
+
goToPage(currentPage.value - 1);
|
|
544
|
+
}
|
|
402
545
|
|
|
546
|
+
function nextPage() {
|
|
547
|
+
goToPage(currentPage.value + 1);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function onPageKeydown(e: KeyboardEvent) {
|
|
551
|
+
if (e.key === 'Enter') validatePageInput();
|
|
552
|
+
}
|
|
403
553
|
|
|
554
|
+
function validatePageInput() {
|
|
555
|
+
const parsed = parseInt(pageInput.value);
|
|
556
|
+
if (!isNaN(parsed)) goToPage(parsed);
|
|
557
|
+
pageInput.value = String(currentPage.value);
|
|
558
|
+
}
|
|
404
559
|
</script>
|
|
405
560
|
|
|
406
561
|
<style scoped>
|
|
@@ -418,4 +573,9 @@ function isHovered(recordId: string | number, field: string) {
|
|
|
418
573
|
transform: scale(0.95);
|
|
419
574
|
opacity: 0;
|
|
420
575
|
}
|
|
576
|
+
|
|
577
|
+
.overflow-auto {
|
|
578
|
+
scrollbar-width: thin;
|
|
579
|
+
scrollbar-color: #d1d5db transparent;
|
|
580
|
+
}
|
|
421
581
|
</style>
|