adminforth 1.3.56-next.2 → 1.3.56-next.21

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 (36) hide show
  1. package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
  2. package/dist/dataConnectors/clickhouse.js.map +1 -1
  3. package/dist/modules/configValidator.d.ts.map +1 -1
  4. package/dist/modules/configValidator.js +12 -0
  5. package/dist/modules/configValidator.js.map +1 -1
  6. package/dist/modules/restApi.d.ts.map +1 -1
  7. package/dist/modules/restApi.js +32 -6
  8. package/dist/modules/restApi.js.map +1 -1
  9. package/dist/modules/styles.js +2 -2
  10. package/dist/modules/styles.js.map +1 -1
  11. package/dist/spa/index.html +2 -2
  12. package/dist/spa/src/App.vue +12 -22
  13. package/dist/spa/src/components/AfTooltip.vue +43 -0
  14. package/dist/spa/src/components/BreadcrumbsWithButtons.vue +3 -4
  15. package/dist/spa/src/components/Dropdown.vue +2 -2
  16. package/dist/spa/src/components/GroupsTable.vue +168 -0
  17. package/dist/spa/src/components/ResourceForm.vue +70 -156
  18. package/dist/spa/src/components/ResourceListTable.vue +121 -124
  19. package/dist/spa/src/components/ValueRenderer.vue +1 -4
  20. package/dist/spa/src/composables/useStores.ts +1 -1
  21. package/dist/spa/src/renderers/CompactField.vue +48 -0
  22. package/dist/spa/src/renderers/CompactUUID.vue +1 -1
  23. package/dist/spa/src/renderers/CountryFlag.vue +16 -21
  24. package/dist/spa/src/renderers/HumanNumber.vue +62 -0
  25. package/dist/spa/src/stores/core.ts +18 -1
  26. package/dist/spa/src/stores/user.ts +0 -1
  27. package/dist/spa/src/types/AdminForthConfig.ts +41 -14
  28. package/dist/spa/src/views/CreateView.vue +1 -1
  29. package/dist/spa/src/views/EditView.vue +1 -1
  30. package/dist/spa/src/views/ListView.vue +1 -6
  31. package/dist/spa/src/views/ResourceParent.vue +36 -2
  32. package/dist/spa/src/views/ShowView.vue +4 -3
  33. package/dist/types/AdminForthConfig.d.ts +34 -11
  34. package/dist/types/AdminForthConfig.d.ts.map +1 -1
  35. package/dist/types/AdminForthConfig.js.map +1 -1
  36. package/package.json +3 -1
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <!-- table -->
3
- <div class="relative overflow-x-auto shadow-listTableShadow dark:shadow-darkListTableShadow overflow-y-hidden"
3
+ <div class="relative shadow-listTableShadow dark:shadow-darkListTableShadow overflow-auto "
4
4
  :class="{'rounded-default': !noRoundings}"
5
5
  >
6
6
  <!-- skelet loader -->
@@ -11,53 +11,55 @@
11
11
  <div class="h-2 bg-gray-200 rounded-full dark:bg-gray-700 max-w-[360px]"></div>
12
12
  </div>
13
13
  </div>
14
+ <table v-else class=" w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-default">
14
15
 
15
- <table v-else class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400 rounded-default">
16
- <thead class="text-xs text-view-table-heading-text bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
17
- <tr>
18
- <th scope="col" class="p-4">
19
- <div v-if="rows && rows.length" class="flex items-center">
20
- <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
21
- 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">
22
- <label for="checkbox-all-search" class="sr-only">checkbox</label>
23
- </div>
24
- </th>
25
-
26
- <th v-for="c in columnsListed" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
27
-
28
- <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
29
- class="flex items-center " :class="{'cursor-pointer':c.sortable}">
30
- {{ c.label }}
31
-
32
- <div v-if="c.sortable"
33
- :style="{ 'color':ascArr.includes(c.name)?'green':descArr.includes(c.name)?'red':'currentColor'}">
34
- <svg v-if="ascArr.includes(c.name) || descArr.includes(c.name)" class="w-3 h-3 ms-1.5"
35
- :class="{'rotate-180':descArr.includes(c.name)}" viewBox="0 0 24 24">
36
- <path
37
- 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"/>
38
- </svg>
39
- <svg v-else class="w-3 h-3 ms-1.5 opacity-30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
40
- fill='currentColor'
41
- viewBox="0 0 24 24">
42
- <path
43
- 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"/>
44
- </svg>
45
- </div>
46
- <span
47
- 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"
48
- v-if="sort.findIndex((s) => s.field === c.name) !== -1 && sort?.length > 1">
49
- {{ sort.findIndex((s) => s.field === c.name) + 1 }}
50
- </span>
51
-
52
- </div>
53
- </th>
54
-
55
- <th scope="col" class="px-6 py-3">
56
- Actions
57
- </th>
58
- </tr>
59
- </thead>
60
16
  <tbody>
17
+
18
+ <!-- table header -->
19
+ <tr class="t-header sticky z-1 top-0 text-xs bg-lightListTableHeading dark:bg-darkListTableHeading dark:text-gray-400">
20
+ <td scope="col" class="p-4">
21
+ <div v-if="rows && rows.length" class="flex items-center">
22
+ <input id="checkbox-all-search" type="checkbox" :checked="allFromThisPageChecked" @change="selectAll()"
23
+ class="w-4 h-4 cursor-pointer text-blue-600 bg-gray-100 border-gray-300 rounded
24
+ 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">
25
+ <label for="checkbox-all-search" class="sr-only">checkbox</label>
26
+ </div>
27
+ </td>
28
+
29
+ <td v-for="c in columnsListed" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
30
+
31
+ <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
32
+ class="flex items-center " :class="{'cursor-pointer':c.sortable}">
33
+ {{ c.label }}
34
+
35
+ <div v-if="c.sortable">
36
+ <svg v-if="ascArr.includes(c.name) || descArr.includes(c.name)" class="w-3 h-3 ms-1.5"
37
+ fill='currentColor'
38
+ :class="{'rotate-180':descArr.includes(c.name)}" viewBox="0 0 24 24">
39
+ <path
40
+ 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"/>
41
+ </svg>
42
+ <svg v-else class="w-3 h-3 ms-1.5 opacity-30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
43
+ fill='currentColor'
44
+ viewBox="0 0 24 24">
45
+ <path
46
+ 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"/>
47
+ </svg>
48
+ </div>
49
+ <span
50
+ 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"
51
+ v-if="sort.findIndex((s) => s.field === c.name) !== -1 && sort?.length > 1">
52
+ {{ sort.findIndex((s) => s.field === c.name) + 1 }}
53
+ </span>
54
+
55
+ </div>
56
+ </td>
57
+
58
+ <td scope="col" class="px-6 py-3">
59
+ Actions
60
+ </td>
61
+ </tr>
62
+ <!-- table header end -->
61
63
  <SkeleteLoader
62
64
  v-if="!rows"
63
65
  :columns="resource?.columns.filter(c => c.showIn.includes('list')).length + 2"
@@ -106,65 +108,57 @@
106
108
  />
107
109
  </td>
108
110
  <td class=" items-center px-2 md:px-3 lg:px-6 py-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
