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
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<aside
|
|
3
|
+
ref="sidebarAside"
|
|
4
|
+
@mouseover="!isTogglingSidebar && (isSidebarHovering = true)"
|
|
5
|
+
@mouseleave="!isTogglingSidebar && (isSidebarHovering = false)"
|
|
6
|
+
id="logo-lightSidebar"
|
|
7
|
+
class="sidebar-container fixed border-none top-0 left-0 z-40 h-screen transition-all duration-300 ease-in-out bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder sm:translate-x-0 dark:border-darkSidebarBorder"
|
|
8
|
+
:class="{
|
|
9
|
+
'-translate-x-full': !sideBarOpen,
|
|
10
|
+
'transform-none': sideBarOpen,
|
|
11
|
+
'sidebar-collapsed': iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering,
|
|
12
|
+
'sidebar-expanded': !iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)
|
|
13
|
+
}"
|
|
14
|
+
aria-label="Sidebar"
|
|
15
|
+
>
|
|
16
|
+
<div class="h-full px-3 pb-20 md:pb-4 bg-lightSidebar dark:bg-darkSidebar border-r border-lightSidebarBorder dark:border-darkSidebarBorder" :class="{'sidebar-scroll':!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
|
|
17
|
+
<div class="af-logo-title-wrapper flex relative transition-all duration-300 ease-in-out h-8 items-center" :class="{'my-4 ': isSidebarIconOnly && !isSidebarHovering, 'm-4': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering)}">
|
|
18
|
+
<img :src="loadFile(coreStore.config?.brandLogo || '@/assets/logo.svg')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-logo h-8 me-3" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))) }" />
|
|
19
|
+
<img :src="loadFile(coreStore.config?.iconOnlySidebar?.logo || '')" :alt="`${ coreStore.config?.brandName } Logo`" class="af-sidebar-icon-only-logo h-8 me-3" :class="{ 'hidden': !(coreStore.config?.showBrandLogoInSidebar !== false && coreStore.config?.iconOnlySidebar?.logo && iconOnlySidebarEnabled && isSidebarIconOnly && !isSidebarHovering) }" />
|
|
20
|
+
<span
|
|
21
|
+
v-if="coreStore.config?.showBrandNameInSidebar && (!iconOnlySidebarEnabled || !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))"
|
|
22
|
+
class="af-title self-center text-lightNavbarText-size font-semibold sm:text-lightNavbarText-size whitespace-nowrap dark:text-darkSidebarText text-lightSidebarText"
|
|
23
|
+
>
|
|
24
|
+
{{ coreStore.config?.brandName }}
|
|
25
|
+
</span>
|
|
26
|
+
<div class="flex items-center gap-2 w-auto" :class="{'w-full justify-end': coreStore.config?.showBrandLogoInSidebar === false}">
|
|
27
|
+
<component
|
|
28
|
+
v-for="c in coreStore?.config?.globalInjections?.sidebarTop || []"
|
|
29
|
+
:is="getCustomComponent(c)"
|
|
30
|
+
:meta="c.meta"
|
|
31
|
+
:adminUser="coreStore.adminUser"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="absolute top-1.5 -right-4 z-10 hidden sm:block" v-if="!forceIconOnly && iconOnlySidebarEnabled && (!isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering))">
|
|
35
|
+
<button class="text-sm text-lightSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" @click="toggleSidebar">
|
|
36
|
+
<IconCloseSidebarSolid v-if="!isSidebarIconOnly" class="w-5 h-5 active:scale-95 transition-all duration-200 hover:text-lightSidebarIconsHover dark:hover:text-darkSidebarIconsHover" />
|
|
37
|
+
<IconOpenSidebarSolid v-else class="w-5 h-5 active:scale-95 transition-all duration-200 hover:text-lightSidebarIconsHover dark:hover:text-darkSidebarIconsHover" />
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<ul class="af-sidebar-container space-y-2 font-medium" >
|
|
43
|
+
<template v-if="!iconOnlySidebarEnabled || !isSidebarIconOnly" v-for="(item, i) in coreStore.menu" :key="`menu-${i}`">
|
|
44
|
+
<div v-if="item.type === 'divider'" class="border-t border-lightSidebarDevider dark:border-darkSidebarDevider"></div>
|
|
45
|
+
<div v-else-if="item.type === 'gap'" class="flex items-center justify-center h-8"></div>
|
|
46
|
+
<div v-else-if="item.type === 'heading'" class="flex items-center justify-left pl-2 h-8 text-lightSidebarHeading dark:text-darkSidebarHeading
|
|
47
|
+
">{{ item.label }}</div>
|
|
48
|
+
<li v-else-if="item.children" class="af-sidebar-expand-container">
|
|
49
|
+
<button @click="clickOnMenuItem(i)" type="button" class="af-sidebar-expand-button flex items-center w-full px-3.5 py-2 text-base text-lightSidebarText rounded-default transition duration-75 group hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:text-darkSidebarText dark:hover:bg-darkSidebarHover dark:hover:text-darkSidebarTextHover"
|
|
50
|
+
:class="opened.includes(i) ? 'af-sidebar-dropdown-expanded' : 'af-sidebar-dropdown-collapsed'"
|
|
51
|
+
:aria-controls="`dropdown-example${i}`"
|
|
52
|
+
:data-collapse-toggle="`dropdown-example${i}`"
|
|
53
|
+
>
|
|
54
|
+
<component v-if="item.icon" :is="getIcon(item.icon)" class="w-5 h-5 text-lightSidebarIcons group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
|
|
55
|
+
<span class="overflow-hidden flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">{{ item.label }}
|
|
56
|
+
<span v-if="item.badge" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
57
|
+
fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
|
|
58
|
+
<Tooltip v-if="item.badgeTooltip">
|
|
59
|
+
{{ item.badge }}
|
|
60
|
+
<template #tooltip>
|
|
61
|
+
{{ item.badgeTooltip }}
|
|
62
|
+
</template>
|
|
63
|
+
</Tooltip>
|
|
64
|
+
<template v-else>
|
|
65
|
+
{{ item.badge }}
|
|
66
|
+
</template>
|
|
67
|
+
</span>
|
|
68
|
+
</span>
|
|
69
|
+
|
|
70
|
+
<svg :class="{'rotate-180': opened.includes(i) }" class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
|
71
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
|
72
|
+
</svg>
|
|
73
|
+
</button>
|
|
74
|
+
|
|
75
|
+
<ul :id="`dropdown-example${i}`" role="none" class="af-sidebar-dropdown pt-1 space-y-1" :class="{ 'hidden': !opened.includes(i) }">
|
|
76
|
+
<template v-for="(child, j) in item.children" :key="`menu-${i}-${j}`">
|
|
77
|
+
<li class="af-sidebar-menu-link">
|
|
78
|
+
<MenuLink :item="child" isChild="true" @click="$emit('hideSidebar')"/>
|
|
79
|
+
</li>
|
|
80
|
+
</template>
|
|
81
|
+
</ul>
|
|
82
|
+
</li>
|
|
83
|
+
<li v-else class="af-sidebar-menu-link">
|
|
84
|
+
<MenuLink :item="item" @click="$emit('hideSidebar')"/>
|
|
85
|
+
</li>
|
|
86
|
+
</template>
|
|
87
|
+
<template v-if="iconOnlySidebarEnabled && isSidebarIconOnly" v-for="(item, i) in coreStore.menu" :key="`menu-${i}`">
|
|
88
|
+
<div v-if="item.type === 'divider'" class="border-t border-lightSidebarDevider dark:border-darkSidebarDevider"></div>
|
|
89
|
+
<div v-else-if="item.type === 'gap'" class="flex items-center justify-center h-8"></div>
|
|
90
|
+
<div v-else-if="item.type === 'heading' && isSidebarHovering" class="flex items-center justify-left pl-2 h-8 text-lightSidebarHeading dark:text-darkSidebarHeading
|
|
91
|
+
">{{ item.label }}</div>
|
|
92
|
+
<div v-else-if="item.type === 'heading' && !isSidebarHovering" class="opacity-0 w-1 h-8">{{ item.label }}</div>
|
|
93
|
+
<li v-else-if="item.children" class="af-sidebar-expand-container">
|
|
94
|
+
<button @click="clickOnMenuItem(i)" type="button" class="af-sidebar-expand-button relative flex items-center h-10 w-full px-3.5 py-2 text-base text-lightSidebarText rounded-default group hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:text-darkSidebarText dark:hover:bg-darkSidebarHover dark:hover:text-darkSidebarTextHover"
|
|
95
|
+
:class="opened.includes(i) ? 'af-sidebar-dropdown-expanded' : 'af-sidebar-dropdown-collapsed'"
|
|
96
|
+
:aria-controls="`dropdown-example${i}`"
|
|
97
|
+
:data-collapse-toggle="`dropdown-example${i}`"
|
|
98
|
+
>
|
|
99
|
+
<component v-if="item.icon" :is="getIcon(item.icon)" class="min-w-5 min-h-5 text-lightSidebarIcons group-hover:text-lightSidebarIconsHover transition duration-75 dark:group-hover:text-darkSidebarIconsHover dark:text-darkSidebarIcons" ></component>
|
|
100
|
+
|
|
101
|
+
<span
|
|
102
|
+
class="overflow-hidden block ms-3 text-left rtl:text-right transition-all duration-200 ease-in-out"
|
|
103
|
+
:class="{
|
|
104
|
+
'opacity-0 ms-0 translate-x-4 flex-none': isSidebarIconOnly && !isSidebarHovering,
|
|
105
|
+
'opacity-100 ms-3 translate-x-0 flex-none': isSidebarIconOnly && isSidebarHovering,
|
|
106
|
+
'opacity-100 ms-3 translate-x-0 flex-1': !isSidebarIconOnly
|
|
107
|
+
}"
|
|
108
|
+
:style="isSidebarIconOnly ? {
|
|
109
|
+
minWidth: 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)',
|
|
110
|
+
width: 'calc(16.5rem - 0.75rem*2 - 0.875rem*2 - 1.25rem - 0.75rem)'
|
|
111
|
+
} : {}"
|
|
112
|
+
>{{ item.label }}
|
|
113
|
+
|
|
114
|
+
<span v-if="item.badge" class="inline-flex items-center justify-center w-3 h-3 p-3 ms-3 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
115
|
+
fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent">
|
|
116
|
+
<Tooltip v-if="item.badgeTooltip">
|
|
117
|
+
{{ item.badge }}
|
|
118
|
+
<template #tooltip>
|
|
119
|
+
{{ item.badgeTooltip }}
|
|
120
|
+
</template>
|
|
121
|
+
</Tooltip>
|
|
122
|
+
<template v-else>
|
|
123
|
+
{{ item.badge }}
|
|
124
|
+
</template>
|
|
125
|
+
</span>
|
|
126
|
+
</span>
|
|
127
|
+
|
|
128
|
+
<svg :class="{'rotate-180': opened.includes(i) }" class="w-3 h-3 ml-2 absolute right-3.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
|
129
|
+
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
|
130
|
+
</svg>
|
|
131
|
+
</button>
|
|
132
|
+
|
|
133
|
+
<ul :id="`dropdown-example${i}`" role="none" class="af-sidebar-dropdown pt-1 space-y-1" :class="{ 'hidden': !opened.includes(i), 'relative after:absolute after:-left-3 after:inset-y-0 after:w-0.5 after:bg-lightSidebarIcons/50 dark:after:bg-darkSidebarIcons/50 after:rounded-full': isSidebarIconOnly && !isSidebarHovering && opened.includes(i) }">
|
|
134
|
+
<template v-for="(child, j) in item.children" :key="`menu-${i}-${j}`">
|
|
135
|
+
<li class="af-sidebar-menu-link">
|
|
136
|
+
<MenuLink :item="child" isChild="true" @click="$emit('hideSidebar')" :isSidebarIconOnly="isSidebarIconOnly" :isSidebarHovering="isSidebarHovering"/>
|
|
137
|
+
</li>
|
|
138
|
+
</template>
|
|
139
|
+
</ul>
|
|
140
|
+
</li>
|
|
141
|
+
<li v-else class="af-sidebar-menu-link">
|
|
142
|
+
<MenuLink :item="item" @click="$emit('hideSidebar')" :isSidebarIconOnly="isSidebarIconOnly" :isSidebarHovering="isSidebarHovering"/>
|
|
143
|
+
</li>
|
|
144
|
+
</template>
|
|
145
|
+
</ul>
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
<div id="dropdown-cta" class="p-4 mt-6 w-[230px] rounded-lg bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
149
|
+
fill-lightAnnouncementText dark:fill-darkAccent text-lightAnnouncementText dark:text-darkAccent text-sm" role="alert"
|
|
150
|
+
v-if="(ctaBadge && !isSidebarIconOnly) || (ctaBadge && isSidebarIconOnly && isSidebarHovering)"
|
|
151
|
+
>
|
|
152
|
+
<div class="flex items-center mb-3" :class="!ctaBadge.title ? 'float-right' : ''">
|
|
153
|
+
<!-- <span class="bg-lightPrimaryOpacity dark:bg-darkPrimaryOpacity text-sm font-semibold me-2 px-2.5 py-0.5 rounded "
|
|
154
|
+
v-if="ctaBadge.title"
|
|
155
|
+
> -->
|
|
156
|
+
<span>
|
|
157
|
+
{{ctaBadge.title}}
|
|
158
|
+
</span>
|
|
159
|
+
<button type="button"
|
|
160
|
+
class="ms-auto -mx-1.5 -my-1.5 bg-lightPrimaryOpacity dark:bg-darkPrimaryOpacity inline-flex justify-center items-center w-6 h-6 rounded-lg p-1 hover:brightness-110"
|
|
161
|
+
|
|
162
|
+
data-dismiss-target="#dropdown-cta" aria-label="Close"
|
|
163
|
+
v-if="ctaBadge?.closable" @click="closeCTA"
|
|
164
|
+
>
|
|
165
|
+
<span class="sr-only">Close</span>
|
|
166
|
+
<svg class="w-2.5 h-2.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
|
167
|
+
<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"/>
|
|
168
|
+
</svg>
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
<p class="mb-3 text-sm " v-if="ctaBadge.html" v-html="ctaBadge.html"></p>
|
|
172
|
+
<p class="mb-3 text-sm fill-lightNavbarText dark:fill-darkPrimary text-lightNavbarText dark:text-darkNavbarPrimary" v-else>
|
|
173
|
+
{{ ctaBadge.text }}
|
|
174
|
+
</p>
|
|
175
|
+
<!-- <a class="text-sm text-lightPrimary underline font-medium hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" href="#">Turn new navigation off</a> -->
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<component
|
|
179
|
+
v-for="c in coreStore?.config?.globalInjections?.sidebar || []"
|
|
180
|
+
:is="getCustomComponent(c)"
|
|
181
|
+
:meta="c.meta"
|
|
182
|
+
:adminUser="coreStore.adminUser"
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
</aside>
|
|
186
|
+
</template>
|
|
187
|
+
|
|
188
|
+
<style lang="scss" scoped>
|
|
189
|
+
/* Sidebar width animations */
|
|
190
|
+
.sidebar-container {
|
|
191
|
+
width: 16.5rem; /* Default expanded width (w-64) */
|
|
192
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
193
|
+
overflow: hidden; /* Prevent content from showing during animation */
|
|
194
|
+
will-change: width, transform;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.sidebar-collapsed {
|
|
198
|
+
width: 4.5rem; /* Collapsed width (w-18) */
|
|
199
|
+
box-shadow: 12px 0px 18px -8px rgba(0, 0, 0, 0.15);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.sidebar-expanded {
|
|
203
|
+
width: 16.5rem; /* Expanded width (w-64) */
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
:deep(.dark) .sidebar-collapsed {
|
|
207
|
+
box-shadow: 12px 0px 18px -8px rgba(0, 0, 0, 0.45);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/* Text visibility transitions */
|
|
211
|
+
.sidebar-collapsed .af-title {
|
|
212
|
+
opacity: 0;
|
|
213
|
+
transform: translateX(-12px);
|
|
214
|
+
transition: opacity 0.2s ease-out, transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
215
|
+
}
|
|
216
|
+
.sidebar-collapsed svg.w-3 {
|
|
217
|
+
opacity: 0;
|
|
218
|
+
transition: opacity 0.2s ease-out;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.sidebar-expanded .af-title {
|
|
222
|
+
opacity: 1;
|
|
223
|
+
transform: translateX(0);
|
|
224
|
+
transition: opacity 0.25s ease-in 0.1s, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1) 0.05s;
|
|
225
|
+
}
|
|
226
|
+
.sidebar-expanded svg.w-3 {
|
|
227
|
+
opacity: 1;
|
|
228
|
+
transition: opacity 0.25s ease-in 0.1s;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Overlay scrollbar styling */
|
|
232
|
+
.sidebar-scroll {
|
|
233
|
+
overflow-y: auto;
|
|
234
|
+
overflow-x: hidden;
|
|
235
|
+
scrollbar-width: thin;
|
|
236
|
+
scrollbar-color: transparent transparent;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* Webkit overlay scrollbar */
|
|
240
|
+
.sidebar-scroll::-webkit-scrollbar {
|
|
241
|
+
width: 8px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.sidebar-scroll::-webkit-scrollbar-track {
|
|
245
|
+
background: transparent;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.sidebar-scroll::-webkit-scrollbar-thumb {
|
|
249
|
+
background-color: transparent;
|
|
250
|
+
border-radius: 4px;
|
|
251
|
+
transition: background-color 0.2s ease;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* Show scrollbar on hover/scroll */
|
|
255
|
+
.sidebar-scroll:hover::-webkit-scrollbar-thumb,
|
|
256
|
+
.sidebar-scroll:active::-webkit-scrollbar-thumb {
|
|
257
|
+
background-color: rgba(156, 163, 175, 0.4);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.sidebar-scroll:hover {
|
|
261
|
+
scrollbar-color: rgba(156, 163, 175, 0.4) transparent;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Dark mode scrollbar */
|
|
265
|
+
.dark .sidebar-scroll:hover {
|
|
266
|
+
scrollbar-color: rgba(75, 85, 99, 0.4) transparent;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.dark .sidebar-scroll:hover::-webkit-scrollbar-thumb,
|
|
270
|
+
.dark .sidebar-scroll:active::-webkit-scrollbar-thumb {
|
|
271
|
+
background-color: rgba(75, 85, 99, 0.4);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* For browsers that support overlay scrollbars natively */
|
|
275
|
+
@supports (overflow: overlay) {
|
|
276
|
+
.sidebar-scroll {
|
|
277
|
+
overflow-y: overlay;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
</style>
|
|
281
|
+
|
|
282
|
+
<script setup lang="ts">
|
|
283
|
+
import { computed, ref, watch, nextTick, onMounted, onUnmounted, type Ref } from 'vue';
|
|
284
|
+
import { useCoreStore } from '@/stores/core';
|
|
285
|
+
import MenuLink from './MenuLink.vue';
|
|
286
|
+
import { IconCloseSidebarSolid, IconOpenSidebarSolid } from '@iconify-prerendered/vue-flowbite';
|
|
287
|
+
import { getIcon, verySimpleHash, loadFile, getCustomComponent } from '@/utils';
|
|
288
|
+
import { Tooltip } from '@/afcl';
|
|
289
|
+
import type { AnnouncementBadgeResponse } from '@/types/Common';
|
|
290
|
+
import adminforth from '@/adminforth';
|
|
291
|
+
|
|
292
|
+
interface Props {
|
|
293
|
+
sideBarOpen: boolean;
|
|
294
|
+
forceIconOnly?: boolean;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const props = defineProps<Props>();
|
|
298
|
+
|
|
299
|
+
const emit = defineEmits<{
|
|
300
|
+
hideSidebar: [];
|
|
301
|
+
loadMenu: [];
|
|
302
|
+
sidebarStateChange: [{ isSidebarIconOnly: boolean; isSidebarHovering: boolean }];
|
|
303
|
+
}>();
|
|
304
|
+
|
|
305
|
+
const coreStore = useCoreStore();
|
|
306
|
+
|
|
307
|
+
//create a ref to store the opened menu items with ts type;
|
|
308
|
+
const opened = ref<(string|number)[]>([]);
|
|
309
|
+
const sidebarAside = ref(null);
|
|
310
|
+
|
|
311
|
+
const smQuery = window.matchMedia('(min-width: 640px)');
|
|
312
|
+
const isMobile = ref(!smQuery.matches);
|
|
313
|
+
const iconOnlySidebarEnabled = computed(() => props.forceIconOnly === true || coreStore.config?.iconOnlySidebar?.enabled !== false);
|
|
314
|
+
const isSidebarIconOnly = ref(false);
|
|
315
|
+
|
|
316
|
+
function handleBreakpointChange(e: MediaQueryListEvent) {
|
|
317
|
+
isMobile.value = !e.matches;
|
|
318
|
+
if (isMobile.value) {
|
|
319
|
+
isSidebarIconOnly.value = false;
|
|
320
|
+
} else {
|
|
321
|
+
if (props.forceIconOnly === true) {
|
|
322
|
+
isSidebarIconOnly.value = true;
|
|
323
|
+
} else if (iconOnlySidebarEnabled.value && localStorage.getItem('afIconOnlySidebar') === 'true') {
|
|
324
|
+
isSidebarIconOnly.value = true;
|
|
325
|
+
} else {
|
|
326
|
+
isSidebarIconOnly.value = false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
smQuery.addEventListener('change', handleBreakpointChange);
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
const isSidebarHovering = ref(false);
|
|
335
|
+
const isTogglingSidebar = ref(false);
|
|
336
|
+
|
|
337
|
+
function toggleSidebar() {
|
|
338
|
+
if (props.forceIconOnly) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (!iconOnlySidebarEnabled.value) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (isMobile.value) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
isTogglingSidebar.value = true;
|
|
348
|
+
isSidebarIconOnly.value = !isSidebarIconOnly.value;
|
|
349
|
+
if (isSidebarIconOnly.value) {
|
|
350
|
+
isSidebarHovering.value = false;
|
|
351
|
+
}
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
isTogglingSidebar.value = false;
|
|
354
|
+
}, 100);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function clickOnMenuItem(label: string | number) {
|
|
358
|
+
if (opened.value.includes(label)) {
|
|
359
|
+
opened.value = opened.value.filter((item) => item !== label);
|
|
360
|
+
} else {
|
|
361
|
+
opened.value.push(label);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
watch(()=>coreStore.menu, () => {
|
|
366
|
+
coreStore.menu.forEach((item, i) => {
|
|
367
|
+
if (item.open) {
|
|
368
|
+
opened.value.push(i);
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
watch(isSidebarIconOnly, (isIconOnly) => {
|
|
376
|
+
if (!isMobile.value && iconOnlySidebarEnabled.value && !props.forceIconOnly) {
|
|
377
|
+
localStorage.setItem('afIconOnlySidebar', isIconOnly.toString());
|
|
378
|
+
}
|
|
379
|
+
emit('sidebarStateChange', { isSidebarIconOnly: isIconOnly, isSidebarHovering: isSidebarHovering.value });
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
watch(isSidebarHovering, (hovering) => {
|
|
383
|
+
emit('sidebarStateChange', { isSidebarIconOnly: isSidebarIconOnly.value, isSidebarHovering: hovering });
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
watch(sidebarAside, (sidebarAside) => {
|
|
387
|
+
if (sidebarAside) {
|
|
388
|
+
coreStore.fetchMenuBadges();
|
|
389
|
+
}
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
const ctaBadge: Ref<(AnnouncementBadgeResponse & { hash: string; }) | null> = computed(() => {
|
|
393
|
+
const badge = coreStore.config?.announcementBadge;
|
|
394
|
+
if (!badge) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const hash = badge.closable ? verySimpleHash(JSON.stringify(badge)) : '';
|
|
398
|
+
if (badge.closable && window.localStorage.getItem(`ctaBadge-${hash}`)) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return {...badge, hash};
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
function closeCTA() {
|
|
405
|
+
if (!ctaBadge.value) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const hash = ctaBadge.value.hash;
|
|
409
|
+
window.localStorage.setItem(`ctaBadge-${hash}`, '1');
|
|
410
|
+
nextTick( async() => {
|
|
411
|
+
emit('loadMenu');
|
|
412
|
+
await coreStore.fetchMenuBadges();
|
|
413
|
+
adminforth.menu.refreshMenuBadges();
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
onMounted(() => {
|
|
418
|
+
if (!iconOnlySidebarEnabled.value) {
|
|
419
|
+
isSidebarIconOnly.value = false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
coreStore.menu.forEach((item, i) => {
|
|
423
|
+
if (item.open) {
|
|
424
|
+
opened.value.push(i);
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
// Emit initial state
|
|
428
|
+
emit('sidebarStateChange', { isSidebarIconOnly: isSidebarIconOnly.value, isSidebarHovering: isSidebarHovering.value });
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
onUnmounted(() => {
|
|
432
|
+
smQuery.removeEventListener('change', handleBreakpointChange);
|
|
433
|
+
if (isMobile.value && props.sideBarOpen) {
|
|
434
|
+
document.body.style.overflow = '';
|
|
435
|
+
document.body.style.position = '';
|
|
436
|
+
document.body.style.width = '';
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
watch(() => props.forceIconOnly, (force) => {
|
|
441
|
+
if (isMobile.value) {
|
|
442
|
+
isSidebarIconOnly.value = false;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (props.forceIconOnly === true) {
|
|
446
|
+
isSidebarIconOnly.value = true;
|
|
447
|
+
} else if (iconOnlySidebarEnabled.value && localStorage.getItem('afIconOnlySidebar') === 'true') {
|
|
448
|
+
isSidebarIconOnly.value = true;
|
|
449
|
+
} else {
|
|
450
|
+
isSidebarIconOnly.value = false;
|
|
451
|
+
}
|
|
452
|
+
}, { immediate: true })
|
|
453
|
+
|
|
454
|
+
watch(() => props.sideBarOpen, (isOpen) => {
|
|
455
|
+
if (isMobile.value) {
|
|
456
|
+
if (isOpen) {
|
|
457
|
+
// Lock body scroll
|
|
458
|
+
document.body.style.overflow = 'hidden';
|
|
459
|
+
document.body.style.position = 'fixed';
|
|
460
|
+
document.body.style.width = '100%';
|
|
461
|
+
} else {
|
|
462
|
+
// Unlock body scroll
|
|
463
|
+
document.body.style.overflow = '';
|
|
464
|
+
document.body.style.position = '';
|
|
465
|
+
document.body.style.width = '';
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}, { immediate: true })
|
|
469
|
+
|
|
470
|
+
</script>
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
<div
|
|
3
3
|
role="status" class="max-w-sm animate-pulse"
|
|
4
4
|
>
|
|
5
|
-
<div class="h-2.5 bg-
|
|
6
|
-
<div class="h-2 bg-
|
|
7
|
-
<div class="h-2 bg-
|
|
8
|
-
<div class="h-2 bg-
|
|
9
|
-
<div class="h-2 bg-
|
|
10
|
-
<div class="h-2 bg-
|
|
5
|
+
<div class="h-2.5 bg-lightSkeletonBackgroundColor rounded-full dark:bg-darkSkeletonBackgroundColor w-48 mb-4"></div>
|
|
6
|
+
<div class="h-2 bg-lightSkeletonBackgroundColor rounded-full dark:bg-darkSkeletonBackgroundColor max-w-[360px] mb-2.5"></div>
|
|
7
|
+
<div class="h-2 bg-lightSkeletonBackgroundColor rounded-full dark:bg-darkSkeletonBackgroundColor mb-2.5"></div>
|
|
8
|
+
<div class="h-2 bg-lightSkeletonBackgroundColor rounded-full dark:bg-darkSkeletonBackgroundColor max-w-[330px] mb-2.5"></div>
|
|
9
|
+
<div class="h-2 bg-lightSkeletonBackgroundColor rounded-full dark:bg-darkSkeletonBackgroundColor max-w-[300px] mb-2.5"></div>
|
|
10
|
+
<div class="h-2 bg-lightSkeletonBackgroundColor rounded-full dark:bg-darkSkeletonBackgroundColor max-w-[360px]"></div>
|
|
11
11
|
<span class="sr-only">{{ $t('Loading...') }}</span>
|
|
12
12
|
</div>
|
|
13
13
|
</template>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
: '']"
|
|
13
13
|
>
|
|
14
14
|
<div role="status" class="max-w-sm animate-pulse">
|
|
15
|
-
<div class="h-2 bg-
|
|
15
|
+
<div class="h-2 bg-lightSkeletonBackgroundColor rounded-full dark:bg-darkSkeletonBackgroundColor max-w-[360px]"></div>
|
|
16
16
|
</div>
|
|
17
17
|
</td>
|
|
18
18
|
</tr>
|
|
@@ -26,8 +26,8 @@ const props = withDefaults(defineProps<{
|
|
|
26
26
|
rowHeights?: number[];
|
|
27
27
|
columnWidths?: number[];
|
|
28
28
|
}>(), {
|
|
29
|
-
rowHeights: [],
|
|
30
|
-
columnWidths: [],
|
|
29
|
+
rowHeights: () => [],
|
|
30
|
+
columnWidths: () => [],
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
</script>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<template >
|
|
2
|
-
<template v-if="threeDotsDropdownItems?.length || customActions?.length">
|
|
2
|
+
<template v-if="threeDotsDropdownItems?.length || customActions?.length || (bulkActions?.some((action: AdminForthBulkActionCommon) => action.showInThreeDotsDropdown))">
|
|
3
3
|
<button
|
|
4
4
|
data-dropdown-toggle="listThreeDotsDropdown"
|
|
5
|
-
class="flex items-center py-2 px-2 text-sm font-medium text-
|
|
5
|
+
class="flex items-center py-2 px-2 text-sm font-medium text-lightThreeDotsMenuIconDots focus:outline-none bg-lightThreeDotsMenuIconBackground rounded border border-lightThreeDotsMenuIconBackgroundBorder hover:bg-lightThreeDotsMenuIconBackgroundHover hover:text-lightThreeDotsMenuIconDotsHover focus:z-10 focus:ring-4 focus:ring-lightThreeDotsMenuIconFocus dark:focus:ring-darkThreeDotsMenuIconFocus dark:bg-darkThreeDotsMenuIconBackground dark:text-darkThreeDotsMenuIconDots dark:border-darkThreeDotsMenuIconBackgroundBorder dark:hover:text-darkThreeDotsMenuIconDotsHover dark:hover:bg-darkThreeDotsMenuIconBackgroundHover rounded-default"
|
|
6
6
|
>
|
|
7
7
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 4 15">
|
|
8
8
|
<path d="M3.5 1.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 6.041a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Zm0 5.959a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0Z"/>
|
|
@@ -12,26 +12,60 @@
|
|
|
12
12
|
<!-- Dropdown menu -->
|
|
13
13
|
<div
|
|
14
14
|
id="listThreeDotsDropdown"
|
|
15
|
-
class="z-
|
|
16
|
-
<ul class="py-2 text-sm text-
|
|
17
|
-
<li v-for="item in threeDotsDropdownItems" :key="`dropdown-item-${
|
|
18
|
-
<a
|
|
19
|
-
|
|
15
|
+
class="z-30 hidden bg-lightThreeDotsMenuBodyBackground divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-darkThreeDotsMenuBodyBackground dark:divide-gray-600">
|
|
16
|
+
<ul class="py-2 text-sm text-lightThreeDotsMenuBodyText dark:text-darkThreeDotsMenuBodyText" aria-labelledby="dropdownMenuIconButton">
|
|
17
|
+
<li v-for="(item, i) in threeDotsDropdownItems" :key="`dropdown-item-${i}`">
|
|
18
|
+
<a href="#"
|
|
19
|
+
class="block px-4 py-2 hover:bg-lightThreeDotsMenuBodyBackgroundHover hover:text-lightThreeDotsMenuBodyTextHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover"
|
|
20
|
+
:class="{
|
|
21
|
+
'pointer-events-none': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
|
|
22
|
+
'opacity-50': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
|
|
23
|
+
'cursor-not-allowed': checkboxes && checkboxes.length === 0 && item.meta?.disabledWhenNoCheckboxes,
|
|
24
|
+
}"
|
|
25
|
+
@click="injectedComponentClick(i)">
|
|
26
|
+
<component :ref="(el: any) => setComponentRef(el, i)" :is="getCustomComponent(item)"
|
|
20
27
|
:meta="item.meta"
|
|
21
28
|
:resource="coreStore.resource"
|
|
22
29
|
:adminUser="coreStore.adminUser"
|
|
30
|
+
:checkboxes="checkboxes"
|
|
31
|
+
:updateList="props.updateList"
|
|
32
|
+
:clearCheckboxes="clearCheckboxes"
|
|
23
33
|
/>
|
|
24
34
|
</a>
|
|
25
35
|
</li>
|
|
26
36
|
<li v-for="action in customActions" :key="action.id">
|
|
27
|
-
<
|
|
37
|
+
<component
|
|
38
|
+
:is="(action.customComponent && getCustomComponent(action.customComponent)) || CallActionWrapper"
|
|
39
|
+
:meta="action.customComponent?.meta"
|
|
40
|
+
@callAction="(payload? : Object) => handleActionClick(action, payload)"
|
|
41
|
+
>
|
|
42
|
+
<a href="#" @click.prevent class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover">
|
|
43
|
+
<div class="flex items-center gap-2">
|
|
44
|
+
<component
|
|
45
|
+
v-if="action.icon"
|
|
46
|
+
:is="getIcon(action.icon)"
|
|
47
|
+
class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
|
|
48
|
+
/>
|
|
49
|
+
{{ action.name }}
|
|
50
|
+
</div>
|
|
51
|
+
</a>
|
|
52
|
+
</component>
|
|
53
|
+
</li>
|
|
54
|
+
<li v-for="action in (bulkActions ?? []).filter(a => a.showInThreeDotsDropdown)" :key="action.id">
|
|
55
|
+
<a href="#" @click.prevent="startBulkAction(action.id)"
|
|
56
|
+
class="block px-4 py-2 hover:text-lightThreeDotsMenuBodyTextHover hover:bg-lightThreeDotsMenuBodyBackgroundHover dark:hover:bg-darkThreeDotsMenuBodyBackgroundHover dark:hover:text-darkThreeDotsMenuBodyTextHover"
|
|
57
|
+
:class="{
|
|
58
|
+
'pointer-events-none': checkboxes && checkboxes.length === 0,
|
|
59
|
+
'opacity-50': checkboxes && checkboxes.length === 0,
|
|
60
|
+
'cursor-not-allowed': checkboxes && checkboxes.length === 0
|
|
61
|
+
}">
|
|
28
62
|
<div class="flex items-center gap-2">
|
|
29
63
|
<component
|
|
30
64
|
v-if="action.icon"
|
|
31
65
|
:is="getIcon(action.icon)"
|
|
32
66
|
class="w-4 h-4 text-lightPrimary dark:text-darkPrimary"
|
|
33
67
|
/>
|
|
34
|
-
{{ action.
|
|
68
|
+
{{ action.label }}
|
|
35
69
|
</div>
|
|
36
70
|
</a>
|
|
37
71
|
</li>
|
|
@@ -47,17 +81,39 @@ import { useCoreStore } from '@/stores/core';
|
|
|
47
81
|
import adminforth from '@/adminforth';
|
|
48
82
|
import { callAdminForthApi } from '@/utils';
|
|
49
83
|
import { useRoute, useRouter } from 'vue-router';
|
|
84
|
+
import CallActionWrapper from '@/components/CallActionWrapper.vue'
|
|
85
|
+
import { ref, type ComponentPublicInstance } from 'vue';
|
|
86
|
+
import type { AdminForthBulkActionCommon, AdminForthComponentDeclarationFull } from '@/types/Common';
|
|
87
|
+
import type { AdminForthActionInput } from '@/types/Back';
|
|
88
|
+
|
|
50
89
|
|
|
51
90
|
const route = useRoute();
|
|
52
91
|
const coreStore = useCoreStore();
|
|
53
92
|
const router = useRouter();
|
|
93
|
+
const threeDotsDropdownItemsRefs = ref<Array<ComponentPublicInstance | null>>([]);
|
|
54
94
|
|
|
55
95
|
const props = defineProps({
|
|
56
|
-
threeDotsDropdownItems: Array
|
|
57
|
-
customActions: Array
|
|
96
|
+
threeDotsDropdownItems: Array<AdminForthComponentDeclarationFull>,
|
|
97
|
+
customActions: Array<AdminForthActionInput>,
|
|
98
|
+
bulkActions: Array<AdminForthBulkActionCommon>,
|
|
99
|
+
checkboxes: Array,
|
|
100
|
+
updateList: {
|
|
101
|
+
type: Function,
|
|
102
|
+
},
|
|
103
|
+
clearCheckboxes: {
|
|
104
|
+
type: Function
|
|
105
|
+
}
|
|
58
106
|
});
|
|
59
107
|
|
|
60
|
-
|
|
108
|
+
const emit = defineEmits(['startBulkAction']);
|
|
109
|
+
|
|
110
|
+
function setComponentRef(el: ComponentPublicInstance | null, index: number) {
|
|
111
|
+
if (el) {
|
|
112
|
+
threeDotsDropdownItemsRefs.value[index] = el;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handleActionClick(action: AdminForthActionInput, payload: any) {
|
|
61
117
|
adminforth.list.closeThreeDotsDropdown();
|
|
62
118
|
|
|
63
119
|
const actionId = action.id;
|
|
@@ -67,7 +123,8 @@ async function handleActionClick(action) {
|
|
|
67
123
|
body: {
|
|
68
124
|
resourceId: route.params.resourceId,
|
|
69
125
|
actionId: actionId,
|
|
70
|
-
recordId: route.params.primaryKey
|
|
126
|
+
recordId: route.params.primaryKey,
|
|
127
|
+
extra: payload || {},
|
|
71
128
|
}
|
|
72
129
|
});
|
|
73
130
|
|
|
@@ -88,8 +145,8 @@ async function handleActionClick(action) {
|
|
|
88
145
|
|
|
89
146
|
if (data?.ok) {
|
|
90
147
|
await coreStore.fetchRecord({
|
|
91
|
-
resourceId: route.params.resourceId,
|
|
92
|
-
primaryKey: route.params.primaryKey,
|
|
148
|
+
resourceId: route.params.resourceId as string,
|
|
149
|
+
primaryKey: route.params.primaryKey as string,
|
|
93
150
|
source: 'show',
|
|
94
151
|
});
|
|
95
152
|
|
|
@@ -108,4 +165,16 @@ async function handleActionClick(action) {
|
|
|
108
165
|
});
|
|
109
166
|
}
|
|
110
167
|
}
|
|
168
|
+
|
|
169
|
+
function startBulkAction(actionId: string) {
|
|
170
|
+
adminforth.list.closeThreeDotsDropdown();
|
|
171
|
+
emit('startBulkAction', actionId);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function injectedComponentClick(index: number) {
|
|
175
|
+
const componentRef = threeDotsDropdownItemsRefs.value[index];
|
|
176
|
+
if (componentRef && 'click' in componentRef) {
|
|
177
|
+
(componentRef as any).click?.();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
111
180
|
</script>
|