@verdnatura/vn-front-lib 0.0.9 → 0.0.11

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.
Files changed (51) hide show
  1. package/dist/components-BrEjZWQj.js +3278 -0
  2. package/dist/components-Dty6kM2e.cjs +2 -0
  3. package/dist/components.cjs +1 -1
  4. package/dist/components.js +2 -2
  5. package/dist/composables-DUOUI4DI.js +3558 -0
  6. package/dist/composables-ZxHJjJGN.cjs +4 -0
  7. package/dist/composables.cjs +1 -1
  8. package/dist/composables.js +2 -2
  9. package/dist/filters.cjs +1 -0
  10. package/dist/filters.js +62 -0
  11. package/dist/rolldown-runtime-DF3sbh8G.cjs +1 -0
  12. package/dist/stores.cjs +1 -1
  13. package/dist/stores.js +144 -2
  14. package/dist/utils.cjs +1 -0
  15. package/dist/utils.js +131 -0
  16. package/dist/vn-front-lib.cjs +1 -1
  17. package/dist/vn-front-lib.css +1 -1
  18. package/dist/vn-front-lib.js +6 -7
  19. package/lib/components/dialogs/VnConfirm.vue +142 -0
  20. package/lib/components/feedback/VnSkeletonForm.vue +14 -0
  21. package/lib/components/feedback/VnSpinner.vue +63 -0
  22. package/lib/components/index.js +5 -0
  23. package/lib/components/models/VnFetch.vue +65 -0
  24. package/lib/components/models/VnForm.vue +492 -0
  25. package/lib/composables/index.js +7 -0
  26. package/lib/composables/tMobile.js +8 -0
  27. package/lib/composables/useArrayData.js +451 -0
  28. package/lib/composables/useForm.js +12 -0
  29. package/lib/composables/useNotify.js +24 -0
  30. package/lib/composables/useObjectComparison.js +67 -0
  31. package/lib/composables/useQueryResolver.js +306 -0
  32. package/lib/composables/useState.js +81 -0
  33. package/lib/filters/filterPanel.js +95 -0
  34. package/lib/filters/getDifferences.js +18 -0
  35. package/lib/filters/getUpdatedValues.js +6 -0
  36. package/lib/filters/index.js +5 -0
  37. package/lib/filters/isDialogOpened.js +3 -0
  38. package/lib/filters/onBeforeSave.js +9 -0
  39. package/lib/stores/index.js +2 -0
  40. package/lib/stores/useArrayDataStore.js +82 -0
  41. package/lib/stores/useStateStore.js +84 -0
  42. package/lib/utils/advancedFilters.js +125 -0
  43. package/lib/utils/index.js +2 -0
  44. package/lib/utils/trackAnalytics.js +18 -0
  45. package/package.json +9 -2
  46. package/dist/components-BLui5eyq.cjs +0 -1
  47. package/dist/components-Dl-Ieazh.js +0 -2466
  48. package/dist/useRequired-BqsSq8Mw.cjs +0 -1
  49. package/dist/useRequired-CTxBp3qy.js +0 -58
  50. package/dist/useValidationsStore-8JX1-oLO.cjs +0 -1
  51. package/dist/useValidationsStore-9Fxtj_pd.js +0 -12