109
- <div class="flex">
110
- <RouterLink
111
- v-if="resource.options?.allowedActions.show"
112
- :to="{
113
- name: 'resource-show',
114
- params: {
115
- resourceId: resource.resourceId,
116
- primaryKey: row._primaryKeyValue,
117
- }
118
- }"
119
- class="font-medium text-lightPrimary dark:text-darkPrimary hover:underline"
120
- :data-tooltip-target="`tooltip-show-${rowI}`"
121
- >
122
- <IconEyeSolid class="w-5 h-5 me-2"/>
123
- </RouterLink>
124
-
125
- <div :id="`tooltip-show-${rowI}`"
126
- role="tooltip"
127
- class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
128
- Show item
129
- <div class="tooltip-arrow" data-popper-arrow></div>
130
- </div>
131
-
132
- <RouterLink v-if="resource.options?.allowedActions.edit"
133
- :to="{
134
- name: 'resource-edit',
135
- params: {
136
- resourceId: resource.resourceId,
137
- primaryKey: row._primaryKeyValue
138
- }
139
- }"
140
- class="font-medium text-lightPrimary dark:text-darkPrimary hover:underline ms-3"
141
- :data-tooltip-target="`tooltip-edit-${rowI}`"
142
- >
143
- <IconPenSolid class="w-5 h-5 me-2"/>
144
- </RouterLink>
145
-
146
- <div :id="`tooltip-edit-${rowI}`"
147
- role="tooltip"
148
- class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
149
- Edit
150
- <div class="tooltip-arrow" data-popper-arrow></div>
151
- </div>
152
-
153
- <button v-if="resource.options?.allowedActions.delete"
154
- class="font-medium text-lightPrimary dark:text-darkPrimary hover:underline ms-3"
155
- :data-tooltip-target="`tooltip-delete-${rowI}`"
156
- @click="deleteRecord(row)"
157
- >
158
- <IconTrashBinSolid class="w-5 h-5 me-2"/>
159
- </button>
160
-
161
- <div :id="`tooltip-delete-${rowI}`"
162
- role="tooltip"
163
- class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
164
- Delete
165
- <div class="tooltip-arrow" data-popper-arrow></div>
166
- </div>
167
-
111
+ <div class="flex text-lightPrimary dark:text-darkPrimary items-center">
112
+ <AfTooltip>
113
+ <RouterLink
114
+ v-if="resource.options?.allowedActions.show"
115
+ :to="{
116
+ name: 'resource-show',
117
+ params: {
118
+ resourceId: resource.resourceId,
119
+ primaryKey: row._primaryKeyValue,
120
+ }
121
+ }"
122
+
123
+ >
124
+ <IconEyeSolid class="w-5 h-5 me-2"/>
125
+ </RouterLink>
126
+
127
+ <template v-slot:tooltip>
128
+ Show item
129
+ </template>
130
+ </AfTooltip>
131
+
132
+ <AfTooltip>
133
+ <RouterLink
134
+ v-if="resource.options?.allowedActions.edit"
135
+ :to="{
136
+ name: 'resource-edit',
137
+ params: {
138
+ resourceId: resource.resourceId,
139
+ primaryKey: row._primaryKeyValue,
140
+ }
141
+ }"
142
+ >
143
+ <IconPenSolid class="w-5 h-5 me-2"/>
144
+ </RouterLink>
145
+ <template v-slot:tooltip>
146
+ Edit item
147
+ </template>
148
+ </AfTooltip>
149
+
150
+ <AfTooltip>
151
+ <button
152
+ v-if="resource.options?.allowedActions.delete"
153
+ @click="deleteRecord(row)"
154
+ >
155
+ <IconTrashBinSolid class="w-5 h-5 me-2"/>
156
+ </button>
157
+
158
+ <template v-slot:tooltip>
159
+ Delete item
160
+ </template>
161
+ </AfTooltip>
168
162
 
169
163
  <template v-if="coreStore.resourceOptions?.pageInjections?.list?.customActionIcons">
170
164
  <component
@@ -185,30 +179,23 @@
185
179
  <!-- pagination
186
180
  totalRows in v-if is used to not hide page input during loading when user puts cursor into it and edit directly (rows gets null there during edit)
187
181
  -->
188
- <div class="flex flex-col items-center mt-4 mb-4 xs:flex-row xs:justify-between xs:items-center"
182
+ <div class="flex flex-row items-center mt-4 xs:flex-row xs:justify-between xs:items-center gap-3"
189
183
  v-if="(rows || totalRows) && totalRows >= pageSize && totalRows > 0"
190
184
  >
191
- <!-- Help text -->
192
- <span class="text-sm text-gray-700 dark:text-gray-400">
193
- Showing <span class="font-semibold text-gray-900 dark:text-white">
194
- {{ ((page || 1) - 1) * pageSize + 1 }}
195
- </span> to <span class="font-semibold text-gray-900 dark:text-white">
196
- {{ Math.min((page || 1) * pageSize, totalRows) }}
197
- </span> of <span class="font-semibold text-gray-900 dark:text-white">{{
198
- totalRows
199
- }}</span> Entries
200
- </span>
201
- <div class="inline-flex mt-2 xs:mt-0">
185
+
186
+ <div class="inline-flex ">
202
187
  <!-- Buttons -->
203
188
  <button
204
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 rounded-s border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
189
+ class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 rounded-s border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
205
190
  @click="page--" :disabled="page <= 1">
206
- <svg class="w-3.5 h-3.5 me-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
191
+ <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
207
192
  viewBox="0 0 14 10">
208
193
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
209
194
  d="M13 5H1m0 0 4 4M1 5l4-4"/>
210
195
  </svg>
211
- Prev
196
+ <span class="hidden sm:inline">
197
+ Prev
198
+ </span>
212
199
  </button>
213
200
  <button
214
201
  class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-r-0 border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
@@ -232,11 +219,10 @@
232
219
  <!-- <IconChevronDoubleRightOutline class="w-4 h-4" /> -->
233
220
  </button>
234
221
  <button
235
- class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 rounded-e border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
236
-
222
+ class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-gray-900 focus:outline-none bg-white border-l-0 rounded-e border border-gray-300 hover:bg-gray-100 hover:text-lightPrimary focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50"
237
223
  @click="page++" :disabled="page >= totalPages">
238
- Next
239
- <svg class="w-3.5 h-3.5 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
224
+ <span class="hidden sm:inline">Next</span>
225
+ <svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
240
226
  viewBox="0 0 14 10">
241
227
  <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
242
228
  d="M1 5h12m0 0L9 1m4 4L9 9"/>
@@ -244,6 +230,15 @@
244
230
  </button>
245
231
  </div>
246
232
 
233
+ <!-- Help text -->
234
+ <span class="text-sm text-gray-700 dark:text-gray-400">
235
+ Showing <span class="font-semibold text-gray-900 dark:text-white">
236
+ {{ ((page || 1) - 1) * pageSize + 1 }}
237
+ </span> to <span class="font-semibold text-gray-900 dark:text-white">
238
+ {{ Math.min((page || 1) * pageSize, totalRows) }}
239
+ </span> of <span class="font-semibold text-gray-900 dark:text-white">{{
240
+ totalRows }}</span> <span class="hidden sm:inline">Entries</span>
241
+ </span>
247
242
  </div>
248
243
  </template>
249
244
 
@@ -269,9 +264,11 @@ import {
269
264
  IconTrashBinSolid
270
265
  } from '@iconify-prerendered/vue-flowbite';
271
266
  import router from '@/router';
267
+ import AfTooltip from './AfTooltip.vue';
272
268
 
273
269
  const coreStore = useCoreStore();
274
270
 
