openvsx-webui-test 0.20.0-dev.5 → 0.20.2-dev.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.
Files changed (136) hide show
  1. package/lib/components/scan-admin/scan-card/scan-card-expanded-content.d.ts +2 -1
  2. package/lib/components/scan-admin/scan-card/scan-card-expanded-content.d.ts.map +1 -1
  3. package/lib/components/scan-admin/scan-card/scan-card-expanded-content.js.map +1 -1
  4. package/lib/components/scan-admin/scan-card/scan-card-header.js +1 -1
  5. package/lib/components/scan-admin/scan-card/scan-card-header.js.map +1 -1
  6. package/lib/components/scan-admin/scan-card/utils.js +1 -1
  7. package/lib/components/scan-admin/scan-card/utils.js.map +1 -1
  8. package/lib/components/sidepanel/navigation-item.d.ts +0 -1
  9. package/lib/components/sidepanel/navigation-item.d.ts.map +1 -1
  10. package/lib/components/sidepanel/navigation-item.js +22 -8
  11. package/lib/components/sidepanel/navigation-item.js.map +1 -1
  12. package/lib/components/sidepanel/sidebar-context.d.ts +16 -0
  13. package/lib/components/sidepanel/sidebar-context.d.ts.map +1 -0
  14. package/lib/components/sidepanel/sidebar-context.js +15 -0
  15. package/lib/components/sidepanel/sidebar-context.js.map +1 -0
  16. package/lib/components/sidepanel/sidepanel.d.ts +4 -4
  17. package/lib/components/sidepanel/sidepanel.d.ts.map +1 -1
  18. package/lib/components/sidepanel/sidepanel.js +47 -10
  19. package/lib/components/sidepanel/sidepanel.js.map +1 -1
  20. package/lib/default/menu-content.d.ts +1 -1
  21. package/lib/default/menu-content.js +1 -1
  22. package/lib/default/menu-content.js.map +1 -1
  23. package/lib/extension-registry-service.d.ts +6 -0
  24. package/lib/extension-registry-service.d.ts.map +1 -1
  25. package/lib/extension-registry-service.js +17 -0
  26. package/lib/extension-registry-service.js.map +1 -1
  27. package/lib/hooks/use-local-storage.d.ts +23 -0
  28. package/lib/hooks/use-local-storage.d.ts.map +1 -0
  29. package/lib/hooks/use-local-storage.js +62 -0
  30. package/lib/hooks/use-local-storage.js.map +1 -0
  31. package/lib/main.d.ts.map +1 -1
  32. package/lib/main.js +5 -5
  33. package/lib/main.js.map +1 -1
  34. package/lib/other-pages.d.ts.map +1 -1
  35. package/lib/other-pages.js +7 -7
  36. package/lib/other-pages.js.map +1 -1
  37. package/lib/pages/admin-dashboard/{admin-routes.d.ts → admin-dashboard-routes.d.ts} +6 -9
  38. package/lib/pages/admin-dashboard/admin-dashboard-routes.d.ts.map +1 -0
  39. package/lib/pages/admin-dashboard/{admin-routes.js → admin-dashboard-routes.js} +6 -9
  40. package/lib/pages/admin-dashboard/admin-dashboard-routes.js.map +1 -0
  41. package/lib/pages/admin-dashboard/admin-dashboard.d.ts.map +1 -1
  42. package/lib/pages/admin-dashboard/admin-dashboard.js +45 -101
  43. package/lib/pages/admin-dashboard/admin-dashboard.js.map +1 -1
  44. package/lib/pages/admin-dashboard/admin-header.d.ts +19 -0
  45. package/lib/pages/admin-dashboard/admin-header.d.ts.map +1 -0
  46. package/lib/pages/admin-dashboard/admin-header.js +16 -0
  47. package/lib/pages/admin-dashboard/admin-header.js.map +1 -0
  48. package/lib/pages/admin-dashboard/admin-sidepanel.d.ts +19 -0
  49. package/lib/pages/admin-dashboard/admin-sidepanel.d.ts.map +1 -0
  50. package/lib/pages/admin-dashboard/admin-sidepanel.js +17 -0
  51. package/lib/pages/admin-dashboard/admin-sidepanel.js.map +1 -0
  52. package/lib/pages/admin-dashboard/customers/customer-member-list.js +1 -1
  53. package/lib/pages/admin-dashboard/customers/customer-member-list.js.map +1 -1
  54. package/lib/pages/admin-dashboard/customers/customers.js +1 -1
  55. package/lib/pages/admin-dashboard/customers/customers.js.map +1 -1
  56. package/lib/pages/admin-dashboard/namespace-admin.d.ts.map +1 -1
  57. package/lib/pages/admin-dashboard/namespace-admin.js +4 -1
  58. package/lib/pages/admin-dashboard/namespace-admin.js.map +1 -1
  59. package/lib/pages/admin-dashboard/namespace-change-dialog.d.ts.map +1 -1
  60. package/lib/pages/admin-dashboard/namespace-change-dialog.js +6 -1
  61. package/lib/pages/admin-dashboard/namespace-change-dialog.js.map +1 -1
  62. package/lib/pages/admin-dashboard/namespace-delete-dialog.d.ts +23 -0
  63. package/lib/pages/admin-dashboard/namespace-delete-dialog.d.ts.map +1 -0
  64. package/lib/pages/admin-dashboard/namespace-delete-dialog.js +53 -0
  65. package/lib/pages/admin-dashboard/namespace-delete-dialog.js.map +1 -0
  66. package/lib/pages/admin-dashboard/nav-types.d.ts +27 -0
  67. package/lib/pages/admin-dashboard/nav-types.d.ts.map +1 -0
  68. package/lib/pages/admin-dashboard/nav-types.js +14 -0
  69. package/lib/pages/admin-dashboard/nav-types.js.map +1 -0
  70. package/lib/pages/admin-dashboard/publisher-admin.js +1 -1
  71. package/lib/pages/admin-dashboard/publisher-admin.js.map +1 -1
  72. package/lib/pages/admin-dashboard/scan-admin.d.ts.map +1 -1
  73. package/lib/pages/admin-dashboard/scan-admin.js +2 -5
  74. package/lib/pages/admin-dashboard/scan-admin.js.map +1 -1
  75. package/lib/pages/admin-dashboard/usage-stats/usage-stats.js +1 -1
  76. package/lib/pages/admin-dashboard/usage-stats/usage-stats.js.map +1 -1
  77. package/lib/pages/admin-dashboard/welcome.d.ts +5 -1
  78. package/lib/pages/admin-dashboard/welcome.d.ts.map +1 -1
  79. package/lib/pages/admin-dashboard/welcome.js +18 -16
  80. package/lib/pages/admin-dashboard/welcome.js.map +1 -1
  81. package/lib/pages/extension-detail/extension-detail-routes.d.ts +0 -1
  82. package/lib/pages/extension-detail/extension-detail-routes.d.ts.map +1 -1
  83. package/lib/pages/extension-detail/extension-detail-routes.js +2 -3
  84. package/lib/pages/extension-detail/extension-detail-routes.js.map +1 -1
  85. package/lib/pages/extension-detail/extension-detail.d.ts.map +1 -1
  86. package/lib/pages/extension-detail/extension-detail.js +120 -249
  87. package/lib/pages/extension-detail/extension-detail.js.map +1 -1
  88. package/lib/pages/extension-detail/use-extension-details.d.ts +23 -0
  89. package/lib/pages/extension-detail/use-extension-details.d.ts.map +1 -0
  90. package/lib/pages/extension-detail/use-extension-details.js +80 -0
  91. package/lib/pages/extension-detail/use-extension-details.js.map +1 -0
  92. package/lib/pages/user/avatar.js +1 -1
  93. package/lib/pages/user/avatar.js.map +1 -1
  94. package/lib/pages/user/user-setting-tabs.d.ts.map +1 -1
  95. package/lib/pages/user/user-setting-tabs.js +1 -1
  96. package/lib/pages/user/user-setting-tabs.js.map +1 -1
  97. package/lib/pages/user/user-settings-namespace-detail.d.ts +1 -0
  98. package/lib/pages/user/user-settings-namespace-detail.d.ts.map +1 -1
  99. package/lib/pages/user/user-settings-namespace-detail.js +19 -3
  100. package/lib/pages/user/user-settings-namespace-detail.js.map +1 -1
  101. package/lib/pages/user/user-settings-namespaces.js.map +1 -1
  102. package/package.json +3 -1
  103. package/src/components/scan-admin/scan-card/scan-card-expanded-content.tsx +4 -4
  104. package/src/components/scan-admin/scan-card/scan-card-header.tsx +1 -1
  105. package/src/components/scan-admin/scan-card/utils.ts +1 -1
  106. package/src/components/sidepanel/navigation-item.tsx +79 -23
  107. package/src/components/sidepanel/sidebar-context.tsx +17 -0
  108. package/src/components/sidepanel/sidepanel.tsx +57 -29
  109. package/src/default/menu-content.tsx +1 -1
  110. package/src/extension-registry-service.ts +18 -0
  111. package/src/hooks/use-local-storage.ts +67 -0
  112. package/src/main.tsx +11 -6
  113. package/src/other-pages.tsx +20 -16
  114. package/src/pages/admin-dashboard/{admin-routes.ts → admin-dashboard-routes.ts} +5 -8
  115. package/src/pages/admin-dashboard/admin-dashboard.tsx +71 -216
  116. package/src/pages/admin-dashboard/admin-header.tsx +59 -0
  117. package/src/pages/admin-dashboard/admin-sidepanel.tsx +59 -0
  118. package/src/pages/admin-dashboard/customers/customer-member-list.tsx +1 -1
  119. package/src/pages/admin-dashboard/customers/customers.tsx +1 -1
  120. package/src/pages/admin-dashboard/namespace-admin.tsx +5 -0
  121. package/src/pages/admin-dashboard/namespace-change-dialog.tsx +55 -46
  122. package/src/pages/admin-dashboard/namespace-delete-dialog.tsx +89 -0
  123. package/src/pages/admin-dashboard/nav-types.ts +31 -0
  124. package/src/pages/admin-dashboard/publisher-admin.tsx +1 -1
  125. package/src/pages/admin-dashboard/scan-admin.tsx +2 -5
  126. package/src/pages/admin-dashboard/usage-stats/usage-stats.tsx +1 -1
  127. package/src/pages/admin-dashboard/welcome.tsx +80 -48
  128. package/src/pages/extension-detail/extension-detail-routes.ts +2 -3
  129. package/src/pages/extension-detail/extension-detail.tsx +290 -409
  130. package/src/pages/extension-detail/use-extension-details.tsx +101 -0
  131. package/src/pages/user/avatar.tsx +1 -1
  132. package/src/pages/user/user-setting-tabs.tsx +3 -2
  133. package/src/pages/user/user-settings-namespace-detail.tsx +38 -5
  134. package/src/pages/user/user-settings-namespaces.tsx +1 -1
  135. package/lib/pages/admin-dashboard/admin-routes.d.ts.map +0 -1
  136. package/lib/pages/admin-dashboard/admin-routes.js.map +0 -1
