adminforth 1.2.56 → 1.2.57

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 (34) hide show
  1. package/dist/spa/index.html +1 -1
  2. package/dist/spa/package-lock.json +215 -1
  3. package/dist/spa/package.json +2 -0
  4. package/dist/spa/spa/src/types/AdminForthConfig.ts +1477 -0
  5. package/dist/spa/spa/src/types/FrontendAPI.ts +121 -0
  6. package/dist/spa/src/App.vue +176 -133
  7. package/dist/spa/src/assets/base.css +2 -0
  8. package/dist/spa/src/components/Breadcrumbs.vue +3 -2
  9. package/dist/spa/src/components/CustomDatePicker.vue +6 -4
  10. package/dist/spa/src/components/CustomRangePicker.vue +11 -7
  11. package/dist/spa/src/components/Filters.vue +16 -5
  12. package/dist/spa/src/components/MenuLink.vue +3 -3
  13. package/dist/spa/src/components/ResourceForm.vue +140 -111
  14. package/dist/spa/src/components/ResourceListTable.vue +76 -101
  15. package/dist/spa/src/components/SkeleteLoader.vue +23 -0
  16. package/dist/spa/src/components/Toast.vue +18 -7
  17. package/dist/spa/src/components/ValueRenderer.vue +10 -4
  18. package/dist/spa/src/composables/useStores.ts +25 -11
  19. package/dist/spa/src/index.scss +2 -1
  20. package/dist/spa/src/main.ts +1 -1
  21. package/dist/spa/src/router/index.ts +0 -1
  22. package/dist/spa/src/stores/user.ts +21 -3
  23. package/dist/spa/src/types/AdminForthConfig.ts +1477 -0
  24. package/dist/spa/src/types/FrontendAPI.ts +121 -0
  25. package/dist/spa/src/utils.ts +6 -4
  26. package/dist/spa/src/views/CreateView.vue +2 -9
  27. package/dist/spa/src/views/EditView.vue +1 -9
  28. package/dist/spa/src/views/ListView.vue +17 -16
  29. package/dist/spa/src/views/LoginView.vue +57 -31
  30. package/dist/spa/src/views/ResourceParent.vue +1 -2
  31. package/dist/spa/src/views/ShowView.vue +17 -21
  32. package/dist/spa/tailwind.config.js +3 -1
  33. package/dist/spa/vite.config.ts +7 -0
  34. package/package.json +2 -2
@@ -1,42 +1,29 @@
1
1
  <template>
2
2
  <!-- table -->
3
- <div class="relative overflow-x-auto shadow-list-table-shadow dark:shadow-black overflow-y-hidden rounded-default">
3
+ <div class="relative overflow-x-auto shadow-listTableShadow dark:shadow-darkListTableShadow overflow-y-hidden rounded-default">
4
4
 
5
5
  <!-- skelet loader -->
6
6
  <div role="status" v-if="!resource || !resource.columns"
7
7
  class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded shadow animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
8
- <div class="flex items-center justify-between h-16">
9
- <div>
10
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
11
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
12
- </div>
13
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
14
- </div>
15
-
16
- <div class="flex items-center justify-between h-16 pt-4" v-for="i in new Array(10)">
17
- <div>
18
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
19
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
20
- </div>
21
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
22
- </div>
23
- <span class="sr-only">Loading...</span>
8
+
24
9
  </div>
25
10
 
26
11
  <table v-else class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-default">
27
- <thead class="text-xs text-view-table-heading-text bg-list-view-table-heading dark:bg-gray-700 dark:text-gray-400">
12
+ <thead class="text-xs text-view-table-heading-text bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
28
13
  <tr>
29
14
  <th scope="col" class="p-4">
30
15
  <div v-if="rows && rows.length" class="flex items-center">
31
- <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
32
- class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
16
+ <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
17
+ class="w-4 h-4 cursor-pointer text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
33
18
  <label for="checkbox-all-search" class="sr-only">checkbox</label>
34
19
  </div>
35
20
  </th>
36
21
 
37
22
  <th v-for="c in columnsListed" scope="col" class="px-6 py-3">
23
+
38
24
  <div @click="() => c.sortable && onSortButtonClick(c.name)" class="flex items-center " :class="{'cursor-pointer':c.sortable}">
