@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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# MattermostChat Component
|
|
2
|
+
|
|
3
|
+
A React component that embeds Mattermost chat in an iframe with JWT authentication, notification tracking, and postMessage communication.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔐 JWT token authentication via URL parameter
|
|
8
|
+
- 🔔 Unread message notification badge
|
|
9
|
+
- 📋 Clipboard permissions (read/write)
|
|
10
|
+
- 🎯 postMessage listener for Mattermost events
|
|
11
|
+
- 🔒 Origin validation for security
|
|
12
|
+
- 📱 Responsive and customizable dimensions
|
|
13
|
+
- ♻️ Proper cleanup of event listeners
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
The component is already included in your project. Make sure you have the required dependencies:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @mui/material @emotion/react @emotion/styled
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Environment Variables
|
|
24
|
+
|
|
25
|
+
Create a `.env` file in your project root:
|
|
26
|
+
|
|
27
|
+
```env
|
|
28
|
+
VITE_MATTERMOST_URL=http://localhost:8065
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For production, update this to your actual Mattermost server URL.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Basic Example
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { MattermostChat } from './components'
|
|
39
|
+
|
|
40
|
+
function App() {
|
|
41
|
+
const jwtToken = 'your-jwt-token-here'
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
45
|
+
<MattermostChat jwtToken={jwtToken} />
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Advanced Example with Callbacks
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { MattermostChat } from './components'
|
|
55
|
+
import { useState } from 'react'
|
|
56
|
+
|
|
57
|
+
function ChatPage() {
|
|
58
|
+
const [notificationCount, setNotificationCount] = useState(0)
|
|
59
|
+
const jwtToken = 'your-jwt-token-here'
|
|
60
|
+
|
|
61
|
+
const handleNotification = (count: number) => {
|
|
62
|
+
console.log(`Unread messages: ${count}`)
|
|
63
|
+
setNotificationCount(count)
|
|
64
|
+
|
|
65
|
+
// You can trigger browser notifications here
|
|
66
|
+
if (count > 0 && Notification.permission === 'granted') {
|
|
67
|
+
new Notification('New Mattermost Message', {
|
|
68
|
+
body: `You have ${count} unread message(s)`
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const handleUrlClick = (url: string) => {
|
|
74
|
+
console.log(`User clicked URL in Mattermost: ${url}`)
|
|
75
|
+
// Handle navigation or custom logic
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div style={{ width: '100%', height: 'calc(100vh - 64px)' }}>
|
|
80
|
+
<MattermostChat
|
|
81
|
+
jwtToken={jwtToken}
|
|
82
|
+
onNotification={handleNotification}
|
|
83
|
+
onUrlClick={handleUrlClick}
|
|
84
|
+
width="100%"
|
|
85
|
+
height="100%"
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Custom Dimensions
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
<MattermostChat
|
|
96
|
+
jwtToken={jwtToken}
|
|
97
|
+
width="800px"
|
|
98
|
+
height="600px"
|
|
99
|
+
/>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Integration with NavKit
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
// In your App.tsx or main component
|
|
106
|
+
import { useState } from 'react'
|
|
107
|
+
import { MattermostChat } from './components'
|
|
108
|
+
|
|
109
|
+
function NavkitApp() {
|
|
110
|
+
const [showChat, setShowChat] = useState(false)
|
|
111
|
+
const jwtToken = 'your-jwt-token'
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
{/* Your existing NavKit code */}
|
|
116
|
+
<AppBar>
|
|
117
|
+
<Toolbar>
|
|
118
|
+
<IconButton onClick={() => setShowChat(!showChat)}>
|
|
119
|
+
<ChatIcon />
|
|
120
|
+
</IconButton>
|
|
121
|
+
</Toolbar>
|
|
122
|
+
</AppBar>
|
|
123
|
+
|
|
124
|
+
{/* Mattermost Chat Modal or Sidebar */}
|
|
125
|
+
{showChat && (
|
|
126
|
+
<Box
|
|
127
|
+
sx={{
|
|
128
|
+
position: 'fixed',
|
|
129
|
+
right: 0,
|
|
130
|
+
top: 64,
|
|
131
|
+
width: 400,
|
|
132
|
+
height: 'calc(100vh - 64px)',
|
|
133
|
+
zIndex: 1200,
|
|
134
|
+
boxShadow: 3
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
<MattermostChat jwtToken={jwtToken} />
|
|
138
|
+
</Box>
|
|
139
|
+
)}
|
|
140
|
+
</>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Props
|
|
146
|
+
|
|
147
|
+
| Prop | Type | Required | Default | Description |
|
|
148
|
+
|------|------|----------|---------|-------------|
|
|
149
|
+
| `jwtToken` | `string` | Yes | - | JWT token for Mattermost authentication |
|
|
150
|
+
| `onNotification` | `(count: number) => void` | No | - | Callback when notification count changes |
|
|
151
|
+
| `onUrlClick` | `(url: string) => void` | No | - | Callback when user clicks a URL in Mattermost |
|
|
152
|
+
| `width` | `string \| number` | No | `'100%'` | Width of the iframe container |
|
|
153
|
+
| `height` | `string \| number` | No | `'100%'` | Height of the iframe container |
|
|
154
|
+
|
|
155
|
+
## How It Works
|
|
156
|
+
|
|
157
|
+
### Authentication
|
|
158
|
+
The component constructs the iframe URL as:
|
|
159
|
+
```
|
|
160
|
+
${VITE_MATTERMOST_URL}/login?oxauth=${jwtToken}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This allows automatic authentication with your Mattermost server using the provided JWT token.
|
|
164
|
+
|
|
165
|
+
### Notification Tracking
|
|
166
|
+
- When the window loses focus (user switches tabs/windows), incoming Mattermost messages increment the unread counter
|
|
167
|
+
- A red badge appears in the top-right corner showing the unread count
|
|
168
|
+
- When the window regains focus, the counter resets to 0
|
|
169
|
+
- The `onNotification` callback is triggered whenever the count changes
|
|
170
|
+
|
|
171
|
+
### Security
|
|
172
|
+
- Origin validation ensures only messages from your configured Mattermost server are processed
|
|
173
|
+
- Messages from other origins are logged and ignored
|
|
174
|
+
- Clipboard permissions are explicitly granted for proper copy/paste functionality
|
|
175
|
+
|
|
176
|
+
### postMessage Communication
|
|
177
|
+
The component listens for `Notify` events from the Mattermost iframe with the following structure:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
{
|
|
181
|
+
event: 'Notify',
|
|
182
|
+
data: {
|
|
183
|
+
title: 'Message title',
|
|
184
|
+
body: 'Message content',
|
|
185
|
+
channel: 'channel-id',
|
|
186
|
+
teamId: 'team-id',
|
|
187
|
+
url: 'deep-link-url'
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Browser Compatibility
|
|
193
|
+
|
|
194
|
+
- Chrome/Edge: ✅ Full support
|
|
195
|
+
- Firefox: ✅ Full support
|
|
196
|
+
- Safari: ✅ Full support
|
|
197
|
+
- Mobile browsers: ✅ Full support (with responsive sizing)
|
|
198
|
+
|
|
199
|
+
## Security Considerations
|
|
200
|
+
|
|
201
|
+
1. **JWT Token Storage**: Never store JWT tokens in localStorage or sessionStorage if they contain sensitive data. Consider using httpOnly cookies or secure session management.
|
|
202
|
+
|
|
203
|
+
2. **Origin Validation**: The component validates the origin of postMessage events. Ensure `VITE_MATTERMOST_URL` is set correctly in production.
|
|
204
|
+
|
|
205
|
+
3. **HTTPS**: In production, always use HTTPS for both your app and Mattermost server.
|
|
206
|
+
|
|
207
|
+
## Troubleshooting
|
|
208
|
+
|
|
209
|
+
### Iframe not loading
|
|
210
|
+
- Check that `VITE_MATTERMOST_URL` is set correctly in `.env`
|
|
211
|
+
- Verify the JWT token is valid
|
|
212
|
+
- Check browser console for CORS or CSP errors
|
|
213
|
+
- Ensure Mattermost server allows iframe embedding
|
|
214
|
+
|
|
215
|
+
### Notifications not working
|
|
216
|
+
- Verify the window focus/blur events are firing
|
|
217
|
+
- Check that postMessage events are being sent from Mattermost
|
|
218
|
+
- Ensure origin validation is passing (check console for warnings)
|
|
219
|
+
|
|
220
|
+
### Clipboard not working
|
|
221
|
+
- Verify the `allow` attribute includes clipboard permissions
|
|
222
|
+
- Check that the user has granted clipboard permissions in their browser
|
|
223
|
+
- Some browsers require HTTPS for clipboard API access
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
Part of the Taruvi NavKit project.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions } from '../../styles/variables'
|
|
2
|
+
|
|
3
|
+
export const profileStyles = {
|
|
4
|
+
container: {
|
|
5
|
+
position: 'relative' as const,
|
|
6
|
+
},
|
|
7
|
+
trigger: {
|
|
8
|
+
display: 'flex',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
gap: spacing.sm,
|
|
11
|
+
ml: spacing.md,
|
|
12
|
+
cursor: 'pointer',
|
|
13
|
+
},
|
|
14
|
+
avatar: {
|
|
15
|
+
width: dimensions.avatarSize,
|
|
16
|
+
height: dimensions.avatarSize,
|
|
17
|
+
bgcolor: colours.bg.avatar,
|
|
18
|
+
color: colours.text.primary,
|
|
19
|
+
fontWeight: typography.weights.semibold,
|
|
20
|
+
fontSize: typography.sizes.sm,
|
|
21
|
+
},
|
|
22
|
+
name: {
|
|
23
|
+
color: colours.text.primary,
|
|
24
|
+
fontSize: typography.sizes.sm,
|
|
25
|
+
fontWeight: typography.weights.regular,
|
|
26
|
+
letterSpacing: typography.letterSpacing.default,
|
|
27
|
+
display: { xs: 'none', md: 'block' },
|
|
28
|
+
},
|
|
29
|
+
menu: {
|
|
30
|
+
position: 'absolute' as const,
|
|
31
|
+
top: 60,
|
|
32
|
+
right: 0,
|
|
33
|
+
width: 200,
|
|
34
|
+
borderRadius: borderRadius.default,
|
|
35
|
+
boxShadow: shadows.dropdown,
|
|
36
|
+
backgroundColor: colours.bg.white,
|
|
37
|
+
zIndex: zIndex.overlay,
|
|
38
|
+
py: 1,
|
|
39
|
+
},
|
|
40
|
+
menuItem: {
|
|
41
|
+
display: 'flex',
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
gap: spacing.md,
|
|
44
|
+
px: spacing.md,
|
|
45
|
+
py: spacing.sm,
|
|
46
|
+
'&:hover': {
|
|
47
|
+
backgroundColor: colours.bg.light,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
menuItemText: {
|
|
51
|
+
color: colours.text.secondary,
|
|
52
|
+
fontSize: typography.sizes.sm,
|
|
53
|
+
fontWeight: typography.weights.regular,
|
|
54
|
+
},
|
|
55
|
+
iconStyle: {
|
|
56
|
+
fontSize: dimensions.iconSize.sm,
|
|
57
|
+
color: colours.text.secondary,
|
|
58
|
+
},
|
|
59
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Box, Avatar, Typography } from '@mui/material'
|
|
2
|
+
import { profileStyles } from './Profile.styles'
|
|
3
|
+
import nopfp from "../../assets/nopfp.png"
|
|
4
|
+
|
|
5
|
+
interface UserData {
|
|
6
|
+
full_name?: string
|
|
7
|
+
first_name?: string
|
|
8
|
+
last_name?: string
|
|
9
|
+
pfp?: string
|
|
10
|
+
username?: string
|
|
11
|
+
email?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ProfileProps {
|
|
15
|
+
userData: UserData
|
|
16
|
+
onClick: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const Profile = (props: ProfileProps) => {
|
|
20
|
+
const { userData, onClick } = props
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Box sx={profileStyles.trigger} onClick={onClick}>
|
|
24
|
+
<Avatar
|
|
25
|
+
src={userData.pfp || nopfp}
|
|
26
|
+
sx={profileStyles.avatar}
|
|
27
|
+
>
|
|
28
|
+
</Avatar>
|
|
29
|
+
<Typography sx={profileStyles.name}>
|
|
30
|
+
{userData.first_name} {userData.last_name}
|
|
31
|
+
</Typography>
|
|
32
|
+
</Box>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default Profile
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Card, MenuItem, Typography } from '@mui/material'
|
|
2
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
3
|
+
import { profileStyles } from './Profile.styles'
|
|
4
|
+
import { useNavigation } from '../../NavkitContext'
|
|
5
|
+
|
|
6
|
+
const ProfileMenu = () => {
|
|
7
|
+
const { navigateToUrl } = useNavigation()
|
|
8
|
+
|
|
9
|
+
const handlePreferences = () => {
|
|
10
|
+
navigateToUrl('preferences', true)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const handleLogout = async () => {
|
|
14
|
+
//handle logout
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Card sx={profileStyles.menu}>
|
|
19
|
+
<MenuItem onClick={handlePreferences} sx={profileStyles.menuItem}>
|
|
20
|
+
<FontAwesomeIcon
|
|
21
|
+
icon={["fas", "gear"]}
|
|
22
|
+
style={profileStyles.iconStyle}
|
|
23
|
+
/>
|
|
24
|
+
<Typography sx={profileStyles.menuItemText}>
|
|
25
|
+
Preferences
|
|
26
|
+
</Typography>
|
|
27
|
+
</MenuItem>
|
|
28
|
+
|
|
29
|
+
<MenuItem onClick={handleLogout} sx={profileStyles.menuItem}>
|
|
30
|
+
<FontAwesomeIcon
|
|
31
|
+
icon={["fas", "right-from-bracket"]}
|
|
32
|
+
style={profileStyles.iconStyle}
|
|
33
|
+
/>
|
|
34
|
+
<Typography sx={profileStyles.menuItemText}>
|
|
35
|
+
Logout
|
|
36
|
+
</Typography>
|
|
37
|
+
</MenuItem>
|
|
38
|
+
</Card>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default ProfileMenu
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
2
|
+
import { TextField, InputAdornment } from '@mui/material'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { searchStyles } from './Search.styles'
|
|
5
|
+
import type { AppData } from '../../types'
|
|
6
|
+
|
|
7
|
+
interface SearchProps {
|
|
8
|
+
appsList?: AppData[]
|
|
9
|
+
onSearchChange?: (filteredApps: AppData[]) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Search = ({ appsList, onSearchChange }: SearchProps) => {
|
|
13
|
+
const [searchTerm, setSearchTerm] = useState('')
|
|
14
|
+
|
|
15
|
+
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
16
|
+
const value = event.target.value
|
|
17
|
+
setSearchTerm(value)
|
|
18
|
+
|
|
19
|
+
// Filter apps based on search term
|
|
20
|
+
if (appsList) {
|
|
21
|
+
const filtered = value.trim() === ''
|
|
22
|
+
? appsList
|
|
23
|
+
: appsList.filter(app =>
|
|
24
|
+
app.appname.toLowerCase().includes(value.toLowerCase())
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Call parent callback with filtered results
|
|
28
|
+
onSearchChange?.(filtered)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<TextField
|
|
34
|
+
fullWidth
|
|
35
|
+
placeholder="Search for apps"
|
|
36
|
+
variant="outlined"
|
|
37
|
+
value={searchTerm}
|
|
38
|
+
onChange={handleSearchChange}
|
|
39
|
+
sx={searchStyles.textField}
|
|
40
|
+
InputProps={{
|
|
41
|
+
startAdornment: (
|
|
42
|
+
<InputAdornment position="start">
|
|
43
|
+
<FontAwesomeIcon icon={["fas", "magnifying-glass"]} style={{ color: '#9e9e9e' }} />
|
|
44
|
+
</InputAdornment>
|
|
45
|
+
),
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default Search
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { colours, spacing, dimensions } from '../../styles/variables'
|
|
2
|
+
|
|
3
|
+
export const shortcutsStyles = {
|
|
4
|
+
desktopContainer: {
|
|
5
|
+
display: { xs: 'none', md: 'flex' },
|
|
6
|
+
alignItems: 'center',
|
|
7
|
+
gap: 3,
|
|
8
|
+
ml: spacing.md,
|
|
9
|
+
},
|
|
10
|
+
mobileTrigger: {
|
|
11
|
+
display: { xs: 'flex', md: 'none' },
|
|
12
|
+
ml: spacing.sm,
|
|
13
|
+
},
|
|
14
|
+
shortcut: {
|
|
15
|
+
display: 'flex',
|
|
16
|
+
flexDirection: 'column' as const,
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
textDecoration: 'none',
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
color: colours.text.secondary,
|
|
21
|
+
'&:hover': {
|
|
22
|
+
opacity: 0.7,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
iconStyle: {
|
|
26
|
+
fontSize: dimensions.iconSize.lg,
|
|
27
|
+
color: colours.text.secondary,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { AppData } from '../../types'
|
|
2
|
+
import { Box, Link, IconButton } from '@mui/material'
|
|
3
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
4
|
+
import { shortcutsStyles } from './Shortcuts.styles'
|
|
5
|
+
import { useNavigation } from '../../NavkitContext'
|
|
6
|
+
|
|
7
|
+
interface ShortcutsProps {
|
|
8
|
+
showChat?: (showChat: boolean) => void
|
|
9
|
+
onMenuToggle?: () => void
|
|
10
|
+
triggerRef?: React.RefObject<HTMLButtonElement | null>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const Shortcuts = (props: ShortcutsProps) => {
|
|
14
|
+
|
|
15
|
+
const { showChat, onMenuToggle, triggerRef } = props
|
|
16
|
+
const { navigateToUrl, siteSettings, isUserAuthenticated } = useNavigation()
|
|
17
|
+
const shortcuts = siteSettings.shortcuts
|
|
18
|
+
|
|
19
|
+
const handleShortcutClick = (shortcut: AppData) => {
|
|
20
|
+
if (shortcut.url) {
|
|
21
|
+
navigateToUrl(shortcut.url, false)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if ((!shortcuts || shortcuts.length === 0) && !siteSettings.showChat) {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
{/* Desktop view - inline shortcuts */}
|
|
31
|
+
<Box sx={shortcutsStyles.desktopContainer}>
|
|
32
|
+
{shortcuts?.map((shortcut) => (
|
|
33
|
+
<Link
|
|
34
|
+
key={shortcut.id}
|
|
35
|
+
onClick={() => handleShortcutClick(shortcut)}
|
|
36
|
+
sx={shortcutsStyles.shortcut}
|
|
37
|
+
>
|
|
38
|
+
<FontAwesomeIcon
|
|
39
|
+
icon={["far", shortcut.icon] as any}
|
|
40
|
+
style={shortcutsStyles.iconStyle}
|
|
41
|
+
/>
|
|
42
|
+
</Link>
|
|
43
|
+
))}
|
|
44
|
+
{siteSettings.showChat && isUserAuthenticated && <Link
|
|
45
|
+
key={"chat"}
|
|
46
|
+
onClick={() => showChat?.(true)}
|
|
47
|
+
sx={shortcutsStyles.shortcut}
|
|
48
|
+
>
|
|
49
|
+
<FontAwesomeIcon
|
|
50
|
+
icon={["far", "comment"] as any}
|
|
51
|
+
style={shortcutsStyles.iconStyle}
|
|
52
|
+
/>
|
|
53
|
+
</Link>
|
|
54
|
+
}
|
|
55
|
+
</Box>
|
|
56
|
+
|
|
57
|
+
{/* Mobile view - trigger button */}
|
|
58
|
+
<IconButton
|
|
59
|
+
ref={triggerRef}
|
|
60
|
+
onClick={onMenuToggle}
|
|
61
|
+
sx={shortcutsStyles.mobileTrigger}
|
|
62
|
+
>
|
|
63
|
+
<FontAwesomeIcon
|
|
64
|
+
icon={["fas", "ellipsis-v"]}
|
|
65
|
+
style={shortcutsStyles.iconStyle}
|
|
66
|
+
/>
|
|
67
|
+
</IconButton>
|
|
68
|
+
</>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default Shortcuts
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { colours, spacing, shadows, borderRadius, typography, zIndex, dimensions } from '../../styles/variables'
|
|
2
|
+
|
|
3
|
+
export const shortcutsMenuStyles = {
|
|
4
|
+
menu: {
|
|
5
|
+
position: 'absolute' as const,
|
|
6
|
+
top: 60,
|
|
7
|
+
right: 0,
|
|
8
|
+
width: 200,
|
|
9
|
+
borderRadius: borderRadius.default,
|
|
10
|
+
boxShadow: shadows.dropdown,
|
|
11
|
+
backgroundColor: colours.bg.white,
|
|
12
|
+
zIndex: zIndex.overlay,
|
|
13
|
+
py: 1,
|
|
14
|
+
},
|
|
15
|
+
menuItem: {
|
|
16
|
+
display: 'flex',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
gap: spacing.md,
|
|
19
|
+
px: spacing.md,
|
|
20
|
+
py: spacing.sm,
|
|
21
|
+
'&:hover': {
|
|
22
|
+
backgroundColor: colours.bg.light,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
menuItemText: {
|
|
26
|
+
color: colours.text.secondary,
|
|
27
|
+
fontSize: typography.sizes.sm,
|
|
28
|
+
fontWeight: typography.weights.regular,
|
|
29
|
+
},
|
|
30
|
+
iconStyle: {
|
|
31
|
+
fontSize: dimensions.iconSize.sm,
|
|
32
|
+
color: colours.text.secondary,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Card, MenuItem, Typography } from '@mui/material'
|
|
2
|
+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
|
3
|
+
import type { AppData } from '../../types'
|
|
4
|
+
import { shortcutsMenuStyles } from './ShortcutsMenu.styles'
|
|
5
|
+
import { useNavigation } from '../../NavkitContext'
|
|
6
|
+
|
|
7
|
+
interface ShortcutsMenuProps {
|
|
8
|
+
showChat: (showChat: boolean) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ShortcutsMenu = (props: ShortcutsMenuProps) => {
|
|
12
|
+
const { showChat } = props
|
|
13
|
+
const { navigateToUrl, siteSettings, isUserAuthenticated } = useNavigation()
|
|
14
|
+
const shortcuts = siteSettings.shortcuts
|
|
15
|
+
|
|
16
|
+
const handleShortcutClick = (shortcut: AppData) => {
|
|
17
|
+
if (shortcut.url) {
|
|
18
|
+
navigateToUrl(shortcut.url, false)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if ((!shortcuts || shortcuts.length === 0) && !siteSettings.showChat) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Card sx={shortcutsMenuStyles.menu}>
|
|
28
|
+
{siteSettings.showChat && isUserAuthenticated && (
|
|
29
|
+
<MenuItem
|
|
30
|
+
key="chat"
|
|
31
|
+
onClick={() => showChat(true)}
|
|
32
|
+
sx={shortcutsMenuStyles.menuItem}
|
|
33
|
+
>
|
|
34
|
+
<FontAwesomeIcon
|
|
35
|
+
icon={["far", "comment"] as any}
|
|
36
|
+
style={shortcutsMenuStyles.iconStyle}
|
|
37
|
+
/>
|
|
38
|
+
<Typography sx={shortcutsMenuStyles.menuItemText}>
|
|
39
|
+
Chat
|
|
40
|
+
</Typography>
|
|
41
|
+
</MenuItem>
|
|
42
|
+
)}
|
|
43
|
+
{shortcuts?.map((shortcut) => (
|
|
44
|
+
<MenuItem
|
|
45
|
+
key={shortcut.id}
|
|
46
|
+
onClick={() => handleShortcutClick(shortcut)}
|
|
47
|
+
sx={shortcutsMenuStyles.menuItem}
|
|
48
|
+
>
|
|
49
|
+
<FontAwesomeIcon
|
|
50
|
+
icon={["far", shortcut.icon] as any}
|
|
51
|
+
style={shortcutsMenuStyles.iconStyle}
|
|
52
|
+
/>
|
|
53
|
+
<Typography sx={shortcutsMenuStyles.menuItemText}>
|
|
54
|
+
{shortcut.appname}
|
|
55
|
+
</Typography>
|
|
56
|
+
</MenuItem>
|
|
57
|
+
))}
|
|
58
|
+
</Card>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default ShortcutsMenu
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import AppLauncher from './AppLauncher/AppLauncher'
|
|
2
|
+
import Shortcuts from './Shortucts/Shortcuts'
|
|
3
|
+
import ShortcutsMenu from './Shortucts/ShortcutsMenu'
|
|
4
|
+
import Profile from './Profile/Profile'
|
|
5
|
+
import ProfileMenu from './Profile/ProfileMenu'
|
|
6
|
+
import MattermostChat from './MattermostChat/MattermostChat'
|
|
7
|
+
|
|
8
|
+
export { AppLauncher, Shortcuts, ShortcutsMenu, Profile, ProfileMenu, MattermostChat }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Common Style Patterns - Reusable style objects
|
|
2
|
+
|
|
3
|
+
export const flexCenter = {
|
|
4
|
+
display: 'flex',
|
|
5
|
+
alignItems: 'center',
|
|
6
|
+
justifyContent: 'center',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const flexRow = {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const flexColumn = {
|
|
15
|
+
display: 'flex',
|
|
16
|
+
flexDirection: 'column' as const,
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const hoverFade = {
|
|
21
|
+
'&:hover': {
|
|
22
|
+
opacity: 0.7,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const hoverBgLight = {
|
|
27
|
+
'&:hover': {
|
|
28
|
+
backgroundColor: '#f5f5f5',
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const cursorPointer = {
|
|
33
|
+
cursor: 'pointer',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const absolutePositioned = {
|
|
37
|
+
position: 'absolute' as const,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const relativePositioned = {
|
|
41
|
+
position: 'relative' as const,
|
|
42
|
+
}
|