@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 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) => $fetch('/api/products', { query: 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vuno",
3
3
  "configKey": "vuno",
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
@@ -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
- :loading="props.loading ?? false"
63
- name="footer"
64
- :reset="() => form?.reset?.()"
65
- >
66
- <UButton
67
- color="neutral"
68
- :disabled="props.loading"
69
- :label="messages.cancel"
70
- variant="ghost"
71
- @click="close"
72
- />
73
- <UButton
74
- :form="formId"
75
- :label="messages.save"
76
- :loading="props.loading"
77
- type="submit"
78
- />
79
- </slot>
80
- </template>
81
- </UModal>
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(() => isError.value ? { retry: () => void refetch() } : void 0);
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 });
@@ -50,6 +50,9 @@ export const defaultTheme = {
50
50
  fieldSlug: {
51
51
  slots: { root: "w-full", trailing: "pe-1" }
52
52
  },
53
+ fieldSwitch: {
54
+ slots: { root: "w-full" }
55
+ },
53
56
  fieldText: {
54
57
  slots: { root: "w-full" }
55
58
  },
@@ -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
- /** Convierte una fila en el estado del formulario al editar. */
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, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groupteknology/vuno",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {