adminforth 1.5.15-next.6 → 1.5.15
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +63 -1
- package/dist/index.js.map +1 -1
- package/dist/spa/.eslintrc.cjs +14 -0
- package/dist/spa/README.md +39 -0
- package/dist/spa/env.d.ts +1 -0
- package/dist/spa/index.html +23 -0
- package/dist/spa/package-lock.json +5062 -0
- package/dist/spa/package.json +58 -0
- package/dist/spa/postcss.config.js +6 -0
- package/dist/spa/public/assets/favicon.png +0 -0
- package/dist/spa/src/App.vue +432 -0
- package/dist/spa/src/adminforth.ts +160 -0
- package/dist/spa/src/afcl/AreaChart.vue +160 -0
- package/dist/spa/src/afcl/BarChart.vue +170 -0
- package/dist/spa/src/afcl/Button.vue +27 -0
- package/dist/spa/src/afcl/Checkbox.vue +24 -0
- package/dist/spa/src/afcl/Dropzone.vue +128 -0
- package/dist/spa/src/afcl/Input.vue +42 -0
- package/dist/spa/src/afcl/Link.vue +17 -0
- package/dist/spa/src/afcl/LinkButton.vue +25 -0
- package/dist/spa/src/afcl/PieChart.vue +175 -0
- package/dist/spa/src/afcl/ProgressBar.vue +57 -0
- package/dist/spa/src/afcl/Select.vue +246 -0
- package/dist/spa/src/afcl/Skeleton.vue +26 -0
- package/dist/spa/src/afcl/Spinner.vue +9 -0
- package/dist/spa/src/afcl/Table.vue +116 -0
- package/dist/spa/src/afcl/Tooltip.vue +43 -0
- package/dist/spa/src/afcl/VerticalTabs.vue +49 -0
- package/dist/spa/src/afcl/index.ts +20 -0
- package/dist/spa/src/assets/base.css +2 -0
- package/dist/spa/src/assets/logo.svg +19 -0
- package/dist/spa/src/components/AcceptModal.vue +44 -0
- package/dist/spa/src/components/Breadcrumbs.vue +41 -0
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +25 -0
- package/dist/spa/src/components/CustomDatePicker.vue +180 -0
- package/dist/spa/src/components/CustomDateRangePicker.vue +218 -0
- package/dist/spa/src/components/CustomRangePicker.vue +156 -0
- package/dist/spa/src/components/Filters.vue +235 -0
- package/dist/spa/src/components/GroupsTable.vue +218 -0
- package/dist/spa/src/components/HelloWorld.vue +17 -0
- package/dist/spa/src/components/MenuLink.vue +41 -0
- package/dist/spa/src/components/ResourceForm.vue +260 -0
- package/dist/spa/src/components/ResourceListTable.vue +486 -0
- package/dist/spa/src/components/ShowTable.vue +81 -0
- package/dist/spa/src/components/SingleSkeletLoader.vue +13 -0
- package/dist/spa/src/components/SkeleteLoader.vue +18 -0
- package/dist/spa/src/components/ThreeDotsMenu.vue +43 -0
- package/dist/spa/src/components/Toast.vue +78 -0
- package/dist/spa/src/components/ValueRenderer.vue +141 -0
- package/dist/spa/src/components/icons/IconCalendar.vue +5 -0
- package/dist/spa/src/components/icons/IconCommunity.vue +7 -0
- package/dist/spa/src/components/icons/IconDocumentation.vue +7 -0
- package/dist/spa/src/components/icons/IconEcosystem.vue +7 -0
- package/dist/spa/src/components/icons/IconSupport.vue +7 -0
- package/dist/spa/src/components/icons/IconTime.vue +5 -0
- package/dist/spa/src/components/icons/IconTooling.vue +19 -0
- package/dist/spa/src/composables/useFrontendApi.ts +28 -0
- package/dist/spa/src/i18n.ts +54 -0
- package/dist/spa/src/index.scss +34 -0
- package/dist/spa/src/main.ts +22 -0
- package/dist/spa/src/renderers/CompactField.vue +46 -0
- package/dist/spa/src/renderers/CompactUUID.vue +46 -0
- package/dist/spa/src/renderers/CountryFlag.vue +65 -0
- package/dist/spa/src/renderers/HumanNumber.vue +58 -0
- package/dist/spa/src/renderers/RelativeTime.vue +42 -0
- package/dist/spa/src/renderers/URL.vue +18 -0
- package/dist/spa/src/router/index.ts +70 -0
- package/dist/spa/src/spa_types/core.ts +51 -0
- package/dist/spa/src/stores/core.ts +228 -0
- package/dist/spa/src/stores/filters.ts +27 -0
- package/dist/spa/src/stores/modal.ts +48 -0
- package/dist/spa/src/stores/toast.ts +30 -0
- package/dist/spa/src/stores/user.ts +79 -0
- package/dist/spa/src/types/Adapters.ts +26 -0
- package/dist/spa/src/types/Back.ts +1344 -0
- package/dist/spa/src/types/Common.ts +940 -0
- package/dist/spa/src/types/FrontendAPI.ts +189 -0
- package/dist/spa/src/utils.ts +184 -0
- package/dist/spa/src/views/CreateView.vue +167 -0
- package/dist/spa/src/views/EditView.vue +171 -0
- package/dist/spa/src/views/ListView.vue +442 -0
- package/dist/spa/src/views/LoginView.vue +199 -0
- package/dist/spa/src/views/PageNotFound.vue +20 -0
- package/dist/spa/src/views/ResourceParent.vue +50 -0
- package/dist/spa/src/views/ShowView.vue +209 -0
- package/dist/spa/src/websocket.ts +129 -0
- package/dist/spa/tailwind.config.js +19 -0
- package/dist/spa/tsconfig.app.json +14 -0
- package/dist/spa/tsconfig.json +11 -0
- package/dist/spa/tsconfig.node.json +19 -0
- package/dist/spa/vite.config.ts +52 -0
- package/package.json +2 -5
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<div class="flex items-center w-full p-4 text-gray-500 rounded-lg shadow-lg dark:text-gray-400 dark:bg-gray-800 bg-white"
|
|
5
|
+
role="alert"
|
|
6
|
+
:class="
|
|
7
|
+
{
|
|
8
|
+
'danger': 'bg-red-100',
|
|
9
|
+
}[toast.variant]
|
|
10
|
+
"
|
|
11
|
+
>
|
|
12
|
+
<div v-if="toast.variant == 'info'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-lightPrimary dark:text-darkPrimary bg-lightPrimaryOpacity rounded-lg dark:bg-blue-800 dark:text-blue-200">
|
|
13
|
+
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
|
|
14
|
+
<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"/>
|
|
15
|
+
</svg>
|
|
16
|
+
<span class="sr-only">{{ $t('Fire icon') }}</span>
|
|
17
|
+
</div>
|
|
18
|
+
<div v-else-if="toast.variant == 'danger'" class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
|
|
19
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
20
|
+
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
|
|
21
|
+
</svg>
|
|
22
|
+
<span class="sr-only">{{ $t('Error icon') }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div v-else-if="toast.variant == 'warning'"class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
|
|
25
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
26
|
+
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
|
|
27
|
+
</svg>
|
|
28
|
+
<span class="sr-only">{{ $t('Warning icon') }}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div v-else class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
|
|
31
|
+
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
32
|
+
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
|
|
33
|
+
</svg>
|
|
34
|
+
<span class="sr-only">{{ $t('Check icon') }}</span>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-if="toast.messageHtml" v-html="toast.messageHtml"></div>
|
|
38
|
+
<div class="ms-3 text-sm font-normal max-w-xs pr-2" v-else>{{toast.message}}</div>
|
|
39
|
+
<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" >
|
|
40
|
+
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
41
|
+
<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"/>
|
|
42
|
+
</svg>
|
|
43
|
+
</button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup lang="ts">
|
|
50
|
+
import { onMounted } from 'vue';
|
|
51
|
+
import { useToastStore } from '@/stores/toast';
|
|
52
|
+
const toastStore = useToastStore();
|
|
53
|
+
const emit = defineEmits(['close']);
|
|
54
|
+
const props = defineProps<{
|
|
55
|
+
toast: {
|
|
56
|
+
message?: string;
|
|
57
|
+
messageHtml?: string;
|
|
58
|
+
variant: string;
|
|
59
|
+
id: string;
|
|
60
|
+
timeout?: number|'unlimited';
|
|
61
|
+
}
|
|
62
|
+
}>();
|
|
63
|
+
function closeToast() {
|
|
64
|
+
emit('close');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onMounted(() => {
|
|
68
|
+
if (props.toast.timeout === 'unlimited') return;
|
|
69
|
+
else {
|
|
70
|
+
setTimeout(() => {emit('close');}, (props.toast.timeout || 10) * 1e3 );
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<style lang="scss" scoped>
|
|
77
|
+
|
|
78
|
+
</style>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<span @click="(e)=>{e.stopPropagation()}" v-if="column.foreignResource">
|
|
4
|
+
<RouterLink v-if="record[column.name]" class="font-medium text-lightPrimary dark:text-darkPrimary hover:brightness-110 whitespace-nowrap"
|
|
5
|
+
:to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: record[column.name].pk } }">
|
|
6
|
+
{{ record[column.name].label }}
|
|
7
|
+
</RouterLink>
|
|
8
|
+
<div v-else>
|
|
9
|
+
<span class="text-gray-400">-</span>
|
|
10
|
+
</div>
|
|
11
|
+
</span>
|
|
12
|
+
|
|
13
|
+
<span v-else-if="column.type === 'boolean'">
|
|
14
|
+
<span v-if="record[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">{{ $t('Yes') }}</span>
|
|
15
|
+
<span v-else class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">{{ $t('No') }}</span>
|
|
16
|
+
</span>
|
|
17
|
+
<span v-else-if="column.enum">
|
|
18
|
+
{{ checkEmptyValues(column.enum.find(e => e.value === record[column.name])?.label || record[column.name], route.meta.type) }}
|
|
19
|
+
</span>
|
|
20
|
+
<span v-else-if="column.type === 'datetime'" class="whitespace-nowrap">
|
|
21
|
+
{{ checkEmptyValues(formatDateTime(record[column.name]), route.meta.type) }}
|
|
22
|
+
</span>
|
|
23
|
+
<span v-else-if="column.type === 'date'" class="whitespace-nowrap">
|
|
24
|
+
{{ checkEmptyValues(formatDate(record[column.name]), route.meta.type) }}
|
|
25
|
+
</span>
|
|
26
|
+
<span v-else-if="column.type === 'time'" class="whitespace-nowrap">
|
|
27
|
+
{{ checkEmptyValues(formatTime(record[column.name]), route.meta.type) }}
|
|
28
|
+
</span>
|
|
29
|
+
<span v-else-if="column.type === 'richtext'">
|
|
30
|
+
<div v-html="protectAgainstXSS(record[column.name])" class="allow-lists"></div>
|
|
31
|
+
</span>
|
|
32
|
+
<span v-else-if="column.type === 'decimal'">
|
|
33
|
+
{{ checkEmptyValues(record[column.name] && parseFloat(record[column.name]), route.meta.type) }}
|
|
34
|
+
</span>
|
|
35
|
+
<span v-else-if="column.type === 'json'">
|
|
36
|
+
<JsonViewer :value="record[column.name]" :expandDepth="column.extra?.jsonCollapsedLevel" copyable sort :theme="coreStore.theme" />
|
|
37
|
+
</span>
|
|
38
|
+
<span v-else>
|
|
39
|
+
{{ checkEmptyValues(record[column.name],route.meta.type) }}
|
|
40
|
+
</span>
|
|
41
|
+
</div>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
<script setup lang="ts">
|
|
46
|
+
|
|
47
|
+
import dayjs from 'dayjs';
|
|
48
|
+
import utc from 'dayjs/plugin/utc';
|
|
49
|
+
import timezone from 'dayjs/plugin/timezone';
|
|
50
|
+
import {checkEmptyValues} from '@/utils';
|
|
51
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
52
|
+
import sanitizeHtml from 'sanitize-html';
|
|
53
|
+
import { JsonViewer } from "vue3-json-viewer";
|
|
54
|
+
import "vue3-json-viewer/dist/index.css";
|
|
55
|
+
import type { AdminForthResourceColumnCommon } from '@/types/Common';
|
|
56
|
+
|
|
57
|
+
import { useCoreStore } from '@/stores/core';
|
|
58
|
+
|
|
59
|
+
const coreStore = useCoreStore();
|
|
60
|
+
const route = useRoute();
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
dayjs.extend(utc);
|
|
64
|
+
dayjs.extend(timezone);
|
|
65
|
+
|
|
66
|
+
const props = defineProps<{
|
|
67
|
+
column: AdminForthResourceColumnCommon,
|
|
68
|
+
record: any
|
|
69
|
+
}>();
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
function protectAgainstXSS(value: string) {
|
|
73
|
+
return sanitizeHtml(value, {
|
|
74
|
+
allowedTags: [
|
|
75
|
+
"address", "article", "aside", "footer", "header", "h1", "h2", "h3", "h4",
|
|
76
|
+
"h5", "h6", "hgroup", "main", "nav", "section", "blockquote", "dd", "div",
|
|
77
|
+
"dl", "dt", "figcaption", "figure", "hr", "li", "main", "ol", "p", "pre",
|
|
78
|
+
"ul", "a", "abbr", "b", "bdi", "bdo", "br", "cite", "code", "data", "dfn",
|
|
79
|
+
"em", "i", "kbd", "mark", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp",
|
|
80
|
+
"small", "span", "strong", "sub", "sup", "time", "u", "var", "wbr", "caption",
|
|
81
|
+
"col", "colgroup", "table", "tbody", "td", "tfoot", "th", "thead", "tr", 'img'
|
|
82
|
+
],
|
|
83
|
+
allowedAttributes: {
|
|
84
|
+
'li': [ 'data-list' ],
|
|
85
|
+
'img': [ 'src', 'srcset', 'alt', 'title', 'width', 'height', 'loading' ]
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
function formatDateTime(date: string) {
|
|
92
|
+
if (!date) return '';
|
|
93
|
+
return dayjs.utc(date).local().format(`${coreStore.config?.datesFormat} ${coreStore.config?.timeFormat}` || 'YYYY-MM-DD HH:mm:ss');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function formatDate(date: string) {
|
|
97
|
+
if (!date) return '';
|
|
98
|
+
return dayjs.utc(date).local().format(coreStore.config?.datesFormat || 'YYYY-MM-DD');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatTime(time: string) {
|
|
102
|
+
if (!time) return '';
|
|
103
|
+
return dayjs(`0000-00-00 ${time}`).format(coreStore.config?.timeFormat || 'HH:mm:ss');
|
|
104
|
+
}
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
<style lang="scss">
|
|
108
|
+
|
|
109
|
+
.allow-lists {
|
|
110
|
+
ol {
|
|
111
|
+
list-style-type: decimal;
|
|
112
|
+
padding-left: 1.5em;
|
|
113
|
+
|
|
114
|
+
li[data-list="bullet"] {
|
|
115
|
+
list-style-type: disc;
|
|
116
|
+
}
|
|
117
|
+
li[data-list="ordered"] {
|
|
118
|
+
list-style-type: decimal;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
</style>
|
|
124
|
+
|
|
125
|
+
<style lang="scss" >
|
|
126
|
+
|
|
127
|
+
.jv-container .jv-code {
|
|
128
|
+
padding: 10px 10px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.jv-container .jv-button[class] {
|
|
132
|
+
@apply text-lightPrimary;
|
|
133
|
+
@apply dark:text-darkPrimary;
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.jv-container.jv-dark {
|
|
138
|
+
background: transparent;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
</style>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
3
|
+
<path d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"/>
|
|
4
|
+
</svg>
|
|
5
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
|
3
|
+
<path
|
|
4
|
+
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
|
5
|
+
/>
|
|
6
|
+
</svg>
|
|
7
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
|
3
|
+
<path
|
|
4
|
+
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
|
5
|
+
/>
|
|
6
|
+
</svg>
|
|
7
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
|
3
|
+
<path
|
|
4
|
+
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
|
5
|
+
/>
|
|
6
|
+
</svg>
|
|
7
|
+
</template>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
|
3
|
+
<path
|
|
4
|
+
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
|
5
|
+
/>
|
|
6
|
+
</svg>
|
|
7
|
+
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24">
|
|
3
|
+
<path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm11-4a1 1 0 1 0-2 0v4a1 1 0 0 0 .293.707l3 3a1 1 0 0 0 1.414-1.414L13 11.586V8Z" clip-rule="evenodd"></path>
|
|
4
|
+
</svg>
|
|
5
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
|
2
|
+
<template>
|
|
3
|
+
<svg
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
role="img"
|
|
8
|
+
class="iconify iconify--mdi"
|
|
9
|
+
width="24"
|
|
10
|
+
height="24"
|
|
11
|
+
preserveAspectRatio="xMidYMid meet"
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
>
|
|
14
|
+
<path
|
|
15
|
+
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
|
16
|
+
fill="currentColor"
|
|
17
|
+
></path>
|
|
18
|
+
</svg>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import adminforth from '@/adminforth';
|
|
2
|
+
|
|
3
|
+
export function showSuccesTost(message: string) {
|
|
4
|
+
adminforth.alert({ message, variant: 'success' });
|
|
5
|
+
return message;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function showWarningTost(message: string) {
|
|
9
|
+
adminforth.alert({ message, variant: 'warning' });
|
|
10
|
+
return message;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function showErrorTost(message: string, timeout?: number) {
|
|
14
|
+
adminforth.alert({ message, variant: 'danger', timeout: timeout || 30});
|
|
15
|
+
return message;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const useFrontendApi = () => {
|
|
20
|
+
return {
|
|
21
|
+
showSuccesTost,
|
|
22
|
+
showWarningTost,
|
|
23
|
+
showErrorTost
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export default useFrontendApi;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createI18n } from 'vue-i18n';
|
|
2
|
+
import { createApp } from 'vue';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// taken from here https://vue-i18n.intlify.dev/guide/essentials/pluralization.html#custom-pluralization
|
|
6
|
+
function slavicPluralRule(choice, choicesLength, orgRule) {
|
|
7
|
+
if (choice === 0) {
|
|
8
|
+
return 0
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const teen = choice > 10 && choice < 20
|
|
12
|
+
const endsWithOne = choice % 10 === 1
|
|
13
|
+
|
|
14
|
+
if (!teen && endsWithOne) {
|
|
15
|
+
return 1
|
|
16
|
+
}
|
|
17
|
+
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
|
|
18
|
+
return 2
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return choicesLength < 4 ? 2 : 3
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function initI18n(app: ReturnType<typeof createApp>) {
|
|
25
|
+
const i18n = createI18n({
|
|
26
|
+
legacy: false,
|
|
27
|
+
|
|
28
|
+
missingWarn: false,
|
|
29
|
+
fallbackWarn: false,
|
|
30
|
+
|
|
31
|
+
pluralRules: {
|
|
32
|
+
'uk': slavicPluralRule,
|
|
33
|
+
'bg': slavicPluralRule,
|
|
34
|
+
'cs': slavicPluralRule,
|
|
35
|
+
'hr': slavicPluralRule,
|
|
36
|
+
'mk': slavicPluralRule,
|
|
37
|
+
'pl': slavicPluralRule,
|
|
38
|
+
'sk': slavicPluralRule,
|
|
39
|
+
'sl': slavicPluralRule,
|
|
40
|
+
'sr': slavicPluralRule,
|
|
41
|
+
'be': slavicPluralRule,
|
|
42
|
+
'ru': slavicPluralRule,
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
missing: (locale, key) => {
|
|
46
|
+
// very very dirty hack to make work $t("a {key} b", { key: "c" }) as "a c b" when translation is missing
|
|
47
|
+
// e.g. relevant for "Showing {from} to {to} of {total} entries" on list page
|
|
48
|
+
return key + ' ';
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
app.use(i18n);
|
|
53
|
+
return i18n
|
|
54
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// @layer base {
|
|
7
|
+
// /* width */
|
|
8
|
+
// ::-webkit-scrollbar {
|
|
9
|
+
// @apply w-2 p-2
|
|
10
|
+
// }
|
|
11
|
+
|
|
12
|
+
// /* Track */
|
|
13
|
+
// ::-webkit-scrollbar-track {
|
|
14
|
+
// @apply bg-inherit
|
|
15
|
+
// }
|
|
16
|
+
|
|
17
|
+
// /* Handle */
|
|
18
|
+
// ::-webkit-scrollbar-thumb {
|
|
19
|
+
// @apply bg-gray-200 dark:bg-gray-600 rounded-xl
|
|
20
|
+
// }
|
|
21
|
+
|
|
22
|
+
// /* Handle on hover */
|
|
23
|
+
// ::-webkit-scrollbar-thumb:hover {
|
|
24
|
+
// @apply bg-gray-700
|
|
25
|
+
// }
|
|
26
|
+
// }
|
|
27
|
+
|
|
28
|
+
body {
|
|
29
|
+
scrollbar-gutter: stable;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
*{
|
|
33
|
+
-moz-user-select: none;
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createApp } from 'vue'
|
|
2
|
+
import { createPinia } from 'pinia'
|
|
3
|
+
/* IMPORTANT:ADMINFORTH IMPORTS */
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import App from './App.vue'
|
|
7
|
+
import router from './router'
|
|
8
|
+
import { initI18n } from './i18n'
|
|
9
|
+
|
|
10
|
+
export const app: ReturnType<typeof createApp> = createApp(App)
|
|
11
|
+
/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */
|
|
12
|
+
|
|
13
|
+
app.use(createPinia())
|
|
14
|
+
app.use(router)
|
|
15
|
+
|
|
16
|
+
// get access to i18n instance outside components
|
|
17
|
+
window.i18n = initI18n(app);
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/* IMPORTANT:ADMINFORTH CUSTOM USES */
|
|
21
|
+
|
|
22
|
+
app.mount('#app')
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Tooltip>
|
|
3
|
+
<span class="flex items-center">
|
|
4
|
+
{{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
|
|
5
|
+
</span>
|
|
6
|
+
<template #tooltip v-if="visualValue">
|
|
7
|
+
{{ props.record[props.column.name] }}
|
|
8
|
+
</template>
|
|
9
|
+
</Tooltip>
|
|
10
|
+
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
|
|
15
|
+
import { computed, ref, onMounted, nextTick } from 'vue';
|
|
16
|
+
import { IconFileCopyAltSolid } from '@iconify-prerendered/vue-flowbite';
|
|
17
|
+
import Tooltip from '@/afcl/Tooltip.vue';
|
|
18
|
+
import adminforth from '@/adminforth';
|
|
19
|
+
|
|
20
|
+
const visualValue = computed(() => {
|
|
21
|
+
// if lenght is more then 8, show only first 4 and last 4 characters, ... in the middle
|
|
22
|
+
const val = props.record[props.column.name];
|
|
23
|
+
if (val && val.length > 8) {
|
|
24
|
+
return `${val.substr(0, 4)}...${val.substr(val.length - 4)}`;
|
|
25
|
+
}
|
|
26
|
+
return val;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
30
|
+
|
|
31
|
+
const id = ref();
|
|
32
|
+
|
|
33
|
+
function copyToCB() {
|
|
34
|
+
navigator.clipboard.writeText(props.record[props.column.name]);
|
|
35
|
+
adminforth.alert({
|
|
36
|
+
message: 'ID copied to clipboard',
|
|
37
|
+
variant: 'success',
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onMounted(async () => {
|
|
42
|
+
id.value = Math.random().toString(36).substring(7);
|
|
43
|
+
await nextTick();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
</script>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Tooltip>
|
|
3
|
+
<span class="flex items-center">
|
|
4
|
+
{{ visualValue }} <IconFileCopyAltSolid @click.stop="copyToCB" class="w-5 h-5 text-lightPrimary dark:text-darkPrimary" v-if="visualValue"/>
|
|
5
|
+
</span>
|
|
6
|
+
<template #tooltip v-if="visualValue">
|
|
7
|
+
{{ props.record[props.column.name] }}
|
|
8
|
+
</template>
|
|
9
|
+
</Tooltip>
|
|
10
|
+
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
|
|
15
|
+
import { computed, ref, onMounted, nextTick } from 'vue';
|
|
16
|
+
import { IconFileCopyAltSolid } from '@iconify-prerendered/vue-flowbite';
|
|
17
|
+
import Tooltip from '@/afcl/Tooltip.vue';
|
|
18
|
+
import adminforth from '@/adminforth';
|
|
19
|
+
|
|
20
|
+
const visualValue = computed(() => {
|
|
21
|
+
// if lenght is more then 8, show only first 4 and last 4 characters, ... in the middle
|
|
22
|
+
const val = props.record[props.column.name];
|
|
23
|
+
if (val && val.length > 8) {
|
|
24
|
+
return `${val.substr(0, 4)}...${val.substr(val.length - 4)}`;
|
|
25
|
+
}
|
|
26
|
+
return val;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
30
|
+
|
|
31
|
+
const id = ref();
|
|
32
|
+
|
|
33
|
+
function copyToCB() {
|
|
34
|
+
navigator.clipboard.writeText(props.record[props.column.name]);
|
|
35
|
+
adminforth.alert({
|
|
36
|
+
message: 'ID copied to clipboard',
|
|
37
|
+
variant: 'success',
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onMounted(async () => {
|
|
42
|
+
id.value = Math.random().toString(36).substring(7);
|
|
43
|
+
await nextTick();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
</script>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Tooltip>
|
|
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
|
+
</Tooltip>
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
|
|
18
|
+
import { computed, ref, onMounted } from 'vue';
|
|
19
|
+
import 'flag-icons/css/flag-icons.min.css';
|
|
20
|
+
import isoCountries from 'i18n-iso-countries';
|
|
21
|
+
import enLocal from 'i18n-iso-countries/langs/en.json';
|
|
22
|
+
import Tooltip from '@/afcl/Tooltip.vue';
|
|
23
|
+
|
|
24
|
+
isoCountries.registerLocale(enLocal);
|
|
25
|
+
|
|
26
|
+
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
27
|
+
|
|
28
|
+
const id = ref();
|
|
29
|
+
|
|
30
|
+
const shouldShowTooltip = computed(() => {
|
|
31
|
+
return !props.meta.showCountryName && countryName.value;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
onMounted(async () => {
|
|
35
|
+
id.value = Math.random().toString(36).substring(7);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const countryIsoLow = computed(() => {
|
|
39
|
+
return props.record[props.column.name]?.toLowerCase();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const countryName = computed(() => {
|
|
43
|
+
if (!countryIsoLow.value) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
return isoCountries.getName(countryIsoLow.value, 'en');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<style scoped lang="scss">
|
|
52
|
+
|
|
53
|
+
.flag-icon {
|
|
54
|
+
width: 1.6rem;
|
|
55
|
+
height: 1.2rem;
|
|
56
|
+
flex-shrink: 0;
|
|
57
|
+
|
|
58
|
+
// border radius for background
|
|
59
|
+
border-radius: 2px;
|
|
60
|
+
box-shadow: inset -0.3px -0.3px 0.3px 0px rgba(0 0 0 / 0.2),
|
|
61
|
+
inset 0.3px 0.3px 0.3px 0px rgba(255 255 255 / 0.2),
|
|
62
|
+
0px 0px 3px #00000030;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
</style>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Tooltip>
|
|
3
|
+
{{ formattedValue }}
|
|
4
|
+
<template #tooltip v-if="formattedValue">
|
|
5
|
+
{{ formattedTooltipValue }}
|
|
6
|
+
</template>
|
|
7
|
+
</Tooltip>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup>
|
|
11
|
+
import { computed, ref, onMounted, nextTick } from 'vue';
|
|
12
|
+
import Tooltip from '@/afcl/Tooltip.vue';
|
|
13
|
+
|
|
14
|
+
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
15
|
+
|
|
16
|
+
const id = ref();
|
|
17
|
+
const userLocale = ref(navigator.language || 'en-US');
|
|
18
|
+
|
|
19
|
+
const formattedValue = computed(() => {
|
|
20
|
+
const val = props.record[props.column.name];
|
|
21
|
+
if (typeof val === 'number') {
|
|
22
|
+
return formatNumber(val, userLocale.value);
|
|
23
|
+
}
|
|
24
|
+
return val;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const formattedTooltipValue = computed(() => {
|
|
28
|
+
const val = props.record[props.column.name];
|
|
29
|
+
if (typeof val === 'number') {
|
|
30
|
+
return formatNumberUsingIntl(val, userLocale.value);
|
|
31
|
+
}
|
|
32
|
+
return val;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function formatNumber(num, locale) {
|
|
36
|
+
if (typeof num !== 'number') {
|
|
37
|
+
return num.toString();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (num >= 1_000_000) {
|
|
41
|
+
return `${(num / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`;
|
|
42
|
+
} else if (num >= 1_000) {
|
|
43
|
+
return `${(num / 1_000).toFixed(1).replace(/\.0$/, '')}k`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// If it's less than 1000, format using locale
|
|
47
|
+
return new Intl.NumberFormat(locale).format(num);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatNumberUsingIntl(num, locale) {
|
|
51
|
+
return new Intl.NumberFormat(locale).format(num);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onMounted(async () => {
|
|
55
|
+
id.value = Math.random().toString(36).substring(7);
|
|
56
|
+
await nextTick();
|
|
57
|
+
});
|
|
58
|
+
</script>
|