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.
- 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/components/sidepanel/navigation-item.d.ts +0 -1
- package/lib/components/sidepanel/navigation-item.d.ts.map +1 -1
- package/lib/components/sidepanel/navigation-item.js +22 -8
- package/lib/components/sidepanel/navigation-item.js.map +1 -1
- package/lib/components/sidepanel/sidebar-context.d.ts +16 -0
- package/lib/components/sidepanel/sidebar-context.d.ts.map +1 -0
- package/lib/components/sidepanel/sidebar-context.js +15 -0
- package/lib/components/sidepanel/sidebar-context.js.map +1 -0
- package/lib/components/sidepanel/sidepanel.d.ts +4 -4
- package/lib/components/sidepanel/sidepanel.d.ts.map +1 -1
- package/lib/components/sidepanel/sidepanel.js +47 -10
- package/lib/components/sidepanel/sidepanel.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/extension-registry-service.d.ts +6 -0
- package/lib/extension-registry-service.d.ts.map +1 -1
- package/lib/extension-registry-service.js +17 -0
- package/lib/extension-registry-service.js.map +1 -1
- package/lib/hooks/use-local-storage.d.ts +23 -0
- package/lib/hooks/use-local-storage.d.ts.map +1 -0
- package/lib/hooks/use-local-storage.js +62 -0
- package/lib/hooks/use-local-storage.js.map +1 -0
- 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 +45 -101
- package/lib/pages/admin-dashboard/admin-dashboard.js.map +1 -1
- package/lib/pages/admin-dashboard/admin-header.d.ts +19 -0
- package/lib/pages/admin-dashboard/admin-header.d.ts.map +1 -0
- package/lib/pages/admin-dashboard/admin-header.js +16 -0
- package/lib/pages/admin-dashboard/admin-header.js.map +1 -0
- package/lib/pages/admin-dashboard/admin-sidepanel.d.ts +19 -0
- package/lib/pages/admin-dashboard/admin-sidepanel.d.ts.map +1 -0
- package/lib/pages/admin-dashboard/admin-sidepanel.js +17 -0
- package/lib/pages/admin-dashboard/admin-sidepanel.js.map +1 -0
- 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/namespace-admin.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/namespace-admin.js +4 -1
- package/lib/pages/admin-dashboard/namespace-admin.js.map +1 -1
- package/lib/pages/admin-dashboard/namespace-change-dialog.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/namespace-change-dialog.js +6 -1
- package/lib/pages/admin-dashboard/namespace-change-dialog.js.map +1 -1
- package/lib/pages/admin-dashboard/namespace-delete-dialog.d.ts +23 -0
- package/lib/pages/admin-dashboard/namespace-delete-dialog.d.ts.map +1 -0
- package/lib/pages/admin-dashboard/namespace-delete-dialog.js +53 -0
- package/lib/pages/admin-dashboard/namespace-delete-dialog.js.map +1 -0
- package/lib/pages/admin-dashboard/nav-types.d.ts +27 -0
- package/lib/pages/admin-dashboard/nav-types.d.ts.map +1 -0
- package/lib/pages/admin-dashboard/nav-types.js +14 -0
- package/lib/pages/admin-dashboard/nav-types.js.map +1 -0
- 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/scan-admin.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/scan-admin.js +2 -5
- package/lib/pages/admin-dashboard/scan-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.d.ts +5 -1
- package/lib/pages/admin-dashboard/welcome.d.ts.map +1 -1
- package/lib/pages/admin-dashboard/welcome.js +18 -16
- package/lib/pages/admin-dashboard/welcome.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 -249
- 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-setting-tabs.d.ts.map +1 -1
- package/lib/pages/user/user-setting-tabs.js +1 -1
- package/lib/pages/user/user-setting-tabs.js.map +1 -1
- package/lib/pages/user/user-settings-namespace-detail.d.ts +1 -0
- package/lib/pages/user/user-settings-namespace-detail.d.ts.map +1 -1
- package/lib/pages/user/user-settings-namespace-detail.js +19 -3
- package/lib/pages/user/user-settings-namespace-detail.js.map +1 -1
- package/lib/pages/user/user-settings-namespaces.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/components/sidepanel/navigation-item.tsx +79 -23
- package/src/components/sidepanel/sidebar-context.tsx +17 -0
- package/src/components/sidepanel/sidepanel.tsx +57 -29
- package/src/default/menu-content.tsx +1 -1
- package/src/extension-registry-service.ts +18 -0
- package/src/hooks/use-local-storage.ts +67 -0
- 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 +71 -216
- package/src/pages/admin-dashboard/admin-header.tsx +59 -0
- package/src/pages/admin-dashboard/admin-sidepanel.tsx +59 -0
- 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/namespace-admin.tsx +5 -0
- package/src/pages/admin-dashboard/namespace-change-dialog.tsx +55 -46
- package/src/pages/admin-dashboard/namespace-delete-dialog.tsx +89 -0
- package/src/pages/admin-dashboard/nav-types.ts +31 -0
- package/src/pages/admin-dashboard/publisher-admin.tsx +1 -1
- package/src/pages/admin-dashboard/scan-admin.tsx +2 -5
- package/src/pages/admin-dashboard/usage-stats/usage-stats.tsx +1 -1
- package/src/pages/admin-dashboard/welcome.tsx +80 -48
- package/src/pages/extension-detail/extension-detail-routes.ts +2 -3
- package/src/pages/extension-detail/extension-detail.tsx +290 -409
- package/src/pages/extension-detail/use-extension-details.tsx +101 -0
- package/src/pages/user/avatar.tsx +1 -1
- package/src/pages/user/user-setting-tabs.tsx +3 -2
- package/src/pages/user/user-settings-namespace-detail.tsx +38 -5
- package/src/pages/user/user-settings-namespaces.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,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
|
|
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
|
-
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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 “{extension.displayName ?? extension.name}” 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 “{extension.namespace}” 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} <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
|
-
<
|
|
181
|
+
<PreviewBadge color='secondary' badgeContent='Preview' invisible={!extension.preview}>
|
|
319
182
|
<Typography variant='h5' sx={{ fontWeight: 'bold', mb: 1 }}>
|
|
320
|
-
{
|
|
183
|
+
{extension.displayName ?? extension.name}
|
|
321
184
|
</Typography>
|
|
322
|
-
</
|
|
323
|
-
|
|
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.
|
|
328
|
-
|
|
329
|
-
|
|
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
|
+
)}
|
|
330
197
|
</Typography>
|
|
331
198
|
</Stack>
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
color
|
|
337
|
-
|
|
338
|
-
}}
|
|
339
|
-
>
|
|
340
|
-
<Box sx={alignVertically}>
|
|
341
|
-
{renderAccessInfo(extension, headerTextColor)}
|
|
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} />
|
|
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
|
|
349
|
-
<Box sx={
|
|
350
|
-
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} />
|
|
351
211
|
</Box>
|
|
352
|
-
<TextDivider backgroundColor={headerTextColor} collapseSmall
|
|
353
|
-
<Box sx={
|
|
354
|
-
{
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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' /> {downloadCountFormatted} {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
|
|
380
|
-
|
|
381
|
-
:
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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 & 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
|
+
};
|