@groupteknology/vuno 0.3.0 → 0.4.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/README.md +21 -2
- package/dist/module.json +1 -1
- package/dist/runtime/app/components/Field/FieldSwitch.d.vue.ts +5 -0
- package/dist/runtime/app/components/Field/FieldSwitch.vue +27 -0
- package/dist/runtime/app/components/Field/FieldSwitch.vue.d.ts +5 -0
- package/dist/runtime/app/components/Form/FormField.vue +18 -16
- package/dist/runtime/app/components/Form/FormModal.d.vue.ts +6 -0
- package/dist/runtime/app/components/Form/FormModal.vue +47 -46
- package/dist/runtime/app/components/Form/FormModal.vue.d.ts +6 -0
- package/dist/runtime/app/composables/useVunoCrud.js +9 -1
- package/dist/runtime/app/theme/index.js +3 -0
- package/dist/runtime/app/types/crud.d.ts +17 -1
- package/dist/runtime/app/types/field.d.ts +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ const definition = {
|
|
|
62
62
|
|
|
63
63
|
### Tipos de campo
|
|
64
64
|
|
|
65
|
-
`text` · `email` · `password` · `textarea` · `number` · `select` · `radio` · `checkbox` · `color` · `slug` · `file` · `array`
|
|
65
|
+
`text` · `email` · `password` · `textarea` · `number` · `select` · `radio` · `checkbox` · `switch` · `color` · `slug` · `file` · `array`
|
|
66
66
|
|
|
67
67
|
`source` del campo `slug` es el **nombre** del campo del que deriva, no su valor.
|
|
68
68
|
|
|
@@ -89,6 +89,17 @@ Con `layout: 'tabs'`, cada pestaña muestra un contador de los campos con error
|
|
|
89
89
|
|
|
90
90
|
### En un modal
|
|
91
91
|
|
|
92
|
+
El slot `footer` recibe `formId`, necesario para enviar desde un botón propio: el pie vive fuera del `<form>`, así que la asociación se hace con el atributo nativo.
|
|
93
|
+
|
|
94
|
+
```vue
|
|
95
|
+
<VFormModal v-model:open="open" v-model:state="state" :definition="definition" :title="title">
|
|
96
|
+
<template #footer="{ close, formId, loading }">
|
|
97
|
+
<UButton label="Cancelar" variant="ghost" @click="close" />
|
|
98
|
+
<UButton :form="formId" :label="editando ? 'Guardar cambios' : 'Crear'" :loading="loading" type="submit" />
|
|
99
|
+
</template>
|
|
100
|
+
</VFormModal>
|
|
101
|
+
```
|
|
102
|
+
|
|
92
103
|
`VFormModal` añade el diálogo, los botones y el ancho adecuado. Los botones van en el pie del modal —fijo mientras el cuerpo hace scroll— asociados al formulario por `id`.
|
|
93
104
|
|
|
94
105
|
```vue
|
|
@@ -101,6 +112,10 @@ Con `layout: 'tabs'`, cada pestaña muestra un contador de los campos con error
|
|
|
101
112
|
|
|
102
113
|
```vue
|
|
103
114
|
<script setup lang="ts">
|
|
115
|
+
// En SSR, `$fetch` no reenvía la cookie de la petición y una ruta con sesión
|
|
116
|
+
// responde 401 durante el render. `useRequestFetch()` sí la lleva.
|
|
117
|
+
const request = useRequestFetch()
|
|
118
|
+
|
|
104
119
|
const query = useVunoTableQuery({ sort: 'sku', url: true })
|
|
105
120
|
|
|
106
121
|
const { items, total, failure, isPending, isSaving, isDeleting,
|
|
@@ -109,7 +124,7 @@ const { items, total, failure, isPending, isSaving, isDeleting,
|
|
|
109
124
|
key: 'products',
|
|
110
125
|
query,
|
|
111
126
|
emptyForm: () => ({ name: '', sku: '' }),
|
|
112
|
-
list: (q) =>
|
|
127
|
+
list: (q) => request('/api/products', { query: q }),
|
|
113
128
|
create: (form) => $fetch('/api/products', { body: form, method: 'POST' }),
|
|
114
129
|
update: (id, form) => $fetch(`/api/products/${id}`, { body: form, method: 'PATCH' }),
|
|
115
130
|
remove: (row) => $fetch(`/api/products/${row.id}`, { method: 'DELETE' }),
|
|
@@ -117,6 +132,8 @@ const { items, total, failure, isPending, isSaving, isDeleting,
|
|
|
117
132
|
</script>
|
|
118
133
|
```
|
|
119
134
|
|
|
135
|
+
Solo `list` necesita `useRequestFetch()`: es la única que corre en servidor. El alta, la edición y el borrado salen siempre del navegador, donde `$fetch` lleva la cookie por su cuenta.
|
|
136
|
+
|
|
120
137
|
La consulta forma parte de la clave de caché, así que cada página, filtro y orden se guarda por separado. Usa `placeholderData: keepPreviousData`, que mantiene en pantalla los datos anteriores mientras llega la nueva página: sin eso, cada cambio de filtro deja `data` en `undefined` un instante y la tabla parpadea enseñando su estado vacío. Los textos de los avisos salen de `vuno.messages` y se pueden afinar por pantalla con la opción `messages`.
|
|
121
138
|
|
|
122
139
|
### Claves que dependen de estado reactivo
|
|
@@ -180,6 +197,8 @@ export default defineNuxtPlugin((nuxtApp) => {
|
|
|
180
197
|
})
|
|
181
198
|
```
|
|
182
199
|
|
|
200
|
+
**Si tu API tiene sesión, `list` tiene que usar `useRequestFetch()`.** Un `$fetch` pelado no reenvía la cookie de la petición entrante, así que durante el render en servidor la llamada sale sin autenticar y la ruta responde 401. Es el tropiezo más probable al estrenar esto, porque en cliente el mismo código funciona.
|
|
201
|
+
|
|
183
202
|
Con la clave incompleta no se espera nada: la consulta está suspendida y esperarla colgaría el render entero, porque una consulta deshabilitada nunca se asienta.
|
|
184
203
|
|
|
185
204
|
`server: false` desactiva la espera para lo que no se ve en la primera pintada —una lista dentro de un modal, una pestaña oculta—, donde solo retrasaría el HTML.
|
package/dist/module.json
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VunoFieldSwitch } from '#vuno/types/field';
|
|
2
|
+
type __VLS_Props = Omit<VunoFieldSwitch, 'class' | 'columns'>;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
4
|
+
declare const _default: typeof __VLS_export;
|
|
5
|
+
export default _default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useVunoField } from "#vuno/composables/useVunoField";
|
|
3
|
+
import { useVunoTheme } from "#vuno/composables/useVunoTheme";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
name: { type: String, required: true },
|
|
6
|
+
description: { type: String, required: false },
|
|
7
|
+
disabled: { type: Boolean, required: false },
|
|
8
|
+
help: { type: String, required: false },
|
|
9
|
+
hint: { type: String, required: false },
|
|
10
|
+
label: { type: String, required: false },
|
|
11
|
+
required: { type: Boolean, required: false },
|
|
12
|
+
size: { type: String, required: false },
|
|
13
|
+
ui: { type: Object, required: false }
|
|
14
|
+
});
|
|
15
|
+
const { value } = useVunoField(() => props.name);
|
|
16
|
+
const ui = useVunoTheme("fieldSwitch", () => props.ui);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<USwitch
|
|
21
|
+
v-model="value"
|
|
22
|
+
:disabled="props.disabled"
|
|
23
|
+
:name="props.name"
|
|
24
|
+
:size="props.size"
|
|
25
|
+
:ui="ui"
|
|
26
|
+
/>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { VunoFieldSwitch } from '#vuno/types/field';
|
|
2
|
+
type __VLS_Props = Omit<VunoFieldSwitch, 'class' | 'columns'>;
|
|
3
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
4
|
+
declare const _default: typeof __VLS_export;
|
|
5
|
+
export default _default;
|
|
@@ -12,6 +12,7 @@ import FieldPassword from "#vuno/components/Field/FieldPassword.vue";
|
|
|
12
12
|
import FieldRadio from "#vuno/components/Field/FieldRadio.vue";
|
|
13
13
|
import FieldSelect from "#vuno/components/Field/FieldSelect.vue";
|
|
14
14
|
import FieldSlug from "#vuno/components/Field/FieldSlug.vue";
|
|
15
|
+
import FieldSwitch from "#vuno/components/Field/FieldSwitch.vue";
|
|
15
16
|
import FieldText from "#vuno/components/Field/FieldText.vue";
|
|
16
17
|
import FieldTextarea from "#vuno/components/Field/FieldTextarea.vue";
|
|
17
18
|
const props = defineProps({
|
|
@@ -28,6 +29,7 @@ const components = {
|
|
|
28
29
|
radio: FieldRadio,
|
|
29
30
|
select: FieldSelect,
|
|
30
31
|
slug: FieldSlug,
|
|
32
|
+
switch: FieldSwitch,
|
|
31
33
|
text: FieldText,
|
|
32
34
|
textarea: FieldTextarea
|
|
33
35
|
};
|
|
@@ -42,20 +44,20 @@ const fieldProps = computed(() => {
|
|
|
42
44
|
</script>
|
|
43
45
|
|
|
44
46
|
<template>
|
|
45
|
-
<UFormField
|
|
46
|
-
:class="props.field.class"
|
|
47
|
-
:description="props.field.description"
|
|
48
|
-
:help="props.field.help"
|
|
49
|
-
:hint="props.field.hint"
|
|
50
|
-
:label="props.field.label"
|
|
51
|
-
:name="props.field.name"
|
|
52
|
-
:required="props.field.required"
|
|
53
|
-
:size="size"
|
|
54
|
-
:ui="ui"
|
|
55
|
-
>
|
|
56
|
-
<component
|
|
57
|
-
:is="component"
|
|
58
|
-
v-bind="fieldProps"
|
|
59
|
-
/>
|
|
60
|
-
</UFormField>
|
|
47
|
+
<UFormField
|
|
48
|
+
:class="props.field.class"
|
|
49
|
+
:description="props.field.description"
|
|
50
|
+
:help="props.field.help"
|
|
51
|
+
:hint="props.field.hint"
|
|
52
|
+
:label="props.field.label"
|
|
53
|
+
:name="props.field.name"
|
|
54
|
+
:required="props.field.required"
|
|
55
|
+
:size="size"
|
|
56
|
+
:ui="ui"
|
|
57
|
+
>
|
|
58
|
+
<component
|
|
59
|
+
:is="component"
|
|
60
|
+
v-bind="fieldProps"
|
|
61
|
+
/>
|
|
62
|
+
</UFormField>
|
|
61
63
|
</template>
|
|
@@ -32,8 +32,14 @@ declare const __VLS_export: <TState extends VunoFormState>(__VLS_props: NonNulla
|
|
|
32
32
|
expose: (exposed: {}) => void;
|
|
33
33
|
attrs: any;
|
|
34
34
|
slots: {
|
|
35
|
+
/**
|
|
36
|
+
* `formId` es imprescindible, no un extra: el pie del modal vive fuera
|
|
37
|
+
* del `<form>`, así que un botón propio solo puede enviarlo con
|
|
38
|
+
* `:form="formId"`. Sin exponerlo, cualquier pie a medida queda mudo.
|
|
39
|
+
*/
|
|
35
40
|
footer?: (props: {
|
|
36
41
|
dirty: boolean;
|
|
42
|
+
formId: string;
|
|
37
43
|
loading: boolean;
|
|
38
44
|
close: () => void;
|
|
39
45
|
reset: () => void;
|
|
@@ -33,50 +33,51 @@ function onSubmit(event) {
|
|
|
33
33
|
</script>
|
|
34
34
|
|
|
35
35
|
<template>
|
|
36
|
-
<UModal
|
|
37
|
-
v-model:open="open"
|
|
38
|
-
:description="props.description"
|
|
39
|
-
:dismissible="!props.loading"
|
|
40
|
-
scrollable
|
|
41
|
-
:title="props.title"
|
|
42
|
-
:ui="modalUi"
|
|
43
|
-
>
|
|
44
|
-
<slot name="trigger" />
|
|
45
|
-
|
|
46
|
-
<template #body>
|
|
47
|
-
<Form
|
|
48
|
-
:id="formId"
|
|
49
|
-
ref="form"
|
|
50
|
-
v-model:state="state"
|
|
51
|
-
:definition="props.definition"
|
|
52
|
-
:schema="props.schema"
|
|
53
|
-
:ui="props.formUi"
|
|
54
|
-
@submit="onSubmit"
|
|
55
|
-
/>
|
|
56
|
-
</template>
|
|
57
|
-
|
|
58
|
-
<template #footer>
|
|
59
|
-
<slot
|
|
60
|
-
:close="close"
|
|
61
|
-
:dirty="form?.dirty ?? false"
|
|
62
|
-
:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
:
|
|
76
|
-
:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
36
|
+
<UModal
|
|
37
|
+
v-model:open="open"
|
|
38
|
+
:description="props.description"
|
|
39
|
+
:dismissible="!props.loading"
|
|
40
|
+
scrollable
|
|
41
|
+
:title="props.title"
|
|
42
|
+
:ui="modalUi"
|
|
43
|
+
>
|
|
44
|
+
<slot name="trigger" />
|
|
45
|
+
|
|
46
|
+
<template #body>
|
|
47
|
+
<Form
|
|
48
|
+
:id="formId"
|
|
49
|
+
ref="form"
|
|
50
|
+
v-model:state="state"
|
|
51
|
+
:definition="props.definition"
|
|
52
|
+
:schema="props.schema"
|
|
53
|
+
:ui="props.formUi"
|
|
54
|
+
@submit="onSubmit"
|
|
55
|
+
/>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template #footer>
|
|
59
|
+
<slot
|
|
60
|
+
:close="close"
|
|
61
|
+
:dirty="form?.dirty ?? false"
|
|
62
|
+
:formId="formId"
|
|
63
|
+
:loading="props.loading ?? false"
|
|
64
|
+
name="footer"
|
|
65
|
+
:reset="() => form?.reset?.()"
|
|
66
|
+
>
|
|
67
|
+
<UButton
|
|
68
|
+
color="neutral"
|
|
69
|
+
:disabled="props.loading"
|
|
70
|
+
:label="messages.cancel"
|
|
71
|
+
variant="ghost"
|
|
72
|
+
@click="close"
|
|
73
|
+
/>
|
|
74
|
+
<UButton
|
|
75
|
+
:form="formId"
|
|
76
|
+
:label="messages.save"
|
|
77
|
+
:loading="props.loading"
|
|
78
|
+
type="submit"
|
|
79
|
+
/>
|
|
80
|
+
</slot>
|
|
81
|
+
</template>
|
|
82
|
+
</UModal>
|
|
82
83
|
</template>
|
|
@@ -32,8 +32,14 @@ declare const __VLS_export: <TState extends VunoFormState>(__VLS_props: NonNulla
|
|
|
32
32
|
expose: (exposed: {}) => void;
|
|
33
33
|
attrs: any;
|
|
34
34
|
slots: {
|
|
35
|
+
/**
|
|
36
|
+
* `formId` es imprescindible, no un extra: el pie del modal vive fuera
|
|
37
|
+
* del `<form>`, así que un botón propio solo puede enviarlo con
|
|
38
|
+
* `:form="formId"`. Sin exponerlo, cualquier pie a medida queda mudo.
|
|
39
|
+
*/
|
|
35
40
|
footer?: (props: {
|
|
36
41
|
dirty: boolean;
|
|
42
|
+
formId: string;
|
|
37
43
|
loading: boolean;
|
|
38
44
|
close: () => void;
|
|
39
45
|
reset: () => void;
|
|
@@ -43,7 +43,15 @@ export function useVunoCrud(options) {
|
|
|
43
43
|
});
|
|
44
44
|
const items = computed(() => data.value?.data ?? []);
|
|
45
45
|
const total = computed(() => data.value?.total ?? 0);
|
|
46
|
-
const failure = computed(
|
|
46
|
+
const failure = computed(
|
|
47
|
+
() => isError.value ? {
|
|
48
|
+
// Sin valor propio los deja en `undefined` y el estado de
|
|
49
|
+
// fallo cae a los textos globales de `vuno.messages`.
|
|
50
|
+
hint: options.messages?.listFailedHint,
|
|
51
|
+
title: options.messages?.listFailed,
|
|
52
|
+
retry: () => void refetch()
|
|
53
|
+
} : void 0
|
|
54
|
+
);
|
|
47
55
|
function invalidate() {
|
|
48
56
|
if (!resolvedKey.value.ready) return;
|
|
49
57
|
void queryClient.invalidateQueries({ queryKey: baseKey.value });
|
|
@@ -10,6 +10,14 @@ export type VunoCrudMessages = {
|
|
|
10
10
|
createFailed: string;
|
|
11
11
|
deleted: string;
|
|
12
12
|
deleteFailed: string;
|
|
13
|
+
/** Título cuando falla el listado. Por defecto, el de `vuno.messages`. */
|
|
14
|
+
listFailed: string;
|
|
15
|
+
/**
|
|
16
|
+
* Pista bajo ese título. Es el sitio para lo que evita un error a quien
|
|
17
|
+
* mira una tabla que no cargó: «no des una de alta sin verlas, puede
|
|
18
|
+
* existir ya».
|
|
19
|
+
*/
|
|
20
|
+
listFailedHint: string;
|
|
13
21
|
updated: string;
|
|
14
22
|
updateFailed: string;
|
|
15
23
|
};
|
|
@@ -55,7 +63,15 @@ export type VunoCrudOptions<TRow extends VunoTableRow, TForm> = {
|
|
|
55
63
|
emptyForm: () => TForm;
|
|
56
64
|
list: (query: VunoTableQuery) => Promise<VunoCrudPage<TRow>>;
|
|
57
65
|
remove?: (row: TRow) => Promise<unknown>;
|
|
58
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* Convierte una fila en el estado del formulario al editar.
|
|
68
|
+
*
|
|
69
|
+
* Es opcional solo mientras la fila y el formulario tengan la misma forma.
|
|
70
|
+
* En cuanto la fila traiga campos que no son del formulario —un contador,
|
|
71
|
+
* una fecha de alta— hace falta: sin él se copia la fila entera al estado
|
|
72
|
+
* y esos campos acaban viajando en el guardado, que es donde se nota, lejos
|
|
73
|
+
* de aquí.
|
|
74
|
+
*/
|
|
59
75
|
toForm?: (row: TRow) => TForm;
|
|
60
76
|
update?: (id: number | string, form: TForm) => Promise<unknown>;
|
|
61
77
|
};
|
|
@@ -52,6 +52,13 @@ export type VunoFieldArray = VunoFieldBase & {
|
|
|
52
52
|
export type VunoFieldCheckbox = VunoFieldBase & {
|
|
53
53
|
indeterminate?: boolean;
|
|
54
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Interruptor para un booleano que es un estado encendido o apagado, no una
|
|
57
|
+
* opción que se marca. La diferencia con `checkbox` es de lectura, no de dato:
|
|
58
|
+
* en una pantalla de ajustes el interruptor dice «esto está activo» y la
|
|
59
|
+
* casilla dice «esto queda seleccionado».
|
|
60
|
+
*/
|
|
61
|
+
export type VunoFieldSwitch = VunoFieldBase;
|
|
55
62
|
export type VunoFieldColor = VunoFieldBase & {
|
|
56
63
|
placeholder?: string;
|
|
57
64
|
};
|
|
@@ -112,7 +119,7 @@ export type VunoFieldTextarea = VunoFieldBase & {
|
|
|
112
119
|
* Unión discriminada por `type`. Es lo que consume `VunoFormDefinition`, de modo
|
|
113
120
|
* que cada campo de la definición queda tipado con sus propios props.
|
|
114
121
|
*/
|
|
115
|
-
export type VunoField = WithType<VunoFieldArray, 'array'> | WithType<VunoFieldCheckbox, 'checkbox'> | WithType<VunoFieldColor, 'color'> | WithType<VunoFieldEmail, 'email'> | WithType<VunoFieldFile, 'file'> | WithType<VunoFieldNumber, 'number'> | WithType<VunoFieldPassword, 'password'> | WithType<VunoFieldRadio, 'radio'> | WithType<VunoFieldSelect, 'select'> | WithType<VunoFieldSlug, 'slug'> | WithType<VunoFieldText, 'text'> | WithType<VunoFieldTextarea, 'textarea'>;
|
|
122
|
+
export type VunoField = WithType<VunoFieldArray, 'array'> | WithType<VunoFieldCheckbox, 'checkbox'> | WithType<VunoFieldColor, 'color'> | WithType<VunoFieldEmail, 'email'> | WithType<VunoFieldFile, 'file'> | WithType<VunoFieldNumber, 'number'> | WithType<VunoFieldPassword, 'password'> | WithType<VunoFieldRadio, 'radio'> | WithType<VunoFieldSelect, 'select'> | WithType<VunoFieldSlug, 'slug'> | WithType<VunoFieldSwitch, 'switch'> | WithType<VunoFieldText, 'text'> | WithType<VunoFieldTextarea, 'textarea'>;
|
|
116
123
|
export type VunoFieldType = VunoField['type'];
|
|
117
124
|
/** Extrae los props de un tipo concreto de campo: `VunoFieldOf<'slug'>`. */
|
|
118
125
|
export type VunoFieldOf<TType extends VunoFieldType> = Extract<VunoField, {
|