@taruvi/navkit 0.0.36 → 0.0.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taruvi/navkit",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "main": "src/App.tsx",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -13,10 +13,10 @@
13
13
  "peerDependencies": {
14
14
  "@emotion/react": ">=11 <12",
15
15
  "@emotion/styled": ">=11 <12",
16
- "@fortawesome/fontawesome-svg-core": ">=6 <7",
17
- "@fortawesome/free-regular-svg-icons": ">=6 <7",
18
- "@fortawesome/free-solid-svg-icons": ">=6 <7",
19
- "@fortawesome/react-fontawesome": ">=0.2 <1",
16
+ "@fortawesome/fontawesome-svg-core": ">=6 <8",
17
+ "@fortawesome/free-regular-svg-icons": ">=6 <8",
18
+ "@fortawesome/free-solid-svg-icons": ">=6 <8",
19
+ "@fortawesome/react-fontawesome": ">=0.2 <4",
20
20
  "@mui/icons-material": ">=5 <8",
21
21
  "@mui/material": ">=5 <8",
22
22
  "react": ">=18 <20"
@@ -48,6 +48,6 @@
48
48
  "vitest": "^4.0.18"
49
49
  },
50
50
  "dependencies": {
51
- "@taruvi/sdk": "^1.3.4-beta.0"
51
+ "@taruvi/sdk": "^1.4.0"
52
52
  }
53
53
  }
package/src/App.styles.ts CHANGED
@@ -153,8 +153,8 @@ export const getAppStyles = (mode: ThemeMode) => {
153
153
  minHeight: 40,
154
154
  },
155
155
  appLauncherLogo: {
156
- width: 20,
157
- height: 20,
156
+ width: 23,
157
+ height: 23,
158
158
  },
159
159
  appLauncherHover: {
160
160
  display: 'flex',
package/src/App.tsx CHANGED
@@ -101,7 +101,7 @@ const NavkitContent = () => {
101
101
  <FontAwesomeIcon icon={["fas", "external-link-alt"]} style={{ fontSize: "10px" }} />
102
102
  </IconButton>
103
103
  </Box>
104
- <MattermostChat jwtToken={jwtToken} mattermostUrl={siteSettings['chat-url']} width="100%" height="100%" />
104
+ <MattermostChat jwtToken={jwtToken} mattermostUrl={siteSettings['chat-url']} loginId={userData?.email || ''} width="100%" height="100%" />
105
105
  </DraggableResizable>
106
106
  )}
107
107
  </>
@@ -12,7 +12,6 @@ const getInitialTheme = (): ThemeMode => {
12
12
  if (typeof window !== 'undefined') {
13
13
  const stored = localStorage.getItem(THEME_STORAGE_KEY)
14
14
  if (stored === 'dark' || stored === 'light') return stored
15
- if (window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark'
16
15
  }
17
16
  return 'light'
18
17
  }
@@ -30,6 +29,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName }: Nav
30
29
  'show-chat': false,
31
30
  'chat-url': '',
32
31
  frontendUrl: '',
32
+ enableDarkMode: false,
33
33
  })
34
34
  const appSettings = useRef<AppSettings>({})
35
35
 
@@ -104,6 +104,7 @@ export const NavkitProvider = ({ children, client, onThemeChange, appName }: Nav
104
104
  'chat-url': rawSettings['navkit.chat-url'] ?? '',
105
105
  frontendUrl: rawSettings['navkit.frontend-url'] ?? '',
106
106
  logo: rawSettings['navkit.logo'] ?? '',
107
+ enableDarkMode: rawSettings['navkit.enable-dark-mode'] ?? false,
107
108
  }
