adminforth 3.35.1 → 4.0.1
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/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +20 -3
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/styleGenerator.d.ts +13 -0
- package/dist/modules/styleGenerator.d.ts.map +1 -1
- package/dist/modules/styleGenerator.js +274 -1
- package/dist/modules/styleGenerator.js.map +1 -1
- package/dist/modules/styles.d.ts +24 -0
- package/dist/modules/styles.d.ts.map +1 -1
- package/dist/modules/styles.js +452 -427
- package/dist/modules/styles.js.map +1 -1
- package/dist/modules/utils.d.ts +1 -13
- package/dist/modules/utils.d.ts.map +1 -1
- package/dist/modules/utils.js +2 -275
- package/dist/modules/utils.js.map +1 -1
- package/dist/spa/index.html +2 -2
- package/dist/spa/package-lock.json +1373 -1136
- package/dist/spa/package.json +18 -15
- package/dist/spa/pnpm-lock.yaml +1067 -648
- package/dist/spa/src/App.vue +84 -92
- package/dist/spa/src/afcl/AreaChart.vue +8 -24
- package/dist/spa/src/afcl/BarChart.vue +8 -23
- package/dist/spa/src/afcl/Button.vue +8 -2
- package/dist/spa/src/afcl/ButtonGroup.vue +9 -9
- package/dist/spa/src/afcl/Card.vue +40 -19
- package/dist/spa/src/afcl/Checkbox.vue +6 -6
- package/dist/spa/src/afcl/CountryFlag.vue +17 -11
- package/dist/spa/src/afcl/DatePicker.vue +7 -7
- package/dist/spa/src/afcl/Dialog.vue +8 -9
- package/dist/spa/src/afcl/DropdownMenu.vue +210 -0
- package/dist/spa/src/afcl/DropdownMenuAccordion.vue +56 -0
- package/dist/spa/src/afcl/DropdownMenuItem.vue +86 -0
- package/dist/spa/src/afcl/Dropzone.vue +10 -26
- package/dist/spa/src/afcl/Input.vue +4 -4
- package/dist/spa/src/afcl/Link.vue +1 -1
- package/dist/spa/src/afcl/LinkButton.vue +15 -1
- package/dist/spa/src/afcl/MixedChart.vue +4 -18
- package/dist/spa/src/afcl/Modal.vue +9 -9
- package/dist/spa/src/afcl/PieChart.vue +8 -29
- package/dist/spa/src/afcl/ProgressBar.vue +2 -2
- package/dist/spa/src/afcl/Select.vue +20 -20
- package/dist/spa/src/afcl/Skeleton.vue +1 -1
- package/dist/spa/src/afcl/Spinner.vue +1 -1
- package/dist/spa/src/afcl/Table.vue +19 -26
- package/dist/spa/src/afcl/Textarea.vue +1 -1
- package/dist/spa/src/afcl/Toggle.vue +4 -4
- package/dist/spa/src/afcl/Tooltip.vue +1 -1
- package/dist/spa/src/afcl/TreeMapChart.vue +5 -20
- package/dist/spa/src/afcl/VerticalTabs.vue +4 -4
- package/dist/spa/src/afcl/buttonStyles.ts +73 -33
- package/dist/spa/src/afcl/chartUtils.ts +19 -0
- package/dist/spa/src/afcl/classMerge.ts +11 -0
- package/dist/spa/src/afcl/dropdownMenuContext.ts +10 -0
- package/dist/spa/src/afcl/fileSize.ts +12 -0
- package/dist/spa/src/afcl/index.ts +3 -0
- package/dist/spa/src/components/AcceptModal.vue +17 -19
- package/dist/spa/src/components/Breadcrumbs.vue +11 -13
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +2 -2
- package/dist/spa/src/components/CallActionWrapper.vue +3 -0
- package/dist/spa/src/components/ColumnValueInput.vue +5 -5
- package/dist/spa/src/components/ColumnValueInputWrapper.vue +7 -6
- package/dist/spa/src/components/CustomDateRangePicker.vue +8 -8
- package/dist/spa/src/components/CustomRangePicker.vue +2 -2
- package/dist/spa/src/components/ErrorMessage.vue +3 -4
- package/dist/spa/src/components/Filters.vue +13 -13
- package/dist/spa/src/components/GroupsTable.vue +12 -12
- package/dist/spa/src/components/ListActionsThreeDots.vue +89 -207
- package/dist/spa/src/components/MenuLink.vue +23 -20
- package/dist/spa/src/components/RangePicker.vue +9 -10
- package/dist/spa/src/components/ResourceListTable.vue +68 -65
- package/dist/spa/src/components/ShowTable.vue +8 -8
- package/dist/spa/src/components/Sidebar.vue +31 -38
- package/dist/spa/src/components/ThreeDotsMenu.vue +88 -136
- package/dist/spa/src/components/Toast.vue +16 -23
- package/dist/spa/src/components/UserMenuSettingsButton.vue +15 -36
- package/dist/spa/src/components/ValueRenderer.vue +13 -8
- package/dist/spa/src/components/icons/IconCalendar.vue +3 -3
- package/dist/spa/src/components/icons/IconTime.vue +3 -3
- package/dist/spa/src/index.scss +6 -2
- package/dist/spa/src/main.ts +2 -0
- package/dist/spa/src/renderers/CompactField.vue +3 -3
- package/dist/spa/src/renderers/CompactUUID.vue +3 -3
- package/dist/spa/src/renderers/SensitiveBlurCell.vue +9 -9
- package/dist/spa/src/utils/threeDotsMenu.ts +7 -0
- package/dist/spa/src/utils/userMenu.ts +5 -0
- package/dist/spa/src/utils/utils.ts +12 -39
- package/dist/spa/src/views/CreateEditSkeleton.vue +3 -3
- package/dist/spa/src/views/CreateView.vue +5 -6
- package/dist/spa/src/views/EditView.vue +5 -6
- package/dist/spa/src/views/ListView.vue +114 -115
- package/dist/spa/src/views/LoginView.vue +4 -4
- package/dist/spa/src/views/ShowView.vue +17 -18
- package/dist/spa/vite.config.ts +23 -16
- package/package.json +1 -2
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow dark:
|
|
3
|
-
<div v-if="group.groupName && !group.noTitle" class="text-
|
|
2
|
+
<div class="rounded-lg shadow-resourseFormShadow dark:shadow-darkResourseFormShadow border border-lightFormBorder bg-lightForm dark:bg-darkForm dark:border-darkFormBorder">
|
|
3
|
+
<div v-if="group.groupName && !group.noTitle" class="text-sm font-semibold px-5 py-3 flex flex-1 items-center border-b border-lightFormBorder dark:border-darkFormBorder text-lightFormFieldTextColor bg-lightFormHeading dark:bg-darkFormHeading dark:text-darkFormFieldTextColor rounded-t-lg">
|
|
4
4
|
{{ group.groupName }}
|
|
5
5
|
</div>
|
|
6
6
|
<table class="w-full text-sm text-left rtl:text-right text-lightFormFieldTextColor dark:text-darkFormFieldTextColor">
|
|
7
|
-
<thead v-if="!allColumnsHaveCustomComponent" class="text-xs text-
|
|
7
|
+
<thead v-if="!allColumnsHaveCustomComponent" class="text-xs font-medium text-lightListTablePaginationHelpText dark:text-darkListTablePaginationHelpText bg-lightFormHeading dark:bg-darkFormHeading block md:table-row-group border-b border-lightFormBorder dark:border-darkFormBorder">
|
|
8
8
|
<tr>
|
|
9
|
-
<th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-
|
|
9
|
+
<th scope="col" :class="{'rounded-tl-lg': !group.groupName}" class="px-5 py-2.5 font-medium hidden md:w-56 md:table-cell">
|
|
10
10
|
{{ $t('Field') }}
|
|
11
11
|
</th>
|
|
12
|
-
<th scope="col" :class="{'rounded-tr-lg': !group.groupName}" class="px-
|
|
12
|
+
<th scope="col" :class="{'rounded-tr-lg': !group.groupName}" class="px-5 py-2.5 font-medium hidden md:table-cell">
|
|
13
13
|
{{ $t('Value') }}
|
|
14
14
|
</th>
|
|
15
15
|
</tr>
|
|
@@ -19,17 +19,17 @@
|
|
|
19
19
|
v-for="(column, i) in group.columns"
|
|
20
20
|
:key="column.name"
|
|
21
21
|
v-if="currentValues !== null"
|
|
22
|
-
class="bg-lightForm dark:bg-darkForm dark:border-darkFormBorder block md:table-row"
|
|
22
|
+
class="bg-lightForm dark:bg-darkForm border-lightFormBorder dark:border-darkFormBorder block md:table-row"
|
|
23
23
|
:class="{ 'border-b': i !== group.columns.length - 1}"
|
|
24
24
|
>
|
|
25
|
-
<td class="px-
|
|
25
|
+
<td class="px-5 py-3 flex items-center block pb-0 md:pb-3 relative md:table-cell"
|
|
26
26
|
:class="{'rounded-bl-lg border-b-none': i === group.columns.length - 1}"> <!--align-top-->
|
|
27
|
-
<div class="absolute inset-0 flex items-center overflow-hidden px-
|
|
28
|
-
<span class="flex items-center gap-1">
|
|
27
|
+
<div class="absolute inset-0 flex items-center overflow-hidden px-5 py-3 max-h-32">
|
|
28
|
+
<span class="flex items-center gap-1 font-medium">
|
|
29
29
|
{{ column.label }}
|
|
30
30
|
<Tooltip v-if="column.required[mode]">
|
|
31
31
|
|
|
32
|
-
<
|
|
32
|
+
<IconAsterisk v-if="column.required[mode]" class="w-3 h-3"
|
|
33
33
|
:class="(columnsWithErrors[column.name] && validatingMode && !isValidating) ? 'text-lightInputErrorColor dark:text-darkInputErrorColor' : 'text-lightRequiredIconColor dark:text-darkRequiredIconColor'"
|
|
34
34
|
/>
|
|
35
35
|
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
</div>
|
|
42
42
|
</td>
|
|
43
43
|
<td
|
|
44
|
-
class="px-
|
|
44
|
+
class="px-5 py-3 whitespace-pre-wrap relative block md:table-cell"
|
|
45
45
|
:class="{'rounded-br-lg': i === group.columns.length - 1}"
|
|
46
46
|
>
|
|
47
47
|
<ColumnValueInputWrapper
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
</template>
|
|
69
69
|
|
|
70
70
|
<script setup lang="ts">
|
|
71
|
-
import {
|
|
71
|
+
import { IconAsterisk } from '@iconify-prerendered/vue-lucide';
|
|
72
72
|
import { Tooltip } from '@/afcl';
|
|
73
73
|
import { ref, computed, watch, nextTick, type Ref } from 'vue';
|
|
74
74
|
import { useI18n } from 'vue-i18n';
|
|
@@ -1,115 +1,93 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
<DropdownMenu
|
|
3
|
+
v-model:open="showMenu"
|
|
4
|
+
placement="bottom-end"
|
|
5
|
+
teleportToBody
|
|
6
|
+
:contentClass="['af-list-actions-three-dots z-40 min-w-[10rem]', THREE_DOTS_MENU_CONTENT_CLASSES]"
|
|
7
|
+
>
|
|
8
|
+
<template #trigger>
|
|
9
|
+
<Button variant="ghost" size="sm" iconOnly>
|
|
10
|
+
<IconEllipsis />
|
|
11
|
+
</Button>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<DropdownMenuItem
|
|
15
|
+
v-if="isBaseActionInMenu('show')"
|
|
16
|
+
:class="THREE_DOTS_MENU_ITEM_CLASSES"
|
|
17
|
+
:to="{ name: 'resource-show', params: { resourceId: props.resourceId, primaryKey: record._primaryKeyValue } }"
|
|
18
|
+
>
|
|
19
|
+
<template #icon><IconEye /></template>
|
|
20
|
+
{{ $t('Show item') }}
|
|
21
|
+
</DropdownMenuItem>
|
|
22
|
+
<DropdownMenuItem
|
|
23
|
+
v-if="isBaseActionInMenu('edit')"
|
|
24
|
+
:class="THREE_DOTS_MENU_ITEM_CLASSES"
|
|
25
|
+
:to="{ name: 'resource-edit', params: { resourceId: props.resourceId, primaryKey: record._primaryKeyValue } }"
|
|
26
|
+
>
|
|
27
|
+
<template #icon><IconPencil /></template>
|
|
28
|
+
{{ $t('Edit item') }}
|
|
29
|
+
</DropdownMenuItem>
|
|
30
|
+
<DropdownMenuItem
|
|
31
|
+
v-if="isBaseActionInMenu('delete')"
|
|
32
|
+
:class="THREE_DOTS_MENU_ITEM_CLASSES"
|
|
33
|
+
@click="deleteRecord(record)"
|
|
34
|
+
>
|
|
35
|
+
<template #icon><IconTrash2 /></template>
|
|
36
|
+
{{ $t('Delete item') }}
|
|
37
|
+
</DropdownMenuItem>
|
|
38
|
+
|
|
39
|
+
<DropdownMenuItem
|
|
40
|
+
v-for="action in menuActions"
|
|
41
|
+
:key="action.id"
|
|
42
|
+
:class="THREE_DOTS_MENU_ITEM_CLASSES"
|
|
43
|
+
@click="actionItemClick(action.id, $event)"
|
|
7
44
|
>
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
45
|
+
<component
|
|
46
|
+
:ref="(el: any) => { actionRefs[action.id] = el; }"
|
|
47
|
+
:is="action.customComponent ? getCustomComponent(formatComponent(action.customComponent)) : CallActionWrapper"
|
|
48
|
+
:meta="formatComponent(action.customComponent).meta"
|
|
49
|
+
:row="record"
|
|
50
|
+
:resource="coreStore.resource"
|
|
51
|
+
:adminUser="coreStore.adminUser"
|
|
52
|
+
@callAction="(payload? : Object) => startCustomAction(action.id, record, payload)"
|
|
16
53
|
>
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
<!-- preventDefault marks the click as handled by the action component -->
|
|
55
|
+
<span @click.prevent class="flex flex-1 items-center gap-2">
|
|
56
|
+
<component
|
|
57
|
+
v-if="action.icon"
|
|
58
|
+
:is="getIcon(action.icon)"
|
|
59
|
+
class="w-4 h-4 text-lightInputIconColor dark:text-darkInputIconColor"
|
|
60
|
+
/>
|
|
61
|
+
{{ $t(action.name) }}
|
|
62
|
+
</span>
|
|
63
|
+
</component>
|
|
64
|
+
</DropdownMenuItem>
|
|
28
65
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
name: 'resource-edit',
|
|
40
|
-
params: {
|
|
41
|
-
resourceId: props.resourceId,
|
|
42
|
-
primaryKey: record._primaryKeyValue,
|
|
43
|
-
}
|
|
44
|
-
}"
|
|
45
|
-
>
|
|
46
|
-
<IconPenSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
|
|
47
|
-
{{ $t('Edit item') }}
|
|
48
|
-
</RouterLink>
|
|
49
|
-
</template>
|
|
50
|
-
<template v-if="!resourceOptions?.baseActionsAsQuickIcons || (resourceOptions?.baseActionsAsQuickIcons && !resourceOptions?.baseActionsAsQuickIcons.includes('delete'))">
|
|
51
|
-
<button
|
|
52
|
-
v-if="resourceOptions?.allowedActions?.delete"
|
|
53
|
-
class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300"
|
|
54
|
-
@click="deleteRecord(record)"
|
|
55
|
-
>
|
|
56
|
-
<IconTrashBinSolid class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary"/>
|
|
57
|
-
{{ $t('Delete item') }}
|
|
58
|
-
</button>
|
|
59
|
-
</template>
|
|
60
|
-
<div v-for="action in (resourceOptions.actions ?? []).filter((a: AdminForthActionInput) => a.showIn?.listThreeDotsMenu)" :key="action.id" >
|
|
61
|
-
<button class="flex text-nowrap p-1 hover:bg-gray-100 dark:hover:bg-gray-800 w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300" @click="() => { startCustomAction(action.id, record); showMenu = false; }">
|
|
62
|
-
<component
|
|
63
|
-
:is="action.customComponent ? getCustomComponent(action.customComponent) : CallActionWrapper"
|
|
64
|
-
:meta="action.customComponent?.meta"
|
|
65
|
-
:row="record"
|
|
66
|
-
:resource="coreStore.resource"
|
|
67
|
-
:adminUser="coreStore.adminUser"
|
|
68
|
-
@callAction="(payload? : Object) => startCustomAction(action.id, record, payload)"
|
|
69
|
-
>
|
|
70
|
-
<component
|
|
71
|
-
v-if="action.icon"
|
|
72
|
-
:is="getIcon(action.icon)"
|
|
73
|
-
class="w-5 h-5 mr-2 text-lightPrimary dark:text-darkPrimary dark:brightness-200"
|
|
74
|
-
/>
|
|
75
|
-
{{ $t(action.name) }}
|
|
76
|
-
</component>
|
|
77
|
-
</button>
|
|
78
|
-
</div>
|
|
79
|
-
<template v-if="customActionIconsThreeDotsMenuItems">
|
|
80
|
-
<component
|
|
81
|
-
v-for="c in customActionIconsThreeDotsMenuItems"
|
|
82
|
-
:is="getCustomComponent(formatComponent(c))"
|
|
83
|
-
:meta="formatComponent(c).meta"
|
|
84
|
-
:resource="coreStore.resource"
|
|
85
|
-
:adminUser="coreStore.adminUser"
|
|
86
|
-
:record="record"
|
|
87
|
-
:updateRecords="props.updateRecords"
|
|
88
|
-
/>
|
|
89
|
-
</template>
|
|
90
|
-
</div>
|
|
91
|
-
</teleport>
|
|
92
|
-
</div>
|
|
66
|
+
<component
|
|
67
|
+
v-for="c in customActionIconsThreeDotsMenuItems"
|
|
68
|
+
:is="getCustomComponent(formatComponent(c))"
|
|
69
|
+
:meta="formatComponent(c).meta"
|
|
70
|
+
:resource="coreStore.resource"
|
|
71
|
+
:adminUser="coreStore.adminUser"
|
|
72
|
+
:record="record"
|
|
73
|
+
:updateRecords="props.updateRecords"
|
|
74
|
+
/>
|
|
75
|
+
</DropdownMenu>
|
|
93
76
|
</template>
|
|
94
77
|
|
|
95
78
|
<script lang="ts" setup>
|
|
96
|
-
import {
|
|
97
|
-
|
|
98
|
-
IconPenSolid,
|
|
99
|
-
IconTrashBinSolid,
|
|
100
|
-
IconDotsHorizontalOutline
|
|
101
|
-
} from '@iconify-prerendered/vue-flowbite';
|
|
102
|
-
import { onMounted, onBeforeUnmount, ref, nextTick, watch } from 'vue';
|
|
79
|
+
import { IconEye, IconPencil, IconTrash2, IconEllipsis } from '@iconify-prerendered/vue-lucide';
|
|
80
|
+
import { computed, ref } from 'vue';
|
|
103
81
|
import { getIcon, getCustomComponent, formatComponent } from '@/utils';
|
|
82
|
+
import { THREE_DOTS_MENU_CONTENT_CLASSES, THREE_DOTS_MENU_ITEM_CLASSES } from '@/utils/threeDotsMenu';
|
|
104
83
|
import { useCoreStore } from '@/stores/core';
|
|
84
|
+
import { Button, DropdownMenu, DropdownMenuItem } from '@/afcl';
|
|
105
85
|
import CallActionWrapper from '@/components/CallActionWrapper.vue'
|
|
106
|
-
import { type
|
|
86
|
+
import { type AdminForthActionFront, type AdminForthComponentDeclaration } from '@/types/Common';
|
|
107
87
|
|
|
108
88
|
const coreStore = useCoreStore();
|
|
109
89
|
const showMenu = ref(false);
|
|
110
|
-
const
|
|
111
|
-
const menuRef = ref<HTMLElement | null>(null);
|
|
112
|
-
const menuStyles = ref<Record<string, string>>({});
|
|
90
|
+
const actionRefs: Record<string, any> = {};
|
|
113
91
|
|
|
114
92
|
const props = defineProps<{
|
|
115
93
|
resourceOptions: any;
|
|
@@ -121,116 +99,20 @@ const props = defineProps<{
|
|
|
121
99
|
startCustomAction: (actionId: string, row: any, extraData?: Record<string, any>) => void;
|
|
122
100
|
}>();
|
|
123
101
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
document.addEventListener('click', handleOutsideClick, true);
|
|
128
|
-
});
|
|
102
|
+
const menuActions = computed<AdminForthActionFront[]>(
|
|
103
|
+
() => (props.resourceOptions.actions ?? []).filter((a: AdminForthActionFront) => a.showIn?.listThreeDotsMenu)
|
|
104
|
+
);
|
|
129
105
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
document.removeEventListener('click', handleOutsideClick, true);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
watch(showMenu, async (isOpen) => {
|
|
137
|
-
if (isOpen) {
|
|
138
|
-
await nextTick();
|
|
139
|
-
// First pass: after DOM mount
|
|
140
|
-
updateMenuPosition();
|
|
141
|
-
// Second pass: after layout/paint to catch width changes (fonts/icons)
|
|
142
|
-
requestAnimationFrame(() => {
|
|
143
|
-
updateMenuPosition();
|
|
144
|
-
// Final safety: one micro-delay retry if width was still 0
|
|
145
|
-
setTimeout(() => updateMenuPosition(), 0);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
function toggleMenu() {
|
|
151
|
-
if (!showMenu.value) {
|
|
152
|
-
// Provisional position to avoid flashing at left:0 on first open
|
|
153
|
-
const el = triggerRef.value;
|
|
154
|
-
if (el) {
|
|
155
|
-
const rect = el.getBoundingClientRect();
|
|
156
|
-
const gap = 8;
|
|
157
|
-
menuStyles.value = {
|
|
158
|
-
position: 'fixed',
|
|
159
|
-
top: `${Math.round(rect.bottom)}px`,
|
|
160
|
-
left: `${Math.round(rect.left)}px`,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
showMenu.value = !showMenu.value;
|
|
106
|
+
// a base action is listed here unless it is rendered as a quick icon in the row
|
|
107
|
+
function isBaseActionInMenu(action: 'show' | 'edit' | 'delete') {
|
|
108
|
+
return props.resourceOptions?.allowedActions?.[action] && !props.resourceOptions?.baseActionsAsQuickIcons?.includes(action);
|
|
165
109
|
}
|
|
166
110
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const menuEl = menuRef.value;
|
|
173
|
-
// Measure current menu size to align and decide flipping
|
|
174
|
-
let menuWidth = rect.width; // fallback to trigger width
|
|
175
|
-
let menuHeight = 0;
|
|
176
|
-
if (menuEl) {
|
|
177
|
-
const menuRect = menuEl.getBoundingClientRect();
|
|
178
|
-
// Prefer bounding rect; fallback to offset/scroll width if needed
|
|
179
|
-
const measuredW = menuRect.width || menuEl.offsetWidth || menuEl.scrollWidth;
|
|
180
|
-
if (measuredW > 0) menuWidth = measuredW;
|
|
181
|
-
const measuredH = menuRect.height || menuEl.offsetHeight || menuEl.scrollHeight;
|
|
182
|
-
if (measuredH > 0) menuHeight = measuredH;
|
|
111
|
+
// a click on the item padding is forwarded to the action component (see `click` expose of custom action components),
|
|
112
|
+
// clicks on its content are already handled by the component itself
|
|
113
|
+
function actionItemClick(actionId: string, event: MouseEvent) {
|
|
114
|
+
if (!event.defaultPrevented) {
|
|
115
|
+
actionRefs[actionId]?.click?.();
|
|
183
116
|
}
|
|
184
|
-
// Right-align: right edge of menu == right edge of trigger
|
|
185
|
-
let left = rect.right - menuWidth;
|
|
186
|
-
// Clamp within viewport with small margin so it doesn't render off-screen
|
|
187
|
-
const minLeft = margin;
|
|
188
|
-
const maxLeft = Math.max(minLeft, window.innerWidth - margin - menuWidth);
|
|
189
|
-
left = Math.min(Math.max(left, minLeft), maxLeft);
|
|
190
|
-
|
|
191
|
-
// Determine whether to place above or below based on available space
|
|
192
|
-
const spaceBelow = window.innerHeight - rect.bottom - margin;
|
|
193
|
-
const spaceAbove = rect.top - margin;
|
|
194
|
-
const maxMenuHeight = Math.max(0, window.innerHeight - 2 * margin);
|
|
195
|
-
|
|
196
|
-
let top: number;
|
|
197
|
-
if (menuHeight === 0) {
|
|
198
|
-
// Unknown height yet (first pass). Prefer placing below; a subsequent pass will correct if needed.
|
|
199
|
-
top = rect.bottom + margin;
|
|
200
|
-
} else if (menuHeight <= spaceBelow) {
|
|
201
|
-
// Enough space below
|
|
202
|
-
top = rect.bottom + margin;
|
|
203
|
-
} else if (menuHeight <= spaceAbove) {
|
|
204
|
-
// Not enough below but enough above -> flip
|
|
205
|
-
top = rect.top - margin - menuHeight;
|
|
206
|
-
} else {
|
|
207
|
-
// Not enough space on either side: pick the side with more room and clamp within viewport
|
|
208
|
-
if (spaceBelow >= spaceAbove) {
|
|
209
|
-
top = Math.min(rect.bottom + margin, window.innerHeight - margin - menuHeight);
|
|
210
|
-
} else {
|
|
211
|
-
top = Math.max(margin, rect.top - margin - menuHeight);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
menuStyles.value = {
|
|
216
|
-
position: 'fixed',
|
|
217
|
-
top: `${Math.round(top)}px`,
|
|
218
|
-
left: `${Math.round(left)}px`,
|
|
219
|
-
maxHeight: `${Math.round(maxMenuHeight)}px`,
|
|
220
|
-
overflowY: 'auto',
|
|
221
|
-
};
|
|
222
117
|
}
|
|
223
|
-
|
|
224
|
-
function handleScrollOrResize() {
|
|
225
|
-
showMenu.value = false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function handleOutsideClick(e: MouseEvent) {
|
|
229
|
-
const target = e.target as Node | null;
|
|
230
|
-
if (!target) return;
|
|
231
|
-
if (menuRef.value?.contains(target)) return;
|
|
232
|
-
if (triggerRef.value?.contains(target)) return;
|
|
233
|
-
showMenu.value = false;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
</script>
|
|
118
|
+
</script>
|
|
@@ -5,55 +5,58 @@
|
|
|
5
5
|
? { href: item.url }
|
|
6
6
|
: { to: item.url || { name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {} } }"
|
|
7
7
|
:target="item.isOpenInNewTab ? '_blank' : '_self'"
|
|
8
|
-
class="af-menu-link flex group relative items-center py-
|
|
8
|
+
class="af-menu-link flex group relative items-center py-1.5 text-lightSidebarText dark:text-darkSidebarText rounded-default transition-colors duration-150 ease-in-out"
|
|
9
9
|
:class="{
|
|
10
|
-
'hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-
|
|
11
|
-
'pl-
|
|
10
|
+
'hover:bg-lightSidebarItemHover hover:text-lightSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextHover active:bg-lightSidebarItemActive dark:active:bg-darkSidebarItemActive': !['divider', 'gap', 'heading'].includes(item.type),
|
|
11
|
+
'pl-2.5 pr-3': isChild && !isSidebarIconOnly && !isSidebarHovering,
|
|
12
|
+
'pl-6 pr-3': isChild && isSidebarIconOnly && isSidebarHovering,
|
|
12
13
|
'w-full': !isSidebarIconOnly || isSidebarHovering,
|
|
13
|
-
'px-3
|
|
14
|
-
'w-10 h-
|
|
15
|
-
'bg-lightSidebarItemActive dark:bg-darkSidebarItemActive': isItemActive(item)
|
|
14
|
+
'px-3': !isChild && (!isSidebarIconOnly || isSidebarHovering),
|
|
15
|
+
'w-10 h-9 ml-1 px-3': isSidebarIconOnly && !isSidebarHovering,
|
|
16
|
+
'bg-lightSidebarItemActive text-lightSidebarTextActive font-medium dark:bg-darkSidebarItemActive dark:text-darkSidebarTextActive': isItemActive(item)
|
|
16
17
|
}"
|
|
17
18
|
>
|
|
18
|
-
<component v-if="item.icon" :is="getIcon(item.icon)"
|
|
19
|
-
class="min-w-
|
|
19
|
+
<component v-if="item.icon" :is="getIcon(item.icon)"
|
|
20
|
+
class="min-w-4 w-4 h-4 text-lightSidebarIcons dark:text-darkSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover transition-colors duration-150 ease-in-out"
|
|
21
|
+
:class="{ 'text-lightSidebarIconsHover dark:text-darkSidebarIconsHover': isItemActive(item) }"
|
|
20
22
|
>
|
|
21
23
|
</component>
|
|
22
|
-
<
|
|
23
|
-
class="min-w-
|
|
24
|
+
<IconFileImage v-else-if="!isChild || isSidebarIconOnly"
|
|
25
|
+
class="min-w-4 w-4 h-4 text-lightSidebarIcons dark:text-darkSidebarIcons group-hover:text-lightSidebarIconsHover dark:group-hover:text-darkSidebarIconsHover transition-colors duration-150 ease-in-out"
|
|
24
26
|
/>
|
|
25
27
|
<div
|
|
26
28
|
class="overflow-hidden block ms-3 pr-4 text-left rtl:text-right transition-all duration-200 ease-in-out"
|
|
27
29
|
:class="{
|
|
28
30
|
'opacity-0 ms-0 translate-x-4 flex-none': isSidebarIconOnly && !isSidebarHovering,
|
|
29
31
|
'opacity-100 ms-3 translate-x-0 flex-1': !isSidebarIconOnly || (isSidebarIconOnly && isSidebarHovering),
|
|
32
|
+
'!ms-0': isChild && !item.icon && !isSidebarIconOnly,
|
|
30
33
|
}"
|
|
31
34
|
:style="isSidebarIconOnly ? {
|
|
32
35
|
minWidth: isChild
|
|
33
|
-
? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 -
|
|
34
|
-
: `calc(${expandedWidth} - 0.75rem*2 - 0.
|
|
36
|
+
? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 - 1rem - 0.75rem)`
|
|
37
|
+
: `calc(${expandedWidth} - 0.75rem*2 - 0.75rem*2 - 1rem - 0.75rem)`,
|
|
35
38
|
width: isChild
|
|
36
|
-
? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 -
|
|
37
|
-
: `calc(${expandedWidth} - 0.75rem*2 - 0.
|
|
39
|
+
? `calc(${expandedWidth} - 0.75rem*2 - 1.5rem*2 - 1rem - 0.75rem)`
|
|
40
|
+
: `calc(${expandedWidth} - 0.75rem*2 - 0.75rem*2 - 1rem - 0.75rem)`
|
|
38
41
|
} : {}"
|
|
39
42
|
>
|
|
40
43
|
{{ item.label }}
|
|
41
44
|
</div>
|
|
42
45
|
<span class="absolute flex items-center justify-center right-1 top-1/2 -translate-y-1/2" v-if="(item.badge || item.badge === 0) && showExpandedBadge ">
|
|
43
46
|
<Tooltip v-if="item.badgeTooltip">
|
|
44
|
-
<div class="af-badge inline-flex items-center justify-center h-
|
|
45
|
-
fill-lightAnnouncementText dark:fill-
|
|
47
|
+
<div class="af-badge inline-flex items-center justify-center h-5 px-1.5 ms-2 text-[11px] font-medium tabular-nums rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
48
|
+
fill-lightAnnouncementText dark:fill-darkAnnouncementText text-lightAnnouncementText dark:text-darkAnnouncementText min-w-[1.25rem] max-w-[3rem]">{{ item.badge }}</div>
|
|
46
49
|
<template #tooltip>
|
|
47
50
|
{{ item.badgeTooltip }}
|
|
48
51
|
</template>
|
|
49
52
|
</Tooltip>
|
|
50
53
|
<template v-else>
|
|
51
|
-
<div class="af-badge inline-flex items-center justify-center h-
|
|
52
|
-
fill-lightAnnouncementText dark:fill-
|
|
54
|
+
<div class="af-badge inline-flex items-center justify-center h-5 px-1.5 ms-2 text-[11px] font-medium tabular-nums rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
55
|
+
fill-lightAnnouncementText dark:fill-darkAnnouncementText text-lightAnnouncementText dark:text-darkAnnouncementText min-w-[1.25rem] max-w-[3rem]">{{ item.badge }}</div>
|
|
53
56
|
</template>
|
|
54
57
|
</span>
|
|
55
58
|
<div v-if="(item.badge || item.badge === 0) && isSidebarIconOnly && !isSidebarHovering" class="af-badge absolute right-0.5 bottom-1 -translate-y-1/2 inline-flex items-center justify-center h-2 w-2 text-sm font-medium rounded-full bg-lightAnnouncementBG dark:bg-darkAnnouncementBG
|
|
56
|
-
fill-lightAnnouncementText dark:fill-
|
|
59
|
+
fill-lightAnnouncementText dark:fill-darkAnnouncementText text-lightAnnouncementText dark:text-darkAnnouncementText">
|
|
57
60
|
</div>
|
|
58
61
|
</component>
|
|
59
62
|
</template>
|
|
@@ -63,7 +66,7 @@ import { getIcon } from '@/utils';
|
|
|
63
66
|
import { Tooltip } from '@/afcl';
|
|
64
67
|
import { ref, watch, computed } from 'vue';
|
|
65
68
|
import { useCoreStore } from '@/stores/core';
|
|
66
|
-
import {
|
|
69
|
+
import { IconFileImage } from '@iconify-prerendered/vue-lucide';
|
|
67
70
|
import { useRoute, RouterLink } from 'vue-router';
|
|
68
71
|
|
|
69
72
|
const isExternalUrl = (url: string ) => /^https?:\/\//.test(url || '');
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="range-slider" ref="trackRef" @mousedown="onTrackMouseDown">
|
|
3
|
-
<div class="track"></div>
|
|
4
|
-
<div class="range bg-lightPrimary
|
|
3
|
+
<div class="track bg-lightProgressBarUnfilledColor dark:bg-darkProgressBarUnfilledColor"></div>
|
|
4
|
+
<div class="range bg-lightPrimary dark:bg-darkPrimary" :style="rangeStyle"></div>
|
|
5
5
|
|
|
6
6
|
<div
|
|
7
|
-
class="bg-
|
|
7
|
+
class="thumb bg-white border border-lightInputBorderHover shadow-[0_1px_2px_rgba(24,24,27,0.2)] dark:border-darkInputBorder"
|
|
8
8
|
:style="minThumbStyle"
|
|
9
9
|
@mousedown.stop.prevent="startDrag('min', $event)"
|
|
10
10
|
@mouseenter="minHovered = true"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<div v-if="minHovered || activeThumb === 'min'" class="thumb-tooltip" :style="minTooltipStyle">{{ minVal }}</div>
|
|
14
14
|
|
|
15
15
|
<div
|
|
16
|
-
class="bg-
|
|
16
|
+
class="thumb bg-white border border-lightInputBorderHover shadow-[0_1px_2px_rgba(24,24,27,0.2)] dark:border-darkInputBorder"
|
|
17
17
|
:style="maxThumbStyle"
|
|
18
18
|
@mousedown.stop.prevent="startDrag('max', $event)"
|
|
19
19
|
@mouseenter="maxHovered = true"
|
|
@@ -34,8 +34,8 @@ const props = defineProps({
|
|
|
34
34
|
},
|
|
35
35
|
min: { type: Number, default: 0 },
|
|
36
36
|
max: { type: Number, default: 100 },
|
|
37
|
-
dotSize: { type: Number, default:
|
|
38
|
-
height: { type: String, default: '
|
|
37
|
+
dotSize: { type: Number, default: 16 },
|
|
38
|
+
height: { type: String, default: '4px' }
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
const emit = defineEmits(['update:modelValue'])
|
|
@@ -184,14 +184,13 @@ onBeforeUnmount(() => {
|
|
|
184
184
|
.track {
|
|
185
185
|
position: absolute;
|
|
186
186
|
width: 100%;
|
|
187
|
-
height:
|
|
188
|
-
background: #e5e7eb;
|
|
187
|
+
height: 4px;
|
|
189
188
|
border-radius: 9999px;
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
.range {
|
|
193
192
|
position: absolute;
|
|
194
|
-
height:
|
|
193
|
+
height: 4px;
|
|
195
194
|
border-radius: 9999px;
|
|
196
195
|
}
|
|
197
196
|
|
|
@@ -209,7 +208,7 @@ onBeforeUnmount(() => {
|
|
|
209
208
|
transform: translateX(-50%);
|
|
210
209
|
background: rgba(0, 0, 0, 0.75);
|
|
211
210
|
color: #fff;
|
|
212
|
-
font-size:
|
|
211
|
+
font-size: 12px;
|
|
213
212
|
font-weight: 500;
|
|
214
213
|
line-height: 1;
|
|
215
214
|
padding: 6px 6px;
|