@@ -8,13 +8,13 @@
8
8
  * SPDX-License-Identifier: EPL-2.0
9
9
  ********************************************************************************/
10
10
 
11
- import { ChangeEvent, FunctionComponent, ReactElement, ReactNode, useContext, useEffect, useState, useRef } from 'react';
11
+ import { FunctionComponent, useCallback, useContext } from 'react';
12
12
  import {
13
- Typography, Box, Theme, Container, Link, Avatar, Paper, Badge, SxProps, Tabs, Tab, Stack, useTheme, PaletteMode,
13
+ Typography, Box, Container, Link, Avatar, Paper, Badge, Tabs, Tab, Stack, useTheme, PaletteMode,
14
14
  decomposeColor
15
15
  } from '@mui/material';
16
16
  import { styled } from '@mui/material/styles';
17
- import { Link as RouteLink, useNavigate, useParams } from 'react-router-dom';
17
+ import { Link as RouteLink, Route, Routes, useNavigate, useParams } from 'react-router-dom';
18
18
  import SaveAltIcon from '@mui/icons-material/SaveAlt';
19
19
  import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
20
20
  import WarningIcon from '@mui/icons-material/Warning';
@@ -22,456 +22,337 @@ import { MainContext } from '../../context';
22
22
  import { createRoute } from '../../utils';