39
25
  {{ c.label }}
26
+
40
27
 
41
28
  <div v-if="c.sortable"
42
29
  :style="{ 'color':ascArr.includes(c.name)?'green':descArr.includes(c.name)?'red':'currentColor'}">
@@ -52,6 +39,12 @@
52
39
  d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z"/>
53
40
  </svg>
54
41
  </div>
42
+ <span
43
+ class="bg-red-100 text-red-800 text-xs font-medium me-1 px-1 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
44
+ v-if="sort.findIndex((s) => s.field === c.name) !== -1 && sort?.length > 1">
45
+ {{ sort.findIndex((s) => s.field === c.name) + 1 }}
46
+ </span>
47
+
55
48
  </div>
56
49
  </th>
57
50
 
@@ -61,51 +54,8 @@
61
54
  </tr>
62
55
  </thead>
63
56
  <tbody>
64
- <tr v-if="!rows" class="bg-list-view-table-bg border-b dark:bg-gray-800 dark:border-gray-700">
65
- <td :colspan="resource?.columns.length + 2">
66
-
67
- <div role="status"
68
- class="max-w p-4 space-y-4 divide-y divide-gray-200 rounded animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700">
69
- <div class="flex items-center justify-between">
70
- <div>
71
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
72
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
73
- </div>
74
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
75
- </div>
76
- <div class="flex items-center justify-between pt-4">
77
- <div>
78
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
79
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
80
- </div>
81
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
82
- </div>
83
- <div class="flex items-center justify-between pt-4">
84
- <div>
85
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
86
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
87
- </div>
88
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
89
- </div>
90
- <div class="flex items-center justify-between pt-4">
91
- <div>
92
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
93
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
94
- </div>
95
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
96
- </div>
97
- <div class="flex items-center justify-between pt-4">
98
- <div>
99
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
100
- <div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
101
- </div>
102
- <div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
103
- </div>
104
- <span class="sr-only">Loading...</span>
105
- </div>
106
- </td>
107
- </tr>
108
- <tr v-else-if="rows.length === 0" class="bg-list-view-table-bg border-b dark:bg-gray-800 dark:border-gray-700">
57
+ <SkeleteLoader v-if="!rows" :columns="3" :rows="resource?.columns.length + 2"/>
58
+ <tr v-else-if="rows.length === 0" class="bg-lightListTable dark:bg-darkListTable dark:border-darkListTableBorder">
109
59
  <td :colspan="resource?.columns.length + 2">
110
60
 
111
61
  <div id="toast-simple"
@@ -118,16 +68,19 @@
118
68
  </td>
119
69
  </tr>
120
70
 
121
- <tr v-else v-for="(row, rowI) in rows" :key="row.id"
122
- class="bg-list-view-table-bg border-b dark:bg-gray-800 border-list-view-border-color dark:border-gray-700 hover:bg-list-view-table-row-hover dark:hover:bg-gray-600">
123
- <td class="w-4 p-4">
124
- <div class="flex items center">
71
+ <tr @click="onClick($event,row)" v-else v-for="(row, rowI) in rows" :key="row.id"
72
+ class="bg-lightListTable dark:bg-darkListTable border-lightListBorder dark:border-gray-700 hover:bg-lightListTableRowHover dark:hover:bg-darkListTableRowHover cursor-pointer"
73
+ :class="{'border-b': rowI !== rows.length - 1}"
74
+ >
75
+ <td class="w-4 p-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
76
+ <div class="flex items center ">
125
77
  <input
78
+ @click="(e)=>{e.stopPropagation()}"
126
79
  id="checkbox-table-search-1"
127
80
  type="checkbox"
128
81
  :checked="checkboxesInternal.includes(row.id)"
129
82
  @change="(e)=>{addToCheckedValues(row.id)}"
130
- class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
83
+ class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer">
131
84
  <label for="checkbox-table-search-1" class="sr-only">checkbox</label>
132
85
  </div>
133
86
  </td>
@@ -135,14 +88,14 @@
135
88
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
136
89
  <component
137
90
  :is="c?.components?.list ? getCustomComponent(c.components.list) : ValueRenderer"
138
- :meta="c?.components?.list.meta"
91
+ :meta="c?.components?.list?.meta"
139
92
  :column="c"
