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.
@@ -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
+ }