@taruvi/navkit 0.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/.claude/settings.local.json +9 -0
- package/README.md +757 -0
- package/adr.md +1414 -0
- package/eslint.config.js +23 -0
- package/index.html +13 -0
- package/package.json +39 -0
- package/src/App.styles.ts +65 -0
- package/src/App.tsx +111 -0
- package/src/NavkitContext.tsx +152 -0
- package/src/assets/logo-text.png +0 -0
- package/src/assets/nopfp.png +0 -0
- package/src/assets/taruvi-logo.png +0 -0
- package/src/components/AppLauncher/AppLauncher.styles.ts +59 -0
- package/src/components/AppLauncher/AppLauncher.tsx +74 -0
- package/src/components/MattermostChat/MattermostChat.tsx +132 -0
- package/src/components/MattermostChat/README.md +227 -0
- package/src/components/Profile/Profile.styles.ts +59 -0
- package/src/components/Profile/Profile.tsx +36 -0
- package/src/components/Profile/ProfileMenu.tsx +42 -0
- package/src/components/Search/Search.styles.ts +7 -0
- package/src/components/Search/Search.tsx +51 -0
- package/src/components/Shortucts/Shortcuts.styles.ts +29 -0
- package/src/components/Shortucts/Shortcuts.tsx +72 -0
- package/src/components/Shortucts/ShortcutsMenu.styles.ts +34 -0
- package/src/components/Shortucts/ShortcutsMenu.tsx +62 -0
- package/src/components/index.ts +8 -0
- package/src/styles/commonStyles.ts +42 -0
- package/src/styles/variables.ts +61 -0
- package/src/types.ts +38 -0
- package/tsconfig.app.json +28 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +13 -0
package/eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs['recommended-latest'],
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
])
|
package/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>taruvi-navkit</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/App.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@taruvi/navkit",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "src/App.tsx",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview"
|
|
11
|
+
},
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@emotion/react": "^11.0.0",
|
|
14
|
+
"@emotion/styled": "^11.0.0",
|
|
15
|
+
"@fortawesome/fontawesome-svg-core": "^6.0.0",
|
|
16
|
+
"@fortawesome/free-regular-svg-icons": "^6.0.0",
|
|
17
|
+
"@fortawesome/free-solid-svg-icons": "^6.0.0",
|
|
18
|
+
"@fortawesome/react-fontawesome": "^0.2.0",
|
|
19
|
+
"@mui/material": "^5.0.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@taruvi/sdk": "^1.0.7",
|
|
23
|
+
"react": "^19.1.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@eslint/js": "^9.36.0",
|
|
27
|
+
"@types/node": "^24.6.0",
|
|
28
|
+
"@types/react": "^19.1.16",
|
|
29
|
+
"@vitejs/plugin-react": "^5.0.4",
|
|
30
|
+
"babel-plugin-react-compiler": "^19.1.0-rc.3",
|
|
31
|
+
"eslint": "^9.36.0",
|
|
32
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
33
|
+
"eslint-plugin-react-refresh": "^0.4.22",
|
|
34
|
+
"globals": "^16.4.0",
|
|
35
|
+
"typescript": "~5.9.3",
|
|
36
|
+
"typescript-eslint": "^8.45.0",
|
|
37
|
+
"vite": "^7.1.7"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { colours, dimensions } from './styles/variables'
|
|
2
|
+
|
|
3
|
+
export const appStyles = {
|
|
4
|
+
appBar: {
|
|
5
|
+
height: dimensions.navHeight,
|
|
6
|
+
},
|
|
7
|
+
toolbar: {
|
|
8
|
+
backgroundColor: colours.bg.white,
|
|
9
|
+
},
|
|
10
|
+
leftSection: {
|
|
11
|
+
display: 'flex',
|
|
12
|
+
flexGrow: 1,
|
|
13
|
+
alignItems: 'center',
|
|
14
|
+
justifyContent: 'start',
|
|
15
|
+
gap: '10px',
|
|
16
|
+
},
|
|
17
|
+
logo: {
|
|
18
|
+
maxHeight: '40px',
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
},
|
|
21
|
+
defaultLogo: {
|
|
22
|
+
maxHeight: '22px',
|
|
23
|
+
cursor: 'pointer',
|
|
24
|
+
},
|
|
25
|
+
rightSection: {
|
|
26
|
+
display: 'flex',
|
|
27
|
+
gap: '10px',
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
},
|
|
30
|
+
modal: {
|
|
31
|
+
display: 'flex',
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
justifyContent: 'center',
|
|
34
|
+
},
|
|
35
|
+
chatCard: {
|
|
36
|
+
width: '90vw',
|
|
37
|
+
maxWidth: '1200px',
|
|
38
|
+
height: '80vh',
|
|
39
|
+
outline: 'none',
|
|
40
|
+
overflow: 'hidden',
|
|
41
|
+
position: 'relative' as const,
|
|
42
|
+
},
|
|
43
|
+
chatHeader: {
|
|
44
|
+
position: 'absolute' as const,
|
|
45
|
+
top: 0,
|
|
46
|
+
right: 0,
|
|
47
|
+
zIndex: 1400,
|
|
48
|
+
padding: '8px',
|
|
49
|
+
},
|
|
50
|
+
chatOpenButton: {
|
|
51
|
+
backgroundColor: colours.bg.white,
|
|
52
|
+
'&:hover': {
|
|
53
|
+
backgroundColor: colours.bg.light,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
iconStyle: {
|
|
57
|
+
cursor: 'pointer',
|
|
58
|
+
fontSize: '20px',
|
|
59
|
+
},
|
|
60
|
+
backdrop: {
|
|
61
|
+
position: "absolute" as const,
|
|
62
|
+
inset: 0,
|
|
63
|
+
zIndex: 10,
|
|
64
|
+
},
|
|
65
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import AppBar from "@mui/material/AppBar"
|
|
2
|
+
import Toolbar from "@mui/material/Toolbar"
|
|
3
|
+
import IconButton from "@mui/material/IconButton"
|
|
4
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
5
|
+
import { useState } from "react"
|
|
6
|
+
import { AppLauncher, Profile, ProfileMenu, Shortcuts, ShortcutsMenu, MattermostChat } from "./components"
|
|
7
|
+
import { Box, Modal, Card } from "@mui/material"
|
|
8
|
+
import { appStyles } from "./App.styles"
|
|
9
|
+
import { NavkitProvider, useNavigation } from "./NavkitContext"
|
|
10
|
+
import defaultLogo from "./assets/taruvi-logo.png"
|
|
11
|
+
|
|
12
|
+
const NavkitContent = () => {
|
|
13
|
+
const { isUserAuthenticated, siteSettings, userData, appsList, jwtToken } = useNavigation()
|
|
14
|
+
const [showAppLauncher, setShowAppLauncher] = useState<boolean>(false)
|
|
15
|
+
const [showProfileMenu, setShowProfileMenu] = useState<boolean>(false)
|
|
16
|
+
const [showShortcutsMenu, setShowShortcutsMenu] = useState<boolean>(false)
|
|
17
|
+
const [showChat, setShowChat] = useState<boolean>(false)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<>
|
|
21
|
+
{(showAppLauncher || showProfileMenu || showShortcutsMenu) && (
|
|
22
|
+
<div
|
|
23
|
+
style={appStyles.backdrop}
|
|
24
|
+
onClick={() => {
|
|
25
|
+
setShowAppLauncher(false)
|
|
26
|
+
setShowProfileMenu(false)
|
|
27
|
+
setShowShortcutsMenu(false)
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
)}
|
|
31
|
+
<AppBar position="static" sx={appStyles.appBar}>
|
|
32
|
+
<Toolbar sx={appStyles.toolbar}>
|
|
33
|
+
<Box component={"div"} sx={appStyles.leftSection}>
|
|
34
|
+
{isUserAuthenticated &&
|
|
35
|
+
<IconButton onClick={() => setShowAppLauncher(!showAppLauncher)}>
|
|
36
|
+
<FontAwesomeIcon icon={["fas", "th"]} style={appStyles.iconStyle} />
|
|
37
|
+
</IconButton>
|
|
38
|
+
}
|
|
39
|
+
<img
|
|
40
|
+
style={(siteSettings?.logo && siteSettings.logo !== '') ? appStyles.logo : appStyles.defaultLogo}
|
|
41
|
+
src={(siteSettings?.logo && siteSettings.logo !== '') ? siteSettings.logo : defaultLogo}
|
|
42
|
+
onClick={() => window.location.href = siteSettings.frontendUrl || '/desk'}
|
|
43
|
+
/>
|
|
44
|
+
</Box>
|
|
45
|
+
<Box component={'div'} sx={appStyles.rightSection}>
|
|
46
|
+
<Shortcuts
|
|
47
|
+
showChat={setShowChat}
|
|
48
|
+
onMenuToggle={() => setShowShortcutsMenu(!showShortcutsMenu)}
|
|
49
|
+
/>
|
|
50
|
+
{isUserAuthenticated && userData && (
|
|
51
|
+
<Profile userData={userData} onClick={() => setShowProfileMenu(!showProfileMenu)} />
|
|
52
|
+
)}
|
|
53
|
+
</Box>
|
|
54
|
+
</Toolbar>
|
|
55
|
+
</AppBar>
|
|
56
|
+
|
|
57
|
+
{showAppLauncher && (
|
|
58
|
+
<Box>
|
|
59
|
+
<AppLauncher />
|
|
60
|
+
</Box>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{showProfileMenu && (
|
|
64
|
+
<Box>
|
|
65
|
+
<ProfileMenu />
|
|
66
|
+
</Box>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{showShortcutsMenu && (
|
|
70
|
+
<Box>
|
|
71
|
+
<ShortcutsMenu showChat={setShowChat} />
|
|
72
|
+
</Box>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{showChat && siteSettings.mattermostUrl && (
|
|
76
|
+
<Modal
|
|
77
|
+
open={showChat}
|
|
78
|
+
onClose={() => setShowChat(false)}
|
|
79
|
+
sx={appStyles.modal}
|
|
80
|
+
>
|
|
81
|
+
<Card sx={appStyles.chatCard}>
|
|
82
|
+
<Box sx={appStyles.chatHeader}>
|
|
83
|
+
<IconButton
|
|
84
|
+
onClick={() => window.open(siteSettings.mattermostUrl, '_blank')}
|
|
85
|
+
sx={appStyles.chatOpenButton}
|
|
86
|
+
>
|
|
87
|
+
<FontAwesomeIcon icon={["fas", "external-link-alt"]} style={{ fontSize: '10px' }} />
|
|
88
|
+
</IconButton>
|
|
89
|
+
</Box>
|
|
90
|
+
<MattermostChat
|
|
91
|
+
jwtToken={"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3NjM1Mzk1NjUsImp0aSI6IjFyMEVBZnNJODNvYzg5Vjd4ellUbTVIU2wzZGdkcWRkVFNUdFMybXdzcUE9IiwibmJmIjoxNzYzNTM5NTY1LCJleHAiOjE3NjM2MTE1NjUsImRhdGEiOnsidXNlcm5hbWUiOiJjdXJyYW5jZG9kZGFiZWxlIiwiYWNjb3VudElkIjozLCJzZXNzaW9uSWQiOiI2ZjE4YTgwYy1iMWZiLTRkMzEtODI4My0yOTJhYmNmNzExZDEifSwiaXNzIjoiZW9zLXBsYXRmb3JtIiwiYXVkIjoiaHR0cHM6Ly90YXJ1dmkuZW94dmFudGFnZS5jb206NDQzIn0.XhOHyHj6LZixoh084IGdyb636hLsUereUDd2_oaeyotlHMZVelSS2OIJTurX3IPTsfpUyBGpsVv4OxthmSTBdg"}
|
|
92
|
+
mattermostUrl={siteSettings.mattermostUrl}
|
|
93
|
+
width="100%"
|
|
94
|
+
height="100%"
|
|
95
|
+
/>
|
|
96
|
+
</Card>
|
|
97
|
+
</Modal>
|
|
98
|
+
)}
|
|
99
|
+
</>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const Navkit = ({ client }: { client: object }) => {
|
|
104
|
+
return (
|
|
105
|
+
<NavkitProvider client={client}>
|
|
106
|
+
<NavkitContent />
|
|
107
|
+
</NavkitProvider>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default Navkit
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createContext, startTransition, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
2
|
+
import type { AppData, NavigationContextType, NavkitProviderProps, SiteSettings, UserData } from './types'
|
|
3
|
+
import { User, Settings, Auth } from "@taruvi-io/sdk"
|
|
4
|
+
import { library } from '@fortawesome/fontawesome-svg-core'
|
|
5
|
+
import { fas } from '@fortawesome/free-solid-svg-icons'
|
|
6
|
+
import { far } from '@fortawesome/free-regular-svg-icons'
|
|
7
|
+
|
|
8
|
+
export const NavigationContext = createContext<NavigationContextType | undefined>(undefined)
|
|
9
|
+
|
|
10
|
+
export const NavkitProvider = ({ children, client }: NavkitProviderProps) => {
|
|
11
|
+
const auth = new Auth(client)
|
|
12
|
+
|
|
13
|
+
const user = useRef<any>(null)
|
|
14
|
+
const settings = useRef<any>(null)
|
|
15
|
+
const siteSettings = useRef<SiteSettings>({
|
|
16
|
+
shortcuts: [],
|
|
17
|
+
logo: '',
|
|
18
|
+
showChat: false,
|
|
19
|
+
frontendUrl: '',
|
|
20
|
+
mattermostUrl: ''
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const [isDesk, setIsDesk] = useState<boolean>(false)
|
|
24
|
+
const [appsList, setAppsList] = useState<AppData[]>([])
|
|
25
|
+
const [userData, setUserData] = useState<UserData | null>(null)
|
|
26
|
+
const [jwtToken, setJwtToken] = useState<string>('')
|
|
27
|
+
const [isUserAuthenticated, setIsUserAuthenticated] = useState<boolean>(false)
|
|
28
|
+
|
|
29
|
+
const checkIfDesk = async () => {
|
|
30
|
+
const fetchedSettings = await settings.current.get()
|
|
31
|
+
const frontendUrl = fetchedSettings?.frontendUrl
|
|
32
|
+
// const frontendUrl = "http://localhost:5173/desk"
|
|
33
|
+
if (!frontendUrl) return false
|
|
34
|
+
return window.location.href.includes(frontendUrl)
|
|
35
|
+
}
|
|
36
|
+
const navigateToUrl = (url: string, openInLine: boolean) => {
|
|
37
|
+
if (isDesk && openInLine) {
|
|
38
|
+
try {
|
|
39
|
+
const urlObj = new URL(url)
|
|
40
|
+
window.location.href = urlObj.pathname
|
|
41
|
+
} catch {
|
|
42
|
+
window.location.href = url.startsWith('/') ? url : `/${url}`
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
window.open(url, "_blank")
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const authenticateUser = async () => {
|
|
50
|
+
const authenticated = await auth.isUserAuthenticated()
|
|
51
|
+
setIsUserAuthenticated(authenticated)
|
|
52
|
+
getData(authenticated)
|
|
53
|
+
|
|
54
|
+
// Get JWT token if authenticated from sdk
|
|
55
|
+
if (authenticated) {
|
|
56
|
+
const token = ""
|
|
57
|
+
setJwtToken(token)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const getData = useCallback(async (isUserAuthenticated: boolean) => {
|
|
62
|
+
user.current = new User(client)
|
|
63
|
+
settings.current = new Settings(client)
|
|
64
|
+
const fetchedSettings = await settings?.current?.get()
|
|
65
|
+
// const fetchedSettings = {
|
|
66
|
+
// frontendUrl: "http://localhost:5173/",
|
|
67
|
+
// showChat: true,
|
|
68
|
+
// mattermostUrl: "https://chatprod.eoxvantage.com/",
|
|
69
|
+
// shortcuts: [
|
|
70
|
+
// {
|
|
71
|
+
// id: "shortcut-helpdesk",
|
|
72
|
+
// appname: "Helpdesk",
|
|
73
|
+
// icon: "comment-dots",
|
|
74
|
+
// url: "/helpdesk"
|
|
75
|
+
// },
|
|
76
|
+
// {
|
|
77
|
+
// id: "shortcut-crm",
|
|
78
|
+
// appname: "CRM",
|
|
79
|
+
// icon: "address-book",
|
|
80
|
+
// url: "/crm"
|
|
81
|
+
// },
|
|
82
|
+
// {
|
|
83
|
+
// id: "shortcut-tasks",
|
|
84
|
+
// appname: "Tasks",
|
|
85
|
+
// icon: "square-check",
|
|
86
|
+
// url: "/tasks"
|
|
87
|
+
// }
|
|
88
|
+
// ]
|
|
89
|
+
// }
|
|
90
|
+
siteSettings.current = fetchedSettings
|
|
91
|
+
if (isUserAuthenticated) {
|
|
92
|
+
const [apps, userDataResponse] = await Promise.all([
|
|
93
|
+
user.current.getApps?.(),
|
|
94
|
+
user.current.getUserData(),
|
|
95
|
+
]);
|
|
96
|
+
startTransition(() => {
|
|
97
|
+
setAppsList(apps || [])
|
|
98
|
+
setUserData(userDataResponse || null)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
}, [client])
|
|
103
|
+
|
|
104
|
+
useLayoutEffect(() => {
|
|
105
|
+
const init = async () => {
|
|
106
|
+
library.add(fas, far)
|
|
107
|
+
await authenticateUser()
|
|
108
|
+
}
|
|
109
|
+
init()
|
|
110
|
+
}, [])
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const initIsDesk = async () => {
|
|
114
|
+
const isDeskValue = await checkIfDesk()
|
|
115
|
+
setIsDesk(isDeskValue)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
initIsDesk()
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
// Listen for user profile updates via BroadcastChannel
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
const channel = new BroadcastChannel('taruvi-updates')
|
|
124
|
+
|
|
125
|
+
const handleMessage = (event: MessageEvent) => {
|
|
126
|
+
if (event.data === 'refreshNavkit') {
|
|
127
|
+
getData(isUserAuthenticated)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
channel.addEventListener('message', handleMessage)
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
channel.removeEventListener('message', handleMessage)
|
|
135
|
+
channel.close()
|
|
136
|
+
}
|
|
137
|
+
}, [getData])
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<NavigationContext.Provider value={{ navigateToUrl, isDesk, appsList, userData, siteSettings: siteSettings.current, jwtToken, isUserAuthenticated }}>
|
|
141
|
+
{children}
|
|
142
|
+
</NavigationContext.Provider>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const useNavigation = () => {
|
|
147
|
+
const context = useContext(NavigationContext)
|
|
148
|
+
if (context === undefined) {
|
|
149
|
+
throw new Error('useNavigation must be used within a NavkitProvider')
|
|
150
|
+
}
|
|
151
|
+
return context
|
|
152
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions } from '../../styles/variables'
|
|
2
|
+
|
|
3
|
+
export const appLauncherStyles = {
|
|
4
|
+
container: {
|
|
5
|
+
position: 'absolute' as const,
|
|
6
|
+
top: 60,
|
|
7
|
+
left: { xs: '2.5vw', sm: '10vw', md: '15vw', lg: 40 },
|
|
8
|
+
width: { xs: '95vw', sm: '80vw', md: '70vw', lg: '560px' },
|
|
9
|
+
maxWidth: '800px',
|
|
10
|
+
maxHeight: { xs: 'calc(100vh - 80px)', md: '335px' },
|
|
11
|
+
borderRadius: borderRadius.default,
|
|
12
|
+
boxShadow: shadows.dropdown,
|
|
13
|
+
p: spacing.lg,
|
|
14
|
+
backgroundColor: colours.bg.white,
|
|
15
|
+
zIndex: zIndex.overlay,
|
|
16
|
+
},
|
|
17
|
+
scrollContainer: {
|
|
18
|
+
mt: spacing.md,
|
|
19
|
+
maxHeight: { xs: '300px', md: '220px' },
|
|
20
|
+
overflowY: 'auto' as const,
|
|
21
|
+
},
|
|
22
|
+
appItem: {
|
|
23
|
+
display: 'flex',
|
|
24
|
+
flexDirection: 'column' as const,
|
|
25
|
+
alignItems: 'center',
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
p: spacing.xs,
|
|
28
|
+
borderRadius: borderRadius.default,
|
|
29
|
+
cursor: 'pointer',
|
|
30
|
+
transition: 'all 0.2s ease',
|
|
31
|
+
'&:hover': {
|
|
32
|
+
backgroundColor: colours.bg.light,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
iconContainer: {
|
|
36
|
+
width: 48,
|
|
37
|
+
height: 48,
|
|
38
|
+
display: 'flex',
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
justifyContent: 'center',
|
|
41
|
+
borderRadius: borderRadius.default,
|
|
42
|
+
border: `1px solid ${colours.border.light}`,
|
|
43
|
+
mb: spacing.sm,
|
|
44
|
+
},
|
|
45
|
+
iconStyle: {
|
|
46
|
+
fontSize: dimensions.iconSize.md,
|
|
47
|
+
color: colours.text.secondary,
|
|
48
|
+
},
|
|
49
|
+
appName: {
|
|
50
|
+
fontWeight: typography.weights.regular,
|
|
51
|
+
color: colours.text.secondary,
|
|
52
|
+
fontSize: typography.sizes.xs,
|
|
53
|
+
textAlign: 'center' as const,
|
|
54
|
+
lineHeight: 1.3,
|
|
55
|
+
maxWidth: '100%',
|
|
56
|
+
wordWrap: 'break-word' as const,
|
|
57
|
+
letterSpacing: typography.letterSpacing.default,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState, useEffect } from "react"
|
|
2
|
+
import type { AppData } from "../../types"
|
|
3
|
+
import Search from "../Search/Search"
|
|
4
|
+
import { Card, Box, Grid, Typography } from "@mui/material"
|
|
5
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
|
6
|
+
import type { IconName } from "@fortawesome/fontawesome-svg-core"
|
|
7
|
+
import { useNavigation } from "../../NavkitContext"
|
|
8
|
+
import { appLauncherStyles } from "./AppLauncher.styles"
|
|
9
|
+
import siteData from "../../../mocks/site.json"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const AppLauncher = () => {
|
|
14
|
+
const { navigateToUrl } = useNavigation()
|
|
15
|
+
|
|
16
|
+
// Transform apps from site.json to include id
|
|
17
|
+
const appsList: AppData[] = siteData.apps.map((app, index) => ({
|
|
18
|
+
...app,
|
|
19
|
+
id: `app-${index}`
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
const [filteredApps, setFilteredApps] = useState<AppData[]>(appsList)
|
|
23
|
+
|
|
24
|
+
const handleAppClick = (app: AppData) => {
|
|
25
|
+
navigateToUrl(app.url, false)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const handleSearchChange = (filtered: AppData[]) => {
|
|
29
|
+
setFilteredApps(filtered)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
setFilteredApps(appsList)
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<Card sx={appLauncherStyles.container}>
|
|
38
|
+
<Search appsList={appsList} onSearchChange={handleSearchChange} />
|
|
39
|
+
{filteredApps && filteredApps.length > 0 && (
|
|
40
|
+
<Box sx={appLauncherStyles.scrollContainer}>
|
|
41
|
+
<Grid container spacing={1}>
|
|
42
|
+
{filteredApps.map((app) => {
|
|
43
|
+
return (
|
|
44
|
+
<Grid item xs={3} key={app.id}>
|
|
45
|
+
<Box
|
|
46
|
+
sx={appLauncherStyles.appItem}
|
|
47
|
+
onClick={() => handleAppClick(app)}
|
|
48
|
+
>
|
|
49
|
+
<Box
|
|
50
|
+
sx={appLauncherStyles.iconContainer}
|
|
51
|
+
>
|
|
52
|
+
<FontAwesomeIcon
|
|
53
|
+
icon={["far", app.icon.replace('fa-', '') as IconName]}
|
|
54
|
+
style={appLauncherStyles.iconStyle}
|
|
55
|
+
/>
|
|
56
|
+
</Box>
|
|
57
|
+
<Typography
|
|
58
|
+
variant="body2"
|
|
59
|
+
sx={appLauncherStyles.appName}
|
|
60
|
+
>
|
|
61
|
+
{app.appname}
|
|
62
|
+
</Typography>
|
|
63
|
+
</Box>
|
|
64
|
+
</Grid>
|
|
65
|
+
)
|
|
66
|
+
})}
|
|
67
|
+
</Grid>
|
|
68
|
+
</Box>
|
|
69
|
+
)}
|
|
70
|
+
</Card>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default AppLauncher
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { Box, Badge } from '@mui/material'
|
|
3
|
+
|
|
4
|
+
interface MattermostChatProps {
|
|
5
|
+
jwtToken: string
|
|
6
|
+
mattermostUrl: string // Mattermost server URL (must be passed from parent app)
|
|
7
|
+
onNotification?: (count: number) => void
|
|
8
|
+
onUrlClick?: (url: string) => void
|
|
9
|
+
width?: string | number
|
|
10
|
+
height?: string | number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface NotifyMessage {
|
|
14
|
+
event: string
|
|
15
|
+
data?: {
|
|
16
|
+
title?: string
|
|
17
|
+
body?: string
|
|
18
|
+
channel?: string
|
|
19
|
+
teamId?: string
|
|
20
|
+
url?: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MattermostChat = ({
|
|
25
|
+
jwtToken,
|
|
26
|
+
mattermostUrl,
|
|
27
|
+
onNotification,
|
|
28
|
+
onUrlClick,
|
|
29
|
+
width = '100%',
|
|
30
|
+
height = '100%'
|
|
31
|
+
}: MattermostChatProps) => {
|
|
32
|
+
const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
33
|
+
const [unreadCount, setUnreadCount] = useState(0)
|
|
34
|
+
const [isWindowFocused, setIsWindowFocused] = useState(true)
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
// Handle postMessage events from Mattermost iframe
|
|
37
|
+
const handleMessage = (event: MessageEvent<NotifyMessage>) => {
|
|
38
|
+
// Validate origin for security
|
|
39
|
+
const mattermostOrigin = new URL(mattermostUrl).origin
|
|
40
|
+
if (event.origin !== mattermostOrigin) {
|
|
41
|
+
console.warn('Received message from untrusted origin:', event.origin)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Handle Notify events
|
|
46
|
+
if (event.data?.event === 'Notify') {
|
|
47
|
+
if (!isWindowFocused) {
|
|
48
|
+
setUnreadCount(prev => {
|
|
49
|
+
const newCount = prev + 1
|
|
50
|
+
onNotification?.(newCount)
|
|
51
|
+
return newCount
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle URL clicks if provided
|
|
56
|
+
if (event.data.data?.url && onUrlClick) {
|
|
57
|
+
onUrlClick(event.data.data.url)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Handle window focus/blur to track unread messages
|
|
63
|
+
const handleFocus = () => {
|
|
64
|
+
setIsWindowFocused(true)
|
|
65
|
+
setUnreadCount(0)
|
|
66
|
+
onNotification?.(0)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const handleBlur = () => {
|
|
70
|
+
setIsWindowFocused(false)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Add event listeners
|
|
74
|
+
window.addEventListener('message', handleMessage)
|
|
75
|
+
window.addEventListener('focus', handleFocus)
|
|
76
|
+
window.addEventListener('blur', handleBlur)
|
|
77
|
+
|
|
78
|
+
// Cleanup event listeners on unmount
|
|
79
|
+
return () => {
|
|
80
|
+
window.removeEventListener('message', handleMessage)
|
|
81
|
+
window.removeEventListener('focus', handleFocus)
|
|
82
|
+
window.removeEventListener('blur', handleBlur)
|
|
83
|
+
}
|
|
84
|
+
}, [mattermostUrl, isWindowFocused, onNotification, onUrlClick])
|
|
85
|
+
|
|
86
|
+
// Construct iframe URL with JWT authentication
|
|
87
|
+
const iframeUrl = `${mattermostUrl}/login?oxauth=${jwtToken}`
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Box
|
|
91
|
+
sx={{
|
|
92
|
+
position: 'relative',
|
|
93
|
+
width,
|
|
94
|
+
height,
|
|
95
|
+
overflow: 'hidden'
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{unreadCount > 0 && (
|
|
99
|
+
<Badge
|
|
100
|
+
badgeContent={unreadCount}
|
|
101
|
+
color="error"
|
|
102
|
+
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
|
+
}
|
|
113
|
+
}}
|
|
114
|
+
/>
|
|
115
|
+
)}
|
|
116
|
+
<iframe
|
|
117
|
+
ref={iframeRef}
|
|
118
|
+
src={iframeUrl}
|
|
119
|
+
title="Mattermost Chat"
|
|
120
|
+
allow="clipboard-read; clipboard-write"
|
|
121
|
+
style={{
|
|
122
|
+
width: '100%',
|
|
123
|
+
height: '100%',
|
|
124
|
+
border: 'none',
|
|
125
|
+
display: 'block'
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</Box>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default MattermostChat
|