23
23
  import { DelayedLoadIndicator } from '../../components/delayed-load-indicator';
24
24
  import { HoverPopover } from '../../components/hover-popover';
25
- import { Extension, UserData, isError } from '../../extension-registry-types';
25
+ import { Extension, UserData } from '../../extension-registry-types';
26
26
  import { TextDivider } from '../../components/text-divider';
27
27
  import { ExtensionRatingStars } from './extension-rating-stars';
28
28
  import { NamespaceDetailRoutes } from '../namespace-detail/namespace-detail-routes';
29
29
  import { ExtensionDetailOverview } from './extension-detail-overview';
30
30
  import { ExtensionDetailChanges } from './extension-detail-changes';
31
31
  import { ExtensionDetailReviews } from './extension-detail-reviews';
32
- import { ExtensionDetailRoutes } from './extension-detail-routes';
33
32
 
34
- const alignVertically = {
35
- display: 'flex',
36
- alignItems: 'center'
37
- };
33
+ import { ExtensionDetailRoutes } from './extension-detail-routes';
34
+ import { useExtensionDetail } from './use-extension-details';
38
35
 
39
- const link = {
36
+ const inlineLinkStyle = {
40
37
  display: 'contents',
41
38
  cursor: 'pointer',
42
39
  textDecoration: 'none',
43
- '&:hover': {
44
- textDecoration: 'underline'
45
- }
40
+ '&:hover': { textDecoration: 'underline' }
41
+ } as const;
42
+
43
+ const StyledRouteLink = styled(RouteLink)(inlineLinkStyle);
44
+ const StyledLink = styled(Link)(inlineLinkStyle);
45
+ const StyledHoverPopover = styled(HoverPopover)({ display: 'flex', alignItems: 'center' });
46
+ const PreviewBadge = styled(Badge)(({ theme }) => ({
47
+ '& .MuiBadge-badge': { top: theme.spacing(1), right: theme.spacing(-5) }
48
+ }));
49
+
50
+ enum ExtensionTab {
51
+ OVERVIEW = 'overview',
52
+ CHANGES = 'changes',
53
+ REVIEWS = 'reviews',
54
+ }
55
+
56
+ const TAB_VALUES = new Set<string>(Object.values(ExtensionTab));
57
+
58
+ const isTabSegment = (segment?: string): segment is ExtensionTab =>
59
+ TAB_VALUES.has(segment ?? '');
60
+
61
+ const parseTab = (segment?: string): ExtensionTab =>
62
+ isTabSegment(segment) ? segment : ExtensionTab.OVERVIEW;
63
+
64
+ const buildExtensionPath = (namespace: string, name: string, target?: string, ...extra: string[]) => {
65
+ const arr = [ExtensionDetailRoutes.ROOT, namespace, name];
66
+ if (target) arr.push(target);
67
+ arr.push(...extra);
68
+ return createRoute(arr);
46
69
  };
47
70
 
48
- const StyledRouteLink = styled(RouteLink)(link);
49
- const StyledLink = styled(Link)(link);
50
- const StyledHoverPopover = styled(HoverPopover)(alignVertically);
71
+ const UnverifiedBanner: FunctionComponent<{
72
+ extension: Extension;
73
+ headerTextColor: string;
74
+ themeType: PaletteMode;
75
+ }> = ({ extension, headerTextColor, themeType }) => {
76
+ const { pageSettings } = useContext(MainContext);
77
+
78
+ if (extension.verified) return null;
79
+
80
+ return (
81
+ <Paper
82
+ sx={{
83
+ display: 'flex',
84
+ maxWidth: '800px',
85
+ p: 2,
86
+ mt: 0,
87
+ mx: { xs: 0, md: 6 },
88
+ mb: { xs: 2, md: 4 },
89
+ bgcolor: `warning.${themeType}`,
90
+ color: headerTextColor,
91
+ '& a': { color: headerTextColor, textDecoration: 'underline' }
92
+ }}
93
+ >
94
+ <WarningIcon fontSize='large' />
95
+ <Box ml={1}>
96
+ This version of the &ldquo;{extension.displayName ?? extension.name}&rdquo; extension was published
97
+ by <Link href={extension.publishedBy.homepage}>
98
+ {extension.publishedBy.loginName}
99
+ </Link>. That user account is not a verified publisher of
100
+ the namespace &ldquo;{extension.namespace}&rdquo; of
101
+ this extension. <Link href={pageSettings.urls.namespaceAccessInfo} target='_blank'>
102
+ See the documentation
103
+ </Link> to learn how we handle namespaces and what you can do to eliminate this warning.
104
+ </Box>
105
+ </Paper>
106
+ );
107
+ };
51
108
 
52
- export const ExtensionDetail: FunctionComponent = () => {
53
- const theme = useTheme();
54
- const [loading, setLoading] = useState<boolean>(true);
55
- const [notFoundError, setNotFoundError] = useState<string>();
56
- const [extension, setExtension] = useState<Extension>();
57
- const [icon, setIcon] = useState<string>();
109
+ const VerificationIcon: FunctionComponent<{
110
+ verified: boolean;
111
+ color: string;
112
+ }> = ({ verified, color }) => {
113
+ const { pageSettings } = useContext(MainContext);
114
+ const icon = verified ? <VerifiedUserIcon fontSize='small' /> : <WarningIcon fontSize='small' />;
115
+ const title = verified ? 'Verified publisher' : 'Unverified publisher';
58
116
 
59
- const navigate = useNavigate();
60
- const { namespace, name, target, version } = useParams();
61
- const { handleError, pageSettings, service } = useContext(MainContext);
62
-
63
- const abortController = useRef<AbortController>(new AbortController());
64
-
65
- useEffect(() => {
66
- if (extension === undefined) {
67
- updateExtension();
68
- }
69
- return () => {
70
- abortController.current.abort();
71
- if (icon) {
72
- URL.revokeObjectURL(icon);
73
- }
74
- };
75
- }, []);
76
-
77
- useEffect(() => {
78
- if (versionPointsToTab(version)) {
79
- return;
80
- }
81
-
82
- setLoading(true);
83
- updateExtension();
84
- }, [namespace, name, target, version]);
85
-
86
- const updateExtension = async (): Promise<void> => {
87
- const extensionUrl = getExtensionApiUrl();
88
- try {
89
- const response = await service.getExtensionDetail(abortController.current, extensionUrl);
90
- if (isError(response)) {
91
- throw response;
92
- }
93
- const extension = response as Extension;
94
- const icon = await updateIcon(extension);
95
- setExtension(extension);
96
- setIcon(icon);
97
- } catch (err) {
98
- if (err && err.status === 404) {
99
- setNotFoundError(`Extension Not Found: ${namespace}.${name}`);
100
- } else {
101
- handleError(err);
102
- }
103
- } finally {
104
- setLoading(false);
105
- }
106
- };
107
-
108
- const getExtensionApiUrl = (): string => {
109
- return versionPointsToTab(version)
110
- ? service.getExtensionApiUrl({ namespace: namespace as string, name: name as string })
111
- : service.getExtensionApiUrl({ namespace: namespace as string, name: name as string, target: target, version: version });
112
- };
113
-
114
- const updateIcon = async (extension: Extension): Promise<string | undefined> => {
115
- if (icon) {
116
- URL.revokeObjectURL(icon);
117
- }
118
-
119
- return await service.getExtensionIcon(abortController.current, extension);
120
- };
121
-
122
- const onVersionSelect = (version: string): void => {
123
- const arr = [ExtensionDetailRoutes.ROOT, namespace as string, name as string];
124
- if (target) {
125
- arr.push(target);
126
- }
127
- if (version !== 'latest') {
128
- arr.push(version);
129
- }
130
-
131
- navigate(createRoute(arr));
132
- };
133
-
134
- const onReviewUpdate = (): void => {
135
- updateExtension();
136
- };
137
-
138
- const handleTabChange = (event: ChangeEvent, newTab: string): void => {
139
- const previousTab = versionPointsToTab(version) ? version : 'overview';
140
- if (newTab !== previousTab) {
141
- const arr = [ExtensionDetailRoutes.ROOT, namespace as string, name as string];
142
- if (target) {
143
- arr.push(target);
144
- }
145
-
146
- if (newTab === 'reviews' || newTab === 'changes') {
147
- arr.push(newTab);
148
- } else if (version && !versionPointsToTab(version)) {
149
- arr.push(version);
150
- } else if (extension && !isLatestVersion(extension)) {
151
- arr.push(extension.version);
152
- }
153
-
154
- navigate(createRoute(arr));
155
- }
156
- };
157
-
158
- const isLatestVersion = (extension: Extension): boolean => {
159
- return extension.versionAlias.indexOf('latest') >= 0;
160
- };
161
-
162
- const versionPointsToTab = (version?: string): boolean => {
163
- return version === 'reviews' || version === 'changes';
164
- };
165
-
166
- const renderHeaderTags = (extension?: Extension): ReactNode => {
167
- const { extensionHeadTags: ExtensionHeadTagsComponent } = pageSettings.elements;
168
- return <>
169
- { ExtensionHeadTagsComponent
170
- ? <ExtensionHeadTagsComponent extension={extension} pageSettings={pageSettings}/>
171
- : null
172
- }
173
- </>;
174
- };
175
-
176
- const renderNotFound = (): ReactNode => {
177
- return <>
178
- {
179
- notFoundError ?
180
- <Box p={4}>
181
- <Typography variant='h5'>
182
- {notFoundError}
183
- </Typography>
184
- </Box>
185
- : null
186
- }
187
- </>;
188
- };
189
-
190
- const renderTab = (tab: string, extension: Extension): ReactNode => {
191
- switch (tab) {
192
- case 'changes':
193
- return <ExtensionDetailChanges extension={extension} />;
194
- case 'reviews':
195
- return <ExtensionDetailReviews extension={extension} reviewsDidUpdate={onReviewUpdate} />;
196
- default:
197
- return <ExtensionDetailOverview extension={extension} selectVersion={onVersionSelect} />;
198
- }
199
- };
200
-
201
- const renderExtension = (extension: Extension): ReactNode => {
202
- const tab = versionPointsToTab(version) ? version as string : 'overview';
203
- const themeType = (extension.galleryTheme || pageSettings.themeType) ?? 'light';
204
- const fallbackColor = theme.palette.neutral[themeType] as string;
205
- let headerColor = extension.galleryColor || fallbackColor;
206
-
207
- try {
208
- // check if the color string can be decomposed, i.e. if mui understands it, otherwise
209
- // fall back to the neutral color of the used palette.
210
- decomposeColor(headerColor);
211
- } catch (error) {
212
- headerColor = fallbackColor;
213
- }
214
-
215
- const headerTextColor = theme.palette.getContrastText(headerColor);
216
-
217
- return <>
218
- <Box
219
- sx={{
220
- bgcolor: headerColor,
221
- color: headerTextColor,
222
- filter: extension.deprecated ? 'grayscale(100%)' : undefined
223
- }}
224
- >
225
- <Container maxWidth='xl'>
226
- <Box sx={{ display: 'flex', alignItems: 'center', flexDirection: 'column', py: 4, px: 0 }}>
227
- {renderBanner(extension, headerTextColor, themeType)}
228
- <Box
229
- sx={{
230
- display: 'flex',
231
- width: '100%',
232
- flexDirection: { xs: 'column', sm: 'column', md: 'row', lg: 'row', xl: 'row' },
233
- textAlign: { xs: 'center', sm: 'center', md: 'start', lg: 'start', xl: 'start' },
234
- alignItems: { xs: 'center', sm: 'center', md: 'normal', lg: 'normal', xl: 'normal' }
235
- }}
236
- >
237
- <Box
238
- component='img'
239
- src={icon ?? pageSettings.urls.extensionDefaultIcon }
240
- alt={extension.displayName ?? extension.name}
241
- sx={{
242
- height: '7.5rem',
243
- maxWidth: '9rem',
244
- mr: { xs: 0, sm: 0, md: '2rem', lg: '2rem', xl: '2rem' },
245
- pt: 1
246
- }}
247
- />
248
- {renderHeaderInfo(extension, headerTextColor)}
249
- </Box>
250
- </Box>
251
- </Container>
117
+ return (
118
+ <StyledLink href={pageSettings.urls.namespaceAccessInfo} target='_blank' title={title} sx={{ color }}>
119
+ {icon}
120
+ </StyledLink>
121
+ );
122
+ };
123
+
124
+ const UserPopover: FunctionComponent<{
125
+ user: UserData;
126
+ color: string;
127
+ }> = ({ user, color }) => {
128
+ const popupContent = (
129
+ <Box display='flex' flexDirection='row'>
130
+ {user.avatarUrl && (
131
+ <Avatar
132
+ src={user.avatarUrl}
133
+ alt={user.fullName ?? user.loginName}
134
+ variant='rounded'
135
+ sx={{ width: '60px', height: '60px' }}
136
+ />
137
+ )}
138
+ <Box ml={2}>
139
+ {user.fullName && <Typography variant='h6'>{user.fullName}</Typography>}
140
+ <Typography variant='body1'>{user.loginName}</Typography>
252
141
  </Box>
253
- <Container maxWidth='xl'>
254
- <Box>
255
- <Box>
256
- <Tabs value={tab} onChange={handleTabChange} indicatorColor='secondary'>
257
- <Tab value='overview' label='Overview' />
258
- <Tab value='changes' label='Changes' />
259
- <Tab value='reviews' label='Ratings &amp; Reviews' />
260
- </Tabs>
261
- {renderTab(tab, extension)}
262
- </Box>
263
- </Box>
264
- </Container>
265
- </>;
266
- };
267
-
268
- const renderBanner = (extension: Extension, headerTextColor: string, themeType: PaletteMode): ReactNode => {
269
- if (!extension.verified) {
270
- return <Paper
271
- sx={{
272
- display: 'flex',
273
- maxWidth: '800px',
274
- p: 2,
275
- mt: 0,
276
- mr: { xs: 0, sm: 0, md: 6, lg: 6, xl: 6 },
277
- mb: { xs: 2, sm: 2, md: 4, lg: 4, xl: 4 },
278
- ml: { xs: 0, sm: 0, md: 6, lg: 6, xl: 6 },
279
- bgcolor: `warning.${themeType}`,
280
- color: headerTextColor,
281
- '& a': {
282
- color: headerTextColor,
283
- textDecoration: 'underline'
284
- }
285
- }}
286
- >
287
- <WarningIcon fontSize='large' />
288
- <Box ml={1}>
289
- This version of the &ldquo;{extension.displayName ?? extension.name}&rdquo; extension was published
290
- by <Link href={extension.publishedBy.homepage}>
291
- {extension.publishedBy.loginName}
292
- </Link>. That user account is not a verified publisher of
293
- the namespace &ldquo;{extension.namespace}&rdquo; of
294
- this extension. <Link
295
- href={pageSettings.urls.namespaceAccessInfo}
296
- target='_blank' >
297
- See the documentation
298
- </Link> to learn how we handle namespaces and what you can do to eliminate this warning.
299
- </Box>
300
- </Paper>;
301
- }
302
- return null;
303
- };
304
-
305
- const renderHeaderInfo = (extension: Extension, headerTextColor: string): ReactNode => {
306
- const numberFormat = new Intl.NumberFormat(undefined, { notation: 'compact', compactDisplay: 'short' } as any);
307
- const downloadCountFormatted = numberFormat.format(extension.downloadCount || 0);
308
- const reviewCountFormatted = numberFormat.format(extension.reviewCount || 0);
309
- const previewBadgeStyle = (theme: Theme) => ({
310
- "& .MuiBadge-badge": {
311
- top: theme.spacing(1),
312
- right: theme.spacing(-5)
313
- }
314
- });
142
+ </Box>
143
+ );
144
+
145
+ return (
146
+ <StyledHoverPopover id={`user_${user.loginName}_popover`} popupContent={popupContent}>
147
+ <StyledLink href={user.homepage} sx={{ color }}>
148
+ {user.avatarUrl
149
+ ? <>{user.loginName}&nbsp;<Avatar src={user.avatarUrl} alt={user.loginName} sx={{ width: '20px', height: '20px' }} /></>
150
+ : user.loginName}
151
+ </StyledLink>
152
+ </StyledHoverPopover>
153
+ );
154
+ };
315
155
 
156
+ const LicenseLink: FunctionComponent<{
157
+ extension: Extension;
158
+ color: string;
159
+ }> = ({ extension, color }) => {
160
+ if (extension.files.license) {
316
161
  return (
162
+ <StyledLink href={extension.files.license} sx={{ color }} title={extension.license ? 'License type' : undefined}>
163
+ {extension.license || 'Provided license'}
164
+ </StyledLink>
165
+ );
166
+ }
167
+ return <>{extension.license || 'Unlicensed'}</>;
168
+ };
169
+
170
+ const compactNumber = new Intl.NumberFormat(undefined, { notation: 'compact', compactDisplay: 'short' } as Intl.NumberFormatOptions);
171
+
172
+ const ExtensionHeaderInfo: FunctionComponent<{
173
+ extension: Extension;
174
+ headerTextColor: string;
175
+ }> = ({ extension, headerTextColor }) => {
176
+ const downloadCountFormatted = compactNumber.format(extension.downloadCount || 0);
177
+ const reviewCountFormatted = compactNumber.format(extension.reviewCount || 0);
178
+
179
+ return (
317
180
  <Box overflow='auto' sx={{ pt: 1, overflow: 'visible' }}>
318
- <Badge color='secondary' badgeContent='Preview' invisible={!extension.preview} sx={previewBadgeStyle}>
181
+ <PreviewBadge color='secondary' badgeContent='Preview' invisible={!extension.preview}>
319
182
  <Typography variant='h5' sx={{ fontWeight: 'bold', mb: 1 }}>
320
- { extension.displayName ?? extension.name}
183
+ {extension.displayName ?? extension.name}
321
184
  </Typography>
322
- </Badge>
323
- { extension.deprecated &&
185
+ </PreviewBadge>
186
+
187
+ {extension.deprecated && (
324
188
  <Stack direction='row' alignItems='center'>
325
189
  <WarningIcon fontSize='small' />
326
190
  <Typography>
327
- This extension has been deprecated.{extension.replacement && <>&nbsp;Use <StyledLink sx={{ color: headerTextColor }} href={extension.replacement.url}>
328
- {extension.replacement.displayName}
329
- </StyledLink> instead.</>}
191
+ This extension has been deprecated.
192
+ {extension.replacement && (
193
+ <>&nbsp;Use <StyledLink sx={{ color: headerTextColor }} href={extension.replacement.url}>
194
+ {extension.replacement.displayName}
195
+ </StyledLink> instead.</>
196
+ )}
330
197
  </Typography>
331
198
  </Stack>
332
- }
333
- <Box
334
- sx={{
335
- ...alignVertically,
336
- color: headerTextColor,
337
- flexDirection: { xs: 'column', sm: 'column', md: 'row', lg: 'row', xl: 'row' }
338
- }}
339
- >
340
- <Box sx={alignVertically}>
341
- {renderAccessInfo(extension, headerTextColor)}&nbsp;
342
- <StyledRouteLink
343
- to={createRoute([NamespaceDetailRoutes.ROOT, extension.namespace])}
344
- style={{ color: headerTextColor }}>
199
+ )}
200
+
201
+ <Box sx={{ display: 'flex', alignItems: 'center', color: headerTextColor, flexDirection: { xs: 'column', md: 'row' } }}>
202
+ <Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 }}>
203
+ <VerificationIcon verified={extension.verified} color={headerTextColor} />&nbsp;
204
+ <StyledRouteLink to={createRoute([NamespaceDetailRoutes.ROOT, extension.namespace])} style={{ color: headerTextColor }}>
345
205
  {extension.namespaceDisplayName}
346
206
  </StyledRouteLink>
347
207
  </Box>
348
- <TextDivider backgroundColor={headerTextColor} collapseSmall={true} />
349
- <Box sx={alignVertically}>
350
- Published by&nbsp;{renderUser(extension.publishedBy, headerTextColor, alignVertically)}
208
+ <TextDivider backgroundColor={headerTextColor} collapseSmall />
209
+ <Box sx={{ display: 'flex', alignItems: 'center', flexShrink: 0 }}>
210
+ Published by&nbsp;<UserPopover user={extension.publishedBy} color={headerTextColor} />
351
211
  </Box>
352
- <TextDivider backgroundColor={headerTextColor} collapseSmall={true} />
353
- <Box sx={alignVertically}>
354
- {renderLicense(extension, headerTextColor)}
212
+ <TextDivider backgroundColor={headerTextColor} collapseSmall />
213
+ <Box sx={{ display: 'flex', alignItems: 'center' }}>
214
+ <LicenseLink extension={extension} color={headerTextColor} />
355
215
  </Box>
356
216
  </Box>
217
+
357
218
  <Box mt={2} mb={2} overflow='auto'>
358
219
  <Typography sx={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{extension.description}</Typography>
359
220
  </Box>
360
- <Box
361
- sx={{
362
- ...alignVertically,
363
- color: headerTextColor,
364
- justifyContent: { xs: 'center', sm: 'center', md: 'flex-start', lg: 'flex-start', xl: 'flex-start' }
365
- }}
366
- >
367
- <Box component='span' sx={alignVertically}
368
- title={extension.downloadCount && extension.downloadCount >= 1000 ? `${extension.downloadCount} downloads` : undefined}>
221
+
222
+ <Box sx={{ display: 'flex', alignItems: 'center', color: headerTextColor, justifyContent: { xs: 'center', md: 'flex-start' } }}>
223
+ <Box component='span' sx={{ display: 'flex', alignItems: 'center' }}
224
+ title={extension.downloadCount && extension.downloadCount >= 1000 ? `${extension.downloadCount} downloads` : undefined}
225
+ >
369
226
  <SaveAltIcon fontSize='small' />&nbsp;{downloadCountFormatted}&nbsp;{extension.downloadCount === 1 ? 'download' : 'downloads'}
370
227
  </Box>
371
228
  <TextDivider backgroundColor={headerTextColor} />
372
229
  <StyledLink
373
230
  href={createRoute([ExtensionDetailRoutes.ROOT, extension.namespace, extension.name, 'reviews'])}
374
- sx={{
375
- ...alignVertically,
376
- color: headerTextColor
377
- }}
231
+ sx={{ display: 'flex', alignItems: 'center', color: headerTextColor }}
378
232
  title={
379
- extension.averageRating !== undefined ?
380
- `Average rating: ${getRoundedRating(extension.averageRating)} out of 5 (${extension.reviewCount} reviews)`
381
- : 'Not rated yet'
382
- }>
233
+ extension.averageRating === undefined
234
+ ? 'Not rated yet'
235
+ : `Average rating: ${Math.round(extension.averageRating * 10) / 10} out of 5 (${extension.reviewCount} reviews)`
236
+ }
237
+ >
383
238
  <ExtensionRatingStars number={extension.averageRating ?? 0} fontSize='small' />
384
239
  ({reviewCountFormatted})
385
240
  </StyledLink>
386
- </Box>
387
241
  </Box>
388
- );
389
- };
390
-
391
- const getRoundedRating = (rating: number): number => {
392
- return Math.round(rating * 10) / 10;
393
- };
394
-
395
- const renderAccessInfo = (extension: Extension, themeColor: string): ReactNode => {
396
- let icon: ReactElement;
397
- let title: string;
398
- if (extension.verified) {
399
- icon = <VerifiedUserIcon fontSize='small' />;
400
- title = 'Verified publisher';
401
- } else {
402
- icon = <WarningIcon fontSize='small' />;
403
- title = 'Unverified publisher';
404
- }
405
- return <StyledLink
406
- href={pageSettings.urls.namespaceAccessInfo}
407
- target='_blank'
408
- title={title}
409
- sx={{ color: themeColor }}>
410
- {icon}
411
- </StyledLink>;
412
- };
242
+ </Box>
243
+ );
244
+ };
413
245
 
