mgv-backoffice 1.19.0 → 1.20.0
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/README.md +142 -2
- package/dist/components/BaseGoogleSignInButton.vue.d.ts +22 -0
- package/dist/components/BaseLoginForm.vue.d.ts +51 -0
- package/dist/components/BaseNotificationPanel.vue.d.ts +19 -0
- package/dist/components/BaseSidebar.vue.d.ts +9 -3
- package/dist/composables/useNotifications.d.ts +29 -0
- package/dist/composables/useSidebarCollapse.d.ts +22 -0
- package/dist/index.d.ts +8 -0
- package/dist/types/auth.d.ts +11 -0
- package/dist/types/notification.d.ts +20 -0
- package/dist/ui-lib.css +1 -1
- package/dist/ui-lib.js +1082 -665
- package/dist/ui-lib.umd.cjs +1 -1
- package/package.json +1 -1
- package/src/components/BaseAppLayout.vue +4 -1
- package/src/components/BaseGoogleSignInButton.vue +83 -0
- package/src/components/BaseLoginForm.test.ts +52 -0
- package/src/components/BaseLoginForm.vue +204 -0
- package/src/components/BaseNotificationPanel.vue +183 -0
- package/src/components/BaseSidebar.vue +140 -31
- package/src/composables/useNotifications.test.ts +64 -0
- package/src/composables/useNotifications.ts +81 -0
- package/src/composables/useSidebarCollapse.ts +64 -0
- package/src/index.ts +8 -0
- package/src/types/auth.ts +11 -0
- package/src/types/notification.ts +20 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { useNotifications } from './useNotifications'
|
|
3
|
+
|
|
4
|
+
describe('useNotifications', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Singleton state persists across tests — reset it each time.
|
|
7
|
+
const { clear, closePanel } = useNotifications()
|
|
8
|
+
clear()
|
|
9
|
+
closePanel()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('counts only unread notifications', () => {
|
|
13
|
+
const { setNotifications, unreadCount } = useNotifications()
|
|
14
|
+
setNotifications([
|
|
15
|
+
{ id: 1, title: 'a' },
|
|
16
|
+
{ id: 2, title: 'b', read: true },
|
|
17
|
+
{ id: 3, title: 'c' },
|
|
18
|
+
])
|
|
19
|
+
expect(unreadCount.value).toBe(2)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('markAllRead clears the unread count', () => {
|
|
23
|
+
const { setNotifications, markAllRead, unreadCount } = useNotifications()
|
|
24
|
+
setNotifications([
|
|
25
|
+
{ id: 1, title: 'a' },
|
|
26
|
+
{ id: 2, title: 'b' },
|
|
27
|
+
])
|
|
28
|
+
expect(unreadCount.value).toBe(2)
|
|
29
|
+
markAllRead()
|
|
30
|
+
expect(unreadCount.value).toBe(0)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('markRead marks a single notification read', () => {
|
|
34
|
+
const { setNotifications, markRead, unreadCount } = useNotifications()
|
|
35
|
+
setNotifications([
|
|
36
|
+
{ id: 1, title: 'a' },
|
|
37
|
+
{ id: 2, title: 'b' },
|
|
38
|
+
])
|
|
39
|
+
markRead(1)
|
|
40
|
+
expect(unreadCount.value).toBe(1)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('add prepends newest-first and remove deletes by id', () => {
|
|
44
|
+
const { add, remove, notifications } = useNotifications()
|
|
45
|
+
add({ id: 1, title: 'first' })
|
|
46
|
+
add({ id: 2, title: 'second' })
|
|
47
|
+
expect(notifications.value.map((n) => n.id)).toEqual([2, 1])
|
|
48
|
+
remove(1)
|
|
49
|
+
expect(notifications.value.map((n) => n.id)).toEqual([2])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('toggles and sets the panel open state', () => {
|
|
53
|
+
const { open, openPanel, closePanel, togglePanel } = useNotifications()
|
|
54
|
+
expect(open.value).toBe(false)
|
|
55
|
+
openPanel()
|
|
56
|
+
expect(open.value).toBe(true)
|
|
57
|
+
togglePanel()
|
|
58
|
+
expect(open.value).toBe(false)
|
|
59
|
+
togglePanel()
|
|
60
|
+
expect(open.value).toBe(true)
|
|
61
|
+
closePanel()
|
|
62
|
+
expect(open.value).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import type { NotificationItem } from '../types/notification'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared notification state for the sidebar bell + `BaseNotificationPanel`.
|
|
6
|
+
*
|
|
7
|
+
* Module-scoped singleton (mirrors useMobileSidebar / useToast) so the
|
|
8
|
+
* sidebar's unread badge, the bell's open/close toggle, and the panel all
|
|
9
|
+
* read and mutate the same source. The consuming app owns the data: call
|
|
10
|
+
* `setNotifications()` (or `add()`) to feed it in.
|
|
11
|
+
*
|
|
12
|
+
* There's only ever one notification surface per app, so a singleton is
|
|
13
|
+
* the right shape here.
|
|
14
|
+
*/
|
|
15
|
+
const notifications = ref<NotificationItem[]>([])
|
|
16
|
+
const open = ref(false)
|
|
17
|
+
|
|
18
|
+
const unreadCount = computed(
|
|
19
|
+
() => notifications.value.filter((n) => !n.read).length,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
export function useNotifications() {
|
|
23
|
+
function openPanel() {
|
|
24
|
+
open.value = true
|
|
25
|
+
}
|
|
26
|
+
function closePanel() {
|
|
27
|
+
open.value = false
|
|
28
|
+
}
|
|
29
|
+
function togglePanel() {
|
|
30
|
+
open.value = !open.value
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Replace the whole list (typical after a fetch). */
|
|
34
|
+
function setNotifications(items: NotificationItem[]) {
|
|
35
|
+
notifications.value = items
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Prepend a single notification (newest first). */
|
|
39
|
+
function add(item: NotificationItem) {
|
|
40
|
+
notifications.value = [item, ...notifications.value]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Remove one by id. */
|
|
44
|
+
function remove(id: NotificationItem['id']) {
|
|
45
|
+
notifications.value = notifications.value.filter((n) => n.id !== id)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Mark a single notification read. */
|
|
49
|
+
function markRead(id: NotificationItem['id']) {
|
|
50
|
+
notifications.value = notifications.value.map((n) =>
|
|
51
|
+
n.id === id ? { ...n, read: true } : n,
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Mark every notification read (clears the badge). */
|
|
56
|
+
function markAllRead() {
|
|
57
|
+
notifications.value = notifications.value.map((n) =>
|
|
58
|
+
n.read ? n : { ...n, read: true },
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Remove every notification. */
|
|
63
|
+
function clear() {
|
|
64
|
+
notifications.value = []
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
notifications,
|
|
69
|
+
unreadCount,
|
|
70
|
+
open,
|
|
71
|
+
openPanel,
|
|
72
|
+
closePanel,
|
|
73
|
+
togglePanel,
|
|
74
|
+
setNotifications,
|
|
75
|
+
add,
|
|
76
|
+
remove,
|
|
77
|
+
markRead,
|
|
78
|
+
markAllRead,
|
|
79
|
+
clear,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ref, watch, effectScope } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared collapsed/expanded state for the desktop sidebar.
|
|
5
|
+
*
|
|
6
|
+
* Module-scoped singleton (mirrors useMobileSidebar) so `BaseSidebar` and
|
|
7
|
+
* the page layout (`BaseAppLayout`, which offsets `<main>` by the sidebar
|
|
8
|
+
* width) stay in sync. When collapsed, the sidebar shrinks to an icon-only
|
|
9
|
+
* rail on desktop.
|
|
10
|
+
*
|
|
11
|
+
* The preference is persisted to localStorage. The first consumer can pass
|
|
12
|
+
* an options object to override the key; subsequent callers reuse the same
|
|
13
|
+
* singleton and the option is only consulted on the first call.
|
|
14
|
+
*/
|
|
15
|
+
export interface UseSidebarCollapseOptions {
|
|
16
|
+
/** localStorage key used to persist the preference. */
|
|
17
|
+
storageKey?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const collapsed = ref(false)
|
|
21
|
+
|
|
22
|
+
let initialized = false
|
|
23
|
+
let configuredStorageKey = 'mgv-sidebar-collapsed'
|
|
24
|
+
|
|
25
|
+
function initialize(storageKey: string) {
|
|
26
|
+
if (initialized) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
initialized = true
|
|
30
|
+
configuredStorageKey = storageKey
|
|
31
|
+
|
|
32
|
+
if (typeof window !== 'undefined') {
|
|
33
|
+
const saved = window.localStorage?.getItem(configuredStorageKey)
|
|
34
|
+
if (saved) {
|
|
35
|
+
collapsed.value = saved === 'true'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Detached scope so the persistence watcher lives for the app's
|
|
39
|
+
// lifetime and is NOT torn down when the first consuming component
|
|
40
|
+
// unmounts.
|
|
41
|
+
const scope = effectScope(true)
|
|
42
|
+
scope.run(() => {
|
|
43
|
+
watch(collapsed, (value) => {
|
|
44
|
+
window.localStorage?.setItem(configuredStorageKey, String(value))
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function useSidebarCollapse(options?: UseSidebarCollapseOptions) {
|
|
51
|
+
initialize(options?.storageKey ?? configuredStorageKey)
|
|
52
|
+
|
|
53
|
+
function collapse() {
|
|
54
|
+
collapsed.value = true
|
|
55
|
+
}
|
|
56
|
+
function expand() {
|
|
57
|
+
collapsed.value = false
|
|
58
|
+
}
|
|
59
|
+
function toggle() {
|
|
60
|
+
collapsed.value = !collapsed.value
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { collapsed, collapse, expand, toggle }
|
|
64
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -30,6 +30,9 @@ export { default as BasePageHeader } from './components/BasePageHeader.vue'
|
|
|
30
30
|
export { default as BaseToolbarButton } from './components/BaseToolbarButton.vue'
|
|
31
31
|
export { default as BaseActionButton } from './components/BaseActionButton.vue'
|
|
32
32
|
export { default as BaseCopyButton } from './components/BaseCopyButton.vue'
|
|
33
|
+
export { default as BaseGoogleSignInButton } from './components/BaseGoogleSignInButton.vue'
|
|
34
|
+
export { default as BaseLoginForm } from './components/BaseLoginForm.vue'
|
|
35
|
+
export { default as BaseNotificationPanel } from './components/BaseNotificationPanel.vue'
|
|
33
36
|
|
|
34
37
|
// Composables
|
|
35
38
|
export { useTheme, initTheme } from './composables/useTheme'
|
|
@@ -40,6 +43,9 @@ export { useEscapeKey } from './composables/useEscapeKey'
|
|
|
40
43
|
export { useDebouncedRef } from './composables/useDebounce'
|
|
41
44
|
export { useToast } from './composables/useToast'
|
|
42
45
|
export { useMobileSidebar } from './composables/useMobileSidebar'
|
|
46
|
+
export { useSidebarCollapse } from './composables/useSidebarCollapse'
|
|
47
|
+
export type { UseSidebarCollapseOptions } from './composables/useSidebarCollapse'
|
|
48
|
+
export { useNotifications } from './composables/useNotifications'
|
|
43
49
|
|
|
44
50
|
// Enums
|
|
45
51
|
export { AlertEnum } from './enums/AlertEnum'
|
|
@@ -57,6 +63,8 @@ export { PositioningEnum } from './enums/PositioningEnum'
|
|
|
57
63
|
export type { BreadCrumb } from './components/BaseBreadcrumb.vue'
|
|
58
64
|
export type { NavItem, NavSection } from './types/sidebar'
|
|
59
65
|
export type { EntityPickerItem } from './types/entityPicker'
|
|
66
|
+
export type { LoginCredentials } from './types/auth'
|
|
67
|
+
export type { NotificationItem } from './types/notification'
|
|
60
68
|
|
|
61
69
|
// Utils
|
|
62
70
|
export { getBaseColor, getBaseColorOf } from './utils/util'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credentials emitted by `BaseLoginForm` on submit.
|
|
3
|
+
*
|
|
4
|
+
* `remember` is only present when the form is rendered with the
|
|
5
|
+
* "remember me" checkbox enabled.
|
|
6
|
+
*/
|
|
7
|
+
export interface LoginCredentials {
|
|
8
|
+
email: string
|
|
9
|
+
password: string
|
|
10
|
+
remember?: boolean
|
|
11
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single entry rendered in `BaseNotificationPanel` and counted by the
|
|
3
|
+
* sidebar's unread badge.
|
|
4
|
+
*
|
|
5
|
+
* • `id` — stable key, used for list rendering and mark/remove ops.
|
|
6
|
+
* • `title` — bold headline line.
|
|
7
|
+
* • `message` — optional secondary line.
|
|
8
|
+
* • `time` — optional pre-formatted timestamp (e.g. "2h ago"). The
|
|
9
|
+
* library does not format dates for you.
|
|
10
|
+
* • `read` — unread entries are highlighted and count toward the badge.
|
|
11
|
+
* • `type` — drives the small status dot colour.
|
|
12
|
+
*/
|
|
13
|
+
export interface NotificationItem {
|
|
14
|
+
id: string | number
|
|
15
|
+
title: string
|
|
16
|
+
message?: string
|
|
17
|
+
time?: string
|
|
18
|
+
read?: boolean
|
|
19
|
+
type?: 'info' | 'success' | 'warning' | 'error'
|
|
20
|
+
}
|