271
+
275
272
  const props = defineProps([
276
273
  'page',
277
274
  'resource',
@@ -30,7 +30,7 @@
30
30
  <div v-html="protectAgainstXSS(record[column.name])" class="allow-lists"></div>
31
31
  </span>
32
32
  <span v-else-if="column.type === 'json'">
33
- <JsonViewer :value="record[column.name]" copyable sort :theme="theme" />
33
+ <JsonViewer :value="record[column.name]" copyable sort :theme="coreStore.theme" />
34
34
  </span>
35
35
  <span v-else>
36
36
  {{ checkEmptyValues(record[column.name],route.meta.type) }}
@@ -57,9 +57,6 @@ import { computed } from 'vue';
57
57
  const coreStore = useCoreStore();
58
58
  const route = useRoute();
59
59
 
60
- const theme = computed(() => {
61
- return window.localStorage.getItem('af__theme') || 'light';
62
- });
63
60
 
64
61
  dayjs.extend(utc);
65
62
  dayjs.extend(timezone);
@@ -7,7 +7,7 @@ import { useFiltersStore } from '@/stores/filters';
7
7
  import router from '@/router'
8
8
  import type { AdminForthResourceColumn } from '@/types/AdminForthConfig';
9
9
 
10
- type FilterParams = {
10
+ type FilterParams = {
11
11
  /**
12
12
  * Field of resource to filter
13
13
  */
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <span class="flex items-center"
3
+ :data-tooltip-target="visualValue ? `tooltip-${id}` : undefined"
4
+ data-tooltip-placement="top"
5
+ >
6
+ {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
7
+
8
+ <div :id="`tooltip-${id}`" role="tooltip" v-if="visualValue"
9
+ class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
10
+ {{ props.record[props.column.name] }}
11
+ <div class="tooltip-arrow" data-popper-arrow></div>
12
+ </div>
13
+ </span>
14
+ </template>
15
+
16
+ <script setup>
17
+ import { computed, ref, onMounted } from 'vue';
18
+ import { IconFileCopyAltSolid } from '@iconify-prerendered/vue-flowbite';
19
+ import { initFlowbite } from 'flowbite';
20
+
21
+ const visualValue = computed(() => {
22
+ // if lenght is more then 8, show only first 4 and last 4 characters, ... in the middle
23
+ const val = props.record[props.column.name];
24
+ if (val && val.length > 8) {
25
+ return `${val.substr(0, 4)}...${val.substr(val.length - 4)}`;
26
+ }
27
+ return val;
28
+ });
29
+
30
+ const props = defineProps(['column', 'record', 'meta']);
31
+
32
+ const id = ref();
33
+
34
+ function copyToCB() {
35
+ navigator.clipboard.writeText(props.record[props.column.name]);
36
+ window.adminforth.alert({
37
+ message: 'ID copied to clipboard',
38
+ variant: 'success',
39
+ })
40
+ }
41
+
42
+ onMounted(async () => {
43
+ id.value = Math.random().toString(36).substring(7);
44
+ await new Promise(resolve => setTimeout(resolve, 0));
45
+ initFlowbite();
46
+ });
47
+
48
+ </script>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <span class="flex items-center"
3
- :data-tooltip-target="val && `tooltip-${id}`"
3
+ :data-tooltip-target="visualValue ? `tooltip-${id}` : undefined"
4
4
  data-tooltip-placement="top"
5
5
  >
6
6
  {{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
@@ -1,31 +1,25 @@
1
1
  <template>
2
- <span class="flex items-center">
3
- <span
4
- :class="{[`fi-${countryIsoLow}`]: true, 'flag-icon': countryName}"
5
- :data-tooltip-target="`tooltip-${id}`"
6
- ></span>
7
-
8
- <span v-if="meta.showCountryName" class="ms-2">{{ countryName }}</span>
9
-
10
- <div
11
- v-if="!meta.showCountryName && countryName"
12
- :id="`tooltip-${id}`" role="tooltip"
13
- class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700"
14
- >
15
- {{ countryName }}
16
- <div class="tooltip-arrow" data-popper-arrow></div>
17
- </div>
18
- </span>
19
-
2
+ <AfTooltip>
3
+ <span class="flex items-center">
4
+ <span
5
+ :class="{[`fi-${countryIsoLow}`]: true, 'flag-icon': countryName}"
6
+ ></span>
7
+ <span v-if="meta.showCountryName" class="ms-2">{{ countryName }}</span>
8
+ </span>
9
+
10
+ <template v-if="shouldShowTooltip" #tooltip>
11
+ {{ countryName }}
12
+ </template>
13
+ </AfTooltip>
20
14
  </template>
21
15
 
22
16
  <script setup>
23
17
 
24
18
  import { computed, ref, onMounted } from 'vue';
25
- import { initFlowbite } from 'flowbite';
26
19
  import 'flag-icons/css/flag-icons.min.css';
27
20
  import isoCountries from 'i18n-iso-countries';
28
21
  import enLocal from 'i18n-iso-countries/langs/en.json';
22
+ import AfTooltip from '@/components/AfTooltip.vue';
29
23
 
30
24
  isoCountries.registerLocale(enLocal);
31
25
 
@@ -33,11 +27,12 @@ const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser'])
33
27
 
34
28
  const id = ref();
35
29
 
30
+ const shouldShowTooltip = computed(() => {
31
+ return !props.meta.showCountryName && countryName.value;
32
+ });
36
33
 
37
34
  onMounted(async () => {
38
35
  id.value = Math.random().toString(36).substring(7);
39
- await new Promise(resolve => setTimeout(resolve, 0));
40
- initFlowbite();
41
36
  });
42
37
 
43
38
  const countryIsoLow = computed(() => {
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <span class="flex items-center"
3
+ :data-tooltip-target="formattedValue && `tooltip-${id}`"
4
+ data-tooltip-placement="left">
5
+ {{ formattedValue }}
6
+ <div :id="`tooltip-${id}`" role="tooltip" v-if="formattedValue"
7
+ class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
8
+ {{ formattedTooltipValue }}
9
+ <div class="tooltip-arrow" data-popper-arrow></div>
10
+ </div>
11
+ </span>
12
+ </template>
13
+
14
+ <script setup>
15
+ import { computed, ref, onMounted } from 'vue';
16
+ import { initFlowbite } from 'flowbite';
17
+
18
+ const props = defineProps(['column', 'record']);
19
+ const id = ref();
20
+ const userLocale = ref(navigator.language || 'en-US');
21
+
22
+ const formattedValue = computed(() => {
23
+ const val = props.record[props.column.name];
24
+ if (typeof val === 'number') {
25
+ return formatNumber(val, userLocale.value);
26
+ }
27
+ return val;
28
+ });
29
+
30
+ const formattedTooltipValue = computed(() => {
31
+ const val = props.record[props.column.name];
32
+ if (typeof val === 'number') {
33
+ return formatNumberUsingIntl(val, userLocale.value);
34
+ }
35
+ return val;
36
+ });
37
+
38
+ function formatNumber(num, locale) {
39
+ if (typeof num !== 'number') {
40
+ return num.toString();
41
+ }
42
+
43
+ if (num >= 1_000_000) {
44
+ return `${(num / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`;
45
+ } else if (num >= 1_000) {
46
+ return `${(num / 1_000).toFixed(1).replace(/\.0$/, '')}k`;
47
+ }
48
+
49
+ // If it's less than 1000, format using locale
50
+ return new Intl.NumberFormat(locale).format(num);
51
+ }
52
+
53
+ function formatNumberUsingIntl(num, locale) {
54
+ return new Intl.NumberFormat(locale).format(num);
55
+ }
56
+
57
+ onMounted(async () => {
58
+ id.value = Math.random().toString(36).substring(7);
59
+ await new Promise(resolve => setTimeout(resolve, 0));
60
+ initFlowbite();
61
+ });
62
+ </script>
@@ -6,6 +6,8 @@ import type { Ref } from 'vue'
6
6
 
7
7
  export const useCoreStore = defineStore('core', () => {
8
8
  const resourceById: Ref<Object> = ref({});
9
+ const theme: Ref<'light'| 'dark'> = ref(window.localStorage.getItem('af__theme') as ('light'|'dark') || 'light');
10
+
9
11
  const menu = ref([]);
10
12
  const config = ref({});
11
13
  const record: Ref<any | null> = ref({});
@@ -23,6 +25,19 @@ export const useCoreStore = defineStore('core', () => {
23
25
  const resourceColumnsId = ref(null);
24
26
  const adminUser = ref(null);
25
27
 
28
+ async function toggleTheme() {
29
+ theme.value = theme.value === 'light' ? 'dark' : 'light';
30
+ if (theme.value === 'light') {
31
+ document.documentElement.classList.remove('dark');
32
+ } else {
33
+ document.documentElement.classList.add('dark');
34
+ }
35
+
36
+ document.documentElement.setAttribute('data-theme', theme.value);
37
+ theme.value = theme.value;
38
+ window.localStorage.setItem('af__theme', theme.value);
39
+ }
40
+
26
41
  async function fetchMenuAndResource() {
27
42
  const resp = await callAdminForthApi({
28
43
  path: '/get_base_config',
@@ -143,6 +158,8 @@ export const useCoreStore = defineStore('core', () => {
143
158
  resourceOptions,
144
159
  resource,
145
160
  adminUser,
146
- resourceColumnsWithFilters
161
+ resourceColumnsWithFilters,
162
+ toggleTheme,
163
+ theme,
147
164
  }
148
165
  })
@@ -2,7 +2,6 @@ import { ref } from 'vue';
2
2
  import { defineStore } from 'pinia';
3
3
  import { callAdminForthApi } from '@/utils';
4
4
  import { initFlowbite } from 'flowbite'
5
- import { useRouter } from 'vue-router';
6
5
  import { useCoreStore } from './core';
7
6
  import router from '@/router';
8
7