@@ -0,0 +1,492 @@
1
+ <script setup>
2
+ import axios from 'axios';
3
+ import { onMounted, onUnmounted, computed, ref, watch, nextTick } from 'vue';
4
+ import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router';
5
+ import { useI18n } from 'vue-i18n';
6
+ import { useQuasar } from 'quasar';
7
+ import {
8
+ useState,
9
+ useValidator,
10
+ useNotify,
11
+ tMobile,
12
+ useArrayData,
13
+ focusFirstInput,
14
+ } from 'lib/composables';
15
+ import { useStateStore } from 'lib/stores';
16
+ import { VnConfirm, VnSpinner, VnSkeletonForm } from 'lib/components';
17
+ import { onBeforeSave } from 'lib/filters';
18
+ const { push } = useRouter();
19
+ const quasar = useQuasar();
20
+ const state = useState();
21
+ const stateStore = useStateStore();
22
+ const { t } = useI18n();
23
+ const { validate, validations } = useValidator();
24
+ const { notify } = useNotify();
25
+ const route = useRoute();
26
+ const myForm = ref(null);
27
+ const $props = defineProps({
28
+ url: {
29
+ type: String,
30
+ default: '',
31
+ },
32
+ model: {
33
+ type: String,
34
+ default: null,
35
+ },
36
+ filter: {
37
+ type: Object,
38
+ default: null,
39
+ },
40
+ urlUpdate: {
41
+ type: String,
42
+ default: null,
43
+ },
44
+ urlCreate: {
45
+ type: String,
46
+ default: null,
47
+ },
48
+ defaultActions: {
49
+ type: Boolean,
50
+ default: true,
51
+ },
52
+ defaultButtons: {
53
+ type: Object,
54
+ default: () => {},
55
+ },
56
+ autoLoad: {
57
+ type: Boolean,
58
+ default: false,
59
+ },
60
+ formInitialData: {
61
+ type: Object,
62
+ default: () => {},
63
+ },
64
+ observeFormChanges: {
65
+ type: Boolean,
66
+ default: true,
67
+ description:
68
+ 'Esto se usa principalmente para permitir guardar sin hacer cambios (Útil para la feature de clonar ya que en este caso queremos poder guardar de primeras)',
69
+ },
70
+ mapper: {
71
+ type: Function,
72
+ default: onBeforeSave,
73
+ },
74
+ clearStoreOnUnmount: {
75
+ type: Boolean,
76
+ default: true,
77
+ },
78
+ saveFn: {
79
+ type: Function,
80
+ default: null,
81
+ },
82
+ goTo: {
83
+ type: String,
84
+ default: '',
85
+ description: 'It is used for redirect on click "save and continue"',
86
+ },
87
+ reload: {
88
+ type: Boolean,
89
+ default: true,
90
+ },
91
+ defaultTrim: {
92
+ type: Boolean,
93
+ default: true,
94
+ },
95
+ maxWidth: {
96
+ type: [String, Boolean],
97
+ default: '800px',
98
+ },
99
+ onDataSaved: {
100
+ type: Function,
101
+ default: () => {},
102
+ },
103
+ preventSubmit: {
104
+ type: Boolean,
105
+ default: false,
106
+ },
107
+ customMethod: {
108
+ type: String,
109
+ default: null,
110
+ },
111
+ formUrl: {
112
+ type: [String, Boolean],
113
+ default: 'createForm',
114
+ },
115
+ paddingClass: {
116
+ type: String,
117
+ default: 'q-pa-md',
118
+ },
119
+ showTitle: {
120
+ type: Boolean,
121
+ default: false,
122
+ },
123
+ title: {
124
+ type: String,
125
+ default: null,
126
+ },
127
+ });
128
+ const emit = defineEmits(['onFetch', 'onDataSaved', 'onDataSaveFail', 'submit']);
129
+ const modelValue = computed(
130
+ () => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`,
131
+ ).value;
132
+ const componentIsRendered = ref(false);
133
+ const arrayData = useArrayData(modelValue);
134
+ const isLoading = ref(false);
135
+ // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
136
+ const isResetting = ref(false);
137
+ const hasChanges = ref(!$props.observeFormChanges);
138
+ let urlData = {};
139
+ const originalData = computed(() => state.get(modelValue));
140
+ const formData = ref();
141
+ const formEl = computed(() => myForm.value?.$el);
142
+ const defaultButtons = computed(() => ({
143
+ save: {
144
+ dataCy: 'saveDefaultBtn',
145
+ color: 'primary',
146
+ icon: 'save',
147
+ label: 'globals.save',
148
+ click: async (evt) => submitForm(evt),
149
+ type: 'submit',
150
+ },
151
+ reset: {
152
+ dataCy: 'resetDefaultBtn',
153
+ color: 'primary',
154
+ icon: 'restart_alt',
155
+ label: 'globals.reset',
156
+ click: () => reset(),
157
+ },
158
+ ...$props.defaultButtons,
159
+ }));
160
+
161
+ const submitForm = async (evt) => {
162
+ const isFormValid = await myForm.value.validate();
163
+ if (isFormValid) {
164
+ await save(evt);
165
+ }
166
+ };
167
+
168
+ function getUrlData() {
169
+ urlData = $props.formUrl
170
+ ? JSON.parse(route?.query[$props.formUrl] ?? '{}')
171
+ : undefined;
172
+ }
173
+
174
+ onMounted(async () => {
175
+ if (!arrayData.store.url && $props.url)
176
+ arrayData.setOptions({ url: $props.url, filter: $props.filter });
177
+ getUrlData();
178
+ nextTick(() => (componentIsRendered.value = true));
179
+
180
+ // Podemos enviarle al form la estructura de data inicial sin necesidad de fetchearla
181
+ if ($props.formInitialData && urlData)
182
+ state.set(modelValue, { ...$props.formInitialData, ...urlData });
183
+
184
+ if (!$props.formInitialData) {
185
+ if ($props.autoLoad && $props.url) await fetch();
186
+ else if (arrayData.store.data)
187
+ updateAndEmit('onFetch', { val: arrayData.store.data });
188
+ }
189
+ if ($props.observeFormChanges) {
190
+ watch(
191
+ () => formData.value,
192
+ (newVal, oldVal) => {
193
+ if (!oldVal) return;
194
+ hasChanges.value =
195
+ !isResetting.value &&
196
+ JSON.stringify(newVal) !== JSON.stringify(originalData.value);
197
+ isResetting.value = false;
198
+ },
199
+ { deep: true },
200
+ );
201
+ }
202
+ });
203
+
204
+ if (!$props.url)
205
+ watch(
206
+ () => arrayData.store.data,
207
+ (val) => updateAndEmit('onFetch', { val }),
208
+ );
209
+
210
+ watch(
211
+ originalData,
212
+ async (val) => {
213
+ if (val) formData.value = JSON.parse(JSON.stringify(val));
214
+ await nextTick();
215
+ if (formEl.value) focusFirstInput(formEl.value);
216
+ },
217
+ { immediate: true },
218
+ );
219
+
220
+ watch(
221
+ () => [$props.url, $props.filter],
222
+ async () => {
223
+ state.set(modelValue, null);
224
+ reset();
225
+ await fetch();
226
+ },
227
+ );
228
+
229
+ if ($props.formInitialData)
230
+ watch(
231
+ () => $props.formInitialData,
232
+ (val) => {
233
+ if (val) state.set(modelValue, { ...val, ...urlData });
234
+ },
235
+ );
236
+
237
+ onBeforeRouteLeave((to, from, next) => {
238
+ if (hasChanges.value && $props.observeFormChanges)
239
+ quasar.dialog({
240
+ component: VnConfirm,
241
+ componentProps: {
242
+ title: t('globals.unsavedPopup.title'),
243
+ message: t('globals.unsavedPopup.subtitle'),
244
+ promise: () => next(),
245
+ },
246
+ });
247
+ else next();
248
+ });
249
+
250
+ onUnmounted(() => {
251
+ // Restauramos los datos originales en el store si se realizaron cambios en el formulario pero no se guardaron, evitando modificaciones erróneas.
252
+ if (hasChanges.value) return state.set(modelValue, originalData.value);
253
+ if ($props.clearStoreOnUnmount) state.unset(modelValue);
254
+ });
255
+
256
+ async function fetch() {
257
+ try {
258
+ let data;
259
+ if ($props.model)
260
+ data = (await arrayData.addFilter({ filter: $props.filter })).response?.data;
261
+ else
262
+ data = (
263
+ await axios.get($props.url, {
264
+ params: { filter: JSON.stringify($props.filter) },
265
+ })
266
+ ).data;
267
+ if (Array.isArray(data)) data = data[0] ?? {};
268
+
269
+ updateAndEmit('onFetch', { val: data });
270
+ } catch (e) {
271
+ state.set(modelValue, {});
272
+ throw e;
273
+ }
274
+ }
275
+
276
+ async function save() {
277
+ if ($props.observeFormChanges && !hasChanges.value)
278
+ return notify('globals.noChanges', 'negative');
279
+
280
+ isLoading.value = true;
281
+ try {
282
+ formData.value = trimData(formData.value);
283
+ const method = $props.urlCreate ? 'post' : 'patch';
284
+ const body =
285
+ method === 'post'
286
+ ? $props.mapper(formData.value)
287
+ : $props.mapper(formData.value, originalData.value);
288
+ const url =
289
+ $props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
290
+ const response = await Promise.resolve(
291
+ $props.saveFn
292
+ ? $props.saveFn(body)
293
+ : axios[$props.customMethod ?? method](url, body),
294
+ );
295
+
296
+ if ($props.urlCreate) notify('globals.dataCreated', 'positive');
297
+
298
+ hasChanges.value = false;
299
+ updateAndEmit('onDataSaved', {
300
+ val: formData.value,
301
+ res: response?.data,
302
+ old: originalData.value,
303
+ rawRes: response,
304
+ });
305
+ if ($props.reload) await arrayData.fetch({});
306
+ } catch (e) {
307
+ emit('onDataSaveFail', formData.value);
308
+ throw e;
309
+ } finally {
310
+ isLoading.value = false;
311
+ }
312
+ }
313
+
314
+ async function saveAndGo() {
315
+ await save(true);
316
+ push({ path: $props.goTo });
317
+ }
318
+
319
+ function reset() {
320
+ formData.value = JSON.parse(JSON.stringify(originalData.value));
321
+ updateAndEmit('onFetch', { val: originalData.value });
322
+ if ($props.observeFormChanges) {
323
+ hasChanges.value = false;
324
+ isResetting.value = true;
325
+ }
326
+ }
327
+
328
+ // eslint-disable-next-line vue/no-dupe-keys
329
+ function filter(value, update, filterOptions) {
330
+ update(
331
+ () => {
332
+ const { options, filterFn } = filterOptions;
333
+
334
+ options.value = filterFn(options, value);
335
+ },
336
+ (ref) => {
337
+ ref.setOptionIndex(-1);
338
+ ref.moveOptionSelection(1, true);
339
+ },
340
+ );
341
+ }
342
+
343
+ async function updateAndEmit(
344
+ evt,
345
+ { val, res, old, rawRes } = { val: null, res: null, old: null, rawRes: null },
346
+ ) {
347
+ state.set(modelValue, val);
348
+ if (!$props.url) arrayData.store.data = val;
349
+
350
+ emit(evt, state.get(modelValue), res, old, formData, rawRes);
351
+ if (formEl.value) focusFirstInput(formEl.value);
352
+ }
353
+
354
+ function trimData(data) {
355
+ if (!$props.defaultTrim) return data;
356
+ for (const key in data) {
357
+ if (typeof data[key] == 'string') data[key] = data[key].trim();
358
+ }
359
+ return data;
360
+ }
361
+
362
+ async function onKeyup(evt) {
363
+ if (evt.key === 'Enter' && !$props.preventSubmit && hasChanges.value) {
364
+ const input = evt.target;
365
+ if (input.type == 'textarea' && evt.shiftKey) {
366
+ let { selectionStart, selectionEnd } = input;
367
+ input.value =
368
+ input.value.substring(0, selectionStart) +
369
+ '\n' +
370
+ input.value.substring(selectionEnd);
371
+ selectionStart = selectionEnd = selectionStart + 1;
372
+ return;
373
+ }
374
+ await myForm.value.submit(evt);
375
+ }
376
+ }
377
+
378
+ defineExpose({
379
+ submitForm,
380
+ myForm,
381
+ save,
382
+ isLoading,
383
+ hasChanges,
384
+ reset,
385
+ fetch,
386
+ formData,
387
+ });
388
+ </script>
389
+ <template>
390
+ <div class="column items-center full-width" style="position: relative">
391
+ <QInnerLoading
392
+ :showing="isLoading"
393
+ :label="t('globals.pleaseWait')"
394
+ color="primary"
395
+ style="min-width: 100%"
396
+ >
397
+ <VnSpinner size="md" />
398
+ </QInnerLoading>
399
+ <QForm
400
+ id="vnForm"
401
+ v-bind="$attrs"
402
+ ref="myForm"
403
+ v-if="formData"
404
+ @submit.prevent="save"
405
+ @keyup.prevent="onKeyup"
406
+ @reset="reset"
407
+ class="full-width"
408
+ :class="[paddingClass]"
409
+ :style="{ maxWidth: maxWidth || 'none', width: '100%' }"
410
+ >
411
+ <QCard class="q-pa-none">
412
+ <QCardSection
413
+ v-if="showTitle"
414
+ class="vn-basic-data__header q-px-none q-pb-none"
415
+ >
416
+ <span
417
+ class="text-h6"
418
+ v-text="title || $t('globals.pageTitles.basicData')"
419
+ />
420
+ </QCardSection>
421
+ <slot
422
+ v-if="formData"
423
+ name="form"
424
+ :data="formData"
425
+ :validate="validate"
426
+ :validations="validations()"
427
+ :filter="filter"
428
+ />
429
+ <VnSkeletonForm v-else />
430
+ </QCard>
431
+ </QForm>
432
+ </div>
433
+ <Teleport
434
+ to="#st-actions"
435
+ v-if="
436
+ $props.defaultActions &&
437
+ stateStore?.isSubToolbarShown() &&
438
+ componentIsRendered
439
+ "
440
+ >
441
+ <QBtnGroup push class="q-gutter-x-sm">
442
+ <slot name="moreActions" />
443
+ <QBtn
444
+ :label="tMobile(defaultButtons.reset.label)"
445
+ :color="defaultButtons.reset.color"
446
+ :icon="defaultButtons.reset.icon"
447
+ flat
448
+ @click="defaultButtons.reset.click"
449
+ :disable="!hasChanges"
450
+ :title="t(defaultButtons.reset.label)"
451
+ data-cy="resetDefaultBtn"
452
+ />
453
+ <QBtn
454
+ data-cy="saveDefaultBtn"
455
+ :label="tMobile('globals.save')"
456
+ color="primary"
457
+ icon="save"
458
+ @click="defaultButtons.save.click"
459
+ :disable="!hasChanges"
460
+ :title="t(defaultButtons.save.label)"
461
+ :flat="!!$props.goTo"
462
+ />
463
+ <QBtn
464
+ v-if="$props.goTo"
465
+ data-cy="saveAndContinueDefaultBtn"
466
+ @click="saveAndGo"
467
+ :label="
468
+ tMobile('globals.saveAndContinue') +
469
+ ' ' +
470
+ t('globals.' + $props.goTo.split('/').pop())
471
+ "
472
+ :title="
473
+ t('globals.saveAndContinue') +
474
+ ' ' +
475
+ t('globals.' + $props.goTo.split('/').pop())
476
+ "
477
+ :disable="!hasChanges"
478
+ color="primary"
479
+ icon="save"
480
+ />
481
+ </QBtnGroup>
482
+ </Teleport>
483
+ </template>
484
+ <style lang="scss" scoped>
485
+ .q-notifications {
486
+ color: black;
487
+ }
488
+
489
+ .q-card {
490
+ padding: 32px;
491
+ }
492
+ </style>
@@ -1,2 +1,9 @@
1
+ export * from './tMobile';
2
+ export * from './useArrayData';
3
+ export * from './useForm';
4
+ export * from './useNotify';
5
+ export * from './useObjectComparison';
6
+ export * from './useQueryResolver';
1
7
  export * from './useRequired';
8
+ export * from './useState';
2
9
  export * from './useValidator';
@@ -0,0 +1,8 @@
1
+ import { useQuasar } from 'quasar';
2
+ import { useI18n } from 'vue-i18n';
3
+
4
+ export function tMobile(...args) {
5
+ const quasar = useQuasar();
6
+ const { t } = useI18n();
7
+ if (!quasar.screen.xs) return t(...args);
8
+ }