@hostlink/nuxt-light 1.21.6 → 1.21.7

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.
@@ -1,718 +0,0 @@
1
- <script setup lang="ts">
2
- import { useQuasar, QTable, type QTableColumn, type QTableProps, Dialog, QTab } from 'quasar';
3
-
4
- import { ref, computed, onMounted, useSlots, useAttrs, watch, defineComponent, h } from "vue";
5
- import { t, q, useLight, GQLFieldBuilder, model } from '../';
6
- import { toQuery } from '@hostlink/light';
7
-
8
- const $q = useQuasar();
9
-
10
-
11
- const minimized = defineModel<boolean>("minimized", { default: false })
12
- const maximized = defineModel<boolean>("maximized", { default: false })
13
-
14
- // extends QTableColumn
15
- export type LTableColumn = QTableColumn & {
16
- searchable?: boolean,
17
- searchType?: string,
18
- searchOptions?: Array<any> | Function,
19
- searchMultiple?: boolean,
20
- gqlField?: string | Array<string> | Object,
21
- backgroundColor?: string | Function
22
- }
23
-
24
-
25
- const errors = ref<InstanceType<any>>([
26
- ]);
27
-
28
- export type LTableProps = QTableProps & {
29
- columns?: Array<LTableColumn>,
30
- actions?: Array<string>,
31
- sortBy?: string | null | undefined,
32
- rowKey?: string,
33
- modelName?: string | null | undefined,
34
- searchable?: boolean | null | undefined,
35
- selected?: Array<any>,
36
- maximizable?: boolean,
37
- minimizable?: boolean,
38
- rows?: Array<any>,
39
- onRequestData?: (request: LTableRequest) => void
40
- addComponent?: Dialog
41
- addComponentProps?: any
42
- }
43
-
44
-
45
- const props = withDefaults(defineProps<LTableProps>(), {
46
- actions: () => [],
47
- rowsPerPageLabel: "Records per page:",
48
- selection: "none",
49
- rowsPerPageOptions: () => [3, 5, 7, 10, 15, 20, 25, 50, 0],
50
- pagination: () => {
51
- return {
52
- sortBy: null,
53
- descending: true,
54
- page: 1,
55
- rowsPerPage: 10,
56
- }
57
- },
58
- fullscreen: false,
59
- searchable: false,
60
- maximizable: false,
61
- minimizable: false,
62
- selected: () => [],
63
- loadingLabel: "Loading...",
64
- noDataLabel: "No data available",
65
- dark: undefined,
66
- });
67
-
68
-
69
- const isServerSide = (props.rows == undefined);
70
-
71
-
72
- const light = useLight();
73
-
74
- const pagination = ref(props.pagination);
75
-
76
- if (props.rowsPerPageOptions[0] == 0) {
77
- pagination.value.rowsPerPage = 0;
78
- }
79
-
80
- //apply all aligns to the columns
81
- props.columns?.forEach((col) => {
82
- if (!col.align) {
83
- col.align = "left";
84
- }
85
- if (!col.field && col.name) {
86
- col.field = col.name
87
- }
88
-
89
- col.label = t(col.label)
90
- })
91
-
92
- if (props.columns) {
93
- for (let i = 0; i < props.columns.length; i++) {
94
- if (props.columns[i].searchOptions) {
95
- let opts = props.columns[i].searchOptions;
96
- if (typeof opts == "function") {
97
- props.columns[i].searchOptions = await opts();
98
- }
99
-
100
-
101
- }
102
- }
103
- }
104
-
105
- export interface LTableRequest {
106
- sort: string,
107
- fields: Array<string>,
108
- gql: {
109
- __args: {
110
- filters: any,
111
- sort: string
112
- },
113
- data: {
114
- __args: {
115
- limit: number
116
- },
117
- [key: string]: any
118
- },
119
- meta: {
120
- total: boolean,
121
- key: boolean,
122
- name: boolean
123
- }
124
- },
125
- offset: number,
126
- limit: number,
127
- loadData: (model: string, filters: any, fields: Array<any>) => void,
128
- loadObjects: (model: string, filters: any, fields: Array<any>) => void,
129
- setData: (data: { data: Array<{ data: any }>, meta: { total: Number, key: string, name: string } }) => void,
130
- }
131
-
132
- const emits = defineEmits<{
133
- "request-data": [p: LTableRequest],
134
- "update:selected": [p: Array<any>]
135
- }>()
136
-
137
-
138
- const loading = ref(false);
139
-
140
- let activeEdit = false;
141
- let actionView = false;
142
- let actionDelete = false;
143
- if (props.actions.length > 0) {
144
- actionView = props.actions.includes("view");
145
- activeEdit = props.actions.includes("edit");
146
- actionDelete = props.actions.includes("delete");
147
- }
148
-
149
-
150
-
151
- if (props.sortBy) {
152
- let [sortBy, descending] = props.sortBy.split(":");
153
- pagination.value.sortBy = sortBy;
154
- pagination.value.descending = descending == "desc";
155
- }
156
-
157
- const hasSearch = computed(() => {
158
- return props.columns?.some((col) => {
159
- return col.searchable;
160
- })
161
- })
162
-
163
- const table = ref<InstanceType<typeof QTable>>();
164
-
165
- const filters = ref<InstanceType<any>>({});
166
- const rows = ref<InstanceType<any>>(props.rows);
167
- if (rows.value == undefined) {
168
- rows.value = [];
169
- }
170
-
171
- onMounted(() => {
172
- table.value && table.value.requestServerInteraction();
173
- })
174
-
175
-
176
- let primaryKey = ref(props.rowKey);
177
- const modelName = ref(props.modelName);
178
-
179
-
180
- const validateData = () => {
181
- if (primaryKey.value == null) return;
182
-
183
- if (props.actions.includes("edit")) {
184
- if (rows.value.length > 0) {
185
- //get first row
186
- let row = rows.value[0];
187
- //check has primary key
188
-
189
- if (!row[primaryKey.value]) {
190
- errors.value.push("[edit] Primary key not found in the data");
191
- }
192
- }
193
- }
194
-
195
- if (props.actions.includes("delete")) {
196
- if (rows.value.length > 0) {
197
- //get first row
198
- let row = rows.value[0];
199
- //check has primary key
200
-
201
- if (!row[primaryKey.value]) {
202
- errors.value.push("[delete] Primary key not found in the data");
203
- }
204
- }
205
- }
206
-
207
- }
208
-
209
- const getData = async (
210
- operation: string,
211
- filters = {},
212
- sort: String,
213
- offset: number,
214
- limit: number,
215
- fields: Array<string> = []) => {
216
-
217
- const query: any = {};
218
- query[operation] = {
219
- meta: {
220
- total: true,
221
- key: true,
222
- name: true
223
- }
224
- }
225
-
226
- let offset_limit: any = {}
227
- if (offset) {
228
- offset_limit.offset = offset;
229
- }
230
-
231
- if (limit) {
232
- offset_limit.limit = limit;
233
- }
234
-
235
- if (offset_limit) {
236
- query[operation].data = {
237
- __args: offset_limit
238
- };
239
- }
240
-
241
- let params: any = {}
242
- if (sort) {
243
- params.sort = sort;
244
- }
245
-
246
- if (filters) {
247
- params.filters = filters
248
- }
249
-
250
- query[operation].__args = params;
251
-
252
- query[operation].data = query[operation].data || {};
253
- Object.assign(query[operation].data, fields);
254
-
255
- const resp = await q(query);
256
- return resp[operation];
257
-
258
- }
259
-
260
- const onLocalRequest = async (p: any) => {
261
- if (!isServerSide) return;
262
-
263
-
264
- let sort = "";
265
- if (p.pagination.sortBy) {
266
- sort = p.pagination.sortBy + ":" + (p.pagination.descending ? "desc" : "asc");
267
- }
268
-
269
- //fields
270
- const builder = GQLFieldBuilder();
271
- if (props.rowKey) {
272
- builder.add(props.rowKey);
273
- }
274
-
275
- props.columns?.forEach((col) => {
276
- if (col.gqlField) {
277
- builder.add(col.gqlField);
278
- return;
279
- }
280
-
281
- if (!col.name) return;
282
- if (col.name.startsWith("_")) {
283
- return;
284
- }
285
- builder.add(col.name);
286
-
287
- });
288
-
289
- if (actionView) {
290
- builder.add("canView");
291
- }
292
-
293
- if (actionDelete) {
294
- builder.add("canDelete");
295
- }
296
-
297
- if (activeEdit) {
298
- builder.add("canUpdate");
299
- }
300
-
301
-
302
- const callback: LTableRequest = {
303
- sort: sort,
304
- fields: builder.get(),
305
- offset: (p.pagination.page - 1) * p.pagination.rowsPerPage,
306
- limit: p.pagination.rowsPerPage,
307
- gql: {
308
- __args: {
309
- filters: getFilterValue(),
310
- sort: sort
311
- },
312
- data: {
313
- __args: {
314
- limit: p.pagination.rowsPerPage,
315
- offset: (p.pagination.page - 1) * p.pagination.rowsPerPage
316
- },
317
- ...toQuery(builder.get())
318
- },
319
- meta: {
320
- total: true,
321
- key: true,
322
- name: true
323
- }
324
- },
325
- setData(data: { data: Array<{ data: any }>, meta: { total: number, key: string, name: string } }) {
326
- rows.value = data.data;
327
- primaryKey.value = data.meta.key;
328
- modelName.value = data.meta.name;
329
- pagination.value.rowsNumber = data.meta.total;
330
-
331
- pagination.value.page = p.pagination.page;
332
- pagination.value.sortBy = p.pagination.sortBy;
333
- pagination.value.descending = p.pagination.descending;
334
- pagination.value.rowsPerPage = p.pagination.rowsPerPage;
335
-
336
- loading.value = false;
337
-
338
- validateData();
339
- },
340
- loadObjects(model: string, filters: any = null, fields: Array<any> = []) {
341
- return this.loadData("list" + model, filters, fields);
342
- },
343
- async loadData(operation: string, filters: any = null, fields: Array<any> = []) {
344
- fields.forEach((f) => {
345
- builder.add(f);
346
- })
347
-
348
- let localFilters = getFilterValue();
349
- if (filters) {
350
- localFilters = {
351
- ...localFilters,
352
- ...filters
353
- }
354
- }
355
- loading.value = true;
356
- try {
357
- const allData = await getData(operation, localFilters, sort, this.offset, this.limit, builder.get());
358
- this.setData(allData);
359
- } catch (e: any) {
360
- quasar.dialog({
361
- message: e,
362
- color: "negative"
363
- })
364
- this.setData({ data: [], meta: { total: 0, key: "", name: "" } });
365
- }
366
-
367
-
368
-
369
- }
370
- }
371
-
372
-
373
- loading.value = true;
374
- //emits("request", p);
375
- emits("request-data", callback);
376
-
377
- }
378
-
379
- const getFilterValue = () => {
380
- let f: any = {};
381
-
382
- props.columns?.forEach((col) => {
383
- if (col.searchable) {
384
- if (filters.value[col.name] !== null && filters.value[col.name] !== "" && filters.value[col.name] !== undefined) {
385
-
386
- if (col.searchType == "number") {
387
- f[col.name] = filters.value[col.name]
388
- } else if (col.searchType == "date") {
389
- if (filters.value[col.name].from) {
390
- f[col.name] = {
391
- between: [filters.value[col.name].from, filters.value[col.name].to]
392
- }
393
- }
394
- } else if (col.searchType == "select") {
395
- f[col.name] = filters.value[col.name]
396
-
397
- } else {
398
- f[col.name] = {
399
- contains: filters.value[col.name]
400
- }
401
- }
402
- }
403
- }
404
- });
405
- return f;
406
- }
407
-
408
-
409
- const onFilters = () => {
410
-
411
- //clone the filters
412
- onLocalRequest({
413
- pagination: {
414
- page: 1,
415
- rowsPerPage: props.pagination.rowsPerPage,
416
- sortBy: props.pagination.sortBy,
417
- descending: props.pagination.descending
418
- }
419
- })
420
- }
421
-
422
- const slots = useSlots();
423
-
424
- const ss = Object.entries(slots).map(([key, value]) => {
425
- return key;
426
- });
427
-
428
- const quasar = useQuasar();
429
- const onDelete = async (id: any) => {
430
- if (modelName.value == null) return;
431
-
432
- try {
433
- await model(modelName.value).delete(id);
434
- } catch (e: any) {
435
- quasar.notify({
436
- message: e.message,
437
- color: "negative"
438
- })
439
- return;
440
- }
441
- // refresh the table
442
- if (table.value) {
443
- table.value.requestServerInteraction();
444
- }
445
- }
446
- const attrs = computed(() => {
447
- return {
448
- ...props,
449
-
450
- ...{
451
- dense: light.getStyle("tableDense"),
452
- flat: light.getStyle("tableFlat"),
453
- bordered: light.getStyle("tableBorder"),
454
- separator: light.getStyle("tableSeparator"),
455
- },
456
- loadingLabel: t(props.loadingLabel),
457
- noDataLabel: t(props.noDataLabel),
458
- rowsPerPageOptions: props.rowsPerPageOptions,
459
- rowsPerPageLabel: t(props.rowsPerPageLabel),
460
- selection: props.selection,
461
- rowKey: props.rowKey,
462
- }
463
- });
464
-
465
- const filter = ref('');
466
-
467
- const toColumns = props.columns?.filter((col: any) => {
468
- return col.to;
469
- }).map((col: any) => {
470
- col.slot_name = 'body-cell-' + col.name;
471
- return col;
472
-
473
- })
474
- /*
475
- const filterFn = (val, update, abort) {
476
- update(() => {
477
- const needle = val.toLocaleLowerCase()
478
- options.value = stringOptions.filter(v => v.toLocaleLowerCase().indexOf(needle) > -1)
479
- })
480
- },
481
- */
482
-
483
- const isDark = computed(() => {
484
- return light.theme == "dark";
485
- })
486
-
487
- const hasRowExpand = computed(() => {
488
- return ss.indexOf("row-expand") >= 0;
489
- })
490
-
491
- const hasActions = computed(() => {
492
- return props.actions.length > 0;
493
- })
494
-
495
- const getCellStyle = (col: any, row: any) => {
496
- const style: any = col.cellStyle ?? {};
497
- if (col.backgroundColor) {
498
- if (typeof col.backgroundColor == "function") {
499
- style.backgroundColor = col.backgroundColor(row);
500
- } else {
501
- style.backgroundColor = col.backgroundColor;
502
- }
503
-
504
- }
505
- return style;
506
- }
507
-
508
- const getCellClass = (col: any, row: any) => {
509
- const cl: any = [];
510
-
511
- if (col.cellClass) {
512
- if (typeof col.cellClass == "function") {
513
- cl.push(col.cellClass(row));
514
- } else {
515
- cl.push(col.cellClass);
516
- }
517
-
518
- }
519
- return cl;
520
- }
521
-
522
- const localSelected = computed({
523
- get() {
524
- return props.selected;
525
- },
526
- set(val) {
527
- emits("update:selected", val);
528
- }
529
- });
530
-
531
- watch(() => props.rows, (val) => {
532
- rows.value = val;
533
- });
534
-
535
- const computedColumns = computed(() => {
536
- return props.columns;
537
- })
538
-
539
-
540
- function requestServerInteraction() {
541
- table.value && table.value.requestServerInteraction();
542
- }
543
-
544
- defineExpose({
545
- requestServerInteraction
546
- })
547
-
548
- const onAdd = () => {
549
- if (!props.addComponent) return;
550
-
551
- $q.dialog({
552
- component: props.addComponent,
553
- componentProps: props.addComponentProps,
554
-
555
- }).onOk(() => {
556
- requestServerInteraction();
557
- })
558
- }
559
- </script>
560
-
561
-
562
- <template>
563
- <l-card flat bordered :maximizable="maximizable" :minimizable="minimizable" :title="title"
564
- v-model:minimized="minimized" v-model:maximized="maximized">
565
-
566
- <q-toolbar v-if="addComponent">
567
- <q-btn icon="sym_o_add" :label="$t('Add')" :color="$light.color" @click="onAdd" v-if="addComponent" />
568
- </q-toolbar>
569
-
570
-
571
- <template v-if="errors.length > 0">
572
- <div class="q-mb-sm">
573
- <div class="q-gutter-sm">
574
- <l-alert type="negative" v-for="e in errors">{{ e }}</l-alert>
575
- </div>
576
- </div>
577
- </template>
578
-
579
- <q-table v-bind="attrs" :loading="loading" :rows="rows" ref="table" @request="onLocalRequest" :columns="columns"
580
- v-model:pagination="pagination" :filter="filter" v-model:selected="localSelected">
581
-
582
-
583
-
584
-
585
-
586
- <template #header="props">
587
- <q-tr :props="props">
588
- <q-td v-if="selection != 'none'" auto-width>
589
- <q-checkbox v-model="props.selected" />
590
- </q-td>
591
- <q-th v-if="hasRowExpand" auto-width></q-th>
592
- <q-th v-if="hasActions" auto-width></q-th>
593
- <q-th v-for="col in props.cols" :key="col.name" :props="props">{{ col.label }}</q-th>
594
- </q-tr>
595
- </template>
596
-
597
-
598
-
599
- <template #body="props">
600
- <q-tr :props="props">
601
- <q-td v-if="selection != 'none'" auto-width>
602
- <q-checkbox v-model="props.selected" />
603
- </q-td>
604
- <q-td v-if="hasRowExpand" auto-width>
605
- <q-btn :class="{ 'text-grey-8': !isDark }" flat round size="sm"
606
- :icon="props.expand ? 'sym_o_expand_more' : 'sym_o_expand_less'"
607
- @click="props.expand = !props.expand"></q-btn>
608
- </q-td>
609
-
610
- <q-td v-if="hasActions" auto-width>
611
- <div :class="{ 'text-grey-8': !isDark }" v-if="primaryKey">
612
-
613
- <l-view-btn v-if="actionView && props.row.canView"
614
- :to="`/${modelName}/${props.row[primaryKey]}/view`" size="sm" />
615
-
616
- <l-edit-btn v-if="activeEdit && props.row.canUpdate"
617
- :to="`/${modelName}/${props.row[primaryKey]}/edit`" size="sm" />
618
-
619
- <l-delete-btn v-if="actionDelete && props.row.canDelete"
620
- @submit="onDelete(props.row[primaryKey])" size="sm"></l-delete-btn>
621
-
622
- <slot name="actions" v-bind="props"></slot>
623
- </div>
624
-
625
- </q-td>
626
-
627
-
628
-
629
- <template v-for="col in props.cols">
630
- <template v-if="ss.indexOf('body-cell-' + col.name) >= 0">
631
- <slot :name="'body-cell-' + col.name" v-bind="props"></slot>
632
- </template>
633
- <template v-else>
634
- <q-td :key="col.name" :props="props" :auto-width="col.autoWidth ?? false"
635
- :style="getCellStyle(col, props.row)" :class="getCellClass(col, props.row)"><template
636
- v-if="col.to" class="bg-primary">
637
- <l-link :to="col.to(props.row)">{{ col.value }}</l-link>
638
- </template><template v-else>{{ col.value }}</template></q-td>
639
- </template>
640
- </template>
641
- </q-tr>
642
- <q-tr v-show="props.expand" :props="props">
643
- <q-td colspan="100%">
644
- <slot name="row-expand" v-bind="props"></slot>
645
- </q-td>
646
- </q-tr>
647
- </template>
648
-
649
-
650
- <template #top-right="props" v-if="searchable">
651
- <q-input v-if="searchable" outlined dense debounce="300" v-model="filter" :placeholder="$t('Search')"
652
- :color="$light.color">
653
- <template v-slot:append>
654
- <q-icon name="search" />
655
- </template>
656
- </q-input>
657
-
658
-
659
-
660
-
661
- </template>
662
-
663
-
664
- <!-- template v-for="col in toColumns" v-slot:[col.slot_name]="props">
665
- <q-td :props="props">
666
- <l-link :to="col.to(props.row)">
667
- {{ col.field(props.row) }}
668
- </l-link>
669
- </q-td>
670
- </template -->
671
-
672
-
673
- <template #top-row="props" v-if="hasSearch && isServerSide">
674
- <q-tr>
675
- <q-td v-if="selection != 'none'" auto-width />
676
- <q-td v-if="hasRowExpand" auto-width />
677
- <q-td v-if="hasActions" auto-width />
678
- <q-td v-for="col in props.cols">
679
- <template v-if="col.searchable">
680
-
681
- <template v-if="col.searchType == 'number'">
682
- <q-input style="min-width: 80px;" dense clearable filled square
683
- v-model.number="filters[col.name]" @keydown.enter.prevent="onFilters"
684
- @clear="onFilters" mask="##########" :enterkeyhint="$t('search')"></q-input>
685
- </template>
686
-
687
- <template v-if="col.searchType == 'select'">
688
- <q-select dense clearable filled square v-model="filters[col.name]"
689
- @update:model-value="onFilters" options-dense :options="col.searchOptions"
690
- emit-value map-options :multiple="col.searchMultiple" :color="$light.color" />
691
-
692
- </template>
693
-
694
-
695
- <template v-if="col.searchType == 'date'">
696
- <l-date-picker dense clearable filled square :outlined="false" hide-bottom-space
697
- v-model="filters[col.name]" @update:model-value="onFilters" range
698
- @clear="onFilters" />
699
- </template>
700
-
701
- <template v-if="!col.searchType || col.searchType == 'text'">
702
- <q-input style="min-width: 80px;" dense clearable filled square
703
- v-model="filters[col.name]" @keydown.enter.prevent="onFilters" @clear="onFilters"
704
- :enterkeyhint="$t('search')" :color="$light.color"></q-input>
705
-
706
- </template>
707
-
708
- </template>
709
- </q-td>
710
- </q-tr>
711
- </template>
712
-
713
- <template v-for="slot of ss" v-slot:[slot]="props">
714
- <slot :name="slot" v-bind="props"></slot>
715
- </template>
716
- </q-table>
717
- </l-card>
718
- </template>