adminforth 2.4.0-next.31 → 2.4.0-next.310
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/commands/callTsProxy.js +14 -4
- package/commands/createApp/templates/api.ts.hbs +10 -0
- package/commands/createApp/templates/custom/tsconfig.json.hbs +2 -3
- package/commands/createApp/templates/index.ts.hbs +12 -1
- package/commands/createApp/templates/package.json.hbs +1 -1
- package/commands/createApp/templates/prisma.config.ts.hbs +8 -0
- package/commands/createApp/templates/schema.prisma.hbs +0 -1
- package/commands/createApp/utils.js +10 -0
- package/commands/createCustomComponent/configLoader.js +17 -4
- package/commands/createCustomComponent/main.js +13 -7
- package/commands/createCustomComponent/templates/customCrud/beforeActionButtons.vue.hbs +38 -0
- package/commands/createCustomComponent/templates/customCrud/saveButton.vue.hbs +28 -0
- package/commands/createPlugin/templates/custom/tsconfig.json.hbs +2 -5
- package/commands/createPlugin/templates/package.json.hbs +1 -1
- package/commands/generateModels.js +30 -22
- package/dist/auth.d.ts +9 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +21 -2
- package/dist/auth.js.map +1 -1
- package/dist/dataConnectors/baseConnector.d.ts +1 -1
- package/dist/dataConnectors/baseConnector.d.ts.map +1 -1
- package/dist/dataConnectors/baseConnector.js +69 -17
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +15 -0
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/dataConnectors/mongo.d.ts.map +1 -1
- package/dist/dataConnectors/mongo.js +50 -15
- package/dist/dataConnectors/mongo.js.map +1 -1
- package/dist/dataConnectors/mysql.d.ts.map +1 -1
- package/dist/dataConnectors/mysql.js +11 -0
- package/dist/dataConnectors/mysql.js.map +1 -1
- package/dist/dataConnectors/postgres.d.ts.map +1 -1
- package/dist/dataConnectors/postgres.js +43 -14
- package/dist/dataConnectors/postgres.js.map +1 -1
- package/dist/dataConnectors/sqlite.d.ts.map +1 -1
- package/dist/dataConnectors/sqlite.js +11 -0
- package/dist/dataConnectors/sqlite.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -22
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts +2 -0
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +62 -6
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts +6 -0
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +202 -25
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +172 -31
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/styles.d.ts +499 -13
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +555 -31
- package/dist/modules/styles.js.map +1 -1
- package/dist/modules/utils.d.ts +7 -15
- package/dist/modules/utils.d.ts.map +1 -1
- package/dist/modules/utils.js +45 -68
- package/dist/modules/utils.js.map +1 -1
- package/dist/servers/express.d.ts +5 -0
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/servers/express.js +40 -1
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/index.html +1 -1
- package/dist/spa/package-lock.json +1208 -708
- package/dist/spa/package.json +34 -34
- package/dist/spa/src/App.vue +59 -174
- package/dist/spa/src/adminforth.ts +42 -18
- package/dist/spa/src/afcl/AreaChart.vue +0 -1
- package/dist/spa/src/afcl/BarChart.vue +2 -2
- package/dist/spa/src/afcl/Button.vue +6 -6
- package/dist/spa/src/afcl/ButtonGroup.vue +91 -0
- package/dist/spa/src/afcl/Card.vue +25 -0
- package/dist/spa/src/afcl/Checkbox.vue +21 -13
- package/dist/spa/src/afcl/CountryFlag.vue +4 -1
- package/dist/spa/src/{components/CustomDatePicker.vue → afcl/DatePicker.vue} +95 -9
- package/dist/spa/src/afcl/Dialog.vue +47 -27
- package/dist/spa/src/afcl/Dropzone.vue +127 -48
- package/dist/spa/src/afcl/Input.vue +14 -6
- package/dist/spa/src/afcl/JsonViewer.vue +25 -0
- package/dist/spa/src/afcl/LinkButton.vue +3 -3
- package/dist/spa/src/afcl/PieChart.vue +5 -5
- package/dist/spa/src/afcl/ProgressBar.vue +7 -7
- package/dist/spa/src/afcl/Select.vue +82 -34
- package/dist/spa/src/afcl/Skeleton.vue +6 -6
- package/dist/spa/src/afcl/Table.vue +315 -73
- package/dist/spa/src/afcl/Textarea.vue +31 -0
- package/dist/spa/src/afcl/Toggle.vue +32 -0
- package/dist/spa/src/afcl/Tooltip.vue +28 -18
- package/dist/spa/src/afcl/VerticalTabs.vue +16 -7
- package/dist/spa/src/afcl/index.ts +6 -3
- package/dist/spa/src/components/AcceptModal.vue +48 -14
- package/dist/spa/src/components/Breadcrumbs.vue +5 -5
- package/dist/spa/src/components/CallActionWrapper.vue +15 -0
- package/dist/spa/src/components/ColumnValueInput.vue +38 -18
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +4 -3
- package/dist/spa/src/components/CustomDateRangePicker.vue +9 -8
- package/dist/spa/src/components/CustomRangePicker.vue +37 -21
- package/dist/spa/src/components/ErrorMessage.vue +21 -0
- package/dist/spa/src/components/Filters.vue +195 -132
- package/dist/spa/src/components/GroupsTable.vue +9 -8
- package/dist/spa/src/components/MenuLink.vue +90 -23
- package/dist/spa/src/components/ResourceForm.vue +94 -51
- package/dist/spa/src/components/ResourceListTable.vue +115 -85
- package/dist/spa/src/components/ResourceListTableVirtual.vue +114 -80
- package/dist/spa/src/components/ShowTable.vue +21 -15
- package/dist/spa/src/components/Sidebar.vue +470 -0
- package/dist/spa/src/components/SingleSkeletLoader.vue +6 -6
- package/dist/spa/src/components/SkeleteLoader.vue +3 -3
- package/dist/spa/src/components/ThreeDotsMenu.vue +84 -15
- package/dist/spa/src/components/Toast.vue +40 -29
- package/dist/spa/src/components/UserMenuSettingsButton.vue +69 -0
- package/dist/spa/src/components/ValueRenderer.vue +44 -17
- package/dist/spa/src/controls/BoolToggle.vue +34 -0
- package/dist/spa/src/i18n.ts +5 -3
- package/dist/spa/src/main.ts +1 -1
- package/dist/spa/src/renderers/CompactField.vue +1 -1
- package/dist/spa/src/renderers/CompactUUID.vue +1 -1
- package/dist/spa/src/router/index.ts +8 -0
- package/dist/spa/src/shims-vue.d.ts +5 -0
- package/dist/spa/src/spa_types/core.ts +13 -1
- package/dist/spa/src/stores/core.ts +13 -1
- package/dist/spa/src/stores/filters.ts +33 -2
- package/dist/spa/src/stores/modal.ts +6 -1
- package/dist/spa/src/stores/toast.ts +22 -3
- package/dist/spa/src/types/Back.ts +163 -23
- package/dist/spa/src/types/Common.ts +91 -32
- package/dist/spa/src/types/FrontendAPI.ts +31 -5
- package/dist/spa/src/types/adapters/CaptchaAdapter.ts +34 -0
- package/dist/spa/src/types/adapters/EmailAdapter.ts +2 -2
- package/dist/spa/src/types/adapters/ImageVisionAdapter.ts +30 -0
- package/dist/spa/src/types/adapters/KeyValueAdapter.ts +16 -0
- package/dist/spa/src/types/adapters/index.ts +8 -0
- package/dist/spa/src/utils.ts +291 -11
- package/dist/spa/src/views/CreateView.vue +63 -21
- package/dist/spa/src/views/EditView.vue +55 -22
- package/dist/spa/src/views/ListView.vue +144 -87
- package/dist/spa/src/views/LoginView.vue +26 -35
- package/dist/spa/src/views/ResourceParent.vue +2 -2
- package/dist/spa/src/views/SettingsView.vue +121 -0
- package/dist/spa/src/views/ShowView.vue +83 -53
- package/dist/spa/src/websocket.ts +6 -1
- package/dist/spa/tsconfig.app.json +1 -1
- package/dist/spa/vite.config.ts +45 -2
- package/dist/types/Back.d.ts +146 -14
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js +15 -0
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +106 -29
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +31 -3
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.js.map +1 -1
- package/dist/types/adapters/CaptchaAdapter.d.ts +30 -0
- package/dist/types/adapters/CaptchaAdapter.d.ts.map +1 -0
- package/dist/types/adapters/CaptchaAdapter.js +5 -0
- package/dist/types/adapters/CaptchaAdapter.js.map +1 -0
- package/dist/types/adapters/EmailAdapter.d.ts +1 -1
- package/dist/types/adapters/ImageVisionAdapter.d.ts +25 -0
- package/dist/types/adapters/ImageVisionAdapter.d.ts.map +1 -0
- package/dist/types/adapters/ImageVisionAdapter.js +2 -0
- package/dist/types/adapters/ImageVisionAdapter.js.map +1 -0
- package/dist/types/adapters/KeyValueAdapter.d.ts +10 -0
- package/dist/types/adapters/KeyValueAdapter.d.ts.map +1 -0
- package/dist/types/adapters/KeyValueAdapter.js +2 -0
- package/dist/types/adapters/KeyValueAdapter.js.map +1 -0
- package/dist/types/adapters/index.d.ts +9 -0
- package/dist/types/adapters/index.d.ts.map +1 -0
- package/dist/types/adapters/index.js +2 -0
- package/dist/types/adapters/index.js.map +1 -0
- package/package.json +4 -2
- package/dist/spa/src/types/adapters/index.js +0 -5
|
@@ -1,116 +1,358 @@
|
|
|
1
1
|
<template>
|
|
2
|
+
<div class="afcl-table-container relative overflow-x-auto shadow-md rounded-lg">
|
|
3
|
+
<div class="overflow-x-auto w-full">
|
|
4
|
+
<table class="afcl-table w-full text-sm text-left rtl:text-right text-lightTableText dark:text-darkTableText overflow-x-auto">
|
|
5
|
+
<thead class="afcl-table-thread text-xs text-lightTableHeadingText uppercase bg-lightTableHeadingBackground dark:bg-darkTableHeadingBackground dark:text-darkTableHeadingText">
|
|
6
|
+
<tr>
|
|
7
|
+
<th
|
|
8
|
+
scope="col"
|
|
9
|
+
class="px-6 py-3"
|
|
10
|
+
ref="headerRefs"
|
|
11
|
+
:key="`header-${column.fieldName}`"
|
|
12
|
+
v-for="column in columns"
|
|
13
|
+
:aria-sort="getAriaSort(column)"
|
|
14
|
+
:class="{ 'cursor-pointer select-none afcl-table-header-sortable': isColumnSortable(column) }"
|
|
15
|
+
@click="onHeaderClick(column)"
|
|
16
|
+
>
|
|
17
|
+
<slot v-if="$slots[`header:${column.fieldName}`]" :name="`header:${column.fieldName}`" :column="column" />
|
|
2
18
|
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
v-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
<span v-else class="inline-flex items-center">
|
|
20
|
+
{{ column.label }}
|
|
21
|
+
<span v-if="isColumnSortable(column)" class="text-lightTableHeadingText dark:text-darkTableHeadingText">
|
|
22
|
+
<!-- Unsorted -->
|
|
23
|
+
<svg v-if="currentSortField !== column.fieldName" class="w-3 h-3 ms-1.5 opacity-30" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24"><path 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"/></svg>
|
|
24
|
+
|
|
25
|
+
<!-- Sorted ascending -->
|
|
26
|
+
<svg v-else-if="currentSortDirection === 'asc'" class="w-3 h-3 ms-1.5" fill="currentColor" viewBox="0 0 24 24"><path 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 0z"/></svg>
|
|
27
|
+
|
|
28
|
+
<!-- Sorted descending -->
|
|
29
|
+
<svg v-else class="w-3 h-3 ms-1.5 rotate-180" fill="currentColor" viewBox="0 0 24 24"><path 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 0z"/></svg>
|
|
30
|
+
</span>
|
|
31
|
+
</span>
|
|
32
|
+
</th>
|
|
33
|
+
</tr>
|
|
34
|
+
</thead>
|
|
35
|
+
<tbody>
|
|
36
|
+
<SkeleteLoader
|
|
37
|
+
v-if="isLoading || props.isLoading"
|
|
38
|
+
:rows="pageSize"
|
|
39
|
+
:columns="columns.length"
|
|
40
|
+
:row-heights="rowHeights"
|
|
41
|
+
:column-widths="columnWidths"
|
|
42
|
+
/>
|
|
43
|
+
<tr v-else-if="!isLoading && !props.isLoading && dataPage.length === 0" class="afcl-table-empty-body">
|
|
44
|
+
<td :colspan="columns.length" class="px-6 py-12 text-center">
|
|
45
|
+
<div class="flex flex-col items-center justify-center text-lightTableText dark:text-darkTableText">
|
|
46
|
+
<IconTableRowOutline class="w-10 h-10 mb-4 text-gray-400 dark:text-gray-500" />
|
|
47
|
+
<p class="text-md">{{ $t('No data available') }}</p>
|
|
48
|
+
</div>
|
|
49
|
+
</td>
|
|
50
|
+
</tr>
|
|
51
|
+
<tr
|
|
52
|
+
v-else="!isLoading && !props.isLoading"
|
|
53
|
+
v-for="(item, index) in dataPage"
|
|
54
|
+
:key="`row-${index}`"
|
|
55
|
+
ref="rowRefs"
|
|
56
|
+
:class="{
|
|
57
|
+
'afcl-table-body odd:bg-lightTableOddBackground odd:dark:bg-darkTableOddBackground even:bg-lightTableEvenBackground even:dark:bg-darkTableEvenBackground': evenHighlights,
|
|
58
|
+
'border-b border-lightTableBorder dark:border-darkTableBorder': index !== dataPage.length - 1 || totalPages > 1,
|
|
59
|
+
}"
|
|
60
|
+
@click="tableRowClick(item)"
|
|
32
61
|
>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
<td class="px-6 py-4" :key="`cell-${index}-${column.fieldName}`"
|
|
63
|
+
v-for="column in props.columns"
|
|
64
|
+
>
|
|
65
|
+
<slot v-if="$slots[`cell:${column.fieldName}`]"
|
|
66
|
+
:name="`cell:${column.fieldName}`"
|
|
67
|
+
:item="item" :column="column"
|
|
68
|
+
>
|
|
69
|
+
</slot>
|
|
70
|
+
<span v-else-if="!isLoading || props.isLoading" >
|
|
71
|
+
{{ item[column.fieldName] }}
|
|
72
|
+
</span>
|
|
73
|
+
<div v-else>
|
|
74
|
+
<div class=" w-full">
|
|
75
|
+
<Skeleton class="h-4" />
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</td>
|
|
79
|
+
</tr>
|
|
80
|
+
</tbody>
|
|
81
|
+
</table>
|
|
82
|
+
</div>
|
|
83
|
+
<nav class="afcl-table-pagination-container bg-lightTableBackground dark:bg-darkTableBackground mt-2 flex flex-col gap-2 items-center sm:flex-row justify-center sm:justify-between px-4 pb-4"
|
|
84
|
+
v-if="totalPages > 1"
|
|
85
|
+
:aria-label="$t('Table navigation')">
|
|
44
86
|
<i18n-t
|
|
45
|
-
keypath="Showing {from} to {to} of {total}" tag="span" class="text-sm font-normal text-
|
|
87
|
+
keypath="Showing {from} to {to} of {total}" tag="span" class="afcl-table-pagination-text text-sm font-normal text-center text-lightTablePaginationText dark:text-darkTablePaginationText sm:mb-4 md:mb-0 block w-full md:inline md:w-auto"
|
|
46
88
|
>
|
|
47
|
-
<template #from><span class="font-semibold text-
|
|
48
|
-
<template #to><span class="font-semibold text-
|
|
49
|
-
<template #total><span class="font-semibold text-
|
|
89
|
+
<template #from><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min((currentPage - 1) * props.pageSize + 1, dataResult.total) }}</span></template>
|
|
90
|
+
<template #to><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ Math.min(currentPage * props.pageSize, dataResult.total) }}</span></template>
|
|
91
|
+
<template #total><span class="font-semibold text-lightTablePaginationNumeration dark:text-darkTablePaginationNumeration">{{ dataResult.total }}</span></template>
|
|
50
92
|
</i18n-t>
|
|
93
|
+
<div class="af-pagination-container flex flex-row items-center xs:flex-row xs:justify-between xs:items-center gap-3">
|
|
94
|
+
<div class="inline-flex" :class="isLoading || props.isLoading ? 'pointer-events-none select-none opacity-50' : ''">
|
|
95
|
+
<!-- Buttons -->
|
|
96
|
+
<button
|
|
97
|
+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText bg-lightActivePaginationButtonBackground border-r-0 rounded-s hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
98
|
+
@click="currentPage--; pageInput = currentPage.toString();"
|
|
99
|
+
:disabled="currentPage <= 1 || isLoading || props.isLoading">
|
|
100
|
+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
101
|
+
viewBox="0 0 14 10">
|
|
102
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
103
|
+
d="M13 5H1m0 0 4 4M1 5l4-4"/>
|
|
104
|
+
</svg>
|
|
105
|
+
</button>
|
|
106
|
+
<button
|
|
107
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-r-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
108
|
+
@click="switchPage(1); pageInput = currentPage.toString();"
|
|
109
|
+
:disabled="currentPage <= 1 || isLoading || props.isLoading">
|
|
110
|
+
<!-- <IconChevronDoubleLeftOutline class="w-4 h-4" /> -->
|
|
111
|
+
1
|
|
112
|
+
</button>
|
|
113
|
+
<div
|
|
114
|
+
:contenteditable="!isLoading && !props.isLoading"
|
|
115
|
+
class="min-w-10 outline-none inline-block w-auto py-1.5 px-3 text-sm text-center text-lightTablePaginationInputText border border-lightTablePaginationInputBorder bg-lightTablePaginationInputBackground dark:border-darkTablePaginationInputBorder dark:text-darkTablePaginationInputText dark:bg-darkTablePaginationInputBackground z-10"
|
|
116
|
+
@keydown="onPageKeydown($event)"
|
|
117
|
+
@input="onPageInput($event)"
|
|
118
|
+
@blur="validatePageInput()"
|
|
119
|
+
>
|
|
120
|
+
{{ pageInput }}
|
|
121
|
+
</div>
|
|
51
122
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"text-blue-600 bg-lightPrimary text-lightPrimaryContrast dark:bg-darkPrimary dark:text-darkPrimaryContrast hover:opacity-90": page === currentPage,
|
|
59
|
-
"text-gray-500 border bg-white border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white": page !== currentPage,
|
|
60
|
-
"rounded-s-lg ms-0": page === 1,
|
|
61
|
-
"rounded-e-lg": page === totalPages,
|
|
62
|
-
}'
|
|
63
|
-
class="flex items-center justify-center px-3 h-8 leading-tight ">
|
|
64
|
-
{{ page }}
|
|
65
|
-
</a>
|
|
66
|
-
</li>
|
|
67
|
-
</ul>
|
|
68
|
-
</nav>
|
|
69
|
-
</div>
|
|
70
|
-
|
|
123
|
+
<button
|
|
124
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-lightUnactivePaginationButtonText focus:outline-none bg-lightUnactivePaginationButtonBackground border-l-0 border border-lightUnactivePaginationButtonBorder hover:bg-lightUnactivePaginationButtonHoverBackground hover:text-lightUnactivePaginationButtonHoverText dark:bg-darkUnactivePaginationButtonBackground dark:text-darkUnactivePaginationButtonText dark:border-darkUnactivePaginationButtonBorder dark:hover:text-darkUnactivePaginationButtonHoverText dark:hover:bg-darkUnactivePaginationButtonHoverBackground disabled:opacity-50"
|
|
125
|
+
@click="currentPage = totalPages; pageInput = currentPage.toString();"
|
|
126
|
+
:disabled="currentPage >= totalPages || isLoading || props.isLoading"
|
|
127
|
+
>
|
|
128
|
+
{{ totalPages }}
|
|
71
129
|
|
|
72
|
-
|
|
130
|
+
</button>
|
|
131
|
+
<button
|
|
132
|
+
class="flex items-center py-1 px-3 gap-1 text-sm font-medium text-lightActivePaginationButtonText focus:outline-none bg-lightActivePaginationButtonBackground border-l-0 rounded-e hover:opacity-90 dark:bg-darkActivePaginationButtonBackground dark:text-darkActivePaginationButtonText disabled:opacity-50"
|
|
133
|
+
@click="currentPage++; pageInput = currentPage.toString();"
|
|
134
|
+
:disabled="currentPage >= totalPages || isLoading || props.isLoading"
|
|
135
|
+
>
|
|
136
|
+
<svg class="w-3.5 h-3.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
|
137
|
+
viewBox="0 0 14 10">
|
|
138
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
139
|
+
d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
|
140
|
+
</svg>
|
|
141
|
+
</button>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</nav>
|
|
145
|
+
</div>
|
|
73
146
|
</template>
|
|
74
147
|
|
|
75
148
|
<script setup lang="ts">
|
|
76
|
-
import { ref,
|
|
149
|
+
import { ref, computed, useTemplateRef, watch, onMounted } from 'vue';
|
|
150
|
+
import SkeleteLoader from '@/components/SkeleteLoader.vue';
|
|
151
|
+
import { IconTableRowOutline } from '@iconify-prerendered/vue-flowbite';
|
|
152
|
+
|
|
153
|
+
defineExpose({
|
|
154
|
+
refreshTable
|
|
155
|
+
})
|
|
77
156
|
|
|
78
157
|
const props = withDefaults(
|
|
79
158
|
defineProps<{
|
|
80
159
|
columns: {
|
|
81
160
|
label: string,
|
|
82
161
|
fieldName: string,
|
|
162
|
+
sortable?: boolean,
|
|
83
163
|
}[],
|
|
84
164
|
data: {
|
|
85
165
|
[key: string]: any,
|
|
86
|
-
}[],
|
|
166
|
+
}[] | ((params: { offset: number, limit: number, sortField?: string, sortDirection?: 'asc' | 'desc' }) => Promise<{data: {[key: string]: any}[], total: number}>),
|
|
87
167
|
evenHighlights?: boolean,
|
|
88
168
|
pageSize?: number,
|
|
169
|
+
isLoading?: boolean,
|
|
170
|
+
defaultSortField?: string,
|
|
171
|
+
defaultSortDirection?: 'asc' | 'desc',
|
|
89
172
|
}>(), {
|
|
90
173
|
evenHighlights: true,
|
|
91
|
-
pageSize:
|
|
174
|
+
pageSize: 5,
|
|
92
175
|
}
|
|
93
176
|
);
|
|
94
177
|
|
|
178
|
+
const pageInput = ref('1');
|
|
179
|
+
const rowRefs = useTemplateRef<HTMLElement[]>('rowRefs');
|
|
180
|
+
const headerRefs = useTemplateRef<HTMLElement[]>('headerRefs');
|
|
181
|
+
const rowHeights = ref<number[]>([]);
|
|
182
|
+
const columnWidths = ref<number[]>([]);
|
|
95
183
|
const currentPage = ref(1);
|
|
184
|
+
const isLoading = ref(false);
|
|
185
|
+
const dataResult = ref<{data: {[key: string]: any}[], total: number}>({data: [], total: 0});
|
|
186
|
+
const isAtLeastOneLoading = ref<boolean[]>([false]);
|
|
187
|
+
const currentSortField = ref<string | undefined>(props.defaultSortField);
|
|
188
|
+
const currentSortDirection = ref<'asc' | 'desc'>(props.defaultSortDirection ?? 'asc');
|
|
189
|
+
|
|
190
|
+
onMounted(() => {
|
|
191
|
+
// If defaultSortField points to a non-sortable column, ignore it
|
|
192
|
+
if (currentSortField.value) {
|
|
193
|
+
const col = props.columns?.find(c => c.fieldName === currentSortField.value);
|
|
194
|
+
if (!col || !isColumnSortable(col)) {
|
|
195
|
+
currentSortField.value = undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
refresh();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
watch([currentPage, () => props.data], async () => {
|
|
202
|
+
refresh();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
watch(() => currentPage.value, () => {
|
|
206
|
+
rowHeights.value = !rowRefs.value ? [] : rowRefs.value.map((el: HTMLElement) => el.offsetHeight);
|
|
207
|
+
columnWidths.value = !headerRefs.value ? [] : headerRefs.value.map((el: HTMLElement) => el.offsetWidth);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
watch([isLoading, () => props.isLoading], () => {
|
|
211
|
+
emit('update:tableLoading', isLoading.value || props.isLoading);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
watch([() => currentSortField.value, () => currentSortDirection.value], () => {
|
|
215
|
+
const needsPageReset = currentPage.value !== 1;
|
|
216
|
+
if (needsPageReset) {
|
|
217
|
+
currentPage.value = 1;
|
|
218
|
+
} else {
|
|
219
|
+
refresh();
|
|
220
|
+
}
|
|
221
|
+
emit('update:sortField', currentSortField.value);
|
|
222
|
+
emit('update:sortDirection', currentSortField.value ? currentSortDirection.value : undefined);
|
|
223
|
+
const field = currentSortField.value ?? null;
|
|
224
|
+
const direction = currentSortField.value ? currentSortDirection.value : null;
|
|
225
|
+
emit('sort-change', { field, direction });
|
|
226
|
+
}, { immediate: false });
|
|
96
227
|
|
|
97
228
|
const totalPages = computed(() => {
|
|
98
|
-
return Math.ceil(
|
|
229
|
+
return dataResult.value?.total ? Math.ceil(dataResult.value.total / props.pageSize) : 1;
|
|
99
230
|
});
|
|
100
231
|
|
|
101
232
|
const dataPage = computed(() => {
|
|
102
|
-
|
|
103
|
-
const end = start + props.pageSize;
|
|
104
|
-
return props.data.slice(start, end);
|
|
233
|
+
return dataResult.value?.data;
|
|
105
234
|
});
|
|
106
235
|
|
|
107
236
|
function switchPage(p: number) {
|
|
108
237
|
currentPage.value = p;
|
|
238
|
+
pageInput.value = p.toString();
|
|
109
239
|
}
|
|
110
240
|
|
|
111
|
-
const
|
|
112
|
-
'update:
|
|
241
|
+
const emit = defineEmits([
|
|
242
|
+
'update:tableLoading',
|
|
243
|
+
'update:sortField',
|
|
244
|
+
'update:sortDirection',
|
|
245
|
+
'sort-change',
|
|
246
|
+
'clickTableRow'
|
|
113
247
|
]);
|
|
114
248
|
|
|
249
|
+
function onPageInput(event: any) {
|
|
250
|
+
pageInput.value = event.target.innerText;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function validatePageInput() {
|
|
254
|
+
const newPage = parseInt(pageInput.value) || 1;
|
|
255
|
+
const validPage = Math.max(1, Math.min(newPage, totalPages.value));
|
|
256
|
+
currentPage.value = validPage;
|
|
257
|
+
pageInput.value = validPage.toString();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
watch(() => currentPage.value, (newPage) => {
|
|
261
|
+
pageInput.value = newPage.toString();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
async function onPageKeydown(event: any) {
|
|
265
|
+
// page input should accept only numbers, arrow keys and backspace
|
|
266
|
+
if (['Enter', 'Space'].includes(event.code) ||
|
|
267
|
+
(!['Backspace', 'ArrowRight', 'ArrowLeft'].includes(event.code)
|
|
268
|
+
&& isNaN(Number(String.fromCharCode(event.keyCode || 0))))) {
|
|
269
|
+
event.preventDefault();
|
|
270
|
+
if (event.code === 'Enter') {
|
|
271
|
+
validatePageInput();
|
|
272
|
+
event.target.blur();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function refresh() {
|
|
278
|
+
if (typeof props.data === 'function') {
|
|
279
|
+
isLoading.value = true;
|
|
280
|
+
const currentLoadingIndex = currentPage.value;
|
|
281
|
+
isAtLeastOneLoading.value[currentLoadingIndex] = true;
|
|
282
|
+
const result = await props.data({
|
|
283
|
+
offset: (currentLoadingIndex - 1) * props.pageSize,
|
|
284
|
+
limit: props.pageSize,
|
|
285
|
+
sortField: currentSortField.value,
|
|
286
|
+
...(currentSortField.value ? { sortDirection: currentSortDirection.value } : {}),
|
|
287
|
+
});
|
|
288
|
+
isAtLeastOneLoading.value[currentLoadingIndex] = false;
|
|
289
|
+
if (isAtLeastOneLoading.value.every(v => v === false)) {
|
|
290
|
+
isLoading.value = false;
|
|
291
|
+
}
|
|
292
|
+
dataResult.value = result;
|
|
293
|
+
} else if (typeof props.data === 'object' && Array.isArray(props.data)) {
|
|
294
|
+
const start = (currentPage.value - 1) * props.pageSize;
|
|
295
|
+
const end = start + props.pageSize;
|
|
296
|
+
const total = props.data.length;
|
|
297
|
+
const sorted = sortArrayData(props.data, currentSortField.value, currentSortDirection.value);
|
|
298
|
+
dataResult.value = { data: sorted.slice(start, end), total };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function refreshTable() {
|
|
303
|
+
if ( currentPage.value !== 1 ) {
|
|
304
|
+
currentPage.value = 1;
|
|
305
|
+
} else {
|
|
306
|
+
refresh();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isColumnSortable(col:{fieldName:string; sortable?:boolean}) {
|
|
311
|
+
// Sorting is controlled per column; default is NOT sortable. Enable with `sortable: true`.
|
|
312
|
+
return col.sortable === true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function onHeaderClick(col:{fieldName:string; sortable?:boolean}) {
|
|
316
|
+
if (!isColumnSortable(col)) return;
|
|
317
|
+
if (currentSortField.value !== col.fieldName) {
|
|
318
|
+
currentSortField.value = col.fieldName;
|
|
319
|
+
currentSortDirection.value = props.defaultSortDirection ?? 'asc';
|
|
320
|
+
} else {
|
|
321
|
+
currentSortDirection.value =
|
|
322
|
+
currentSortDirection.value === 'asc' ? 'desc' :
|
|
323
|
+
currentSortField.value ? (currentSortField.value = undefined, props.defaultSortDirection ?? 'asc') :
|
|
324
|
+
'asc';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getAriaSort(col:{fieldName:string; sortable?:boolean}) {
|
|
329
|
+
if (!isColumnSortable(col)) return undefined;
|
|
330
|
+
if (currentSortField.value !== col.fieldName) return 'none';
|
|
331
|
+
return currentSortDirection.value === 'asc' ? 'ascending' : 'descending';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
|
335
|
+
|
|
336
|
+
function sortArrayData(data:any[], sortField?:string, dir:'asc'|'desc'='asc') {
|
|
337
|
+
if (!sortField) return data;
|
|
338
|
+
// Helper function to get nested properties by path
|
|
339
|
+
const getByPath = (o:any, p:string) => p.split('.').reduce((a:any,k)=>a?.[k], o);
|
|
340
|
+
return [...data].sort((a,b) => {
|
|
341
|
+
const av = getByPath(a, sortField), bv = getByPath(b, sortField);
|
|
342
|
+
// Handle null/undefined values
|
|
343
|
+
if (av == null && bv == null) return 0;
|
|
344
|
+
// Handle null/undefined values
|
|
345
|
+
if (av == null) return 1; if (bv == null) return -1;
|
|
346
|
+
// Data types
|
|
347
|
+
if (av instanceof Date && bv instanceof Date) return dir === 'asc' ? av.getTime() - bv.getTime() : bv.getTime() - av.getTime();
|
|
348
|
+
// Strings and numbers
|
|
349
|
+
if (typeof av === 'number' && typeof bv === 'number') return dir === 'asc' ? av - bv : bv - av;
|
|
350
|
+
const cmp = collator.compare(String(av), String(bv));
|
|
351
|
+
return dir === 'asc' ? cmp : -cmp;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
115
354
|
|
|
355
|
+
function tableRowClick(row) {
|
|
356
|
+
emit("clickTableRow", row)
|
|
357
|
+
}
|
|
116
358
|
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<textarea
|
|
4
|
+
ref="input"
|
|
5
|
+
class="bg-lightInputBackground border border-lightInputBorder text-lightInputText placeholder-lightInputPlaceholderText text-sm rounded-lg block w-full p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder dark:placeholder-darkInputPlaceholderText dark:text-darkInputText dark:border-darkInputBorder focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder"
|
|
6
|
+
:placeholder="placeholder"
|
|
7
|
+
:value="modelValue"
|
|
8
|
+
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
|
9
|
+
:readonly="readonly"
|
|
10
|
+
/>
|
|
11
|
+
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
|
|
16
|
+
import { ref } from 'vue';
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
modelValue: string,
|
|
20
|
+
readonly?: boolean,
|
|
21
|
+
placeholder?: string,
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
const input = ref<HTMLInputElement | null>(null)
|
|
25
|
+
|
|
26
|
+
defineExpose({
|
|
27
|
+
focus: () => input.value?.focus(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
</script>
|
|
31
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<label class="inline-flex items-center cursor-pointer bor" :class="{'opacity-50' : props.disabled}">
|
|
3
|
+
<input :id="id"
|
|
4
|
+
type="checkbox"
|
|
5
|
+
value="" class="sr-only peer"
|
|
6
|
+
:disabled="props.disabled"
|
|
7
|
+
:checked="props.modelValue"
|
|
8
|
+
@change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
|
|
9
|
+
>
|
|
10
|
+
<div class="afcl-toggle border border-lightToggleBorderUnactive relative min-w-11 min-h-6 bg-lightToggleBgUnactive peer-focus:outline-none peer-focus:ring-4
|
|
11
|
+
peer-focus:ring-lightToggleRing dark:peer-focus:ring-darkToggleRing rounded-full peer dark:bg-darkToggleBgUnactive
|
|
12
|
+
peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:top-[2px] peer-checked:border-none
|
|
13
|
+
peer-checked:after:border-lightToggleBorderActive after:content-[''] after:absolute after:top-[1px]
|
|
14
|
+
after:start-[2px] after:bg-lightToggleCircleUnactive peer-checked:after:bg-lightToggleCircleActive dark:after:bg-darkToggleCircleUnactive after:border-lightToggleBgUnactive dark:after:border-darkToggleBgUnactive after:border after:rounded-full
|
|
15
|
+
after:h-5 after:w-5 after:transition-all dark:border-darkToggleBorderUnactive peer-checked:bg-lightToggleBgActive
|
|
16
|
+
dark:peer-checked:bg-darkToggleBgActive dark:peer-checked:after:border-darkToggleBorderActive dark:peer-checked:after:bg-darkToggleCircleActive">
|
|
17
|
+
</div>
|
|
18
|
+
<label :for="id" class="cursor-pointer ms-3 text-sm font-medium text-lightToggleText dark:text-darkToggleText">
|
|
19
|
+
<slot></slot>
|
|
20
|
+
</label>
|
|
21
|
+
</label>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
const props = defineProps({
|
|
26
|
+
modelValue: Boolean,
|
|
27
|
+
disabled: Boolean,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
defineEmits(['update:modelValue']);
|
|
31
|
+
const id = `afcb-${Math.random().toString(36).substring(7)}`
|
|
32
|
+
</script>
|
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div ref="triggerEl" class="afcl-tooltip inline-flex items-center">
|
|
2
|
+
<div ref="triggerEl" class="afcl-tooltip inline-flex items-center" @mouseenter="mouseOn" @mouseleave="mouseOff">
|
|
3
3
|
<slot></slot>
|
|
4
4
|
</div>
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
<teleport to="body" v-if="showTooltip">
|
|
6
|
+
<div
|
|
7
|
+
role="tooltip"
|
|
8
|
+
class="absolute z-[100] invisible inline-block px-3 py-2 text-sm font-medium text-lightTooltipText dark:darkTooltipText transition-opacity duration-300 bg-lightTooltipBackground rounded-lg shadow-sm opacity-0 tooltip dark:bg-darkTooltipBackground"
|
|
9
|
+
ref="tooltip"
|
|
10
|
+
>
|
|
11
|
+
<slot name="tooltip"></slot>
|
|
12
|
+
<div class="tooltip-arrow absolute -top-2" data-popper-arrow>
|
|
13
|
+
<div class="absolute top-0 -left-0.5 w-0 h-0 border-l-8 border-r-8 border-b-8 border-l-transparent border-r-transparent border-b-lightTooltipBackground dark:border-b-darkTooltipBackground"></div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</teleport>
|
|
14
17
|
</template>
|
|
15
18
|
|
|
16
19
|
<script setup lang="ts">
|
|
17
|
-
import { ref,
|
|
20
|
+
import { ref, nextTick, type Ref } from 'vue';
|
|
18
21
|
import { Tooltip } from 'flowbite';
|
|
19
22
|
|
|
20
23
|
const triggerEl = ref(null);
|
|
21
24
|
const tooltip = ref(null);
|
|
22
|
-
|
|
25
|
+
const showTooltip = ref(false);
|
|
23
26
|
const tp: Ref<Tooltip|null> = ref(null);
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
async function mouseOn() {
|
|
29
|
+
showTooltip.value = true;
|
|
27
30
|
await nextTick();
|
|
31
|
+
|
|
32
|
+
tp.value?.destroy();
|
|
33
|
+
|
|
28
34
|
tp.value = new Tooltip(
|
|
29
35
|
tooltip.value,
|
|
30
36
|
triggerEl.value,
|
|
@@ -33,11 +39,15 @@ onMounted(async () => {
|
|
|
33
39
|
triggerType: 'hover',
|
|
34
40
|
},
|
|
35
41
|
);
|
|
36
|
-
})
|
|
37
42
|
|
|
43
|
+
tp.value.show();
|
|
44
|
+
}
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
function mouseOff() {
|
|
47
|
+
tp.value?.hide();
|
|
41
48
|
tp.value?.destroy();
|
|
42
|
-
|
|
49
|
+
tp.value = null;
|
|
50
|
+
showTooltip.value = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
</script>
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="md:flex">
|
|
3
|
-
<ul class="flex-column space-y space-y-4 text-sm font-medium text-
|
|
3
|
+
<ul class="ps-6 flex-column space-y space-y-4 text-sm font-medium text-lightVerticalTabsText dark:text-darkVerticalTabsText md:me-4 mb-4 md:mb-0 md:mr-0 mr-6">
|
|
4
4
|
<li v-for="tab in tabs" :key="`${tab}-tab-controll`">
|
|
5
5
|
<a
|
|
6
6
|
href="#"
|
|
7
|
-
@click="
|
|
7
|
+
@click="setActiveTab(tab)"
|
|
8
8
|
class="inline-flex items-center px-4 py-3 rounded-lg w-full"
|
|
9
|
-
:class="tab === activeTab ? 'text-
|
|
9
|
+
:class="tab === activeTab ? 'text-lightVerticalTabsTextActive bg-lightVerticalTabsBackgroundActive active dark:bg-darkVerticalTabsBackgroundActive dark:text-darkVerticalTabsTextActive' : 'text-lightVerticalTabsText dark:text-darkVerticalTabsText hover:text-lightVerticalTabsTextHover bg-lightVerticalTabsBackground hover:bg-lightVerticalTabsBackgroundHover dark:bg-darkVerticalTabsBackground dark:hover:bg-darkVerticalTabsBackgroundHover dark:hover:darkVerticalTabsTextHover'"
|
|
10
10
|
aria-current="page"
|
|
11
11
|
>
|
|
12
12
|
<slot :name="`tab:${tab}`"></slot>
|
|
13
13
|
</a>
|
|
14
14
|
</li>
|
|
15
15
|
</ul>
|
|
16
|
-
<div class="ps-6
|
|
16
|
+
<div class="ps-6 text-medium text-lightVerticalTabsSlotText dark:text-darkVerticalTabsSlotText w-full ">
|
|
17
17
|
<slot :name="activeTab"></slot>
|
|
18
18
|
</div>
|
|
19
19
|
</div>
|
|
20
20
|
</template>
|
|
21
21
|
|
|
22
22
|
<script setup lang="ts">
|
|
23
|
-
|
|
23
|
+
import { onMounted, useSlots, ref, type Ref } from 'vue';
|
|
24
24
|
const tabs : Ref<string[]> = ref([]);
|
|
25
25
|
const activeTab = ref('');
|
|
26
26
|
const props = defineProps({
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
const emites = defineEmits([
|
|
32
32
|
'update:activeTab',
|
|
33
33
|
]);
|
|
34
|
+
|
|
35
|
+
defineExpose({
|
|
36
|
+
setActiveTab
|
|
37
|
+
});
|
|
38
|
+
|
|
34
39
|
onMounted(() => {
|
|
35
40
|
const slots = useSlots();
|
|
36
41
|
tabs.value = Object.keys(slots).reduce((tbs: string[], tb: string) => {
|
|
@@ -44,6 +49,10 @@
|
|
|
44
49
|
}
|
|
45
50
|
});
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
function setActiveTab(tab: string) {
|
|
53
|
+
if (tabs.value.includes(tab)) {
|
|
54
|
+
activeTab.value = tab;
|
|
55
|
+
emites('update:activeTab', tab);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
49
58
|
</script>
|
|
@@ -19,6 +19,9 @@ export { default as Skeleton } from './Skeleton.vue';
|
|
|
19
19
|
export { default as Dialog } from './Dialog.vue';
|
|
20
20
|
export { default as MixedChart } from './MixedChart.vue';
|
|
21
21
|
export { default as CountryFlag } from './CountryFlag.vue';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
export { default as JsonViewer } from './JsonViewer.vue';
|
|
23
|
+
export { default as Toggle } from './Toggle.vue';
|
|
24
|
+
export { default as DatePicker } from './DatePicker.vue';
|
|
25
|
+
export { default as Textarea } from './Textarea.vue';
|
|
26
|
+
export { default as ButtonGroup } from './ButtonGroup.vue';
|
|
27
|
+
export { default as Card } from './Card.vue';
|