140
93
  :record="row"
141
94
  :adminUser="coreStore.adminUser"
142
95
  :resource="resource"
143
96
  />
144
97
  </td>
145
- <td class=" items-center px-6 py-4">
98
+ <td class=" items-center px-6 py-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
146
99
  <div class="flex">
147
100
  <RouterLink
148
101
  v-if="resource.options?.allowedActions.show"
@@ -277,7 +230,8 @@ import { callAdminForthApi, getIcon } from '@/utils';
277
230
  import ValueRenderer from '@/components/ValueRenderer.vue';
278
231
  import { getCustomComponent } from '@/utils';
279
232
  import { useCoreStore } from '@/stores/core';
280
- import { showSuccesTost } from '@/composables/useFrontendApi';
233
+ import { showSuccesTost, showErrorTost } from '@/composables/useFrontendApi';
234
+ import SkeleteLoader from '@/components/SkeleteLoader.vue';
281
235
 
282
236
  import {
283
237
  IconInboxOutline,
@@ -291,6 +245,7 @@ IconFilterOutline,
291
245
  IconPenSolid,
292
246
  IconTrashBinSolid
293
247
  } from '@iconify-prerendered/vue-flowbite';
248
+ import router from '@/router';
294
249
 
295
250
  const coreStore = useCoreStore();
296
251
 
@@ -299,7 +254,8 @@ const props = defineProps([
299
254
  'rows',
300
255
  'totalRows',
301
256
  'pageSize',
302
- 'checkboxes'
257
+ 'checkboxes',
258
+ 'sort'
303
259
  ])
304
260
 
305
261
  // emits, update page
@@ -335,6 +291,10 @@ watch(() => props.checkboxes, (newCheckboxes) => {
335
291
  checkboxesInternal.value = newCheckboxes;
336
292
  });
337
293
 
