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