innoboxrr-vue-datatable 2.2.0 → 3.0.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/src/table.js ADDED
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Lo que la tabla decide sin depender de Vue ni de React: cómo se traduce el
3
+ * contrato del modelo a columnas de TanStack Table, qué se pide al servidor,
4
+ * qué se le dice al usuario cuando algo falla y en qué se convierte cada
5
+ * acción.
6
+ *
7
+ * El archivo es idéntico en innoboxrr-vue-datatable y en
8
+ * innoboxrr-react-datatable. Es lo que hace que las dos tablas se comporten
9
+ * igual: si cambia aquí, cambia allí.
10
+ */
11
+
12
+ /** Las acciones que borran se pintan en rojo aunque el modelo no lo diga. */
13
+ const DANGER_ACTIONS = ['delete', 'forceDelete']
14
+
15
+ /** La clave con la que se guardan los permisos de la barra superior. */
16
+ export const CRUD_POLICIES = '__crud__'
17
+
18
+ export const DEFAULT_LABELS = {
19
+ actions: 'Acciones',
20
+ rowActions: 'Acciones del registro',
21
+ refresh: 'Actualizar',
22
+ filters: 'Filtros',
23
+ selectAll: 'Seleccionar todos los de esta página',
24
+ selectRow: 'Seleccionar el registro',
25
+ selection: 'Selección',
26
+ selected: 'seleccionados',
27
+ clearSelection: 'Quitar selección',
28
+ empty: 'No hay resultados',
29
+ retry: 'Reintentar',
30
+ of: 'de',
31
+ page: 'Página',
32
+ previous: 'Página anterior',
33
+ next: 'Página siguiente',
34
+ forbidden: 'No tienes permiso para ver estos registros.',
35
+ offline: 'No se pudo conectar con el servidor.',
36
+ failed: 'No se pudieron cargar los registros.',
37
+ policiesFailed: 'No se pudieron comprobar los permisos.',
38
+ notAllowed: 'No tienes permiso para esta acción.',
39
+ actionFailed: 'No se pudo completar la acción.',
40
+ }
41
+
42
+ /**
43
+ * Se leía de la global `csrf_token`, que la aplicación anfitriona tenía que
44
+ * definir en window: el componente no se podía montar fuera de ella.
45
+ */
46
+ export const csrfToken = () => globalThis.csrf_token
47
+ ?? globalThis.document?.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
48
+ ?? ''
49
+
50
+ /**
51
+ * Sustituye a `_.isEqual` de lodash, que se usaba como global sin declararla
52
+ * como dependencia.
53
+ */
54
+ export const isEqual = (a, b) => {
55
+ if (a === b) {
56
+ return true
57
+ }
58
+
59
+ if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
60
+ return false
61
+ }
62
+
63
+ const keysA = Object.keys(a)
64
+ const keysB = Object.keys(b)
65
+
66
+ return keysA.length === keysB.length && keysA.every((key) => isEqual(a[key], b[key]))
67
+ }
68
+
69
+ /**
70
+ * Una foto de un objeto de filtros. Comparar contra el valor anterior no sirve
71
+ * cuando el anfitrión muta el mismo objeto: el anterior y el nuevo son el mismo.
72
+ */
73
+ export const snapshot = (value) => JSON.stringify(value ?? {})
74
+
75
+ /**
76
+ * Se admite tanto ['name'] como [{ id: 'name' }]: el contrato nunca estuvo
77
+ * documentado y por ahí circulan las dos formas.
78
+ */
79
+ export const hiddenColumnIds = (hideColumns = []) => hideColumns
80
+ .map((column) => (typeof column === 'string' ? column : column?.id))
81
+ .filter(Boolean)
82
+
83
+ /**
84
+ * `dataTableHead()` del modelo, en columnas de TanStack Table. La columna
85
+ * original viaja en `meta.head`, que es de donde la tabla saca el parser, el
86
+ * componente y si el valor es HTML.
87
+ */
88
+ export const columnsFrom = (head = []) => head.map((column) => ({
89
+ id: column.id,
90
+ accessorFn: (row) => row?.[column.id],
91
+ header: column.value,
92
+ enableSorting: column.sortable === true,
93
+ meta: { head: column },
94
+ }))
95
+
96
+ /** Solo un `false` explícito oculta una columna en TanStack Table. */
97
+ export const visibilityFrom = (hideColumns) => Object.fromEntries(
98
+ hiddenColumnIds(hideColumns).map((id) => [id, false])
99
+ )
100
+
101
+ /** El orden lo decide el servidor; la tabla solo lo refleja. */
102
+ export const sortingFrom = (orderBy, sort = {}) => (
103
+ orderBy ? [{ id: orderBy, desc: sort[orderBy] === 'desc' }] : []
104
+ )
105
+
106
+ /**
107
+ * La primera pulsación sobre una columna que el modelo no ordena por defecto
108
+ * la deja ascendente; las siguientes alternan.
109
+ */
110
+ export const toggledSort = (sort, id) => ({ ...sort, [id]: sort[id] === 'asc' ? 'desc' : 'asc' })
111
+
112
+ export const ariaSort = (column, orderBy, sort = {}) => {
113
+ if (column.sortable !== true) {
114
+ return undefined
115
+ }
116
+
117
+ if (column.id !== orderBy) {
118
+ return 'none'
119
+ }
120
+
121
+ return sort[column.id] === 'desc' ? 'descending' : 'ascending'
122
+ }
123
+
124
+ export const sortIcon = (column, orderBy, sort = {}) => {
125
+ const state = ariaSort(column, orderBy, sort)
126
+
127
+ return state === 'ascending' ? 'sortUp' : state === 'descending' ? 'sortDown' : 'sort'
128
+ }
129
+
130
+ /**
131
+ * Lo que espera el backend. Con orden interno, el del usuario gana a lo que
132
+ * traigan los filtros externos; sin él, es al revés.
133
+ */
134
+ export const requestFilters = ({ formFilters = {}, externalFilters = {}, orderBy, sort = {}, page, internalSort }) => {
135
+ const params = { _token: csrfToken(), managed: true, except_view_any: true }
136
+ const order = { orderBy, orderMode: sort[orderBy] }
137
+
138
+ return internalSort
139
+ ? { ...params, ...formFilters, ...externalFilters, ...order, page }
140
+ : { ...params, ...formFilters, ...order, ...externalFilters, page }
141
+ }
142
+
143
+ export const requestConfig = (method, url, payload) => ({
144
+ method,
145
+ url,
146
+ data: method === 'post' ? payload : null,
147
+ params: method === 'get' ? payload : null,
148
+ })
149
+
150
+ /** El usuario canceló una confirmación: no es un error que haya que contar. */
151
+ export const isCancelled = (error) => ['RequestCancelledError', 'CanceledError'].includes(error?.name)
152
+
153
+ /**
154
+ * Qué decirle al usuario. Antes un fallo se reintentaba tres veces en silencio
155
+ * y un 403 dejaba la tabla vacía con «No results found», como si no hubiera
156
+ * registros.
157
+ */
158
+ export const describeError = (error, labels = DEFAULT_LABELS, fallback = labels.failed) => {
159
+ const status = error?.response?.status ?? null
160
+
161
+ if (status === 403) {
162
+ return { status, message: labels.forbidden, retryable: false }
163
+ }
164
+
165
+ // Un fallo de red no trae respuesta; un error de programa, tampoco, y no
166
+ // es cosa de la conexión.
167
+ if (! error?.response) {
168
+ const offline = Boolean(error?.request) || error?.code === 'ERR_NETWORK' || error?.message === 'Network Error'
169
+
170
+ return { status, message: offline ? labels.offline : fallback, retryable: true }
171
+ }
172
+
173
+ const message = error.response.data?.message
174
+
175
+ return {
176
+ status,
177
+ message: typeof message === 'string' && message !== '' ? message : fallback,
178
+ retryable: true,
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Marca `policy` en cada acción según lo que respondió el backend. Una acción
184
+ * que el modelo declara ya permitida se queda así.
185
+ */
186
+ export const withPolicies = (actions = [], allowed = null) => actions.map((action) => ({
187
+ ...action,
188
+ policy: action.policy === true || allowed?.[action.id] === true,
189
+ }))
190
+
191
+ export const actionKind = (action) => {
192
+ if (! action.route) {
193
+ return 'callback'
194
+ }
195
+
196
+ return action.link ? 'link' : 'route'
197
+ }
198
+
199
+ export const routeTarget = (action, extraParams = {}, extraQuery = {}) => {
200
+ const to = action.params?.to ?? {}
201
+
202
+ return {
203
+ name: to.name,
204
+ params: { ...(to.params ?? {}), ...extraParams },
205
+ query: to.query ? { ...to.query, ...extraQuery } : { ...extraQuery },
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Las acciones del contrato, como elementos de MenuComponent. Una acción sin
211
+ * permiso se ve deshabilitada y dice por qué: quien no puede tiene que saber
212
+ * que existe.
213
+ */
214
+ export const menuItems = (actions = [], labels, run) => actions.map((action, index) => {
215
+ const allowed = action.policy === true
216
+
217
+ return {
218
+ id: action.id ?? `${action.name}-${index}`,
219
+ label: action.name,
220
+ icon: action.icon || undefined,
221
+ danger: action.danger ?? DANGER_ACTIONS.includes(action.id),
222
+ disabled: ! allowed,
223
+ disabledReason: allowed ? undefined : labels.notAllowed,
224
+ action: () => run(action),
225
+ }
226
+ })
227
+
228
+ /**
229
+ * Copia aislada de una fila, para que un parser del modelo no pueda mutar los
230
+ * datos de la tabla. Se hace una vez por fila y carga, no una por celda.
231
+ */
232
+ export const cloneRow = (row) => JSON.parse(JSON.stringify(row ?? {}))
233
+
234
+ export const cellValue = (column, row) => (
235
+ typeof column.parser === 'function' ? column.parser(row?.[column.id], row) : row?.[column.id]
236
+ )
237
+
238
+ /** Un componente de celda recibe el objeto que devuelve el parser, o `{ value }`. */
239
+ export const componentProps = (value) => (
240
+ typeof value === 'object' && value !== null && ! Array.isArray(value) ? value : { value }
241
+ )
242
+
243
+ export const summary = (meta = {}, labels = DEFAULT_LABELS) => (
244
+ meta.total > 0 ? `${meta.from}–${meta.to} ${labels.of} ${meta.total}` : labels.empty
245
+ )
246
+
247
+ /**
248
+ * La casilla de un rango con Mayúsculas. El evento de cambio de una casilla
249
+ * no siempre trae la tecla; el clic, sí.
250
+ */
251
+ export const isRangeEvent = (event) => Boolean(event?.shiftKey ?? event?.nativeEvent?.shiftKey)
@@ -0,0 +1,370 @@
1
+ import { computed, onMounted, ref, shallowRef, unref, watch } from 'vue'
2
+ import axios from 'axios'
3
+ import {
4
+ columnVisibilityFeature,
5
+ rowSelectionFeature,
6
+ rowSortingFeature,
7
+ tableFeatures,
8
+ useTable,
9
+ } from '@tanstack/vue-table'
10
+ import { notifyError, notifySuccess } from 'innoboxrr-form-core'
11
+
12
+ import * as core from './table.js'
13
+
14
+ /**
15
+ * Solo lo que la tabla usa. El orden y la paginación los hace el servidor, así
16
+ * que no hay modelos de filas ordenadas ni paginadas: TanStack Table lleva el
17
+ * estado —qué columnas se ven, qué filas están seleccionadas, por qué columna
18
+ * se ordena— y la tabla pinta.
19
+ */
20
+ const features = tableFeatures({ rowSortingFeature, columnVisibilityFeature, rowSelectionFeature })
21
+
22
+ /**
23
+ * Toda la lógica de la tabla: cargar, ordenar, paginar, seleccionar y resolver
24
+ * permisos. Es la misma que `useDataTable` de innoboxrr-react-datatable.
25
+ *
26
+ * Está fuera del componente para poder probarla y para pintar la misma tabla
27
+ * de otra forma.
28
+ *
29
+ * @param {object} props los props de DataTable (reactivos)
30
+ * @param {{ navigate?: (target: object) => unknown, labels?: object }} options
31
+ */
32
+ export default function useDataTable(props, { navigate = null, labels = core.DEFAULT_LABELS } = {}) {
33
+ const text = () => unref(labels)
34
+
35
+ const head = computed(() => props.model.dataTableHead())
36
+ const columns = computed(() => core.columnsFrom(head.value))
37
+
38
+ const visibleHead = computed(() => {
39
+ const hidden = core.hiddenColumnIds(props.hideColumns)
40
+
41
+ return head.value.filter((column) => ! hidden.includes(column.id))
42
+ })
43
+
44
+ // Las filas llegan del servidor y se sustituyen enteras: una referencia
45
+ // superficial evita que Vue envuelva cada una en un proxy.
46
+ const rows = shallowRef([])
47
+ const meta = shallowRef({})
48
+ const links = shallowRef([])
49
+ const loading = ref(false)
50
+ const error = shallowRef(null)
51
+
52
+ const sort = ref({ ...props.model.dataTableSort() })
53
+ const orderBy = ref('id')
54
+ const page = ref(1)
55
+
56
+ /** Lo que respondió el backend de permisos, por fila y para la barra. */
57
+ const allowed = shallowRef({})
58
+
59
+ // A partir del primer clic en una cabecera, el orden del usuario manda
60
+ // sobre el que traigan los filtros externos.
61
+ let internalSort = false
62
+ let requestId = 0
63
+
64
+ const table = useTable({
65
+ features,
66
+ columns,
67
+ data: rows,
68
+ getRowId: (row, index) => String(row?.id ?? index),
69
+ manualSorting: true,
70
+ enableRowSelection: computed(() => props.selectable === true),
71
+ isRowRangeSelectionEvent: core.isRangeEvent,
72
+ state: computed(() => ({
73
+ columnVisibility: core.visibilityFrom(props.hideColumns),
74
+ sorting: core.sortingFrom(orderBy.value, sort.value),
75
+ })),
76
+ })
77
+
78
+ const selection = computed(() => table.atoms.rowSelection.get() ?? {})
79
+ const selectedIds = computed(() => Object.keys(selection.value).filter((id) => selection.value[id]))
80
+
81
+ /**
82
+ * Una copia por fila y carga, para que un parser del modelo no pueda mutar
83
+ * los datos de la tabla.
84
+ */
85
+ const clones = computed(() => rows.value.map(core.cloneRow))
86
+
87
+ const crudActions = computed(() => core.withPolicies(
88
+ props.model.crudActions(),
89
+ allowed.value[core.CRUD_POLICIES]
90
+ ))
91
+
92
+ const bulkActions = computed(() => (
93
+ typeof props.model.bulkActions === 'function' ? props.model.bulkActions() : []
94
+ ))
95
+
96
+ const rowActions = (row) => core.withPolicies(row?.actions ?? [], allowed.value[String(row?.id)])
97
+
98
+ const load = async () => {
99
+ const id = ++requestId
100
+
101
+ const filters = core.requestFilters({
102
+ formFilters: props.formFilters,
103
+ externalFilters: props.externalFilters,
104
+ orderBy: orderBy.value,
105
+ sort: sort.value,
106
+ page: page.value,
107
+ internalSort,
108
+ })
109
+
110
+ props.model.setFilters?.(filters)
111
+ loading.value = true
112
+
113
+ try {
114
+ const response = await axios(core.requestConfig(props.dataMethod, props.dataUrl, filters))
115
+
116
+ // Una respuesta que llega tarde no pisa a la de una petición
117
+ // posterior: quien cambia de página dos veces seguidas ve la última.
118
+ if (id !== requestId) {
119
+ return
120
+ }
121
+
122
+ rows.value = response.data?.data ?? []
123
+ meta.value = response.data?.meta ?? {}
124
+ links.value = response.data?.links ?? []
125
+ error.value = null
126
+
127
+ // Con datos nuevos los permisos se vuelven a preguntar.
128
+ allowed.value = {}
129
+ } catch (failure) {
130
+ if (id !== requestId) {
131
+ return
132
+ }
133
+
134
+ const described = core.describeError(failure, text())
135
+
136
+ if (described.status === 403) {
137
+ rows.value = []
138
+ meta.value = {}
139
+ } else if (rows.value.length > 0) {
140
+ // Con filas en pantalla el error no tiene sitio en la tabla:
141
+ // se avisa y se deja lo que había.
142
+ notifyError(described.message)
143
+ }
144
+
145
+ error.value = described
146
+ } finally {
147
+ if (id === requestId) {
148
+ loading.value = false
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Varios cambios en el mismo tick —un filtro nuevo que además vuelve a la
155
+ * primera página— son una sola petición. Antes eran dos: el paginador
156
+ * tenía su propia copia de la página y la devolvía al padre.
157
+ */
158
+ let scheduled = false
159
+
160
+ const schedule = () => {
161
+ if (scheduled) {
162
+ return
163
+ }
164
+
165
+ scheduled = true
166
+
167
+ queueMicrotask(() => {
168
+ scheduled = false
169
+ load()
170
+ })
171
+ }
172
+
173
+ const refresh = () => load()
174
+
175
+ const clearSelection = () => table.resetRowSelection(true)
176
+
177
+ const sortColumn = (column) => {
178
+ if (column?.sortable !== true) {
179
+ return
180
+ }
181
+
182
+ internalSort = true
183
+ sort.value = core.toggledSort(sort.value, column.id)
184
+ orderBy.value = column.id
185
+ }
186
+
187
+ const updatePage = (next) => {
188
+ const value = Number(next)
189
+
190
+ if (Number.isInteger(value) && value >= 1) {
191
+ page.value = value
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Pregunta al backend qué se puede hacer con una fila —o con la barra, sin
197
+ * id— antes de abrir su menú. Lo que responde se guarda hasta la próxima
198
+ * carga, así que abrir dos veces el mismo menú no pregunta dos veces.
199
+ */
200
+ const preparePolicies = async (id = null) => {
201
+ const key = id == null ? core.CRUD_POLICIES : String(id)
202
+
203
+ if (allowed.value[key]) {
204
+ return
205
+ }
206
+
207
+ try {
208
+ const response = await axios(core.requestConfig(props.policyMethod, props.policyUrl, {
209
+ _token: core.csrfToken(),
210
+ id,
211
+ }))
212
+
213
+ allowed.value = { ...allowed.value, [key]: response.data ?? {} }
214
+ } catch {
215
+ // El menú se abre igual, con todo deshabilitado: es mejor que un
216
+ // botón que no hace nada.
217
+ notifyError(text().policiesFailed)
218
+ }
219
+ }
220
+
221
+ const run = async (action) => {
222
+ const kind = core.actionKind(action)
223
+
224
+ if (kind === 'route') {
225
+ if (! navigate) {
226
+ notifyError(text().actionFailed)
227
+
228
+ return undefined
229
+ }
230
+
231
+ try {
232
+ await navigate(core.routeTarget(action, props.extraParams, props.extraQuery))
233
+ } catch (failure) {
234
+ // Una ruta que no existe es un error de quien declaró la
235
+ // acción: el usuario recibe un aviso y la consola, el detalle.
236
+ notifyError(text().actionFailed)
237
+ console.error(failure)
238
+ }
239
+
240
+ return undefined
241
+ }
242
+
243
+ if (kind === 'link') {
244
+ globalThis.window?.open(action.params?.link, action.params?.target ?? '_self')
245
+
246
+ return undefined
247
+ }
248
+
249
+ if (typeof props.model[action.callback] !== 'function') {
250
+ notifyError(text().actionFailed)
251
+
252
+ return undefined
253
+ }
254
+
255
+ try {
256
+ await props.model[action.callback](action.params)
257
+ } catch (failure) {
258
+ if (! core.isCancelled(failure)) {
259
+ notifyError(core.describeError(failure, text(), text().actionFailed).message)
260
+ }
261
+
262
+ return undefined
263
+ }
264
+
265
+ if (action.success) {
266
+ notifySuccess(action.success)
267
+ }
268
+
269
+ return load()
270
+ }
271
+
272
+ /**
273
+ * Una acción masiva recibe los ids seleccionados —también los de otras
274
+ * páginas— y las filas cargadas que están entre ellos.
275
+ */
276
+ const runBulk = async (action) => {
277
+ if (typeof props.model[action.callback] !== 'function') {
278
+ notifyError(text().actionFailed)
279
+
280
+ return undefined
281
+ }
282
+
283
+ const ids = [...selectedIds.value]
284
+ const loaded = table.getSelectedRowModel().rows.map((row) => row.original)
285
+
286
+ try {
287
+ await props.model[action.callback](ids, loaded)
288
+ } catch (failure) {
289
+ if (! core.isCancelled(failure)) {
290
+ notifyError(core.describeError(failure, text(), text().actionFailed).message)
291
+ }
292
+
293
+ return undefined
294
+ }
295
+
296
+ clearSelection()
297
+
298
+ if (action.success) {
299
+ notifySuccess(action.success)
300
+ }
301
+
302
+ return load()
303
+ }
304
+
305
+ let lastForm = core.snapshot(props.formFilters)
306
+ let lastExternal = core.snapshot(props.externalFilters)
307
+
308
+ // Un filtro nuevo vuelve a la primera página; uno externo no. Los dos
309
+ // descartan la selección, que era de otro listado.
310
+ watch(() => props.formFilters, (value) => {
311
+ const next = core.snapshot(value)
312
+
313
+ if (next === lastForm) {
314
+ return
315
+ }
316
+
317
+ lastForm = next
318
+ page.value = 1
319
+ clearSelection()
320
+ schedule()
321
+ }, { deep: true })
322
+
323
+ watch(() => props.externalFilters, (value) => {
324
+ const next = core.snapshot(value)
325
+
326
+ if (next === lastExternal) {
327
+ return
328
+ }
329
+
330
+ lastExternal = next
331
+ clearSelection()
332
+ schedule()
333
+ }, { deep: true })
334
+
335
+ watch([orderBy, sort, page], schedule)
336
+
337
+ onMounted(load)
338
+
339
+ // Lo que exponía la versión anterior, para quien lo lea desde fuera.
340
+ const dataTable = computed(() => ({ head: visibleHead.value, body: rows.value }))
341
+ const pagination = computed(() => ({ meta: meta.value, links: links.value }))
342
+
343
+ return {
344
+ table,
345
+ head,
346
+ visibleHead,
347
+ rows,
348
+ clones,
349
+ meta,
350
+ links,
351
+ loading,
352
+ error,
353
+ sort,
354
+ orderBy,
355
+ page,
356
+ crudActions,
357
+ bulkActions,
358
+ rowActions,
359
+ selectedIds,
360
+ dataTable,
361
+ pagination,
362
+ refresh,
363
+ clearSelection,
364
+ sortColumn,
365
+ updatePage,
366
+ preparePolicies,
367
+ run,
368
+ runBulk,
369
+ }
370
+ }
@@ -0,0 +1,20 @@
1
+ import { onScopeDispose, shallowRef } from 'vue'
2
+ import { getTheme, onThemeChange } from 'innoboxrr-form-core'
3
+
4
+ /**
5
+ * El tema de innoboxrr-form-core, como referencia reactiva.
6
+ *
7
+ * El tema es estado de módulo, fuera de Vue: sin la suscripción, un `setTheme`
8
+ * con la tabla ya montada no la repintaría.
9
+ *
10
+ * @returns {import('vue').ShallowRef<Record<string, string>>}
11
+ */
12
+ export default function useTheme() {
13
+ const theme = shallowRef(getTheme())
14
+
15
+ onScopeDispose(onThemeChange((next) => {
16
+ theme.value = { ...next }
17
+ }))
18
+
19
+ return theme
20
+ }
@@ -1,45 +0,0 @@
1
- <template>
2
-
3
- <a
4
- href="#"
5
- class="disabled-link block px-4 py-2 dark:hover:text-white dark:text-slate-400"
6
- data-tooltip="This action is not authorized"
7
- data-tooltip-pos="right">
8
-
9
- <Icon
10
- v-if="showIcon"
11
- class="fe-mr-sm"
12
- :icon="resolved"
13
- aria-hidden="true" />
14
-
15
- <span>{{ text }}</span>
16
-
17
- </a>
18
-
19
- </template>
20
-
21
- <script setup>
22
-
23
- import { computed } from 'vue'
24
- import { Icon } from '@iconify/vue'
25
- import { iconFor } from 'innoboxrr-form-core'
26
-
27
- const props = defineProps({
28
-
29
- icon: {
30
- type: String,
31
- default: ''
32
- },
33
-
34
- text: {
35
- type: String,
36
- required: true
37
- }
38
-
39
- })
40
-
41
- const showIcon = computed(() => props.icon !== '' && props.icon != null)
42
-
43
- const resolved = computed(() => iconFor(props.icon))
44
-
45
- </script>