108
109
  if (isUserAuthenticated) {
109
110
  const userDataResponse = await auth.getCurrentUser()
@@ -6,6 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
6
6
  import { findIconDefinition, type IconName } from "@fortawesome/fontawesome-svg-core"
7
7
  import { useNavigation } from "../../NavkitContext"
8
8
  import { getAppLauncherStyles } from "./AppLauncher.styles"
9
+ import { typography } from "../../styles/variables"
9
10
 
10
11
 
11
12
  const isIconUrl = (icon: string): boolean => {
@@ -37,7 +38,11 @@ const AppLauncher = () => {
37
38
  return (
38
39
  <Card sx={styles.container}>
39
40
  <Search appsList={appsList} onSearchChange={handleSearchChange} />
40
- {filteredApps && filteredApps.length > 0 && (
41
+ {filteredApps.length === 0 ? (
42
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', py: 6 }}>
43
+ <Typography sx={{ color: styles.appName.color, fontSize: typography.sizes.sm }}>No apps available</Typography>
44
+ </Box>
45
+ ) : (
41
46
  <Box sx={styles.scrollContainer}>
42
47
  <Grid container spacing={0}>
43
48
  {filteredApps.map((app) => {
@@ -1,9 +1,10 @@
1
1
  import { useEffect, useRef, useState } from 'react'
2
- import { Box, Badge } from '@mui/material'
2
+ import { Box, Badge, CircularProgress, Typography } from '@mui/material'
3
3
 
4
4
  interface MattermostChatProps {
5
5
  jwtToken: string
6
- mattermostUrl: string // Mattermost server URL (must be passed from parent app)
6
+ mattermostUrl: string
7
+ loginId: string
7
8
  onNotification?: (count: number) => void
8
9
  onUrlClick?: (url: string) => void
9
10
  width?: string | number
@@ -24,6 +25,7 @@ interface NotifyMessage {
24
25
  const MattermostChat = ({
25
26
  jwtToken,
26
27
  mattermostUrl,
28
+ loginId,
27
29
  onNotification,
28
30
  onUrlClick,
29
31
  width = '100%',
@@ -32,17 +34,65 @@ const MattermostChat = ({
32
34
  const iframeRef = useRef<HTMLIFrameElement>(null)
33
35
  const [unreadCount, setUnreadCount] = useState(0)
34
36
  const [isWindowFocused, setIsWindowFocused] = useState(true)
37
+ const [authToken, setAuthToken] = useState<string | null>(null)
38
+ const [authError, setAuthError] = useState<string | null>(null)
39
+ const [isLoading, setIsLoading] = useState(true)
40
+
41
+ // Authenticate with Mattermost on mount
42
+ useEffect(() => {
43
+ const login = async () => {
44
+ try {
45
+ const res = await fetch(`${mattermostUrl.replace(/\/$/, '')}/api/v4/users/login`, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
48
+ credentials: 'omit',
49
+ body: JSON.stringify({ login_id: loginId, password: jwtToken, token: '', deviceId: '' })
50
+ })
51
+
52
+ if (!res.ok) {
53
+ setAuthError(`Login failed (${res.status})`)
54
+ setIsLoading(false)
55
+ return
56
+ }
57
+
58
+ const token = res.headers.get('Token')
59
+ if (token) {
60
+ setAuthToken(token)
61
+ } else {
62
+ setAuthError('No token in response')
63
+ }
64
+ } catch (e: any) {
65
+ setAuthError(e.message || 'Login failed')
66
+ }
67
+ setIsLoading(false)
68
+ }
69
+
70
+ if (loginId && jwtToken) login()
71
+ else { setAuthError('Missing credentials'); setIsLoading(false) }
72
+ }, [mattermostUrl, loginId, jwtToken])
73
+
74
+ // Inject auth token into iframe via postMessage once loaded
75
+ useEffect(() => {
76
+ if (!authToken || !iframeRef.current) return
77
+
78
+ const handleIframeLoad = () => {
79
+ // Set the auth cookie/token in the iframe context
80
+ iframeRef.current?.contentWindow?.postMessage(
81
+ { type: 'token', data: authToken },
82
+ new URL(mattermostUrl).origin
83
+ )
84
+ }
85
+
86
+ const iframe = iframeRef.current
87
+ iframe.addEventListener('load', handleIframeLoad)
88
+ return () => iframe.removeEventListener('load', handleIframeLoad)
89
+ }, [authToken, mattermostUrl])
90
+
35
91
  useEffect(() => {
36
- // Handle postMessage events from Mattermost iframe
37
92
  const handleMessage = (event: MessageEvent<NotifyMessage>) => {
38
- // Validate origin for security
39
93
  const mattermostOrigin = new URL(mattermostUrl).origin
40
- if (event.origin !== mattermostOrigin) {
41
- console.warn('Received message from untrusted origin:', event.origin)
42
- return
43
- }
94
+ if (event.origin !== mattermostOrigin) return
44
95
 
45
- // Handle Notify events
46
96
  if (event.data?.event === 'Notify') {
47
97
  if (!isWindowFocused) {
48
98
  setUnreadCount(prev => {
@@ -51,31 +101,16 @@ const MattermostChat = ({
51
101
  return newCount
52
102
  })
53
103
  }
54
-
55
- // Handle URL clicks if provided
56
- if (event.data.data?.url && onUrlClick) {
57
- onUrlClick(event.data.data.url)
58
- }
104
+ if (event.data.data?.url && onUrlClick) onUrlClick(event.data.data.url)
59
105
  }
60
106
  }
61
107
 
62
- // Handle window focus/blur to track unread messages
63
- const handleFocus = () => {
64
- setIsWindowFocused(true)
65
- setUnreadCount(0)
66
- onNotification?.(0)
67
- }
108
+ const handleFocus = () => { setIsWindowFocused(true); setUnreadCount(0); onNotification?.(0) }
109
+ const handleBlur = () => setIsWindowFocused(false)
68
110
 
69
- const handleBlur = () => {
70
- setIsWindowFocused(false)
71
- }
72
-
73
- // Add event listeners
74
111
  window.addEventListener('message', handleMessage)
75
112
  window.addEventListener('focus', handleFocus)
76
113
  window.addEventListener('blur', handleBlur)
77
-
78
- // Cleanup event listeners on unmount
79
114
  return () => {
80
115
  window.removeEventListener('message', handleMessage)
81
116
  window.removeEventListener('focus', handleFocus)
@@ -83,33 +118,34 @@ const MattermostChat = ({
83
118
  }
84
119
  }, [mattermostUrl, isWindowFocused, onNotification, onUrlClick])
85
120
 
86
- // Construct iframe URL with JWT authentication
87
- const iframeUrl = `${mattermostUrl.replace(/\/$/, '')}/login`
121
+ if (isLoading) {
122
+ return (
123
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width, height }}>
124
+ <CircularProgress size={32} />
125
+ </Box>
126
+ )
127
+ }
128
+
129
+ if (authError) {
130
+ return (
131
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width, height }}>
132
+ <Typography color="error">{authError}</Typography>
133
+ </Box>
134
+ )
135
+ }
136
+
137
+ // Load Mattermost with the auth token as a cookie header via the access_token param
138
+ const iframeUrl = `${mattermostUrl.replace(/\/$/, '')}?access_token=${authToken}`
88
139
 
89
140
  return (
90
- <Box
91
- sx={{
92
- position: 'relative',
93
- width,
94
- height,
95
- overflow: 'hidden'
96
- }}
97
- >
141
+ <Box sx={{ position: 'relative', width, height, overflow: 'hidden' }}>
98
142
  {unreadCount > 0 && (
99
143
  <Badge
100
144
  badgeContent={unreadCount}
101
145
  color="error"
102
146
  sx={{
103
- position: 'absolute',
104
- top: 16,
105
- right: 16,
106
- zIndex: 1000,
107
- '& .MuiBadge-badge': {
108
- fontSize: '1rem',
109
- height: '28px',
110
- minWidth: '28px',
111
- borderRadius: '14px'
112
- }
147
+ position: 'absolute', top: 16, right: 16, zIndex: 1000,
148
+ '& .MuiBadge-badge': { fontSize: '1rem', height: '28px', minWidth: '28px', borderRadius: '14px' }
113
149
  }}
114
150
  />
115
151
  )}
@@ -118,12 +154,7 @@ const MattermostChat = ({
118
154
  src={iframeUrl}
119
155
  title="Mattermost Chat"
120
156
  allow="clipboard-read; clipboard-write"
121
- style={{
122
- width: '100%',
123
- height: '100%',
124
- border: 'none',
125
- display: 'block'
126
- }}
157
+ style={{ width: '100%', height: '100%', border: 'none', display: 'block' }}
127
158
  />
128
159
  </Box>
129
160
  )
@@ -10,14 +10,7 @@ const ProfileIcon = () => (
10
10
  </SvgIcon>
11
11
  )
12
12
 
13
- interface UserData {
14
- full_name?: string
15
- first_name?: string
16
- last_name?: string
17
- pfp?: string
18
- username?: string
19
- email?: string
20
- }
13
+ import type { UserData } from '../../types'
21
14
 
22
15
  interface ProfileProps {
23
16
  userData: UserData
@@ -31,8 +24,8 @@ const Profile = (props: ProfileProps) => {
31
24
 
32
25
  return (
33
26
  <Box sx={styles.trigger} onClick={onClick}>
34
- {userData.pfp ? (
35
- <Box component="img" src={userData.pfp} sx={{ width: 23, height: 23, borderRadius: '50%' }} />
27
+ {userData.icon_url ? (
28
+ <Box component="img" src={userData.icon_url} sx={{ width: 23, height: 23, borderRadius: '50%' }} />
36
29
  ) : (
37
30
  <ProfileIcon />
38
31
  )}
@@ -6,7 +6,7 @@ import { getProfileStyles } from './Profile.styles'
6
6
  import { useNavigation } from '../../NavkitContext'
7
7
 
8
8
  const ProfileMenu = () => {
9
- const { auth, themeMode, toggleTheme, userData } = useNavigation()
9
+ const { auth, themeMode, toggleTheme, userData, siteSettings } = useNavigation()
10
10
  const styles = getProfileStyles(themeMode)
11
11
 
12
12
  // const handlePreferences = () => {
@@ -14,6 +14,7 @@ const ProfileMenu = () => {
14
14
  // }
15
15
 
16
16
  const handleLogout = async () => {
17
+ localStorage.removeItem('navkit-theme-mode')
17
18
  await auth.logout()
18
19
  }
19
20
 
@@ -35,6 +36,7 @@ const ProfileMenu = () => {
35
36
  </Typography>
36
37
  </MenuItem> */}
37
38
 
39
+ {siteSettings.enableDarkMode && (
38
40
  <MenuItem onClick={toggleTheme} sx={styles.menuItem}>
39
41
  {themeMode === 'dark' ? (
40
42
  <LightModeOutlined sx={styles.iconStyle} />
@@ -45,6 +47,7 @@ const ProfileMenu = () => {
45
47
  {themeMode === 'dark' ? 'Light Mode' : 'Dark Mode'}
46
48
  </Typography>
47
49
  </MenuItem>
50
+ )}
48
51
 
49
52
  <MenuItem onClick={handleLogout} sx={styles.menuItem}>
50
53
  <FontAwesomeIcon
@@ -24,7 +24,7 @@ export const shortcutsStyles = {
24
24
  },
25
25
  },
26
26
  iconStyle: {
27
- fontSize: '20px',
27
+ fontSize: '23px',
28
28
  color: colours.text.secondary,
29
29
  },
30
30
  }
package/src/types.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { ReactNode } from "react"
2
- import type { Client, Auth } from "@taruvi/sdk"
2
+ import type { Client, Auth, UserData, UserRole } from "@taruvi/sdk"
3
3
  import type { ThemeMode } from "./styles/variables"
4
4
 
5
+ export type { UserData, UserRole }
6
+
5
7
  export interface AppData {
6
8
  appname: string,
7
9
  icon: string,
@@ -9,41 +11,13 @@ export interface AppData {
9
11
  id: string
10
12
  }
11
13
 
12
- export interface UserRole {
13
- name: string
14
- slug: string
15
- type: string
16
- app_slug?: string
17
- source?: string
18
- }
19
-
20
- export interface UserData {
21
- id?: number
22
- username?: string
23
- email?: string
24
- first_name?: string
25
- last_name?: string
26
- full_name?: string
27
- pfp?: string
28
- is_active?: boolean
29
- is_staff?: boolean
30
- is_superuser?: boolean
31
- is_deleted?: boolean
32
- date_joined?: string
33
- last_login?: string
34
- groups?: { id: number; name: string }[]
35
- user_permissions?: { id: number; name: string; codename: string; content_type: string }[]
36
- attributes?: Record<string, unknown>
37
- missing_attributes?: string[]
38
- roles?: UserRole[]
39
- }
40
-
41
14
  export interface SiteSettings {
42
15
  shortcuts?: AppData[]
43
16
  logo?: string
44
17
  'show-chat': boolean
45
18
  'chat-url': string
46
19
  frontendUrl: string
20
+ enableDarkMode: boolean
47
21
  }
48
22
 
49
23
  export interface AppSettings {