414
- const renderUser = (user: UserData, themeColor: string, alignVertically: SxProps<Theme>): ReactNode => {
415
- const popupContent = <Box display='flex' flexDirection='row'>
416
- {
417
- user.avatarUrl ?
418
- <Avatar
419
- src={user.avatarUrl}
420
- alt={user.fullName ?? user.loginName}
421
- variant='rounded'
422
- sx={{ width: '60px', height: '60px' }} />
423
- : null
424
- }
425
- <Box ml={2}>
426
- {
427
- user.fullName ?
428
- <Typography variant='h6'>{user.fullName}</Typography>
429
- : null
430
- }
431
- <Typography variant='body1'>{user.loginName}</Typography>
432
- </Box>
433
- </Box>;
434
- return <StyledHoverPopover
435
- id={`user_${user.loginName}_popover`}
436
- popupContent={popupContent}
246
+ const ExtensionHeader: FunctionComponent<{
247
+ extension: Extension;
248
+ icon: string | undefined;
249
+ }> = ({ extension, icon }) => {
250
+ const theme = useTheme();
251
+ const { pageSettings } = useContext(MainContext);
252
+
253
+ const themeType = (extension.galleryTheme || pageSettings.themeType) ?? 'light';
254
+ const fallbackColor = theme.palette.neutral[themeType] as string;
255
+ let headerColor = extension.galleryColor || fallbackColor;
256
+
257
+ try {
258
+ decomposeColor(headerColor);
259
+ } catch {
260
+ headerColor = fallbackColor;
261
+ }
262
+
263
+ const headerTextColor = theme.palette.getContrastText(headerColor);
264
+
265
+ return (
266
+ <Box
267
+ sx={{
268
+ bgcolor: headerColor,
269
+ color: headerTextColor,
270
+ filter: extension.deprecated ? 'grayscale(100%)' : undefined
271
+ }}
437
272
  >
438
- <StyledLink href={user.homepage} sx={{ color: themeColor }}>
439
- {
440
- user.avatarUrl ?
441
- <>
442
- {user.loginName}&nbsp;<Avatar
443
- src={user.avatarUrl}
444
- alt={user.loginName}
445
- sx={{ width: '20px', height: '20px' }} />
446
- </>
447
- : user.loginName
448
- }
449
- </StyledLink>
450
- </StyledHoverPopover>;
451
- };
452
-
453
- const renderLicense = (extension: Extension, themeColor: string): ReactNode => {
454
- if (extension.files.license) {
455
- return <StyledLink
456
- href={extension.files.license}
457
- sx={{ color: themeColor }}
458
- title={extension.license ? 'License type' : undefined} >
459
- {extension.license || 'Provided license'}
460
- </StyledLink>;
461
- } else if (extension.license) {
462
- return extension.license;
463
- } else {
464
- return 'Unlicensed';
465
- }
466
- };
467
-
468
- return <>
469
- { renderHeaderTags(extension) }
470
- <DelayedLoadIndicator loading={loading} />
471
- {
472
- extension
473
- ? renderExtension(extension)
474
- : renderNotFound()
475
- }
476
- </>;
477
- };
273
+ <Container maxWidth='xl'>
274
+ <Box sx={{ display: 'flex', alignItems: 'center', flexDirection: 'column', py: 4, px: 0 }}>
275
+ <UnverifiedBanner extension={extension} headerTextColor={headerTextColor} themeType={themeType} />
276
+ <Box
277
+ sx={{
278
+ display: 'flex',
279
+ width: '100%',
280
+ flexDirection: { xs: 'column', md: 'row' },
281
+ textAlign: { xs: 'center', md: 'start' },
282
+ alignItems: { xs: 'center', md: 'normal' }
283
+ }}
284
+ >
285
+ <Box
286
+ component='img'
287
+ src={icon ?? pageSettings.urls.extensionDefaultIcon}
288
+ alt={extension.displayName ?? extension.name}
289
+ sx={{ height: '7.5rem', maxWidth: '9rem', mr: { xs: 0, md: '2rem' }, pt: 1 }}
290
+ />
291
+ <ExtensionHeaderInfo extension={extension} headerTextColor={headerTextColor} />
292
+ </Box>
293
+ </Box>
294
+ </Container>
295
+ </Box>
296
+ );
297
+ };
298
+
299
+ export const ExtensionDetail: FunctionComponent = () => {
300
+ const { namespace, name, target, '*': splat } = useParams();
301
+
302
+ const navigate = useNavigate();
303
+ const { pageSettings } = useContext(MainContext);
304
+
305
+ const version = splat || undefined;
306
+ const effectiveVersion = isTabSegment(version) ? undefined : version;
307
+ const activeTab = parseTab(version);
308
+
309
+ // React Router v6 returns a possibly undefined type for params, but our route configuration guarantees these will be defined.
310
+ const { loading, error, extension, icon, reload } = useExtensionDetail(namespace!, name!, target!, effectiveVersion!);
311
+
312
+ const navigateToVersion = useCallback((selectedVersion: string) => {
313
+ if (!namespace || !name) return;
314
+ navigate(selectedVersion === 'latest'
315
+ ? buildExtensionPath(namespace, name, target)
316
+ : buildExtensionPath(namespace, name, target, selectedVersion));
317
+ }, [navigate, namespace, name, target]);
318
+
319
+ if (!namespace || !name) return null;
320
+
321
+ const basePath = buildExtensionPath(namespace, name, target);
322
+ const reviewsPath = buildExtensionPath(namespace, name, target, ExtensionTab.REVIEWS);
323
+ const changesPath = buildExtensionPath(namespace, name, target, ExtensionTab.CHANGES);
324
+
325
+ let overviewPath = basePath;
326
+ if (version && !isTabSegment(version)) {
327
+ overviewPath = buildExtensionPath(namespace, name, target, version);
328
+ } else if (extension && !extension.versionAlias.includes('latest')) {
329
+ overviewPath = buildExtensionPath(namespace, name, target, extension.version);
330
+ }
331
+
332
+ const HeadTags = pageSettings.elements.extensionHeadTags;
333
+
334
+ return (
335
+ <>
336
+ {HeadTags && <HeadTags extension={extension} pageSettings={pageSettings} />}
337
+ <DelayedLoadIndicator loading={loading} />
338
+ {extension && (
339
+ <>
340
+ <ExtensionHeader extension={extension} icon={icon} />
341
+ <Container maxWidth='xl'>
342
+ <Tabs value={activeTab} indicatorColor='secondary'>
343
+ <Tab value={ExtensionTab.OVERVIEW} label='Overview' component={RouteLink} to={overviewPath} />
344
+ <Tab value={ExtensionTab.CHANGES} label='Changes' component={RouteLink} to={changesPath} />
345
+ <Tab value={ExtensionTab.REVIEWS} label='Ratings &amp; Reviews' component={RouteLink} to={reviewsPath} />
346
+ </Tabs>
347
+ <Routes>
348
+ <Route path={ExtensionTab.REVIEWS} element={<ExtensionDetailReviews extension={extension} reviewsDidUpdate={reload} />} />
349
+ <Route path={ExtensionTab.CHANGES} element={<ExtensionDetailChanges extension={extension} />} />
350
+ <Route path='*' element={<ExtensionDetailOverview extension={extension} selectVersion={navigateToVersion} />} />
351
+ </Routes>
352
+ </Container>
353
+ </>
354
+ )}
355
+ {error && <Box p={4}><Typography variant='h5'>{error.message}</Typography></Box>}
356
+ </>
357
+ );
358
+ };