adminforth 4.5.1 → 4.6.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.
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-bind="restAttrs" :class="rootClasses">
|
|
3
|
+
<div :class="mergeClasses(
|
|
4
|
+
'afcl-dialog-panel-header flex items-center gap-3.5 border-b border-lightDialogBreakLine py-5 pl-6 dark:border-darkDialogBreakLine',
|
|
5
|
+
closable ? 'pr-4' : 'pr-6',
|
|
6
|
+
classes.header,
|
|
7
|
+
)">
|
|
8
|
+
<div
|
|
9
|
+
v-if="$slots.icon"
|
|
10
|
+
:class="mergeClasses(
|
|
11
|
+
'afcl-dialog-panel-icon flex w-10 h-10 shrink-0 items-center justify-center rounded-lg [&>svg]:w-5 [&>svg]:h-5',
|
|
12
|
+
'bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/20 dark:text-darkPrimary',
|
|
13
|
+
classes.icon,
|
|
14
|
+
)"
|
|
15
|
+
>
|
|
16
|
+
<slot name="icon" />
|
|
17
|
+
</div>
|
|
18
|
+
<div :class="mergeClasses('afcl-dialog-panel-title-wrap flex-1 min-w-0', classes.titleWrap)">
|
|
19
|
+
<slot name="title">
|
|
20
|
+
<h2 :class="mergeClasses('afcl-dialog-panel-title break-words text-lg leading-6 font-semibold text-lightDialogHeaderText dark:text-darkDialogHeaderText', classes.title)">{{ title }}</h2>
|
|
21
|
+
<p v-if="subtitle" :class="mergeClasses('afcl-dialog-panel-subtitle mt-0.5 text-sm leading-5 text-lightInputIconColor dark:text-darkInputIconColor', classes.subtitle)">{{ subtitle }}</p>
|
|
22
|
+
</slot>
|
|
23
|
+
</div>
|
|
24
|
+
<Button
|
|
25
|
+
v-if="closable"
|
|
26
|
+
type="button"
|
|
27
|
+
variant="ghost"
|
|
28
|
+
size="sm"
|
|
29
|
+
iconOnly
|
|
30
|
+
:aria-label="$t('Close')"
|
|
31
|
+
:class="mergeClasses(
|
|
32
|
+
'afcl-dialog-panel-close text-lightDialogCloseButton hover:text-lightDialogCloseButtonHover hover:bg-lightDialogCloseButtonHoverBackground',
|
|
33
|
+
'dark:text-darkDialogCloseButton dark:hover:text-darkDialogCloseButtonHover dark:hover:bg-darkDialogCloseButtonHoverBackground',
|
|
34
|
+
classes.close,
|
|
35
|
+
)"
|
|
36
|
+
@click="emit('close')"
|
|
37
|
+
>
|
|
38
|
+
<IconX aria-hidden="true" />
|
|
39
|
+
</Button>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div :class="mergeClasses('afcl-dialog-panel-body flex flex-col gap-4 px-6 pt-5 pb-6', classes.body)">
|
|
43
|
+
<slot />
|
|
44
|
+
<p v-if="error" role="alert" :class="mergeClasses('afcl-dialog-panel-error -mt-1 flex items-center gap-1.5 text-sm text-lightDanger dark:text-darkDanger', classes.error)">
|
|
45
|
+
<IconCircleAlert class="w-4 h-4 shrink-0" aria-hidden="true" />
|
|
46
|
+
{{ error }}
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- a main action on the right, a secondary one may sit on the left with ms-0 me-auto -->
|
|
51
|
+
<div
|
|
52
|
+
v-if="$slots.footer"
|
|
53
|
+
:class="mergeClasses(
|
|
54
|
+
'afcl-dialog-panel-footer flex flex-wrap items-center justify-end gap-x-3 gap-y-2 border-t border-lightDialogBreakLine bg-lightFormHeading py-3 pl-6 pr-5',
|
|
55
|
+
'dark:border-darkDialogBreakLine dark:bg-darkFormHeading',
|
|
56
|
+
classes.footer,
|
|
57
|
+
)"
|
|
58
|
+
>
|
|
59
|
+
<slot name="footer" />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script setup lang="ts">
|
|
65
|
+
import { computed, useAttrs } from 'vue';
|
|
66
|
+
import { type ClassNameValue } from 'tailwind-merge';
|
|
67
|
+
import { IconCircleAlert, IconX } from '@iconify-prerendered/vue-lucide';
|
|
68
|
+
import { mergeClasses } from './classMerge';
|
|
69
|
+
import Button from './Button.vue';
|
|
70
|
+
|
|
71
|
+
type DialogPanelPart = 'header' | 'icon' | 'titleWrap' | 'title' | 'subtitle' | 'close' | 'body' | 'error' | 'footer';
|
|
72
|
+
|
|
73
|
+
defineOptions({ inheritAttrs: false });
|
|
74
|
+
|
|
75
|
+
withDefaults(defineProps<{
|
|
76
|
+
title?: string,
|
|
77
|
+
subtitle?: string,
|
|
78
|
+
error?: string | null,
|
|
79
|
+
// shows a close button in the header which emits close
|
|
80
|
+
closable?: boolean,
|
|
81
|
+
// extra classes for the parts, e.g. the af-* classes apps restyle a screen by
|
|
82
|
+
classes?: Partial<Record<DialogPanelPart, ClassNameValue>>,
|
|
83
|
+
}>(), {
|
|
84
|
+
title: '',
|
|
85
|
+
subtitle: '',
|
|
86
|
+
error: null,
|
|
87
|
+
closable: false,
|
|
88
|
+
classes: () => ({}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const emit = defineEmits<{
|
|
92
|
+
(e: 'close'): void,
|
|
93
|
+
}>();
|
|
94
|
+
|
|
95
|
+
const attrs = useAttrs();
|
|
96
|
+
const restAttrs = computed(() => {
|
|
97
|
+
const { class: _class, ...rest } = attrs;
|
|
98
|
+
return rest;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// a panel on the page is drawn like other surfaces: a hairline border and the light shadow of controls,
|
|
102
|
+
// a panel which floats over the page (in a modal) passes shadow-popover
|
|
103
|
+
const rootClasses = computed(() => mergeClasses(
|
|
104
|
+
'afcl-dialog-panel relative flex flex-col w-full max-w-md overflow-hidden rounded-xl border shadow-customLight',
|
|
105
|
+
'border-lightDialogBreakLine bg-lightDialogBackgorund dark:border-darkDialogBreakLine dark:bg-darkDialogBackgorund',
|
|
106
|
+
attrs.class as ClassNameValue,
|
|
107
|
+
));
|
|
108
|
+
</script>
|
|
@@ -11,19 +11,39 @@
|
|
|
11
11
|
<input
|
|
12
12
|
ref="input"
|
|
13
13
|
v-bind="$attrs"
|
|
14
|
-
:type="type"
|
|
14
|
+
:type="passwordToggle && revealed ? 'text' : type"
|
|
15
15
|
@input="$emit('update:modelValue', type === 'number' ? numberInputValue(($event.target as HTMLInputElement)?.value) : ($event.target as HTMLInputElement)?.value)"
|
|
16
16
|
:value="modelValue"
|
|
17
17
|
aria-describedby="helper-text-explanation"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
:aria-invalid="invalid || undefined"
|
|
19
|
+
class="afcl-input inline-flex bg-lightInputBackground text-lightInputText dark:text-darkInputText border rounded-0 shadow-customLight focus:outline-none focus:ring-[3px]
|
|
20
|
+
block w-20 px-2.5 py-1.5 leading-5 dark:bg-darkInputBackground placeholder-lightInputPlaceholderText dark:placeholder-darkInputPlaceholderText dark:text-darkInputText translate-y-0 transition-colors disabled:cursor-not-allowed"
|
|
21
|
+
:class="[
|
|
22
|
+
invalid
|
|
23
|
+
? 'border-lightDanger focus:border-lightDanger focus:ring-lightDanger/20 dark:border-darkDanger dark:focus:border-darkDanger dark:focus:ring-darkDanger/30'
|
|
24
|
+
: 'border-lightInputBorder hover:border-lightInputBorderHover focus:ring-lightInputFocusRing focus:border-lightInputFocusBorder dark:border-darkInputBorder dark:hover:border-darkInputBorderHover dark:focus:ring-darkInputFocusRing dark:focus:border-darkInputFocusBorder',
|
|
25
|
+
{'rounded-l-default': !$slots.prefix && !prefix, 'rounded-r-default': !$slots.suffix && !suffix, 'w-full': fullWidth, 'text-base': isIos, 'text-sm': !isIos, 'pr-10': $slots.rightIcon || passwordToggle },
|
|
26
|
+
]"
|
|
21
27
|
:disabled="readonly"
|
|
22
28
|
>
|
|
23
29
|
|
|
24
30
|
<div v-if="$slots.rightIcon" class="absolute inset-y-0 right-0 pr-3 flex items-center">
|
|
25
31
|
<slot name="rightIcon" />
|
|
26
32
|
</div>
|
|
33
|
+
<div v-else-if="passwordToggle" class="absolute inset-y-0 right-0 pr-1 flex items-center">
|
|
34
|
+
<Button
|
|
35
|
+
type="button"
|
|
36
|
+
variant="ghost"
|
|
37
|
+
size="sm"
|
|
38
|
+
iconOnly
|
|
39
|
+
class="afcl-input-password-toggle"
|
|
40
|
+
:aria-label="revealed ? $t('Hide password') : $t('Show password')"
|
|
41
|
+
@click="revealed = !revealed"
|
|
42
|
+
>
|
|
43
|
+
<IconEyeOff v-if="revealed" aria-hidden="true" />
|
|
44
|
+
<IconEye v-else aria-hidden="true" />
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
27
47
|
<span
|
|
28
48
|
v-if="$slots.suffix || suffix"
|
|
29
49
|
class="inline-flex items-center px-2.5 text-sm text-lightInputIconColor bg-lightSecondaryDarken border border-s-0 border-lightInputBorder rounded-e-default dark:bg-darkSecondaryLighten dark:text-darkInputIconColor dark:border-darkInputBorder ">
|
|
@@ -36,8 +56,10 @@
|
|
|
36
56
|
<script setup lang="ts">
|
|
37
57
|
|
|
38
58
|
import { ref } from 'vue';
|
|
59
|
+
import { IconEye, IconEyeOff } from '@iconify-prerendered/vue-lucide';
|
|
39
60
|
import { useCoreStore } from '@/stores/core';
|
|
40
61
|
import { numberInputValue } from './inputValue';
|
|
62
|
+
import Button from './Button.vue';
|
|
41
63
|
|
|
42
64
|
const coreStore = useCoreStore();
|
|
43
65
|
const isIos = coreStore.isIos;
|
|
@@ -49,9 +71,14 @@ const props = defineProps<{
|
|
|
49
71
|
suffix?: string,
|
|
50
72
|
prefix?: string,
|
|
51
73
|
readonly?: boolean,
|
|
74
|
+
// paints the border red, e.g. for a value which did not pass validation
|
|
75
|
+
invalid?: boolean,
|
|
76
|
+
// for type="password": a button on the right shows and hides the value
|
|
77
|
+
passwordToggle?: boolean,
|
|
52
78
|
}>()
|
|
53
79
|
|
|
54
80
|
const input = ref<HTMLInputElement | null>(null)
|
|
81
|
+
const revealed = ref(false)
|
|
55
82
|
|
|
56
83
|
defineExpose({
|
|
57
84
|
focus: () => input.value?.focus(),
|
|
@@ -19,6 +19,7 @@ export { default as ProgressBar } from './ProgressBar.vue';
|
|
|
19
19
|
export { default as Spinner } from './Spinner.vue';
|
|
20
20
|
export { default as Skeleton } from './Skeleton.vue';
|
|
21
21
|
export { default as Dialog } from './Dialog.vue';
|
|
22
|
+
export { default as DialogPanel } from './DialogPanel.vue';
|
|
22
23
|
export { default as MixedChart } from './MixedChart.vue';
|
|
23
24
|
export { default as CountryFlag } from './CountryFlag.vue';
|
|
24
25
|
export { default as JsonViewer } from './JsonViewer.vue';
|
package/dist/spa/src/main.ts
CHANGED
|
@@ -18,6 +18,14 @@ app.use(router)
|
|
|
18
18
|
// get access to i18n instance outside components
|
|
19
19
|
initI18n(app);
|
|
20
20
|
|
|
21
|
+
// Chrome steps a focused number input on wheel, so scrolling the page with the cursor over the field
|
|
22
|
+
// silently changes a price or amount; blurring it first lets the page scroll and keeps the value
|
|
23
|
+
document.addEventListener('wheel', (e) => {
|
|
24
|
+
if (e.target instanceof HTMLInputElement && e.target.type === 'number' && e.target === document.activeElement) {
|
|
25
|
+
e.target.blur();
|
|
26
|
+
}
|
|
27
|
+
}, { passive: true });
|
|
28
|
+
|
|
21
29
|
|
|
22
30
|
/* IMPORTANT:ADMINFORTH CUSTOM USES */
|
|
23
31
|
|
|
@@ -32,11 +32,15 @@
|
|
|
32
32
|
class="af-login-modal overflow-y-auto flex flex-grow
|
|
33
33
|
overflow-x-hidden z-50 min-w-[350px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
|
34
34
|
<div class="relative p-4 w-full max-h-full max-w-[400px]">
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
<DialogPanel
|
|
36
|
+
class="af-login-popup af-login-modal-content max-w-none bg-lightLoginViewBackground dark:bg-darkLoginViewBackground"
|
|
37
|
+
:error="error"
|
|
38
|
+
:classes="{
|
|
39
|
+
header: 'af-login-modal-header',
|
|
40
|
+
body: 'af-login-modal-body',
|
|
41
|
+
}"
|
|
42
|
+
>
|
|
43
|
+
<template #title>
|
|
40
44
|
<template v-if="coreStore?.config?.loginPageInjections?.panelHeader.length && coreStore?.config?.loginPageInjections?.panelHeader.length > 0">
|
|
41
45
|
<component
|
|
42
46
|
v-for="(c, index) in coreStore?.config?.loginPageInjections?.panelHeader || []"
|
|
@@ -46,15 +50,13 @@
|
|
|
46
50
|
@update:setLoginParam="setLoginParam"
|
|
47
51
|
/>
|
|
48
52
|
</template>
|
|
49
|
-
<
|
|
53
|
+
<p v-else class="text-lg leading-6 font-semibold text-lightLoginViewTextColor dark:text-darkLoginViewTextColor">
|
|
50
54
|
{{ $t('Sign in to') }} {{ coreStore.config?.brandName }}
|
|
51
|
-
</
|
|
52
|
-
</
|
|
53
|
-
|
|
54
|
-
<div class="af-login-modal-body p-4 md:p-5">
|
|
55
|
-
<form class="space-y-4" @submit.prevent>
|
|
55
|
+
</p>
|
|
56
|
+
</template>
|
|
57
|
+
<form class="space-y-4" @submit.prevent>
|
|
56
58
|
<div>
|
|
57
|
-
<label for="username" class="block mb-2 text-sm font-medium text-
|
|
59
|
+
<label for="username" class="block mb-2 text-sm font-medium text-lightLoginViewTextColor dark:text-darkLoginViewTextColor">{{ $t('Your') }} {{ coreStore.config?.usernameFieldName?.toLowerCase() }}</label>
|
|
58
60
|
<Input
|
|
59
61
|
v-model="username"
|
|
60
62
|
autocomplete="username"
|
|
@@ -67,20 +69,13 @@
|
|
|
67
69
|
placeholder="name@company.com" required />
|
|
68
70
|
</div>
|
|
69
71
|
<div class="">
|
|
70
|
-
<label for="password" class="block mb-2 text-sm font-medium text-
|
|
72
|
+
<label for="password" class="block mb-2 text-sm font-medium text-lightLoginViewTextColor dark:text-darkLoginViewTextColor">{{ $t('Your password') }}</label>
|
|
71
73
|
<Input
|
|
72
74
|
v-model="password"
|
|
73
75
|
ref="passwordInput"
|
|
74
76
|
autocomplete="current-password"
|
|
75
77
|
@keydown.enter="login"
|
|
76
|
-
|
|
77
|
-
<template #rightIcon>
|
|
78
|
-
<button type="button" @click="showPw = !showPw" class="text-lightLoginViewSubTextColor dark:text-darkLoginViewSubTextColor">
|
|
79
|
-
<IconEye class="w-4 h-4" v-if="!showPw" />
|
|
80
|
-
<IconEyeOff class="w-4 h-4" v-else />
|
|
81
|
-
</button>
|
|
82
|
-
</template>
|
|
83
|
-
</Input>
|
|
78
|
+
type="password" passwordToggle name="password" id="password" placeholder="••••••••" class="w-full" required />
|
|
84
79
|
</div>
|
|
85
80
|
|
|
86
81
|
<div v-if="coreStore?.config?.rememberMeDuration"
|
|
@@ -123,10 +118,8 @@
|
|
|
123
118
|
@update:setLoginParam="setLoginParam"
|
|
124
119
|
@update:oauthRedirecting="oauthRedirecting = $event"
|
|
125
120
|
/>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</div>
|
|
129
|
-
<ErrorMessage v-if="error" :error="error" class="absolute left-4 right-4 rounded-t-none mb-0 shadow px-9" />
|
|
121
|
+
</form>
|
|
122
|
+
</DialogPanel>
|
|
130
123
|
</div>
|
|
131
124
|
</div>
|
|
132
125
|
|
|
@@ -140,11 +133,10 @@ import { getCustomComponent, formatComponent } from '@/utils';
|
|
|
140
133
|
import { onMounted, ref, computed } from 'vue';
|
|
141
134
|
import { useCoreStore } from '@/stores/core';
|
|
142
135
|
import { useUserStore } from '@/stores/user';
|
|
143
|
-
import {
|
|
136
|
+
import { IconInfo } from '@iconify-prerendered/vue-lucide';
|
|
144
137
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
145
138
|
import { useRouter, useRoute } from 'vue-router';
|
|
146
|
-
import { Button, Checkbox, Input, Spinner } from '@/afcl';
|
|
147
|
-
import ErrorMessage from '@/components/ErrorMessage.vue';
|
|
139
|
+
import { Button, Checkbox, DialogPanel, Input, Spinner } from '@/afcl';
|
|
148
140
|
|
|
149
141
|
const passwordInput = ref<InstanceType<typeof Input> | null>(null);
|
|
150
142
|
const usernameInput = ref<InstanceType<typeof Input> | null>(null);
|
|
@@ -161,7 +153,6 @@ const isSuccess = ref<boolean>(false);
|
|
|
161
153
|
const coreStore = useCoreStore();
|
|
162
154
|
const user = useUserStore();
|
|
163
155
|
|
|
164
|
-
const showPw = ref(false);
|
|
165
156
|
|
|
166
157
|
const error = ref(null);
|
|
167
158
|
const disableLoginButton = ref(false);
|