294
+ watch(() => props.sort, (newSort) => {
295
+ sort.value = newSort;
296
+ });
297
+
338
298
  function addToCheckedValues(id) {
339
299
  if (checkboxesInternal.value.includes(id)) {
340
300
  checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== id);
@@ -347,25 +307,25 @@ function addToCheckedValues(id) {
347
307
  const columnsListed = computed(() => props.resource?.columns?.filter(c => c.showIn.includes('list')));
348
308
 
349
309
  async function selectAll(value) {
350
- props.rows.forEach((r) => {
351
- if (!checkboxesInternal.value.includes(r.id)) {
352
- checkboxesInternal.value.push(r.id)
353
- } else {
354
- checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r.id)
355
- }
356
- });
357
- checkboxesInternal.value = [ ...checkboxesInternal.value ]
358
- // checkboxes.value = rows.value.map((v) => v.id);
310
+ if (!allFromThisPageChecked.value) {
311
+ props.rows.forEach((r) => {
312
+ if (!checkboxesInternal.value.includes(r.id)) {
313
+ checkboxesInternal.value.push(r.id)
314
+ }
315
+ });
316
+ } else {
317
+ props.rows.forEach((r) => {
318
+ checkboxesInternal.value = checkboxesInternal.value.filter((item) => item !== r.id);
319
+ });
320
+ }
321
+ checkboxesInternal.value = [ ...checkboxesInternal.value ];
359
322
  }
360
323
 
361
324
  const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
362
325
 
363
-
364
-
365
-
366
326
  const allFromThisPageChecked = computed(() => {
367
327
  if (!props.rows) return false;
368
- return props.rows.every((r) => props.checkboxes.includes(r.id));
328
+ return props.rows.every((r) => checkboxesInternal.value.includes(r.id));
369
329
  });
370
330
  const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
371
331
  const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
@@ -374,7 +334,7 @@ const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').
374
334
  function onSortButtonClick(field) {
375
335
  const sortIndex = sort.value.findIndex((s) => s.field === field);
376
336
  if (sortIndex === -1) {
377
- sort.value = [{field, direction: 'asc'}, ...sort.value];
337
+ sort.value = [...sort.value,{field, direction: 'asc'}];
378
338
  } else {
379
339
  const sortField = sort.value[sortIndex];
380
340
  if (sortField.direction === 'asc') {
@@ -385,6 +345,22 @@ function onSortButtonClick(field) {
385
345
  }
386
346
  }
387
347
 
348
+
349
+ const clickTarget = ref(null);
350
+ async function onClick(e,row) {
351
+ if(clickTarget.value === e.target) return;
352
+ clickTarget.value = e.target;
353
+ await new Promise((resolve) => setTimeout(resolve, 300));
354
+ if (window.getSelection().toString()) return;
355
+ else {router.push({
356
+ name: 'resource-show',
357
+ params: {
358
+ resourceId: props.resource.resourceId,
359
+ primaryKey: row._primaryKeyValue,
360
+ },
361
+ })}
362
+ }
363
+
388
364
  async function deleteRecord(row) {
389
365
  const data = await window.adminforth.confirm({
390
366
  message: 'Are you sure you want to delete this item?',
@@ -394,26 +370,25 @@ async function deleteRecord(row) {
394
370
  if (data) {
395
371
  try {
396
372
  const res = await callAdminForthApi({
397
- path: '/delete_record',
398
- method: 'POST',
399
- body: {
400
- resourceId: props.resource.resourceId,
401
- primaryKey: row._primaryKeyValue,
402
- }});
373
+ path: '/delete_record',
374
+ method: 'POST',
375
+ body: {
376
+ resourceId: props.resource.resourceId,
377
+ primaryKey: row._primaryKeyValue,
378
+ }
379
+ });
380
+ console.log('safsa', res)
403
381
  if (!res.error){
404
382
  emits('update:records', true)
405
383
  showSuccesTost('Record deleted successfully')
406
384
  } else {
407
- console.error(res.error)
385
+ showErrorTost(res.error)
408
386
  }
409
387
 
410
388
  } catch (e) {
389
+ showErrorTost(`Something went wrong, please try again later`);
411
390
  console.error(e);
412
- };
413
- }
414
-
415
-
391
+ };
392
+ }
416
393
  }
417
-
418
-
419
394
  </script>
@@ -0,0 +1,23 @@
1
+ <template>
2
+ <tr v-for="c in new Array(props.columns)" class="bg-lightListTable border-b dark:bg-darkListTable dark:border-darkListBorder">
3
+ <td v-for="r in new Array(props.rows)" class="items-center px-6 py-8 cursor-default" >
4
+ <div role="status" class="max-w-sm animate-pulse">
5
+ <div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
6
+ </div>
7
+ </td>
8
+ </tr>
9
+ </template>
10
+
11
+ <script setup>
12
+
13
+ const props = defineProps({
14
+ columns:Number,
15
+ rows:Number
16
+ });
17
+
18
+
19
+ </script>
20
+
21
+ <style lang="scss" scoped>
22
+
23
+ </style>
@@ -1,7 +1,13 @@
1
1
  <template>
2
2
 
3
3
 
4
- <div id="toast-default" class="flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert">
4
+ <div id="toast-default" class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800" role="alert"
5
+ :class="
6
+ {
7
+ 'danger': 'bg-red-100',
8
+ }[toast.variant]
9
+ "
10
+ >
5
11
  <div v-if="toast.variant == 'info'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-blue-500 bg-blue-100 rounded-lg dark:bg-blue-800 dark:text-blue-200">
6
12
  <svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
7
13
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.147 15.085a7.159 7.159 0 0 1-6.189 3.307A6.713 6.713 0 0 1 3.1 15.444c-2.679-4.513.287-8.737.888-9.548A4.373 4.373 0 0 0 5 1.608c1.287.953 6.445 3.218 5.537 10.5 1.5-1.122 2.706-3.01 2.853-6.14 1.433 1.049 3.993 5.395 1.757 9.117Z"/>
@@ -26,7 +32,9 @@
26
32
  </svg>
27
33
  <span class="sr-only">Check icon</span>
28
34
  </div>
29
- <div class="ms-3 text-sm font-normal">{{toast.message}}</div>
35
+
36
+ <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
37
+ <div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>{{toast.message}}</div>
30
38
  <button @click="closeToast" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" >
31
39
  <svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
32
40
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
@@ -43,10 +51,13 @@ import { useToastStore } from '@/stores/toast';
43
51
  const toastStore = useToastStore();
44
52
  const emit = defineEmits(['close']);
45
53
  const props = defineProps<{
46
- toast:{message: string;
47
- variant: string;
48
- id: string;
49
- timeout?: number|'unlimited';}
54
+ toast: {
55
+ message?: string;
56
+ messageHtml?: string;
57
+ variant: string;
58
+ id: string;
59
+ timeout?: number|'unlimited';
60
+ }
50
61
  }>();
51
62
  function closeToast() {
52
63
  emit('close');
@@ -55,7 +66,7 @@ function closeToast() {
55
66
  onMounted(() => {
56
67
  if (props.toast.timeout === 'unlimited') return;
57
68
  else {
58
- setTimeout(() => {emit('close');}, props.toast.timeout || 10000 );
69
+ setTimeout(() => {emit('close');}, (props.toast.timeout || 10) * 1e3 );
59
70
  }
60
71
  });
61
72
 
@@ -1,7 +1,6 @@
1
1
  <template>
2
2
  <div>
3
-
4
- <span v-if="column.foreignResource">
3
+ <span @click="(e)=>{e.stopPropagation()}" v-if="column.foreignResource">
5
4
  <RouterLink v-if="record[column.name]" class="font-medium text-blue-600 dark:text-blue-500 hover:brightness-110"
6
5
  :to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: record[column.name].pk } }">
7
6
  {{ record[column.name].label }}
@@ -18,9 +17,11 @@
18
17
  <span v-else-if="column.enum">
19
18
  {{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label,route.meta.type) }}
20
19
  </span>
21
- <span v-else-if="column.type === 'datetime'">
20
+ <span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
22
21
  {{ checkEmptyValues(formatDate(record[column.name]),route.meta.type) }}
23
-
22
+ </span>
23
+ <span v-else-if="column.type === 'richtext'">
24
+ <div v-html="protectAgainstXSS(record[column.name])"></div>
24
25
  </span>
25
26
  <span v-else>
26
27
  {{ checkEmptyValues(record[column.name],route.meta.type) }}
@@ -36,6 +37,7 @@ import utc from 'dayjs/plugin/utc';
36
37
  import timezone from 'dayjs/plugin/timezone';
37
38
  import {checkEmptyValues} from '@/utils';
38
39
  import { useRoute, useRouter } from 'vue-router';
40
+ import sanitizeHtml from 'sanitize-html';
39
41
 
40
42
 
41
43
  import { useCoreStore } from '@/stores/core';
@@ -52,6 +54,10 @@ const props = defineProps({
52
54
  record: Object
53
55
  });
54
56
 
57
+ function protectAgainstXSS(value) {
58
+ return sanitizeHtml(value);
59
+ }
60
+
55
61
 
56
62
  function formatDate(date) {
57
63
  if (!date) return '';
@@ -1,4 +1,5 @@
1
- import type { FrontendAPIInterface, ConfirmParams, AlertParams, FilterParams,Operator } from '../types/FrontendAPI';
1
+ import type { FrontendAPIInterface, ConfirmParams, AlertParams, } from '../types/FrontendAPI';
2
+ import type { AdminForthFilterOperators } from '@/types/AdminForthConfig';
2
3
  import { useToastStore } from '../stores/toast';
3
4
  import { useModalStore } from '../stores/modal';
4
5
  import { useCoreStore } from '@/stores/core';
@@ -6,9 +7,20 @@ import { useFiltersStore } from '@/stores/filters';
6
7
  import router from '@/router'
7
8
  import type { AdminForthResourceColumn } from '@/types/AdminForthConfig';
8
9
 
9
-
10
-
11
-
10
+ type FilterParams = {
11
+ /**
12
+ * Field of resource to filter
13
+ */
14
+ field: string;
15
+ /**
16
+ * Operator of filter
17
+ */
18
+ operator: AdminForthFilterOperators;
19
+ /**
20
+ * Value of filter
21
+ */
22
+ value: string | number | boolean ;
23
+ }
12
24
 
13
25
  declare global {
14
26
  interface Window {
@@ -23,7 +35,6 @@ declare global {
23
35
  }
24
36
 
25
37
  export class FrontendAPI implements FrontendAPIInterface {
26
- private validOperators: Operator[] = ['lte', 'gte', 'in','ilike'];
27
38
  private toastStore:any
28
39
  private modalStore:any
29
40
  private filtersStore:any
@@ -56,6 +67,7 @@ export class FrontendAPI implements FrontendAPIInterface {
56
67
  alert(params: AlertParams): void {
57
68
  this.toastStore.addToast({
58
69
  message: params.message,
70
+ messageHtml: params.messageHtml,
59
71
  variant: params.variant,
60
72
  timeout: params.timeout
61
73
  })
@@ -70,12 +82,8 @@ export class FrontendAPI implements FrontendAPIInterface {
70
82
  const filterField = this.coreStore.resourceColumnsWithFilters.find((col: AdminForthResourceColumn) => col.name === filter.field)
71
83
  if(!filterField){
72
84
  throw new Error(`Field ${filter.field} is not available for filtering`)
73
- }
74
- if(filterField) {
75
- if(!this.validOperators.includes(filter.operator)){
76
- throw new Error(`Operator ${filter.operator} is not valid`)
77
- }
78
85
  }
86
+
79
87
  }
80
88
  return true
81
89
  }
@@ -103,7 +111,13 @@ export class FrontendAPI implements FrontendAPIInterface {
103
111
  if(index === -1) {
104
112
  this.filtersStore.setFilter(filter)
105
113
  } else {
106
- this.filtersStore.setFilters([...this.filtersStore.filters.slice(0, index), filter, ...this.filtersStore.filters.slice(index + 1)])
114
+ const filters = [...this.filtersStore.filters];
115
+ if (filter.value === undefined) {
116
+ filters.splice(index, 1);
117
+ } else {
118
+ filters[index] = filter;
119
+ }
120
+ this.filtersStore.setFilters(filters);
107
121
  }
108
122
  }
109
123
  }
@@ -23,4 +23,5 @@
23
23
  // ::-webkit-scrollbar-thumb:hover {
24
24
  // @apply bg-gray-700
25
25
  // }
26
- // }
26
+ // }
27
+
@@ -6,7 +6,7 @@ import { createPinia } from 'pinia'
6
6
  import App from './App.vue'
7
7
  import router from './router'
8
8
 
9
- const app = createApp(App)
9
+ export const app = createApp(App)
10
10
  /* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */
11
11
 
12
12
 
@@ -12,7 +12,6 @@ const router = createRouter({
12
12
  component: () => import('@/views/LoginView.vue'),
13
13
  meta: { title: 'login' },
14
14
  beforeEnter: async (to, from, next) => {
15
- const userStore = useUserStore()
16
15
  if(localStorage.getItem('isAuthorized') === 'true'){
17
16
  next({name: 'home'})
18
17
  } else {
@@ -1,6 +1,12 @@
1
- import {ref} from 'vue';
2
- import {defineStore} from 'pinia';
1
+ import { ref } from 'vue';
2
+ import { defineStore } from 'pinia';
3
3
  import { callAdminForthApi } from '@/utils';
4
+ import { initFlowbite } from 'flowbite'
5
+ import { useRouter } from 'vue-router';
6
+ import { useCoreStore } from './core';
7
+ import router from '@/router';
8
+
9
+
4
10
 
5
11
  export const useUserStore = defineStore('user', () => {
6
12
  const isAuthorized = ref(false);
@@ -15,6 +21,17 @@ export const useUserStore = defineStore('user', () => {
15
21
  localStorage.setItem('isAuthorized', 'false');
16
22
  }
17
23
 
24
+ async function finishLogin() {
25
+ const coreStore = useCoreStore();
26
+ authorize(); // TODO not sure we need this approach with localStorage
27
+ await router.push('/');
28
+ await router.isReady();
29
+ await coreStore.fetchMenuAndResource();
30
+ setTimeout(() => {
31
+ initFlowbite();
32
+ });
33
+ }
34
+
18
35
  async function logout() {
19
36
  await callAdminForthApi({
20
37
  path: '/logout',
@@ -48,7 +65,8 @@ export const useUserStore = defineStore('user', () => {
48
65
  isAuthorized,
49
66
  authorize,
50
67
  unauthorize,
51
- logout
68
+ logout,
69
+ finishLogin
52
70
  }
53
71
 
54
72
  });