@drax/crud-vue 3.14.0 → 3.17.0
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/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.17.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./src/index.ts",
|
|
9
9
|
"module": "./src/index.ts",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@drax/common-front": "^3.11.0",
|
|
28
28
|
"@drax/crud-front": "^3.11.0",
|
|
29
|
-
"@drax/crud-share": "^3.
|
|
30
|
-
"@drax/media-vue": "^3.
|
|
29
|
+
"@drax/crud-share": "^3.17.0",
|
|
30
|
+
"@drax/media-vue": "^3.17.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"pinia": "^3.0.4",
|
|
@@ -50,5 +50,5 @@
|
|
|
50
50
|
"vue-tsc": "^3.2.4",
|
|
51
51
|
"vuetify": "^3.11.8"
|
|
52
52
|
},
|
|
53
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "d8a056b087d67157bc8c2e684b7e6696399d06a4"
|
|
54
54
|
}
|
package/src/components/Crud.vue
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {computed, onBeforeMount, type PropType} from "vue";
|
|
2
|
+
import {computed, onBeforeMount, ref, watch, type PropType} from "vue";
|
|
3
3
|
import type {IEntityCrud} from "@drax/crud-share";
|
|
4
4
|
import CrudListTable from "./CrudListTable.vue";
|
|
5
5
|
import CrudListGallery from "./CrudListGallery.vue";
|
|
6
6
|
import CrudForm from "./CrudForm.vue";
|
|
7
7
|
import CrudNotify from "./CrudNotify.vue";
|
|
8
8
|
import CrudDialog from "./CrudDialog.vue";
|
|
9
|
+
import CrudAiButton from "./buttons/CrudAiButton.vue";
|
|
10
|
+
import CrudAi from "./CrudAi.vue";
|
|
9
11
|
import {useCrud} from "../composables/UseCrud";
|
|
10
12
|
import {useDisplay} from 'vuetify'
|
|
13
|
+
import {useAuth} from "@drax/identity-vue";
|
|
11
14
|
|
|
12
15
|
const {entity} = defineProps({
|
|
13
16
|
entity: {type: Object as PropType<IEntityCrud>, required: true},
|
|
@@ -19,6 +22,8 @@ const {
|
|
|
19
22
|
prepareFilters, prepareSort, form
|
|
20
23
|
} = useCrud(entity);
|
|
21
24
|
|
|
25
|
+
const {hasPermission} = useAuth()
|
|
26
|
+
|
|
22
27
|
onBeforeMount(() => {
|
|
23
28
|
resetCrudStore()
|
|
24
29
|
prepareFilters()
|
|
@@ -26,6 +31,7 @@ onBeforeMount(() => {
|
|
|
26
31
|
})
|
|
27
32
|
|
|
28
33
|
const emit = defineEmits(['created', 'updated', 'deleted', 'viewed', 'canceled'])
|
|
34
|
+
const aiExpanded = ref(false)
|
|
29
35
|
|
|
30
36
|
const listComponent = computed(() => {
|
|
31
37
|
const listMode = entity?.listMode
|
|
@@ -43,6 +49,16 @@ const listComponent = computed(() => {
|
|
|
43
49
|
|
|
44
50
|
const {xs} = useDisplay()
|
|
45
51
|
|
|
52
|
+
function applyAiSuggestions(values: Record<string, any>) {
|
|
53
|
+
form.value = values
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
watch(dialog, (value) => {
|
|
57
|
+
if (!value) {
|
|
58
|
+
aiExpanded.value = false
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
46
62
|
|
|
47
63
|
</script>
|
|
48
64
|
|
|
@@ -127,7 +143,21 @@ const {xs} = useDisplay()
|
|
|
127
143
|
:entity="entity"
|
|
128
144
|
:operation="operation"
|
|
129
145
|
>
|
|
146
|
+
<template #toolbar-actions>
|
|
147
|
+
<crud-ai-button
|
|
148
|
+
v-if="entity.isAiAssistable && ['create', 'edit'].includes(operation) && hasPermission('ai:promptCrud')"
|
|
149
|
+
:entity="entity"
|
|
150
|
+
v-model="aiExpanded"
|
|
151
|
+
/>
|
|
152
|
+
</template>
|
|
153
|
+
|
|
130
154
|
<slot name="tools">
|
|
155
|
+
<crud-ai
|
|
156
|
+
v-if="entity.isAiAssistable && ['create', 'edit'].includes(operation) && hasPermission('ai:promptCrud')"
|
|
157
|
+
:entity="entity"
|
|
158
|
+
v-model="aiExpanded"
|
|
159
|
+
@apply="applyAiSuggestions"
|
|
160
|
+
/>
|
|
131
161
|
|
|
132
162
|
</slot>
|
|
133
163
|
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {computed, ref, watch} from "vue";
|
|
3
|
+
import type {PropType} from "vue";
|
|
4
|
+
import {useI18n} from "vue-i18n";
|
|
5
|
+
import type {IEntityCrud, IEntityCrudField} from "@drax/crud-share";
|
|
6
|
+
import {HttpRestClientFactory} from "@drax/common-front";
|
|
7
|
+
import {useCrud} from "../composables/UseCrud";
|
|
8
|
+
import {useAuth} from "@drax/identity-vue";
|
|
9
|
+
|
|
10
|
+
type CrudAiResponse = {
|
|
11
|
+
message?: string | null
|
|
12
|
+
suggestions?: Record<string, any>
|
|
13
|
+
warnings?: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const expanded = defineModel<boolean>({default: false})
|
|
17
|
+
|
|
18
|
+
const emit = defineEmits<{
|
|
19
|
+
apply: [form: Record<string, any>]
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const {entity} = defineProps({
|
|
23
|
+
entity: {type: Object as PropType<IEntityCrud>, required: true},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const {t, te} = useI18n()
|
|
27
|
+
const {hasPermission} = useAuth()
|
|
28
|
+
const {form, operation} = useCrud(entity)
|
|
29
|
+
const httpClient = HttpRestClientFactory.getInstance()
|
|
30
|
+
|
|
31
|
+
const prompt = ref("")
|
|
32
|
+
const loading = ref(false)
|
|
33
|
+
const error = ref("")
|
|
34
|
+
const response = ref<CrudAiResponse | null>(null)
|
|
35
|
+
|
|
36
|
+
function cloneValue<T>(value: T): T {
|
|
37
|
+
if (value === undefined) {
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof structuredClone === 'function') {
|
|
42
|
+
try {
|
|
43
|
+
return structuredClone(value)
|
|
44
|
+
} catch (_error) {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
return value.map((item) => cloneValue(item)) as T
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (value instanceof Date) {
|
|
53
|
+
return new Date(value.getTime()) as T
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (value && typeof value === 'object') {
|
|
57
|
+
const objectValue = value as Record<string, any>
|
|
58
|
+
const plainObject = Object.keys(objectValue).reduce((acc, key) => {
|
|
59
|
+
const item = objectValue[key]
|
|
60
|
+
|
|
61
|
+
if (typeof item === 'function') {
|
|
62
|
+
return acc
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
acc[key] = cloneValue(item)
|
|
67
|
+
} catch (_error) {
|
|
68
|
+
acc[key] = item
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return acc
|
|
72
|
+
}, {} as Record<string, any>)
|
|
73
|
+
|
|
74
|
+
return plainObject as T
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return value
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeField(field: IEntityCrudField): Record<string, any> {
|
|
81
|
+
return {
|
|
82
|
+
name: field.name,
|
|
83
|
+
type: field.type,
|
|
84
|
+
label: field.label,
|
|
85
|
+
hint: field.hint,
|
|
86
|
+
placeholder: field.placeholder,
|
|
87
|
+
readonly: field.readonly,
|
|
88
|
+
default: field.default,
|
|
89
|
+
enum: field.enum,
|
|
90
|
+
items: field.items?.map(item => ({
|
|
91
|
+
title: item.title,
|
|
92
|
+
value: item.value,
|
|
93
|
+
})),
|
|
94
|
+
ref: field.ref,
|
|
95
|
+
refDisplay: field.refDisplay,
|
|
96
|
+
objectFields: field.objectFields?.map(normalizeField),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseStructuredValue(field: IEntityCrudField, value: any) {
|
|
101
|
+
if (value === null || value === undefined) {
|
|
102
|
+
return value
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const jsonEncodedTypes = ['record', 'array.object', 'array.record', 'array.fullFile', 'file', 'fullFile']
|
|
106
|
+
|
|
107
|
+
if (field.type === 'object' && field.objectFields && typeof value === 'object') {
|
|
108
|
+
return value
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (field.type === 'object' && typeof value === 'string') {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(value)
|
|
114
|
+
} catch (_error) {
|
|
115
|
+
return value
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (jsonEncodedTypes.includes(field.type) && typeof value === 'string') {
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(value)
|
|
122
|
+
} catch (_error) {
|
|
123
|
+
return value
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return value
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const editableFields = computed(() => {
|
|
131
|
+
const sourceFields = operation.value === 'edit'
|
|
132
|
+
? entity.updateFields
|
|
133
|
+
: entity.createFields
|
|
134
|
+
|
|
135
|
+
return sourceFields.filter((field: IEntityCrudField) => {
|
|
136
|
+
if (field.readonly) {
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (field.permission && !hasPermission(field.permission)) {
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return !['id', 'password', 'file', 'fullFile', 'array.fullFile'].includes(field.type)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const allowedFieldNames = computed(() => {
|
|
149
|
+
return new Set(editableFields.value.map(field => field.name))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const currentForm = computed(() => {
|
|
153
|
+
return cloneValue(form.value || entity.form || {})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const sanitizedSuggestions = computed(() => {
|
|
157
|
+
const suggestions = response.value?.suggestions || {}
|
|
158
|
+
|
|
159
|
+
return Object.keys(suggestions).reduce((acc, key) => {
|
|
160
|
+
if (allowedFieldNames.value.has(key)) {
|
|
161
|
+
const field = editableFields.value.find(item => item.name === key)
|
|
162
|
+
acc[key] = field ? parseStructuredValue(field, suggestions[key]) : suggestions[key]
|
|
163
|
+
}
|
|
164
|
+
return acc
|
|
165
|
+
}, {} as Record<string, any>)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
function isEqualValue(left: any, right: any) {
|
|
169
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function deepMerge(target: any, source: any): any {
|
|
173
|
+
if (Array.isArray(source)) {
|
|
174
|
+
return cloneValue(source)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (source && typeof source === 'object') {
|
|
178
|
+
const base = target && typeof target === 'object' && !Array.isArray(target)
|
|
179
|
+
? cloneValue(target)
|
|
180
|
+
: {}
|
|
181
|
+
|
|
182
|
+
Object.keys(source).forEach((key) => {
|
|
183
|
+
base[key] = deepMerge(base[key], source[key])
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
return base
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return source
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const previewForm = computed(() => {
|
|
193
|
+
return deepMerge(currentForm.value, sanitizedSuggestions.value)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
const changedEntries = computed(() => {
|
|
197
|
+
return editableFields.value
|
|
198
|
+
.map((field) => {
|
|
199
|
+
const currentValue = currentForm.value[field.name]
|
|
200
|
+
const suggestedValue = previewForm.value[field.name]
|
|
201
|
+
|
|
202
|
+
if (isEqualValue(currentValue, suggestedValue)) {
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
field,
|
|
208
|
+
currentValue,
|
|
209
|
+
suggestedValue,
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
.filter(Boolean) as Array<{
|
|
213
|
+
field: IEntityCrudField
|
|
214
|
+
currentValue: any
|
|
215
|
+
suggestedValue: any
|
|
216
|
+
}>
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const canSubmit = computed(() => {
|
|
220
|
+
return prompt.value.trim().length > 0 && editableFields.value.length > 0 && !loading.value
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
const showCurrentValues = computed(() => {
|
|
224
|
+
return operation.value === 'edit'
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const subtitle = computed(() => {
|
|
228
|
+
const entityName = te(`${entity.name.toLowerCase()}.entity`)
|
|
229
|
+
? t(`${entity.name.toLowerCase()}.entity`)
|
|
230
|
+
: entity.name
|
|
231
|
+
|
|
232
|
+
return operation.value === 'edit'
|
|
233
|
+
? `Editar ${entityName} con ayuda de IA`
|
|
234
|
+
: `Completar ${entityName} con ayuda de IA`
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
function formatValue(value: any) {
|
|
238
|
+
if (value === undefined || value === null || value === '') {
|
|
239
|
+
return '-'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (typeof value === 'object') {
|
|
243
|
+
return JSON.stringify(value, null, 2)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return String(value)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function generateSuggestions() {
|
|
250
|
+
if (!canSubmit.value) {
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
loading.value = true
|
|
255
|
+
error.value = ""
|
|
256
|
+
response.value = null
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
response.value = await httpClient.post('/api/ai/prompt/crud', {
|
|
260
|
+
prompt: prompt.value.trim(),
|
|
261
|
+
operation: operation.value === 'edit' ? 'edit' : 'create',
|
|
262
|
+
entity: {
|
|
263
|
+
name: entity.name,
|
|
264
|
+
identifier: entity.identifier,
|
|
265
|
+
},
|
|
266
|
+
currentValues: currentForm.value,
|
|
267
|
+
fields: editableFields.value.map(normalizeField),
|
|
268
|
+
}, {}) as CrudAiResponse
|
|
269
|
+
} catch (e: any) {
|
|
270
|
+
error.value = e?.message || 'No se pudo obtener una sugerencia de IA'
|
|
271
|
+
response.value = null
|
|
272
|
+
} finally {
|
|
273
|
+
loading.value = false
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function applySuggestions() {
|
|
278
|
+
emit('apply', previewForm.value)
|
|
279
|
+
expanded.value = false
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function resetPanelState() {
|
|
283
|
+
prompt.value = ""
|
|
284
|
+
response.value = null
|
|
285
|
+
error.value = ""
|
|
286
|
+
loading.value = false
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
watch(expanded, (value) => {
|
|
290
|
+
if (!value) {
|
|
291
|
+
resetPanelState()
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
watch(prompt, () => {
|
|
296
|
+
response.value = null
|
|
297
|
+
error.value = ""
|
|
298
|
+
})
|
|
299
|
+
</script>
|
|
300
|
+
|
|
301
|
+
<template>
|
|
302
|
+
<v-expand-transition>
|
|
303
|
+
<div v-if="expanded" class="mb-4">
|
|
304
|
+
<v-card variant="tonal" class="crud-ai-panel">
|
|
305
|
+
<v-card-text>
|
|
306
|
+
<div class="d-flex align-center mb-4">
|
|
307
|
+
<div class="text-subtitle-1">{{ subtitle }}</div>
|
|
308
|
+
<v-spacer />
|
|
309
|
+
<v-btn icon="mdi-close" variant="text" @click="expanded = false" />
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<v-alert
|
|
313
|
+
v-if="error"
|
|
314
|
+
type="error"
|
|
315
|
+
variant="tonal"
|
|
316
|
+
class="mb-4"
|
|
317
|
+
:text="error"
|
|
318
|
+
/>
|
|
319
|
+
|
|
320
|
+
<v-alert
|
|
321
|
+
v-if="editableFields.length === 0"
|
|
322
|
+
type="warning"
|
|
323
|
+
variant="tonal"
|
|
324
|
+
class="mb-4"
|
|
325
|
+
:text="te('ai.noEditableFields') ? t('ai.noEditableFields') : 'No hay campos editables disponibles para asistir con IA.'"
|
|
326
|
+
/>
|
|
327
|
+
|
|
328
|
+
<v-textarea
|
|
329
|
+
v-model="prompt"
|
|
330
|
+
rows="3"
|
|
331
|
+
auto-grow
|
|
332
|
+
variant="outlined"
|
|
333
|
+
:label="te('ai.prompt') ? t('ai.prompt') : 'Prompt'"
|
|
334
|
+
:placeholder="te('ai.promptPlaceholder') ? t('ai.promptPlaceholder') : 'Ejemplo: completá este formulario para un producto premium orientado a pequeñas empresas.'"
|
|
335
|
+
/>
|
|
336
|
+
|
|
337
|
+
<div class="d-flex justify-end mt-2">
|
|
338
|
+
<v-btn
|
|
339
|
+
color="primary"
|
|
340
|
+
variant="flat"
|
|
341
|
+
:loading="loading"
|
|
342
|
+
:disabled="!canSubmit"
|
|
343
|
+
@click="generateSuggestions"
|
|
344
|
+
>
|
|
345
|
+
{{ te('action.generate') ? t('action.generate') : 'Generar sugerencia' }}
|
|
346
|
+
</v-btn>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<v-card v-if="response" variant="outlined" class="mt-6">
|
|
350
|
+
<v-card-title>
|
|
351
|
+
{{ te('ai.preview') ? t('ai.preview') : 'Vista previa' }}
|
|
352
|
+
</v-card-title>
|
|
353
|
+
|
|
354
|
+
<v-card-text>
|
|
355
|
+
<v-alert
|
|
356
|
+
v-if="response.message"
|
|
357
|
+
variant="tonal"
|
|
358
|
+
type="info"
|
|
359
|
+
class="mb-4"
|
|
360
|
+
:text="response.message"
|
|
361
|
+
/>
|
|
362
|
+
|
|
363
|
+
<v-alert
|
|
364
|
+
v-if="changedEntries.length === 0"
|
|
365
|
+
variant="tonal"
|
|
366
|
+
type="info"
|
|
367
|
+
:text="te('ai.noChanges') ? t('ai.noChanges') : 'La IA no propuso cambios aplicables para este formulario.'"
|
|
368
|
+
/>
|
|
369
|
+
|
|
370
|
+
<v-list v-else lines="three">
|
|
371
|
+
<v-list-item
|
|
372
|
+
v-for="entry in changedEntries"
|
|
373
|
+
:key="entry.field.name"
|
|
374
|
+
class="px-0"
|
|
375
|
+
>
|
|
376
|
+
<v-list-item-title>
|
|
377
|
+
{{ entry.field.label || entry.field.name }}
|
|
378
|
+
</v-list-item-title>
|
|
379
|
+
<div class="mt-1 preview-block">
|
|
380
|
+
<template v-if="showCurrentValues">
|
|
381
|
+
<div class="text-caption text-medium-emphasis">Actual</div>
|
|
382
|
+
<pre class="value-preview">{{ formatValue(entry.currentValue) }}</pre>
|
|
383
|
+
</template>
|
|
384
|
+
<div class="text-caption text-medium-emphasis mt-2">Sugerido</div>
|
|
385
|
+
<pre class="value-preview">{{ formatValue(entry.suggestedValue) }}</pre>
|
|
386
|
+
</div>
|
|
387
|
+
</v-list-item>
|
|
388
|
+
</v-list>
|
|
389
|
+
|
|
390
|
+
<div v-if="(response.warnings || []).length > 0" class="mt-4">
|
|
391
|
+
<v-alert
|
|
392
|
+
v-for="warning in response.warnings || []"
|
|
393
|
+
:key="warning"
|
|
394
|
+
variant="tonal"
|
|
395
|
+
type="warning"
|
|
396
|
+
class="mb-3"
|
|
397
|
+
:text="warning"
|
|
398
|
+
/>
|
|
399
|
+
</div>
|
|
400
|
+
</v-card-text>
|
|
401
|
+
</v-card>
|
|
402
|
+
</v-card-text>
|
|
403
|
+
|
|
404
|
+
<v-card-actions v-if="response">
|
|
405
|
+
<v-spacer />
|
|
406
|
+
<v-btn variant="text" @click="expanded = false">
|
|
407
|
+
{{ t('action.cancel') }}
|
|
408
|
+
</v-btn>
|
|
409
|
+
<v-btn
|
|
410
|
+
color="primary"
|
|
411
|
+
variant="flat"
|
|
412
|
+
:disabled="changedEntries.length === 0"
|
|
413
|
+
@click="applySuggestions"
|
|
414
|
+
>
|
|
415
|
+
{{ te('action.apply') ? t('action.apply') : 'Aplicar' }}
|
|
416
|
+
</v-btn>
|
|
417
|
+
</v-card-actions>
|
|
418
|
+
</v-card>
|
|
419
|
+
</div>
|
|
420
|
+
</v-expand-transition>
|
|
421
|
+
</template>
|
|
422
|
+
|
|
423
|
+
<style scoped>
|
|
424
|
+
.crud-ai-panel {
|
|
425
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.value-preview {
|
|
429
|
+
margin: 0;
|
|
430
|
+
padding: 8px 12px;
|
|
431
|
+
white-space: pre-wrap;
|
|
432
|
+
word-break: break-word;
|
|
433
|
+
border-radius: 8px;
|
|
434
|
+
background: rgba(0, 0, 0, 0.04);
|
|
435
|
+
font-family: inherit;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.preview-block {
|
|
439
|
+
white-space: normal;
|
|
440
|
+
}
|
|
441
|
+
</style>
|
|
@@ -41,6 +41,7 @@ const title = computed(() => {
|
|
|
41
41
|
<v-toolbar>
|
|
42
42
|
<v-toolbar-title>{{title}}</v-toolbar-title>
|
|
43
43
|
<v-spacer></v-spacer>
|
|
44
|
+
<slot name="toolbar-actions"></slot>
|
|
44
45
|
<v-btn icon @click="dialog = false"><v-icon>mdi-close</v-icon></v-btn>
|
|
45
46
|
</v-toolbar>
|
|
46
47
|
<v-card-text>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type {PropType} from "vue";
|
|
3
|
+
import {computed} from "vue";
|
|
4
|
+
import {useI18n} from "vue-i18n";
|
|
5
|
+
import type {IEntityCrud} from "@drax/crud-share";
|
|
6
|
+
|
|
7
|
+
const expanded = defineModel<boolean>({default: false})
|
|
8
|
+
|
|
9
|
+
defineProps({
|
|
10
|
+
entity: {type: Object as PropType<IEntityCrud>, required: true},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const {t, te} = useI18n()
|
|
14
|
+
|
|
15
|
+
const tooltip = computed(() => {
|
|
16
|
+
return te('action.aiAssist') ? t('action.aiAssist') : 'Asistencia IA'
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function toggleExpanded() {
|
|
20
|
+
expanded.value = !expanded.value
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<v-tooltip location="top">
|
|
26
|
+
<template v-slot:activator="{ props }">
|
|
27
|
+
<v-btn
|
|
28
|
+
v-bind="{ ...$attrs, ...props }"
|
|
29
|
+
icon="mdi-robot-outline"
|
|
30
|
+
class="mr-1"
|
|
31
|
+
variant="text"
|
|
32
|
+
@click="toggleExpanded"
|
|
33
|
+
/>
|
|
34
|
+
</template>
|
|
35
|
+
{{ tooltip }}
|
|
36
|
+
</v-tooltip>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<style scoped>
|
|
40
|
+
|
|
41
|
+
</style>
|
package/src/cruds/EntityCrud.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import CrudList from "./components/CrudList.vue";
|
|
|
7
7
|
import CrudListTable from "./components/CrudListTable.vue";
|
|
8
8
|
import CrudListGallery from "./components/CrudListGallery.vue";
|
|
9
9
|
import CrudFilters from "./components/CrudFilters.vue";
|
|
10
|
+
import CrudFiltersAction from "./components/CrudFiltersAction.vue";
|
|
10
11
|
import CrudNotify from "./components/CrudNotify.vue";
|
|
11
12
|
import CrudSearch from "./components/CrudSearch.vue";
|
|
12
13
|
import CrudAutocomplete from "./components/CrudAutocomplete.vue";
|
|
@@ -33,6 +34,7 @@ export {
|
|
|
33
34
|
CrudSearch,
|
|
34
35
|
CrudAutocomplete,
|
|
35
36
|
CrudFilters,
|
|
37
|
+
CrudFiltersAction,
|
|
36
38
|
useCrud,
|
|
37
39
|
useFormUtils,
|
|
38
40
|
useCrudStore,
|